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'; | import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; | ||||||
| 
 | 
 | ||||||
| // Typescript is annoying with parameter forwarding from class.ts
 | // Typescript is annoying with parameter forwarding from class.ts
 | ||||||
| interface AssignmentParams { | export interface AssignmentParams { | ||||||
|     classid: string; |     classid: string; | ||||||
|     id: string; |     id: string; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,8 +1,15 @@ | ||||||
| import { Request } from 'express'; | import { Request } from 'express'; | ||||||
| import { JwtPayload } from 'jsonwebtoken'; | import { JwtPayload } from 'jsonwebtoken'; | ||||||
| import { AuthenticationInfo } from './authentication-info.js'; | 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.
 |     // Properties are optional since the user is not necessarily authenticated.
 | ||||||
|     jwtPayload?: JwtPayload; |     jwtPayload?: JwtPayload; | ||||||
|     auth?: AuthenticationInfo; |     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 * as express from "express"; | ||||||
| import {UnauthorizedException} from "../../../exceptions/unauthorized-exception"; | import {UnauthorizedException} from "../../../exceptions/unauthorized-exception"; | ||||||
| import {ForbiddenException} from "../../../exceptions/forbidden-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 |  * 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 |  * @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates | ||||||
|  *                        to true. |  *                        to true. | ||||||
|  */ |  */ | ||||||
| export function authorize( | export function authorize<P,ResBody,ReqBody,ReqQuery,Locals extends Record<string, unknown>>( | ||||||
|     accessCondition: (auth: AuthenticationInfo, req: AuthenticatedRequest) => boolean | Promise<boolean> |     accessCondition: (auth: AuthenticationInfo, req: AuthenticatedRequest<P,ResBody,ReqBody,ReqQuery,Locals>) => boolean | Promise<boolean> | ||||||
| ) { | ): RequestHandler<P,ResBody,ReqBody,ReqQuery,Locals> { | ||||||
|     return async (req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): Promise<void> => { |     return async (req: AuthenticatedRequest<P,ResBody,ReqBody,ReqQuery,Locals>, _res: express.Response, next: express.NextFunction): Promise<void> => { | ||||||
|         if (!req.auth) { |         if (!req.auth) { | ||||||
|             throw new UnauthorizedException(); |             throw new UnauthorizedException(); | ||||||
|         } else if (!await accessCondition(req.auth, req)) { |         } else if (!await accessCondition(req.auth, req)) { | ||||||
|  |  | ||||||
|  | @ -3,9 +3,9 @@ import {AuthenticationInfo} from "../authentication-info"; | ||||||
| import {AuthenticatedRequest} from "../authenticated-request"; | import {AuthenticatedRequest} from "../authenticated-request"; | ||||||
| import {getClass} from "../../../services/classes"; | 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); |     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; |             return true; | ||||||
|         } else if (auth.accountType === "teacher") { |         } else if (auth.accountType === "teacher") { | ||||||
|             return teaches(auth.username, req.params.classId); |             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 |  * 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) |  * or teacher) | ||||||
|  */ |  */ | ||||||
| function createOnlyAllowIfInClass(onlyTeacher: boolean) { | export const onlyAllowIfInClass = authorize( | ||||||
|     return authorize( |     async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { | ||||||
|         async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { |         const classId = req.params.classId ?? req.params.classid ?? req.params.id; | ||||||
|             const classId = req.params.classId ?? req.params.classid ?? req.params.id; |         const clazz = await getClass(classId); | ||||||
|             const clazz = await getClass(classId); |         if (clazz === null) { | ||||||
|             if (clazz == null) { |             return false; | ||||||
|                 return false; |         } else if (auth.accountType === "teacher") { | ||||||
|             } else if (onlyTeacher || auth.accountType === "teacher") { |             return auth.username in clazz.teachers; | ||||||
|                 return auth.username in clazz.teachers; |  | ||||||
|             } else { |  | ||||||
|                 return auth.username in clazz.students; |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|     ); |         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, |     getAssignmentsSubmissionsHandler, | ||||||
| } from '../controllers/assignments.js'; | } from '../controllers/assignments.js'; | ||||||
| import groupRouter from './groups.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 }); | const router = express.Router({ mergeParams: true }); | ||||||
| 
 | 
 | ||||||
| // Root endpoint used to search objects
 | // 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'
 | // 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({ |     res.json({ | ||||||
|         questions: ['0'], |         questions: ['0'], | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ import { | ||||||
| } from '../controllers/classes.js'; | } from '../controllers/classes.js'; | ||||||
| import assignmentRouter from './assignments.js'; | import assignmentRouter from './assignments.js'; | ||||||
| import {adminOnly, teachersOnly} from "../middleware/auth/checks/auth-checks"; | 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(); | const router = express.Router(); | ||||||
| 
 | 
 | ||||||
|  | @ -20,7 +20,7 @@ router.post('/', teachersOnly, createClassHandler); | ||||||
| // Information about an class with id 'id'
 | // Information about an class with id 'id'
 | ||||||
| router.get('/:id', onlyAllowIfInClass, getClassHandler); | 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); | router.get('/:id/students', onlyAllowIfInClass, getClassStudentsHandler); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| import express from 'express'; | import express from 'express'; | ||||||
| import { createGroupHandler, getAllGroupsHandler, getGroupHandler, getGroupSubmissionsHandler } from '../controllers/groups.js'; | import { createGroupHandler, getAllGroupsHandler, getGroupHandler, getGroupSubmissionsHandler } from '../controllers/groups.js'; | ||||||
|  | import {onlyAllowIfHasAccessToGroup} from "../middleware/auth/checks/group-auth-checker"; | ||||||
| 
 | 
 | ||||||
| const router = express.Router({ mergeParams: true }); | const router = express.Router({ mergeParams: true }); | ||||||
| 
 | 
 | ||||||
|  | @ -9,12 +10,12 @@ router.get('/', getAllGroupsHandler); | ||||||
| router.post('/', createGroupHandler); | router.post('/', createGroupHandler); | ||||||
| 
 | 
 | ||||||
| // Information about a group (members, ... [TODO DOC])
 | // 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
 | // The list of questions a group has made
 | ||||||
| router.get('/:id/questions', (_req, res) => { | router.get('/:groupid/questions', onlyAllowIfHasAccessToGroup, (_req, res) => { | ||||||
|     res.json({ |     res.json({ | ||||||
|         questions: ['0'], |         questions: ['0'], | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  | @ -20,7 +20,7 @@ const router = express.Router(); | ||||||
| router.get('/', adminOnly, getAllStudentsHandler); | router.get('/', adminOnly, getAllStudentsHandler); | ||||||
| 
 | 
 | ||||||
| // Users will be created automatically when some resource is created for them. Therefore, this endpoint
 | // 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.post('/', adminOnly, createStudentHandler); | ||||||
| 
 | 
 | ||||||
| router.delete('/:username', onlyAllowUserHimself, deleteStudentHandler); | router.delete('/:username', onlyAllowUserHimself, deleteStudentHandler); | ||||||
|  |  | ||||||
|  | @ -52,7 +52,7 @@ export async function getStudent(username: string): Promise<StudentDTO> { | ||||||
|     return mapToStudentDTO(user); |     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 studentRepository = getStudentRepository(); | ||||||
| 
 | 
 | ||||||
|     const newStudent = mapToStudent(userData); |     const newStudent = mapToStudent(userData); | ||||||
|  |  | ||||||
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger