feat(backend): Endpoints van assignments en groepen beschermd.
This commit is contained in:
parent
a1ce8a209c
commit
bc2cd145ab
11 changed files with 111 additions and 38 deletions
|
@ -3,7 +3,7 @@ import { createAssignment, getAllAssignments, getAssignment, getAssignmentsSubmi
|
|||
import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment';
|
||||
|
||||
// Typescript is annoying with parameter forwarding from class.ts
|
||||
interface AssignmentParams {
|
||||
export interface AssignmentParams {
|
||||
classid: string;
|
||||
id: string;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
import { Request } from 'express';
|
||||
import { JwtPayload } from 'jsonwebtoken';
|
||||
import { AuthenticationInfo } from './authentication-info.js';
|
||||
import * as core from "express-serve-static-core";
|
||||
|
||||
export interface AuthenticatedRequest extends Request {
|
||||
export interface AuthenticatedRequest<
|
||||
P = core.ParamsDictionary,
|
||||
ResBody = unknown,
|
||||
ReqBody = unknown,
|
||||
ReqQuery = core.Query,
|
||||
Locals extends Record<string, unknown> = Record<string, unknown>,
|
||||
> extends Request<P,ResBody,ReqBody,ReqQuery,Locals> {
|
||||
// Properties are optional since the user is not necessarily authenticated.
|
||||
jwtPayload?: JwtPayload;
|
||||
auth?: AuthenticationInfo;
|
||||
|
|
26
backend/src/middleware/auth/checks/assignment-auth-checks.ts
Normal file
26
backend/src/middleware/auth/checks/assignment-auth-checks.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import {authorize} from "./auth-checks";
|
||||
import {getAssignment} from "../../../services/assignments";
|
||||
import {getClass} from "../../../services/classes";
|
||||
import {getAllGroups} from "../../../services/groups";
|
||||
|
||||
/**
|
||||
* Expects the path to contain the path parameters 'classId' and 'id' (meaning the ID of the assignment).
|
||||
* Only allows requests from users who are
|
||||
* - either teachers of the class the assignment was posted in,
|
||||
* - or students in a group of the assignment.
|
||||
*/
|
||||
export const onlyAllowIfHasAccessToAssignment = authorize(
|
||||
async (auth, req) => {
|
||||
const { classid: classId, id: assignmentId } = req.params as { classid: string, id: number };
|
||||
const assignment = await getAssignment(classId, assignmentId);
|
||||
if (assignment === null) {
|
||||
return false;
|
||||
} else if (auth.accountType === "teacher") {
|
||||
const clazz = await getClass(assignment.class);
|
||||
return auth.username in clazz!.teachers;
|
||||
} else {
|
||||
const groups = await getAllGroups(classId, assignmentId, false);
|
||||
return groups.some(group => auth.username in (group.members as string[]));
|
||||
}
|
||||
}
|
||||
);
|
|
@ -3,6 +3,7 @@ import {AuthenticatedRequest} from "../authenticated-request";
|
|||
import * as express from "express";
|
||||
import {UnauthorizedException} from "../../../exceptions/unauthorized-exception";
|
||||
import {ForbiddenException} from "../../../exceptions/forbidden-exception";
|
||||
import {RequestHandler} from "express";
|
||||
|
||||
/**
|
||||
* Middleware which rejects unauthenticated users (with HTTP 401) and authenticated users which do not fulfill
|
||||
|
@ -10,10 +11,10 @@ import {ForbiddenException} from "../../../exceptions/forbidden-exception";
|
|||
* @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates
|
||||
* to true.
|
||||
*/
|
||||
export function authorize(
|
||||
accessCondition: (auth: AuthenticationInfo, req: AuthenticatedRequest) => boolean | Promise<boolean>
|
||||
) {
|
||||
return async (req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): Promise<void> => {
|
||||
export function authorize<P,ResBody,ReqBody,ReqQuery,Locals extends Record<string, unknown>>(
|
||||
accessCondition: (auth: AuthenticationInfo, req: AuthenticatedRequest<P,ResBody,ReqBody,ReqQuery,Locals>) => boolean | Promise<boolean>
|
||||
): RequestHandler<P,ResBody,ReqBody,ReqQuery,Locals> {
|
||||
return async (req: AuthenticatedRequest<P,ResBody,ReqBody,ReqQuery,Locals>, _res: express.Response, next: express.NextFunction): Promise<void> => {
|
||||
if (!req.auth) {
|
||||
throw new UnauthorizedException();
|
||||
} else if (!await accessCondition(req.auth, req)) {
|
||||
|
|
|
@ -3,9 +3,9 @@ import {AuthenticationInfo} from "../authentication-info";
|
|||
import {AuthenticatedRequest} from "../authenticated-request";
|
||||
import {getClass} from "../../../services/classes";
|
||||
|
||||
async function teaches(teacherUsername: string, classId: string) {
|
||||
async function teaches(teacherUsername: string, classId: string): Promise<boolean> {
|
||||
const clazz = await getClass(classId);
|
||||
return clazz != null && teacherUsername in clazz.teachers;
|
||||
return clazz !== null && teacherUsername in clazz.teachers;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -19,9 +19,9 @@ export const onlyAllowStudentHimselfAndTeachersOfClass = authorize(
|
|||
return true;
|
||||
} else if (auth.accountType === "teacher") {
|
||||
return teaches(auth.username, req.params.classId);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -38,21 +38,32 @@ export const onlyAllowTeacherOfClass = authorize(
|
|||
* Only let the request pass through if the class id in it refers to a class the current user is in (as a student
|
||||
* or teacher)
|
||||
*/
|
||||
function createOnlyAllowIfInClass(onlyTeacher: boolean) {
|
||||
return authorize(
|
||||
async (auth: AuthenticationInfo, req: AuthenticatedRequest) => {
|
||||
const classId = req.params.classId ?? req.params.classid ?? req.params.id;
|
||||
const clazz = await getClass(classId);
|
||||
if (clazz == null) {
|
||||
return false;
|
||||
} else if (onlyTeacher || auth.accountType === "teacher") {
|
||||
return auth.username in clazz.teachers;
|
||||
} else {
|
||||
return auth.username in clazz.students;
|
||||
}
|
||||
export const onlyAllowIfInClass = authorize(
|
||||
async (auth: AuthenticationInfo, req: AuthenticatedRequest) => {
|
||||
const classId = req.params.classId ?? req.params.classid ?? req.params.id;
|
||||
const clazz = await getClass(classId);
|
||||
if (clazz === null) {
|
||||
return false;
|
||||
} else if (auth.accountType === "teacher") {
|
||||
return auth.username in clazz.teachers;
|
||||
}
|
||||
);
|
||||
}
|
||||
return auth.username in clazz.students;
|
||||
}
|
||||
);
|
||||
|
||||
export const onlyAllowIfInClass = createOnlyAllowIfInClass(false);
|
||||
export const onlyAllowIfTeacherInClass = createOnlyAllowIfInClass(true);
|
||||
/**
|
||||
* Only allows the request to pass if the 'class' property in its body is a class the current user is a member of.
|
||||
*/
|
||||
export const onlyAllowOwnClassInBody = authorize(
|
||||
async (auth, req) => {
|
||||
const classId = (req.body as {class: string})?.class;
|
||||
const clazz = await getClass(classId);
|
||||
|
||||
if (clazz === null) {
|
||||
return false;
|
||||
} else if (auth.accountType === "teacher") {
|
||||
return auth.username in clazz.teachers;
|
||||
}
|
||||
return auth.username in clazz.students;
|
||||
}
|
||||
);
|
||||
|
|
24
backend/src/middleware/auth/checks/group-auth-checker.ts
Normal file
24
backend/src/middleware/auth/checks/group-auth-checker.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import {authorize} from "./auth-checks";
|
||||
import {getClass} from "../../../services/classes";
|
||||
import {getGroup} from "../../../services/groups";
|
||||
|
||||
/**
|
||||
* Expects the path to contain the path parameters 'classid', 'assignmentid' and 'groupid'.
|
||||
* Only allows requests from users who are
|
||||
* - either teachers of the class the assignment for the group was posted in,
|
||||
* - or students in the group
|
||||
*/
|
||||
export const onlyAllowIfHasAccessToGroup = authorize(
|
||||
async (auth, req) => {
|
||||
const { classid: classId, assignmentid: assignmentId, groupid: groupId } =
|
||||
req.params as { classid: string, assignmentid: number, groupid: number };
|
||||
|
||||
if (auth.accountType === "teacher") {
|
||||
const clazz = await getClass(classId);
|
||||
return auth.username in clazz!.teachers;
|
||||
} else { // user is student
|
||||
const group = await getGroup(classId, assignmentId, groupId, false);
|
||||
return group === null ? false : auth.username in (group.members as string[]);
|
||||
}
|
||||
}
|
||||
);
|
|
@ -6,20 +6,23 @@ import {
|
|||
getAssignmentsSubmissionsHandler,
|
||||
} from '../controllers/assignments.js';
|
||||
import groupRouter from './groups.js';
|
||||
import {adminOnly, teachersOnly} from "../middleware/auth/checks/auth-checks";
|
||||
import {onlyAllowOwnClassInBody} from "../middleware/auth/checks/class-auth-checks";
|
||||
import {onlyAllowIfHasAccessToAssignment} from "../middleware/auth/checks/assignment-auth-checks";
|
||||
|
||||
const router = express.Router({ mergeParams: true });
|
||||
|
||||
// Root endpoint used to search objects
|
||||
router.get('/', getAllAssignmentsHandler);
|
||||
router.get('/', adminOnly, getAllAssignmentsHandler);
|
||||
|
||||
router.post('/', createAssignmentHandler);
|
||||
router.post('/', teachersOnly, onlyAllowOwnClassInBody, createAssignmentHandler);
|
||||
|
||||
// Information about an assignment with id 'id'
|
||||
router.get('/:id', getAssignmentHandler);
|
||||
router.get('/:id', onlyAllowIfHasAccessToAssignment, getAssignmentHandler);
|
||||
|
||||
router.get('/:id/submissions', getAssignmentsSubmissionsHandler);
|
||||
router.get('/:id/submissions', onlyAllowIfHasAccessToAssignment, getAssignmentsSubmissionsHandler);
|
||||
|
||||
router.get('/:id/questions', (_req, res) => {
|
||||
router.get('/:id/questions', onlyAllowIfHasAccessToAssignment, (_req, res) => {
|
||||
res.json({
|
||||
questions: ['0'],
|
||||
});
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
} from '../controllers/classes.js';
|
||||
import assignmentRouter from './assignments.js';
|
||||
import {adminOnly, teachersOnly} from "../middleware/auth/checks/auth-checks";
|
||||
import {onlyAllowIfInClass, onlyAllowIfTeacherInClass} from "../middleware/auth/checks/class-auth-checks";
|
||||
import {onlyAllowIfInClass} from "../middleware/auth/checks/class-auth-checks";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
|
@ -20,7 +20,7 @@ router.post('/', teachersOnly, createClassHandler);
|
|||
// Information about an class with id 'id'
|
||||
router.get('/:id', onlyAllowIfInClass, getClassHandler);
|
||||
|
||||
router.get('/:id/teacher-invitations', onlyAllowIfTeacherInClass, getTeacherInvitationsHandler);
|
||||
router.get('/:id/teacher-invitations', teachersOnly, onlyAllowIfInClass, getTeacherInvitationsHandler);
|
||||
|
||||
router.get('/:id/students', onlyAllowIfInClass, getClassStudentsHandler);
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import express from 'express';
|
||||
import { createGroupHandler, getAllGroupsHandler, getGroupHandler, getGroupSubmissionsHandler } from '../controllers/groups.js';
|
||||
import {onlyAllowIfHasAccessToGroup} from "../middleware/auth/checks/group-auth-checker";
|
||||
|
||||
const router = express.Router({ mergeParams: true });
|
||||
|
||||
|
@ -9,12 +10,12 @@ router.get('/', getAllGroupsHandler);
|
|||
router.post('/', createGroupHandler);
|
||||
|
||||
// Information about a group (members, ... [TODO DOC])
|
||||
router.get('/:groupid', getGroupHandler);
|
||||
router.get('/:groupid', onlyAllowIfHasAccessToGroup, getGroupHandler);
|
||||
|
||||
router.get('/:groupid/submissions', getGroupSubmissionsHandler);
|
||||
router.get('/:groupid/submissions', onlyAllowIfHasAccessToGroup, getGroupSubmissionsHandler);
|
||||
|
||||
// The list of questions a group has made
|
||||
router.get('/:id/questions', (_req, res) => {
|
||||
router.get('/:groupid/questions', onlyAllowIfHasAccessToGroup, (_req, res) => {
|
||||
res.json({
|
||||
questions: ['0'],
|
||||
});
|
||||
|
|
|
@ -20,7 +20,7 @@ const router = express.Router();
|
|||
router.get('/', adminOnly, getAllStudentsHandler);
|
||||
|
||||
// Users will be created automatically when some resource is created for them. Therefore, this endpoint
|
||||
// can only be used by an administrator.
|
||||
// Can only be used by an administrator.
|
||||
router.post('/', adminOnly, createStudentHandler);
|
||||
|
||||
router.delete('/:username', onlyAllowUserHimself, deleteStudentHandler);
|
||||
|
|
|
@ -52,7 +52,7 @@ export async function getStudent(username: string): Promise<StudentDTO> {
|
|||
return mapToStudentDTO(user);
|
||||
}
|
||||
|
||||
export async function createStudent(userData: StudentDTO, allowUpdate: boolean = false): Promise<StudentDTO> {
|
||||
export async function createStudent(userData: StudentDTO, allowUpdate = false): Promise<StudentDTO> {
|
||||
const studentRepository = getStudentRepository();
|
||||
|
||||
const newStudent = mapToStudent(userData);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue