[Breaking change] Prepare non-system permission flags (#20847)
# Summary Replaces the enum-keyed `permissionFlags: PermissionFlag[]` on roles with `permissionFlagUniversalIdentifiers: string[]` This unlocks mixing system flags (`SystemPermissionFlag.*`) with app-defined flags in a role config. This is a breaking change. Existing app source must switch to the new field. # Breaking changes - `RoleManifest.permissionFlags` removed. Use `RoleManifest.permissionFlagUniversalIdentifiers: string[]`. - `RoleConfig.permissionFlags` removed (was `PermissionFlagType[]`). Use `RoleConfig.permissionFlagUniversalIdentifiers: string[]`. - `PermissionFlagManifest` type removed from `twenty-shared/application`. - `PermissionFlag` re-export removed from `twenty-sdk/define`. `SystemPermissionFlag` is re-exported in its place. - Retargeting a permission flag between roles is now classified as delete + create instead of update ### Not in this PR - definePermissionFlag SDK function and top-level Manifest.permissionFlags catalog (apps defining their own custom flags). Until those land, permissionFlagUniversalIdentifiers only accepts SystemPermissionFlag.* UUIDs; arbitrary UUIDs fail validation.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { PermissionFlag, defineRole } from 'twenty-sdk/define';
|
||||
import { SystemPermissionFlag, defineRole } from 'twenty-sdk/define';
|
||||
import {
|
||||
CONTENT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
@@ -36,5 +36,5 @@ export default defineRole({
|
||||
canUpdateFieldValue: true,
|
||||
},
|
||||
],
|
||||
permissionFlags: [PermissionFlag.APPLICATIONS],
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.APPLICATIONS],
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PermissionFlag, defineRole } from 'twenty-sdk/define';
|
||||
import { SystemPermissionFlag, defineRole } from 'twenty-sdk/define';
|
||||
import {
|
||||
CONTENT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
@@ -36,5 +36,5 @@ export default defineRole({
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
],
|
||||
permissionFlags: [PermissionFlag.APPLICATIONS],
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.APPLICATIONS],
|
||||
});
|
||||
|
||||
+16
-3
@@ -15,10 +15,21 @@ import {
|
||||
type ObjectManifest,
|
||||
type RoleManifest,
|
||||
} from 'twenty-shared/application';
|
||||
import {
|
||||
type PermissionFlagType,
|
||||
SystemPermissionFlag,
|
||||
} from 'twenty-shared/constants';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
const SYSTEM_PERMISSION_FLAG_BY_UNIVERSAL_IDENTIFIER = Object.fromEntries(
|
||||
Object.entries(SystemPermissionFlag).map(([key, uuid]) => [
|
||||
uuid,
|
||||
key as PermissionFlagType,
|
||||
]),
|
||||
);
|
||||
|
||||
type SettingsApplicationPermissionsTabProps = {
|
||||
defaultRoleId?: string | null;
|
||||
marketplaceAppDefaultRole?: RoleManifest;
|
||||
@@ -121,12 +132,14 @@ const buildSyntheticRole = (
|
||||
canReadFieldValue: permission.canReadFieldValue,
|
||||
canUpdateFieldValue: permission.canUpdateFieldValue,
|
||||
})),
|
||||
permissionFlags: (defaultRole.permissionFlags ?? []).map(
|
||||
(permissionFlag) => ({
|
||||
permissionFlags: (defaultRole.permissionFlagUniversalIdentifiers ?? []).map(
|
||||
(permissionFlagUniversalIdentifier) => ({
|
||||
__typename: 'RolePermissionFlag' as const,
|
||||
id: uuidv4(),
|
||||
roleId: defaultRole.universalIdentifier,
|
||||
flag: permissionFlag.flag,
|
||||
flag: SYSTEM_PERMISSION_FLAG_BY_UNIVERSAL_IDENTIFIER[
|
||||
permissionFlagUniversalIdentifier
|
||||
],
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
+1
-1
@@ -330,7 +330,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
canBeAssignedToApiKeys: false,
|
||||
fieldPermissions: [],
|
||||
objectPermissions: [],
|
||||
permissionFlags: [],
|
||||
permissionFlagUniversalIdentifiers: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
+3
-8
@@ -1,6 +1,6 @@
|
||||
import { FieldType } from '@/sdk/define';
|
||||
import type { Manifest } from 'twenty-shared/application';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { SystemPermissionFlag } from 'twenty-shared/constants';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
NavigationMenuItemType,
|
||||
@@ -1385,7 +1385,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
universalIdentifier: 'c0c1c2c3-c4c5-4000-8000-000000000001',
|
||||
fieldPermissions: [],
|
||||
objectPermissions: [],
|
||||
permissionFlags: [],
|
||||
permissionFlagUniversalIdentifiers: [],
|
||||
},
|
||||
{
|
||||
canBeAssignedToAgents: false,
|
||||
@@ -1417,12 +1417,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
objectUniversalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
|
||||
},
|
||||
],
|
||||
permissionFlags: [
|
||||
{
|
||||
universalIdentifier: '01d7865a-7700-5d49-b2aa-62623c2cbac7',
|
||||
flag: PermissionFlagType.APPLICATIONS,
|
||||
},
|
||||
],
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.APPLICATIONS],
|
||||
universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
|
||||
},
|
||||
],
|
||||
|
||||
+1
-1
@@ -13,10 +13,10 @@ exports[`stub-twenty-sdk-define plugin > matches the recorded export partition 1
|
||||
"ObjectRecordGroupByDateGranularity",
|
||||
"OnDeleteAction",
|
||||
"PageLayoutTabLayoutMode",
|
||||
"PermissionFlag",
|
||||
"RelationType",
|
||||
"STANDARD_OBJECT",
|
||||
"STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS",
|
||||
"SystemPermissionFlag",
|
||||
"ViewCalendarLayout",
|
||||
"ViewFilterGroupLogicalOperator",
|
||||
"ViewFilterOperand",
|
||||
|
||||
+2
-9
@@ -28,14 +28,7 @@ export const fromRoleConfigToRoleManifest = (
|
||||
),
|
||||
}),
|
||||
),
|
||||
permissionFlags: (roleConfig.permissionFlags ?? []).map(
|
||||
(permissionFlag) => ({
|
||||
universalIdentifier: uuidv5(
|
||||
`${roleConfig.universalIdentifier}:${permissionFlag}`,
|
||||
ROLE_UNIVERSAL_IDENTIFIER_NAMESPACE,
|
||||
),
|
||||
flag: permissionFlag,
|
||||
}),
|
||||
),
|
||||
permissionFlagUniversalIdentifiers:
|
||||
roleConfig.permissionFlagUniversalIdentifiers ?? [],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -86,7 +86,7 @@ export type {
|
||||
|
||||
export { defineApplicationRole } from '@/sdk/define/roles/define-application-role';
|
||||
export { defineRole } from '@/sdk/define/roles/define-role';
|
||||
export { PermissionFlag } from '@/sdk/define/roles/permission-flag-type';
|
||||
export { SystemPermissionFlag } from 'twenty-shared/constants';
|
||||
|
||||
export { defineSkill } from '@/sdk/define/skills/define-skill';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineApplicationRole } from '@/sdk/define';
|
||||
import { SystemPermissionFlag, defineApplicationRole } from '@/sdk/define';
|
||||
|
||||
describe('defineApplicationRole', () => {
|
||||
const validConfig = {
|
||||
@@ -32,16 +32,16 @@ describe('defineApplicationRole', () => {
|
||||
expect(result.config?.canReadAllObjectRecords).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept permissionFlags', () => {
|
||||
it('should accept permissionFlagUniversalIdentifiers', () => {
|
||||
const config = {
|
||||
...validConfig,
|
||||
permissionFlags: ['UPLOAD_FILE'],
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.UPLOAD_FILE],
|
||||
};
|
||||
|
||||
const result = defineApplicationRole(config as any);
|
||||
const result = defineApplicationRole(config);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.config?.permissionFlags).toHaveLength(1);
|
||||
expect(result.config?.permissionFlagUniversalIdentifiers).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should return error when universalIdentifier is missing', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineRole } from '@/sdk/define';
|
||||
import { SystemPermissionFlag, defineRole } from '@/sdk/define';
|
||||
|
||||
describe('defineRole', () => {
|
||||
const validConfig = {
|
||||
@@ -32,16 +32,19 @@ describe('defineRole', () => {
|
||||
expect(result.config?.canReadAllObjectRecords).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept permissionFlags', () => {
|
||||
it('should accept permissionFlagUniversalIdentifiers', () => {
|
||||
const config = {
|
||||
...validConfig,
|
||||
permissionFlags: ['UPLOAD_FILE', 'DOWNLOAD_FILE'],
|
||||
permissionFlagUniversalIdentifiers: [
|
||||
SystemPermissionFlag.UPLOAD_FILE,
|
||||
SystemPermissionFlag.DOWNLOAD_FILE,
|
||||
],
|
||||
};
|
||||
|
||||
const result = defineRole(config as any);
|
||||
const result = defineRole(config);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.config?.permissionFlags).toHaveLength(2);
|
||||
expect(result.config?.permissionFlagUniversalIdentifiers).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should return error when universalIdentifier is missing', () => {
|
||||
|
||||
@@ -3,13 +3,11 @@ import type {
|
||||
ObjectPermissionManifest,
|
||||
RoleManifest,
|
||||
} from 'twenty-shared/application';
|
||||
import { type PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
export type RoleConfig = Omit<
|
||||
RoleManifest,
|
||||
'objectPermissions' | 'fieldPermissions' | 'permissionFlags'
|
||||
'objectPermissions' | 'fieldPermissions'
|
||||
> & {
|
||||
objectPermissions?: Omit<ObjectPermissionManifest, 'universalIdentifier'>[];
|
||||
fieldPermissions?: Omit<FieldPermissionManifest, 'universalIdentifier'>[];
|
||||
permissionFlags?: PermissionFlagType[];
|
||||
};
|
||||
|
||||
+27
-8
@@ -1,26 +1,45 @@
|
||||
import { type PermissionFlagManifest } from 'twenty-shared/application';
|
||||
import { SystemPermissionFlag } from 'twenty-shared/constants';
|
||||
import {
|
||||
PermissionFlagType,
|
||||
SystemPermissionFlag,
|
||||
} from 'twenty-shared/constants';
|
||||
import { v5 } from 'uuid';
|
||||
|
||||
import { type UniversalFlatRolePermissionFlag } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-role-permission-flag.type';
|
||||
|
||||
export const ROLE_PERMISSION_FLAG_UUID_NAMESPACE =
|
||||
'b9a3b3b3-58a3-4f6c-9c1f-3a4f6c9c1f3a';
|
||||
|
||||
const SYSTEM_PERMISSION_FLAG_BY_UNIVERSAL_IDENTIFIER = Object.fromEntries(
|
||||
Object.entries(SystemPermissionFlag).map(([key, uuid]) => [uuid, key]),
|
||||
) as Record<string, PermissionFlagType | undefined>;
|
||||
|
||||
export const fromPermissionFlagToUniversalFlatRolePermissionFlag = ({
|
||||
permissionFlag,
|
||||
permissionFlagUniversalIdentifier,
|
||||
roleUniversalIdentifier,
|
||||
applicationUniversalIdentifier,
|
||||
now,
|
||||
}: {
|
||||
permissionFlag: PermissionFlagManifest;
|
||||
permissionFlagUniversalIdentifier: string;
|
||||
roleUniversalIdentifier: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
now: string;
|
||||
}): UniversalFlatRolePermissionFlag => {
|
||||
const universalIdentifier = v5(
|
||||
`${roleUniversalIdentifier}:${permissionFlagUniversalIdentifier}`,
|
||||
ROLE_PERMISSION_FLAG_UUID_NAMESPACE,
|
||||
);
|
||||
|
||||
const resolvedFlag =
|
||||
SYSTEM_PERMISSION_FLAG_BY_UNIVERSAL_IDENTIFIER[
|
||||
permissionFlagUniversalIdentifier
|
||||
];
|
||||
|
||||
return {
|
||||
universalIdentifier: permissionFlag.universalIdentifier,
|
||||
universalIdentifier,
|
||||
applicationUniversalIdentifier,
|
||||
roleUniversalIdentifier,
|
||||
permissionFlagUniversalIdentifier:
|
||||
SystemPermissionFlag[permissionFlag.flag],
|
||||
flag: permissionFlag.flag,
|
||||
permissionFlagUniversalIdentifier,
|
||||
flag: resolvedFlag as PermissionFlagType,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
+3
-2
@@ -212,11 +212,12 @@ export const computeApplicationManifestAllUniversalFlatEntityMaps = ({
|
||||
});
|
||||
}
|
||||
|
||||
for (const permissionFlag of roleManifest.permissionFlags ?? []) {
|
||||
for (const permissionFlagUniversalIdentifier of roleManifest.permissionFlagUniversalIdentifiers ??
|
||||
[]) {
|
||||
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
|
||||
universalFlatEntity:
|
||||
fromPermissionFlagToUniversalFlatRolePermissionFlag({
|
||||
permissionFlag,
|
||||
permissionFlagUniversalIdentifier,
|
||||
roleUniversalIdentifier: roleManifest.universalIdentifier,
|
||||
applicationUniversalIdentifier,
|
||||
now,
|
||||
|
||||
+3
-1
@@ -105,11 +105,13 @@ exports[`Sync application should fail when retargeting existing permissions to a
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"permissionFlagUniversalIdentifier": Any<String>,
|
||||
"roleUniversalIdentifier": Any<String>,
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "rolePermissionFlag",
|
||||
"status": "fail",
|
||||
"type": "update",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
+2
-8
@@ -1,4 +1,4 @@
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { SystemPermissionFlag } from 'twenty-shared/constants';
|
||||
|
||||
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
|
||||
import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.util';
|
||||
@@ -10,7 +10,6 @@ import { STANDARD_ROLE } from 'src/engine/workspace-manager/twenty-standard-appl
|
||||
|
||||
const TEST_APP_ID = 'a1b2c3d4-0010-4000-a000-000000000010';
|
||||
const TEST_ROLE_ID = 'a1b2c3d4-0010-4000-a000-000000000011';
|
||||
const TEST_ROLE_PERMISSION_FLAG_ID = 'a1b2c3d4-0010-4000-a000-000000000012';
|
||||
const TEST_OBJECT_PERMISSION_ID = 'a1b2c3d4-0010-4000-a000-000000000013';
|
||||
const TEST_FIELD_PERMISSION_ID = 'a1b2c3d4-0010-4000-a000-000000000014';
|
||||
const FAKE_OBJECT_ID = 'a1b2c3d4-0010-4000-a000-000000000020';
|
||||
@@ -48,12 +47,7 @@ describe('Sync application should fail when creating permissions on a standard r
|
||||
label: 'Stolen Admin Role',
|
||||
description:
|
||||
'Attempts to add permissions to the standard admin role',
|
||||
permissionFlags: [
|
||||
{
|
||||
universalIdentifier: TEST_ROLE_PERMISSION_FLAG_ID,
|
||||
flag: PermissionFlagType.WORKSPACE,
|
||||
},
|
||||
],
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.WORKSPACE],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
+3
-14
@@ -1,4 +1,4 @@
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { SystemPermissionFlag } from 'twenty-shared/constants';
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
|
||||
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
|
||||
@@ -11,7 +11,6 @@ import { STANDARD_ROLE } from 'src/engine/workspace-manager/twenty-standard-appl
|
||||
|
||||
const TEST_APP_ID = 'a1b2c3d4-0020-4000-a000-000000000010';
|
||||
const TEST_ROLE_ID = 'a1b2c3d4-0020-4000-a000-000000000011';
|
||||
const TEST_ROLE_PERMISSION_FLAG_ID = 'a1b2c3d4-0020-4000-a000-000000000012';
|
||||
const TEST_OBJECT_PERMISSION_ID = 'a1b2c3d4-0020-4000-a000-000000000013';
|
||||
const TEST_FIELD_PERMISSION_ID = 'a1b2c3d4-0020-4000-a000-000000000014';
|
||||
|
||||
@@ -45,12 +44,7 @@ describe('Sync application should fail when retargeting existing permissions to
|
||||
universalIdentifier: TEST_ROLE_ID,
|
||||
label: 'App Default Role',
|
||||
description: 'Default role for the test app',
|
||||
permissionFlags: [
|
||||
{
|
||||
universalIdentifier: TEST_ROLE_PERMISSION_FLAG_ID,
|
||||
flag: PermissionFlagType.WORKSPACE,
|
||||
},
|
||||
],
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.WORKSPACE],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -75,12 +69,7 @@ describe('Sync application should fail when retargeting existing permissions to
|
||||
universalIdentifier: STANDARD_ROLE.admin.universalIdentifier,
|
||||
label: 'Admin',
|
||||
description: 'Attempts to retarget permission flag to admin role',
|
||||
permissionFlags: [
|
||||
{
|
||||
universalIdentifier: TEST_ROLE_PERMISSION_FLAG_ID,
|
||||
flag: PermissionFlagType.WORKSPACE,
|
||||
},
|
||||
],
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.WORKSPACE],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -58,7 +58,6 @@ export type { PreInstallLogicFunctionApplicationManifest } from './preInstallLog
|
||||
export type {
|
||||
ObjectPermissionManifest,
|
||||
FieldPermissionManifest,
|
||||
PermissionFlagManifest,
|
||||
RoleManifest,
|
||||
} from './roleManifestType';
|
||||
export type { ServerVariables } from './server-variables.type';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { type PermissionFlagType } from '@/constants';
|
||||
import { type SyncableEntityOptions } from '@/application/syncableEntityOptionsType';
|
||||
|
||||
export type ObjectPermissionManifest = SyncableEntityOptions & {
|
||||
@@ -16,10 +15,6 @@ export type FieldPermissionManifest = SyncableEntityOptions & {
|
||||
canUpdateFieldValue?: boolean;
|
||||
};
|
||||
|
||||
export type PermissionFlagManifest = SyncableEntityOptions & {
|
||||
flag: PermissionFlagType;
|
||||
};
|
||||
|
||||
export type RoleManifest = SyncableEntityOptions & {
|
||||
label: string;
|
||||
description?: string;
|
||||
@@ -35,5 +30,5 @@ export type RoleManifest = SyncableEntityOptions & {
|
||||
canBeAssignedToApiKeys?: boolean;
|
||||
objectPermissions?: ObjectPermissionManifest[];
|
||||
fieldPermissions?: FieldPermissionManifest[];
|
||||
permissionFlags?: PermissionFlagManifest[];
|
||||
permissionFlagUniversalIdentifiers?: string[];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user