From 7f36a8f21a9594b74c35a56e1a23ccc3ccc43d99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= Date: Wed, 11 Dec 2024 10:53:35 +0100 Subject: [PATCH 1/6] add timeout and skip provisioning --- .../schulconnex-client-config.ts | 1 + .../schulconnex-client.module.ts | 3 +- .../schulconnex-rest-client-options.ts | 2 + .../schulconnex-rest-client.spec.ts | 6 +- .../schulconnex-rest-client.ts | 8 +- .../modules/idp-console/idp-console.config.ts | 13 +- .../provisioning/provisioning.config.ts | 1 + .../schulconnex/schulconnex.strategy.spec.ts | 37 +++++ .../schulconnex/schulconnex.strategy.ts | 11 +- .../src/modules/server/server.config.ts | 6 + config/default.schema.json | 148 +++++++++++++++--- 11 files changed, 204 insertions(+), 32 deletions(-) diff --git a/apps/server/src/infra/schulconnex-client/schulconnex-client-config.ts b/apps/server/src/infra/schulconnex-client/schulconnex-client-config.ts index e7d5e6b23b6..709f4a1ea71 100644 --- a/apps/server/src/infra/schulconnex-client/schulconnex-client-config.ts +++ b/apps/server/src/infra/schulconnex-client/schulconnex-client-config.ts @@ -1,4 +1,5 @@ export interface SchulconnexClientConfig { + SCHULCONNEX_CLIENT__PERSON_INFO_TIMEOUT_IN_MS: number; SCHULCONNEX_CLIENT__PERSONEN_INFO_TIMEOUT_IN_MS: number; SCHULCONNEX_CLIENT__POLICIES_INFO_TIMEOUT_IN_MS: number; SCHULCONNEX_CLIENT__API_URL?: string; diff --git a/apps/server/src/infra/schulconnex-client/schulconnex-client.module.ts b/apps/server/src/infra/schulconnex-client/schulconnex-client.module.ts index b16a7f55458..bff42d9bbdb 100644 --- a/apps/server/src/infra/schulconnex-client/schulconnex-client.module.ts +++ b/apps/server/src/infra/schulconnex-client/schulconnex-client.module.ts @@ -8,7 +8,7 @@ import { SchulconnexRestClientOptions } from './schulconnex-rest-client-options' @Module({}) export class SchulconnexClientModule { - static registerAsync(): DynamicModule { + public static registerAsync(): DynamicModule { return { imports: [HttpModule, LoggerModule], module: SchulconnexClientModule, @@ -27,6 +27,7 @@ export class SchulconnexClientModule { tokenEndpoint: configService.get('SCHULCONNEX_CLIENT__TOKEN_ENDPOINT'), clientId: configService.get('SCHULCONNEX_CLIENT__CLIENT_ID'), clientSecret: configService.get('SCHULCONNEX_CLIENT__CLIENT_SECRET'), + personInfoTimeoutInMs: configService.get('SCHULCONNEX_CLIENT__PERSON_INFO_TIMEOUT_IN_MS'), personenInfoTimeoutInMs: configService.get('SCHULCONNEX_CLIENT__PERSONEN_INFO_TIMEOUT_IN_MS'), policiesInfoTimeoutInMs: configService.get('SCHULCONNEX_CLIENT__POLICIES_INFO_TIMEOUT_IN_MS'), }; diff --git a/apps/server/src/infra/schulconnex-client/schulconnex-rest-client-options.ts b/apps/server/src/infra/schulconnex-client/schulconnex-rest-client-options.ts index 01391ec207e..5316df7e74a 100644 --- a/apps/server/src/infra/schulconnex-client/schulconnex-rest-client-options.ts +++ b/apps/server/src/infra/schulconnex-client/schulconnex-rest-client-options.ts @@ -7,6 +7,8 @@ export interface SchulconnexRestClientOptions { clientSecret?: string; + personInfoTimeoutInMs?: number; + personenInfoTimeoutInMs?: number; policiesInfoTimeoutInMs?: number; diff --git a/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.spec.ts b/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.spec.ts index 5af753d8554..49ad5e2fa29 100644 --- a/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.spec.ts +++ b/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.spec.ts @@ -25,8 +25,9 @@ describe(SchulconnexRestClient.name, () => { clientId: 'clientId', clientSecret: 'clientSecret', tokenEndpoint: 'https://schulconnex.url/token', - personenInfoTimeoutInMs: 30000, - policiesInfoTimeoutInMs: 30000, + personInfoTimeoutInMs: 30001, + personenInfoTimeoutInMs: 30002, + policiesInfoTimeoutInMs: 30003, }; beforeAll(() => { @@ -100,6 +101,7 @@ describe(SchulconnexRestClient.name, () => { Authorization: `Bearer ${accessToken}`, 'Accept-Encoding': 'gzip', }, + timeout: options.personInfoTimeoutInMs, }); }); diff --git a/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.ts b/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.ts index 820668c16ce..d9a3b829cd3 100644 --- a/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.ts +++ b/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.ts @@ -30,10 +30,14 @@ export class SchulconnexRestClient implements SchulconnexApiInterface { this.SCHULCONNEX_API_BASE_URL = options.apiUrl || ''; } - public async getPersonInfo(accessToken: string, options?: { overrideUrl: string }): Promise { + public getPersonInfo(accessToken: string, options?: { overrideUrl: string }): Promise { const url: URL = new URL(options?.overrideUrl ?? `${this.SCHULCONNEX_API_BASE_URL}/person-info`); - const response: Promise = this.getRequest(url, accessToken); + const response: Promise = this.getRequest( + url, + accessToken, + this.options.personInfoTimeoutInMs + ); return response; } diff --git a/apps/server/src/modules/idp-console/idp-console.config.ts b/apps/server/src/modules/idp-console/idp-console.config.ts index 08a1e9fe301..30b14264858 100644 --- a/apps/server/src/modules/idp-console/idp-console.config.ts +++ b/apps/server/src/modules/idp-console/idp-console.config.ts @@ -1,12 +1,12 @@ +import { Configuration } from '@hpi-schul-cloud/commons'; import { ConsoleWriterConfig } from '@infra/console'; -import { LoggerConfig } from '@src/core/logger'; +import { RabbitMqConfig } from '@infra/rabbitmq'; +import { SchulconnexClientConfig } from '@infra/schulconnex-client'; import { AccountConfig } from '@modules/account'; -import { UserConfig } from '@modules/user'; import { SynchronizationConfig } from '@modules/synchronization'; -import { SchulconnexClientConfig } from '@infra/schulconnex-client'; -import { Configuration } from '@hpi-schul-cloud/commons'; +import { UserConfig } from '@modules/user'; import { LanguageType } from '@shared/domain/interface'; -import { RabbitMqConfig } from '@infra/rabbitmq'; +import { LoggerConfig } from '@src/core/logger'; export interface IdpConsoleConfig extends ConsoleWriterConfig, @@ -33,6 +33,9 @@ const config: IdpConsoleConfig = { TEACHER_VISIBILITY_FOR_EXTERNAL_TEAM_INVITATION: Configuration.get( 'TEACHER_VISIBILITY_FOR_EXTERNAL_TEAM_INVITATION' ) as string, + SCHULCONNEX_CLIENT__PERSON_INFO_TIMEOUT_IN_MS: Configuration.get( + 'SCHULCONNEX_CLIENT__PERSON_INFO_TIMEOUT_IN_MS' + ) as number, SCHULCONNEX_CLIENT__PERSONEN_INFO_TIMEOUT_IN_MS: Configuration.get( 'SCHULCONNEX_CLIENT__PERSONEN_INFO_TIMEOUT_IN_MS' ) as number, diff --git a/apps/server/src/modules/provisioning/provisioning.config.ts b/apps/server/src/modules/provisioning/provisioning.config.ts index 0314bf8b277..9ba480fbcea 100644 --- a/apps/server/src/modules/provisioning/provisioning.config.ts +++ b/apps/server/src/modules/provisioning/provisioning.config.ts @@ -2,6 +2,7 @@ export interface ProvisioningConfig { FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED: boolean; FEATURE_SCHULCONNEX_MEDIA_LICENSE_ENABLED: boolean; PROVISIONING_SCHULCONNEX_POLICIES_INFO_URL: string; + PROVISIONING_SCHULCONNEX_GROUP_USERS_LIMIT?: number; FEATURE_SANIS_GROUP_PROVISIONING_ENABLED: boolean; FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED: boolean; } diff --git a/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.spec.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.spec.ts index 26fbc0202df..3f23f776ebd 100644 --- a/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.spec.ts @@ -116,6 +116,7 @@ describe(SchulconnexProvisioningStrategy.name, () => { config.FEATURE_SANIS_GROUP_PROVISIONING_ENABLED = false; config.FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED = false; config.FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED = true; + config.PROVISIONING_SCHULCONNEX_GROUP_USERS_LIMIT = undefined; }); afterAll(async () => { @@ -336,6 +337,42 @@ describe(SchulconnexProvisioningStrategy.name, () => { }); }); + describe('when there are too many users in groups', () => { + const setup = () => { + config.PROVISIONING_SCHULCONNEX_GROUP_USERS_LIMIT = 1; + + const externalUserId = 'externalUserId'; + const externalGroups: ExternalGroupDto[] = externalGroupDtoFactory.buildList(2); + const oauthData: OauthDataDto = new OauthDataDto({ + system: new ProvisioningSystemDto({ + systemId: new ObjectId().toHexString(), + provisioningStrategy: SystemProvisioningStrategy.OIDC, + }), + externalSchool: externalSchoolDtoFactory.build(), + externalUser: externalUserDtoFactory.build({ externalId: externalUserId }), + externalGroups, + }); + + const user: UserDO = userDoFactory.withRoles([{ id: 'roleId', name: RoleName.USER }]).build({ + externalId: externalUserId, + }); + + schulconnexUserProvisioningService.provisionExternalUser.mockResolvedValueOnce(user); + + return { + oauthData, + }; + }; + + it('should not run group provisioning', async () => { + const { oauthData } = setup(); + + await strategy.apply(oauthData); + + expect(schulconnexGroupProvisioningService.provisionExternalGroup).not.toHaveBeenCalled(); + }); + }); + describe('when group data is not provided', () => { const setup = () => { config.FEATURE_SANIS_GROUP_PROVISIONING_ENABLED = true; diff --git a/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.ts index b965aabebcd..0cd90ff4431 100644 --- a/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.ts @@ -45,7 +45,16 @@ export abstract class SchulconnexProvisioningStrategy extends ProvisioningStrate ); if (this.configService.get('FEATURE_SANIS_GROUP_PROVISIONING_ENABLED')) { - await this.provisionGroups(data, school); + const usersInGroupsCount: number = + data.externalGroups?.reduce( + (count: number, group: ExternalGroupDto) => count + (group.otherUsers?.length ?? 0), + data.externalGroups?.length ?? 0 + ) ?? 0; + + const limit: number | undefined = this.configService.get('PROVISIONING_SCHULCONNEX_GROUP_USERS_LIMIT'); + if (!limit || usersInGroupsCount < limit) { + await this.provisionGroups(data, school); + } } if (this.configService.get('FEATURE_SCHULCONNEX_MEDIA_LICENSE_ENABLED') && user.id) { diff --git a/apps/server/src/modules/server/server.config.ts b/apps/server/src/modules/server/server.config.ts index 3c8f9d89708..6d4c2906844 100644 --- a/apps/server/src/modules/server/server.config.ts +++ b/apps/server/src/modules/server/server.config.ts @@ -267,12 +267,18 @@ const config: ServerConfig = { SCHULCONNEX_CLIENT__CLIENT_SECRET: Configuration.has('SCHULCONNEX_CLIENT__CLIENT_SECRET') ? (Configuration.get('SCHULCONNEX_CLIENT__CLIENT_SECRET') as string) : undefined, + SCHULCONNEX_CLIENT__PERSON_INFO_TIMEOUT_IN_MS: Configuration.get( + 'SCHULCONNEX_CLIENT__PERSON_INFO_TIMEOUT_IN_MS' + ) as number, SCHULCONNEX_CLIENT__PERSONEN_INFO_TIMEOUT_IN_MS: Configuration.get( 'SCHULCONNEX_CLIENT__PERSONEN_INFO_TIMEOUT_IN_MS' ) as number, SCHULCONNEX_CLIENT__POLICIES_INFO_TIMEOUT_IN_MS: Configuration.get( 'SCHULCONNEX_CLIENT__POLICIES_INFO_TIMEOUT_IN_MS' ) as number, + PROVISIONING_SCHULCONNEX_GROUP_USERS_LIMIT: Configuration.has('PROVISIONING_SCHULCONNEX_GROUP_USERS_LIMIT') + ? (Configuration.get('PROVISIONING_SCHULCONNEX_GROUP_USERS_LIMIT') as number) + : undefined, FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED: Configuration.get('FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED') as boolean, ...getTldrawClientConfig(), FEATURE_MEDIA_SHELF_ENABLED: Configuration.get('FEATURE_MEDIA_SHELF_ENABLED') as boolean, diff --git a/config/default.schema.json b/config/default.schema.json index 14cb3e99a1c..8b44ccda4cf 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -8,7 +8,12 @@ }, "NODE_ENV": { "type": "string", - "enum": ["development", "test", "production", "migration"], + "enum": [ + "development", + "test", + "production", + "migration" + ], "default": "production" }, "REQUEST_OPTION": { @@ -72,7 +77,10 @@ "DEFAULT_LANGUAGE": { "type": "string", "default": "de", - "enum": ["de", "en"], + "enum": [ + "de", + "en" + ], "description": "Value for the default language" }, "DEFAULT_TIMEZONE": { @@ -100,13 +108,23 @@ "TEACHER_VISIBILITY_FOR_EXTERNAL_TEAM_INVITATION": { "type": "string", "default": "disabled", - "enum": ["disabled", "opt-in", "opt-out", "enabled"], + "enum": [ + "disabled", + "opt-in", + "opt-out", + "enabled" + ], "description": "defines wheter external team invitation shows teachers from different schools or not. if enabled system wide there are options general enabled or opt-in/-out by user required." }, "STUDENT_TEAM_CREATION": { "type": "string", "default": "opt-out", - "enum": ["disabled", "opt-in", "opt-out", "enabled"], + "enum": [ + "disabled", + "opt-in", + "opt-out", + "enabled" + ], "description": "defines wheter students may create teams or not. if enabled system wide there are options general enabled or opt-in/-out by school admin required." }, "REDIS_URI": { @@ -282,7 +300,13 @@ "FILES_STORAGE": { "type": "object", "description": "Files storage server properties, required always to be defined", - "required": ["S3_ENDPOINT", "S3_REGION", "S3_BUCKET", "S3_ACCESS_KEY_ID", "S3_SECRET_ACCESS_KEY"], + "required": [ + "S3_ENDPOINT", + "S3_REGION", + "S3_BUCKET", + "S3_ACCESS_KEY_ID", + "S3_SECRET_ACCESS_KEY" + ], "properties": { "SERVICE_BASE_URL": { "type": "string", @@ -339,7 +363,13 @@ "FWU_CONTENT": { "type": "object", "description": "Properties of the S3 storage containing FWU content", - "required": ["S3_ENDPOINT", "S3_REGION", "S3_BUCKET", "S3_ACCESS_KEY", "S3_SECRET_KEY"], + "required": [ + "S3_ENDPOINT", + "S3_REGION", + "S3_BUCKET", + "S3_ACCESS_KEY", + "S3_SECRET_KEY" + ], "properties": { "S3_ENDPOINT": { "type": "string", @@ -378,7 +408,12 @@ "H5P_EDITOR": { "type": "object", "description": "Properties of the H5P server microservice and library management job", - "required": ["S3_ENDPOINT", "S3_REGION", "S3_BUCKET_CONTENT", "S3_BUCKET_LIBRARIES"], + "required": [ + "S3_ENDPOINT", + "S3_REGION", + "S3_BUCKET_CONTENT", + "S3_BUCKET_LIBRARIES" + ], "default": {}, "properties": { "S3_ENDPOINT": { @@ -438,7 +473,12 @@ "H5P_Library": { "type": "object", "description": "Properties of the H5P server microservice", - "required": ["S3_ENDPOINT", "S3_BUCKET_LIBRARIES", "S3_ACCESS_KEY_ID", "S3_SECRET_ACCESS_KEY"], + "required": [ + "S3_ENDPOINT", + "S3_BUCKET_LIBRARIES", + "S3_ACCESS_KEY_ID", + "S3_SECRET_ACCESS_KEY" + ], "default": {}, "properties": { "S3_ENDPOINT": { @@ -872,7 +912,9 @@ "ETHERPAD": { "type": "object", "description": "Etherpad settings", - "required": ["PAD_URI"], + "required": [ + "PAD_URI" + ], "properties": { "URI": { "type": "string", @@ -1030,7 +1072,9 @@ "API_VALIDATION_WHITELIST_EXTENSION": { "type": "string", "description": "when set, this is interpreted as a regex to extend the ignorelist for the API validation with any routes matching the regex.", - "examples": [".*/courses/[0-9a-f]{24}($|/$)"] + "examples": [ + ".*/courses/[0-9a-f]{24}($|/$)" + ] }, "FEATURE_PROMETHEUS_METRICS_ENABLED": { "type": "boolean", @@ -1105,13 +1149,31 @@ "type": "string", "default": "error", "description": "Log level for api.", - "enum": ["emerg", "alert", "crit", "error", "warning", "notice", "info", "debug"] + "enum": [ + "emerg", + "alert", + "crit", + "error", + "warning", + "notice", + "info", + "debug" + ] }, "NEST_LOG_LEVEL": { "type": "string", "default": "notice", "description": "Nest Log level for api. The http flag is for request logging. The http flag do only work by api methods with added 'request logging interceptor'.", - "enum": ["emerg", "alert", "crit", "error", "warning", "notice", "info", "debug"] + "enum": [ + "emerg", + "alert", + "crit", + "error", + "warning", + "notice", + "info", + "debug" + ] }, "EXIT_ON_ERROR": { "type": "boolean", @@ -1122,7 +1184,12 @@ "type": "string", "default": "requestError", "description": "Special logs.", - "enum": ["requestError", "systemLogs", "request", "sendRequests"] + "enum": [ + "requestError", + "systemLogs", + "request", + "sendRequests" + ] }, "SYNC_QUEUE_NAME": { "type": "string", @@ -1309,7 +1376,11 @@ "SAME_SITE": { "type": "string", "default": "none", - "enum": ["none", "lax", "strict"], + "enum": [ + "none", + "lax", + "strict" + ], "description": "Value for cookies sameSite property. When SECURE flag is false, 'None' is not allowed in SAME_SITE and Lax should be used as default instead" }, "HTTP_ONLY": { @@ -1333,7 +1404,13 @@ "description": "Expiration in seconds from now" } }, - "required": ["SAME_SITE", "HTTP_ONLY", "HOST_ONLY", "SECURE", "EXPIRES_SECONDS"], + "required": [ + "SAME_SITE", + "HTTP_ONLY", + "HOST_ONLY", + "SECURE", + "EXPIRES_SECONDS" + ], "allOf": [ { "$ref": "#/properties/COOKIE/definitions/SAME_SITE_SECURE_VALID" @@ -1351,7 +1428,10 @@ "then": { "properties": { "SAME_SITE": { - "enum": ["lax", "strict"] + "enum": [ + "lax", + "strict" + ] } } } @@ -1623,7 +1703,9 @@ "type": "string", "default": "image/png,image/jpeg,image/gif,image/svg+xml", "description": "List with allowed assets MIME types, comma separated, empty if all MIME types supported by tldraw should be allowed", - "examples": ["image/gif,image/jpeg,video/webm"] + "examples": [ + "image/gif,image/jpeg,video/webm" + ] }, "PERFORMANCE_MEASURE_ENABLED": { "type": "boolean", @@ -1634,7 +1716,16 @@ "type": "string", "default": "info", "description": "Define log level for tldraw.", - "enum": ["emerg", "alert", "crit", "error", "warning", "notice", "info", "debug"] + "enum": [ + "emerg", + "alert", + "crit", + "error", + "warning", + "notice", + "info", + "debug" + ] } } }, @@ -1655,12 +1746,16 @@ "API_URL": { "type": "string", "description": "Base URL of the schulconnex API (from dof)", - "examples": ["https://api-dienste.stage.niedersachsen-login.schule/v1/"] + "examples": [ + "https://api-dienste.stage.niedersachsen-login.schule/v1/" + ] }, "TOKEN_ENDPOINT": { "type": "string", "description": "Token endpoint of the schulconnex API (from dof)", - "examples": ["https://api-dienste.stage.niedersachsen-login.schule/v1/oauth2/token"] + "examples": [ + "https://api-dienste.stage.niedersachsen-login.schule/v1/oauth2/token" + ] }, "CLIENT_ID": { "type": "string", @@ -1670,6 +1765,11 @@ "type": "string", "description": "Client secret for accessing the schulconnex API (from server vault)" }, + "PERSON_INFO_TIMEOUT_IN_MS": { + "type": "integer", + "description": "Timeout in milliseconds for fetching person info from schulconnex", + "default": 3000 + }, "PERSONEN_INFO_TIMEOUT_IN_MS": { "type": "integer", "description": "Timeout in milliseconds for fetching personen info from schulconnex", @@ -1731,7 +1831,13 @@ "type": "string", "default": "", "description": "URL for fetching policies info from moin.schule schulconnex", - "examples": ["https://api-dienste.stage.niedersachsen-login.schule/v1/policies-info"] + "examples": [ + "https://api-dienste.stage.niedersachsen-login.schule/v1/policies-info" + ] + }, + "PROVISIONING_SCHULCONNEX_GROUP_USERS_LIMIT": { + "type": "number", + "description": "Maximum number of users in group that still get processed during schulconnex provisioning" }, "BOARD_COLLABORATION_URI": { "type": "string", From d3eaf4af0425fee50fef77f1797e91b79177c1ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= Date: Thu, 12 Dec 2024 10:09:39 +0100 Subject: [PATCH 2/6] change the logic to only affect other group members --- .../schulconnex-response-mapper.spec.ts | 57 +++++++++++++++++++ .../schulconnex-response-mapper.ts | 19 +++++-- .../schulconnex/schulconnex.strategy.spec.ts | 37 ------------ .../schulconnex/schulconnex.strategy.ts | 11 +--- 4 files changed, 73 insertions(+), 51 deletions(-) diff --git a/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex-response-mapper.spec.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex-response-mapper.spec.ts index 36ad4321943..1d413fd4aad 100644 --- a/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex-response-mapper.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex-response-mapper.spec.ts @@ -47,6 +47,11 @@ describe(SchulconnexResponseMapper.name, () => { mapper = module.get(SchulconnexResponseMapper); }); + beforeEach(() => { + config.FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED = false; + config.PROVISIONING_SCHULCONNEX_GROUP_USERS_LIMIT = undefined; + }); + describe('mapToExternalSchoolDto', () => { describe('when a schulconnex response is provided', () => { const setup = () => { @@ -316,6 +321,8 @@ describe(SchulconnexResponseMapper.name, () => { describe('when other participants have unknown roles', () => { const setup = () => { + config.FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED = true; + const schulconnexResponse: SchulconnexResponse = schulconnexResponseFactory.build(); schulconnexResponse.personenkontexte[0].gruppen![0]!.sonstige_gruppenzugehoerige = [ { @@ -514,6 +521,56 @@ describe(SchulconnexResponseMapper.name, () => { ); }); }); + + describe('when there are too many users in groups', () => { + const setup = () => { + config.FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED = true; + config.PROVISIONING_SCHULCONNEX_GROUP_USERS_LIMIT = 1; + + const schulconnexResponse: SchulconnexResponse = schulconnexResponseFactory.build(); + + return { + schulconnexResponse, + }; + }; + + it('should not map other group users', () => { + const { schulconnexResponse } = setup(); + + const result: ExternalGroupDto[] | undefined = mapper.mapToExternalGroupDtos(schulconnexResponse); + + expect(result).toEqual([ + expect.objectContaining>({ + otherUsers: undefined, + }), + ]); + }); + }); + + describe('when there are not too many users in groups', () => { + const setup = () => { + config.FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED = true; + config.PROVISIONING_SCHULCONNEX_GROUP_USERS_LIMIT = 10; + + const schulconnexResponse: SchulconnexResponse = schulconnexResponseFactory.build(); + + return { + schulconnexResponse, + }; + }; + + it('should not map other group users', () => { + const { schulconnexResponse } = setup(); + + const result: ExternalGroupDto[] | undefined = mapper.mapToExternalGroupDtos(schulconnexResponse); + + expect(result).not.toEqual([ + expect.objectContaining({ + otherUsers: undefined, + }), + ]); + }); + }); }); describe('mapLernperiode', () => { diff --git a/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex-response-mapper.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex-response-mapper.ts index 4a7543cac70..9b6631d9d38 100644 --- a/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex-response-mapper.ts +++ b/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex-response-mapper.ts @@ -120,14 +120,25 @@ export class SchulconnexResponseMapper { return undefined; } + const usersInGroupsCount: number = groups.reduce( + (count: number, group: SchulconnexGruppenResponse) => count + (group.sonstige_gruppenzugehoerige?.length ?? 0), + groups.length + ); + const limit: number | undefined = this.configService.get('PROVISIONING_SCHULCONNEX_GROUP_USERS_LIMIT'); + const shouldProvisionOtherUsers: boolean = !limit || usersInGroupsCount < limit; + const mapped: ExternalGroupDto[] = groups - .map((group) => this.mapExternalGroup(source, group)) - .filter((group): group is ExternalGroupDto => group !== null); + .map((group: SchulconnexGruppenResponse) => this.mapExternalGroup(source, group, shouldProvisionOtherUsers)) + .filter((group: ExternalGroupDto | null): group is ExternalGroupDto => group !== null); return mapped; } - private mapExternalGroup(source: SchulconnexResponse, group: SchulconnexGruppenResponse): ExternalGroupDto | null { + private mapExternalGroup( + source: SchulconnexResponse, + group: SchulconnexGruppenResponse, + shouldProvisionOtherUsers: boolean + ): ExternalGroupDto | null { const groupType: GroupTypes | undefined = GroupTypeMapping[group.gruppe.typ]; if (!groupType) { @@ -144,7 +155,7 @@ export class SchulconnexResponseMapper { } let otherUsers: ExternalGroupUserDto[] | undefined; - if (this.configService.get('FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED')) { + if (this.configService.get('FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED') && shouldProvisionOtherUsers) { otherUsers = group.sonstige_gruppenzugehoerige ? group.sonstige_gruppenzugehoerige .map((relation): ExternalGroupUserDto | null => this.mapToExternalGroupUser(relation)) diff --git a/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.spec.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.spec.ts index 3f23f776ebd..26fbc0202df 100644 --- a/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.spec.ts @@ -116,7 +116,6 @@ describe(SchulconnexProvisioningStrategy.name, () => { config.FEATURE_SANIS_GROUP_PROVISIONING_ENABLED = false; config.FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED = false; config.FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED = true; - config.PROVISIONING_SCHULCONNEX_GROUP_USERS_LIMIT = undefined; }); afterAll(async () => { @@ -337,42 +336,6 @@ describe(SchulconnexProvisioningStrategy.name, () => { }); }); - describe('when there are too many users in groups', () => { - const setup = () => { - config.PROVISIONING_SCHULCONNEX_GROUP_USERS_LIMIT = 1; - - const externalUserId = 'externalUserId'; - const externalGroups: ExternalGroupDto[] = externalGroupDtoFactory.buildList(2); - const oauthData: OauthDataDto = new OauthDataDto({ - system: new ProvisioningSystemDto({ - systemId: new ObjectId().toHexString(), - provisioningStrategy: SystemProvisioningStrategy.OIDC, - }), - externalSchool: externalSchoolDtoFactory.build(), - externalUser: externalUserDtoFactory.build({ externalId: externalUserId }), - externalGroups, - }); - - const user: UserDO = userDoFactory.withRoles([{ id: 'roleId', name: RoleName.USER }]).build({ - externalId: externalUserId, - }); - - schulconnexUserProvisioningService.provisionExternalUser.mockResolvedValueOnce(user); - - return { - oauthData, - }; - }; - - it('should not run group provisioning', async () => { - const { oauthData } = setup(); - - await strategy.apply(oauthData); - - expect(schulconnexGroupProvisioningService.provisionExternalGroup).not.toHaveBeenCalled(); - }); - }); - describe('when group data is not provided', () => { const setup = () => { config.FEATURE_SANIS_GROUP_PROVISIONING_ENABLED = true; diff --git a/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.ts index 0cd90ff4431..b965aabebcd 100644 --- a/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.ts @@ -45,16 +45,7 @@ export abstract class SchulconnexProvisioningStrategy extends ProvisioningStrate ); if (this.configService.get('FEATURE_SANIS_GROUP_PROVISIONING_ENABLED')) { - const usersInGroupsCount: number = - data.externalGroups?.reduce( - (count: number, group: ExternalGroupDto) => count + (group.otherUsers?.length ?? 0), - data.externalGroups?.length ?? 0 - ) ?? 0; - - const limit: number | undefined = this.configService.get('PROVISIONING_SCHULCONNEX_GROUP_USERS_LIMIT'); - if (!limit || usersInGroupsCount < limit) { - await this.provisionGroups(data, school); - } + await this.provisionGroups(data, school); } if (this.configService.get('FEATURE_SCHULCONNEX_MEDIA_LICENSE_ENABLED') && user.id) { From 56b36bad4a3a7929f19c87c594da0595d71026a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= Date: Fri, 13 Dec 2024 09:33:08 +0100 Subject: [PATCH 3/6] add loggable --- .../group-provisioning-info.loggable.spec.ts | 41 +++++++++++++++++++ .../group-provisioning-info.loggable.ts | 27 ++++++++++++ .../modules/provisioning/loggable/index.ts | 1 + .../strategy/schulconnex/sanis.strategy.ts | 7 ++-- .../schulconnex-response-mapper.ts | 2 +- .../schulconnex/schulconnex.strategy.ts | 12 +++++- .../testing/external-group-dto.factory.ts | 18 ++++++++ .../external-group-user-dto.factory.ts | 12 ++++++ .../src/modules/provisioning/testing/index.ts | 2 + 9 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 apps/server/src/modules/provisioning/loggable/group-provisioning-info.loggable.spec.ts create mode 100644 apps/server/src/modules/provisioning/loggable/group-provisioning-info.loggable.ts create mode 100644 apps/server/src/modules/provisioning/testing/external-group-dto.factory.ts create mode 100644 apps/server/src/modules/provisioning/testing/external-group-user-dto.factory.ts diff --git a/apps/server/src/modules/provisioning/loggable/group-provisioning-info.loggable.spec.ts b/apps/server/src/modules/provisioning/loggable/group-provisioning-info.loggable.spec.ts new file mode 100644 index 00000000000..9ca9ddf1990 --- /dev/null +++ b/apps/server/src/modules/provisioning/loggable/group-provisioning-info.loggable.spec.ts @@ -0,0 +1,41 @@ +import { externalGroupDtoFactory, externalGroupUserDtoFactory, externalUserDtoFactory } from '../testing'; +import { GroupProvisioningInfoLoggable } from './group-provisioning-info.loggable'; + +describe(GroupProvisioningInfoLoggable.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const groupCount = 2; + const otherUserCount = 5; + const totalUserCount = groupCount * otherUserCount + groupCount; + const externalUser = externalUserDtoFactory.build(); + const externalGroups = externalGroupDtoFactory.buildList(groupCount, { + otherUsers: externalGroupUserDtoFactory.buildList(otherUserCount), + }); + + const loggable = new GroupProvisioningInfoLoggable(externalUser, externalGroups, 100); + + return { + loggable, + externalUser, + totalUserCount, + groupCount, + }; + }; + + it('should return a loggable message', () => { + const { loggable, externalUser, totalUserCount, groupCount } = setup(); + + const message = loggable.getLogMessage(); + + expect(message).toEqual({ + message: 'Group provisioning has finished.', + data: { + externalUserId: externalUser.externalId, + groupCount, + userCount: totalUserCount, + durationMs: 100, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/provisioning/loggable/group-provisioning-info.loggable.ts b/apps/server/src/modules/provisioning/loggable/group-provisioning-info.loggable.ts new file mode 100644 index 00000000000..72800368868 --- /dev/null +++ b/apps/server/src/modules/provisioning/loggable/group-provisioning-info.loggable.ts @@ -0,0 +1,27 @@ +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; +import { ExternalGroupDto, ExternalUserDto } from '../dto'; + +export class GroupProvisioningInfoLoggable implements Loggable { + constructor( + private readonly groupUser: ExternalUserDto, + private readonly groups: ExternalGroupDto[], + private readonly durationMs: number + ) {} + + public getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + const userCount = this.groups.reduce( + (count: number, group: ExternalGroupDto) => count + (group.otherUsers?.length ?? 0), + this.groups.length + ); + + return { + message: 'Group provisioning has finished.', + data: { + externalUserId: this.groupUser.externalId, + groupCount: this.groups.length, + userCount, + durationMs: this.durationMs, + }, + }; + } +} diff --git a/apps/server/src/modules/provisioning/loggable/index.ts b/apps/server/src/modules/provisioning/loggable/index.ts index 01e7c2ae5cd..93010e22353 100644 --- a/apps/server/src/modules/provisioning/loggable/index.ts +++ b/apps/server/src/modules/provisioning/loggable/index.ts @@ -8,3 +8,4 @@ export { FetchingPoliciesInfoFailedLoggable } from './fetching-policies-info-fai export { PoliciesInfoErrorResponseLoggable } from './policies-info-error-response-loggable'; export { UserRoleUnknownLoggableException } from './user-role-unknown.loggable-exception'; export { SchoolMissingLoggableException } from './school-missing.loggable-exception'; +export { GroupProvisioningInfoLoggable } from './group-provisioning-info.loggable'; diff --git a/apps/server/src/modules/provisioning/strategy/schulconnex/sanis.strategy.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/sanis.strategy.ts index bc57f6fee50..6a441c35909 100644 --- a/apps/server/src/modules/provisioning/strategy/schulconnex/sanis.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/schulconnex/sanis.strategy.ts @@ -46,9 +46,9 @@ export class SanisProvisioningStrategy extends SchulconnexProvisioningStrategy { protected readonly schulconnexLicenseProvisioningService: SchulconnexLicenseProvisioningService, protected readonly schulconnexToolProvisioningService: SchulconnexToolProvisioningService, protected readonly configService: ConfigService, + protected readonly logger: Logger, private readonly responseMapper: SchulconnexResponseMapper, - private readonly schulconnexRestClient: SchulconnexRestClient, - private readonly logger: Logger + private readonly schulconnexRestClient: SchulconnexRestClient ) { super( schulconnexSchoolProvisioningService, @@ -58,7 +58,8 @@ export class SanisProvisioningStrategy extends SchulconnexProvisioningStrategy { schulconnexLicenseProvisioningService, schulconnexToolProvisioningService, groupService, - configService + configService, + logger ); } diff --git a/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex-response-mapper.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex-response-mapper.ts index 9b6631d9d38..07ce885a1b9 100644 --- a/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex-response-mapper.ts +++ b/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex-response-mapper.ts @@ -125,7 +125,7 @@ export class SchulconnexResponseMapper { groups.length ); const limit: number | undefined = this.configService.get('PROVISIONING_SCHULCONNEX_GROUP_USERS_LIMIT'); - const shouldProvisionOtherUsers: boolean = !limit || usersInGroupsCount < limit; + const shouldProvisionOtherUsers: boolean = limit === undefined || usersInGroupsCount < limit; const mapped: ExternalGroupDto[] = groups .map((group: SchulconnexGruppenResponse) => this.mapExternalGroup(source, group, shouldProvisionOtherUsers)) diff --git a/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.ts index b965aabebcd..f5b82fad2a6 100644 --- a/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.ts @@ -2,7 +2,9 @@ import { Group, GroupService } from '@modules/group'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { LegacySchoolDo, UserDO } from '@shared/domain/domainobject'; +import { Logger } from '@src/core/logger'; import { ExternalGroupDto, OauthDataDto, ProvisioningDto } from '../../dto'; +import { GroupProvisioningInfoLoggable } from '../../loggable'; import { ProvisioningConfig } from '../../provisioning.config'; import { ProvisioningStrategy } from '../base.strategy'; import { @@ -24,7 +26,8 @@ export abstract class SchulconnexProvisioningStrategy extends ProvisioningStrate protected readonly schulconnexLicenseProvisioningService: SchulconnexLicenseProvisioningService, protected readonly schulconnexToolProvisioningService: SchulconnexToolProvisioningService, protected readonly groupService: GroupService, - protected readonly configService: ConfigService + protected readonly configService: ConfigService, + protected readonly logger: Logger ) { super(); } @@ -61,6 +64,8 @@ export abstract class SchulconnexProvisioningStrategy extends ProvisioningStrate } private async provisionGroups(data: OauthDataDto, school?: LegacySchoolDo): Promise { + const startTime = performance.now(); + await this.removeUserFromGroups(data); if (data.externalGroups) { @@ -96,6 +101,11 @@ export abstract class SchulconnexProvisioningStrategy extends ProvisioningStrate await Promise.all(groupProvisioningPromises); } + + const endTime = performance.now(); + this.logger.warning( + new GroupProvisioningInfoLoggable(data.externalUser, data.externalGroups ?? [], endTime - startTime) + ); } private async removeUserFromGroups(data: OauthDataDto): Promise { diff --git a/apps/server/src/modules/provisioning/testing/external-group-dto.factory.ts b/apps/server/src/modules/provisioning/testing/external-group-dto.factory.ts new file mode 100644 index 00000000000..d33808d811a --- /dev/null +++ b/apps/server/src/modules/provisioning/testing/external-group-dto.factory.ts @@ -0,0 +1,18 @@ +import { UUID } from 'bson'; +import { Factory } from 'fishery'; +import { GroupTypes } from '../../group'; +import { ExternalGroupDto } from '../dto'; +import { externalGroupUserDtoFactory } from './external-group-user-dto.factory'; + +export const externalGroupDtoFactory = Factory.define( + ({ sequence }) => + new ExternalGroupDto({ + type: GroupTypes.CLASS, + name: `External Group ${sequence}`, + externalId: new UUID().toString(), + user: externalGroupUserDtoFactory.build(), + otherUsers: externalGroupUserDtoFactory.buildList(2), + from: new Date(), + until: new Date(), + }) +); diff --git a/apps/server/src/modules/provisioning/testing/external-group-user-dto.factory.ts b/apps/server/src/modules/provisioning/testing/external-group-user-dto.factory.ts new file mode 100644 index 00000000000..938eff3e073 --- /dev/null +++ b/apps/server/src/modules/provisioning/testing/external-group-user-dto.factory.ts @@ -0,0 +1,12 @@ +import { RoleName } from '@shared/domain/interface'; +import { UUID } from 'bson'; +import { Factory } from 'fishery'; +import { ExternalGroupUserDto } from '../dto'; + +export const externalGroupUserDtoFactory = Factory.define( + () => + new ExternalGroupUserDto({ + externalUserId: new UUID().toString(), + roleName: RoleName.TEACHER, + }) +); diff --git a/apps/server/src/modules/provisioning/testing/index.ts b/apps/server/src/modules/provisioning/testing/index.ts index 770f3e74f37..32854894142 100644 --- a/apps/server/src/modules/provisioning/testing/index.ts +++ b/apps/server/src/modules/provisioning/testing/index.ts @@ -1 +1,3 @@ export { externalUserDtoFactory } from './external-user-dto.factory'; +export { externalGroupDtoFactory } from './external-group-dto.factory'; +export { externalGroupUserDtoFactory } from './external-group-user-dto.factory'; From fc8ae4af159025f9076a1f281258701830ef46ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= Date: Fri, 13 Dec 2024 12:51:32 +0100 Subject: [PATCH 4/6] review changes --- .../group-provisioning-info.loggable.ts | 9 +- .../schulconnex/schulconnex.strategy.ts | 4 +- config/default.schema.json | 139 +++--------------- 3 files changed, 24 insertions(+), 128 deletions(-) diff --git a/apps/server/src/modules/provisioning/loggable/group-provisioning-info.loggable.ts b/apps/server/src/modules/provisioning/loggable/group-provisioning-info.loggable.ts index 72800368868..537a31e7855 100644 --- a/apps/server/src/modules/provisioning/loggable/group-provisioning-info.loggable.ts +++ b/apps/server/src/modules/provisioning/loggable/group-provisioning-info.loggable.ts @@ -1,12 +1,8 @@ import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; -import { ExternalGroupDto, ExternalUserDto } from '../dto'; +import { ExternalGroupDto } from '../dto'; export class GroupProvisioningInfoLoggable implements Loggable { - constructor( - private readonly groupUser: ExternalUserDto, - private readonly groups: ExternalGroupDto[], - private readonly durationMs: number - ) {} + constructor(private readonly groups: ExternalGroupDto[], private readonly durationMs: number) {} public getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { const userCount = this.groups.reduce( @@ -17,7 +13,6 @@ export class GroupProvisioningInfoLoggable implements Loggable { return { message: 'Group provisioning has finished.', data: { - externalUserId: this.groupUser.externalId, groupCount: this.groups.length, userCount, durationMs: this.durationMs, diff --git a/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.ts index f5b82fad2a6..1c3737a6877 100644 --- a/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.ts @@ -103,9 +103,7 @@ export abstract class SchulconnexProvisioningStrategy extends ProvisioningStrate } const endTime = performance.now(); - this.logger.warning( - new GroupProvisioningInfoLoggable(data.externalUser, data.externalGroups ?? [], endTime - startTime) - ); + this.logger.warning(new GroupProvisioningInfoLoggable(data.externalGroups ?? [], endTime - startTime)); } private async removeUserFromGroups(data: OauthDataDto): Promise { diff --git a/config/default.schema.json b/config/default.schema.json index 8b44ccda4cf..d3eae8b17ad 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -8,12 +8,7 @@ }, "NODE_ENV": { "type": "string", - "enum": [ - "development", - "test", - "production", - "migration" - ], + "enum": ["development", "test", "production", "migration"], "default": "production" }, "REQUEST_OPTION": { @@ -77,10 +72,7 @@ "DEFAULT_LANGUAGE": { "type": "string", "default": "de", - "enum": [ - "de", - "en" - ], + "enum": ["de", "en"], "description": "Value for the default language" }, "DEFAULT_TIMEZONE": { @@ -108,23 +100,13 @@ "TEACHER_VISIBILITY_FOR_EXTERNAL_TEAM_INVITATION": { "type": "string", "default": "disabled", - "enum": [ - "disabled", - "opt-in", - "opt-out", - "enabled" - ], + "enum": ["disabled", "opt-in", "opt-out", "enabled"], "description": "defines wheter external team invitation shows teachers from different schools or not. if enabled system wide there are options general enabled or opt-in/-out by user required." }, "STUDENT_TEAM_CREATION": { "type": "string", "default": "opt-out", - "enum": [ - "disabled", - "opt-in", - "opt-out", - "enabled" - ], + "enum": ["disabled", "opt-in", "opt-out", "enabled"], "description": "defines wheter students may create teams or not. if enabled system wide there are options general enabled or opt-in/-out by school admin required." }, "REDIS_URI": { @@ -300,13 +282,7 @@ "FILES_STORAGE": { "type": "object", "description": "Files storage server properties, required always to be defined", - "required": [ - "S3_ENDPOINT", - "S3_REGION", - "S3_BUCKET", - "S3_ACCESS_KEY_ID", - "S3_SECRET_ACCESS_KEY" - ], + "required": ["S3_ENDPOINT", "S3_REGION", "S3_BUCKET", "S3_ACCESS_KEY_ID", "S3_SECRET_ACCESS_KEY"], "properties": { "SERVICE_BASE_URL": { "type": "string", @@ -363,13 +339,7 @@ "FWU_CONTENT": { "type": "object", "description": "Properties of the S3 storage containing FWU content", - "required": [ - "S3_ENDPOINT", - "S3_REGION", - "S3_BUCKET", - "S3_ACCESS_KEY", - "S3_SECRET_KEY" - ], + "required": ["S3_ENDPOINT", "S3_REGION", "S3_BUCKET", "S3_ACCESS_KEY", "S3_SECRET_KEY"], "properties": { "S3_ENDPOINT": { "type": "string", @@ -408,12 +378,7 @@ "H5P_EDITOR": { "type": "object", "description": "Properties of the H5P server microservice and library management job", - "required": [ - "S3_ENDPOINT", - "S3_REGION", - "S3_BUCKET_CONTENT", - "S3_BUCKET_LIBRARIES" - ], + "required": ["S3_ENDPOINT", "S3_REGION", "S3_BUCKET_CONTENT", "S3_BUCKET_LIBRARIES"], "default": {}, "properties": { "S3_ENDPOINT": { @@ -473,12 +438,7 @@ "H5P_Library": { "type": "object", "description": "Properties of the H5P server microservice", - "required": [ - "S3_ENDPOINT", - "S3_BUCKET_LIBRARIES", - "S3_ACCESS_KEY_ID", - "S3_SECRET_ACCESS_KEY" - ], + "required": ["S3_ENDPOINT", "S3_BUCKET_LIBRARIES", "S3_ACCESS_KEY_ID", "S3_SECRET_ACCESS_KEY"], "default": {}, "properties": { "S3_ENDPOINT": { @@ -912,9 +872,7 @@ "ETHERPAD": { "type": "object", "description": "Etherpad settings", - "required": [ - "PAD_URI" - ], + "required": ["PAD_URI"], "properties": { "URI": { "type": "string", @@ -1072,9 +1030,7 @@ "API_VALIDATION_WHITELIST_EXTENSION": { "type": "string", "description": "when set, this is interpreted as a regex to extend the ignorelist for the API validation with any routes matching the regex.", - "examples": [ - ".*/courses/[0-9a-f]{24}($|/$)" - ] + "examples": [".*/courses/[0-9a-f]{24}($|/$)"] }, "FEATURE_PROMETHEUS_METRICS_ENABLED": { "type": "boolean", @@ -1149,31 +1105,13 @@ "type": "string", "default": "error", "description": "Log level for api.", - "enum": [ - "emerg", - "alert", - "crit", - "error", - "warning", - "notice", - "info", - "debug" - ] + "enum": ["emerg", "alert", "crit", "error", "warning", "notice", "info", "debug"] }, "NEST_LOG_LEVEL": { "type": "string", "default": "notice", "description": "Nest Log level for api. The http flag is for request logging. The http flag do only work by api methods with added 'request logging interceptor'.", - "enum": [ - "emerg", - "alert", - "crit", - "error", - "warning", - "notice", - "info", - "debug" - ] + "enum": ["emerg", "alert", "crit", "error", "warning", "notice", "info", "debug"] }, "EXIT_ON_ERROR": { "type": "boolean", @@ -1184,12 +1122,7 @@ "type": "string", "default": "requestError", "description": "Special logs.", - "enum": [ - "requestError", - "systemLogs", - "request", - "sendRequests" - ] + "enum": ["requestError", "systemLogs", "request", "sendRequests"] }, "SYNC_QUEUE_NAME": { "type": "string", @@ -1376,11 +1309,7 @@ "SAME_SITE": { "type": "string", "default": "none", - "enum": [ - "none", - "lax", - "strict" - ], + "enum": ["none", "lax", "strict"], "description": "Value for cookies sameSite property. When SECURE flag is false, 'None' is not allowed in SAME_SITE and Lax should be used as default instead" }, "HTTP_ONLY": { @@ -1404,13 +1333,7 @@ "description": "Expiration in seconds from now" } }, - "required": [ - "SAME_SITE", - "HTTP_ONLY", - "HOST_ONLY", - "SECURE", - "EXPIRES_SECONDS" - ], + "required": ["SAME_SITE", "HTTP_ONLY", "HOST_ONLY", "SECURE", "EXPIRES_SECONDS"], "allOf": [ { "$ref": "#/properties/COOKIE/definitions/SAME_SITE_SECURE_VALID" @@ -1428,10 +1351,7 @@ "then": { "properties": { "SAME_SITE": { - "enum": [ - "lax", - "strict" - ] + "enum": ["lax", "strict"] } } } @@ -1703,9 +1623,7 @@ "type": "string", "default": "image/png,image/jpeg,image/gif,image/svg+xml", "description": "List with allowed assets MIME types, comma separated, empty if all MIME types supported by tldraw should be allowed", - "examples": [ - "image/gif,image/jpeg,video/webm" - ] + "examples": ["image/gif,image/jpeg,video/webm"] }, "PERFORMANCE_MEASURE_ENABLED": { "type": "boolean", @@ -1716,16 +1634,7 @@ "type": "string", "default": "info", "description": "Define log level for tldraw.", - "enum": [ - "emerg", - "alert", - "crit", - "error", - "warning", - "notice", - "info", - "debug" - ] + "enum": ["emerg", "alert", "crit", "error", "warning", "notice", "info", "debug"] } } }, @@ -1746,16 +1655,12 @@ "API_URL": { "type": "string", "description": "Base URL of the schulconnex API (from dof)", - "examples": [ - "https://api-dienste.stage.niedersachsen-login.schule/v1/" - ] + "examples": ["https://api-dienste.stage.niedersachsen-login.schule/v1/"] }, "TOKEN_ENDPOINT": { "type": "string", "description": "Token endpoint of the schulconnex API (from dof)", - "examples": [ - "https://api-dienste.stage.niedersachsen-login.schule/v1/oauth2/token" - ] + "examples": ["https://api-dienste.stage.niedersachsen-login.schule/v1/oauth2/token"] }, "CLIENT_ID": { "type": "string", @@ -1831,9 +1736,7 @@ "type": "string", "default": "", "description": "URL for fetching policies info from moin.schule schulconnex", - "examples": [ - "https://api-dienste.stage.niedersachsen-login.schule/v1/policies-info" - ] + "examples": ["https://api-dienste.stage.niedersachsen-login.schule/v1/policies-info"] }, "PROVISIONING_SCHULCONNEX_GROUP_USERS_LIMIT": { "type": "number", From ce65545eaff6c692966c0ce2e4cdfee9b0bce7b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= Date: Fri, 13 Dec 2024 13:15:38 +0100 Subject: [PATCH 5/6] fix test --- .../loggable/group-provisioning-info.loggable.spec.ts | 9 +++------ .../strategy/schulconnex/schulconnex.strategy.spec.ts | 6 +++++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/server/src/modules/provisioning/loggable/group-provisioning-info.loggable.spec.ts b/apps/server/src/modules/provisioning/loggable/group-provisioning-info.loggable.spec.ts index 9ca9ddf1990..fb67f57ea03 100644 --- a/apps/server/src/modules/provisioning/loggable/group-provisioning-info.loggable.spec.ts +++ b/apps/server/src/modules/provisioning/loggable/group-provisioning-info.loggable.spec.ts @@ -1,4 +1,4 @@ -import { externalGroupDtoFactory, externalGroupUserDtoFactory, externalUserDtoFactory } from '../testing'; +import { externalGroupDtoFactory, externalGroupUserDtoFactory } from '../testing'; import { GroupProvisioningInfoLoggable } from './group-provisioning-info.loggable'; describe(GroupProvisioningInfoLoggable.name, () => { @@ -7,30 +7,27 @@ describe(GroupProvisioningInfoLoggable.name, () => { const groupCount = 2; const otherUserCount = 5; const totalUserCount = groupCount * otherUserCount + groupCount; - const externalUser = externalUserDtoFactory.build(); const externalGroups = externalGroupDtoFactory.buildList(groupCount, { otherUsers: externalGroupUserDtoFactory.buildList(otherUserCount), }); - const loggable = new GroupProvisioningInfoLoggable(externalUser, externalGroups, 100); + const loggable = new GroupProvisioningInfoLoggable(externalGroups, 100); return { loggable, - externalUser, totalUserCount, groupCount, }; }; it('should return a loggable message', () => { - const { loggable, externalUser, totalUserCount, groupCount } = setup(); + const { loggable, totalUserCount, groupCount } = setup(); const message = loggable.getLogMessage(); expect(message).toEqual({ message: 'Group provisioning has finished.', data: { - externalUserId: externalUser.externalId, groupCount, userCount: totalUserCount, durationMs: 100, diff --git a/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.spec.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.spec.ts index 26fbc0202df..44b03e6a6db 100644 --- a/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.spec.ts @@ -1,7 +1,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { Group, GroupService } from '@modules/group'; -import { NotImplementedException } from '@nestjs/common'; +import { Logger, NotImplementedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { LegacySchoolDo, UserDO } from '@shared/domain/domainobject'; @@ -98,6 +98,10 @@ describe(SchulconnexProvisioningStrategy.name, () => { get: jest.fn().mockImplementation((key: keyof ProvisioningConfig) => config[key]), }, }, + { + provide: Logger, + useValue: createMock(), + }, ], }).compile(); From 6304ca41ff42c150d77a5b56de1210789999aed4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= Date: Fri, 13 Dec 2024 13:54:54 +0100 Subject: [PATCH 6/6] fix import --- .../strategy/schulconnex/schulconnex.strategy.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.spec.ts b/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.spec.ts index 44b03e6a6db..f86346d37eb 100644 --- a/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/schulconnex/schulconnex.strategy.spec.ts @@ -1,7 +1,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { Group, GroupService } from '@modules/group'; -import { Logger, NotImplementedException } from '@nestjs/common'; +import { NotImplementedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { LegacySchoolDo, UserDO } from '@shared/domain/domainobject'; @@ -14,6 +14,7 @@ import { legacySchoolDoFactory, userDoFactory, } from '@shared/testing'; +import { Logger } from '@src/core/logger'; import { ExternalGroupDto, ExternalSchoolDto,