Fix: allow API key creation without Roles permission (#23102)

## Problem

A user with the **API keys & webhooks** permission but **without** the
**Roles** setting permission cannot create an API key through the UI.
The role selector relies on the `getRoles` query, which is guarded by
the `ROLES` permission, so the roles list comes back empty,
`SettingsDevelopersRoleSelector` early-returns, and no role can be
selected — leaving the form unsavable.

<img width="1058" height="408" alt="Screenshot 2026-07-21 at 13 38 34"
src="https://github.com/user-attachments/assets/fe97ba78-e116-458d-af10-11c5969c4636"
/>

## Fix

Expose the assignable roles through the API-key permission scope so
users can **pick** a role to assign to an API key without being able to
**edit** roles.

- **Backend**: add `getApiKeyRoles` query on `ApiKeyResolver` (already
guarded by `API_KEYS_AND_WEBHOOKS`), backed by
`ApiKeyRoleService.getApiKeyAssignableRoles` which returns roles where
`canBeAssignedToApiKeys = true`.
- **Frontend**: add a `GetApiKeyRoles` query and use it in the API key
create and detail pages instead of `getRoles`. The role selector prop
type is narrowed to the fields it actually uses.

<img width="1025" height="455" alt="Screenshot 2026-07-21 at 13 45 01"
src="https://github.com/user-attachments/assets/f1be8f97-5a30-4afc-9eee-c928f4607471"
/>
This commit is contained in:
Marie
2026-07-21 15:31:05 +02:00
committed by GitHub
parent e5fc5054cc
commit bc3112a999
10 changed files with 66 additions and 11 deletions
@@ -43,6 +43,13 @@ export class ApiKeyResolver {
return this.apiKeyService.findActiveByWorkspaceId(workspace.id);
}
@Query(() => [RoleDTO])
async getApiKeyRoles(
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<RoleDTO[]> {
return this.apiKeyRoleService.getApiKeyAssignableRoles(workspace.id);
}
@Query(() => ApiKeyEntity, { nullable: true })
async apiKey(
@Args('input') input: GetApiKeyInput,
@@ -14,7 +14,10 @@ import { RoleTargetService } from 'src/engine/metadata-modules/role-target/servi
import { type RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { fromFlatRoleToRoleDto } from 'src/engine/metadata-modules/role/utils/fromFlatRoleToRoleDto.util';
import { fromRoleEntityToRoleDto } from 'src/engine/metadata-modules/role/utils/fromRoleEntityToRoleDto.util';
import {
fromRoleEntitiesToRoleDtos,
fromRoleEntityToRoleDto,
} from 'src/engine/metadata-modules/role/utils/fromRoleEntityToRoleDto.util';
import { InjectWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/inject-workspace-scoped-repository.decorator';
import { WorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/workspace-scoped-repository';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
@@ -173,6 +176,17 @@ export class ApiKeyRoleService {
};
}
public async getApiKeyAssignableRoles(
workspaceId: string,
): Promise<RoleDTO[]> {
const roles = await this.roleRepository.find(workspaceId, {
where: { canBeAssignedToApiKeys: true },
order: { label: 'ASC' },
});
return fromRoleEntitiesToRoleDtos(roles);
}
public async getRolesByApiKeys({
apiKeyIds,
workspaceId,