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
@@ -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 {}
@@ -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({
@@ -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<FlatFieldMetadata>;
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<ObjectRecord>(
workspaceId,