diff --git a/apps/server/src/apps/server.app.ts b/apps/server/src/apps/server.app.ts index 6322bcd568f..7f51ee9b92e 100644 --- a/apps/server/src/apps/server.app.ts +++ b/apps/server/src/apps/server.app.ts @@ -1,33 +1,34 @@ /* istanbul ignore file */ +import { Mail, MailService } from '@infra/mail'; // application imports /* eslint-disable no-console */ import { MikroORM } from '@mikro-orm/core'; -import { NestFactory } from '@nestjs/core'; -import { ExpressAdapter } from '@nestjs/platform-express'; -import { enableOpenApiDocs } from '@shared/controller/swagger'; -import { Mail, MailService } from '@infra/mail'; -import { LegacyLogger, Logger } from '@src/core/logger'; import { AccountService } from '@modules/account'; -import { TeamService } from '@modules/teams/service/team.service'; import { AccountValidationService } from '@modules/account/services/account.validation.service'; import { AccountUc } from '@modules/account/uc/account.uc'; +import { SystemRule } from '@modules/authorization/domain/rules'; import { CollaborativeStorageUc } from '@modules/collaborative-storage/uc/collaborative-storage.uc'; import { GroupService } from '@modules/group'; +import { FeathersRosterService } from '@modules/pseudonym'; import { RocketChatService } from '@modules/rocketchat'; import { ServerModule } from '@modules/server'; +import { TeamService } from '@modules/teams/service/team.service'; +import { NestFactory } from '@nestjs/core'; +import { ExpressAdapter } from '@nestjs/platform-express'; +import { enableOpenApiDocs } from '@shared/controller/swagger'; +import { LegacyLogger, Logger } from '@src/core/logger'; import express from 'express'; import { join } from 'path'; // register source-map-support for debugging import { install as sourceMapInstall } from 'source-map-support'; -import { FeathersRosterService } from '@modules/pseudonym'; -import legacyAppPromise = require('../../../../src/app'); import { AppStartLoggable } from './helpers/app-start-loggable'; import { addPrometheusMetricsMiddlewaresIfEnabled, createAndStartPrometheusMetricsAppIfEnabled, } from './helpers/prometheus-metrics'; +import legacyAppPromise = require('../../../../src/app'); async function bootstrap() { sourceMapInstall(); @@ -85,6 +86,8 @@ async function bootstrap() { feathersExpress.services['nest-feathers-roster-service'] = nestApp.get(FeathersRosterService); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access feathersExpress.services['nest-group-service'] = nestApp.get(GroupService); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access + feathersExpress.services['nest-system-rule'] = nestApp.get(SystemRule); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment feathersExpress.services['nest-orm'] = orm; diff --git a/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.integration.spec.ts b/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.integration.spec.ts index 98ed3552918..bae96a2d119 100644 --- a/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.integration.spec.ts +++ b/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.integration.spec.ts @@ -1,14 +1,14 @@ import { createMock } from '@golevelup/ts-jest'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import KeycloakAdminClient from '@keycloak/keycloak-admin-client-cjs/keycloak-admin-client-cjs-index'; import AuthenticationExecutionExportRepresentation from '@keycloak/keycloak-admin-client/lib/defs/authenticationExecutionExportRepresentation'; import AuthenticationFlowRepresentation from '@keycloak/keycloak-admin-client/lib/defs/authenticationFlowRepresentation'; +import { LegacySystemService } from '@modules/system'; import { ConfigModule } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { MongoMemoryDatabaseModule } from '@infra/database'; -import { SystemRepo } from '@shared/repo/system/system.repo'; -import { systemFactory } from '@shared/testing/factory'; +import { LegacySystemRepo } from '@shared/repo'; +import { systemEntityFactory } from '@shared/testing/factory'; import { LoggerModule } from '@src/core/logger'; -import { SystemService } from '@modules/system/service/system.service'; import { v1 } from 'uuid'; import { KeycloakAdministrationService } from '../../keycloak-administration/service/keycloak-administration.service'; import { KeycloakConfigurationModule } from '../keycloak-configuration.module'; @@ -17,15 +17,15 @@ import { KeycloakConfigurationService } from './keycloak-configuration.service'; describe('KeycloakConfigurationService Integration', () => { let module: TestingModule; let keycloak: KeycloakAdminClient; - let systemRepo: SystemRepo; + let systemRepo: LegacySystemRepo; let keycloakAdministrationService: KeycloakAdministrationService; let keycloakConfigurationService: KeycloakConfigurationService; let isKeycloakAvailable = false; const testRealm = `test-realm-${v1().toString()}`; const flowAlias = 'Direct Broker Flow'; - const systemServiceMock = createMock(); - const systems = systemFactory.withOidcConfig().buildList(1); + const systemServiceMock = createMock(); + const systems = systemEntityFactory.withOidcConfig().buildList(1); beforeAll(async () => { module = await Test.createTestingModule({ @@ -38,9 +38,9 @@ describe('KeycloakConfigurationService Integration', () => { validationOptions: { infer: true }, }), ], - providers: [SystemRepo], + providers: [LegacySystemRepo], }).compile(); - systemRepo = module.get(SystemRepo); + systemRepo = module.get(LegacySystemRepo); keycloakAdministrationService = module.get(KeycloakAdministrationService); keycloakConfigurationService = module.get(KeycloakConfigurationService); isKeycloakAvailable = await keycloakAdministrationService.testKcConnection(); diff --git a/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.spec.ts b/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.spec.ts index 0b47e876ba0..76bcbceebf2 100644 --- a/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.spec.ts +++ b/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.spec.ts @@ -13,7 +13,7 @@ import { HttpService } from '@nestjs/axios'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { SystemEntity, SystemTypeEnum } from '@shared/domain'; -import { systemFactory } from '@shared/testing'; +import { systemEntityFactory } from '@shared/testing'; import { AxiosResponse } from 'axios'; import { of } from 'rxjs'; import { v1 } from 'uuid'; @@ -63,7 +63,9 @@ describe('KeycloakConfigurationService Unit', () => { }; }; - const systems: SystemEntity[] = systemFactory.withOidcConfig().buildListWithId(1, { type: SystemTypeEnum.OIDC }); + const systems: SystemEntity[] = systemEntityFactory + .withOidcConfig() + .buildListWithId(1, { type: SystemTypeEnum.OIDC }); const oidcSystems = SystemOidcMapper.mapFromEntitiesToDtos(systems); const idps: IdentityProviderRepresentation[] = [ { diff --git a/apps/server/src/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.ts b/apps/server/src/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.ts index cbfa289165e..b6458640c75 100644 --- a/apps/server/src/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.ts +++ b/apps/server/src/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.ts @@ -1,5 +1,5 @@ import { DefaultEncryptionService, EncryptionService } from '@infra/encryption'; -import { OauthConfigDto } from '@modules/system/service'; +import { OauthConfigDto } from '@modules/system/service/dto'; import { HttpService } from '@nestjs/axios'; import { Inject, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; diff --git a/apps/server/src/modules/account/account.module.ts b/apps/server/src/modules/account/account.module.ts index 9a57ba37a44..2a60d4c453a 100644 --- a/apps/server/src/modules/account/account.module.ts +++ b/apps/server/src/modules/account/account.module.ts @@ -2,7 +2,7 @@ import { IdentityManagementModule } from '@infra/identity-management'; import { Module } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { PermissionService } from '@shared/domain'; -import { SystemRepo, UserRepo } from '@shared/repo'; +import { LegacySystemRepo, UserRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger/logger.module'; import { ServerConfig } from '../server/server.config'; import { AccountIdmToDtoMapper, AccountIdmToDtoMapperDb, AccountIdmToDtoMapperIdm } from './mapper'; @@ -24,7 +24,7 @@ function accountIdmToDtoMapperFactory(configService: ConfigService { mockOtherTeacherAccount = accountFactory.buildWithId({ userId: mockOtherTeacherUser.id, }); - const externalSystemA = systemFactory.buildWithId(); - const externalSystemB = systemFactory.buildWithId(); + const externalSystemA = systemEntityFactory.buildWithId(); + const externalSystemB = systemEntityFactory.buildWithId(); mockExternalUserAccount = accountFactory.buildWithId({ userId: mockExternalUser.id, username: 'unique.within@system', diff --git a/apps/server/src/modules/account/uc/account.uc.spec.ts b/apps/server/src/modules/account/uc/account.uc.spec.ts index 6810c2baa5c..2552469b8cf 100644 --- a/apps/server/src/modules/account/uc/account.uc.spec.ts +++ b/apps/server/src/modules/account/uc/account.uc.spec.ts @@ -20,7 +20,7 @@ import { User, } from '@shared/domain'; import { UserRepo } from '@shared/repo'; -import { accountFactory, schoolFactory, setupEntities, systemFactory, userFactory } from '@shared/testing'; +import { accountFactory, schoolFactory, setupEntities, systemEntityFactory, userFactory } from '@shared/testing'; import { BruteForcePrevention } from '@src/imports-from-feathers'; import { ObjectId } from 'bson'; import { @@ -431,7 +431,7 @@ describe('AccountUc', () => { userId: mockUnknownRoleUser.id, password: defaultPasswordHash, }); - const externalSystem = systemFactory.buildWithId(); + const externalSystem = systemEntityFactory.buildWithId(); mockExternalUserAccount = accountFactory.buildWithId({ userId: mockExternalUser.id, password: defaultPasswordHash, @@ -440,25 +440,25 @@ describe('AccountUc', () => { mockAccountWithoutUser = accountFactory.buildWithId({ userId: undefined, password: defaultPasswordHash, - systemId: systemFactory.buildWithId().id, + systemId: systemEntityFactory.buildWithId().id, }); mockAccountWithSystemId = accountFactory.withSystemId(new ObjectId(10)).build(); mockAccountWithLastFailedLogin = accountFactory.buildWithId({ userId: undefined, password: defaultPasswordHash, - systemId: systemFactory.buildWithId().id, + systemId: systemEntityFactory.buildWithId().id, lasttriedFailedLogin: new Date(), }); mockAccountWithOldLastFailedLogin = accountFactory.buildWithId({ userId: undefined, password: defaultPasswordHash, - systemId: systemFactory.buildWithId().id, + systemId: systemEntityFactory.buildWithId().id, lasttriedFailedLogin: new Date(new Date().getTime() - LOGIN_BLOCK_TIME - 1), }); mockAccountWithNoLastFailedLogin = accountFactory.buildWithId({ userId: undefined, password: defaultPasswordHash, - systemId: systemFactory.buildWithId().id, + systemId: systemEntityFactory.buildWithId().id, lasttriedFailedLogin: undefined, }); diff --git a/apps/server/src/modules/authentication/authentication.module.ts b/apps/server/src/modules/authentication/authentication.module.ts index 8f2bdcd3b0d..ca9872f75e0 100644 --- a/apps/server/src/modules/authentication/authentication.module.ts +++ b/apps/server/src/modules/authentication/authentication.module.ts @@ -1,14 +1,14 @@ -import { Module } from '@nestjs/common'; -import { JwtModule, JwtModuleOptions } from '@nestjs/jwt'; -import { PassportModule } from '@nestjs/passport'; import { CacheWrapperModule } from '@infra/cache'; import { IdentityManagementModule } from '@infra/identity-management'; -import { LegacySchoolRepo, SystemRepo, UserRepo } from '@shared/repo'; -import { LoggerModule } from '@src/core/logger'; import { AccountModule } from '@modules/account'; import { OauthModule } from '@modules/oauth/oauth.module'; import { RoleModule } from '@modules/role'; import { SystemModule } from '@modules/system'; +import { Module } from '@nestjs/common'; +import { JwtModule, JwtModuleOptions } from '@nestjs/jwt'; +import { PassportModule } from '@nestjs/passport'; +import { LegacySchoolRepo, LegacySystemRepo, UserRepo } from '@shared/repo'; +import { LoggerModule } from '@src/core/logger'; import { Algorithm, SignOptions } from 'jsonwebtoken'; import { jwtConstants } from './constants'; import { AuthenticationService } from './services/authentication.service'; @@ -69,7 +69,7 @@ const jwtModuleOptions: JwtModuleOptions = { JwtStrategy, JwtValidationAdapter, UserRepo, - SystemRepo, + LegacySystemRepo, LegacySchoolRepo, LocalStrategy, AuthenticationService, diff --git a/apps/server/src/modules/authentication/controllers/api-test/login.api.spec.ts b/apps/server/src/modules/authentication/controllers/api-test/login.api.spec.ts index 04683e182a8..6541da27b08 100644 --- a/apps/server/src/modules/authentication/controllers/api-test/login.api.spec.ts +++ b/apps/server/src/modules/authentication/controllers/api-test/login.api.spec.ts @@ -5,7 +5,7 @@ import { ServerTestModule } from '@modules/server/server.module'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Account, RoleName, SchoolEntity, SystemEntity, User } from '@shared/domain'; -import { accountFactory, roleFactory, schoolFactory, systemFactory, userFactory } from '@shared/testing'; +import { accountFactory, roleFactory, schoolFactory, systemEntityFactory, userFactory } from '@shared/testing'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import crypto, { KeyPairKeyObjectResult } from 'crypto'; @@ -149,7 +149,7 @@ describe('Login Controller (api)', () => { describe('when user login succeeds', () => { const setup = async () => { const schoolExternalId = 'mockSchoolExternalId'; - const system: SystemEntity = systemFactory.withLdapConfig().buildWithId({}); + const system: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({}); const school: SchoolEntity = schoolFactory.buildWithId({ systems: [system], externalId: schoolExternalId }); const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); @@ -200,7 +200,7 @@ describe('Login Controller (api)', () => { describe('when user login fails', () => { const setup = async () => { const schoolExternalId = 'mockSchoolExternalId'; - const system: SystemEntity = systemFactory.withLdapConfig().buildWithId({}); + const system: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({}); const school: SchoolEntity = schoolFactory.buildWithId({ systems: [system], externalId: schoolExternalId }); const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); @@ -238,7 +238,7 @@ describe('Login Controller (api)', () => { describe('when logging in as a user of the Central LDAP of Brandenburg', () => { const setup = async () => { const officialSchoolNumber = '01234'; - const system: SystemEntity = systemFactory.withLdapConfig().buildWithId({}); + const system: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({}); const school: SchoolEntity = schoolFactory.buildWithId({ systems: [system], externalId: officialSchoolNumber, @@ -301,7 +301,7 @@ describe('Login Controller (api)', () => { const schoolExternalId = 'schoolExternalId'; const userExternalId = 'userExternalId'; - const system = systemFactory.withOauthConfig().buildWithId({}); + const system = systemEntityFactory.withOauthConfig().buildWithId({}); const school = schoolFactory.buildWithId({ systems: [system], externalId: schoolExternalId }); const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); const user = userFactory.buildWithId({ school, roles: [studentRoles], externalId: userExternalId }); @@ -391,7 +391,7 @@ describe('Login Controller (api)', () => { const schoolExternalId = 'schoolExternalId'; const userExternalId = 'userExternalId'; - const system = systemFactory.withOauthConfig().buildWithId({}); + const system = systemEntityFactory.withOauthConfig().buildWithId({}); const school = schoolFactory.buildWithId({ systems: [system], externalId: schoolExternalId }); const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); const user = userFactory.buildWithId({ school, roles: [studentRoles], externalId: userExternalId }); diff --git a/apps/server/src/modules/authentication/services/ldap.service.spec.ts b/apps/server/src/modules/authentication/services/ldap.service.spec.ts index 8b334ac195c..ec0b8c05bb3 100644 --- a/apps/server/src/modules/authentication/services/ldap.service.spec.ts +++ b/apps/server/src/modules/authentication/services/ldap.service.spec.ts @@ -2,7 +2,7 @@ import { createMock } from '@golevelup/ts-jest'; import { UnauthorizedException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { SystemEntity } from '@shared/domain'; -import { systemFactory } from '@shared/testing'; +import { systemEntityFactory } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { LdapService } from './ldap.service'; @@ -59,7 +59,7 @@ describe('LdapService', () => { describe('checkLdapCredentials', () => { describe('when credentials are correct', () => { it('should login successfully', async () => { - const system: SystemEntity = systemFactory.withLdapConfig().buildWithId(); + const system: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId(); await expect( ldapService.checkLdapCredentials(system, 'connectSucceeds', 'mockPassword') ).resolves.not.toThrow(); @@ -68,7 +68,7 @@ describe('LdapService', () => { describe('when no ldap config is provided', () => { it('should throw error', async () => { - const system: SystemEntity = systemFactory.buildWithId(); + const system: SystemEntity = systemEntityFactory.buildWithId(); await expect(ldapService.checkLdapCredentials(system, 'mockUsername', 'mockPassword')).rejects.toThrow( new Error(`no LDAP config found in system ${system.id}`) ); @@ -77,7 +77,7 @@ describe('LdapService', () => { describe('when user is not authorized', () => { it('should throw unauthorized error', async () => { - const system: SystemEntity = systemFactory.withLdapConfig().buildWithId(); + const system: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId(); await expect(ldapService.checkLdapCredentials(system, 'mockUsername', 'mockPassword')).rejects.toThrow( new UnauthorizedException('User could not authenticate') ); @@ -86,7 +86,7 @@ describe('LdapService', () => { describe('when connected flag is not set', () => { it('should throw unauthorized error', async () => { - const system: SystemEntity = systemFactory.withLdapConfig().buildWithId(); + const system: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId(); await expect(ldapService.checkLdapCredentials(system, 'connectWithoutFlag', 'mockPassword')).rejects.toThrow( new UnauthorizedException('User could not authenticate') ); diff --git a/apps/server/src/modules/authentication/strategy/ldap.strategy.spec.ts b/apps/server/src/modules/authentication/strategy/ldap.strategy.spec.ts index b3067de04eb..c45a25cc310 100644 --- a/apps/server/src/modules/authentication/strategy/ldap.strategy.spec.ts +++ b/apps/server/src/modules/authentication/strategy/ldap.strategy.spec.ts @@ -4,7 +4,7 @@ import { UnauthorizedException } from '@nestjs/common'; import { PassportModule } from '@nestjs/passport'; import { Test, TestingModule } from '@nestjs/testing'; import { LegacySchoolDo, RoleName, SystemEntity, User } from '@shared/domain'; -import { LegacySchoolRepo, SystemRepo, UserRepo } from '@shared/repo'; +import { LegacySchoolRepo, LegacySystemRepo, UserRepo } from '@shared/repo'; import { accountDtoFactory, defaultTestPassword, @@ -12,7 +12,7 @@ import { legacySchoolDoFactory, schoolFactory, setupEntities, - systemFactory, + systemEntityFactory, userFactory, } from '@shared/testing'; import { Logger } from '@src/core/logger'; @@ -30,7 +30,7 @@ describe('LdapStrategy', () => { let schoolRepoMock: DeepMocked; let authenticationServiceMock: DeepMocked; let ldapServiceMock: DeepMocked; - let systemRepo: DeepMocked; + let systemRepo: DeepMocked; beforeAll(async () => { await setupEntities(); @@ -56,8 +56,8 @@ describe('LdapStrategy', () => { useValue: createMock(), }, { - provide: SystemRepo, - useValue: createMock(), + provide: LegacySystemRepo, + useValue: createMock(), }, { provide: Logger, @@ -71,7 +71,7 @@ describe('LdapStrategy', () => { schoolRepoMock = module.get(LegacySchoolRepo); userRepoMock = module.get(UserRepo); ldapServiceMock = module.get(LdapService); - systemRepo = module.get(SystemRepo); + systemRepo = module.get(LegacySystemRepo); }); afterAll(async () => { @@ -87,7 +87,7 @@ describe('LdapStrategy', () => { const setup = () => { const username = 'mockUserName'; - const system: SystemEntity = systemFactory.withLdapConfig({ rootPath: 'rootPath' }).buildWithId(); + const system: SystemEntity = systemEntityFactory.withLdapConfig({ rootPath: 'rootPath' }).buildWithId(); const user: User = userFactory.withRoleByName(RoleName.STUDENT).buildWithId({ ldapDn: undefined }); @@ -134,7 +134,7 @@ describe('LdapStrategy', () => { const setup = () => { const username = 'mockUserName'; - const system: SystemEntity = systemFactory.withLdapConfig({ rootPath: 'rootPath' }).buildWithId(); + const system: SystemEntity = systemEntityFactory.withLdapConfig({ rootPath: 'rootPath' }).buildWithId(); const user: User = userFactory.withRoleByName(RoleName.STUDENT).buildWithId({ ldapDn: 'mockLdapDn' }); @@ -181,7 +181,7 @@ describe('LdapStrategy', () => { const setup = () => { const username = 'mockUserName'; - const system: SystemEntity = systemFactory.withLdapConfig({ rootPath: 'rootPath' }).buildWithId(); + const system: SystemEntity = systemEntityFactory.withLdapConfig({ rootPath: 'rootPath' }).buildWithId(); const user: User = userFactory.withRoleByName(RoleName.STUDENT).buildWithId({ ldapDn: 'mockLdapDn' }); @@ -228,7 +228,7 @@ describe('LdapStrategy', () => { const setup = () => { const username = 'mockUserName'; - const system: SystemEntity = systemFactory.withLdapConfig({ rootPath: 'rootPath' }).buildWithId(); + const system: SystemEntity = systemEntityFactory.withLdapConfig({ rootPath: 'rootPath' }).buildWithId(); const user: User = userFactory.withRoleByName(RoleName.STUDENT).buildWithId({ ldapDn: 'mockLdapDn' }); @@ -275,7 +275,7 @@ describe('LdapStrategy', () => { const setup = () => { const username = 'mockUserName'; - const system: SystemEntity = systemFactory.withLdapConfig({ rootPath: 'rootPath' }).buildWithId(); + const system: SystemEntity = systemEntityFactory.withLdapConfig({ rootPath: 'rootPath' }).buildWithId(); const user: User = userFactory.withRoleByName(RoleName.STUDENT).buildWithId({ ldapDn: 'mockLdapDn' }); @@ -327,7 +327,7 @@ describe('LdapStrategy', () => { const error = new Error('error'); const username = 'mockUserName'; - const system: SystemEntity = systemFactory.withLdapConfig({ rootPath: 'rootPath' }).buildWithId(); + const system: SystemEntity = systemEntityFactory.withLdapConfig({ rootPath: 'rootPath' }).buildWithId(); const user: User = userFactory.withRoleByName(RoleName.STUDENT).buildWithId({ ldapDn: 'mockLdapDn' }); @@ -382,7 +382,7 @@ describe('LdapStrategy', () => { const setup = () => { const username = 'mockUserName'; - const system: SystemEntity = systemFactory.withLdapConfig().buildWithId(); + const system: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId(); const user: User = userFactory .withRoleByName(RoleName.STUDENT) @@ -445,7 +445,7 @@ describe('LdapStrategy', () => { const setup = () => { const username = 'mockUserName'; - const system: SystemEntity = systemFactory.withLdapConfig().buildWithId(); + const system: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId(); const user: User = userFactory .withRoleByName(RoleName.STUDENT) diff --git a/apps/server/src/modules/authentication/strategy/ldap.strategy.ts b/apps/server/src/modules/authentication/strategy/ldap.strategy.ts index 6f33e92f21a..8ffd1aa0bb6 100644 --- a/apps/server/src/modules/authentication/strategy/ldap.strategy.ts +++ b/apps/server/src/modules/authentication/strategy/ldap.strategy.ts @@ -2,7 +2,7 @@ import { AccountDto } from '@modules/account/services/dto'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { LegacySchoolDo, SystemEntity, User } from '@shared/domain'; -import { LegacySchoolRepo, SystemRepo, UserRepo } from '@shared/repo'; +import { LegacySchoolRepo, LegacySystemRepo, UserRepo } from '@shared/repo'; import { ErrorLoggable } from '@src/core/error/loggable/error.loggable'; import { Logger } from '@src/core/logger'; import { Strategy } from 'passport-custom'; @@ -15,7 +15,7 @@ import { LdapService } from '../services/ldap.service'; @Injectable() export class LdapStrategy extends PassportStrategy(Strategy, 'ldap') { constructor( - private readonly systemRepo: SystemRepo, + private readonly systemRepo: LegacySystemRepo, private readonly schoolRepo: LegacySchoolRepo, private readonly ldapService: LdapService, private readonly authenticationService: AuthenticationService, diff --git a/apps/server/src/modules/authorization/authorization.module.ts b/apps/server/src/modules/authorization/authorization.module.ts index f734a72ed8a..ea4f5d178fe 100644 --- a/apps/server/src/modules/authorization/authorization.module.ts +++ b/apps/server/src/modules/authorization/authorization.module.ts @@ -13,6 +13,7 @@ import { LessonRule, SchoolExternalToolRule, SubmissionRule, + SystemRule, TaskRule, TeamRule, UserLoginMigrationRule, @@ -43,7 +44,8 @@ import { FeathersAuthorizationService, FeathersAuthProvider } from './feathers'; UserRule, UserLoginMigrationRule, LegacySchoolRule, + SystemRule, ], - exports: [FeathersAuthorizationService, AuthorizationService], + exports: [FeathersAuthorizationService, AuthorizationService, SystemRule], }) export class AuthorizationModule {} diff --git a/apps/server/src/modules/authorization/domain/index.ts b/apps/server/src/modules/authorization/domain/index.ts index 0f5cfe67874..ccc52341b5b 100644 --- a/apps/server/src/modules/authorization/domain/index.ts +++ b/apps/server/src/modules/authorization/domain/index.ts @@ -2,3 +2,4 @@ export * from './service'; export * from './mapper'; export * from './error'; export * from './type'; +export { SystemRule } from './rules'; diff --git a/apps/server/src/modules/authorization/domain/rules/group.rule.ts b/apps/server/src/modules/authorization/domain/rules/group.rule.ts index e25e90230c8..9f9b82e15e5 100644 --- a/apps/server/src/modules/authorization/domain/rules/group.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/group.rule.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { User } from '@shared/domain'; +import { User } from '@shared/domain/entity'; import { Group } from '@src/modules/group'; -import { AuthorizationContext, Rule } from '../type'; import { AuthorizationHelper } from '../service/authorization.helper'; +import { AuthorizationContext, Rule } from '../type'; @Injectable() export class GroupRule implements Rule { diff --git a/apps/server/src/modules/authorization/domain/rules/index.ts b/apps/server/src/modules/authorization/domain/rules/index.ts index b78f43051d0..8a4f1df5109 100644 --- a/apps/server/src/modules/authorization/domain/rules/index.ts +++ b/apps/server/src/modules/authorization/domain/rules/index.ts @@ -15,3 +15,4 @@ export * from './team.rule'; export * from './user-login-migration.rule'; export * from './user.rule'; export * from './group.rule'; +export { SystemRule } from './system.rule'; diff --git a/apps/server/src/modules/authorization/domain/rules/system.rule.spec.ts b/apps/server/src/modules/authorization/domain/rules/system.rule.spec.ts new file mode 100644 index 00000000000..589e21a03d3 --- /dev/null +++ b/apps/server/src/modules/authorization/domain/rules/system.rule.spec.ts @@ -0,0 +1,260 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { System } from '@modules/system'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Permission, SchoolEntity, SystemEntity, User } from '@shared/domain'; +import { schoolFactory, setupEntities, systemEntityFactory, systemFactory, userFactory } from '@shared/testing'; +import { AuthorizationContextBuilder } from '../mapper'; +import { AuthorizationHelper } from '../service/authorization.helper'; +import { SystemRule } from './system.rule'; + +describe(SystemRule.name, () => { + let module: TestingModule; + let rule: SystemRule; + + let authorizationHelper: DeepMocked; + + beforeAll(async () => { + await setupEntities(); + + module = await Test.createTestingModule({ + providers: [ + SystemRule, + { + provide: AuthorizationHelper, + useValue: createMock(), + }, + ], + }).compile(); + + rule = module.get(SystemRule); + authorizationHelper = module.get(AuthorizationHelper); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('isApplicable', () => { + describe('when the entity is applicable', () => { + const setup = () => { + const user: User = userFactory.buildWithId(); + const system: System = systemFactory.build(); + + return { + user, + system, + }; + }; + + it('should return true', () => { + const { user, system } = setup(); + + const result = rule.isApplicable(user, system); + + expect(result).toEqual(true); + }); + }); + + describe('when the entity is not applicable', () => { + const setup = () => { + const user: User = userFactory.buildWithId(); + + return { + user, + }; + }; + + it('should return false', () => { + const { user } = setup(); + + const result = rule.isApplicable(user, {} as unknown as System); + + expect(result).toEqual(false); + }); + }); + }); + + describe('hasPermission', () => { + describe('when the user reads a system at his school and has the required permission', () => { + const setup = () => { + const system: System = systemFactory.build(); + const systemEntity: SystemEntity = systemEntityFactory.buildWithId(undefined, system.id); + const school: SchoolEntity = schoolFactory.buildWithId({ + systems: [systemEntity], + }); + const user: User = userFactory.buildWithId({ school }); + const authorizationContext = AuthorizationContextBuilder.read([Permission.SYSTEM_VIEW]); + + authorizationHelper.hasAllPermissions.mockReturnValueOnce(true); + + return { + user, + system, + authorizationContext, + }; + }; + + it('should check the permission', () => { + const { user, system, authorizationContext } = setup(); + + rule.hasPermission(user, system, authorizationContext); + + expect(authorizationHelper.hasAllPermissions).toHaveBeenCalledWith( + user, + authorizationContext.requiredPermissions + ); + }); + + it('should return true', () => { + const { user, system, authorizationContext } = setup(); + + const result = rule.hasPermission(user, system, authorizationContext); + + expect(result).toEqual(true); + }); + }); + + describe('when the user reads a system, but does not have the permission', () => { + const setup = () => { + const system: System = systemFactory.build(); + const systemEntity: SystemEntity = systemEntityFactory.buildWithId(undefined, system.id); + const school: SchoolEntity = schoolFactory.buildWithId({ + systems: [systemEntity], + }); + const user: User = userFactory.buildWithId({ school }); + const authorizationContext = AuthorizationContextBuilder.read([Permission.SYSTEM_VIEW]); + + authorizationHelper.hasAllPermissions.mockReturnValueOnce(false); + + return { + user, + system, + authorizationContext, + }; + }; + + it('should return false', () => { + const { user, system, authorizationContext } = setup(); + + const result = rule.hasPermission(user, system, authorizationContext); + + expect(result).toEqual(false); + }); + }); + + describe('when the user reads a system that is not at his school', () => { + const setup = () => { + const system: System = systemFactory.build(); + const school: SchoolEntity = schoolFactory.buildWithId({ + systems: [], + }); + const user: User = userFactory.buildWithId({ school }); + const authorizationContext = AuthorizationContextBuilder.read([Permission.SYSTEM_VIEW]); + + authorizationHelper.hasAllPermissions.mockReturnValueOnce(true); + + return { + user, + system, + authorizationContext, + }; + }; + + it('should return false', () => { + const { user, system, authorizationContext } = setup(); + + const result = rule.hasPermission(user, system, authorizationContext); + + expect(result).toEqual(false); + }); + }); + + describe('when the user writes a ldap system at his school and has the required permission and the ldap provider is "general"', () => { + const setup = () => { + const system: System = systemFactory.build({ ldapConfig: { provider: 'general' } }); + const systemEntity: SystemEntity = systemEntityFactory.buildWithId(undefined, system.id); + const school: SchoolEntity = schoolFactory.buildWithId({ + systems: [systemEntity], + }); + const user: User = userFactory.buildWithId({ school }); + const authorizationContext = AuthorizationContextBuilder.write([Permission.SYSTEM_CREATE]); + + authorizationHelper.hasAllPermissions.mockReturnValueOnce(true); + + return { + user, + system, + authorizationContext, + }; + }; + + it('should return true', () => { + const { user, system, authorizationContext } = setup(); + + const result = rule.hasPermission(user, system, authorizationContext); + + expect(result).toEqual(true); + }); + }); + + describe('when the user writes a ldap system at his school and has the required permission and the ldap provider is not "general"', () => { + const setup = () => { + const system: System = systemFactory.build({ ldapConfig: { provider: 'other provider' } }); + const systemEntity: SystemEntity = systemEntityFactory.buildWithId(undefined, system.id); + const school: SchoolEntity = schoolFactory.buildWithId({ + systems: [systemEntity], + }); + const user: User = userFactory.buildWithId({ school }); + const authorizationContext = AuthorizationContextBuilder.write([Permission.SYSTEM_CREATE]); + + authorizationHelper.hasAllPermissions.mockReturnValueOnce(true); + + return { + user, + system, + authorizationContext, + }; + }; + + it('should return false', () => { + const { user, system, authorizationContext } = setup(); + + const result = rule.hasPermission(user, system, authorizationContext); + + expect(result).toEqual(false); + }); + }); + + describe('when the user writes a non-ldap system at his school and has the required permission', () => { + const setup = () => { + const system: System = systemFactory.build({ ldapConfig: undefined }); + const systemEntity: SystemEntity = systemEntityFactory.buildWithId(undefined, system.id); + const school: SchoolEntity = schoolFactory.buildWithId({ + systems: [systemEntity], + }); + const user: User = userFactory.buildWithId({ school }); + const authorizationContext = AuthorizationContextBuilder.write([Permission.SYSTEM_CREATE]); + + authorizationHelper.hasAllPermissions.mockReturnValueOnce(true); + + return { + user, + system, + authorizationContext, + }; + }; + + it('should return false', () => { + const { user, system, authorizationContext } = setup(); + + const result = rule.hasPermission(user, system, authorizationContext); + + expect(result).toEqual(false); + }); + }); + }); +}); diff --git a/apps/server/src/modules/authorization/domain/rules/system.rule.ts b/apps/server/src/modules/authorization/domain/rules/system.rule.ts new file mode 100644 index 00000000000..8e63dae744c --- /dev/null +++ b/apps/server/src/modules/authorization/domain/rules/system.rule.ts @@ -0,0 +1,43 @@ +import { System } from '@modules/system'; +import { Injectable } from '@nestjs/common'; +import { User } from '@shared/domain/entity'; +import { AuthorizationHelper } from '../service/authorization.helper'; +import { Action, AuthorizationContext, Rule } from '../type'; + +@Injectable() +export class SystemRule implements Rule { + constructor(private readonly authorizationHelper: AuthorizationHelper) {} + + public isApplicable(user: User, domainObject: System): boolean { + const isMatched: boolean = domainObject instanceof System; + + return isMatched; + } + + public hasPermission(user: User, domainObject: System, context: AuthorizationContext): boolean { + const hasPermissions: boolean = this.authorizationHelper.hasAllPermissions(user, context.requiredPermissions); + + const hasAccess: boolean = user.school.systems.getIdentifiers().includes(domainObject.id); + + let isAuthorized: boolean = hasPermissions && hasAccess; + + if (context.action === Action.write) { + isAuthorized = isAuthorized && this.canEdit(domainObject); + } + + return isAuthorized; + } + + public canEdit(system: unknown): boolean { + const canEdit: boolean = + typeof system === 'object' && + !!system && + 'ldapConfig' in system && + typeof system.ldapConfig === 'object' && + !!system.ldapConfig && + 'provider' in system.ldapConfig && + system.ldapConfig.provider === 'general'; + + return canEdit; + } +} diff --git a/apps/server/src/modules/authorization/domain/service/rule-manager.spec.ts b/apps/server/src/modules/authorization/domain/service/rule-manager.spec.ts index 5b62f850416..950c881a9a7 100644 --- a/apps/server/src/modules/authorization/domain/service/rule-manager.spec.ts +++ b/apps/server/src/modules/authorization/domain/service/rule-manager.spec.ts @@ -8,15 +8,16 @@ import { ContextExternalToolRule, CourseGroupRule, CourseRule, + GroupRule, + LegacySchoolRule, LessonRule, SchoolExternalToolRule, - LegacySchoolRule, SubmissionRule, + SystemRule, TaskRule, TeamRule, - UserRule, UserLoginMigrationRule, - GroupRule, + UserRule, } from '../rules'; import { RuleManager } from './rule-manager'; @@ -35,6 +36,7 @@ describe('RuleManager', () => { let contextExternalToolRule: DeepMocked; let userLoginMigrationRule: DeepMocked; let groupRule: DeepMocked; + let systemRule: DeepMocked; beforeAll(async () => { await setupEntities(); @@ -55,6 +57,7 @@ describe('RuleManager', () => { { provide: BoardDoRule, useValue: createMock() }, { provide: ContextExternalToolRule, useValue: createMock() }, { provide: UserLoginMigrationRule, useValue: createMock() }, + { provide: SystemRule, useValue: createMock() }, ], }).compile(); @@ -72,6 +75,7 @@ describe('RuleManager', () => { contextExternalToolRule = await module.get(ContextExternalToolRule); userLoginMigrationRule = await module.get(UserLoginMigrationRule); groupRule = await module.get(GroupRule); + systemRule = await module.get(SystemRule); }); afterEach(() => { @@ -103,6 +107,7 @@ describe('RuleManager', () => { contextExternalToolRule.isApplicable.mockReturnValueOnce(false); userLoginMigrationRule.isApplicable.mockReturnValueOnce(false); groupRule.isApplicable.mockReturnValueOnce(false); + systemRule.isApplicable.mockReturnValueOnce(false); return { user, object, context }; }; @@ -125,6 +130,7 @@ describe('RuleManager', () => { expect(contextExternalToolRule.isApplicable).toBeCalled(); expect(userLoginMigrationRule.isApplicable).toBeCalled(); expect(groupRule.isApplicable).toBeCalled(); + expect(systemRule.isApplicable).toBeCalled(); }); it('should return CourseRule', () => { @@ -155,6 +161,7 @@ describe('RuleManager', () => { contextExternalToolRule.isApplicable.mockReturnValueOnce(false); userLoginMigrationRule.isApplicable.mockReturnValueOnce(false); groupRule.isApplicable.mockReturnValueOnce(false); + systemRule.isApplicable.mockReturnValueOnce(false); return { user, object, context }; }; @@ -185,6 +192,7 @@ describe('RuleManager', () => { contextExternalToolRule.isApplicable.mockReturnValueOnce(false); userLoginMigrationRule.isApplicable.mockReturnValueOnce(false); groupRule.isApplicable.mockReturnValueOnce(false); + systemRule.isApplicable.mockReturnValueOnce(false); return { user, object, context }; }; diff --git a/apps/server/src/modules/authorization/domain/service/rule-manager.ts b/apps/server/src/modules/authorization/domain/service/rule-manager.ts index 6e6237d125f..2f35f5e9939 100644 --- a/apps/server/src/modules/authorization/domain/service/rule-manager.ts +++ b/apps/server/src/modules/authorization/domain/service/rule-manager.ts @@ -1,22 +1,23 @@ import { Injectable, InternalServerErrorException, NotImplementedException } from '@nestjs/common'; import { BaseDO, User } from '@shared/domain'; import { AuthorizableObject } from '@shared/domain/domain-object'; // fix import when it is avaible -import type { AuthorizationContext, Rule } from '../type'; import { BoardDoRule, ContextExternalToolRule, CourseGroupRule, CourseRule, + GroupRule, LegacySchoolRule, LessonRule, SchoolExternalToolRule, SubmissionRule, + SystemRule, TaskRule, TeamRule, UserLoginMigrationRule, UserRule, - GroupRule, } from '../rules'; +import type { AuthorizationContext, Rule } from '../type'; @Injectable() export class RuleManager { @@ -35,7 +36,8 @@ export class RuleManager { private readonly boardDoRule: BoardDoRule, private readonly contextExternalToolRule: ContextExternalToolRule, private readonly userLoginMigrationRule: UserLoginMigrationRule, - private readonly groupRule: GroupRule + private readonly groupRule: GroupRule, + private readonly systemRule: SystemRule ) { this.rules = [ this.courseRule, @@ -51,6 +53,7 @@ export class RuleManager { this.contextExternalToolRule, this.userLoginMigrationRule, this.groupRule, + this.systemRule, ]; } diff --git a/apps/server/src/modules/authorization/index.ts b/apps/server/src/modules/authorization/index.ts index 13ae209f13d..b90a586ff54 100644 --- a/apps/server/src/modules/authorization/index.ts +++ b/apps/server/src/modules/authorization/index.ts @@ -12,6 +12,8 @@ export { AuthorizationService, ForbiddenLoggableException, Rule, + // For the use in feathers + SystemRule, } from './domain'; // Should not used anymore export { FeathersAuthorizationService } from './feathers'; diff --git a/apps/server/src/modules/group/controller/api-test/group.api.spec.ts b/apps/server/src/modules/group/controller/api-test/group.api.spec.ts index dc1753a6c7f..f5ab532f9b7 100644 --- a/apps/server/src/modules/group/controller/api-test/group.api.spec.ts +++ b/apps/server/src/modules/group/controller/api-test/group.api.spec.ts @@ -10,7 +10,7 @@ import { roleFactory, schoolFactory, schoolYearFactory, - systemFactory, + systemEntityFactory, TestApiClient, UserAndAccountTestFactory, userFactory, @@ -51,7 +51,7 @@ describe('Group (API)', () => { const teacherRole: Role = roleFactory.buildWithId({ name: RoleName.TEACHER }); const teacherUser: User = userFactory.buildWithId({ school, roles: [teacherRole] }); - const system: SystemEntity = systemFactory.buildWithId(); + const system: SystemEntity = systemEntityFactory.buildWithId(); const schoolYear: SchoolYearEntity = schoolYearFactory.buildWithId(); const clazz: ClassEntity = classEntityFactory.buildWithId({ name: 'Group A', diff --git a/apps/server/src/modules/group/uc/group.uc.spec.ts b/apps/server/src/modules/group/uc/group.uc.spec.ts index 51cf6151d45..10cfc03821f 100644 --- a/apps/server/src/modules/group/uc/group.uc.spec.ts +++ b/apps/server/src/modules/group/uc/group.uc.spec.ts @@ -7,7 +7,7 @@ import { classFactory } from '@modules/class/domain/testing/factory/class.factor import { LegacySchoolService, SchoolYearService } from '@modules/legacy-school'; import { RoleService } from '@modules/role'; import { RoleDto } from '@modules/role/service/dto/role.dto'; -import { SystemDto, SystemService } from '@modules/system'; +import { LegacySystemService, SystemDto } from '@modules/system'; import { UserService } from '@modules/user'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; @@ -23,13 +23,13 @@ import { userDoFactory, userFactory, } from '@shared/testing'; +import { SchoolYearQueryType } from '../controller/dto/interface'; import { Group, GroupTypes } from '../domain'; +import { UnknownQueryTypeLoggableException } from '../loggable'; import { GroupService } from '../service'; import { ClassInfoDto, ResolvedGroupDto } from './dto'; import { ClassRootType } from './dto/class-root-type'; import { GroupUc } from './group.uc'; -import { SchoolYearQueryType } from '../controller/dto/interface'; -import { UnknownQueryTypeLoggableException } from '../loggable'; describe('GroupUc', () => { let module: TestingModule; @@ -37,7 +37,7 @@ describe('GroupUc', () => { let groupService: DeepMocked; let classService: DeepMocked; - let systemService: DeepMocked; + let systemService: DeepMocked; let userService: DeepMocked; let roleService: DeepMocked; let schoolService: DeepMocked; @@ -57,8 +57,8 @@ describe('GroupUc', () => { useValue: createMock(), }, { - provide: SystemService, - useValue: createMock(), + provide: LegacySystemService, + useValue: createMock(), }, { provide: UserService, @@ -86,7 +86,7 @@ describe('GroupUc', () => { uc = module.get(GroupUc); groupService = module.get(GroupService); classService = module.get(ClassService); - systemService = module.get(SystemService); + systemService = module.get(LegacySystemService); userService = module.get(UserService); roleService = module.get(RoleService); schoolService = module.get(LegacySchoolService); diff --git a/apps/server/src/modules/group/uc/group.uc.ts b/apps/server/src/modules/group/uc/group.uc.ts index f7399fa2fc9..2de4d9d5a2c 100644 --- a/apps/server/src/modules/group/uc/group.uc.ts +++ b/apps/server/src/modules/group/uc/group.uc.ts @@ -4,24 +4,24 @@ import { Class } from '@modules/class/domain'; import { LegacySchoolService, SchoolYearService } from '@modules/legacy-school'; import { RoleService } from '@modules/role'; import { RoleDto } from '@modules/role/service/dto/role.dto'; -import { SystemDto, SystemService } from '@modules/system'; +import { LegacySystemService, SystemDto } from '@modules/system'; import { UserService } from '@modules/user'; import { Injectable } from '@nestjs/common'; import { EntityId, LegacySchoolDo, Page, Permission, SchoolYearEntity, SortOrder, User, UserDO } from '@shared/domain'; +import { SchoolYearQueryType } from '../controller/dto/interface'; import { Group, GroupUser } from '../domain'; +import { UnknownQueryTypeLoggableException } from '../loggable'; import { GroupService } from '../service'; import { SortHelper } from '../util'; import { ClassInfoDto, ResolvedGroupDto, ResolvedGroupUser } from './dto'; import { GroupUcMapper } from './mapper/group-uc.mapper'; -import { SchoolYearQueryType } from '../controller/dto/interface'; -import { UnknownQueryTypeLoggableException } from '../loggable'; @Injectable() export class GroupUc { constructor( private readonly groupService: GroupService, private readonly classService: ClassService, - private readonly systemService: SystemService, + private readonly systemService: LegacySystemService, private readonly userService: UserService, private readonly roleService: RoleService, private readonly schoolService: LegacySchoolService, diff --git a/apps/server/src/modules/management/seed-data/systems.ts b/apps/server/src/modules/management/seed-data/systems.ts index a9140458f6d..7aca67d896d 100644 --- a/apps/server/src/modules/management/seed-data/systems.ts +++ b/apps/server/src/modules/management/seed-data/systems.ts @@ -1,10 +1,10 @@ /* eslint-disable no-template-curly-in-string */ -import { SystemProperties } from '@shared/domain'; +import { SystemEntityProps } from '@shared/domain'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { systemFactory } from '@shared/testing'; +import { systemEntityFactory } from '@shared/testing'; import { DeepPartial } from 'fishery'; -type SystemPartial = DeepPartial & { +type SystemPartial = DeepPartial & { id?: string; createdAt?: string; updatedAt?: string; @@ -66,7 +66,7 @@ const data: SystemPartial[] = [ export function generateSystems(injectEnvVars: (s: string) => string) { const systems = data.map((d) => { d = JSON.parse(injectEnvVars(JSON.stringify(d))) as typeof d; - const params: DeepPartial = { + const params: DeepPartial = { alias: d.alias, displayName: d.displayName, type: d.type, @@ -77,7 +77,7 @@ export function generateSystems(injectEnvVars: (s: string) => string) { provisioningUrl: d.provisioningUrl, url: d.url, }; - const system = systemFactory.buildWithId(params, d.id); + const system = systemEntityFactory.buildWithId(params, d.id); if (d.createdAt) system.createdAt = new Date(d.createdAt); if (d.updatedAt) system.updatedAt = new Date(d.updatedAt); diff --git a/apps/server/src/modules/oauth/service/hydra.service.spec.ts b/apps/server/src/modules/oauth/service/hydra.service.spec.ts index 2dc2a22a6ce..37db961eb9d 100644 --- a/apps/server/src/modules/oauth/service/hydra.service.spec.ts +++ b/apps/server/src/modules/oauth/service/hydra.service.spec.ts @@ -1,18 +1,18 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { DefaultEncryptionService, SymetricKeyEncryptionService } from '@infra/encryption'; import { ObjectId } from '@mikro-orm/mongodb'; +import { CookiesDto } from '@modules/oauth/service/dto/cookies.dto'; +import { HydraRedirectDto } from '@modules/oauth/service/dto/hydra.redirect.dto'; +import { HydraSsoService } from '@modules/oauth/service/hydra.service'; import { HttpService } from '@nestjs/axios'; import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { LtiPrivacyPermission, LtiRoleType, OauthConfig } from '@shared/domain'; +import { LtiPrivacyPermission, LtiRoleType, OauthConfigEntity } from '@shared/domain'; import { LtiToolDO } from '@shared/domain/domainobject/ltitool.do'; -import { DefaultEncryptionService, SymetricKeyEncryptionService } from '@infra/encryption'; import { LtiToolRepo } from '@shared/repo'; import { axiosResponseFactory } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; -import { CookiesDto } from '@modules/oauth/service/dto/cookies.dto'; -import { HydraRedirectDto } from '@modules/oauth/service/dto/hydra.redirect.dto'; -import { HydraSsoService } from '@modules/oauth/service/hydra.service'; import { AxiosResponse } from 'axios'; import { of } from 'rxjs'; import { StatelessAuthorizationParams } from '../controller/dto/stateless-authorization.params'; @@ -47,7 +47,7 @@ describe('HydraService', () => { const scopes = 'openid uuid'; const apiHost = 'localhost'; - const oauthConfig: OauthConfig = new OauthConfig({ + const oauthConfig: OauthConfigEntity = new OauthConfigEntity({ clientId: '12345', clientSecret: 'mocksecret', tokenEndpoint: `${hydraUri}/oauth2/token`, @@ -242,7 +242,7 @@ describe('HydraService', () => { ltiToolRepo.findByOauthClientId.mockResolvedValue(ltiToolDoMock); // Act - const result: OauthConfig = await service.generateConfig(oauthConfig.clientId); + const result: OauthConfigEntity = await service.generateConfig(oauthConfig.clientId); // Assert expect(result).toEqual(oauthConfig); diff --git a/apps/server/src/modules/oauth/service/hydra.service.ts b/apps/server/src/modules/oauth/service/hydra.service.ts index 9c02a537d42..a74e1936af8 100644 --- a/apps/server/src/modules/oauth/service/hydra.service.ts +++ b/apps/server/src/modules/oauth/service/hydra.service.ts @@ -6,7 +6,7 @@ import { HydraRedirectDto } from '@modules/oauth/service/dto/hydra.redirect.dto' import { HttpService } from '@nestjs/axios'; import { Inject, InternalServerErrorException } from '@nestjs/common'; import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; -import { OauthConfig } from '@shared/domain'; +import { OauthConfigEntity } from '@shared/domain'; import { LtiToolDO } from '@shared/domain/domainobject/ltitool.do'; import { LtiToolRepo } from '@shared/repo'; import { LegacyLogger } from '@src/core/logger'; @@ -26,7 +26,7 @@ export class HydraSsoService { private readonly HOST: string = Configuration.get('HOST') as string; - async initAuth(oauthConfig: OauthConfig, axiosConfig: AxiosRequestConfig): Promise { + async initAuth(oauthConfig: OauthConfigEntity, axiosConfig: AxiosRequestConfig): Promise { const query = QueryString.stringify({ response_type: oauthConfig.responseType, scope: oauthConfig.scope, @@ -96,7 +96,7 @@ export class HydraSsoService { return cookiesDto; } - async generateConfig(oauthClientId: string): Promise { + async generateConfig(oauthClientId: string): Promise { const tool: LtiToolDO = await this.ltiRepo.findByOauthClientId(oauthClientId); // Needs to be checked, because the fields can be undefined @@ -105,7 +105,7 @@ export class HydraSsoService { } const hydraUri: string = Configuration.get('HYDRA_PUBLIC_URI') as string; - const hydraOauthConfig = new OauthConfig({ + const hydraOauthConfig = new OauthConfigEntity({ authEndpoint: `${hydraUri}/oauth2/auth`, clientId: tool.oAuthClientId, clientSecret: this.oAuthEncryptionService.encrypt(tool.secret), diff --git a/apps/server/src/modules/oauth/service/oauth.service.spec.ts b/apps/server/src/modules/oauth/service/oauth.service.spec.ts index 368586af7c4..390ee95c5e5 100644 --- a/apps/server/src/modules/oauth/service/oauth.service.spec.ts +++ b/apps/server/src/modules/oauth/service/oauth.service.spec.ts @@ -6,16 +6,16 @@ import { LegacySchoolService } from '@modules/legacy-school'; import { ProvisioningService } from '@modules/provisioning'; import { OauthConfigDto } from '@modules/system/service'; import { SystemDto } from '@modules/system/service/dto/system.dto'; -import { SystemService } from '@modules/system/service/system.service'; import { UserService } from '@modules/user'; import { MigrationCheckService } from '@modules/user-login-migration'; import { Test, TestingModule } from '@nestjs/testing'; -import { LegacySchoolDo, OauthConfig, SchoolFeatures, SystemEntity, UserDO } from '@shared/domain'; +import { LegacySchoolDo, OauthConfigEntity, SchoolFeatures, SystemEntity, UserDO } from '@shared/domain'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { legacySchoolDoFactory, setupEntities, systemFactory, userDoFactory } from '@shared/testing'; +import { legacySchoolDoFactory, setupEntities, systemEntityFactory, userDoFactory } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { OauthDataDto } from '@src/modules/provisioning/dto'; import jwt, { JwtPayload } from 'jsonwebtoken'; +import { LegacySystemService } from '../../system/service/legacy-system.service'; import { OAuthTokenDto } from '../interface'; import { OAuthSSOError, UserNotFoundAfterProvisioningLoggableException } from '../loggable'; import { OauthTokenResponse } from './dto'; @@ -44,13 +44,13 @@ describe('OAuthService', () => { let oAuthEncryptionService: DeepMocked; let provisioningService: DeepMocked; let userService: DeepMocked; - let systemService: DeepMocked; + let systemService: DeepMocked; let oauthAdapterService: DeepMocked; let migrationCheckService: DeepMocked; let schoolService: DeepMocked; let testSystem: SystemEntity; - let testOauthConfig: OauthConfig; + let testOauthConfig: OauthConfigEntity; const hostUri = 'https://mock.de'; @@ -81,8 +81,8 @@ describe('OAuthService', () => { useValue: createMock(), }, { - provide: SystemService, - useValue: createMock(), + provide: LegacySystemService, + useValue: createMock(), }, { provide: OauthAdapterService, @@ -99,7 +99,7 @@ describe('OAuthService', () => { oAuthEncryptionService = module.get(DefaultEncryptionService); provisioningService = module.get(ProvisioningService); userService = module.get(UserService); - systemService = module.get(SystemService); + systemService = module.get(LegacySystemService); oauthAdapterService = module.get(OauthAdapterService); migrationCheckService = module.get(MigrationCheckService); schoolService = module.get(LegacySchoolService); @@ -124,8 +124,8 @@ describe('OAuthService', () => { } }); - testSystem = systemFactory.withOauthConfig().buildWithId(); - testOauthConfig = testSystem.oauthConfig as OauthConfig; + testSystem = systemEntityFactory.withOauthConfig().buildWithId(); + testOauthConfig = testSystem.oauthConfig as OauthConfigEntity; }); describe('requestToken', () => { diff --git a/apps/server/src/modules/oauth/service/oauth.service.ts b/apps/server/src/modules/oauth/service/oauth.service.ts index 299198ef33a..5f13511766c 100644 --- a/apps/server/src/modules/oauth/service/oauth.service.ts +++ b/apps/server/src/modules/oauth/service/oauth.service.ts @@ -1,13 +1,13 @@ import { DefaultEncryptionService, EncryptionService } from '@infra/encryption'; import { LegacySchoolService } from '@modules/legacy-school'; import { OauthDataDto, ProvisioningService } from '@modules/provisioning'; -import { SystemService } from '@modules/system'; +import { LegacySystemService } from '@modules/system'; import { SystemDto } from '@modules/system/service'; import { UserService } from '@modules/user'; import { MigrationCheckService } from '@modules/user-login-migration'; import { Inject } from '@nestjs/common'; import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; -import { EntityId, LegacySchoolDo, OauthConfig, SchoolFeatures, UserDO } from '@shared/domain'; +import { EntityId, LegacySchoolDo, OauthConfigEntity, SchoolFeatures, UserDO } from '@shared/domain'; import { LegacyLogger } from '@src/core/logger'; import jwt, { JwtPayload } from 'jsonwebtoken'; import { OAuthTokenDto } from '../interface'; @@ -24,7 +24,7 @@ export class OAuthService { @Inject(DefaultEncryptionService) private readonly oAuthEncryptionService: EncryptionService, private readonly logger: LegacyLogger, private readonly provisioningService: ProvisioningService, - private readonly systemService: SystemService, + private readonly systemService: LegacySystemService, private readonly migrationCheckService: MigrationCheckService, private readonly schoolService: LegacySchoolService ) { @@ -118,7 +118,7 @@ export class OAuthService { return !!school.features?.includes(SchoolFeatures.OAUTH_PROVISIONING_ENABLED); } - async requestToken(code: string, oauthConfig: OauthConfig, redirectUri: string): Promise { + async requestToken(code: string, oauthConfig: OauthConfigEntity, redirectUri: string): Promise { const payload: AuthenticationCodeGrantTokenRequest = this.buildTokenRequestPayload(code, oauthConfig, redirectUri); const responseToken: OauthTokenResponse = await this.oauthAdapterService.sendAuthenticationCodeTokenRequest( @@ -130,7 +130,7 @@ export class OAuthService { return tokenDto; } - async validateToken(idToken: string, oauthConfig: OauthConfig): Promise { + async validateToken(idToken: string, oauthConfig: OauthConfigEntity): Promise { const publicKey: string = await this.oauthAdapterService.getPublicKey(oauthConfig.jwksEndpoint); const decodedJWT: string | JwtPayload = jwt.verify(idToken, publicKey, { algorithms: ['RS256'], @@ -147,7 +147,7 @@ export class OAuthService { private buildTokenRequestPayload( code: string, - oauthConfig: OauthConfig, + oauthConfig: OauthConfigEntity, redirectUri: string ): AuthenticationCodeGrantTokenRequest { const decryptedClientSecret: string = this.oAuthEncryptionService.decrypt(oauthConfig.clientSecret); diff --git a/apps/server/src/modules/oauth/uc/hydra-oauth.uc.spec.ts b/apps/server/src/modules/oauth/uc/hydra-oauth.uc.spec.ts index b889995b9e5..e66be61ef83 100644 --- a/apps/server/src/modules/oauth/uc/hydra-oauth.uc.spec.ts +++ b/apps/server/src/modules/oauth/uc/hydra-oauth.uc.spec.ts @@ -1,19 +1,19 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { HydraSsoService, OAuthService } from '@modules/oauth'; +import { HydraRedirectDto } from '@modules/oauth/service/dto/hydra.redirect.dto'; import { HttpModule } from '@nestjs/axios'; import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { OauthConfig } from '@shared/domain'; +import { OauthConfigEntity } from '@shared/domain'; import { axiosResponseFactory } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; -import { HydraRedirectDto } from '@modules/oauth/service/dto/hydra.redirect.dto'; -import { OAuthService, HydraSsoService } from '@modules/oauth'; import { AxiosResponse } from 'axios'; import { HydraOauthUc } from '.'; import { AuthorizationParams } from '../controller/dto'; import { StatelessAuthorizationParams } from '../controller/dto/stateless-authorization.params'; -import { OAuthSSOError } from '../loggable'; import { OAuthTokenDto } from '../interface'; +import { OAuthSSOError } from '../loggable'; class HydraOauthUcSpec extends HydraOauthUc { public validateStatusSpec = (status: number) => this.validateStatus(status); @@ -40,7 +40,7 @@ describe('HydraOauthUc', () => { const hydraUri = 'hydraUri'; const apiHost = 'apiHost'; const nextcloudScopes = 'nextcloudscope'; - const hydraOauthConfig = new OauthConfig({ + const hydraOauthConfig = new OauthConfigEntity({ authEndpoint: `${hydraUri}/oauth2/auth`, clientId: 'toolClientId', clientSecret: 'toolSecret', diff --git a/apps/server/src/modules/oauth/uc/hydra-oauth.uc.ts b/apps/server/src/modules/oauth/uc/hydra-oauth.uc.ts index 2c461e6db4d..258fb2fb7d5 100644 --- a/apps/server/src/modules/oauth/uc/hydra-oauth.uc.ts +++ b/apps/server/src/modules/oauth/uc/hydra-oauth.uc.ts @@ -1,6 +1,6 @@ import { HydraRedirectDto } from '@modules/oauth/service/dto/hydra.redirect.dto'; import { Injectable, InternalServerErrorException } from '@nestjs/common'; -import { OauthConfig } from '@shared/domain'; +import { OauthConfigEntity } from '@shared/domain'; import { LegacyLogger } from '@src/core/logger'; import { AxiosRequestConfig, AxiosResponse } from 'axios'; import { AuthorizationParams } from '../controller/dto'; @@ -28,7 +28,7 @@ export class HydraOauthUc { error || 'sso_auth_code_step' ); } - const hydraOauthConfig: OauthConfig = await this.hydraSsoService.generateConfig(oauthClientId); + const hydraOauthConfig: OauthConfigEntity = await this.hydraSsoService.generateConfig(oauthClientId); const oauthTokens: OAuthTokenDto = await this.oauthService.requestToken( code, @@ -44,7 +44,7 @@ export class HydraOauthUc { protected validateStatus = (status: number): boolean => status === 200 || status === 302; async requestAuthCode(userId: string, jwt: string, oauthClientId: string): Promise { - const hydraOauthConfig: OauthConfig = await this.hydraSsoService.generateConfig(oauthClientId); + const hydraOauthConfig: OauthConfigEntity = await this.hydraSsoService.generateConfig(oauthClientId); const axiosConfig: AxiosRequestConfig = { headers: {}, withCredentials: true, diff --git a/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts b/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts index 1d80c6c7b90..1664c649b10 100644 --- a/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts @@ -1,9 +1,9 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { SystemDto } from '@modules/system/service/dto/system.dto'; import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { SystemDto } from '@modules/system/service/dto/system.dto'; -import { SystemService } from '@modules/system/service/system.service'; +import { LegacySystemService } from '../../system/service/legacy-system.service'; import { ExternalUserDto, OauthDataDto, @@ -18,7 +18,7 @@ describe('ProvisioningService', () => { let module: TestingModule; let service: ProvisioningService; - let systemService: DeepMocked; + let systemService: DeepMocked; let provisioningStrategy: DeepMocked; beforeAll(async () => { @@ -26,8 +26,8 @@ describe('ProvisioningService', () => { providers: [ ProvisioningService, { - provide: SystemService, - useValue: createMock(), + provide: LegacySystemService, + useValue: createMock(), }, { provide: SanisProvisioningStrategy, @@ -57,7 +57,7 @@ describe('ProvisioningService', () => { }).compile(); service = module.get(ProvisioningService); - systemService = module.get(SystemService); + systemService = module.get(LegacySystemService); provisioningStrategy = module.get(SanisProvisioningStrategy); }); diff --git a/apps/server/src/modules/provisioning/service/provisioning.service.ts b/apps/server/src/modules/provisioning/service/provisioning.service.ts index 50ee527001c..8f7330645b5 100644 --- a/apps/server/src/modules/provisioning/service/provisioning.service.ts +++ b/apps/server/src/modules/provisioning/service/provisioning.service.ts @@ -1,7 +1,7 @@ +import { LegacySystemService } from '@modules/system'; +import { SystemDto } from '@modules/system/service/dto/system.dto'; import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { SystemService } from '@modules/system'; -import { SystemDto } from '@modules/system/service/dto/system.dto'; import { OauthDataDto, OauthDataStrategyInputDto, ProvisioningDto, ProvisioningSystemDto } from '../dto'; import { ProvisioningSystemInputMapper } from '../mapper/provisioning-system-input.mapper'; import { @@ -19,7 +19,7 @@ export class ProvisioningService { >(); constructor( - private readonly systemService: SystemService, + private readonly systemService: LegacySystemService, private readonly sanisStrategy: SanisProvisioningStrategy, private readonly iservStrategy: IservProvisioningStrategy, private readonly oidcMockStrategy: OidcMockProvisioningStrategy diff --git a/apps/server/src/modules/system/controller/api-test/system.api.spec.ts b/apps/server/src/modules/system/controller/api-test/system.api.spec.ts index 3167558461d..a449db0ffc9 100644 --- a/apps/server/src/modules/system/controller/api-test/system.api.spec.ts +++ b/apps/server/src/modules/system/controller/api-test/system.api.spec.ts @@ -1,54 +1,40 @@ -import { EntityManager } from '@mikro-orm/mongodb'; -import { ICurrentUser } from '@modules/authentication'; -import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { ServerTestModule } from '@modules/server'; -import { ExecutionContext, INestApplication } from '@nestjs/common'; +import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { OauthConfig, SystemEntity } from '@shared/domain'; -import { cleanupCollections, systemFactory } from '@shared/testing'; -import { Request } from 'express'; -import request, { Response } from 'supertest'; -import { PublicSystemListResponse } from '../dto/public-system-list.response'; -import { PublicSystemResponse } from '../dto/public-system-response'; +import { OauthConfigEntity, SchoolEntity, SystemEntity } from '@shared/domain'; +import { schoolFactory, systemEntityFactory, TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; +import { Response } from 'supertest'; +import { PublicSystemListResponse, PublicSystemResponse } from '../dto'; + +const baseRouteName = '/systems'; describe('System (API)', () => { let app: INestApplication; let em: EntityManager; - let currentUser: ICurrentUser; + let testApiClient: TestApiClient; beforeAll(async () => { - const moduleRef: TestingModule = await Test.createTestingModule({ + const module: TestingModule = await Test.createTestingModule({ imports: [ServerTestModule], - }) - .overrideGuard(JwtAuthGuard) - .useValue({ - canActivate(context: ExecutionContext) { - const req: Request = context.switchToHttp().getRequest(); - req.user = currentUser; - return true; - }, - }) - .compile(); - - app = moduleRef.createNestApplication(); + }).compile(); + + app = module.createNestApplication(); await app.init(); - em = app.get(EntityManager); + em = module.get(EntityManager); + testApiClient = new TestApiClient(app, baseRouteName); }); afterAll(async () => { await app.close(); }); - afterEach(async () => { - await cleanupCollections(em); - }); - describe('[GET] systems/public', () => { describe('when the endpoint is called', () => { const setup = async () => { - const system1: SystemEntity = systemFactory.buildWithId(); - const system2: SystemEntity = systemFactory.withOauthConfig().buildWithId(); - const system2OauthConfig: OauthConfig = system2.oauthConfig as OauthConfig; + const system1: SystemEntity = systemEntityFactory.buildWithId(); + const system2: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId(); + const system2OauthConfig: OauthConfigEntity = system2.oauthConfig as OauthConfigEntity; await em.persistAndFlush([system1, system2]); em.clear(); @@ -59,7 +45,7 @@ describe('System (API)', () => { it('should return a list of all systems', async () => { const { system1, system2, system2OauthConfig } = await setup(); - const response: Response = await request(app.getHttpServer()).get(`/systems/public`).expect(200); + const response: Response = await testApiClient.get(`/public`).expect(200); expect(response.body).toEqual({ data: [ @@ -98,8 +84,8 @@ describe('System (API)', () => { describe('[GET] systems/public/:systemId', () => { describe('when the endpoint is called with a known systemId', () => { const setup = async () => { - const system1: SystemEntity = systemFactory.buildWithId(); - const system2: SystemEntity = systemFactory.buildWithId(); + const system1: SystemEntity = systemEntityFactory.buildWithId(); + const system2: SystemEntity = systemEntityFactory.buildWithId(); await em.persistAndFlush([system1, system2]); em.clear(); @@ -110,7 +96,7 @@ describe('System (API)', () => { it('should return the system', async () => { const { system1 } = await setup(); - const response: Response = await request(app.getHttpServer()).get(`/systems/public/${system1.id}`).expect(200); + const response: Response = await testApiClient.get(`/public/${system1.id}`).expect(200); expect(response.body).toEqual({ id: system1.id, @@ -121,4 +107,41 @@ describe('System (API)', () => { }); }); }); + + describe('[DELETE] systems/:systemId', () => { + describe('when the endpoint is called with a known systemId', () => { + const setup = async () => { + const system: SystemEntity = systemEntityFactory.withLdapConfig({ provider: 'general' }).buildWithId(); + const school: SchoolEntity = schoolFactory.build({ systems: [system] }); + const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }); + + await em.persistAndFlush([system, adminAccount, adminUser, school]); + em.clear(); + + const adminClient = await testApiClient.login(adminAccount); + + return { + system, + adminClient, + }; + }; + + it('should delete the system', async () => { + const { system, adminClient } = await setup(); + + const response = await adminClient.delete(system.id); + + expect(response.status).toEqual(HttpStatus.NO_CONTENT); + expect(await em.findOne(SystemEntity, { id: system.id })).toBeNull(); + }); + }); + + describe('when not authenticated', () => { + it('should return unauthorized', async () => { + const response = await testApiClient.delete(new ObjectId().toHexString()); + + expect(response.status).toEqual(HttpStatus.UNAUTHORIZED); + }); + }); + }); }); diff --git a/apps/server/src/modules/system/controller/dto/index.ts b/apps/server/src/modules/system/controller/dto/index.ts new file mode 100644 index 00000000000..9ff34a1b421 --- /dev/null +++ b/apps/server/src/modules/system/controller/dto/index.ts @@ -0,0 +1,5 @@ +export { SystemIdParams } from './system-id.params'; +export { SystemFilterParams } from './system.filter.params'; +export { OauthConfigResponse } from './oauth-config.response'; +export { PublicSystemResponse } from './public-system-response'; +export { PublicSystemListResponse } from './public-system-list.response'; diff --git a/apps/server/src/modules/system/controller/system.controller.ts b/apps/server/src/modules/system/controller/system.controller.ts index bbeca71b83c..6eabcc53762 100644 --- a/apps/server/src/modules/system/controller/system.controller.ts +++ b/apps/server/src/modules/system/controller/system.controller.ts @@ -1,11 +1,9 @@ -import { Controller, Get, Param, Query } from '@nestjs/common'; -import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; -import { SystemFilterParams } from '@modules/system/controller/dto/system.filter.params'; +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; +import { Controller, Delete, Get, HttpCode, HttpStatus, Param, Query } from '@nestjs/common'; +import { ApiForbiddenResponse, ApiOperation, ApiResponse, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; import { SystemDto } from '../service'; import { SystemUc } from '../uc/system.uc'; -import { PublicSystemListResponse } from './dto/public-system-list.response'; -import { PublicSystemResponse } from './dto/public-system-response'; -import { SystemIdParams } from './dto/system-id.params'; +import { PublicSystemListResponse, PublicSystemResponse, SystemFilterParams, SystemIdParams } from './dto'; import { SystemResponseMapper } from './mapper/system-response.mapper'; @ApiTags('Systems') @@ -42,4 +40,14 @@ export class SystemController { return mapped; } + + @Authenticate('jwt') + @Delete(':systemId') + @ApiForbiddenResponse() + @ApiUnauthorizedResponse() + @ApiOperation({ summary: 'Deletes a system.' }) + @HttpCode(HttpStatus.NO_CONTENT) + async deleteSystem(@CurrentUser() currentUser: ICurrentUser, @Param() params: SystemIdParams): Promise { + await this.systemUc.delete(currentUser.userId, params.systemId); + } } diff --git a/apps/server/src/modules/system/domain/index.ts b/apps/server/src/modules/system/domain/index.ts new file mode 100644 index 00000000000..16e2a044335 --- /dev/null +++ b/apps/server/src/modules/system/domain/index.ts @@ -0,0 +1,3 @@ +export { System, SystemProps } from './system.do'; +export { LdapConfig } from './ldap-config'; +export { OauthConfig } from './oauth-config'; diff --git a/apps/server/src/modules/system/domain/ldap-config.ts b/apps/server/src/modules/system/domain/ldap-config.ts new file mode 100644 index 00000000000..137d1fc92f6 --- /dev/null +++ b/apps/server/src/modules/system/domain/ldap-config.ts @@ -0,0 +1,13 @@ +export class LdapConfig { + active: boolean; + + url: string; + + provider?: string; + + constructor(props: LdapConfig) { + this.active = props.active; + this.url = props.url; + this.provider = props.provider; + } +} diff --git a/apps/server/src/modules/system/domain/oauth-config.ts b/apps/server/src/modules/system/domain/oauth-config.ts new file mode 100644 index 00000000000..165691a0797 --- /dev/null +++ b/apps/server/src/modules/system/domain/oauth-config.ts @@ -0,0 +1,46 @@ +export class OauthConfig { + clientId: string; + + clientSecret: string; + + idpHint?: string; + + redirectUri: string; + + grantType: string; + + tokenEndpoint: string; + + authEndpoint: string; + + responseType: string; + + scope: string; + + provider: string; + + /** + * If this is set it will be used to redirect the user after login to the logout endpoint of the identity provider. + */ + logoutEndpoint?: string; + + issuer: string; + + jwksEndpoint: string; + + constructor(oauthConfigDto: OauthConfig) { + this.clientId = oauthConfigDto.clientId; + this.clientSecret = oauthConfigDto.clientSecret; + this.idpHint = oauthConfigDto.idpHint; + this.redirectUri = oauthConfigDto.redirectUri; + this.grantType = oauthConfigDto.grantType; + this.tokenEndpoint = oauthConfigDto.tokenEndpoint; + this.authEndpoint = oauthConfigDto.authEndpoint; + this.responseType = oauthConfigDto.responseType; + this.scope = oauthConfigDto.scope; + this.provider = oauthConfigDto.provider; + this.logoutEndpoint = oauthConfigDto.logoutEndpoint; + this.issuer = oauthConfigDto.issuer; + this.jwksEndpoint = oauthConfigDto.jwksEndpoint; + } +} diff --git a/apps/server/src/modules/system/domain/system.do.ts b/apps/server/src/modules/system/domain/system.do.ts new file mode 100644 index 00000000000..f909d3196e9 --- /dev/null +++ b/apps/server/src/modules/system/domain/system.do.ts @@ -0,0 +1,28 @@ +import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object'; +import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; +import { LdapConfig } from './ldap-config'; +import { OauthConfig } from './oauth-config'; + +export interface SystemProps extends AuthorizableObject { + type: string; + + url?: string; + + alias?: string; + + displayName?: string; + + provisioningStrategy?: SystemProvisioningStrategy; + + provisioningUrl?: string; + + oauthConfig?: OauthConfig; + + ldapConfig?: LdapConfig; +} + +export class System extends DomainObject { + get ldapConfig(): LdapConfig | undefined { + return this.props.ldapConfig; + } +} diff --git a/apps/server/src/modules/system/index.ts b/apps/server/src/modules/system/index.ts index 2e912fd9246..9cedfa41938 100644 --- a/apps/server/src/modules/system/index.ts +++ b/apps/server/src/modules/system/index.ts @@ -1,2 +1,3 @@ export * from './system.module'; -export * from './service'; +export { SystemService, LegacySystemService, SystemDto, OauthConfigDto, OidcConfigDto } from './service'; +export { System, SystemProps, OauthConfig, LdapConfig } from './domain'; diff --git a/apps/server/src/modules/system/mapper/index.ts b/apps/server/src/modules/system/mapper/index.ts new file mode 100644 index 00000000000..9dc568e504c --- /dev/null +++ b/apps/server/src/modules/system/mapper/index.ts @@ -0,0 +1,2 @@ +export { SystemMapper } from './system.mapper'; +export { SystemOidcMapper } from './system-oidc.mapper'; diff --git a/apps/server/src/modules/system/mapper/system-oidc.mapper.spec.ts b/apps/server/src/modules/system/mapper/system-oidc.mapper.spec.ts index 2486d4a1872..0b230c87eed 100644 --- a/apps/server/src/modules/system/mapper/system-oidc.mapper.spec.ts +++ b/apps/server/src/modules/system/mapper/system-oidc.mapper.spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { SystemEntity } from '@shared/domain'; -import { systemFactory } from '@shared/testing'; -import { SystemOidcMapper } from '@modules/system/mapper/system-oidc.mapper'; +import { systemEntityFactory } from '@shared/testing'; +import { SystemOidcMapper } from './system-oidc.mapper'; describe('SystemOidcMapper', () => { let module: TestingModule; @@ -18,7 +18,7 @@ describe('SystemOidcMapper', () => { describe('mapFromEntityToDto', () => { it('should map all fields', () => { - const systemEntity = systemFactory.withOauthConfig().withOidcConfig().build(); + const systemEntity = systemEntityFactory.withOauthConfig().withOidcConfig().build(); const result = SystemOidcMapper.mapFromEntityToDto(systemEntity); expect(result).toBeDefined(); @@ -34,7 +34,7 @@ describe('SystemOidcMapper', () => { expect(result?.defaultScopes).toEqual(systemEntity.oidcConfig?.defaultScopes); }); it('should return undefined if parent system has no oidc config', () => { - const systemEntity = systemFactory.withOauthConfig().build(); + const systemEntity = systemEntityFactory.withOauthConfig().build(); const result = SystemOidcMapper.mapFromEntityToDto(systemEntity); expect(result).toBeUndefined(); }); @@ -43,8 +43,8 @@ describe('SystemOidcMapper', () => { describe('mapFromEntitiesToDtos', () => { it('should map all given entities', () => { const systemEntities: SystemEntity[] = [ - systemFactory.withOidcConfig().build(), - systemFactory.withOidcConfig().build(), + systemEntityFactory.withOidcConfig().build(), + systemEntityFactory.withOidcConfig().build(), ]; const result = SystemOidcMapper.mapFromEntitiesToDtos(systemEntities); @@ -53,8 +53,8 @@ describe('SystemOidcMapper', () => { }); it('should map oidc config only config if exists', () => { - const systemEntity = systemFactory.withOidcConfig().build(); - const systemEntities: SystemEntity[] = [systemEntity, systemFactory.withOauthConfig().build()]; + const systemEntity = systemEntityFactory.withOidcConfig().build(); + const systemEntities: SystemEntity[] = [systemEntity, systemEntityFactory.withOauthConfig().build()]; const results = SystemOidcMapper.mapFromEntitiesToDtos(systemEntities); diff --git a/apps/server/src/modules/system/mapper/system-oidc.mapper.ts b/apps/server/src/modules/system/mapper/system-oidc.mapper.ts index 8726ce09a78..e486ae458fd 100644 --- a/apps/server/src/modules/system/mapper/system-oidc.mapper.ts +++ b/apps/server/src/modules/system/mapper/system-oidc.mapper.ts @@ -1,5 +1,5 @@ -import { OidcConfig, SystemEntity } from '@shared/domain'; import { OidcConfigDto } from '@modules/system/service/dto/oidc-config.dto'; +import { OidcConfigEntity, SystemEntity } from '@shared/domain'; export class SystemOidcMapper { static mapFromEntityToDto(entity: SystemEntity): OidcConfigDto | undefined { @@ -9,7 +9,7 @@ export class SystemOidcMapper { return undefined; } - static mapFromOidcConfigEntityToDto(systemId: string, oidcConfig: OidcConfig): OidcConfigDto { + static mapFromOidcConfigEntityToDto(systemId: string, oidcConfig: OidcConfigEntity): OidcConfigDto { return new OidcConfigDto({ parentSystemId: systemId, clientId: oidcConfig.clientId, diff --git a/apps/server/src/modules/system/mapper/system.mapper.spec.ts b/apps/server/src/modules/system/mapper/system.mapper.spec.ts index 54c20cc0cff..8bbf4986d9e 100644 --- a/apps/server/src/modules/system/mapper/system.mapper.spec.ts +++ b/apps/server/src/modules/system/mapper/system.mapper.spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { SystemEntity } from '@shared/domain'; -import { systemFactory } from '@shared/testing'; -import { SystemMapper } from '@modules/system/mapper/system.mapper'; +import { systemEntityFactory } from '@shared/testing'; +import { SystemMapper } from './system.mapper'; describe('SystemMapper', () => { let module: TestingModule; @@ -18,7 +18,7 @@ describe('SystemMapper', () => { describe('mapFromEntityToDto', () => { it('should map all fields', () => { - const systemEntity = systemFactory.withOauthConfig().withOidcConfig().build(); + const systemEntity = systemEntityFactory.withOauthConfig().withOidcConfig().build(); const result = SystemMapper.mapFromEntityToDto(systemEntity); @@ -32,7 +32,7 @@ describe('SystemMapper', () => { }); it('should map take alias as default instead of displayName', () => { // Arrange - const systemEntity = systemFactory.withOauthConfig().build(); + const systemEntity = systemEntityFactory.withOauthConfig().build(); systemEntity.displayName = undefined; // Act @@ -47,8 +47,8 @@ describe('SystemMapper', () => { describe('mapFromEntitiesToDtos', () => { it('should map all given entities', () => { const systemEntities: SystemEntity[] = [ - systemFactory.withOauthConfig().build(), - systemFactory.build({ oauthConfig: undefined }), + systemEntityFactory.withOauthConfig().build(), + systemEntityFactory.build({ oauthConfig: undefined }), ]; const result = SystemMapper.mapFromEntitiesToDtos(systemEntities); @@ -58,8 +58,8 @@ describe('SystemMapper', () => { it('should map oauth config if exists', () => { const systemEntities: SystemEntity[] = [ - systemFactory.withOauthConfig().build(), - systemFactory.build({ oauthConfig: undefined }), + systemEntityFactory.withOauthConfig().build(), + systemEntityFactory.build({ oauthConfig: undefined }), ]; const result = SystemMapper.mapFromEntitiesToDtos(systemEntities); diff --git a/apps/server/src/modules/system/mapper/system.mapper.ts b/apps/server/src/modules/system/mapper/system.mapper.ts index ae29fea67c8..420de9f2a69 100644 --- a/apps/server/src/modules/system/mapper/system.mapper.ts +++ b/apps/server/src/modules/system/mapper/system.mapper.ts @@ -1,6 +1,6 @@ -import { OauthConfig, SystemEntity } from '@shared/domain'; import { OauthConfigDto } from '@modules/system/service/dto/oauth-config.dto'; import { SystemDto } from '@modules/system/service/dto/system.dto'; +import { OauthConfigEntity, SystemEntity } from '@shared/domain'; export class SystemMapper { static mapFromEntityToDto(entity: SystemEntity): SystemDto { @@ -17,7 +17,7 @@ export class SystemMapper { }); } - static mapFromOauthConfigEntityToDto(oauthConfig: OauthConfig | undefined): OauthConfigDto | undefined { + static mapFromOauthConfigEntityToDto(oauthConfig: OauthConfigEntity | undefined): OauthConfigDto | undefined { if (!oauthConfig) return undefined; return new OauthConfigDto({ clientId: oauthConfig.clientId, diff --git a/apps/server/src/modules/system/repo/index.ts b/apps/server/src/modules/system/repo/index.ts new file mode 100644 index 00000000000..7bf41b20479 --- /dev/null +++ b/apps/server/src/modules/system/repo/index.ts @@ -0,0 +1 @@ +export { SystemRepo } from './system.repo'; diff --git a/apps/server/src/modules/system/repo/system-domain.mapper.ts b/apps/server/src/modules/system/repo/system-domain.mapper.ts new file mode 100644 index 00000000000..422f838c0ed --- /dev/null +++ b/apps/server/src/modules/system/repo/system-domain.mapper.ts @@ -0,0 +1,50 @@ +import { LdapConfigEntity, OauthConfigEntity, SystemEntity } from '@shared/domain'; +import { LdapConfig, OauthConfig, SystemProps } from '../domain'; + +export class SystemDomainMapper { + public static mapEntityToDomainObjectProperties(entity: SystemEntity): SystemProps { + const mapped: SystemProps = { + id: entity.id, + url: entity.url, + type: entity.type, + provisioningUrl: entity.provisioningUrl, + provisioningStrategy: entity.provisioningStrategy, + displayName: entity.displayName, + alias: entity.alias, + oauthConfig: entity.oauthConfig ? this.mapOauthConfigEntityToDomainObject(entity.oauthConfig) : undefined, + ldapConfig: entity.ldapConfig ? this.mapLdapConfigEntityToDomainObject(entity.ldapConfig) : undefined, + }; + + return mapped; + } + + private static mapOauthConfigEntityToDomainObject(oauthConfig: OauthConfigEntity): OauthConfig { + const mapped: OauthConfig = new OauthConfig({ + clientId: oauthConfig.clientId, + clientSecret: oauthConfig.clientSecret, + idpHint: oauthConfig.idpHint, + authEndpoint: oauthConfig.authEndpoint, + responseType: oauthConfig.responseType, + scope: oauthConfig.scope, + provider: oauthConfig.provider, + logoutEndpoint: oauthConfig.logoutEndpoint, + issuer: oauthConfig.issuer, + jwksEndpoint: oauthConfig.jwksEndpoint, + grantType: oauthConfig.grantType, + tokenEndpoint: oauthConfig.tokenEndpoint, + redirectUri: oauthConfig.redirectUri, + }); + + return mapped; + } + + private static mapLdapConfigEntityToDomainObject(ldapConfig: LdapConfigEntity): LdapConfig { + const mapped: LdapConfig = new LdapConfig({ + active: !!ldapConfig.active, + url: ldapConfig.url, + provider: ldapConfig.provider, + }); + + return mapped; + } +} diff --git a/apps/server/src/modules/system/repo/system.repo.spec.ts b/apps/server/src/modules/system/repo/system.repo.spec.ts new file mode 100644 index 00000000000..584035c0e46 --- /dev/null +++ b/apps/server/src/modules/system/repo/system.repo.spec.ts @@ -0,0 +1,177 @@ +import { MongoMemoryDatabaseModule } from '@infra/database'; +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { Test, TestingModule } from '@nestjs/testing'; +import { LdapConfigEntity, OauthConfigEntity, SystemEntity } from '@shared/domain'; +import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; +import { cleanupCollections, systemEntityFactory } from '@shared/testing'; +import { System, SystemProps } from '../domain'; +import { SystemDomainMapper } from './system-domain.mapper'; +import { SystemRepo } from './system.repo'; + +describe(SystemRepo.name, () => { + let module: TestingModule; + let repo: SystemRepo; + let em: EntityManager; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [MongoMemoryDatabaseModule.forRoot()], + providers: [SystemRepo], + }).compile(); + + repo = module.get(SystemRepo); + em = module.get(EntityManager); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(async () => { + await cleanupCollections(em); + }); + + describe('findById', () => { + describe('when the system exists', () => { + const setup = async () => { + const oauthConfig = new OauthConfigEntity({ + clientId: '12345', + clientSecret: 'mocksecret', + idpHint: 'mock-oauth-idpHint', + tokenEndpoint: 'http://mock.de/mock/auth/public/mockToken', + grantType: 'authorization_code', + redirectUri: 'http://mockhost:3030/api/v3/sso/oauth/', + scope: 'openid uuid', + responseType: 'code', + authEndpoint: 'http://mock.de/auth', + provider: 'mock_type', + logoutEndpoint: 'http://mock.de/logout', + issuer: 'mock_issuer', + jwksEndpoint: 'http://mock.de/jwks', + }); + const ldapConfig = new LdapConfigEntity({ + url: 'ldaps:mock.de:389', + active: true, + provider: 'mock_provider', + }); + const system: SystemEntity = systemEntityFactory.buildWithId({ + type: 'oauth', + url: 'https://mock.de', + alias: 'alias', + displayName: 'displayName', + provisioningStrategy: SystemProvisioningStrategy.OIDC, + provisioningUrl: 'https://provisioningurl.de', + oauthConfig, + ldapConfig, + }); + + await em.persistAndFlush([system]); + em.clear(); + + return { + system, + oauthConfig, + ldapConfig, + }; + }; + + it('should return the system', async () => { + const { system, oauthConfig, ldapConfig } = await setup(); + + const result = await repo.findById(system.id); + + expect(result?.getProps()).toEqual({ + id: system.id, + type: system.type, + url: system.url, + displayName: system.displayName, + alias: system.alias, + provisioningStrategy: system.provisioningStrategy, + provisioningUrl: system.provisioningUrl, + oauthConfig: { + issuer: oauthConfig.issuer, + provider: oauthConfig.provider, + jwksEndpoint: oauthConfig.jwksEndpoint, + redirectUri: oauthConfig.redirectUri, + idpHint: oauthConfig.idpHint, + authEndpoint: oauthConfig.authEndpoint, + clientSecret: oauthConfig.clientSecret, + grantType: oauthConfig.grantType, + logoutEndpoint: oauthConfig.logoutEndpoint, + responseType: oauthConfig.responseType, + tokenEndpoint: oauthConfig.tokenEndpoint, + clientId: oauthConfig.clientId, + scope: oauthConfig.scope, + }, + ldapConfig: { + url: ldapConfig.url, + provider: ldapConfig.provider, + active: !!ldapConfig.active, + }, + }); + }); + }); + + describe('when the system does not exist', () => { + it('should return null', async () => { + const result = await repo.findById(new ObjectId().toHexString()); + + expect(result).toBeNull(); + }); + }); + }); + + describe('delete', () => { + describe('when the system exists', () => { + const setup = async () => { + const systemEntity: SystemEntity = systemEntityFactory.buildWithId(); + + await em.persistAndFlush([systemEntity]); + em.clear(); + + const props: SystemProps = SystemDomainMapper.mapEntityToDomainObjectProperties(systemEntity); + const system: System = new System(props); + + return { + system, + }; + }; + + it('should delete the system', async () => { + const { system } = await setup(); + + await repo.delete(system); + + expect(await em.findOne(SystemEntity, { id: system.id })).toBeNull(); + }); + + it('should return true', async () => { + const { system } = await setup(); + + const result = await repo.delete(system); + + expect(result).toEqual(true); + }); + }); + + describe('when the system does not exists', () => { + const setup = () => { + const systemEntity: SystemEntity = systemEntityFactory.buildWithId(); + const props: SystemProps = SystemDomainMapper.mapEntityToDomainObjectProperties(systemEntity); + const system: System = new System(props); + + return { + system, + }; + }; + + it('should return false', async () => { + const { system } = setup(); + + const result = await repo.delete(system); + + expect(result).toEqual(false); + }); + }); + }); +}); diff --git a/apps/server/src/modules/system/repo/system.repo.ts b/apps/server/src/modules/system/repo/system.repo.ts new file mode 100644 index 00000000000..3251ec87230 --- /dev/null +++ b/apps/server/src/modules/system/repo/system.repo.ts @@ -0,0 +1,36 @@ +import { EntityManager } from '@mikro-orm/mongodb'; +import { Injectable } from '@nestjs/common'; +import { EntityId, SystemEntity } from '@shared/domain'; +import { System, SystemProps } from '../domain'; +import { SystemDomainMapper } from './system-domain.mapper'; + +@Injectable() +export class SystemRepo { + constructor(private readonly em: EntityManager) {} + + public async findById(id: EntityId): Promise { + const entity: SystemEntity | null = await this.em.findOne(SystemEntity, { id }); + + if (!entity) { + return null; + } + + const props: SystemProps = SystemDomainMapper.mapEntityToDomainObjectProperties(entity); + + const domainObject: System = new System(props); + + return domainObject; + } + + public async delete(domainObject: System): Promise { + const entity: SystemEntity | null = await this.em.findOne(SystemEntity, { id: domainObject.id }); + + if (!entity) { + return false; + } + + await this.em.removeAndFlush(entity); + + return true; + } +} diff --git a/apps/server/src/modules/system/service/dto/system.dto.ts b/apps/server/src/modules/system/service/dto/system.dto.ts index 1ea7e4a84ee..bdffdf75423 100644 --- a/apps/server/src/modules/system/service/dto/system.dto.ts +++ b/apps/server/src/modules/system/service/dto/system.dto.ts @@ -1,6 +1,6 @@ -import { EntityId } from '@shared/domain'; -import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { OauthConfigDto } from '@modules/system/service/dto/oauth-config.dto'; +import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; +import { EntityId } from '@shared/domain/types'; export class SystemDto { id?: EntityId; diff --git a/apps/server/src/modules/system/service/index.ts b/apps/server/src/modules/system/service/index.ts index f0956aa12ad..6be1d3fb0fa 100644 --- a/apps/server/src/modules/system/service/index.ts +++ b/apps/server/src/modules/system/service/index.ts @@ -1,2 +1,4 @@ -export * from './system.service'; -export * from './dto'; +export { LegacySystemService } from './legacy-system.service'; +export { SystemDto, OauthConfigDto, OidcConfigDto } from './dto'; +export { SystemService } from './system.service'; +export { SystemOidcService } from './system-oidc.service'; diff --git a/apps/server/src/modules/system/service/legacy-system.service.spec.ts b/apps/server/src/modules/system/service/legacy-system.service.spec.ts new file mode 100644 index 00000000000..b600e4d2891 --- /dev/null +++ b/apps/server/src/modules/system/service/legacy-system.service.spec.ts @@ -0,0 +1,237 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { IdentityManagementOauthService } from '@infra/identity-management'; +import { Test, TestingModule } from '@nestjs/testing'; +import { EntityNotFoundError } from '@shared/common'; +import { OauthConfigEntity, SystemEntity, SystemTypeEnum } from '@shared/domain'; +import { LegacySystemRepo } from '@shared/repo'; +import { systemEntityFactory } from '@shared/testing'; +import { SystemMapper } from '../mapper'; +import { LegacySystemService } from './legacy-system.service'; + +describe(LegacySystemService.name, () => { + let module: TestingModule; + let systemService: LegacySystemService; + let systemRepoMock: DeepMocked; + let kcIdmOauthServiceMock: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + LegacySystemService, + { + provide: LegacySystemRepo, + useValue: createMock(), + }, + { + provide: IdentityManagementOauthService, + useValue: createMock(), + }, + ], + }).compile(); + systemRepoMock = module.get(LegacySystemRepo); + systemService = module.get(LegacySystemService); + kcIdmOauthServiceMock = module.get(IdentityManagementOauthService); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('findById', () => { + describe('when identity management is available', () => { + const standaloneSystem = systemEntityFactory.buildWithId({ alias: 'standaloneSystem' }); + const oidcSystem = systemEntityFactory.withOidcConfig().buildWithId({ alias: 'oidcSystem' }); + const oauthSystem = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'oauthSystem' }); + const setup = (system: SystemEntity) => { + systemRepoMock.findById.mockResolvedValue(system); + kcIdmOauthServiceMock.isOauthConfigAvailable.mockResolvedValue(true); + kcIdmOauthServiceMock.getOauthConfig.mockResolvedValue(oauthSystem.oauthConfig as OauthConfigEntity); + }; + + it('should return found system', async () => { + setup(standaloneSystem); + const result = await systemService.findById(standaloneSystem.id); + expect(result).toStrictEqual(SystemMapper.mapFromEntityToDto(standaloneSystem)); + }); + + it('should return found system with generated oauth config for oidc systems', async () => { + setup(oidcSystem); + if (oauthSystem.oauthConfig === undefined) { + fail('oauth system has no oauth configuration'); + } + const result = await systemService.findById(oidcSystem.id); + expect(result).toEqual( + expect.objectContaining({ + id: oidcSystem.id, + type: SystemTypeEnum.OAUTH, + alias: oidcSystem.alias, + displayName: oidcSystem.displayName, + url: oidcSystem.url, + provisioningStrategy: oidcSystem.provisioningStrategy, + provisioningUrl: oidcSystem.provisioningUrl, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + oauthConfig: expect.objectContaining({ + clientId: oauthSystem.oauthConfig.clientId, + clientSecret: oauthSystem.oauthConfig.clientSecret, + idpHint: oidcSystem.oidcConfig?.idpHint, + redirectUri: oauthSystem.oauthConfig.redirectUri + oidcSystem.id, + grantType: oauthSystem.oauthConfig.grantType, + tokenEndpoint: oauthSystem.oauthConfig.tokenEndpoint, + authEndpoint: oauthSystem.oauthConfig.authEndpoint, + responseType: oauthSystem.oauthConfig.responseType, + scope: oauthSystem.oauthConfig.scope, + provider: oauthSystem.oauthConfig.provider, + logoutEndpoint: oauthSystem.oauthConfig.logoutEndpoint, + issuer: oauthSystem.oauthConfig.issuer, + jwksEndpoint: oauthSystem.oauthConfig.jwksEndpoint, + }), + }) + ); + }); + }); + + describe('when identity management is not available', () => { + const standaloneSystem = systemEntityFactory.buildWithId(); + const oidcSystem = systemEntityFactory.withOidcConfig().buildWithId(); + const setup = (system: SystemEntity) => { + systemRepoMock.findById.mockResolvedValue(system); + kcIdmOauthServiceMock.isOauthConfigAvailable.mockResolvedValue(false); + }; + + it('should return found system', async () => { + setup(standaloneSystem); + const result = await systemService.findById(standaloneSystem.id); + expect(result).toStrictEqual(SystemMapper.mapFromEntityToDto(standaloneSystem)); + }); + + it('should throw and not generate oauth config for oidc systems', async () => { + setup(oidcSystem); + await expect(systemService.findById(oidcSystem.id)).rejects.toThrow(EntityNotFoundError); + }); + }); + }); + + describe('findByType', () => { + describe('when identity management is available', () => { + const ldapSystem = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'ldapSystem' }); + const oauthSystem = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'oauthSystem' }); + const oidcSystem = systemEntityFactory.withOidcConfig().buildWithId({ alias: 'oidcSystem' }); + const setup = () => { + systemRepoMock.findAll.mockResolvedValue([ldapSystem, oauthSystem, oidcSystem]); + systemRepoMock.findByFilter.mockImplementation((type: SystemTypeEnum) => { + if (type === SystemTypeEnum.LDAP) return Promise.resolve([ldapSystem]); + if (type === SystemTypeEnum.OAUTH) return Promise.resolve([oauthSystem]); + if (type === SystemTypeEnum.OIDC) return Promise.resolve([oidcSystem]); + return Promise.resolve([]); + }); + kcIdmOauthServiceMock.isOauthConfigAvailable.mockResolvedValue(true); + kcIdmOauthServiceMock.getOauthConfig.mockResolvedValue(oauthSystem.oauthConfig as OauthConfigEntity); + }; + + it('should return all systems', async () => { + setup(); + const result = await systemService.findByType(); + expect(result).toEqual( + expect.arrayContaining([ + ...SystemMapper.mapFromEntitiesToDtos([ldapSystem, oauthSystem]), + expect.objectContaining({ + alias: oidcSystem.alias, + displayName: oidcSystem.displayName, + }), + ]) + ); + }); + + it('should return found systems', async () => { + setup(); + const result = await systemService.findByType(SystemTypeEnum.LDAP); + expect(result).toStrictEqual(SystemMapper.mapFromEntitiesToDtos([ldapSystem])); + }); + + it('should return found systems with generated oauth config for oidc systems', async () => { + setup(); + if (oauthSystem.oauthConfig === undefined) { + fail('oauth system has no oauth configuration'); + } + const resultingSystems = await systemService.findByType(SystemTypeEnum.OAUTH); + + expect(resultingSystems).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: SystemTypeEnum.OAUTH, + alias: oidcSystem.alias, + displayName: oidcSystem.displayName, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + oauthConfig: expect.objectContaining({ + clientId: oauthSystem.oauthConfig.clientId, + clientSecret: oauthSystem.oauthConfig.clientSecret, + idpHint: oidcSystem.oidcConfig?.idpHint, + redirectUri: oauthSystem.oauthConfig.redirectUri + oidcSystem.id, + grantType: oauthSystem.oauthConfig.grantType, + tokenEndpoint: oauthSystem.oauthConfig.tokenEndpoint, + authEndpoint: oauthSystem.oauthConfig.authEndpoint, + responseType: oauthSystem.oauthConfig.responseType, + scope: oauthSystem.oauthConfig.scope, + provider: oauthSystem.oauthConfig.provider, + logoutEndpoint: oauthSystem.oauthConfig.logoutEndpoint, + issuer: oauthSystem.oauthConfig.issuer, + jwksEndpoint: oauthSystem.oauthConfig.jwksEndpoint, + }), + }), + ]) + ); + }); + }); + + describe('when identity management is not available', () => { + const oauthSystem = systemEntityFactory.withOauthConfig().buildWithId(); + const oidcSystem = systemEntityFactory.withOidcConfig().buildWithId(); + const setup = () => { + systemRepoMock.findByFilter.mockImplementation((type: SystemTypeEnum) => { + if (type === SystemTypeEnum.OAUTH) return Promise.resolve([oauthSystem]); + if (type === SystemTypeEnum.OIDC) return Promise.resolve([oidcSystem]); + return Promise.resolve([]); + }); + kcIdmOauthServiceMock.isOauthConfigAvailable.mockResolvedValue(false); + }; + it('should filter out oidc systems', async () => { + setup(); + const result = await systemService.findByType(SystemTypeEnum.OAUTH); + expect(result).toStrictEqual(SystemMapper.mapFromEntitiesToDtos([oauthSystem])); + }); + }); + }); + + describe('save', () => { + describe('when creating a new system', () => { + const newSystem = systemEntityFactory.build(); + const setup = () => { + systemRepoMock.save.mockResolvedValue(); + }; + + it('should save new system', async () => { + setup(); + const result = await systemService.save(newSystem); + expect(result).toStrictEqual(SystemMapper.mapFromEntityToDto(newSystem)); + }); + }); + + describe('when updating an existing system', () => { + const existingSystem = systemEntityFactory.buildWithId(); + const setup = () => { + systemRepoMock.findById.mockResolvedValue(existingSystem); + }; + + it('should update existing system', async () => { + setup(); + const result = await systemService.save(existingSystem); + expect(systemRepoMock.findById).toHaveBeenCalledTimes(1); + expect(result).toStrictEqual(SystemMapper.mapFromEntityToDto(existingSystem)); + }); + }); + }); +}); diff --git a/apps/server/src/modules/system/service/legacy-system.service.ts b/apps/server/src/modules/system/service/legacy-system.service.ts new file mode 100644 index 00000000000..9b59baad7f5 --- /dev/null +++ b/apps/server/src/modules/system/service/legacy-system.service.ts @@ -0,0 +1,95 @@ +import { IdentityManagementOauthService } from '@infra/identity-management'; +import { Injectable } from '@nestjs/common'; +import { EntityNotFoundError } from '@shared/common'; +import { EntityId, SystemEntity, SystemTypeEnum } from '@shared/domain'; +import { LegacySystemRepo } from '@shared/repo'; +import { SystemMapper } from '../mapper'; +import { SystemDto } from './dto'; + +// TODO N21-1547: Fully replace this service with SystemService +/** + * @deprecated use {@link SystemService} + */ +@Injectable() +export class LegacySystemService { + constructor( + private readonly systemRepo: LegacySystemRepo, + private readonly idmOauthService: IdentityManagementOauthService + ) {} + + async findById(id: EntityId): Promise { + let system = await this.systemRepo.findById(id); + [system] = await this.generateBrokerSystems([system]); + if (!system) { + throw new EntityNotFoundError(SystemEntity.name, { id }); + } + return SystemMapper.mapFromEntityToDto(system); + } + + async findByType(type?: SystemTypeEnum): Promise { + let systems: SystemEntity[]; + if (type && type === SystemTypeEnum.OAUTH) { + const oauthSystems = await this.systemRepo.findByFilter(SystemTypeEnum.OAUTH); + const oidcSystems = await this.systemRepo.findByFilter(SystemTypeEnum.OIDC); + systems = [...oauthSystems, ...oidcSystems]; + } else if (type) { + systems = await this.systemRepo.findByFilter(type); + } else { + systems = await this.systemRepo.findAll(); + } + systems = await this.generateBrokerSystems(systems); + return SystemMapper.mapFromEntitiesToDtos(systems); + } + + async save(systemDto: SystemDto): Promise { + let system: SystemEntity; + if (systemDto.id) { + system = await this.systemRepo.findById(systemDto.id); + system.type = systemDto.type; + system.alias = systemDto.alias; + system.displayName = systemDto.displayName; + system.oauthConfig = systemDto.oauthConfig; + system.provisioningStrategy = systemDto.provisioningStrategy; + system.provisioningUrl = systemDto.provisioningUrl; + system.url = systemDto.url; + } else { + system = new SystemEntity({ + type: systemDto.type, + alias: systemDto.alias, + displayName: systemDto.displayName, + oauthConfig: systemDto.oauthConfig, + provisioningStrategy: systemDto.provisioningStrategy, + provisioningUrl: systemDto.provisioningUrl, + url: systemDto.url, + }); + } + await this.systemRepo.save(system); + return SystemMapper.mapFromEntityToDto(system); + } + + private async generateBrokerSystems(systems: SystemEntity[]): Promise<[] | SystemEntity[]> { + if (!(await this.idmOauthService.isOauthConfigAvailable())) { + return systems.filter((system) => !(system.oidcConfig && !system.oauthConfig)); + } + const brokerConfig = await this.idmOauthService.getOauthConfig(); + let generatedSystem: SystemEntity; + return systems.map((system) => { + if (system.oidcConfig && !system.oauthConfig) { + generatedSystem = new SystemEntity({ + type: SystemTypeEnum.OAUTH, + alias: system.alias, + displayName: system.displayName ? system.displayName : system.alias, + provisioningStrategy: system.provisioningStrategy, + provisioningUrl: system.provisioningUrl, + url: system.url, + }); + generatedSystem.id = system.id; + generatedSystem.oauthConfig = { ...brokerConfig }; + generatedSystem.oauthConfig.idpHint = system.oidcConfig.idpHint; + generatedSystem.oauthConfig.redirectUri += system.id; + return generatedSystem; + } + return system; + }); + } +} diff --git a/apps/server/src/modules/system/service/system-oidc.service.spec.ts b/apps/server/src/modules/system/service/system-oidc.service.spec.ts index 6d85d5fb730..53e73d2e780 100644 --- a/apps/server/src/modules/system/service/system-oidc.service.spec.ts +++ b/apps/server/src/modules/system/service/system-oidc.service.spec.ts @@ -2,27 +2,27 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { EntityNotFoundError } from '@shared/common'; import { SystemEntity } from '@shared/domain'; -import { SystemRepo } from '@shared/repo'; -import { systemFactory } from '@shared/testing'; +import { LegacySystemRepo } from '@shared/repo'; +import { systemEntityFactory } from '@shared/testing'; import { SystemOidcMapper } from '../mapper/system-oidc.mapper'; import { SystemOidcService } from './system-oidc.service'; describe('SystemService', () => { let module: TestingModule; let systemService: SystemOidcService; - let systemRepoMock: DeepMocked; + let systemRepoMock: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ providers: [ SystemOidcService, { - provide: SystemRepo, - useValue: createMock(), + provide: LegacySystemRepo, + useValue: createMock(), }, ], }).compile(); - systemRepoMock = module.get(SystemRepo); + systemRepoMock = module.get(LegacySystemRepo); systemService = module.get(SystemOidcService); }); @@ -35,8 +35,8 @@ describe('SystemService', () => { }); describe('findById', () => { - const oidcSystem = systemFactory.withOidcConfig().buildWithId({ alias: 'oidcSystem' }); - const standaloneSystem = systemFactory.buildWithId({ alias: 'standaloneSystem' }); + const oidcSystem = systemEntityFactory.withOidcConfig().buildWithId({ alias: 'oidcSystem' }); + const standaloneSystem = systemEntityFactory.buildWithId({ alias: 'standaloneSystem' }); const setup = (system: SystemEntity) => { systemRepoMock.findById.mockResolvedValue(system); }; @@ -54,9 +54,9 @@ describe('SystemService', () => { }); describe('findAll', () => { - const ldapSystem = systemFactory.withLdapConfig().buildWithId({ alias: 'ldapSystem' }); - const oauthSystem = systemFactory.withOauthConfig().buildWithId({ alias: 'oauthSystem' }); - const oidcSystem = systemFactory.withOidcConfig().buildWithId({ alias: 'oidcSystem' }); + const ldapSystem = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'ldapSystem' }); + const oauthSystem = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'oauthSystem' }); + const oidcSystem = systemEntityFactory.withOidcConfig().buildWithId({ alias: 'oidcSystem' }); it('should return oidc systems only', async () => { systemRepoMock.findByFilter.mockResolvedValue([ldapSystem, oauthSystem, oidcSystem]); diff --git a/apps/server/src/modules/system/service/system-oidc.service.ts b/apps/server/src/modules/system/service/system-oidc.service.ts index c1f1cf0a4c1..c8703da8ff6 100644 --- a/apps/server/src/modules/system/service/system-oidc.service.ts +++ b/apps/server/src/modules/system/service/system-oidc.service.ts @@ -1,13 +1,13 @@ import { Injectable } from '@nestjs/common'; import { EntityNotFoundError } from '@shared/common'; import { EntityId, SystemEntity, SystemTypeEnum } from '@shared/domain'; -import { SystemRepo } from '@shared/repo'; -import { SystemOidcMapper } from '@modules/system/mapper/system-oidc.mapper'; +import { LegacySystemRepo } from '@shared/repo'; +import { SystemOidcMapper } from '../mapper'; import { OidcConfigDto } from './dto'; @Injectable() export class SystemOidcService { - constructor(private readonly systemRepo: SystemRepo) {} + constructor(private readonly systemRepo: LegacySystemRepo) {} async findById(id: EntityId): Promise { const system = await this.systemRepo.findById(id); diff --git a/apps/server/src/modules/system/service/system.service.spec.ts b/apps/server/src/modules/system/service/system.service.spec.ts index 89ef533058b..b565a74a2a2 100644 --- a/apps/server/src/modules/system/service/system.service.spec.ts +++ b/apps/server/src/modules/system/service/system.service.spec.ts @@ -1,18 +1,15 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; -import { EntityNotFoundError } from '@shared/common'; -import { OauthConfig, SystemEntity, SystemTypeEnum } from '@shared/domain'; -import { IdentityManagementOauthService } from '@infra/identity-management'; -import { SystemRepo } from '@shared/repo'; import { systemFactory } from '@shared/testing'; -import { SystemMapper } from '../mapper/system.mapper'; +import { SystemRepo } from '../repo'; import { SystemService } from './system.service'; -describe('SystemService', () => { +describe(SystemService.name, () => { let module: TestingModule; - let systemService: SystemService; - let systemRepoMock: DeepMocked; - let kcIdmOauthServiceMock: DeepMocked; + let service: SystemService; + + let systemRepo: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -22,15 +19,11 @@ describe('SystemService', () => { provide: SystemRepo, useValue: createMock(), }, - { - provide: IdentityManagementOauthService, - useValue: createMock(), - }, ], }).compile(); - systemRepoMock = module.get(SystemRepo); - systemService = module.get(SystemService); - kcIdmOauthServiceMock = module.get(IdentityManagementOauthService); + + service = module.get(SystemService); + systemRepo = module.get(SystemRepo); }); afterAll(async () => { @@ -38,199 +31,83 @@ describe('SystemService', () => { }); afterEach(() => { - jest.clearAllMocks(); + jest.resetAllMocks(); }); describe('findById', () => { - describe('when identity management is available', () => { - const standaloneSystem = systemFactory.buildWithId({ alias: 'standaloneSystem' }); - const oidcSystem = systemFactory.withOidcConfig().buildWithId({ alias: 'oidcSystem' }); - const oauthSystem = systemFactory.withOauthConfig().buildWithId({ alias: 'oauthSystem' }); - const setup = (system: SystemEntity) => { - systemRepoMock.findById.mockResolvedValue(system); - kcIdmOauthServiceMock.isOauthConfigAvailable.mockResolvedValue(true); - kcIdmOauthServiceMock.getOauthConfig.mockResolvedValue(oauthSystem.oauthConfig as OauthConfig); - }; + describe('when the system exists', () => { + const setup = () => { + const system = systemFactory.build(); - it('should return found system', async () => { - setup(standaloneSystem); - const result = await systemService.findById(standaloneSystem.id); - expect(result).toStrictEqual(SystemMapper.mapFromEntityToDto(standaloneSystem)); - }); + systemRepo.findById.mockResolvedValueOnce(system); - it('should return found system with generated oauth config for oidc systems', async () => { - setup(oidcSystem); - if (oauthSystem.oauthConfig === undefined) { - fail('oauth system has no oauth configuration'); - } - const result = await systemService.findById(oidcSystem.id); - expect(result).toEqual( - expect.objectContaining({ - id: oidcSystem.id, - type: SystemTypeEnum.OAUTH, - alias: oidcSystem.alias, - displayName: oidcSystem.displayName, - url: oidcSystem.url, - provisioningStrategy: oidcSystem.provisioningStrategy, - provisioningUrl: oidcSystem.provisioningUrl, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - oauthConfig: expect.objectContaining({ - clientId: oauthSystem.oauthConfig.clientId, - clientSecret: oauthSystem.oauthConfig.clientSecret, - idpHint: oidcSystem.oidcConfig?.idpHint, - redirectUri: oauthSystem.oauthConfig.redirectUri + oidcSystem.id, - grantType: oauthSystem.oauthConfig.grantType, - tokenEndpoint: oauthSystem.oauthConfig.tokenEndpoint, - authEndpoint: oauthSystem.oauthConfig.authEndpoint, - responseType: oauthSystem.oauthConfig.responseType, - scope: oauthSystem.oauthConfig.scope, - provider: oauthSystem.oauthConfig.provider, - logoutEndpoint: oauthSystem.oauthConfig.logoutEndpoint, - issuer: oauthSystem.oauthConfig.issuer, - jwksEndpoint: oauthSystem.oauthConfig.jwksEndpoint, - }), - }) - ); - }); - }); - - describe('when identity management is not available', () => { - const standaloneSystem = systemFactory.buildWithId(); - const oidcSystem = systemFactory.withOidcConfig().buildWithId(); - const setup = (system: SystemEntity) => { - systemRepoMock.findById.mockResolvedValue(system); - kcIdmOauthServiceMock.isOauthConfigAvailable.mockResolvedValue(false); + return { + system, + }; }; - it('should return found system', async () => { - setup(standaloneSystem); - const result = await systemService.findById(standaloneSystem.id); - expect(result).toStrictEqual(SystemMapper.mapFromEntityToDto(standaloneSystem)); - }); + it('should return the system', async () => { + const { system } = setup(); - it('should throw and not generate oauth config for oidc systems', async () => { - setup(oidcSystem); - await expect(systemService.findById(oidcSystem.id)).rejects.toThrow(EntityNotFoundError); + const result = await service.findById(system.id); + + expect(result).toEqual(system); }); }); - }); - describe('findByType', () => { - describe('when identity management is available', () => { - const ldapSystem = systemFactory.withLdapConfig().buildWithId({ alias: 'ldapSystem' }); - const oauthSystem = systemFactory.withOauthConfig().buildWithId({ alias: 'oauthSystem' }); - const oidcSystem = systemFactory.withOidcConfig().buildWithId({ alias: 'oidcSystem' }); + describe('when the system does not exist', () => { const setup = () => { - systemRepoMock.findAll.mockResolvedValue([ldapSystem, oauthSystem, oidcSystem]); - systemRepoMock.findByFilter.mockImplementation((type: SystemTypeEnum) => { - if (type === SystemTypeEnum.LDAP) return Promise.resolve([ldapSystem]); - if (type === SystemTypeEnum.OAUTH) return Promise.resolve([oauthSystem]); - if (type === SystemTypeEnum.OIDC) return Promise.resolve([oidcSystem]); - return Promise.resolve([]); - }); - kcIdmOauthServiceMock.isOauthConfigAvailable.mockResolvedValue(true); - kcIdmOauthServiceMock.getOauthConfig.mockResolvedValue(oauthSystem.oauthConfig as OauthConfig); + systemRepo.findById.mockResolvedValueOnce(null); }; - it('should return all systems', async () => { + it('should return null', async () => { setup(); - const result = await systemService.findByType(); - expect(result).toEqual( - expect.arrayContaining([ - ...SystemMapper.mapFromEntitiesToDtos([ldapSystem, oauthSystem]), - expect.objectContaining({ - alias: oidcSystem.alias, - displayName: oidcSystem.displayName, - }), - ]) - ); - }); - it('should return found systems', async () => { - setup(); - const result = await systemService.findByType(SystemTypeEnum.LDAP); - expect(result).toStrictEqual(SystemMapper.mapFromEntitiesToDtos([ldapSystem])); - }); + const result = await service.findById(new ObjectId().toHexString()); - it('should return found systems with generated oauth config for oidc systems', async () => { - setup(); - if (oauthSystem.oauthConfig === undefined) { - fail('oauth system has no oauth configuration'); - } - const resultingSystems = await systemService.findByType(SystemTypeEnum.OAUTH); - - expect(resultingSystems).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - type: SystemTypeEnum.OAUTH, - alias: oidcSystem.alias, - displayName: oidcSystem.displayName, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - oauthConfig: expect.objectContaining({ - clientId: oauthSystem.oauthConfig.clientId, - clientSecret: oauthSystem.oauthConfig.clientSecret, - idpHint: oidcSystem.oidcConfig?.idpHint, - redirectUri: oauthSystem.oauthConfig.redirectUri + oidcSystem.id, - grantType: oauthSystem.oauthConfig.grantType, - tokenEndpoint: oauthSystem.oauthConfig.tokenEndpoint, - authEndpoint: oauthSystem.oauthConfig.authEndpoint, - responseType: oauthSystem.oauthConfig.responseType, - scope: oauthSystem.oauthConfig.scope, - provider: oauthSystem.oauthConfig.provider, - logoutEndpoint: oauthSystem.oauthConfig.logoutEndpoint, - issuer: oauthSystem.oauthConfig.issuer, - jwksEndpoint: oauthSystem.oauthConfig.jwksEndpoint, - }), - }), - ]) - ); - }); - }); - - describe('when identity management is not available', () => { - const oauthSystem = systemFactory.withOauthConfig().buildWithId(); - const oidcSystem = systemFactory.withOidcConfig().buildWithId(); - const setup = () => { - systemRepoMock.findByFilter.mockImplementation((type: SystemTypeEnum) => { - if (type === SystemTypeEnum.OAUTH) return Promise.resolve([oauthSystem]); - if (type === SystemTypeEnum.OIDC) return Promise.resolve([oidcSystem]); - return Promise.resolve([]); - }); - kcIdmOauthServiceMock.isOauthConfigAvailable.mockResolvedValue(false); - }; - it('should filter out oidc systems', async () => { - setup(); - const result = await systemService.findByType(SystemTypeEnum.OAUTH); - expect(result).toStrictEqual(SystemMapper.mapFromEntitiesToDtos([oauthSystem])); + expect(result).toBeNull(); }); }); }); - describe('save', () => { - describe('when creating a new system', () => { - const newSystem = systemFactory.build(); + describe('delete', () => { + describe('when the system was deleted', () => { const setup = () => { - systemRepoMock.save.mockResolvedValue(); + const system = systemFactory.build(); + + systemRepo.delete.mockResolvedValueOnce(true); + + return { + system, + }; }; - it('should save new system', async () => { - setup(); - const result = await systemService.save(newSystem); - expect(result).toStrictEqual(SystemMapper.mapFromEntityToDto(newSystem)); + it('should return true', async () => { + const { system } = setup(); + + const result = await service.delete(system); + + expect(result).toEqual(true); }); }); - describe('when updating an existing system', () => { - const existingSystem = systemFactory.buildWithId(); + describe('when the system was not deleted', () => { const setup = () => { - systemRepoMock.findById.mockResolvedValue(existingSystem); + const system = systemFactory.build(); + + systemRepo.delete.mockResolvedValueOnce(false); + + return { + system, + }; }; - it('should update existing system', async () => { - setup(); - const result = await systemService.save(existingSystem); - expect(systemRepoMock.findById).toHaveBeenCalledTimes(1); - expect(result).toStrictEqual(SystemMapper.mapFromEntityToDto(existingSystem)); + it('should return false', async () => { + const { system } = setup(); + + const result = await service.delete(system); + + expect(result).toEqual(false); }); }); }); diff --git a/apps/server/src/modules/system/service/system.service.ts b/apps/server/src/modules/system/service/system.service.ts index bfb6a2ec7bf..50ad7fadabf 100644 --- a/apps/server/src/modules/system/service/system.service.ts +++ b/apps/server/src/modules/system/service/system.service.ts @@ -1,91 +1,21 @@ import { Injectable } from '@nestjs/common'; -import { EntityNotFoundError } from '@shared/common'; -import { EntityId, SystemEntity, SystemTypeEnum } from '@shared/domain'; -import { IdentityManagementOauthService } from '@infra/identity-management/identity-management-oauth.service'; -import { SystemRepo } from '@shared/repo'; -import { SystemMapper } from '@modules/system/mapper/system.mapper'; -import { SystemDto } from '@modules/system/service/dto/system.dto'; +import { EntityId } from '@shared/domain/types'; +import { System } from '../domain'; +import { SystemRepo } from '../repo'; @Injectable() export class SystemService { - constructor( - private readonly systemRepo: SystemRepo, - private readonly idmOauthService: IdentityManagementOauthService - ) {} + constructor(private readonly systemRepo: SystemRepo) {} - async findById(id: EntityId): Promise { - let system = await this.systemRepo.findById(id); - [system] = await this.generateBrokerSystems([system]); - if (!system) { - throw new EntityNotFoundError(SystemEntity.name, { id }); - } - return SystemMapper.mapFromEntityToDto(system); - } + public async findById(id: EntityId): Promise { + const system: System | null = await this.systemRepo.findById(id); - async findByType(type?: SystemTypeEnum): Promise { - let systems: SystemEntity[]; - if (type && type === SystemTypeEnum.OAUTH) { - const oauthSystems = await this.systemRepo.findByFilter(SystemTypeEnum.OAUTH); - const oidcSystems = await this.systemRepo.findByFilter(SystemTypeEnum.OIDC); - systems = [...oauthSystems, ...oidcSystems]; - } else if (type) { - systems = await this.systemRepo.findByFilter(type); - } else { - systems = await this.systemRepo.findAll(); - } - systems = await this.generateBrokerSystems(systems); - return SystemMapper.mapFromEntitiesToDtos(systems); + return system; } - async save(systemDto: SystemDto): Promise { - let system: SystemEntity; - if (systemDto.id) { - system = await this.systemRepo.findById(systemDto.id); - system.type = systemDto.type; - system.alias = systemDto.alias; - system.displayName = systemDto.displayName; - system.oauthConfig = systemDto.oauthConfig; - system.provisioningStrategy = systemDto.provisioningStrategy; - system.provisioningUrl = systemDto.provisioningUrl; - system.url = systemDto.url; - } else { - system = new SystemEntity({ - type: systemDto.type, - alias: systemDto.alias, - displayName: systemDto.displayName, - oauthConfig: systemDto.oauthConfig, - provisioningStrategy: systemDto.provisioningStrategy, - provisioningUrl: systemDto.provisioningUrl, - url: systemDto.url, - }); - } - await this.systemRepo.save(system); - return SystemMapper.mapFromEntityToDto(system); - } + public async delete(domainObject: System): Promise { + const deleted: boolean = await this.systemRepo.delete(domainObject); - private async generateBrokerSystems(systems: SystemEntity[]): Promise<[] | SystemEntity[]> { - if (!(await this.idmOauthService.isOauthConfigAvailable())) { - return systems.filter((system) => !(system.oidcConfig && !system.oauthConfig)); - } - const brokerConfig = await this.idmOauthService.getOauthConfig(); - let generatedSystem: SystemEntity; - return systems.map((system) => { - if (system.oidcConfig && !system.oauthConfig) { - generatedSystem = new SystemEntity({ - type: SystemTypeEnum.OAUTH, - alias: system.alias, - displayName: system.displayName ? system.displayName : system.alias, - provisioningStrategy: system.provisioningStrategy, - provisioningUrl: system.provisioningUrl, - url: system.url, - }); - generatedSystem.id = system.id; - generatedSystem.oauthConfig = { ...brokerConfig }; - generatedSystem.oauthConfig.idpHint = system.oidcConfig.idpHint; - generatedSystem.oauthConfig.redirectUri += system.id; - return generatedSystem; - } - return system; - }); + return deleted; } } diff --git a/apps/server/src/modules/system/system-api.module.ts b/apps/server/src/modules/system/system-api.module.ts index e7213c1fac7..e9201f376b8 100644 --- a/apps/server/src/modules/system/system-api.module.ts +++ b/apps/server/src/modules/system/system-api.module.ts @@ -1,10 +1,11 @@ -import { Module } from '@nestjs/common'; +import { AuthorizationModule } from '@modules/authorization'; import { SystemController } from '@modules/system/controller/system.controller'; import { SystemUc } from '@modules/system/uc/system.uc'; +import { Module } from '@nestjs/common'; import { SystemModule } from './system.module'; @Module({ - imports: [SystemModule], + imports: [SystemModule, AuthorizationModule], providers: [SystemUc], controllers: [SystemController], }) diff --git a/apps/server/src/modules/system/system.module.ts b/apps/server/src/modules/system/system.module.ts index 37ca8d7a858..54c9d51224b 100644 --- a/apps/server/src/modules/system/system.module.ts +++ b/apps/server/src/modules/system/system.module.ts @@ -1,12 +1,13 @@ -import { Module } from '@nestjs/common'; import { IdentityManagementModule } from '@infra/identity-management/identity-management.module'; -import { SystemRepo } from '@shared/repo'; -import { SystemService } from '@modules/system/service/system.service'; +import { Module } from '@nestjs/common'; +import { LegacySystemRepo } from '@shared/repo'; +import { SystemRepo } from './repo'; +import { LegacySystemService, SystemService } from './service'; import { SystemOidcService } from './service/system-oidc.service'; @Module({ imports: [IdentityManagementModule], - providers: [SystemRepo, SystemService, SystemOidcService], - exports: [SystemService, SystemOidcService], + providers: [LegacySystemRepo, LegacySystemService, SystemOidcService, SystemService, SystemRepo], + exports: [LegacySystemService, SystemOidcService, SystemService], }) export class SystemModule {} diff --git a/apps/server/src/modules/system/uc/system.uc.spec.ts b/apps/server/src/modules/system/uc/system.uc.spec.ts index 45bd65694d9..e498718321a 100644 --- a/apps/server/src/modules/system/uc/system.uc.spec.ts +++ b/apps/server/src/modules/system/uc/system.uc.spec.ts @@ -1,12 +1,15 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { EntityNotFoundError } from '@shared/common'; -import { EntityId, SystemEntity, SystemTypeEnum } from '@shared/domain'; -import { systemFactory } from '@shared/testing'; -import { SystemMapper } from '@modules/system/mapper/system.mapper'; +import { ObjectId } from '@mikro-orm/mongodb'; import { SystemDto } from '@modules/system/service/dto/system.dto'; -import { SystemService } from '@modules/system/service/system.service'; import { SystemUc } from '@modules/system/uc/system.uc'; +import { Test, TestingModule } from '@nestjs/testing'; +import { EntityNotFoundError } from '@shared/common'; +import { NotFoundLoggableException } from '@shared/common/loggable-exception'; +import { EntityId, Permission, SystemEntity, SystemTypeEnum } from '@shared/domain'; +import { setupEntities, systemEntityFactory, systemFactory, userFactory } from '@shared/testing'; +import { AuthorizationContextBuilder, AuthorizationService } from '../../authorization'; +import { SystemMapper } from '../mapper'; +import { LegacySystemService, SystemService } from '../service'; describe('SystemUc', () => { let module: TestingModule; @@ -17,44 +20,63 @@ describe('SystemUc', () => { let system1: SystemEntity; let system2: SystemEntity; + let legacySystemService: DeepMocked; let systemService: DeepMocked; - - afterAll(async () => { - await module.close(); - }); + let authorizationService: DeepMocked; beforeAll(async () => { + await setupEntities(); + module = await Test.createTestingModule({ providers: [ SystemUc, + { + provide: LegacySystemService, + useValue: createMock(), + }, { provide: SystemService, useValue: createMock(), }, + { + provide: AuthorizationService, + useValue: createMock(), + }, ], }).compile(); + systemUc = module.get(SystemUc); + legacySystemService = module.get(LegacySystemService); systemService = module.get(SystemService); + authorizationService = module.get(AuthorizationService); }); - beforeEach(() => { - system1 = systemFactory.buildWithId(); - system2 = systemFactory.buildWithId(); - - mockSystem1 = SystemMapper.mapFromEntityToDto(system1); - mockSystem2 = SystemMapper.mapFromEntityToDto(system2); - mockSystems = [mockSystem1, mockSystem2]; + afterAll(async () => { + await module.close(); + }); - systemService.findByType.mockImplementation((type: string | undefined) => { - if (type === SystemTypeEnum.OAUTH) return Promise.resolve([mockSystem1]); - return Promise.resolve(mockSystems); - }); - systemService.findById.mockImplementation( - (id: EntityId): Promise => (id === system1.id ? Promise.resolve(mockSystem1) : Promise.reject()) - ); + afterEach(() => { + jest.clearAllMocks(); }); describe('findByFilter', () => { + beforeEach(() => { + system1 = systemEntityFactory.buildWithId(); + system2 = systemEntityFactory.buildWithId(); + + mockSystem1 = SystemMapper.mapFromEntityToDto(system1); + mockSystem2 = SystemMapper.mapFromEntityToDto(system2); + mockSystems = [mockSystem1, mockSystem2]; + + legacySystemService.findByType.mockImplementation((type: string | undefined) => { + if (type === SystemTypeEnum.OAUTH) return Promise.resolve([mockSystem1]); + return Promise.resolve(mockSystems); + }); + legacySystemService.findById.mockImplementation( + (id: EntityId): Promise => (id === system1.id ? Promise.resolve(mockSystem1) : Promise.reject()) + ); + }); + it('should return systems by default', async () => { const systems: SystemDto[] = await systemUc.findByFilter(); @@ -78,13 +100,30 @@ describe('SystemUc', () => { }); it('should return empty system list, because none exist', async () => { - systemService.findByType.mockResolvedValue([]); + legacySystemService.findByType.mockResolvedValue([]); const resultResponse = await systemUc.findByFilter(); expect(resultResponse).toHaveLength(0); }); }); describe('findById', () => { + beforeEach(() => { + system1 = systemEntityFactory.buildWithId(); + system2 = systemEntityFactory.buildWithId(); + + mockSystem1 = SystemMapper.mapFromEntityToDto(system1); + mockSystem2 = SystemMapper.mapFromEntityToDto(system2); + mockSystems = [mockSystem1, mockSystem2]; + + legacySystemService.findByType.mockImplementation((type: string | undefined) => { + if (type === SystemTypeEnum.OAUTH) return Promise.resolve([mockSystem1]); + return Promise.resolve(mockSystems); + }); + legacySystemService.findById.mockImplementation( + (id: EntityId): Promise => (id === system1.id ? Promise.resolve(mockSystem1) : Promise.reject()) + ); + }); + it('should return a system by id', async () => { const receivedSystem: SystemDto = await systemUc.findById(system1.id); @@ -102,7 +141,7 @@ describe('SystemUc', () => { type: 'ldap', }); - systemService.findById.mockResolvedValue(system); + legacySystemService.findById.mockResolvedValue(system); }; it('should reject promise, because ldap is not active', async () => { @@ -114,4 +153,97 @@ describe('SystemUc', () => { }); }); }); + + describe('delete', () => { + describe('when the system exists and the user can delete it', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const system = systemFactory.build(); + + systemService.findById.mockResolvedValueOnce(system); + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + + return { + user, + system, + }; + }; + + it('should check the permission', async () => { + const { user, system } = setup(); + + await systemUc.delete(user.id, system.id); + + expect(authorizationService.checkPermission).toHaveBeenCalledWith( + user, + system, + AuthorizationContextBuilder.write([Permission.SYSTEM_CREATE]) + ); + }); + + it('should delete the system', async () => { + const { user, system } = setup(); + + await systemUc.delete(user.id, system.id); + + expect(systemService.delete).toHaveBeenCalledWith(system); + }); + }); + + describe('when the system does not exist', () => { + const setup = () => { + systemService.findById.mockResolvedValueOnce(null); + }; + + it('should throw a not found exception', async () => { + setup(); + + await expect(systemUc.delete(new ObjectId().toHexString(), new ObjectId().toHexString())).rejects.toThrow( + NotFoundLoggableException + ); + }); + + it('should not delete any system', async () => { + setup(); + + await expect(systemUc.delete(new ObjectId().toHexString(), new ObjectId().toHexString())).rejects.toThrow(); + + expect(systemService.delete).not.toHaveBeenCalled(); + }); + }); + + describe('when the user is not authorized', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const system = systemFactory.build(); + const error = new Error(); + + systemService.findById.mockResolvedValueOnce(system); + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + authorizationService.checkPermission.mockImplementation(() => { + throw error; + }); + + return { + user, + system, + error, + }; + }; + + it('should throw an error', async () => { + const { user, system, error } = setup(); + + await expect(systemUc.delete(user.id, system.id)).rejects.toThrow(error); + }); + + it('should not delete any system', async () => { + const { user, system } = setup(); + + await expect(systemUc.delete(user.id, system.id)).rejects.toThrow(); + + expect(systemService.delete).not.toHaveBeenCalled(); + }); + }); + }); }); diff --git a/apps/server/src/modules/system/uc/system.uc.ts b/apps/server/src/modules/system/uc/system.uc.ts index 00665191da7..4ced419519b 100644 --- a/apps/server/src/modules/system/uc/system.uc.ts +++ b/apps/server/src/modules/system/uc/system.uc.ts @@ -1,20 +1,26 @@ +import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { Injectable } from '@nestjs/common'; import { EntityNotFoundError } from '@shared/common'; -import { EntityId, SystemEntity, SystemType, SystemTypeEnum } from '@shared/domain'; -import { SystemDto } from '@modules/system/service/dto/system.dto'; -import { SystemService } from '@modules/system/service/system.service'; +import { NotFoundLoggableException } from '@shared/common/loggable-exception'; +import { EntityId, Permission, SystemEntity, SystemType, SystemTypeEnum, User } from '@shared/domain'; +import { System } from '../domain'; +import { LegacySystemService, SystemDto, SystemService } from '../service'; @Injectable() export class SystemUc { - constructor(private readonly systemService: SystemService) {} + constructor( + private readonly legacySystemService: LegacySystemService, + private readonly systemService: SystemService, + private readonly authorizationService: AuthorizationService + ) {} async findByFilter(type?: SystemType, onlyOauth = false): Promise { let systems: SystemDto[]; if (onlyOauth) { - systems = await this.systemService.findByType(SystemTypeEnum.OAUTH); + systems = await this.legacySystemService.findByType(SystemTypeEnum.OAUTH); } else { - systems = await this.systemService.findByType(type); + systems = await this.legacySystemService.findByType(type); } systems = systems.filter((system: SystemDto) => system.ldapActive !== false); @@ -23,7 +29,7 @@ export class SystemUc { } async findById(id: EntityId): Promise { - const system: SystemDto = await this.systemService.findById(id); + const system: SystemDto = await this.legacySystemService.findById(id); if (system.ldapActive === false) { throw new EntityNotFoundError(SystemEntity.name, { id }); @@ -31,4 +37,21 @@ export class SystemUc { return system; } + + async delete(userId: EntityId, systemId: EntityId): Promise { + const system: System | null = await this.systemService.findById(systemId); + + if (!system) { + throw new NotFoundLoggableException(System.name, 'id', systemId); + } + + const user: User = await this.authorizationService.getUserWithPermissions(userId); + this.authorizationService.checkPermission( + user, + system, + AuthorizationContextBuilder.write([Permission.SYSTEM_CREATE]) + ); + + await this.systemService.delete(system); + } } diff --git a/apps/server/src/modules/tool/common/controller/dto/context-external-tool-count-per-context.response.ts b/apps/server/src/modules/tool/common/controller/dto/context-external-tool-count-per-context.response.ts new file mode 100644 index 00000000000..7e34d995267 --- /dev/null +++ b/apps/server/src/modules/tool/common/controller/dto/context-external-tool-count-per-context.response.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class ContextExternalToolCountPerContextResponse { + @ApiProperty() + course: number; + + @ApiProperty() + boardElement: number; + + constructor(props: ContextExternalToolCountPerContextResponse) { + this.course = props.course; + this.boardElement = props.boardElement; + } +} diff --git a/apps/server/src/modules/tool/common/controller/dto/index.ts b/apps/server/src/modules/tool/common/controller/dto/index.ts new file mode 100644 index 00000000000..87fa450d468 --- /dev/null +++ b/apps/server/src/modules/tool/common/controller/dto/index.ts @@ -0,0 +1 @@ +export { ContextExternalToolCountPerContextResponse } from './context-external-tool-count-per-context.response'; diff --git a/apps/server/src/modules/tool/external-tool/controller/dto/response/external-tool-metadata.response.ts b/apps/server/src/modules/tool/external-tool/controller/dto/response/external-tool-metadata.response.ts index d38b48cc503..745d0208a39 100644 --- a/apps/server/src/modules/tool/external-tool/controller/dto/response/external-tool-metadata.response.ts +++ b/apps/server/src/modules/tool/external-tool/controller/dto/response/external-tool-metadata.response.ts @@ -1,17 +1,12 @@ import { ApiProperty } from '@nestjs/swagger'; -import { ContextExternalToolType } from '../../../../context-external-tool/entity'; +import { ContextExternalToolCountPerContextResponse } from '../../../../common/controller/dto'; export class ExternalToolMetadataResponse { @ApiProperty() schoolExternalToolCount: number; - @ApiProperty({ - type: 'object', - properties: Object.fromEntries( - Object.values(ContextExternalToolType).map((key: ContextExternalToolType) => [key, { type: 'number' }]) - ), - }) - contextExternalToolCountPerContext: Record; + @ApiProperty() + contextExternalToolCountPerContext: ContextExternalToolCountPerContextResponse; constructor(externalToolMetadataResponse: ExternalToolMetadataResponse) { this.schoolExternalToolCount = externalToolMetadataResponse.schoolExternalToolCount; diff --git a/apps/server/src/modules/tool/external-tool/domain/external-tool-metadata.ts b/apps/server/src/modules/tool/external-tool/domain/external-tool-metadata.ts index 04492680ff7..5fa4b0f3f7c 100644 --- a/apps/server/src/modules/tool/external-tool/domain/external-tool-metadata.ts +++ b/apps/server/src/modules/tool/external-tool/domain/external-tool-metadata.ts @@ -1,7 +1,9 @@ +import { ContextExternalToolType } from '../../context-external-tool/entity'; + export class ExternalToolMetadata { schoolExternalToolCount: number; - contextExternalToolCountPerContext: Record; + contextExternalToolCountPerContext: Record; constructor(externalToolMetadata: ExternalToolMetadata) { this.schoolExternalToolCount = externalToolMetadata.schoolExternalToolCount; diff --git a/apps/server/src/modules/tool/external-tool/mapper/external-tool-metadata.mapper.ts b/apps/server/src/modules/tool/external-tool/mapper/external-tool-metadata.mapper.ts index b3d6555f898..f5c257b5430 100644 --- a/apps/server/src/modules/tool/external-tool/mapper/external-tool-metadata.mapper.ts +++ b/apps/server/src/modules/tool/external-tool/mapper/external-tool-metadata.mapper.ts @@ -1,3 +1,4 @@ +import { ContextExternalToolCountPerContextResponse } from '../../common/controller/dto'; import { ExternalToolMetadataResponse } from '../controller/dto'; import { ExternalToolMetadata } from '../domain'; @@ -5,7 +6,9 @@ export class ExternalToolMetadataMapper { static mapToExternalToolMetadataResponse(externalToolMetadata: ExternalToolMetadata): ExternalToolMetadataResponse { const externalToolMetadataResponse: ExternalToolMetadataResponse = new ExternalToolMetadataResponse({ schoolExternalToolCount: externalToolMetadata.schoolExternalToolCount, - contextExternalToolCountPerContext: externalToolMetadata.contextExternalToolCountPerContext, + contextExternalToolCountPerContext: new ContextExternalToolCountPerContextResponse( + externalToolMetadata.contextExternalToolCountPerContext + ), }); return externalToolMetadataResponse; diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool-metadata.service.ts b/apps/server/src/modules/tool/external-tool/service/external-tool-metadata.service.ts index 9476e738a87..ef82629e87d 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool-metadata.service.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool-metadata.service.ts @@ -2,10 +2,10 @@ import { Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain'; import { ContextExternalToolRepo, SchoolExternalToolRepo } from '@shared/repo'; import { ToolContextType } from '../../common/enum'; +import { ToolContextMapper } from '../../common/mapper/tool-context.mapper'; import { ContextExternalToolType } from '../../context-external-tool/entity'; import { SchoolExternalTool } from '../../school-external-tool/domain'; import { ExternalToolMetadata } from '../domain'; -import { ToolContextMapper } from '../../common/mapper/tool-context.mapper'; @Injectable() export class ExternalToolMetadataService { @@ -40,11 +40,11 @@ export class ExternalToolMetadataService { ); } - const externaltoolMetadata: ExternalToolMetadata = new ExternalToolMetadata({ + const externalToolMetadata: ExternalToolMetadata = new ExternalToolMetadata({ schoolExternalToolCount: schoolExternalTools.length, contextExternalToolCountPerContext: contextExternalToolCount, }); - return externaltoolMetadata; + return externalToolMetadata; } } 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 ff209ac301d..35a20ca8997 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 @@ -7,10 +7,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { IFindOptions, Permission, Role, SortOrder, User } from '@shared/domain'; import { Page } from '@shared/domain/domainobject/page'; import { roleFactory, setupEntities, userFactory } from '@shared/testing'; -import { - externalToolFactory, - oauth2ToolConfigFactory, -} from '@shared/testing/factory/domainobject/tool/external-tool.factory'; +import { externalToolFactory, oauth2ToolConfigFactory } from '@shared/testing/factory'; import { ExternalToolSearchQuery } from '../../common/interface'; import { ExternalTool, ExternalToolMetadata, Oauth2ToolConfig } from '../domain'; import { @@ -550,7 +547,7 @@ describe('ExternalToolUc', () => { const externalToolMetadata: ExternalToolMetadata = new ExternalToolMetadata({ schoolExternalToolCount: 2, - contextExternalToolCountPerContext: { course: 3, 'board-element': 3 }, + contextExternalToolCountPerContext: { course: 3, boardElement: 3 }, }); externalToolMetadataService.getMetadata.mockResolvedValue(externalToolMetadata); diff --git a/apps/server/src/modules/tool/school-external-tool/controller/dto/school-external-tool-metadata.response.ts b/apps/server/src/modules/tool/school-external-tool/controller/dto/school-external-tool-metadata.response.ts index db4806d23ec..c61b24df3c9 100644 --- a/apps/server/src/modules/tool/school-external-tool/controller/dto/school-external-tool-metadata.response.ts +++ b/apps/server/src/modules/tool/school-external-tool/controller/dto/school-external-tool-metadata.response.ts @@ -1,14 +1,9 @@ import { ApiProperty } from '@nestjs/swagger'; -import { ContextExternalToolType } from '../../../context-external-tool/entity'; +import { ContextExternalToolCountPerContextResponse } from '../../../common/controller/dto'; export class SchoolExternalToolMetadataResponse { - @ApiProperty({ - type: 'object', - properties: Object.fromEntries( - Object.values(ContextExternalToolType).map((key: ContextExternalToolType) => [key, { type: 'number' }]) - ), - }) - contextExternalToolCountPerContext: Record; + @ApiProperty() + contextExternalToolCountPerContext: ContextExternalToolCountPerContextResponse; constructor(schoolExternalToolMetadataResponse: SchoolExternalToolMetadataResponse) { this.contextExternalToolCountPerContext = schoolExternalToolMetadataResponse.contextExternalToolCountPerContext; diff --git a/apps/server/src/modules/tool/school-external-tool/domain/school-external-tool-metadata.ts b/apps/server/src/modules/tool/school-external-tool/domain/school-external-tool-metadata.ts index 4cccdfe11a1..741857352d3 100644 --- a/apps/server/src/modules/tool/school-external-tool/domain/school-external-tool-metadata.ts +++ b/apps/server/src/modules/tool/school-external-tool/domain/school-external-tool-metadata.ts @@ -1,5 +1,7 @@ +import { ContextExternalToolType } from '../../context-external-tool/entity'; + export class SchoolExternalToolMetadata { - contextExternalToolCountPerContext: Record; + contextExternalToolCountPerContext: Record; constructor(schoolExternalToolMetadata: SchoolExternalToolMetadata) { this.contextExternalToolCountPerContext = schoolExternalToolMetadata.contextExternalToolCountPerContext; diff --git a/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-metadata.mapper.ts b/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-metadata.mapper.ts index 1e42f22ebf2..acc58dd6ef8 100644 --- a/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-metadata.mapper.ts +++ b/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-metadata.mapper.ts @@ -1,3 +1,4 @@ +import { ContextExternalToolCountPerContextResponse } from '../../common/controller/dto'; import { SchoolExternalToolMetadataResponse } from '../controller/dto'; import { SchoolExternalToolMetadata } from '../domain'; @@ -6,7 +7,9 @@ export class SchoolExternalToolMetadataMapper { schoolExternalToolMetadata: SchoolExternalToolMetadata ): SchoolExternalToolMetadataResponse { const externalToolMetadataResponse: SchoolExternalToolMetadataResponse = new SchoolExternalToolMetadataResponse({ - contextExternalToolCountPerContext: schoolExternalToolMetadata.contextExternalToolCountPerContext, + contextExternalToolCountPerContext: new ContextExternalToolCountPerContextResponse( + schoolExternalToolMetadata.contextExternalToolCountPerContext + ), }); return externalToolMetadataResponse; diff --git a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-metadata.service.ts b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-metadata.service.ts index fa8fd4fc926..53061147aca 100644 --- a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-metadata.service.ts +++ b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-metadata.service.ts @@ -28,10 +28,10 @@ export class SchoolExternalToolMetadataService { }) ); - const schoolExternaltoolMetadata: SchoolExternalToolMetadata = new SchoolExternalToolMetadata({ + const schoolExternalToolMetadata: SchoolExternalToolMetadata = new SchoolExternalToolMetadata({ contextExternalToolCountPerContext: contextExternalToolCount, }); - return schoolExternaltoolMetadata; + return schoolExternalToolMetadata; } } diff --git a/apps/server/src/modules/user-import/controller/api-test/import-user.api.spec.ts b/apps/server/src/modules/user-import/controller/api-test/import-user.api.spec.ts index 51bea6b1224..5ee7db64ce0 100644 --- a/apps/server/src/modules/user-import/controller/api-test/import-user.api.spec.ts +++ b/apps/server/src/modules/user-import/controller/api-test/import-user.api.spec.ts @@ -39,7 +39,7 @@ import { mapUserToCurrentUser, roleFactory, schoolFactory, - systemFactory, + systemEntityFactory, userFactory, } from '@shared/testing'; import { Request } from 'express'; @@ -51,7 +51,7 @@ describe('ImportUser Controller (API)', () => { let currentUser: ICurrentUser; const authenticatedUser = async (permissions: Permission[] = [], features: SchoolFeatures[] = []) => { - const system = systemFactory.buildWithId(); // TODO no id? + const system = systemEntityFactory.buildWithId(); // TODO no id? const school = schoolFactory.build({ officialSchoolNumber: 'foo', features }); const roles = [roleFactory.build({ name: RoleName.ADMINISTRATOR, permissions })]; await em.persistAndFlush([school, system, ...roles]); diff --git a/apps/server/src/modules/user-import/controller/import-user.controller.spec.ts b/apps/server/src/modules/user-import/controller/import-user.controller.spec.ts index c93c7bc58a7..ab94fc43143 100644 --- a/apps/server/src/modules/user-import/controller/import-user.controller.spec.ts +++ b/apps/server/src/modules/user-import/controller/import-user.controller.spec.ts @@ -1,10 +1,10 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { ImportUserRepo, SystemRepo, UserRepo } from '@shared/repo'; import { AccountService } from '@modules/account/services/account.service'; import { AuthorizationService } from '@modules/authorization'; import { LegacySchoolService } from '@modules/legacy-school'; -import { LoggerModule } from '@src/core/logger'; import { ConfigModule } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ImportUserRepo, LegacySystemRepo, UserRepo } from '@shared/repo'; +import { LoggerModule } from '@src/core/logger'; import { UserImportUc } from '../uc/user-import.uc'; import { ImportUserController } from './import-user.controller'; @@ -38,7 +38,7 @@ describe('ImportUserController', () => { useValue: {}, }, { - provide: SystemRepo, + provide: LegacySystemRepo, useValue: {}, }, { diff --git a/apps/server/src/modules/user-import/uc/user-import.uc.spec.ts b/apps/server/src/modules/user-import/uc/user-import.uc.spec.ts index ff4aa4c266c..b68cff8074f 100644 --- a/apps/server/src/modules/user-import/uc/user-import.uc.spec.ts +++ b/apps/server/src/modules/user-import/uc/user-import.uc.spec.ts @@ -1,6 +1,10 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { ObjectId } from '@mikro-orm/mongodb'; +import { AccountService } from '@modules/account/services/account.service'; +import { AuthorizationService } from '@modules/authorization'; +import { LegacySchoolService } from '@modules/legacy-school'; import { BadRequestException, ForbiddenException } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; @@ -16,14 +20,10 @@ import { SystemEntity, User, } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@infra/database'; -import { ImportUserRepo, SystemRepo, UserRepo } from '@shared/repo'; +import { ImportUserRepo, LegacySystemRepo, UserRepo } from '@shared/repo'; import { federalStateFactory, importUserFactory, schoolFactory, userFactory } from '@shared/testing'; -import { systemFactory } from '@shared/testing/factory/system.factory'; +import { systemEntityFactory } from '@shared/testing/factory/systemEntityFactory'; import { LoggerModule } from '@src/core/logger'; -import { AccountService } from '@modules/account/services/account.service'; -import { AuthorizationService } from '@modules/authorization'; -import { LegacySchoolService } from '@modules/legacy-school'; import { LdapAlreadyPersistedException, MigrationAlreadyActivatedException, @@ -38,7 +38,7 @@ describe('[ImportUserModule]', () => { let accountService: DeepMocked; let importUserRepo: DeepMocked; let schoolService: DeepMocked; - let systemRepo: DeepMocked; + let systemRepo: DeepMocked; let userRepo: DeepMocked; let authorizationService: DeepMocked; let configurationSpy: jest.SpyInstance; @@ -65,8 +65,8 @@ describe('[ImportUserModule]', () => { useValue: createMock(), }, { - provide: SystemRepo, - useValue: createMock(), + provide: LegacySystemRepo, + useValue: createMock(), }, { provide: UserRepo, @@ -82,7 +82,7 @@ describe('[ImportUserModule]', () => { accountService = module.get(AccountService); importUserRepo = module.get(ImportUserRepo); schoolService = module.get(LegacySchoolService); - systemRepo = module.get(SystemRepo); + systemRepo = module.get(LegacySystemRepo); userRepo = module.get(UserRepo); authorizationService = module.get(AuthorizationService); }); @@ -472,7 +472,7 @@ describe('[ImportUserModule]', () => { let userRepoFlushSpy: jest.SpyInstance; let accountServiceFindByUserIdSpy: jest.SpyInstance; beforeEach(() => { - system = systemFactory.buildWithId(); + system = systemEntityFactory.buildWithId(); school = schoolFactory.buildWithId({ systems: [system] }); school.externalId = 'foo'; school.inMaintenanceSince = new Date(); @@ -605,7 +605,7 @@ describe('[ImportUserModule]', () => { const currentDate = new Date('2022-03-10T00:00:00.000Z'); let dateSpy: jest.SpyInstance; beforeEach(() => { - system = systemFactory.buildWithId({ ldapConfig: {} }); + system = systemEntityFactory.buildWithId({ ldapConfig: {} }); school = schoolFactory.buildWithId(); school.officialSchoolNumber = 'foo'; currentUser = userFactory.buildWithId({ school }); diff --git a/apps/server/src/modules/user-import/uc/user-import.uc.ts b/apps/server/src/modules/user-import/uc/user-import.uc.ts index a9290bb5e42..4e3b14b6734 100644 --- a/apps/server/src/modules/user-import/uc/user-import.uc.ts +++ b/apps/server/src/modules/user-import/uc/user-import.uc.ts @@ -21,7 +21,7 @@ import { SystemEntity, User, } from '@shared/domain'; -import { ImportUserRepo, SystemRepo, UserRepo } from '@shared/repo'; +import { ImportUserRepo, LegacySystemRepo, UserRepo } from '@shared/repo'; import { Logger } from '@src/core/logger'; import { AccountSaveDto } from '../../account/services/dto'; import { @@ -50,7 +50,7 @@ export class UserImportUc { private readonly importUserRepo: ImportUserRepo, private readonly authorizationService: AuthorizationService, private readonly schoolService: LegacySchoolService, - private readonly systemRepo: SystemRepo, + private readonly systemRepo: LegacySystemRepo, private readonly userRepo: UserRepo, private readonly logger: Logger ) { diff --git a/apps/server/src/modules/user-import/user-import.module.ts b/apps/server/src/modules/user-import/user-import.module.ts index 2cf4d94704e..48fa730c61f 100644 --- a/apps/server/src/modules/user-import/user-import.module.ts +++ b/apps/server/src/modules/user-import/user-import.module.ts @@ -1,7 +1,7 @@ +import { LegacySchoolModule } from '@modules/legacy-school'; import { Module } from '@nestjs/common'; -import { ImportUserRepo, LegacySchoolRepo, SystemRepo, UserRepo } from '@shared/repo'; +import { ImportUserRepo, LegacySchoolRepo, LegacySystemRepo, UserRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; -import { LegacySchoolModule } from '@modules/legacy-school'; import { AccountModule } from '../account'; import { AuthorizationModule } from '../authorization'; import { ImportUserController } from './controller/import-user.controller'; @@ -10,7 +10,7 @@ import { UserImportUc } from './uc/user-import.uc'; @Module({ imports: [LoggerModule, AccountModule, LegacySchoolModule, AuthorizationModule], controllers: [ImportUserController], - providers: [UserImportUc, ImportUserRepo, LegacySchoolRepo, SystemRepo, UserRepo], + providers: [UserImportUc, ImportUserRepo, LegacySchoolRepo, LegacySystemRepo, UserRepo], exports: [], }) /** diff --git a/apps/server/src/modules/user-login-migration/controller/api-test/user-login-migration.api.spec.ts b/apps/server/src/modules/user-login-migration/controller/api-test/user-login-migration.api.spec.ts index f23795a35ab..d44280df28a 100644 --- a/apps/server/src/modules/user-login-migration/controller/api-test/user-login-migration.api.spec.ts +++ b/apps/server/src/modules/user-login-migration/controller/api-test/user-login-migration.api.spec.ts @@ -12,7 +12,7 @@ import { cleanupCollections, JwtTestFactory, schoolFactory, - systemFactory, + systemEntityFactory, TestApiClient, UserAndAccountTestFactory, userFactory, @@ -71,8 +71,8 @@ describe('UserLoginMigrationController (API)', () => { describe('when data is given', () => { const setup = async () => { const date: Date = new Date(2023, 5, 4); - const sourceSystem: SystemEntity = systemFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); - const targetSystem: SystemEntity = systemFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); + const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); + const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); const school: SchoolEntity = schoolFactory.buildWithId({ systems: [sourceSystem], }); @@ -136,8 +136,8 @@ describe('UserLoginMigrationController (API)', () => { describe('when a user login migration exists', () => { const setup = async () => { const date: Date = new Date(2023, 5, 4); - const sourceSystem: SystemEntity = systemFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); - const targetSystem: SystemEntity = systemFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); + const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); + const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); const school: SchoolEntity = schoolFactory.buildWithId({ systems: [sourceSystem], }); @@ -232,8 +232,8 @@ describe('UserLoginMigrationController (API)', () => { describe('[POST] /start', () => { describe('when current user start the migration successfully', () => { const setup = async () => { - const sourceSystem: SystemEntity = systemFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); - const targetSystem: SystemEntity = systemFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); + const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); + const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); const school: SchoolEntity = schoolFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', @@ -316,8 +316,8 @@ describe('UserLoginMigrationController (API)', () => { describe('when migration already started', () => { const setup = async () => { const date: Date = new Date(2023, 5, 4); - const sourceSystem: SystemEntity = systemFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); - const targetSystem: SystemEntity = systemFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); + const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); + const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); const school: SchoolEntity = schoolFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', @@ -355,8 +355,8 @@ describe('UserLoginMigrationController (API)', () => { describe('when migration already closed', () => { const setup = async () => { const date: Date = new Date(2023, 5, 4); - const sourceSystem: SystemEntity = systemFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); - const targetSystem: SystemEntity = systemFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); + const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); + const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); const school: SchoolEntity = schoolFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', @@ -395,8 +395,8 @@ describe('UserLoginMigrationController (API)', () => { describe('when official school number is not set', () => { const setup = async () => { - const sourceSystem: SystemEntity = systemFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); - const targetSystem: SystemEntity = systemFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); + const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); + const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); const school: SchoolEntity = schoolFactory.buildWithId({ systems: [sourceSystem], }); @@ -468,7 +468,7 @@ describe('UserLoginMigrationController (API)', () => { describe('when providing a code and being eligible to migrate', () => { const setup = async () => { - const targetSystem: SystemEntity = systemFactory + const targetSystem: SystemEntity = systemEntityFactory .withOauthConfig() .buildWithId({ provisioningStrategy: SystemProvisioningStrategy.SANIS }); @@ -478,7 +478,7 @@ describe('UserLoginMigrationController (API)', () => { query.systemId = targetSystem.id; query.redirectUri = 'redirectUri'; - const sourceSystem: SystemEntity = systemFactory.buildWithId(); + const sourceSystem: SystemEntity = systemEntityFactory.buildWithId(); const officialSchoolNumber = '12345'; const externalId = 'aef1f4fd-c323-466e-962b-a84354c0e713'; @@ -536,7 +536,7 @@ describe('UserLoginMigrationController (API)', () => { describe('when migration failed, because of schoolnumbers mismatch', () => { const setup = async () => { - const targetSystem: SystemEntity = systemFactory + const targetSystem: SystemEntity = systemEntityFactory .withOauthConfig() .buildWithId({ provisioningStrategy: SystemProvisioningStrategy.SANIS }); @@ -545,7 +545,7 @@ describe('UserLoginMigrationController (API)', () => { query.systemId = targetSystem.id; query.redirectUri = 'redirectUri'; - const sourceSystem: SystemEntity = systemFactory.buildWithId(); + const sourceSystem: SystemEntity = systemEntityFactory.buildWithId(); const officialSchoolNumber = '12345'; const externalId = 'aef1f4fd-c323-466e-962b-a84354c0e713'; @@ -622,8 +622,8 @@ describe('UserLoginMigrationController (API)', () => { describe('[POST] /restart', () => { describe('when current User restart the migration successfully', () => { const setup = async () => { - const sourceSystem: SystemEntity = systemFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); - const targetSystem: SystemEntity = systemFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); + const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); + const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); const school: SchoolEntity = schoolFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', @@ -721,8 +721,8 @@ describe('UserLoginMigrationController (API)', () => { describe('when migration is already started', () => { const setup = async () => { - const sourceSystem: SystemEntity = systemFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); - const targetSystem: SystemEntity = systemFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); + const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); + const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); const school: SchoolEntity = schoolFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', @@ -759,8 +759,8 @@ describe('UserLoginMigrationController (API)', () => { describe('when migration is finally finished', () => { const setup = async () => { - const sourceSystem: SystemEntity = systemFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); - const targetSystem: SystemEntity = systemFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); + const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); + const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); const school: SchoolEntity = schoolFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', @@ -802,8 +802,8 @@ describe('UserLoginMigrationController (API)', () => { describe('[PUT] /mandatory', () => { describe('when migration is set from optional to mandatory', () => { const setup = async () => { - const sourceSystem: SystemEntity = systemFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); - const targetSystem: SystemEntity = systemFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); + const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); + const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); const school: SchoolEntity = schoolFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', @@ -854,8 +854,8 @@ describe('UserLoginMigrationController (API)', () => { describe('when migration is set from mandatory to optional', () => { const setup = async () => { - const sourceSystem: SystemEntity = systemFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); - const targetSystem: SystemEntity = systemFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); + const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); + const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); const school: SchoolEntity = schoolFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', @@ -906,8 +906,8 @@ describe('UserLoginMigrationController (API)', () => { describe('when migration is not started', () => { const setup = async () => { - const sourceSystem: SystemEntity = systemFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); - const targetSystem: SystemEntity = systemFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); + const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); + const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); const school: SchoolEntity = schoolFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', @@ -936,8 +936,8 @@ describe('UserLoginMigrationController (API)', () => { describe('when the migration is closed', () => { const setup = async () => { - const sourceSystem: SystemEntity = systemFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); - const targetSystem: SystemEntity = systemFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); + const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); + const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); const school: SchoolEntity = schoolFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', @@ -984,8 +984,8 @@ describe('UserLoginMigrationController (API)', () => { describe('when user has not the required permission', () => { const setup = async () => { - const sourceSystem: SystemEntity = systemFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); - const targetSystem: SystemEntity = systemFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); + const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); + const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); const school: SchoolEntity = schoolFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', @@ -1026,8 +1026,8 @@ describe('UserLoginMigrationController (API)', () => { describe('[POST] /close', () => { describe('when the user login migration is running', () => { const setup = async () => { - const sourceSystem: SystemEntity = systemFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); - const targetSystem: SystemEntity = systemFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); + const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); + const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); const school: SchoolEntity = schoolFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', @@ -1102,8 +1102,8 @@ describe('UserLoginMigrationController (API)', () => { describe('when migration is not started', () => { const setup = async () => { - const sourceSystem: SystemEntity = systemFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); - const targetSystem: SystemEntity = systemFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); + const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); + const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); const school: SchoolEntity = schoolFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', @@ -1145,8 +1145,8 @@ describe('UserLoginMigrationController (API)', () => { describe('when the migration is already closed', () => { const setup = async () => { - const sourceSystem: SystemEntity = systemFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); - const targetSystem: SystemEntity = systemFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); + const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); + const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); const school: SchoolEntity = schoolFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', @@ -1211,8 +1211,8 @@ describe('UserLoginMigrationController (API)', () => { describe('when the migration is finished', () => { const setup = async () => { - const sourceSystem: SystemEntity = systemFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); - const targetSystem: SystemEntity = systemFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); + const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); + const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); const school: SchoolEntity = schoolFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', @@ -1259,8 +1259,8 @@ describe('UserLoginMigrationController (API)', () => { describe('when user has not the required permission', () => { const setup = async () => { - const sourceSystem: SystemEntity = systemFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); - const targetSystem: SystemEntity = systemFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); + const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); + const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); const school: SchoolEntity = schoolFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', @@ -1298,8 +1298,8 @@ describe('UserLoginMigrationController (API)', () => { describe('when no user has migrate', () => { const setup = async () => { - const sourceSystem: SystemEntity = systemFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); - const targetSystem: SystemEntity = systemFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); + const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); + const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); const school: SchoolEntity = schoolFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', diff --git a/apps/server/src/modules/user-login-migration/service/user-login-migration.service.spec.ts b/apps/server/src/modules/user-login-migration/service/user-login-migration.service.spec.ts index 0edaf9d1a38..35e9e02b473 100644 --- a/apps/server/src/modules/user-login-migration/service/user-login-migration.service.spec.ts +++ b/apps/server/src/modules/user-login-migration/service/user-login-migration.service.spec.ts @@ -2,7 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { ObjectId } from '@mikro-orm/mongodb'; import { LegacySchoolService } from '@modules/legacy-school'; -import { SystemService } from '@modules/system'; +import { LegacySystemService } from '@modules/system'; import { SystemDto } from '@modules/system/service'; import { UserService } from '@modules/user'; import { InternalServerErrorException } from '@nestjs/common'; @@ -22,7 +22,7 @@ describe(UserLoginMigrationService.name, () => { let userService: DeepMocked; let schoolService: DeepMocked; - let systemService: DeepMocked; + let systemService: DeepMocked; let userLoginMigrationRepo: DeepMocked; const mockedDate: Date = new Date('2023-05-02'); @@ -46,8 +46,8 @@ describe(UserLoginMigrationService.name, () => { useValue: createMock(), }, { - provide: SystemService, - useValue: createMock(), + provide: LegacySystemService, + useValue: createMock(), }, { provide: UserLoginMigrationRepo, @@ -59,7 +59,7 @@ describe(UserLoginMigrationService.name, () => { service = module.get(UserLoginMigrationService); userService = module.get(UserService); schoolService = module.get(LegacySchoolService); - systemService = module.get(SystemService); + systemService = module.get(LegacySystemService); userLoginMigrationRepo = module.get(UserLoginMigrationRepo); }); diff --git a/apps/server/src/modules/user-login-migration/service/user-login-migration.service.ts b/apps/server/src/modules/user-login-migration/service/user-login-migration.service.ts index 459b163f119..f9ee07a7fde 100644 --- a/apps/server/src/modules/user-login-migration/service/user-login-migration.service.ts +++ b/apps/server/src/modules/user-login-migration/service/user-login-migration.service.ts @@ -1,6 +1,6 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { LegacySchoolService } from '@modules/legacy-school'; -import { SystemDto, SystemService } from '@modules/system'; +import { LegacySystemService, SystemDto } from '@modules/system'; import { UserService } from '@modules/user'; import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { EntityId, LegacySchoolDo, SchoolFeatures, SystemTypeEnum, UserDO, UserLoginMigrationDO } from '@shared/domain'; @@ -16,7 +16,7 @@ export class UserLoginMigrationService { private readonly userService: UserService, private readonly userLoginMigrationRepo: UserLoginMigrationRepo, private readonly schoolService: LegacySchoolService, - private readonly systemService: SystemService + private readonly systemService: LegacySystemService ) {} public async startMigration(schoolId: string): Promise { diff --git a/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.spec.ts b/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.spec.ts index 4179505d009..59271414f1d 100644 --- a/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.spec.ts +++ b/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.spec.ts @@ -3,12 +3,12 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { AuthenticationService } from '@modules/authentication'; import { Action, AuthorizationService } from '@modules/authorization'; import { LegacySchoolService } from '@modules/legacy-school'; -import { OAuthTokenDto, OAuthService } from '@modules/oauth'; +import { OAuthService, OAuthTokenDto } from '@modules/oauth'; import { - ProvisioningService, ExternalSchoolDto, ExternalUserDto, OauthDataDto, + ProvisioningService, ProvisioningSystemDto, } from '@modules/provisioning'; import { ForbiddenException } from '@nestjs/common'; @@ -19,7 +19,7 @@ import { SystemProvisioningStrategy } from '@shared/domain/interface/system-prov import { legacySchoolDoFactory, setupEntities, - systemFactory, + systemEntityFactory, userFactory, userLoginMigrationDOFactory, } from '@shared/testing'; @@ -339,7 +339,7 @@ describe(UserLoginMigrationUc.name, () => { describe('when external school and official school number is defined and school has to be migrated', () => { const setup = () => { - const sourceSystem: SystemEntity = systemFactory + const sourceSystem: SystemEntity = systemEntityFactory .withOauthConfig() .buildWithId({ provisioningStrategy: SystemProvisioningStrategy.SANIS }); diff --git a/apps/server/src/shared/domain/entity/system.entity.spec.ts b/apps/server/src/shared/domain/entity/system.entity.spec.ts index 9b1c538673f..06c45b5c5e4 100644 --- a/apps/server/src/shared/domain/entity/system.entity.spec.ts +++ b/apps/server/src/shared/domain/entity/system.entity.spec.ts @@ -1,6 +1,5 @@ import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { setupEntities } from '@shared/testing'; -import { systemFactory } from '@shared/testing/factory/system.factory'; +import { setupEntities, systemEntityFactory } from '@shared/testing'; import { SystemEntity } from './system.entity'; describe('System Entity', () => { @@ -16,13 +15,13 @@ describe('System Entity', () => { }); it('should create a system by passing required properties', () => { - const system = systemFactory.build(); + const system = systemEntityFactory.build(); expect(system instanceof SystemEntity).toEqual(true); }); it('should create a system by passing required and optional properties', () => { - const system = systemFactory + const system = systemEntityFactory .withOauthConfig() .build({ url: 'SAMPLE_URL', alias: 'SAMPLE_ALIAS', displayName: 'SAMPLE_NAME' }); @@ -39,16 +38,16 @@ describe('System Entity', () => { clientId: '12345', clientSecret: 'mocksecret', idpHint: 'mock-oauth-idpHint', - tokenEndpoint: 'http://mock.de/mock/auth/public/mockToken', + tokenEndpoint: 'https://mock.de/mock/auth/public/mockToken', grantType: 'authorization_code', - redirectUri: 'http://mockhost:3030/api/v3/sso/oauth/', + redirectUri: 'https://mockhost:3030/api/v3/sso/oauth/', scope: 'openid uuid', responseType: 'code', - authEndpoint: 'http://mock.de/auth', + authEndpoint: 'https://mock.de/auth', provider: 'mock_type', - logoutEndpoint: 'http://mock.de/logout', + logoutEndpoint: 'https://mock.de/logout', issuer: 'mock_issuer', - jwksEndpoint: 'http://mock.de/jwks', + jwksEndpoint: 'https://mock.de/jwks', }, }) ); diff --git a/apps/server/src/shared/domain/entity/system.entity.ts b/apps/server/src/shared/domain/entity/system.entity.ts index 07cfea5bb59..3f7f1c46c88 100644 --- a/apps/server/src/shared/domain/entity/system.entity.ts +++ b/apps/server/src/shared/domain/entity/system.entity.ts @@ -3,20 +3,20 @@ import { SystemProvisioningStrategy } from '@shared/domain/interface/system-prov import { EntityId } from '../types'; import { BaseEntityWithTimestamps } from './base.entity'; -export interface SystemProperties { +export interface SystemEntityProps { type: string; url?: string; alias?: string; displayName?: string; - oauthConfig?: OauthConfig; - oidcConfig?: OidcConfig; - ldapConfig?: LdapConfig; + oauthConfig?: OauthConfigEntity; + oidcConfig?: OidcConfigEntity; + ldapConfig?: LdapConfigEntity; provisioningStrategy?: SystemProvisioningStrategy; provisioningUrl?: string; } -export class OauthConfig { - constructor(oauthConfig: OauthConfig) { +export class OauthConfigEntity { + constructor(oauthConfig: OauthConfigEntity) { this.clientId = oauthConfig.clientId; this.clientSecret = oauthConfig.clientSecret; this.idpHint = oauthConfig.idpHint; @@ -73,8 +73,8 @@ export class OauthConfig { } @Embeddable() -export class LdapConfig { - constructor(ldapConfig: Readonly) { +export class LdapConfigEntity { + constructor(ldapConfig: Readonly) { this.active = ldapConfig.active; this.federalState = ldapConfig.federalState; this.lastSyncAttempt = ldapConfig.lastSyncAttempt; @@ -150,8 +150,8 @@ export class LdapConfig { }; }; } -export class OidcConfig { - constructor(oidcConfig: OidcConfig) { +export class OidcConfigEntity { + constructor(oidcConfig: OidcConfigEntity) { this.clientId = oidcConfig.clientId; this.clientSecret = oidcConfig.clientSecret; this.idpHint = oidcConfig.idpHint; @@ -189,7 +189,7 @@ export class OidcConfig { @Entity({ tableName: 'systems' }) export class SystemEntity extends BaseEntityWithTimestamps { - constructor(props: SystemProperties) { + constructor(props: SystemEntityProps) { super(); this.type = props.type; this.url = props.url; @@ -215,17 +215,17 @@ export class SystemEntity extends BaseEntityWithTimestamps { displayName?: string; @Property({ nullable: true }) - oauthConfig?: OauthConfig; + oauthConfig?: OauthConfigEntity; @Property({ nullable: true }) @Enum() provisioningStrategy?: SystemProvisioningStrategy; @Property({ nullable: true }) - oidcConfig?: OidcConfig; + oidcConfig?: OidcConfigEntity; - @Embedded({ entity: () => LdapConfig, object: true, nullable: true }) - ldapConfig?: LdapConfig; + @Embedded({ entity: () => LdapConfigEntity, object: true, nullable: true }) + ldapConfig?: LdapConfigEntity; @Property({ nullable: true }) provisioningUrl?: string; diff --git a/apps/server/src/shared/repo/school/legacy-school.repo.integration.spec.ts b/apps/server/src/shared/repo/school/legacy-school.repo.integration.spec.ts index 097f5498685..090e9fc4fd9 100644 --- a/apps/server/src/shared/repo/school/legacy-school.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/school/legacy-school.repo.integration.spec.ts @@ -18,7 +18,7 @@ import { legacySchoolDoFactory, schoolFactory, schoolYearFactory, - systemFactory, + systemEntityFactory, userLoginMigrationFactory, } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; @@ -114,7 +114,7 @@ describe('LegacySchoolRepo', () => { describe('findByExternalId', () => { it('should find school by external ID', async () => { - const system: SystemEntity = systemFactory.buildWithId(); + const system: SystemEntity = systemEntityFactory.buildWithId(); const schoolEntity: SchoolEntity = schoolFactory.build({ externalId: 'externalId' }); schoolEntity.systems.add(system); @@ -182,7 +182,7 @@ describe('LegacySchoolRepo', () => { describe('mapEntityToDO is called', () => { it('should map school entity to school domain object', () => { - const system: SystemEntity = systemFactory.buildWithId(); + const system: SystemEntity = systemEntityFactory.buildWithId(); const schoolYear: SchoolYearEntity = schoolYearFactory.buildWithId(); const schoolEntity: SchoolEntity = schoolFactory.buildWithId({ systems: [system], features: [], schoolYear }); const userLoginMigration: UserLoginMigrationEntity = userLoginMigrationFactory.build({ school: schoolEntity }); @@ -219,8 +219,8 @@ describe('LegacySchoolRepo', () => { describe('mapDOToEntityProperties is called', () => { const setup = async () => { - const system1: SystemEntity = systemFactory.buildWithId(); - const system2: SystemEntity = systemFactory.buildWithId(); + const system1: SystemEntity = systemEntityFactory.buildWithId(); + const system2: SystemEntity = systemEntityFactory.buildWithId(); const userLoginMigration: UserLoginMigrationEntity = userLoginMigrationFactory.buildWithId(); diff --git a/apps/server/src/shared/repo/system/index.ts b/apps/server/src/shared/repo/system/index.ts index cfc3117fc5f..2c071b949c9 100644 --- a/apps/server/src/shared/repo/system/index.ts +++ b/apps/server/src/shared/repo/system/index.ts @@ -1 +1 @@ -export * from './system.repo'; +export * from './legacy-system.repo'; diff --git a/apps/server/src/shared/repo/system/system.repo.integration.spec.ts b/apps/server/src/shared/repo/system/legacy-system.repo.integration.spec.ts similarity index 82% rename from apps/server/src/shared/repo/system/system.repo.integration.spec.ts rename to apps/server/src/shared/repo/system/legacy-system.repo.integration.spec.ts index d8a5c1d87e3..bf8d9ea18fa 100644 --- a/apps/server/src/shared/repo/system/system.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/system/legacy-system.repo.integration.spec.ts @@ -1,22 +1,22 @@ +import { MongoMemoryDatabaseModule } from '@infra/database'; import { NotFoundError } from '@mikro-orm/core'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { SystemEntity, SystemTypeEnum } from '@shared/domain'; -import { MongoMemoryDatabaseModule } from '@infra/database'; -import { SystemRepo } from '@shared/repo'; -import { systemFactory } from '@shared/testing/factory/system.factory'; +import { LegacySystemRepo } from '@shared/repo'; +import { systemEntityFactory } from '@shared/testing'; describe('system repo', () => { let module: TestingModule; - let repo: SystemRepo; + let repo: LegacySystemRepo; let em: EntityManager; beforeAll(async () => { module = await Test.createTestingModule({ imports: [MongoMemoryDatabaseModule.forRoot()], - providers: [SystemRepo], + providers: [LegacySystemRepo], }).compile(); - repo = module.get(SystemRepo); + repo = module.get(LegacySystemRepo); em = module.get(EntityManager); }); @@ -39,7 +39,7 @@ describe('system repo', () => { }); it('should return right keys', async () => { - const system = systemFactory.build(); + const system = systemEntityFactory.build(); await em.persistAndFlush([system]); const result = await repo.findById(system.id); expect(Object.keys(result).sort()).toEqual( @@ -61,7 +61,7 @@ describe('system repo', () => { }); it('should return a System that matched by id', async () => { - const system = systemFactory.build(); + const system = systemEntityFactory.build(); await em.persistAndFlush([system]); const result = await repo.findById(system.id); expect(result).toEqual(system); @@ -80,7 +80,7 @@ describe('system repo', () => { }); it('should return all systems', async () => { - const systems = [systemFactory.build(), systemFactory.build({ oauthConfig: undefined })]; + const systems = [systemEntityFactory.build(), systemEntityFactory.build({ oauthConfig: undefined })]; await em.persistAndFlush(systems); const result = await repo.findAll(); @@ -91,9 +91,9 @@ describe('system repo', () => { }); describe('findByFilter', () => { - const ldapSystems = systemFactory.withLdapConfig().buildListWithId(2); - const oauthSystems = systemFactory.withOauthConfig().buildListWithId(2); - const oidcSystems = systemFactory.withOidcConfig().buildListWithId(2); + const ldapSystems = systemEntityFactory.withLdapConfig().buildListWithId(2); + const oauthSystems = systemEntityFactory.withOauthConfig().buildListWithId(2); + const oidcSystems = systemEntityFactory.withOidcConfig().buildListWithId(2); beforeAll(async () => { await em.persistAndFlush([...ldapSystems, ...oauthSystems, ...oidcSystems]); diff --git a/apps/server/src/shared/repo/system/system.repo.ts b/apps/server/src/shared/repo/system/legacy-system.repo.ts similarity index 81% rename from apps/server/src/shared/repo/system/system.repo.ts rename to apps/server/src/shared/repo/system/legacy-system.repo.ts index 65fd257cc24..85a8fdbdc84 100644 --- a/apps/server/src/shared/repo/system/system.repo.ts +++ b/apps/server/src/shared/repo/system/legacy-system.repo.ts @@ -3,8 +3,12 @@ import { SystemEntity, SystemTypeEnum } from '@shared/domain'; import { BaseRepo } from '@shared/repo/base.repo'; import { SystemScope } from '@shared/repo/system/system-scope'; +// TODO N21-1547: Fully replace this service with SystemService +/** + * @deprecated use the {@link SystemRepo} from the system module instead + */ @Injectable() -export class SystemRepo extends BaseRepo { +export class LegacySystemRepo extends BaseRepo { get entityName() { return SystemEntity; } diff --git a/apps/server/src/shared/repo/user/user-do.repo.integration.spec.ts b/apps/server/src/shared/repo/user/user-do.repo.integration.spec.ts index 451285eebca..ef01a96503c 100644 --- a/apps/server/src/shared/repo/user/user-do.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/user/user-do.repo.integration.spec.ts @@ -23,7 +23,7 @@ import { cleanupCollections, roleFactory, schoolFactory, - systemFactory, + systemEntityFactory, userDoFactory, userFactory, } from '@shared/testing'; @@ -146,7 +146,7 @@ describe('UserRepo', () => { let user: User; beforeEach(async () => { - system = systemFactory.buildWithId(); + system = systemEntityFactory.buildWithId(); school = schoolFactory.buildWithId(); school.systems.add(system); user = userFactory.buildWithId({ externalId, school }); @@ -190,7 +190,7 @@ describe('UserRepo', () => { let user: User; beforeEach(async () => { - system = systemFactory.buildWithId(); + system = systemEntityFactory.buildWithId(); school = schoolFactory.buildWithId(); school.systems.add(system); user = userFactory.buildWithId({ externalId, school }); diff --git a/apps/server/src/shared/repo/user/user.repo.integration.spec.ts b/apps/server/src/shared/repo/user/user.repo.integration.spec.ts index 3d44d0edfb8..a923b8d128f 100644 --- a/apps/server/src/shared/repo/user/user.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/user/user.repo.integration.spec.ts @@ -3,8 +3,14 @@ import { NotFoundError } from '@mikro-orm/core'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { MatchCreator, SortOrder, SystemEntity, User } from '@shared/domain'; -import { cleanupCollections, importUserFactory, roleFactory, schoolFactory, userFactory } from '@shared/testing'; -import { systemFactory } from '@shared/testing/factory/system.factory'; +import { + cleanupCollections, + importUserFactory, + roleFactory, + schoolFactory, + systemEntityFactory, + userFactory, +} from '@shared/testing'; import { UserRepo } from './user.repo'; describe('user repo', () => { @@ -124,7 +130,7 @@ describe('user repo', () => { let userA: User; let userB: User; beforeEach(async () => { - sys = systemFactory.build(); + sys = systemEntityFactory.build(); await em.persistAndFlush([sys]); const school = schoolFactory.build({ systems: [sys] }); // const school = schoolFactory.withSystem().build(); diff --git a/apps/server/src/shared/repo/userloginmigration/user-login-migration.repo.integration.spec.ts b/apps/server/src/shared/repo/userloginmigration/user-login-migration.repo.integration.spec.ts index 266cd0381c1..a78424f7525 100644 --- a/apps/server/src/shared/repo/userloginmigration/user-login-migration.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/userloginmigration/user-login-migration.repo.integration.spec.ts @@ -1,10 +1,10 @@ import { createMock } from '@golevelup/ts-jest'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { EntityManager } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { SchoolEntity, SystemEntity, UserLoginMigrationDO } from '@shared/domain'; import { UserLoginMigrationEntity } from '@shared/domain/entity/user-login-migration.entity'; -import { MongoMemoryDatabaseModule } from '@infra/database'; -import { cleanupCollections, schoolFactory, systemFactory } from '@shared/testing'; +import { cleanupCollections, schoolFactory, systemEntityFactory } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { userLoginMigrationFactory } from '../../testing/factory/user-login-migration.factory'; import { UserLoginMigrationRepo } from './user-login-migration.repo'; @@ -42,8 +42,8 @@ describe('UserLoginMigrationRepo', () => { describe('when saving a UserLoginMigrationDO', () => { const setup = async () => { const school: SchoolEntity = schoolFactory.buildWithId(); - const sourceSystem: SystemEntity = systemFactory.buildWithId(); - const targetSystem: SystemEntity = systemFactory.buildWithId(); + const sourceSystem: SystemEntity = systemEntityFactory.buildWithId(); + const targetSystem: SystemEntity = systemEntityFactory.buildWithId(); const domainObject: UserLoginMigrationDO = new UserLoginMigrationDO({ schoolId: school.id, diff --git a/apps/server/src/shared/testing/factory/domainobject/index.ts b/apps/server/src/shared/testing/factory/domainobject/index.ts index 8e02c37399c..9314c2a829e 100644 --- a/apps/server/src/shared/testing/factory/domainobject/index.ts +++ b/apps/server/src/shared/testing/factory/domainobject/index.ts @@ -7,3 +7,4 @@ export * from './domain-object.factory'; export * from './user-login-migration-do.factory'; export * from './lti-tool.factory'; export * from './pseudonym.factory'; +export { systemFactory } from './system/system.factory'; diff --git a/apps/server/src/shared/testing/factory/domainobject/system/system.factory.ts b/apps/server/src/shared/testing/factory/domainobject/system/system.factory.ts new file mode 100644 index 00000000000..35e1c3f438c --- /dev/null +++ b/apps/server/src/shared/testing/factory/domainobject/system/system.factory.ts @@ -0,0 +1,10 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { System, SystemProps } from '@modules/system/domain'; +import { DomainObjectFactory } from '../domain-object.factory'; + +export const systemFactory = DomainObjectFactory.define(System, () => { + return { + id: new ObjectId().toHexString(), + type: 'oauth2', + }; +}); diff --git a/apps/server/src/shared/testing/factory/group-entity.factory.ts b/apps/server/src/shared/testing/factory/group-entity.factory.ts index 482aca971cf..1e65d13d808 100644 --- a/apps/server/src/shared/testing/factory/group-entity.factory.ts +++ b/apps/server/src/shared/testing/factory/group-entity.factory.ts @@ -1,9 +1,9 @@ -import { ExternalSourceEntity, RoleName } from '@shared/domain'; import { GroupEntity, GroupEntityProps, GroupEntityTypes, GroupValidPeriodEntity } from '@modules/group/entity'; +import { ExternalSourceEntity, RoleName } from '@shared/domain'; import { BaseFactory } from './base.factory'; import { roleFactory } from './role.factory'; import { schoolFactory } from './school.factory'; -import { systemFactory } from './system.factory'; +import { systemEntityFactory } from './systemEntityFactory'; import { userFactory } from './user.factory'; export const groupEntityFactory = BaseFactory.define(GroupEntity, ({ sequence }) => { @@ -27,7 +27,7 @@ export const groupEntityFactory = BaseFactory.define { matched(matchedBy: MatchCreator, user: User): this { @@ -16,7 +15,7 @@ class ImportUserFactory extends BaseFactory { export const importUserFactory = ImportUserFactory.define(ImportUser, ({ sequence }) => { return { school: schoolFactory.build(), - system: systemFactory.build(), + system: systemEntityFactory.build(), ldapDn: `uid=john${sequence},cn=schueler,cn=users,ou=1,dc=training,dc=ucs`, // eslint-disable-next-line @typescript-eslint/no-unsafe-call externalId: uuidv4() as unknown as string, diff --git a/apps/server/src/shared/testing/factory/index.ts b/apps/server/src/shared/testing/factory/index.ts index 54fac672098..f4d5e0050ce 100644 --- a/apps/server/src/shared/testing/factory/index.ts +++ b/apps/server/src/shared/testing/factory/index.ts @@ -29,7 +29,7 @@ export * from './schoolyear.factory'; export * from './share-token.do.factory'; export * from './storageprovider.factory'; export * from './submission.factory'; -export * from './system.factory'; +export * from './systemEntityFactory'; export * from './task.factory'; export * from './team.factory'; export * from './teamuser.factory'; diff --git a/apps/server/src/shared/testing/factory/system.factory.ts b/apps/server/src/shared/testing/factory/systemEntityFactory.ts similarity index 58% rename from apps/server/src/shared/testing/factory/system.factory.ts rename to apps/server/src/shared/testing/factory/systemEntityFactory.ts index f686c406851..567645210af 100644 --- a/apps/server/src/shared/testing/factory/system.factory.ts +++ b/apps/server/src/shared/testing/factory/systemEntityFactory.ts @@ -1,33 +1,33 @@ -import { LdapConfig, OauthConfig, OidcConfig, SystemEntity, SystemProperties } from '@shared/domain'; +import { LdapConfigEntity, OauthConfigEntity, OidcConfigEntity, SystemEntity, SystemEntityProps } from '@shared/domain'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { DeepPartial } from 'fishery'; import { BaseFactory } from './base.factory'; -export class SystemFactory extends BaseFactory { +export class SystemEntityFactory extends BaseFactory { withOauthConfig(): this { - const params: DeepPartial = { - oauthConfig: new OauthConfig({ + const params: DeepPartial = { + oauthConfig: new OauthConfigEntity({ clientId: '12345', clientSecret: 'mocksecret', idpHint: 'mock-oauth-idpHint', - tokenEndpoint: 'http://mock.de/mock/auth/public/mockToken', + tokenEndpoint: 'https://mock.de/mock/auth/public/mockToken', grantType: 'authorization_code', - redirectUri: 'http://mockhost:3030/api/v3/sso/oauth/', + redirectUri: 'https://mockhost:3030/api/v3/sso/oauth/', scope: 'openid uuid', responseType: 'code', - authEndpoint: 'http://mock.de/auth', + authEndpoint: 'https://mock.de/auth', provider: 'mock_type', - logoutEndpoint: 'http://mock.de/logout', + logoutEndpoint: 'https://mock.de/logout', issuer: 'mock_issuer', - jwksEndpoint: 'http://mock.de/jwks', + jwksEndpoint: 'https://mock.de/jwks', }), }; return this.params(params); } - withLdapConfig(otherParams?: DeepPartial): this { - const params: DeepPartial = { - ldapConfig: new LdapConfig({ + withLdapConfig(otherParams?: DeepPartial): this { + const params: DeepPartial = { + ldapConfig: new LdapConfigEntity({ url: 'ldaps:mock.de:389', active: true, ...otherParams, @@ -39,7 +39,7 @@ export class SystemFactory extends BaseFactory { withOidcConfig(): this { const params = { - oidcConfig: new OidcConfig({ + oidcConfig: new OidcConfigEntity({ clientId: 'mock-client-id', clientSecret: 'mock-client-secret', idpHint: 'mock-oidc-idpHint', @@ -54,10 +54,10 @@ export class SystemFactory extends BaseFactory { } } -export const systemFactory = SystemFactory.define(SystemEntity, ({ sequence }) => { +export const systemEntityFactory = SystemEntityFactory.define(SystemEntity, ({ sequence }) => { return { type: 'oauth', - url: 'http://mock.de', + url: 'https://mock.de', alias: `system #${sequence}`, displayName: `system #${sequence}DisplayName`, provisioningStrategy: SystemProvisioningStrategy.OIDC, diff --git a/apps/server/src/shared/testing/factory/user-login-migration.factory.ts b/apps/server/src/shared/testing/factory/user-login-migration.factory.ts index a3759a853ca..a3feaedbd27 100644 --- a/apps/server/src/shared/testing/factory/user-login-migration.factory.ts +++ b/apps/server/src/shared/testing/factory/user-login-migration.factory.ts @@ -1,7 +1,7 @@ import { IUserLoginMigration, UserLoginMigrationEntity } from '../../domain/entity/user-login-migration.entity'; import { BaseFactory } from './base.factory'; import { schoolFactory } from './school.factory'; -import { systemFactory } from './system.factory'; +import { systemEntityFactory } from './systemEntityFactory'; export const userLoginMigrationFactory = BaseFactory.define( UserLoginMigrationEntity, @@ -9,7 +9,7 @@ export const userLoginMigrationFactory = BaseFactory.define { const currentSystem = await context.app.service('systems').get(context.id); - if (currentSystem.type === 'ldap' && currentSystem.ldapConfig && currentSystem.ldapConfig.provider === 'iserv-idm') { + + if (!context.app.service('nest-system-rule').canEdit(currentSystem)) { throw new Forbidden('Not allowed to change this system'); } diff --git a/src/services/system/model.js b/src/services/system/model.js index 9491b3cdbe2..1a4650348a4 100644 --- a/src/services/system/model.js +++ b/src/services/system/model.js @@ -18,6 +18,7 @@ const types = [ 'iserv', // SSO providers 'ldap', // general and provider-specific LDAP 'oidc', + 'oauth', tspBaseType, tspSchoolType, // Thüringer Schul-Portal ]; diff --git a/test/services/system/index.test.js b/test/services/system/index.test.js index 95e84bf15c3..985e5886851 100644 --- a/test/services/system/index.test.js +++ b/test/services/system/index.test.js @@ -132,7 +132,7 @@ describe('systemId service', () => { it('CREATE fails without the right permissions', async () => { const usersSchool = await testObjects.createTestSchool(); - const data = { type: 'ldap' }; + const data = { type: 'ldap', ldapConfig: { provider: 'general' } }; const user = await testObjects.createTestUser({ roles: ['student'], schoolId: [usersSchool._id] }); const params = await testObjects.generateRequestParamsFromUser(user); @@ -154,6 +154,7 @@ describe('systemId service', () => { type: 'ldap', ldapConfig: { searchUserPassword: 'somePassword', + provider: 'general', }, }; @@ -167,7 +168,7 @@ describe('systemId service', () => { it('CREATE system is added to the users school after its creation', async () => { const usersSchool = await testObjects.createTestSchool(); - const data = { type: 'ldap' }; + const data = { type: 'ldap', ldapConfig: { provider: 'general' } }; const user = await testObjects.createTestUser({ roles: ['administrator'], schoolId: [usersSchool._id] }); const params = await testObjects.generateRequestParamsFromUser(user); @@ -186,6 +187,7 @@ describe('systemId service', () => { type: 'ldap', ldapConfig: { searchUserPassword: 'somePassword', + provider: 'general', }, }); const usersSchool = await testObjects.createTestSchool({ systems: [usersSystem._id] }); @@ -195,6 +197,7 @@ describe('systemId service', () => { url: 'http://someurl.com', ldapConfig: { searchUserPassword: 'somePassword', + provider: 'general', }, }; @@ -215,6 +218,7 @@ describe('systemId service', () => { type: 'ldap', ldapConfig: { searchUserPassword: 'somePassword', + provider: 'general', }, }); const usersSchool = await testObjects.createTestSchool(); @@ -225,6 +229,7 @@ describe('systemId service', () => { url: 'http://someurl.com', ldapConfig: { searchUserPassword: 'somePassword', + provider: 'general', }, }; @@ -245,6 +250,7 @@ describe('systemId service', () => { type: 'ldap', ldapConfig: { searchUserPassword: 'somePassword', + provider: 'general', }, }); const usersSchool = await testObjects.createTestSchool({ systems: [usersSystem._id] }); @@ -254,6 +260,7 @@ describe('systemId service', () => { url: 'http://someurl.com', ldapConfig: { searchUserPassword: 'somePassword', + provider: 'general', }, }; @@ -263,7 +270,7 @@ describe('systemId service', () => { const result = await app.service('systems').update(usersSystem._id, data, params); expect(result.ldapConfig.searchUserPassword).to.be.undefined; }); - it('UPDATE iServ configuration should not be editable', async () => { + it('UPDATE global configuration should not be editable', async () => { const usersSystem = await testObjects.createTestSystem({ type: 'ldap', ldapConfig: { @@ -302,6 +309,7 @@ describe('systemId service', () => { type: 'ldap', ldapConfig: { searchUserPassword: 'somePassword', + provider: 'general', }, }); const usersSchool = await testObjects.createTestSchool({ systems: [usersSystem._id] }); @@ -328,6 +336,7 @@ describe('systemId service', () => { type: 'ldap', ldapConfig: { searchUserPassword: 'somePassword', + provider: 'general', }, }); const usersSchool = await testObjects.createTestSchool(); @@ -354,6 +363,7 @@ describe('systemId service', () => { type: 'ldap', ldapConfig: { searchUserPassword: 'somePassword', + provider: 'general', }, }); const usersSchool = await testObjects.createTestSchool({ systems: [usersSystem._id] }); @@ -369,7 +379,7 @@ describe('systemId service', () => { expect(result.ldapConfig.searchUserPassword).to.be.undefined; }); - it('PATCH iServ configuration should not be editable', async () => { + it('PATCH global configuration should not be editable', async () => { const usersSystem = await testObjects.createTestSystem({ type: 'ldap', ldapConfig: { @@ -403,6 +413,7 @@ describe('systemId service', () => { type: 'ldap', ldapConfig: { searchUserPassword: 'somePassword', + provider: 'general', }, }); const usersSchool = await testObjects.createTestSchool({ systems: [usersSystem._id] }); @@ -425,6 +436,7 @@ describe('systemId service', () => { type: 'ldap', ldapConfig: { searchUserPassword: 'somePassword', + provider: 'general', }, }); const usersSchool = await testObjects.createTestSchool(); @@ -447,6 +459,7 @@ describe('systemId service', () => { type: 'ldap', ldapConfig: { searchUserPassword: 'somePassword', + provider: 'general', }, }); const usersSchool = await testObjects.createTestSchool({ systems: [usersSystem._id] }); @@ -461,6 +474,9 @@ describe('systemId service', () => { it('REMOVE system is removed from the school after its removal', async () => { const usersSystem = await testObjects.createTestSystem({ type: 'ldap', + ldapConfig: { + provider: 'general', + }, }); const usersSchool = await testObjects.createTestSchool({ systems: [usersSystem._id] }); @@ -477,6 +493,9 @@ describe('systemId service', () => { it('REMOVE should remove ldapschoolidentifier from school if ldap system is removed', async () => { const usersSystem = await testObjects.createTestSystem({ type: 'ldap', + ldapConfig: { + provider: 'general', + }, }); const usersSchool = await testObjects.createTestSchool({ systems: [usersSystem._id], @@ -496,6 +515,9 @@ describe('systemId service', () => { it('REMOVE should remove ldapLastSync from school if ldap system is removed', async () => { const usersSystem = await testObjects.createTestSystem({ type: 'ldap', + ldapConfig: { + provider: 'general', + }, }); const usersSchool = await testObjects.createTestSchool({ systems: [usersSystem._id], @@ -512,7 +534,7 @@ describe('systemId service', () => { expect(usersSchoolUpdated.ldapLastSync).to.be.undefined; }); - it('REMOVE iServ configuration should not be removable', async () => { + it('REMOVE global configuration should not be removable', async () => { const usersSystem = await testObjects.createTestSystem({ type: 'ldap', ldapConfig: { @@ -533,5 +555,24 @@ describe('systemId service', () => { expect(err.message).to.equal('Not allowed to change this system'); } }); + + it('REMOVE non-ldap configuration should not be removable', async () => { + const usersSystem = await testObjects.createTestSystem({ + type: 'oauth', + }); + const usersSchool = await testObjects.createTestSchool({ systems: [usersSystem._id] }); + + const user = await testObjects.createTestUser({ roles: ['administrator'], schoolId: [usersSchool._id] }); + const params = await testObjects.generateRequestParamsFromUser(user); + + try { + await app.service('systems').remove(usersSystem._id, params); + throw new Error('should have failed'); + } catch (err) { + expect(err.message).to.not.equal('should have failed'); + expect(err.code).to.equal(403); + expect(err.message).to.equal('Not allowed to change this system'); + } + }); }); }); diff --git a/test/utils/setup.nest.services.js b/test/utils/setup.nest.services.js index 4de3bce181f..5c56311df27 100644 --- a/test/utils/setup.nest.services.js +++ b/test/utils/setup.nest.services.js @@ -15,6 +15,8 @@ const { DB_PASSWORD, DB_URL, DB_USERNAME } = require('../../dist/apps/server/con const { ALL_ENTITIES } = require('../../dist/apps/server/shared/domain/entity/all-entities'); const { TeamService } = require('../../dist/apps/server/modules/teams/service/team.service'); const { TeamsApiModule } = require('../../dist/apps/server/modules/teams/teams-api.module'); +const { AuthorizationModule } = require('../../dist/apps/server/modules/authorization'); +const { SystemRule } = require('../../dist/apps/server/modules/authorization'); const setupNestServices = async (app) => { const module = await Test.createTestingModule({ @@ -31,6 +33,7 @@ const setupNestServices = async (app) => { ConfigModule.forRoot({ ignoreEnvFile: true, ignoreEnvVars: true, isGlobal: true }), AccountApiModule, TeamsApiModule, + AuthorizationModule, ], }).compile(); const nestApp = await module.createNestApplication().init(); @@ -39,11 +42,13 @@ const setupNestServices = async (app) => { const accountService = nestApp.get(AccountService); const accountValidationService = nestApp.get(AccountValidationService); const teamService = nestApp.get(TeamService); + const systemRule = nestApp.get(SystemRule); app.services['nest-account-uc'] = accountUc; app.services['nest-account-service'] = accountService; app.services['nest-account-validation-service'] = accountValidationService; app.services['nest-team-service'] = teamService; + app.services['nest-system-rule'] = systemRule; app.services['nest-orm'] = orm; return { nestApp, orm, accountUc, accountService };