From d5b41b2801250141d9f0c04dfb6d116aeaabe000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Tue, 24 Mar 2026 21:37:58 +0100 Subject: [PATCH] =?UTF-8?q?Unify=20auth=20context=20=E2=86=92=20role=20per?= =?UTF-8?q?mission=20config=20resolution=20into=20a=20single=20pure=20util?= =?UTF-8?q?ity=20(#18927)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Consolidates duplicated auth-context-to-role-ID resolution logic (previously in `PermissionsService.resolveRolePermissionConfigFromAuthContext` and `CommonBaseQueryRunnerService.getRoleIdOrThrow`) into a single pure utility function `resolveRolePermissionConfig` in the ORM layer - The utility is synchronous and operates on cached data (`userWorkspaceRoleMap`, `apiKeyRoleMap`) already loaded into the workspace context — no async calls, no service dependencies - Adds `apiKeyRoleMap` to `ORMWorkspaceContext` (it was already in the workspace cache, just not loaded into the ORM context) - Removes `PermissionsService` dependency from `NavigationMenuItemRecordIdentifierService` - Removes `UserRoleService` and `ApiKeyRoleService` injections from `CommonBaseQueryRunnerService` ## Test plan - [ ] Existing typecheck passes (`npx nx typecheck twenty-server`) - [ ] Verify record identifier resolution still works for navigation menu items (user, system, API key, and application auth contexts) - [ ] Verify GraphQL CRUD queries still enforce correct role-based permissions - [ ] Verify API key authenticated requests resolve permissions correctly Made with [Cursor](https://cursor.com) --- .../common-base-query-runner.service.ts | 69 +++++-------------- .../core-modules/search/search.module.ts | 9 +-- .../core-modules/search/search.resolver.ts | 35 ---------- .../search/services/search.service.ts | 13 +++- ...ion-menu-item-record-identifier.service.ts | 22 +++--- .../permissions/permissions.service.ts | 46 ------------- .../workspace-entity-manager.spec.ts | 1 + .../global-workspace-orm.manager.ts | 4 ++ .../storage/orm-workspace-context.storage.ts | 1 + .../resolve-role-permission-config.util.ts | 52 ++++++++++++++ 10 files changed, 100 insertions(+), 152 deletions(-) create mode 100644 packages/twenty-server/src/engine/twenty-orm/utils/resolve-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 3fa32e0f06..f0b071ad6d 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 @@ -1,7 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { type PermissionFlagType } from 'twenty-shared/constants'; -import { isDefined } from 'twenty-shared/utils'; import { QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-field-value'; @@ -29,9 +28,7 @@ import { OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS } from 'src/engine/api/g import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; import { WorkspacePreQueryHookPayload } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type'; import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service'; -import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service'; import { isApiKeyAuthContext } from 'src/engine/core-modules/auth/guards/is-api-key-auth-context.guard'; -import { isApplicationAuthContext } from 'src/engine/core-modules/auth/guards/is-application-auth-context.guard'; import { isUserAuthContext } from 'src/engine/core-modules/auth/guards/is-user-auth-context.guard'; import { WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; @@ -48,9 +45,9 @@ import { PermissionsExceptionMessage, } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; -import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; -import type { RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; +import { getWorkspaceContext } from 'src/engine/twenty-orm/storage/orm-workspace-context.storage'; +import { resolveRolePermissionConfig } from 'src/engine/twenty-orm/utils/resolve-role-permission-config.util'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; @Injectable() @@ -73,10 +70,6 @@ export abstract class CommonBaseQueryRunnerService< @Inject() protected readonly permissionsService: PermissionsService; @Inject() - protected readonly userRoleService: UserRoleService; - @Inject() - protected readonly apiKeyRoleService: ApiKeyRoleService; - @Inject() protected readonly workspaceCacheService: WorkspaceCacheService; @Inject() protected readonly commonResultGettersService: CommonResultGettersService; @@ -140,7 +133,6 @@ export abstract class CommonBaseQueryRunnerService< async () => this.executeQueryAndEnrichResults( processedArgs, - authContext, queryRunnerContext, commonQueryParser, ), @@ -205,13 +197,11 @@ export abstract class CommonBaseQueryRunnerService< private async executeQueryAndEnrichResults( processedArgs: CommonExtendedInput, - authContext: WorkspaceAuthContext, queryRunnerContext: CommonBaseQueryRunnerContext, commonQueryParser: GraphqlQueryParser, ): Promise { const extendedQueryRunnerContext = await this.prepareExtendedQueryRunnerContextWithGlobalDatasource( - authContext, queryRunnerContext, ); @@ -223,7 +213,7 @@ export abstract class CommonBaseQueryRunnerService< return this.enrichResultsWithGettersAndHooks({ results, operationName: this.operationName, - authContext, + authContext: extendedQueryRunnerContext.authContext, flatObjectMetadata: queryRunnerContext.flatObjectMetadata, flatObjectMetadataMaps: queryRunnerContext.flatObjectMetadataMaps, flatFieldMetadataMaps: queryRunnerContext.flatFieldMetadataMaps, @@ -302,49 +292,24 @@ export abstract class CommonBaseQueryRunnerService< } } - private async getRoleIdOrThrow( - authContext: WorkspaceAuthContext, - workspaceId: string, - ): Promise { - if (isApiKeyAuthContext(authContext)) { - return this.apiKeyRoleService.getRoleIdForApiKeyId( - authContext.apiKey.id, - workspaceId, - ); - } - - if ( - isApplicationAuthContext(authContext) && - isDefined(authContext.application.defaultRoleId) - ) { - return authContext.application.defaultRoleId; - } - - if (isUserAuthContext(authContext)) { - return this.userRoleService.getRoleIdForUserWorkspace({ - userWorkspaceId: authContext.userWorkspaceId, - workspaceId, - }); - } - - throw new CommonQueryRunnerException( - 'Invalid auth context', - CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT, - { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, - ); - } - private async prepareExtendedQueryRunnerContextWithGlobalDatasource( - authContext: WorkspaceAuthContext, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise> { - const workspaceId = authContext.workspace.id; + const context = getWorkspaceContext(); - const roleId = await this.getRoleIdOrThrow(authContext, workspaceId); + const rolePermissionConfig = resolveRolePermissionConfig({ + authContext: context.authContext, + userWorkspaceRoleMap: context.userWorkspaceRoleMap, + apiKeyRoleMap: context.apiKeyRoleMap, + }); - const rolePermissionConfig: RolePermissionConfig = { - intersectionOf: [roleId], - }; + if (!rolePermissionConfig) { + throw new CommonQueryRunnerException( + 'Invalid auth context', + CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT, + { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, + ); + } const globalWorkspaceDataSource = this.isReadOnly ? await this.globalWorkspaceOrmManager.getGlobalWorkspaceDataSourceReplica() @@ -357,7 +322,7 @@ export abstract class CommonBaseQueryRunnerService< return { ...queryRunnerContext, - authContext, + authContext: context.authContext, workspaceDataSource: globalWorkspaceDataSource, rolePermissionConfig, repository, diff --git a/packages/twenty-server/src/engine/core-modules/search/search.module.ts b/packages/twenty-server/src/engine/core-modules/search/search.module.ts index ff7eabd8a3..2c8aa75407 100644 --- a/packages/twenty-server/src/engine/core-modules/search/search.module.ts +++ b/packages/twenty-server/src/engine/core-modules/search/search.module.ts @@ -4,16 +4,9 @@ import { FileModule } from 'src/engine/core-modules/file/file.module'; import { SearchResolver } from 'src/engine/core-modules/search/search.resolver'; import { SearchService } from 'src/engine/core-modules/search/services/search.service'; import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; -import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; -import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; @Module({ - imports: [ - FileModule, - WorkspaceManyOrAllFlatEntityMapsCacheModule, - UserRoleModule, - PermissionsModule, - ], + imports: [FileModule, WorkspaceManyOrAllFlatEntityMapsCacheModule], providers: [SearchResolver, SearchService], }) export class SearchModule {} diff --git a/packages/twenty-server/src/engine/core-modules/search/search.resolver.ts b/packages/twenty-server/src/engine/core-modules/search/search.resolver.ts index 72a544aa43..374b7c2f38 100644 --- a/packages/twenty-server/src/engine/core-modules/search/search.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/search/search.resolver.ts @@ -4,8 +4,6 @@ import { Args, Query } from '@nestjs/graphql'; import { isDefined } from 'twenty-shared/utils'; import { CoreResolver } from 'src/engine/api/graphql/graphql-config/decorators/core-resolver.decorator'; -import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity'; -import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service'; import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter'; import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe'; import { SearchArgs } from 'src/engine/core-modules/search/dtos/search-args'; @@ -13,14 +11,10 @@ import { SearchResultConnectionDTO } from 'src/engine/core-modules/search/dtos/s import { SearchApiExceptionFilter } from 'src/engine/core-modules/search/filters/search-api-exception.filter'; import { SearchService } from 'src/engine/core-modules/search/services/search.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; -import { AuthApiKey } from 'src/engine/decorators/auth/auth-api-key.decorator'; -import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { CustomPermissionGuard } from 'src/engine/guards/custom-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; -import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; -import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; @CoreResolver() @UseFilters(SearchApiExceptionFilter, PreventNestToAutoLogGraphqlErrorsFilter) @@ -30,16 +24,11 @@ export class SearchResolver { constructor( private readonly searchService: SearchService, private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, - private readonly userRoleService: UserRoleService, - private readonly apiKeyRoleService: ApiKeyRoleService, ) {} @Query(() => SearchResultConnectionDTO) async search( @AuthWorkspace() workspace: WorkspaceEntity, - @AuthUserWorkspaceId({ allowUndefined: true }) - userWorkspaceId: string | undefined, - @AuthApiKey() apiKey: ApiKeyEntity | undefined, @Args() { searchInput, @@ -69,29 +58,6 @@ export class SearchResolver { excludedObjectNameSingulars: excludedObjectNameSingulars ?? [], }); - // TODO: move to a service - let rolePermissionConfig: RolePermissionConfig | undefined; - - if (isDefined(apiKey)) { - const roleId = await this.apiKeyRoleService.getRoleIdForApiKeyId( - apiKey.id, - workspace.id, - ); - - if (isDefined(roleId)) { - rolePermissionConfig = { unionOf: [roleId] }; - } - } else if (isDefined(userWorkspaceId)) { - const roleId = await this.userRoleService.getRoleIdForUserWorkspace({ - userWorkspaceId, - workspaceId: workspace.id, - }); - - if (isDefined(roleId)) { - rolePermissionConfig = { unionOf: [roleId] }; - } - } - const allRecordsWithObjectMetadataItems = await this.searchService.getAllRecordsWithObjectMetadataItems({ flatObjectMetadatas: filteredObjectMetadataItems, @@ -103,7 +69,6 @@ export class SearchResolver { excludedObjectNameSingulars, after, workspaceId: workspace.id, - rolePermissionConfig, }); return this.searchService.computeSearchObjectResults({ diff --git a/packages/twenty-server/src/engine/core-modules/search/services/search.service.ts b/packages/twenty-server/src/engine/core-modules/search/services/search.service.ts index 366c488794..c561309e83 100644 --- a/packages/twenty-server/src/engine/core-modules/search/services/search.service.ts +++ b/packages/twenty-server/src/engine/core-modules/search/services/search.service.ts @@ -35,7 +35,8 @@ import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants'; import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; -import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; +import { getWorkspaceContext } from 'src/engine/twenty-orm/storage/orm-workspace-context.storage'; +import { resolveRolePermissionConfig } from 'src/engine/twenty-orm/utils/resolve-role-permission-config.util'; type LastRanks = { tsRankCD: number; tsRank: number }; @@ -64,12 +65,10 @@ export class SearchService { filter, after, workspaceId, - rolePermissionConfig, }: { flatObjectMetadatas: FlatObjectMetadata[]; flatFieldMetadataMaps: FlatEntityMaps; workspaceId: string; - rolePermissionConfig?: RolePermissionConfig; } & SearchArgs) { const filteredObjectMetadataItems = this.filterObjectMetadataItems({ flatObjectMetadatas, @@ -90,6 +89,14 @@ export class SearchService { objectMetadataItemChunk.map(async (flatObjectMetadata) => { return this.globalWorkspaceOrmManager.executeInWorkspaceContext( async () => { + const context = getWorkspaceContext(); + const rolePermissionConfig = + resolveRolePermissionConfig({ + authContext: context.authContext, + userWorkspaceRoleMap: context.userWorkspaceRoleMap, + apiKeyRoleMap: context.apiKeyRoleMap, + }) ?? undefined; + const repository = await this.globalWorkspaceOrmManager.getRepository( workspaceId, diff --git a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/services/navigation-menu-item-record-identifier.service.ts b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/services/navigation-menu-item-record-identifier.service.ts index 3d873ff503..b4470d26e9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/services/navigation-menu-item-record-identifier.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/services/navigation-menu-item-record-identifier.service.ts @@ -10,9 +10,10 @@ import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadat import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; import { RecordIdentifierDTO } from 'src/engine/metadata-modules/navigation-menu-item/dtos/record-identifier.dto'; import { getMinimalSelectForRecordIdentifier } from 'src/engine/metadata-modules/navigation-menu-item/utils/get-minimal-select-for-record-identifier.util'; -import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { getWorkspaceContext } from 'src/engine/twenty-orm/storage/orm-workspace-context.storage'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; +import { resolveRolePermissionConfig } from 'src/engine/twenty-orm/utils/resolve-role-permission-config.util'; @Injectable() export class NavigationMenuItemRecordIdentifierService { @@ -20,7 +21,6 @@ export class NavigationMenuItemRecordIdentifierService { private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly fileService: FileService, - private readonly permissionsService: PermissionsService, ) {} async resolveRecordIdentifier({ @@ -63,18 +63,24 @@ export class NavigationMenuItemRecordIdentifierService { workspace: { id: workspaceId }, } as WorkspaceAuthContext); - const rolePermissionConfig = - await this.permissionsService.resolveRolePermissionConfigFromAuthContext( - resolvedAuthContext, - ); - const record = await this.globalWorkspaceOrmManager.executeInWorkspaceContext( async () => { + const context = getWorkspaceContext(); + const rolePermissionConfig = resolveRolePermissionConfig({ + authContext: context.authContext, + userWorkspaceRoleMap: context.userWorkspaceRoleMap, + apiKeyRoleMap: context.apiKeyRoleMap, + }); + + if (!rolePermissionConfig) { + return null; + } + const repository = await this.globalWorkspaceOrmManager.getRepository( workspaceId, objectMetadata.nameSingular, - rolePermissionConfig ?? { shouldBypassPermissionChecks: true }, + rolePermissionConfig, ); const alias = objectMetadata.nameSingular; 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 adeabc5660..8a3166319f 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 @@ -13,10 +13,6 @@ import { ApplicationExceptionCode, } from 'src/engine/core-modules/application/application.exception'; import { isApiKeyAuthContext } from 'src/engine/core-modules/auth/guards/is-api-key-auth-context.guard'; -import { isApplicationAuthContext } from 'src/engine/core-modules/auth/guards/is-application-auth-context.guard'; -import { isSystemAuthContext } from 'src/engine/core-modules/auth/guards/is-system-auth-context.guard'; -import { isUserAuthContext } from 'src/engine/core-modules/auth/guards/is-user-auth-context.guard'; -import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type'; import { TOOL_PERMISSION_FLAGS } from 'src/engine/metadata-modules/permissions/constants/tool-permission-flags'; import { PermissionsException, @@ -134,48 +130,6 @@ export class PermissionsService { objectsPermissions: {}, }) as const satisfies UserWorkspacePermissions; - // TODO: this could likely be handled in the ORM layer - public async resolveRolePermissionConfigFromAuthContext( - authContext: WorkspaceAuthContext, - ): Promise { - const workspaceId = authContext.workspace.id; - - if (isSystemAuthContext(authContext)) { - return { shouldBypassPermissionChecks: true }; - } - - if (isApiKeyAuthContext(authContext)) { - const roleId = await this.apiKeyRoleService.getRoleIdForApiKeyId( - authContext.apiKey.id, - workspaceId, - ); - - return { intersectionOf: [roleId] }; - } - - if ( - isApplicationAuthContext(authContext) && - isDefined(authContext.application.defaultRoleId) - ) { - return { intersectionOf: [authContext.application.defaultRoleId] }; - } - - if (isUserAuthContext(authContext)) { - const roleId = await this.userRoleService.getRoleIdForUserWorkspace({ - userWorkspaceId: authContext.userWorkspaceId, - workspaceId, - }); - - if (!isDefined(roleId)) { - return null; - } - - return { intersectionOf: [roleId] }; - } - - return null; - } - public async userHasWorkspaceSettingPermission({ userWorkspaceId, workspaceId, 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 c114f66c56..9127407d22 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 @@ -321,6 +321,7 @@ describe('WorkspaceEntityManager', () => { userWorkspaceRoleMap: { 'user-workspace-id': 'role-id', }, + apiKeyRoleMap: {}, }; setWorkspaceContext(mockWorkspaceContext); diff --git a/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager.ts b/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager.ts index d9ff786192..42edc02786 100644 --- a/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager.ts +++ b/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager.ts @@ -93,6 +93,7 @@ export class GlobalWorkspaceOrmManager { rolesPermissions: permissionsPerRoleId, ORMEntityMetadatas: entityMetadatas, userWorkspaceRoleMap, + apiKeyRoleMap, flatRowLevelPermissionPredicateMaps, flatRowLevelPermissionPredicateGroupMaps, } = await this.workspaceCacheService.getOrRecompute(workspaceId, [ @@ -103,6 +104,7 @@ export class GlobalWorkspaceOrmManager { 'rolesPermissions', 'ORMEntityMetadatas', 'userWorkspaceRoleMap', + 'apiKeyRoleMap', 'flatRowLevelPermissionPredicateMaps', 'flatRowLevelPermissionPredicateGroupMaps', ]); @@ -122,6 +124,7 @@ export class GlobalWorkspaceOrmManager { permissionsPerRoleId, entityMetadatas, userWorkspaceRoleMap, + apiKeyRoleMap, }; } @@ -167,6 +170,7 @@ export class GlobalWorkspaceOrmManager { permissionsPerRoleId: {}, entityMetadatas, userWorkspaceRoleMap: {}, + apiKeyRoleMap: {}, }; } } diff --git a/packages/twenty-server/src/engine/twenty-orm/storage/orm-workspace-context.storage.ts b/packages/twenty-server/src/engine/twenty-orm/storage/orm-workspace-context.storage.ts index b2fff4461e..f133700731 100644 --- a/packages/twenty-server/src/engine/twenty-orm/storage/orm-workspace-context.storage.ts +++ b/packages/twenty-server/src/engine/twenty-orm/storage/orm-workspace-context.storage.ts @@ -25,6 +25,7 @@ export type ORMWorkspaceContext = { permissionsPerRoleId: ObjectsPermissionsByRoleId; entityMetadatas: EntityMetadata[]; userWorkspaceRoleMap: UserWorkspaceRoleMap; + apiKeyRoleMap: Record; flatRowLevelPermissionPredicateMaps: FlatRowLevelPermissionPredicateMaps; flatRowLevelPermissionPredicateGroupMaps: FlatRowLevelPermissionPredicateGroupMaps; }; diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/resolve-role-permission-config.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/resolve-role-permission-config.util.ts new file mode 100644 index 0000000000..ee07411aa8 --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/utils/resolve-role-permission-config.util.ts @@ -0,0 +1,52 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { isApiKeyAuthContext } from 'src/engine/core-modules/auth/guards/is-api-key-auth-context.guard'; +import { isApplicationAuthContext } from 'src/engine/core-modules/auth/guards/is-application-auth-context.guard'; +import { isSystemAuthContext } from 'src/engine/core-modules/auth/guards/is-system-auth-context.guard'; +import { isUserAuthContext } from 'src/engine/core-modules/auth/guards/is-user-auth-context.guard'; +import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type'; +import { type UserWorkspaceRoleMap } from 'src/engine/metadata-modules/role-target/types/user-workspace-role-map'; +import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; + +export const resolveRolePermissionConfig = ({ + authContext, + userWorkspaceRoleMap, + apiKeyRoleMap, +}: { + authContext: WorkspaceAuthContext; + userWorkspaceRoleMap: UserWorkspaceRoleMap; + apiKeyRoleMap: Record; +}): RolePermissionConfig | null => { + if (isSystemAuthContext(authContext)) { + return { shouldBypassPermissionChecks: true }; + } + + if (isApiKeyAuthContext(authContext)) { + const roleId = apiKeyRoleMap[authContext.apiKey.id]; + + if (!isDefined(roleId)) { + return null; + } + + return { intersectionOf: [roleId] }; + } + + if ( + isApplicationAuthContext(authContext) && + isDefined(authContext.application.defaultRoleId) + ) { + return { intersectionOf: [authContext.application.defaultRoleId] }; + } + + if (isUserAuthContext(authContext)) { + const roleId = userWorkspaceRoleMap[authContext.userWorkspaceId]; + + if (!isDefined(roleId)) { + return null; + } + + return { intersectionOf: [roleId] }; + } + + return null; +};