From 8744aca63f616ef0f795bec8992ab6d76e2ecd37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolas=20G=C3=B6rlitz?= Date: Sun, 19 May 2024 20:33:12 +0200 Subject: [PATCH] Start adding permissions --- .../20221121101837-PermissionSeeder.ts | 2 + ...tionRequirementAdministrationController.ts | 23 +- .../admin-logs/JoblogAdminController.ts | 22 +- .../admin-logs/SyslogAdminController.ts | 10 +- .../course/CourseAdministrationController.ts | 391 +++++++++++------- backend/src/models/User.ts | 6 +- .../src/models/extensions/UserExtensions.ts | 35 +- backend/src/utility/Validator.ts | 2 +- 8 files changed, 328 insertions(+), 163 deletions(-) diff --git a/backend/db/seeders/20221121101837-PermissionSeeder.ts b/backend/db/seeders/20221121101837-PermissionSeeder.ts index e61f6e5..0dbe14e 100644 --- a/backend/db/seeders/20221121101837-PermissionSeeder.ts +++ b/backend/db/seeders/20221121101837-PermissionSeeder.ts @@ -7,6 +7,8 @@ const allPerms = [ "mentor.view", "lm.view", + "lm.action_requirements.view", + "lm.course.view", "atd.view", "atd.solo.delete", diff --git a/backend/src/controllers/action-requirement/ActionRequirementAdministrationController.ts b/backend/src/controllers/action-requirement/ActionRequirementAdministrationController.ts index a9421ce..961dc25 100644 --- a/backend/src/controllers/action-requirement/ActionRequirementAdministrationController.ts +++ b/backend/src/controllers/action-requirement/ActionRequirementAdministrationController.ts @@ -1,10 +1,25 @@ -import { Request, Response } from "express"; +import { NextFunction, Request, Response } from "express"; import { ActionRequirement } from "../../models/ActionRequirement"; +import { User } from "../../models/User"; +import PermissionHelper from "../../utility/helper/PermissionHelper"; -async function getAll(request: Request, response: Response) { - const actionRequirements = await ActionRequirement.findAll(); +/** + * Get all action requirements stored in the database + * @param _request + * @param response + * @param next + */ +async function getAll(_request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + PermissionHelper.checkUserHasPermission(user, "lm.action_requirements.view"); - response.send(actionRequirements); + const actionRequirements: ActionRequirement[] = await ActionRequirement.findAll(); + + response.send(actionRequirements); + } catch (e) { + next(e); + } } export default { diff --git a/backend/src/controllers/admin-logs/JoblogAdminController.ts b/backend/src/controllers/admin-logs/JoblogAdminController.ts index 317cea4..cb6778b 100644 --- a/backend/src/controllers/admin-logs/JoblogAdminController.ts +++ b/backend/src/controllers/admin-logs/JoblogAdminController.ts @@ -4,7 +4,13 @@ import { Job } from "../../models/Job"; import Validator, { ValidationTypeEnum } from "../../utility/Validator"; import { HttpStatusCode } from "axios"; -async function getAll(request: Request, response: Response, next: NextFunction) { +/** + * Returns all currently stored Job-Logs + * @param _request + * @param response + * @param next + */ +async function getAll(_request: Request, response: Response, next: NextFunction) { try { const user = response.locals.user; PermissionHelper.checkUserHasPermission(user, "tech.joblog.view"); @@ -16,13 +22,19 @@ async function getAll(request: Request, response: Response, next: NextFunction) } } +/** + * Gets information on a single Job-Log + * @param request + * @param response + * @param next + */ async function getInformationByID(request: Request, response: Response, next: NextFunction) { try { const user = response.locals.user; - const params = request.params as { id: string }; + PermissionHelper.checkUserHasPermission(user, "tech.joblog.view"); + const params = request.params as { id: string }; Validator.validate(params, { id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER] }); - PermissionHelper.checkUserHasPermission(user, "tech.joblog.view"); const job = await Job.findOne({ where: { @@ -36,7 +48,9 @@ async function getInformationByID(request: Request, response: Response, next: Ne } response.send(job); - } catch (e) {} + } catch (e) { + next(e); + } } export default { diff --git a/backend/src/controllers/admin-logs/SyslogAdminController.ts b/backend/src/controllers/admin-logs/SyslogAdminController.ts index 7270e8e..21f9bfe 100644 --- a/backend/src/controllers/admin-logs/SyslogAdminController.ts +++ b/backend/src/controllers/admin-logs/SyslogAdminController.ts @@ -6,16 +6,16 @@ import Validator, { ValidationTypeEnum } from "../../utility/Validator"; /** * Gets all system log entries - * @param request + * @param _request * @param response * @param next */ -async function getAll(request: Request, response: Response, next: NextFunction) { +async function getAll(_request: Request, response: Response, next: NextFunction) { try { const user: User = response.locals.user; PermissionHelper.checkUserHasPermission(user, "tech.syslog.view"); - const sysLogs = await SysLog.findAll({ + const sysLogs: SysLog[] = await SysLog.findAll({ order: [["id", "desc"]], attributes: ["id", "method", "path", "createdAt"], }); @@ -29,14 +29,14 @@ async function getAll(request: Request, response: Response, next: NextFunction) async function getInformationByID(request: Request, response: Response, next: NextFunction) { try { const user: User = response.locals.user; - const params = request.params; PermissionHelper.checkUserHasPermission(user, "tech.syslog.view"); + const params = request.params; Validator.validate(params, { id: [ValidationTypeEnum.NON_NULL], }); - const sysLog = await SysLog.findOne({ + const sysLog: SysLog | null = await SysLog.findOne({ where: { id: params.id, }, diff --git a/backend/src/controllers/course/CourseAdministrationController.ts b/backend/src/controllers/course/CourseAdministrationController.ts index db1bc1e..c213b85 100644 --- a/backend/src/controllers/course/CourseAdministrationController.ts +++ b/backend/src/controllers/course/CourseAdministrationController.ts @@ -10,14 +10,14 @@ import { sequelize } from "../../core/Sequelize"; import { TrainingTypesBelongsToCourses } from "../../models/through/TrainingTypesBelongsToCourses"; import { generateUUID } from "../../utility/UUID"; import Validator, { ValidationTypeEnum } from "../../utility/Validator"; - -// TODO: Move all course related things into this controller +import PermissionHelper from "../../utility/helper/PermissionHelper"; +import { Transaction } from "sequelize"; +import { ForbiddenException } from "../../exceptions/ForbiddenException"; /** * The ICourseBody Interface is the type which all requests that wish to create or update a course must satisfy */ interface ICourseBody { - course_uuid: string; name_de: string; name_en: string; description_de: string; @@ -29,27 +29,33 @@ interface ICourseBody { } /** - * Validates and creates a new course based on the request + * Creates a new course within a given mentor group * @param request * @param response * @param next */ async function createCourse(request: Request, response: Response, next: NextFunction) { - const transaction = await sequelize.transaction(); + const transaction: Transaction = await sequelize.transaction(); + try { const user: User = response.locals.user; const body: ICourseBody = request.body as ICourseBody; - // Validator.validate(body, [ - // { - // name: "course_uuid", - // toValidate: [ValidationOptions.NON_NULL] - // } - // ]) + Validator.validate(body, { + name_de: [ValidationTypeEnum.NON_NULL], + name_en: [ValidationTypeEnum.NON_NULL], + description_de: [ValidationTypeEnum.NON_NULL], + description_en: [ValidationTypeEnum.NON_NULL], + active: [ValidationTypeEnum.NON_NULL], + self_enrol_enabled: [ValidationTypeEnum.NON_NULL], + training_type_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER], + mentor_group_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER], + }); - if (!(await user.canManageCourseInMentorGroup(Number(body.mentor_group_id)))) { - response.sendStatus(HttpStatusCode.Forbidden); - return; + // Check if user is allowed to create a course in this mentor group + const mentorGroupID = Number(body.mentor_group_id); + if (!(await user.canManageCourseInMentorGroup(mentorGroupID))) { + throw new ForbiddenException("You don't have the required permission to create a course in this mentor group.", false); } const uuid = generateUUID(); @@ -72,7 +78,7 @@ async function createCourse(request: Request, response: Response, next: NextFunc await MentorGroupsBelongsToCourses.create( { - mentor_group_id: Number(body.mentor_group_id), + mentor_group_id: mentorGroupID, course_id: course.id, can_edit_course: true, }, @@ -106,13 +112,14 @@ async function createCourse(request: Request, response: Response, next: NextFunc * @param next */ async function updateCourse(request: Request, response: Response, next: NextFunction) { + type ICourseBodyWithUUID = ICourseBody & { course_uuid: string }; + try { const user: User = response.locals.user; - const body: ICourseBody = request.body as ICourseBody; + const body: ICourseBodyWithUUID = request.body as ICourseBodyWithUUID; if (!(await user.canEditCourse(body.course_uuid))) { - response.sendStatus(HttpStatusCode.Forbidden); - return; + throw new ForbiddenException("You don't have the required permission to edit this course", false); } await Course.update( @@ -140,12 +147,20 @@ async function updateCourse(request: Request, response: Response, next: NextFunc /** * Returns a list of all courses - * @param request + * @param _request * @param response + * @param next */ -async function getAllCourses(request: Request, response: Response) { - const courses = await Course.findAll(); - response.send(courses); +async function getAllCourses(_request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + PermissionHelper.checkUserHasPermission(user, "lm.course.view"); + + const courses = await Course.findAll(); + response.send(courses); + } catch (e) { + next(e); + } } /** @@ -153,169 +168,239 @@ async function getAllCourses(request: Request, response: Response) { * This includes the initial training type used for this course * @param request * @param response + * @param next */ -async function getCourse(request: Request, response: Response) { - const params = request.params as { course_uuid: string }; - - const course = await Course.findOne({ - where: { - uuid: params.course_uuid, - }, - include: [Course.associations.training_type], - }); - - if (course == null) { - response.sendStatus(HttpStatusCode.BadRequest); - return; - } +async function getCourse(request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + PermissionHelper.checkUserHasPermission(user, "lm.course.view"); - response.send(course); + const params = request.params as { course_uuid: string }; + + const course = await Course.findOne({ + where: { + uuid: params.course_uuid, + }, + include: [Course.associations.training_type], + }); + + if (course == null) { + response.sendStatus(HttpStatusCode.BadRequest); + return; + } + + response.send(course); + } catch (e) { + next(e); + } } /** * Returns a list of users that are enrolled in this course. * @param request * @param response + * @param next */ -async function getCourseParticipants(request: Request, response: Response) { - const params = request.params as { course_uuid: string }; - - const course = await Course.findOne({ - where: { - uuid: params.course_uuid, - }, - include: [Course.associations.users], - }); - - if (course == null) { - response.sendStatus(HttpStatusCode.BadRequest); - return; - } +async function getCourseParticipants(request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + const params = request.params as { course_uuid: string }; + if (!(await user.isMentorInCourse(params.course_uuid))) { + throw new ForbiddenException("You are not a mentor in this course."); + } + + const course = await Course.findOne({ + where: { + uuid: params.course_uuid, + }, + include: [Course.associations.users], + }); + + if (course == null) { + response.sendStatus(HttpStatusCode.BadRequest); + return; + } - response.send(course.users); + response.send(course.users); + } catch (e) { + next(e); + } } /** * Removed a user from the specified course * @param request * @param response + * @param next */ -async function removeCourseParticipant(request: Request, response: Response) { - const params = request.params as { course_uuid: string }; - const body = request.body as { user_id: number }; - - const courseID = await Course.getIDFromUUID(params.course_uuid); - if (courseID == -1) { - response.sendStatus(HttpStatusCode.BadRequest); - return; - } +async function removeCourseParticipant(request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + const params = request.params as { course_uuid: string }; + const body = request.body as { user_id: number }; - await UsersBelongsToCourses.destroy({ - where: { - user_id: body.user_id, - course_id: courseID, - }, - }); + if (!(await user.isMentorInCourse(params.course_uuid))) { + throw new ForbiddenException("You are not a mentor in this course."); + } - await TrainingRequest.destroy({ - where: { - user_id: body.user_id, - course_id: courseID, - }, - }); + Validator.validate(body, { + user_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER], + }); + + const courseID = await Course.getIDFromUUID(params.course_uuid); + if (courseID == -1) { + response.sendStatus(HttpStatusCode.BadRequest); + return; + } + + await UsersBelongsToCourses.destroy({ + where: { + user_id: body.user_id, + course_id: courseID, + }, + }); + + await TrainingRequest.destroy({ + where: { + user_id: body.user_id, + course_id: courseID, + }, + }); - response.sendStatus(HttpStatusCode.NoContent); + response.sendStatus(HttpStatusCode.NoContent); + } catch (e) { + next(e); + } } /** * Returns a list of mentor groups that are associated with this course * This does not include the permission such as 'can_edit' + * + * Restricted to mentors of this course. * @param request * @param response + * @param next */ -async function getCourseMentorGroups(request: Request, response: Response) { - const params = request.params as { course_uuid: string }; - - const course = await Course.findOne({ - where: { - uuid: params.course_uuid, - }, - include: [ - { - association: Course.associations.mentor_groups, - through: { - attributes: ["can_edit_course", "createdAt"], - }, - include: [ - { - association: MentorGroup.associations.users, - attributes: ["first_name", "last_name", "id"], - through: { - attributes: [], - }, - }, - ], +async function getCourseMentorGroups(request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + const params = request.params as { course_uuid: string }; + + if (!(await user.isMentorInCourse(params.course_uuid))) { + throw new ForbiddenException("You are not a mentor in this course."); + } + + const course = await Course.findOne({ + where: { + uuid: params.course_uuid, }, - ], - }); + include: [ + { + association: Course.associations.mentor_groups, + through: { + attributes: ["can_edit_course", "createdAt"], + }, + include: [ + { + association: MentorGroup.associations.users, + attributes: ["first_name", "last_name", "id"], + through: { + attributes: [], + }, + }, + ], + }, + ], + }); - if (course == null) { - response.sendStatus(HttpStatusCode.BadRequest); - return; - } + if (course == null) { + response.sendStatus(HttpStatusCode.BadRequest); + return; + } - response.send(course.mentor_groups); + response.send(course.mentor_groups); + } catch (e) { + next(e); + } } /** * Adds a mentor group with the specified information to the course * @param request * @param response + * @param next */ -async function addMentorGroupToCourse(request: Request, response: Response) { - const body = request.body as { course_uuid: string; mentor_group_id: number; can_edit: boolean }; +async function addMentorGroupToCourse(request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + const body = request.body as { course_uuid: string; mentor_group_id: number; can_edit: boolean }; - const courseID = await Course.getIDFromUUID(body.course_uuid); - if (courseID == -1) { - response.sendStatus(HttpStatusCode.BadRequest); - return; - } + Validator.validate(body, { + course_uuid: [ValidationTypeEnum.NON_NULL], + mentor_group_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER], + can_edit: [ValidationTypeEnum.NON_NULL], + }); - await MentorGroupsBelongsToCourses.create({ - mentor_group_id: body.mentor_group_id, - course_id: courseID, - can_edit_course: body.can_edit, - }); + if (!(await user.canEditCourse(body.course_uuid))) { + throw new ForbiddenException("You are not allowed to edit this course"); + } - response.sendStatus(HttpStatusCode.NoContent); + // If this returns -1, sequelize will complain due to a foreign constraint. + // Therefore, no need to check at this point. + const courseID = await Course.getIDFromUUID(body.course_uuid); + + await MentorGroupsBelongsToCourses.create({ + mentor_group_id: body.mentor_group_id, + course_id: courseID, + can_edit_course: body.can_edit, + }); + + response.sendStatus(HttpStatusCode.NoContent); + } catch (e) { + next(e); + } } /** * Removes a mentor group from the specified course * @param request * @param response + * @param next */ -async function removeMentorGroupFromCourse(request: Request, response: Response) { - const params = request.params as { course_uuid: string }; - const body = request.body as { mentor_group_id: number }; +async function removeMentorGroupFromCourse(request: Request, response: Response, next: NextFunction) { + try { + const user: User = response.locals.user; + const params = request.params as { course_uuid: string }; + const body = request.body as { mentor_group_id: number }; - Validator.validate(params, { course_uuid: [ValidationTypeEnum.NON_NULL] }); - Validator.validate(body, { mentor_group_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER] }); + Validator.validate(body, { + course_uuid: [ValidationTypeEnum.NON_NULL], + mentor_group_id: [ValidationTypeEnum.NON_NULL, ValidationTypeEnum.NUMBER], + can_edit: [ValidationTypeEnum.NON_NULL], + }); - const courseID = await Course.getIDFromUUID(params.course_uuid); - if (courseID == -1) { - response.sendStatus(HttpStatusCode.BadRequest); - return; - } + if (!(await user.canEditCourse(params.course_uuid))) { + throw new ForbiddenException("You are not allowed to edit this course"); + } - await MentorGroupsBelongsToCourses.destroy({ - where: { - course_id: courseID, - mentor_group_id: body.mentor_group_id, - }, - }); + const courseID = await Course.getIDFromUUID(params.course_uuid); + if (courseID == -1) { + response.sendStatus(HttpStatusCode.BadRequest); + return; + } - response.sendStatus(HttpStatusCode.NoContent); + await MentorGroupsBelongsToCourses.destroy({ + where: { + course_id: courseID, + mentor_group_id: body.mentor_group_id, + }, + }); + + response.sendStatus(HttpStatusCode.NoContent); + } catch (e) { + next(e); + } } /** @@ -326,10 +411,14 @@ async function removeMentorGroupFromCourse(request: Request, response: Response) */ async function getCourseTrainingTypes(request: Request, response: Response, next: NextFunction) { try { + const user: User = response.locals.user; const params = request.params as { course_uuid: string }; - // TODO: Validate - const course = await Course.findOne({ + if (!(await user.isMentorInCourse(params.course_uuid))) { + throw new ForbiddenException("You are not a mentor in this course."); + } + + const course: Course | null = await Course.findOne({ where: { uuid: params.course_uuid, }, @@ -355,18 +444,26 @@ async function getCourseTrainingTypes(request: Request, response: Response, next */ async function addCourseTrainingType(request: Request, response: Response, next: NextFunction) { try { + const user: User = response.locals.user; const params = request.params as { course_uuid: string }; const body = request.body as { training_type_id: string }; - // TODO: Validate - const course = await Course.findOne({ where: { uuid: params.course_uuid } }); + Validator.validate(body, { + training_type_id: [ValidationTypeEnum.NON_NULL], + }); + + if (!(await user.canEditCourse(params.course_uuid))) { + throw new ForbiddenException("You are not allowed to edit this course."); + } + + const course_id = await Course.getIDFromUUID(params.course_uuid); await TrainingTypesBelongsToCourses.create({ - course_id: course?.id, + course_id: course_id, training_type_id: Number(body.training_type_id), }); - response.sendStatus(HttpStatusCode.Ok); + response.sendStatus(HttpStatusCode.Created); } catch (e) { next(e); } @@ -380,20 +477,28 @@ async function addCourseTrainingType(request: Request, response: Response, next: */ async function removeCourseTrainingType(request: Request, response: Response, next: NextFunction) { try { + const user: User = response.locals.user; const params = request.params as { course_uuid: string }; const body = request.body as { training_type_id: string }; - // TODO: Validate - const course = await Course.findOne({ where: { uuid: params.course_uuid } }); + Validator.validate(body, { + training_type_id: [ValidationTypeEnum.NON_NULL], + }); + + if (!(await user.canEditCourse(params.course_uuid))) { + throw new ForbiddenException("You are not allowed to edit this course."); + } + + const course_id = await Course.getIDFromUUID(params.course_uuid); await TrainingTypesBelongsToCourses.destroy({ where: { - course_id: course?.id, + course_id: course_id, training_type_id: Number(body.training_type_id), }, }); - response.sendStatus(HttpStatusCode.Ok); + response.sendStatus(HttpStatusCode.NoContent); } catch (e) { next(e); } diff --git a/backend/src/models/User.ts b/backend/src/models/User.ts index 6b70b60..c5cd4d5 100644 --- a/backend/src/models/User.ts +++ b/backend/src/models/User.ts @@ -103,6 +103,7 @@ export class User extends Model, InferCreationAttributes { const course = await Course.findOne({ @@ -171,7 +172,7 @@ export class User extends Model, InferCreationAttributes { - const user = await User.findOne({ + const user: User | null = await User.findOne({ where: { id: this.id, }, @@ -179,9 +180,6 @@ export class User extends Model, InferCreationAttributes Courses include: [ diff --git a/backend/src/models/extensions/UserExtensions.ts b/backend/src/models/extensions/UserExtensions.ts index 679c9b3..bf56cf6 100644 --- a/backend/src/models/extensions/UserExtensions.ts +++ b/backend/src/models/extensions/UserExtensions.ts @@ -70,6 +70,25 @@ async function getGroupAdminMentorGroups(this: User): Promise { return user?.mentor_groups ?? []; } +/** + * Returns true if and only if the user is a member of a mentor group, which is assigned to the specified + * course and is, by extension, allowed to mentor it. + * @param uuid + */ +async function isMentorInCourse(this: User, uuid: string): Promise { + const mentorGroups = await this.getMentorGroupsAndCourses(); + + for (const mentorGroup of mentorGroups) { + for (const course of mentorGroup.courses ?? []) { + if (course.uuid == uuid) { + return true; + } + } + } + + return false; +} + /** * Returns a list of mentor groups this user can manage courses * Manage in this context means, create, update, delete - although this might be reevaluated in the future @@ -115,8 +134,19 @@ async function canManageCourseInMentorGroup(this: User, mentorGroupID: number): * @param courseUUID */ async function canEditCourse(this: User, courseUUID: string): Promise { - // TODO: Find an overlap between 1 & 2 (above) and return a boolean if the case is true - return true; + const mentorGroups: MentorGroup[] = await this.getMentorGroupsAndCourses(); + + for (const mentorGroup of mentorGroups) { + if (!mentorGroup.UserBelongToMentorGroups?.can_manage_course) break; + + for (const course of mentorGroup.courses ?? []) { + if (course.uuid == courseUUID) { + return true; + } + } + } + + return false; } /** @@ -156,4 +186,5 @@ export default { canEditCourse, getCourses, getCoursesWithInformation, + isMentorInCourse, }; diff --git a/backend/src/utility/Validator.ts b/backend/src/utility/Validator.ts index 6c9544a..61c3d44 100644 --- a/backend/src/utility/Validator.ts +++ b/backend/src/utility/Validator.ts @@ -135,7 +135,7 @@ function _validateEntry( case ValidationTypeEnum.NUMBER: let n = Number(data); - if (Number.isNaN(n)) { + if (isNaN(n)) { addErrorEntry(`Parameter is not a number`); } break;