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
@@ -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;
@@ -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<RolePermissionConfig | null> {
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,