diff --git a/apps/server/src/infra/etherpad-client/etherpad-client.adapter.spec.ts b/apps/server/src/infra/etherpad-client/etherpad-client.adapter.spec.ts index a85501f9357..e8708d5ca88 100644 --- a/apps/server/src/infra/etherpad-client/etherpad-client.adapter.spec.ts +++ b/apps/server/src/infra/etherpad-client/etherpad-client.adapter.spec.ts @@ -995,7 +995,12 @@ describe(EtherpadClientAdapter.name, () => { it('should throw EtherpadErrorLoggableException', async () => { const groupId = setup(); - const exception = new EtherpadErrorLoggableException(EtherpadErrorType.INTERNAL_ERROR, { padId: groupId }, {}); + const exception = new EtherpadErrorLoggableException( + EtherpadErrorType.INTERNAL_ERROR, + { padId: groupId }, + undefined, + {} + ); await expect(service.deleteGroup(groupId)).rejects.toThrowError(exception); }); }); @@ -1084,7 +1089,12 @@ describe(EtherpadClientAdapter.name, () => { it('should throw EtherpadErrorLoggableException', async () => { const sessionId = setup(); - const exception = new EtherpadErrorLoggableException(EtherpadErrorType.BAD_REQUEST, { sessionId }, {}); + const exception = new EtherpadErrorLoggableException( + EtherpadErrorType.BAD_REQUEST, + { sessionId }, + undefined, + {} + ); await expect(service.deleteSession(sessionId)).rejects.toThrowError(exception); }); }); @@ -1150,7 +1160,7 @@ describe(EtherpadClientAdapter.name, () => { it('should throw EtherpadErrorLoggableException', async () => { const padId = setup(); - const exception = new EtherpadErrorLoggableException(EtherpadErrorType.BAD_REQUEST, { padId }, {}); + const exception = new EtherpadErrorLoggableException(EtherpadErrorType.BAD_REQUEST, { padId }, undefined, {}); await expect(service.deletePad(padId)).rejects.toThrowError(exception); }); }); diff --git a/apps/server/src/infra/etherpad-client/loggable/etherpad-error-loggable-exception.ts b/apps/server/src/infra/etherpad-client/loggable/etherpad-error-loggable-exception.ts index 93fa4140076..4ce8c6ba1da 100644 --- a/apps/server/src/infra/etherpad-client/loggable/etherpad-error-loggable-exception.ts +++ b/apps/server/src/infra/etherpad-client/loggable/etherpad-error-loggable-exception.ts @@ -6,6 +6,7 @@ export class EtherpadErrorLoggableException extends InternalServerErrorException constructor( private readonly type: EtherpadErrorType, private readonly payload: EtherpadParams, + private readonly originalMessage: string | undefined, private readonly exceptionOptions: HttpExceptionOptions ) { super(type, exceptionOptions); @@ -20,6 +21,7 @@ export class EtherpadErrorLoggableException extends InternalServerErrorException data: { userId, parentId, + originalMessage: this.originalMessage, }, }; diff --git a/apps/server/src/infra/etherpad-client/loggable/etherpad-server-error-exception.spec.ts b/apps/server/src/infra/etherpad-client/loggable/etherpad-server-error-exception.spec.ts index 63cbfadfabb..b81c0398ed5 100644 --- a/apps/server/src/infra/etherpad-client/loggable/etherpad-server-error-exception.spec.ts +++ b/apps/server/src/infra/etherpad-client/loggable/etherpad-server-error-exception.spec.ts @@ -13,7 +13,7 @@ describe('EtherpadErrorLoggableException', () => { const error = new Error('error'); const httpExceptionOptions = ErrorUtils.createHttpExceptionOptions(error); - const exception = new EtherpadErrorLoggableException(type, payload, httpExceptionOptions); + const exception = new EtherpadErrorLoggableException(type, payload, 'hugo ist nudeln', httpExceptionOptions); const result = exception.getLogMessage(); expect(result).toStrictEqual({ @@ -22,6 +22,7 @@ describe('EtherpadErrorLoggableException', () => { data: { userId: 'userId', parentId: 'parentId', + originalMessage: 'hugo ist nudeln', }, }); }); diff --git a/apps/server/src/infra/etherpad-client/mappers/etherpad-response.mapper.ts b/apps/server/src/infra/etherpad-client/mappers/etherpad-response.mapper.ts index cadab81fc7f..0857b3c502b 100644 --- a/apps/server/src/infra/etherpad-client/mappers/etherpad-response.mapper.ts +++ b/apps/server/src/infra/etherpad-client/mappers/etherpad-response.mapper.ts @@ -62,7 +62,12 @@ export class EtherpadResponseMapper { payload: EtherpadParams, response: T | Error ): EtherpadErrorLoggableException { - return new EtherpadErrorLoggableException(type, payload, ErrorUtils.createHttpExceptionOptions(response.message)); + return new EtherpadErrorLoggableException( + type, + payload, + response.message, + ErrorUtils.createHttpExceptionOptions(response.message) + ); } static mapEtherpadSessionsToSessions(etherpadSessions: unknown): Session[] { 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/authorization/domain/mapper/authorization-context.builder.ts b/apps/server/src/modules/authorization/domain/mapper/authorization-context.builder.ts index 14ba6714cca..5ad33af1d35 100644 --- a/apps/server/src/modules/authorization/domain/mapper/authorization-context.builder.ts +++ b/apps/server/src/modules/authorization/domain/mapper/authorization-context.builder.ts @@ -8,13 +8,13 @@ export class AuthorizationContextBuilder { return context; } - static write(requiredPermissions: Permission[]): AuthorizationContext { + public static write(requiredPermissions: Permission[]): AuthorizationContext { const context = this.build(requiredPermissions, Action.write); return context; } - static read(requiredPermissions: Permission[]): AuthorizationContext { + public static read(requiredPermissions: Permission[]): AuthorizationContext { const context = this.build(requiredPermissions, Action.read); return context; diff --git a/apps/server/src/modules/board/controller/api-test/board-copy-in-room.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-copy-in-room.api.spec.ts index d833eb510b2..5e02e65be08 100644 --- a/apps/server/src/modules/board/controller/api-test/board-copy-in-room.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-copy-in-room.api.spec.ts @@ -6,6 +6,7 @@ import { cleanupCollections, groupEntityFactory, roleFactory, + schoolEntityFactory, TestApiClient, UserAndAccountTestFactory, } from '@shared/testing'; @@ -49,12 +50,17 @@ describe(`board copy with room relation (api)`, () => { name: RoleName.ROOMEDITOR, permissions: [Permission.ROOM_EDIT], }); - const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); + const school = schoolEntityFactory.buildWithId(); + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }); const userGroup = groupEntityFactory.buildWithId({ type: GroupEntityTypes.ROOM, users: [{ role, user: teacherUser }], }); - const roomMembership = roomMembershipEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); + const roomMembership = roomMembershipEntityFactory.build({ + roomId: room.id, + userGroupId: userGroup.id, + schoolId: teacherUser.school.id, + }); const columnBoardNode = columnBoardEntityFactory.build({ ...columnBoardProps, context: { id: room.id, type: BoardExternalReferenceType.Room }, diff --git a/apps/server/src/modules/board/controller/api-test/board-create-in-room.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-create-in-room.api.spec.ts index bb10e3fb33c..bb439583707 100644 --- a/apps/server/src/modules/board/controller/api-test/board-create-in-room.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-create-in-room.api.spec.ts @@ -4,7 +4,14 @@ import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Permission } from '@shared/domain/interface'; import { RoleName } from '@shared/domain/interface/rolename.enum'; -import { cleanupCollections, groupEntityFactory, roleFactory, TestApiClient, userFactory } from '@shared/testing'; +import { + cleanupCollections, + groupEntityFactory, + roleFactory, + schoolEntityFactory, + TestApiClient, + userFactory, +} from '@shared/testing'; import { accountFactory } from '@src/modules/account/testing'; import { GroupEntityTypes } from '@src/modules/group/entity'; import { roomMembershipEntityFactory } from '@src/modules/room-membership/testing'; @@ -42,7 +49,8 @@ describe(`create board in room (api)`, () => { describe('When request is valid', () => { describe('When user is allowed to edit the room', () => { const setup = async () => { - const user = userFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); + const user = userFactory.buildWithId({ school }); const account = accountFactory.withUser(user).build(); const role = roleFactory.buildWithId({ name: RoleName.ROOMEDITOR, permissions: [Permission.ROOM_EDIT] }); @@ -52,9 +60,13 @@ describe(`create board in room (api)`, () => { users: [{ user, role }], }); - const room = roomEntityFactory.buildWithId(); + const room = roomEntityFactory.buildWithId({ schoolId: user.school.id }); - const roomMembership = roomMembershipEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); + const roomMembership = roomMembershipEntityFactory.build({ + roomId: room.id, + userGroupId: userGroup.id, + schoolId: user.school.id, + }); await em.persistAndFlush([account, user, role, userGroup, room, roomMembership]); em.clear(); diff --git a/apps/server/src/modules/board/service/internal/board-context.service.spec.ts b/apps/server/src/modules/board/service/internal/board-context.service.spec.ts index 489c09f8a8f..322d9ea4361 100644 --- a/apps/server/src/modules/board/service/internal/board-context.service.spec.ts +++ b/apps/server/src/modules/board/service/internal/board-context.service.spec.ts @@ -236,6 +236,7 @@ describe(BoardContextService.name, () => { id: 'foo', roomId: columnBoard.context.id, members: [{ userId: user.id, roles: [role] }], + schoolId: user.school.id, }); const result = await service.getUsersWithBoardRoles(columnBoard); @@ -271,6 +272,7 @@ describe(BoardContextService.name, () => { id: 'foo', roomId: columnBoard.context.id, members: [{ userId: user.id, roles: [role] }], + schoolId: user.school.id, }); const result = await service.getUsersWithBoardRoles(columnBoard); @@ -306,6 +308,7 @@ describe(BoardContextService.name, () => { id: 'foo', roomId: columnBoard.context.id, members: [{ userId: user.id, roles: [role] }], + schoolId: user.school.id, }); const result = await service.getUsersWithBoardRoles(columnBoard); 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/room-membership/authorization/room-membership.rule.spec.ts b/apps/server/src/modules/room-membership/authorization/room-membership.rule.spec.ts index 6946c83aa8d..0326bb2d02b 100644 --- a/apps/server/src/modules/room-membership/authorization/room-membership.rule.spec.ts +++ b/apps/server/src/modules/room-membership/authorization/room-membership.rule.spec.ts @@ -1,7 +1,8 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { Permission } from '@shared/domain/interface'; -import { roleDtoFactory, setupEntities, userFactory } from '@shared/testing'; import { Action, AuthorizationHelper, AuthorizationInjectionService } from '@modules/authorization'; +import { roomFactory } from '@modules/room/testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Permission, RoleName } from '@shared/domain/interface'; +import { roleDtoFactory, roleFactory, schoolEntityFactory, setupEntities, userFactory } from '@shared/testing'; import { RoomMembershipAuthorizable } from '../do/room-membership-authorizable.do'; import { RoomMembershipRule } from './room-membership.rule'; @@ -30,7 +31,7 @@ describe(RoomMembershipRule.name, () => { describe('when entity is applicable', () => { const setup = () => { const user = userFactory.buildWithId(); - const roomMembershipAuthorizable = new RoomMembershipAuthorizable('', []); + const roomMembershipAuthorizable = new RoomMembershipAuthorizable('', [], user.school.id); return { user, roomMembershipAuthorizable }; }; @@ -60,66 +61,135 @@ describe(RoomMembershipRule.name, () => { }); describe('hasPermission', () => { - describe('when user is viewer member of room', () => { - const setup = () => { - const user = userFactory.buildWithId(); - const roleDto = roleDtoFactory.build({ permissions: [Permission.ROOM_VIEW] }); - const roomMembershipAuthorizable = new RoomMembershipAuthorizable('', [{ roles: [roleDto], userId: user.id }]); + describe("when user's primary school is room's school", () => { + describe('when user is not member of the room', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const roomMembershipAuthorizable = new RoomMembershipAuthorizable('', [], user.school.id); - return { user, roomMembershipAuthorizable }; - }; + return { user, roomMembershipAuthorizable }; + }; - it('should return "true" for read action', () => { - const { user, roomMembershipAuthorizable } = setup(); + it('should return "false" for read action', () => { + const { user, roomMembershipAuthorizable } = setup(); - const res = service.hasPermission(user, roomMembershipAuthorizable, { - action: Action.read, - requiredPermissions: [], - }); + const res = service.hasPermission(user, roomMembershipAuthorizable, { + action: Action.read, + requiredPermissions: [], + }); - expect(res).toBe(true); + expect(res).toBe(false); + }); }); - it('should return "false" for write action', () => { - const { user, roomMembershipAuthorizable } = setup(); + describe('when user has view permission for room', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const roleDto = roleDtoFactory.build({ permissions: [Permission.ROOM_VIEW] }); + const roomMembershipAuthorizable = new RoomMembershipAuthorizable( + '', + [{ roles: [roleDto], userId: user.id }], + user.school.id + ); + + return { user, roomMembershipAuthorizable }; + }; + + it('should return "true" for read action', () => { + const { user, roomMembershipAuthorizable } = setup(); - const res = service.hasPermission(user, roomMembershipAuthorizable, { - action: Action.write, - requiredPermissions: [], + const res = service.hasPermission(user, roomMembershipAuthorizable, { + action: Action.read, + requiredPermissions: [], + }); + + expect(res).toBe(true); }); - expect(res).toBe(false); + it('should return "false" for write action', () => { + const { user, roomMembershipAuthorizable } = setup(); + + const res = service.hasPermission(user, roomMembershipAuthorizable, { + action: Action.write, + requiredPermissions: [], + }); + + expect(res).toBe(false); + }); }); - }); - describe('when user is not member of room', () => { - const setup = () => { - const user = userFactory.buildWithId(); - const roomMembershipAuthorizable = new RoomMembershipAuthorizable('', []); + describe('when user is not member of room', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const roomMembershipAuthorizable = new RoomMembershipAuthorizable('', [], user.school.id); - return { user, roomMembershipAuthorizable }; - }; + return { user, roomMembershipAuthorizable }; + }; - it('should return "false" for read action', () => { - const { user, roomMembershipAuthorizable } = setup(); + it('should return "false" for read action', () => { + const { user, roomMembershipAuthorizable } = setup(); - const res = service.hasPermission(user, roomMembershipAuthorizable, { - action: Action.read, - requiredPermissions: [], + const res = service.hasPermission(user, roomMembershipAuthorizable, { + action: Action.read, + requiredPermissions: [], + }); + + expect(res).toBe(false); }); - expect(res).toBe(false); - }); + it('should return "false" for write action', () => { + const { user, roomMembershipAuthorizable } = setup(); - it('should return "false" for write action', () => { - const { user, roomMembershipAuthorizable } = setup(); + const res = service.hasPermission(user, roomMembershipAuthorizable, { + action: Action.write, + requiredPermissions: [], + }); + + expect(res).toBe(false); + }); + }); + }); - const res = service.hasPermission(user, roomMembershipAuthorizable, { - action: Action.write, - requiredPermissions: [], + describe("when user is guest at room's school", () => { + describe('when user has view permission for room', () => { + const setup = () => { + const otherSchool = schoolEntityFactory.buildWithId(); + const guestTeacherRole = roleFactory.buildWithId({ name: RoleName.GUESTTEACHER }); + const user = userFactory.buildWithId({ + secondarySchools: [{ school: otherSchool, role: guestTeacherRole }], + }); + const room = roomFactory.build({ schoolId: otherSchool.id }); + const roleDto = roleDtoFactory.build({ permissions: [Permission.ROOM_VIEW] }); + const roomMembershipAuthorizable = new RoomMembershipAuthorizable( + room.id, + [{ roles: [roleDto], userId: user.id }], + otherSchool.id + ); + + return { user, roomMembershipAuthorizable }; + }; + + it('should return "true" for read action', () => { + const { user, roomMembershipAuthorizable } = setup(); + + const res = service.hasPermission(user, roomMembershipAuthorizable, { + action: Action.read, + requiredPermissions: [], + }); + + expect(res).toBe(true); }); - expect(res).toBe(false); + it('should return "false" for write action', () => { + const { user, roomMembershipAuthorizable } = setup(); + + const res = service.hasPermission(user, roomMembershipAuthorizable, { + action: Action.write, + requiredPermissions: [], + }); + + expect(res).toBe(false); + }); }); }); }); diff --git a/apps/server/src/modules/room-membership/authorization/room-membership.rule.ts b/apps/server/src/modules/room-membership/authorization/room-membership.rule.ts index cfcd11c33af..3336e93892f 100644 --- a/apps/server/src/modules/room-membership/authorization/room-membership.rule.ts +++ b/apps/server/src/modules/room-membership/authorization/room-membership.rule.ts @@ -1,7 +1,7 @@ +import { Action, AuthorizationContext, AuthorizationInjectionService, Rule } from '@modules/authorization'; import { Injectable } from '@nestjs/common'; import { User } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; -import { AuthorizationInjectionService, Action, AuthorizationContext, Rule } from '@modules/authorization'; import { RoomMembershipAuthorizable } from '../do/room-membership-authorizable.do'; @Injectable() @@ -17,6 +17,14 @@ export class RoomMembershipRule implements Rule { } public hasPermission(user: User, object: RoomMembershipAuthorizable, context: AuthorizationContext): boolean { + const primarySchoolId = user.school.id; + const secondarySchools = user.secondarySchools ?? []; + const secondarySchoolIds = secondarySchools.map(({ school }) => school.id); + + if (![primarySchoolId, ...secondarySchoolIds].includes(object.schoolId)) { + return false; + } + const { action } = context; const permissionsThisUserHas = object.members .filter((member) => member.userId === user.id) diff --git a/apps/server/src/modules/room-membership/do/room-membership-authorizable.do.ts b/apps/server/src/modules/room-membership/do/room-membership-authorizable.do.ts index 61821fa4b82..dbd969a84da 100644 --- a/apps/server/src/modules/room-membership/do/room-membership-authorizable.do.ts +++ b/apps/server/src/modules/room-membership/do/room-membership-authorizable.do.ts @@ -12,10 +12,13 @@ export class RoomMembershipAuthorizable implements AuthorizableObject { public readonly roomId: EntityId; + public readonly schoolId: EntityId; + public readonly members: UserWithRoomRoles[]; - public constructor(roomId: EntityId, members: UserWithRoomRoles[]) { + constructor(roomId: EntityId, members: UserWithRoomRoles[], schoolId: EntityId) { this.members = members; this.roomId = roomId; + this.schoolId = schoolId; } } diff --git a/apps/server/src/modules/room-membership/service/room-membership.service.spec.ts b/apps/server/src/modules/room-membership/service/room-membership.service.spec.ts index 6f6b39139b2..98763e33358 100644 --- a/apps/server/src/modules/room-membership/service/room-membership.service.spec.ts +++ b/apps/server/src/modules/room-membership/service/room-membership.service.spec.ts @@ -371,6 +371,7 @@ describe('RoomMembershipService', () => { it('should return empty RoomMembershipAuthorizable when roomMembership not exists', async () => { const roomId = 'nonexistent'; roomMembershipRepo.findByRoomId.mockResolvedValue(null); + roomService.getSingleRoom.mockResolvedValue(roomFactory.build({ id: roomId })); const result = await service.getRoomMembershipAuthorizable(roomId); diff --git a/apps/server/src/modules/room-membership/service/room-membership.service.ts b/apps/server/src/modules/room-membership/service/room-membership.service.ts index 59fa6167c6d..8baa23dd643 100644 --- a/apps/server/src/modules/room-membership/service/room-membership.service.ts +++ b/apps/server/src/modules/room-membership/service/room-membership.service.ts @@ -51,7 +51,8 @@ export class RoomMembershipService { private buildRoomMembershipAuthorizable( roomId: EntityId, group: Group, - roleSet: RoleDto[] + roleSet: RoleDto[], + schoolId: EntityId ): RoomMembershipAuthorizable { const members = group.users.map((groupUser): UserWithRoomRoles => { const roleDto = roleSet.find((role) => role.id === groupUser.roleId); @@ -62,7 +63,7 @@ export class RoomMembershipService { }; }); - const roomMembershipAuthorizable = new RoomMembershipAuthorizable(roomId, members); + const roomMembershipAuthorizable = new RoomMembershipAuthorizable(roomId, members, schoolId); return roomMembershipAuthorizable; } @@ -120,7 +121,7 @@ export class RoomMembershipService { .map((item) => { const group = groupPage.data.find((g) => g.id === item.userGroupId); if (!group) return null; - return this.buildRoomMembershipAuthorizable(item.roomId, group, roleSet); + return this.buildRoomMembershipAuthorizable(item.roomId, group, roleSet, item.schoolId); }) .filter((item): item is RoomMembershipAuthorizable => item !== null); @@ -130,7 +131,8 @@ export class RoomMembershipService { public async getRoomMembershipAuthorizable(roomId: EntityId): Promise { const roomMembership = await this.roomMembershipRepo.findByRoomId(roomId); if (roomMembership === null) { - return new RoomMembershipAuthorizable(roomId, []); + const room = await this.roomService.getSingleRoom(roomId); + return new RoomMembershipAuthorizable(roomId, [], room.schoolId); } const group = await this.groupService.findById(roomMembership.userGroupId); const roleSet = await this.roleService.findByIds(group.users.map((groupUser) => groupUser.roleId)); @@ -144,7 +146,7 @@ export class RoomMembershipService { }; }); - const roomMembershipAuthorizable = new RoomMembershipAuthorizable(roomId, members); + const roomMembershipAuthorizable = new RoomMembershipAuthorizable(roomId, members, roomMembership.schoolId); return roomMembershipAuthorizable; } diff --git a/apps/server/src/modules/room/api/test/room-add-members.api.spec.ts b/apps/server/src/modules/room/api/test/room-add-members.api.spec.ts index a1ed8579853..ad8f5e6a3b7 100644 --- a/apps/server/src/modules/room/api/test/room-add-members.api.spec.ts +++ b/apps/server/src/modules/room/api/test/room-add-members.api.spec.ts @@ -65,7 +65,11 @@ describe('Room Controller (API)', () => { externalSource: undefined, }); - const roomMemberships = roomMembershipEntityFactory.build({ userGroupId: userGroupEntity.id, roomId: room.id }); + const roomMemberships = roomMembershipEntityFactory.build({ + userGroupId: userGroupEntity.id, + roomId: room.id, + schoolId: school.id, + }); await em.persistAndFlush([ room, roomMemberships, @@ -87,7 +91,9 @@ describe('Room Controller (API)', () => { describe('when the user is not authenticated', () => { it('should return a 401 error', async () => { const { room } = await setupRoomWithMembers(); + const response = await testApiClient.patch(`/${room.id}/members/add`); + expect(response.status).toBe(HttpStatus.UNAUTHORIZED); }); }); @@ -96,7 +102,9 @@ describe('Room Controller (API)', () => { const setupLoggedInUser = async () => { const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); await em.persistAndFlush([teacherAccount, teacherUser]); + const loggedInClient = await testApiClient.login(teacherAccount); + return { loggedInClient }; }; diff --git a/apps/server/src/modules/room/api/test/room-delete.api.spec.ts b/apps/server/src/modules/room/api/test/room-delete.api.spec.ts index 22f74c7edc8..4e8be194dfe 100644 --- a/apps/server/src/modules/room/api/test/room-delete.api.spec.ts +++ b/apps/server/src/modules/room/api/test/room-delete.api.spec.ts @@ -10,6 +10,7 @@ import { cleanupCollections, groupEntityFactory, roleFactory, + schoolEntityFactory, } from '@shared/testing'; import { RoomMembershipEntity } from '@src/modules/room-membership'; import { roomMembershipEntityFactory } from '@src/modules/room-membership/testing/room-membership-entity.factory'; @@ -99,12 +100,17 @@ describe('Room Controller (API)', () => { name: RoleName.ROOMEDITOR, permissions: [Permission.ROOM_EDIT], }); - const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); + const school = schoolEntityFactory.buildWithId(); + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }); const userGroup = groupEntityFactory.buildWithId({ type: GroupEntityTypes.ROOM, users: [{ role, user: teacherUser }], }); - const roomMembership = roomMembershipEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); + const roomMembership = roomMembershipEntityFactory.build({ + roomId: room.id, + userGroupId: userGroup.id, + schoolId: teacherUser.school.id, + }); await em.persistAndFlush([room, roomMembership, teacherAccount, teacherUser, userGroup, role]); em.clear(); diff --git a/apps/server/src/modules/room/api/test/room-get-boards.api.spec.ts b/apps/server/src/modules/room/api/test/room-get-boards.api.spec.ts index 4f1646ec708..2f5dc502c70 100644 --- a/apps/server/src/modules/room/api/test/room-get-boards.api.spec.ts +++ b/apps/server/src/modules/room/api/test/room-get-boards.api.spec.ts @@ -6,6 +6,7 @@ import { cleanupCollections, groupEntityFactory, roleFactory, + schoolEntityFactory, TestApiClient, UserAndAccountTestFactory, } from '@shared/testing'; @@ -94,11 +95,12 @@ describe('Room Controller (API)', () => { describe('when the user has the required permissions', () => { const setup = async () => { - const room = roomEntityFactory.build(); + const school = schoolEntityFactory.buildWithId(); + const room = roomEntityFactory.build({ schoolId: school.id }); const board = columnBoardEntityFactory.build({ context: { type: BoardExternalReferenceType.Room, id: room.id }, }); - const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); + const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent({ school }); const role = roleFactory.buildWithId({ name: RoleName.ROOMVIEWER, permissions: [Permission.ROOM_VIEW], @@ -109,7 +111,11 @@ describe('Room Controller (API)', () => { organization: studentUser.school, externalSource: undefined, }); - const roomMembership = roomMembershipEntityFactory.build({ userGroupId: userGroupEntity.id, roomId: room.id }); + const roomMembership = roomMembershipEntityFactory.build({ + userGroupId: userGroupEntity.id, + roomId: room.id, + schoolId: school.id, + }); await em.persistAndFlush([room, board, studentAccount, studentUser, role, userGroupEntity, roomMembership]); em.clear(); diff --git a/apps/server/src/modules/room/api/test/room-get.api.spec.ts b/apps/server/src/modules/room/api/test/room-get.api.spec.ts index 719889d82ec..b0aa9afcc68 100644 --- a/apps/server/src/modules/room/api/test/room-get.api.spec.ts +++ b/apps/server/src/modules/room/api/test/room-get.api.spec.ts @@ -8,6 +8,7 @@ import { cleanupCollections, groupEntityFactory, roleFactory, + schoolEntityFactory, } from '@shared/testing'; import { GroupEntityTypes } from '@modules/group/entity/group.entity'; import { roomMembershipEntityFactory } from '@src/modules/room-membership/testing'; @@ -92,8 +93,9 @@ describe('Room Controller (API)', () => { describe('when the user has the required permissions', () => { const setup = async () => { - const room = roomEntityFactory.build(); - const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); + const school = schoolEntityFactory.buildWithId(); + const room = roomEntityFactory.build({ schoolId: school.id }); + const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent({ school }); const role = roleFactory.buildWithId({ name: RoleName.ROOMVIEWER, permissions: [Permission.ROOM_VIEW], @@ -104,7 +106,11 @@ describe('Room Controller (API)', () => { organization: studentUser.school, externalSource: undefined, }); - const roomMembership = roomMembershipEntityFactory.build({ userGroupId: userGroupEntity.id, roomId: room.id }); + const roomMembership = roomMembershipEntityFactory.build({ + userGroupId: userGroupEntity.id, + roomId: room.id, + schoolId: school.id, + }); await em.persistAndFlush([room, studentAccount, studentUser, role, userGroupEntity, roomMembership]); em.clear(); diff --git a/apps/server/src/modules/room/api/test/room-index.api.spec.ts b/apps/server/src/modules/room/api/test/room-index.api.spec.ts index cbb68d0f38c..162fb503a1b 100644 --- a/apps/server/src/modules/room/api/test/room-index.api.spec.ts +++ b/apps/server/src/modules/room/api/test/room-index.api.spec.ts @@ -9,6 +9,7 @@ import { cleanupCollections, groupEntityFactory, roleFactory, + schoolEntityFactory, } from '@shared/testing'; import { GroupEntityTypes } from '@modules/group/entity/group.entity'; import { roomMembershipEntityFactory } from '@src/modules/room-membership/testing/room-membership-entity.factory'; @@ -128,8 +129,9 @@ describe('Room Controller (API)', () => { describe('when the user has the required permissions', () => { const setup = async () => { - const rooms = roomEntityFactory.buildListWithId(2); - const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); + const school = schoolEntityFactory.buildWithId(); + const rooms = roomEntityFactory.buildListWithId(2, { schoolId: school.id }); + const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent({ school }); const role = roleFactory.buildWithId({ name: RoleName.ROOMVIEWER, permissions: [Permission.ROOM_VIEW], @@ -141,7 +143,7 @@ describe('Room Controller (API)', () => { externalSource: undefined, }); const roomMemberships = rooms.map((room) => - roomMembershipEntityFactory.build({ userGroupId: userGroupEntity.id, roomId: room.id }) + roomMembershipEntityFactory.build({ userGroupId: userGroupEntity.id, roomId: room.id, schoolId: school.id }) ); await em.persistAndFlush([...rooms, ...roomMemberships, studentAccount, studentUser, userGroupEntity]); em.clear(); diff --git a/apps/server/src/modules/room/api/test/room-members.api.spec.ts b/apps/server/src/modules/room/api/test/room-members.api.spec.ts index c509e59c41b..2543d750b9e 100644 --- a/apps/server/src/modules/room/api/test/room-members.api.spec.ts +++ b/apps/server/src/modules/room/api/test/room-members.api.spec.ts @@ -9,6 +9,7 @@ import { cleanupCollections, groupEntityFactory, roleFactory, + schoolEntityFactory, userFactory, } from '@shared/testing'; import { GroupEntityTypes } from '@modules/group/entity/group.entity'; @@ -47,8 +48,9 @@ describe('Room Controller (API)', () => { describe('GET /rooms/:roomId/members', () => { const setupRoomWithMembers = async () => { + const school = schoolEntityFactory.buildWithId(); const room = roomEntityFactory.buildWithId(); - const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }); const editRole = roleFactory.buildWithId({ name: RoleName.ROOMEDITOR, permissions: [Permission.ROOM_VIEW, Permission.ROOM_EDIT], @@ -57,8 +59,8 @@ describe('Room Controller (API)', () => { name: RoleName.ROOMVIEWER, permissions: [Permission.ROOM_VIEW], }); - const students = userFactory.buildList(2); - const teachers = userFactory.buildList(2); + const students = userFactory.buildList(2, { school }); + const teachers = userFactory.buildList(2, { school }); const userGroupEntity = groupEntityFactory.buildWithId({ users: [ { role: editRole, user: teacherUser }, @@ -71,7 +73,11 @@ describe('Room Controller (API)', () => { organization: teacherUser.school, externalSource: undefined, }); - const roomMemberships = roomMembershipEntityFactory.build({ userGroupId: userGroupEntity.id, roomId: room.id }); + const roomMemberships = roomMembershipEntityFactory.build({ + userGroupId: userGroupEntity.id, + roomId: room.id, + schoolId: school.id, + }); await em.persistAndFlush([ room, roomMemberships, diff --git a/apps/server/src/modules/room/api/test/room-remove-members.api.spec.ts b/apps/server/src/modules/room/api/test/room-remove-members.api.spec.ts index d87d0e68314..3810a9f4f39 100644 --- a/apps/server/src/modules/room/api/test/room-remove-members.api.spec.ts +++ b/apps/server/src/modules/room/api/test/room-remove-members.api.spec.ts @@ -9,6 +9,7 @@ import { cleanupCollections, groupEntityFactory, roleFactory, + schoolEntityFactory, } from '@shared/testing'; import { GroupEntityTypes } from '@modules/group/entity/group.entity'; import { roomMembershipEntityFactory } from '@src/modules/room-membership/testing/room-membership-entity.factory'; @@ -57,9 +58,10 @@ describe('Room Controller (API)', () => { }; const setupRoomWithMembers = async () => { - const room = roomEntityFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); + const room = roomEntityFactory.buildWithId({ schoolId: school.id }); - const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }); const { teacherUser: inRoomEditor2 } = UserAndAccountTestFactory.buildTeacher({ school: teacherUser.school }); const { teacherUser: inRoomEditor3 } = UserAndAccountTestFactory.buildTeacher({ school: teacherUser.school }); const { teacherUser: inRoomViewer } = UserAndAccountTestFactory.buildTeacher({ school: teacherUser.school }); @@ -81,7 +83,11 @@ describe('Room Controller (API)', () => { externalSource: undefined, }); - const roomMemberships = roomMembershipEntityFactory.build({ userGroupId: userGroupEntity.id, roomId: room.id }); + const roomMemberships = roomMembershipEntityFactory.build({ + userGroupId: userGroupEntity.id, + roomId: room.id, + schoolId: school.id, + }); await em.persistAndFlush([...Object.values(users), room, roomMemberships, teacherAccount, userGroupEntity]); em.clear(); diff --git a/apps/server/src/modules/room/api/test/room-update.api.spec.ts b/apps/server/src/modules/room/api/test/room-update.api.spec.ts index 782c23961d4..bc505ddc6aa 100644 --- a/apps/server/src/modules/room/api/test/room-update.api.spec.ts +++ b/apps/server/src/modules/room/api/test/room-update.api.spec.ts @@ -8,6 +8,7 @@ import { cleanupCollections, groupEntityFactory, roleFactory, + schoolEntityFactory, } from '@shared/testing'; import { roomMembershipEntityFactory } from '@src/modules/room-membership/testing'; import { ServerTestModule, serverConfig, type ServerConfig } from '@modules/server'; @@ -94,19 +95,25 @@ describe('Room Controller (API)', () => { describe('when the user has the required permissions', () => { const setup = async () => { + const school = schoolEntityFactory.buildWithId(); const room = roomEntityFactory.build({ startDate: new Date('2024-10-01'), endDate: new Date('2024-10-20'), + schoolId: school.id, }); const role = roleFactory.buildWithId({ name: RoleName.ROOMEDITOR, permissions: [Permission.ROOM_EDIT], }); - const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }); const userGroup = groupEntityFactory.buildWithId({ users: [{ role, user: teacherUser }], }); - const roomMembership = roomMembershipEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); + const roomMembership = roomMembershipEntityFactory.build({ + roomId: room.id, + userGroupId: userGroup.id, + schoolId: school.id, + }); await em.persistAndFlush([room, roomMembership, teacherAccount, teacherUser, userGroup, role]); em.clear(); diff --git a/apps/server/src/modules/sharing/controller/api-test/sharing-import-room-board-token.api.spec.ts b/apps/server/src/modules/sharing/controller/api-test/sharing-import-room-board-token.api.spec.ts index 799a1bcca32..c63c27bf6d5 100644 --- a/apps/server/src/modules/sharing/controller/api-test/sharing-import-room-board-token.api.spec.ts +++ b/apps/server/src/modules/sharing/controller/api-test/sharing-import-room-board-token.api.spec.ts @@ -7,6 +7,7 @@ import { cleanupCollections, groupEntityFactory, roleFactory, + schoolEntityFactory, TestApiClient, UserAndAccountTestFactory, } from '@shared/testing'; @@ -48,17 +49,22 @@ describe('Sharing Controller (API)', () => { describe('POST /sharetoken/:token/import', () => { const setup = async () => { - const room = roomEntityFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); + const room = roomEntityFactory.buildWithId({ schoolId: school.id }); const role = roleFactory.buildWithId({ name: RoleName.ROOMEDITOR, permissions: [Permission.ROOM_EDIT], }); - const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }); const userGroup = groupEntityFactory.buildWithId({ type: GroupEntityTypes.ROOM, users: [{ role, user: teacherUser }], }); - const roomMembership = roomMembershipEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); + const roomMembership = roomMembershipEntityFactory.build({ + roomId: room.id, + userGroupId: userGroup.id, + schoolId: school.id, + }); const board = columnBoardEntityFactory.build({ context: { id: room.id, type: BoardExternalReferenceType.Room }, }); diff --git a/apps/server/src/modules/sharing/controller/share-token.controller.ts b/apps/server/src/modules/sharing/controller/share-token.controller.ts index d85937cbb62..04e9ec01b8c 100644 --- a/apps/server/src/modules/sharing/controller/share-token.controller.ts +++ b/apps/server/src/modules/sharing/controller/share-token.controller.ts @@ -35,7 +35,7 @@ export class ShareTokenController { @ApiResponse({ status: 403, type: ForbiddenException }) @ApiResponse({ status: 500, type: InternalServerErrorException }) @Post() - async createShareToken( + public async createShareToken( @CurrentUser() currentUser: ICurrentUser, @Body() body: ShareTokenBodyParams ): Promise { @@ -62,7 +62,7 @@ export class ShareTokenController { @ApiResponse({ status: 404, type: NotFoundException }) @ApiResponse({ status: 500, type: InternalServerErrorException }) @Get(':token') - async lookupShareToken( + public async lookupShareToken( @CurrentUser() currentUser: ICurrentUser, @Param() urlParams: ShareTokenUrlParams ): Promise { @@ -81,7 +81,7 @@ export class ShareTokenController { @ApiResponse({ status: 501, type: NotImplementedException }) @Post(':token/import') @RequestTimeout('INCOMING_REQUEST_TIMEOUT_COPY_API') - async importShareToken( + public async importShareToken( @CurrentUser() currentUser: ICurrentUser, @Param() urlParams: ShareTokenUrlParams, @Body() body: ShareTokenImportBodyParams diff --git a/apps/server/src/modules/sharing/uc/share-token.uc.ts b/apps/server/src/modules/sharing/uc/share-token.uc.ts index 9cd1a9f5c7e..a9498d0b2a1 100644 --- a/apps/server/src/modules/sharing/uc/share-token.uc.ts +++ b/apps/server/src/modules/sharing/uc/share-token.uc.ts @@ -52,7 +52,7 @@ export class ShareTokenUC { this.logger.setContext(ShareTokenUC.name); } - async createShareToken( + public async createShareToken( userId: EntityId, payload: ShareTokenPayload, options?: { schoolExclusive?: boolean; expiresInDays?: number } @@ -80,7 +80,7 @@ export class ShareTokenUC { return shareToken; } - async lookupShareToken(userId: EntityId, token: string): Promise { + public async lookupShareToken(userId: EntityId, token: string): Promise { this.logger.debug({ action: 'lookupShareToken', userId, token }); const { shareToken, parentName } = await this.shareTokenService.lookupTokenWithParentName(token); @@ -102,7 +102,7 @@ export class ShareTokenUC { return shareTokenInfo; } - async importShareToken( + public async importShareToken( userId: EntityId, token: string, newName: string, @@ -282,14 +282,14 @@ export class ShareTokenUC { ); } - private async checkSchoolReadPermission(user: User, schoolId: EntityId) { + private async checkSchoolReadPermission(user: User, schoolId: EntityId): Promise { const school = await this.schoolService.getSchoolById(schoolId); const authorizationContext = AuthorizationContextBuilder.read([]); this.authorizationService.checkPermission(user, school, authorizationContext); } - private async checkContextReadPermission(userId: EntityId, context: ShareTokenContext) { + private async checkContextReadPermission(userId: EntityId, context: ShareTokenContext): Promise { const user = await this.authorizationService.getUserWithPermissions(userId); if (context.contextType === ShareTokenContextType.School) { diff --git a/apps/server/src/modules/tldraw-client/service/drawing-element-adapter.service.ts b/apps/server/src/modules/tldraw-client/service/drawing-element-adapter.service.ts index baa16703a94..1acced96d98 100644 --- a/apps/server/src/modules/tldraw-client/service/drawing-element-adapter.service.ts +++ b/apps/server/src/modules/tldraw-client/service/drawing-element-adapter.service.ts @@ -5,6 +5,11 @@ import { LegacyLogger } from '@src/core/logger'; import { firstValueFrom } from 'rxjs'; import { TldrawClientConfig } from '../interface'; +type ApiKeyHeader = { + 'X-Api-Key': string; + Accept: string; +}; + @Injectable() export class DrawingElementAdapterService { constructor( @@ -15,7 +20,7 @@ export class DrawingElementAdapterService { this.logger.setContext(DrawingElementAdapterService.name); } - async deleteDrawingBinData(parentId: string): Promise { + public async deleteDrawingBinData(parentId: string): Promise { const baseUrl = this.configService.get('TLDRAW_ADMIN_API_CLIENT_BASE_URL'); const endpointUrl = '/api/tldraw-document'; const tldrawDocumentEndpoint = new URL(endpointUrl, baseUrl).toString(); @@ -23,13 +28,13 @@ export class DrawingElementAdapterService { await firstValueFrom(this.httpService.delete(`${tldrawDocumentEndpoint}/${parentId}`, this.defaultHeaders())); } - private apiKeyHeader() { + private apiKeyHeader(): ApiKeyHeader { const apiKey = this.configService.get('TLDRAW_ADMIN_API_CLIENT_API_KEY'); return { 'X-Api-Key': apiKey, Accept: 'Application/json' }; } - private defaultHeaders() { + private defaultHeaders(): { headers: ApiKeyHeader } { return { headers: this.apiKeyHeader(), }; diff --git a/apps/server/src/modules/tldraw/controller/api-test/tldraw.controller.401.api.spec.ts b/apps/server/src/modules/tldraw/controller/api-test/tldraw.controller.401.api.spec.ts index 09d5d81cbb4..88b24bce8b9 100644 --- a/apps/server/src/modules/tldraw/controller/api-test/tldraw.controller.401.api.spec.ts +++ b/apps/server/src/modules/tldraw/controller/api-test/tldraw.controller.401.api.spec.ts @@ -37,7 +37,7 @@ describe('tldraw controller (api)', () => { return { drawingItemData }; }; - it('should return status 401 for delete', async () => { + it('should return status 204 for delete', async () => { const { drawingItemData } = await setup(); const response = await testApiClient.delete(`${drawingItemData.docName}`); 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',