diff --git a/src/config/constants/translations.ts b/src/config/constants/translations.ts new file mode 100644 index 00000000..ac5aa98d --- /dev/null +++ b/src/config/constants/translations.ts @@ -0,0 +1,6 @@ +// OS2iot won't be translated in any other language than Danish in the foreseeable future +export enum Translations { + OrganizationAdmin = "Organisationsadministrator", + ApplicationAdmin = "Applikationsadministrator", + ReadLevel = "Læserettigheder", +} diff --git a/src/controllers/admin-controller/application.controller.ts b/src/controllers/admin-controller/application.controller.ts index 30f0f4da..c5ac1c18 100644 --- a/src/controllers/admin-controller/application.controller.ts +++ b/src/controllers/admin-controller/application.controller.ts @@ -108,15 +108,14 @@ export class ApplicationController { req: AuthenticatedRequest, query: ListAllApplicationsDto ) { - // If org admin give all - const allFromOrg = req.user.permissions.getAllOrganizationsWithApplicationAdmin(); - if (this.isOrganizationAdmin(allFromOrg, query)) { + // User admins have access to all applications in the organization + const allFromOrg = req.user.permissions.getAllOrganizationsWithUserAdmin(); + if (allFromOrg.some(x => x === query?.organizationId)) { return await this.applicationService.findAndCountWithPagination(query, [ query.organizationId, ]); } - // If not org admin only allowed ones const allowedApplications = req.user.permissions.getAllApplicationsWithAtLeastRead(); return await this.applicationService.findAndCountInList( query, @@ -125,10 +124,6 @@ export class ApplicationController { ); } - private isOrganizationAdmin(allFromOrg: number[], query: ListAllApplicationsDto) { - return allFromOrg.some(x => x === query?.organizationId); - } - @Read() @Get(":id") @ApiOperation({ summary: "Find one Application by id" }) diff --git a/src/controllers/user-management/auth.controller.ts b/src/controllers/user-management/auth.controller.ts index d1947b92..77a4073a 100644 --- a/src/controllers/user-management/auth.controller.ts +++ b/src/controllers/user-management/auth.controller.ts @@ -32,7 +32,6 @@ import { JwtPayloadDto } from "@dto/internal/jwt-payload.dto"; import { LoginDto } from "@dto/login.dto"; import { Organization } from "@entities/organization.entity"; import { User } from "@entities/user.entity"; -import { PermissionType } from "@enum/permission-type.enum"; import { AuthService } from "@services/user-management/auth.service"; import { OrganizationService } from "@services/user-management/organization.service"; import { UserService } from "@services/user-management/user.service"; diff --git a/src/entities/dto/permission-organization-application.dto.ts b/src/entities/dto/permission-organization-application.dto.ts index 975a45f4..af3bd393 100644 --- a/src/entities/dto/permission-organization-application.dto.ts +++ b/src/entities/dto/permission-organization-application.dto.ts @@ -3,13 +3,13 @@ import * as _ from "lodash"; export class UserPermissions { constructor() { this.orgToReadPermissions = new Map(); - this.orgToUserAdminPermissions = new Set(); + this.orgToUserAdminPermissions = new Map(); this.orgToGatewayAdminPermissions = new Set(); this.orgToApplicationAdminPermissions = new Map(); } orgToReadPermissions: Map; - orgToUserAdminPermissions: Set; + orgToUserAdminPermissions: Map; orgToGatewayAdminPermissions: Set; orgToApplicationAdminPermissions: Map; isGlobalAdmin = false; @@ -17,6 +17,7 @@ export class UserPermissions { getAllApplicationsWithAtLeastRead(): number[] { return _.union( this.extractValues(this.orgToReadPermissions), + this.extractValues(this.orgToUserAdminPermissions), this.getAllApplicationsWithAdmin() ); } @@ -35,12 +36,13 @@ export class UserPermissions { getAllOrganizationsWithAtLeastApplicationRead(): number[] { return _.union( this.extractKeys(this.orgToReadPermissions), - this.getAllOrganizationsWithApplicationAdmin() + this.getAllOrganizationsWithApplicationAdmin(), + this.getAllOrganizationsWithUserAdmin(), ); } getAllOrganizationsWithUserAdmin(): number[] { - return Array.from(this.orgToUserAdminPermissions); + return this.extractKeys(this.orgToUserAdminPermissions); } getAllOrganizationsWithGatewayAdmin(): number[] { diff --git a/src/entities/permissions/permission.entity.ts b/src/entities/permissions/permission.entity.ts index 0e2b4111..ae60ef9f 100644 --- a/src/entities/permissions/permission.entity.ts +++ b/src/entities/permissions/permission.entity.ts @@ -8,11 +8,13 @@ import { TableInheritance, OneToMany, ManyToOne, + RelationId, } 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"; +import { nameof } from "@helpers/type-helper"; @Entity("permission") export class Permission extends DbBaseEntity { @@ -42,6 +44,10 @@ export class Permission extends DbBaseEntity { @ManyToMany(() => Application, application => application.permissions) applications: Application[]; + // ORM-specific column for extracting only the ids of related applications + @RelationId(nameof("applications")) + applicationIds: Application["id"][]; + @Column({ nullable: true, default: false, type: Boolean }) automaticallyAddNewApplications = false; diff --git a/src/migration/1652951529000-revised-permissions.ts b/src/migration/1652951529000-revised-permissions.ts index 27759193..1a9cc2e2 100644 --- a/src/migration/1652951529000-revised-permissions.ts +++ b/src/migration/1652951529000-revised-permissions.ts @@ -1,3 +1,4 @@ +import { Application } from "@entities/application.entity"; import { MigrationInterface, QueryRunner } from "typeorm"; type AppPermissions = { @@ -24,8 +25,8 @@ type AppPermissionInfo = PermissionInfo & { }; type ApiKeyPermissionInfo = PermissionInfo & { - apiKeyIds?: number[]; -} + apiKeyIds?: number[]; +}; type UserPermissions = { userId: number; @@ -39,7 +40,7 @@ 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 revisedPermissions1652951529000 implements MigrationInterface { - name = "revisedPermissions1652951529000"; + name = "revisedPermissions1652951529000"; public async up(queryRunner: QueryRunner): Promise { await queryRunner.query( @@ -48,14 +49,17 @@ export class revisedPermissions1652951529000 implements MigrationInterface { await queryRunner.query( `CREATE TYPE "permission_type_enum" AS ENUM('GlobalAdmin', 'OrganizationUserAdmin', 'OrganizationGatewayAdmin', 'OrganizationApplicationAdmin', 'Read')` ); + await queryRunner.query(createPermissionTypeUnionSql); - // 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(`DROP TYPE "permission_type_enum_old"`); - - // Update permission so it can refer to multiple types + // Migrate permission types to the new permission type table without altering them await this.migratePermissionTypeUp(queryRunner); + // Migrate existing data. Old permission levels are updated to the new ones + await this.migratePermissionUp(queryRunner); + + // Cleanup + await this.migratePermissionTypeUpCleanup(queryRunner); + await queryRunner.query(`DROP TYPE "${permissionTypeUnionName}"`); + await queryRunner.query(`DROP TYPE "permission_type_enum_old"`); } public async down(queryRunner: QueryRunner): Promise { @@ -73,7 +77,7 @@ export class revisedPermissions1652951529000 implements MigrationInterface { // ASSUMPTION: this migration is only reverted immediately after executing it. // There's no 1-1 translation between the three organization admin permissions and the old ones. Ex. OrganizationGatewayAdmin has no equivalent // To avoid elevating users unexpectedly, for security reasons, we translate them to "Read". - await this.migrateDown(queryRunner); + await this.migratePermissionDown(queryRunner); await queryRunner.query( `ALTER TABLE "permission" ALTER COLUMN "type" TYPE "permission_type_enum_old" USING "type"::"text"::"permission_type_enum_old"` @@ -82,82 +86,37 @@ export class revisedPermissions1652951529000 implements MigrationInterface { await queryRunner.query( `ALTER TYPE "permission_type_enum_old" RENAME TO "permission_type_enum"` ); - } - private async migrateUp(queryRunner: QueryRunner): Promise { - await queryRunner.query(createPermissionTypeUnionSql); + private async migratePermissionUp(queryRunner: QueryRunner): Promise { await queryRunner.query( `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 + // Begin migrating to the new permission types. await queryRunner.query( - `ALTER TABLE "permission" ADD COLUMN "clonedFromId" integer` - ); - - // Begin cloning. Store both the old and new ids as mappings - const applicationAdminFromWriteInfo: PermissionInfo[] = await queryRunner.query( - this.copyPermissionsQuery("Write", "OrganizationApplicationAdmin") - ); - const readFromWriteInfo: PermissionInfo[] = await queryRunner.query( - this.copyPermissionsQuery("Write", "Read") - ); - const userAdminFromOrgAdminInfo: PermissionInfo[] = await queryRunner.query( - this.copyPermissionsQuery("OrganizationAdmin", "OrganizationUserAdmin") - ); - const applicationAdminFromOrgAdminInfo: PermissionInfo[] = await queryRunner.query( - this.copyPermissionsQuery("OrganizationAdmin", "OrganizationApplicationAdmin") - ); - const gatewayAdminFromOrgAdminInfo: PermissionInfo[] = await queryRunner.query( - this.copyPermissionsQuery("OrganizationAdmin", "OrganizationGatewayAdmin") - ); - const readAdminFromOrgAdminInfo: PermissionInfo[] = await queryRunner.query( - this.copyPermissionsQuery("OrganizationAdmin", "Read") - ); - - // Migrate entities in other tables with a foreign key to the permission table - await this.migrateUserPermissions ( - queryRunner, - applicationAdminFromWriteInfo, - readFromWriteInfo, - userAdminFromOrgAdminInfo, - applicationAdminFromOrgAdminInfo, - gatewayAdminFromOrgAdminInfo, - readAdminFromOrgAdminInfo - ); - - await this.migrateApplicationPermissions ( - queryRunner, - applicationAdminFromWriteInfo, - readFromWriteInfo, - userAdminFromOrgAdminInfo, - applicationAdminFromOrgAdminInfo, - gatewayAdminFromOrgAdminInfo, - readAdminFromOrgAdminInfo + this.copyPermissionTypeQuery("OrganizationAdmin", "OrganizationUserAdmin") ); - - await this.migrateApiKeyPermissions ( - queryRunner, - applicationAdminFromWriteInfo, - readFromWriteInfo, - userAdminFromOrgAdminInfo, - applicationAdminFromOrgAdminInfo, - gatewayAdminFromOrgAdminInfo, - readAdminFromOrgAdminInfo + await queryRunner.query( + this.copyPermissionTypeQuery( + "OrganizationAdmin", + "OrganizationApplicationAdmin" + ) + ); + // OrganizationAdmin has access to all applications despite not being mapped. + // The new system requires an application admin to be mapped to an application to access it + await this.migrateApplicationsForOrganizationAdminsUp(queryRunner); + await queryRunner.query( + this.copyPermissionTypeQuery("OrganizationAdmin", "OrganizationGatewayAdmin") ); - - // Cleanup - await queryRunner.query(`ALTER TABLE "permission" DROP COLUMN "clonedFromId"`); - await this.cleanupPermissionRelations(queryRunner, "'Write', 'OrganizationAdmin'"); - await queryRunner.query( - `DELETE FROM "public"."permission" where type IN ('OrganizationAdmin', 'Write')` + this.copyPermissionTypeQuery("OrganizationAdmin", "Read") ); + await queryRunner.query( - `ALTER TABLE "permission" ALTER COLUMN "type" TYPE "permission_type_enum" USING "type"::"text"::"permission_type_enum"` + this.copyPermissionTypeQuery("Write", "OrganizationApplicationAdmin") ); - await queryRunner.query(`DROP TYPE "${permissionTypeUnionName}"`); + await queryRunner.query(this.copyPermissionTypeQuery("Write", "Read")); } private async migrateUserPermissions( @@ -197,7 +156,7 @@ from public.application_permissions_permission`); await queryRunner.query(this.copyAppPermissionsQuery(newAppPermissions)); } - private async migrateApiKeyPermissions( + private async migrateApiKeyPermissions( queryRunner: QueryRunner, ...permissionInfos: PermissionInfo[][] ) { @@ -215,24 +174,51 @@ from public.api_key_permissions_permission`); await queryRunner.query(this.copyApiKeyPermissionsQuery(newApiKeyPermissions)); } - private fetchPermissionsIdsQuery(oldType: string, newType = oldType): string { - return `select "createdAt", + private buildPermissionColumns(newType: string) { + return `"createdAt", "updatedAt", '${newType}', name, "automaticallyAddNewApplications", "createdById", "updatedById", - "organizationId", - "id" AS "clonedFromId" - from "public"."permission" - where type = '${oldType}'`; + "organizationId"`; + } + + private buildPermissionColumnsForPermissionType(newType: string) { + return `"createdAt", + "updatedAt", + '${newType}', + "createdById", + "updatedById", + "id"`; + } + + private fetchPermissionsIdsQuery(oldType: string, newType = oldType): string { + return `select ${this.buildPermissionColumns(newType)}, + "id" AS "clonedFromId" + from "public"."permission" + where type = '${oldType}'`; + } + + private fetchPermissionsForPermissionTypeQuery( + oldType: string, + newType = oldType + ): string { + return `select ${this.buildPermissionColumnsForPermissionType(newType)} + from "public"."permission" + where type = '${oldType}'`; } private copyPermissionsQuery(oldType: string, newType: string): string { return `INSERT INTO "public"."permission"("createdAt","updatedAt",type,name,"automaticallyAddNewApplications","createdById","updatedById","organizationId","clonedFromId") - ${this.fetchPermissionsIdsQuery(oldType, newType)} -returning id, "permission"."clonedFromId"`; + ${this.fetchPermissionsIdsQuery(oldType, newType)} + returning id, "permission"."clonedFromId"`; + } + + private copyPermissionTypeQuery(oldType: string, newType: string): string { + return `INSERT INTO "public"."permission_type"("createdAt","updatedAt",type,"createdById","updatedById","permissionId") + ${this.fetchPermissionsForPermissionTypeQuery(oldType, newType)}`; } private mapUserPermissions( @@ -240,7 +226,9 @@ returning id, "permission"."clonedFromId"`; infos: UserPermissionInfo[] ): PermissionInfo[] { const mappedInfos = infos.map(info => { - const matches = userPermissions.filter(p => p.permissionId === info.clonedFromId); + const matches = userPermissions.filter( + p => p.permissionId === info.clonedFromId + ); return matches.length ? { ...info, userIds: matches.map(x => x.userId) } : info; @@ -253,7 +241,9 @@ returning id, "permission"."clonedFromId"`; infos: AppPermissionInfo[] ): PermissionInfo[] { const mappedInfos = infos.map(info => { - const matches = appPermissions.filter(p => p.permissionId === info.clonedFromId); + const matches = appPermissions.filter( + p => p.permissionId === info.clonedFromId + ); return matches.length ? { ...info, applicationIds: matches.map(x => x.applicationId) } : info; @@ -261,12 +251,14 @@ returning id, "permission"."clonedFromId"`; return mappedInfos.filter(info => info.applicationIds?.length); } - private mapApiKeyPermissions( + private mapApiKeyPermissions( apiKeyPermissions: ApiKeyPermissions, infos: ApiKeyPermissionInfo[] ): PermissionInfo[] { const mappedInfos = infos.map(info => { - const matches = apiKeyPermissions.filter(p => p.permissionId === info.clonedFromId); + const matches = apiKeyPermissions.filter( + p => p.permissionId === info.clonedFromId + ); return matches ? { ...info, apiKeyIds: matches.map(x => x.apiKeyId) } : info; }); return mappedInfos.filter(info => info.apiKeyIds?.length); @@ -292,7 +284,7 @@ returning id, "permission"."clonedFromId"`; ${insertIntoStatements}`; } - private copyApiKeyPermissionsQuery(infos: ApiKeyPermissionInfo[]): string { + private copyApiKeyPermissionsQuery(infos: ApiKeyPermissionInfo[]): string { if (!infos.length) return ""; const insertIntoStatements = infos @@ -304,13 +296,17 @@ 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')`); - 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"))`); + 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" ${permissionTypeUnionName} NOT NULL, "createdById" integer, "updatedById" integer, "permissionId" integer, CONSTRAINT "PK_3f2a17e0bff1bc4e34254b27d78" PRIMARY KEY ("id"))` + ); const fetchAllPermissions = `select "createdAt", "updatedAt", -- Casting from one enum to another requires casting it to text first - type::text::"public"."permission_type_type_enum", + type::text::${permissionTypeUnionName}, "createdById", "updatedById", "id" AS "permissionId" @@ -319,16 +315,62 @@ returning id, "permission"."clonedFromId"`; // For each permission, create a corresponding permission type await queryRunner.query(`INSERT INTO "public"."permission_type"("createdAt","updatedAt",type,"createdById","updatedById","permissionId") ${fetchAllPermissions}`); + } + /** + * Must be called after migrating 'OrganizationAdmin' but before migrating 'Write' + * as they both result in 'OrganizationApplicationAdmin' + * @param queryRunner + */ + private async migrateApplicationsForOrganizationAdminsUp(queryRunner: QueryRunner) { + // Get applications not mapped to existing organization admins + const unmappedApplications: (Application & { + permissionid: number; + })[] = await queryRunner.query(`SELECT app.* + , pm.id as permissionid + FROM public.application app + JOIN public.permission pm ON pm."organizationId" = app."belongsToId" + JOIN public.permission_type pt ON pt."permissionId" = pm.id + LEFT JOIN public.application_permissions_permission appPm ON appPm."permissionId" = pm."id" + WHERE pt.type = 'OrganizationApplicationAdmin' + -- Ignore mapped applications + AND appPm."applicationId" IS NULL + AND appPm."permissionId" IS NULL`); + + if (!unmappedApplications.length) return; + + const insertIntoStatements = unmappedApplications + .map(app => `(${app.id}, ${app.permissionid})`) + .join(","); + await queryRunner.query(`INSERT INTO "public"."application_permissions_permission"("applicationId","permissionId") VALUES + ${insertIntoStatements}`); + } + + private async migratePermissionTypeUpCleanup(queryRunner: QueryRunner) { 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`); - await queryRunner.query(`ALTER TABLE "permission_type" ADD CONSTRAINT "UQ_3acd70f5a3895ee2fb92b2a4290" UNIQUE ("type", "permissionId")`); + + await queryRunner.query( + `DELETE FROM "public"."permission_type" where type IN ('OrganizationAdmin', 'Write')` + ); + await queryRunner.query( + `ALTER TABLE "permission_type" ALTER COLUMN "type" TYPE "permission_type_type_enum" USING "type"::"text"::"permission_type_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` + ); + await queryRunner.query( + `ALTER TABLE "permission_type" ADD CONSTRAINT "UQ_3acd70f5a3895ee2fb92b2a4290" UNIQUE ("type", "permissionId")` + ); } - private async migrateDown(queryRunner: QueryRunner): Promise { + private async migratePermissionDown(queryRunner: QueryRunner): Promise { await queryRunner.query( `ALTER TABLE "permission" ALTER COLUMN "type" TYPE "${permissionTypeUnionName}" USING "type"::"text"::"${permissionTypeUnionName}"` ); @@ -340,7 +382,7 @@ returning id, "permission"."clonedFromId"`; // 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") + this.copyPermissionsQuery("OrganizationUserAdmin", "OrganizationAdmin") ); const readFromGatewayAdminInfo: PermissionInfo[] = await queryRunner.query( this.copyPermissionsQuery("OrganizationGatewayAdmin", "Read") @@ -381,7 +423,10 @@ returning id, "permission"."clonedFromId"`; // Cleanup await queryRunner.query(`ALTER TABLE "permission" DROP COLUMN "clonedFromId"`); - await this.cleanupPermissionRelations(queryRunner, "'OrganizationUserAdmin', 'OrganizationGatewayAdmin', 'OrganizationApplicationAdmin'"); + await this.cleanupPermissionRelations( + queryRunner, + "'OrganizationUserAdmin', 'OrganizationGatewayAdmin', 'OrganizationApplicationAdmin'" + ); await queryRunner.query( `DELETE FROM "public"."permission" where type IN ('OrganizationUserAdmin', 'OrganizationGatewayAdmin', 'OrganizationApplicationAdmin')` @@ -393,13 +438,23 @@ returning id, "permission"."clonedFromId"`; } private async migratePermissionTypeDown(queryRunner: QueryRunner) { - 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( + `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"` + ); // Add permission level "type". It's nullable to make the migration possible - await queryRunner.query(`ALTER TABLE "permission" ADD "type" "${permissionTypeUnionName}"`); + 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. @@ -425,22 +480,29 @@ returning id, "permission"."clonedFromId"`; SELECT pt.* FROM permission_type_priority ptp 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 + -- Order specifically so that the last update, for any permission, is the type + -- with highest priority (lowest value) + ORDER BY ptp.priority ASC ) highestPmType WHERE - pm.id = highestPmType."permissionId"`) + 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( + `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") `); + await queryRunner.query( + `CREATE INDEX "IDX_71bf2818fb2ad92e208d7aeadf" ON "permission" ("type") ` + ); } - private async cleanupPermissionRelations(queryRunner: QueryRunner, permissionTypesToRemove: string) { + private async cleanupPermissionRelations( + queryRunner: QueryRunner, + permissionTypesToRemove: string + ) { await queryRunner.query(`DELETE FROM user_permissions_permission WHERE "permissionId" IN ( @@ -449,7 +511,7 @@ WHERE "permissionId" IN WHERE permission.type IN (${permissionTypesToRemove}) );`); - await queryRunner.query(`DELETE FROM application_permissions_permission + await queryRunner.query(`DELETE FROM application_permissions_permission WHERE "permissionId" IN ( SELECT "permission"."id" FROM application_permissions_permission @@ -457,7 +519,7 @@ WHERE "permissionId" IN WHERE permission.type IN (${permissionTypesToRemove}) )`); - await queryRunner.query(`DELETE FROM api_key_permissions_permission + await queryRunner.query(`DELETE FROM api_key_permissions_permission WHERE "permissionId" IN ( SELECT "permission"."id" FROM api_key_permissions_permission diff --git a/src/services/user-management/permission.service.ts b/src/services/user-management/permission.service.ts index c3188be0..85fb7ffe 100644 --- a/src/services/user-management/permission.service.ts +++ b/src/services/user-management/permission.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Inject, Injectable, forwardRef } from "@nestjs/common"; +import { Inject, Injectable, forwardRef } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import * as _ from "lodash"; import { @@ -32,6 +32,7 @@ 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"; +import { Translations } from "@config/constants/translations"; @Injectable() export class PermissionService { @@ -50,15 +51,14 @@ export class PermissionService { org: Organization, userId: number ): Promise { - const { readPermission, orgApplicationAdminPermission, orgAdminPermission, orgGatewayAadminPermission } = this.instantiateDefaultPermissions(org, userId); + const { readPermission, orgApplicationAdminPermission, orgAllAdminPermission } = 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([ readPermission, orgApplicationAdminPermission, - orgAdminPermission, - orgGatewayAadminPermission, + orgAllAdminPermission, ]); r.forEach(val => AuditLog.success(ActionType.CREATE, Permission.name, userId, val.id, val.name) @@ -68,39 +68,49 @@ export class PermissionService { private instantiateDefaultPermissions(org: Organization, userId: number) { const nameSuffixSeparator = " - "; - const readSuffix = `${nameSuffixSeparator}${PermissionType.Read}`; - const organizationUserAdminSuffix = `${nameSuffixSeparator}${PermissionType.OrganizationUserAdmin}`; - const organizationGatewayAdminSuffix = `${nameSuffixSeparator}${PermissionType.OrganizationGatewayAdmin}`; - const organizationApplicationAdminSuffix = `${nameSuffixSeparator}${PermissionType.OrganizationApplicationAdmin}`; + const allAdminSuffix = `${nameSuffixSeparator}${Translations.OrganizationAdmin}`; + const organizationApplicationAdminSuffix = `${nameSuffixSeparator}${Translations.ApplicationAdmin}`; + const readSuffix = `${nameSuffixSeparator}${Translations.ReadLevel}`; - const readPermission = PermissionCreator.createRead(org.name + readSuffix, org, true); + const readPermission = PermissionCreator.createRead( + org.name + readSuffix, + org, + true + ); const orgApplicationAdminPermission = PermissionCreator.createApplicationAdmin( org.name + organizationApplicationAdminSuffix, org, true ); - const orgAdminPermission = PermissionCreator.createUserAdmin( - org.name + organizationUserAdminSuffix, - org - ); - const orgGatewayAadminPermission = PermissionCreator.createGatewayAdmin( - org.name + organizationGatewayAdminSuffix, + orgApplicationAdminPermission.type.push({ + type: PermissionType.Read, + } as PermissionTypeEntity); + + const orgAllAdminPermission = PermissionCreator.createUserAdmin( + org.name + allAdminSuffix, org ); + orgAllAdminPermission.type.push({ + type: PermissionType.OrganizationApplicationAdmin, + } as PermissionTypeEntity); + orgAllAdminPermission.type.push({ + type: PermissionType.OrganizationGatewayAdmin, + } as PermissionTypeEntity); + orgAllAdminPermission.type.push({ + type: PermissionType.Read, + } as PermissionTypeEntity); + this.setUserIdOnPermissions(readPermission, userId); this.setUserIdOnPermissions(orgApplicationAdminPermission, userId); - this.setUserIdOnPermissions(orgAdminPermission, userId); - this.setUserIdOnPermissions(orgGatewayAadminPermission, userId); + this.setUserIdOnPermissions(orgAllAdminPermission, userId); readPermission.createdBy = userId; readPermission.updatedBy = userId; orgApplicationAdminPermission.createdBy = userId; orgApplicationAdminPermission.updatedBy = userId; - orgAdminPermission.createdBy = userId; - orgAdminPermission.updatedBy = userId; - orgGatewayAadminPermission.createdBy = userId; - orgGatewayAadminPermission.updatedBy = userId; - return { readPermission, orgApplicationAdminPermission, orgAdminPermission, orgGatewayAadminPermission }; + orgAllAdminPermission.createdBy = userId; + orgAllAdminPermission.updatedBy = userId; + return { readPermission, orgApplicationAdminPermission, orgAllAdminPermission }; } private setUserIdOnPermissions(permission: Permission, userId: number) { @@ -380,7 +390,7 @@ export class PermissionService { return await this.buildPermissionsWithApplicationsQuery() .leftJoin("permission.users", "user") .where("permission_type.type = :permType AND user.id = :id", { - permType: PermissionType.OrganizationApplicationAdmin, + permType: PermissionType.OrganizationUserAdmin, id: userId, }) .getRawMany(); @@ -406,7 +416,7 @@ export class PermissionService { .leftJoin("permission.apiKeys", "apiKey") .leftJoin("permission.type", "type") .where("type.type = :permType AND apiKey.id = :id", { - permType: PermissionType.OrganizationApplicationAdmin, + permType: PermissionType.OrganizationUserAdmin, id: apiKeyId, }) .getRawMany(); @@ -414,7 +424,7 @@ export class PermissionService { async findPermissionGroupedByLevelForUser(userId: number): Promise { let permissions = await this.findPermissionsForUser(userId); - if (this.isOrganizationApplicationAdmin(permissions)) { + if (this.hasAccessToAllApplicationsInOrganization(permissions)) { // For organization admins, we need to fetch all applications they have permissions to const permissionsForOrgAdmin = await this.findPermissionsForOrgAdminWithApplications( userId @@ -429,7 +439,7 @@ export class PermissionService { apiKeyId: number ): Promise { let permissions = await this.findPermissionsForApiKey(apiKeyId); - if (this.isOrganizationApplicationAdmin(permissions)) { + if (this.hasAccessToAllApplicationsInOrganization(permissions)) { // For organization admins, we need to fetch all applications they have permissions to const permissionsForOrgAdmin = await this.findPermissionsForApiKeyOrgAdminWithApplications( apiKeyId @@ -452,7 +462,9 @@ export class PermissionService { } else if (p.permission_type_type == PermissionType.OrganizationGatewayAdmin) { res.orgToGatewayAdminPermissions.add(p.organization_id); } else if (p.permission_type_type == PermissionType.OrganizationUserAdmin) { - res.orgToUserAdminPermissions.add(p.organization_id); + // A user admin can map applications to permissions, so they should also + // have access to them + this.addOrUpdateApplicationIds(res.orgToUserAdminPermissions, p); } else if (p.permission_type_type == PermissionType.Read) { this.addOrUpdateApplicationIds(res.orgToReadPermissions, p); } @@ -465,9 +477,9 @@ export class PermissionService { return await this.permissionRepository.findByIds(ids); } - private isOrganizationApplicationAdmin(permissions: PermissionMinimalDto[]) { + private hasAccessToAllApplicationsInOrganization(permissions: PermissionMinimalDto[]) { return permissions.some( - x => x.permission_type_type == PermissionType.OrganizationApplicationAdmin + 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 7237f4f5..d42f6ebc 100644 --- a/src/services/user-management/user.service.ts +++ b/src/services/user-management/user.service.ts @@ -28,6 +28,7 @@ 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"; +import { nameof } from "@helpers/type-helper"; @Injectable() export class UserService { @@ -123,6 +124,9 @@ export class UserService { async findOneWithOrganizations(id: number): Promise { return await this.userRepository.findOne(id, { relations: ["permissions", "permissions.organization", "permissions.type"], + loadRelationIds: { + relations: [`permissions.${nameof("applicationIds")}`], + }, }); }