Skip to content
Snippets Groups Projects
Commit d405e46c authored by michael.minelli's avatar michael.minelli
Browse files

Merge branch 'exercise_delete_integration' into v4.2.0

parents ae63b116 a2082654
No related branches found
No related tags found
No related merge requests found
Pipeline #33470 passed
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/dojobackendapi.iml" filepath="$PROJECT_DIR$/.idea/dojobackendapi.iml" />
</modules>
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
\ No newline at end of file
This diff is collapsed.
-- AlterTable
ALTER TABLE `Exercise` ADD COLUMN `deleted` BOOLEAN NOT NULL DEFAULT false;
...@@ -57,6 +57,7 @@ model Exercise { ...@@ -57,6 +57,7 @@ model Exercise {
gitlabCreationInfo Json @db.Json gitlabCreationInfo Json @db.Json
gitlabLastInfo Json @db.Json gitlabLastInfo Json @db.Json
gitlabLastInfoDate DateTime gitlabLastInfoDate DateTime
deleted Boolean @default(false)
correctionCommit Json? @db.Json correctionCommit Json? @db.Json
correctionDescription String? @db.VarChar(80) correctionDescription String? @db.VarChar(80)
......
...@@ -44,7 +44,7 @@ class Config { ...@@ -44,7 +44,7 @@ class Config {
}; account: { }; account: {
id: number; username: string; token: string; id: number; username: string; token: string;
}; group: { }; group: {
root: number; templates: number; assignments: number; exercises: number; root: number; templates: number; assignments: number; exercises: number; deletedAssignments: number; deletedExercises: number;
}, badges: { }, badges: {
pipeline: ConfigGitlabBadge pipeline: ConfigGitlabBadge
} }
...@@ -98,10 +98,12 @@ class Config { ...@@ -98,10 +98,12 @@ class Config {
timeoutAfterCreation: Number(process.env.GITLAB_REPOSITORY_CREATION_TIMEOUT || 5000) timeoutAfterCreation: Number(process.env.GITLAB_REPOSITORY_CREATION_TIMEOUT || 5000)
}, },
group : { group : {
root : Number(process.env.GITLAB_GROUP_ROOT_ID || 0), root : Number(process.env.GITLAB_GROUP_ROOT_ID || 0),
templates : Number(process.env.GITLAB_GROUP_TEMPLATES_ID || 0), templates : Number(process.env.GITLAB_GROUP_TEMPLATES_ID || 0),
assignments: Number(process.env.GITLAB_GROUP_ASSIGNMENTS_ID || 0), assignments : Number(process.env.GITLAB_GROUP_ASSIGNMENTS_ID || 0),
exercises : Number(process.env.GITLAB_GROUP_EXERCISES_ID || 0) exercises : Number(process.env.GITLAB_GROUP_EXERCISES_ID || 0),
deletedAssignments: Number(process.env.GITLAB_GROUP_DELETED_ASSIGNMENTS_ID || 0),
deletedExercises : Number(process.env.GITLAB_GROUP_DELETED_EXERCISES_ID || 0)
}, },
badges : { badges : {
pipeline: { pipeline: {
......
import { Prisma } from '@prisma/client'; import { Prisma } from '@prisma/client';
import { Exercise } from '../types/DatabaseTypes.js'; import { Exercise, User } from '../types/DatabaseTypes.js';
import db from '../helpers/DatabaseHelper.js'; import db from '../helpers/DatabaseHelper.js';
class ExerciseManager { class ExerciseManager {
...@@ -23,6 +23,24 @@ class ExerciseManager { ...@@ -23,6 +23,24 @@ class ExerciseManager {
include: include include: include
}) as Promise<Array<Exercise>>; }) as Promise<Array<Exercise>>;
} }
async isUserAllowedToAccessExercise(exercise: Exercise, user: User): Promise<boolean> {
if ( !exercise.members ) {
exercise.members = await db.exercise.findUnique({
where: {
id: exercise.id
}
}).members() ?? [];
}
const assignmentStaff = (await db.assignment.findUnique({
where: {
name: exercise.assignmentName
}
}).staff()) ?? [];
return exercise.members.findIndex(member => member.id === user.id) !== -1 || assignmentStaff.findIndex(staff => staff.id === user.id) !== -1;
}
} }
......
...@@ -29,9 +29,9 @@ class GitlabManager extends SharedGitlabManager { ...@@ -29,9 +29,9 @@ class GitlabManager extends SharedGitlabManager {
} }
} }
async getRepositoryMembers(idOrNamespace: string): Promise<Array<MemberSchema>> { async getRepositoryMembers(idOrNamespace: string, includeInherited: boolean = true): Promise<Array<MemberSchema>> {
try { try {
return await this.api.ProjectMembers.all(idOrNamespace, { includeInherited: true }); return await this.api.ProjectMembers.all(idOrNamespace, { includeInherited: includeInherited });
} catch ( e ) { } catch ( e ) {
logger.error(JSON.stringify(e)); logger.error(JSON.stringify(e));
return Promise.reject(e); return Promise.reject(e);
...@@ -122,6 +122,27 @@ class GitlabManager extends SharedGitlabManager { ...@@ -122,6 +122,27 @@ class GitlabManager extends SharedGitlabManager {
} }
} }
async deleteRepositoryMember(repoId: number, userId: number, skipSubresources: boolean = false, unassignIssuables: boolean = false): Promise<void> {
try {
return await this.api.ProjectMembers.remove(repoId, userId, {
skipSubresourceS: skipSubresources,
unassignIssuables
});
} catch ( e ) {
logger.error(JSON.stringify(e));
return Promise.reject(e);
}
}
async moveRepository(repoId: number, newRepoId: number): Promise<ProjectSchema> {
try {
return await this.api.Projects.transfer(repoId, newRepoId);
} catch ( e ) {
logger.error(JSON.stringify(e));
return Promise.reject(e);
}
}
changeRepositoryVisibility(repoId: number, visibility: GitlabVisibility): Promise<ProjectSchema> { changeRepositoryVisibility(repoId: number, visibility: GitlabVisibility): Promise<ProjectSchema> {
return this.editRepository(repoId, { visibility: visibility }); return this.editRepository(repoId, { visibility: visibility });
} }
......
...@@ -3,6 +3,7 @@ import { StatusCodes } from 'http-status-codes'; ...@@ -3,6 +3,7 @@ import { StatusCodes } from 'http-status-codes';
import SecurityCheckType from '../types/SecurityCheckType.js'; import SecurityCheckType from '../types/SecurityCheckType.js';
import logger from '../shared/logging/WinstonLogger.js'; import logger from '../shared/logging/WinstonLogger.js';
import AssignmentManager from '../managers/AssignmentManager.js'; import AssignmentManager from '../managers/AssignmentManager.js';
import ExerciseManager from '../managers/ExerciseManager';
class SecurityMiddleware { class SecurityMiddleware {
...@@ -17,6 +18,8 @@ class SecurityMiddleware { ...@@ -17,6 +18,8 @@ class SecurityMiddleware {
return req.session.profile.isAdmin; return req.session.profile.isAdmin;
case SecurityCheckType.TEACHING_STAFF.valueOf(): case SecurityCheckType.TEACHING_STAFF.valueOf():
return req.session.profile.isTeachingStaff; return req.session.profile.isTeachingStaff;
case SecurityCheckType.EXERCISE_MEMBERS.valueOf():
return await ExerciseManager.isUserAllowedToAccessExercise(req.boundParams.exercise!, req.session.profile);
case SecurityCheckType.ASSIGNMENT_STAFF.valueOf(): case SecurityCheckType.ASSIGNMENT_STAFF.valueOf():
return await AssignmentManager.isUserAllowedToAccessAssignment(req.boundParams.assignment!, req.session.profile); return await AssignmentManager.isUserAllowedToAccessAssignment(req.boundParams.assignment!, req.session.profile);
case SecurityCheckType.ASSIGNMENT_IS_PUBLISHED.valueOf(): case SecurityCheckType.ASSIGNMENT_IS_PUBLISHED.valueOf():
......
...@@ -67,7 +67,12 @@ class ExerciseRoutes implements RoutesManager { ...@@ -67,7 +67,12 @@ class ExerciseRoutes implements RoutesManager {
registerOnBackend(backend: Express) { registerOnBackend(backend: Express) {
backend.post('/assignments/:assignmentNameOrUrl/exercises', SecurityMiddleware.check(true, SecurityCheckType.ASSIGNMENT_IS_PUBLISHED), ParamsValidatorMiddleware.validate(this.exerciseValidator), this.createExercise.bind(this) as RequestHandler); backend.post('/assignments/:assignmentNameOrUrl/exercises', SecurityMiddleware.check(true, SecurityCheckType.ASSIGNMENT_IS_PUBLISHED), ParamsValidatorMiddleware.validate(this.exerciseValidator), this.createExercise.bind(this) as RequestHandler);
backend.get('/exercises', SecurityMiddleware.check(true, SecurityCheckType.ADMIN), this.getAllExercises.bind(this) as RequestHandler);
backend.get('/exercises/:exerciseIdOrUrl/assignment', SecurityMiddleware.check(false, SecurityCheckType.EXERCISE_SECRET), this.getAssignment.bind(this) as RequestHandler); backend.get('/exercises/:exerciseIdOrUrl/assignment', SecurityMiddleware.check(false, SecurityCheckType.EXERCISE_SECRET), this.getAssignment.bind(this) as RequestHandler);
backend.get('/exercises/:exerciseIdOrUrl/members', SecurityMiddleware.check(true, SecurityCheckType.ADMIN, SecurityCheckType.EXERCISE_MEMBERS), this.getExerciseMembers.bind(this) as RequestHandler);
backend.get('/exercises/:exerciseIdOrUrl/results', SecurityMiddleware.check(true, SecurityCheckType.ADMIN, SecurityCheckType.EXERCISE_MEMBERS), this.getExerciseResults.bind(this) as RequestHandler);
backend.delete('/exercises/:exerciseIdOrUrl', SecurityMiddleware.check(true, SecurityCheckType.ADMIN, SecurityCheckType.EXERCISE_MEMBERS), this.deleteExercise.bind(this) as RequestHandler);
backend.post('/exercises/:exerciseIdOrUrl/results', SecurityMiddleware.check(false, SecurityCheckType.EXERCISE_SECRET), ParamsValidatorMiddleware.validate(this.resultValidator), this.createResult.bind(this) as RequestHandler); backend.post('/exercises/:exerciseIdOrUrl/results', SecurityMiddleware.check(false, SecurityCheckType.EXERCISE_SECRET), ParamsValidatorMiddleware.validate(this.resultValidator), this.createResult.bind(this) as RequestHandler);
} }
...@@ -78,10 +83,55 @@ class ExerciseRoutes implements RoutesManager { ...@@ -78,10 +83,55 @@ class ExerciseRoutes implements RoutesManager {
return `DojoEx - ${ assignment.name } - ${ memberNames }${ suffixString }`; return `DojoEx - ${ assignment.name } - ${ memberNames }${ suffixString }`;
} }
private async getExerciseMembers(req: express.Request, res: express.Response) {
const repoId = req.boundParams.exercise!.gitlabId;
const members = await GitlabManager.getRepositoryMembers(String(repoId));
return req.session.sendResponse(res, StatusCodes.OK, members);
}
private async getExerciseResults(req: express.Request, res: express.Response) {
const results = await db.result.findMany({
where : { exerciseId: req.boundParams.exercise!.id },
orderBy: { dateTime: 'desc' }
});
return req.session.sendResponse(res, StatusCodes.OK, results);
}
private async deleteExercise(req: express.Request, res: express.Response) {
const repoId = req.boundParams.exercise!.gitlabId;
const members = await GitlabManager.getRepositoryMembers(String(repoId), false);
for ( const member of members ) {
if ( member.id !== Config.gitlab.account.id ) {
await GitlabManager.deleteRepositoryMember(repoId, member.id);
}
}
await GitlabManager.moveRepository(repoId, Config.gitlab.group.deletedExercises);
await db.exercise.update({
where: { id: req.boundParams.exercise!.id },
data : { deleted: true }
});
return req.session.sendResponse(res, StatusCodes.OK);
}
private getExercisePath(assignment: Assignment, exerciseId: string): string { private getExercisePath(assignment: Assignment, exerciseId: string): string {
return `dojo-ex_${ (assignment.gitlabLastInfo as unknown as Gitlab.ProjectSchema).path }_${ exerciseId }`; return `dojo-ex_${ (assignment.gitlabLastInfo as unknown as Gitlab.ProjectSchema).path }_${ exerciseId }`;
} }
// Get all exercise
private async getAllExercises(req: express.Request, res: express.Response) {
const exos = await db.exercise.findMany();
return req.session.sendResponse(res, StatusCodes.OK, exos);
}
private async checkExerciseLimit(assignment: Assignment, members: Array<Gitlab.UserSchema>): Promise<Array<Gitlab.UserSchema>> { private async checkExerciseLimit(assignment: Assignment, members: Array<Gitlab.UserSchema>): Promise<Array<Gitlab.UserSchema>> {
const exercises: Array<Exercise> | undefined = await ExerciseManager.getFromAssignment(assignment.name, { members: true }); const exercises: Array<Exercise> | undefined = await ExerciseManager.getFromAssignment(assignment.name, { members: true });
const reachedLimitUsers: Array<Gitlab.UserSchema> = []; const reachedLimitUsers: Array<Gitlab.UserSchema> = [];
...@@ -259,3 +309,5 @@ class ExerciseRoutes implements RoutesManager { ...@@ -259,3 +309,5 @@ class ExerciseRoutes implements RoutesManager {
export default new ExerciseRoutes(); export default new ExerciseRoutes();
enum SecurityCheckType { enum SecurityCheckType {
TEACHING_STAFF = 'teachingStaff', TEACHING_STAFF = 'teachingStaff',
ADMIN = 'admin', ADMIN = 'admin',
EXERCISE_MEMBERS = 'exerciseMembers',
ASSIGNMENT_STAFF = 'assignmentStaff', ASSIGNMENT_STAFF = 'assignmentStaff',
ASSIGNMENT_IS_PUBLISHED = 'assignmentIsPublished', ASSIGNMENT_IS_PUBLISHED = 'assignmentIsPublished',
EXERCISE_SECRET = 'exerciseSecret', EXERCISE_SECRET = 'exerciseSecret',
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment