From 7067f6ef88762ec1aba131e36a434aa2298ca23a Mon Sep 17 00:00:00 2001
From: Abdul Rahman <81605929+abdulrahmancodes@users.noreply.github.com>
Date: Fri, 24 Jul 2026 20:51:27 +0530
Subject: [PATCH] fix: honor agent rolePermissionConfig in record CRUD (#23248)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
- Agent tools were built with the agent’s `rolePermissionConfig`, but
record CRUD ignored it and re-resolved permissions from `authContext`
(app `defaultRoleId`)
- CRUD services now pass `rolePermissionConfig` through
`CommonApiContextBuilder` and the common query runner, so repository
access matches the agent role
- Workflow/chat paths already use the same role for auth and
`rolePermissionConfig`, so their behavior should be unchanged
---
.../common-base-query-runner.service.ts | 12 +--
.../common-base-query-runner-context.type.ts | 2 +
.../common-api-context-builder.service.ts | 38 +++++++--
.../services/create-many-records.service.ts | 4 +-
.../services/create-record.service.ts | 4 +-
.../services/delete-many-records.service.ts | 3 +-
.../services/delete-record.service.ts | 9 +-
.../services/find-records.service.ts | 2 +
.../services/group-by-records.service.ts | 2 +
.../services/update-many-records.service.ts | 4 +-
.../services/update-record.service.ts | 2 +
.../services/upsert-many-records.service.ts | 4 +-
.../services/upsert-record.service.ts | 4 +-
.../providers/database-tool.provider.ts | 41 ++-------
.../agent-async-executor.service.spec.ts | 4 +-
.../services/agent-async-executor.service.ts | 2 +-
...s-from-role-permission-config.util.spec.ts | 83 +++++++++++++++++++
...ssions-from-role-permission-config.util.ts | 33 ++++++++
18 files changed, 193 insertions(+), 60 deletions(-)
create mode 100644 packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-objects-permissions-from-role-permission-config.util.spec.ts
create mode 100644 packages/twenty-server/src/engine/twenty-orm/utils/get-objects-permissions-from-role-permission-config.util.ts
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 c902b80942..0189c6c80f 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
@@ -317,11 +317,13 @@ export abstract class CommonBaseQueryRunnerService<
): Promise> {
const context = getWorkspaceContext();
- const rolePermissionConfig = resolveRolePermissionConfig({
- authContext: context.authContext,
- userWorkspaceRoleMap: context.userWorkspaceRoleMap,
- apiKeyRoleMap: context.apiKeyRoleMap,
- });
+ const rolePermissionConfig =
+ queryRunnerContext.rolePermissionConfig ??
+ resolveRolePermissionConfig({
+ authContext: context.authContext,
+ userWorkspaceRoleMap: context.userWorkspaceRoleMap,
+ apiKeyRoleMap: context.apiKeyRoleMap,
+ });
if (!rolePermissionConfig) {
throw new CommonQueryRunnerException(
diff --git a/packages/twenty-server/src/engine/api/common/types/common-base-query-runner-context.type.ts b/packages/twenty-server/src/engine/api/common/types/common-base-query-runner-context.type.ts
index 938ffac8f6..aff74ac614 100644
--- a/packages/twenty-server/src/engine/api/common/types/common-base-query-runner-context.type.ts
+++ b/packages/twenty-server/src/engine/api/common/types/common-base-query-runner-context.type.ts
@@ -3,6 +3,7 @@ import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/typ
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
+import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
export type CommonBaseQueryRunnerContext = {
authContext: WorkspaceAuthContext;
@@ -11,4 +12,5 @@ export type CommonBaseQueryRunnerContext = {
flatFieldMetadataMaps: FlatEntityMaps;
flatIndexMaps?: FlatEntityMaps;
objectIdByNameSingular: Record;
+ rolePermissionConfig?: RolePermissionConfig;
};
diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/common-api-context-builder.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/common-api-context-builder.service.ts
index 7b4c623df4..48b72e21f2 100644
--- a/packages/twenty-server/src/engine/core-modules/record-crud/services/common-api-context-builder.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/common-api-context-builder.service.ts
@@ -22,6 +22,8 @@ import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-m
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
+import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
+import { getObjectsPermissionsFromRolePermissionConfig } from 'src/engine/twenty-orm/utils/get-objects-permissions-from-role-permission-config.util';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
export type CommonApiContext = {
@@ -45,9 +47,11 @@ export class CommonApiContextBuilderService {
async build({
authContext,
objectName,
+ rolePermissionConfig,
}: {
authContext: WorkspaceAuthContext;
objectName: string;
+ rolePermissionConfig?: RolePermissionConfig;
}): Promise {
const workspaceId = authContext.workspace.id;
@@ -94,7 +98,10 @@ export class CommonApiContextBuilderService {
);
}
- const objectsPermissions = await this.getObjectsPermissions(authContext);
+ const objectsPermissions = await this.getObjectsPermissions({
+ authContext,
+ rolePermissionConfig,
+ });
const restrictedFields =
objectsPermissions[flatObjectMetadata.id]?.restrictedFields ?? {};
@@ -113,6 +120,7 @@ export class CommonApiContextBuilderService {
flatFieldMetadataMaps,
flatIndexMaps,
objectIdByNameSingular: idByNameSingular,
+ rolePermissionConfig,
},
selectedFields,
flatObjectMetadata,
@@ -122,10 +130,27 @@ export class CommonApiContextBuilderService {
};
}
- private async getObjectsPermissions(
- authContext: WorkspaceAuthContext,
- ): Promise {
+ private async getObjectsPermissions({
+ authContext,
+ rolePermissionConfig,
+ }: {
+ authContext: WorkspaceAuthContext;
+ rolePermissionConfig?: RolePermissionConfig;
+ }): Promise {
const workspaceId = authContext.workspace.id;
+
+ const { rolesPermissions } =
+ await this.workspaceCacheService.getOrRecompute(workspaceId, [
+ 'rolesPermissions',
+ ]);
+
+ if (isDefined(rolePermissionConfig)) {
+ return getObjectsPermissionsFromRolePermissionConfig({
+ rolesPermissions,
+ rolePermissionConfig,
+ });
+ }
+
let roleId: string;
if (isApiKeyAuthContext(authContext)) {
@@ -160,11 +185,6 @@ export class CommonApiContextBuilderService {
);
}
- const { rolesPermissions } =
- await this.workspaceCacheService.getOrRecompute(workspaceId, [
- 'rolesPermissions',
- ]);
-
return rolesPermissions[roleId] ?? {};
}
}
diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/create-many-records.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/create-many-records.service.ts
index 0ec3ba5e18..9c4bba5f85 100644
--- a/packages/twenty-server/src/engine/core-modules/record-crud/services/create-many-records.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/create-many-records.service.ts
@@ -24,7 +24,8 @@ export class CreateManyRecordsService {
) {}
async execute(params: CreateManyRecordsParams): Promise {
- const { objectName, objectRecords, authContext } = params;
+ const { objectName, objectRecords, authContext, rolePermissionConfig } =
+ params;
try {
const {
@@ -35,6 +36,7 @@ export class CreateManyRecordsService {
} = await this.commonApiContextBuilder.build({
authContext,
objectName,
+ rolePermissionConfig,
});
if (
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 cd36ddb67e..aefb3f23e9 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
@@ -24,7 +24,8 @@ export class CreateRecordService {
) {}
async execute(params: CreateRecordParams): Promise {
- const { objectName, objectRecord, authContext } = params;
+ const { objectName, objectRecord, authContext, rolePermissionConfig } =
+ params;
try {
const {
@@ -35,6 +36,7 @@ export class CreateRecordService {
} = await this.commonApiContextBuilder.build({
authContext,
objectName,
+ rolePermissionConfig,
});
if (
diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/delete-many-records.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/delete-many-records.service.ts
index 473db7fb07..3966a0a90d 100644
--- a/packages/twenty-server/src/engine/core-modules/record-crud/services/delete-many-records.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/delete-many-records.service.ts
@@ -21,7 +21,7 @@ export class DeleteManyRecordsService {
) {}
async execute(params: DeleteManyRecordsParams): Promise {
- const { objectName, filter, authContext } = params;
+ const { objectName, filter, authContext, rolePermissionConfig } = params;
if (!isDefined(filter) || isEmptyObject(filter)) {
return {
@@ -37,6 +37,7 @@ export class DeleteManyRecordsService {
await this.commonApiContextBuilder.build({
authContext,
objectName,
+ rolePermissionConfig,
});
if (
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 b6eea807b6..b8a3d7ecee 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
@@ -24,7 +24,13 @@ export class DeleteRecordService {
) {}
async execute(params: DeleteRecordParams): Promise {
- const { objectName, objectRecordId, authContext, soft = true } = params;
+ const {
+ objectName,
+ objectRecordId,
+ authContext,
+ rolePermissionConfig,
+ soft = true,
+ } = params;
if (!isDefined(objectRecordId) || !isValidUuid(objectRecordId)) {
return {
@@ -39,6 +45,7 @@ export class DeleteRecordService {
await this.commonApiContextBuilder.build({
authContext,
objectName,
+ rolePermissionConfig,
});
if (
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 929f5f986e..2374a85a64 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
@@ -34,6 +34,7 @@ export class FindRecordsService {
limit,
offset = 0,
authContext,
+ rolePermissionConfig,
select,
shouldBuildEffectiveSelectFields,
} = params;
@@ -55,6 +56,7 @@ export class FindRecordsService {
} = await this.commonApiContextBuilder.build({
authContext,
objectName,
+ rolePermissionConfig,
});
const { effectiveSelectedFields, warnings } =
diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/group-by-records.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/group-by-records.service.ts
index ed452d360b..610f1b4d6d 100644
--- a/packages/twenty-server/src/engine/core-modules/record-crud/services/group-by-records.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/group-by-records.service.ts
@@ -41,6 +41,7 @@ export class GroupByRecordsService {
orderBy = 'DESC',
filter,
authContext,
+ rolePermissionConfig,
} = params;
try {
@@ -52,6 +53,7 @@ export class GroupByRecordsService {
} = await this.commonApiContextBuilder.build({
authContext,
objectName,
+ rolePermissionConfig,
});
const availableAggregations =
diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/update-many-records.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/update-many-records.service.ts
index dbe83ad114..61ccab7c16 100644
--- a/packages/twenty-server/src/engine/core-modules/record-crud/services/update-many-records.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/update-many-records.service.ts
@@ -23,7 +23,8 @@ export class UpdateManyRecordsService {
) {}
async execute(params: UpdateManyRecordsParams): Promise {
- const { objectName, filter, data, authContext } = params;
+ const { objectName, filter, data, authContext, rolePermissionConfig } =
+ params;
try {
const {
@@ -34,6 +35,7 @@ export class UpdateManyRecordsService {
} = await this.commonApiContextBuilder.build({
authContext,
objectName,
+ rolePermissionConfig,
});
if (
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 ce07e5bd01..e8ac275add 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
@@ -30,6 +30,7 @@ export class UpdateRecordService {
objectRecord,
fieldsToUpdate,
authContext,
+ rolePermissionConfig,
} = params;
if (!isDefined(objectRecordId) || !isValidUuid(objectRecordId)) {
@@ -49,6 +50,7 @@ export class UpdateRecordService {
} = await this.commonApiContextBuilder.build({
authContext,
objectName,
+ rolePermissionConfig,
});
if (
diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/upsert-many-records.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/upsert-many-records.service.ts
index 3f7b01ff23..3a7c1dae16 100644
--- a/packages/twenty-server/src/engine/core-modules/record-crud/services/upsert-many-records.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/upsert-many-records.service.ts
@@ -25,7 +25,8 @@ export class UpsertManyRecordsService {
) {}
async execute(params: UpsertManyRecordsParams): Promise {
- const { objectName, objectRecords, authContext } = params;
+ const { objectName, objectRecords, authContext, rolePermissionConfig } =
+ params;
try {
const {
@@ -36,6 +37,7 @@ export class UpsertManyRecordsService {
} = await this.commonApiContextBuilder.build({
authContext,
objectName,
+ rolePermissionConfig,
});
if (
diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/upsert-record.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/upsert-record.service.ts
index 58ee63f859..cd2ca17308 100644
--- a/packages/twenty-server/src/engine/core-modules/record-crud/services/upsert-record.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/upsert-record.service.ts
@@ -22,13 +22,15 @@ export class UpsertRecordService {
) {}
async execute(params: UpsertRecordParams): Promise {
- const { objectName, objectRecord, authContext } = params;
+ const { objectName, objectRecord, authContext, rolePermissionConfig } =
+ params;
try {
const { queryRunnerContext, selectedFields, flatObjectMetadata } =
await this.commonApiContextBuilder.build({
authContext,
objectName,
+ rolePermissionConfig,
});
if (
diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/providers/database-tool.provider.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/providers/database-tool.provider.ts
index 6422372623..1c85dc8937 100644
--- a/packages/twenty-server/src/engine/core-modules/tool-provider/providers/database-tool.provider.ts
+++ b/packages/twenty-server/src/engine/core-modules/tool-provider/providers/database-tool.provider.ts
@@ -1,9 +1,5 @@
import { Injectable } from '@nestjs/common';
-import {
- type ObjectsPermissions,
- type ObjectsPermissionsByRoleId,
-} from 'twenty-shared/types';
import { camelToSnakeCase, isDefined } from 'twenty-shared/utils';
import { canObjectBeManagedByAutomation } from 'twenty-shared/workflow';
@@ -32,7 +28,7 @@ import { type ToolIndexEntry } from 'src/engine/core-modules/tool-provider/types
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
import { getDatabaseCrudToolFlatObjects } from 'src/engine/metadata-modules/ai/ai-agent/utils/get-database-crud-tool-flat-objects.util';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
-import { computePermissionIntersection } from 'src/engine/twenty-orm/utils/compute-permission-intersection.util';
+import { getObjectsPermissionsFromRolePermissionConfig } from 'src/engine/twenty-orm/utils/get-objects-permissions-from-role-permission-config.util';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { ToolCategory } from 'twenty-shared/ai';
@@ -77,12 +73,12 @@ export class DatabaseToolProvider implements ToolProvider {
'rolesPermissions',
]);
- const objectPermissions = this.getObjectPermissions(
+ const objectPermissions = getObjectsPermissionsFromRolePermissionConfig({
rolesPermissions,
- context.rolePermissionConfig,
- );
+ rolePermissionConfig: context.rolePermissionConfig,
+ });
- if (!objectPermissions) {
+ if (Object.keys(objectPermissions).length === 0) {
return descriptors;
}
@@ -427,31 +423,4 @@ export class DatabaseToolProvider implements ToolProvider {
toolNames.has(`upsert_many_${snakePlural}`)
);
}
-
- private getObjectPermissions(
- rolesPermissions: ObjectsPermissionsByRoleId,
- rolePermissionConfig: ToolProviderContext['rolePermissionConfig'],
- ): ObjectsPermissions | null {
- if ('intersectionOf' in rolePermissionConfig) {
- const allRolePermissions = rolePermissionConfig.intersectionOf.map(
- (roleId: string) => rolesPermissions[roleId],
- );
-
- return allRolePermissions.length === 1
- ? allRolePermissions[0]
- : computePermissionIntersection(allRolePermissions);
- }
-
- if ('unionOf' in rolePermissionConfig) {
- if (rolePermissionConfig.unionOf.length === 1) {
- return rolesPermissions[rolePermissionConfig.unionOf[0]];
- }
-
- throw new Error(
- 'Union permission logic for multiple roles not yet implemented',
- );
- }
-
- return null;
- }
}
diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/__tests__/agent-async-executor.service.spec.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/__tests__/agent-async-executor.service.spec.ts
index 6bb9dd1b1b..d647d8d971 100644
--- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/__tests__/agent-async-executor.service.spec.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/__tests__/agent-async-executor.service.spec.ts
@@ -132,7 +132,7 @@ describe('AgentAsyncExecutorService — workflow agent role-scoped tool resoluti
service = module.get(AgentAsyncExecutorService);
});
- it('passes unionOf: [agentRoleId] when the agent has a role assigned', async () => {
+ it('passes intersectionOf: [agentRoleId] when the agent has a role assigned', async () => {
roleTargetRepository.findOne.mockResolvedValueOnce({ roleId: agentRoleId });
await service.executeAgent({
@@ -145,7 +145,7 @@ describe('AgentAsyncExecutorService — workflow agent role-scoped tool resoluti
expect(toolRegistry.getToolsByCategories).toHaveBeenCalledWith(
expect.objectContaining({
roleId: agentRoleId,
- rolePermissionConfig: { unionOf: [agentRoleId] },
+ rolePermissionConfig: { intersectionOf: [agentRoleId] },
workspaceId,
}),
expect.objectContaining({ wrapWithErrorContext: false }),
diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-async-executor.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-async-executor.service.ts
index 32e07bca0d..944b5f6715 100644
--- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-async-executor.service.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-async-executor.service.ts
@@ -176,7 +176,7 @@ export class AgentAsyncExecutorService {
// permission-tab role. No role means no registry tools.
if (isDefined(agentRoleId)) {
const agentRolePermissionConfig: RolePermissionConfig = {
- unionOf: [agentRoleId],
+ intersectionOf: [agentRoleId],
};
const toolProviderContext: ToolProviderContext = {
diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-objects-permissions-from-role-permission-config.util.spec.ts b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-objects-permissions-from-role-permission-config.util.spec.ts
new file mode 100644
index 0000000000..1f7d601a8c
--- /dev/null
+++ b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-objects-permissions-from-role-permission-config.util.spec.ts
@@ -0,0 +1,83 @@
+import { type ObjectsPermissionsByRoleId } from 'twenty-shared/types';
+
+import { getObjectsPermissionsFromRolePermissionConfig } from 'src/engine/twenty-orm/utils/get-objects-permissions-from-role-permission-config.util';
+
+const OBJECT_ID = 'object-1';
+
+const agentRolePermissions = {
+ [OBJECT_ID]: {
+ canReadObjectRecords: true,
+ canUpdateObjectRecords: false,
+ canSoftDeleteObjectRecords: false,
+ canDestroyObjectRecords: false,
+ restrictedFields: {},
+ rowLevelPermissionPredicates: [],
+ rowLevelPermissionPredicateGroups: [],
+ },
+};
+
+const defaultRolePermissions = {
+ [OBJECT_ID]: {
+ canReadObjectRecords: false,
+ canUpdateObjectRecords: false,
+ canSoftDeleteObjectRecords: false,
+ canDestroyObjectRecords: false,
+ restrictedFields: {},
+ rowLevelPermissionPredicates: [],
+ rowLevelPermissionPredicateGroups: [],
+ },
+};
+
+const rolesPermissions: ObjectsPermissionsByRoleId = {
+ 'agent-role-id': agentRolePermissions,
+ 'default-role-id': defaultRolePermissions,
+};
+
+describe('getObjectsPermissionsFromRolePermissionConfig', () => {
+ it('should resolve a single union role', () => {
+ expect(
+ getObjectsPermissionsFromRolePermissionConfig({
+ rolesPermissions,
+ rolePermissionConfig: { unionOf: ['agent-role-id'] },
+ }),
+ ).toEqual(agentRolePermissions);
+ });
+
+ it('should resolve a single intersection role', () => {
+ expect(
+ getObjectsPermissionsFromRolePermissionConfig({
+ rolesPermissions,
+ rolePermissionConfig: { intersectionOf: ['default-role-id'] },
+ }),
+ ).toEqual(defaultRolePermissions);
+ });
+
+ it('should use the first role when multiple are provided', () => {
+ expect(
+ getObjectsPermissionsFromRolePermissionConfig({
+ rolesPermissions,
+ rolePermissionConfig: {
+ intersectionOf: ['agent-role-id', 'default-role-id'],
+ },
+ }),
+ ).toEqual(agentRolePermissions);
+ });
+
+ it('should return empty permissions when bypassing checks', () => {
+ expect(
+ getObjectsPermissionsFromRolePermissionConfig({
+ rolesPermissions,
+ rolePermissionConfig: { shouldBypassPermissionChecks: true },
+ }),
+ ).toEqual({});
+ });
+
+ it('should return empty permissions when the role is missing from the cache', () => {
+ expect(
+ getObjectsPermissionsFromRolePermissionConfig({
+ rolesPermissions,
+ rolePermissionConfig: { unionOf: ['missing-role-id'] },
+ }),
+ ).toEqual({});
+ });
+});
diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/get-objects-permissions-from-role-permission-config.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/get-objects-permissions-from-role-permission-config.util.ts
new file mode 100644
index 0000000000..f2fb20537d
--- /dev/null
+++ b/packages/twenty-server/src/engine/twenty-orm/utils/get-objects-permissions-from-role-permission-config.util.ts
@@ -0,0 +1,33 @@
+import {
+ type ObjectsPermissions,
+ type ObjectsPermissionsByRoleId,
+} from 'twenty-shared/types';
+import { isDefined } from 'twenty-shared/utils';
+
+import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
+
+// Multi-role union/intersection is not ready — use the first assigned role only.
+export const getObjectsPermissionsFromRolePermissionConfig = ({
+ rolesPermissions,
+ rolePermissionConfig,
+}: {
+ rolesPermissions: ObjectsPermissionsByRoleId;
+ rolePermissionConfig: RolePermissionConfig;
+}): ObjectsPermissions => {
+ if ('shouldBypassPermissionChecks' in rolePermissionConfig) {
+ return {};
+ }
+
+ const roleId =
+ 'intersectionOf' in rolePermissionConfig
+ ? rolePermissionConfig.intersectionOf[0]
+ : 'unionOf' in rolePermissionConfig
+ ? rolePermissionConfig.unionOf[0]
+ : undefined;
+
+ if (!isDefined(roleId)) {
+ return {};
+ }
+
+ return rolesPermissions[roleId] ?? {};
+};