From 3bec43696fd614d8cc885e4654b18675a400f50a Mon Sep 17 00:00:00 2001 From: Abdul Rahman <81605929+abdulrahmancodes@users.noreply.github.com> Date: Sun, 19 Oct 2025 13:00:05 +0530 Subject: [PATCH] feat: multi role permission intersection (#15150) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements permission intersection (AND logic) to prevent permission escalation when agents act on behalf of users. ### Changes: - **Permission Intersection**: Operations requiring both user AND agent permissions - **RoleContext Type**: Unified type supporting single `roleId` or multiple `roleIds` for intersection - **CRUD Services**: Updated to accept `roleContext` for granular permission control - **Agent Integration**: Chat agents now use user + agent role intersection for all operations - **ORM Layer**: Enhanced `getRepository` to support multi-role permission checks ### Related: - Part 2 of ["Acting on behalf of user" concept PR](https://github.com/twentyhq/twenty/pull/15103) [Closes #1661](https://github.com/twentyhq/core-team-issues/issues/1661) --------- Co-authored-by: Félix Malfait --- .../1-8-deduplicate-unique-fields.command.ts | 10 +- .../common-base-query-runner.service.ts | 9 +- ...common-create-many-query-runner.service.ts | 27 +- .../common-find-many-query-runner.service.ts | 17 +- .../common-find-one-query-runner.service.ts | 17 +- .../process-nested-relations-v2.helper.ts | 22 +- .../process-nested-relations.helper.ts | 10 +- .../interfaces/base-resolver-service.ts | 15 +- ...phql-query-create-many-resolver.service.ts | 8 +- ...phql-query-delete-many-resolver.service.ts | 6 +- ...aphql-query-delete-one-resolver.service.ts | 6 +- ...hql-query-destroy-many-resolver.service.ts | 6 +- ...phql-query-destroy-one-resolver.service.ts | 6 +- ...raphql-query-find-many-resolver.service.ts | 6 +- ...graphql-query-find-one-resolver.service.ts | 6 +- ...aphql-query-merge-many-resolver.service.ts | 13 +- ...hql-query-restore-many-resolver.service.ts | 6 +- ...phql-query-restore-one-resolver.service.ts | 6 +- ...phql-query-update-many-resolver.service.ts | 6 +- ...aphql-query-update-one-resolver.service.ts | 6 +- .../core/interfaces/rest-api-base.handler.ts | 5 +- .../ai/services/__tests__/mcp.service.spec.ts | 2 +- .../__tests__/tool-adapter.service.spec.ts | 17 +- .../services/__tests__/tool.service.spec.ts | 10 +- .../core-modules/ai/services/mcp.service.ts | 5 +- .../ai/services/tool-adapter.service.ts | 10 +- .../core-modules/ai/services/tool.service.ts | 50 ++- .../services/create-record.service.ts | 5 +- .../services/delete-record.service.ts | 4 +- .../services/find-records.service.ts | 4 +- .../services/update-record.service.ts | 4 +- .../types/create-record-params.type.ts | 3 +- .../types/delete-record-params.type.ts | 4 +- .../types/find-records-params.type.ts | 4 +- .../types/update-record-params.type.ts | 3 +- .../agent/agent-execution.service.ts | 26 +- .../agent/agent-tool-generator.service.ts | 32 +- .../services/agent-actor-context.service.ts | 16 +- .../field-permissions.service.spec.ts | 12 +- .../field-permission.service.ts | 4 +- .../permissions/permissions.service.ts | 102 +++++- ...space-permissions-cache-storage.service.ts | 10 +- .../workspace-permissions-cache.service.ts | 24 +- .../datasource/workspace.datasource.ts | 36 +- .../workspace-entity-manager.spec.ts | 12 +- .../workspace-entity-manager.ts | 76 ++-- .../factories/workspace-datasource.factory.ts | 8 +- .../__tests__/workspace.repository.spec.ts | 12 +- .../repository/permissions.utils.ts | 14 +- .../workspace-delete-query-builder.ts | 8 +- .../workspace-insert-query-builder.ts | 8 +- .../workspace-select-query-builder.ts | 6 +- .../workspace-soft-delete-query-builder.ts | 8 +- .../workspace-update-query-builder.ts | 8 +- .../repository/workspace.repository.ts | 6 +- .../twenty-orm/twenty-orm-global.manager.ts | 21 +- .../engine/twenty-orm/twenty-orm.manager.ts | 3 +- .../types/role-permission-config.ts | 6 + ...mpute-permission-intersection.util.spec.ts | 340 ++++++++++++++++++ .../compute-permission-intersection.util.ts | 92 +++++ .../workflow-execution-context.service.ts | 6 +- .../types/workflow-execution-context.type.ts | 3 +- .../ai-agent/ai-agent.workflow-action.ts | 2 +- .../services/ai-agent-executor.service.ts | 38 +- .../create-record.workflow-action.ts | 2 +- .../delete-record.workflow-action.ts | 2 +- .../find-records.workflow-action.ts | 2 +- .../update-record.workflow-action.ts | 2 +- .../workflow-tool.workspace-service.ts | 30 +- .../agent-tool.service.integration-spec.ts | 45 +-- .../agent/utils/agent-tool-test-utils.ts | 6 +- .../src/types/ObjectPermissionsDeprecated.ts | 10 - .../ObjectsPermissionsByRoleIdDeprecated.ts | 10 - .../src/types/ObjectsPermissionsDeprecated.ts | 9 - packages/twenty-shared/src/types/index.ts | 3 - 75 files changed, 909 insertions(+), 489 deletions(-) create mode 100644 packages/twenty-server/src/engine/twenty-orm/types/role-permission-config.ts create mode 100644 packages/twenty-server/src/engine/twenty-orm/utils/__tests__/compute-permission-intersection.util.spec.ts create mode 100644 packages/twenty-server/src/engine/twenty-orm/utils/compute-permission-intersection.util.ts delete mode 100644 packages/twenty-shared/src/types/ObjectPermissionsDeprecated.ts delete mode 100644 packages/twenty-shared/src/types/ObjectsPermissionsByRoleIdDeprecated.ts delete mode 100644 packages/twenty-shared/src/types/ObjectsPermissionsDeprecated.ts diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-8/1-8-deduplicate-unique-fields.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-8/1-8-deduplicate-unique-fields.command.ts index b5e9f231d0..6891847354 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-8/1-8-deduplicate-unique-fields.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-8/1-8-deduplicate-unique-fields.command.ts @@ -187,7 +187,7 @@ export class DeduplicateUniqueFieldsCommand extends ActiveOrSuspendedWorkspacesM }) { const workspaceMemberRepository = dataSource.getRepository( 'workspaceMember', - true, + { shouldBypassPermissionChecks: true }, ); const duplicates = await workspaceMemberRepository @@ -245,7 +245,9 @@ export class DeduplicateUniqueFieldsCommand extends ActiveOrSuspendedWorkspacesM dataSource: WorkspaceDataSource; dryRun: boolean; }) { - const companyRepository = dataSource.getRepository('company', true); + const companyRepository = dataSource.getRepository('company', { + shouldBypassPermissionChecks: true, + }); const duplicates = await companyRepository .createQueryBuilder('company') @@ -303,7 +305,9 @@ export class DeduplicateUniqueFieldsCommand extends ActiveOrSuspendedWorkspacesM dataSource: WorkspaceDataSource; dryRun: boolean; }) { - const personRepository = dataSource.getRepository('person', true); + const personRepository = dataSource.getRepository('person', { + shouldBypassPermissionChecks: true, + }); const duplicates = await personRepository .createQueryBuilder('person') diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts index 87b41a6450..8b955f573b 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts @@ -79,19 +79,18 @@ export abstract class CommonBaseQueryRunnerService { workspace.id, ); + const rolePermissionConfig = { unionOf: [roleId] }; + const repository = workspaceDataSource.getRepository( objectMetadataItemWithFieldMaps.nameSingular, - false, - roleId, + rolePermissionConfig, authContext, ); return { workspaceDataSource, repository, - isExecutedByApiKey: isDefined(authContext.apiKey), - roleId, - shouldBypassPermissionChecks: false, + rolePermissionConfig, }; } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service.ts index 3cfbccaa49..16f06cebc7 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service.ts @@ -31,6 +31,7 @@ import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/typ import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; +import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; @Injectable() export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerService { @@ -62,15 +63,11 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer } }); - const { - workspaceDataSource, - repository, - roleId, - shouldBypassPermissionChecks, - } = await this.prepareQueryRunnerContext({ - authContext, - objectMetadataItemWithFieldMaps, - }); + const { workspaceDataSource, repository, rolePermissionConfig } = + await this.prepareQueryRunnerContext({ + authContext, + objectMetadataItemWithFieldMaps, + }); const processedArgs = await this.processQueryArgs({ authContext, @@ -98,10 +95,9 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer records: upsertedRecords, objectMetadataItemWithFieldMaps, objectMetadataMaps, - roleId, + rolePermissionConfig, authContext, workspaceDataSource, - shouldBypassPermissionChecks, }); return upsertedRecords; @@ -371,19 +367,17 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer records, objectMetadataItemWithFieldMaps, objectMetadataMaps, - roleId, + rolePermissionConfig, authContext, workspaceDataSource, - shouldBypassPermissionChecks, }: { args: CreateManyQueryArgs; records: ObjectRecord[]; objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; objectMetadataMaps: ObjectMetadataMaps; - roleId?: string; + rolePermissionConfig?: RolePermissionConfig; authContext: AuthContext; workspaceDataSource: WorkspaceDataSource; - shouldBypassPermissionChecks: boolean; }): Promise { if (!args.selectedFieldsResult.relations) { return; @@ -401,8 +395,7 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer limit: QUERY_MAX_RECORDS, authContext, workspaceDataSource, - roleId, - shouldBypassPermissionChecks, + rolePermissionConfig, selectedFields: args.selectedFieldsResult.select, }); } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-many-query-runner.service.ts index ceb5f583e1..1b5b48d276 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-many-query-runner.service.ts @@ -60,15 +60,11 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi ); } - const { - workspaceDataSource, - repository, - roleId, - shouldBypassPermissionChecks, - } = await this.prepareQueryRunnerContext({ - authContext, - objectMetadataItemWithFieldMaps, - }); + const { workspaceDataSource, repository, rolePermissionConfig } = + await this.prepareQueryRunnerContext({ + authContext, + objectMetadataItemWithFieldMaps, + }); const processedArgs = await this.processQueryArgs({ authContext, @@ -195,8 +191,7 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi limit: QUERY_MAX_RECORDS, authContext, workspaceDataSource, - roleId, - shouldBypassPermissionChecks, + rolePermissionConfig, selectedFields: processedArgs.selectedFieldsResult.select, }); } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-one-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-one-query-runner.service.ts index 3aa1c3c3e1..ba64973c39 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-one-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-one-query-runner.service.ts @@ -46,15 +46,11 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic ); } - const { - workspaceDataSource, - repository, - roleId, - shouldBypassPermissionChecks, - } = await this.prepareQueryRunnerContext({ - authContext, - objectMetadataItemWithFieldMaps, - }); + const { workspaceDataSource, repository, rolePermissionConfig } = + await this.prepareQueryRunnerContext({ + authContext, + objectMetadataItemWithFieldMaps, + }); const processedArgs = await this.processQueryArgs({ authContext, @@ -128,8 +124,7 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic limit: QUERY_MAX_RECORDS, authContext, workspaceDataSource, - roleId, - shouldBypassPermissionChecks, + rolePermissionConfig, selectedFields: args.selectedFieldsResult.select, }); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts index 87460b4e4b..152d75eb3a 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts @@ -20,6 +20,7 @@ import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/objec import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; import { type WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; import { type WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder'; +import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; @Injectable() @@ -36,8 +37,7 @@ export class ProcessNestedRelationsV2Helper { limit, authContext, workspaceDataSource, - roleId, - shouldBypassPermissionChecks, + rolePermissionConfig, selectedFields, }: { objectMetadataMaps: ObjectMetadataMaps; @@ -50,10 +50,9 @@ export class ProcessNestedRelationsV2Helper { limit: number; authContext: AuthContext; workspaceDataSource: WorkspaceDataSource; - shouldBypassPermissionChecks: boolean; + rolePermissionConfig?: RolePermissionConfig; // eslint-disable-next-line @typescript-eslint/no-explicit-any selectedFields: Record; - roleId?: string; }): Promise { const processRelationTasks = Object.entries(relations).map( ([sourceFieldName, nestedRelations]) => @@ -68,8 +67,7 @@ export class ProcessNestedRelationsV2Helper { limit, authContext, workspaceDataSource, - shouldBypassPermissionChecks, - roleId, + rolePermissionConfig, selectedFields: selectedFields[sourceFieldName] instanceof Object ? selectedFields[sourceFieldName] @@ -91,8 +89,7 @@ export class ProcessNestedRelationsV2Helper { limit, authContext, workspaceDataSource, - shouldBypassPermissionChecks, - roleId, + rolePermissionConfig, selectedFields, }: { objectMetadataMaps: ObjectMetadataMaps; @@ -106,8 +103,7 @@ export class ProcessNestedRelationsV2Helper { limit: number; authContext: AuthContext; workspaceDataSource: WorkspaceDataSource; - shouldBypassPermissionChecks: boolean; - roleId?: string; + rolePermissionConfig?: RolePermissionConfig; selectedFields: Record; }): Promise { const sourceFieldMetadata = getFieldMetadataFromGraphQLField({ @@ -147,8 +143,7 @@ export class ProcessNestedRelationsV2Helper { const targetObjectRepository = workspaceDataSource.getRepository( targetObjectMetadata.nameSingular, - shouldBypassPermissionChecks, - roleId, + rolePermissionConfig, ); const targetObjectNameSingular = targetObjectMetadata.nameSingular; @@ -240,8 +235,7 @@ export class ProcessNestedRelationsV2Helper { limit, authContext, workspaceDataSource, - shouldBypassPermissionChecks, - roleId, + rolePermissionConfig, selectedFields, }); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts index af7ecdb80a..f7fa7bba55 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts @@ -9,6 +9,7 @@ import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-contex import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { type WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; +import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; @Injectable() export class ProcessNestedRelationsHelper { @@ -26,8 +27,7 @@ export class ProcessNestedRelationsHelper { limit, authContext, workspaceDataSource, - shouldBypassPermissionChecks, - roleId, + rolePermissionConfig, selectedFields, }: { objectMetadataMaps: ObjectMetadataMaps; @@ -40,10 +40,9 @@ export class ProcessNestedRelationsHelper { limit: number; authContext: AuthContext; workspaceDataSource: WorkspaceDataSource; - shouldBypassPermissionChecks: boolean; + rolePermissionConfig?: RolePermissionConfig; // eslint-disable-next-line @typescript-eslint/no-explicit-any selectedFields: Record; - roleId?: string; }): Promise { return this.processNestedRelationsV2Helper.processNestedRelations({ objectMetadataMaps, @@ -55,8 +54,7 @@ export class ProcessNestedRelationsHelper { limit, authContext, workspaceDataSource, - shouldBypassPermissionChecks, - roleId, + rolePermissionConfig, selectedFields, }); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts index 8de842623b..53d094f3f1 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts @@ -40,6 +40,7 @@ import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role import { type WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; export type GraphqlQueryResolverExecutionArgs = { args: Input; @@ -49,8 +50,7 @@ export type GraphqlQueryResolverExecutionArgs = { graphqlQueryParser: GraphqlQueryParser; graphqlQuerySelectedFieldsResult: GraphqlQuerySelectedFieldsResult; isExecutedByApiKey: boolean; - roleId?: string; - shouldBypassPermissionChecks: boolean; + rolePermissionConfig?: RolePermissionConfig; }; @Injectable() @@ -120,7 +120,6 @@ export abstract class GraphqlQueryBaseResolverService< )) as Input; let roleId: string | undefined; - let shouldBypassPermissionChecks = false; if (isDefined(authContext.apiKey)) { roleId = await this.apiKeyRoleService.getRoleIdForApiKey( @@ -152,11 +151,12 @@ export abstract class GraphqlQueryBaseResolverService< PermissionsExceptionCode.NO_AUTHENTICATION_CONTEXT, ); } - + const rolePermissionConfig: RolePermissionConfig | undefined = roleId + ? { unionOf: [roleId] } + : undefined; const repository = workspaceDataSource.getRepository( objectMetadataItemWithFieldMaps.nameSingular, - shouldBypassPermissionChecks, - roleId, + rolePermissionConfig, authContext, ); @@ -182,8 +182,7 @@ export abstract class GraphqlQueryBaseResolverService< graphqlQueryParser, graphqlQuerySelectedFieldsResult, isExecutedByApiKey: isDefined(authContext.apiKey), - roleId, - shouldBypassPermissionChecks, + rolePermissionConfig, }; const results = await this.resolve( graphqlQueryResolverExecutionArgs, diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts index 7bf701ee44..7a63342702 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts @@ -55,8 +55,6 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol const { objectMetadataItemWithFieldMaps, objectMetadataMaps } = executionArgs.options; - const { roleId } = executionArgs; - const objectRecords = await this.insertOrUpsertRecords(executionArgs); const upsertedRecords = await this.fetchUpsertedRecords( @@ -71,7 +69,6 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol records: upsertedRecords, objectMetadataItemWithFieldMaps, objectMetadataMaps, - roleId, }); return this.formatRecordsForResponse( @@ -462,13 +459,11 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol records, objectMetadataItemWithFieldMaps, objectMetadataMaps, - roleId, }: { executionArgs: GraphqlQueryResolverExecutionArgs; records: ObjectRecord[]; objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; objectMetadataMaps: ObjectMetadataMaps; - roleId?: string; }): Promise { if (!executionArgs.graphqlQuerySelectedFieldsResult.relations) { return; @@ -482,8 +477,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol limit: QUERY_MAX_RECORDS, authContext: executionArgs.options.authContext, workspaceDataSource: executionArgs.workspaceDataSource, - roleId, - shouldBypassPermissionChecks: executionArgs.shouldBypassPermissionChecks, + rolePermissionConfig: executionArgs.rolePermissionConfig, selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select, }); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service.ts index dac5d78681..6332200f60 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service.ts @@ -27,8 +27,6 @@ export class GraphqlQueryDeleteManyResolverService extends GraphqlQueryBaseResol const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = executionArgs.options; - const { roleId } = executionArgs; - const queryBuilder = executionArgs.repository.createQueryBuilder( objectMetadataItemWithFieldMaps.nameSingular, ); @@ -66,9 +64,7 @@ export class GraphqlQueryDeleteManyResolverService extends GraphqlQueryBaseResol limit: QUERY_MAX_RECORDS, authContext, workspaceDataSource: executionArgs.workspaceDataSource, - roleId, - shouldBypassPermissionChecks: - executionArgs.shouldBypassPermissionChecks, + rolePermissionConfig: executionArgs.rolePermissionConfig, selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select, }); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service.ts index f4189337a7..1ae7925e2e 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service.ts @@ -30,8 +30,6 @@ export class GraphqlQueryDeleteOneResolverService extends GraphqlQueryBaseResolv const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = executionArgs.options; - const { roleId } = executionArgs; - const queryBuilder = executionArgs.repository.createQueryBuilder( objectMetadataItemWithFieldMaps.nameSingular, ); @@ -67,9 +65,7 @@ export class GraphqlQueryDeleteOneResolverService extends GraphqlQueryBaseResolv limit: QUERY_MAX_RECORDS, authContext, workspaceDataSource: executionArgs.workspaceDataSource, - roleId, - shouldBypassPermissionChecks: - executionArgs.shouldBypassPermissionChecks, + rolePermissionConfig: executionArgs.rolePermissionConfig, selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select, }); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts index b045732caf..043791d74f 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts @@ -24,8 +24,6 @@ export class GraphqlQueryDestroyManyResolverService extends GraphqlQueryBaseReso const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = executionArgs.options; - const { roleId } = executionArgs; - const queryBuilder = executionArgs.repository.createQueryBuilder( objectMetadataItemWithFieldMaps.nameSingular, ); @@ -58,9 +56,7 @@ export class GraphqlQueryDestroyManyResolverService extends GraphqlQueryBaseReso limit: QUERY_MAX_RECORDS, authContext, workspaceDataSource: executionArgs.workspaceDataSource, - roleId, - shouldBypassPermissionChecks: - executionArgs.shouldBypassPermissionChecks, + rolePermissionConfig: executionArgs.rolePermissionConfig, selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select, }); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts index e5b6ca3d5e..c25fd7f548 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts @@ -28,8 +28,6 @@ export class GraphqlQueryDestroyOneResolverService extends GraphqlQueryBaseResol const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = executionArgs.options; - const { roleId } = executionArgs; - const queryBuilder = executionArgs.repository.createQueryBuilder( objectMetadataItemWithFieldMaps.nameSingular, ); @@ -64,9 +62,7 @@ export class GraphqlQueryDestroyOneResolverService extends GraphqlQueryBaseResol limit: QUERY_MAX_RECORDS, authContext, workspaceDataSource: executionArgs.workspaceDataSource, - roleId, - shouldBypassPermissionChecks: - executionArgs.shouldBypassPermissionChecks, + rolePermissionConfig: executionArgs.rolePermissionConfig, selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select, }); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts index 31e5fb356a..91b26d433f 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts @@ -47,8 +47,6 @@ export class GraphqlQueryFindManyResolverService extends GraphqlQueryBaseResolve const objectMetadataNameSingular = objectMetadataItemWithFieldMaps.nameSingular; - const { roleId } = executionArgs; - const queryBuilder = executionArgs.repository.createQueryBuilder( objectMetadataNameSingular, ); @@ -163,9 +161,7 @@ export class GraphqlQueryFindManyResolverService extends GraphqlQueryBaseResolve limit: QUERY_MAX_RECORDS, authContext, workspaceDataSource: executionArgs.workspaceDataSource, - roleId, - shouldBypassPermissionChecks: - executionArgs.shouldBypassPermissionChecks, + rolePermissionConfig: executionArgs.rolePermissionConfig, selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select, }); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts index 4d42026ca7..296ecf27e4 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts @@ -34,8 +34,6 @@ export class GraphqlQueryFindOneResolverService extends GraphqlQueryBaseResolver const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = executionArgs.options; - const { roleId } = executionArgs; - const queryBuilder = executionArgs.repository.createQueryBuilder( objectMetadataItemWithFieldMaps.nameSingular, ); @@ -82,9 +80,7 @@ export class GraphqlQueryFindOneResolverService extends GraphqlQueryBaseResolver limit: QUERY_MAX_RECORDS, authContext, workspaceDataSource: executionArgs.workspaceDataSource, - roleId, - shouldBypassPermissionChecks: - executionArgs.shouldBypassPermissionChecks, + rolePermissionConfig: executionArgs.rolePermissionConfig, selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select, }); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-merge-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-merge-many-resolver.service.ts index ae69c6377f..37a9e95a9c 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-merge-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-merge-many-resolver.service.ts @@ -47,7 +47,6 @@ export class GraphqlQueryMergeManyResolverService extends GraphqlQueryBaseResolv const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = executionArgs.options; const { ids, conflictPriorityIndex, dryRun } = executionArgs.args; - const { roleId } = executionArgs; const recordsToMerge = await this.fetchRecordsToMerge(executionArgs, ids); @@ -96,12 +95,11 @@ export class GraphqlQueryMergeManyResolverService extends GraphqlQueryBaseResolv mergedData, ); - if (roleId) { + if (executionArgs.rolePermissionConfig) { await this.processNestedRelations({ executionArgs, updatedRecords: [updatedRecord], authContext, - roleId, }); } @@ -335,8 +333,7 @@ export class GraphqlQueryMergeManyResolverService extends GraphqlQueryBaseResolv try { const repository = executionArgs.workspaceDataSource.getRepository( relationField.objectMetadata.nameSingular, - executionArgs.isExecutedByApiKey, - executionArgs.roleId, + executionArgs.rolePermissionConfig, ); const whereCondition = { [relationField.joinColumnName]: In(fromIds) }; @@ -363,12 +360,10 @@ export class GraphqlQueryMergeManyResolverService extends GraphqlQueryBaseResolv executionArgs, updatedRecords, authContext, - roleId, }: { executionArgs: GraphqlQueryResolverExecutionArgs; updatedRecords: ObjectRecord[]; authContext: AuthContext; - roleId: string; }): Promise { const { objectMetadataMaps, objectMetadataItemWithFieldMaps } = executionArgs.options; @@ -382,9 +377,7 @@ export class GraphqlQueryMergeManyResolverService extends GraphqlQueryBaseResolv limit: QUERY_MAX_RECORDS, authContext, workspaceDataSource: executionArgs.workspaceDataSource, - roleId, - shouldBypassPermissionChecks: - executionArgs.shouldBypassPermissionChecks, + rolePermissionConfig: executionArgs.rolePermissionConfig, selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select, }); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service.ts index f3af4f5d70..851036a77b 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service.ts @@ -27,8 +27,6 @@ export class GraphqlQueryRestoreManyResolverService extends GraphqlQueryBaseReso const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = executionArgs.options; - const { roleId } = executionArgs; - const queryBuilder = executionArgs.repository.createQueryBuilder( objectMetadataItemWithFieldMaps.nameSingular, ); @@ -66,9 +64,7 @@ export class GraphqlQueryRestoreManyResolverService extends GraphqlQueryBaseReso limit: QUERY_MAX_RECORDS, authContext, workspaceDataSource: executionArgs.workspaceDataSource, - roleId, - shouldBypassPermissionChecks: - executionArgs.shouldBypassPermissionChecks, + rolePermissionConfig: executionArgs.rolePermissionConfig, selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select, }); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service.ts index bd8eb2d223..abeb78b26d 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service.ts @@ -30,8 +30,6 @@ export class GraphqlQueryRestoreOneResolverService extends GraphqlQueryBaseResol const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = executionArgs.options; - const { roleId } = executionArgs; - const queryBuilder = executionArgs.repository.createQueryBuilder( objectMetadataItemWithFieldMaps.nameSingular, ); @@ -68,9 +66,7 @@ export class GraphqlQueryRestoreOneResolverService extends GraphqlQueryBaseResol limit: QUERY_MAX_RECORDS, authContext, workspaceDataSource: executionArgs.workspaceDataSource, - roleId, - shouldBypassPermissionChecks: - executionArgs.shouldBypassPermissionChecks, + rolePermissionConfig: executionArgs.rolePermissionConfig, selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select, }); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts index edf8a28367..9354667daa 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts @@ -27,8 +27,6 @@ export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResol const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = executionArgs.options; - const { roleId } = executionArgs; - const queryBuilder = executionArgs.repository.createQueryBuilder( objectMetadataItemWithFieldMaps.nameSingular, ); @@ -67,9 +65,7 @@ export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResol limit: QUERY_MAX_RECORDS, authContext, workspaceDataSource: executionArgs.workspaceDataSource, - roleId, - shouldBypassPermissionChecks: - executionArgs.shouldBypassPermissionChecks, + rolePermissionConfig: executionArgs.rolePermissionConfig, selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select, }); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts index e5b26556e0..911966ea63 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts @@ -30,8 +30,6 @@ export class GraphqlQueryUpdateOneResolverService extends GraphqlQueryBaseResolv const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = executionArgs.options; - const { roleId } = executionArgs; - const queryBuilder = executionArgs.repository.createQueryBuilder( objectMetadataItemWithFieldMaps.nameSingular, ); @@ -68,9 +66,7 @@ export class GraphqlQueryUpdateOneResolverService extends GraphqlQueryBaseResolv limit: QUERY_MAX_RECORDS, authContext, workspaceDataSource: executionArgs.workspaceDataSource, - roleId, - shouldBypassPermissionChecks: - executionArgs.shouldBypassPermissionChecks, + rolePermissionConfig: executionArgs.rolePermissionConfig, selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select, }); } diff --git a/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts b/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts index 6cf87062c6..b0645a92a4 100644 --- a/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts @@ -175,8 +175,9 @@ export abstract class RestApiBaseHandler { const repository = workspaceDataSource.getRepository( objectMetadataNameSingular, - false, - roleId, + { + unionOf: [roleId], + }, ); const objectMetadataPermissions = diff --git a/packages/twenty-server/src/engine/core-modules/ai/services/__tests__/mcp.service.spec.ts b/packages/twenty-server/src/engine/core-modules/ai/services/__tests__/mcp.service.spec.ts index be552b65a8..bc07ae7662 100644 --- a/packages/twenty-server/src/engine/core-modules/ai/services/__tests__/mcp.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/ai/services/__tests__/mcp.service.spec.ts @@ -285,7 +285,7 @@ describe('McpService', () => { }); expect(toolService.listTools).toHaveBeenCalledWith( - mockAdminRoleId, + { unionOf: [mockAdminRoleId] }, mockWorkspace.id, ); expect(mockTool.execute).toHaveBeenCalledWith( diff --git a/packages/twenty-server/src/engine/core-modules/ai/services/__tests__/tool-adapter.service.spec.ts b/packages/twenty-server/src/engine/core-modules/ai/services/__tests__/tool-adapter.service.spec.ts index 51bc7bb26d..d5292ff36e 100644 --- a/packages/twenty-server/src/engine/core-modules/ai/services/__tests__/tool-adapter.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/ai/services/__tests__/tool-adapter.service.spec.ts @@ -9,6 +9,7 @@ import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.ty import { type Tool } from 'src/engine/core-modules/tool/types/tool.type'; import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; +import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; const createMockToolRegistry = () => ({ getAllToolTypes: jest.fn(), @@ -18,7 +19,7 @@ const createMockToolRegistry = () => ({ const createMockPermissions = () => ({ hasToolPermission: jest.fn< Promise, - [string, string, PermissionFlagType] + [RolePermissionConfig, string, PermissionFlagType] >(), }); @@ -89,7 +90,9 @@ describe('ToolAdapterService', () => { expect(Object.keys(toolsNoContext)).toContain('http_request'); - const toolsWithPartialContext = await service.getTools('role-1'); + const toolsWithPartialContext = await service.getTools({ + unionOf: ['role-1'], + }); expect(Object.keys(toolsWithPartialContext)).toContain('http_request'); }); @@ -99,7 +102,9 @@ describe('ToolAdapterService', () => { expect(Object.keys(toolsNoContext)).not.toContain('send_email'); - const toolsRoleOnly = await service.getTools('role-1'); + const toolsRoleOnly = await service.getTools({ + unionOf: ['role-1'], + }); expect(Object.keys(toolsRoleOnly)).not.toContain('send_email'); @@ -111,10 +116,10 @@ describe('ToolAdapterService', () => { it('should include flagged tools when permission is granted', async () => { mockPermissions.hasToolPermission.mockResolvedValueOnce(true); - const tools = await service.getTools('role-1', 'ws-1'); + const tools = await service.getTools({ unionOf: ['role-1'] }, 'ws-1'); expect(mockPermissions.hasToolPermission).toHaveBeenCalledWith( - 'role-1', + { unionOf: ['role-1'] }, 'ws-1', PermissionFlagType.SEND_EMAIL_TOOL, ); @@ -125,7 +130,7 @@ describe('ToolAdapterService', () => { it('should exclude flagged tools when permission is denied', async () => { mockPermissions.hasToolPermission.mockResolvedValueOnce(false); - const tools = await service.getTools('role-1', 'ws-1'); + const tools = await service.getTools({ unionOf: ['role-1'] }, 'ws-1'); expect(Object.keys(tools)).not.toContain('send_email'); }); diff --git a/packages/twenty-server/src/engine/core-modules/ai/services/__tests__/tool.service.spec.ts b/packages/twenty-server/src/engine/core-modules/ai/services/__tests__/tool.service.spec.ts index 1b47a57120..e3d0e342a4 100644 --- a/packages/twenty-server/src/engine/core-modules/ai/services/__tests__/tool.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/ai/services/__tests__/tool.service.spec.ts @@ -69,10 +69,10 @@ describe('ToolService', () => { data: { [roleId]: { [testObject.id]: { - canRead: true, - canUpdate: true, - canSoftDelete: true, - canDestroy: false, + canReadObjectRecords: true, + canUpdateObjectRecords: true, + canSoftDeleteObjectRecords: true, + canDestroyObjectRecords: false, restrictedFields: {}, }, }, @@ -128,7 +128,7 @@ describe('ToolService', () => { describe('listTools', () => { it('should return tools based on role permissions', async () => { - const tools = await service.listTools(roleId, workspaceId); + const tools = await service.listTools({ unionOf: [roleId] }, workspaceId); expect( permissionsCacheService.getRolesPermissionsFromCache, diff --git a/packages/twenty-server/src/engine/core-modules/ai/services/mcp.service.ts b/packages/twenty-server/src/engine/core-modules/ai/services/mcp.service.ts index 3b3564296e..2fa9272f5e 100644 --- a/packages/twenty-server/src/engine/core-modules/ai/services/mcp.service.ts +++ b/packages/twenty-server/src/engine/core-modules/ai/services/mcp.service.ts @@ -124,7 +124,10 @@ export class McpService { apiKey, ); - const toolSet = await this.toolService.listTools(roleId, workspace.id); + const toolSet = await this.toolService.listTools( + { unionOf: [roleId] }, + workspace.id, + ); if (method === 'tools/call' && params) { return await this.handleToolCall(id, toolSet, params); diff --git a/packages/twenty-server/src/engine/core-modules/ai/services/tool-adapter.service.ts b/packages/twenty-server/src/engine/core-modules/ai/services/tool-adapter.service.ts index 26a2545d23..5e2797e55f 100644 --- a/packages/twenty-server/src/engine/core-modules/ai/services/tool-adapter.service.ts +++ b/packages/twenty-server/src/engine/core-modules/ai/services/tool-adapter.service.ts @@ -7,6 +7,7 @@ import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.ty import { type Tool } from 'src/engine/core-modules/tool/types/tool.type'; import { type PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; +import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; @Injectable() export class ToolAdapterService { @@ -15,7 +16,10 @@ export class ToolAdapterService { private readonly permissionsService: PermissionsService, ) {} - async getTools(roleId?: string, workspaceId?: string): Promise { + async getTools( + rolePermissionConfig?: RolePermissionConfig, + workspaceId?: string, + ): Promise { const tools: ToolSet = {}; for (const toolType of this.toolRegistry.getAllToolTypes()) { @@ -23,9 +27,9 @@ export class ToolAdapterService { if (!tool.flag) { tools[toolType.toLowerCase()] = this.createToolSet(tool); - } else if (roleId && workspaceId) { + } else if (rolePermissionConfig && workspaceId) { const hasPermission = await this.permissionsService.hasToolPermission( - roleId, + rolePermissionConfig, workspaceId, tool.flag as PermissionFlagType, ); diff --git a/packages/twenty-server/src/engine/core-modules/ai/services/tool.service.ts b/packages/twenty-server/src/engine/core-modules/ai/services/tool.service.ts index dd0af3e389..d0dab78958 100644 --- a/packages/twenty-server/src/engine/core-modules/ai/services/tool.service.ts +++ b/packages/twenty-server/src/engine/core-modules/ai/services/tool.service.ts @@ -18,6 +18,8 @@ import { type ActorMetadata } from 'src/engine/metadata-modules/field-metadata/c import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; +import { computePermissionIntersection } from 'src/engine/twenty-orm/utils/compute-permission-intersection.util'; @Injectable() export class ToolService { @@ -32,7 +34,7 @@ export class ToolService { ) {} async listTools( - roleId: string, + rolePermissionConfig: RolePermissionConfig, workspaceId: string, actorContext?: ActorMetadata, ): Promise { @@ -43,7 +45,29 @@ export class ToolService { workspaceId, }); - const objectPermissions = rolesPermissions[roleId]; + let objectPermissions; + + if ('unionOf' in rolePermissionConfig) { + if (rolePermissionConfig.unionOf.length === 1) { + objectPermissions = rolesPermissions[rolePermissionConfig.unionOf[0]]; + } else { + // TODO: Implement union logic for multiple roles + throw new Error( + 'Union permission logic for multiple roles not yet implemented', + ); + } + } else if ('intersectionOf' in rolePermissionConfig) { + const allRolePermissions = rolePermissionConfig.intersectionOf.map( + (roleId: string) => rolesPermissions[roleId], + ); + + objectPermissions = + allRolePermissions.length === 1 + ? allRolePermissions[0] + : computePermissionIntersection(allRolePermissions); + } else { + return tools; + } const allObjectMetadata = await this.objectMetadataService.findManyWithinWorkspace(workspaceId, { @@ -65,7 +89,7 @@ export class ToolService { return; } - if (objectPermission.canUpdate) { + if (objectPermission.canUpdateObjectRecords) { tools[`create_${objectMetadata.nameSingular}`] = { description: `Create a new ${objectMetadata.labelSingular} record. Provide all required fields and any optional fields you want to set. The system will automatically handle timestamps and IDs. Returns the created record with all its data.`, inputSchema: getRecordInputSchema(objectMetadata), @@ -74,7 +98,7 @@ export class ToolService { objectName: objectMetadata.nameSingular, objectRecord: parameters.input, workspaceId, - roleId, + rolePermissionConfig, createdBy: actorContext, }); }, @@ -91,13 +115,13 @@ export class ToolService { objectRecordId: id, objectRecord, workspaceId, - roleId, + rolePermissionConfig, }); }, }; } - if (objectPermission.canRead) { + if (objectPermission.canReadObjectRecords) { tools[`find_${objectMetadata.nameSingular}`] = { description: `Search for ${objectMetadata.labelSingular} records using flexible filtering criteria. Supports exact matches, pattern matching, ranges, and null checks. Use limit/offset for pagination. Returns an array of matching records with their full data.`, inputSchema: generateFindToolSchema(objectMetadata), @@ -110,7 +134,7 @@ export class ToolService { limit, offset, workspaceId, - roleId, + rolePermissionConfig, }); }, }; @@ -124,13 +148,13 @@ export class ToolService { filter: { id: { eq: parameters.input.id } }, limit: 1, workspaceId, - roleId, + rolePermissionConfig, }); }, }; } - if (objectPermission.canSoftDelete) { + if (objectPermission.canSoftDeleteObjectRecords) { tools[`soft_delete_${objectMetadata.nameSingular}`] = { description: `Soft delete a ${objectMetadata.labelSingular} record by marking it as deleted. The record remains in the database but is hidden from normal queries. This is reversible and preserves all data. Use this for temporary removal.`, inputSchema: generateSoftDeleteToolSchema(), @@ -139,7 +163,7 @@ export class ToolService { objectName: objectMetadata.nameSingular, objectRecordId: parameters.input.id, workspaceId, - roleId, + rolePermissionConfig, soft: true, }); }, @@ -153,7 +177,7 @@ export class ToolService { objectMetadata.nameSingular, parameters.input, workspaceId, - roleId, + rolePermissionConfig, ); }, }; @@ -167,14 +191,14 @@ export class ToolService { objectName: string, parameters: Record, workspaceId: string, - roleId: string, + rolePermissionConfig: RolePermissionConfig, ) { try { const repository = await this.twentyORMGlobalManager.getRepositoryForWorkspace( workspaceId, objectName, - { roleId }, + rolePermissionConfig, ); const { filter } = parameters; diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/create-record.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/create-record.service.ts index 1a094feb99..f35b67a824 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/services/create-record.service.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/create-record.service.ts @@ -28,7 +28,8 @@ export class CreateRecordService { ) {} async execute(params: CreateRecordParams): Promise { - const { objectName, objectRecord, workspaceId, roleId } = params; + const { objectName, objectRecord, workspaceId, rolePermissionConfig } = + params; if (!workspaceId) { return { @@ -43,7 +44,7 @@ export class CreateRecordService { await this.twentyORMGlobalManager.getRepositoryForWorkspace( workspaceId, objectName, - roleId ? { roleId } : { shouldBypassPermissionChecks: true }, + rolePermissionConfig, ); const { objectMetadataItemWithFieldsMaps } = diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/delete-record.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/delete-record.service.ts index ad2e10f897..a7a1102ab2 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/services/delete-record.service.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/delete-record.service.ts @@ -27,7 +27,7 @@ export class DeleteRecordService { objectName, objectRecordId, workspaceId, - roleId, + rolePermissionConfig, soft = true, } = params; @@ -52,7 +52,7 @@ export class DeleteRecordService { await this.twentyORMGlobalManager.getRepositoryForWorkspace( workspaceId, objectName, - roleId ? { roleId } : { shouldBypassPermissionChecks: true }, + rolePermissionConfig, ); const { objectMetadataItemWithFieldsMaps } = diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/find-records.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/find-records.service.ts index 80f39bf902..85f8e4debd 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/services/find-records.service.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/find-records.service.ts @@ -37,7 +37,7 @@ export class FindRecordsService { limit, offset = 0, workspaceId, - roleId, + rolePermissionConfig, } = params; if (!workspaceId) { @@ -53,7 +53,7 @@ export class FindRecordsService { await this.twentyORMGlobalManager.getRepositoryForWorkspace( workspaceId, objectName, - roleId ? { roleId } : { shouldBypassPermissionChecks: true }, + rolePermissionConfig, ); const { objectMetadataItemWithFieldsMaps, objectMetadataMaps } = diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/update-record.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/update-record.service.ts index 154a68afd3..4b4f1c2ea1 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/services/update-record.service.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/update-record.service.ts @@ -32,7 +32,7 @@ export class UpdateRecordService { objectRecord, fieldsToUpdate, workspaceId, - roleId, + rolePermissionConfig, } = params; if (!workspaceId) { @@ -56,7 +56,7 @@ export class UpdateRecordService { await this.twentyORMGlobalManager.getRepositoryForWorkspace( workspaceId, objectName, - roleId ? { roleId } : { shouldBypassPermissionChecks: true }, + rolePermissionConfig, ); const previousObjectRecord = await repository.findOne({ diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/types/create-record-params.type.ts b/packages/twenty-server/src/engine/core-modules/record-crud/types/create-record-params.type.ts index 8e6912662f..79a1efcd5d 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/types/create-record-params.type.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/types/create-record-params.type.ts @@ -1,10 +1,11 @@ import { type ObjectRecordProperties } from 'src/engine/core-modules/record-crud/types/object-record-properties.type'; import { type ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; +import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; export type CreateRecordParams = { objectName: string; objectRecord: ObjectRecordProperties; workspaceId: string; - roleId?: string; + rolePermissionConfig?: RolePermissionConfig; createdBy?: ActorMetadata; }; diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/types/delete-record-params.type.ts b/packages/twenty-server/src/engine/core-modules/record-crud/types/delete-record-params.type.ts index c546d831b3..347ded94bd 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/types/delete-record-params.type.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/types/delete-record-params.type.ts @@ -1,7 +1,9 @@ +import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; + export type DeleteRecordParams = { objectName: string; objectRecordId: string; workspaceId: string; - roleId?: string; + rolePermissionConfig?: RolePermissionConfig; soft?: boolean; }; diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/types/find-records-params.type.ts b/packages/twenty-server/src/engine/core-modules/record-crud/types/find-records-params.type.ts index 5381645bbf..9af8d9d343 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/types/find-records-params.type.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/types/find-records-params.type.ts @@ -3,6 +3,8 @@ import { type ObjectRecordOrderBy, } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; +import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; + export type FindRecordsParams = { objectName: string; filter?: @@ -14,5 +16,5 @@ export type FindRecordsParams = { limit?: number; offset?: number; workspaceId: string; - roleId?: string; + rolePermissionConfig?: RolePermissionConfig; }; diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/types/update-record-params.type.ts b/packages/twenty-server/src/engine/core-modules/record-crud/types/update-record-params.type.ts index e90700ecdb..7901abf6b4 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/types/update-record-params.type.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/types/update-record-params.type.ts @@ -1,4 +1,5 @@ import { type ObjectRecordProperties } from 'src/engine/core-modules/record-crud/types/object-record-properties.type'; +import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; export type UpdateRecordParams = { objectName: string; @@ -6,5 +7,5 @@ export type UpdateRecordParams = { objectRecord: ObjectRecordProperties; fieldsToUpdate?: string[]; workspaceId: string; - roleId?: string; + rolePermissionConfig?: RolePermissionConfig; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/agent/agent-execution.service.ts b/packages/twenty-server/src/engine/metadata-modules/agent/agent-execution.service.ts index 573fbdd78f..23fb6642de 100644 --- a/packages/twenty-server/src/engine/metadata-modules/agent/agent-execution.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/agent/agent-execution.service.ts @@ -1,5 +1,4 @@ import { Injectable, Logger } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; import { convertToModelMessages, @@ -13,15 +12,15 @@ import { } from 'ai'; import { AppPath } from 'twenty-shared/types'; import { getAppPath } from 'twenty-shared/utils'; -import { In, Repository } from 'typeorm'; +import { In } from 'typeorm'; import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils'; import { AIBillingService } from 'src/engine/core-modules/ai/services/ai-billing.service'; import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service'; import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; -import { FileService } from 'src/engine/core-modules/file/services/file.service'; import { type Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { AgentHandoffToolService } from 'src/engine/metadata-modules/agent/agent-handoff-tool.service'; +import { AgentService } from 'src/engine/metadata-modules/agent/agent.service'; import { AGENT_CONFIG } from 'src/engine/metadata-modules/agent/constants/agent-config.const'; import { AGENT_SYSTEM_PROMPTS } from 'src/engine/metadata-modules/agent/constants/agent-system-prompts.const'; import { AgentActorContextService } from 'src/engine/metadata-modules/agent/services/agent-actor-context.service'; @@ -48,7 +47,6 @@ export class AgentExecutionService implements AgentExecutionContext { constructor( private readonly agentHandoffToolService: AgentHandoffToolService, - private readonly fileService: FileService, private readonly domainManagerService: DomainManagerService, private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, @@ -57,8 +55,7 @@ export class AgentExecutionService implements AgentExecutionContext { private readonly agentModelConfigService: AgentModelConfigService, private readonly aiBillingService: AIBillingService, private readonly agentActorContextService: AgentActorContextService, - @InjectRepository(AgentEntity) - private readonly agentRepository: Repository, + private readonly agentService: AgentService, ) {} async prepareAIRequestConfig({ @@ -66,14 +63,14 @@ export class AgentExecutionService implements AgentExecutionContext { system, agent, actorContext, - roleIdOverride, + roleIds, excludeHandoffTools = false, }: { system: string; agent: AgentEntity | null; messages: UIMessage[]; actorContext?: ActorMetadata; - roleIdOverride?: string; + roleIds?: string[]; excludeHandoffTools?: boolean; }) { try { @@ -95,7 +92,7 @@ export class AgentExecutionService implements AgentExecutionContext { agent.id, agent.workspaceId, actorContext, - roleIdOverride, + roleIds, ); let handoffTools = {}; @@ -193,8 +190,7 @@ export class AgentExecutionService implements AgentExecutionContext { const repository = workspaceDataSource.getRepository( recordsWithObjectMetadataNameSingular.objectMetadataNameSingular, - false, - roleId, + { unionOf: [roleId] }, ); const restrictedFields = @@ -254,9 +250,7 @@ export class AgentExecutionService implements AgentExecutionContext { recordIdsByObjectMetadataNameSingular: RecordIdsByObjectMetadataNameSingularType; }) { try { - const agent = await this.agentRepository.findOneOrFail({ - where: { id: agentId }, - }); + const agent = await this.agentService.findOneAgent(agentId, workspace.id); let contextString = ''; @@ -271,7 +265,7 @@ export class AgentExecutionService implements AgentExecutionContext { } const { actorContext, roleId } = - await this.agentActorContextService.buildUserActorContext( + await this.agentActorContextService.buildUserAndAgentActorContext( userWorkspaceId, workspace.id, ); @@ -281,7 +275,7 @@ export class AgentExecutionService implements AgentExecutionContext { agent, messages, actorContext, - roleIdOverride: roleId, + roleIds: [roleId, ...(agent?.roleId ? [agent?.roleId] : [])], }); this.logger.log( diff --git a/packages/twenty-server/src/engine/metadata-modules/agent/agent-tool-generator.service.ts b/packages/twenty-server/src/engine/metadata-modules/agent/agent-tool-generator.service.ts index d013fbe7ce..fa0154fd56 100644 --- a/packages/twenty-server/src/engine/metadata-modules/agent/agent-tool-generator.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/agent/agent-tool-generator.service.ts @@ -6,7 +6,6 @@ import { Repository } from 'typeorm'; import { ToolAdapterService } from 'src/engine/core-modules/ai/services/tool-adapter.service'; import { ToolService } from 'src/engine/core-modules/ai/services/tool.service'; -import { AgentService } from 'src/engine/metadata-modules/agent/agent.service'; import { type ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; @@ -24,58 +23,43 @@ export class AgentToolGeneratorService { private readonly toolService: ToolService, private readonly workflowToolService: WorkflowToolService, private readonly permissionsService: PermissionsService, - private readonly agentService: AgentService, ) {} async generateToolsForAgent( agentId: string, workspaceId: string, actorContext?: ActorMetadata, - roleIdOverride?: string, + roleIds?: string[], ): Promise { let tools: ToolSet = {}; try { - const agent = await this.agentService.findOneAgent(agentId, workspaceId); const actionTools = await this.toolAdapterService.getTools(); tools = { ...actionTools }; - const effectiveRoleId = roleIdOverride || agent.roleId; - - if (!effectiveRoleId) { - return tools; - } - - const role = await this.roleRepository.findOne({ - where: { - id: effectiveRoleId, - workspaceId, - }, - relations: ['permissionFlags'], - }); - - if (!role) { + if (!roleIds) { return tools; } const hasWorkflowPermission = - this.permissionsService.checkRolePermissions( - role, + await this.permissionsService.checkRolesPermissions( + { intersectionOf: roleIds }, + workspaceId, PermissionFlagType.WORKFLOWS, ); if (hasWorkflowPermission) { const workflowTools = this.workflowToolService.generateWorkflowTools( workspaceId, - effectiveRoleId, + { intersectionOf: roleIds }, ); tools = { ...tools, ...workflowTools }; } const databaseTools = await this.toolService.listTools( - effectiveRoleId, + { intersectionOf: roleIds }, workspaceId, actorContext, ); @@ -83,7 +67,7 @@ export class AgentToolGeneratorService { tools = { ...tools, ...databaseTools }; const roleActionTools = await this.toolAdapterService.getTools( - effectiveRoleId, + { intersectionOf: roleIds }, workspaceId, ); diff --git a/packages/twenty-server/src/engine/metadata-modules/agent/services/agent-actor-context.service.ts b/packages/twenty-server/src/engine/metadata-modules/agent/services/agent-actor-context.service.ts index 36d56dc0b4..eae956e76f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/agent/services/agent-actor-context.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/agent/services/agent-actor-context.service.ts @@ -12,7 +12,7 @@ import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global. export type AgentActorContext = { actorContext: ActorMetadata; - roleId: string | undefined; + roleId: string; }; @Injectable() @@ -23,7 +23,7 @@ export class AgentActorContextService { private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} - async buildUserActorContext( + async buildUserAndAgentActorContext( userWorkspaceId: string, workspaceId: string, ): Promise { @@ -61,11 +61,21 @@ export class AgentActorContextService { workspaceId, }); + if (!roleId) { + throw new AgentException( + 'User role not found', + AgentExceptionCode.AGENT_EXECUTION_FAILED, + ); + } + const actorContext = buildCreatedByFromFullNameMetadata({ fullNameMetadata: workspaceMember.name, workspaceMemberId: workspaceMember.id, }); - return { actorContext, roleId }; + return { + actorContext, + roleId, + }; } } diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/__tests__/field-permissions.service.spec.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/__tests__/field-permissions.service.spec.ts index 53b8cf95b0..2ad09c459f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/__tests__/field-permissions.service.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/__tests__/field-permissions.service.spec.ts @@ -1,7 +1,7 @@ import { Test, type TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; -import { type ObjectsPermissionsByRoleIdDeprecated } from 'twenty-shared/types'; +import { type ObjectsPermissionsByRoleId } from 'twenty-shared/types'; import { In, type Repository } from 'typeorm'; import { @@ -53,13 +53,9 @@ describe('FieldPermissionService', () => { isEditable: true, } as RoleEntity; - const mockRolesPermissions: ObjectsPermissionsByRoleIdDeprecated = { + const mockRolesPermissions: ObjectsPermissionsByRoleId = { [testRoleId]: { [testObjectMetadataId]: { - canRead: true, - canUpdate: true, - canSoftDelete: false, - canDestroy: false, canReadObjectRecords: true, canUpdateObjectRecords: true, canSoftDeleteObjectRecords: false, @@ -67,10 +63,6 @@ describe('FieldPermissionService', () => { restrictedFields: {}, }, [fieldRelationMock.objectMetadataId]: { - canRead: true, - canUpdate: true, - canSoftDelete: false, - canDestroy: false, canReadObjectRecords: true, canUpdateObjectRecords: true, canSoftDeleteObjectRecords: false, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/field-permission.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/field-permission.service.ts index f1c8690ba4..0049616339 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/field-permission.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/field-permission.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { msg } from '@lingui/core/macro'; -import { type ObjectsPermissionsByRoleIdDeprecated } from 'twenty-shared/types'; +import { type ObjectsPermissionsByRoleId } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { In, Repository } from 'typeorm'; @@ -183,7 +183,7 @@ export class FieldPermissionService { allFieldPermissions: UpsertFieldPermissionsInput['fieldPermissions']; fieldPermission: UpsertFieldPermissionsInput['fieldPermissions'][0]; objectMetadataMapsById: ObjectMetadataMaps['byId']; - rolesPermissions: ObjectsPermissionsByRoleIdDeprecated; + rolesPermissions: ObjectsPermissionsByRoleId; role: RoleEntity; }) { const duplicateFieldPermissions = allFieldPermissions.filter( diff --git a/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.service.ts b/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.service.ts index ee61472031..902ab6df24 100644 --- a/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.service.ts @@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { msg } from '@lingui/core/macro'; import { isDefined } from 'twenty-shared/utils'; -import { Repository } from 'typeorm'; +import { In, Repository } from 'typeorm'; import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/api-key-role.service'; import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; @@ -17,6 +17,7 @@ import { type UserWorkspacePermissions } from 'src/engine/metadata-modules/permi import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; +import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; @Injectable() export class PermissionsService { @@ -188,30 +189,101 @@ export class PermissionsService { ); } + private async getRolesFromPermissionConfig( + rolePermissionConfig: RolePermissionConfig, + workspaceId: string, + relations: string[] = [], + ): Promise<{ roles: RoleEntity[]; useIntersection: boolean } | null> { + if ('shouldBypassPermissionChecks' in rolePermissionConfig) { + return null; + } + + let roleIds: string[] = []; + let useIntersection = false; + + if ('intersectionOf' in rolePermissionConfig) { + roleIds = rolePermissionConfig.intersectionOf; + useIntersection = true; + } else if ('unionOf' in rolePermissionConfig) { + roleIds = rolePermissionConfig.unionOf; + useIntersection = false; + } + + if (roleIds.length === 0) { + throw new Error('No role IDs provided'); + } + + const roles = await this.roleRepository.find({ + where: { id: In(roleIds), workspaceId }, + relations, + }); + + if (roles.length !== roleIds.length) { + throw new Error('Some roles not found'); + } + + return { roles, useIntersection }; + } + + public async checkRolesPermissions( + rolePermissionConfig: RolePermissionConfig, + workspaceId: string, + setting: PermissionFlagType, + ): Promise { + try { + const result = await this.getRolesFromPermissionConfig( + rolePermissionConfig, + workspaceId, + ['permissionFlags'], + ); + + if (result === null) { + return true; + } + + const { roles, useIntersection } = result; + + return useIntersection + ? roles.every((role) => this.checkRolePermissions(role, setting)) + : roles.some((role) => this.checkRolePermissions(role, setting)); + } catch { + return false; + } + } + public async hasToolPermission( - roleId: string, + rolePermissionConfig: RolePermissionConfig, workspaceId: string, flag: PermissionFlagType, ): Promise { try { - const role = await this.roleRepository.findOne({ - where: { id: roleId, workspaceId }, - relations: ['permissionFlags'], - }); + const result = await this.getRolesFromPermissionConfig( + rolePermissionConfig, + workspaceId, + ['permissionFlags'], + ); - if (!role) { - return false; - } - - if (role.canAccessAllTools === true) { + if (result === null) { return true; } - const permissionFlags = role.permissionFlags ?? []; + const { roles, useIntersection } = result; - return permissionFlags.some( - (permissionFlag) => permissionFlag.flag === flag, - ); + const checkRoleHasPermission = (role: RoleEntity) => { + if (role.canAccessAllTools === true) { + return true; + } + + const permissionFlags = role.permissionFlags ?? []; + + return permissionFlags.some( + (permissionFlag) => permissionFlag.flag === flag, + ); + }; + + return useIntersection + ? roles.every(checkRoleHasPermission) + : roles.some(checkRoleHasPermission); } catch { return false; } diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service.ts index b583f63442..162ccd2934 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service.ts @@ -1,6 +1,6 @@ import { Injectable, Logger } from '@nestjs/common'; -import { type ObjectsPermissionsByRoleIdDeprecated } from 'twenty-shared/types'; +import { type ObjectsPermissionsByRoleId } from 'twenty-shared/types'; import { v4 } from 'uuid'; import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; @@ -34,12 +34,12 @@ export class WorkspacePermissionsCacheStorageService { async setRolesPermissions( workspaceId: string, - permissions: ObjectsPermissionsByRoleIdDeprecated, + permissions: ObjectsPermissionsByRoleId, ): Promise<{ newRolesPermissionsVersion: string; }> { const [, newRolesPermissionsVersion] = await Promise.all([ - this.cacheStorageService.set( + this.cacheStorageService.set( `${WORKSPACE_CACHE_KEYS.MetadataPermissionsRolesPermissions}:${workspaceId}`, permissions, TTL_INFINITE, @@ -52,8 +52,8 @@ export class WorkspacePermissionsCacheStorageService { getRolesPermissions( workspaceId: string, - ): Promise { - return this.cacheStorageService.get( + ): Promise { + return this.cacheStorageService.get( `${WORKSPACE_CACHE_KEYS.MetadataPermissionsRolesPermissions}:${workspaceId}`, ); } diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service.ts index 692964c23c..1db1b8b505 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service.ts @@ -3,9 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Record } from 'cloudflare/core'; import { - ObjectsPermissions, - type ObjectsPermissionsByRoleIdDeprecated, - type ObjectsPermissionsDeprecated, + type ObjectsPermissions, + type ObjectsPermissionsByRoleId, type RestrictedFieldsPermissions, } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; @@ -48,7 +47,7 @@ export class WorkspacePermissionsCacheService { private readonly workspacePermissionsCacheStorageService: WorkspacePermissionsCacheStorageService, private readonly getRolesPermissionsFromCacheWithRecomputeService: GetDataFromCacheWithRecomputeService< string, - ObjectsPermissionsByRoleIdDeprecated + ObjectsPermissionsByRoleId >, private readonly getUserWorkspaceRoleMapFromCacheWithRecomputeService: GetDataFromCacheWithRecomputeService< string, @@ -67,9 +66,7 @@ export class WorkspacePermissionsCacheService { workspaceId: string; roleIds?: string[]; }): Promise { - let currentRolesPermissions: - | ObjectsPermissionsByRoleIdDeprecated - | undefined; + let currentRolesPermissions: ObjectsPermissionsByRoleId | undefined; if (roleIds) { currentRolesPermissions = @@ -124,7 +121,7 @@ export class WorkspacePermissionsCacheService { workspaceId, }: { workspaceId: string; - }): Promise> { + }): Promise> { return this.getRolesPermissionsFromCacheWithRecomputeService.getFromCacheWithRecompute( { workspaceId, @@ -194,7 +191,7 @@ export class WorkspacePermissionsCacheService { }: { workspaceId: string; roleIds?: string[]; - }): Promise { + }): Promise { let roles: RoleEntity[] = []; roles = await this.roleRepository.find({ @@ -208,11 +205,10 @@ export class WorkspacePermissionsCacheService { const workspaceObjectMetadataCollection = await this.getWorkspaceObjectMetadataCollection(workspaceId); - const permissionsByRoleId: ObjectsPermissionsByRoleIdDeprecated = {}; + const permissionsByRoleId: ObjectsPermissionsByRoleId = {}; for (const role of roles) { - const objectRecordsPermissions: ObjectsPermissionsDeprecated & - ObjectsPermissions = {}; + const objectRecordsPermissions: ObjectsPermissions = {}; for (const objectMetadata of workspaceObjectMetadataCollection) { const { id: objectMetadataId, isSystem, standardId } = objectMetadata; @@ -288,10 +284,6 @@ export class WorkspacePermissionsCacheService { } objectRecordsPermissions[objectMetadataId] = { - canRead, - canUpdate, - canSoftDelete, - canDestroy, canReadObjectRecords: canRead, canUpdateObjectRecords: canUpdate, canSoftDeleteObjectRecords: canSoftDelete, diff --git a/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts b/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts index 90d91d865b..3e73c5ed7e 100644 --- a/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts +++ b/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts @@ -1,6 +1,6 @@ import { type Entity } from '@microsoft/microsoft-graph-types'; import { isDefined } from 'class-validator'; -import { type ObjectsPermissionsByRoleIdDeprecated } from 'twenty-shared/types'; +import { type ObjectsPermissionsByRoleId } from 'twenty-shared/types'; import { DataSource, type DataSourceOptions, @@ -23,6 +23,7 @@ import { import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; import { type WorkspaceQueryRunner } from 'src/engine/twenty-orm/query-runner/workspace-query-runner'; import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; +import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; type CreateQueryBuilderOptions = { calledByWorkspaceEntityManager?: boolean; @@ -34,7 +35,7 @@ export class WorkspaceDataSource extends DataSource { featureFlagMapVersion: string; featureFlagMap: FeatureFlagMap; rolesPermissionsVersion: string; - permissionsPerRoleId: ObjectsPermissionsByRoleIdDeprecated; + permissionsPerRoleId: ObjectsPermissionsByRoleId; dataSourceWithOverridenCreateQueryBuilder: WorkspaceDataSource; isPoolSharingEnabled: boolean; @@ -44,7 +45,7 @@ export class WorkspaceDataSource extends DataSource { featureFlagMapVersion: string, featureFlagMap: FeatureFlagMap, rolesPermissionsVersion: string, - permissionsPerRoleId: ObjectsPermissionsByRoleIdDeprecated, + permissionsPerRoleId: ObjectsPermissionsByRoleId, isPoolSharingEnabled: boolean, ) { super(options); @@ -60,31 +61,10 @@ export class WorkspaceDataSource extends DataSource { override getRepository( target: EntityTarget, - shouldBypassPermissionChecks = false, - roleId?: string, + permissionOptions?: RolePermissionConfig, authContext?: AuthContext, ): WorkspaceRepository { - if (shouldBypassPermissionChecks === true) { - return this.manager.getRepository( - target, - { - shouldBypassPermissionChecks: true, - }, - authContext, - ); - } - - if (roleId) { - return this.manager.getRepository( - target, - { - roleId, - }, - authContext, - ); - } - - return this.manager.getRepository(target, undefined, authContext); + return this.manager.getRepository(target, permissionOptions, authContext); } override createEntityManager( @@ -238,9 +218,7 @@ export class WorkspaceDataSource extends DataSource { this.rolesPermissionsVersion = rolesPermissionsVersion; } - setRolesPermissions( - permissionsPerRoleId: ObjectsPermissionsByRoleIdDeprecated, - ) { + setRolesPermissions(permissionsPerRoleId: ObjectsPermissionsByRoleId) { this.permissionsPerRoleId = permissionsPerRoleId; } diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts index d264a03010..9b03c7e521 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts @@ -1,4 +1,4 @@ -import { type ObjectsPermissionsDeprecated } from 'twenty-shared/types'; +import { type ObjectsPermissions } from 'twenty-shared/types'; import { EntityManager } from 'typeorm'; import { EntityPersistExecutor } from 'typeorm/persistence/EntityPersistExecutor'; import { PlainObjectToDatabaseEntityTransformer } from 'typeorm/query-builder/transformer/PlainObjectToDatabaseEntityTransformer'; @@ -75,7 +75,7 @@ describe('WorkspaceEntityManager', () => { let mockDataSource: WorkspaceDataSource; let mockPermissionOptions: { shouldBypassPermissionChecks: boolean; - objectRecordsPermissions?: ObjectsPermissionsDeprecated; + objectRecordsPermissions?: ObjectsPermissions; }; beforeEach(() => { @@ -184,10 +184,10 @@ describe('WorkspaceEntityManager', () => { shouldBypassPermissionChecks: false, objectRecordsPermissions: { 'test-entity': { - canRead: true, - canUpdate: false, - canSoftDelete: false, - canDestroy: false, + canReadObjectRecords: true, + canUpdateObjectRecords: false, + canSoftDeleteObjectRecords: false, + canDestroyObjectRecords: false, restrictedFields: {}, }, }, diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts index 5cbc899162..6d415666f1 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts @@ -1,5 +1,8 @@ import isEmpty from 'lodash.isempty'; -import { type ObjectsPermissionsDeprecated } from 'twenty-shared/types'; +import { + type ObjectsPermissions, + type ObjectsPermissionsByRoleId, +} from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { type DeleteResult, @@ -53,14 +56,16 @@ import { } from 'src/engine/twenty-orm/repository/permissions.utils'; import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; +import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; +import { computePermissionIntersection } from 'src/engine/twenty-orm/utils/compute-permission-intersection.util'; import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; -import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util'; import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util'; +import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util'; type PermissionOptions = { shouldBypassPermissionChecks?: boolean; - objectRecordsPermissions?: ObjectsPermissionsDeprecated; + objectRecordsPermissions?: ObjectsPermissions; }; export class WorkspaceEntityManager extends EntityManager { @@ -83,35 +88,62 @@ export class WorkspaceEntityManager extends EntityManager { return this.connection.featureFlagMap; } + private getPermissionsForRole( + roleId: string, + permissionsPerRoleId: ObjectsPermissionsByRoleId, + ): ObjectsPermissions { + if (!isDefined(permissionsPerRoleId?.[roleId])) { + throw new PermissionsException( + `No permissions found for role in datasource (roleId: ${roleId})`, + PermissionsExceptionCode.NO_PERMISSIONS_FOUND_IN_DATASOURCE, + ); + } + + return permissionsPerRoleId[roleId]; + } + override getRepository( target: EntityTarget, - permissionOptions?: { - shouldBypassPermissionChecks?: boolean; - roleId?: string; - }, + rolePermissionConfig?: RolePermissionConfig, authContext?: AuthContext, ): WorkspaceRepository { const dataSource = this.connection; let objectPermissions = {}; + let shouldBypassPermissionChecks = false; + const objectPermissionsByRoleId = dataSource.permissionsPerRoleId; - if (permissionOptions?.roleId) { - const objectPermissionsByRoleId = dataSource.permissionsPerRoleId; + if ( + rolePermissionConfig && + 'shouldBypassPermissionChecks' in rolePermissionConfig + ) { + shouldBypassPermissionChecks = + rolePermissionConfig.shouldBypassPermissionChecks; + } - if (!isDefined(objectPermissionsByRoleId?.[permissionOptions.roleId])) { - throw new PermissionsException( - `No permissions found for role in datasource (missing ${ - !isDefined(objectPermissionsByRoleId) - ? 'objectPermissionsByRoleId object' - : `roleId in objectPermissionsByRoleId object (${permissionOptions.roleId})` - })`, - PermissionsExceptionCode.NO_PERMISSIONS_FOUND_IN_DATASOURCE, + if (rolePermissionConfig && 'unionOf' in rolePermissionConfig) { + if (rolePermissionConfig.unionOf.length === 1) { + objectPermissions = this.getPermissionsForRole( + rolePermissionConfig.unionOf[0], + objectPermissionsByRoleId, ); } else { - objectPermissions = objectPermissionsByRoleId[permissionOptions.roleId]; + // TODO: Implement union logic for combining permissions across multiple roles + throw new Error( + 'Union permission logic for multiple roles not yet implemented', + ); } } + if (rolePermissionConfig && 'intersectionOf' in rolePermissionConfig) { + const allRolePermissions = rolePermissionConfig.intersectionOf.map( + (roleId: string) => + this.getPermissionsForRole(roleId, objectPermissionsByRoleId), + ); + + objectPermissions = computePermissionIntersection(allRolePermissions); + } + const newRepository = new WorkspaceRepository( this.internalContext, target, @@ -119,7 +151,7 @@ export class WorkspaceEntityManager extends EntityManager { dataSource.featureFlagMap, this.queryRunner, objectPermissions, - permissionOptions?.shouldBypassPermissionChecks, + shouldBypassPermissionChecks, authContext, ); @@ -132,7 +164,7 @@ export class WorkspaceEntityManager extends EntityManager { queryRunner?: QueryRunner, options: { shouldBypassPermissionChecks?: boolean; - objectRecordsPermissions?: ObjectsPermissionsDeprecated; + objectRecordsPermissions?: ObjectsPermissions; } = { shouldBypassPermissionChecks: false, objectRecordsPermissions: {}, @@ -200,7 +232,7 @@ export class WorkspaceEntityManager extends EntityManager { conflictPathsOrOptions: string[] | UpsertOptions, permissionOptions?: { shouldBypassPermissionChecks?: boolean; - objectRecordsPermissions?: ObjectsPermissionsDeprecated; + objectRecordsPermissions?: ObjectsPermissions; }, selectedColumns: string[] | '*' = '*', ): Promise { @@ -389,7 +421,7 @@ export class WorkspaceEntityManager extends EntityManager { operationType: OperationType; permissionOptions?: { shouldBypassPermissionChecks?: boolean; - objectRecordsPermissions?: ObjectsPermissionsDeprecated; + objectRecordsPermissions?: ObjectsPermissions; }; selectedColumns: string[]; updatedColumns?: string[]; diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts index 476accd244..1dbaae9bf0 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts @@ -1,7 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { type ObjectsPermissionsByRoleIdDeprecated } from 'twenty-shared/types'; +import { type ObjectsPermissionsByRoleId } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { EntitySchema, Repository } from 'typeorm'; @@ -55,7 +55,7 @@ export class WorkspaceDatasourceFactory { private readonly workspaceEventEmitter: WorkspaceEventEmitter, private readonly getFromCacheWithRecomputeService: GetDataFromCacheWithRecomputeService< string, - ObjectsPermissionsByRoleIdDeprecated + ObjectsPermissionsByRoleId >, ) {} @@ -239,7 +239,7 @@ export class WorkspaceDatasourceFactory { workspaceId, }: { workspaceId: string; - }): Promise> { + }): Promise> { return this.getFromCacheWithRecomputeService.getFromCacheWithRecompute({ workspaceId, getCacheData: () => @@ -292,7 +292,7 @@ export class WorkspaceDatasourceFactory { }: { workspaceDataSource: WorkspaceDataSource; cachedRolesPermissionsVersion: string; - cachedRolesPermissions: ObjectsPermissionsByRoleIdDeprecated; + cachedRolesPermissions: ObjectsPermissionsByRoleId; }): Promise { this.updateWorkspaceDataSourceIfNeeded({ workspaceDataSource, diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts b/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts index f0b7efd8ab..8f4b601d60 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts @@ -1,4 +1,4 @@ -import { type ObjectsPermissionsDeprecated } from 'twenty-shared/types'; +import { type ObjectsPermissions } from 'twenty-shared/types'; import { type DeepPartial, type FindManyOptions, @@ -20,7 +20,7 @@ describe('WorkspaceRepository', () => { let mockEntityManager: jest.Mocked; let mockInternalContext: WorkspaceInternalContext; let mockFeatureFlagMap: FeatureFlagMap; - let mockObjectRecordsPermissions: ObjectsPermissionsDeprecated; + let mockObjectRecordsPermissions: ObjectsPermissions; let mockQueryRunner: QueryRunner; beforeEach(() => { @@ -71,10 +71,10 @@ describe('WorkspaceRepository', () => { ); mockObjectRecordsPermissions = { 'test-entity': { - canRead: true, - canUpdate: false, - canSoftDelete: false, - canDestroy: false, + canReadObjectRecords: true, + canUpdateObjectRecords: false, + canSoftDeleteObjectRecords: false, + canDestroyObjectRecords: false, restrictedFields: {}, }, }; diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/permissions.utils.ts b/packages/twenty-server/src/engine/twenty-orm/repository/permissions.utils.ts index fdaa99e604..357d303a8e 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/permissions.utils.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/permissions.utils.ts @@ -1,7 +1,7 @@ import { isNonEmptyString } from '@sniptt/guards'; import isEmpty from 'lodash.isempty'; import { - type ObjectsPermissionsDeprecated, + type ObjectsPermissions, type RestrictedFieldsPermissions, } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; @@ -38,7 +38,7 @@ export type OperationType = type ValidateOperationIsPermittedOrThrowArgs = { entityName: string; operationType: OperationType; - objectsPermissions: ObjectsPermissionsDeprecated; + objectsPermissions: ObjectsPermissions; objectMetadataMaps: ObjectMetadataMaps; selectedColumns: string[] | '*'; allFieldsSelected: boolean; @@ -86,7 +86,7 @@ export const validateOperationIsPermittedOrThrow = ({ switch (operationType) { case 'select': - if (!permissionsForEntity?.canRead) { + if (!permissionsForEntity?.canReadObjectRecords) { throw new PermissionsException( PermissionsExceptionMessage.PERMISSION_DENIED, PermissionsExceptionCode.PERMISSION_DENIED, @@ -102,7 +102,7 @@ export const validateOperationIsPermittedOrThrow = ({ break; case 'insert': case 'update': - if (!permissionsForEntity?.canUpdate) { + if (!permissionsForEntity?.canUpdateObjectRecords) { throw new PermissionsException( PermissionsExceptionMessage.PERMISSION_DENIED, PermissionsExceptionCode.PERMISSION_DENIED, @@ -124,7 +124,7 @@ export const validateOperationIsPermittedOrThrow = ({ } break; case 'delete': - if (!permissionsForEntity?.canDestroy) { + if (!permissionsForEntity?.canDestroyObjectRecords) { throw new PermissionsException( PermissionsExceptionMessage.PERMISSION_DENIED, PermissionsExceptionCode.PERMISSION_DENIED, @@ -139,7 +139,7 @@ export const validateOperationIsPermittedOrThrow = ({ break; case 'restore': case 'soft-delete': - if (!permissionsForEntity?.canSoftDelete) { + if (!permissionsForEntity?.canSoftDeleteObjectRecords) { throw new PermissionsException( PermissionsExceptionMessage.PERMISSION_DENIED, PermissionsExceptionCode.PERMISSION_DENIED, @@ -166,7 +166,7 @@ export const validateOperationIsPermittedOrThrow = ({ type ValidateQueryIsPermittedOrThrowArgs = { expressionMap: QueryExpressionMap; - objectsPermissions: ObjectsPermissionsDeprecated; + objectsPermissions: ObjectsPermissions; objectMetadataMaps: ObjectMetadataMaps; shouldBypassPermissionChecks: boolean; }; diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-delete-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-delete-query-builder.ts index c9e3a4ec36..02913ddd54 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-delete-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-delete-query-builder.ts @@ -1,4 +1,4 @@ -import { type ObjectsPermissionsDeprecated } from 'twenty-shared/types'; +import { type ObjectsPermissions } from 'twenty-shared/types'; import { DeleteQueryBuilder, type DeleteResult, @@ -25,21 +25,21 @@ import { type WorkspaceSoftDeleteQueryBuilder } from 'src/engine/twenty-orm/repo import { type WorkspaceUpdateQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-update-query-builder'; import { applyTableAliasOnWhereCondition } from 'src/engine/twenty-orm/utils/apply-table-alias-on-where-condition'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; +import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util'; import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util'; import { computeTableName } from 'src/engine/utils/compute-table-name.util'; -import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util'; export class WorkspaceDeleteQueryBuilder< T extends ObjectLiteral, > extends DeleteQueryBuilder { - private objectRecordsPermissions: ObjectsPermissionsDeprecated; + private objectRecordsPermissions: ObjectsPermissions; private shouldBypassPermissionChecks: boolean; private internalContext: WorkspaceInternalContext; private authContext?: AuthContext; private featureFlagMap?: FeatureFlagMap; constructor( queryBuilder: DeleteQueryBuilder, - objectRecordsPermissions: ObjectsPermissionsDeprecated, + objectRecordsPermissions: ObjectsPermissions, internalContext: WorkspaceInternalContext, shouldBypassPermissionChecks: boolean, authContext?: AuthContext, diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-insert-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-insert-query-builder.ts index 2692134705..11cb616a64 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-insert-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-insert-query-builder.ts @@ -1,4 +1,4 @@ -import { type ObjectsPermissionsDeprecated } from 'twenty-shared/types'; +import { type ObjectsPermissions } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { type EntityTarget, @@ -29,13 +29,13 @@ import { type WorkspaceSoftDeleteQueryBuilder } from 'src/engine/twenty-orm/repo import { type WorkspaceUpdateQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-update-query-builder'; import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; -import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util'; import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util'; +import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util'; export class WorkspaceInsertQueryBuilder< T extends ObjectLiteral, > extends InsertQueryBuilder { - private objectRecordsPermissions: ObjectsPermissionsDeprecated; + private objectRecordsPermissions: ObjectsPermissions; private shouldBypassPermissionChecks: boolean; private internalContext: WorkspaceInternalContext; private authContext?: AuthContext; @@ -47,7 +47,7 @@ export class WorkspaceInsertQueryBuilder< constructor( queryBuilder: InsertQueryBuilder, - objectRecordsPermissions: ObjectsPermissionsDeprecated, + objectRecordsPermissions: ObjectsPermissions, internalContext: WorkspaceInternalContext, shouldBypassPermissionChecks: boolean, authContext?: AuthContext, diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-select-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-select-query-builder.ts index 24c314fc1c..b87ca1ae8b 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-select-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-select-query-builder.ts @@ -1,4 +1,4 @@ -import { type ObjectsPermissionsDeprecated } from 'twenty-shared/types'; +import { type ObjectsPermissions } from 'twenty-shared/types'; import { type EntityTarget, type ObjectLiteral, @@ -30,14 +30,14 @@ import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/g export class WorkspaceSelectQueryBuilder< T extends ObjectLiteral, > extends SelectQueryBuilder { - objectRecordsPermissions: ObjectsPermissionsDeprecated; + objectRecordsPermissions: ObjectsPermissions; shouldBypassPermissionChecks: boolean; internalContext: WorkspaceInternalContext; authContext?: AuthContext; featureFlagMap?: FeatureFlagMap; constructor( queryBuilder: SelectQueryBuilder, - objectRecordsPermissions: ObjectsPermissionsDeprecated, + objectRecordsPermissions: ObjectsPermissions, internalContext: WorkspaceInternalContext, shouldBypassPermissionChecks: boolean, authContext?: AuthContext, diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-soft-delete-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-soft-delete-query-builder.ts index 698d722be9..fc3889adbf 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-soft-delete-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-soft-delete-query-builder.ts @@ -1,4 +1,4 @@ -import { type ObjectsPermissionsDeprecated } from 'twenty-shared/types'; +import { type ObjectsPermissions } from 'twenty-shared/types'; import { type EntityTarget, type InsertQueryBuilder, @@ -22,13 +22,13 @@ import { type WorkspaceDeleteQueryBuilder } from 'src/engine/twenty-orm/reposito import { type WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder'; import { type WorkspaceUpdateQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-update-query-builder'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; -import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util'; import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util'; +import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util'; export class WorkspaceSoftDeleteQueryBuilder< T extends ObjectLiteral, > extends SoftDeleteQueryBuilder { - private objectRecordsPermissions: ObjectsPermissionsDeprecated; + private objectRecordsPermissions: ObjectsPermissions; private shouldBypassPermissionChecks: boolean; private internalContext: WorkspaceInternalContext; private authContext?: AuthContext; @@ -36,7 +36,7 @@ export class WorkspaceSoftDeleteQueryBuilder< constructor( queryBuilder: SoftDeleteQueryBuilder, - objectRecordsPermissions: ObjectsPermissionsDeprecated, + objectRecordsPermissions: ObjectsPermissions, internalContext: WorkspaceInternalContext, shouldBypassPermissionChecks: boolean, authContext?: AuthContext, diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts index 51d153a0d2..a450133d4e 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts @@ -1,4 +1,4 @@ -import { type ObjectsPermissionsDeprecated } from 'twenty-shared/types'; +import { type ObjectsPermissions } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { type EntityTarget, @@ -28,13 +28,13 @@ import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/wo import { type WorkspaceSoftDeleteQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-soft-delete-query-builder'; import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; -import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util'; import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util'; +import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util'; export class WorkspaceUpdateQueryBuilder< T extends ObjectLiteral, > extends UpdateQueryBuilder { - private objectRecordsPermissions: ObjectsPermissionsDeprecated; + private objectRecordsPermissions: ObjectsPermissions; private shouldBypassPermissionChecks: boolean; private internalContext: WorkspaceInternalContext; private authContext?: AuthContext; @@ -50,7 +50,7 @@ export class WorkspaceUpdateQueryBuilder< constructor( queryBuilder: UpdateQueryBuilder, - objectRecordsPermissions: ObjectsPermissionsDeprecated, + objectRecordsPermissions: ObjectsPermissions, internalContext: WorkspaceInternalContext, shouldBypassPermissionChecks: boolean, authContext?: AuthContext, diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts index 28d99877d4..38bce46765 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts @@ -1,4 +1,4 @@ -import { type ObjectsPermissionsDeprecated } from 'twenty-shared/types'; +import { type ObjectsPermissions } from 'twenty-shared/types'; import { type DeepPartial, type DeleteResult, @@ -40,7 +40,7 @@ export class WorkspaceRepository< private readonly internalContext: WorkspaceInternalContext; private shouldBypassPermissionChecks: boolean; private featureFlagMap: FeatureFlagMap; - public readonly objectRecordsPermissions?: ObjectsPermissionsDeprecated; + public readonly objectRecordsPermissions?: ObjectsPermissions; private authContext?: AuthContext; declare manager: WorkspaceEntityManager; @@ -50,7 +50,7 @@ export class WorkspaceRepository< manager: WorkspaceEntityManager, featureFlagMap: FeatureFlagMap, queryRunner?: QueryRunner, - objectRecordsPermissions?: ObjectsPermissionsDeprecated, + objectRecordsPermissions?: ObjectsPermissions, shouldBypassPermissionChecks = false, authContext?: AuthContext, ) { diff --git a/packages/twenty-server/src/engine/twenty-orm/twenty-orm-global.manager.ts b/packages/twenty-server/src/engine/twenty-orm/twenty-orm-global.manager.ts index af5ed7138a..8f72b71fb3 100644 --- a/packages/twenty-server/src/engine/twenty-orm/twenty-orm-global.manager.ts +++ b/packages/twenty-server/src/engine/twenty-orm/twenty-orm-global.manager.ts @@ -4,6 +4,7 @@ import { type ObjectLiteral } from 'typeorm'; import { WorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/workspace-datasource.factory'; import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; +import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util'; @Injectable() @@ -15,30 +16,19 @@ export class TwentyORMGlobalManager { async getRepositoryForWorkspace( workspaceId: string, workspaceEntity: Type, - options?: { - shouldBypassPermissionChecks?: boolean; - roleId?: string; - }, + options?: RolePermissionConfig, ): Promise>; async getRepositoryForWorkspace( workspaceId: string, objectMetadataName: string, - options?: { - shouldBypassPermissionChecks?: boolean; - roleId?: string; - }, + options?: RolePermissionConfig, ): Promise>; async getRepositoryForWorkspace( workspaceId: string, workspaceEntityOrObjectMetadataName: Type | string, - options: { - shouldBypassPermissionChecks?: boolean; - roleId?: string; - } = { - shouldBypassPermissionChecks: false, - }, + options?: RolePermissionConfig, ): Promise> { let objectMetadataName: string; @@ -55,8 +45,7 @@ export class TwentyORMGlobalManager { const repository = workspaceDataSource.getRepository( objectMetadataName, - options.shouldBypassPermissionChecks, - options.roleId, + options, ); return repository; diff --git a/packages/twenty-server/src/engine/twenty-orm/twenty-orm.manager.ts b/packages/twenty-server/src/engine/twenty-orm/twenty-orm.manager.ts index 039dc9443e..36147c7e13 100644 --- a/packages/twenty-server/src/engine/twenty-orm/twenty-orm.manager.ts +++ b/packages/twenty-server/src/engine/twenty-orm/twenty-orm.manager.ts @@ -74,8 +74,7 @@ export class TwentyORMManager { return workspaceDataSource.getRepository( objectMetadataName, - false, - roleId, + roleId ? { unionOf: [roleId] } : undefined, ); } diff --git a/packages/twenty-server/src/engine/twenty-orm/types/role-permission-config.ts b/packages/twenty-server/src/engine/twenty-orm/types/role-permission-config.ts new file mode 100644 index 0000000000..48a3a3cb50 --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/types/role-permission-config.ts @@ -0,0 +1,6 @@ +type RoleId = string; + +export type RolePermissionConfig = + | { shouldBypassPermissionChecks: true } + | { unionOf: RoleId[] } + | { intersectionOf: RoleId[] }; diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/compute-permission-intersection.util.spec.ts b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/compute-permission-intersection.util.spec.ts new file mode 100644 index 0000000000..dd5dd2c182 --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/compute-permission-intersection.util.spec.ts @@ -0,0 +1,340 @@ +import { type ObjectsPermissions } from 'twenty-shared/types'; + +import { computePermissionIntersection } from 'src/engine/twenty-orm/utils/compute-permission-intersection.util'; + +describe('computePermissionIntersection', () => { + const objectMetadataId1 = 'object-1'; + const objectMetadataId2 = 'object-2'; + + describe('edge cases', () => { + it('should return empty object for empty array', () => { + const result = computePermissionIntersection([]); + + expect(result).toEqual({}); + }); + + it('should return same permissions for single role', () => { + const permissions: ObjectsPermissions = { + [objectMetadataId1]: { + canReadObjectRecords: true, + canUpdateObjectRecords: true, + canSoftDeleteObjectRecords: false, + canDestroyObjectRecords: false, + restrictedFields: {}, + }, + }; + + const result = computePermissionIntersection([permissions]); + + expect(result).toEqual(permissions); + }); + }); + + describe('intersection logic (AND)', () => { + it('should require ALL roles to have permission (true AND true = true)', () => { + const role1Permissions: ObjectsPermissions = { + [objectMetadataId1]: { + canReadObjectRecords: true, + canUpdateObjectRecords: true, + canSoftDeleteObjectRecords: true, + canDestroyObjectRecords: true, + restrictedFields: {}, + }, + }; + + const role2Permissions: ObjectsPermissions = { + [objectMetadataId1]: { + canReadObjectRecords: true, + canUpdateObjectRecords: true, + canSoftDeleteObjectRecords: true, + canDestroyObjectRecords: true, + restrictedFields: {}, + }, + }; + + const result = computePermissionIntersection([ + role1Permissions, + role2Permissions, + ]); + + expect(result[objectMetadataId1]).toEqual({ + canReadObjectRecords: true, + canUpdateObjectRecords: true, + canSoftDeleteObjectRecords: true, + canDestroyObjectRecords: true, + restrictedFields: {}, + }); + }); + + it('should deny if ANY role lacks permission (true AND false = false)', () => { + const role1Permissions: ObjectsPermissions = { + [objectMetadataId1]: { + canReadObjectRecords: true, + canUpdateObjectRecords: true, + canSoftDeleteObjectRecords: true, + canDestroyObjectRecords: true, + restrictedFields: {}, + }, + }; + + const role2Permissions: ObjectsPermissions = { + [objectMetadataId1]: { + canReadObjectRecords: true, + canUpdateObjectRecords: false, + canSoftDeleteObjectRecords: false, + canDestroyObjectRecords: true, + restrictedFields: {}, + }, + }; + + const result = computePermissionIntersection([ + role1Permissions, + role2Permissions, + ]); + + expect(result[objectMetadataId1]).toEqual({ + canReadObjectRecords: true, + canUpdateObjectRecords: false, + canSoftDeleteObjectRecords: false, + canDestroyObjectRecords: true, + restrictedFields: {}, + }); + }); + + it('should deny all permissions if role lacks access to object entirely', () => { + const role1Permissions: ObjectsPermissions = { + [objectMetadataId1]: { + canReadObjectRecords: true, + canUpdateObjectRecords: true, + canSoftDeleteObjectRecords: true, + canDestroyObjectRecords: true, + restrictedFields: {}, + }, + }; + + const role2Permissions: ObjectsPermissions = {}; + + const result = computePermissionIntersection([ + role1Permissions, + role2Permissions, + ]); + + expect(result[objectMetadataId1]).toEqual({ + canReadObjectRecords: false, + canUpdateObjectRecords: false, + canSoftDeleteObjectRecords: false, + canDestroyObjectRecords: false, + restrictedFields: {}, + }); + }); + }); + + describe('multiple objects', () => { + it('should compute intersection independently for each object', () => { + const role1Permissions: ObjectsPermissions = { + [objectMetadataId1]: { + canReadObjectRecords: true, + canUpdateObjectRecords: true, + canSoftDeleteObjectRecords: false, + canDestroyObjectRecords: false, + restrictedFields: {}, + }, + [objectMetadataId2]: { + canReadObjectRecords: true, + canUpdateObjectRecords: false, + canSoftDeleteObjectRecords: false, + canDestroyObjectRecords: false, + restrictedFields: {}, + }, + }; + + const role2Permissions: ObjectsPermissions = { + [objectMetadataId1]: { + canReadObjectRecords: true, + canUpdateObjectRecords: false, + canSoftDeleteObjectRecords: true, + canDestroyObjectRecords: false, + restrictedFields: {}, + }, + [objectMetadataId2]: { + canReadObjectRecords: true, + canUpdateObjectRecords: true, + canSoftDeleteObjectRecords: false, + canDestroyObjectRecords: false, + restrictedFields: {}, + }, + }; + + const result = computePermissionIntersection([ + role1Permissions, + role2Permissions, + ]); + + expect(result[objectMetadataId1]).toEqual({ + canReadObjectRecords: true, + canUpdateObjectRecords: false, + canSoftDeleteObjectRecords: false, + canDestroyObjectRecords: false, + restrictedFields: {}, + }); + + expect(result[objectMetadataId2]).toEqual({ + canReadObjectRecords: true, + canUpdateObjectRecords: false, + canSoftDeleteObjectRecords: false, + canDestroyObjectRecords: false, + restrictedFields: {}, + }); + }); + }); + + describe('restricted fields', () => { + it('should compute intersection for restricted fields', () => { + const role1Permissions: ObjectsPermissions = { + [objectMetadataId1]: { + canReadObjectRecords: true, + canUpdateObjectRecords: true, + canSoftDeleteObjectRecords: true, + canDestroyObjectRecords: true, + restrictedFields: { + email: { + canRead: null, + canUpdate: false, + }, + salary: { + canRead: false, + canUpdate: null, + }, + }, + }, + }; + + const role2Permissions: ObjectsPermissions = { + [objectMetadataId1]: { + canReadObjectRecords: true, + canUpdateObjectRecords: true, + canSoftDeleteObjectRecords: true, + canDestroyObjectRecords: true, + restrictedFields: { + email: { + canRead: null, + canUpdate: null, + }, + salary: { + canRead: null, + canUpdate: false, + }, + }, + }, + }; + + const result = computePermissionIntersection([ + role1Permissions, + role2Permissions, + ]); + + expect(result[objectMetadataId1].restrictedFields).toEqual({ + email: { + canRead: null, + canUpdate: false, + }, + salary: { + canRead: false, + canUpdate: false, + }, + }); + }); + + it('should handle fields that only exist in some roles', () => { + const role1Permissions: ObjectsPermissions = { + [objectMetadataId1]: { + canReadObjectRecords: true, + canUpdateObjectRecords: true, + canSoftDeleteObjectRecords: true, + canDestroyObjectRecords: true, + restrictedFields: { + email: { + canRead: false, + canUpdate: false, + }, + }, + }, + }; + + const role2Permissions: ObjectsPermissions = { + [objectMetadataId1]: { + canReadObjectRecords: true, + canUpdateObjectRecords: true, + canSoftDeleteObjectRecords: true, + canDestroyObjectRecords: true, + restrictedFields: { + salary: { + canRead: false, + canUpdate: false, + }, + }, + }, + }; + + const result = computePermissionIntersection([ + role1Permissions, + role2Permissions, + ]); + + expect(result[objectMetadataId1].restrictedFields).toEqual({ + email: { + canRead: false, + canUpdate: false, + }, + salary: { + canRead: false, + canUpdate: false, + }, + }); + }); + }); + + describe('three or more roles', () => { + it('should compute intersection across all roles', () => { + const role1: ObjectsPermissions = { + [objectMetadataId1]: { + canReadObjectRecords: true, + canUpdateObjectRecords: true, + canSoftDeleteObjectRecords: true, + canDestroyObjectRecords: true, + restrictedFields: {}, + }, + }; + + const role2: ObjectsPermissions = { + [objectMetadataId1]: { + canReadObjectRecords: true, + canUpdateObjectRecords: true, + canSoftDeleteObjectRecords: false, + canDestroyObjectRecords: true, + restrictedFields: {}, + }, + }; + + const role3: ObjectsPermissions = { + [objectMetadataId1]: { + canReadObjectRecords: true, + canUpdateObjectRecords: false, + canSoftDeleteObjectRecords: true, + canDestroyObjectRecords: true, + restrictedFields: {}, + }, + }; + + const result = computePermissionIntersection([role1, role2, role3]); + + expect(result[objectMetadataId1]).toEqual({ + canReadObjectRecords: true, + canUpdateObjectRecords: false, + canSoftDeleteObjectRecords: false, + canDestroyObjectRecords: true, + restrictedFields: {}, + }); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/compute-permission-intersection.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/compute-permission-intersection.util.ts new file mode 100644 index 0000000000..f6ea3c373b --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/utils/compute-permission-intersection.util.ts @@ -0,0 +1,92 @@ +import { + type ObjectsPermissions, + type RestrictedFieldPermissions, +} from 'twenty-shared/types'; + +export const computePermissionIntersection = ( + permissionsArray: ObjectsPermissions[], +): ObjectsPermissions => { + if (permissionsArray.length === 0) { + return {}; + } + + if (permissionsArray.length === 1) { + return permissionsArray[0]; + } + + const result: ObjectsPermissions = {}; + + const allObjectMetadataIds = new Set(); + + for (const permissions of permissionsArray) { + for (const id of Object.keys(permissions)) { + allObjectMetadataIds.add(id); + } + } + + for (const objectMetadataId of allObjectMetadataIds) { + let canReadObjectRecords = true; + let canUpdateObjectRecords = true; + let canSoftDeleteObjectRecords = true; + let canDestroyObjectRecords = true; + const restrictedFields: Record = {}; + + for (const permissions of permissionsArray) { + const objPerm = permissions[objectMetadataId]; + + if (!objPerm) { + canReadObjectRecords = false; + canUpdateObjectRecords = false; + canSoftDeleteObjectRecords = false; + canDestroyObjectRecords = false; + continue; + } + + canReadObjectRecords = + canReadObjectRecords && objPerm.canReadObjectRecords === true; + canUpdateObjectRecords = + canUpdateObjectRecords && objPerm.canUpdateObjectRecords === true; + canSoftDeleteObjectRecords = + canSoftDeleteObjectRecords && + objPerm.canSoftDeleteObjectRecords === true; + canDestroyObjectRecords = + canDestroyObjectRecords && objPerm.canDestroyObjectRecords === true; + + if (objPerm.restrictedFields) { + for (const [fieldName, fieldPerm] of Object.entries( + objPerm.restrictedFields, + )) { + if (!restrictedFields[fieldName]) { + restrictedFields[fieldName] = { + canRead: null, + canUpdate: null, + }; + } + + const current = restrictedFields[fieldName]; + + restrictedFields[fieldName] = { + canRead: + current.canRead === false || fieldPerm.canRead === false + ? false + : null, + canUpdate: + current.canUpdate === false || fieldPerm.canUpdate === false + ? false + : null, + }; + } + } + } + + result[objectMetadataId] = { + canReadObjectRecords, + canUpdateObjectRecords, + canSoftDeleteObjectRecords, + canDestroyObjectRecords, + restrictedFields, + }; + } + + return result; +}; diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/services/workflow-execution-context.service.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/services/workflow-execution-context.service.ts index 3b72431924..32b714da8d 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/services/workflow-execution-context.service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/services/workflow-execution-context.service.ts @@ -49,10 +49,14 @@ export class WorkflowExecutionContextService { }); } + const rolePermissionConfig = roleId + ? { unionOf: [roleId] } + : { shouldBypassPermissionChecks: true as const }; + return { isActingOnBehalfOfUser, initiator: workflowRun.createdBy, - roleId, + rolePermissionConfig, }; } } diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/types/workflow-execution-context.type.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/types/workflow-execution-context.type.ts index f989210e59..0bb89ad60d 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/types/workflow-execution-context.type.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/types/workflow-execution-context.type.ts @@ -1,7 +1,8 @@ import { type ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; +import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; export type WorkflowExecutionContext = { isActingOnBehalfOfUser: boolean; initiator: ActorMetadata; - roleId?: string; + rolePermissionConfig: RolePermissionConfig; }; diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/ai-agent/ai-agent.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/ai-agent/ai-agent.workflow-action.ts index 64a1e32173..3df983e3b0 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/ai-agent/ai-agent.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/ai-agent/ai-agent.workflow-action.ts @@ -85,7 +85,7 @@ export class AiAgentWorkflowAction implements WorkflowAction { actorContext: executionContext.isActingOnBehalfOfUser ? executionContext.initiator : undefined, - roleId: executionContext.roleId, + rolePermissionConfig: executionContext.rolePermissionConfig, }, ); diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/ai-agent/services/ai-agent-executor.service.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/ai-agent/services/ai-agent-executor.service.ts index 44e2ebcd8d..5edb74fc12 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/ai-agent/services/ai-agent-executor.service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/ai-agent/services/ai-agent-executor.service.ts @@ -18,6 +18,7 @@ import { AGENT_SYSTEM_PROMPTS } from 'src/engine/metadata-modules/agent/constant import { convertOutputSchemaToZod } from 'src/engine/metadata-modules/agent/utils/convert-output-schema-to-zod'; import { type ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; +import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; import { OutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type'; @Injectable() @@ -35,7 +36,7 @@ export class AiAgentExecutorService { agentId: string, workspaceId: string, actorContext?: ActorMetadata, - roleIdOverride?: string, + rolePermissionConfig?: RolePermissionConfig, ): Promise { const roleTarget = await this.roleTargetsRepository.findOne({ where: { @@ -45,21 +46,33 @@ export class AiAgentExecutorService { select: ['roleId'], }); - const effectiveRoleId = roleIdOverride || roleTarget?.roleId; + const agentRoleId = roleTarget?.roleId; - if (!effectiveRoleId) { - const actionTools = await this.toolAdapterService.getTools(); + if (!rolePermissionConfig && !agentRoleId) { + return await this.toolAdapterService.getTools(); + } - return { ...actionTools }; + let effectiveRoleContext: RolePermissionConfig; + + if ( + rolePermissionConfig && + ('intersectionOf' in rolePermissionConfig || + 'unionOf' in rolePermissionConfig) + ) { + effectiveRoleContext = rolePermissionConfig; + } else if (agentRoleId) { + effectiveRoleContext = { unionOf: [agentRoleId] }; + } else { + return await this.toolAdapterService.getTools(); } const actionTools = await this.toolAdapterService.getTools( - effectiveRoleId, + effectiveRoleContext, workspaceId, ); const databaseTools = await this.toolService.listTools( - effectiveRoleId, + effectiveRoleContext, workspaceId, actorContext, ); @@ -75,20 +88,25 @@ export class AiAgentExecutorService { schema, userPrompt, actorContext, - roleId, + rolePermissionConfig, }: { agent: AgentEntity | null; schema: OutputSchema; userPrompt: string; actorContext?: ActorMetadata; - roleId?: string; + rolePermissionConfig?: RolePermissionConfig; }): Promise { try { const registeredModel = await this.aiModelRegistryService.resolveModelForAgent(agent); const tools = agent - ? await this.getTools(agent.id, agent.workspaceId, actorContext, roleId) + ? await this.getTools( + agent.id, + agent.workspaceId, + actorContext, + rolePermissionConfig, + ) : {}; this.logger.log(`Generated ${Object.keys(tools).length} tools for agent`); diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts index 19693ef9b0..d7114a4f64 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts @@ -64,7 +64,7 @@ export class CreateRecordWorkflowAction implements WorkflowAction { objectRecord: workflowActionInput.objectRecord, workspaceId, createdBy, - roleId: executionContext.roleId, + rolePermissionConfig: executionContext.rolePermissionConfig, }); if (!toolOutput.success) { diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/delete-record.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/delete-record.workflow-action.ts index 7fc755e2d2..3d5d1b2d28 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/delete-record.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/delete-record.workflow-action.ts @@ -79,7 +79,7 @@ export class DeleteRecordWorkflowAction implements WorkflowAction { objectName: workflowActionInput.objectName, objectRecordId: workflowActionInput.objectRecordId, workspaceId, - roleId: executionContext.roleId, + rolePermissionConfig: executionContext.rolePermissionConfig, soft: true, }); diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts index 02f89a4707..7bf6818842 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts @@ -70,7 +70,7 @@ export class FindRecordsWorkflowAction implements WorkflowAction { orderBy: workflowActionInput.orderBy, limit: workflowActionInput.limit, workspaceId, - roleId: executionContext.roleId, + rolePermissionConfig: executionContext.rolePermissionConfig, }); if (!toolOutput.success) { diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts index cfdde28e3e..62ec017610 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts @@ -81,7 +81,7 @@ export class UpdateRecordWorkflowAction implements WorkflowAction { objectRecord: workflowActionInput.objectRecord, fieldsToUpdate: workflowActionInput.fieldsToUpdate, workspaceId, - roleId: executionContext.roleId, + rolePermissionConfig: executionContext.rolePermissionConfig, }); if (!toolOutput.success) { diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service.ts index a08aa659cd..307b201dbd 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service.ts @@ -8,6 +8,7 @@ import type { CreateWorkflowVersionStepInput } from 'src/engine/core-modules/wor import type { UpdateWorkflowVersionPositionsInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-version-positions-input.dto'; import type { UpdateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-version-step-input.dto'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; import { WorkflowVersionStatus } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; import { WorkflowStatus } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity'; import { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service'; @@ -43,7 +44,10 @@ export class WorkflowToolWorkspaceService { private readonly recordPositionService: RecordPositionService, ) {} - generateWorkflowTools(workspaceId: string, roleId: string): ToolSet { + generateWorkflowTools( + workspaceId: string, + rolePermissionConfig: RolePermissionConfig, + ): ToolSet { const tools: ToolSet = {}; tools.create_complete_workflow = { @@ -88,7 +92,7 @@ This is the most efficient way for AI to create workflows as it handles all the const workflowId = await this.createWorkflow({ workspaceId, name: parameters.name, - roleId, + rolePermissionConfig, }); const workflowVersionId = await this.createWorkflowVersion({ @@ -96,7 +100,7 @@ This is the most efficient way for AI to create workflows as it handles all the workflowId, trigger: parameters.trigger, steps: parameters.steps, - roleId, + rolePermissionConfig, }); if (parameters.stepPositions && parameters.stepPositions.length > 0) { @@ -132,7 +136,7 @@ This is the most efficient way for AI to create workflows as it handles all the workspaceId, workflowId, workflowVersionId, - roleId, + rolePermissionConfig, }); } @@ -400,17 +404,17 @@ This is the most efficient way for AI to create workflows as it handles all the private async createWorkflow({ workspaceId, name, - roleId, + rolePermissionConfig, }: { workspaceId: string; name: string; - roleId: string; + rolePermissionConfig: RolePermissionConfig; }): Promise { const workflowRepository = await this.twentyORMGlobalManager.getRepositoryForWorkspace( workspaceId, 'workflow', - { roleId }, + rolePermissionConfig, ); const workflowPosition = @@ -440,19 +444,19 @@ This is the most efficient way for AI to create workflows as it handles all the workflowId, trigger, steps, - roleId, + rolePermissionConfig, }: { workspaceId: string; workflowId: string; trigger: WorkflowTrigger; steps: WorkflowAction[]; - roleId: string; + rolePermissionConfig: RolePermissionConfig; }): Promise { const workflowVersionRepository = await this.twentyORMGlobalManager.getRepositoryForWorkspace( workspaceId, 'workflowVersion', - { roleId }, + rolePermissionConfig, ); const versionPosition = @@ -484,18 +488,18 @@ This is the most efficient way for AI to create workflows as it handles all the workspaceId, workflowId, workflowVersionId, - roleId, + rolePermissionConfig, }: { workspaceId: string; workflowId: string; workflowVersionId: string; - roleId: string; + rolePermissionConfig: RolePermissionConfig; }) { const workflowRepository = await this.twentyORMGlobalManager.getRepositoryForWorkspace( workspaceId, 'workflow', - { roleId }, + rolePermissionConfig, ); await workflowRepository.update(workflowId, { diff --git a/packages/twenty-server/test/integration/metadata/suites/agent/agent-tool.service.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/agent/agent-tool.service.integration-spec.ts index 37ae610418..f0c9535731 100644 --- a/packages/twenty-server/test/integration/metadata/suites/agent/agent-tool.service.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/agent/agent-tool.service.integration-spec.ts @@ -22,11 +22,8 @@ describe('AgentToolGeneratorService Integration', () => { }; jest - .spyOn(context.agentService, 'findOneAgent') - .mockResolvedValue(context.testAgent as any); - jest - .spyOn(context.roleRepository, 'findOne') - .mockResolvedValue(roleWithFullPermissions); + .spyOn(context.roleRepository, 'find') + .mockResolvedValue([roleWithFullPermissions]); jest .spyOn( context.workspacePermissionsCacheService, @@ -36,10 +33,6 @@ describe('AgentToolGeneratorService Integration', () => { data: { [context.testRoleId]: { [context.testObjectMetadata.id]: { - canRead: true, - canUpdate: true, - canSoftDelete: true, - canDestroy: true, canReadObjectRecords: true, canUpdateObjectRecords: true, canSoftDeleteObjectRecords: true, @@ -57,6 +50,8 @@ describe('AgentToolGeneratorService Integration', () => { const tools = await context.agentToolService.generateToolsForAgent( context.testAgentId, context.testWorkspaceId, + undefined, + [context.testRoleId], ); expect(tools).toBeDefined(); @@ -72,11 +67,8 @@ describe('AgentToolGeneratorService Integration', () => { it('should generate read-only tools for agent with read permissions only', async () => { jest - .spyOn(context.agentService, 'findOneAgent') - .mockResolvedValue(context.testAgent as any); - jest - .spyOn(context.roleRepository, 'findOne') - .mockResolvedValue(context.testRole); + .spyOn(context.roleRepository, 'find') + .mockResolvedValue([context.testRole]); jest .spyOn( context.workspacePermissionsCacheService, @@ -86,10 +78,6 @@ describe('AgentToolGeneratorService Integration', () => { data: { [context.testRoleId]: { [context.testObjectMetadata.id]: { - canRead: true, - canUpdate: false, - canSoftDelete: false, - canDestroy: false, canReadObjectRecords: true, canUpdateObjectRecords: false, canSoftDeleteObjectRecords: false, @@ -107,6 +95,8 @@ describe('AgentToolGeneratorService Integration', () => { const tools = await context.agentToolService.generateToolsForAgent( context.testAgentId, context.testWorkspaceId, + undefined, + [context.testRoleId], ); expect(tools).toBeDefined(); @@ -118,12 +108,6 @@ describe('AgentToolGeneratorService Integration', () => { }); it('should return only http request tool for agent without role', async () => { - const agentWithoutRole = { ...context.testAgent, roleId: null }; - - jest - .spyOn(context.agentService, 'findOneAgent') - .mockResolvedValue(agentWithoutRole as any); - const tools = await context.agentToolService.generateToolsForAgent( context.testAgentId, context.testWorkspaceId, @@ -141,11 +125,8 @@ describe('AgentToolGeneratorService Integration', () => { }; jest - .spyOn(context.agentService, 'findOneAgent') - .mockResolvedValue(context.testAgent as any); - jest - .spyOn(context.roleRepository, 'findOne') - .mockResolvedValue(context.testRole); + .spyOn(context.roleRepository, 'find') + .mockResolvedValue([context.testRole]); jest .spyOn( context.workspacePermissionsCacheService, @@ -155,10 +136,6 @@ describe('AgentToolGeneratorService Integration', () => { data: { [context.testRoleId]: { [workflowObject.id]: { - canRead: true, - canUpdate: true, - canSoftDelete: true, - canDestroy: false, canReadObjectRecords: true, canUpdateObjectRecords: true, canSoftDeleteObjectRecords: true, @@ -176,6 +153,8 @@ describe('AgentToolGeneratorService Integration', () => { const tools = await context.agentToolService.generateToolsForAgent( context.testAgentId, context.testWorkspaceId, + undefined, + [context.testRoleId], ); expect(Object.keys(tools)).toHaveLength(7); diff --git a/packages/twenty-server/test/integration/metadata/suites/agent/utils/agent-tool-test-utils.ts b/packages/twenty-server/test/integration/metadata/suites/agent/utils/agent-tool-test-utils.ts index 331ab75602..4f41132ae3 100644 --- a/packages/twenty-server/test/integration/metadata/suites/agent/utils/agent-tool-test-utils.ts +++ b/packages/twenty-server/test/integration/metadata/suites/agent/utils/agent-tool-test-utils.ts @@ -65,6 +65,7 @@ export const createAgentToolTestModule = provide: getRepositoryToken(RoleEntity), useValue: { findOne: jest.fn(), + find: jest.fn(), }, }, { @@ -161,6 +162,7 @@ export const createAgentToolTestModule = useValue: { hasToolPermission: jest.fn(), checkRolePermissions: jest.fn().mockReturnValue(true), + checkRolesPermissions: jest.fn().mockResolvedValue(true), }, }, { @@ -341,10 +343,6 @@ export const setupBasicPermissions = (context: AgentToolTestContext) => { data: { [context.testRoleId]: { [context.testObjectMetadata.id]: { - canRead: true, - canUpdate: true, - canSoftDelete: true, - canDestroy: false, canReadObjectRecords: true, canUpdateObjectRecords: true, canSoftDeleteObjectRecords: true, diff --git a/packages/twenty-shared/src/types/ObjectPermissionsDeprecated.ts b/packages/twenty-shared/src/types/ObjectPermissionsDeprecated.ts deleted file mode 100644 index 1be4733015..0000000000 --- a/packages/twenty-shared/src/types/ObjectPermissionsDeprecated.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { type RestrictedFieldsPermissions } from './RestrictedFieldsPermissions'; - -// TODO: DEPRECATE THIS -export type ObjectPermissionsDeprecated = { - canRead: boolean; - canUpdate: boolean; - canSoftDelete: boolean; - canDestroy: boolean; - restrictedFields: RestrictedFieldsPermissions; -}; diff --git a/packages/twenty-shared/src/types/ObjectsPermissionsByRoleIdDeprecated.ts b/packages/twenty-shared/src/types/ObjectsPermissionsByRoleIdDeprecated.ts deleted file mode 100644 index f58805ee3b..0000000000 --- a/packages/twenty-shared/src/types/ObjectsPermissionsByRoleIdDeprecated.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { type ObjectsPermissions } from '@/types/ObjectsPermissions'; -import { type ObjectsPermissionsDeprecated } from './ObjectsPermissionsDeprecated'; - -type RoleId = string; - -// TODO: DEPRECATE THIS -export type ObjectsPermissionsByRoleIdDeprecated = Record< - RoleId, - ObjectsPermissionsDeprecated & ObjectsPermissions ->; diff --git a/packages/twenty-shared/src/types/ObjectsPermissionsDeprecated.ts b/packages/twenty-shared/src/types/ObjectsPermissionsDeprecated.ts deleted file mode 100644 index 5274225263..0000000000 --- a/packages/twenty-shared/src/types/ObjectsPermissionsDeprecated.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { type ObjectPermissionsDeprecated } from './ObjectPermissionsDeprecated'; - -type ObjectMetadataId = string; - -// TODO: DEPRECATE THIS -export type ObjectsPermissionsDeprecated = Record< - ObjectMetadataId, - ObjectPermissionsDeprecated ->; diff --git a/packages/twenty-shared/src/types/index.ts b/packages/twenty-shared/src/types/index.ts index b5f56232cf..349a494a1f 100644 --- a/packages/twenty-shared/src/types/index.ts +++ b/packages/twenty-shared/src/types/index.ts @@ -34,7 +34,6 @@ export type { NonNullableRequired } from './NonNullableRequired'; export type { Nullable } from './Nullable'; export type { NullablePartial } from './NullablePartial'; export type { ObjectPermissions } from './ObjectPermissions'; -export type { ObjectPermissionsDeprecated } from './ObjectPermissionsDeprecated'; export type { ObjectRecord } from './ObjectRecord'; export type { AggregateOrderByWithGroupByField, @@ -47,8 +46,6 @@ export { OrderByDirection } from './ObjectRecordGroupBy'; export { ObjectRecordGroupByDateGranularity } from './ObjectRecordGroupByDateGranularity'; export type { ObjectsPermissions } from './ObjectsPermissions'; export type { ObjectsPermissionsByRoleId } from './ObjectsPermissionsByRoleId'; -export type { ObjectsPermissionsByRoleIdDeprecated } from './ObjectsPermissionsByRoleIdDeprecated'; -export type { ObjectsPermissionsDeprecated } from './ObjectsPermissionsDeprecated'; export type { PartialFieldMetadataItem } from './PartialFieldMetadataItem'; export type { PartialFieldMetadataItemOption } from './PartialFieldMetadataOption'; export { RecordFilterGroupLogicalOperator } from './RecordFilterGroupLogicalOperator';