Unify auth context → role permission config resolution into a single pure utility (#18927)

## 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)
This commit is contained in:
Félix Malfait
2026-03-24 21:37:58 +01:00
committed by GitHub
parent 37640521d5
commit d5b41b2801
10 changed files with 100 additions and 152 deletions
@@ -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<Args>,
authContext: WorkspaceAuthContext,
queryRunnerContext: CommonBaseQueryRunnerContext,
commonQueryParser: GraphqlQueryParser,
): Promise<Output> {
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<string> {
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<Omit<CommonExtendedQueryRunnerContext, 'commonQueryParser'>> {
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,