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
@@ -247,7 +247,7 @@ type FieldPermission {
type RolePermissionFlag {
id: UUID!
roleId: UUID!
flag: PermissionFlagType!
flag: String!
}
type ApiKeyForRole {
@@ -4029,7 +4029,7 @@ input ObjectPermissionInput {
input UpsertPermissionFlagsInput {
roleId: UUID!
permissionFlagKeys: [PermissionFlagType!]!
permissionFlagKeys: [String!]!
}
input UpsertFieldPermissionsInput {
@@ -193,7 +193,7 @@ export interface FieldPermission {
export interface RolePermissionFlag {
id: Scalars['UUID']
roleId: Scalars['UUID']
flag: PermissionFlagType
flag: Scalars['String']
__typename: 'RolePermissionFlag'
}
@@ -6160,7 +6160,7 @@ export interface UpsertObjectPermissionsInput {roleId: Scalars['UUID'],objectPer
export interface ObjectPermissionInput {objectMetadataId: Scalars['UUID'],canReadObjectRecords?: (Scalars['Boolean'] | null),canUpdateObjectRecords?: (Scalars['Boolean'] | null),canSoftDeleteObjectRecords?: (Scalars['Boolean'] | null),canDestroyObjectRecords?: (Scalars['Boolean'] | null)}
export interface UpsertPermissionFlagsInput {roleId: Scalars['UUID'],permissionFlagKeys: PermissionFlagType[]}
export interface UpsertPermissionFlagsInput {roleId: Scalars['UUID'],permissionFlagKeys: Scalars['String'][]}
export interface UpsertFieldPermissionsInput {roleId: Scalars['UUID'],fieldPermissions: FieldPermissionInput[]}
@@ -508,7 +508,7 @@ export default {
3
],
"flag": [
18
1
],
"__typename": [
1
@@ -10279,7 +10279,7 @@ export default {
3
],
"permissionFlagKeys": [
18
1
],
"__typename": [
1
@@ -4651,7 +4651,7 @@ export type Role = {
export type RolePermissionFlag = {
__typename?: 'RolePermissionFlag';
flag: PermissionFlagType;
flag: Scalars['String'];
id: Scalars['UUID'];
roleId: Scalars['UUID'];
};
@@ -5424,7 +5424,7 @@ export type UpsertObjectPermissionsInput = {
};
export type UpsertPermissionFlagsInput = {
permissionFlagKeys: Array<PermissionFlagType>;
permissionFlagKeys: Array<Scalars['String']>;
roleId: Scalars['UUID'];
};
@@ -7347,7 +7347,7 @@ export type ObjectPermissionFragmentFragment = { __typename?: 'ObjectPermission'
export type RoleFragmentFragment = { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canBeAssignedToUsers: boolean, canBeAssignedToAgents: boolean, canBeAssignedToApiKeys: boolean };
export type RolePermissionFlagFragmentFragment = { __typename?: 'RolePermissionFlag', id: string, flag: PermissionFlagType, roleId: string };
export type RolePermissionFlagFragmentFragment = { __typename?: 'RolePermissionFlag', id: string, flag: string, roleId: string };
export type RowLevelPermissionPredicateFragmentFragment = { __typename?: 'RowLevelPermissionPredicate', id: string, fieldMetadataId: string, objectMetadataId: string, operand: RowLevelPermissionPredicateOperand, subFieldName?: string | null, workspaceMemberFieldMetadataId?: string | null, workspaceMemberSubFieldName?: string | null, rowLevelPermissionPredicateGroupId?: string | null, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, value?: any | null };
@@ -7401,7 +7401,7 @@ export type UpsertPermissionFlagsMutationVariables = Exact<{
}>;
export type UpsertPermissionFlagsMutation = { __typename?: 'Mutation', upsertPermissionFlags: Array<{ __typename?: 'RolePermissionFlag', id: string, flag: PermissionFlagType, roleId: string }> };
export type UpsertPermissionFlagsMutation = { __typename?: 'Mutation', upsertPermissionFlags: Array<{ __typename?: 'RolePermissionFlag', id: string, flag: string, roleId: string }> };
export type UpsertRowLevelPermissionPredicatesMutationVariables = Exact<{
input: UpsertRowLevelPermissionPredicatesInput;
@@ -7413,7 +7413,7 @@ export type UpsertRowLevelPermissionPredicatesMutation = { __typename?: 'Mutatio
export type GetRolesQueryVariables = Exact<{ [key: string]: never; }>;
export type GetRolesQuery = { __typename?: 'Query', getRoles: Array<{ __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canBeAssignedToUsers: boolean, canBeAssignedToAgents: boolean, canBeAssignedToApiKeys: boolean, workspaceMembers: Array<{ __typename?: 'WorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, userWorkspaceId?: string | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }>, agents: Array<{ __typename?: 'Agent', id: string, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: string | null, isCustom: boolean, modelConfiguration?: any | null, evaluationInputs: Array<string>, applicationId?: string | null, createdAt: string, updatedAt: string }>, apiKeys: Array<{ __typename?: 'ApiKeyForRole', id: string, name: string, expiresAt: string, revokedAt?: string | null }>, permissionFlags?: Array<{ __typename?: 'RolePermissionFlag', id: string, flag: PermissionFlagType, roleId: string }> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null, rowLevelPermissionPredicates?: Array<{ __typename?: 'RowLevelPermissionPredicate', id: string, fieldMetadataId: string, objectMetadataId: string, operand: RowLevelPermissionPredicateOperand, subFieldName?: string | null, workspaceMemberFieldMetadataId?: string | null, workspaceMemberSubFieldName?: string | null, rowLevelPermissionPredicateGroupId?: string | null, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, value?: any | null }> | null, rowLevelPermissionPredicateGroups?: Array<{ __typename?: 'RowLevelPermissionPredicateGroup', id: string, parentRowLevelPermissionPredicateGroupId?: string | null, logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, objectMetadataId: string }> | null }> | null, fieldPermissions?: Array<{ __typename?: 'FieldPermission', objectMetadataId: string, fieldMetadataId: string, canReadFieldValue?: boolean | null, canUpdateFieldValue?: boolean | null, id: string, roleId: string }> | null, rowLevelPermissionPredicates?: Array<{ __typename?: 'RowLevelPermissionPredicate', id: string, fieldMetadataId: string, objectMetadataId: string, operand: RowLevelPermissionPredicateOperand, subFieldName?: string | null, workspaceMemberFieldMetadataId?: string | null, workspaceMemberSubFieldName?: string | null, rowLevelPermissionPredicateGroupId?: string | null, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, value?: any | null }> | null, rowLevelPermissionPredicateGroups?: Array<{ __typename?: 'RowLevelPermissionPredicateGroup', id: string, parentRowLevelPermissionPredicateGroupId?: string | null, logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, objectMetadataId: string }> | null }> };
export type GetRolesQuery = { __typename?: 'Query', getRoles: Array<{ __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canBeAssignedToUsers: boolean, canBeAssignedToAgents: boolean, canBeAssignedToApiKeys: boolean, workspaceMembers: Array<{ __typename?: 'WorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, userWorkspaceId?: string | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }>, agents: Array<{ __typename?: 'Agent', id: string, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: string | null, isCustom: boolean, modelConfiguration?: any | null, evaluationInputs: Array<string>, applicationId?: string | null, createdAt: string, updatedAt: string }>, apiKeys: Array<{ __typename?: 'ApiKeyForRole', id: string, name: string, expiresAt: string, revokedAt?: string | null }>, permissionFlags?: Array<{ __typename?: 'RolePermissionFlag', id: string, flag: string, roleId: string }> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null, rowLevelPermissionPredicates?: Array<{ __typename?: 'RowLevelPermissionPredicate', id: string, fieldMetadataId: string, objectMetadataId: string, operand: RowLevelPermissionPredicateOperand, subFieldName?: string | null, workspaceMemberFieldMetadataId?: string | null, workspaceMemberSubFieldName?: string | null, rowLevelPermissionPredicateGroupId?: string | null, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, value?: any | null }> | null, rowLevelPermissionPredicateGroups?: Array<{ __typename?: 'RowLevelPermissionPredicateGroup', id: string, parentRowLevelPermissionPredicateGroupId?: string | null, logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, objectMetadataId: string }> | null }> | null, fieldPermissions?: Array<{ __typename?: 'FieldPermission', objectMetadataId: string, fieldMetadataId: string, canReadFieldValue?: boolean | null, canUpdateFieldValue?: boolean | null, id: string, roleId: string }> | null, rowLevelPermissionPredicates?: Array<{ __typename?: 'RowLevelPermissionPredicate', id: string, fieldMetadataId: string, objectMetadataId: string, operand: RowLevelPermissionPredicateOperand, subFieldName?: string | null, workspaceMemberFieldMetadataId?: string | null, workspaceMemberSubFieldName?: string | null, rowLevelPermissionPredicateGroupId?: string | null, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, value?: any | null }> | null, rowLevelPermissionPredicateGroups?: Array<{ __typename?: 'RowLevelPermissionPredicateGroup', id: string, parentRowLevelPermissionPredicateGroupId?: string | null, logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, objectMetadataId: string }> | null }> };
export type CreateApprovedAccessDomainMutationVariables = Exact<{
input: CreateApprovedAccessDomainInput;
@@ -1,16 +1,15 @@
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
import { type SettingsRolePermissionsSettingPermission } from '@/settings/roles/role-permissions/permission-flags/types/SettingsRolePermissionsSettingPermission';
import { type PermissionFlagType } from '~/generated-metadata/graphql';
import { WorkflowAiAgentPermissionsFlagRow } from './WorkflowAiAgentPermissionsFlagRow';
type WorkflowAiAgentPermissionsFlagListProps = {
title: string;
permissions: SettingsRolePermissionsSettingPermission[];
enabledPermissionFlagKeys: PermissionFlagType[];
enabledPermissionFlagKeys: string[];
readonly: boolean;
showDeleteButton?: boolean;
onAddPermissionFlag?: (permissionKey: PermissionFlagType) => void;
onDeletePermissionFlag?: (permissionKey: PermissionFlagType) => void;
onAddPermissionFlag?: (permissionKey: string) => void;
onDeletePermissionFlag?: (permissionKey: string) => void;
};
export const WorkflowAiAgentPermissionsFlagList = ({
@@ -1,4 +1,3 @@
import type { PermissionFlagType } from '~/generated-metadata/graphql';
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
export type FilteredPermissionsResult<T> = {
@@ -7,14 +6,14 @@ export type FilteredPermissionsResult<T> = {
};
export const getFilteredPermissions = <
T extends { key: PermissionFlagType; name: string },
T extends { key: string; name: string },
>({
permissions,
permissionFlagKeys,
searchQuery,
}: {
permissions: T[];
permissionFlagKeys: PermissionFlagType[];
permissionFlagKeys: string[];
searchQuery: string;
}): FilteredPermissionsResult<T> => {
const filteredPermissions = filterBySearchQuery<T>({
@@ -16,7 +16,6 @@ import { useMutation } from '@apollo/client/react';
import {
type Agent,
type ObjectPermission,
type PermissionFlagType,
AssignRoleToAgentDocument,
CreateOneRoleDocument,
UpsertObjectPermissionsDocument,
@@ -26,7 +25,7 @@ import {
type UseWorkflowAiAgentPermissionActionsParams = {
readonly: boolean;
objectPermissions: ObjectPermission[];
permissionFlagKeys: PermissionFlagType[];
permissionFlagKeys: string[];
refetchAgentAndRoles: () => Promise<{ refetchedAgent?: Agent }>;
};
@@ -67,7 +66,7 @@ export const useWorkflowAiAgentPermissionActions = ({
const permissionFlagLabelMap = useMemo(
() =>
[...settingsPermissionsConfig, ...actionPermissionsConfig].reduce<
Partial<Record<PermissionFlagType, string>>
Partial<Record<string, string>>
>((acc, permission) => {
acc[permission.key] = permission.name;
return acc;
@@ -296,9 +295,7 @@ export const useWorkflowAiAgentPermissionActions = ({
}
};
const handleAddPermissionFlag = async (
permissionFlagKey: PermissionFlagType,
) => {
const handleAddPermissionFlag = async (permissionFlagKey: string) => {
if (readonly) {
return;
}
@@ -323,9 +320,7 @@ export const useWorkflowAiAgentPermissionActions = ({
setWorkflowAiAgentPermissionsSelectedObjectId(undefined);
};
const handleDeletePermissionFlag = async (
permissionFlagKey: PermissionFlagType,
) => {
const handleDeletePermissionFlag = async (permissionFlagKey: string) => {
if (!isDefined(roleId) || readonly) {
return;
}
@@ -16,6 +16,7 @@ export const EXPECTED_MANIFEST: Manifest = {
packageJsonChecksum: '[checksum]',
yarnLockChecksum: '[checksum]',
},
permissionFlags: [],
skills: [],
agents: [],
publicAssets: [],
@@ -13,6 +13,7 @@ import {
export const EXPECTED_MANIFEST: Manifest = {
commandMenuItems: [],
permissionFlags: [],
pageLayouts: [],
pageLayoutTabs: [
{
@@ -43,6 +43,7 @@ exports[`stub-twenty-sdk-define plugin > matches the recorded export partition 1
"defineObject",
"definePageLayout",
"definePageLayoutTab",
"definePermissionFlag",
"definePostInstallLogicFunction",
"definePreInstallLogicFunction",
"defineRole",
@@ -31,6 +31,7 @@ const validManifest: Manifest = {
frontComponents: [],
fields: [],
logicFunctions: [],
permissionFlags: [],
roles: [],
skills: [],
agents: [],
@@ -40,6 +40,7 @@ import {
type ObjectManifest,
type PageLayoutManifest,
type PageLayoutTabManifest,
type PermissionFlagManifest,
type PostInstallLogicFunctionApplicationManifest,
type PreInstallLogicFunctionApplicationManifest,
type RoleManifest,
@@ -85,6 +86,7 @@ export const buildManifest = async (
const objects: ObjectManifest[] = [];
const fields: FieldManifest[] = [];
const indexes: IndexManifest[] = [];
const permissionFlags: PermissionFlagManifest[] = [];
const roles: RoleManifest[] = [];
const skills: SkillManifest[] = [];
const agents: AgentManifest[] = [];
@@ -106,6 +108,7 @@ export const buildManifest = async (
const objectsFilePaths: string[] = [];
const fieldsFilePaths: string[] = [];
const indexesFilePaths: string[] = [];
const permissionFlagsFilePaths: string[] = [];
const rolesFilePaths: string[] = [];
const skillsFilePaths: string[] = [];
const agentsFilePaths: string[] = [];
@@ -193,6 +196,17 @@ export const buildManifest = async (
fieldsFilePaths.push(relativePath);
break;
}
case ManifestEntityKey.PermissionFlags: {
const extract = await extractManifestFromFile<PermissionFlagManifest>({
appPath,
filePath,
});
permissionFlags.push(extract.config);
errors.push(...extract.errors);
warnings.push(...(extract.warnings ?? []));
permissionFlagsFilePaths.push(relativePath);
break;
}
case ManifestEntityKey.Roles: {
const extract = await extractManifestFromFile<RoleConfig>({
appPath,
@@ -547,6 +561,7 @@ export const buildManifest = async (
objects: objects.sort(byId),
fields: fields.sort(byId),
indexes: indexes.sort(byId),
permissionFlags: permissionFlags.sort(byId),
roles: roles.sort(byId),
skills: skills.sort(byId),
agents: agents.sort(byId),
@@ -566,6 +581,7 @@ export const buildManifest = async (
objects: objectsFilePaths,
fields: fieldsFilePaths,
indexes: indexesFilePaths,
permissionFlags: permissionFlagsFilePaths,
roles: rolesFilePaths,
skills: skillsFilePaths,
agents: agentsFilePaths,
@@ -9,6 +9,7 @@ export enum TargetFunction {
DefinePostInstallLogicFunction = 'definePostInstallLogicFunction',
DefinePreInstallLogicFunction = 'definePreInstallLogicFunction',
DefineObject = 'defineObject',
DefinePermissionFlag = 'definePermissionFlag',
DefineRole = 'defineRole',
DefineSkill = 'defineSkill',
DefineAgent = 'defineAgent',
@@ -27,6 +28,7 @@ export enum ManifestEntityKey {
Indexes = 'indexes',
LogicFunctions = 'logicFunctions',
Objects = 'objects',
PermissionFlags = 'permissionFlags',
Roles = 'roles',
Skills = 'skills',
Agents = 'agents',
@@ -56,6 +58,7 @@ export const TARGET_FUNCTION_TO_ENTITY_KEY_MAPPING: Record<
[TargetFunction.DefinePreInstallLogicFunction]:
ManifestEntityKey.LogicFunctions,
[TargetFunction.DefineObject]: ManifestEntityKey.Objects,
[TargetFunction.DefinePermissionFlag]: ManifestEntityKey.PermissionFlags,
[TargetFunction.DefineRole]: ManifestEntityKey.Roles,
[TargetFunction.DefineSkill]: ManifestEntityKey.Skills,
[TargetFunction.DefineAgent]: ManifestEntityKey.Agents,
@@ -6,6 +6,7 @@ import { type LogicFunctionConfig } from '@/sdk/define/logic-functions/logic-fun
import { type ObjectConfig } from '@/sdk/define/objects/object-config';
import { type PageLayoutConfig } from '@/sdk/define/page-layouts/page-layout-config';
import { type PageLayoutTabConfig } from '@/sdk/define/page-layouts/page-layout-tab-config';
import { type PermissionFlagConfig } from '@/sdk/define/permission-flags/permission-flag-config';
import { type ViewConfig } from '@/sdk/define/views/view-config';
import { type PostInstallLogicFunctionConfig } from '@/sdk/define/logic-functions/post-install-logic-function-config';
import { type PreInstallLogicFunctionConfig } from '@/sdk/define/logic-functions/pre-install-logic-function-config';
@@ -42,6 +43,7 @@ export type DefinableEntity =
| NavigationMenuItemManifest
| PageLayoutConfig
| PageLayoutTabConfig
| PermissionFlagConfig
| CommandMenuItemConfig;
export type DefineEntity<T extends DefinableEntity = DefinableEntity> = (
@@ -91,6 +91,13 @@ export type {
PageLayoutWidgetManifest,
} from 'twenty-shared/application';
export { definePermissionFlag } from '@/sdk/define/permission-flags/define-permission-flag';
export type { PermissionFlagConfig } from '@/sdk/define/permission-flags/permission-flag-config';
export type {
PermissionFlagManifest,
PermissionFlagPermissionType,
} from 'twenty-shared/application';
export { defineApplicationRole } from '@/sdk/define/roles/define-application-role';
export { defineRole } from '@/sdk/define/roles/define-role';
export { SystemPermissionFlag } from 'twenty-shared/constants';
@@ -0,0 +1,23 @@
import { type DefineEntity } from '@/sdk/define/common/types/define-entity.type';
import { createValidationResult } from '@/sdk/define/common/utils/create-validation-result';
import { type PermissionFlagConfig } from '@/sdk/define/permission-flags/permission-flag-config';
export const definePermissionFlag: DefineEntity<PermissionFlagConfig> = (
config,
) => {
const errors: string[] = [];
if (!config.universalIdentifier) {
errors.push('Permission flag must have a universalIdentifier');
}
if (!config.key) {
errors.push('Permission flag must have a key');
}
if (!config.label) {
errors.push('Permission flag must have a label');
}
return createValidationResult({ config, errors });
};
@@ -0,0 +1,3 @@
import { type PermissionFlagManifest } from 'twenty-shared/application';
export type PermissionFlagConfig = PermissionFlagManifest;
@@ -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: [],
@@ -1,7 +1,5 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { PermissionFlagType } from 'twenty-shared/constants';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@ObjectType('RolePermissionFlag')
@@ -12,6 +10,6 @@ export class RolePermissionFlagDTO {
@Field(() => UUIDScalarType, { nullable: false })
roleId: string;
@Field({ nullable: false })
flag: PermissionFlagType;
@Field(() => String, { nullable: false })
flag: string;
}
@@ -1,7 +1,6 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsArray, IsEnum, IsNotEmpty, IsUUID } from 'class-validator';
import { PermissionFlagType } from 'twenty-shared/constants';
import { IsArray, IsNotEmpty, IsString, IsUUID } from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@@ -13,7 +12,7 @@ export class UpsertPermissionFlagsInput {
roleId: string;
@IsArray()
@IsEnum(PermissionFlagType, { each: true })
@Field(() => [PermissionFlagType])
permissionFlagKeys: PermissionFlagType[];
@IsString({ each: true })
@Field(() => [String])
permissionFlagKeys: string[];
}
@@ -1,4 +1,3 @@
import { PermissionFlagType } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { type FlatPermissionFlagMaps } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag-maps.type';
@@ -28,6 +27,6 @@ export const fromFlatRolePermissionFlagToRolePermissionFlagDto = (
return {
id: flatRolePermissionFlag.id,
roleId: flatRolePermissionFlag.roleId,
flag: permissionFlag.key as PermissionFlagType,
flag: permissionFlag.key,
};
};
@@ -55,9 +55,9 @@ export class FlatPermissionFlagValidatorService {
});
}
const duplicateKey = Object.values(
const collidingPermissionFlag = Object.values(
optimisticFlatPermissionFlagMaps.byUniversalIdentifier,
).filter(
).find(
(definition) =>
isDefined(definition) &&
definition.key === flatPermissionFlagToValidate.key &&
@@ -65,11 +65,11 @@ export class FlatPermissionFlagValidatorService {
flatPermissionFlagToValidate.universalIdentifier,
);
if (duplicateKey.length > 0) {
if (isDefined(collidingPermissionFlag)) {
validationResult.errors.push({
code: PermissionFlagExceptionCode.PERMISSION_FLAG_ALREADY_EXISTS,
message: t`Permission flag definition with key ${flatPermissionFlagToValidate.key} already exists in this workspace`,
userFriendlyMessage: msg`A permission flag with this key already exists`,
message: t`Permission flag definition with key "${flatPermissionFlagToValidate.key}" is already registered in this workspace.`,
userFriendlyMessage: msg`Another application in this workspace has already registered a permission flag with this key.`,
});
}
@@ -62,7 +62,7 @@ describe('Granular settings permissions', () => {
mutation UpsertPermissionFlags {
upsertPermissionFlags(upsertPermissionFlagsInput: {
roleId: "${customRoleId}"
permissionFlagKeys: [${PermissionFlagType.DATA_MODEL}, ${PermissionFlagType.WORKSPACE}, ${PermissionFlagType.WORKFLOWS}]
permissionFlagKeys: ["${PermissionFlagType.DATA_MODEL}", "${PermissionFlagType.WORKSPACE}", "${PermissionFlagType.WORKFLOWS}"]
}) {
id
flag
@@ -368,7 +368,7 @@ describe('Granular settings permissions', () => {
mutation UpsertPermissionFlags {
upsertPermissionFlags(upsertPermissionFlagsInput: {
roleId: "${customRoleId}"
permissionFlagKeys: [${PermissionFlagType.DATA_MODEL}, ${PermissionFlagType.WORKSPACE}, ${PermissionFlagType.SECURITY}]
permissionFlagKeys: ["${PermissionFlagType.DATA_MODEL}", "${PermissionFlagType.WORKSPACE}", "${PermissionFlagType.SECURITY}"]
}) {
id
flag
@@ -420,7 +420,7 @@ describe('Granular settings permissions', () => {
mutation UpsertPermissionFlags {
upsertPermissionFlags(upsertPermissionFlagsInput: {
roleId: "${customRoleId}"
permissionFlagKeys: [${PermissionFlagType.DATA_MODEL}, ${PermissionFlagType.WORKSPACE}]
permissionFlagKeys: ["${PermissionFlagType.DATA_MODEL}", "${PermissionFlagType.WORKSPACE}"]
}) {
id
flag
@@ -565,7 +565,7 @@ describe('roles permissions', () => {
roleId: string;
}) => `
mutation UpsertPermissionFlags {
upsertPermissionFlags(upsertPermissionFlagsInput: {roleId: "${roleId}", permissionFlagKeys: [${PermissionFlagType.DATA_MODEL}]}) {
upsertPermissionFlags(upsertPermissionFlagsInput: {roleId: "${roleId}", permissionFlagKeys: ["${PermissionFlagType.DATA_MODEL}"]}) {
id
roleId
flag
@@ -25,6 +25,7 @@ export const buildBaseManifest = ({
description: 'A test role',
},
],
permissionFlags: [],
skills: [],
agents: [],
objects: [],
@@ -4,13 +4,11 @@ exports[`Permission flag upsert should fail when permissionFlagKeys contains inv
{
"extensions": {
"code": "BAD_USER_INPUT",
"http": {
"status": 400,
},
"userFriendlyMessage": "An error occurred.",
"subCode": "INVALID_SETTING_PERMISSIONS",
"userFriendlyMessage": "Some of the permissions you selected are not valid. Please try again with valid permission settings.",
},
"message": "Value "INVALID_FLAG" does not exist in "PermissionFlagType" enum.",
"name": "GraphQLError",
"message": "Invalid permission setting (unknown value): INVALID_FLAG",
"name": "UserInputError",
}
`;
@@ -55,6 +55,10 @@ export type {
PageLayoutTabManifest,
PageLayoutManifest,
} from './pageLayoutManifestType';
export type {
PermissionFlagPermissionType,
PermissionFlagManifest,
} from './permissionFlagManifestType';
export type { PostInstallLogicFunctionApplicationManifest } from './postInstallLogicFunctionApplicationType';
export type { PreInstallLogicFunctionApplicationManifest } from './preInstallLogicFunctionApplicationType';
export type {
@@ -15,6 +15,7 @@ import {
type PageLayoutManifest,
type PageLayoutTabManifest,
} from './pageLayoutManifestType';
import { type PermissionFlagManifest } from './permissionFlagManifestType';
import { type RoleManifest } from './roleManifestType';
import { type SkillManifest } from './skillManifestType';
import { type ViewManifest } from './viewManifestType';
@@ -26,6 +27,7 @@ export type Manifest = {
indexes?: IndexManifest[];
logicFunctions: LogicFunctionManifest[];
frontComponents: FrontComponentManifest[];
permissionFlags: PermissionFlagManifest[];
roles: RoleManifest[];
skills: SkillManifest[];
agents: AgentManifest[];
@@ -0,0 +1,10 @@
import { type SyncableEntityOptions } from '@/application/syncableEntityOptionsType';
export type PermissionFlagPermissionType = 'settings' | 'tool';
export type PermissionFlagManifest = SyncableEntityOptions & {
key: string;
label: string;
description?: string | null;
icon?: string | null;
};