diff --git a/apps/server/src/infra/schulconnex-client/response/schulconnex-gruppe-response.ts b/apps/server/src/infra/schulconnex-client/response/schulconnex-gruppe-response.ts index 2533d306743..82eb6abc73a 100644 --- a/apps/server/src/infra/schulconnex-client/response/schulconnex-gruppe-response.ts +++ b/apps/server/src/infra/schulconnex-client/response/schulconnex-gruppe-response.ts @@ -1,6 +1,5 @@ import { Type } from 'class-transformer'; -import { IsEnum, IsObject, IsOptional, IsString, ValidateNested } from 'class-validator'; -import { SchulconnexGroupType } from './schulconnex-group-type'; +import { IsObject, IsOptional, IsString, ValidateNested } from 'class-validator'; import { SchulconnexLaufzeitResponse } from './schulconnex-laufzeit-response'; export class SchulconnexGruppeResponse { @@ -10,8 +9,8 @@ export class SchulconnexGruppeResponse { @IsString() bezeichnung!: string; - @IsEnum(SchulconnexGroupType) - typ!: SchulconnexGroupType; + @IsString() + typ!: string; @IsOptional() @IsObject() diff --git a/apps/server/src/infra/schulconnex-client/response/schulconnex-personenkontext-response.ts b/apps/server/src/infra/schulconnex-client/response/schulconnex-personenkontext-response.ts index f127688af59..0e6c4474c39 100644 --- a/apps/server/src/infra/schulconnex-client/response/schulconnex-personenkontext-response.ts +++ b/apps/server/src/infra/schulconnex-client/response/schulconnex-personenkontext-response.ts @@ -1,17 +1,16 @@ import { Type } from 'class-transformer'; -import { IsArray, IsEnum, IsObject, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { IsArray, IsObject, IsOptional, IsString, ValidateNested } from 'class-validator'; import { SchulconnexErreichbarkeitenResponse } from './schulconnex-erreichbarkeiten-response'; import { SchulconnexGruppenResponse } from './schulconnex-gruppen-response'; import { SchulconnexOrganisationResponse } from './schulconnex-organisation-response'; import { SchulconnexResponseValidationGroups } from './schulconnex-response-validation-groups'; -import { SchulconnexRole } from './schulconnex-role'; export class SchulconnexPersonenkontextResponse { @IsString({ groups: [SchulconnexResponseValidationGroups.USER, SchulconnexResponseValidationGroups.GROUPS] }) id!: string; - @IsEnum(SchulconnexRole, { groups: [SchulconnexResponseValidationGroups.USER] }) - rolle!: SchulconnexRole; + @IsString({ groups: [SchulconnexResponseValidationGroups.USER] }) + rolle!: string; @IsObject({ groups: [SchulconnexResponseValidationGroups.SCHOOL] }) @ValidateNested({ groups: [SchulconnexResponseValidationGroups.SCHOOL] }) diff --git a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts index 31904871123..dcadb75014f 100644 --- a/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts +++ b/apps/server/src/infra/sync/tsp/tsp-oauth-data.mapper.ts @@ -34,7 +34,7 @@ export class TspOauthDataMapper { }); const externalSchools = new Map(); - const externalClasses = new Map(); + const externalClasses = new Map(); const teacherForClasses = new Map>(); const oauthDataDtos: OauthDataDto[] = []; @@ -85,9 +85,9 @@ export class TspOauthDataMapper { }); const classIds = teacherForClasses.get(tspTeacher.lehrerUid) ?? []; - const classes = classIds + const classes: ExternalClassDto[] = classIds .map((classId) => externalClasses.get(classId)) - .filter((externalClass) => !!externalClass); + .filter((externalClass: ExternalClassDto | undefined): externalClass is ExternalClassDto => !!externalClass); const externalSchool = tspTeacher.schuleNummer == null ? undefined : externalSchools.get(tspTeacher.schuleNummer); diff --git a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts b/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts index b4d8a3b8a52..b4303779eef 100644 --- a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts @@ -22,8 +22,8 @@ import { schoolFactory } from '@src/modules/school/testing'; import { System } from '@src/modules/system'; import { systemFactory } from '@src/modules/system/testing'; import { SyncStrategyTarget } from '../sync-strategy.types'; -import { TspLegacyMigrationService } from './tsp-legacy-migration.service'; import { TspFetchService } from './tsp-fetch.service'; +import { TspLegacyMigrationService } from './tsp-legacy-migration.service'; import { TspOauthDataMapper } from './tsp-oauth-data.mapper'; import { TspSyncConfig } from './tsp-sync.config'; import { TspSyncService } from './tsp-sync.service'; @@ -171,6 +171,7 @@ describe(TspSyncStrategy.name, () => { }), externalUser: new ExternalUserDto({ externalId: faker.string.alpha(), + roles: [], }), }); const tspTeacher: RobjExportLehrerMigration = { 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 c850532d7ea..1a1101c2dca 100644 --- a/apps/server/src/modules/oauth/service/oauth.service.spec.ts +++ b/apps/server/src/modules/oauth/service/oauth.service.spec.ts @@ -18,6 +18,7 @@ import { LegacyLogger } from '@src/core/logger'; import { OauthDataDto } from '@src/modules/provisioning/dto'; import { System } from '@src/modules/system'; import jwt, { JwtPayload } from 'jsonwebtoken'; +import { externalUserDtoFactory } from '../../provisioning/testing'; import { OAuthTokenDto } from '../interface'; import { OauthConfigMissingLoggableException, @@ -378,9 +379,9 @@ describe('OAuthService', () => { provisioningStrategy: SystemProvisioningStrategy.SANIS, provisioningUrl: 'https://mock.person-info.de/', }, - externalUser: { + externalUser: externalUserDtoFactory.build({ externalId: externalUserId, - }, + }), externalSchool: { externalId: 'externalSchoolId', name: 'External School', @@ -429,9 +430,9 @@ describe('OAuthService', () => { provisioningStrategy: SystemProvisioningStrategy.SANIS, provisioningUrl: 'https://mock.person-info.de/', }, - externalUser: { + externalUser: externalUserDtoFactory.build({ externalId: externalUserId, - }, + }), externalSchool: { externalId: 'externalSchoolId', name: 'External School', @@ -476,9 +477,9 @@ describe('OAuthService', () => { provisioningStrategy: SystemProvisioningStrategy.SANIS, provisioningUrl: 'https://mock.person-info.de/', }, - externalUser: { + externalUser: externalUserDtoFactory.build({ externalId: externalUserId, - }, + }), externalSchool: { externalId: 'externalSchoolId', name: 'External School', @@ -544,9 +545,9 @@ describe('OAuthService', () => { provisioningStrategy: SystemProvisioningStrategy.SANIS, provisioningUrl: 'https://mock.person-info.de/', }, - externalUser: { + externalUser: externalUserDtoFactory.build({ externalId: externalUserId, - }, + }), externalSchool: { externalId: externalSchoolId, name: school.name, @@ -612,9 +613,9 @@ describe('OAuthService', () => { provisioningStrategy: SystemProvisioningStrategy.SANIS, provisioningUrl: 'https://mock.person-info.de/', }, - externalUser: { + externalUser: externalUserDtoFactory.build({ externalId: externalUserId, - }, + }), externalSchool: { externalId: externalSchoolId, name: school.name, @@ -675,9 +676,9 @@ describe('OAuthService', () => { provisioningStrategy: SystemProvisioningStrategy.SANIS, provisioningUrl: 'https://mock.person-info.de/', }, - externalUser: { + externalUser: externalUserDtoFactory.build({ externalId: externalUserId, - }, + }), externalSchool: { externalId: externalSchoolId, name: school.name, @@ -737,9 +738,9 @@ describe('OAuthService', () => { provisioningStrategy: SystemProvisioningStrategy.SANIS, provisioningUrl: 'https://mock.person-info.de/', }, - externalUser: { + externalUser: externalUserDtoFactory.build({ externalId: externalUserId, - }, + }), externalSchool: { externalId: externalSchoolId, name: school.name, @@ -804,9 +805,9 @@ describe('OAuthService', () => { provisioningStrategy: SystemProvisioningStrategy.SANIS, provisioningUrl: 'https://mock.person-info.de/', }, - externalUser: { + externalUser: externalUserDtoFactory.build({ externalId: externalUserId, - }, + }), externalSchool: { externalId: externalSchoolId, name: school.name, diff --git a/apps/server/src/modules/provisioning/dto/external-user.dto.ts b/apps/server/src/modules/provisioning/dto/external-user.dto.ts index bc5bcc9c80b..013e8e370fa 100644 --- a/apps/server/src/modules/provisioning/dto/external-user.dto.ts +++ b/apps/server/src/modules/provisioning/dto/external-user.dto.ts @@ -1,17 +1,17 @@ import { RoleName } from '@shared/domain/interface'; export class ExternalUserDto { - externalId: string; + public externalId: string; - firstName?: string; + public firstName?: string; - lastName?: string; + public lastName?: string; - email?: string; + public email?: string; - roles?: RoleName[]; + public roles: RoleName[]; - birthday?: Date; + public birthday?: Date; constructor(props: ExternalUserDto) { this.externalId = props.externalId; diff --git a/apps/server/src/modules/provisioning/loggable/fetching-policies-info-failed.loggable.spec.ts b/apps/server/src/modules/provisioning/loggable/fetching-policies-info-failed.loggable.spec.ts index 60818f61582..d3b188c975c 100644 --- a/apps/server/src/modules/provisioning/loggable/fetching-policies-info-failed.loggable.spec.ts +++ b/apps/server/src/modules/provisioning/loggable/fetching-policies-info-failed.loggable.spec.ts @@ -1,12 +1,10 @@ -import { ExternalUserDto } from '../dto'; +import { externalUserDtoFactory } from '../testing'; import { FetchingPoliciesInfoFailedLoggable } from './fetching-policies-info-failed.loggable'; describe(FetchingPoliciesInfoFailedLoggable.name, () => { describe('getLogMessage', () => { const setup = () => { - const externalUserDto: ExternalUserDto = { - externalId: 'someId', - }; + const externalUserDto = externalUserDtoFactory.build(); const policiesInfoEndpoint = 'someEndpoint'; const loggable = new FetchingPoliciesInfoFailedLoggable(externalUserDto, policiesInfoEndpoint); diff --git a/apps/server/src/modules/provisioning/loggable/index.ts b/apps/server/src/modules/provisioning/loggable/index.ts index 00068126737..01e7c2ae5cd 100644 --- a/apps/server/src/modules/provisioning/loggable/index.ts +++ b/apps/server/src/modules/provisioning/loggable/index.ts @@ -6,3 +6,5 @@ export * from './group-role-unknown.loggable'; export { SchoolExternalToolCreatedLoggable } from './school-external-tool-created.loggable'; export { FetchingPoliciesInfoFailedLoggable } from './fetching-policies-info-failed.loggable'; export { PoliciesInfoErrorResponseLoggable } from './policies-info-error-response-loggable'; +export { UserRoleUnknownLoggableException } from './user-role-unknown.loggable-exception'; +export { SchoolMissingLoggableException } from './school-missing.loggable-exception'; diff --git a/apps/server/src/modules/provisioning/loggable/school-missing.loggable-exception.spec.ts b/apps/server/src/modules/provisioning/loggable/school-missing.loggable-exception.spec.ts new file mode 100644 index 00000000000..45d19533798 --- /dev/null +++ b/apps/server/src/modules/provisioning/loggable/school-missing.loggable-exception.spec.ts @@ -0,0 +1,32 @@ +import { externalUserDtoFactory } from '../testing'; +import { SchoolMissingLoggableException } from './school-missing.loggable-exception'; + +describe(SchoolMissingLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const externalUser = externalUserDtoFactory.build(); + + const loggable = new SchoolMissingLoggableException(externalUser); + + return { + loggable, + externalUser, + }; + }; + + it('should return a loggable message', () => { + const { loggable, externalUser } = setup(); + + const message = loggable.getLogMessage(); + + expect(message).toEqual({ + type: 'SCHOOL_MISSING', + stack: expect.any(String), + message: 'Unable to create new external user without a school', + data: { + externalUserId: externalUser.externalId, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/provisioning/loggable/school-missing.loggable-exception.ts b/apps/server/src/modules/provisioning/loggable/school-missing.loggable-exception.ts new file mode 100644 index 00000000000..54727ba8f34 --- /dev/null +++ b/apps/server/src/modules/provisioning/loggable/school-missing.loggable-exception.ts @@ -0,0 +1,28 @@ +import { HttpStatus } from '@nestjs/common'; +import { BusinessError } from '@shared/common'; +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; +import { ExternalUserDto } from '../dto'; + +export class SchoolMissingLoggableException extends BusinessError implements Loggable { + constructor(private readonly externalUser: ExternalUserDto) { + super( + { + type: 'SCHOOL_MISSING', + title: 'Invalid school data', + defaultMessage: 'Unable to create new external user without a school', + }, + HttpStatus.UNPROCESSABLE_ENTITY + ); + } + + public getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + type: this.type, + message: this.message, + stack: this.stack, + data: { + externalUserId: this.externalUser.externalId, + }, + }; + } +} diff --git a/apps/server/src/modules/provisioning/loggable/user-role-unknown.loggable-exception.spec.ts b/apps/server/src/modules/provisioning/loggable/user-role-unknown.loggable-exception.spec.ts new file mode 100644 index 00000000000..c63fb42930b --- /dev/null +++ b/apps/server/src/modules/provisioning/loggable/user-role-unknown.loggable-exception.spec.ts @@ -0,0 +1,32 @@ +import { externalUserDtoFactory } from '../testing'; +import { UserRoleUnknownLoggableException } from './user-role-unknown.loggable-exception'; + +describe(UserRoleUnknownLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const externalUser = externalUserDtoFactory.build(); + + const loggable = new UserRoleUnknownLoggableException(externalUser); + + return { + loggable, + externalUser, + }; + }; + + it('should return a loggable message', () => { + const { loggable, externalUser } = setup(); + + const message = loggable.getLogMessage(); + + expect(message).toEqual({ + type: 'EXTERNAL_USER_ROLE_UNKNOWN', + stack: expect.any(String), + message: 'External user has no or no known role assigned to them', + data: { + externalUserId: externalUser.externalId, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/provisioning/loggable/user-role-unknown.loggable-exception.ts b/apps/server/src/modules/provisioning/loggable/user-role-unknown.loggable-exception.ts new file mode 100644 index 00000000000..a17ab899708 --- /dev/null +++ b/apps/server/src/modules/provisioning/loggable/user-role-unknown.loggable-exception.ts @@ -0,0 +1,28 @@ +import { HttpStatus } from '@nestjs/common'; +import { BusinessError } from '@shared/common'; +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; +import { ExternalUserDto } from '../dto'; + +export class UserRoleUnknownLoggableException extends BusinessError implements Loggable { + constructor(private readonly externalUser: ExternalUserDto) { + super( + { + type: 'EXTERNAL_USER_ROLE_UNKNOWN', + title: 'Invalid user role', + defaultMessage: 'External user has no or no known role assigned to them', + }, + HttpStatus.UNPROCESSABLE_ENTITY + ); + } + + public getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + type: this.type, + message: this.message, + stack: this.stack, + data: { + externalUserId: this.externalUser.externalId, + }, + }; + } +} diff --git a/apps/server/src/modules/provisioning/provisioning.module.ts b/apps/server/src/modules/provisioning/provisioning.module.ts index 6474efc61ce..b6e468180d4 100644 --- a/apps/server/src/modules/provisioning/provisioning.module.ts +++ b/apps/server/src/modules/provisioning/provisioning.module.ts @@ -1,4 +1,5 @@ import { AccountModule } from '@modules/account'; +import { ClassModule } from '@modules/class'; import { GroupModule } from '@modules/group'; import { LearnroomModule } from '@modules/learnroom'; import { LegacySchoolModule } from '@modules/legacy-school'; @@ -8,11 +9,10 @@ import { SystemModule } from '@modules/system/system.module'; import { ExternalToolModule } from '@modules/tool'; import { SchoolExternalToolModule } from '@modules/tool/school-external-tool'; import { UserModule } from '@modules/user'; +import { UserLicenseModule } from '@modules/user-license'; import { Module } from '@nestjs/common'; import { LoggerModule } from '@src/core/logger'; import { SchulconnexClientModule } from '@src/infra/schulconnex-client/schulconnex-client.module'; -import { ClassModule } from '../class'; -import { UserLicenseModule } from '../user-license'; import { ProvisioningService } from './service/provisioning.service'; import { TspProvisioningService } from './service/tsp-provisioning.service'; import { @@ -28,8 +28,8 @@ import { SchulconnexSchoolProvisioningService, SchulconnexToolProvisioningService, SchulconnexUserProvisioningService, -} from './strategy/oidc/service'; -import { TspProvisioningStrategy } from './strategy/tsp/tsp.strategy'; +} from './strategy/schulconnex/service'; +import { TspProvisioningStrategy } from './strategy/tsp'; @Module({ imports: [ 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 37790f6cadd..eae04147cd7 100644 --- a/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts @@ -4,16 +4,11 @@ import { systemFactory } from '@modules/system/testing'; import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { - ExternalUserDto, - OauthDataDto, - OauthDataStrategyInputDto, - ProvisioningDto, - ProvisioningSystemDto, -} from '../dto'; +import { OauthDataDto, OauthDataStrategyInputDto, ProvisioningDto, ProvisioningSystemDto } from '../dto'; import { IservProvisioningStrategy, OidcMockProvisioningStrategy, SanisProvisioningStrategy } from '../strategy'; -import { ProvisioningService } from './provisioning.service'; import { TspProvisioningStrategy } from '../strategy/tsp/tsp.strategy'; +import { externalUserDtoFactory } from '../testing'; +import { ProvisioningService } from './provisioning.service'; describe('ProvisioningService', () => { let module: TestingModule; @@ -88,14 +83,13 @@ describe('ProvisioningService', () => { provisioningUrl: 'https://api.moin.schule/', provisioningStrategy: SystemProvisioningStrategy.SANIS, }); + const externalUser = externalUserDtoFactory.build(); const oauthDataDto: OauthDataDto = new OauthDataDto({ system: provisioningSystemDto, - externalUser: new ExternalUserDto({ - externalId: 'externalUserId', - }), + externalUser, }); const provisioningDto: ProvisioningDto = new ProvisioningDto({ - externalUserId: 'externalUserId', + externalUserId: externalUser.externalId, }); return { diff --git a/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts b/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts index da637580400..b42ff993b92 100644 --- a/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts @@ -41,7 +41,7 @@ describe('TspProvisioningService', () => { return new ExternalClassDto({ ...baseProps, ...props }); }; const setupExternalUser = (props?: Partial) => { - const baseProps = { externalId: faker.string.uuid(), username: faker.internet.userName() }; + const baseProps = { externalId: faker.string.uuid(), username: faker.internet.userName(), roles: [] }; return new ExternalUserDto({ ...baseProps, ...props }); }; diff --git a/apps/server/src/modules/provisioning/strategy/index.ts b/apps/server/src/modules/provisioning/strategy/index.ts index 632463a04fa..8406f9de7ac 100644 --- a/apps/server/src/modules/provisioning/strategy/index.ts +++ b/apps/server/src/modules/provisioning/strategy/index.ts @@ -1,5 +1,4 @@ export * from './base.strategy'; export * from './iserv/iserv.strategy'; -export * from './oidc'; +export * from './schulconnex'; export * from './oidc-mock/oidc-mock.strategy'; -export * from './sanis'; diff --git a/apps/server/src/modules/provisioning/strategy/iserv/iserv.strategy.spec.ts b/apps/server/src/modules/provisioning/strategy/iserv/iserv.strategy.spec.ts index 3e2d78b75a2..181a5b6971f 100644 --- a/apps/server/src/modules/provisioning/strategy/iserv/iserv.strategy.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/iserv/iserv.strategy.spec.ts @@ -183,7 +183,7 @@ describe('IservProvisioningStrategy', () => { systemId: 'systemId', provisioningStrategy: SystemProvisioningStrategy.ISERV, }), - externalUser: new ExternalUserDto({ externalId: userUUID }), + externalUser: new ExternalUserDto({ externalId: userUUID, roles: [] }), }); const result: ProvisioningDto = await strategy.apply(data); diff --git a/apps/server/src/modules/provisioning/strategy/oidc-mock/oidc-mock.strategy.spec.ts b/apps/server/src/modules/provisioning/strategy/oidc-mock/oidc-mock.strategy.spec.ts index 5c0c8901077..24d0c6b494d 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc-mock/oidc-mock.strategy.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc-mock/oidc-mock.strategy.spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import jwt from 'jsonwebtoken'; import { IdTokenExtractionFailureLoggableException } from '@src/modules/oauth/loggable'; +import jwt from 'jsonwebtoken'; import { ExternalUserDto, OauthDataDto, @@ -73,7 +73,7 @@ describe('OidcMockProvisioningStrategy', () => { expect(result).toEqual({ system: input.system, - externalUser: new ExternalUserDto({ externalId: userName }), + externalUser: new ExternalUserDto({ externalId: userName, roles: [] }), }); }); @@ -106,7 +106,7 @@ describe('OidcMockProvisioningStrategy', () => { systemId: 'systemId', provisioningStrategy: SystemProvisioningStrategy.OIDC, }), - externalUser: new ExternalUserDto({ externalId: userName }), + externalUser: new ExternalUserDto({ externalId: userName, roles: [] }), }); const result: ProvisioningDto = await strategy.apply(data); diff --git a/apps/server/src/modules/provisioning/strategy/oidc-mock/oidc-mock.strategy.ts b/apps/server/src/modules/provisioning/strategy/oidc-mock/oidc-mock.strategy.ts index dd15672c3c9..0402e4628a4 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc-mock/oidc-mock.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc-mock/oidc-mock.strategy.ts @@ -1,7 +1,7 @@ +import { IdTokenExtractionFailureLoggableException } from '@modules/oauth/loggable'; import { Injectable } from '@nestjs/common'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import jwt, { JwtPayload } from 'jsonwebtoken'; -import { IdTokenExtractionFailureLoggableException } from '@modules/oauth/loggable'; import { ExternalUserDto, OauthDataDto, OauthDataStrategyInputDto, ProvisioningDto } from '../../dto'; import { ProvisioningStrategy } from '../base.strategy'; @@ -19,6 +19,7 @@ export class OidcMockProvisioningStrategy extends ProvisioningStrategy { const externalUser: ExternalUserDto = new ExternalUserDto({ externalId: idToken.external_sub, + roles: [], }); const oauthData: OauthDataDto = new OauthDataDto({ diff --git a/apps/server/src/modules/provisioning/strategy/oidc/index.ts b/apps/server/src/modules/provisioning/strategy/oidc/index.ts deleted file mode 100644 index a35eb285666..00000000000 --- a/apps/server/src/modules/provisioning/strategy/oidc/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { SchulconnexProvisioningStrategy } from './schulconnex.strategy'; diff --git a/apps/server/src/modules/provisioning/strategy/sanis/index.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/index.ts similarity index 64% rename from apps/server/src/modules/provisioning/strategy/sanis/index.ts rename to apps/server/src/modules/provisioning/strategy/schulconnex/index.ts index 03132b1fcd6..f8fc2ce3f82 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/index.ts +++ b/apps/server/src/modules/provisioning/strategy/schulconnex/index.ts @@ -1,2 +1,3 @@ +export { SchulconnexProvisioningStrategy } from './schulconnex.strategy'; export { SanisProvisioningStrategy } from './sanis.strategy'; export { SchulconnexResponseMapper } from './schulconnex-response-mapper'; diff --git a/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.spec.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/sanis.strategy.spec.ts similarity index 97% rename from apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.spec.ts rename to apps/server/src/modules/provisioning/strategy/schulconnex/sanis.strategy.spec.ts index cdbcf05532d..bbd5d7ee0d5 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/schulconnex/sanis.strategy.spec.ts @@ -34,6 +34,9 @@ import { } from '../../dto'; import { PoliciesInfoErrorResponseLoggable } from '../../loggable'; import { ProvisioningConfig } from '../../provisioning.config'; +import { externalUserDtoFactory } from '../../testing'; +import { SanisProvisioningStrategy } from './sanis.strategy'; +import { SchulconnexResponseMapper } from './schulconnex-response-mapper'; import { SchulconnexCourseSyncService, SchulconnexGroupProvisioningService, @@ -41,9 +44,7 @@ import { SchulconnexSchoolProvisioningService, SchulconnexToolProvisioningService, SchulconnexUserProvisioningService, -} from '../oidc/service'; -import { SanisProvisioningStrategy } from './sanis.strategy'; -import { SchulconnexResponseMapper } from './schulconnex-response-mapper'; +} from './service'; import ArgsType = jest.ArgsType; import SpyInstance = jest.SpyInstance; @@ -156,9 +157,7 @@ describe(SanisProvisioningStrategy.name, () => { accessToken: 'sanisAccessToken', }); const schulconnexResponse: SchulconnexResponse = setupSchulconnexResponse(); - const user: ExternalUserDto = new ExternalUserDto({ - externalId: 'externalUserId', - }); + const user: ExternalUserDto = externalUserDtoFactory.build(); const school: ExternalSchoolDto = new ExternalSchoolDto({ externalId: 'externalSchoolId', name: 'schoolName', @@ -280,9 +279,7 @@ describe(SanisProvisioningStrategy.name, () => { accessToken: 'sanisAccessToken', }); const schulconnexResponse: SchulconnexResponse = setupSchulconnexResponse(); - const user: ExternalUserDto = new ExternalUserDto({ - externalId: 'externalUserId', - }); + const user: ExternalUserDto = externalUserDtoFactory.build(); const school: ExternalSchoolDto = new ExternalSchoolDto({ externalId: 'externalSchoolId', name: 'schoolName', @@ -334,9 +331,7 @@ describe(SanisProvisioningStrategy.name, () => { accessToken: 'sanisAccessToken', }); const schulconnexResponse: SchulconnexResponse = setupSchulconnexResponse(); - const user: ExternalUserDto = new ExternalUserDto({ - externalId: 'externalUserId', - }); + const user: ExternalUserDto = externalUserDtoFactory.build(); const school: ExternalSchoolDto = new ExternalSchoolDto({ externalId: 'externalSchoolId', name: 'schoolName', @@ -385,9 +380,7 @@ describe(SanisProvisioningStrategy.name, () => { accessToken: 'sanisAccessToken', }); const schulconnexResponse: SchulconnexResponse = setupSchulconnexResponse(); - const user: ExternalUserDto = new ExternalUserDto({ - externalId: 'externalUserId', - }); + const user: ExternalUserDto = externalUserDtoFactory.build(); const school: ExternalSchoolDto = new ExternalSchoolDto({ externalId: 'externalSchoolId', name: 'schoolName', @@ -428,9 +421,7 @@ describe(SanisProvisioningStrategy.name, () => { accessToken: 'sanisAccessToken', }); const schulconnexResponse: SchulconnexResponse = setupSchulconnexResponse(); - const user: ExternalUserDto = new ExternalUserDto({ - externalId: 'externalUserId', - }); + const user: ExternalUserDto = externalUserDtoFactory.build(); const school: ExternalSchoolDto = new ExternalSchoolDto({ externalId: 'externalSchoolId', name: 'schoolName', @@ -535,9 +526,7 @@ describe(SanisProvisioningStrategy.name, () => { accessToken: 'sanisAccessToken', }); const schulconnexResponse: SchulconnexResponse = setupSchulconnexResponse(); - const user: ExternalUserDto = new ExternalUserDto({ - externalId: 'externalUserId', - }); + const user: ExternalUserDto = externalUserDtoFactory.build(); const school: ExternalSchoolDto = new ExternalSchoolDto({ externalId: 'externalSchoolId', name: 'schoolName', diff --git a/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/sanis.strategy.ts similarity index 96% rename from apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts rename to apps/server/src/modules/provisioning/strategy/schulconnex/sanis.strategy.ts index 590dd214240..bc57f6fee50 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/schulconnex/sanis.strategy.ts @@ -24,7 +24,8 @@ import { } from '../../dto'; import { FetchingPoliciesInfoFailedLoggable, PoliciesInfoErrorResponseLoggable } from '../../loggable'; import { ProvisioningConfig } from '../../provisioning.config'; -import { SchulconnexProvisioningStrategy } from '../oidc'; +import { SchulconnexResponseMapper } from './schulconnex-response-mapper'; +import { SchulconnexProvisioningStrategy } from './schulconnex.strategy'; import { SchulconnexCourseSyncService, SchulconnexGroupProvisioningService, @@ -32,8 +33,7 @@ import { SchulconnexSchoolProvisioningService, SchulconnexToolProvisioningService, SchulconnexUserProvisioningService, -} from '../oidc/service'; -import { SchulconnexResponseMapper } from './schulconnex-response-mapper'; +} from './service'; @Injectable() export class SanisProvisioningStrategy extends SchulconnexProvisioningStrategy { @@ -62,11 +62,11 @@ export class SanisProvisioningStrategy extends SchulconnexProvisioningStrategy { ); } - getType(): SystemProvisioningStrategy { + public getType(): SystemProvisioningStrategy { return SystemProvisioningStrategy.SANIS; } - override async getData(input: OauthDataStrategyInputDto): Promise { + public override async getData(input: OauthDataStrategyInputDto): Promise { if (!input.system.provisioningUrl) { throw new InternalServerErrorException( `Sanis system with id: ${input.system.systemId} is missing a provisioning url` diff --git a/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.spec.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex-response-mapper.spec.ts similarity index 100% rename from apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.spec.ts rename to apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex-response-mapper.spec.ts diff --git a/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex-response-mapper.ts similarity index 95% rename from apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.ts rename to apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex-response-mapper.ts index cd15c272342..4a7543cac70 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.ts +++ b/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex-response-mapper.ts @@ -27,7 +27,7 @@ import { import { GroupRoleUnknownLoggable } from '../../loggable'; import { ProvisioningConfig } from '../../provisioning.config'; -const RoleMapping: Record = { +const RoleMapping: Partial> = { [SchulconnexRole.LEHR]: RoleName.TEACHER, [SchulconnexRole.LERN]: RoleName.STUDENT, [SchulconnexRole.LEIT]: RoleName.ADMINISTRATOR, @@ -39,7 +39,7 @@ const GroupRoleMapping: Partial> [SchulconnexGroupRole.STUDENT]: RoleName.STUDENT, }; -const GroupTypeMapping: Partial> = { +const GroupTypeMapping: Partial> = { [SchulconnexGroupType.CLASS]: GroupTypes.CLASS, [SchulconnexGroupType.COURSE]: GroupTypes.COURSE, [SchulconnexGroupType.OTHER]: GroupTypes.OTHER, @@ -85,10 +85,12 @@ export class SchulconnexResponseMapper { email = emailContact?.kennung; } + const role: RoleName | undefined = SchulconnexResponseMapper.mapSanisRoleToRoleName(source); + const mapped = new ExternalUserDto({ firstName: source.person.name.vorname, lastName: source.person.name.familienname, - roles: [SchulconnexResponseMapper.mapSanisRoleToRoleName(source)], + roles: role ? [role] : [], externalId: source.pid, birthday: source.person.geburt?.datum ? new Date(source.person.geburt?.datum) : undefined, email, @@ -97,7 +99,7 @@ export class SchulconnexResponseMapper { return mapped; } - public static mapSanisRoleToRoleName(source: SchulconnexResponse): RoleName { + public static mapSanisRoleToRoleName(source: SchulconnexResponse): RoleName | undefined { return RoleMapping[source.personenkontexte[0].rolle]; } @@ -173,7 +175,7 @@ export class SchulconnexResponseMapper { const userRole: RoleName | undefined = GroupRoleMapping[relation.rollen[0]]; if (!userRole) { - this.logger.info(new GroupRoleUnknownLoggable(relation)); + this.logger.warning(new GroupRoleUnknownLoggable(relation)); return null; } diff --git a/apps/server/src/modules/provisioning/strategy/oidc/schulconnex.strategy.spec.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.spec.ts similarity index 95% rename from apps/server/src/modules/provisioning/strategy/oidc/schulconnex.strategy.spec.ts rename to apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.spec.ts index 30408619f57..26fbc0202df 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/schulconnex.strategy.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.spec.ts @@ -17,13 +17,13 @@ import { import { ExternalGroupDto, ExternalSchoolDto, - ExternalUserDto, OauthDataDto, OauthDataStrategyInputDto, ProvisioningDto, ProvisioningSystemDto, } from '../../dto'; import { ProvisioningConfig } from '../../provisioning.config'; +import { externalUserDtoFactory } from '../../testing'; import { SchulconnexProvisioningStrategy } from './schulconnex.strategy'; import { SchulconnexCourseSyncService, @@ -141,9 +141,7 @@ describe(SchulconnexProvisioningStrategy.name, () => { externalId: externalSchoolId, name: 'schoolName', }), - externalUser: new ExternalUserDto({ - externalId: externalUserId, - }), + externalUser: externalUserDtoFactory.build(), }); const user: UserDO = userDoFactory.withRoles([{ id: 'roleId', name: RoleName.USER }]).build({ firstName: 'firstName', @@ -193,9 +191,7 @@ describe(SchulconnexProvisioningStrategy.name, () => { externalId: externalSchoolId, name: 'schoolName', }), - externalUser: new ExternalUserDto({ - externalId: externalUserId, - }), + externalUser: externalUserDtoFactory.build({ externalId: externalUserId }), }); const user: UserDO = userDoFactory.withRoles([{ id: 'roleId', name: RoleName.USER }]).build({ firstName: 'firstName', @@ -252,9 +248,7 @@ describe(SchulconnexProvisioningStrategy.name, () => { provisioningStrategy: SystemProvisioningStrategy.OIDC, }), externalSchool: externalSchoolDtoFactory.build(), - externalUser: new ExternalUserDto({ - externalId: externalUserId, - }), + externalUser: externalUserDtoFactory.build({ externalId: externalUserId }), externalGroups, }); @@ -310,9 +304,7 @@ describe(SchulconnexProvisioningStrategy.name, () => { systemId: 'systemId', provisioningStrategy: SystemProvisioningStrategy.OIDC, }), - externalUser: new ExternalUserDto({ - externalId: externalUserId, - }), + externalUser: externalUserDtoFactory.build({ externalId: externalUserId }), externalGroups: externalGroupDtoFactory.buildList(2), }); @@ -354,9 +346,7 @@ describe(SchulconnexProvisioningStrategy.name, () => { systemId: 'systemId', provisioningStrategy: SystemProvisioningStrategy.OIDC, }), - externalUser: new ExternalUserDto({ - externalId: externalUserId, - }), + externalUser: externalUserDtoFactory.build({ externalId: externalUserId }), externalGroups: undefined, }); @@ -398,9 +388,7 @@ describe(SchulconnexProvisioningStrategy.name, () => { provisioningStrategy: SystemProvisioningStrategy.OIDC, }), externalSchool: externalSchoolDtoFactory.build(), - externalUser: new ExternalUserDto({ - externalId: externalUserId, - }), + externalUser: externalUserDtoFactory.build({ externalId: externalUserId }), externalGroups, }); @@ -448,9 +436,7 @@ describe(SchulconnexProvisioningStrategy.name, () => { provisioningStrategy: SystemProvisioningStrategy.OIDC, }), externalSchool: externalSchoolDtoFactory.build(), - externalUser: new ExternalUserDto({ - externalId: externalUserId, - }), + externalUser: externalUserDtoFactory.build({ externalId: externalUserId }), externalGroups, }); @@ -492,9 +478,7 @@ describe(SchulconnexProvisioningStrategy.name, () => { provisioningStrategy: SystemProvisioningStrategy.OIDC, }), externalSchool: externalSchoolDtoFactory.build(), - externalUser: new ExternalUserDto({ - externalId: externalUserId, - }), + externalUser: externalUserDtoFactory.build({ externalId: externalUserId }), externalGroups: [], }); @@ -532,9 +516,7 @@ describe(SchulconnexProvisioningStrategy.name, () => { systemId: new ObjectId().toHexString(), provisioningStrategy: SystemProvisioningStrategy.OIDC, }), - externalUser: new ExternalUserDto({ - externalId: 'externalUserId', - }), + externalUser: externalUserDtoFactory.build(), externalLicenses: [], }); const user: UserDO = userDoFactory.build({ @@ -581,9 +563,7 @@ describe(SchulconnexProvisioningStrategy.name, () => { systemId: new ObjectId().toHexString(), provisioningStrategy: SystemProvisioningStrategy.OIDC, }), - externalUser: new ExternalUserDto({ - externalId: 'externalUserId', - }), + externalUser: externalUserDtoFactory.build(), externalLicenses: [], }); const user: UserDO = userDoFactory.build({ diff --git a/apps/server/src/modules/provisioning/strategy/oidc/schulconnex.strategy.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.ts similarity index 98% rename from apps/server/src/modules/provisioning/strategy/oidc/schulconnex.strategy.ts rename to apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.ts index 007b70319bc..b965aabebcd 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/schulconnex.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.ts @@ -29,7 +29,7 @@ export abstract class SchulconnexProvisioningStrategy extends ProvisioningStrate super(); } - override async apply(data: OauthDataDto): Promise { + public override async apply(data: OauthDataDto): Promise { let school: LegacySchoolDo | undefined; if (data.externalSchool) { school = await this.schulconnexSchoolProvisioningService.provisionExternalSchool( diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/index.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/service/index.ts similarity index 100% rename from apps/server/src/modules/provisioning/strategy/oidc/service/index.ts rename to apps/server/src/modules/provisioning/strategy/schulconnex/service/index.ts diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-course-sync.service.spec.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-course-sync.service.spec.ts similarity index 100% rename from apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-course-sync.service.spec.ts rename to apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-course-sync.service.spec.ts diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-course-sync.service.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-course-sync.service.ts similarity index 100% rename from apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-course-sync.service.ts rename to apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-course-sync.service.ts diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-group-provisioning.service.spec.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-group-provisioning.service.spec.ts similarity index 100% rename from apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-group-provisioning.service.spec.ts rename to apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-group-provisioning.service.spec.ts diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-group-provisioning.service.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-group-provisioning.service.ts similarity index 100% rename from apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-group-provisioning.service.ts rename to apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-group-provisioning.service.ts diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-license-provisioning.service.spec.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-license-provisioning.service.spec.ts similarity index 100% rename from apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-license-provisioning.service.spec.ts rename to apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-license-provisioning.service.spec.ts diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-license-provisioning.service.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-license-provisioning.service.ts similarity index 100% rename from apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-license-provisioning.service.ts rename to apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-license-provisioning.service.ts diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-school-provisioning.service.spec.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-school-provisioning.service.spec.ts similarity index 100% rename from apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-school-provisioning.service.spec.ts rename to apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-school-provisioning.service.spec.ts index 42b2e72bb74..ba5995ff324 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-school-provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-school-provisioning.service.spec.ts @@ -6,8 +6,8 @@ import { LegacySchoolDo } from '@shared/domain/domainobject'; import { SchoolFeature } from '@shared/domain/types'; import { federalStateFactory, legacySchoolDoFactory, schoolYearFactory } from '@shared/testing'; import { ExternalSchoolDto } from '../../../dto'; -import { SchulconnexSchoolProvisioningService } from './schulconnex-school-provisioning.service'; import { SchoolNameRequiredLoggableException } from '../../../loggable'; +import { SchulconnexSchoolProvisioningService } from './schulconnex-school-provisioning.service'; describe(SchulconnexSchoolProvisioningService.name, () => { let module: TestingModule; diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-school-provisioning.service.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-school-provisioning.service.ts similarity index 100% rename from apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-school-provisioning.service.ts rename to apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-school-provisioning.service.ts diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-tool-provisioning.service.spec.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-tool-provisioning.service.spec.ts similarity index 98% rename from apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-tool-provisioning.service.spec.ts rename to apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-tool-provisioning.service.spec.ts index ed3a05aa0c1..9492425822a 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-tool-provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-tool-provisioning.service.spec.ts @@ -1,7 +1,6 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { SchoolSystemOptionsService, SchulConneXProvisioningOptions } from '@modules/legacy-school'; -import { SchulconnexToolProvisioningService } from '@modules/provisioning/strategy/oidc/service/schulconnex-tool-provisioning.service'; import { ExternalToolService } from '@modules/tool'; import { ExternalTool } from '@modules/tool/external-tool/domain'; import { customParameterFactory, externalToolFactory } from '@modules/tool/external-tool/testing'; @@ -12,6 +11,7 @@ import { MediaUserLicense, mediaUserLicenseFactory, MediaUserLicenseService } fr import { Test, TestingModule } from '@nestjs/testing'; import { schoolSystemOptionsFactory } from '@shared/testing'; import { Logger } from '@src/core/logger'; +import { SchulconnexToolProvisioningService } from './schulconnex-tool-provisioning.service'; describe(SchulconnexToolProvisioningService.name, () => { let module: TestingModule; diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-tool-provisioning.service.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-tool-provisioning.service.ts similarity index 100% rename from apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-tool-provisioning.service.ts rename to apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-tool-provisioning.service.ts diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-user-provisioning.service.spec.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-user-provisioning.service.spec.ts similarity index 84% rename from apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-user-provisioning.service.spec.ts rename to apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-user-provisioning.service.spec.ts index 1f78350f6ec..4915119a983 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-user-provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-user-provisioning.service.spec.ts @@ -4,7 +4,6 @@ import { AccountSave, AccountService } from '@modules/account'; import { RoleService } from '@modules/role'; import { RoleDto } from '@modules/role/service/dto/role.dto'; import { UserService } from '@modules/user'; -import { UnprocessableEntityException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { UserDO } from '@shared/domain/domainobject'; import { RoleName } from '@shared/domain/interface'; @@ -12,6 +11,8 @@ import { userDoFactory } from '@shared/testing'; import { Logger } from '@src/core/logger'; import CryptoJS from 'crypto-js'; import { ExternalUserDto } from '../../../dto'; +import { SchoolMissingLoggableException, UserRoleUnknownLoggableException } from '../../../loggable'; +import { externalUserDtoFactory } from '../../../testing'; import { SchulconnexUserProvisioningService } from './schulconnex-user-provisioning.service'; jest.mock('crypto-js'); @@ -88,7 +89,7 @@ describe(SchulconnexUserProvisioningService.name, () => { }, 'userId' ); - const externalUser: ExternalUserDto = new ExternalUserDto({ + const externalUser: ExternalUserDto = externalUserDtoFactory.build({ externalId: 'externalUserId', firstName: 'firstName', lastName: 'lastName', @@ -96,7 +97,10 @@ describe(SchulconnexUserProvisioningService.name, () => { roles: [RoleName.USER], birthday, }); - const minimalViableExternalUser: ExternalUserDto = new ExternalUserDto({ externalId: 'externalUserId' }); + const minimalViableExternalUser: ExternalUserDto = new ExternalUserDto({ + externalId: 'externalUserId', + roles: [RoleName.USER], + }); const userRole: RoleDto = new RoleDto({ id: new ObjectId().toHexString(), name: RoleName.USER, @@ -126,8 +130,32 @@ describe(SchulconnexUserProvisioningService.name, () => { }; }; + describe('when the user has no role', () => { + const setup = () => { + const systemId = new ObjectId().toHexString(); + const schoolId = new ObjectId().toHexString(); + const externalUser = externalUserDtoFactory.build({ + roles: [], + }); + + return { + systemId, + schoolId, + externalUser, + }; + }; + + it('should throw UserRoleUnknownLoggableException', async () => { + const { externalUser, schoolId, systemId } = setup(); + + await expect(service.provisionExternalUser(externalUser, systemId, schoolId)).rejects.toThrow( + UserRoleUnknownLoggableException + ); + }); + }); + describe('when the user does not exist yet', () => { - describe('when the external user has no email or roles', () => { + describe('when the external user has no email', () => { it('should return the saved user', async () => { const { minimalViableExternalUser, schoolId, savedUser, systemId } = setupUser(); @@ -166,14 +194,14 @@ describe(SchulconnexUserProvisioningService.name, () => { }); describe('when no schoolId is provided', () => { - it('should throw UnprocessableEntityException', async () => { + it('should throw SchoolMissingLoggableException', async () => { const { externalUser } = setupUser(); userService.findByExternalId.mockResolvedValue(null); const promise: Promise = service.provisionExternalUser(externalUser, 'systemId', undefined); - await expect(promise).rejects.toThrow(UnprocessableEntityException); + await expect(promise).rejects.toThrow(SchoolMissingLoggableException); }); }); }); diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-user-provisioning.service.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-user-provisioning.service.ts similarity index 85% rename from apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-user-provisioning.service.ts rename to apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-user-provisioning.service.ts index d7b764389bc..3558cdad4f9 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-user-provisioning.service.ts +++ b/apps/server/src/modules/provisioning/strategy/schulconnex/service/schulconnex-user-provisioning.service.ts @@ -1,12 +1,14 @@ import { AccountSave, AccountService } from '@modules/account'; import { RoleDto, RoleService } from '@modules/role'; import { UserService } from '@modules/user'; -import { Injectable, UnprocessableEntityException } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { RoleReference, UserDO } from '@shared/domain/domainobject'; import { RoleName } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import CryptoJS from 'crypto-js'; import { ExternalUserDto } from '../../../dto'; +import { UserRoleUnknownLoggableException } from '../../../loggable'; +import { SchoolMissingLoggableException } from '../../../loggable/school-missing.loggable-exception'; @Injectable() export class SchulconnexUserProvisioningService { @@ -24,6 +26,9 @@ export class SchulconnexUserProvisioningService { const foundUser: UserDO | null = await this.userService.findByExternalId(externalUser.externalId, systemId); const roleRefs: RoleReference[] | undefined = await this.createRoleReferences(externalUser.roles); + if (!roleRefs?.length) { + throw new UserRoleUnknownLoggableException(externalUser); + } let createNewAccount = false; let user: UserDO; @@ -31,9 +36,7 @@ export class SchulconnexUserProvisioningService { user = this.updateUser(externalUser, foundUser, roleRefs, schoolId); } else { if (!schoolId) { - throw new UnprocessableEntityException( - `Unable to create new external user ${externalUser.externalId} without a school` - ); + throw new SchoolMissingLoggableException(externalUser); } createNewAccount = true; @@ -55,10 +58,10 @@ export class SchulconnexUserProvisioningService { } private async createRoleReferences(roles?: RoleName[]): Promise { - if (roles) { + if (roles?.length) { const foundRoles: RoleDto[] = await this.roleService.findByNames(roles); - const roleRefs = foundRoles.map( - (role: RoleDto): RoleReference => new RoleReference({ id: role.id || '', name: role.name }) + const roleRefs: RoleReference[] = foundRoles.map( + (role: RoleDto): RoleReference => new RoleReference({ id: role.id, name: role.name }) ); return roleRefs; diff --git a/apps/server/src/modules/provisioning/strategy/tsp/index.ts b/apps/server/src/modules/provisioning/strategy/tsp/index.ts new file mode 100644 index 00000000000..0d472196907 --- /dev/null +++ b/apps/server/src/modules/provisioning/strategy/tsp/index.ts @@ -0,0 +1 @@ +export { TspProvisioningStrategy } from './tsp.strategy'; diff --git a/apps/server/src/modules/provisioning/testing/external-user-dto.factory.ts b/apps/server/src/modules/provisioning/testing/external-user-dto.factory.ts new file mode 100644 index 00000000000..8a257d9f9ff --- /dev/null +++ b/apps/server/src/modules/provisioning/testing/external-user-dto.factory.ts @@ -0,0 +1,16 @@ +import { RoleName } from '@shared/domain/interface'; +import { UUID } from 'bson'; +import { Factory } from 'fishery'; +import { ExternalUserDto } from '../dto'; + +export const externalUserDtoFactory = Factory.define( + () => + new ExternalUserDto({ + externalId: new UUID().toString(), + email: 'external@schul-cloud.org', + birthday: new Date(1998, 11, 18), + firstName: 'ex', + lastName: 'ternal', + roles: [RoleName.TEACHER], + }) +); diff --git a/apps/server/src/modules/provisioning/testing/index.ts b/apps/server/src/modules/provisioning/testing/index.ts new file mode 100644 index 00000000000..770f3e74f37 --- /dev/null +++ b/apps/server/src/modules/provisioning/testing/index.ts @@ -0,0 +1 @@ +export { externalUserDtoFactory } from './external-user-dto.factory'; diff --git a/apps/server/src/modules/user-import/entity/import-user.entity.ts b/apps/server/src/modules/user-import/entity/import-user.entity.ts index 24d8ba33653..5944ae5f4c5 100644 --- a/apps/server/src/modules/user-import/entity/import-user.entity.ts +++ b/apps/server/src/modules/user-import/entity/import-user.entity.ts @@ -117,7 +117,7 @@ export class ImportUser extends BaseEntityWithTimestamps implements EntityWithSc @Property({ nullable: true }) externalRoleNames?: string[]; - setMatch(user: User, matchedBy: MatchCreator) { + public setMatch(user: User, matchedBy: MatchCreator): void { if (this.school.id !== user.school.id) { throw new Error('not same school'); } @@ -125,12 +125,12 @@ export class ImportUser extends BaseEntityWithTimestamps implements EntityWithSc this.matchedBy = matchedBy; } - revokeMatch() { + public revokeMatch(): void { this.user = undefined; this.matchedBy = undefined; } - static isImportUserRole(role: RoleName): role is ImportUserRoleName { + public static isImportUserRole(role: unknown): role is ImportUserRoleName { return role === RoleName.ADMINISTRATOR || role === RoleName.STUDENT || role === RoleName.TEACHER; } } diff --git a/apps/server/src/modules/user-import/mapper/schulconnex-import-user.mapper.ts b/apps/server/src/modules/user-import/mapper/schulconnex-import-user.mapper.ts index f5f655a222d..83d7f5a7455 100644 --- a/apps/server/src/modules/user-import/mapper/schulconnex-import-user.mapper.ts +++ b/apps/server/src/modules/user-import/mapper/schulconnex-import-user.mapper.ts @@ -15,7 +15,8 @@ export class SchulconnexImportUserMapper { em: EntityManager ): ImportUser[] { const importUsers: ImportUser[] = response.map((externalUser: SchulconnexResponse): ImportUser => { - const role: RoleName = SchulconnexResponseMapper.mapSanisRoleToRoleName(externalUser); + const role: RoleName | undefined = SchulconnexResponseMapper.mapSanisRoleToRoleName(externalUser); + const groups: SchulconnexGruppenResponse[] | undefined = externalUser.personenkontexte[0]?.gruppen?.filter( (group) => group.gruppe.typ === SchulconnexGroupType.CLASS ); 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 6ce87f1d431..3d52fb94525 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 @@ -4,13 +4,7 @@ import { AuthenticationService } from '@modules/authentication'; import { Action, AuthorizationService } from '@modules/authorization'; import { LegacySchoolService } from '@modules/legacy-school'; import { OAuthService, OAuthTokenDto } from '@modules/oauth'; -import { - ExternalSchoolDto, - ExternalUserDto, - OauthDataDto, - ProvisioningService, - ProvisioningSystemDto, -} from '@modules/provisioning'; +import { ExternalSchoolDto, OauthDataDto, ProvisioningService, ProvisioningSystemDto } from '@modules/provisioning'; import { SystemEntity } from '@modules/system/entity'; import { UserService } from '@modules/user'; import { ForbiddenException } from '@nestjs/common'; @@ -30,6 +24,7 @@ import { userLoginMigrationDOFactory, } from '@shared/testing'; import { Logger } from '@src/core/logger'; +import { externalUserDtoFactory } from '../../provisioning/testing'; import { ExternalSchoolNumberMissingLoggableException, InvalidUserLoginMigrationLoggableException, @@ -294,9 +289,7 @@ describe(UserLoginMigrationUc.name, () => { systemId: 'systemId', provisioningStrategy: SystemProvisioningStrategy.SANIS, }), - externalUser: new ExternalUserDto({ - externalId: 'externalUserId', - }), + externalUser: externalUserDtoFactory.build(), }); const tokenDto: OAuthTokenDto = new OAuthTokenDto({ @@ -376,9 +369,7 @@ describe(UserLoginMigrationUc.name, () => { systemId: 'systemId', provisioningStrategy: SystemProvisioningStrategy.SANIS, }), - externalUser: new ExternalUserDto({ - externalId: 'externalUserId', - }), + externalUser: externalUserDtoFactory.build(), externalSchool: new ExternalSchoolDto({ externalId: 'externalId', officialSchoolNumber: 'officialSchoolNumber', @@ -441,9 +432,7 @@ describe(UserLoginMigrationUc.name, () => { systemId: 'systemId', provisioningStrategy: SystemProvisioningStrategy.SANIS, }), - externalUser: new ExternalUserDto({ - externalId: 'externalUserId', - }), + externalUser: externalUserDtoFactory.build(), externalSchool: new ExternalSchoolDto({ externalId: 'externalId', officialSchoolNumber: 'officialSchoolNumber', @@ -490,9 +479,7 @@ describe(UserLoginMigrationUc.name, () => { systemId: 'systemId', provisioningStrategy: SystemProvisioningStrategy.SANIS, }), - externalUser: new ExternalUserDto({ - externalId: 'externalUserId', - }), + externalUser: externalUserDtoFactory.build(), }); const tokenDto: OAuthTokenDto = new OAuthTokenDto({ @@ -531,9 +518,7 @@ describe(UserLoginMigrationUc.name, () => { systemId: 'systemId', provisioningStrategy: SystemProvisioningStrategy.SANIS, }), - externalUser: new ExternalUserDto({ - externalId: 'externalUserId', - }), + externalUser: externalUserDtoFactory.build(), externalSchool: new ExternalSchoolDto({ externalId: 'externalId', name: 'schoolName',