From 0e942dc72b312e0e2960cd4437d6f006d6f738b5 Mon Sep 17 00:00:00 2001 From: Omar Ezzat Date: Sat, 18 Nov 2023 22:20:54 +0100 Subject: [PATCH] add in-place migration for board permission context --- .../board/service/column-board.service.ts | 221 ++++++++++++++---- apps/server/src/modules/board/uc/board.uc.ts | 50 +++- 2 files changed, 223 insertions(+), 48 deletions(-) diff --git a/apps/server/src/modules/board/service/column-board.service.ts b/apps/server/src/modules/board/service/column-board.service.ts index 13ae1cbae44..bc7bbb3666d 100644 --- a/apps/server/src/modules/board/service/column-board.service.ts +++ b/apps/server/src/modules/board/service/column-board.service.ts @@ -4,7 +4,6 @@ import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { AnyBoardDo, BoardExternalReference, - BoardExternalReferenceType, Card, Column, ColumnBoard, @@ -15,6 +14,10 @@ import { Permission, PermissionContextEntity, UserDelta, + Course, + isColumn, + SubmissionItem, + isCard, } from '@shared/domain'; import { PermissionContextRepo, CourseRepo } from '@shared/repo'; import { ObjectId } from 'bson'; @@ -40,7 +43,7 @@ export class ColumnBoardService { async findIdsByExternalReference(reference: BoardExternalReference): Promise { const ids = await this.boardDoRepo.findIdsByExternalReference(reference); // run migrateColumnBoardToPermissionContext for each id await - await Promise.all(ids.map((id) => this.migrateColumnBoardToPermissionContext(id))); + await Promise.all(ids.map((id) => this.pocMigrateBoardToPermissionContext(id))); return ids; } @@ -148,7 +151,7 @@ export class ColumnBoardService { await this.boardDoRepo.save(columnBoard); - await this.migrateColumnBoardToPermissionContext(columnBoard.id); + await this.pocMigrateBoardToPermissionContext(columnBoard.id); return columnBoard; } @@ -161,7 +164,7 @@ export class ColumnBoardService { } // NOTE: this is a idempotent in-place migration for POC purposes - private async migrateColumnBoardToPermissionContext(id: EntityId) { + private async pocMigrateBoardToPermissionContext(id: EntityId) { const hasPermissionContext = await this.permissionCtxRepo .findByContextReference(id) .then(() => true) @@ -172,55 +175,181 @@ export class ColumnBoardService { }); const board = await this.boardDoRepo.findById(id, 1); - if ( - !hasPermissionContext && - board instanceof ColumnBoard && - board.context.type === BoardExternalReferenceType.Course - ) { - // NOTE: apply migration, defaulting to course context + if (hasPermissionContext) return; + + if (board instanceof ColumnBoard) { + // const contextStore: Map = new Map(); + const course = await this.courseRepo.findById(board.context.id); + await this.pocMigrateColumnBoardToPermissionContext(board, course); + } + } - // NOTE: hardcoded permissions for POC purposes - const STUDENT_PERMISSIONS = [Permission.BOARD_READ]; - const TEACHER_PERMISSIONS = [ - Permission.BOARD_READ, - Permission.BOARD_CREATE_COLUMN, - Permission.BOARD_CREATE_COLUMN, - Permission.BOARD_DELETE, - Permission.BOARD_UPDATE_TITLE, - ]; - const SUBSTITUTE_TEACHER_PERMISSIONS = [...TEACHER_PERMISSIONS]; - - const studentIds = course.getStudentIds().map((userId) => { - return { - userId, - includedPermissions: STUDENT_PERMISSIONS, - excludedPermissions: [], - }; - }); - const teacherIds = course.getTeacherIds().map((userId) => { - return { - userId, - includedPermissions: TEACHER_PERMISSIONS, - excludedPermissions: [], - }; - }); - const substituteTeacherIds = course.getSubstitutionTeacherIds().map((userId) => { + private async pocMigrateColumnBoardToPermissionContext( + columnBoard: ColumnBoard, + course: Course + ): Promise { + // NOTE: apply migration, defaulting to course context + + // NOTE: hardcoded permissions for POC purposes + const STUDENT_PERMISSIONS = [Permission.BOARD_READ]; + const TEACHER_PERMISSIONS = [ + Permission.BOARD_READ, + Permission.BOARD_CREATE_COLUMN, + Permission.BOARD_CREATE_COLUMN, + Permission.BOARD_DELETE, + Permission.BOARD_UPDATE_TITLE, + ]; + const SUBSTITUTE_TEACHER_PERMISSIONS = [...TEACHER_PERMISSIONS]; + + const studentIds = course.getStudentIds().map((userId) => { + return { + userId, + includedPermissions: STUDENT_PERMISSIONS, + excludedPermissions: [], + }; + }); + const teacherIds = course.getTeacherIds().map((userId) => { + return { + userId, + includedPermissions: TEACHER_PERMISSIONS, + excludedPermissions: [], + }; + }); + const substituteTeacherIds = course.getSubstitutionTeacherIds().map((userId) => { + return { + userId, + includedPermissions: SUBSTITUTE_TEACHER_PERMISSIONS, + excludedPermissions: [], + }; + }); + + const permissionCtxEntity = new PermissionContextEntity({ + name: 'ColumnBoard with course context', + parentContext: null, + contextReference: new ObjectId(columnBoard.id), + userDelta: new UserDelta([...studentIds, ...teacherIds, ...substituteTeacherIds]), + }); + + await this.permissionCtxRepo.save(permissionCtxEntity); + + const columns = columnBoard.children.filter((child): child is Column => isColumn(child)); + await Promise.all( + columns.map(async (column) => this.pocMigrateColumnToPermissionContext(column, permissionCtxEntity, course)) + ); + + return permissionCtxEntity; + } + + private async pocMigrateColumnToPermissionContext( + column: Column, + parentContext: PermissionContextEntity, + course: Course + ): Promise { + // NOTE: apply migration, defaulting to course context + + const permissionCtxEntity = new PermissionContextEntity({ + name: 'Column with course context', + parentContext, + contextReference: new ObjectId(column.id), + userDelta: new UserDelta([]), + }); + + await this.permissionCtxRepo.save(permissionCtxEntity); + + const { children } = await this.boardDoRepo.findById(column.id, 1); + + const cards = children.filter((child): child is Card => isCard(child)); + + await Promise.all( + cards.map(async (card) => + this.pocMigrateOtherToPermissionContext(card, permissionCtxEntity, course, 'Card with course context') + ) + ); + + return permissionCtxEntity; + } + + private async pocMigrateOtherToPermissionContext( + boardNode: T, + parentContext: PermissionContextEntity, + course: Course, + name = 'Boardnode with course context' + ): Promise { + // NOTE: apply migration, defaulting to course context + + const permissionCtxEntity = new PermissionContextEntity({ + name, + parentContext, + contextReference: new ObjectId(boardNode.id), + userDelta: new UserDelta([]), + }); + + await this.permissionCtxRepo.save(permissionCtxEntity); + + const elements = boardNode.children.filter((el): el is SubmissionItem => !(el instanceof SubmissionItem)); + await Promise.all( + elements.map((el) => + this.pocMigrateOtherToPermissionContext(el, permissionCtxEntity, course, 'Element with course context') + ) + ); + + const { children } = await this.boardDoRepo.findById(boardNode.id, 1); + + const submissionItems = children.filter((el): el is SubmissionItem => el instanceof SubmissionItem); + await Promise.all( + submissionItems.map((submissionItem) => + this.pocMigrateSubmissionItemToPermissionContext(submissionItem, permissionCtxEntity, course) + ) + ); + + return permissionCtxEntity; + } + + private async pocMigrateSubmissionItemToPermissionContext( + boardNode: SubmissionItem, + parentContext: PermissionContextEntity, + course: Course + ): Promise { + // NOTE: apply migration, defaulting to course context + const otherStudentIds = course + .getStudentIds() + .filter((userId) => userId !== boardNode.userId) + .map((userId) => { return { userId, - includedPermissions: SUBSTITUTE_TEACHER_PERMISSIONS, - excludedPermissions: [], + includedPermissions: [], + excludedPermissions: [Permission.BOARD_READ], }; }); - const permissionCtxEntity = new PermissionContextEntity({ - name: 'ColumnBoard with course context', - parentContext: null, - contextReference: new ObjectId(id), - userDelta: new UserDelta([...studentIds, ...teacherIds, ...substituteTeacherIds]), - }); + const studentIds = [ + ...otherStudentIds, + { userId: boardNode.userId, includedPermissions: [Permission.BOARD_READ], excludedPermissions: [] }, + ]; - await this.permissionCtxRepo.save(permissionCtxEntity); - } + const permissionCtxEntity = new PermissionContextEntity({ + name: 'Submission item with course context', + parentContext, + contextReference: new ObjectId(boardNode.id), + userDelta: new UserDelta([...studentIds]), + }); + + await this.permissionCtxRepo.save(permissionCtxEntity); + + const { children: submissionItemElements } = await this.boardDoRepo.findById(boardNode.id, 1); + + await Promise.all( + submissionItemElements.map((el) => + this.pocMigrateOtherToPermissionContext( + el, + permissionCtxEntity, + course, + 'SubmissionItemElement with course context' + ) + ) + ); + + return permissionCtxEntity; } } diff --git a/apps/server/src/modules/board/uc/board.uc.ts b/apps/server/src/modules/board/uc/board.uc.ts index 6b2b9130b03..24b7e056275 100644 --- a/apps/server/src/modules/board/uc/board.uc.ts +++ b/apps/server/src/modules/board/uc/board.uc.ts @@ -23,24 +23,70 @@ export class BoardUc extends BaseUc { this.logger.setContext(BoardUc.name); } - private async pocCheckPermission( + private async pocHasPermission( userId: EntityId, contextReference: EntityId, permissionsToContain: Permission[] - ): Promise { + ): Promise { + this.logger.debug({ action: 'pocHasPermission', userId, contextReference, permissionsToContain }); const permissions = await this.permissionContextService.resolvePermissions(userId, contextReference); const hasPermission = permissionsToContain.every((permission) => permissions.includes(permission)); + return hasPermission; + } + + private async pocCheckPermission( + userId: EntityId, + contextReference: EntityId, + permissionsToContain: Permission[] + ): Promise { + const hasPermission = await this.pocHasPermission(userId, contextReference, permissionsToContain); if (!hasPermission) { throw new UnauthorizedException(); } } + private async pocFilterColumnBoardChildrenByPermission(userId: EntityId, columnBoard: ColumnBoard): Promise { + // NOTE: This function will be obsolete once the authorization can be applied in the repo level + const columnsToRemove = await Promise.all( + columnBoard.children.map(async (child) => { + return { + column: child, + hasPermission: await this.pocHasPermission(userId, child.id, [Permission.BOARD_READ]), + }; + }) + ); + + columnsToRemove.forEach((columnToRemove) => { + if (!columnToRemove.hasPermission) { + columnBoard.removeChild(columnToRemove.column); + } + }); + + const cardsToRemove = await Promise.all( + columnBoard.children + .flatMap((child) => child.children) + .map(async (child) => { + return { + card: child, + hasPermission: await this.pocHasPermission(userId, child.id, [Permission.BOARD_READ]), + }; + }) + ); + + cardsToRemove.forEach((cardToRemove) => { + if (!cardToRemove.hasPermission) { + columnBoard.removeChild(cardToRemove.card); + } + }); + } + async findBoard(userId: EntityId, boardId: EntityId): Promise { this.logger.debug({ action: 'findBoard', userId, boardId }); await this.pocCheckPermission(userId, boardId, [Permission.BOARD_READ]); const board = await this.columnBoardService.findById(boardId); // await this.checkPermission(userId, board, Action.read); + await this.pocFilterColumnBoardChildrenByPermission(userId, board); return board; }