diff --git a/ExpressAPI/src/express/API.ts b/ExpressAPI/src/express/API.ts index 0f885ecf1ca1297c12abbc42b0c384a20521ccf7..a55896ae65a74961ce491a26e3081bb0f14719d9 100644 --- a/ExpressAPI/src/express/API.ts +++ b/ExpressAPI/src/express/API.ts @@ -38,9 +38,7 @@ class API implements WorkerTask { private initBaseMiddlewares() { this.backend.use(multer({ - limits: { - fieldSize: 15728640 // 15MB - } + limits: { fieldSize: 100 * 1024 * 1024 } }).none()); //Used for extract params from body with format "form-data", The none is for say that we do not wait a file in params this.backend.use(morganMiddleware); //Log API accesses this.backend.use(helmet()); //Help to secure express, https://helmetjs.github.io/ diff --git a/ExpressAPI/src/helpers/Prisma/Extensions/UserResultExtension.ts b/ExpressAPI/src/helpers/Prisma/Extensions/UserResultExtension.ts index cb52f6e1dcddf3d58c25a239a9b8e9430345f7f0..15d6cb8f4c583a10e083d2a0dc3ffaf2f634633b 100644 --- a/ExpressAPI/src/helpers/Prisma/Extensions/UserResultExtension.ts +++ b/ExpressAPI/src/helpers/Prisma/Extensions/UserResultExtension.ts @@ -26,7 +26,7 @@ export default Prisma.defineExtension(client => { }, gitlabProfile : { compute(user) { - return new LazyVal<Gitlab.UserSchema | undefined>(() => GitlabManager.getUserById(user.id)); + return new LazyVal<Gitlab.UserSchema | undefined>(() => GlobalHelper.sharedGitlabManager.getUserById(user.id)); } } } diff --git a/ExpressAPI/src/managers/GitlabManager.ts b/ExpressAPI/src/managers/GitlabManager.ts index 1c7c959944bf56a88eb49ad352d7adb62873c8c2..f5e45ad18ccf37302e43b5de47bfa295bd4cc28e 100644 --- a/ExpressAPI/src/managers/GitlabManager.ts +++ b/ExpressAPI/src/managers/GitlabManager.ts @@ -7,6 +7,7 @@ import { CommitSchema, ExpandedUserSchema, Gitlab, MemberSchema, ProjectBadgeSch import logger from '../shared/logging/WinstonLogger'; import { AccessLevel, EditProjectOptions, ProjectVariableSchema, ProtectedBranchAccessLevel, ProtectedBranchSchema } from '@gitbeaker/core'; import GlobalHelper from '../helpers/GlobalHelper'; +import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode'; class GitlabManager { @@ -29,11 +30,11 @@ class GitlabManager { } async getRepositoryMembers(idOrNamespace: string): Promise<Array<MemberSchema>> { - return await this.api.ProjectMembers.all(idOrNamespace, { includeInherited: true }); + return this.api.ProjectMembers.all(idOrNamespace, { includeInherited: true }); } async getRepositoryReleases(repoId: number): Promise<Array<ReleaseSchema>> { - return await this.api.ProjectReleases.all(repoId); + return this.api.ProjectReleases.all(repoId); } async getRepositoryLastCommit(repoId: number, branch: string = 'main'): Promise<CommitSchema | undefined> { @@ -52,24 +53,24 @@ class GitlabManager { } async createRepository(name: string, description: string, visibility: 'public' | 'internal' | 'private', initializeWithReadme: boolean, namespace: number, sharedRunnersEnabled: boolean, wikiEnabled: boolean, import_url: string): Promise<ProjectSchema> { - return await this.api.Projects.create({ - name : name, - description : description, - importUrl : import_url, - initializeWithReadme: initializeWithReadme, - namespaceId : namespace, - sharedRunnersEnabled: sharedRunnersEnabled, - visibility : visibility, - wikiAccessLevel : wikiEnabled ? 'enabled' : 'disabled' - }); + return this.api.Projects.create({ + name : name, + description : description, + importUrl : import_url, + initializeWithReadme: initializeWithReadme, + namespaceId : namespace, + sharedRunnersEnabled: sharedRunnersEnabled, + visibility : visibility, + wikiAccessLevel : wikiEnabled ? 'enabled' : 'disabled' + }); } async deleteRepository(repoId: number): Promise<void> { - return await this.api.Projects.remove(repoId); + return this.api.Projects.remove(repoId); } async forkRepository(forkId: number, name: string, path: string, description: string, visibility: 'public' | 'internal' | 'private', namespace: number): Promise<ProjectSchema> { - return await this.api.Projects.fork(forkId, { + return this.api.Projects.fork(forkId, { name : name, path : path, description: description, @@ -79,19 +80,19 @@ class GitlabManager { } async editRepository(repoId: number, newAttributes: EditProjectOptions): Promise<ProjectSchema> { - return await this.api.Projects.edit(repoId, newAttributes); + return this.api.Projects.edit(repoId, newAttributes); } async changeRepositoryVisibility(repoId: number, visibility: GitlabVisibility): Promise<ProjectSchema> { - return await this.editRepository(repoId, { visibility: visibility }); + return this.editRepository(repoId, { visibility: visibility }); } async addRepositoryMember(repoId: number, userId: number, accessLevel: Exclude<AccessLevel, AccessLevel.ADMIN>): Promise<MemberSchema> { - return await this.api.ProjectMembers.add(repoId, userId, accessLevel); + return this.api.ProjectMembers.add(repoId, userId, accessLevel); } async addRepositoryVariable(repoId: number, key: string, value: string, isProtected: boolean, isMasked: boolean): Promise<ProjectVariableSchema> { - return await this.api.ProjectVariables.create(repoId, key, value, { + return this.api.ProjectVariables.create(repoId, key, value, { variableType: 'env_var', protected : isProtected, masked : isMasked @@ -99,7 +100,7 @@ class GitlabManager { } async addRepositoryBadge(repoId: number, linkUrl: string, imageUrl: string, name: string): Promise<ProjectBadgeSchema> { - return await this.api.ProjectBadges.add(repoId, linkUrl, imageUrl, { + return this.api.ProjectBadges.add(repoId, linkUrl, imageUrl, { name: name }); } @@ -110,7 +111,8 @@ class GitlabManager { const project: ProjectSchema = await GlobalHelper.sharedGitlabManager.getRepository(projectIdOrNamespace); if ( [ 'public', 'internal' ].includes(project.visibility) ) { - return StatusCodes.OK; + req.session.sendResponse(res, StatusCodes.OK); + return true; } } catch ( e ) { req.session.sendResponse(res, StatusCodes.NOT_FOUND, undefined, 'Template not found', DojoStatusCode.GITLAB_TEMPLATE_NOT_FOUND); @@ -143,7 +145,7 @@ class GitlabManager { } async protectBranch(repoId: number, branchName: string, allowForcePush: boolean, allowedToMerge: ProtectedBranchAccessLevel, allowedToPush: ProtectedBranchAccessLevel, allowedToUnprotect: ProtectedBranchAccessLevel): Promise<ProtectedBranchSchema> { - return await this.api.ProtectedBranches.protect(repoId, branchName, { + return this.api.ProtectedBranches.protect(repoId, branchName, { allowForcePush : allowForcePush, mergeAccessLevel : allowedToMerge, pushAccessLevel : allowedToPush, @@ -152,20 +154,20 @@ class GitlabManager { } async getRepositoryTree(repoId: number, recursive: boolean = true, branch: string = 'main'): Promise<Array<RepositoryTreeSchema>> { - return await this.api.Repositories.allRepositoryTrees(repoId, { + return this.api.Repositories.allRepositoryTrees(repoId, { recursive: recursive, ref : branch }); } async getFile(repoId: number, filePath: string, branch: string = 'main'): Promise<RepositoryFileExpandedSchema> { - return await this.api.RepositoryFiles.show(repoId, filePath, branch); + return this.api.RepositoryFiles.show(repoId, filePath, branch); } private async createUpdateFile(create: boolean, repoId: number, filePath: string, fileBase64: string, commitMessage: string, branch: string = 'main', authorName: string = 'Dojo', authorMail: string | undefined = undefined): Promise<RepositoryFileSchema> { const gitFunction = create ? this.api.RepositoryFiles.create : this.api.RepositoryFiles.edit; - return await gitFunction(repoId, filePath, branch, fileBase64, commitMessage, { + return gitFunction(repoId, filePath, branch, fileBase64, commitMessage, { encoding : 'base64', authorName : authorName, authorEmail: authorMail diff --git a/ExpressAPI/src/middlewares/SecurityMiddleware.ts b/ExpressAPI/src/middlewares/SecurityMiddleware.ts index 6b4a0756419e4b5052988c6cf08cc5c527b2ba70..ccec6fa9dcbbaf59ab7057be4927eec51d1e9039 100644 --- a/ExpressAPI/src/middlewares/SecurityMiddleware.ts +++ b/ExpressAPI/src/middlewares/SecurityMiddleware.ts @@ -6,40 +6,38 @@ import AssignmentManager from '../managers/AssignmentManager'; class SecurityMiddleware { + private isConnected(checkIfConnected: boolean, req: express.Request): boolean { + return checkIfConnected && req.session.profile !== null && req.session.profile !== undefined; + } + + private async checkType(checkType: SecurityCheckType, req: express.Request): Promise<boolean> { + try { + switch ( String(checkType) ) { + case SecurityCheckType.TEACHING_STAFF: + return req.session.profile.isTeachingStaff; + case SecurityCheckType.ASSIGNMENT_STAFF: + return await AssignmentManager.isUserAllowedToAccessAssignment(req.boundParams.assignment!, req.session.profile); + case SecurityCheckType.ASSIGNMENT_IS_PUBLISHED: + return req.boundParams.assignment?.published ?? false; + case SecurityCheckType.EXERCISE_SECRET: + return (req.headers.exercisesecret as string | undefined) === req.boundParams.exercise!.secret; + default: + return false; + } + } catch ( e ) { + logger.error('Security check failed !!! => ' + e); + return false; + } + } + // First check if connected then check if at least ONE rule match. It's NOT an AND but it's a OR function. check(checkIfConnected: boolean, ...checkTypes: Array<SecurityCheckType>): (req: express.Request, res: express.Response, next: express.NextFunction) => void { return async (req: express.Request, res: express.Response, next: express.NextFunction) => { - if ( checkIfConnected && (req.session.profile === null || req.session.profile === undefined) ) { + if ( !this.isConnected(checkIfConnected, req) ) { return req.session.sendResponse(res, StatusCodes.UNAUTHORIZED); } - let isAllowed = checkTypes.length === 0; - - if ( !isAllowed ) { - for ( const checkType of checkTypes ) { - try { - switch ( String(checkType) ) { - case SecurityCheckType.TEACHING_STAFF: - isAllowed = isAllowed || req.session.profile.isTeachingStaff; - break; - case SecurityCheckType.ASSIGNMENT_STAFF: - isAllowed = isAllowed || await AssignmentManager.isUserAllowedToAccessAssignment(req.boundParams.assignment!, req.session.profile); - break; - case SecurityCheckType.ASSIGNMENT_IS_PUBLISHED: - isAllowed = isAllowed || (req.boundParams.assignment?.published ?? false); - break; - case SecurityCheckType.EXERCISE_SECRET: - isAllowed = isAllowed || (req.headers.exercisesecret as string | undefined) === req.boundParams.exercise!.secret; - break; - default: - break; - } - } catch ( e ) { - logger.error('Security check failed !!! => ' + e); - isAllowed = isAllowed || false; - } - } - } + const isAllowed: boolean = checkTypes.length === 0 ? true : checkTypes.find(async (checkType) => this.checkType(checkType, req)) !== undefined; if ( !isAllowed ) { return req.session.sendResponse(res, StatusCodes.FORBIDDEN); diff --git a/ExpressAPI/src/routes/ExerciseRoutes.ts b/ExpressAPI/src/routes/ExerciseRoutes.ts index 72a0276668b393ac8f8a4940182fdc4401e522a8..febc46da83cd622cad40e9b02d59ca653c4e6409 100644 --- a/ExpressAPI/src/routes/ExerciseRoutes.ts +++ b/ExpressAPI/src/routes/ExerciseRoutes.ts @@ -103,7 +103,7 @@ class ExerciseRoutes implements RoutesManager { let suffix: number = 0; do { try { - repository = await GitlabManager.forkRepository((assignment.gitlabCreationInfo as unknown as Gitlab.ProjectSchema).id, this.getExerciseName(assignment, params.members, suffix), this.getExercisePath(req.boundParams.assignment!, exerciseId), Config.exercise.default.description.replace('{{ASSIGNMENT_NAME}}', assignment.name), Config.exercise.default.visibility, Config.gitlab.group.exercises); + repository = await GitlabManager.forkRepository((assignment.gitlabCreationInfo as unknown as Gitlab.ProjectSchema).id, this.getExerciseName(assignment, members, suffix), this.getExercisePath(req.boundParams.assignment!, exerciseId), Config.exercise.default.description.replace('{{ASSIGNMENT_NAME}}', assignment.name), Config.exercise.default.visibility, Config.gitlab.group.exercises); break; } catch ( error ) { logger.error('Repo creation error'); @@ -133,12 +133,12 @@ class ExerciseRoutes implements RoutesManager { } private async createExercise(req: express.Request, res: express.Response) { - const params: { members: Array<GitlabUser> } = req.body; + const params: { members: Array<Gitlab.UserSchema> } = req.body; params.members = [ await req.session.profile.gitlabProfile.value, ...params.members ].removeObjectDuplicates(gitlabUser => gitlabUser.id); const assignment: Assignment = req.boundParams.assignment!; - const reachedLimitUsers: Array<GitlabUser> = await this.checkExerciseLimit(assignment, params.members); + const reachedLimitUsers: Array<Gitlab.UserSchema> = await this.checkExerciseLimit(assignment, params.members); if ( reachedLimitUsers.length > 0 ) { req.session.sendResponse(res, StatusCodes.INSUFFICIENT_SPACE_ON_RESOURCE, reachedLimitUsers, 'Max exercise per assignment reached', DojoStatusCode.MAX_EXERCISE_PER_ASSIGNMENT_REACHED); return; @@ -147,7 +147,7 @@ class ExerciseRoutes implements RoutesManager { const exerciseId: string = uuidv4(); const secret: string = uuidv4(); - const repository: GitlabRepository | undefined = await this.createExerciseRepository(assignment, params.members, exerciseId, req, res); + const repository: Gitlab.ProjectSchema | undefined = await this.createExerciseRepository(assignment, params.members, exerciseId, req, res); if ( !repository ) { return; @@ -164,14 +164,14 @@ class ExerciseRoutes implements RoutesManager { await GitlabManager.addRepositoryBadge(repository.id, Config.gitlab.badges.pipeline.link, Config.gitlab.badges.pipeline.imageUrl, 'Pipeline Status'); } catch ( error ) { - GlobalHelper.repositoryCreationError('Repo params error', error, req, res, DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR, DojoStatusCode.EXERCISE_CREATION_INTERNAL_ERROR, repository); + await GlobalHelper.repositoryCreationError('Repo params error', error, req, res, DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR, DojoStatusCode.EXERCISE_CREATION_INTERNAL_ERROR, repository); return; } try { await GitlabManager.updateFile(repository.id, '.gitlab-ci.yml', fs.readFileSync(path.join(__dirname, '../../assets/exercise_gitlab_ci.yml'), 'base64'), 'Add .gitlab-ci.yml (DO NOT MODIFY THIS FILE)'); } catch ( error ) { - GlobalHelper.repositoryCreationError('CI file update error', error, req, res, DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR, DojoStatusCode.EXERCISE_CREATION_INTERNAL_ERROR, repository); + await GlobalHelper.repositoryCreationError('CI file update error', error, req, res, DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR, DojoStatusCode.EXERCISE_CREATION_INTERNAL_ERROR, repository); return; }