From 8a06c84e6911c29410310b3af6407adfb3d20242 Mon Sep 17 00:00:00 2001 From: Frederik Christ Vestergaard Date: Fri, 23 May 2025 10:49:25 +0200 Subject: [PATCH] Added expired on options for api key and user --- src/auth/api-key.strategy.ts | 3 + src/controllers/api-key/api-key.controller.ts | 4 +- .../organization.controller.ts | 13 +- .../user-management/permission.controller.ts | 4 +- .../user-management/user.controller.ts | 9 +- src/entities/api-key.entity.ts | 3 + .../dto/api-key/create-api-key.dto.ts | 6 + src/entities/dto/list-all-entities.dto.ts | 3 +- .../dto/user-management/create-user.dto.ts | 7 +- src/entities/enum/error-codes.enum.ts | 1 + src/entities/user.entity.ts | 3 + ...172231928-AddedExpiresOnToUserAndApiKey.ts | 16 ++ src/modules/user-management/user.module.ts | 5 +- .../api-key-management/api-key.service.ts | 8 +- src/services/temporary-access.service.ts | 13 ++ src/services/user-management/user.service.ts | 170 ++++++++++-------- 16 files changed, 169 insertions(+), 99 deletions(-) create mode 100644 src/migration/1746172231928-AddedExpiresOnToUserAndApiKey.ts create mode 100644 src/services/temporary-access.service.ts diff --git a/src/auth/api-key.strategy.ts b/src/auth/api-key.strategy.ts index 4f53dc2a..033a846a 100644 --- a/src/auth/api-key.strategy.ts +++ b/src/auth/api-key.strategy.ts @@ -26,6 +26,9 @@ export class ApiKeyStrategy extends PassportStrategy(HeaderAPIKeyStrategy, ApiKe if (!apiKeyDb) { throw new UnauthorizedException(ErrorCodes.ApiKeyAuthFailed); } + if (apiKeyDb.expiresOn < new Date()) { + throw new UnauthorizedException(ErrorCodes.ApiKeyExpired); + } // Get the permissions and the UserID from the API Key instead of the user const permissions = await this.permissionService.findPermissionGroupedByLevelForApiKey(apiKeyDb.id); diff --git a/src/controllers/api-key/api-key.controller.ts b/src/controllers/api-key/api-key.controller.ts index c2e07a68..458b4ad1 100644 --- a/src/controllers/api-key/api-key.controller.ts +++ b/src/controllers/api-key/api-key.controller.ts @@ -27,6 +27,7 @@ import { } from "@nestjs/common"; import { ApiBadRequestResponse, + ApiBearerAuth, ApiForbiddenResponse, ApiNotFoundResponse, ApiOperation, @@ -38,10 +39,9 @@ import { AuditLog } from "@services/audit-log.service"; import { OrganizationService } from "@services/user-management/organization.service"; import { UpdateApiKeyDto } from "@dto/api-key/update-api-key.dto"; import { checkIfUserHasAccessToOrganization, OrganizationAccessScope } from "@helpers/security-helper"; -import { ApiAuth } from "@auth/swagger-auth-decorator"; @UseGuards(JwtAuthGuard, RolesGuard) -@ApiAuth() +@ApiBearerAuth() @UserAdmin() @ApiForbiddenResponse() @ApiUnauthorizedResponse() diff --git a/src/controllers/user-management/organization.controller.ts b/src/controllers/user-management/organization.controller.ts index 6996acee..cba6b4eb 100644 --- a/src/controllers/user-management/organization.controller.ts +++ b/src/controllers/user-management/organization.controller.ts @@ -14,14 +14,13 @@ import { UseGuards, } from "@nestjs/common"; import { + ApiBearerAuth, ApiForbiddenResponse, ApiNotFoundResponse, ApiOperation, ApiTags, ApiUnauthorizedResponse, } from "@nestjs/swagger"; - -import { JwtAuthGuard } from "@auth/jwt-auth.guard"; import { ApplicationAdmin, GatewayAdmin, GlobalAdmin, Read, UserAdmin } from "@auth/roles.decorator"; import { RolesGuard } from "@auth/roles.guard"; import { DeleteResponseDto } from "@dto/delete-application-response.dto"; @@ -38,21 +37,21 @@ import { OrganizationService } from "@services/user-management/organization.serv import { AuditLog } from "@services/audit-log.service"; import { ActionType } from "@entities/audit-log-entry"; import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; -import { ApiAuth } from "@auth/swagger-auth-decorator"; import { checkIfUserHasAccessToOrganization, OrganizationAccessScope } from "@helpers/security-helper"; -import { PermissionType } from "@enum/permission-type.enum"; +import { ComposeAuthGuard } from "@auth/compose-auth.guard"; -@UseGuards(JwtAuthGuard, RolesGuard) -@ApiAuth() +@UseGuards(ComposeAuthGuard, RolesGuard) +@ApiBearerAuth() @ApiForbiddenResponse() @ApiUnauthorizedResponse() @ApiTags("Organization") @Controller("organization") @GlobalAdmin() export class OrganizationController { - constructor(private organizationService: OrganizationService) {} private readonly logger = new Logger(OrganizationController.name); + constructor(private organizationService: OrganizationService) {} + @Post() @ApiOperation({ summary: "Create a new Organization" }) async create( diff --git a/src/controllers/user-management/permission.controller.ts b/src/controllers/user-management/permission.controller.ts index 349b7f55..2c7e59b4 100644 --- a/src/controllers/user-management/permission.controller.ts +++ b/src/controllers/user-management/permission.controller.ts @@ -14,6 +14,7 @@ import { UseGuards, } from "@nestjs/common"; import { + ApiBearerAuth, ApiForbiddenResponse, ApiNotFoundResponse, ApiOperation, @@ -49,11 +50,10 @@ import { PermissionRequestAcceptUser } from "@dto/user-management/add-user-to-pe import { OrganizationService } from "@services/user-management/organization.service"; import { Organization } from "@entities/organization.entity"; import { User } from "@entities/user.entity"; -import { ApiAuth } from "@auth/swagger-auth-decorator"; import { ListAllPermissionsSlimResponseDto } from "@dto/list-all-permissions-slim-response.dto"; @UseGuards(JwtAuthGuard, RolesGuard) -@ApiAuth() +@ApiBearerAuth() @ApiForbiddenResponse() @ApiUnauthorizedResponse() @ApiTags("Permission") diff --git a/src/controllers/user-management/user.controller.ts b/src/controllers/user-management/user.controller.ts index 3be024f4..30b22f1d 100644 --- a/src/controllers/user-management/user.controller.ts +++ b/src/controllers/user-management/user.controller.ts @@ -15,7 +15,7 @@ import { Req, UseGuards, } from "@nestjs/common"; -import { ApiForbiddenResponse, ApiOperation, ApiTags, ApiUnauthorizedResponse } from "@nestjs/swagger"; +import { ApiBearerAuth, ApiForbiddenResponse, ApiOperation, ApiTags, ApiUnauthorizedResponse } from "@nestjs/swagger"; import { QueryFailedError } from "typeorm"; import { JwtAuthGuard } from "@auth/jwt-auth.guard"; @@ -41,20 +41,19 @@ import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; import { OrganizationService } from "@services/user-management/organization.service"; import { Organization } from "@entities/organization.entity"; import { RejectUserDto } from "@dto/user-management/reject-user.dto"; -import { ApiAuth } from "@auth/swagger-auth-decorator"; @UseGuards(JwtAuthGuard, RolesGuard) @UserAdmin() -@ApiAuth() +@ApiBearerAuth() @ApiForbiddenResponse() @ApiUnauthorizedResponse() @ApiTags("User Management") @Controller("user") export class UserController { - constructor(private userService: UserService, private organizationService: OrganizationService) {} - private readonly logger = new Logger(UserController.name); + constructor(private userService: UserService, private organizationService: OrganizationService) {} + @Get("minimal") @ApiOperation({ summary: "Get all id,names of users" }) async findAllMinimal(): Promise { diff --git a/src/entities/api-key.entity.ts b/src/entities/api-key.entity.ts index d7ef790d..79560a4e 100644 --- a/src/entities/api-key.entity.ts +++ b/src/entities/api-key.entity.ts @@ -23,4 +23,7 @@ export class ApiKey extends DbBaseEntity { }) @JoinColumn() systemUser: User; + + @Column({ nullable: true }) + expiresOn?: Date; } diff --git a/src/entities/dto/api-key/create-api-key.dto.ts b/src/entities/dto/api-key/create-api-key.dto.ts index df35483e..d45f62ec 100644 --- a/src/entities/dto/api-key/create-api-key.dto.ts +++ b/src/entities/dto/api-key/create-api-key.dto.ts @@ -1,5 +1,7 @@ import { ApiProperty } from "@nestjs/swagger"; import { ArrayNotEmpty, ArrayUnique, IsArray, IsString, Length } from "class-validator"; +import { IsSwaggerOptional } from "@helpers/optional-validator"; +import { ValidateDate } from "@helpers/date.validator"; export class CreateApiKeyDto { @ApiProperty({ required: true }) @@ -18,4 +20,8 @@ export class CreateApiKeyDto { @ArrayNotEmpty() @ArrayUnique() permissionIds: number[]; + + @IsSwaggerOptional() + @ValidateDate() + expiresOn?: Date; } diff --git a/src/entities/dto/list-all-entities.dto.ts b/src/entities/dto/list-all-entities.dto.ts index e8e38484..fef4673a 100644 --- a/src/entities/dto/list-all-entities.dto.ts +++ b/src/entities/dto/list-all-entities.dto.ts @@ -43,5 +43,6 @@ export class ListAllEntitiesDto { | "dataTargets" | "organizationName" | "commentOnLocation" - | "statusCheck"; + | "statusCheck" + | "expiresOn"; } diff --git a/src/entities/dto/user-management/create-user.dto.ts b/src/entities/dto/user-management/create-user.dto.ts index 9d370778..6cab996c 100644 --- a/src/entities/dto/user-management/create-user.dto.ts +++ b/src/entities/dto/user-management/create-user.dto.ts @@ -1,7 +1,8 @@ -import { Permission } from "@entities/permissions/permission.entity"; import { IsNotBlank } from "@helpers/is-not-blank.validator"; import { ApiProperty } from "@nestjs/swagger"; import { IsEmail, IsString, Length } from "class-validator"; +import { IsSwaggerOptional } from "@helpers/optional-validator"; +import { ValidateDate } from "@helpers/date.validator"; export class CreateUserDto { @ApiProperty({ required: true }) @@ -27,4 +28,8 @@ export class CreateUserDto { @ApiProperty({ required: false }) permissionIds?: number[]; + + @IsSwaggerOptional() + @ValidateDate() + expiresOn?: Date; } diff --git a/src/entities/enum/error-codes.enum.ts b/src/entities/enum/error-codes.enum.ts index 050a031d..f9a79381 100644 --- a/src/entities/enum/error-codes.enum.ts +++ b/src/entities/enum/error-codes.enum.ts @@ -38,6 +38,7 @@ export enum ErrorCodes { DeleteNotAllowedHasLoRaWANDevices = "MESSAGE.DELETE-NOT-ALLOWED-HAS-LORAWAN-DEVICE", KOMBITLoginFailed = "MESSAGE.KOMBIT-LOGIN-FAILED", ApiKeyAuthFailed = "MESSAGE.API-KEY-AUTH-FAILED", + ApiKeyExpired = "MESSAGE.API-KEY-EXPIRED", TooMuchData = "MESSAGE.TOO-MUCH-DATA", ApplicationDoesNotExist = "MESSAGE.APPLICATION-DOES-NOT-EXIST", FailedToCreateOrUpdateIotDevice = "MESSAGE.FAILED-TO-CREATE-OR-UPDATE-IOT-DEVICE", diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts index f601ce12..f42c5e44 100644 --- a/src/entities/user.entity.ts +++ b/src/entities/user.entity.ts @@ -50,4 +50,7 @@ export class User extends DbBaseEntity { @Column({ default: false }) showWelcomeScreen: boolean; + + @Column({ nullable: true }) + expiresOn?: Date; } diff --git a/src/migration/1746172231928-AddedExpiresOnToUserAndApiKey.ts b/src/migration/1746172231928-AddedExpiresOnToUserAndApiKey.ts new file mode 100644 index 00000000..134ab249 --- /dev/null +++ b/src/migration/1746172231928-AddedExpiresOnToUserAndApiKey.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddedExpiresOnToUserAndApiKey1746172231928 implements MigrationInterface { + name = 'AddedExpiresOnToUserAndApiKey1746172231928' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "api_key" ADD "expiresOn" TIMESTAMP`); + await queryRunner.query(`ALTER TABLE "user" ADD "expiresOn" TIMESTAMP`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "expiresOn"`); + await queryRunner.query(`ALTER TABLE "api_key" DROP COLUMN "expiresOn"`); + } + +} diff --git a/src/modules/user-management/user.module.ts b/src/modules/user-management/user.module.ts index e72069bc..1f13727e 100644 --- a/src/modules/user-management/user.module.ts +++ b/src/modules/user-management/user.module.ts @@ -1,4 +1,4 @@ -import { Module, forwardRef } from "@nestjs/common"; +import { forwardRef, Module } from "@nestjs/common"; import { SharedModule } from "@modules/shared.module"; import { PermissionModule } from "@modules/user-management/permission.module"; @@ -9,6 +9,7 @@ import { OrganizationModule } from "./organization.module"; import { ConfigModule } from "@nestjs/config"; import configuration from "@config/configuration"; import { OS2IoTMail } from "@services/os2iot-mail.service"; +import { TemporaryAccessService } from "@services/temporary-access.service"; @Module({ imports: [ @@ -18,7 +19,7 @@ import { OS2IoTMail } from "@services/os2iot-mail.service"; forwardRef(() => OrganizationModule), ], controllers: [UserController], - providers: [UserService, UserBootstrapperService, OS2IoTMail], + providers: [TemporaryAccessService, UserService, UserBootstrapperService, OS2IoTMail], exports: [UserService], }) export class UserModule {} diff --git a/src/services/api-key-management/api-key.service.ts b/src/services/api-key-management/api-key.service.ts index 505a06d0..53f43a90 100644 --- a/src/services/api-key-management/api-key.service.ts +++ b/src/services/api-key-management/api-key.service.ts @@ -5,7 +5,7 @@ import { ListAllApiKeysResponseDto } from "@dto/api-key/list-all-api-keys-respon import { ListAllApiKeysDto } from "@dto/api-key/list-all-api-keys.dto"; import { DeleteResponseDto } from "@dto/delete-application-response.dto"; import { ApiKey } from "@entities/api-key.entity"; -import { forwardRef, Inject, Injectable, Logger } from "@nestjs/common"; +import { forwardRef, Inject, Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { PermissionService } from "@services/user-management/permission.service"; import { Repository } from "typeorm"; @@ -21,7 +21,6 @@ export class ApiKeyService { @Inject(forwardRef(() => PermissionService)) private permissionService: PermissionService ) {} - private readonly logger = new Logger(ApiKeyService.name, { timestamp: true }); findOne(key: string): Promise { return this.apiKeyRepository.findOne({ @@ -61,7 +60,7 @@ export class ApiKeyService { } if (query.orderOn && query.sort) { - dbQuery = dbQuery.orderBy(`api_key.${query.orderOn}`, query.sort.toUpperCase() as "ASC" | "DESC"); + dbQuery = dbQuery.orderBy(`api_key.${query.orderOn}`, query.sort.toUpperCase() as "ASC" | "DESC", "NULLS LAST"); } const [data, count] = await dbQuery.getManyAndCount(); @@ -79,6 +78,7 @@ export class ApiKeyService { apiKey.name = dto.name; apiKey.updatedBy = userId; apiKey.createdBy = userId; + apiKey.expiresOn = dto.expiresOn; // Create the system user const systemUser = new User(); @@ -86,6 +86,7 @@ export class ApiKeyService { systemUser.isSystemUser = true; systemUser.passwordHash = uuidv4(); // Random password, user can never log in systemUser.name = apiKey.name; + systemUser.expiresOn = dto.expiresOn; apiKey.systemUser = systemUser; if (dto.permissionIds?.length > 0) { @@ -101,6 +102,7 @@ export class ApiKeyService { const apiKey = await this.findOneByIdWithRelations(id); apiKey.name = dto.name; apiKey.updatedBy = userId; + apiKey.expiresOn = dto.expiresOn; if (dto.permissionIds?.length) { const permissionsDb = await this.permissionService.findManyByIds(dto.permissionIds); diff --git a/src/services/temporary-access.service.ts b/src/services/temporary-access.service.ts new file mode 100644 index 00000000..58d61ff2 --- /dev/null +++ b/src/services/temporary-access.service.ts @@ -0,0 +1,13 @@ +import { Cron, CronExpression } from "@nestjs/schedule"; +import { UserService } from "@services/user-management/user.service"; +import { Injectable } from "@nestjs/common"; + +@Injectable() +export class TemporaryAccessService { + constructor(private userService: UserService) {} + + @Cron(CronExpression.EVERY_DAY_AT_1AM) + async expireAccess() { + await this.userService.disableExpiredUsers(); + } +} diff --git a/src/services/user-management/user.service.ts b/src/services/user-management/user.service.ts index 8ef64e30..24eeae55 100644 --- a/src/services/user-management/user.service.ts +++ b/src/services/user-management/user.service.ts @@ -1,7 +1,7 @@ import { BadRequestException, ForbiddenException, forwardRef, Inject, Injectable, Logger } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import * as bcrypt from "bcryptjs"; -import { In, Repository } from "typeorm"; +import { In, LessThan, Repository } from "typeorm"; import { CreateUserDto } from "@dto/user-management/create-user.dto"; import { UpdateUserDto } from "@dto/user-management/update-user.dto"; @@ -26,6 +26,8 @@ import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; @Injectable() export class UserService { + private readonly logger = new Logger(UserService.name, { timestamp: true }); + constructor( @InjectRepository(User) private userRepository: Repository, @@ -35,8 +37,6 @@ export class UserService { private oS2IoTMail: OS2IoTMail ) {} - private readonly logger = new Logger(UserService.name, { timestamp: true }); - async isEmailUsedByAUser(email: string): Promise { return ( (await this.userRepository.count({ @@ -126,6 +126,20 @@ export class UserService { ).permissions; } + async disableExpiredUsers() { + const now = new Date(); + + const users = await this.userRepository.find({ + where: { expiresOn: LessThan(now), active: true }, + }); + + for (const user of users) { + user.active = false; + } + + await this.userRepository.save(users); + } + async updateLastLoginToNow(user: User): Promise { await this.userRepository .createQueryBuilder() @@ -167,52 +181,6 @@ export class UserService { return await this.userRepository.save(user); } - private async mapKombitLoginProfileToUser(user: User, profile: Profile): Promise { - user.active = true; - user.nameId = profile.nameID; - user.name = this.extractNameFromNameIDSAMLAttribute(profile.nameID); - user.active = true; - } - - private extractNameFromNameIDSAMLAttribute(nameId: string): string { - return nameId - ?.split(",") - ?.find(x => x.startsWith("CN=")) - ?.split("=") - ?.pop(); - } - - private async setPasswordHash(mappedUser: User, password: string) { - this.checkPassword(password); - // Hash password with bcrpyt - const salt = await bcrypt.genSalt(10); - mappedUser.passwordHash = await bcrypt.hash(password, salt); - } - - private checkPassword(password: string) { - if (password.length < 6) { - throw new BadRequestException(ErrorCodes.PasswordNotMetRequirements); - } - } - - private mapDtoToUser(user: User, dto: UpdateUserDto): User { - if (user.nameId != null) { - if (dto.name && user.name != dto.name) { - throw new BadRequestException(ErrorCodes.CannotModifyOnKombitUser); - } - if (dto.password) { - throw new BadRequestException(ErrorCodes.CannotModifyOnKombitUser); - } - } - - user.name = dto.name; - user.email = dto.email; - user.permissions = user.permissions ? user.permissions : []; - user.active = dto.active; - - return user; - } - async updateUser(id: number, dto: UpdateUserDto, userId: number, req: AuthenticatedRequest): Promise { const user = await this.userRepository.findOne({ where: { id }, @@ -239,32 +207,6 @@ export class UserService { return await this.userRepository.save(mappedUser); } - private checkForAccessToPermissions(req: AuthenticatedRequest, permissions: Permission[]) { - const allowedOrganizations = req?.user.permissions.getAllOrganizationsWithUserAdmin(); - if (!req.user.permissions.isGlobalAdmin) { - const hasAccessToPermissions = permissions.every(permission => - allowedOrganizations.some(org => org === permission.organization?.id) - ); - - if (!hasAccessToPermissions) { - throw new ForbiddenException(); - } - } - } - - private async updateGlobalAdminStatusIfNeeded(dto: UpdateUserDto, mappedUser: User) { - if (dto.globalAdmin) { - const globalAdminPermission = await this.permissionService.findOrCreateGlobalAdminPermission(); - // Don't do anything if the user already is global admin. - if (!mappedUser.permissions.some(x => x.id == globalAdminPermission.id)) { - await this.permissionService.addUsersToPermission(globalAdminPermission, [mappedUser]); - } - } else { - const globalAdminPermission = await this.permissionService.findOrCreateGlobalAdminPermission(); - await this.permissionService.removeUserFromPermission(globalAdminPermission, mappedUser); - } - } - async newKombitUser(dto: CreateNewKombitUserDto, requestedOrganizations: Organization[], user: User): Promise { user.email = dto.email; user.awaitingConfirmation = true; @@ -280,7 +222,10 @@ export class UserService { async findAll(query?: ListAllEntitiesDto): Promise { const sorting: { [id: string]: string | number } = {}; - if (query.orderOn != null && (query.orderOn == "id" || query.orderOn == "name" || query.orderOn == "lastLogin")) { + if ( + query.orderOn != null && + (query.orderOn == "id" || query.orderOn == "name" || query.orderOn == "lastLogin" || query.orderOn == "expiresOn") + ) { sorting[query.orderOn] = query.sort.toLocaleUpperCase(); } else { sorting["id"] = "ASC"; @@ -469,4 +414,77 @@ export class UserService { return !!value; } } + + private async mapKombitLoginProfileToUser(user: User, profile: Profile): Promise { + user.active = true; + user.nameId = profile.nameID; + user.name = this.extractNameFromNameIDSAMLAttribute(profile.nameID); + user.active = true; + } + + private extractNameFromNameIDSAMLAttribute(nameId: string): string { + return nameId + ?.split(",") + ?.find(x => x.startsWith("CN=")) + ?.split("=") + ?.pop(); + } + + private async setPasswordHash(mappedUser: User, password: string) { + this.checkPassword(password); + // Hash password with bcrpyt + const salt = await bcrypt.genSalt(10); + mappedUser.passwordHash = await bcrypt.hash(password, salt); + } + + private checkPassword(password: string) { + if (password.length < 6) { + throw new BadRequestException(ErrorCodes.PasswordNotMetRequirements); + } + } + + private mapDtoToUser(user: User, dto: UpdateUserDto): User { + if (user.nameId != null) { + if (dto.name && user.name != dto.name) { + throw new BadRequestException(ErrorCodes.CannotModifyOnKombitUser); + } + if (dto.password) { + throw new BadRequestException(ErrorCodes.CannotModifyOnKombitUser); + } + } + + user.name = dto.name; + user.email = dto.email; + user.permissions = user.permissions ? user.permissions : []; + user.active = dto.active; + user.expiresOn = dto.expiresOn; + + return user; + } + + private checkForAccessToPermissions(req: AuthenticatedRequest, permissions: Permission[]) { + const allowedOrganizations = req?.user.permissions.getAllOrganizationsWithUserAdmin(); + if (!req.user.permissions.isGlobalAdmin) { + const hasAccessToPermissions = permissions.every(permission => + allowedOrganizations.some(org => org === permission.organization?.id) + ); + + if (!hasAccessToPermissions) { + throw new ForbiddenException(); + } + } + } + + private async updateGlobalAdminStatusIfNeeded(dto: UpdateUserDto, mappedUser: User) { + if (dto.globalAdmin) { + const globalAdminPermission = await this.permissionService.findOrCreateGlobalAdminPermission(); + // Don't do anything if the user already is global admin. + if (!mappedUser.permissions.some(x => x.id == globalAdminPermission.id)) { + await this.permissionService.addUsersToPermission(globalAdminPermission, [mappedUser]); + } + } else { + const globalAdminPermission = await this.permissionService.findOrCreateGlobalAdminPermission(); + await this.permissionService.removeUserFromPermission(globalAdminPermission, mappedUser); + } + } }