feat(backend): Endpoints van assignments en groepen beschermd.

This commit is contained in:
Gerald Schmittinger 2025-04-08 16:58:14 +02:00
parent a1ce8a209c
commit bc2cd145ab
11 changed files with 111 additions and 38 deletions

View file

@ -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;

View 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[]));
}
}
);

View file

@ -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)) {

View file

@ -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;
}
);

View 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[]);
}
}
);