From 3004f634c9f1f26109f668d56fa3f019a2b561d8 Mon Sep 17 00:00:00 2001 From: AramAlsabti <92869496+AramAlsabti@users.noreply.github.com> Date: Mon, 16 May 2022 15:35:39 +0200 Subject: [PATCH 01/19] Add flow for hiding new welcome screen (#172) --- .../user-management/user.controller.ts | 17 +++++++++++++++++ src/entities/user.entity.ts | 3 +++ src/migration/1652342361070-welcome-screen.ts | 14 ++++++++++++++ src/services/user-management/user.service.ts | 13 ++++++++++--- 4 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 src/migration/1652342361070-welcome-screen.ts diff --git a/src/controllers/user-management/user.controller.ts b/src/controllers/user-management/user.controller.ts index 9102232c..9d6cdaae 100644 --- a/src/controllers/user-management/user.controller.ts +++ b/src/controllers/user-management/user.controller.ts @@ -132,6 +132,23 @@ export class UserController { } } + @Put(":id/hide-welcome") + @ApiOperation({ summary: "Don't show welcome screen for a user again" }) + @Read() + async hideWelcome(@Req() req: AuthenticatedRequest): Promise { + const wasOk = await this.userService.hideWelcome(req.user.userId); + + AuditLog.success( + ActionType.UPDATE, + User.name, + req.user.userId, + req.user.userId, + req.user.username + ); + + return wasOk + } + @Get(":id") @ApiOperation({ summary: "Get one user" }) async find( diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts index 852afe06..79440915 100644 --- a/src/entities/user.entity.ts +++ b/src/entities/user.entity.ts @@ -37,4 +37,7 @@ export class User extends DbBaseEntity { @Column({ default: false }) isSystemUser: boolean; + + @Column({ default: false }) + showWelcomeScreen: boolean; } diff --git a/src/migration/1652342361070-welcome-screen.ts b/src/migration/1652342361070-welcome-screen.ts new file mode 100644 index 00000000..bd75c9da --- /dev/null +++ b/src/migration/1652342361070-welcome-screen.ts @@ -0,0 +1,14 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class welcomeScreen1652342361070 implements MigrationInterface { + name = 'welcomeScreen1652342361070' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user" ADD "showWelcomeScreen" boolean NOT NULL DEFAULT false`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "showWelcomeScreen"`); + } + +} diff --git a/src/services/user-management/user.service.ts b/src/services/user-management/user.service.ts index 8bc1b583..8c187201 100644 --- a/src/services/user-management/user.service.ts +++ b/src/services/user-management/user.service.ts @@ -122,6 +122,7 @@ export class UserService { const mappedUser = this.mapDtoToUser(user, dto); mappedUser.createdBy = userId; mappedUser.updatedBy = userId; + mappedUser.showWelcomeScreen = true; await this.setPasswordHash(mappedUser, dto.password); @@ -138,6 +139,7 @@ export class UserService { async createUserFromKombit(profile: Profile): Promise { const user = new User(); await this.mapKombitLoginProfileToUser(user, profile); + user.showWelcomeScreen = true; return await this.userRepository.save(user); } @@ -256,9 +258,9 @@ export class UserService { take: +query.limit, skip: +query.offset, order: sorting, - where: { - isSystemUser: false - } + where: { + isSystemUser: false, + }, }); return { @@ -294,4 +296,9 @@ export class UserService { users: result, }; } + + async hideWelcome(id: number): Promise { + const res = await this.userRepository.update(id, { showWelcomeScreen: false }); + return !!res.affected + } } From 08a0d9f21861d3899e34709d651970ee5ba91e43 Mon Sep 17 00:00:00 2001 From: Aram Al-Sabti Date: Mon, 16 May 2022 18:12:42 +0200 Subject: [PATCH 02/19] Merge permission entities into one. Support multiple levels per permission --- .../user-management/auth.controller.ts | 1 - .../user-management/permission.controller.ts | 36 +++--- src/entities/api-key-permission.entity.ts | 10 -- src/entities/api-key.entity.ts | 6 +- src/entities/application.entity.ts | 6 +- src/entities/dto/permission-minimal.dto.ts | 2 +- .../user-management/create-permission.dto.ts | 15 +-- .../user-management/permission-type.dto.ts | 7 ++ src/entities/organization.entity.ts | 3 +- .../global-admin-permission.entity.ts | 12 -- ...ion-application-admin-permission.entity.ts | 13 -- ...anization-application-permission.entity.ts | 21 ---- ...ization-gateway-admin-permission.entity.ts | 11 -- .../organization-permission.entity.ts | 17 --- ...ganization-user-admin-permission.entity.ts | 11 -- .../permissions/permission-type.entity.ts | 18 +++ src/entities/permissions/permission.entity.ts | 41 +++++-- .../permissions/read-permission.entity.ts | 13 -- src/helpers/permission.helper.ts | 87 ++++++++++++++ src/helpers/security-helper.ts | 18 +-- .../1651142158492-revised-permissions.ts | 52 +++++++- src/modules/shared.module.ts | 18 +-- .../api-key-management/api-key.service.ts | 3 +- .../user-management/permission.service.ts | 111 +++++++++--------- src/services/user-management/user.service.ts | 2 +- 25 files changed, 294 insertions(+), 240 deletions(-) delete mode 100644 src/entities/api-key-permission.entity.ts create mode 100644 src/entities/dto/user-management/permission-type.dto.ts delete mode 100644 src/entities/permissions/global-admin-permission.entity.ts delete mode 100644 src/entities/permissions/organization-application-admin-permission.entity.ts delete mode 100644 src/entities/permissions/organization-application-permission.entity.ts delete mode 100644 src/entities/permissions/organization-gateway-admin-permission.entity.ts delete mode 100644 src/entities/permissions/organization-permission.entity.ts delete mode 100644 src/entities/permissions/organization-user-admin-permission.entity.ts create mode 100644 src/entities/permissions/permission-type.entity.ts delete mode 100644 src/entities/permissions/read-permission.entity.ts create mode 100644 src/helpers/permission.helper.ts diff --git a/src/controllers/user-management/auth.controller.ts b/src/controllers/user-management/auth.controller.ts index 89baa021..d1947b92 100644 --- a/src/controllers/user-management/auth.controller.ts +++ b/src/controllers/user-management/auth.controller.ts @@ -31,7 +31,6 @@ import { import { JwtPayloadDto } from "@dto/internal/jwt-payload.dto"; import { LoginDto } from "@dto/login.dto"; import { Organization } from "@entities/organization.entity"; -import { OrganizationPermission } from "@entities/permissions/organization-permission.entity"; import { User } from "@entities/user.entity"; import { PermissionType } from "@enum/permission-type.enum"; import { AuthService } from "@services/user-management/auth.service"; diff --git a/src/controllers/user-management/permission.controller.ts b/src/controllers/user-management/permission.controller.ts index c93474cc..d5e30727 100644 --- a/src/controllers/user-management/permission.controller.ts +++ b/src/controllers/user-management/permission.controller.ts @@ -29,7 +29,6 @@ import { ListAllPermissionsResponseDto } from "@dto/list-all-permissions-respons import { CreatePermissionDto } from "@dto/user-management/create-permission.dto"; import { UpdatePermissionDto } from "@dto/user-management/update-permission.dto"; import { AuthenticatedRequest } from "@entities/dto/internal/authenticated-request"; -import { OrganizationPermission } from "@entities/permissions/organization-permission.entity"; import { Permission } from "@entities/permissions/permission.entity"; import { PermissionType } from "@enum/permission-type.enum"; import { @@ -71,7 +70,11 @@ export class PermissionController { @Body() dto: CreatePermissionDto ): Promise { try { - checkIfUserHasAccessToOrganization(req, dto.organizationId, OrganizationAccessScope.UserAdministrationWrite); + checkIfUserHasAccessToOrganization( + req, + dto.organizationId, + OrganizationAccessScope.UserAdministrationWrite + ); const result = await this.permissionService.createNewPermission( dto, @@ -102,13 +105,12 @@ export class PermissionController { ): Promise { try { const permission = await this.permissionService.getPermission(id); - if (permission.type == PermissionType.GlobalAdmin) { + if (permission.type.some(({ type }) => type === PermissionType.GlobalAdmin)) { checkIfUserIsGlobalAdmin(req); } else { - const organizationPermission = permission as OrganizationPermission; checkIfUserHasAccessToOrganization( req, - organizationPermission.organization.id, + permission.organization.id, OrganizationAccessScope.UserAdministrationWrite ); } @@ -142,13 +144,14 @@ export class PermissionController { ): Promise { try { const permission = await this.permissionService.getPermission(id); - if (permission.type == PermissionType.GlobalAdmin) { + if ( + permission.type.some(({ type }) => type === PermissionType.GlobalAdmin) + ) { throw new BadRequestException("You cannot delete GlobalAdmin"); } else { - const organizationPermission = permission as OrganizationPermission; checkIfUserHasAccessToOrganization( req, - organizationPermission.organization.id, + permission.organization.id, OrganizationAccessScope.UserAdministrationWrite ); } @@ -196,18 +199,17 @@ export class PermissionController { if ( req.user.permissions.isGlobalAdmin || - permission.type == PermissionType.GlobalAdmin + permission.type.some(({ type }) => type === PermissionType.GlobalAdmin) ) { return permission; } else { - const organizationPermission = permission as OrganizationPermission; checkIfUserHasAccessToOrganization( req, - organizationPermission.organization.id, + permission.organization.id, OrganizationAccessScope.UserAdministrationWrite ); - return organizationPermission; + return permission; } } @@ -233,14 +235,13 @@ export class PermissionController { if ( req.user.permissions.isGlobalAdmin || - permission.type == PermissionType.GlobalAdmin + permission.type.some(({ type }) => type === PermissionType.GlobalAdmin) ) { return await applicationsPromise; } else { - const organizationPermission = permission as OrganizationPermission; checkIfUserHasAccessToOrganization( req, - organizationPermission.organization.id, + permission.organization.id, OrganizationAccessScope.UserAdministrationWrite ); @@ -267,14 +268,13 @@ export class PermissionController { if ( req.user.permissions.isGlobalAdmin || - permission.type == PermissionType.GlobalAdmin + permission.type.some(({ type }) => type === PermissionType.GlobalAdmin) ) { return await users; } else { - const organizationPermission = permission as OrganizationPermission; checkIfUserHasAccessToOrganization( req, - organizationPermission?.organization?.id, + permission?.organization?.id, OrganizationAccessScope.UserAdministrationWrite ); diff --git a/src/entities/api-key-permission.entity.ts b/src/entities/api-key-permission.entity.ts deleted file mode 100644 index bc28f9c8..00000000 --- a/src/entities/api-key-permission.entity.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { PermissionType } from "@enum/permission-type.enum"; -import { ChildEntity, ManyToMany } from "typeorm"; -import { ApiKey } from "./api-key.entity"; -import { Permission } from "./permissions/permission.entity"; - -@ChildEntity(PermissionType.ApiKeyPermission) -export abstract class ApiKeyPermission extends Permission { - @ManyToMany(_ => ApiKey, key => key.permissions, { onDelete: "CASCADE" }) - apiKeys: ApiKey[]; -} diff --git a/src/entities/api-key.entity.ts b/src/entities/api-key.entity.ts index da650106..e0857227 100644 --- a/src/entities/api-key.entity.ts +++ b/src/entities/api-key.entity.ts @@ -9,8 +9,8 @@ import { OneToOne, Unique, } from "typeorm"; -import { ApiKeyPermission } from "./api-key-permission.entity"; import { DbBaseEntity } from "./base.entity"; +import { Permission } from "./permissions/permission.entity"; @Entity("api_key") @Unique([nameof("key")]) @@ -21,9 +21,9 @@ export class ApiKey extends DbBaseEntity { @Column() name: string; - @ManyToMany(_ => ApiKeyPermission, apiKeyPm => apiKeyPm.apiKeys) + @ManyToMany(_ => Permission, apiKeyPm => apiKeyPm.apiKeys) @JoinTable() - permissions: ApiKeyPermission[]; + permissions: Permission[]; @OneToOne(() => User, u => u.apiKeyRef, { nullable: false, diff --git a/src/entities/application.entity.ts b/src/entities/application.entity.ts index 2ceb328b..a6567a72 100644 --- a/src/entities/application.entity.ts +++ b/src/entities/application.entity.ts @@ -15,8 +15,8 @@ import { } from "typeorm"; import { ApplicationDeviceType } from "./application-device-type.entity"; import { ControlledProperty } from "./controlled-property.entity"; -import { OrganizationApplicationPermission } from "@entities/permissions/organization-application-permission.entity"; import { Multicast } from "./multicast.entity"; +import { Permission } from "./permissions/permission.entity"; @Entity("application") @Unique(["name"]) @@ -61,11 +61,11 @@ export class Application extends DbBaseEntity { @ManyToMany( // eslint-disable-next-line @typescript-eslint/no-unused-vars - type => OrganizationApplicationPermission, + type => Permission, permission => permission.applications ) @JoinTable() - permissions: OrganizationApplicationPermission[]; + permissions: Permission[]; @Column({ nullable: true }) status?: ApplicationStatus; diff --git a/src/entities/dto/permission-minimal.dto.ts b/src/entities/dto/permission-minimal.dto.ts index d5bd19e8..f7aeb829 100644 --- a/src/entities/dto/permission-minimal.dto.ts +++ b/src/entities/dto/permission-minimal.dto.ts @@ -1,7 +1,7 @@ import { PermissionType } from "@enum/permission-type.enum"; export class PermissionMinimalDto { - permission_type: PermissionType; + permission_type_type: PermissionType; organization_id: number; application_id: number; } diff --git a/src/entities/dto/user-management/create-permission.dto.ts b/src/entities/dto/user-management/create-permission.dto.ts index 2c28e57e..feb8186e 100644 --- a/src/entities/dto/user-management/create-permission.dto.ts +++ b/src/entities/dto/user-management/create-permission.dto.ts @@ -1,18 +1,19 @@ import { PermissionType } from "@entities/enum/permission-type.enum"; import { ApiProperty } from "@nestjs/swagger"; -import { IsEnum, IsNumber, IsString, Length } from "class-validator"; +import { IsNumber, IsString, Length, ValidateNested, IsArray, ArrayUnique } from "class-validator"; +import { PermissionTypeDto } from "./permission-type.dto"; +import { Type } from "class-transformer"; export class CreatePermissionDto { @ApiProperty({ required: true, enum: PermissionType, }) - @IsEnum(PermissionType) - level: - | PermissionType.OrganizationUserAdmin - | PermissionType.OrganizationGatewayAdmin - | PermissionType.OrganizationApplicationAdmin - | PermissionType.Read; + @IsArray() + @ArrayUnique() + @Type(() => PermissionTypeDto) + @ValidateNested({ each: true }) + levels: PermissionTypeDto[] @ApiProperty({ required: true }) @IsString() diff --git a/src/entities/dto/user-management/permission-type.dto.ts b/src/entities/dto/user-management/permission-type.dto.ts new file mode 100644 index 00000000..9a48328e --- /dev/null +++ b/src/entities/dto/user-management/permission-type.dto.ts @@ -0,0 +1,7 @@ +import { PermissionType } from "@enum/permission-type.enum"; +import { IsEnum } from "class-validator"; + +export class PermissionTypeDto { + @IsEnum(PermissionType) + type: PermissionType; +} diff --git a/src/entities/organization.entity.ts b/src/entities/organization.entity.ts index e2294e87..b568e1ed 100644 --- a/src/entities/organization.entity.ts +++ b/src/entities/organization.entity.ts @@ -2,7 +2,6 @@ import { Column, Entity, OneToMany, Unique } from "typeorm"; import { Application } from "@entities/application.entity"; import { DbBaseEntity } from "@entities/base.entity"; -import { OrganizationPermission } from "@entities/permissions/organization-permission.entity"; import { PayloadDecoder } from "@entities/payload-decoder.entity"; import { Permission } from "@entities/permissions/permission.entity"; @@ -25,7 +24,7 @@ export class Organization extends DbBaseEntity { @OneToMany( // eslint-disable-next-line @typescript-eslint/no-unused-vars - type => OrganizationPermission, + type => Permission, permission => permission.organization, { onDelete: "CASCADE" } ) diff --git a/src/entities/permissions/global-admin-permission.entity.ts b/src/entities/permissions/global-admin-permission.entity.ts deleted file mode 100644 index 3a07a33b..00000000 --- a/src/entities/permissions/global-admin-permission.entity.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ChildEntity } from "typeorm"; - -import { Permission } from "@entities/permissions/permission.entity"; -import { PermissionType } from "@enum/permission-type.enum"; - -@ChildEntity(PermissionType.GlobalAdmin) -export class GlobalAdminPermission extends Permission { - constructor() { - super("GlobalAdmin"); - this.type = PermissionType.GlobalAdmin; - } -} diff --git a/src/entities/permissions/organization-application-admin-permission.entity.ts b/src/entities/permissions/organization-application-admin-permission.entity.ts deleted file mode 100644 index 354ad146..00000000 --- a/src/entities/permissions/organization-application-admin-permission.entity.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ChildEntity } from "typeorm"; - -import { OrganizationApplicationPermission } from "@entities/permissions/organization-application-permission.entity"; -import { Organization } from "@entities/organization.entity"; -import { PermissionType } from "@enum/permission-type.enum"; - -@ChildEntity(PermissionType.OrganizationApplicationAdmin) -export class OrganizationApplicationAdminPermission extends OrganizationApplicationPermission { - constructor(name: string, org: Organization, addNewApps = false) { - super(name, org, addNewApps); - this.type = PermissionType.OrganizationApplicationAdmin; - } -} diff --git a/src/entities/permissions/organization-application-permission.entity.ts b/src/entities/permissions/organization-application-permission.entity.ts deleted file mode 100644 index 21403eae..00000000 --- a/src/entities/permissions/organization-application-permission.entity.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ChildEntity, Column, ManyToMany } from "typeorm"; - -import { Application } from "@entities/application.entity"; -import { Organization } from "@entities/organization.entity"; -import { OrganizationPermission } from "@entities/permissions/organization-permission.entity"; -import { PermissionType } from "@enum/permission-type.enum"; - -@ChildEntity(PermissionType.OrganizationApplicationPermissions) -export abstract class OrganizationApplicationPermission extends OrganizationPermission { - constructor(name: string, org: Organization, addNewApps?: boolean) { - super(name, org); - this.automaticallyAddNewApplications = - addNewApps != undefined ? addNewApps : false; - } - - @ManyToMany(() => Application, application => application.permissions) - applications: Application[]; - - @Column({ nullable: true, default: false, type: Boolean }) - automaticallyAddNewApplications = false; -} diff --git a/src/entities/permissions/organization-gateway-admin-permission.entity.ts b/src/entities/permissions/organization-gateway-admin-permission.entity.ts deleted file mode 100644 index 20f14d0f..00000000 --- a/src/entities/permissions/organization-gateway-admin-permission.entity.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Organization } from "@entities/organization.entity"; -import { OrganizationPermission } from "@entities/permissions/organization-permission.entity"; -import { PermissionType } from "@enum/permission-type.enum"; -import { ChildEntity } from "typeorm"; - -@ChildEntity(PermissionType.OrganizationGatewayAdmin) -export class OrganizationGatewayAdminPermission extends OrganizationPermission { - constructor(name: string, org: Organization) { - super(name, org); - } -} diff --git a/src/entities/permissions/organization-permission.entity.ts b/src/entities/permissions/organization-permission.entity.ts deleted file mode 100644 index 9d611cd0..00000000 --- a/src/entities/permissions/organization-permission.entity.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ChildEntity, ManyToOne } from "typeorm"; - -import { Organization } from "@entities/organization.entity"; -import { Permission } from "@entities/permissions/permission.entity"; -import { PermissionType } from "@enum/permission-type.enum"; - -@ChildEntity(PermissionType.OrganizationPermission) -export abstract class OrganizationPermission extends Permission { - constructor(name: string, org: Organization) { - super(name); - this.organization = org; - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - @ManyToOne(() => Organization, { onDelete: "CASCADE" }) - organization: Organization; -} diff --git a/src/entities/permissions/organization-user-admin-permission.entity.ts b/src/entities/permissions/organization-user-admin-permission.entity.ts deleted file mode 100644 index df2d98ff..00000000 --- a/src/entities/permissions/organization-user-admin-permission.entity.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Organization } from "@entities/organization.entity"; -import { OrganizationPermission } from "@entities/permissions/organization-permission.entity"; -import { PermissionType } from "@enum/permission-type.enum"; -import { ChildEntity } from "typeorm"; - -@ChildEntity(PermissionType.OrganizationUserAdmin) -export class OrganizationUserAdminPermission extends OrganizationPermission { - constructor(name: string, org: Organization) { - super(name, org); - } -} diff --git a/src/entities/permissions/permission-type.entity.ts b/src/entities/permissions/permission-type.entity.ts new file mode 100644 index 00000000..26a4f037 --- /dev/null +++ b/src/entities/permissions/permission-type.entity.ts @@ -0,0 +1,18 @@ +import { DbBaseEntity } from "@entities/base.entity"; +import { PermissionType } from "@enum/permission-type.enum"; +import { Column, Entity, ManyToOne } from "typeorm"; +import { Permission } from "./permission.entity"; + +@Entity("permission_type") +// TODO: Temp name to avoid clashing with enum value +export class PermissionTypeEntity extends DbBaseEntity { + @Column() + type: PermissionType; + + @ManyToOne(() => Permission, p => p.type, { + onDelete: "CASCADE", + // Delete the row instead of null'ing application. Useful for updates + orphanedRowAction: "delete", + }) + permission: Permission; +} diff --git a/src/entities/permissions/permission.entity.ts b/src/entities/permissions/permission.entity.ts index 58b665fd..c0d44e7e 100644 --- a/src/entities/permissions/permission.entity.ts +++ b/src/entities/permissions/permission.entity.ts @@ -1,22 +1,33 @@ import { DbBaseEntity } from "@entities/base.entity"; import { User } from "@entities/user.entity"; import { PermissionType } from "@enum/permission-type.enum"; -import { Column, Entity, ManyToMany, TableInheritance } from "typeorm"; +import { + Column, + Entity, + ManyToMany, + TableInheritance, + OneToMany, + ManyToOne, +} from "typeorm"; +import { PermissionTypeEntity } from "./permission-type.entity"; +import { Application } from "@entities/application.entity"; +import { Organization } from "@entities/organization.entity"; +import { ApiKey } from "@entities/api-key.entity"; @Entity() -@TableInheritance({ - column: { type: "enum", name: "type", enum: PermissionType }, -}) -export abstract class Permission extends DbBaseEntity { - constructor(name: string) { +export class Permission extends DbBaseEntity { + constructor(name: string, org?: Organization, addNewApps = false) { super(); this.name = name; + this.organization = org; + this.automaticallyAddNewApplications = addNewApps; } - @Column("enum", { - enum: PermissionType, + @OneToMany(() => PermissionTypeEntity, entity => entity.permission, { + nullable: false, + cascade: true, }) - type: PermissionType; + type: PermissionTypeEntity[]; @Column() name: string; @@ -27,4 +38,16 @@ export abstract class Permission extends DbBaseEntity { user => user.permissions ) users: User[]; + + @ManyToMany(() => Application, application => application.permissions) + applications: Application[]; + + @Column({ nullable: true, default: false, type: Boolean }) + automaticallyAddNewApplications = false; + + @ManyToOne(() => Organization, { onDelete: "CASCADE" }) + organization: Organization; + + @ManyToMany(_ => ApiKey, key => key.permissions, { onDelete: "CASCADE" }) + apiKeys: ApiKey[]; } diff --git a/src/entities/permissions/read-permission.entity.ts b/src/entities/permissions/read-permission.entity.ts deleted file mode 100644 index f756d62b..00000000 --- a/src/entities/permissions/read-permission.entity.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ChildEntity } from "typeorm"; - -import { OrganizationApplicationPermission } from "@entities/permissions/organization-application-permission.entity"; -import { Organization } from "@entities/organization.entity"; -import { PermissionType } from "@enum/permission-type.enum"; - -@ChildEntity(PermissionType.Read) -export class ReadPermission extends OrganizationApplicationPermission { - constructor(name: string, org: Organization, addNewApps = false) { - super(name, org, addNewApps); - this.type = PermissionType.Read; - } -} diff --git a/src/helpers/permission.helper.ts b/src/helpers/permission.helper.ts new file mode 100644 index 00000000..4aae2bc1 --- /dev/null +++ b/src/helpers/permission.helper.ts @@ -0,0 +1,87 @@ +import { Organization } from "@entities/organization.entity"; +import { Permission } from "@entities/permissions/permission.entity"; +import { PermissionType } from "@enum/permission-type.enum"; +import { PermissionTypeEntity } from "@entities/permissions/permission-type.entity"; + +export abstract class PermissionCreator { + private static create( + name: string, + org?: Organization, + addNewApps = false + ): Permission { + const pm = new Permission(name, org, addNewApps); + return pm; + } + + static createByTypes( + name: string, + types: PermissionType[], + org?: Organization, + addNewApps = false + ): Permission { + const pm = new Permission(name, org, addNewApps); + pm.type = types.map(type => { + const entity = new PermissionTypeEntity(); + entity.type = type; + return entity; + }); + return pm; + } + + static createGlobalAdmin(): Permission { + const pm = this.create("GlobalAdmin"); + // TODO: Does this auto-fill dates etc. + pm.type = [{ type: PermissionType.GlobalAdmin } as PermissionTypeEntity]; + return pm; + } + + static createRead(name: string, org?: Organization, addNewApps = false): Permission { + const pm = this.create(name, org, addNewApps); + + // TODO: Does this auto-fill dates etc. + pm.type = [{ type: PermissionType.Read } as PermissionTypeEntity]; + return pm; + } + + static createApplicationAdmin( + name: string, + org?: Organization, + addNewApps = false + ): Permission { + const pm = this.create(name, org, addNewApps); + + // TODO: Does this auto-fill dates etc. + pm.type = [ + { type: PermissionType.OrganizationApplicationAdmin } as PermissionTypeEntity, + ]; + return pm; + } + + static createUserAdmin( + name: string, + org?: Organization, + addNewApps = false + ): Permission { + const pm = this.create(name, org, addNewApps); + + // TODO: Does this auto-fill dates etc. + pm.type = [ + { type: PermissionType.OrganizationUserAdmin } as PermissionTypeEntity, + ]; + return pm; + } + + static createGatewayAdmin( + name: string, + org?: Organization, + addNewApps = false + ): Permission { + const pm = this.create(name, org, addNewApps); + + // TODO: Does this auto-fill dates etc. + pm.type = [ + { type: PermissionType.OrganizationGatewayAdmin } as PermissionTypeEntity, + ]; + return pm; + } +} diff --git a/src/helpers/security-helper.ts b/src/helpers/security-helper.ts index 26b61eed..9905e4a9 100644 --- a/src/helpers/security-helper.ts +++ b/src/helpers/security-helper.ts @@ -1,10 +1,9 @@ import { AuthenticatedRequest } from "@entities/dto/internal/authenticated-request"; import { Permission } from "@entities/permissions/permission.entity"; -import { OrganizationPermission } from "@entities/permissions/organization-permission.entity"; import { PermissionType } from "@enum/permission-type.enum"; import { ForbiddenException, BadRequestException } from "@nestjs/common"; import * as _ from "lodash"; -import { OrganizationApplicationPermission } from "@entities/permissions/organization-application-permission.entity"; +import { PermissionTypeEntity } from "@entities/permissions/permission-type.entity"; export enum OrganizationAccessScope { ApplicationRead, @@ -96,20 +95,21 @@ function checkIfGlobalAdminOrInList( } } -export function isOrganizationPermission(p: Permission): p is OrganizationPermission { +export function isOrganizationPermission(p: Permission): p is Permission { return [ PermissionType.OrganizationUserAdmin, PermissionType.OrganizationApplicationAdmin, PermissionType.OrganizationGatewayAdmin, PermissionType.Read, - ].some(x => x === p.type); + ].some(orgPermission => p.type.some(({ type }) => type === orgPermission)); } export function isOrganizationApplicationPermission(p: { - type: PermissionType; -}): p is OrganizationApplicationPermission { - return ( - p.type === PermissionType.Read || - p.type === PermissionType.OrganizationApplicationAdmin + type: PermissionTypeEntity[]; +}): p is Permission { + return p.type.some( + ({ type }) => + type === PermissionType.Read || + type === PermissionType.OrganizationApplicationAdmin ); } diff --git a/src/migration/1651142158492-revised-permissions.ts b/src/migration/1651142158492-revised-permissions.ts index f07b10f5..a84edbfb 100644 --- a/src/migration/1651142158492-revised-permissions.ts +++ b/src/migration/1651142158492-revised-permissions.ts @@ -1,4 +1,5 @@ import { MigrationInterface, QueryRunner } from "typeorm"; +import { NotImplementedException } from "@nestjs/common"; type AppPermissions = { applicationId: number; @@ -32,7 +33,7 @@ type UserPermissions = { permissionId: number; }[]; -export class revisedPermissions1651142158492 implements MigrationInterface { +export class revisedPermissions1651142158492 implements MigrationInterface { name = "revisedPermissions1651142158492"; public async up(queryRunner: QueryRunner): Promise { @@ -52,6 +53,9 @@ export class revisedPermissions1651142158492 implements MigrationInterface { // ); await queryRunner.query(`DROP TYPE "permission_type_enum_old"`); await queryRunner.query(`COMMENT ON COLUMN "permission"."type" IS NULL`); + + // Update permission so it can refer to multiple types + await this.migratePermissionTypeUp(queryRunner); } public async down(queryRunner: QueryRunner): Promise { @@ -73,6 +77,8 @@ export class revisedPermissions1651142158492 implements MigrationInterface { await queryRunner.query( `ALTER TYPE "permission_type_enum_old" RENAME TO "permission_type_enum"` ); + + await this.migratePermissionTypeDown(queryRunner); } private async migrateUp(queryRunner: QueryRunner): Promise { @@ -145,7 +151,7 @@ export class revisedPermissions1651142158492 implements MigrationInterface { await queryRunner.query(`DELETE FROM user_permissions_permission WHERE "permissionId" IN -( +( SELECT "permission"."id" FROM user_permissions_permission JOIN permission ON permission.id = "public"."user_permissions_permission"."permissionId" WHERE permission.type IN ('Write', 'OrganizationAdmin') @@ -153,7 +159,7 @@ WHERE "permissionId" IN await queryRunner.query(`DELETE FROM application_permissions_permission WHERE "permissionId" IN -( +( SELECT "permission"."id" FROM application_permissions_permission JOIN permission ON permission.id = "public"."application_permissions_permission"."permissionId" WHERE permission.type IN ('Write', 'OrganizationAdmin') @@ -161,12 +167,12 @@ WHERE "permissionId" IN await queryRunner.query(`DELETE FROM api_key_permissions_permission WHERE "permissionId" IN -( +( SELECT "permission"."id" FROM api_key_permissions_permission JOIN permission ON permission.id = "public"."api_key_permissions_permission"."permissionId" WHERE permission.type IN ('Write', 'OrganizationAdmin') )`); - + await queryRunner.query( `DELETE FROM "public"."permission" where type IN ('OrganizationAdmin', 'Write')` ); @@ -314,7 +320,31 @@ returning id, "permission"."clonedFromId"`; ${insertIntoStatements}`; } + private async migratePermissionTypeUp(queryRunner: QueryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_71bf2818fb2ad92e208d7aeadf"`); + await queryRunner.query(`CREATE TABLE "permission_type" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "type" character varying NOT NULL, "createdById" integer, "updatedById" integer, "permissionId" integer, CONSTRAINT "PK_3f2a17e0bff1bc4e34254b27d78" PRIMARY KEY ("id"))`); + + const fetchAllPermissions = `select "createdAt", + "updatedAt", + type, + "createdById", + "updatedById", + "id" AS "permissionId" + from "public"."permission"`; + + await queryRunner.query(`INSERT INTO "public"."permission_type"("createdAt","updatedAt",type,"createdById","updatedById","permissionId") + ${fetchAllPermissions}`); + + await queryRunner.query(`ALTER TABLE "permission" DROP COLUMN "type"`); + await queryRunner.query(`DROP TYPE "public"."permission_type_enum"`); + await queryRunner.query(`ALTER TABLE "permission_type" ADD CONSTRAINT "FK_abd46fe625f90edc07441bd0bb2" FOREIGN KEY ("createdById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "permission_type" ADD CONSTRAINT "FK_6ebf76b0f055fe09e42edfe4848" FOREIGN KEY ("updatedById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "permission_type" ADD CONSTRAINT "FK_b8613564bc719a6e37ff0ba243b" FOREIGN KEY ("permissionId") REFERENCES "permission"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + private async migrateDown(queryRunner: QueryRunner): Promise { + await this.migratePermissionTypeDown(queryRunner); + // Create a temporary enum which is a union of both old and new enum values await queryRunner.query( `CREATE TYPE "permission_type_enum_temp" AS ENUM('OrganizationAdmin', 'Write', 'GlobalAdmin', 'OrganizationUserAdmin', 'OrganizationGatewayAdmin', 'OrganizationApplicationAdmin', 'Read', 'OrganizationPermission', 'OrganizationApplicationPermissions', 'ApiKeyPermission')` @@ -371,4 +401,16 @@ returning id, "permission"."clonedFromId"`; ); await queryRunner.query(`DROP TYPE "permission_type_enum_temp"`); } + + private async migratePermissionTypeDown(queryRunner: QueryRunner) { + // TODO: Migrate first? + + await queryRunner.query(`ALTER TABLE "permission_type" DROP CONSTRAINT "FK_b8613564bc719a6e37ff0ba243b"`); + await queryRunner.query(`ALTER TABLE "permission_type" DROP CONSTRAINT "FK_6ebf76b0f055fe09e42edfe4848"`); + await queryRunner.query(`ALTER TABLE "permission_type" DROP CONSTRAINT "FK_abd46fe625f90edc07441bd0bb2"`); + await queryRunner.query(`CREATE TYPE "public"."permission_type_enum" AS ENUM('GlobalAdmin', 'OrganizationAdmin', 'Write', 'Read', 'OrganizationPermission', 'OrganizationApplicationPermissions', 'ApiKeyPermission')`); + await queryRunner.query(`ALTER TABLE "permission" ADD "type" "public"."permission_type_enum" NOT NULL`); + await queryRunner.query(`DROP TABLE "permission_type"`); + await queryRunner.query(`CREATE INDEX "IDX_71bf2818fb2ad92e208d7aeadf" ON "permission" ("type") `); + } } diff --git a/src/modules/shared.module.ts b/src/modules/shared.module.ts index 747d52a1..1eac98de 100644 --- a/src/modules/shared.module.ts +++ b/src/modules/shared.module.ts @@ -4,18 +4,14 @@ import { TypeOrmModule } from "@nestjs/typeorm"; import { Application } from "@entities/application.entity"; import { DataTarget } from "@entities/data-target.entity"; import { GenericHTTPDevice } from "@entities/generic-http-device.entity"; -import { GlobalAdminPermission } from "@entities/permissions/global-admin-permission.entity"; import { HttpPushDataTarget } from "@entities/http-push-data-target.entity"; import { FiwareDataTarget } from "@entities/fiware-data-target.entity"; import { IoTDevicePayloadDecoderDataTargetConnection } from "@entities/iot-device-payload-decoder-data-target-connection.entity"; import { IoTDevice } from "@entities/iot-device.entity"; import { LoRaWANDevice } from "@entities/lorawan-device.entity"; -import { OrganizationApplicationPermission } from "@entities/permissions/organization-application-permission.entity"; import { Organization } from "@entities/organization.entity"; -import { OrganizationPermission } from "@entities/permissions/organization-permission.entity"; import { PayloadDecoder } from "@entities/payload-decoder.entity"; import { Permission } from "@entities/permissions/permission.entity"; -import { ReadPermission } from "@entities/permissions/read-permission.entity"; import { ReceivedMessage } from "@entities/received-message.entity"; import { ReceivedMessageMetadata } from "@entities/received-message-metadata.entity"; import { SigFoxDevice } from "@entities/sigfox-device.entity"; @@ -25,16 +21,13 @@ import { DeviceModel } from "@entities/device-model.entity"; import { OpenDataDkDataset } from "@entities/open-data-dk-dataset.entity"; import { AuditLog } from "@services/audit-log.service"; import { ApiKey } from "@entities/api-key.entity"; -import { ApiKeyPermission } from "@entities/api-key-permission.entity"; import { Multicast } from "@entities/multicast.entity"; import { LorawanMulticastDefinition } from "@entities/lorawan-multicast.entity"; -import { OrganizationApplicationAdminPermission } from "@entities/permissions/organization-application-admin-permission.entity"; -import { OrganizationUserAdminPermission } from "@entities/permissions/organization-user-admin-permission.entity"; -import { OrganizationGatewayAdminPermission } from "@entities/permissions/organization-gateway-admin-permission.entity"; import { ControlledProperty } from "@entities/controlled-property.entity"; import { ApplicationDeviceType } from "@entities/application-device-type.entity"; import { ReceivedMessageSigFoxSignals } from "@entities/received-message-sigfox-signals.entity"; import { MqttDataTarget } from "@entities/mqtt-data-target.entity"; +import { PermissionTypeEntity } from "@entities/permissions/permission-type.entity"; @Module({ imports: [ @@ -43,7 +36,6 @@ import { MqttDataTarget } from "@entities/mqtt-data-target.entity"; Application, DataTarget, GenericHTTPDevice, - GlobalAdminPermission, HttpPushDataTarget, FiwareDataTarget, MqttDataTarget, @@ -53,25 +45,19 @@ import { MqttDataTarget } from "@entities/mqtt-data-target.entity"; LoRaWANDevice, OpenDataDkDataset, Organization, - OrganizationApplicationPermission, - OrganizationApplicationAdminPermission, - OrganizationUserAdminPermission, - OrganizationGatewayAdminPermission, - OrganizationPermission, PayloadDecoder, Permission, - ReadPermission, ReceivedMessage, ReceivedMessageMetadata, SigFoxDevice, SigFoxGroup, User, - ApiKeyPermission, Multicast, LorawanMulticastDefinition, ControlledProperty, ApplicationDeviceType, ReceivedMessageSigFoxSignals, + PermissionTypeEntity, ]), ], providers: [AuditLog], diff --git a/src/services/api-key-management/api-key.service.ts b/src/services/api-key-management/api-key.service.ts index 37208734..1d437c6b 100644 --- a/src/services/api-key-management/api-key.service.ts +++ b/src/services/api-key-management/api-key.service.ts @@ -4,7 +4,6 @@ import { CreateApiKeyDto } from "@dto/api-key/create-api-key.dto"; import { ListAllApiKeysResponseDto } from "@dto/api-key/list-all-api-keys-response.dto"; import { ListAllApiKeysDto } from "@dto/api-key/list-all-api-keys.dto"; import { DeleteResponseDto } from "@dto/delete-application-response.dto"; -import { ApiKeyPermission } from "@entities/api-key-permission.entity"; import { ApiKey } from "@entities/api-key.entity"; import { forwardRef, Inject, Injectable, Logger } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; @@ -102,7 +101,7 @@ export class ApiKeyService { ); apiKey.permissions = permissionsDb.map( - pm => ({ ...pm, apiKeys: null } as ApiKeyPermission) + pm => ({ ...pm, apiKeys: null }) ); } diff --git a/src/services/user-management/permission.service.ts b/src/services/user-management/permission.service.ts index c73539a2..044fa26d 100644 --- a/src/services/user-management/permission.service.ts +++ b/src/services/user-management/permission.service.ts @@ -15,12 +15,8 @@ import { PermissionMinimalDto } from "@dto/permission-minimal.dto"; import { UserPermissions } from "@dto/permission-organization-application.dto"; import { CreatePermissionDto } from "@dto/user-management/create-permission.dto"; import { UpdatePermissionDto } from "@dto/user-management/update-permission.dto"; -import { GlobalAdminPermission } from "@entities/permissions/global-admin-permission.entity"; -import { OrganizationApplicationPermission } from "@entities/permissions/organization-application-permission.entity"; import { Organization } from "@entities/organization.entity"; -import { OrganizationPermission } from "@entities/permissions/organization-permission.entity"; import { Permission } from "@entities/permissions/permission.entity"; -import { ReadPermission } from "@entities/permissions/read-permission.entity"; import { User } from "@entities/user.entity"; import { PermissionType } from "@enum/permission-type.enum"; import { ApplicationService } from "@services/device-management/application.service"; @@ -33,9 +29,8 @@ import { ActionType } from "@entities/audit-log-entry"; import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; import { ListAllPermissionsDto } from "@dto/list-all-permissions.dto"; import { isOrganizationApplicationPermission } from "@helpers/security-helper"; -import { OrganizationGatewayAdminPermission } from "@entities/permissions/organization-gateway-admin-permission.entity"; -import { OrganizationUserAdminPermission } from "@entities/permissions/organization-user-admin-permission.entity"; -import { OrganizationApplicationAdminPermission } from "@entities/permissions/organization-application-admin-permission.entity"; +import { PermissionTypeEntity } from "@entities/permissions/permission-type.entity"; +import { PermissionCreator } from "@helpers/permission.helper"; @Injectable() export class PermissionService { @@ -53,12 +48,12 @@ export class PermissionService { async createDefaultPermissions( org: Organization, userId: number - ): Promise { + ): Promise { const { readPermission, orgApplicationAdminPermission, orgAdminPermission, orgGatewayAadminPermission } = this.instantiateDefaultPermissions(org, userId); // Use the manager since otherwise, we'd need a repository for each of them const entityManager = getManager(); - const r = await entityManager.save([ + const r = await entityManager.save([ readPermission, orgApplicationAdminPermission, orgAdminPermission, @@ -77,17 +72,17 @@ export class PermissionService { const organizationGatewayAdminSuffix = `${nameSuffixSeparator}${PermissionType.OrganizationGatewayAdmin}`; const organizationApplicationAdminSuffix = `${nameSuffixSeparator}${PermissionType.OrganizationApplicationAdmin}`; - const readPermission = new ReadPermission(org.name + readSuffix, org, true); - const orgApplicationAdminPermission = new OrganizationApplicationAdminPermission( + const readPermission = PermissionCreator.createRead(org.name + readSuffix, org, true); + const orgApplicationAdminPermission = PermissionCreator.createApplicationAdmin( org.name + organizationApplicationAdminSuffix, org, true ); - const orgAdminPermission = new OrganizationUserAdminPermission( + const orgAdminPermission = PermissionCreator.createUserAdmin( org.name + organizationUserAdminSuffix, org ); - const orgGatewayAadminPermission = new OrganizationGatewayAdminPermission( + const orgGatewayAadminPermission = PermissionCreator.createGatewayAdmin( org.name + organizationGatewayAdminSuffix, org ); @@ -102,13 +97,20 @@ export class PermissionService { return { readPermission, orgApplicationAdminPermission, orgAdminPermission, orgGatewayAadminPermission }; } - async findOrCreateGlobalAdminPermission(): Promise { - const globalAdmin = await getManager().findOne(GlobalAdminPermission); + async findOrCreateGlobalAdminPermission(): Promise { + const globalAdmin = await this.permissionRepository.findOne({ + where: { + // TODO: PERMISSION REIVSED. Will this work? + type: { + type: PermissionType.GlobalAdmin + } + } + }); if (globalAdmin) { return globalAdmin; } - return await getManager().save(new GlobalAdminPermission()); + return await getManager().save(PermissionCreator.createGlobalAdmin()); } async createNewPermission( @@ -119,7 +121,16 @@ export class PermissionService { dto.organizationId ); - const permission = this.createPermission(dto, org); + const permission = PermissionCreator.createByTypes( + dto.name, + dto.levels.map(level => level.type), + org, + dto.automaticallyAddNewApplications + ); + permission.type.forEach(type => { + type.createdBy = userId; + type.updatedBy = userId; + }); await this.mapToPermission(permission, dto); permission.createdBy = userId; @@ -128,34 +139,14 @@ export class PermissionService { return await getManager().save(permission); } - private createPermission(dto: CreatePermissionDto, org: Organization): Permission { - switch (dto.level) { - case PermissionType.OrganizationApplicationAdmin: { - return new OrganizationApplicationAdminPermission(dto.name, org); - } - case PermissionType.OrganizationGatewayAdmin: { - return new OrganizationGatewayAdminPermission(dto.name, org); - } - case PermissionType.OrganizationUserAdmin: { - return new OrganizationUserAdminPermission(dto.name, org); - } - case PermissionType.Read: { - return new ReadPermission( - dto.name, - org, - dto.automaticallyAddNewApplications - ); - } - default: - throw new BadRequestException("Bad PermissionLevel"); - } - } - async autoAddPermissionsToApplication(app: Application): Promise { - const permissionsInOrganisation = await getManager().find( - OrganizationApplicationPermission, + const permissionsInOrganisation = await this.permissionRepository.find( { where: { + // TODO: PERMISSION REIVSED. Will this work? + type: { + type: PermissionType.OrganizationApplicationAdmin + }, organization: { id: app.belongsTo.id, }, @@ -190,7 +181,7 @@ export class PermissionService { ): Promise { const permission = await getManager().findOne(Permission, { where: { id: id }, - relations: ["organization", "users", "applications"], + relations: ["organization", "users", "applications", "type"], }); permission.name = dto.name; @@ -207,6 +198,10 @@ export class PermissionService { permission: Permission, dto: UpdatePermissionDto ): Promise { + // TODO: Is it just as easy to make the frontend do it? What happens if this issue goes through? + // Sanitize types + permission.type = _.uniqBy(permission.type, type => type.type) + if (isOrganizationApplicationPermission(permission)) { permission.applications = await this.applicationService.findManyByIds( dto.applicationIds @@ -238,6 +233,7 @@ export class PermissionService { ) .leftJoinAndSelect("permission.organization", "org") .leftJoinAndSelect("permission.users", "user") + .leftJoinAndSelect("permission.type", "permission_type") .take(query?.limit ? +query.limit : 100) .skip(query?.offset ? +query.offset : 0) .orderBy(orderBy, order); @@ -286,7 +282,7 @@ export class PermissionService { async getPermission(id: number): Promise { return await getManager().findOneOrFail(Permission, { where: { id: id }, - relations: ["organization", "users", "applications"], + relations: ["organization", "users", "applications", "type"], loadRelationIds: { relations: ["createdBy", "updatedBy"], }, @@ -306,8 +302,13 @@ export class PermissionService { "application", '"application"."id"="application_permission"."applicationId" ' ) + .leftJoinAndSelect( + "permission_type", + "permission_type", + '"permission_type"."permissionId"="permission"."id"' + ) .select([ - "permission.type as permission_type", + "permission_type.type as permission_type_type", "permission.organization as organization_id", "application.id as application_id", ]); @@ -332,7 +333,7 @@ export class PermissionService { ): Promise { return await this.buildPermissionsWithApplicationsQuery() .leftJoin("permission.users", "user") - .where("permission.type = :permType AND user.id = :id", { + .where("permission_type.type = :permType AND user.id = :id", { permType: PermissionType.OrganizationApplicationAdmin, id: userId, }) @@ -344,8 +345,9 @@ export class PermissionService { .createQueryBuilder("permission") .leftJoinAndSelect("permission.organization", "organization") .leftJoinAndSelect("organization.applications", "application") + .leftJoinAndSelect("permission.type", "permission_type") .select([ - "permission.type as permission_type", + "permission_type.type as permission_type_type", "permission.organization as organization_id", "application.id as application_id", ]); @@ -396,16 +398,15 @@ export class PermissionService { const res = new UserPermissions(); permissions.forEach(p => { - if (p.permission_type == PermissionType.GlobalAdmin) { + if (p.permission_type_type == PermissionType.GlobalAdmin) { res.isGlobalAdmin = true; - } else if (p.permission_type == PermissionType.OrganizationApplicationAdmin) { + } else if (p.permission_type_type == PermissionType.OrganizationApplicationAdmin) { this.addOrUpdateApplicationIds(res.orgToApplicationAdminPermissions, p); - // Also grant writePermission to the application - } else if (p.permission_type == PermissionType.OrganizationGatewayAdmin) { + } else if (p.permission_type_type == PermissionType.OrganizationGatewayAdmin) { res.orgToGatewayAdminPermissions.add(p.organization_id); - } else if (p.permission_type == PermissionType.OrganizationUserAdmin) { + } else if (p.permission_type_type == PermissionType.OrganizationUserAdmin) { res.orgToUserAdminPermissions.add(p.organization_id); - } else if (p.permission_type == PermissionType.Read) { + } else if (p.permission_type_type == PermissionType.Read) { this.addOrUpdateApplicationIds(res.orgToReadPermissions, p); } }); @@ -419,13 +420,13 @@ export class PermissionService { private isOrganizationApplicationAdmin(permissions: PermissionMinimalDto[]) { return permissions.some( - x => x.permission_type == PermissionType.OrganizationApplicationAdmin + x => x.permission_type_type == PermissionType.OrganizationApplicationAdmin ); } private isOrganizationUserAdmin(permissions: PermissionMinimalDto[]) { return permissions.some( - x => x.permission_type == PermissionType.OrganizationUserAdmin + x => x.permission_type_type == PermissionType.OrganizationUserAdmin ); } diff --git a/src/services/user-management/user.service.ts b/src/services/user-management/user.service.ts index 8dbcf412..5efc21f8 100644 --- a/src/services/user-management/user.service.ts +++ b/src/services/user-management/user.service.ts @@ -90,7 +90,7 @@ export class UserService { async findOneWithOrganizations(id: number): Promise { return await this.userRepository.findOne(id, { - relations: ["permissions", "permissions.organization"], + relations: ["permissions", "permissions.organization", "permissions.type"], }); } From 465b6963aa9e289e890630591418865d8fb4b311 Mon Sep 17 00:00:00 2001 From: Aram Al-Sabti Date: Mon, 16 May 2022 18:13:04 +0200 Subject: [PATCH 03/19] Fix some test errors --- test/e2e/crud/application.e2e-spec.ts | 8 ++++---- test/e2e/crud/data-target.e2e-spec.ts | 8 ++++++++ test/e2e/crud/permission.e2e-spec.ts | 4 +--- test/e2e/crud/search.e2e-spec.ts | 2 +- test/e2e/test-helpers.ts | 10 ++++++---- .../device-integration-persistence.service.spec.ts | 3 +++ 6 files changed, 23 insertions(+), 12 deletions(-) diff --git a/test/e2e/crud/application.e2e-spec.ts b/test/e2e/crud/application.e2e-spec.ts index 5faa1d6f..8e0f7261 100644 --- a/test/e2e/crud/application.e2e-spec.ts +++ b/test/e2e/crud/application.e2e-spec.ts @@ -553,11 +553,11 @@ describe("ApplicationController (e2e)", () => { const user = await generateSavedReadWriteUser(org); const readPerm = user.permissions.find( - x => x.type == PermissionType.Read + x => x.type.some(({ type }) => type === PermissionType.Read) ) as ReadPermission; readPerm.applications = [app1]; const orgAppAdminPerm = user.permissions.find( - x => x.type == PermissionType.OrganizationApplicationAdmin + x => x.type.some(({ type }) => type === PermissionType.OrganizationApplicationAdmin) ) as OrganizationApplicationAdminPermission; orgAppAdminPerm.applications = [app1]; await getManager().save([readPerm, orgAppAdminPerm]); @@ -617,12 +617,12 @@ describe("ApplicationController (e2e)", () => { const org = await generateSavedOrganization(); const readPerm = org.permissions.find( - x => x.type == PermissionType.Read + x => x.type.some(({ type }) => type === PermissionType.Read) ) as ReadPermission; expect(readPerm.automaticallyAddNewApplications).toBeTruthy(); const orgAppAdminPerm = org.permissions.find( - x => x.type == PermissionType.OrganizationApplicationAdmin + x => x.type.some(({ type }) => type === PermissionType.OrganizationApplicationAdmin) ) as OrganizationApplicationAdminPermission; expect(orgAppAdminPerm.automaticallyAddNewApplications).toBeTruthy(); diff --git a/test/e2e/crud/data-target.e2e-spec.ts b/test/e2e/crud/data-target.e2e-spec.ts index 8b30448a..5e57e8a0 100644 --- a/test/e2e/crud/data-target.e2e-spec.ts +++ b/test/e2e/crud/data-target.e2e-spec.ts @@ -245,6 +245,8 @@ describe("DataTargetController (e2e)", () => { url: "http://example.com/test-endepunkt", timeout: 3000, authorizationHeader: null, + tenant: '', + context: '', }; await request(app.getHttpServer()) @@ -338,6 +340,8 @@ describe("DataTargetController (e2e)", () => { url: "http://example.com/test-endepunkt", timeout: 3000, authorizationHeader: null, + tenant: '', + context: '', }; await request(app.getHttpServer()) @@ -378,6 +382,8 @@ describe("DataTargetController (e2e)", () => { authorEmail: "e2e@test.dk", resourceTitle: "Rumsensor2", }, + tenant: '', + context: '', }; await request(app.getHttpServer()) @@ -421,6 +427,8 @@ describe("DataTargetController (e2e)", () => { authorEmail: "e2e@test.dk", resourceTitle: "Rumsensor2", }, + tenant: '', + context: '', }; await request(app.getHttpServer()) diff --git a/test/e2e/crud/permission.e2e-spec.ts b/test/e2e/crud/permission.e2e-spec.ts index 8926d1d8..1ef45037 100644 --- a/test/e2e/crud/permission.e2e-spec.ts +++ b/test/e2e/crud/permission.e2e-spec.ts @@ -9,8 +9,6 @@ import { getManager } from "typeorm"; import configuration from "@config/configuration"; import { CreatePermissionDto } from "@dto/user-management/create-permission.dto"; import { UpdatePermissionDto } from "@dto/user-management/update-permission.dto"; -import { OrganizationApplicationPermission } from "@entities/permissions/organization-application-permission.entity"; -import { ReadPermission } from "@entities/permissions/read-permission.entity"; import { User } from "@entities/user.entity"; import { PermissionType } from "@enum/permission-type.enum"; import { AuthModule } from "@modules/user-management/auth.module"; @@ -162,7 +160,7 @@ describe("PermissionController (e2e)", () => { const org = await generateSavedOrganization("E2E"); const dto: CreatePermissionDto = { - level: PermissionType.Read, + levels: PermissionType.Read, name: "E2E readers", organizationId: org.id, userIds: [], diff --git a/test/e2e/crud/search.e2e-spec.ts b/test/e2e/crud/search.e2e-spec.ts index d003f982..37383c88 100644 --- a/test/e2e/crud/search.e2e-spec.ts +++ b/test/e2e/crud/search.e2e-spec.ts @@ -128,7 +128,7 @@ describe("SearchController (e2e)", () => { orgAdminJwt = generateValidJwtForUser(orgAdminUser); const orgAppAdminPermission = org1.permissions.find( - x => x.type == PermissionType.OrganizationApplicationAdmin + x => x.type.some(({ type }) => type === PermissionType.OrganizationApplicationAdmin) ) as OrganizationApplicationAdminPermission; orgAppAdminPermission.applications = [app1_1]; await getManager().save(orgAppAdminPermission); diff --git a/test/e2e/test-helpers.ts b/test/e2e/test-helpers.ts index 9736c5fc..c9367d02 100644 --- a/test/e2e/test-helpers.ts +++ b/test/e2e/test-helpers.ts @@ -271,10 +271,10 @@ export async function generateSavedOrganizationAdminUser( } export async function generateSavedReadWriteUser(org: Organization): Promise { - const appAdminPerm = org.permissions.find(x => x.type == PermissionType.OrganizationApplicationAdmin); - const gatewayAdminPerm = org.permissions.find(x => x.type == PermissionType.OrganizationGatewayAdmin); - const userAdminPerm = org.permissions.find(x => x.type == PermissionType.OrganizationUserAdmin); - const readPerm = org.permissions.find(x => x.type == PermissionType.Read); + const appAdminPerm = org.permissions.find(x => x.type.some(({ type }) => type === PermissionType.OrganizationApplicationAdmin)); + const gatewayAdminPerm = org.permissions.find(x => x.type.some(({ type }) => type === PermissionType.OrganizationGatewayAdmin)); + const userAdminPerm = org.permissions.find(x => x.type.some(({ type }) => type === PermissionType.OrganizationUserAdmin)); + const readPerm = org.permissions.find(x => x.type.some(({ type }) => type === PermissionType.Read)); return await getManager().save( generateUser([appAdminPerm, gatewayAdminPerm, userAdminPerm, readPerm]) ); @@ -742,6 +742,7 @@ export function generateLoRaWANRawRequestDto(iotDeviceId?: number): RawRequestDt }`), iotDeviceId: iotDeviceId || 1, unixTimestamp: 1596921546, + type: IoTDeviceType.LoRaWAN, }; } @@ -788,6 +789,7 @@ export function generateSigfoxRawRequestDto(iotDeviceId?: number): RawRequestDto rawPayload: JSON.parse(SIGFOX_PAYLOAD), iotDeviceId: iotDeviceId || 1, unixTimestamp: 1596721546, + type: IoTDeviceType.SigFox, }; } diff --git a/test/unit/device-integration-persistence.service.spec.ts b/test/unit/device-integration-persistence.service.spec.ts index 1510e351..783580dd 100644 --- a/test/unit/device-integration-persistence.service.spec.ts +++ b/test/unit/device-integration-persistence.service.spec.ts @@ -119,6 +119,7 @@ describe("DeviceIntegrationPersistenceService", () => { updatedAt: new Date(), belongsTo: org, permissions: [], + multicasts: [], }, connections: [], name: "Test IoTDevice", @@ -128,6 +129,8 @@ describe("DeviceIntegrationPersistenceService", () => { type: IoTDeviceType.GenericHttp, latestReceivedMessage: null, receivedMessagesMetadata: [], + multicasts: [], + receivedSigFoxSignalsMessages: [], }; it("test mapDtoToNewReceivedMessageMetadata - Sigfox data + with timestmap", async () => { From b0ee4429b37b6b0581bfc5451d89f7c6d70b2c07 Mon Sep 17 00:00:00 2001 From: Nikolaj Gustafsson Date: Tue, 17 May 2022 11:35:32 +0200 Subject: [PATCH 04/19] Feature/iot 1320 fix sig fox connection (#165) * Fixes issues with communication with the SigFox API * Better description for SigFox controller endpoint Co-authored-by: nlg --- .../admin-controller/sigfox/sigfox-group.controller.ts | 4 ++-- src/services/sigfox/generic-sigfox-administation.service.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/controllers/admin-controller/sigfox/sigfox-group.controller.ts b/src/controllers/admin-controller/sigfox/sigfox-group.controller.ts index 51243eee..3bcec8d4 100644 --- a/src/controllers/admin-controller/sigfox/sigfox-group.controller.ts +++ b/src/controllers/admin-controller/sigfox/sigfox-group.controller.ts @@ -71,7 +71,7 @@ export class SigfoxGroupController { @Get(":id") @ApiProduces("application/json") - @ApiOperation({ summary: "List a SigFox Groups" }) + @ApiOperation({ summary: "Get one group by ID" }) @Read() async getOne( @Req() req: AuthenticatedRequest, @@ -81,7 +81,7 @@ export class SigfoxGroupController { try { group = await this.service.findOne(id); } catch (err) { - throw new NotFoundException(); + return null; } checkIfUserHasReadAccessToOrganization(req, group.belongsTo.id); return group; diff --git a/src/services/sigfox/generic-sigfox-administation.service.ts b/src/services/sigfox/generic-sigfox-administation.service.ts index 53c1d19c..15fc42d1 100644 --- a/src/services/sigfox/generic-sigfox-administation.service.ts +++ b/src/services/sigfox/generic-sigfox-administation.service.ts @@ -81,7 +81,7 @@ export class GenericSigfoxAdministationService { path, sigfoxGroup, method, - dto = {}, + dto = undefined, useCache = false, }: RequestParameters): Promise { const config = await this.generateAxiosConfig( From e5eb6485f96a74ed5ab65ec584ee9e6025c7e437 Mon Sep 17 00:00:00 2001 From: augusthjerrild <70511721+augusthjerrild@users.noreply.github.com> Date: Tue, 17 May 2022 15:29:59 +0200 Subject: [PATCH 05/19] Feature/iot 1249 manage kombit users merged (#168) * Made it possible to get all organizations without other permissions that you have to be logged in. Nessesary since a new user should choose which organizations the user wishes to be a part of. * Ready for put setEmail * set email for user * When logging in to kombit user, the entered email and organizations is now saved in db. * changed to ManyToMany between user and organizations. Made a migration * Possible to update the organizations that the user applies. * Changed names so it's more clear that the organizations on the user is requested organizations. Changed the migration. * Made mail work with mail test server. It's possible to send a mail. Only org admins should get the mail, and if no org admins then global admin. Need to take care of exception in user.service * Changed user name to awaiting users. Made api call to get awaiting users. * Made backend for kombit. Sends mail at verification and rejection. Has to be changed to environment variables. Check all TODOS before merging * changed migration to match with stage * Getting ready for OAuth2 mail system. We need to generate refreshtoken ourselves. Made TODO::: for places that needs to do. * Service is now looking at environment variables. Currently set to Ethereal Email server for test. * Changed default email values to mailgun * Made a new controller for new kombit users so endpoints is allowed without permissions. Made map functions * Now possible to get permissions onto an organization * updated to correct and latest migration * Renamed migration * Added frontend.baseurl environment property Changed UserRejectDTO to also have id of the user to reject Minor code quality changes Co-authored-by: August Andersen Co-authored-by: nlg --- package-lock.json | 14 ++ package.json | 2 + src/config/configuration.ts | 13 +- .../user-management/auth.controller.ts | 2 +- .../new-kombit-creation.controller.ts | 178 ++++++++++++++++++ .../organization.controller.ts | 5 +- .../user-management/permission.controller.ts | 54 +++++- .../user-management/user.controller.ts | 45 ++++- src/entities/api-key-permission.entity.ts | 10 - src/entities/api-key.entity.ts | 2 +- src/entities/application.entity.ts | 4 +- .../add-user-to-permission.dto.ts | 20 ++ .../create-new-kombit-user.dto.ts | 13 ++ .../dto/user-management/reject-user.dto.ts | 12 ++ .../user-management/update-user-orgs.dto.ts | 9 + src/entities/enum/error-codes.enum.ts | 4 + .../global-admin-permission.entity.ts | 12 -- src/entities/lorawan-multicast.entity.ts | 2 +- .../organization-admin-permission.entity.ts | 15 -- ...anization-application-permission.entity.ts | 22 --- .../organization-permission.entity.ts | 17 -- src/entities/organization.entity.ts | 11 +- src/entities/permission.entity.ts | 77 +++++++- src/entities/read-permission.entity.ts | 13 -- src/entities/user.entity.ts | 21 ++- src/entities/write-permission.entity.ts | 13 -- .../1652771064000-KombitUserManagement.ts | 24 +++ src/modules/app.module.ts | 2 + src/modules/shared.module.ts | 18 +- .../new-kombit-creation.module.ts | 13 ++ .../user-management/organization.module.ts | 3 +- src/modules/user-management/user.module.ts | 5 +- .../api-key-management/api-key.service.ts | 2 +- .../user-management/organization.service.ts | 109 +++++++++-- .../user-management/permission.service.ts | 48 +++-- src/services/user-management/user.service.ts | 166 +++++++++++++++- 36 files changed, 820 insertions(+), 160 deletions(-) create mode 100644 src/controllers/user-management/new-kombit-creation.controller.ts delete mode 100644 src/entities/api-key-permission.entity.ts create mode 100644 src/entities/dto/user-management/add-user-to-permission.dto.ts create mode 100644 src/entities/dto/user-management/create-new-kombit-user.dto.ts create mode 100644 src/entities/dto/user-management/reject-user.dto.ts create mode 100644 src/entities/dto/user-management/update-user-orgs.dto.ts delete mode 100644 src/entities/global-admin-permission.entity.ts delete mode 100644 src/entities/organization-admin-permission.entity.ts delete mode 100644 src/entities/organization-application-permission.entity.ts delete mode 100644 src/entities/organization-permission.entity.ts delete mode 100644 src/entities/read-permission.entity.ts delete mode 100644 src/entities/write-permission.entity.ts create mode 100644 src/migration/1652771064000-KombitUserManagement.ts create mode 100644 src/modules/user-management/new-kombit-creation.module.ts diff --git a/package-lock.json b/package-lock.json index af9809bf..f4fdade1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1767,6 +1767,15 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.14.tgz", "integrity": "sha512-UHnOPWVWV1z+VV8k6L1HhG7UbGBgIdghqF3l9Ny9ApPghbjICXkUJSd/b9gOgQfjM1r+37cipdw/HJ3F6ICEnQ==" }, + "@types/nodemailer": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.4.tgz", + "integrity": "sha512-Ksw4t7iliXeYGvIQcSIgWQ5BLuC/mljIEbjf615svhZL10PE9t+ei8O9gDaD3FPCasUJn9KTLwz2JFJyiiyuqw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/normalize-package-data": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", @@ -7933,6 +7942,11 @@ "integrity": "sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==", "dev": true }, + "nodemailer": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.7.2.tgz", + "integrity": "sha512-Dz7zVwlef4k5R71fdmxwR8Q39fiboGbu3xgswkzGwczUfjp873rVxt1O46+Fh0j1ORnAC6L9+heI8uUpO6DT7Q==" + }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", diff --git a/package.json b/package.json index e7c4ccfb..df932bd3 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "mqtt": "^4.2.6", "nestjs-pino": "^1.3.0", "njwt": "^1.0.0", + "nodemailer": "^6.7.2", "passport": "^0.4.1", "passport-headerapikey": "^1.2.2", "passport-jwt": "^4.0.0", @@ -83,6 +84,7 @@ "@types/express": "^4.17.9", "@types/lodash": "^4.14.165", "@types/node": "^14.14.14", + "@types/nodemailer": "^6.4.4", "@types/passport-jwt": "^3.0.3", "@types/passport-local": "^1.0.33", "@types/supertest": "^2.0.10", diff --git a/src/config/configuration.ts b/src/config/configuration.ts index 2a601ac0..735d110c 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -35,5 +35,16 @@ export default (): any => { logLevels: process.env.LOG_LEVEL ? GetLogLevels(process.env.LOG_LEVEL) : GetLogLevels("debug"), - }; + email: { + host: process.env.EMAIL_HOST || "smtp.ethereal.email", + port: process.env.EMAIL_PORT || 587, + user: process.env.EMAIL_USER || "ara.kertzmann8@ethereal.email", + pass: process.env.EMAIL_PASS || "KzRSyYReEygpFPPZdd", + from: process.env.EMAIL_FROM || "ara.kertzmann8@ethereal.email" + }, + frontend: { + baseurl: + process.env.FRONTEND_BASEURL || "http://localhost:8081" + } + }; }; diff --git a/src/controllers/user-management/auth.controller.ts b/src/controllers/user-management/auth.controller.ts index 6fb75547..ac22f1e4 100644 --- a/src/controllers/user-management/auth.controller.ts +++ b/src/controllers/user-management/auth.controller.ts @@ -31,7 +31,6 @@ import { import { JwtPayloadDto } from "@dto/internal/jwt-payload.dto"; import { LoginDto } from "@dto/login.dto"; import { Organization } from "@entities/organization.entity"; -import { OrganizationPermission } from "@entities/organization-permission.entity"; import { User } from "@entities/user.entity"; import { PermissionType } from "@enum/permission-type.enum"; import { AuthService } from "@services/user-management/auth.service"; @@ -42,6 +41,7 @@ import { Request as expressRequest, Response } from "express"; import { KombitStrategy } from "@auth/kombit.strategy"; import { ErrorCodes } from "@enum/error-codes.enum"; import { CustomExceptionFilter } from "@auth/custom-exception-filter"; +import { OrganizationPermission } from "@entities/permission.entity"; @UseFilters(new CustomExceptionFilter()) @ApiTags("Auth") diff --git a/src/controllers/user-management/new-kombit-creation.controller.ts b/src/controllers/user-management/new-kombit-creation.controller.ts new file mode 100644 index 00000000..b2605f2d --- /dev/null +++ b/src/controllers/user-management/new-kombit-creation.controller.ts @@ -0,0 +1,178 @@ +import { JwtAuthGuard } from "@auth/jwt-auth.guard"; +import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; +import { ListAllMinimalOrganizationsResponseDto } from "@dto/list-all-organizations-response.dto"; +import { ListAllUsersMinimalResponseDto } from "@dto/list-all-users-minimal-response.dto"; +import { CreateNewKombitUserDto } from "@dto/user-management/create-new-kombit-user.dto"; +import { UpdateUserOrgsDto } from "@dto/user-management/update-user-orgs.dto"; +import { UserResponseDto } from "@dto/user-response.dto"; +import { ActionType } from "@entities/audit-log-entry"; +import { Organization } from "@entities/organization.entity"; +import { User } from "@entities/user.entity"; +import { ErrorCodes } from "@enum/error-codes.enum"; + +import { + BadRequestException, + Body, + Controller, + Get, + NotFoundException, + Param, + ParseIntPipe, + Put, + Query, + Req, + UseGuards, +} from "@nestjs/common"; +import { + ApiBearerAuth, + ApiForbiddenResponse, + ApiNotFoundResponse, + ApiOperation, + ApiTags, + ApiUnauthorizedResponse, +} from "@nestjs/swagger"; +import { AuditLog } from "@services/audit-log.service"; +import { OrganizationService } from "@services/user-management/organization.service"; +import { PermissionService } from "@services/user-management/permission.service"; +import { UserService } from "@services/user-management/user.service"; +import { Permission, OrganizationPermission } from "@entities/permission.entity"; + +@UseGuards(JwtAuthGuard) +@ApiBearerAuth() +@ApiForbiddenResponse() +@ApiUnauthorizedResponse() +@ApiTags("KombitEmailCreation") +@Controller("kombitCreation") +export class NewKombitCreationController { + constructor( + private organizationService: OrganizationService, + private userService: UserService, + private permissionService: PermissionService + ) {} + + @Put("createNewKombitUser") + @ApiOperation({ summary: "Create kombit-user Email" }) + async newKombitUser( + @Req() req: AuthenticatedRequest, + @Body() dto: CreateNewKombitUserDto + ): Promise { + + try { + const user: User = await this.userService.findOne(req.user.userId); + const permissions = await this.permissionService.findManyWithRelations( + dto.requestedOrganizationIds + ); + + const requestedOrganizations: Organization[] = await this.organizationService.mapPermissionsToOrganizations( + permissions + ); + + if (!user.email) { + const updatedUser: User = await this.userService.newKombitUser( + dto, + requestedOrganizations, + user + ); + + for (let index = 0; index < dto.requestedOrganizationIds.length; index++) { + const dbOrg = await this.organizationService.findByIdWithUsers( + requestedOrganizations[index].id + ); + + await this.organizationService.updateAwaitingUsers( + dbOrg, + updatedUser + ); + } + + AuditLog.success(ActionType.UPDATE, User.name, req.user.userId); + return updatedUser; + + } else { + throw new BadRequestException(ErrorCodes.EmailAlreadyExists); + } + } catch (err) { + AuditLog.fail(ActionType.UPDATE, User.name, req.user.userId); + throw err; + } + } + + @Get("minimal") + @ApiOperation({ + summary: + "Get list of the minimal representation of organizations, i.e. id and name.", + }) + async findAllMinimal(): Promise { + return await this.organizationService.findAllMinimal(); + } + + @Get("minimalUsers") + @ApiOperation({ summary: "Get all id,names of users" }) + async findAllMinimalUsers(): Promise { + return await this.userService.findAllMinimal(); + } + + @Put("updateUserOrgs") + @ApiOperation({ summary: "Updates the users organizations" }) + @ApiNotFoundResponse() + async updateUserOrgs( + @Req() req: AuthenticatedRequest, + @Body() updateUserOrgsDto: UpdateUserOrgsDto + ): Promise { + + try { + const user = await this.userService.findOne(req.user.userId); + const permissions: OrganizationPermission[] = await this.permissionService.findManyWithRelations( + updateUserOrgsDto.requestedOrganizationIds + ); + + const requestedOrganizations = await this.organizationService.mapPermissionsToOrganizations( + permissions + ); + + for (let index = 0; index < requestedOrganizations.length; index++) { + await this.userService.sendOrganizationRequestMail( + user, + requestedOrganizations[index] + ); + } + + for (let index = 0; index < updateUserOrgsDto.requestedOrganizationIds.length; index++) { + const dbOrg = await this.organizationService.findByIdWithUsers( + requestedOrganizations[index].id + ); + + await this.organizationService.updateAwaitingUsers(dbOrg, user); + } + + AuditLog.success(ActionType.UPDATE, User.name, req.user.userId); + return updateUserOrgsDto; + } catch (err) { + AuditLog.fail(ActionType.UPDATE, User.name, req.user.userId); + throw err; + } + } + + @Get(":id") + @ApiOperation({ summary: "Get one user" }) + async find( + @Param("id", new ParseIntPipe()) id: number, + @Query("extendedInfo") extendedInfo?: boolean + ): Promise { + + const getExtendedInfo = extendedInfo != null ? extendedInfo : false; + try { + // Don't leak the passwordHash + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { passwordHash, ...user } = await this.userService.findOne( + id, + getExtendedInfo, + getExtendedInfo + ); + + return user; + } catch (err) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); + } + } +} diff --git a/src/controllers/user-management/organization.controller.ts b/src/controllers/user-management/organization.controller.ts index ff1df55f..fc4fc8e5 100644 --- a/src/controllers/user-management/organization.controller.ts +++ b/src/controllers/user-management/organization.controller.ts @@ -41,6 +41,7 @@ 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 { UserService } from "@services/user-management/user.service"; @UseGuards(JwtAuthGuard, RolesGuard) @ApiBearerAuth() @@ -50,7 +51,9 @@ import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; @ApiTags("User Management") @Controller("organization") export class OrganizationController { - constructor(private organizationService: OrganizationService) {} + constructor( + private organizationService: OrganizationService, + ) {} private readonly logger = new Logger(OrganizationController.name); @GlobalAdmin() diff --git a/src/controllers/user-management/permission.controller.ts b/src/controllers/user-management/permission.controller.ts index c4898ff2..fdca60ab 100644 --- a/src/controllers/user-management/permission.controller.ts +++ b/src/controllers/user-management/permission.controller.ts @@ -3,7 +3,6 @@ import { Body, Controller, Delete, - ForbiddenException, Get, NotFoundException, Param, @@ -30,7 +29,7 @@ import { ListAllPermissionsResponseDto } from "@dto/list-all-permissions-respons import { CreatePermissionDto } from "@dto/user-management/create-permission.dto"; import { UpdatePermissionDto } from "@dto/user-management/update-permission.dto"; import { AuthenticatedRequest } from "@entities/dto/internal/authenticated-request"; -import { OrganizationPermission } from "@entities/organization-permission.entity"; +import { OrganizationPermission } from "@entities/permission.entity"; import { Permission } from "@entities/permission.entity"; import { PermissionType } from "@enum/permission-type.enum"; import { @@ -40,14 +39,16 @@ import { import { PermissionService } from "@services/user-management/permission.service"; import { AuditLog } from "@services/audit-log.service"; import { ActionType } from "@entities/audit-log-entry"; -import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; import { UserService } from "@services/user-management/user.service"; -import { UserResponseDto } from "@dto/user-response.dto"; import { ListAllUsersResponseDto } from "@dto/list-all-users-response.dto"; import { ListAllPaginated } from "@dto/list-all-paginated.dto"; import { ListAllPermissionsDto } from "@dto/list-all-permissions.dto"; import { ApplicationService } from "@services/device-management/application.service"; import { ListAllApplicationsResponseDto } from "@dto/list-all-applications-response.dto"; +import { PermissionRequestAcceptUser } from "@dto/user-management/add-user-to-permission.dto"; +import { OrganizationService } from "@services/user-management/organization.service"; +import { Organization } from "@entities/organization.entity"; +import { User } from "@entities/user.entity"; @UseGuards(JwtAuthGuard, RolesGuard) @ApiBearerAuth() @@ -60,7 +61,8 @@ export class PermissionController { constructor( private permissionService: PermissionService, private userService: UserService, - private applicationService: ApplicationService + private applicationService: ApplicationService, + private organizationService: OrganizationService ) {} @Post() @@ -91,6 +93,48 @@ export class PermissionController { } } + @Put("/acceptUser") + @ApiOperation({ summary: "add user to permission" }) + async addUserToPermission( + @Req() req: AuthenticatedRequest, + @Body() dto: PermissionRequestAcceptUser + ): Promise { + try { + checkIfUserHasAdminAccessToOrganization(req, dto.organizationId); + let dbPermission: Permission; + + const permissions: OrganizationPermission[] = await this.permissionService.findOneWithRelations( + dto.organizationId + ); + + const org: Organization = await this.organizationService.mapPermissionsToOneOrganization( + permissions + ); + + const user: User = await this.userService.findOne(dto.userId); + for (let index = 0; index < org.permissions.length; index++) { + if (org.permissions[index].type === dto.level) { + dbPermission = await this.permissionService.getPermission( + org.permissions[index].id + ); + } + } + const resultUser = await this.userService.acceptUser(user, org, dbPermission); + + AuditLog.success( + ActionType.UPDATE, + Permission.name, + req.user.userId, + resultUser.id, + resultUser.name + ); + return resultUser; + } catch (err) { + AuditLog.fail(ActionType.UPDATE, Permission.name, req.user.userId); + throw err; + } + } + @Put(":id") @ApiOperation({ summary: "Update permission" }) async updatePermission( diff --git a/src/controllers/user-management/user.controller.ts b/src/controllers/user-management/user.controller.ts index 9d6cdaae..a28be883 100644 --- a/src/controllers/user-management/user.controller.ts +++ b/src/controllers/user-management/user.controller.ts @@ -6,7 +6,6 @@ import { InternalServerErrorException, NotFoundException, Param, - ParseBoolPipe, ParseIntPipe, Post, Put, @@ -18,6 +17,7 @@ import { Logger } from "@nestjs/common"; import { ApiBearerAuth, ApiForbiddenResponse, + ApiNotFoundResponse, ApiOperation, ApiTags, ApiUnauthorizedResponse, @@ -32,7 +32,10 @@ import { CreateUserDto } from "@dto/user-management/create-user.dto"; import { UpdateUserDto } from "@dto/user-management/update-user.dto"; import { UserResponseDto } from "@dto/user-response.dto"; import { ErrorCodes } from "@entities/enum/error-codes.enum"; -import { checkIfUserIsGlobalAdmin } from "@helpers/security-helper"; +import { + checkIfUserHasAdminAccessToOrganization, + checkIfUserIsGlobalAdmin, +} from "@helpers/security-helper"; import { UserService } from "@services/user-management/user.service"; import { ListAllUsersResponseDto } from "@dto/list-all-users-response.dto"; import { ListAllUsersMinimalResponseDto } from "@dto/list-all-users-minimal-response.dto"; @@ -40,6 +43,11 @@ import { AuditLog } from "@services/audit-log.service"; import { ActionType } from "@entities/audit-log-entry"; import { User } from "@entities/user.entity"; import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; +import { CreateNewKombitUserDto } from "@dto/user-management/create-new-kombit-user.dto"; +import { OrganizationService } from "@services/user-management/organization.service"; +import { UpdateUserOrgsDto } from "@dto/user-management/update-user-orgs.dto"; +import { Organization } from "@entities/organization.entity"; +import { RejectUserDto } from "@dto/user-management/reject-user.dto"; @UseGuards(JwtAuthGuard, RolesGuard) @ApiBearerAuth() @@ -49,7 +57,10 @@ import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; @ApiTags("User Management") @Controller("user") export class UserController { - constructor(private userService: UserService) {} + constructor( + private userService: UserService, + private organizationService: OrganizationService + ) {} private readonly logger = new Logger(UserController.name); @@ -99,6 +110,20 @@ export class UserController { } } + @Put("/rejectUser") + @ApiOperation({ summary: "Rejects user and removes from awaiting users" }) + async rejectUser( + @Req() req: AuthenticatedRequest, + @Body() body: RejectUserDto + ): Promise { + checkIfUserHasAdminAccessToOrganization(req, body.orgId); + + const user = await this.userService.findOne(body.userIdToReject); + const organization = await this.organizationService.findByIdWithUsers(body.orgId); + + return await this.organizationService.rejectAwaitingUser(user, organization); + } + @Put(":id") @ApiOperation({ summary: "Change a user" }) async update( @@ -151,6 +176,7 @@ export class UserController { @Get(":id") @ApiOperation({ summary: "Get one user" }) + @Read() async find( @Param("id", new ParseIntPipe()) id: number, @Query("extendedInfo") extendedInfo?: boolean @@ -176,4 +202,17 @@ export class UserController { async findAll(@Query() query?: ListAllEntitiesDto): Promise { return await this.userService.findAll(query); } + + @Get("/awaitingUsers/:id") + @ApiOperation({ summary: "Get awaiting users" }) + async findAwaitingUsers( + @Param("id", new ParseIntPipe()) organizationId: number, + @Query() query?: ListAllEntitiesDto + ): Promise { + try { + return await this.userService.getAwaitingUsers(organizationId, query); + } catch (err) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); + } + } } diff --git a/src/entities/api-key-permission.entity.ts b/src/entities/api-key-permission.entity.ts deleted file mode 100644 index 4cbcca4e..00000000 --- a/src/entities/api-key-permission.entity.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Permission } from "@entities/permission.entity"; -import { PermissionType } from "@enum/permission-type.enum"; -import { ChildEntity, ManyToMany } from "typeorm"; -import { ApiKey } from "./api-key.entity"; - -@ChildEntity(PermissionType.ApiKeyPermission) -export abstract class ApiKeyPermission extends Permission { - @ManyToMany(_ => ApiKey, key => key.permissions, { onDelete: "CASCADE" }) - apiKeys: ApiKey[]; -} diff --git a/src/entities/api-key.entity.ts b/src/entities/api-key.entity.ts index da650106..57721b84 100644 --- a/src/entities/api-key.entity.ts +++ b/src/entities/api-key.entity.ts @@ -9,8 +9,8 @@ import { OneToOne, Unique, } from "typeorm"; -import { ApiKeyPermission } from "./api-key-permission.entity"; import { DbBaseEntity } from "./base.entity"; +import { ApiKeyPermission } from "./permission.entity"; @Entity("api_key") @Unique([nameof("key")]) diff --git a/src/entities/application.entity.ts b/src/entities/application.entity.ts index 6ee4c817..f8a2c774 100644 --- a/src/entities/application.entity.ts +++ b/src/entities/application.entity.ts @@ -1,7 +1,6 @@ -import { DbBaseEntity } from "@entities/base.entity"; import { DataTarget } from "@entities/data-target.entity"; import { IoTDevice } from "@entities/iot-device.entity"; -import { OrganizationApplicationPermission } from "@entities/organization-application-permission.entity"; +import { OrganizationApplicationPermission } from "@entities/permission.entity"; import { Organization } from "@entities/organization.entity"; import { ApplicationStatus } from "@enum/application-status.enum"; import { @@ -17,6 +16,7 @@ import { import { ApplicationDeviceType } from "./application-device-type.entity"; import { ControlledProperty } from "./controlled-property.entity"; import { Multicast } from "./multicast.entity"; +import { DbBaseEntity } from "@entities/base.entity"; @Entity("application") @Unique(["name"]) diff --git a/src/entities/dto/user-management/add-user-to-permission.dto.ts b/src/entities/dto/user-management/add-user-to-permission.dto.ts new file mode 100644 index 00000000..f7dd7274 --- /dev/null +++ b/src/entities/dto/user-management/add-user-to-permission.dto.ts @@ -0,0 +1,20 @@ +import { PermissionType } from "@enum/permission-type.enum"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsEnum, IsNumber } from "class-validator"; + +export class PermissionRequestAcceptUser { + @ApiProperty({ required: true }) + @IsNumber() + organizationId: number; + + @ApiProperty({ required: true }) + @IsNumber() + userId: number; + + @ApiProperty({ + required: true, + enum: PermissionType, + }) + @IsEnum(PermissionType) + level: "OrganizationAdmin" | "Write" | "Read"; +} diff --git a/src/entities/dto/user-management/create-new-kombit-user.dto.ts b/src/entities/dto/user-management/create-new-kombit-user.dto.ts new file mode 100644 index 00000000..d252077d --- /dev/null +++ b/src/entities/dto/user-management/create-new-kombit-user.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { ArrayMinSize, IsEmail, IsNotEmpty } from "class-validator"; + +export class CreateNewKombitUserDto { + @ApiProperty({ required: true }) + @IsEmail() + @IsNotEmpty() + email: string; + + @ApiProperty({ required: true }) + @ArrayMinSize(1) + requestedOrganizationIds: number[]; +} diff --git a/src/entities/dto/user-management/reject-user.dto.ts b/src/entities/dto/user-management/reject-user.dto.ts new file mode 100644 index 00000000..97d07ff6 --- /dev/null +++ b/src/entities/dto/user-management/reject-user.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsNumber } from "class-validator"; + +export class RejectUserDto { + @ApiProperty({ required: true }) + @IsNumber() + orgId: number; + + @ApiProperty({ required: true }) + @IsNumber() + userIdToReject: number; +} diff --git a/src/entities/dto/user-management/update-user-orgs.dto.ts b/src/entities/dto/user-management/update-user-orgs.dto.ts new file mode 100644 index 00000000..c7c297fa --- /dev/null +++ b/src/entities/dto/user-management/update-user-orgs.dto.ts @@ -0,0 +1,9 @@ +import { Organization } from "@entities/organization.entity"; +import { ApiProperty } from "@nestjs/swagger"; +import { ArrayMinSize } from "class-validator"; + +export class UpdateUserOrgsDto { + @ApiProperty({ required: true }) + @ArrayMinSize(1) + requestedOrganizationIds: number[]; +} diff --git a/src/entities/enum/error-codes.enum.ts b/src/entities/enum/error-codes.enum.ts index 8598d96d..a9d74281 100644 --- a/src/entities/enum/error-codes.enum.ts +++ b/src/entities/enum/error-codes.enum.ts @@ -1,4 +1,5 @@ export enum ErrorCodes { + EmailAlreadyExists = "MESSAGE.USER-ALREADY-HAVE-MAIL", IdDoesNotExists = "MESSAGE.ID-DOES-NOT-EXIST", IdMissing = "MESSAGE.ID-MISSING-FROM-REQUEST", NameInvalidOrAlreadyInUse = "MESSAGE.NAME-INVALID-OR-ALREADY-IN-USE", @@ -44,4 +45,7 @@ export enum ErrorCodes { DeviceModelDoesNotExist = "MESSAGE.DEVICE-MODEL-DOES-NOT-EXIST", InvalidKeyInKeyValuePair = "MESSAGE.INVALID-KEY-IN-KEY-VALUE-PAIR", InvalidValueInKeyValuePair = "MESSAGE.INVALID-VALUE-IN-KEY-VALUE-PAIR", + SendMailError = "MESSAGE.SEND-MAIL-ERROR", + UserDoesNotExistInArray = "MESSAGE.USER-DOES-NOT-EXIST", + UserAlreadyInPermission = "MESSAGE.USER-ALREADY-IN-PERMISSION", } diff --git a/src/entities/global-admin-permission.entity.ts b/src/entities/global-admin-permission.entity.ts deleted file mode 100644 index 2586d579..00000000 --- a/src/entities/global-admin-permission.entity.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ChildEntity } from "typeorm"; - -import { Permission } from "@entities/permission.entity"; -import { PermissionType } from "@enum/permission-type.enum"; - -@ChildEntity(PermissionType.GlobalAdmin) -export class GlobalAdminPermission extends Permission { - constructor() { - super("GlobalAdmin"); - this.type = PermissionType.GlobalAdmin; - } -} diff --git a/src/entities/lorawan-multicast.entity.ts b/src/entities/lorawan-multicast.entity.ts index bec8fe3f..d781bc9a 100644 --- a/src/entities/lorawan-multicast.entity.ts +++ b/src/entities/lorawan-multicast.entity.ts @@ -1,4 +1,4 @@ -import { Column, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn } from "typeorm"; +import { Column, Entity, OneToOne } from "typeorm"; import { multicastGroup } from "@enum/multicast-type.enum"; import { Multicast } from "./multicast.entity"; diff --git a/src/entities/organization-admin-permission.entity.ts b/src/entities/organization-admin-permission.entity.ts deleted file mode 100644 index 567e5a71..00000000 --- a/src/entities/organization-admin-permission.entity.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ChildEntity } from "typeorm"; - -import { Organization } from "@entities/organization.entity"; -import { OrganizationPermission } from "@entities/organization-permission.entity"; -import { PermissionType } from "@enum/permission-type.enum"; - -import { OrganizationAdmin } from "../auth/roles.decorator"; - -@ChildEntity(PermissionType.OrganizationAdmin) -export class OrganizationAdminPermission extends OrganizationPermission { - constructor(name: string, org: Organization) { - super(name, org); - this.type = PermissionType.OrganizationAdmin; - } -} diff --git a/src/entities/organization-application-permission.entity.ts b/src/entities/organization-application-permission.entity.ts deleted file mode 100644 index 2f9c28ce..00000000 --- a/src/entities/organization-application-permission.entity.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ChildEntity, Column, ManyToMany } from "typeorm"; - -import { Application } from "@entities/application.entity"; -import { Organization } from "@entities/organization.entity"; -import { OrganizationPermission } from "@entities/organization-permission.entity"; -import { PermissionType } from "@enum/permission-type.enum"; - -@ChildEntity(PermissionType.OrganizationApplicationPermissions) -export abstract class OrganizationApplicationPermission extends OrganizationPermission { - constructor(name: string, org: Organization, addNewApps?: boolean) { - super(name, org); - this.automaticallyAddNewApplications = - addNewApps != undefined ? addNewApps : false; - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - @ManyToMany(() => Application, application => application.permissions) - applications: Application[]; - - @Column({ nullable: true, default: false, type: Boolean }) - automaticallyAddNewApplications = false; -} diff --git a/src/entities/organization-permission.entity.ts b/src/entities/organization-permission.entity.ts deleted file mode 100644 index 96e5fe0e..00000000 --- a/src/entities/organization-permission.entity.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ChildEntity, ManyToOne } from "typeorm"; - -import { Organization } from "@entities/organization.entity"; -import { Permission } from "@entities/permission.entity"; -import { PermissionType } from "@enum/permission-type.enum"; - -@ChildEntity(PermissionType.OrganizationPermission) -export abstract class OrganizationPermission extends Permission { - constructor(name: string, org: Organization) { - super(name); - this.organization = org; - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - @ManyToOne(() => Organization, { onDelete: "CASCADE" }) - organization: Organization; -} diff --git a/src/entities/organization.entity.ts b/src/entities/organization.entity.ts index 289d6966..c5f8d4ac 100644 --- a/src/entities/organization.entity.ts +++ b/src/entities/organization.entity.ts @@ -1,13 +1,13 @@ -import { Column, Entity, OneToMany, Unique } from "typeorm"; +import { Column, Entity, ManyToMany, OneToMany, Unique } from "typeorm"; import { Application } from "@entities/application.entity"; import { DbBaseEntity } from "@entities/base.entity"; -import { OrganizationPermission } from "@entities/organization-permission.entity"; import { PayloadDecoder } from "@entities/payload-decoder.entity"; -import { Permission } from "@entities/permission.entity"; +import { OrganizationPermission, Permission } from "@entities/permission.entity"; import { SigFoxGroup } from "./sigfox-group.entity"; import { DeviceModel } from "./device-model.entity"; +import { User } from "./user.entity"; @Entity("organization") @Unique(["name"]) @@ -52,4 +52,9 @@ export class Organization extends DbBaseEntity { nullable: true, }) deviceModels?: DeviceModel[]; + + @ManyToMany(_ => User, user => user.requestedOrganizations, { + nullable: true, + }) + awaitingUsers?: User[]; } diff --git a/src/entities/permission.entity.ts b/src/entities/permission.entity.ts index 308dd727..a03df00e 100644 --- a/src/entities/permission.entity.ts +++ b/src/entities/permission.entity.ts @@ -1,8 +1,13 @@ -import { DbBaseEntity } from "@entities/base.entity"; +//All Permissions is included in one file since circular references and typescript makes the program crash unregularaly. +//It happens because circular references can happen between files and not only types. + import { User } from "@entities/user.entity"; import { PermissionType } from "@enum/permission-type.enum"; -import { Column, Entity, ManyToMany, TableInheritance } from "typeorm"; +import { ChildEntity, Column, Entity, ManyToMany, ManyToOne, TableInheritance } from "typeorm"; +import { DbBaseEntity } from "@entities/base.entity"; +import { Organization } from "./organization.entity"; import { ApiKey } from "./api-key.entity"; +import { Application } from "./application.entity"; @Entity() @TableInheritance({ @@ -29,3 +34,71 @@ export abstract class Permission extends DbBaseEntity { ) users: User[]; } + +@ChildEntity(PermissionType.OrganizationPermission) +export abstract class OrganizationPermission extends Permission { + constructor(name: string, org: Organization) { + super(name); + this.organization = org; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + @ManyToOne(() => Organization, { onDelete: "CASCADE" }) + organization: Organization; +} + +@ChildEntity(PermissionType.GlobalAdmin) +export class GlobalAdminPermission extends Permission { + constructor() { + super("GlobalAdmin"); + this.type = PermissionType.GlobalAdmin; + } +} + +@ChildEntity(PermissionType.ApiKeyPermission) +export abstract class ApiKeyPermission extends Permission { + @ManyToMany(_ => ApiKey, key => key.permissions, { onDelete: "CASCADE" }) + apiKeys: ApiKey[]; + +} + +@ChildEntity(PermissionType.OrganizationApplicationPermissions) +export abstract class OrganizationApplicationPermission extends OrganizationPermission { + constructor(name: string, org: Organization, addNewApps?: boolean) { + super(name, org); + this.automaticallyAddNewApplications = + addNewApps != undefined ? addNewApps : false; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + @ManyToMany(() => Application, application => application.permissions) + applications: Application[]; + + @Column({ nullable: true, default: false, type: Boolean }) + automaticallyAddNewApplications = false; +} + +@ChildEntity(PermissionType.OrganizationAdmin) +export class OrganizationAdminPermission extends OrganizationPermission { + constructor(name: string, org: Organization) { + super(name, org); + this.type = PermissionType.OrganizationAdmin; + } +} + +@ChildEntity(PermissionType.Write) +export class WritePermission extends OrganizationApplicationPermission { + constructor(name: string, org: Organization, addNewApps = false) { + super(name, org, addNewApps); + this.type = PermissionType.Write; + } +} + +@ChildEntity(PermissionType.Read) +export class ReadPermission extends OrganizationApplicationPermission { + constructor(name: string, org: Organization, addNewApps = false) { + super(name, org, addNewApps); + this.type = PermissionType.Read; + } +} + diff --git a/src/entities/read-permission.entity.ts b/src/entities/read-permission.entity.ts deleted file mode 100644 index 4de6b0fa..00000000 --- a/src/entities/read-permission.entity.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ChildEntity } from "typeorm"; - -import { OrganizationApplicationPermission } from "@entities/organization-application-permission.entity"; -import { Organization } from "@entities/organization.entity"; -import { PermissionType } from "@enum/permission-type.enum"; - -@ChildEntity(PermissionType.Read) -export class ReadPermission extends OrganizationApplicationPermission { - constructor(name: string, org: Organization, addNewApps = false) { - super(name, org, addNewApps); - this.type = PermissionType.Read; - } -} diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts index 79440915..9d673c07 100644 --- a/src/entities/user.entity.ts +++ b/src/entities/user.entity.ts @@ -1,7 +1,15 @@ import { ApiKey } from "@entities/api-key.entity"; -import { DbBaseEntity } from "@entities/base.entity"; +import { + Column, + Entity, + JoinTable, + ManyToMany, + Unique, + OneToOne, +} from "typeorm"; +import { Organization } from "./organization.entity"; import { Permission } from "@entities/permission.entity"; -import { Column, Entity, JoinTable, ManyToMany, OneToOne, Unique } from "typeorm"; +import { DbBaseEntity } from "@entities/base.entity"; @Entity("user") @Unique(["email"]) @@ -24,11 +32,20 @@ export class User extends DbBaseEntity { @Column({ nullable: true }) nameId: string; + @Column({ nullable: true }) + awaitingConfirmation: boolean; + // eslint-disable-next-line @typescript-eslint/no-unused-vars @ManyToMany(type => Permission, permission => permission.users) @JoinTable() permissions: Permission[]; + @ManyToMany(_ => Organization, requestedOrganizations => requestedOrganizations.awaitingUsers, { + nullable: true, + }) + @JoinTable() + requestedOrganizations: Organization[]; + @OneToOne(type => ApiKey, a => a.systemUser, { nullable: true, cascade: false, diff --git a/src/entities/write-permission.entity.ts b/src/entities/write-permission.entity.ts deleted file mode 100644 index bdc68a28..00000000 --- a/src/entities/write-permission.entity.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ChildEntity } from "typeorm"; - -import { OrganizationApplicationPermission } from "@entities/organization-application-permission.entity"; -import { Organization } from "@entities/organization.entity"; -import { PermissionType } from "@enum/permission-type.enum"; - -@ChildEntity(PermissionType.Write) -export class WritePermission extends OrganizationApplicationPermission { - constructor(name: string, org: Organization, addNewApps = false) { - super(name, org, addNewApps); - this.type = PermissionType.Write; - } -} diff --git a/src/migration/1652771064000-KombitUserManagement.ts b/src/migration/1652771064000-KombitUserManagement.ts new file mode 100644 index 00000000..f4f7aa03 --- /dev/null +++ b/src/migration/1652771064000-KombitUserManagement.ts @@ -0,0 +1,24 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class KombitUserManagement1652771064000 implements MigrationInterface { + name = 'KombitUserManagement1652771064000' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "user_requested_organizations_organization" ("userId" integer NOT NULL, "organizationId" integer NOT NULL, CONSTRAINT "PK_b228a18276f4dc0153b74e04370" PRIMARY KEY ("userId", "organizationId"))`); + await queryRunner.query(`CREATE INDEX "IDX_87ad1ad67570c6ca20f62c9531" ON "user_requested_organizations_organization" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_6a0a3602e88b71bc867af425d8" ON "user_requested_organizations_organization" ("organizationId") `); + await queryRunner.query(`ALTER TABLE "user" ADD "awaitingConfirmation" boolean`); + await queryRunner.query(`ALTER TABLE "user_requested_organizations_organization" ADD CONSTRAINT "FK_87ad1ad67570c6ca20f62c95313" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + await queryRunner.query(`ALTER TABLE "user_requested_organizations_organization" ADD CONSTRAINT "FK_6a0a3602e88b71bc867af425d89" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user_requested_organizations_organization" DROP CONSTRAINT "FK_6a0a3602e88b71bc867af425d89"`); + await queryRunner.query(`ALTER TABLE "user_requested_organizations_organization" DROP CONSTRAINT "FK_87ad1ad67570c6ca20f62c95313"`); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "awaitingConfirmation"`); + await queryRunner.query(`DROP INDEX "public"."IDX_6a0a3602e88b71bc867af425d8"`); + await queryRunner.query(`DROP INDEX "public"."IDX_87ad1ad67570c6ca20f62c9531"`); + await queryRunner.query(`DROP TABLE "user_requested_organizations_organization"`); + } + +} diff --git a/src/modules/app.module.ts b/src/modules/app.module.ts index 70abb3dc..0b3efb28 100644 --- a/src/modules/app.module.ts +++ b/src/modules/app.module.ts @@ -32,6 +32,7 @@ import { MulticastModule } from "./device-management/multicast.module"; import { OpenDataDkSharingModule } from "./open-data-dk-sharing.module"; import { SearchModule } from "./search.module"; import { TestPayloadDecoderModule } from "./test-payload-decoder.module"; +import { NewKombitCreationModule } from "./user-management/new-kombit-creation.module"; @Module({ imports: [ @@ -92,6 +93,7 @@ import { TestPayloadDecoderModule } from "./test-payload-decoder.module"; MulticastModule, IoTLoRaWANDeviceModule, ApiKeyInfoModule, + NewKombitCreationModule, ], controllers: [], providers: [], diff --git a/src/modules/shared.module.ts b/src/modules/shared.module.ts index a57d6ee3..f3cf033d 100644 --- a/src/modules/shared.module.ts +++ b/src/modules/shared.module.ts @@ -4,30 +4,30 @@ import { TypeOrmModule } from "@nestjs/typeorm"; import { Application } from "@entities/application.entity"; import { DataTarget } from "@entities/data-target.entity"; import { GenericHTTPDevice } from "@entities/generic-http-device.entity"; -import { GlobalAdminPermission } from "@entities/global-admin-permission.entity"; +import { GlobalAdminPermission } from "@entities/permission.entity"; import { HttpPushDataTarget } from "@entities/http-push-data-target.entity"; import { FiwareDataTarget } from "@entities/fiware-data-target.entity"; import { IoTDevicePayloadDecoderDataTargetConnection } from "@entities/iot-device-payload-decoder-data-target-connection.entity"; import { IoTDevice } from "@entities/iot-device.entity"; import { LoRaWANDevice } from "@entities/lorawan-device.entity"; -import { OrganizationAdminPermission } from "@entities/organization-admin-permission.entity"; -import { OrganizationApplicationPermission } from "@entities/organization-application-permission.entity"; +import { OrganizationAdminPermission } from "@entities/permission.entity"; +import { OrganizationApplicationPermission } from "@entities/permission.entity"; import { Organization } from "@entities/organization.entity"; -import { OrganizationPermission } from "@entities/organization-permission.entity"; +import { OrganizationPermission } from "@entities/permission.entity"; import { PayloadDecoder } from "@entities/payload-decoder.entity"; import { Permission } from "@entities/permission.entity"; -import { ReadPermission } from "@entities/read-permission.entity"; +import { ReadPermission } from "@entities/permission.entity"; import { ReceivedMessage } from "@entities/received-message.entity"; import { ReceivedMessageMetadata } from "@entities/received-message-metadata.entity"; import { SigFoxDevice } from "@entities/sigfox-device.entity"; import { SigFoxGroup } from "@entities/sigfox-group.entity"; import { User } from "@entities/user.entity"; -import { WritePermission } from "@entities/write-permission.entity"; +import { WritePermission } from "@entities/permission.entity"; import { DeviceModel } from "@entities/device-model.entity"; import { OpenDataDkDataset } from "@entities/open-data-dk-dataset.entity"; import { AuditLog } from "@services/audit-log.service"; import { ApiKey } from "@entities/api-key.entity"; -import { ApiKeyPermission } from "@entities/api-key-permission.entity"; +import { ApiKeyPermission } from "@entities/permission.entity"; import { Multicast } from "@entities/multicast.entity"; import { LorawanMulticastDefinition } from "@entities/lorawan-multicast.entity"; import { ControlledProperty } from "@entities/controlled-property.entity"; @@ -38,7 +38,7 @@ import { MqttDataTarget } from "@entities/mqtt-data-target.entity"; @Module({ imports: [ TypeOrmModule.forFeature([ - ApiKey, + User, Application, DataTarget, GenericHTTPDevice, @@ -62,13 +62,13 @@ import { MqttDataTarget } from "@entities/mqtt-data-target.entity"; ReceivedMessageMetadata, SigFoxDevice, SigFoxGroup, - User, WritePermission, ApiKeyPermission, Multicast, LorawanMulticastDefinition, ControlledProperty, ApplicationDeviceType, + ApiKey, ReceivedMessageSigFoxSignals, ]), ], diff --git a/src/modules/user-management/new-kombit-creation.module.ts b/src/modules/user-management/new-kombit-creation.module.ts new file mode 100644 index 00000000..0c90543d --- /dev/null +++ b/src/modules/user-management/new-kombit-creation.module.ts @@ -0,0 +1,13 @@ +import { Module } from "@nestjs/common"; + +import { SharedModule } from "@modules/shared.module"; +import { NewKombitCreationController } from "@user-management-controller/new-kombit-creation.controller"; +import { OrganizationModule } from "./organization.module"; +import { UserModule } from "./user.module"; +import { PermissionModule } from "./permission.module"; + +@Module({ + imports: [SharedModule, OrganizationModule, UserModule, PermissionModule], + controllers: [NewKombitCreationController], +}) +export class NewKombitCreationModule {} diff --git a/src/modules/user-management/organization.module.ts b/src/modules/user-management/organization.module.ts index 82947830..591d331e 100644 --- a/src/modules/user-management/organization.module.ts +++ b/src/modules/user-management/organization.module.ts @@ -3,9 +3,10 @@ import { PermissionModule } from "@modules/user-management/permission.module"; import { forwardRef, Module } from "@nestjs/common"; import { OrganizationService } from "@services/user-management/organization.service"; import { OrganizationController } from "@user-management-controller/organization.controller"; +import { UserModule } from "./user.module"; @Module({ - imports: [SharedModule, forwardRef(() => PermissionModule)], + imports: [SharedModule, forwardRef(() => PermissionModule), forwardRef(() => UserModule)], providers: [OrganizationService], exports: [OrganizationService], controllers: [OrganizationController], diff --git a/src/modules/user-management/user.module.ts b/src/modules/user-management/user.module.ts index 0b9cfbc9..1cf2d0d8 100644 --- a/src/modules/user-management/user.module.ts +++ b/src/modules/user-management/user.module.ts @@ -5,9 +5,12 @@ import { PermissionModule } from "@modules/user-management/permission.module"; import { UserBootstrapperService } from "@services/user-management/user-bootstrapper.service"; import { UserService } from "@services/user-management/user.service"; import { UserController } from "@user-management-controller/user.controller"; +import { OrganizationModule } from "./organization.module"; +import { ConfigModule } from "@nestjs/config"; +import configuration from "@config/configuration"; @Module({ - imports: [SharedModule, forwardRef(() => PermissionModule)], + imports: [SharedModule, ConfigModule.forRoot({ load: [configuration] }), forwardRef(() => PermissionModule), forwardRef(() => OrganizationModule)], controllers: [UserController], providers: [UserService, UserBootstrapperService], exports: [UserService], diff --git a/src/services/api-key-management/api-key.service.ts b/src/services/api-key-management/api-key.service.ts index 37208734..1fd55e8f 100644 --- a/src/services/api-key-management/api-key.service.ts +++ b/src/services/api-key-management/api-key.service.ts @@ -4,7 +4,7 @@ import { CreateApiKeyDto } from "@dto/api-key/create-api-key.dto"; import { ListAllApiKeysResponseDto } from "@dto/api-key/list-all-api-keys-response.dto"; import { ListAllApiKeysDto } from "@dto/api-key/list-all-api-keys.dto"; import { DeleteResponseDto } from "@dto/delete-application-response.dto"; -import { ApiKeyPermission } from "@entities/api-key-permission.entity"; +import { ApiKeyPermission } from "@entities/permission.entity"; import { ApiKey } from "@entities/api-key.entity"; import { forwardRef, Inject, Injectable, Logger } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; diff --git a/src/services/user-management/organization.service.ts b/src/services/user-management/organization.service.ts index b219dfa5..2e5b127b 100644 --- a/src/services/user-management/organization.service.ts +++ b/src/services/user-management/organization.service.ts @@ -1,3 +1,14 @@ +import { + BadRequestException, + Inject, + Injectable, + Logger, + forwardRef, + NotFoundException, +} from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { In, Repository } from "typeorm"; + import { DeleteResponseDto } from "@dto/delete-application-response.dto"; import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; import { @@ -8,17 +19,11 @@ import { CreateOrganizationDto } from "@dto/user-management/create-organization. import { UpdateOrganizationDto } from "@dto/user-management/update-organization.dto"; import { Organization } from "@entities/organization.entity"; import { ErrorCodes } from "@enum/error-codes.enum"; -import { - BadRequestException, - forwardRef, - Inject, - Injectable, - Logger, - NotFoundException, -} from "@nestjs/common"; -import { InjectRepository } from "@nestjs/typeorm"; -import { In, Repository } from "typeorm"; + import { PermissionService } from "./permission.service"; +import { User } from "@entities/user.entity"; +import { UserService } from "./user.service"; +import { Permission, OrganizationPermission } from "@entities/permission.entity"; @Injectable() export class OrganizationService { @@ -26,7 +31,9 @@ export class OrganizationService { @InjectRepository(Organization) private organizationRepository: Repository, @Inject(forwardRef(() => PermissionService)) - private permissionService: PermissionService + private permissionService: PermissionService, + @Inject(forwardRef(() => UserService)) + private userService: UserService ) {} private readonly logger = new Logger(OrganizationService.name, true); @@ -60,6 +67,28 @@ export class OrganizationService { return await this.organizationRepository.save(org); } + async updateAwaitingUsers(org: Organization, user: User): Promise { + if (!org.awaitingUsers.find(dbUser => dbUser.id === user.id)) { + org.awaitingUsers.push(user); + } + return await this.organizationRepository.save(org); + } + + async rejectAwaitingUser( + user: User, + organization: Organization + ): Promise { + if (organization.awaitingUsers.find(dbUser => dbUser.id === user.id)) { + const index = organization.awaitingUsers.findIndex( + dbUser => dbUser.id === user.id + ); + organization.awaitingUsers.splice(index, 1); + await this.userService.sendRejectionMail(user, organization); + return await this.organizationRepository.save(organization); + } + throw new NotFoundException(ErrorCodes.UserDoesNotExistInArray); + } + async findAll(): Promise { const [data, count] = await this.organizationRepository.findAndCount({ relations: ["applications", "permissions"], @@ -71,6 +100,59 @@ export class OrganizationService { }; } + async mapPermissionsToOrganizations( + permissions: OrganizationPermission[] + ): Promise { + const requestedOrganizations: Organization[] = []; + + for (let index = 0; index < permissions.length; index++) { + if ( + requestedOrganizations.find(org => { + return permissions[index].organization.id === org.id; + }) + ) { + } else { + requestedOrganizations.push(permissions[index].organization); + } + } + + requestedOrganizations.forEach(org => { + org.permissions = []; + permissions.forEach(permission => { + if (org.id === permission.organization.id) { + org.permissions.push(permission); + } + }); + }); + permissions.forEach(permission => { + permission.organization = null; + }); + + return requestedOrganizations; + } + + async mapPermissionsToOneOrganization( + permissions: OrganizationPermission[] + ): Promise { + const org: Organization = new Organization(); + + permissions.map(permission => { + org.id = permission.organization.id; + org.name = permission.organization.name + }); + + org.permissions = []; + permissions.forEach(permission => { + if (org.id === permission.organization.id) { + org.permissions.push(permission); + } + }); + permissions.forEach(permission => { + permission.organization = null; + }); + return org; + } + async findAllPaginated( query?: ListAllEntitiesDto ): Promise { @@ -149,6 +231,11 @@ export class OrganizationService { }, }); } + async findByIdWithUsers(organizationId: number): Promise { + return await this.organizationRepository.findOneOrFail(organizationId, { + relations: ["awaitingUsers"], + }); + } async findByIdWithPermissions(organizationId: number): Promise { return await this.organizationRepository.findOneOrFail(organizationId, { diff --git a/src/services/user-management/permission.service.ts b/src/services/user-management/permission.service.ts index cb54e595..401c3072 100644 --- a/src/services/user-management/permission.service.ts +++ b/src/services/user-management/permission.service.ts @@ -15,15 +15,15 @@ import { PermissionMinimalDto } from "@dto/permission-minimal.dto"; import { UserPermissions } from "@dto/permission-organization-application.dto"; import { CreatePermissionDto } from "@dto/user-management/create-permission.dto"; import { UpdatePermissionDto } from "@dto/user-management/update-permission.dto"; -import { GlobalAdminPermission } from "@entities/global-admin-permission.entity"; -import { OrganizationAdminPermission } from "@entities/organization-admin-permission.entity"; -import { OrganizationApplicationPermission } from "@entities/organization-application-permission.entity"; +import { GlobalAdminPermission } from "@entities/permission.entity"; +import { OrganizationAdminPermission } from "@entities/permission.entity"; +import { OrganizationApplicationPermission } from "@entities/permission.entity"; import { Organization } from "@entities/organization.entity"; -import { OrganizationPermission } from "@entities/organization-permission.entity"; +import { OrganizationPermission } from "@entities/permission.entity"; import { Permission } from "@entities/permission.entity"; -import { ReadPermission } from "@entities/read-permission.entity"; +import { ReadPermission } from "@entities/permission.entity"; import { User } from "@entities/user.entity"; -import { WritePermission } from "@entities/write-permission.entity"; +import { WritePermission } from "@entities/permission.entity"; import { PermissionType } from "@enum/permission-type.enum"; import { ApplicationService } from "@services/device-management/application.service"; import { OrganizationService } from "@services/user-management/organization.service"; @@ -163,11 +163,30 @@ export class PermissionService { x.permissions = _.union(x.permissions, [permission]); }); } - async removeUserFromPermission(permission: Permission, user: User): Promise { user.permissions = user.permissions.filter(x => x.id != permission.id); } + async findManyWithRelations(organizationIds: number[]): Promise + { + const perm = await this.permissionRepository.find({ + relations: ["organization", "users"], + where: {organization: {id: In(organizationIds)}} + }); + + return perm as OrganizationPermission[]; + } + + async findOneWithRelations(organizationId: number): Promise + { + const perm = await this.permissionRepository.find({ + relations: ["organization", "users"], + where: {organization: {id: organizationId}} + }); + + return perm as OrganizationPermission[]; + } + async updatePermission( id: number, dto: UpdatePermissionDto, @@ -281,7 +300,14 @@ export class PermissionService { }); } - buildPermissionsQuery(): SelectQueryBuilder { + async getGlobalPermission(): Promise { + return await getManager().findOneOrFail(Permission, { + where: { type: PermissionType.GlobalAdmin }, + relations: ["users"], + }); + } + + buildPermissionsQuery(): SelectQueryBuilder { return this.permissionRepository .createQueryBuilder("permission") .leftJoinAndSelect( @@ -315,7 +341,7 @@ export class PermissionService { .getRawMany(); } - async findPermissionsForOrgAdminWithApplications( + async findPermissionsForOrgAdminWithApplications( userId: number ): Promise { return await this.buildPermissionsWithApplicationsQuery() @@ -327,7 +353,7 @@ export class PermissionService { .getRawMany(); } - buildPermissionsWithApplicationsQuery(): SelectQueryBuilder { + buildPermissionsWithApplicationsQuery(): SelectQueryBuilder { return this.permissionRepository .createQueryBuilder("permission") .leftJoinAndSelect("permission.organization", "organization") @@ -398,7 +424,7 @@ export class PermissionService { }); return res; - } + } async findManyByIds(ids: number[]): Promise { return await this.permissionRepository.findByIds(ids); diff --git a/src/services/user-management/user.service.ts b/src/services/user-management/user.service.ts index 8c187201..3503199c 100644 --- a/src/services/user-management/user.service.ts +++ b/src/services/user-management/user.service.ts @@ -21,6 +21,12 @@ import { ListAllUsersResponseDto } from "@dto/list-all-users-response.dto"; import { Profile } from "passport-saml"; import { ListAllUsersMinimalResponseDto } from "@dto/list-all-users-minimal-response.dto"; import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; +import { CreateNewKombitUserDto } from "@dto/user-management/create-new-kombit-user.dto"; +import * as nodemailer from "nodemailer"; +import { Organization } from "@entities/organization.entity"; +import SMTPTransport from "nodemailer/lib/smtp-transport"; +import { PermissionType } from "@enum/permission-type.enum"; +import { ConfigService } from "@nestjs/config"; @Injectable() export class UserService { @@ -28,7 +34,8 @@ export class UserService { @InjectRepository(User) private userRepository: Repository, @Inject(forwardRef(() => PermissionService)) - private permissionService: PermissionService + private permissionService: PermissionService, + private configService: ConfigService ) {} private readonly logger = new Logger(UserService.name, true); @@ -41,6 +48,26 @@ export class UserService { ); } + async acceptUser( + user: User, + org: Organization, + dbPermission: Permission + ): Promise { + user.awaitingConfirmation = false; + + if (user.permissions.find(perms => perms.id === dbPermission.id)) { + throw new BadRequestException(ErrorCodes.UserAlreadyInPermission); + } else { + const index = user.requestedOrganizations.findIndex( + dbOrg => dbOrg.id === org.id + ); + user.requestedOrganizations.splice(index, 1); + user.permissions.push(dbPermission); + await this.sendVerificationMail(user, org); + return await this.userRepository.save(user); + } + } + async findOneUserByEmailWithPassword(email: string): Promise { return await this.userRepository.findOne( { email: email }, @@ -62,7 +89,7 @@ export class UserService { getPermissionOrganisationInfo = false, getPermissionUsersInfo = false ): Promise { - const relations = ["permissions"]; + const relations = ["permissions", "requestedOrganizations"]; if (getPermissionOrganisationInfo) { relations.push("permissions.organization"); } @@ -236,6 +263,19 @@ export class UserService { } } + async newKombitUser( + dto: CreateNewKombitUserDto, + requestedOrganizations: Organization[], + user: User + ): Promise { + user.email = dto.email; + user.awaitingConfirmation = true; + for (let index = 0; index < requestedOrganizations.length; index++) { + await this.sendOrganizationRequestMail(user, requestedOrganizations[index]); + } + return await this.userRepository.save(user); + } + async findManyUsersByIds(userIds: number[]): Promise { return await this.userRepository.findByIds(userIds); } @@ -297,6 +337,128 @@ export class UserService { }; } + basicMailTransporter(): nodemailer.Transporter { + return nodemailer.createTransport({ + host: this.configService.get("email.host"), + port: this.configService.get("email.port"), + auth: { + user: this.configService.get("email.user"), + pass: this.configService.get("email.pass") + }, + }); + } + + async sendOrganizationRequestMail( + user: User, + organization: Organization + ): Promise { + const emails = await this.getOrgAdminEmails(organization); + const transporter: nodemailer.Transporter = this.basicMailTransporter(); + try { + await transporter.verify(); + } catch (error) { + throw new BadRequestException(ErrorCodes.SendMailError); + } + try { + await transporter.sendMail({ + from: this.configService.get("email.from"), // sender address + to: emails, // list of receivers + subject: "Ny ansøgning til din organisation!", // Subject line + html: `

Ny ansøgning om tilladelse til organisationen "${organization.name}"!

Klik her for at bekræfte eller afvise brugeren med navnet: "${user.name}."`, // html body + }); + } catch (error) { + throw new BadRequestException(ErrorCodes.SendMailError); + } + } + + async sendRejectionMail(user: User, organization: Organization): Promise { + const transporter = this.basicMailTransporter(); + + try { + await transporter.verify(); + } catch (error) { + throw new BadRequestException(ErrorCodes.SendMailError); + } + try { + await transporter.sendMail({ + from: this.configService.get("email.from"), // sender address + to: user.email, // list of receivers + subject: "Ansøgning afvist!", // Subject line + html: `

Din ansøgning om bekræftelse hos "${organization.name}" er afvist!

`, // html body + }); + } catch (error) { + throw new BadRequestException(ErrorCodes.SendMailError); + } + } + + async sendVerificationMail(user: User, organization: Organization): Promise { + const transporter = this.basicMailTransporter(); + + try { + await transporter.verify(); + } catch (error) { + throw new BadRequestException(ErrorCodes.SendMailError); + } + try { + await transporter.sendMail({ + from: this.configService.get("email.from"), // sender address + to: user.email, // list of receivers + subject: "Ansøgning bekræftet!", // Subject line + html: `

Din ansøgning om bekræftelse hos "${organization.name}" er godkendt!

`, // html body + }); + } catch (error) { + throw new BadRequestException(ErrorCodes.SendMailError); + } + } + + async getOrgAdminEmails(organization: Organization): Promise { + const emails: string[] = []; + const globalAdminPermission: Permission = await this.permissionService.getGlobalPermission(); + organization.permissions.forEach(permission => { + if (permission.type === PermissionType.OrganizationAdmin) { + if (permission.users.length > 0) { + permission.users.forEach(user => { + emails.push(user.email); + }); + } else { + globalAdminPermission.users.forEach(user => { + emails.push(user.email); + }); + } + } + }); + return emails; + } + + async getAwaitingUsers( + organizationId: number, + query?: ListAllEntitiesDto + ): Promise { + let orderBy = `user.id`; + if ( + query.orderOn !== null && + (query.orderOn === "id" || query.orderOn === "name") + ) { + orderBy = `user.${query.orderOn}`; + } + const order: "DESC" | "ASC" = + query?.sort?.toLocaleUpperCase() == "DESC" ? "DESC" : "ASC"; + + const [data, count] = await this.userRepository + .createQueryBuilder("user") + .innerJoin("user.requestedOrganizations", "org") + .where("org.id = :id", { id: organizationId }) + .take(+query.limit) + .skip(+query.offset) + .orderBy(orderBy, order) + .getManyAndCount(); + + return { + data: data.map(x => x as UserResponseDto), + count: count, + }; + } + async hideWelcome(id: number): Promise { const res = await this.userRepository.update(id, { showWelcomeScreen: false }); return !!res.affected From 7b549fb03b2d5395400f810421db614574ffd73c Mon Sep 17 00:00:00 2001 From: nlg Date: Tue, 17 May 2022 15:59:13 +0200 Subject: [PATCH 06/19] Bumped momemnt version one minor version --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index f4fdade1..4616e9c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7651,7 +7651,7 @@ } }, "moment": { - "version": "2.29.1", + "version": "2.29.2", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" }, From 0c9375315cb56cdf85106127c81607e083330194 Mon Sep 17 00:00:00 2001 From: nlg Date: Tue, 17 May 2022 16:05:41 +0200 Subject: [PATCH 07/19] Fixed package.lock --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4616e9c3..525aac65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7652,8 +7652,8 @@ }, "moment": { "version": "2.29.2", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", - "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz", + "integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==" }, "moment-timezone": { "version": "0.5.31", From 8bf6b21d713414eafbf8b7886099bbfa6b25fe5b Mon Sep 17 00:00:00 2001 From: nlg Date: Tue, 17 May 2022 16:25:15 +0200 Subject: [PATCH 08/19] Fixed casing of Kombit migration --- src/migration/1652771064000-KombitUserManagement.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/migration/1652771064000-KombitUserManagement.ts b/src/migration/1652771064000-KombitUserManagement.ts index f4f7aa03..1cc587e3 100644 --- a/src/migration/1652771064000-KombitUserManagement.ts +++ b/src/migration/1652771064000-KombitUserManagement.ts @@ -1,7 +1,7 @@ import {MigrationInterface, QueryRunner} from "typeorm"; -export class KombitUserManagement1652771064000 implements MigrationInterface { - name = 'KombitUserManagement1652771064000' +export class kombitUserManagement1652771064000 implements MigrationInterface { + name = 'kombitUserManagement1652771064000' public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(`CREATE TABLE "user_requested_organizations_organization" ("userId" integer NOT NULL, "organizationId" integer NOT NULL, CONSTRAINT "PK_b228a18276f4dc0153b74e04370" PRIMARY KEY ("userId", "organizationId"))`); From 74d45634eae4382acbdc9f8fc7664b3e566a1501 Mon Sep 17 00:00:00 2001 From: Aram Al-Sabti Date: Tue, 17 May 2022 17:57:37 +0200 Subject: [PATCH 09/19] Finish permission levels migration --- .../permissions/permission-type.entity.ts | 5 +- .../1651142158492-revised-permissions.ts | 90 +++++++++++++------ .../user-management/permission.service.ts | 60 ++++++++----- src/services/user-management/user.service.ts | 2 +- 4 files changed, 107 insertions(+), 50 deletions(-) diff --git a/src/entities/permissions/permission-type.entity.ts b/src/entities/permissions/permission-type.entity.ts index 26a4f037..14004458 100644 --- a/src/entities/permissions/permission-type.entity.ts +++ b/src/entities/permissions/permission-type.entity.ts @@ -1,10 +1,11 @@ import { DbBaseEntity } from "@entities/base.entity"; import { PermissionType } from "@enum/permission-type.enum"; -import { Column, Entity, ManyToOne } from "typeorm"; +import { Column, Entity, ManyToOne, Unique } from "typeorm"; import { Permission } from "./permission.entity"; +import { nameof } from "@helpers/type-helper"; @Entity("permission_type") -// TODO: Temp name to avoid clashing with enum value +@Unique([nameof("type"), nameof("permission")]) export class PermissionTypeEntity extends DbBaseEntity { @Column() type: PermissionType; diff --git a/src/migration/1651142158492-revised-permissions.ts b/src/migration/1651142158492-revised-permissions.ts index a84edbfb..2a8ef65a 100644 --- a/src/migration/1651142158492-revised-permissions.ts +++ b/src/migration/1651142158492-revised-permissions.ts @@ -33,6 +33,12 @@ type UserPermissions = { permissionId: number; }[]; +/** + * Create a temporary enum which is a union of both old and new enum values + */ +const permissionTypeUnionName = "permission_type_enum_temp"; +const createPermissionTypeUnionSql = `CREATE TYPE "${permissionTypeUnionName}" AS ENUM('OrganizationAdmin', 'Write', 'GlobalAdmin', 'OrganizationUserAdmin', 'OrganizationGatewayAdmin', 'OrganizationApplicationAdmin', 'Read', 'OrganizationPermission', 'OrganizationApplicationPermissions', 'ApiKeyPermission')`; + export class revisedPermissions1651142158492 implements MigrationInterface { name = "revisedPermissions1651142158492"; @@ -47,22 +53,22 @@ export class revisedPermissions1651142158492 implements MigrationInterface { // Migrates existing data. This can result in duplicate permissions and duplicates of its dependents // Must be resolved by a user administrator or above or directly on the database await this.migrateUp(queryRunner); - - // await queryRunner.query( - // `ALTER TABLE "permission" ALTER COLUMN "type" TYPE "permission_type_enum" USING "type"::"text"::"permission_type_enum"` - // ); await queryRunner.query(`DROP TYPE "permission_type_enum_old"`); - await queryRunner.query(`COMMENT ON COLUMN "permission"."type" IS NULL`); // Update permission so it can refer to multiple types await this.migratePermissionTypeUp(queryRunner); } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`COMMENT ON COLUMN "permission"."type" IS NULL`); await queryRunner.query( `CREATE TYPE "permission_type_enum_old" AS ENUM('GlobalAdmin', 'OrganizationAdmin', 'Write', 'Read', 'OrganizationPermission', 'OrganizationApplicationPermissions', 'ApiKeyPermission')` ); + // Create a temporary enum which is a union of both old and new enum values + await queryRunner.query(createPermissionTypeUnionSql); + + // Revert permission so each one only has exactly one type (level) + await this.migratePermissionTypeDown(queryRunner); + await queryRunner.query(`COMMENT ON COLUMN "permission"."type" IS NULL`); // Migrates existing data. This can result in duplicate permissions and duplicates of its dependents // ASSUMPTION: this migration is only reverted immediately after executing it. @@ -73,21 +79,17 @@ export class revisedPermissions1651142158492 implements MigrationInterface { await queryRunner.query( `ALTER TABLE "permission" ALTER COLUMN "type" TYPE "permission_type_enum_old" USING "type"::"text"::"permission_type_enum_old"` ); - await queryRunner.query(`DROP TYPE "permission_type_enum"`); + await queryRunner.query(`DROP TYPE IF EXISTS "permission_type_enum"`); await queryRunner.query( `ALTER TYPE "permission_type_enum_old" RENAME TO "permission_type_enum"` ); - await this.migratePermissionTypeDown(queryRunner); } private async migrateUp(queryRunner: QueryRunner): Promise { - // Create a temporary enum which is a union of both old and new enum values + await queryRunner.query(createPermissionTypeUnionSql); await queryRunner.query( - `CREATE TYPE "permission_type_enum_temp" AS ENUM('OrganizationAdmin', 'Write', 'GlobalAdmin', 'OrganizationUserAdmin', 'OrganizationGatewayAdmin', 'OrganizationApplicationAdmin', 'Read', 'OrganizationPermission', 'OrganizationApplicationPermissions', 'ApiKeyPermission')` - ); - await queryRunner.query( - `ALTER TABLE "permission" ALTER COLUMN "type" TYPE "permission_type_enum_temp" USING "type"::"text"::"permission_type_enum_temp"` + `ALTER TABLE "permission" ALTER COLUMN "type" TYPE "${permissionTypeUnionName}" USING "type"::"text"::"${permissionTypeUnionName}"` ); // When migrating permisisons tied to old permission types, we need to keep track of the old id. This is for updating any dependents @@ -179,7 +181,7 @@ WHERE "permissionId" IN await queryRunner.query( `ALTER TABLE "permission" ALTER COLUMN "type" TYPE "permission_type_enum" USING "type"::"text"::"permission_type_enum"` ); - await queryRunner.query(`DROP TYPE "permission_type_enum_temp"`); + await queryRunner.query(`DROP TYPE "${permissionTypeUnionName}"`); } private async migrateUserPermissions( @@ -332,6 +334,7 @@ returning id, "permission"."clonedFromId"`; "id" AS "permissionId" from "public"."permission"`; + // For each permission, create a corresponding permission type await queryRunner.query(`INSERT INTO "public"."permission_type"("createdAt","updatedAt",type,"createdById","updatedById","permissionId") ${fetchAllPermissions}`); @@ -340,15 +343,10 @@ returning id, "permission"."clonedFromId"`; await queryRunner.query(`ALTER TABLE "permission_type" ADD CONSTRAINT "FK_abd46fe625f90edc07441bd0bb2" FOREIGN KEY ("createdById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); await queryRunner.query(`ALTER TABLE "permission_type" ADD CONSTRAINT "FK_6ebf76b0f055fe09e42edfe4848" FOREIGN KEY ("updatedById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); await queryRunner.query(`ALTER TABLE "permission_type" ADD CONSTRAINT "FK_b8613564bc719a6e37ff0ba243b" FOREIGN KEY ("permissionId") REFERENCES "permission"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "permission_type" ADD CONSTRAINT "UQ_3acd70f5a3895ee2fb92b2a4290" UNIQUE ("type", "permissionId")`); } private async migrateDown(queryRunner: QueryRunner): Promise { - await this.migratePermissionTypeDown(queryRunner); - - // Create a temporary enum which is a union of both old and new enum values - await queryRunner.query( - `CREATE TYPE "permission_type_enum_temp" AS ENUM('OrganizationAdmin', 'Write', 'GlobalAdmin', 'OrganizationUserAdmin', 'OrganizationGatewayAdmin', 'OrganizationApplicationAdmin', 'Read', 'OrganizationPermission', 'OrganizationApplicationPermissions', 'ApiKeyPermission')` - ); await queryRunner.query( `ALTER TABLE "permission" ALTER COLUMN "type" TYPE "permission_type_enum_temp" USING "type"::"text"::"permission_type_enum_temp"` ); @@ -358,7 +356,7 @@ returning id, "permission"."clonedFromId"`; `ALTER TABLE "permission" ADD COLUMN "clonedFromId" integer` ); - // Begin cloning. Store both the old and new ids as mappings + // Begin cloning. Store both the old and new ids as mappings. The clone must not have access to more than the original. const readFromUserAdminInfo: PermissionInfo[] = await queryRunner.query( this.copyPermissionsQuery("OrganizationUserAdmin", "Read") ); @@ -391,6 +389,14 @@ returning id, "permission"."clonedFromId"`; writeFromApplicationAdminInfo ); + await this.migrateApiKeyPermissions( + queryRunner, + readFromUserAdminInfo, + readFromGatewayAdminInfo, + readFromApplicationAdminInfo, + writeFromApplicationAdminInfo + ); + // Cleanup await queryRunner.query(`ALTER TABLE "permission" DROP COLUMN "clonedFromId"`); await queryRunner.query( @@ -403,13 +409,47 @@ returning id, "permission"."clonedFromId"`; } private async migratePermissionTypeDown(queryRunner: QueryRunner) { - // TODO: Migrate first? - + await queryRunner.query(`ALTER TABLE "permission_type" DROP CONSTRAINT "UQ_3acd70f5a3895ee2fb92b2a4290"`); await queryRunner.query(`ALTER TABLE "permission_type" DROP CONSTRAINT "FK_b8613564bc719a6e37ff0ba243b"`); await queryRunner.query(`ALTER TABLE "permission_type" DROP CONSTRAINT "FK_6ebf76b0f055fe09e42edfe4848"`); await queryRunner.query(`ALTER TABLE "permission_type" DROP CONSTRAINT "FK_abd46fe625f90edc07441bd0bb2"`); - await queryRunner.query(`CREATE TYPE "public"."permission_type_enum" AS ENUM('GlobalAdmin', 'OrganizationAdmin', 'Write', 'Read', 'OrganizationPermission', 'OrganizationApplicationPermissions', 'ApiKeyPermission')`); - await queryRunner.query(`ALTER TABLE "permission" ADD "type" "public"."permission_type_enum" NOT NULL`); + + // Add permission level "type". It's nullable to make the migration possible + await queryRunner.query(`ALTER TABLE "permission" ADD "type" "${permissionTypeUnionName}"`); + + // Temporary table with every permission settable by a client prioritized. Any permission with an unknown type is ignored. + // Unless otherwise specified, the table is automatically dropped after session end. + await queryRunner.query(`CREATE TEMP TABLE "permission_type_priority" ON COMMIT DROP AS + SELECT * FROM ( + VALUES + ('GlobalAdmin'::character varying, 0), + ('OrganizationUserAdmin'::character varying, 10), + ('OrganizationApplicationAdmin'::character varying, 20), + ('OrganizationGatewayAdmin'::character varying, 30), + ('Read'::character varying, 40) + ) as t (type, priority)`); + + // Migrate permission levels + await queryRunner.query(`UPDATE + public.permission pm + SET + -- The column is updated for each row with identical "permissionId" + type = CAST(highestPmType.type as ${permissionTypeUnionName}) + FROM + ( + SELECT pt.* + FROM permission_type_priority ptp + JOIN permission_type pt ON ptp.type = pt.type + -- Order from lowest to highest priority. The last update for any given permission + -- thus be the type with highest priority (lowest value) + ORDER BY ptp.priority DESC + ) highestPmType + WHERE + pm.id = highestPmType."permissionId"`) + + // Cleanup. If setting "type" to non-nullable fails, then there exists permissions without any level. + // This should not happen. Review them manually. + await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "type" SET NOT NULL`); await queryRunner.query(`DROP TABLE "permission_type"`); await queryRunner.query(`CREATE INDEX "IDX_71bf2818fb2ad92e208d7aeadf" ON "permission" ("type") `); } diff --git a/src/services/user-management/permission.service.ts b/src/services/user-management/permission.service.ts index 044fa26d..acb85940 100644 --- a/src/services/user-management/permission.service.ts +++ b/src/services/user-management/permission.service.ts @@ -31,6 +31,7 @@ import { ListAllPermissionsDto } from "@dto/list-all-permissions.dto"; import { isOrganizationApplicationPermission } from "@helpers/security-helper"; import { PermissionTypeEntity } from "@entities/permissions/permission-type.entity"; import { PermissionCreator } from "@helpers/permission.helper"; +import { nameof } from "@helpers/type-helper"; @Injectable() export class PermissionService { @@ -86,6 +87,11 @@ export class PermissionService { org.name + organizationGatewayAdminSuffix, org ); + this.setUserIdOnPermissions(readPermission, userId); + this.setUserIdOnPermissions(orgApplicationAdminPermission, userId); + this.setUserIdOnPermissions(orgAdminPermission, userId); + this.setUserIdOnPermissions(orgGatewayAadminPermission, userId); + readPermission.createdBy = userId; readPermission.updatedBy = userId; orgApplicationAdminPermission.createdBy = userId; @@ -96,16 +102,26 @@ export class PermissionService { orgGatewayAadminPermission.updatedBy = userId; return { readPermission, orgApplicationAdminPermission, orgAdminPermission, orgGatewayAadminPermission }; } + private setUserIdOnPermissions(permission: Permission, userId: number) { + permission.type.forEach(type => { + type.createdBy = userId; + type.updatedBy = userId; + }); + } async findOrCreateGlobalAdminPermission(): Promise { - const globalAdmin = await this.permissionRepository.findOne({ - where: { - // TODO: PERMISSION REIVSED. Will this work? - type: { - type: PermissionType.GlobalAdmin + // Use query builder since the other syntax doesn't support one-to-many for property querying + const globalAdmin = await this.permissionRepository + .createQueryBuilder("permission") + .where( + " type.type = :permType", + { + permType: PermissionType.GlobalAdmin, } - } - }); + ) + .leftJoin("permission.type", "type") + .getOneOrFail(); + if (globalAdmin) { return globalAdmin; } @@ -140,21 +156,21 @@ export class PermissionService { } async autoAddPermissionsToApplication(app: Application): Promise { - const permissionsInOrganisation = await this.permissionRepository.find( - { - where: { - // TODO: PERMISSION REIVSED. Will this work? - type: { - type: PermissionType.OrganizationApplicationAdmin - }, - organization: { - id: app.belongsTo.id, - }, - automaticallyAddNewApplications: true, - }, - relations: ["applications"], - } - ); + // Use query builder since the other syntax doesn't support one-to-many for property querying + const permissionsInOrganisation = await this.permissionRepository + .createQueryBuilder("permission") + .where( + "permission.organization.id = :orgId" + + " AND type.type IN (:...permType)" + + ` AND "${nameof('automaticallyAddNewApplications')}" = True`, + { + orgId: app.belongsTo.id, + permType: [PermissionType.OrganizationApplicationAdmin, PermissionType.Read], + } + ) + .leftJoinAndSelect("permission.applications", "app") + .leftJoin("permission.type", "type") + .getMany(); await Promise.all( permissionsInOrganisation.map(async p => { diff --git a/src/services/user-management/user.service.ts b/src/services/user-management/user.service.ts index 5efc21f8..6afe63a8 100644 --- a/src/services/user-management/user.service.ts +++ b/src/services/user-management/user.service.ts @@ -252,7 +252,7 @@ export class UserService { } const [data, count] = await this.userRepository.findAndCount({ - relations: ["permissions"], + relations: ["permissions", "permissions.type"], take: +query.limit, skip: +query.offset, order: sorting, From 2436f95229998e0558549c95d095ff7a6b6a3f98 Mon Sep 17 00:00:00 2001 From: Aram Al-Sabti Date: Tue, 17 May 2022 17:58:38 +0200 Subject: [PATCH 10/19] Deny duplicate permission types --- .../user-management/create-permission.dto.ts | 4 +- src/entities/enum/error-codes.enum.ts | 1 + src/helpers/array-distinct.validator.ts | 41 +++++++++++++++++++ src/helpers/permission.helper.ts | 5 --- .../user-management/permission.service.ts | 5 +-- 5 files changed, 46 insertions(+), 10 deletions(-) create mode 100644 src/helpers/array-distinct.validator.ts diff --git a/src/entities/dto/user-management/create-permission.dto.ts b/src/entities/dto/user-management/create-permission.dto.ts index feb8186e..d0eaaa1c 100644 --- a/src/entities/dto/user-management/create-permission.dto.ts +++ b/src/entities/dto/user-management/create-permission.dto.ts @@ -3,6 +3,8 @@ import { ApiProperty } from "@nestjs/swagger"; import { IsNumber, IsString, Length, ValidateNested, IsArray, ArrayUnique } from "class-validator"; import { PermissionTypeDto } from "./permission-type.dto"; import { Type } from "class-transformer"; +import { ArrayDistinct } from "@helpers/array-distinct.validator"; +import { nameof } from "@helpers/type-helper"; export class CreatePermissionDto { @ApiProperty({ @@ -10,7 +12,7 @@ export class CreatePermissionDto { enum: PermissionType, }) @IsArray() - @ArrayUnique() + @ArrayDistinct(nameof('type')) @Type(() => PermissionTypeDto) @ValidateNested({ each: true }) levels: PermissionTypeDto[] diff --git a/src/entities/enum/error-codes.enum.ts b/src/entities/enum/error-codes.enum.ts index 8598d96d..2f5a206b 100644 --- a/src/entities/enum/error-codes.enum.ts +++ b/src/entities/enum/error-codes.enum.ts @@ -44,4 +44,5 @@ export enum ErrorCodes { DeviceModelDoesNotExist = "MESSAGE.DEVICE-MODEL-DOES-NOT-EXIST", InvalidKeyInKeyValuePair = "MESSAGE.INVALID-KEY-IN-KEY-VALUE-PAIR", InvalidValueInKeyValuePair = "MESSAGE.INVALID-VALUE-IN-KEY-VALUE-PAIR", + DuplicatePermissionTypes = "MESSAGE.DUPLICATE-PERMISSION-TYPES", } diff --git a/src/helpers/array-distinct.validator.ts b/src/helpers/array-distinct.validator.ts new file mode 100644 index 00000000..9c5a4df2 --- /dev/null +++ b/src/helpers/array-distinct.validator.ts @@ -0,0 +1,41 @@ +import { + registerDecorator, + ValidationArguments, + ValidationOptions, +} from "class-validator"; +import { ErrorCodes } from "@enum/error-codes.enum"; + +/** + * + * @param property + * @param validationOptions + * @see https://github.com/typestack/class-validator/issues/592#issuecomment-621645012 + */ +export function ArrayDistinct( + property: string, + validationOptions?: ValidationOptions +) { + return (object: unknown, propertyName: string): void => { + registerDecorator({ + name: "ArrayDistinct", + target: object.constructor, + propertyName, + constraints: [property], + options: validationOptions, + validator: { + validate(value: unknown): boolean { + if (Array.isArray(value)) { + const distinct = [ + ...new Set(value.map((v): unknown => v[property])), + ]; + return distinct.length === value.length; + } + return false; + }, + defaultMessage(args: ValidationArguments): string { + return ErrorCodes.DuplicatePermissionTypes; + }, + }, + }); + }; +} diff --git a/src/helpers/permission.helper.ts b/src/helpers/permission.helper.ts index 4aae2bc1..9c161610 100644 --- a/src/helpers/permission.helper.ts +++ b/src/helpers/permission.helper.ts @@ -30,7 +30,6 @@ export abstract class PermissionCreator { static createGlobalAdmin(): Permission { const pm = this.create("GlobalAdmin"); - // TODO: Does this auto-fill dates etc. pm.type = [{ type: PermissionType.GlobalAdmin } as PermissionTypeEntity]; return pm; } @@ -38,7 +37,6 @@ export abstract class PermissionCreator { static createRead(name: string, org?: Organization, addNewApps = false): Permission { const pm = this.create(name, org, addNewApps); - // TODO: Does this auto-fill dates etc. pm.type = [{ type: PermissionType.Read } as PermissionTypeEntity]; return pm; } @@ -50,7 +48,6 @@ export abstract class PermissionCreator { ): Permission { const pm = this.create(name, org, addNewApps); - // TODO: Does this auto-fill dates etc. pm.type = [ { type: PermissionType.OrganizationApplicationAdmin } as PermissionTypeEntity, ]; @@ -64,7 +61,6 @@ export abstract class PermissionCreator { ): Permission { const pm = this.create(name, org, addNewApps); - // TODO: Does this auto-fill dates etc. pm.type = [ { type: PermissionType.OrganizationUserAdmin } as PermissionTypeEntity, ]; @@ -78,7 +74,6 @@ export abstract class PermissionCreator { ): Permission { const pm = this.create(name, org, addNewApps); - // TODO: Does this auto-fill dates etc. pm.type = [ { type: PermissionType.OrganizationGatewayAdmin } as PermissionTypeEntity, ]; diff --git a/src/services/user-management/permission.service.ts b/src/services/user-management/permission.service.ts index acb85940..43415145 100644 --- a/src/services/user-management/permission.service.ts +++ b/src/services/user-management/permission.service.ts @@ -102,6 +102,7 @@ export class PermissionService { orgGatewayAadminPermission.updatedBy = userId; return { readPermission, orgApplicationAdminPermission, orgAdminPermission, orgGatewayAadminPermission }; } + private setUserIdOnPermissions(permission: Permission, userId: number) { permission.type.forEach(type => { type.createdBy = userId; @@ -214,10 +215,6 @@ export class PermissionService { permission: Permission, dto: UpdatePermissionDto ): Promise { - // TODO: Is it just as easy to make the frontend do it? What happens if this issue goes through? - // Sanitize types - permission.type = _.uniqBy(permission.type, type => type.type) - if (isOrganizationApplicationPermission(permission)) { permission.applications = await this.applicationService.findManyByIds( dto.applicationIds From 6c8027492d87a2b555ba4252351e2b08f95f5bdb Mon Sep 17 00:00:00 2001 From: Aram Al-Sabti Date: Wed, 18 May 2022 12:11:06 +0200 Subject: [PATCH 11/19] Constrain permission type to enum --- .../permissions/permission-type.entity.ts | 4 ++- .../1651142158492-revised-permissions.ts | 26 +++++++++++-------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/entities/permissions/permission-type.entity.ts b/src/entities/permissions/permission-type.entity.ts index 14004458..988944be 100644 --- a/src/entities/permissions/permission-type.entity.ts +++ b/src/entities/permissions/permission-type.entity.ts @@ -7,7 +7,9 @@ import { nameof } from "@helpers/type-helper"; @Entity("permission_type") @Unique([nameof("type"), nameof("permission")]) export class PermissionTypeEntity extends DbBaseEntity { - @Column() + @Column("enum", { + enum: PermissionType + }) type: PermissionType; @ManyToOne(() => Permission, p => p.type, { diff --git a/src/migration/1651142158492-revised-permissions.ts b/src/migration/1651142158492-revised-permissions.ts index 2a8ef65a..ceb02f3c 100644 --- a/src/migration/1651142158492-revised-permissions.ts +++ b/src/migration/1651142158492-revised-permissions.ts @@ -324,11 +324,13 @@ returning id, "permission"."clonedFromId"`; private async migratePermissionTypeUp(queryRunner: QueryRunner) { await queryRunner.query(`DROP INDEX "public"."IDX_71bf2818fb2ad92e208d7aeadf"`); - await queryRunner.query(`CREATE TABLE "permission_type" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "type" character varying NOT NULL, "createdById" integer, "updatedById" integer, "permissionId" integer, CONSTRAINT "PK_3f2a17e0bff1bc4e34254b27d78" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TYPE "public"."permission_type_type_enum" AS ENUM('GlobalAdmin', 'OrganizationUserAdmin', 'OrganizationGatewayAdmin', 'OrganizationApplicationAdmin', 'Read', 'OrganizationPermission', 'OrganizationApplicationPermissions', 'ApiKeyPermission')`); + await queryRunner.query(`CREATE TABLE "permission_type" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "type" "public"."permission_type_type_enum" NOT NULL, "createdById" integer, "updatedById" integer, "permissionId" integer, CONSTRAINT "PK_3f2a17e0bff1bc4e34254b27d78" PRIMARY KEY ("id"))`); const fetchAllPermissions = `select "createdAt", "updatedAt", - type, + -- Casting from one enum to another requires casting it to text first + type::text::"public"."permission_type_type_enum", "createdById", "updatedById", "id" AS "permissionId" @@ -348,7 +350,7 @@ returning id, "permission"."clonedFromId"`; private async migrateDown(queryRunner: QueryRunner): Promise { await queryRunner.query( - `ALTER TABLE "permission" ALTER COLUMN "type" TYPE "permission_type_enum_temp" USING "type"::"text"::"permission_type_enum_temp"` + `ALTER TABLE "permission" ALTER COLUMN "type" TYPE "${permissionTypeUnionName}" USING "type"::"text"::"${permissionTypeUnionName}"` ); // When migrating permisisons tied to old permission types, we need to keep track of the old id. This is for updating any dependents @@ -405,7 +407,7 @@ returning id, "permission"."clonedFromId"`; await queryRunner.query( `ALTER TABLE "permission" ALTER COLUMN "type" TYPE "permission_type_enum_old" USING "type"::"text"::"permission_type_enum_old"` ); - await queryRunner.query(`DROP TYPE "permission_type_enum_temp"`); + await queryRunner.query(`DROP TYPE "${permissionTypeUnionName}"`); } private async migratePermissionTypeDown(queryRunner: QueryRunner) { @@ -422,11 +424,11 @@ returning id, "permission"."clonedFromId"`; await queryRunner.query(`CREATE TEMP TABLE "permission_type_priority" ON COMMIT DROP AS SELECT * FROM ( VALUES - ('GlobalAdmin'::character varying, 0), - ('OrganizationUserAdmin'::character varying, 10), - ('OrganizationApplicationAdmin'::character varying, 20), - ('OrganizationGatewayAdmin'::character varying, 30), - ('Read'::character varying, 40) + ('GlobalAdmin'::text, 0), + ('OrganizationUserAdmin'::text, 10), + ('OrganizationApplicationAdmin'::text, 20), + ('OrganizationGatewayAdmin'::text, 30), + ('Read'::text, 40) ) as t (type, priority)`); // Migrate permission levels @@ -434,12 +436,13 @@ returning id, "permission"."clonedFromId"`; public.permission pm SET -- The column is updated for each row with identical "permissionId" - type = CAST(highestPmType.type as ${permissionTypeUnionName}) + -- Casting from one enum to another requires casting it to text first + type = highestPmType.type::text::${permissionTypeUnionName} FROM ( SELECT pt.* FROM permission_type_priority ptp - JOIN permission_type pt ON ptp.type = pt.type + JOIN permission_type pt ON ptp.type::${permissionTypeUnionName} = pt.type::text::${permissionTypeUnionName} -- Order from lowest to highest priority. The last update for any given permission -- thus be the type with highest priority (lowest value) ORDER BY ptp.priority DESC @@ -451,6 +454,7 @@ returning id, "permission"."clonedFromId"`; // This should not happen. Review them manually. await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "type" SET NOT NULL`); await queryRunner.query(`DROP TABLE "permission_type"`); + await queryRunner.query(`DROP TYPE "public"."permission_type_type_enum"`); await queryRunner.query(`CREATE INDEX "IDX_71bf2818fb2ad92e208d7aeadf" ON "permission" ("type") `); } } From 11c6f084aa04fc2700fa951e7e2484acf8e33e96 Mon Sep 17 00:00:00 2001 From: Aram Al-Sabti Date: Wed, 18 May 2022 12:30:20 +0200 Subject: [PATCH 12/19] Removed unused permission types --- src/entities/enum/permission-type.enum.ts | 4 ---- src/migration/1651142158492-revised-permissions.ts | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/entities/enum/permission-type.enum.ts b/src/entities/enum/permission-type.enum.ts index 5d1e165c..16fa326d 100644 --- a/src/entities/enum/permission-type.enum.ts +++ b/src/entities/enum/permission-type.enum.ts @@ -4,8 +4,4 @@ export enum PermissionType { OrganizationGatewayAdmin = "OrganizationGatewayAdmin", OrganizationApplicationAdmin = "OrganizationApplicationAdmin", Read = "Read", - - OrganizationPermission = "OrganizationPermission", - OrganizationApplicationPermissions = "OrganizationApplicationPermissions", - ApiKeyPermission = "ApiKeyPermission", } diff --git a/src/migration/1651142158492-revised-permissions.ts b/src/migration/1651142158492-revised-permissions.ts index ceb02f3c..06879b34 100644 --- a/src/migration/1651142158492-revised-permissions.ts +++ b/src/migration/1651142158492-revised-permissions.ts @@ -47,7 +47,7 @@ export class revisedPermissions1651142158492 implements MigrationInterface { `ALTER TYPE "public"."permission_type_enum" RENAME TO "permission_type_enum_old"` ); await queryRunner.query( - `CREATE TYPE "permission_type_enum" AS ENUM('GlobalAdmin', 'OrganizationUserAdmin', 'OrganizationGatewayAdmin', 'OrganizationApplicationAdmin', 'Read', 'OrganizationPermission', 'OrganizationApplicationPermissions', 'ApiKeyPermission')` + `CREATE TYPE "permission_type_enum" AS ENUM('GlobalAdmin', 'OrganizationUserAdmin', 'OrganizationGatewayAdmin', 'OrganizationApplicationAdmin', 'Read')` ); // Migrates existing data. This can result in duplicate permissions and duplicates of its dependents @@ -324,7 +324,7 @@ returning id, "permission"."clonedFromId"`; private async migratePermissionTypeUp(queryRunner: QueryRunner) { await queryRunner.query(`DROP INDEX "public"."IDX_71bf2818fb2ad92e208d7aeadf"`); - await queryRunner.query(`CREATE TYPE "public"."permission_type_type_enum" AS ENUM('GlobalAdmin', 'OrganizationUserAdmin', 'OrganizationGatewayAdmin', 'OrganizationApplicationAdmin', 'Read', 'OrganizationPermission', 'OrganizationApplicationPermissions', 'ApiKeyPermission')`); + await queryRunner.query(`CREATE TYPE "public"."permission_type_type_enum" AS ENUM('GlobalAdmin', 'OrganizationUserAdmin', 'OrganizationGatewayAdmin', 'OrganizationApplicationAdmin', 'Read')`); await queryRunner.query(`CREATE TABLE "permission_type" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "type" "public"."permission_type_type_enum" NOT NULL, "createdById" integer, "updatedById" integer, "permissionId" integer, CONSTRAINT "PK_3f2a17e0bff1bc4e34254b27d78" PRIMARY KEY ("id"))`); const fetchAllPermissions = `select "createdAt", From ea07580cd621c9b917e8e4f0fe182860568c3f41 Mon Sep 17 00:00:00 2001 From: Aram Al-Sabti Date: Wed, 18 May 2022 16:42:12 +0200 Subject: [PATCH 13/19] Fix kombit permissions --- .../new-kombit-creation.controller.ts | 9 +- .../user-management/permission.controller.ts | 2 +- .../user-management/user.controller.ts | 14 +-- .../add-user-to-permission.dto.ts | 10 +- src/entities/permission.entity.ts | 104 ------------------ src/helpers/security-helper.ts | 9 ++ .../1651142158492-revised-permissions.ts | 1 - .../user-management/organization.service.ts | 14 +-- .../user-management/permission.service.ts | 17 ++- src/services/user-management/user.service.ts | 3 +- 10 files changed, 46 insertions(+), 137 deletions(-) delete mode 100644 src/entities/permission.entity.ts diff --git a/src/controllers/user-management/new-kombit-creation.controller.ts b/src/controllers/user-management/new-kombit-creation.controller.ts index b2605f2d..4c3e32cd 100644 --- a/src/controllers/user-management/new-kombit-creation.controller.ts +++ b/src/controllers/user-management/new-kombit-creation.controller.ts @@ -35,7 +35,6 @@ import { AuditLog } from "@services/audit-log.service"; import { OrganizationService } from "@services/user-management/organization.service"; import { PermissionService } from "@services/user-management/permission.service"; import { UserService } from "@services/user-management/user.service"; -import { Permission, OrganizationPermission } from "@entities/permission.entity"; @UseGuards(JwtAuthGuard) @ApiBearerAuth() @@ -119,14 +118,14 @@ export class NewKombitCreationController { @Req() req: AuthenticatedRequest, @Body() updateUserOrgsDto: UpdateUserOrgsDto ): Promise { - + try { const user = await this.userService.findOne(req.user.userId); - const permissions: OrganizationPermission[] = await this.permissionService.findManyWithRelations( + const permissions = await this.permissionService.findManyWithRelations( updateUserOrgsDto.requestedOrganizationIds ); - const requestedOrganizations = await this.organizationService.mapPermissionsToOrganizations( + const requestedOrganizations = this.organizationService.mapPermissionsToOrganizations( permissions ); @@ -160,7 +159,7 @@ export class NewKombitCreationController { @Query("extendedInfo") extendedInfo?: boolean ): Promise { - const getExtendedInfo = extendedInfo != null ? extendedInfo : false; + const getExtendedInfo = extendedInfo != null ? extendedInfo : false; try { // Don't leak the passwordHash // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/src/controllers/user-management/permission.controller.ts b/src/controllers/user-management/permission.controller.ts index bf4f1f28..84bd12fc 100644 --- a/src/controllers/user-management/permission.controller.ts +++ b/src/controllers/user-management/permission.controller.ts @@ -111,7 +111,7 @@ export class PermissionController { dto.organizationId ); - const org: Organization = await this.organizationService.mapPermissionsToOneOrganization( + const org: Organization = this.organizationService.mapPermissionsToOneOrganization( permissions ); diff --git a/src/controllers/user-management/user.controller.ts b/src/controllers/user-management/user.controller.ts index 81df0079..f246357b 100644 --- a/src/controllers/user-management/user.controller.ts +++ b/src/controllers/user-management/user.controller.ts @@ -17,7 +17,6 @@ import { Logger } from "@nestjs/common"; import { ApiBearerAuth, ApiForbiddenResponse, - ApiNotFoundResponse, ApiOperation, ApiTags, ApiUnauthorizedResponse, @@ -32,10 +31,7 @@ import { CreateUserDto } from "@dto/user-management/create-user.dto"; import { UpdateUserDto } from "@dto/user-management/update-user.dto"; import { UserResponseDto } from "@dto/user-response.dto"; import { ErrorCodes } from "@entities/enum/error-codes.enum"; -import { - checkIfUserHasAdminAccessToOrganization, - checkIfUserIsGlobalAdmin, -} from "@helpers/security-helper"; +import { checkIfUserIsGlobalAdmin, checkIfUserHasAccessToOrganization, OrganizationAccessScope } from "@helpers/security-helper"; import { UserService } from "@services/user-management/user.service"; import { ListAllUsersResponseDto } from "@dto/list-all-users-response.dto"; import { ListAllUsersMinimalResponseDto } from "@dto/list-all-users-minimal-response.dto"; @@ -43,9 +39,7 @@ import { AuditLog } from "@services/audit-log.service"; import { ActionType } from "@entities/audit-log-entry"; import { User } from "@entities/user.entity"; import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; -import { CreateNewKombitUserDto } from "@dto/user-management/create-new-kombit-user.dto"; import { OrganizationService } from "@services/user-management/organization.service"; -import { UpdateUserOrgsDto } from "@dto/user-management/update-user-orgs.dto"; import { Organization } from "@entities/organization.entity"; import { RejectUserDto } from "@dto/user-management/reject-user.dto"; @@ -113,15 +107,15 @@ export class UserController { @Put("/rejectUser") @ApiOperation({ summary: "Rejects user and removes from awaiting users" }) async rejectUser( - @Req() req: AuthenticatedRequest, + @Req() req: AuthenticatedRequest, @Body() body: RejectUserDto ): Promise { - checkIfUserHasAdminAccessToOrganization(req, body.orgId); + checkIfUserHasAccessToOrganization(req, body.orgId, OrganizationAccessScope.UserAdministrationWrite); const user = await this.userService.findOne(body.userIdToReject); const organization = await this.organizationService.findByIdWithUsers(body.orgId); - return await this.organizationService.rejectAwaitingUser(user, organization); + return await this.organizationService.rejectAwaitingUser(user, organization); } @Put(":id") diff --git a/src/entities/dto/user-management/add-user-to-permission.dto.ts b/src/entities/dto/user-management/add-user-to-permission.dto.ts index f7dd7274..65cac1cb 100644 --- a/src/entities/dto/user-management/add-user-to-permission.dto.ts +++ b/src/entities/dto/user-management/add-user-to-permission.dto.ts @@ -1,6 +1,10 @@ import { PermissionType } from "@enum/permission-type.enum"; import { ApiProperty } from "@nestjs/swagger"; import { IsEnum, IsNumber } from "class-validator"; +import { omit } from "lodash"; + +const globalAdminEnumName: keyof typeof PermissionType = 'GlobalAdmin'; +const acceptablePermissionType = omit(PermissionType, globalAdminEnumName); export class PermissionRequestAcceptUser { @ApiProperty({ required: true }) @@ -13,8 +17,8 @@ export class PermissionRequestAcceptUser { @ApiProperty({ required: true, - enum: PermissionType, + enum: acceptablePermissionType, }) - @IsEnum(PermissionType) - level: "OrganizationAdmin" | "Write" | "Read"; + @IsEnum(acceptablePermissionType) + level: keyof typeof acceptablePermissionType; } diff --git a/src/entities/permission.entity.ts b/src/entities/permission.entity.ts deleted file mode 100644 index a03df00e..00000000 --- a/src/entities/permission.entity.ts +++ /dev/null @@ -1,104 +0,0 @@ -//All Permissions is included in one file since circular references and typescript makes the program crash unregularaly. -//It happens because circular references can happen between files and not only types. - -import { User } from "@entities/user.entity"; -import { PermissionType } from "@enum/permission-type.enum"; -import { ChildEntity, Column, Entity, ManyToMany, ManyToOne, TableInheritance } from "typeorm"; -import { DbBaseEntity } from "@entities/base.entity"; -import { Organization } from "./organization.entity"; -import { ApiKey } from "./api-key.entity"; -import { Application } from "./application.entity"; - -@Entity() -@TableInheritance({ - column: { type: "enum", name: "type", enum: PermissionType }, -}) -export abstract class Permission extends DbBaseEntity { - constructor(name: string) { - super(); - this.name = name; - } - - @Column("enum", { - enum: PermissionType, - }) - type: PermissionType; - - @Column() - name: string; - - @ManyToMany( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - () => User, - user => user.permissions - ) - users: User[]; -} - -@ChildEntity(PermissionType.OrganizationPermission) -export abstract class OrganizationPermission extends Permission { - constructor(name: string, org: Organization) { - super(name); - this.organization = org; - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - @ManyToOne(() => Organization, { onDelete: "CASCADE" }) - organization: Organization; -} - -@ChildEntity(PermissionType.GlobalAdmin) -export class GlobalAdminPermission extends Permission { - constructor() { - super("GlobalAdmin"); - this.type = PermissionType.GlobalAdmin; - } -} - -@ChildEntity(PermissionType.ApiKeyPermission) -export abstract class ApiKeyPermission extends Permission { - @ManyToMany(_ => ApiKey, key => key.permissions, { onDelete: "CASCADE" }) - apiKeys: ApiKey[]; - -} - -@ChildEntity(PermissionType.OrganizationApplicationPermissions) -export abstract class OrganizationApplicationPermission extends OrganizationPermission { - constructor(name: string, org: Organization, addNewApps?: boolean) { - super(name, org); - this.automaticallyAddNewApplications = - addNewApps != undefined ? addNewApps : false; - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - @ManyToMany(() => Application, application => application.permissions) - applications: Application[]; - - @Column({ nullable: true, default: false, type: Boolean }) - automaticallyAddNewApplications = false; -} - -@ChildEntity(PermissionType.OrganizationAdmin) -export class OrganizationAdminPermission extends OrganizationPermission { - constructor(name: string, org: Organization) { - super(name, org); - this.type = PermissionType.OrganizationAdmin; - } -} - -@ChildEntity(PermissionType.Write) -export class WritePermission extends OrganizationApplicationPermission { - constructor(name: string, org: Organization, addNewApps = false) { - super(name, org, addNewApps); - this.type = PermissionType.Write; - } -} - -@ChildEntity(PermissionType.Read) -export class ReadPermission extends OrganizationApplicationPermission { - constructor(name: string, org: Organization, addNewApps = false) { - super(name, org, addNewApps); - this.type = PermissionType.Read; - } -} - diff --git a/src/helpers/security-helper.ts b/src/helpers/security-helper.ts index 9905e4a9..195f87e1 100644 --- a/src/helpers/security-helper.ts +++ b/src/helpers/security-helper.ts @@ -113,3 +113,12 @@ export function isOrganizationApplicationPermission(p: { type === PermissionType.OrganizationApplicationAdmin ); } + +export function isPermissionType( + p: { + type: PermissionTypeEntity[]; + }, + targetType: PermissionType +): p is Permission { + return p.type.some(({ type }) => type === targetType); +} diff --git a/src/migration/1651142158492-revised-permissions.ts b/src/migration/1651142158492-revised-permissions.ts index 06879b34..d14fd290 100644 --- a/src/migration/1651142158492-revised-permissions.ts +++ b/src/migration/1651142158492-revised-permissions.ts @@ -1,5 +1,4 @@ import { MigrationInterface, QueryRunner } from "typeorm"; -import { NotImplementedException } from "@nestjs/common"; type AppPermissions = { applicationId: number; diff --git a/src/services/user-management/organization.service.ts b/src/services/user-management/organization.service.ts index 2e5b127b..f1f53d95 100644 --- a/src/services/user-management/organization.service.ts +++ b/src/services/user-management/organization.service.ts @@ -23,7 +23,7 @@ import { ErrorCodes } from "@enum/error-codes.enum"; import { PermissionService } from "./permission.service"; import { User } from "@entities/user.entity"; import { UserService } from "./user.service"; -import { Permission, OrganizationPermission } from "@entities/permission.entity"; +import { Permission } from "@entities/permissions/permission.entity"; @Injectable() export class OrganizationService { @@ -100,9 +100,9 @@ export class OrganizationService { }; } - async mapPermissionsToOrganizations( - permissions: OrganizationPermission[] - ): Promise { + mapPermissionsToOrganizations( + permissions: Permission[] + ): Organization[] { const requestedOrganizations: Organization[] = []; for (let index = 0; index < permissions.length; index++) { @@ -131,9 +131,9 @@ export class OrganizationService { return requestedOrganizations; } - async mapPermissionsToOneOrganization( - permissions: OrganizationPermission[] - ): Promise { + mapPermissionsToOneOrganization( + permissions: Permission[] + ): Organization { const org: Organization = new Organization(); permissions.map(permission => { diff --git a/src/services/user-management/permission.service.ts b/src/services/user-management/permission.service.ts index 3a364d74..816c6f84 100644 --- a/src/services/user-management/permission.service.ts +++ b/src/services/user-management/permission.service.ts @@ -321,11 +321,18 @@ export class PermissionService { }); } - async getGlobalPermission(): Promise { - return await getManager().findOneOrFail(Permission, { - where: { type: PermissionType.GlobalAdmin }, - relations: ["users"], - }); + getGlobalPermission(): Promise { + return this.permissionRepository + .createQueryBuilder("permission") + .where( + " type.type = :permType", + { + permType: PermissionType.GlobalAdmin, + } + ) + .leftJoin("permission.type", "type") + .leftJoinAndSelect("permission.users", "users") + .getOneOrFail(); } buildPermissionsQuery(): SelectQueryBuilder { diff --git a/src/services/user-management/user.service.ts b/src/services/user-management/user.service.ts index 5617894d..89ecf688 100644 --- a/src/services/user-management/user.service.ts +++ b/src/services/user-management/user.service.ts @@ -27,6 +27,7 @@ import { Organization } from "@entities/organization.entity"; import SMTPTransport from "nodemailer/lib/smtp-transport"; import { PermissionType } from "@enum/permission-type.enum"; import { ConfigService } from "@nestjs/config"; +import { isPermissionType } from "@helpers/security-helper"; @Injectable() export class UserService { @@ -415,7 +416,7 @@ export class UserService { const emails: string[] = []; const globalAdminPermission: Permission = await this.permissionService.getGlobalPermission(); organization.permissions.forEach(permission => { - if (permission.type === PermissionType.OrganizationAdmin) { + if (isPermissionType(permission, PermissionType.OrganizationUserAdmin)) { if (permission.users.length > 0) { permission.users.forEach(user => { emails.push(user.email); From 2da087c500199e085ec5635886b1492a02eb24a6 Mon Sep 17 00:00:00 2001 From: AramAlsabti <92869496+AramAlsabti@users.noreply.github.com> Date: Wed, 18 May 2022 17:18:51 +0200 Subject: [PATCH 14/19] Show overview of gateway status (#170) * Init kafka for online status service * Subscribe to gateway connection state Migration, data storage and error handling are missing. * Init get gateway status * Refactor gateway status * Implement gateway status fetch. Missing save in db * Store gateway status messages. Cleanup * Organization id is optional * Re-timestamped migration to to make it fit in the migration timeline * Minor renaming * Bump migration timestamp * Fetch status for single gateway (#171) Co-authored-by: nlg --- package-lock.json | 84 ++++++++++++ package.json | 1 + resources/chirpstack-state.proto | 16 +++ .../lorawan/lorawan-gateway.controller.ts | 56 ++++++++ .../receive-data.controller.ts | 2 +- .../sigfox-listener.controller.ts | 2 +- .../backend/gateway-all-status.dto.ts | 22 ++++ .../chirpstack/backend/gateway-status.dto.ts | 23 ++++ .../chirpstack/chirpstack-mqtt-message.dto.ts | 5 + .../chirpstack-mqtt-state-message.dto.ts | 4 + .../dto/kafka/raw-gateway-state.dto.ts | 5 + .../dto/kafka/raw-iot-device-request.dto.ts | 5 + src/entities/dto/kafka/raw-request.dto.ts | 1 - .../enum/gateway-status-interval.enum.ts | 20 +++ src/entities/enum/kafka-topic.enum.ts | 1 + src/entities/gateway-status-history.entity.ts | 16 +++ .../1652861431000-gateway-status-history.ts | 18 +++ .../gateway-persistence.module.ts | 17 +++ .../data-target/data-target-kafka.module.ts | 2 + .../lorawan-gateway.module.ts | 18 +++ src/modules/shared.module.ts | 2 + src/resources/resource-paths.ts | 8 ++ .../chirpstack/chirpstack-gateway.service.ts | 7 +- .../gateway-status-history.service.ts | 103 +++++++++++++++ .../chirpstack-mqtt-listener.service.ts | 69 +++++++++- .../device-integration-persistence.service.ts | 18 +-- .../gateway-persistence.service.ts | 124 ++++++++++++++++++ .../payload-decoder-listener.service.ts | 17 +-- .../data-management/receive-data.service.ts | 60 +++++++-- .../data-target-kafka-listener.service.ts | 2 +- src/services/kafka/kafka.message.ts | 2 +- 31 files changed, 687 insertions(+), 43 deletions(-) create mode 100644 resources/chirpstack-state.proto create mode 100644 src/controllers/admin-controller/lorawan/lorawan-gateway.controller.ts create mode 100644 src/entities/dto/chirpstack/backend/gateway-all-status.dto.ts create mode 100644 src/entities/dto/chirpstack/backend/gateway-status.dto.ts create mode 100644 src/entities/dto/chirpstack/state/chirpstack-mqtt-state-message.dto.ts create mode 100644 src/entities/dto/kafka/raw-gateway-state.dto.ts create mode 100644 src/entities/dto/kafka/raw-iot-device-request.dto.ts create mode 100644 src/entities/enum/gateway-status-interval.enum.ts create mode 100644 src/entities/gateway-status-history.entity.ts create mode 100644 src/migration/1652861431000-gateway-status-history.ts create mode 100644 src/modules/data-management/gateway-persistence.module.ts create mode 100644 src/modules/device-integrations/lorawan-gateway.module.ts create mode 100644 src/resources/resource-paths.ts create mode 100644 src/services/chirpstack/gateway-status-history.service.ts create mode 100644 src/services/data-management/gateway-persistence.service.ts diff --git a/package-lock.json b/package-lock.json index 525aac65..e70f2fa4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1486,6 +1486,60 @@ } } }, + "@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" + }, + "@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "requires": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" + }, "@schematics/schematics": { "version": "0.1102.6", "resolved": "https://registry.npmjs.org/@schematics/schematics/-/schematics-0.1102.6.tgz", @@ -1757,6 +1811,11 @@ "integrity": "sha512-tjSSOTHhI5mCHTy/OOXYIhi2Wt1qcbHmuXD1Ha7q70CgI/I71afO4XtLb/cVexki1oVYchpul/TOuu3Arcdxrg==", "dev": true }, + "@types/long": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" + }, "@types/mime": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", @@ -7390,6 +7449,11 @@ } } }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -8595,6 +8659,26 @@ "sisteransi": "^1.0.5" } }, + "protobufjs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.2.tgz", + "integrity": "sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==", + "requires": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + } + }, "proxy-addr": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", diff --git a/package.json b/package.json index df932bd3..22e33b1b 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "passport-local": "^1.0.0", "passport-saml": "^1.3.5", "pg": "^8.5.1", + "protobufjs": "^6.11.2", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^6.6.3", diff --git a/resources/chirpstack-state.proto b/resources/chirpstack-state.proto new file mode 100644 index 00000000..1befa749 --- /dev/null +++ b/resources/chirpstack-state.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package gw; + +// ConnState contains the connection state of a gateway. +message ConnState { + // Gateway ID. + bytes gateway_id = 1 [json_name = "gatewayID"]; + + enum State { + OFFLINE = 0; + ONLINE = 1; + } + + State state = 2; +} diff --git a/src/controllers/admin-controller/lorawan/lorawan-gateway.controller.ts b/src/controllers/admin-controller/lorawan/lorawan-gateway.controller.ts new file mode 100644 index 00000000..8d518019 --- /dev/null +++ b/src/controllers/admin-controller/lorawan/lorawan-gateway.controller.ts @@ -0,0 +1,56 @@ +import { ComposeAuthGuard } from "@auth/compose-auth.guard"; +import { Read } from "@auth/roles.decorator"; +import { RolesGuard } from "@auth/roles.guard"; +import { + GatewayGetAllStatusResponseDto, + ListAllGatewayStatusDto, +} from "@dto/chirpstack/backend/gateway-all-status.dto"; +import { GatewayStatus, GetGatewayStatusQuery } from "@dto/chirpstack/backend/gateway-status.dto"; +import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; +import { checkIfUserHasReadAccessToOrganization } from "@helpers/security-helper"; +import { Controller, Get, Param, Query, Req, UseGuards } from "@nestjs/common"; +import { ApiBearerAuth, ApiOperation, ApiProduces, ApiTags } from "@nestjs/swagger"; +import { ChirpstackGatewayService } from "@services/chirpstack/chirpstack-gateway.service"; +import { GatewayStatusHistoryService } from "@services/chirpstack/gateway-status-history.service"; + +@ApiTags("LoRaWAN gateway") +@Controller("lorawan/gateway") +@UseGuards(ComposeAuthGuard, RolesGuard) +@ApiBearerAuth() +@Read() +export class LoRaWANGatewayController { + constructor( + private onlineHistoryService: GatewayStatusHistoryService, + private chirpstackGatewayService: ChirpstackGatewayService + ) {} + + @Get("/status") + @ApiProduces("application/json") + @ApiOperation({ summary: "Get the status for all LoRaWAN gateways" }) + @Read() + async getAllStatus( + @Req() req: AuthenticatedRequest, + @Query() query: ListAllGatewayStatusDto + ): Promise { + if (query.organizationId) { + // TODO: NEW USER MANAGEMENT: Update the rights once it's merged + checkIfUserHasReadAccessToOrganization(req, query.organizationId); + } + + return this.onlineHistoryService.findAllWithChirpstack(query); + } + + @Get("/status/:id") + @ApiProduces("application/json") + @ApiOperation({ summary: "Get the status for a LoRaWAN gateway" }) + async getStatus( + @Req() req: AuthenticatedRequest, + @Param("id") id: string, + @Query() query: GetGatewayStatusQuery + ): Promise { + const gatewayDto = await this.chirpstackGatewayService.getOne(id); + checkIfUserHasReadAccessToOrganization(req, gatewayDto.gateway.internalOrganizationId); + + return this.onlineHistoryService.findOne(gatewayDto.gateway, query.timeInterval); + } +} diff --git a/src/controllers/device-data-controller/receive-data.controller.ts b/src/controllers/device-data-controller/receive-data.controller.ts index 8a45798f..be52563b 100644 --- a/src/controllers/device-data-controller/receive-data.controller.ts +++ b/src/controllers/device-data-controller/receive-data.controller.ts @@ -44,7 +44,7 @@ export class ReceiveDataController { // @HACK: Convert the 'data' back to a string. // NestJS / BodyParser always converts the input to an object for us. const dataAsString = JSON.stringify(data); - await this.receiveDataService.sendToKafka( + await this.receiveDataService.sendRawIotDeviceRequestToKafka( iotDevice, dataAsString, IoTDeviceType.GenericHttp.toString() diff --git a/src/controllers/device-data-controller/sigfox-listener.controller.ts b/src/controllers/device-data-controller/sigfox-listener.controller.ts index 7896688e..df51bd91 100644 --- a/src/controllers/device-data-controller/sigfox-listener.controller.ts +++ b/src/controllers/device-data-controller/sigfox-listener.controller.ts @@ -49,7 +49,7 @@ export class SigFoxListenerController { const sigfoxDevice = await this.findSigFoxDevice(data); const dataAsString = JSON.stringify(data); - await this.receiveDataService.sendToKafka( + await this.receiveDataService.sendRawIotDeviceRequestToKafka( sigfoxDevice, dataAsString, IoTDeviceType.SigFox.toString(), diff --git a/src/entities/dto/chirpstack/backend/gateway-all-status.dto.ts b/src/entities/dto/chirpstack/backend/gateway-all-status.dto.ts new file mode 100644 index 00000000..48d0a3d2 --- /dev/null +++ b/src/entities/dto/chirpstack/backend/gateway-all-status.dto.ts @@ -0,0 +1,22 @@ +import { ListAllEntitiesResponseDto } from "@dto/list-all-entities-response.dto"; +import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; +import { GatewayStatusInterval } from "@enum/gateway-status-interval.enum"; +import { IsSwaggerOptional } from "@helpers/optional-validator"; +import { StringToNumber } from "@helpers/string-to-number-validator"; +import { IsEnum } from "class-validator"; +import { GatewayStatus } from "./gateway-status.dto"; + +export class GatewayGetAllStatusResponseDto extends ListAllEntitiesResponseDto {} + +export class ListAllGatewayStatusDto extends ListAllEntitiesDto { + @IsSwaggerOptional() + @StringToNumber() + organizationId?: number; + + @IsSwaggerOptional({ + default: GatewayStatusInterval.DAY, + enum: GatewayStatusInterval, + }) + @IsEnum(GatewayStatusInterval) + timeInterval: GatewayStatusInterval = GatewayStatusInterval.DAY; +} diff --git a/src/entities/dto/chirpstack/backend/gateway-status.dto.ts b/src/entities/dto/chirpstack/backend/gateway-status.dto.ts new file mode 100644 index 00000000..4acff60d --- /dev/null +++ b/src/entities/dto/chirpstack/backend/gateway-status.dto.ts @@ -0,0 +1,23 @@ +import { GatewayStatusInterval } from "@enum/gateway-status-interval.enum"; +import { IsSwaggerOptional } from "@helpers/optional-validator"; +import { IsEnum } from "class-validator"; + +export interface StatusTimestamp { + timestamp: Date; + wasOnline: boolean; +} + +export interface GatewayStatus { + id: string; + name: string; + statusTimestamps: StatusTimestamp[]; +} + +export class GetGatewayStatusQuery { + @IsSwaggerOptional({ + default: GatewayStatusInterval.DAY, + enum: GatewayStatusInterval, + }) + @IsEnum(GatewayStatusInterval) + timeInterval: GatewayStatusInterval = GatewayStatusInterval.DAY; +} diff --git a/src/entities/dto/chirpstack/chirpstack-mqtt-message.dto.ts b/src/entities/dto/chirpstack/chirpstack-mqtt-message.dto.ts index 3b7b3eb8..9136b803 100644 --- a/src/entities/dto/chirpstack/chirpstack-mqtt-message.dto.ts +++ b/src/entities/dto/chirpstack/chirpstack-mqtt-message.dto.ts @@ -16,3 +16,8 @@ export class ChirpstackMQTTMessageTxInfoDto { frequency: number; dr: number; } + +export class ChirpstackMQTTConnectionStateMessageDto { + gatewayId: string; + isOnline: boolean; +} diff --git a/src/entities/dto/chirpstack/state/chirpstack-mqtt-state-message.dto.ts b/src/entities/dto/chirpstack/state/chirpstack-mqtt-state-message.dto.ts new file mode 100644 index 00000000..40ec92e6 --- /dev/null +++ b/src/entities/dto/chirpstack/state/chirpstack-mqtt-state-message.dto.ts @@ -0,0 +1,4 @@ +export class ChirpstackMQTTConnectionStateMessage { + gatewayId: string; + state: "ONLINE"; +} diff --git a/src/entities/dto/kafka/raw-gateway-state.dto.ts b/src/entities/dto/kafka/raw-gateway-state.dto.ts new file mode 100644 index 00000000..6b9d57fb --- /dev/null +++ b/src/entities/dto/kafka/raw-gateway-state.dto.ts @@ -0,0 +1,5 @@ +import { RawRequestDto } from "./raw-request.dto"; + +export class RawGatewayStateDto extends RawRequestDto { + gatewayId: string; +} diff --git a/src/entities/dto/kafka/raw-iot-device-request.dto.ts b/src/entities/dto/kafka/raw-iot-device-request.dto.ts new file mode 100644 index 00000000..072efb61 --- /dev/null +++ b/src/entities/dto/kafka/raw-iot-device-request.dto.ts @@ -0,0 +1,5 @@ +import { RawRequestDto } from "./raw-request.dto"; + +export class RawIoTDeviceRequestDto extends RawRequestDto { + iotDeviceId: number; +} diff --git a/src/entities/dto/kafka/raw-request.dto.ts b/src/entities/dto/kafka/raw-request.dto.ts index 49805988..b15cf370 100644 --- a/src/entities/dto/kafka/raw-request.dto.ts +++ b/src/entities/dto/kafka/raw-request.dto.ts @@ -3,6 +3,5 @@ import { IoTDeviceType } from "@enum/device-type.enum"; export class RawRequestDto { type: IoTDeviceType[number]; rawPayload: JSON; - iotDeviceId: number; unixTimestamp?: number; } diff --git a/src/entities/enum/gateway-status-interval.enum.ts b/src/entities/enum/gateway-status-interval.enum.ts new file mode 100644 index 00000000..d9fc604d --- /dev/null +++ b/src/entities/enum/gateway-status-interval.enum.ts @@ -0,0 +1,20 @@ +import { subtractDays } from "@helpers/date.helper"; + +export enum GatewayStatusInterval { + DAY = "DAY", + WEEK = "WEEK", + MONTH = "MONTH", +} + +export const gatewayStatusIntervalToDate = (interval: GatewayStatusInterval): Date => { + const now = new Date(); + + switch (interval) { + case GatewayStatusInterval.WEEK: + return subtractDays(now, 7); + case GatewayStatusInterval.MONTH: + return subtractDays(now, 30); + default: + return subtractDays(now, 1); + } +}; diff --git a/src/entities/enum/kafka-topic.enum.ts b/src/entities/enum/kafka-topic.enum.ts index 46572f5e..af881201 100644 --- a/src/entities/enum/kafka-topic.enum.ts +++ b/src/entities/enum/kafka-topic.enum.ts @@ -1,4 +1,5 @@ export enum KafkaTopic { RAW_REQUEST = "request.raw", TRANSFORMED_REQUEST = "request.transformed", + RAW_GATEWAY_STATE = "request.gateway.state" } diff --git a/src/entities/gateway-status-history.entity.ts b/src/entities/gateway-status-history.entity.ts new file mode 100644 index 00000000..622c6120 --- /dev/null +++ b/src/entities/gateway-status-history.entity.ts @@ -0,0 +1,16 @@ +import { nameof } from "@helpers/type-helper"; +import { Column, CreateDateColumn, Entity, Unique } from "typeorm"; +import { DbBaseEntity } from "./base.entity"; + +@Entity("gateway_status_history") +@Unique([nameof("mac"), nameof("timestamp")]) +export class GatewayStatusHistory extends DbBaseEntity { + @Column() + mac: string; + + @Column() + wasOnline: boolean; + + @CreateDateColumn() + timestamp: Date; +} diff --git a/src/migration/1652861431000-gateway-status-history.ts b/src/migration/1652861431000-gateway-status-history.ts new file mode 100644 index 00000000..08fad067 --- /dev/null +++ b/src/migration/1652861431000-gateway-status-history.ts @@ -0,0 +1,18 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class gatewayStatusHistory1652861431000 implements MigrationInterface { + name = 'gatewayStatusHistory1652861431000' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "gateway_status_history" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "mac" character varying NOT NULL, "wasOnline" boolean NOT NULL, "timestamp" TIMESTAMP NOT NULL DEFAULT now(), "createdById" integer, "updatedById" integer, CONSTRAINT "UQ_5370e37f34adf6e9c9350bc46c7" UNIQUE ("mac", "timestamp"), CONSTRAINT "PK_defafcb0f6f1ba7a612395b62f8" PRIMARY KEY ("id"))`); + await queryRunner.query(`ALTER TABLE "gateway_status_history" ADD CONSTRAINT "FK_196255adef4a4011a1dea022630" FOREIGN KEY ("createdById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "gateway_status_history" ADD CONSTRAINT "FK_94b6a464efce5a478b185bf7e89" FOREIGN KEY ("updatedById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "gateway_status_history" DROP CONSTRAINT "FK_94b6a464efce5a478b185bf7e89"`); + await queryRunner.query(`ALTER TABLE "gateway_status_history" DROP CONSTRAINT "FK_196255adef4a4011a1dea022630"`); + await queryRunner.query(`DROP TABLE "gateway_status_history"`); + } + +} diff --git a/src/modules/data-management/gateway-persistence.module.ts b/src/modules/data-management/gateway-persistence.module.ts new file mode 100644 index 00000000..fe9bb710 --- /dev/null +++ b/src/modules/data-management/gateway-persistence.module.ts @@ -0,0 +1,17 @@ +import configuration from "@config/configuration"; +import { LoRaWANGatewayModule } from "@modules/device-integrations/lorawan-gateway.module"; +import { SharedModule } from "@modules/shared.module"; +import { Module } from "@nestjs/common"; +import { ConfigModule } from "@nestjs/config"; +import { GatewayPersistenceService } from "@services/data-management/gateway-persistence.service"; + +@Module({ + imports: [ + SharedModule, + ConfigModule.forRoot({ load: [configuration] }), + LoRaWANGatewayModule, + ], + exports: [], + providers: [GatewayPersistenceService], +}) +export class GatewayPersistenceModule {} diff --git a/src/modules/data-target/data-target-kafka.module.ts b/src/modules/data-target/data-target-kafka.module.ts index 6c841953..217ee3c0 100644 --- a/src/modules/data-target/data-target-kafka.module.ts +++ b/src/modules/data-target/data-target-kafka.module.ts @@ -11,6 +11,7 @@ import { KafkaModule } from "@modules/kafka.module"; import { SharedModule } from "@modules/shared.module"; import { DataTargetKafkaListenerService } from "@services/data-targets/data-target-kafka-listener.service"; import { DataTargetFiwareSenderModule } from "./data-target-fiware-sender.module"; +import { GatewayPersistenceModule } from "@modules/data-management/gateway-persistence.module"; @Module({ imports: [ @@ -25,6 +26,7 @@ import { DataTargetFiwareSenderModule } from "./data-target-fiware-sender.module IoTDevicePayloadDecoderDataTargetConnectionModule, ApplicationModule, DataTargetModule, + GatewayPersistenceModule, ], providers: [DataTargetKafkaListenerService], }) diff --git a/src/modules/device-integrations/lorawan-gateway.module.ts b/src/modules/device-integrations/lorawan-gateway.module.ts new file mode 100644 index 00000000..ed96b405 --- /dev/null +++ b/src/modules/device-integrations/lorawan-gateway.module.ts @@ -0,0 +1,18 @@ +import { LoRaWANGatewayController } from "@admin-controller/lorawan/lorawan-gateway.controller"; +import { SharedModule } from "@modules/shared.module"; +import { HttpModule, Module } from "@nestjs/common"; +import { ChirpstackGatewayService } from "@services/chirpstack/chirpstack-gateway.service"; +import { GatewayStatusHistoryService } from "@services/chirpstack/gateway-status-history.service"; +import { ChirpstackSetupNetworkServerService } from "@services/chirpstack/network-server.service"; + +@Module({ + controllers: [LoRaWANGatewayController], + imports: [SharedModule, HttpModule], + providers: [ + ChirpstackGatewayService, + ChirpstackSetupNetworkServerService, + GatewayStatusHistoryService, + ], + exports: [GatewayStatusHistoryService], +}) +export class LoRaWANGatewayModule {} diff --git a/src/modules/shared.module.ts b/src/modules/shared.module.ts index f3cf033d..fa244f60 100644 --- a/src/modules/shared.module.ts +++ b/src/modules/shared.module.ts @@ -34,6 +34,7 @@ import { ControlledProperty } from "@entities/controlled-property.entity"; import { ApplicationDeviceType } from "@entities/application-device-type.entity"; import { ReceivedMessageSigFoxSignals } from "@entities/received-message-sigfox-signals.entity"; import { MqttDataTarget } from "@entities/mqtt-data-target.entity"; +import { GatewayStatusHistory } from "@entities/gateway-status-history.entity"; @Module({ imports: [ @@ -70,6 +71,7 @@ import { MqttDataTarget } from "@entities/mqtt-data-target.entity"; ApplicationDeviceType, ApiKey, ReceivedMessageSigFoxSignals, + GatewayStatusHistory, ]), ], providers: [AuditLog], diff --git a/src/resources/resource-paths.ts b/src/resources/resource-paths.ts new file mode 100644 index 00000000..430bc93b --- /dev/null +++ b/src/resources/resource-paths.ts @@ -0,0 +1,8 @@ +import { join } from "path"; + +const goToRootFolder = "../../"; + +export const ChirpstackStateTemplatePath = join( + __dirname, + `${goToRootFolder}resources/chirpstack-state.proto` +); diff --git a/src/services/chirpstack/chirpstack-gateway.service.ts b/src/services/chirpstack/chirpstack-gateway.service.ts index f354a107..b1be303e 100644 --- a/src/services/chirpstack/chirpstack-gateway.service.ts +++ b/src/services/chirpstack/chirpstack-gateway.service.ts @@ -83,7 +83,7 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ const limit = 1000; let allResults: GatewayResponseDto[] = []; let totalCount = 0; - let lastResults; + let lastResults: ListAllGatewaysResponseDto; do { // Default parameters if not set lastResults = await this.getAllWithPagination( @@ -112,6 +112,11 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ }; } + /** + * Fetch gateways individually. This gives us the tags which contain the OS2 organization id. + * This is a very expensive operation, but it's the only way to retrieve gateway tags. + * @param results + */ private async enrichWithOrganizationId(results: GatewayResponseDto[]) { await BluebirdPromise.all( BluebirdPromise.map( diff --git a/src/services/chirpstack/gateway-status-history.service.ts b/src/services/chirpstack/gateway-status-history.service.ts new file mode 100644 index 00000000..8f1b1e4c --- /dev/null +++ b/src/services/chirpstack/gateway-status-history.service.ts @@ -0,0 +1,103 @@ +import { + GatewayGetAllStatusResponseDto, + ListAllGatewayStatusDto, +} from "@dto/chirpstack/backend/gateway-all-status.dto"; +import { GatewayStatus } from "@dto/chirpstack/backend/gateway-status.dto"; +import { GatewayStatusHistory } from "@entities/gateway-status-history.entity"; +import { + GatewayStatusInterval, + gatewayStatusIntervalToDate, +} from "@enum/gateway-status-interval.enum"; +import { Injectable, Logger } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { In, MoreThanOrEqual, Repository } from "typeorm"; +import { ChirpstackGatewayService } from "./chirpstack-gateway.service"; + +type GatewayId = { id: string; name: string }; + +@Injectable() +export class GatewayStatusHistoryService { + constructor( + @InjectRepository(GatewayStatusHistory) + private gatewayStatusHistoryRepository: Repository, + private chirpstackGatewayService: ChirpstackGatewayService + ) {} + private readonly logger = new Logger(GatewayStatusHistoryService.name); + + public async findAllWithChirpstack( + query: ListAllGatewayStatusDto + ): Promise { + // Very expensive operation. Since no gateway data is stored on the backend database, we need + // to get them from Chirpstack. There's no filter by tags support so we must fetch all gateways. + const gateways = await this.chirpstackGatewayService.getAll(query.organizationId); + const gatewayIds = gateways.result.map(gateway => gateway.id); + const fromDate = gatewayStatusIntervalToDate(query.timeInterval); + + const statusHistories = await this.gatewayStatusHistoryRepository.find({ + where: { + mac: In(gatewayIds), + timestamp: MoreThanOrEqual(fromDate), + }, + }); + + const data: GatewayStatus[] = this.mapStatusHistoryToGateways( + gateways.result, + statusHistories + ); + + return { + data, + count: gateways.totalCount, + }; + } + + public async findOne( + gateway: Gateway, + timeInterval: GatewayStatusInterval + ): Promise { + const fromDate = gatewayStatusIntervalToDate(timeInterval); + + const statusHistories = await this.gatewayStatusHistoryRepository.find({ + where: { + mac: gateway.id, + timestamp: MoreThanOrEqual(fromDate), + }, + }); + + return this.mapStatusHistoryToGateway(gateway, statusHistories); + } + + private mapStatusHistoryToGateways( + gateways: Gateway[], + statusHistories: GatewayStatusHistory[] + ): GatewayStatus[] { + return gateways.map(gateway => { + return this.mapStatusHistoryToGateway(gateway, statusHistories); + }); + } + + private mapStatusHistoryToGateway( + gateway: Gateway, + statusHistories: GatewayStatusHistory[] + ) { + const statusTimestamps = statusHistories.reduce( + (res: GatewayStatus["statusTimestamps"], history) => { + if (history.mac === gateway.id) { + res.push({ + timestamp: history.timestamp, + wasOnline: history.wasOnline, + }); + } + + return res; + }, + [] + ); + + return { + id: gateway.id, + name: gateway.name, + statusTimestamps, + }; + } +} diff --git a/src/services/data-management/chirpstack-mqtt-listener.service.ts b/src/services/data-management/chirpstack-mqtt-listener.service.ts index 49c9c75c..b04147c0 100644 --- a/src/services/data-management/chirpstack-mqtt-listener.service.ts +++ b/src/services/data-management/chirpstack-mqtt-listener.service.ts @@ -1,43 +1,74 @@ import { MqttClientId } from "@config/constants/mqtt-constants"; -import { ChirpstackMQTTMessageDto } from "@dto/chirpstack/chirpstack-mqtt-message.dto"; +import { + ChirpstackMQTTConnectionStateMessageDto, + ChirpstackMQTTMessageDto, +} from "@dto/chirpstack/chirpstack-mqtt-message.dto"; +import { ChirpstackMQTTConnectionStateMessage } from "@dto/chirpstack/state/chirpstack-mqtt-state-message.dto"; import { IoTDeviceType } from "@enum/device-type.enum"; +import { hasProps, nameof } from "@helpers/type-helper"; import { Injectable, Logger, OnApplicationBootstrap } from "@nestjs/common"; +import { ChirpstackStateTemplatePath } from "@resources/resource-paths"; import { ReceiveDataService } from "@services/data-management/receive-data.service"; import { IoTDeviceService } from "@services/device-management/iot-device.service"; import * as mqtt from "mqtt"; import { Client } from "mqtt"; +import * as Protobuf from "protobufjs"; @Injectable() export class ChirpstackMQTTListenerService implements OnApplicationBootstrap { constructor( private receiveDataService: ReceiveDataService, private iotDeviceService: IoTDeviceService - ) {} + ) { + const connStateFullTemplate = Protobuf.loadSync(ChirpstackStateTemplatePath); + this.connStateType = connStateFullTemplate.lookupType("ConnState"); + } private readonly logger = new Logger(ChirpstackMQTTListenerService.name); + private readonly connStateType: Protobuf.Type; MQTT_URL = `mqtt://${process.env.CS_MQTT_HOSTNAME || "localhost"}:${ process.env.CS_MQTT_PORT || "1883" }`; client: Client; + private readonly CHIRPSTACK_MQTT_DEVICE_DATA_PREFIX = "application/"; private readonly CHIRPSTACK_MQTT_DEVICE_DATA_TOPIC = - "application/+/device/+/event/up"; + this.CHIRPSTACK_MQTT_DEVICE_DATA_PREFIX + "+/device/+/event/up"; + private readonly CHIRPSTACK_MQTT_GATEWAY_PREFIX = "gateway/"; + private readonly CHIRPSTACK_MQTT_GATEWAY_TOPIC = + this.CHIRPSTACK_MQTT_GATEWAY_PREFIX + "+/state/conn"; public async onApplicationBootstrap(): Promise { this.logger.debug("Pre-init"); + this.client = mqtt.connect(this.MQTT_URL, { clean: true, clientId: MqttClientId, }); this.client.on("connect", () => { this.client.subscribe(this.CHIRPSTACK_MQTT_DEVICE_DATA_TOPIC); + this.client.subscribe(this.CHIRPSTACK_MQTT_GATEWAY_TOPIC); this.client.on("message", async (topic, message) => { this.logger.debug( `Received MQTT - Topic: '${topic}' - message: '${message}'` ); - await this.receiveMqttMessage(message.toString()); + + if (topic.startsWith(this.CHIRPSTACK_MQTT_DEVICE_DATA_PREFIX)) { + await this.receiveMqttMessage(message.toString()); + } else if (topic.startsWith(this.CHIRPSTACK_MQTT_GATEWAY_PREFIX)) { + try { + const decoded = this.connStateType.decode(message); + await this.receiveMqttGatewayStatusMessage(decoded.toJSON()); + } catch (error) { + this.logger.error( + `Gateway status data could not be processed. Error: ${error}` + ); + } + } else { + this.logger.warn("Unrecognized MQTT topic " + topic); + } }); this.logger.debug("Connected to MQTT."); }); @@ -56,10 +87,38 @@ export class ChirpstackMQTTListenerService implements OnApplicationBootstrap { return; } - await this.receiveDataService.sendToKafka( + await this.receiveDataService.sendRawIotDeviceRequestToKafka( iotDevice, message, IoTDeviceType.LoRaWAN.toString() ); } + + async receiveMqttGatewayStatusMessage( + message: Record + ): Promise { + if ( + message && + hasProps( + message, + nameof("gatewayId") + ) && + typeof message.gatewayId === "string" + ) { + const dto: ChirpstackMQTTConnectionStateMessageDto = { + gatewayId: Buffer.from(message.gatewayId, "base64").toString("hex"), + isOnline: message.state === "ONLINE", + }; + const jsonDto = JSON.stringify(dto); + + await this.receiveDataService.sendRawGatewayStateToKafka( + dto.gatewayId, + jsonDto + ); + } else { + this.logger.error( + `Gateway status message is not properly formatted. Gateway id, if any, is ${message?.id}` + ); + } + } } diff --git a/src/services/data-management/device-integration-persistence.service.ts b/src/services/data-management/device-integration-persistence.service.ts index 2150e6d7..9740e292 100644 --- a/src/services/data-management/device-integration-persistence.service.ts +++ b/src/services/data-management/device-integration-persistence.service.ts @@ -1,4 +1,4 @@ -import { RawRequestDto } from "@dto/kafka/raw-request.dto"; +import { RawIoTDeviceRequestDto } from "@dto/kafka/raw-iot-device-request.dto"; import { IoTDevice } from "@entities/iot-device.entity"; import { ReceivedMessageMetadata } from "@entities/received-message-metadata.entity"; import { ReceivedMessageSigFoxSignals } from "@entities/received-message-sigfox-signals.entity"; @@ -16,7 +16,7 @@ import { IoTDeviceService } from "@services/device-management/iot-device.service import { AbstractKafkaConsumer } from "@services/kafka/kafka.abstract.consumer"; import { CombinedSubscribeTo } from "@services/kafka/kafka.decorator"; import { KafkaPayload } from "@services/kafka/kafka.message"; -import { MoreThan, Repository, LessThan } from "typeorm"; +import { LessThan, MoreThan, Repository } from "typeorm"; @Injectable() export class DeviceIntegrationPersistenceService extends AbstractKafkaConsumer { @@ -50,7 +50,7 @@ export class DeviceIntegrationPersistenceService extends AbstractKafkaConsumer { @CombinedSubscribeTo(KafkaTopic.RAW_REQUEST, "DeviceIntegrationPersistence") async rawRequestListener(payload: KafkaPayload): Promise { this.logger.debug(`RAW_REQUEST: '${JSON.stringify(payload)}'`); - const dto: RawRequestDto = payload.body; + const dto = payload.body as RawIoTDeviceRequestDto; let relatedIoTDevice; try { relatedIoTDevice = await this.ioTDeviceService.findOne(dto.iotDeviceId); @@ -77,7 +77,7 @@ export class DeviceIntegrationPersistenceService extends AbstractKafkaConsumer { } private async saveLatestMessage( - dto: RawRequestDto, + dto: RawIoTDeviceRequestDto, relatedIoTDevice: IoTDevice ): Promise { let existingMessage = await this.findExistingRecevedMessage(relatedIoTDevice); @@ -115,7 +115,7 @@ export class DeviceIntegrationPersistenceService extends AbstractKafkaConsumer { } mapDtoToReceivedMessage( - dto: RawRequestDto, + dto: RawIoTDeviceRequestDto, existingMessage: ReceivedMessage, relatedIoTDevice: IoTDevice ): ReceivedMessage { @@ -204,7 +204,7 @@ export class DeviceIntegrationPersistenceService extends AbstractKafkaConsumer { } private async saveMessageMetadata( - dto: RawRequestDto, + dto: RawIoTDeviceRequestDto, relatedIoTDevice: IoTDevice ): Promise { // Save this @@ -267,7 +267,7 @@ export class DeviceIntegrationPersistenceService extends AbstractKafkaConsumer { } mapDtoToNewReceivedMessageMetadata( - dto: RawRequestDto, + dto: RawIoTDeviceRequestDto, relatedIoTDevice: IoTDevice ): ReceivedMessageMetadata { const newMetadata = new ReceivedMessageMetadata(); @@ -306,7 +306,7 @@ export class DeviceIntegrationPersistenceService extends AbstractKafkaConsumer { if (oldestToDelete.length === 0) { this.logger.debug( - `Less than ${this.maxSigFoxSignalsMessagesPerHour} SigFox stat objects for device ${deviceId} found in database. Deleting no rows.` + `Less than ${this.maxSigFoxSignalsMessagesPerHour} SigFox stat objects for device ${deviceId} found in database. Deleting no rows.` ); return; } @@ -320,7 +320,7 @@ export class DeviceIntegrationPersistenceService extends AbstractKafkaConsumer { ); } - /** + /** * Clean up SigFox stats for the device if they are older than 1 year * @param deviceId */ diff --git a/src/services/data-management/gateway-persistence.service.ts b/src/services/data-management/gateway-persistence.service.ts new file mode 100644 index 00000000..bd9ca523 --- /dev/null +++ b/src/services/data-management/gateway-persistence.service.ts @@ -0,0 +1,124 @@ +import { ChirpstackMQTTConnectionStateMessageDto } from "@dto/chirpstack/chirpstack-mqtt-message.dto"; +import { RawGatewayStateDto } from "@dto/kafka/raw-gateway-state.dto"; +import { GatewayStatusHistory } from "@entities/gateway-status-history.entity"; +import { KafkaTopic } from "@enum/kafka-topic.enum"; +import { subtractDays, subtractHours } from "@helpers/date.helper"; +import { Injectable, Logger } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { AbstractKafkaConsumer } from "@services/kafka/kafka.abstract.consumer"; +import { CombinedSubscribeTo } from "@services/kafka/kafka.decorator"; +import { KafkaPayload } from "@services/kafka/kafka.message"; +import { LessThan, MoreThan, Repository } from "typeorm"; + +@Injectable() +export class GatewayPersistenceService extends AbstractKafkaConsumer { + private readonly gatewayStatusSavedDays = 30; + /** + * Limit how many messages can be stored for each hour + */ + private readonly maxStatusMessagesPerHour = 10; + + constructor( + @InjectRepository(GatewayStatusHistory) + private gatewayStatusHistoryRepository: Repository + ) { + super(); + } + + private readonly logger = new Logger(GatewayPersistenceService.name); + + protected registerTopic(): void { + this.addTopic(KafkaTopic.RAW_GATEWAY_STATE, "GatewayPersistence"); + } + + // Listen to Kafka event + @CombinedSubscribeTo(KafkaTopic.RAW_GATEWAY_STATE, "GatewayPersistence") + async rawRequestListener(payload: KafkaPayload): Promise { + this.logger.debug(`RAW_GATEWAY_STATE: '${JSON.stringify(payload)}'`); + const dto = payload.body as RawGatewayStateDto; + const messageState = (dto.rawPayload as unknown) as ChirpstackMQTTConnectionStateMessageDto; + + const statusHistory = this.mapDtoToEntity(dto, messageState); + await this.gatewayStatusHistoryRepository.save(statusHistory); + + // Clean up old statuses + await this.deleteStatusHistoriesSinceLastHour( + statusHistory.timestamp, + dto.gatewayId + ); + await this.deleteOldStatusHistories(dto.gatewayId); + } + + private mapDtoToEntity( + dto: RawGatewayStateDto, + messageState: ChirpstackMQTTConnectionStateMessageDto + ) { + const statusHistory = new GatewayStatusHistory(); + statusHistory.mac = dto.gatewayId; + statusHistory.timestamp = dto.unixTimestamp + ? new Date(dto.unixTimestamp) + : new Date(); + statusHistory.wasOnline = !!messageState?.isOnline; + return statusHistory; + } + + /** + * Make sure we never have histories for more than X messages per gateway per hour + * to avoid filling the database + * @param latestMessageTime + * @param gatewayId + */ + private async deleteStatusHistoriesSinceLastHour( + latestMessageTime: Date, + gatewayId: string + ): Promise { + const lastHour = subtractHours(latestMessageTime); + // Find the oldest items since the last hour + const oldestToDelete = await this.gatewayStatusHistoryRepository.find({ + where: { mac: gatewayId, timestamp: MoreThan(lastHour) }, + skip: this.maxStatusMessagesPerHour, + order: { + timestamp: "DESC", + }, + }); + + if (oldestToDelete.length === 0) { + this.logger.debug( + `Less than ${this.maxStatusMessagesPerHour} gateway status' for gateway ${gatewayId} found in database. Deleting no rows.` + ); + return; + } + + const result = await this.gatewayStatusHistoryRepository.delete( + oldestToDelete.map(old => old.id) + ); + + this.logger.debug(`Deleted: ${result.affected} rows from gateway_status_history`); + } + + /** + * Clean up data if it's older than a specified time period + * @param deviceId + */ + private async deleteOldStatusHistories(gatewayId: string): Promise { + const minDate = subtractDays(new Date(), this.gatewayStatusSavedDays); + // Find messages older than a date and delete them + const oldestToDelete = await this.gatewayStatusHistoryRepository.find({ + where: [ + { mac: gatewayId, timestamp: LessThan(minDate) }, + { mac: gatewayId, updatedAt: LessThan(minDate) }, + ], + }); + + if (oldestToDelete.length === 0) { + this.logger.debug("There's no old gateway status messages"); + return; + } + + const result = await this.gatewayStatusHistoryRepository.delete( + oldestToDelete.map(old => old.id) + ); + + this.logger.debug(`Deleted: ${result.affected} rows from gateway_status_history`); + } +} diff --git a/src/services/data-management/payload-decoder-listener.service.ts b/src/services/data-management/payload-decoder-listener.service.ts index 746a4ae2..a76b1453 100644 --- a/src/services/data-management/payload-decoder-listener.service.ts +++ b/src/services/data-management/payload-decoder-listener.service.ts @@ -1,20 +1,17 @@ -import { Injectable, Logger } from "@nestjs/common"; -import { RecordMetadata } from "kafkajs"; -import { VM, VMScript } from "vm2"; - -import { RawRequestDto } from "@dto/kafka/raw-request.dto"; +import { RawIoTDeviceRequestDto } from "@dto/kafka/raw-iot-device-request.dto"; import { TransformedPayloadDto } from "@dto/kafka/transformed-payload.dto"; +import { ListAllConnectionsResponseDto } from "@dto/list-all-connections-response.dto"; import { IoTDevice } from "@entities/iot-device.entity"; import { PayloadDecoder } from "@entities/payload-decoder.entity"; import { KafkaTopic } from "@enum/kafka-topic.enum"; +import { Injectable, Logger } from "@nestjs/common"; import { IoTDevicePayloadDecoderDataTargetConnectionService } from "@services/device-management/iot-device-payload-decoder-data-target-connection.service"; import { AbstractKafkaConsumer } from "@services/kafka/kafka.abstract.consumer"; import { CombinedSubscribeTo } from "@services/kafka/kafka.decorator"; import { KafkaPayload } from "@services/kafka/kafka.message"; - -import { KafkaService } from "../kafka/kafka.service"; +import { RecordMetadata } from "kafkajs"; import * as _ from "lodash"; -import { ListAllConnectionsResponseDto } from "@dto/list-all-connections-response.dto"; +import { KafkaService } from "../kafka/kafka.service"; import { PayloadDecoderExecutorService } from "./payload-decoder-executor.service"; @Injectable() @@ -38,7 +35,7 @@ export class PayloadDecoderListenerService extends AbstractKafkaConsumer { this.logger.debug(`RAW_REQUEST: '${JSON.stringify(payload)}'`); // Fetch related objects - const dto: RawRequestDto = payload.body; + const dto = payload.body as RawIoTDeviceRequestDto; const connections = await this.connectionService.findAllByIoTDeviceIdWithDeviceModel( dto.iotDeviceId ); @@ -52,7 +49,7 @@ export class PayloadDecoderListenerService extends AbstractKafkaConsumer { private async doTransformationsAndSend( connections: ListAllConnectionsResponseDto, - dto: RawRequestDto + dto: RawIoTDeviceRequestDto ) { const uniqueCombinations = _.uniqBy(connections.data, x => x.payloadDecoder?.id); uniqueCombinations.forEach(async connection => { diff --git a/src/services/data-management/receive-data.service.ts b/src/services/data-management/receive-data.service.ts index 48879913..f0a8a07a 100644 --- a/src/services/data-management/receive-data.service.ts +++ b/src/services/data-management/receive-data.service.ts @@ -1,44 +1,78 @@ -import { Injectable, Logger } from "@nestjs/common"; -import { RecordMetadata } from "kafkajs"; - +import { RawGatewayStateDto } from "@dto/kafka/raw-gateway-state.dto"; +import { RawIoTDeviceRequestDto } from "@dto/kafka/raw-iot-device-request.dto"; import { RawRequestDto } from "@dto/kafka/raw-request.dto"; import { KafkaTopic } from "@entities/enum/kafka-topic.enum"; import { IoTDevice } from "@entities/iot-device.entity"; +import { IoTDeviceType } from "@enum/device-type.enum"; +import { Injectable, Logger } from "@nestjs/common"; import { KafkaPayload } from "@services/kafka/kafka.message"; import { KafkaService } from "@services/kafka/kafka.service"; -import { IoTDeviceType } from "@enum/device-type.enum"; +import { RecordMetadata } from "kafkajs"; @Injectable() export class ReceiveDataService { constructor(private kafkaService: KafkaService) {} private readonly logger = new Logger(ReceiveDataService.name); - async sendToKafka( + async sendRawIotDeviceRequestToKafka( iotDevice: IoTDevice, data: string, type: IoTDeviceType[number], timestamp?: number ): Promise { - this.logger.debug(`Received data, sending to Kafka`); - const dto = new RawRequestDto(); + const dto = new RawIoTDeviceRequestDto(); dto.iotDeviceId = iotDevice.id; dto.rawPayload = JSON.parse(data); + const payload = this.buildMessage(dto, type, KafkaTopic.RAW_REQUEST, timestamp); + + await this.doSendToKafka(payload, KafkaTopic.RAW_REQUEST); + } + + async sendRawGatewayStateToKafka( + gatewayId: string, + data: string, + timestamp?: number + ): Promise { + const dto = new RawGatewayStateDto(); + dto.gatewayId = gatewayId; + dto.rawPayload = JSON.parse(data); + const payload = this.buildMessage( + dto, + "GATEWAY", + KafkaTopic.RAW_GATEWAY_STATE, + timestamp + ); + + await this.doSendToKafka(payload, KafkaTopic.RAW_GATEWAY_STATE); + } + + private buildMessage( + dto: RawRequestDto, + type: string, + topicName: KafkaTopic, + timestamp?: number + ): KafkaPayload { + this.logger.debug(`Received data, sending to Kafka`); dto.type = type; // We cannot generically know when it was sent by the device, "now" is accurate enough - dto.unixTimestamp = timestamp != null ? timestamp : new Date().valueOf(); + dto.unixTimestamp = + timestamp !== null && timestamp !== undefined + ? timestamp + : new Date().valueOf(); const payload: KafkaPayload = { messageId: `${type}${new Date().valueOf()}`, body: dto, messageType: `receiveData.${type}`, - topicName: KafkaTopic.RAW_REQUEST, + topicName, }; this.logger.debug(`Made payload: '${JSON.stringify(payload)}'`); - const rawStatus = await this.kafkaService.sendMessage( - KafkaTopic.RAW_REQUEST, - payload - ); + return payload; + } + + private async doSendToKafka(payload: KafkaPayload, topic: KafkaTopic) { + const rawStatus = await this.kafkaService.sendMessage(topic, payload); this.logger.debug(`Sent message to Kafka: ${JSON.stringify(rawStatus)}`); diff --git a/src/services/data-targets/data-target-kafka-listener.service.ts b/src/services/data-targets/data-target-kafka-listener.service.ts index b9a2b137..8363d33e 100644 --- a/src/services/data-targets/data-target-kafka-listener.service.ts +++ b/src/services/data-targets/data-target-kafka-listener.service.ts @@ -39,7 +39,7 @@ export class DataTargetKafkaListenerService extends AbstractKafkaConsumer { async transformedRequestListener(payload: KafkaPayload): Promise { this.logger.debug(`TRANSFORMED_REQUEST: '${JSON.stringify(payload)}'`); - const dto: TransformedPayloadDto = payload.body; + const dto = payload.body as TransformedPayloadDto; let iotDevice: IoTDevice; try { iotDevice = await this.ioTDeviceService.findOne(dto.iotDeviceId); diff --git a/src/services/kafka/kafka.message.ts b/src/services/kafka/kafka.message.ts index 0e5e2b3e..074073cc 100644 --- a/src/services/kafka/kafka.message.ts +++ b/src/services/kafka/kafka.message.ts @@ -1,5 +1,5 @@ export class KafkaPayload { - public body: any; + public body: unknown; public messageId: string; public messageType: string; public topicName: string; From b0fc6e706de56b1f4d9b1eba61761828f6d27d3e Mon Sep 17 00:00:00 2001 From: Aram Al-Sabti Date: Wed, 18 May 2022 17:25:51 +0200 Subject: [PATCH 15/19] Update accept kombit to use group instead of level --- .../user-management/permission.controller.ts | 27 ++++++++++--------- .../add-user-to-permission.dto.ts | 18 +++++-------- src/services/user-management/user.service.ts | 10 ++++--- 3 files changed, 28 insertions(+), 27 deletions(-) diff --git a/src/controllers/user-management/permission.controller.ts b/src/controllers/user-management/permission.controller.ts index 84bd12fc..291a0507 100644 --- a/src/controllers/user-management/permission.controller.ts +++ b/src/controllers/user-management/permission.controller.ts @@ -104,26 +104,30 @@ export class PermissionController { @Body() dto: PermissionRequestAcceptUser ): Promise { try { - checkIfUserHasAccessToOrganization(req, dto.organizationId, OrganizationAccessScope.UserAdministrationWrite); - let dbPermission: Permission; + checkIfUserHasAccessToOrganization( + req, + dto.organizationId, + OrganizationAccessScope.UserAdministrationWrite + ); const permissions = await this.permissionService.findOneWithRelations( dto.organizationId - ); + ); const org: Organization = this.organizationService.mapPermissionsToOneOrganization( permissions ); const user: User = await this.userService.findOne(dto.userId); - for (let index = 0; index < org.permissions.length; index++) { - if (org.permissions[index].type.some(({ type }) => type === dto.level)) { - dbPermission = await this.permissionService.getPermission( - org.permissions[index].id - ); + const newUserPermissions: Permission[] = []; + + for (const orgPermission of org.permissions) { + if (dto.permissionIds.includes(orgPermission.id)) { + newUserPermissions.push(orgPermission); } } - const resultUser = await this.userService.acceptUser(user, org, dbPermission); + + const resultUser = await this.userService.acceptUser(user, org, newUserPermissions); AuditLog.success( ActionType.UPDATE, @@ -133,6 +137,7 @@ export class PermissionController { resultUser.name ); return resultUser; + } catch (err) { AuditLog.fail(ActionType.UPDATE, Permission.name, req.user.userId); throw err; @@ -188,9 +193,7 @@ export class PermissionController { ): Promise { try { const permission = await this.permissionService.getPermission(id); - if ( - permission.type.some(({ type }) => type === PermissionType.GlobalAdmin) - ) { + if (permission.type.some(({ type }) => type === PermissionType.GlobalAdmin)) { throw new BadRequestException("You cannot delete GlobalAdmin"); } else { checkIfUserHasAccessToOrganization( diff --git a/src/entities/dto/user-management/add-user-to-permission.dto.ts b/src/entities/dto/user-management/add-user-to-permission.dto.ts index 65cac1cb..0b084aa9 100644 --- a/src/entities/dto/user-management/add-user-to-permission.dto.ts +++ b/src/entities/dto/user-management/add-user-to-permission.dto.ts @@ -1,10 +1,5 @@ -import { PermissionType } from "@enum/permission-type.enum"; import { ApiProperty } from "@nestjs/swagger"; -import { IsEnum, IsNumber } from "class-validator"; -import { omit } from "lodash"; - -const globalAdminEnumName: keyof typeof PermissionType = 'GlobalAdmin'; -const acceptablePermissionType = omit(PermissionType, globalAdminEnumName); +import { IsNumber, IsArray, ArrayUnique, ArrayNotEmpty } from "class-validator"; export class PermissionRequestAcceptUser { @ApiProperty({ required: true }) @@ -15,10 +10,9 @@ export class PermissionRequestAcceptUser { @IsNumber() userId: number; - @ApiProperty({ - required: true, - enum: acceptablePermissionType, - }) - @IsEnum(acceptablePermissionType) - level: keyof typeof acceptablePermissionType; + @ApiProperty({ required: true }) + @IsArray() + @ArrayNotEmpty() + @ArrayUnique() + permissionIds: number[]; } diff --git a/src/services/user-management/user.service.ts b/src/services/user-management/user.service.ts index 89ecf688..7237f4f5 100644 --- a/src/services/user-management/user.service.ts +++ b/src/services/user-management/user.service.ts @@ -52,18 +52,22 @@ export class UserService { async acceptUser( user: User, org: Organization, - dbPermission: Permission + newUserPermissions: Permission[] ): Promise { user.awaitingConfirmation = false; - if (user.permissions.find(perms => perms.id === dbPermission.id)) { + if ( + user.permissions.find(perms => + newUserPermissions.some(newPerm => newPerm.id === perms.id) + ) + ) { throw new BadRequestException(ErrorCodes.UserAlreadyInPermission); } else { const index = user.requestedOrganizations.findIndex( dbOrg => dbOrg.id === org.id ); user.requestedOrganizations.splice(index, 1); - user.permissions.push(dbPermission); + user.permissions.push(...newUserPermissions); await this.sendVerificationMail(user, org); return await this.userRepository.save(user); } From caabf3c8b61ed53b321238bbe0e7fb01fafbc707 Mon Sep 17 00:00:00 2001 From: Aram Al-Sabti Date: Wed, 18 May 2022 17:41:38 +0200 Subject: [PATCH 16/19] Remove permission check from gateway status --- .../lorawan/lorawan-gateway.controller.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/controllers/admin-controller/lorawan/lorawan-gateway.controller.ts b/src/controllers/admin-controller/lorawan/lorawan-gateway.controller.ts index 8d518019..c5ca309a 100644 --- a/src/controllers/admin-controller/lorawan/lorawan-gateway.controller.ts +++ b/src/controllers/admin-controller/lorawan/lorawan-gateway.controller.ts @@ -7,17 +7,16 @@ import { } from "@dto/chirpstack/backend/gateway-all-status.dto"; import { GatewayStatus, GetGatewayStatusQuery } from "@dto/chirpstack/backend/gateway-status.dto"; import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; -import { checkIfUserHasReadAccessToOrganization } from "@helpers/security-helper"; import { Controller, Get, Param, Query, Req, UseGuards } from "@nestjs/common"; import { ApiBearerAuth, ApiOperation, ApiProduces, ApiTags } from "@nestjs/swagger"; import { ChirpstackGatewayService } from "@services/chirpstack/chirpstack-gateway.service"; import { GatewayStatusHistoryService } from "@services/chirpstack/gateway-status-history.service"; +import { checkIfUserHasAccessToOrganization, OrganizationAccessScope } from "@helpers/security-helper"; @ApiTags("LoRaWAN gateway") @Controller("lorawan/gateway") @UseGuards(ComposeAuthGuard, RolesGuard) @ApiBearerAuth() -@Read() export class LoRaWANGatewayController { constructor( private onlineHistoryService: GatewayStatusHistoryService, @@ -32,11 +31,7 @@ export class LoRaWANGatewayController { @Req() req: AuthenticatedRequest, @Query() query: ListAllGatewayStatusDto ): Promise { - if (query.organizationId) { - // TODO: NEW USER MANAGEMENT: Update the rights once it's merged - checkIfUserHasReadAccessToOrganization(req, query.organizationId); - } - + // Currently, everyone is allowed to get the status return this.onlineHistoryService.findAllWithChirpstack(query); } @@ -48,9 +43,8 @@ export class LoRaWANGatewayController { @Param("id") id: string, @Query() query: GetGatewayStatusQuery ): Promise { + // Currently, everyone is allowed to get the status const gatewayDto = await this.chirpstackGatewayService.getOne(id); - checkIfUserHasReadAccessToOrganization(req, gatewayDto.gateway.internalOrganizationId); - return this.onlineHistoryService.findOne(gatewayDto.gateway, query.timeInterval); } } From ccf13f3ae0806b6df304e014c83124c7670cc521 Mon Sep 17 00:00:00 2001 From: Aram Al-Sabti Date: Thu, 19 May 2022 10:55:25 +0200 Subject: [PATCH 17/19] Cleanup permission relations on down --- .../1651142158492-revised-permissions.ts | 53 ++++++++++--------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/src/migration/1651142158492-revised-permissions.ts b/src/migration/1651142158492-revised-permissions.ts index d14fd290..51adc0e8 100644 --- a/src/migration/1651142158492-revised-permissions.ts +++ b/src/migration/1651142158492-revised-permissions.ts @@ -149,30 +149,7 @@ export class revisedPermissions1651142158492 implements MigrationInterface { // Cleanup await queryRunner.query(`ALTER TABLE "permission" DROP COLUMN "clonedFromId"`); - - await queryRunner.query(`DELETE FROM user_permissions_permission -WHERE "permissionId" IN -( - SELECT "permission"."id" FROM user_permissions_permission - JOIN permission ON permission.id = "public"."user_permissions_permission"."permissionId" - WHERE permission.type IN ('Write', 'OrganizationAdmin') -);`); - - await queryRunner.query(`DELETE FROM application_permissions_permission -WHERE "permissionId" IN -( - SELECT "permission"."id" FROM application_permissions_permission - JOIN permission ON permission.id = "public"."application_permissions_permission"."permissionId" - WHERE permission.type IN ('Write', 'OrganizationAdmin') -)`); - - await queryRunner.query(`DELETE FROM api_key_permissions_permission -WHERE "permissionId" IN -( - SELECT "permission"."id" FROM api_key_permissions_permission - JOIN permission ON permission.id = "public"."api_key_permissions_permission"."permissionId" - WHERE permission.type IN ('Write', 'OrganizationAdmin') -)`); + await this.cleanupPermissionRelations(queryRunner, "'Write', 'OrganizationAdmin'"); await queryRunner.query( `DELETE FROM "public"."permission" where type IN ('OrganizationAdmin', 'Write')` @@ -400,6 +377,8 @@ returning id, "permission"."clonedFromId"`; // Cleanup await queryRunner.query(`ALTER TABLE "permission" DROP COLUMN "clonedFromId"`); + await this.cleanupPermissionRelations(queryRunner, "'OrganizationUserAdmin', 'OrganizationGatewayAdmin', 'OrganizationApplicationAdmin'"); + await queryRunner.query( `DELETE FROM "public"."permission" where type IN ('OrganizationUserAdmin', 'OrganizationGatewayAdmin', 'OrganizationApplicationAdmin')` ); @@ -456,4 +435,30 @@ returning id, "permission"."clonedFromId"`; await queryRunner.query(`DROP TYPE "public"."permission_type_type_enum"`); await queryRunner.query(`CREATE INDEX "IDX_71bf2818fb2ad92e208d7aeadf" ON "permission" ("type") `); } + + private async cleanupPermissionRelations(queryRunner: QueryRunner, permissionTypesToRemove: string) { + await queryRunner.query(`DELETE FROM user_permissions_permission +WHERE "permissionId" IN +( + SELECT "permission"."id" FROM user_permissions_permission + JOIN permission ON permission.id = "public"."user_permissions_permission"."permissionId" + WHERE permission.type IN (${permissionTypesToRemove}) +);`); + + await queryRunner.query(`DELETE FROM application_permissions_permission +WHERE "permissionId" IN +( + SELECT "permission"."id" FROM application_permissions_permission + JOIN permission ON permission.id = "public"."application_permissions_permission"."permissionId" + WHERE permission.type IN (${permissionTypesToRemove}) +)`); + + await queryRunner.query(`DELETE FROM api_key_permissions_permission +WHERE "permissionId" IN +( + SELECT "permission"."id" FROM api_key_permissions_permission + JOIN permission ON permission.id = "public"."api_key_permissions_permission"."permissionId" + WHERE permission.type IN (${permissionTypesToRemove}) +)`); + } } From a0edef98987c973c615b0278078d3167230f4d0a Mon Sep 17 00:00:00 2001 From: Aram Al-Sabti Date: Thu, 19 May 2022 11:56:06 +0200 Subject: [PATCH 18/19] Fix multiple permission relations not mapped to the same new permissions --- ...s => 1652951529000-revised-permissions.ts} | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) rename src/migration/{1651142158492-revised-permissions.ts => 1652951529000-revised-permissions.ts} (94%) diff --git a/src/migration/1651142158492-revised-permissions.ts b/src/migration/1652951529000-revised-permissions.ts similarity index 94% rename from src/migration/1651142158492-revised-permissions.ts rename to src/migration/1652951529000-revised-permissions.ts index 51adc0e8..27759193 100644 --- a/src/migration/1651142158492-revised-permissions.ts +++ b/src/migration/1652951529000-revised-permissions.ts @@ -16,15 +16,15 @@ type PermissionInfo = { }; type UserPermissionInfo = PermissionInfo & { - userId?: number; + userIds?: number[]; }; type AppPermissionInfo = PermissionInfo & { - applicationId?: number; + applicationIds?: number[]; }; type ApiKeyPermissionInfo = PermissionInfo & { - apiKeyId?: number; + apiKeyIds?: number[]; } type UserPermissions = { @@ -38,8 +38,8 @@ type UserPermissions = { const permissionTypeUnionName = "permission_type_enum_temp"; const createPermissionTypeUnionSql = `CREATE TYPE "${permissionTypeUnionName}" AS ENUM('OrganizationAdmin', 'Write', 'GlobalAdmin', 'OrganizationUserAdmin', 'OrganizationGatewayAdmin', 'OrganizationApplicationAdmin', 'Read', 'OrganizationPermission', 'OrganizationApplicationPermissions', 'ApiKeyPermission')`; -export class revisedPermissions1651142158492 implements MigrationInterface { - name = "revisedPermissions1651142158492"; +export class revisedPermissions1652951529000 implements MigrationInterface { + name = "revisedPermissions1652951529000"; public async up(queryRunner: QueryRunner): Promise { await queryRunner.query( @@ -240,10 +240,12 @@ returning id, "permission"."clonedFromId"`; infos: UserPermissionInfo[] ): PermissionInfo[] { const mappedInfos = infos.map(info => { - const match = userPermissions.find(p => p.permissionId === info.clonedFromId); - return match ? { ...info, userId: match.userId } : info; + const matches = userPermissions.filter(p => p.permissionId === info.clonedFromId); + return matches.length + ? { ...info, userIds: matches.map(x => x.userId) } + : info; }); - return mappedInfos.filter(info => typeof info.userId === "number"); + return mappedInfos.filter(info => info.userIds?.length); } private mapAppPermissions( @@ -251,10 +253,12 @@ returning id, "permission"."clonedFromId"`; infos: AppPermissionInfo[] ): PermissionInfo[] { const mappedInfos = infos.map(info => { - const match = appPermissions.find(p => p.permissionId === info.clonedFromId); - return match ? { ...info, applicationId: match.applicationId } : info; + const matches = appPermissions.filter(p => p.permissionId === info.clonedFromId); + return matches.length + ? { ...info, applicationIds: matches.map(x => x.applicationId) } + : info; }); - return mappedInfos.filter(info => typeof info.applicationId === "number"); + return mappedInfos.filter(info => info.applicationIds?.length); } private mapApiKeyPermissions( @@ -262,17 +266,17 @@ returning id, "permission"."clonedFromId"`; infos: ApiKeyPermissionInfo[] ): PermissionInfo[] { const mappedInfos = infos.map(info => { - const match = apiKeyPermissions.find(p => p.permissionId === info.clonedFromId); - return match ? { ...info, apiKeyId: match.apiKeyId } : info; + const matches = apiKeyPermissions.filter(p => p.permissionId === info.clonedFromId); + return matches ? { ...info, apiKeyIds: matches.map(x => x.apiKeyId) } : info; }); - return mappedInfos.filter(info => typeof info.apiKeyId === "number"); + return mappedInfos.filter(info => info.apiKeyIds?.length); } private copyUserPermissionsQuery(infos: UserPermissionInfo[]): string { if (!infos.length) return ""; const insertIntoStatements = infos - .map(info => `(${info.userId}, ${info.id})`) + .map(info => info.userIds.map(userId => `(${userId}, ${info.id})`)) .join(","); return `INSERT INTO "public"."user_permissions_permission"("userId","permissionId") VALUES ${insertIntoStatements}`; @@ -282,7 +286,7 @@ returning id, "permission"."clonedFromId"`; if (!infos.length) return ""; const insertIntoStatements = infos - .map(info => `(${info.applicationId}, ${info.id})`) + .map(info => info.applicationIds.map(appId => `(${appId}, ${info.id})`)) .join(","); return `INSERT INTO "public"."application_permissions_permission"("applicationId","permissionId") VALUES ${insertIntoStatements}`; @@ -292,7 +296,7 @@ returning id, "permission"."clonedFromId"`; if (!infos.length) return ""; const insertIntoStatements = infos - .map(info => `(${info.apiKeyId}, ${info.id})`) + .map(info => info.apiKeyIds.map(keyId => `(${keyId}, ${info.id})`)) .join(","); return `INSERT INTO "public"."api_key_permissions_permission"("apiKeyId","permissionId") VALUES ${insertIntoStatements}`; From 691d96f43b504e7d7c273965f3bb03f7312565ee Mon Sep 17 00:00:00 2001 From: Aram Al-Sabti Date: Thu, 19 May 2022 13:52:25 +0200 Subject: [PATCH 19/19] Fix global admin not created on startup --- src/entities/permissions/permission.entity.ts | 2 +- src/services/user-management/permission.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/entities/permissions/permission.entity.ts b/src/entities/permissions/permission.entity.ts index c0d44e7e..0e2b4111 100644 --- a/src/entities/permissions/permission.entity.ts +++ b/src/entities/permissions/permission.entity.ts @@ -14,7 +14,7 @@ import { Application } from "@entities/application.entity"; import { Organization } from "@entities/organization.entity"; import { ApiKey } from "@entities/api-key.entity"; -@Entity() +@Entity("permission") export class Permission extends DbBaseEntity { constructor(name: string, org?: Organization, addNewApps = false) { super(); diff --git a/src/services/user-management/permission.service.ts b/src/services/user-management/permission.service.ts index 816c6f84..1c88aa1f 100644 --- a/src/services/user-management/permission.service.ts +++ b/src/services/user-management/permission.service.ts @@ -121,7 +121,7 @@ export class PermissionService { } ) .leftJoin("permission.type", "type") - .getOneOrFail(); + .getOne(); if (globalAdmin) { return globalAdmin;