diff --git a/backend/src/middleware/auth/auth.ts b/backend/src/middleware/auth/auth.ts index 73a65b9a..2e1c3765 100644 --- a/backend/src/middleware/auth/auth.ts +++ b/backend/src/middleware/auth/auth.ts @@ -8,6 +8,7 @@ import { AuthenticatedRequest } from './authenticated-request.js'; import { AuthenticationInfo } from './authentication-info.js'; import { UnauthorizedException } from '../../exceptions/unauthorized-exception.js'; import { ForbiddenException } from '../../exceptions/forbidden-exception.js'; +import { RequestHandler } from 'express'; const JWKS_CACHE = true; const JWKS_RATE_LIMIT = true; @@ -115,11 +116,17 @@ export const authenticateUser = [verifyJwtToken, addAuthenticationInfo]; * @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates * to true. */ -export function authorize(accessCondition: (auth: AuthenticationInfo) => boolean) { - return (req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): void => { +export function authorize( + accessCondition: (auth: AuthenticationInfo, req: AuthenticatedRequest) => boolean | Promise +): RequestHandler { + return async ( + req: AuthenticatedRequest, + _res: express.Response, + next: express.NextFunction + ): Promise => { if (!req.auth) { throw new UnauthorizedException(); - } else if (!accessCondition(req.auth)) { + } else if (!(await accessCondition(req.auth, req))) { throw new ForbiddenException(); } else { next(); diff --git a/backend/src/middleware/auth/checks/learning-object-auth-checks.ts b/backend/src/middleware/auth/checks/learning-object-auth-checks.ts new file mode 100644 index 00000000..31387198 --- /dev/null +++ b/backend/src/middleware/auth/checks/learning-object-auth-checks.ts @@ -0,0 +1,16 @@ +import { Language } from "@dwengo-1/common/util/language"; +import learningObjectService from "../../../services/learning-objects/learning-object-service"; +import { authorize } from "../auth"; +import { AuthenticatedRequest } from "../authenticated-request"; +import { AuthenticationInfo } from "../authentication-info"; + +export const onlyAdminsForLearningObject = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { + const { hruid } = req.params; + const { version, language } = req.query; + const admins = await learningObjectService.getAdmins({ + hruid, + language: language as Language, + version: parseInt(version as string) + }); + return auth.username in admins; +}); diff --git a/backend/src/routes/learning-objects.ts b/backend/src/routes/learning-objects.ts index 254d7ebc..46339ce5 100644 --- a/backend/src/routes/learning-objects.ts +++ b/backend/src/routes/learning-objects.ts @@ -4,12 +4,15 @@ import { getAttachment, getLearningObject, getLearningObjectHTML, + handleDeleteLearningObject, handlePostLearningObject } from '../controllers/learning-objects.js'; import submissionRoutes from './submissions.js'; import questionRoutes from './questions.js'; import fileUpload from "express-fileupload"; +import { teachersOnly } from '../middleware/auth/auth.js'; +import { onlyAdminsForLearningObject } from '../middleware/auth/checks/learning-object-auth-checks.js'; const router = express.Router(); @@ -25,7 +28,7 @@ const router = express.Router(); // Example 2: http://localhost:3000/learningObject?full=true&hruid=un_artificiele_intelligentie router.get('/', getAllLearningObjects); -router.post('/', fileUpload({useTempFiles: true}), handlePostLearningObject) +router.post('/', teachersOnly, fileUpload({useTempFiles: true}), handlePostLearningObject) // Parameter: hruid of learning object // Query: language @@ -33,6 +36,12 @@ router.post('/', fileUpload({useTempFiles: true}), handlePostLearningObject) // Example: http://localhost:3000/learningObject/un_ai7 router.get('/:hruid', getLearningObject); +// Parameter: hruid of learning object +// Query: language +// Route to delete a learning object based on its hruid. +// Example: http://localhost:3000/learningObject/un_ai7?language=nl&version=1 +router.delete('/:hruid', onlyAdminsForLearningObject, handleDeleteLearningObject) + router.use('/:hruid/submissions', submissionRoutes); router.use('/:hruid/:version/questions', questionRoutes); diff --git a/backend/src/services/learning-objects/learning-object-service.ts b/backend/src/services/learning-objects/learning-object-service.ts index 4ada879b..f70b88b2 100644 --- a/backend/src/services/learning-objects/learning-object-service.ts +++ b/backend/src/services/learning-objects/learning-object-service.ts @@ -11,6 +11,7 @@ import {getLearningObjectRepository, getTeacherRepository} from "../../data/repo import {processLearningObjectZip} from "./learning-object-zip-processing-service"; import {LearningObject} from "../../entities/content/learning-object.entity"; import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; +import { NotFoundException } from '../../exceptions/not-found-exception.js'; function getProvider(id: LearningObjectIdentifierDTO): LearningObjectProvider { if (id.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) { @@ -92,6 +93,19 @@ const learningObjectService = { async deleteLearningObject(id: LearningObjectIdentifier): Promise { const learningObjectRepository = getLearningObjectRepository(); return await learningObjectRepository.removeByIdentifier(id); + }, + + /** + * Returns a list of the usernames of the administrators of the learning object with the given identifier. + * @throws NotFoundException if the specified learning object was not found in the database. + */ + async getAdmins(id: LearningObjectIdentifier): Promise { + const learningObjectRepo = getLearningObjectRepository(); + const learningObject = await learningObjectRepo.findByIdentifier(id); + if (!learningObject) { + throw new NotFoundException("The specified learning object does not exist."); + } + return learningObject.admins.map(admin => admin.username); } };