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:
@@ -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;
|
||||
|
||||
+3
-4
@@ -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 = ({
|
||||
|
||||
+2
-3
@@ -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>({
|
||||
|
||||
+4
-9
@@ -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;
|
||||
}
|
||||
|
||||
+1
@@ -16,6 +16,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
packageJsonChecksum: '[checksum]',
|
||||
yarnLockChecksum: '[checksum]',
|
||||
},
|
||||
permissionFlags: [],
|
||||
skills: [],
|
||||
agents: [],
|
||||
publicAssets: [],
|
||||
|
||||
+1
@@ -13,6 +13,7 @@ import {
|
||||
|
||||
export const EXPECTED_MANIFEST: Manifest = {
|
||||
commandMenuItems: [],
|
||||
permissionFlags: [],
|
||||
pageLayouts: [],
|
||||
pageLayoutTabs: [
|
||||
{
|
||||
|
||||
+1
@@ -43,6 +43,7 @@ exports[`stub-twenty-sdk-define plugin > matches the recorded export partition 1
|
||||
"defineObject",
|
||||
"definePageLayout",
|
||||
"definePageLayoutTab",
|
||||
"definePermissionFlag",
|
||||
"definePostInstallLogicFunction",
|
||||
"definePreInstallLogicFunction",
|
||||
"defineRole",
|
||||
|
||||
+1
@@ -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;
|
||||
+1
@@ -71,6 +71,7 @@ export class ApplicationManifestMigrationService {
|
||||
fields: [],
|
||||
logicFunctions: [preInstallLogicFunctionManifest],
|
||||
frontComponents: [],
|
||||
permissionFlags: [],
|
||||
roles: [],
|
||||
skills: [],
|
||||
agents: [],
|
||||
|
||||
+26
@@ -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,
|
||||
};
|
||||
};
|
||||
+14
@@ -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({
|
||||
|
||||
+1
@@ -18,6 +18,7 @@ const buildMinimalManifest = (
|
||||
fields: [],
|
||||
logicFunctions: [],
|
||||
frontComponents: [],
|
||||
permissionFlags: [],
|
||||
roles: [],
|
||||
skills: [],
|
||||
agents: [],
|
||||
|
||||
+2
-4
@@ -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;
|
||||
}
|
||||
|
||||
+4
-5
@@ -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
-2
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
+5
-5
@@ -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.`,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
@@ -25,6 +25,7 @@ export const buildBaseManifest = ({
|
||||
description: 'A test role',
|
||||
},
|
||||
],
|
||||
permissionFlags: [],
|
||||
skills: [],
|
||||
agents: [],
|
||||
objects: [],
|
||||
|
||||
+4
-6
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user