From 943220fbe3b168c6ed0248b78dd340fc81c0aba9 Mon Sep 17 00:00:00 2001 From: mrikallab <93978883+mrikallab@users.noreply.github.com> Date: Thu, 10 Oct 2024 10:57:24 +0200 Subject: [PATCH] N21-2236 fix lti encryption (#5281) --- .../templates/configmap_file_init.yml.j2 | 4 +- .../service/external-tool.service.spec.ts | 31 +-- .../service/external-tool.service.ts | 12 +- .../external-tool/uc/external-tool.uc.spec.ts | 247 +++++++++++++++++- .../tool/external-tool/uc/external-tool.uc.ts | 36 ++- .../src/modules/tool/tool-api.module.ts | 2 + 6 files changed, 289 insertions(+), 43 deletions(-) diff --git a/ansible/roles/schulcloud-server-init/templates/configmap_file_init.yml.j2 b/ansible/roles/schulcloud-server-init/templates/configmap_file_init.yml.j2 index f191ce69ca8..238b44226e4 100644 --- a/ansible/roles/schulcloud-server-init/templates/configmap_file_init.yml.j2 +++ b/ansible/roles/schulcloud-server-init/templates/configmap_file_init.yml.j2 @@ -519,8 +519,8 @@ data: echo "Inserting ctl seed data secrets to external-tools..." # Encrypt secrets of external tools that contain an lti11 config. - $CTL_SEED_SECRET_ONLINE_DIA_MATHE=$(node scripts/secret.js -s $AES_KEY -e $CTL_SEED_SECRET_ONLINE_DIA_MATHE) - $CTL_SEED_SECRET_ONLINE_DIA_DEUTSCH=$(node scripts/secret.js -s $AES_KEY -e $CTL_SEED_SECRET_ONLINE_DIA_DEUTSCH) + CTL_SEED_SECRET_ONLINE_DIA_MATHE=$(node scripts/secret.js -s $AES_KEY -e $CTL_SEED_SECRET_ONLINE_DIA_MATHE) + CTL_SEED_SECRET_ONLINE_DIA_DEUTSCH=$(node scripts/secret.js -s $AES_KEY -e $CTL_SEED_SECRET_ONLINE_DIA_DEUTSCH) mongosh $DATABASE__URL --quiet --eval 'db.getCollection("external-tools").updateOne( { diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool.service.spec.ts b/apps/server/src/modules/tool/external-tool/service/external-tool.service.spec.ts index 5e08193da2c..38a14509f9d 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool.service.spec.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool.service.spec.ts @@ -1,5 +1,4 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { DefaultEncryptionService, EncryptionService } from '@infra/encryption'; import { ProviderOauthClient } from '@modules/oauth-provider/domain'; import { UnprocessableEntityException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; @@ -24,7 +23,6 @@ describe(ExternalToolService.name, () => { let oauthProviderService: DeepMocked; let commonToolDeleteService: DeepMocked; let mapper: DeepMocked; - let encryptionService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -42,10 +40,6 @@ describe(ExternalToolService.name, () => { provide: ExternalToolServiceMapper, useValue: createMock(), }, - { - provide: DefaultEncryptionService, - useValue: createMock(), - }, { provide: LegacyLogger, useValue: createMock(), @@ -62,7 +56,6 @@ describe(ExternalToolService.name, () => { oauthProviderService = module.get(OauthProviderService); mapper = module.get(ExternalToolServiceMapper); commonToolDeleteService = module.get(CommonToolDeleteService); - encryptionService = module.get(DefaultEncryptionService); }); afterAll(async () => { @@ -160,29 +153,14 @@ describe(ExternalToolService.name, () => { describe('when lti11 config is set', () => { const setup = () => { - const encryptedSecret = 'encryptedSecret'; const { externalTool, lti11ToolConfig } = createTools(); externalTool.config = lti11ToolConfig; - const lti11ToolConfigDOEncrypted: Lti11ToolConfig = { ...lti11ToolConfig, secret: encryptedSecret }; - const externalToolDOEncrypted: ExternalTool = externalToolFactory.build({ - ...externalTool, - config: lti11ToolConfigDOEncrypted, - }); - encryptionService.encrypt.mockReturnValue(encryptedSecret); - externalToolRepo.save.mockResolvedValue(externalToolDOEncrypted); + externalToolRepo.save.mockResolvedValue(externalTool); - return { externalTool, lti11ToolConfig, encryptedSecret, externalToolDOEncrypted }; + return { externalTool, lti11ToolConfig }; }; - it('should encrypt the secret', async () => { - const { externalTool } = setup(); - - await service.createExternalTool(externalTool); - - expect(encryptionService.encrypt).toHaveBeenCalledWith('secret'); - }); - it('should call the repo to save a tool', async () => { const { externalTool } = setup(); @@ -192,11 +170,11 @@ describe(ExternalToolService.name, () => { }); it('should save DO', async () => { - const { externalTool, externalToolDOEncrypted } = setup(); + const { externalTool } = setup(); const result: ExternalTool = await service.createExternalTool(externalTool); - expect(result).toEqual(externalToolDOEncrypted); + expect(result).toEqual(externalTool); }); }); }); @@ -512,7 +490,6 @@ describe(ExternalToolService.name, () => { describe('updateExternalTool', () => { describe('when external tool with lti11 config is given', () => { const setup = () => { - encryptionService.encrypt.mockReturnValue('newEncryptedSecret'); const changedTool: ExternalTool = externalToolFactory .withLti11Config({ secret: 'newEncryptedSecret' }) .build({ name: 'newName' }); diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts b/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts index 784e64c5643..1d320179cd6 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts @@ -1,7 +1,6 @@ -import { DefaultEncryptionService, EncryptionService } from '@infra/encryption'; import { ProviderOauthClient } from '@modules/oauth-provider/domain'; import { OauthProviderService } from '@modules/oauth-provider/domain/service/oauth-provider.service'; -import { Inject, Injectable, UnprocessableEntityException } from '@nestjs/common'; +import { Injectable, UnprocessableEntityException } from '@nestjs/common'; import { Page } from '@shared/domain/domainobject'; import { IFindOptions } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; @@ -19,15 +18,12 @@ export class ExternalToolService { private readonly externalToolRepo: ExternalToolRepo, private readonly oauthProviderService: OauthProviderService, private readonly mapper: ExternalToolServiceMapper, - @Inject(DefaultEncryptionService) private readonly encryptionService: EncryptionService, private readonly legacyLogger: LegacyLogger, private readonly commonToolDeleteService: CommonToolDeleteService ) {} public async createExternalTool(externalTool: ExternalTool): Promise { - if (ExternalTool.isLti11Config(externalTool.config) && externalTool.config.secret) { - externalTool.config.secret = this.encryptionService.encrypt(externalTool.config.secret); - } else if (ExternalTool.isOauth2Config(externalTool.config)) { + if (ExternalTool.isOauth2Config(externalTool.config)) { const oauthClient: Partial = this.mapper.mapDoToProviderOauthClient( externalTool.name, externalTool.config @@ -42,10 +38,6 @@ export class ExternalToolService { } public async updateExternalTool(toUpdate: ExternalTool): Promise { - if (ExternalTool.isLti11Config(toUpdate.config) && toUpdate.config.secret) { - toUpdate.config.secret = this.encryptionService.encrypt(toUpdate.config.secret); - } - await this.updateOauth2ToolConfig(toUpdate); const externalTool: ExternalTool = await this.externalToolRepo.save(toUpdate); diff --git a/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.spec.ts b/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.spec.ts index b3af5f0e83e..625d60eee53 100644 --- a/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.spec.ts +++ b/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.spec.ts @@ -1,5 +1,6 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { DefaultEncryptionService, EncryptionService } from '@infra/encryption'; import { ObjectId } from '@mikro-orm/mongodb'; import { Action, AuthorizationService } from '@modules/authorization'; import { School, SchoolService } from '@modules/school'; @@ -13,6 +14,7 @@ import { Role, User } from '@shared/domain/entity'; import { IFindOptions, Permission, SortOrder } from '@shared/domain/interface'; import { currentUserFactory, roleFactory, setupEntities, userFactory } from '@shared/testing'; import { CustomParameter } from '../../common/domain'; +import { LtiMessageType, LtiPrivacyPermission, ToolConfigType } from '../../common/enum'; import { ExternalToolSearchQuery } from '../../common/interface'; import { CommonToolMetadataService } from '../../common/service/common-tool-metadata.service'; import { schoolExternalToolFactory } from '../../school-external-tool/testing'; @@ -22,6 +24,7 @@ import { ExternalToolMetadata, ExternalToolParameterDatasheetTemplateProperty, ExternalToolProps, + Lti11ToolConfig, Oauth2ToolConfig, } from '../domain'; import { @@ -36,9 +39,10 @@ import { externalToolDatasheetTemplateDataFactory, externalToolFactory, fileRecordRefFactory, + lti11ToolConfigFactory, oauth2ToolConfigFactory, } from '../testing'; -import { ExternalToolCreate, ExternalToolImportResult, ExternalToolUpdate } from './dto'; +import { ExternalToolCreate, ExternalToolImportResult, ExternalToolUpdate, Lti11ToolConfigUpdate } from './dto'; import { ExternalToolUc } from './external-tool.uc'; describe(ExternalToolUc.name, () => { @@ -54,6 +58,7 @@ describe(ExternalToolUc.name, () => { let commonToolMetadataService: DeepMocked; let pdfService: DeepMocked; let externalToolImageService: DeepMocked; + let encryptionService: DeepMocked; beforeAll(async () => { await setupEntities(); @@ -99,6 +104,10 @@ describe(ExternalToolUc.name, () => { provide: ExternalToolImageService, useValue: createMock(), }, + { + provide: DefaultEncryptionService, + useValue: createMock(), + }, ], }).compile(); @@ -112,6 +121,7 @@ describe(ExternalToolUc.name, () => { commonToolMetadataService = module.get(CommonToolMetadataService); pdfService = module.get(DatasheetPdfService); externalToolImageService = module.get(ExternalToolImageService); + encryptionService = module.get(DefaultEncryptionService); }); afterAll(async () => { @@ -140,6 +150,7 @@ describe(ExternalToolUc.name, () => { const externalTool: ExternalTool = externalToolFactory.withCustomParameters(1).buildWithId(); const oauth2ConfigWithoutExternalData: Oauth2ToolConfig = oauth2ToolConfigFactory.build(); + const lti11ToolConfig: Lti11ToolConfig = lti11ToolConfigFactory.build(); const query: ExternalToolSearchQuery = { name: externalTool.name, @@ -171,6 +182,7 @@ describe(ExternalToolUc.name, () => { query, toolId, mockLogoBase64, + lti11ToolConfig, }; }; @@ -334,6 +346,44 @@ describe(ExternalToolUc.name, () => { ); }); }); + + describe('when external tool with lti11 config is given', () => { + const setupLTI = () => { + const { currentUser } = setupAuthorization(); + const { externalTool, lti11ToolConfig } = setupDefault(); + externalTool.config = lti11ToolConfig; + + encryptionService.encrypt.mockReturnValue('encrypted'); + + return { + currentUser, + externalTool, + }; + }; + it('should call the encryption service', async () => { + const { currentUser, externalTool } = setupLTI(); + + await uc.createExternalTool(currentUser.userId, externalTool.getProps(), 'jwt'); + + expect(encryptionService.encrypt).toHaveBeenCalledWith('secret'); + }); + + it('should call the service to save a tool', async () => { + const { currentUser, externalTool } = setupLTI(); + + await uc.createExternalTool(currentUser.userId, externalTool.getProps(), 'jwt'); + + expect(externalToolService.createExternalTool).toHaveBeenNthCalledWith( + 1, + new ExternalTool({ + ...externalTool.getProps(), + logo: 'base64LogoString', + config: { ...externalTool.config, secret: 'encrypted' }, + id: expect.any(String), + }) + ); + }); + }); }); describe('importExternalTools', () => { @@ -382,6 +432,14 @@ describe(ExternalToolUc.name, () => { }; }; + it('should not call encryption service', async () => { + const { user, externalTool1, externalTool2 } = setup(); + + await uc.importExternalTools(user.id, [externalTool1.getProps(), externalTool2.getProps()], 'jwt'); + + expect(encryptionService.encrypt).not.toHaveBeenCalled(); + }); + it('should check the users permission', async () => { const { user, externalTool1, externalTool2 } = setup(); @@ -477,6 +535,67 @@ describe(ExternalToolUc.name, () => { }); }); + describe('when importing lti tool', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const externalTool1 = externalToolFactory.build({ + name: 'tool1', + medium: { + mediumId: 'medium1', + mediaSourceId: 'mediumSource1', + }, + }); + + const ltiConfig = lti11ToolConfigFactory.build(); + externalTool1.config = ltiConfig; + + const externalToolCreate1: ExternalToolCreate = { + ...externalTool1.getProps(), + thumbnailUrl: 'https://thumbnail.url1', + }; + + const jwt = 'jwt'; + + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + logoService.fetchLogo.mockResolvedValueOnce(undefined); + const thumbnailFileRecordRef = fileRecordRefFactory.build(); + externalToolImageService.uploadImageFileFromUrl.mockResolvedValueOnce(thumbnailFileRecordRef); + externalToolService.createExternalTool.mockResolvedValueOnce(externalTool1); + encryptionService.encrypt.mockReturnValue('encrypted'); + + return { + user, + externalTool1, + externalToolCreate1, + thumbnailFileRecordRef, + jwt, + }; + }; + + it('should call encryption service', async () => { + const { user, externalTool1 } = setup(); + + await uc.importExternalTools(user.id, [externalTool1.getProps()], 'jwt'); + + expect(encryptionService.encrypt).toHaveBeenCalled(); + }); + + it('should save tool', async () => { + const { user, externalTool1 } = setup(); + + await uc.importExternalTools(user.id, [externalTool1.getProps()], 'jwt'); + + expect(externalToolService.createExternalTool).toHaveBeenNthCalledWith( + 1, + new ExternalTool({ + ...externalTool1.getProps(), + config: lti11ToolConfigFactory.build({ ...externalTool1.config, secret: 'encrypted' }), + id: expect.any(String), + }) + ); + }); + }); + describe('when an external tools fails the validation', () => { const setup = () => { const user = userFactory.buildWithId(); @@ -643,6 +762,8 @@ describe(ExternalToolUc.name, () => { url: undefined, }); + const lti11ToolConfig: Lti11ToolConfig = lti11ToolConfigFactory.build(); + externalToolService.updateExternalTool.mockResolvedValue(updatedExternalTool); externalToolService.findById.mockResolvedValue(new ExternalTool(externalToolToUpdate)); @@ -652,6 +773,7 @@ describe(ExternalToolUc.name, () => { externalToolDOtoUpdate: externalToolToUpdate, toolId, mockLogoBase64, + lti11ToolConfig, }; }; @@ -868,6 +990,129 @@ describe(ExternalToolUc.name, () => { ); }); }); + + describe('when lti11 config is given and secret is not encrypted', () => { + const setupLTI = () => { + const { externalTool, toolId, mockLogoBase64 } = setupDefault(); + + const lti11ToolConfig: Lti11ToolConfig = lti11ToolConfigFactory.build(); + const externalToolToUpdate: ExternalToolUpdate = { + ...externalTool.getProps(), + name: 'newName', + config: lti11ToolConfig, + url: undefined, + logo: mockLogoBase64, + }; + + const currentTestTool = new ExternalTool({ ...externalToolToUpdate }); + const expectedLtiConfig = lti11ToolConfigFactory.buildWithId({ secret: 'encrypted' }); + currentTestTool.config = expectedLtiConfig; + + const updatedExternalTool: ExternalTool = externalToolFactory.build({ + ...externalTool.getProps(), + name: 'newName', + config: expectedLtiConfig, + url: undefined, + logo: mockLogoBase64, + }); + + externalToolService.findById.mockResolvedValue(currentTestTool); + encryptionService.encrypt.mockReturnValue(expectedLtiConfig.secret); + externalToolService.updateExternalTool.mockResolvedValue(updatedExternalTool); + + return { + externalTool, + updatedExternalToolDO: updatedExternalTool, + externalToolDOtoUpdate: externalToolToUpdate, + toolId, + mockLogoBase64, + lti11ToolConfig, + }; + }; + + it('should call encryption service', async () => { + const { currentUser } = setupAuthorization(); + const { toolId, externalToolDOtoUpdate, lti11ToolConfig } = setupLTI(); + + await uc.updateExternalTool(currentUser.userId, toolId, externalToolDOtoUpdate, 'jwt'); + + expect(encryptionService.encrypt).toHaveBeenNthCalledWith(1, lti11ToolConfig.secret); + }); + + it('should call the service to update the tool', async () => { + const { currentUser } = setupAuthorization(); + const { toolId, externalToolDOtoUpdate, updatedExternalToolDO } = setupLTI(); + + await uc.updateExternalTool(currentUser.userId, toolId, externalToolDOtoUpdate, 'jwt'); + + expect(externalToolService.updateExternalTool).toHaveBeenCalledWith(updatedExternalToolDO); + }); + }); + + describe('when lti11 config is given and secret is already encrypted', () => { + const setupLTI = () => { + const { externalTool, toolId, mockLogoBase64 } = setupDefault(); + + const lti11ToolConfig: Lti11ToolConfigUpdate = { + type: ToolConfigType.LTI11, + baseUrl: 'https://www.basic-baseUrl.com/', + key: 'key', + privacy_permission: LtiPrivacyPermission.PSEUDONYMOUS, + lti_message_type: LtiMessageType.BASIC_LTI_LAUNCH_REQUEST, + launch_presentation_locale: 'de-DE', + }; + + const externalToolToUpdate: ExternalToolUpdate = { + ...externalTool.getProps(), + config: lti11ToolConfig, + name: 'newName', + url: undefined, + }; + + const updatedExternalTool: ExternalTool = externalToolFactory.build({ + ...externalTool.getProps(), + config: lti11ToolConfigFactory.build({ ...externalTool.config, secret: 'encrypted' }), + name: 'newName', + url: undefined, + logo: mockLogoBase64, + }); + + externalToolService.findById.mockResolvedValue( + new ExternalTool({ + ...externalToolToUpdate, + config: lti11ToolConfigFactory.build({ ...externalToolToUpdate.config, secret: 'encrypted' }), + }) + ); + externalToolService.updateExternalTool.mockResolvedValue(updatedExternalTool); + + return { + externalTool, + updatedExternalToolDO: updatedExternalTool, + externalToolDOtoUpdate: externalToolToUpdate, + toolId, + mockLogoBase64, + lti11ToolConfig, + }; + }; + + it('should not call encryption service', async () => { + const { currentUser } = setupAuthorization(); + const { toolId, externalToolDOtoUpdate } = setupLTI(); + + await uc.updateExternalTool(currentUser.userId, toolId, externalToolDOtoUpdate, 'jwt'); + + expect(encryptionService.encrypt).not.toHaveBeenCalledWith(); + }); + + it('should call the service to update the tool', async () => { + const { currentUser } = setupAuthorization(); + const { toolId, externalToolDOtoUpdate, updatedExternalToolDO } = setupLTI(); + + await uc.updateExternalTool(currentUser.userId, toolId, externalToolDOtoUpdate, 'jwt'); + + expect(externalToolService.updateExternalTool).toHaveBeenCalledWith(updatedExternalToolDO); + }); + }); }); describe('deleteExternalTool', () => { diff --git a/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.ts b/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.ts index bc79c03f090..f9396b3e299 100644 --- a/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.ts +++ b/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.ts @@ -1,16 +1,25 @@ +import { DefaultEncryptionService, EncryptionService } from '@infra/encryption'; import { ObjectId } from '@mikro-orm/mongodb'; import { AuthorizationService } from '@modules/authorization'; import { School, SchoolService } from '@modules/school'; import { SchoolExternalTool } from '@modules/tool/school-external-tool/domain'; import { SchoolExternalToolService } from '@modules/tool/school-external-tool/service'; -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { Page } from '@shared/domain/domainobject'; import { User } from '@shared/domain/entity'; import { IFindOptions, Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { ExternalToolSearchQuery } from '../../common/interface'; import { CommonToolMetadataService } from '../../common/service/common-tool-metadata.service'; -import { ExternalTool, ExternalToolConfig, ExternalToolDatasheetTemplateData, ExternalToolMetadata } from '../domain'; +import { + BasicToolConfig, + ExternalTool, + ExternalToolConfig, + ExternalToolDatasheetTemplateData, + ExternalToolMetadata, + Lti11ToolConfig, + Oauth2ToolConfig, +} from '../domain'; import { ExternalToolDatasheetMapper } from '../mapper/external-tool-datasheet.mapper'; import { DatasheetPdfService, @@ -32,7 +41,8 @@ export class ExternalToolUc { private readonly externalToolLogoService: ExternalToolLogoService, private readonly commonToolMetadataService: CommonToolMetadataService, private readonly datasheetPdfService: DatasheetPdfService, - private readonly externalToolImageService: ExternalToolImageService + private readonly externalToolImageService: ExternalToolImageService, + @Inject(DefaultEncryptionService) private readonly encryptionService: EncryptionService ) {} public async createExternalTool( @@ -42,6 +52,8 @@ export class ExternalToolUc { ): Promise { await this.ensurePermission(userId, Permission.TOOL_ADMIN); + externalToolCreate.config = this.encryptLtiSecret(externalToolCreate); + const tool: ExternalTool = await this.validateAndSaveExternalTool(externalToolCreate, jwt); return tool; @@ -64,6 +76,8 @@ export class ExternalToolUc { }); try { + externalTool.config = this.encryptLtiSecret(externalTool); + // eslint-disable-next-line no-await-in-loop const savedTool: ExternalTool = await this.validateAndSaveExternalTool(externalTool, jwt); @@ -118,6 +132,8 @@ export class ExternalToolUc { ): Promise { await this.ensurePermission(userId, Permission.TOOL_ADMIN); + externalToolUpdate.config = this.encryptLtiSecret(externalToolUpdate); + const { thumbnailUrl, ...externalToolUpdateProps } = externalToolUpdate; const currentExternalTool: ExternalTool = await this.externalToolService.findById(toolId); @@ -248,4 +264,18 @@ export class ExternalToolUc { return fileName; } + + private encryptLtiSecret( + externalTool: ExternalToolCreate | ExternalToolUpdate + ): BasicToolConfig | Lti11ToolConfig | Oauth2ToolConfig { + if (ExternalTool.isLti11Config(externalTool.config) && externalTool.config.secret) { + const encrypted = this.encryptionService.encrypt(externalTool.config.secret); + + const updatedConfig = new Lti11ToolConfig({ ...externalTool.config, secret: encrypted }); + + return updatedConfig; + } + + return externalTool.config; + } } diff --git a/apps/server/src/modules/tool/tool-api.module.ts b/apps/server/src/modules/tool/tool-api.module.ts index 12d33bd3d90..51eec1332ff 100644 --- a/apps/server/src/modules/tool/tool-api.module.ts +++ b/apps/server/src/modules/tool/tool-api.module.ts @@ -1,3 +1,4 @@ +import { EncryptionModule } from '@infra/encryption'; import { AuthorizationModule } from '@modules/authorization'; import { BoardModule } from '@modules/board'; import { LegacySchoolModule } from '@modules/legacy-school'; @@ -35,6 +36,7 @@ import { ToolModule } from './tool.module'; BoardModule, SchoolModule, UserLicenseModule, + EncryptionModule, ], controllers: [ ToolLaunchController,