Add definePermissionFlag for app-defined permission flags (#20887)

## Context
Adds the SDK plumbing for apps to declare custom permission flags and
the server-side manifest pipeline to persist them.

```typescript
import { definePermissionFlag } from 'twenty-sdk/define';

export const MANAGE_INVOICES_PERMISSION_FLAG_UNIVERSAL_IDENTIFIER = '…';

export default definePermissionFlag({
  universalIdentifier: MANAGE_INVOICES_PERMISSION_FLAG_UNIVERSAL_IDENTIFIER,
  key: 'MANAGE_INVOICES',
  label: 'Manage Invoices',
  description: 'Create, edit, and delete invoices',
  icon: 'IconReceipt',
});
```

```typescript
import { defineApplicationRole, SystemPermissionFlag } from 'twenty-sdk/define';
import { MANAGE_INVOICES_PERMISSION_FLAG_UNIVERSAL_IDENTIFIER } from './permission-flags/manage-invoices';

export default defineApplicationRole({
  universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
  label: `${APP_DISPLAY_NAME} default function role`,
  // ...
  permissionFlagUniversalIdentifiers: [
    SystemPermissionFlag.UPLOAD_FILE,
    MANAGE_INVOICES_PERMISSION_FLAG_UNIVERSAL_IDENTIFIER,
  ],
});
```

The flag can then be referenced by UUID in a role's
permissionFlagUniversalIdentifiers. On sync, the catalog row lands in
core.permissionFlag and the link in core.rolePermissionFlag.

## Not in this PR
- Runtime permission checks.
PermissionsService.getUserWorkspacePermissions still builds its result
from Object.values(PermissionFlagType), so custom flags are stored but
not yet enforced, code asking "does this role have MANAGE_INVOICES?"
won't get a meaningful answer. Widening PermissionsService and
UserWorkspacePermissions.permissionFlags to support arbitrary flag keys
is the next PR.
- PermissionFlag from apps can only define "tool" permissions and not
"settings" as a permissionType, this parameter is not mutable. This is
because "settings" are for settings page (until we might decide to
separate both type of permissions into 2 different entities) and apps
can't declare settings page or interact with them so this parameter
would be unnecessary.
This commit is contained in:
Weiko
2026-05-25 18:53:37 +02:00
committed by GitHub
parent 98d47d0dd0
commit 90f711361c
32 changed files with 157 additions and 53 deletions
@@ -71,6 +71,7 @@ export class ApplicationManifestMigrationService {
fields: [],
logicFunctions: [preInstallLogicFunctionManifest],
frontComponents: [],
permissionFlags: [],
roles: [],
skills: [],
agents: [],
@@ -0,0 +1,26 @@
import { type PermissionFlagManifest } from 'twenty-shared/application';
import { type UniversalFlatPermissionFlag } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-permission-flag.type';
export const fromPermissionFlagManifestToUniversalFlatPermissionFlag = ({
permissionFlagManifest,
applicationUniversalIdentifier,
now,
}: {
permissionFlagManifest: PermissionFlagManifest;
applicationUniversalIdentifier: string;
now: string;
}): UniversalFlatPermissionFlag => {
return {
universalIdentifier: permissionFlagManifest.universalIdentifier,
applicationUniversalIdentifier,
key: permissionFlagManifest.key,
label: permissionFlagManifest.label,
description: permissionFlagManifest.description ?? null,
icon: permissionFlagManifest.icon ?? null,
permissionType: 'tool',
rolePermissionFlagUniversalIdentifiers: [],
createdAt: now,
updatedAt: now,
};
};
@@ -19,6 +19,7 @@ import { fromObjectPermissionManifestToUniversalFlatObjectPermission } from 'src
import { fromPageLayoutManifestToUniversalFlatPageLayout } from 'src/engine/core-modules/application/application-manifest/converters/from-page-layout-manifest-to-universal-flat-page-layout.util';
import { fromPageLayoutTabManifestToUniversalFlatPageLayoutTab } from 'src/engine/core-modules/application/application-manifest/converters/from-page-layout-tab-manifest-to-universal-flat-page-layout-tab.util';
import { fromPageLayoutWidgetManifestToUniversalFlatPageLayoutWidget } from 'src/engine/core-modules/application/application-manifest/converters/from-page-layout-widget-manifest-to-universal-flat-page-layout-widget.util';
import { fromPermissionFlagManifestToUniversalFlatPermissionFlag } from 'src/engine/core-modules/application/application-manifest/converters/from-permission-flag-manifest-to-universal-flat-permission-flag.util';
import { fromPermissionFlagToUniversalFlatRolePermissionFlag } from 'src/engine/core-modules/application/application-manifest/converters/from-permission-flag-to-universal-flat-role-permission-flag.util';
import { fromRoleManifestToUniversalFlatRole } from 'src/engine/core-modules/application/application-manifest/converters/from-role-manifest-to-universal-flat-role.util';
import { fromSkillManifestToUniversalFlatSkill } from 'src/engine/core-modules/application/application-manifest/converters/from-skill-manifest-to-universal-flat-skill.util';
@@ -246,6 +247,19 @@ export const computeApplicationManifestAllUniversalFlatEntityMaps = ({
});
}
for (const permissionFlagManifest of manifest.permissionFlags ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromPermissionFlagManifestToUniversalFlatPermissionFlag({
permissionFlagManifest,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatPermissionFlagMaps,
});
}
for (const roleManifest of manifest.roles) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: fromRoleManifestToUniversalFlatRole({
@@ -18,6 +18,7 @@ const buildMinimalManifest = (
fields: [],
logicFunctions: [],
frontComponents: [],
permissionFlags: [],
roles: [],
skills: [],
agents: [],