[1/3] Rename permissionFlag to rolePermissionFlag + add permissionFlag catalog/backfill (#20481)
Split of #20377. ## Summary This PR separates available permission flags from per-role permission flag grants. Previously, `core.permissionFlag` stored the role assignment directly: `roleId + flag`. This PR renames that legacy grant table to `core.rolePermissionFlag`, then recreates `core.permissionFlag` as the catalog of available permission flags. ## What changed - Rename the existing `core.permissionFlag` grant table to `core.rolePermissionFlag`. - Add the new syncable `core.permissionFlag` catalog entity with key, label, description, icon, permission type, relevance flags, and custom/standard metadata. - Add stable `SystemPermissionFlag` universal identifiers for the built-in `PermissionFlagType` values. - Seed the standard permission flags for every workspace under the Twenty standard application. - Backfill existing role grants: - create missing catalog rows for existing grant keys, - add `rolePermissionFlag.permissionFlagId`, - migrate grants from the old string `flag` column to the new catalog FK, - replace the old `(flag, roleId)` uniqueness with `(permissionFlagId, roleId)`. - Rewire role permission flag caches, permission checks, role DTO mapping, and `upsertPermissionFlags` to resolve through the catalog. - Keep the existing public role permission API shape: product/app surfaces still talk about `permissionFlags` and return `{ id, roleId, flag }`. - Update metadata flat-entity machinery, migration builders, validators, action handlers, snapshots, generated schemas, docs, and app fixtures for the new `permissionFlag` / `rolePermissionFlag` split. ## Behavior after this PR - Existing permission flag grants keep working. - Existing GraphQL role permission flows keep the same public naming. - Standard permission flags are represented as catalog rows. - Permission checks now compare grants through catalog universal identifiers instead of the legacy `flag` column. - Workspace deletion cleanup now verifies both `permissionFlag` and `rolePermissionFlag`. ## What is not in this PR - Public GraphQL CRUD for custom permission flags. - App manifest support for declaring new custom permission flags. - Frontend UI for creating or assigning custom permission flags beyond the existing role permission flow. --------- Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
+102
@@ -0,0 +1,102 @@
|
||||
import { QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
|
||||
|
||||
@RegisteredInstanceCommand('2.6.0', 1778235340020)
|
||||
export class RenamePermissionFlagToRolePermissionFlagFastInstanceCommand
|
||||
implements FastInstanceCommand
|
||||
{
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// The legacy permissionFlag table stores per-role grants (`roleId` + `flag`).
|
||||
// Rename it to preserve those rows before creating the new permissionFlag catalog table.
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."permissionFlag" RENAME TO "rolePermissionFlag"`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."rolePermissionFlag" RENAME CONSTRAINT "PK_a02789db60620a1e9f90147b50f" TO "PK_76591adc8035c2e7b0cd6115136"`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."rolePermissionFlag" RENAME CONSTRAINT "IDX_PERMISSION_FLAG_FLAG_ROLE_ID_UNIQUE" TO "IDX_ROLE_PERMISSION_FLAG_FLAG_ROLE_ID_UNIQUE"`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER INDEX "core"."IDX_PERMISSION_FLAG_ROLE_ID" RENAME TO "IDX_ROLE_PERMISSION_FLAG_ROLE_ID"`,
|
||||
);
|
||||
|
||||
// Re-hash inherited constraints/indexes so TypeORM's schema diff matches
|
||||
// the renamed table. Original names were derived from "permissionFlag"
|
||||
// and stay free for the new catalog table created by the next migration.
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."rolePermissionFlag" DROP CONSTRAINT "FK_13f8ca9c517976733a1ce4c10eb"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."rolePermissionFlag" DROP CONSTRAINT "FK_835bc9f7ef959debfc5cd268049"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."rolePermissionFlag" DROP CONSTRAINT "FK_b26a9d39a88d0e72373c677c6c5"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "core"."IDX_da8ffd3c24b4a819430a861067"`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`CREATE UNIQUE INDEX "IDX_e4559ae0dba56e53714137c704" ON "core"."rolePermissionFlag" ("workspaceId", "universalIdentifier")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."rolePermissionFlag" ADD CONSTRAINT "FK_d47b1ebee75d98daa0c870c26e3" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."rolePermissionFlag" ADD CONSTRAINT "FK_3835ecc1019327566d35728c8ba" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."rolePermissionFlag" ADD CONSTRAINT "FK_4c6ea38698de230b0ec18fa2110" FOREIGN KEY ("roleId") REFERENCES "core"."role"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."rolePermissionFlag" DROP CONSTRAINT "FK_4c6ea38698de230b0ec18fa2110"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."rolePermissionFlag" DROP CONSTRAINT "FK_3835ecc1019327566d35728c8ba"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."rolePermissionFlag" DROP CONSTRAINT "FK_d47b1ebee75d98daa0c870c26e3"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "core"."IDX_e4559ae0dba56e53714137c704"`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`CREATE UNIQUE INDEX "IDX_da8ffd3c24b4a819430a861067" ON "core"."rolePermissionFlag" ("workspaceId", "universalIdentifier")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."rolePermissionFlag" ADD CONSTRAINT "FK_b26a9d39a88d0e72373c677c6c5" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."rolePermissionFlag" ADD CONSTRAINT "FK_835bc9f7ef959debfc5cd268049" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."rolePermissionFlag" ADD CONSTRAINT "FK_13f8ca9c517976733a1ce4c10eb" FOREIGN KEY ("roleId") REFERENCES "core"."role"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER INDEX "core"."IDX_ROLE_PERMISSION_FLAG_ROLE_ID" RENAME TO "IDX_PERMISSION_FLAG_ROLE_ID"`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."rolePermissionFlag" RENAME CONSTRAINT "IDX_ROLE_PERMISSION_FLAG_FLAG_ROLE_ID_UNIQUE" TO "IDX_PERMISSION_FLAG_FLAG_ROLE_ID_UNIQUE"`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."rolePermissionFlag" RENAME CONSTRAINT "PK_76591adc8035c2e7b0cd6115136" TO "PK_a02789db60620a1e9f90147b50f"`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."rolePermissionFlag" RENAME TO "permissionFlag"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import { QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
|
||||
|
||||
@RegisteredInstanceCommand('2.6.0', 1778235340021)
|
||||
export class PermissionFlagSyncableEntityFastInstanceCommand
|
||||
implements FastInstanceCommand
|
||||
{
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "core"."permissionFlag" (
|
||||
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"workspaceId" uuid NOT NULL,
|
||||
"applicationId" uuid NOT NULL,
|
||||
"universalIdentifier" uuid NOT NULL,
|
||||
"key" varchar NOT NULL,
|
||||
"label" varchar NOT NULL,
|
||||
"description" text,
|
||||
"icon" varchar,
|
||||
"permissionType" varchar NOT NULL,
|
||||
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
CONSTRAINT "IDX_PERMISSION_FLAG_KEY_WORKSPACE_ID_UNIQUE" UNIQUE ("key", "workspaceId"),
|
||||
CONSTRAINT "PK_a02789db60620a1e9f90147b50f" PRIMARY KEY ("id")
|
||||
)`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_PERMISSION_FLAG_APPLICATION_ID" ON "core"."permissionFlag" ("applicationId")`,
|
||||
);
|
||||
|
||||
// Reuses canonical hash names freed by the rename migration so TypeORM's
|
||||
// schema diff matches without further intervention.
|
||||
await queryRunner.query(
|
||||
`CREATE UNIQUE INDEX "IDX_da8ffd3c24b4a819430a861067" ON "core"."permissionFlag" ("workspaceId", "universalIdentifier")`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."permissionFlag"
|
||||
ADD CONSTRAINT "FK_835bc9f7ef959debfc5cd268049"
|
||||
FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id")
|
||||
ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."permissionFlag"
|
||||
ADD CONSTRAINT "FK_b26a9d39a88d0e72373c677c6c5"
|
||||
FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id")
|
||||
ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."permissionFlag" DROP CONSTRAINT IF EXISTS "FK_b26a9d39a88d0e72373c677c6c5"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."permissionFlag" DROP CONSTRAINT IF EXISTS "FK_835bc9f7ef959debfc5cd268049"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX IF EXISTS "core"."IDX_da8ffd3c24b4a819430a861067"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX IF EXISTS "core"."IDX_PERMISSION_FLAG_APPLICATION_ID"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS "core"."permissionFlag"`);
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
|
||||
|
||||
@RegisteredInstanceCommand('2.6.0', 1778235340022)
|
||||
export class LinkRolePermissionFlagToPermissionFlagFastInstanceCommand
|
||||
implements FastInstanceCommand
|
||||
{
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."rolePermissionFlag"
|
||||
ADD COLUMN IF NOT EXISTS "permissionFlagId" uuid`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."rolePermissionFlag"
|
||||
ADD CONSTRAINT "IDX_ROLE_PERMISSION_FLAG_PERMISSION_FLAG_ID_ROLE_ID_UNIQUE"
|
||||
UNIQUE ("permissionFlagId", "roleId")`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_ROLE_PERMISSION_FLAG_PERMISSION_FLAG_ID"
|
||||
ON "core"."rolePermissionFlag" ("permissionFlagId")`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."rolePermissionFlag"
|
||||
ADD CONSTRAINT "FK_8724e63323f1331591a3e91b0b3"
|
||||
FOREIGN KEY ("permissionFlagId") REFERENCES "core"."permissionFlag"("id")
|
||||
ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."rolePermissionFlag"
|
||||
DROP CONSTRAINT IF EXISTS "FK_8724e63323f1331591a3e91b0b3"`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`DROP INDEX IF EXISTS "core"."IDX_ROLE_PERMISSION_FLAG_PERMISSION_FLAG_ID"`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."rolePermissionFlag"
|
||||
DROP CONSTRAINT IF EXISTS "IDX_ROLE_PERMISSION_FLAG_PERMISSION_FLAG_ID_ROLE_ID_UNIQUE"`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."rolePermissionFlag"
|
||||
DROP COLUMN IF EXISTS "permissionFlagId"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import { TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER } from 'twenty-shared/application';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { DataSource, QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { SlowInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/slow-instance-command.interface';
|
||||
import { STANDARD_PERMISSION_FLAG_DEFINITIONS } from 'src/engine/metadata-modules/permission-flag/constants/standard-permission-flag-definitions.constant';
|
||||
|
||||
const PERMISSION_FLAG_TYPES = Object.values(PermissionFlagType) as string[];
|
||||
|
||||
@RegisteredInstanceCommand('2.6.0', 1778235340023, { type: 'slow' })
|
||||
export class BackfillRolePermissionFlagPermissionFlagIdSlowInstanceCommand
|
||||
implements SlowInstanceCommand
|
||||
{
|
||||
async runDataMigration(dataSource: DataSource): Promise<void> {
|
||||
const unknownFlagRows: { flag: string }[] = await dataSource.query(
|
||||
`SELECT DISTINCT "flag" FROM "core"."rolePermissionFlag"
|
||||
WHERE "flag" <> ALL($1::varchar[])`,
|
||||
[PERMISSION_FLAG_TYPES],
|
||||
);
|
||||
|
||||
if (unknownFlagRows.length > 0) {
|
||||
const unknownFlags = unknownFlagRows.map((row) => row.flag).join(', ');
|
||||
|
||||
throw new Error(
|
||||
`Cannot migrate: rolePermissionFlag rows reference unknown flag value(s): ${unknownFlags}`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const definition of STANDARD_PERMISSION_FLAG_DEFINITIONS) {
|
||||
await dataSource.query(
|
||||
`INSERT INTO "core"."permissionFlag" (
|
||||
"id",
|
||||
"workspaceId",
|
||||
"applicationId",
|
||||
"universalIdentifier",
|
||||
"key",
|
||||
"label",
|
||||
"description",
|
||||
"icon",
|
||||
"permissionType",
|
||||
"createdAt",
|
||||
"updatedAt"
|
||||
)
|
||||
SELECT
|
||||
uuid_generate_v4(),
|
||||
workspace."id",
|
||||
standardApplication."id",
|
||||
$1::uuid,
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5,
|
||||
$6,
|
||||
now(),
|
||||
now()
|
||||
FROM "core"."workspace" workspace
|
||||
INNER JOIN "core"."application" standardApplication
|
||||
ON standardApplication."workspaceId" = workspace."id"
|
||||
AND standardApplication."universalIdentifier" = $7
|
||||
AND standardApplication."deletedAt" IS NULL
|
||||
ON CONFLICT ("key", "workspaceId") DO NOTHING`,
|
||||
[
|
||||
definition.universalIdentifier,
|
||||
definition.key,
|
||||
definition.label,
|
||||
definition.description,
|
||||
definition.icon,
|
||||
definition.permissionType,
|
||||
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
await dataSource.query(
|
||||
`UPDATE "core"."rolePermissionFlag" rolePermissionFlag
|
||||
SET "permissionFlagId" = permissionFlag."id"
|
||||
FROM "core"."permissionFlag" permissionFlag
|
||||
WHERE permissionFlag."workspaceId" = rolePermissionFlag."workspaceId"
|
||||
AND permissionFlag."key" = rolePermissionFlag."flag"
|
||||
AND rolePermissionFlag."permissionFlagId" IS NULL`,
|
||||
);
|
||||
}
|
||||
|
||||
public async up(_queryRunner: QueryRunner): Promise<void> {}
|
||||
|
||||
public async down(_queryRunner: QueryRunner): Promise<void> {}
|
||||
}
|
||||
+8
@@ -31,6 +31,10 @@ import { AddToolAndWorkflowActionTriggerSettingsFastInstanceCommand } from 'src/
|
||||
import { BackfillApplicationVariableUniversalIdentifierSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-slow-1777966965588-backfill-application-variable-universal-identifier';
|
||||
import { MigrateToolTriggerSettingsSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-slow-1797000002000-migrate-tool-trigger-settings';
|
||||
import { AddMetadataToBillingPriceFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-4/2-4-instance-command-fast-1777100000000-add-metadata-to-billing-price';
|
||||
import { RenamePermissionFlagToRolePermissionFlagFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-6/2-6-instance-command-fast-1778235340020-rename-permission-flag-to-role-permission-flag';
|
||||
import { PermissionFlagSyncableEntityFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-6/2-6-instance-command-fast-1778235340021-permission-flag-syncable-entity';
|
||||
import { LinkRolePermissionFlagToPermissionFlagFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-6/2-6-instance-command-fast-1778235340022-link-role-permission-flag-to-permission-flag';
|
||||
import { BackfillRolePermissionFlagPermissionFlagIdSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-6/2-6-instance-command-slow-1778235340023-backfill-role-permission-flag-permission-flag-id';
|
||||
import { AddEmailGroupChannelTypeFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-4/2-4-instance-command-fast-1778256809018-add-email-group-channel-type';
|
||||
import { AddApplicationIdToPublicDomainFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-4/2-4-instance-command-fast-1798000003000-add-application-id-to-public-domain';
|
||||
import { AddIsInternalMessagesImportEnabledFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-5/2-5-instance-command-fast-1778525104406-add-is-internal-messages-import-enabled';
|
||||
@@ -77,6 +81,10 @@ export const INSTANCE_COMMANDS = [
|
||||
RemoveUserDefaultAvatarUrlFastInstanceCommand,
|
||||
TransformApplicationVariableToSyncableEntityFastInstanceCommand,
|
||||
BackfillApplicationVariableUniversalIdentifierSlowInstanceCommand,
|
||||
RenamePermissionFlagToRolePermissionFlagFastInstanceCommand,
|
||||
PermissionFlagSyncableEntityFastInstanceCommand,
|
||||
LinkRolePermissionFlagToPermissionFlagFastInstanceCommand,
|
||||
BackfillRolePermissionFlagPermissionFlagIdSlowInstanceCommand,
|
||||
AddEmailGroupChannelTypeFastInstanceCommand,
|
||||
AddApplicationIdToPublicDomainFastInstanceCommand,
|
||||
AddIsInternalMessagesImportEnabledFastInstanceCommand,
|
||||
|
||||
Reference in New Issue
Block a user