Feat: role applicability controls (#14239)

Closes [#1404](https://github.com/twentyhq/core-team-issues/issues/1404)

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
Abdul Rahman
2025-09-04 08:58:50 +05:30
committed by GitHub
parent 052f91cff1
commit 79bcd90d8d
72 changed files with 1889 additions and 344 deletions
@@ -55,6 +55,9 @@ describe('ApiKeyRoleService', () => {
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: true,
canBeAssignedToAgents: false,
canBeAssignedToUsers: true,
canBeAssignedToApiKeys: true,
};
const mockNewRole: Partial<RoleEntity> = {
@@ -427,6 +430,10 @@ describe('ApiKeyRoleService', () => {
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: true,
canBeAssignedToAgents: false,
canBeAssignedToUsers: true,
canBeAssignedToApiKeys: true,
standardId: undefined,
});
});
@@ -1,7 +1,14 @@
import { Injectable } from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { DataSource, type EntityManager, In, Repository } from 'typeorm';
import {
DataSource,
type EntityManager,
In,
IsNull,
Not,
Repository,
} from 'typeorm';
import { ApiKey } from 'src/engine/core-modules/api-key/api-key.entity';
import {
@@ -145,6 +152,13 @@ export class ApiKeyRoleService {
);
}
if (!role.canBeAssignedToApiKeys) {
throw new ApiKeyException(
`Role "${role.label}" cannot be assigned to API keys`,
ApiKeyExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_API_KEYS,
);
}
const existingRoleTarget = await this.roleTargetsRepository.findOne({
where: {
apiKeyId,
@@ -190,4 +204,35 @@ export class ApiKeyRoleService {
return rolesMap;
}
public async getApiKeysAssignedToRole(
roleId: string,
workspaceId: string,
): Promise<ApiKey[]> {
const roleTargets = await this.roleTargetsRepository.find({
where: {
roleId,
workspaceId,
apiKeyId: Not(IsNull()),
},
});
const apiKeyIds = roleTargets
.map((roleTarget) => roleTarget.apiKeyId)
.filter((apiKeyId): apiKeyId is string => apiKeyId !== null);
if (!apiKeyIds.length) {
return [];
}
const apiKeys = await this.apiKeyRepository.find({
where: {
id: In(apiKeyIds),
workspaceId,
revokedAt: IsNull(),
},
});
return apiKeys;
}
}
@@ -7,4 +7,5 @@ export enum ApiKeyExceptionCode {
API_KEY_REVOKED = 'API_KEY_REVOKED',
API_KEY_EXPIRED = 'API_KEY_EXPIRED',
API_KEY_NO_ROLE_ASSIGNED = 'API_KEY_NO_ROLE_ASSIGNED',
ROLE_CANNOT_BE_ASSIGNED_TO_API_KEYS = 'ROLE_CANNOT_BE_ASSIGNED_TO_API_KEYS',
}
@@ -27,6 +27,10 @@ export const apiKeyGraphqlApiExceptionHandler = (error: Error) => {
throw new ForbiddenError(error.message, {
userFriendlyMessage: error.userFriendlyMessage,
});
case ApiKeyExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_API_KEYS:
throw new UserInputError(error.message, {
userFriendlyMessage: error.userFriendlyMessage,
});
default: {
return assertUnreachable(error.code);
}