diff --git a/apps/server/src/modules/tool/common/common-tool.module.ts b/apps/server/src/modules/tool/common/common-tool.module.ts index f56f595a99c..57375c67e96 100644 --- a/apps/server/src/modules/tool/common/common-tool.module.ts +++ b/apps/server/src/modules/tool/common/common-tool.module.ts @@ -1,28 +1,13 @@ -import { forwardRef, Module } from '@nestjs/common'; +import { LegacySchoolModule } from '@modules/legacy-school'; +import { Module } from '@nestjs/common'; import { ContextExternalToolRepo, SchoolExternalToolRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; -import { AuthorizationModule } from '@modules/authorization'; -import { LegacySchoolModule } from '@modules/legacy-school'; -import { LearnroomModule } from '@modules/learnroom'; import { CommonToolService, CommonToolValidationService } from './service'; -import { ToolPermissionHelper } from './uc/tool-permission-helper'; @Module({ - imports: [LoggerModule, forwardRef(() => AuthorizationModule), LegacySchoolModule, LearnroomModule], + imports: [LoggerModule, LegacySchoolModule], // TODO: make deletion of entities cascading, adjust ExternalToolService.deleteExternalTool and remove the repos from here - providers: [ - CommonToolService, - CommonToolValidationService, - ToolPermissionHelper, - SchoolExternalToolRepo, - ContextExternalToolRepo, - ], - exports: [ - CommonToolService, - CommonToolValidationService, - ToolPermissionHelper, - SchoolExternalToolRepo, - ContextExternalToolRepo, - ], + providers: [CommonToolService, CommonToolValidationService, SchoolExternalToolRepo, ContextExternalToolRepo], + exports: [CommonToolService, CommonToolValidationService, SchoolExternalToolRepo, ContextExternalToolRepo], }) export class CommonToolModule {} diff --git a/apps/server/src/modules/tool/common/mapper/context-type.mapper.spec.ts b/apps/server/src/modules/tool/common/mapper/context-type.mapper.spec.ts deleted file mode 100644 index b05f50fc46c..00000000000 --- a/apps/server/src/modules/tool/common/mapper/context-type.mapper.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { AuthorizableReferenceType } from '@modules/authorization/domain'; -import { ToolContextType } from '../enum'; -import { ContextTypeMapper } from './context-type.mapper'; - -describe('context-type.mapper', () => { - it('should map ToolContextType.COURSE to AuthorizableReferenceType.Course', () => { - const mappedCourse = ContextTypeMapper.mapContextTypeToAllowedAuthorizationEntityType(ToolContextType.COURSE); - - expect(mappedCourse).toEqual(AuthorizableReferenceType.Course); - }); -}); diff --git a/apps/server/src/modules/tool/common/mapper/context-type.mapper.ts b/apps/server/src/modules/tool/common/mapper/context-type.mapper.ts deleted file mode 100644 index 3ae6902c232..00000000000 --- a/apps/server/src/modules/tool/common/mapper/context-type.mapper.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { AuthorizableReferenceType } from '@modules/authorization/domain/'; -import { ToolContextType } from '../enum'; - -const typeMapping: Record = { - [ToolContextType.COURSE]: AuthorizableReferenceType.Course, - [ToolContextType.BOARD_ELEMENT]: AuthorizableReferenceType.BoardNode, -}; - -export class ContextTypeMapper { - static mapContextTypeToAllowedAuthorizationEntityType(type: ToolContextType): AuthorizableReferenceType { - return typeMapping[type]; - } -} diff --git a/apps/server/src/modules/tool/common/uc/tool-permission-helper.ts b/apps/server/src/modules/tool/common/uc/tool-permission-helper.ts index 542903bc4be..525c8c5d3b6 100644 --- a/apps/server/src/modules/tool/common/uc/tool-permission-helper.ts +++ b/apps/server/src/modules/tool/common/uc/tool-permission-helper.ts @@ -1,11 +1,13 @@ -import { forwardRef, Inject, Injectable } from '@nestjs/common'; -import { Course, EntityId, LegacySchoolDo, User } from '@shared/domain'; -import { AuthorizationContext, AuthorizationService } from '@modules/authorization'; -import { LegacySchoolService } from '@modules/legacy-school'; +import { AuthorizationContext, AuthorizationService, ForbiddenLoggableException } from '@modules/authorization'; +import { AuthorizableReferenceType } from '@modules/authorization/domain'; +import { BoardDoAuthorizableService, ContentElementService } from '@modules/board'; import { CourseService } from '@modules/learnroom'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import { BoardDoAuthorizable, Course, EntityId, LegacySchoolDo, User } from '@shared/domain'; import { ContextExternalTool } from '../../context-external-tool/domain'; import { SchoolExternalTool } from '../../school-external-tool/domain'; -// import { ContextTypeMapper } from '../mapper'; +import { ToolContextType } from '../enum'; @Injectable() export class ToolPermissionHelper { @@ -15,7 +17,9 @@ export class ToolPermissionHelper { // invalid dependency on this place it is in UC layer in a other module // loading of ressources should be part of service layer // if it must resolve different loadings based on the request it can be added in own service and use in UC - private readonly courseService: CourseService + private readonly courseService: CourseService, + private readonly boardElementService: ContentElementService, + private readonly boardService: BoardDoAuthorizableService ) {} // TODO build interface to get contextDO by contextType @@ -24,19 +28,24 @@ export class ToolPermissionHelper { contextExternalTool: ContextExternalTool, context: AuthorizationContext ): Promise { - // loading of ressources should be part of the UC -> unnessasary awaits - const [authorizableUser, course]: [User, Course] = await Promise.all([ - this.authorizationService.getUserWithPermissions(userId), - this.courseService.findById(contextExternalTool.contextRef.id), - ]); + const authorizableUser = await this.authorizationService.getUserWithPermissions(userId); - if (contextExternalTool.id) { - this.authorizationService.checkPermission(authorizableUser, contextExternalTool, context); - } + this.authorizationService.checkPermission(authorizableUser, contextExternalTool, context); + + if (contextExternalTool.contextRef.type === ToolContextType.COURSE) { + // loading of ressources should be part of the UC -> unnessasary awaits + const course: Course = await this.courseService.findById(contextExternalTool.contextRef.id); + + this.authorizationService.checkPermission(authorizableUser, course, context); + } else if (contextExternalTool.contextRef.type === ToolContextType.BOARD_ELEMENT) { + const boardElement = await this.boardElementService.findById(contextExternalTool.contextRef.id); - // const type = ContextTypeMapper.mapContextTypeToAllowedAuthorizationEntityType(contextExternalTool.contextRef.type); - // no different types possible until it is fixed. - this.authorizationService.checkPermission(authorizableUser, course, context); + const board: BoardDoAuthorizable = await this.boardService.getBoardAuthorizable(boardElement); + + this.authorizationService.checkPermission(authorizableUser, board, context); + } else { + throw new ForbiddenLoggableException(userId, AuthorizableReferenceType.ContextExternalToolEntity, context); + } } public async ensureSchoolPermissions( diff --git a/apps/server/src/modules/tool/common/uc/tool-permissions-helper.spec.ts b/apps/server/src/modules/tool/common/uc/tool-permissions-helper.spec.ts index ad697a94694..15741e483ac 100644 --- a/apps/server/src/modules/tool/common/uc/tool-permissions-helper.spec.ts +++ b/apps/server/src/modules/tool/common/uc/tool-permissions-helper.spec.ts @@ -1,29 +1,41 @@ -import { Test, TestingModule } from '@nestjs/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { + AuthorizationContext, + AuthorizationContextBuilder, + AuthorizationService, + ForbiddenLoggableException, +} from '@modules/authorization'; +import { BoardDoAuthorizableService, ContentElementService } from '@modules/board'; +import { CourseService } from '@modules/learnroom'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { ForbiddenException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { BoardDoAuthorizable, ExternalToolElement, LegacySchoolDo, Permission } from '@shared/domain'; import { contextExternalToolFactory, courseFactory, + externalToolElementFactory, legacySchoolDoFactory, schoolExternalToolFactory, setupEntities, userFactory, } from '@shared/testing'; -import { Permission, LegacySchoolDo } from '@shared/domain'; -import { AuthorizationContext, AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; -import { LegacySchoolService } from '@modules/legacy-school'; -import { ForbiddenException } from '@nestjs/common'; -import { CourseService } from '@modules/learnroom'; -import { ContextExternalTool } from '../../context-external-tool/domain'; -import { ToolPermissionHelper } from './tool-permission-helper'; +import { boardDoAuthorizableFactory } from '@shared/testing/factory/domainobject/board/board-do-authorizable.factory'; +import { AuthorizableReferenceType } from '../../../authorization/domain'; +import { ContextExternalTool, ContextRef } from '../../context-external-tool/domain'; import { SchoolExternalTool } from '../../school-external-tool/domain'; +import { ToolContextType } from '../enum'; +import { ToolPermissionHelper } from './tool-permission-helper'; describe('ToolPermissionHelper', () => { let module: TestingModule; let helper: ToolPermissionHelper; let authorizationService: DeepMocked; - let courseService: DeepMocked; let schoolService: DeepMocked; + let courseService: DeepMocked; + let contentElementService: DeepMocked; + let boardDoAuthorizableService: DeepMocked; beforeAll(async () => { await setupEntities(); @@ -34,21 +46,31 @@ describe('ToolPermissionHelper', () => { provide: AuthorizationService, useValue: createMock(), }, + { + provide: LegacySchoolService, + useValue: createMock(), + }, { provide: CourseService, useValue: createMock(), }, { - provide: LegacySchoolService, - useValue: createMock(), + provide: ContentElementService, + useValue: createMock(), + }, + { + provide: BoardDoAuthorizableService, + useValue: createMock(), }, ], }).compile(); helper = module.get(ToolPermissionHelper); authorizationService = module.get(AuthorizationService); - courseService = module.get(CourseService); schoolService = module.get(LegacySchoolService); + courseService = module.get(CourseService); + contentElementService = module.get(ContentElementService); + boardDoAuthorizableService = module.get(BoardDoAuthorizableService); }); afterAll(async () => { @@ -60,16 +82,20 @@ describe('ToolPermissionHelper', () => { }); describe('ensureContextPermissions', () => { - describe('when context external tool with id is given', () => { + describe('when a context external tool for context "course" is given', () => { const setup = () => { const user = userFactory.buildWithId(); const course = courseFactory.buildWithId(); - const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId(); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId({ + contextRef: new ContextRef({ + id: course.id, + type: ToolContextType.COURSE, + }), + }); const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_USER]); - courseService.findById.mockResolvedValueOnce(course); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - authorizationService.checkPermission.mockReturnValueOnce().mockReturnValueOnce(); + courseService.findById.mockResolvedValueOnce(course); return { user, @@ -88,70 +114,97 @@ describe('ToolPermissionHelper', () => { expect(authorizationService.checkPermission).toHaveBeenNthCalledWith(1, user, contextExternalTool, context); expect(authorizationService.checkPermission).toHaveBeenNthCalledWith(2, user, course, context); }); + }); - it('should return undefined', async () => { - const { user, contextExternalTool, context } = setup(); + describe('when a context external tool for context "board element" is given', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const externalToolElement: ExternalToolElement = externalToolElementFactory.build(); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId({ + contextRef: new ContextRef({ + id: externalToolElement.id, + type: ToolContextType.BOARD_ELEMENT, + }), + }); + const board: BoardDoAuthorizable = boardDoAuthorizableFactory.build(); + const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_USER]); - const result = await helper.ensureContextPermissions(user.id, contextExternalTool, context); + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + contentElementService.findById.mockResolvedValueOnce(externalToolElement); + boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce(board); - expect(result).toBeUndefined(); + return { + user, + board, + contextExternalTool, + context, + }; + }; + + it('should check permission for context external tool', async () => { + const { user, board, contextExternalTool, context } = setup(); + + await helper.ensureContextPermissions(user.id, contextExternalTool, context); + + expect(authorizationService.checkPermission).toHaveBeenCalledTimes(2); + expect(authorizationService.checkPermission).toHaveBeenNthCalledWith(1, user, contextExternalTool, context); + expect(authorizationService.checkPermission).toHaveBeenNthCalledWith(2, user, board, context); }); }); - describe('when context external tool without id is given', () => { + describe('when the context external tool has an unkown context', () => { const setup = () => { const user = userFactory.buildWithId(); - const course = courseFactory.buildWithId(); - const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build(); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId({ + contextRef: { + type: 'unknown type' as unknown as ToolContextType, + }, + }); const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_USER]); - courseService.findById.mockResolvedValueOnce(course); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); return { user, - course, contextExternalTool, context, }; }; - it('should check permission for context external tool', async () => { - const { user, course, contextExternalTool, context } = setup(); - - await helper.ensureContextPermissions(user.id, contextExternalTool, context); + it('should throw a forbidden loggable exception', async () => { + const { user, contextExternalTool, context } = setup(); - expect(authorizationService.checkPermission).toHaveBeenCalledTimes(1); - expect(authorizationService.checkPermission).toHaveBeenCalledWith(user, course, context); + await expect(helper.ensureContextPermissions(user.id, contextExternalTool, context)).rejects.toThrowError( + new ForbiddenLoggableException(user.id, AuthorizableReferenceType.ContextExternalToolEntity, context) + ); }); }); describe('when user is unauthorized', () => { const setup = () => { const user = userFactory.buildWithId(); - const course = courseFactory.buildWithId(); const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId(); const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_USER]); + const error = new ForbiddenException(); - courseService.findById.mockResolvedValueOnce(course); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); authorizationService.checkPermission.mockImplementationOnce(() => { - throw new ForbiddenException(); + throw error; }); return { user, - course, contextExternalTool, context, + error, }; }; - it('should check permission for context external tool', async () => { - const { user, contextExternalTool, context } = setup(); + it('should check permission for context external tool and fail', async () => { + const { user, contextExternalTool, context, error } = setup(); await expect(helper.ensureContextPermissions(user.id, contextExternalTool, context)).rejects.toThrowError( - new ForbiddenException() + error ); }); }); diff --git a/apps/server/src/modules/tool/tool-api.module.ts b/apps/server/src/modules/tool/tool-api.module.ts index 8513eaed3fc..b8d12a16006 100644 --- a/apps/server/src/modules/tool/tool-api.module.ts +++ b/apps/server/src/modules/tool/tool-api.module.ts @@ -1,10 +1,13 @@ -import { Module } from '@nestjs/common'; -import { LtiToolRepo } from '@shared/repo'; -import { LoggerModule } from '@src/core/logger'; import { AuthorizationModule } from '@modules/authorization'; import { LegacySchoolModule } from '@modules/legacy-school'; import { UserModule } from '@modules/user'; +import { Module } from '@nestjs/common'; +import { LtiToolRepo } from '@shared/repo'; +import { LoggerModule } from '@src/core/logger'; +import { BoardModule } from '../board'; +import { LearnroomModule } from '../learnroom'; import { CommonToolModule } from './common'; +import { ToolPermissionHelper } from './common/uc/tool-permission-helper'; import { ToolContextController } from './context-external-tool/controller'; import { ToolReferenceController } from './context-external-tool/controller/tool-reference.controller'; import { ContextExternalToolUc, ToolReferenceUc } from './context-external-tool/uc'; @@ -29,6 +32,8 @@ import { ToolModule } from './tool.module'; LoggerModule, LegacySchoolModule, ToolConfigModule, + LearnroomModule, + BoardModule, ], controllers: [ ToolLaunchController, @@ -51,6 +56,7 @@ import { ToolModule } from './tool.module'; ContextExternalToolUc, ToolLaunchUc, ToolReferenceUc, + ToolPermissionHelper, ], }) export class ToolApiModule {} diff --git a/apps/server/src/shared/testing/factory/domainobject/board/board-do-authorizable.factory.ts b/apps/server/src/shared/testing/factory/domainobject/board/board-do-authorizable.factory.ts new file mode 100644 index 00000000000..f774ba97bb4 --- /dev/null +++ b/apps/server/src/shared/testing/factory/domainobject/board/board-do-authorizable.factory.ts @@ -0,0 +1,14 @@ +import { BoardDoAuthorizable, BoardDoAuthorizableProps, UserRoleEnum } from '@shared/domain/domainobject/board'; +import { ObjectId } from 'bson'; +import { DomainObjectFactory } from '../domain-object.factory'; + +export const boardDoAuthorizableFactory = DomainObjectFactory.define( + BoardDoAuthorizable, + () => { + return { + id: new ObjectId().toHexString(), + users: [], + requiredUserRole: UserRoleEnum.STUDENT, + }; + } +); diff --git a/apps/server/src/shared/testing/factory/domainobject/board/external-tool.do.factory.ts b/apps/server/src/shared/testing/factory/domainobject/board/external-tool-element.do.factory.ts similarity index 100% rename from apps/server/src/shared/testing/factory/domainobject/board/external-tool.do.factory.ts rename to apps/server/src/shared/testing/factory/domainobject/board/external-tool-element.do.factory.ts diff --git a/apps/server/src/shared/testing/factory/domainobject/board/index.ts b/apps/server/src/shared/testing/factory/domainobject/board/index.ts index 9a6cdf84839..802dcf744f3 100644 --- a/apps/server/src/shared/testing/factory/domainobject/board/index.ts +++ b/apps/server/src/shared/testing/factory/domainobject/board/index.ts @@ -1,7 +1,7 @@ export * from './card.do.factory'; export * from './column-board.do.factory'; export * from './column.do.factory'; -export * from './external-tool.do.factory'; +export * from './external-tool-element.do.factory'; export * from './file-element.do.factory'; export * from './link-element.do.factory'; export * from './rich-text-element.do.factory';