Enable roles on api keys (#13334)
This commit is contained in:
@@ -0,0 +1,546 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getDataSourceToken, getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { In } from 'typeorm';
|
||||
|
||||
import { ApiKey } from 'src/engine/core-modules/api-key/api-key.entity';
|
||||
import {
|
||||
ApiKeyException,
|
||||
ApiKeyExceptionCode,
|
||||
} from 'src/engine/core-modules/api-key/api-key.exception';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
|
||||
import { ApiKeyRoleService } from './api-key-role.service';
|
||||
|
||||
describe('ApiKeyRoleService', () => {
|
||||
let service: ApiKeyRoleService;
|
||||
let mockRoleTargetsRepository: any;
|
||||
let mockRoleRepository: any;
|
||||
let mockWorkspaceRepository: any;
|
||||
let mockApiKeyRepository: any;
|
||||
let mockDataSource: any;
|
||||
let mockWorkspacePermissionsCacheService: any;
|
||||
|
||||
const mockWorkspaceId = 'workspace-123';
|
||||
const mockApiKeyId = 'api-key-456';
|
||||
const mockRoleId = 'role-789';
|
||||
const mockNewRoleId = 'role-999';
|
||||
|
||||
const mockApiKey: ApiKey = {
|
||||
id: mockApiKeyId,
|
||||
name: 'Test API Key',
|
||||
expiresAt: new Date('2025-12-31'),
|
||||
revokedAt: undefined,
|
||||
workspaceId: mockWorkspaceId,
|
||||
createdAt: new Date('2024-01-01'),
|
||||
updatedAt: new Date('2024-01-01'),
|
||||
workspace: {} as any,
|
||||
};
|
||||
|
||||
const mockRole: Partial<RoleEntity> = {
|
||||
id: mockRoleId,
|
||||
label: 'Admin',
|
||||
icon: 'admin-icon',
|
||||
description: 'Admin role',
|
||||
isEditable: true,
|
||||
workspaceId: mockWorkspaceId,
|
||||
createdAt: new Date('2024-01-01'),
|
||||
updatedAt: new Date('2024-01-01'),
|
||||
canUpdateAllSettings: true,
|
||||
canAccessAllTools: true,
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: true,
|
||||
canSoftDeleteAllObjectRecords: true,
|
||||
canDestroyAllObjectRecords: true,
|
||||
};
|
||||
|
||||
const mockNewRole: Partial<RoleEntity> = {
|
||||
...mockRole,
|
||||
id: mockNewRoleId,
|
||||
label: 'Member',
|
||||
};
|
||||
|
||||
const mockRoleTarget = {
|
||||
id: 'role-target-123',
|
||||
roleId: mockRoleId,
|
||||
apiKeyId: mockApiKeyId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
createdAt: new Date('2024-01-01'),
|
||||
updatedAt: new Date('2024-01-01'),
|
||||
role: mockRole,
|
||||
apiKey: mockApiKey,
|
||||
} as RoleTargetsEntity;
|
||||
|
||||
const mockNewRoleTarget = {
|
||||
...mockRoleTarget,
|
||||
id: 'role-target-456',
|
||||
roleId: mockNewRoleId,
|
||||
} as RoleTargetsEntity;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockRoleTargetsRepository = {
|
||||
save: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
};
|
||||
|
||||
mockRoleRepository = {
|
||||
findOne: jest.fn(),
|
||||
};
|
||||
|
||||
mockWorkspaceRepository = {
|
||||
findOne: jest.fn(),
|
||||
};
|
||||
|
||||
mockApiKeyRepository = {
|
||||
findOne: jest.fn(),
|
||||
};
|
||||
|
||||
mockDataSource = {
|
||||
transaction: jest.fn(),
|
||||
};
|
||||
|
||||
mockWorkspacePermissionsCacheService = {
|
||||
recomputeApiKeyRoleMapCache: jest.fn(),
|
||||
getApiKeyRoleMapFromCache: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ApiKeyRoleService,
|
||||
{
|
||||
provide: getRepositoryToken(RoleTargetsEntity, 'core'),
|
||||
useValue: mockRoleTargetsRepository,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(RoleEntity, 'core'),
|
||||
useValue: mockRoleRepository,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(Workspace, 'core'),
|
||||
useValue: mockWorkspaceRepository,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(ApiKey, 'core'),
|
||||
useValue: mockApiKeyRepository,
|
||||
},
|
||||
{
|
||||
provide: getDataSourceToken('core'),
|
||||
useValue: mockDataSource,
|
||||
},
|
||||
{
|
||||
provide: WorkspacePermissionsCacheService,
|
||||
useValue: mockWorkspacePermissionsCacheService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<ApiKeyRoleService>(ApiKeyRoleService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('assignRoleToApiKeyWithManager', () => {
|
||||
it('should assign role using provided transaction manager', async () => {
|
||||
const mockManagerDelete = jest.fn().mockResolvedValue({ affected: 1 });
|
||||
const mockManagerCreate = jest.fn().mockReturnValue(mockNewRoleTarget);
|
||||
const mockManagerSave = jest.fn().mockResolvedValue(mockNewRoleTarget);
|
||||
|
||||
const mockManager = {
|
||||
delete: mockManagerDelete,
|
||||
create: mockManagerCreate,
|
||||
save: mockManagerSave,
|
||||
};
|
||||
|
||||
await service.assignRoleToApiKeyWithManager(mockManager as any, {
|
||||
apiKeyId: mockApiKeyId,
|
||||
roleId: mockNewRoleId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
|
||||
expect(mockManagerDelete).toHaveBeenCalledWith(RoleTargetsEntity, {
|
||||
apiKeyId: mockApiKeyId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
expect(mockManagerCreate).toHaveBeenCalledWith(RoleTargetsEntity, {
|
||||
apiKeyId: mockApiKeyId,
|
||||
roleId: mockNewRoleId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
expect(mockManagerSave).toHaveBeenCalledWith(mockNewRoleTarget);
|
||||
});
|
||||
|
||||
it('should handle manager operation failures', async () => {
|
||||
const mockManagerDelete = jest
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('Delete failed'));
|
||||
const mockManagerCreate = jest.fn();
|
||||
const mockManagerSave = jest.fn();
|
||||
|
||||
const mockManager = {
|
||||
delete: mockManagerDelete,
|
||||
create: mockManagerCreate,
|
||||
save: mockManagerSave,
|
||||
};
|
||||
|
||||
await expect(
|
||||
service.assignRoleToApiKeyWithManager(mockManager as any, {
|
||||
apiKeyId: mockApiKeyId,
|
||||
roleId: mockNewRoleId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
}),
|
||||
).rejects.toThrow('Delete failed');
|
||||
|
||||
expect(mockManagerDelete).toHaveBeenCalled();
|
||||
expect(mockManagerCreate).not.toHaveBeenCalled();
|
||||
expect(mockManagerSave).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('assignRoleToApiKey', () => {
|
||||
it('should assign a new role to API key using transaction', async () => {
|
||||
mockApiKeyRepository.findOne.mockResolvedValue(mockApiKey);
|
||||
mockRoleRepository.findOne.mockResolvedValue(mockNewRole);
|
||||
mockRoleTargetsRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
const mockManagerDelete = jest.fn().mockResolvedValue({ affected: 1 });
|
||||
const mockManagerCreate = jest.fn().mockReturnValue(mockNewRoleTarget);
|
||||
const mockManagerSave = jest.fn().mockResolvedValue(mockNewRoleTarget);
|
||||
|
||||
mockDataSource.transaction.mockImplementation(
|
||||
async (callback: (manager: any) => Promise<any>) => {
|
||||
const mockManager = {
|
||||
delete: mockManagerDelete,
|
||||
create: mockManagerCreate,
|
||||
save: mockManagerSave,
|
||||
};
|
||||
|
||||
return await callback(mockManager);
|
||||
},
|
||||
);
|
||||
|
||||
await service.assignRoleToApiKey({
|
||||
apiKeyId: mockApiKeyId,
|
||||
roleId: mockNewRoleId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
|
||||
expect(mockDataSource.transaction).toHaveBeenCalled();
|
||||
expect(mockManagerDelete).toHaveBeenCalledWith(RoleTargetsEntity, {
|
||||
apiKeyId: mockApiKeyId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
expect(mockManagerCreate).toHaveBeenCalledWith(RoleTargetsEntity, {
|
||||
apiKeyId: mockApiKeyId,
|
||||
roleId: mockNewRoleId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
expect(mockManagerSave).toHaveBeenCalledWith(mockNewRoleTarget);
|
||||
expect(
|
||||
mockWorkspacePermissionsCacheService.recomputeApiKeyRoleMapCache,
|
||||
).toHaveBeenCalledWith({
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip assignment if role is already assigned', async () => {
|
||||
mockApiKeyRepository.findOne.mockResolvedValue(mockApiKey);
|
||||
mockRoleRepository.findOne.mockResolvedValue(mockRole);
|
||||
mockRoleTargetsRepository.findOne.mockResolvedValue(mockRoleTarget);
|
||||
|
||||
await service.assignRoleToApiKey({
|
||||
apiKeyId: mockApiKeyId,
|
||||
roleId: mockRoleId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
|
||||
expect(mockDataSource.transaction).not.toHaveBeenCalled();
|
||||
expect(
|
||||
mockWorkspacePermissionsCacheService.recomputeApiKeyRoleMapCache,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw exception if API key not found', async () => {
|
||||
mockApiKeyRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.assignRoleToApiKey({
|
||||
apiKeyId: 'non-existent',
|
||||
roleId: mockRoleId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
}),
|
||||
).rejects.toThrow(ApiKeyException);
|
||||
|
||||
await expect(
|
||||
service.assignRoleToApiKey({
|
||||
apiKeyId: 'non-existent',
|
||||
roleId: mockRoleId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
code: ApiKeyExceptionCode.API_KEY_NOT_FOUND,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw exception if role not found', async () => {
|
||||
mockApiKeyRepository.findOne.mockResolvedValue(mockApiKey);
|
||||
mockRoleRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.assignRoleToApiKey({
|
||||
apiKeyId: mockApiKeyId,
|
||||
roleId: 'non-existent-role',
|
||||
workspaceId: mockWorkspaceId,
|
||||
}),
|
||||
).rejects.toThrow(ApiKeyException);
|
||||
|
||||
await expect(
|
||||
service.assignRoleToApiKey({
|
||||
apiKeyId: mockApiKeyId,
|
||||
roleId: 'non-existent-role',
|
||||
workspaceId: mockWorkspaceId,
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
code: ApiKeyExceptionCode.API_KEY_NOT_FOUND,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRoleIdForApiKey', () => {
|
||||
it('should return role ID from cache', async () => {
|
||||
const mockCacheData = {
|
||||
data: {
|
||||
[mockApiKeyId]: mockRoleId,
|
||||
},
|
||||
};
|
||||
|
||||
mockWorkspacePermissionsCacheService.getApiKeyRoleMapFromCache.mockResolvedValue(
|
||||
mockCacheData,
|
||||
);
|
||||
|
||||
const result = await service.getRoleIdForApiKey(
|
||||
mockApiKeyId,
|
||||
mockWorkspaceId,
|
||||
);
|
||||
|
||||
expect(
|
||||
mockWorkspacePermissionsCacheService.getApiKeyRoleMapFromCache,
|
||||
).toHaveBeenCalledWith({
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
expect(result).toBe(mockRoleId);
|
||||
});
|
||||
|
||||
it('should throw exception if API key has no role in cache', async () => {
|
||||
const mockCacheData = {
|
||||
data: {},
|
||||
};
|
||||
|
||||
mockWorkspacePermissionsCacheService.getApiKeyRoleMapFromCache.mockResolvedValue(
|
||||
mockCacheData,
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.getRoleIdForApiKey(mockApiKeyId, mockWorkspaceId),
|
||||
).rejects.toThrow(ApiKeyException);
|
||||
|
||||
await expect(
|
||||
service.getRoleIdForApiKey(mockApiKeyId, mockWorkspaceId),
|
||||
).rejects.toMatchObject({
|
||||
code: ApiKeyExceptionCode.API_KEY_NO_ROLE_ASSIGNED,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('recomputeCache', () => {
|
||||
it('should trigger cache recomputation', async () => {
|
||||
await service.recomputeCache(mockWorkspaceId);
|
||||
|
||||
expect(
|
||||
mockWorkspacePermissionsCacheService.recomputeApiKeyRoleMapCache,
|
||||
).toHaveBeenCalledWith({
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRolesByApiKeys', () => {
|
||||
it('should return empty map for empty API key IDs', async () => {
|
||||
const result = await service.getRolesByApiKeys({
|
||||
apiKeyIds: [],
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
|
||||
expect(result).toEqual(new Map());
|
||||
expect(mockRoleTargetsRepository.find).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return roles map for given API key IDs', async () => {
|
||||
const mockApiKeyIds = [mockApiKeyId, 'another-api-key'];
|
||||
const mockRoleTargets = [
|
||||
{
|
||||
apiKeyId: mockApiKeyId,
|
||||
role: mockRole,
|
||||
},
|
||||
{
|
||||
apiKeyId: 'another-api-key',
|
||||
role: mockNewRole,
|
||||
},
|
||||
];
|
||||
|
||||
mockRoleTargetsRepository.find.mockResolvedValue(mockRoleTargets);
|
||||
|
||||
const result = await service.getRolesByApiKeys({
|
||||
apiKeyIds: mockApiKeyIds,
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
|
||||
expect(mockRoleTargetsRepository.find).toHaveBeenCalledWith({
|
||||
where: {
|
||||
apiKeyId: In(mockApiKeyIds),
|
||||
workspaceId: mockWorkspaceId,
|
||||
},
|
||||
relations: ['role'],
|
||||
});
|
||||
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get(mockApiKeyId)).toEqual({
|
||||
id: mockRole.id,
|
||||
label: mockRole.label,
|
||||
icon: mockRole.icon,
|
||||
description: mockRole.description,
|
||||
isEditable: mockRole.isEditable,
|
||||
roleTargets: mockRole.roleTargets,
|
||||
canUpdateAllSettings: true,
|
||||
canAccessAllTools: true,
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: true,
|
||||
canSoftDeleteAllObjectRecords: true,
|
||||
canDestroyAllObjectRecords: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle role targets with missing apiKeyId or role gracefully', async () => {
|
||||
const mockRoleTargets = [
|
||||
{
|
||||
apiKeyId: null,
|
||||
role: mockRole,
|
||||
},
|
||||
{
|
||||
apiKeyId: mockApiKeyId,
|
||||
role: null,
|
||||
},
|
||||
{
|
||||
apiKeyId: 'valid-api-key',
|
||||
role: mockRole,
|
||||
},
|
||||
];
|
||||
|
||||
mockRoleTargetsRepository.find.mockResolvedValue(mockRoleTargets);
|
||||
|
||||
const result = await service.getRolesByApiKeys({
|
||||
apiKeyIds: [mockApiKeyId, 'valid-api-key'],
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
|
||||
expect(result.size).toBe(1);
|
||||
expect(result.has('valid-api-key')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateAssignRoleInput', () => {
|
||||
it('should validate successful role assignment inputs', async () => {
|
||||
mockApiKeyRepository.findOne.mockResolvedValue(mockApiKey);
|
||||
mockRoleRepository.findOne.mockResolvedValue(mockNewRole);
|
||||
mockRoleTargetsRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
const validateMethod = (service as any).validateAssignRoleInput;
|
||||
const result = await validateMethod.call(service, {
|
||||
apiKeyId: mockApiKeyId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
roleId: mockNewRoleId,
|
||||
});
|
||||
|
||||
expect(result.roleToAssignIsSameAsCurrentRole).toBe(false);
|
||||
});
|
||||
|
||||
it('should detect same role assignment', async () => {
|
||||
mockApiKeyRepository.findOne.mockResolvedValue(mockApiKey);
|
||||
mockRoleRepository.findOne.mockResolvedValue(mockRole);
|
||||
mockRoleTargetsRepository.findOne.mockResolvedValue(mockRoleTarget);
|
||||
|
||||
const validateMethod = (service as any).validateAssignRoleInput;
|
||||
const result = await validateMethod.call(service, {
|
||||
apiKeyId: mockApiKeyId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
roleId: mockRoleId,
|
||||
});
|
||||
|
||||
expect(result.roleToAssignIsSameAsCurrentRole).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should handle transaction failures gracefully', async () => {
|
||||
mockApiKeyRepository.findOne.mockResolvedValue(mockApiKey);
|
||||
mockRoleRepository.findOne.mockResolvedValue(mockNewRole);
|
||||
mockRoleTargetsRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
mockDataSource.transaction.mockRejectedValue(
|
||||
new Error('Transaction failed'),
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.assignRoleToApiKey({
|
||||
apiKeyId: mockApiKeyId,
|
||||
roleId: mockNewRoleId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
}),
|
||||
).rejects.toThrow('Transaction failed');
|
||||
|
||||
expect(
|
||||
mockWorkspacePermissionsCacheService.recomputeApiKeyRoleMapCache,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle cache service failures gracefully', async () => {
|
||||
mockApiKeyRepository.findOne.mockResolvedValue(mockApiKey);
|
||||
mockRoleRepository.findOne.mockResolvedValue(mockNewRole);
|
||||
mockRoleTargetsRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
mockDataSource.transaction.mockImplementation(
|
||||
async (callback: (manager: any) => Promise<any>) => {
|
||||
const mockManager = {
|
||||
delete: jest.fn().mockResolvedValue({ affected: 1 }),
|
||||
create: jest.fn().mockReturnValue(mockNewRoleTarget),
|
||||
save: jest.fn().mockResolvedValue(mockNewRoleTarget),
|
||||
};
|
||||
|
||||
return await callback(mockManager);
|
||||
},
|
||||
);
|
||||
|
||||
mockWorkspacePermissionsCacheService.recomputeApiKeyRoleMapCache.mockRejectedValue(
|
||||
new Error('Cache update failed'),
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.assignRoleToApiKey({
|
||||
apiKeyId: mockApiKeyId,
|
||||
roleId: mockNewRoleId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
}),
|
||||
).rejects.toThrow('Cache update failed');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,195 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { DataSource, EntityManager, In, Repository } from 'typeorm';
|
||||
|
||||
import { ApiKey } from 'src/engine/core-modules/api-key/api-key.entity';
|
||||
import {
|
||||
ApiKeyException,
|
||||
ApiKeyExceptionCode,
|
||||
} from 'src/engine/core-modules/api-key/api-key.exception';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
|
||||
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { fromRoleEntityToRoleDto } from 'src/engine/metadata-modules/role/utils/fromRoleEntityToRoleDto.util';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
|
||||
@Injectable()
|
||||
export class ApiKeyRoleService {
|
||||
constructor(
|
||||
@InjectRepository(RoleTargetsEntity, 'core')
|
||||
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
|
||||
@InjectRepository(RoleEntity, 'core')
|
||||
private readonly roleRepository: Repository<RoleEntity>,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
@InjectRepository(ApiKey, 'core')
|
||||
private readonly apiKeyRepository: Repository<ApiKey>,
|
||||
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
|
||||
@InjectDataSource('core')
|
||||
private readonly dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
public async assignRoleToApiKey({
|
||||
apiKeyId,
|
||||
roleId,
|
||||
workspaceId,
|
||||
}: {
|
||||
apiKeyId: string;
|
||||
roleId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<void> {
|
||||
const validationResult = await this.validateAssignRoleInput({
|
||||
apiKeyId,
|
||||
workspaceId,
|
||||
roleId,
|
||||
});
|
||||
|
||||
if (validationResult?.roleToAssignIsSameAsCurrentRole) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
await this.assignRoleToApiKeyWithManager(manager, {
|
||||
apiKeyId,
|
||||
roleId,
|
||||
workspaceId,
|
||||
});
|
||||
});
|
||||
|
||||
await this.workspacePermissionsCacheService.recomputeApiKeyRoleMapCache({
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
public async assignRoleToApiKeyWithManager(
|
||||
manager: EntityManager,
|
||||
{
|
||||
apiKeyId,
|
||||
roleId,
|
||||
workspaceId,
|
||||
}: {
|
||||
apiKeyId: string;
|
||||
roleId: string;
|
||||
workspaceId: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
await manager.delete(RoleTargetsEntity, {
|
||||
apiKeyId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const roleTarget = manager.create(RoleTargetsEntity, {
|
||||
apiKeyId,
|
||||
roleId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
await manager.save(roleTarget);
|
||||
}
|
||||
|
||||
async getRoleIdForApiKey(
|
||||
apiKeyId: string,
|
||||
workspaceId: string,
|
||||
): Promise<string> {
|
||||
const apiKeyRoleMap =
|
||||
await this.workspacePermissionsCacheService.getApiKeyRoleMapFromCache({
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const roleId = apiKeyRoleMap.data[apiKeyId];
|
||||
|
||||
if (!roleId) {
|
||||
throw new ApiKeyException(
|
||||
`API key ${apiKeyId} has no role assigned`,
|
||||
ApiKeyExceptionCode.API_KEY_NO_ROLE_ASSIGNED,
|
||||
);
|
||||
}
|
||||
|
||||
return roleId;
|
||||
}
|
||||
|
||||
async recomputeCache(workspaceId: string): Promise<void> {
|
||||
await this.workspacePermissionsCacheService.recomputeApiKeyRoleMapCache({
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
private async validateAssignRoleInput({
|
||||
apiKeyId,
|
||||
workspaceId,
|
||||
roleId,
|
||||
}: {
|
||||
apiKeyId: string;
|
||||
workspaceId: string;
|
||||
roleId: string;
|
||||
}) {
|
||||
const apiKey = await this.apiKeyRepository.findOne({
|
||||
where: { id: apiKeyId, workspaceId },
|
||||
});
|
||||
|
||||
if (!apiKey) {
|
||||
throw new ApiKeyException(
|
||||
`API Key with id ${apiKeyId} not found in workspace`,
|
||||
ApiKeyExceptionCode.API_KEY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const role = await this.roleRepository.findOne({
|
||||
where: { id: roleId, workspaceId },
|
||||
});
|
||||
|
||||
if (!role) {
|
||||
throw new ApiKeyException(
|
||||
`Role with id ${roleId} not found in workspace`,
|
||||
ApiKeyExceptionCode.API_KEY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const existingRoleTarget = await this.roleTargetsRepository.findOne({
|
||||
where: {
|
||||
apiKeyId,
|
||||
roleId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
roleToAssignIsSameAsCurrentRole: Boolean(existingRoleTarget),
|
||||
};
|
||||
}
|
||||
|
||||
public async getRolesByApiKeys({
|
||||
apiKeyIds,
|
||||
workspaceId,
|
||||
}: {
|
||||
apiKeyIds: string[];
|
||||
workspaceId: string;
|
||||
}): Promise<Map<string, RoleDTO>> {
|
||||
if (!apiKeyIds.length) {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
const roleTargets = await this.roleTargetsRepository.find({
|
||||
where: {
|
||||
apiKeyId: In(apiKeyIds),
|
||||
workspaceId,
|
||||
},
|
||||
relations: ['role'],
|
||||
});
|
||||
|
||||
const rolesMap = new Map<string, RoleDTO>();
|
||||
|
||||
for (const roleTarget of roleTargets) {
|
||||
if (roleTarget.apiKeyId && roleTarget.role) {
|
||||
rolesMap.set(
|
||||
roleTarget.apiKeyId,
|
||||
fromRoleEntityToRoleDto(roleTarget.role),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return rolesMap;
|
||||
}
|
||||
}
|
||||
@@ -6,4 +6,5 @@ export enum ApiKeyExceptionCode {
|
||||
API_KEY_NOT_FOUND = 'API_KEY_NOT_FOUND',
|
||||
API_KEY_REVOKED = 'API_KEY_REVOKED',
|
||||
API_KEY_EXPIRED = 'API_KEY_EXPIRED',
|
||||
API_KEY_NO_ROLE_ASSIGNED = 'API_KEY_NO_ROLE_ASSIGNED',
|
||||
}
|
||||
|
||||
@@ -1,24 +1,37 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/api-key-role.service';
|
||||
import { ApiKey } from 'src/engine/core-modules/api-key/api-key.entity';
|
||||
import { ApiKeyResolver } from 'src/engine/core-modules/api-key/api-key.resolver';
|
||||
import { ApiKeyService } from 'src/engine/core-modules/api-key/api-key.service';
|
||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
|
||||
import { ApiKeyController } from './controllers/api-key.controller';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([ApiKey], 'core'),
|
||||
TypeOrmModule.forFeature(
|
||||
[ApiKey, RoleTargetsEntity, RoleEntity, Workspace],
|
||||
'core',
|
||||
),
|
||||
JwtModule,
|
||||
AuthModule,
|
||||
TokenModule,
|
||||
WorkspacePermissionsCacheModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
PermissionsModule,
|
||||
FeatureFlagModule,
|
||||
],
|
||||
providers: [ApiKeyService, ApiKeyResolver],
|
||||
providers: [ApiKeyService, ApiKeyResolver, ApiKeyRoleService],
|
||||
controllers: [ApiKeyController],
|
||||
exports: [ApiKeyService, TypeOrmModule],
|
||||
exports: [ApiKeyService, ApiKeyRoleService, TypeOrmModule],
|
||||
})
|
||||
export class ApiKeyModule {}
|
||||
|
||||
@@ -1,22 +1,46 @@
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
import {
|
||||
Args,
|
||||
Mutation,
|
||||
Parent,
|
||||
Query,
|
||||
ResolveField,
|
||||
Resolver,
|
||||
} from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
ApiKeyException,
|
||||
ApiKeyExceptionCode,
|
||||
} from 'src/engine/core-modules/api-key/api-key.exception';
|
||||
import { CreateApiKeyDTO } from 'src/engine/core-modules/api-key/dtos/create-api-key.dto';
|
||||
import { GetApiKeyDTO } from 'src/engine/core-modules/api-key/dtos/get-api-key.dto';
|
||||
import { RevokeApiKeyDTO } from 'src/engine/core-modules/api-key/dtos/revoke-api-key.dto';
|
||||
import { UpdateApiKeyDTO } from 'src/engine/core-modules/api-key/dtos/update-api-key.dto';
|
||||
import { apiKeyGraphqlApiExceptionHandler } from 'src/engine/core-modules/api-key/utils/api-key-graphql-api-exception-handler.util';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
|
||||
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
|
||||
|
||||
import { ApiKeyRoleService } from './api-key-role.service';
|
||||
import { ApiKey } from './api-key.entity';
|
||||
import { ApiKeyService } from './api-key.service';
|
||||
|
||||
@Resolver(() => ApiKey)
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@UseGuards(
|
||||
WorkspaceAuthGuard,
|
||||
SettingsPermissionsGuard(PermissionFlagType.API_KEYS_AND_WEBHOOKS),
|
||||
)
|
||||
export class ApiKeyResolver {
|
||||
constructor(private readonly apiKeyService: ApiKeyService) {}
|
||||
constructor(
|
||||
private readonly apiKeyService: ApiKeyService,
|
||||
private readonly apiKeyRoleService: ApiKeyRoleService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {}
|
||||
|
||||
@Query(() => [ApiKey])
|
||||
async apiKeys(@AuthWorkspace() workspace: Workspace): Promise<ApiKey[]> {
|
||||
@@ -52,6 +76,7 @@ export class ApiKeyResolver {
|
||||
expiresAt: new Date(input.expiresAt),
|
||||
revokedAt: input.revokedAt ? new Date(input.revokedAt) : undefined,
|
||||
workspaceId: workspace.id,
|
||||
roleId: input.roleId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -79,4 +104,55 @@ export class ApiKeyResolver {
|
||||
): Promise<ApiKey | null> {
|
||||
return this.apiKeyService.revoke(input.id, workspace.id);
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
async assignRoleToApiKey(
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
@Args('apiKeyId') apiKeyId: string,
|
||||
@Args('roleId') roleId: string,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
await this.apiKeyRoleService.assignRoleToApiKey({
|
||||
apiKeyId,
|
||||
roleId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
apiKeyGraphqlApiExceptionHandler(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@ResolveField(() => RoleDTO, { nullable: true })
|
||||
async role(
|
||||
@Parent() apiKey: ApiKey,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
): Promise<RoleDTO | null> {
|
||||
const isApiKeyRolesEnabled = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_API_KEY_ROLES_ENABLED,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
if (!isApiKeyRolesEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rolesMap = await this.apiKeyRoleService.getRolesByApiKeys({
|
||||
apiKeyIds: [apiKey.id],
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
const role = rolesMap.get(apiKey.id);
|
||||
|
||||
if (!role) {
|
||||
throw new ApiKeyException(
|
||||
`API key ${apiKey.id} has no role assigned`,
|
||||
ApiKeyExceptionCode.API_KEY_NO_ROLE_ASSIGNED,
|
||||
);
|
||||
}
|
||||
|
||||
return role;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { getDataSourceToken, getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { IsNull } from 'typeorm';
|
||||
|
||||
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/api-key-role.service';
|
||||
import {
|
||||
ApiKeyException,
|
||||
ApiKeyExceptionCode,
|
||||
} from 'src/engine/core-modules/api-key/api-key.exception';
|
||||
import { JwtTokenTypeEnum } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
||||
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||
|
||||
import { ApiKey } from './api-key.entity';
|
||||
import { ApiKeyService } from './api-key.service';
|
||||
@@ -16,7 +18,10 @@ import { ApiKeyService } from './api-key.service';
|
||||
describe('ApiKeyService', () => {
|
||||
let service: ApiKeyService;
|
||||
let mockApiKeyRepository: any;
|
||||
let mockRoleTargetsRepository: any;
|
||||
let mockJwtWrapperService: any;
|
||||
let mockApiKeyRoleService: any;
|
||||
let mockDataSource: any;
|
||||
|
||||
const mockWorkspaceId = 'workspace-123';
|
||||
const mockApiKeyId = 'api-key-456';
|
||||
@@ -53,11 +58,27 @@ describe('ApiKeyService', () => {
|
||||
update: jest.fn(),
|
||||
};
|
||||
|
||||
mockRoleTargetsRepository = {
|
||||
delete: jest.fn(),
|
||||
save: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
};
|
||||
|
||||
mockJwtWrapperService = {
|
||||
generateAppSecret: jest.fn(),
|
||||
sign: jest.fn(),
|
||||
};
|
||||
|
||||
mockApiKeyRoleService = {
|
||||
recomputeCache: jest.fn(),
|
||||
assignRoleToApiKey: jest.fn(),
|
||||
assignRoleToApiKeyWithManager: jest.fn(),
|
||||
};
|
||||
|
||||
mockDataSource = {
|
||||
transaction: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ApiKeyService,
|
||||
@@ -69,6 +90,18 @@ describe('ApiKeyService', () => {
|
||||
provide: JwtWrapperService,
|
||||
useValue: mockJwtWrapperService,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(RoleTargetsEntity, 'core'),
|
||||
useValue: mockRoleTargetsRepository,
|
||||
},
|
||||
{
|
||||
provide: ApiKeyRoleService,
|
||||
useValue: mockApiKeyRoleService,
|
||||
},
|
||||
{
|
||||
provide: getDataSourceToken('core'),
|
||||
useValue: mockDataSource,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@@ -84,22 +117,124 @@ describe('ApiKeyService', () => {
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create and save an API key', async () => {
|
||||
it('should create and save an API key using transaction', async () => {
|
||||
const apiKeyData = {
|
||||
name: 'New API Key',
|
||||
expiresAt: new Date('2025-12-31'),
|
||||
workspaceId: mockWorkspaceId,
|
||||
roleId: 'mock-role-id',
|
||||
};
|
||||
|
||||
const expectedApiKeyFields = {
|
||||
name: 'New API Key',
|
||||
expiresAt: new Date('2025-12-31'),
|
||||
workspaceId: mockWorkspaceId,
|
||||
};
|
||||
|
||||
mockApiKeyRepository.create.mockReturnValue(mockApiKey);
|
||||
mockApiKeyRepository.save.mockResolvedValue(mockApiKey);
|
||||
mockApiKeyRoleService.assignRoleToApiKeyWithManager.mockResolvedValue(
|
||||
undefined,
|
||||
);
|
||||
mockApiKeyRoleService.recomputeCache.mockResolvedValue(undefined);
|
||||
|
||||
const mockManagerCreate = jest.fn().mockReturnValue(mockApiKey);
|
||||
const mockManagerSave = jest.fn().mockResolvedValue(mockApiKey);
|
||||
|
||||
mockDataSource.transaction.mockImplementation(
|
||||
async (callback: (manager: any) => Promise<any>) => {
|
||||
const mockManager = {
|
||||
create: mockManagerCreate,
|
||||
save: mockManagerSave,
|
||||
};
|
||||
|
||||
return await callback(mockManager);
|
||||
},
|
||||
);
|
||||
|
||||
const result = await service.create(apiKeyData);
|
||||
|
||||
expect(mockApiKeyRepository.create).toHaveBeenCalledWith(apiKeyData);
|
||||
expect(mockApiKeyRepository.save).toHaveBeenCalledWith(mockApiKey);
|
||||
expect(mockDataSource.transaction).toHaveBeenCalled();
|
||||
expect(mockManagerCreate).toHaveBeenCalledWith(
|
||||
ApiKey,
|
||||
expectedApiKeyFields,
|
||||
);
|
||||
expect(mockManagerSave).toHaveBeenCalledWith(mockApiKey);
|
||||
expect(
|
||||
mockApiKeyRoleService.assignRoleToApiKeyWithManager,
|
||||
).toHaveBeenCalledWith(
|
||||
expect.any(Object), // manager
|
||||
{
|
||||
apiKeyId: mockApiKey.id,
|
||||
roleId: 'mock-role-id',
|
||||
workspaceId: mockWorkspaceId,
|
||||
},
|
||||
);
|
||||
expect(mockApiKeyRoleService.recomputeCache).toHaveBeenCalledWith(
|
||||
mockWorkspaceId,
|
||||
);
|
||||
expect(result).toEqual(mockApiKey);
|
||||
});
|
||||
|
||||
it('should handle role assignment failures within transaction', async () => {
|
||||
const apiKeyData = {
|
||||
name: 'New API Key',
|
||||
expiresAt: new Date('2025-12-31'),
|
||||
workspaceId: mockWorkspaceId,
|
||||
roleId: 'mock-role-id',
|
||||
};
|
||||
|
||||
const mockManagerCreate = jest.fn().mockReturnValue(mockApiKey);
|
||||
const mockManagerSave = jest.fn().mockResolvedValue(mockApiKey);
|
||||
|
||||
mockApiKeyRoleService.assignRoleToApiKeyWithManager.mockRejectedValue(
|
||||
new Error('Role assignment failed'),
|
||||
);
|
||||
|
||||
mockDataSource.transaction.mockImplementation(
|
||||
async (callback: (manager: any) => Promise<any>) => {
|
||||
const mockManager = {
|
||||
create: mockManagerCreate,
|
||||
save: mockManagerSave,
|
||||
};
|
||||
|
||||
return await callback(mockManager);
|
||||
},
|
||||
);
|
||||
|
||||
await expect(service.create(apiKeyData)).rejects.toThrow(
|
||||
'Role assignment failed',
|
||||
);
|
||||
|
||||
expect(mockDataSource.transaction).toHaveBeenCalled();
|
||||
expect(mockManagerCreate).toHaveBeenCalled();
|
||||
expect(mockManagerSave).toHaveBeenCalled();
|
||||
expect(
|
||||
mockApiKeyRoleService.assignRoleToApiKeyWithManager,
|
||||
).toHaveBeenCalled();
|
||||
expect(mockApiKeyRoleService.recomputeCache).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle transaction failures gracefully', async () => {
|
||||
const apiKeyData = {
|
||||
name: 'New API Key',
|
||||
expiresAt: new Date('2025-12-31'),
|
||||
workspaceId: mockWorkspaceId,
|
||||
roleId: 'mock-role-id',
|
||||
};
|
||||
|
||||
mockDataSource.transaction.mockRejectedValue(
|
||||
new Error('Transaction failed'),
|
||||
);
|
||||
|
||||
await expect(service.create(apiKeyData)).rejects.toThrow(
|
||||
'Transaction failed',
|
||||
);
|
||||
|
||||
expect(mockDataSource.transaction).toHaveBeenCalled();
|
||||
expect(
|
||||
mockApiKeyRoleService.assignRoleToApiKeyWithManager,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(mockApiKeyRoleService.recomputeCache).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('findById', () => {
|
||||
@@ -329,6 +464,30 @@ describe('ApiKeyService', () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use custom expiration time if provided', async () => {
|
||||
mockApiKeyRepository.findOne.mockResolvedValue(mockApiKey);
|
||||
const expiresAt = new Date(Date.now() + 3600000); // 1 hour from now
|
||||
|
||||
await service.generateApiKeyToken(
|
||||
mockWorkspaceId,
|
||||
mockApiKeyId,
|
||||
expiresAt,
|
||||
);
|
||||
|
||||
expect(mockJwtWrapperService.sign).toHaveBeenCalledWith(
|
||||
{
|
||||
sub: mockWorkspaceId,
|
||||
type: JwtTokenTypeEnum.API_KEY,
|
||||
workspaceId: mockWorkspaceId,
|
||||
},
|
||||
expect.objectContaining({
|
||||
secret: mockSecret,
|
||||
expiresIn: expect.any(Number),
|
||||
jwtid: mockApiKeyId,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('utility methods', () => {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
import { DataSource, IsNull, Repository } from 'typeorm';
|
||||
|
||||
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/api-key-role.service';
|
||||
import { ApiKey } from 'src/engine/core-modules/api-key/api-key.entity';
|
||||
import {
|
||||
ApiKeyException,
|
||||
@@ -18,12 +19,34 @@ export class ApiKeyService {
|
||||
@InjectRepository(ApiKey, 'core')
|
||||
private readonly apiKeyRepository: Repository<ApiKey>,
|
||||
private readonly jwtWrapperService: JwtWrapperService,
|
||||
private readonly apiKeyRoleService: ApiKeyRoleService,
|
||||
@InjectDataSource('core')
|
||||
private readonly dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
async create(apiKeyData: Partial<ApiKey>): Promise<ApiKey> {
|
||||
const apiKey = this.apiKeyRepository.create(apiKeyData);
|
||||
async create(
|
||||
apiKeyData: Partial<ApiKey> & { roleId: string },
|
||||
): Promise<ApiKey> {
|
||||
const { roleId, ...apiKeyFields } = apiKeyData;
|
||||
|
||||
return await this.apiKeyRepository.save(apiKey);
|
||||
return await this.dataSource
|
||||
.transaction(async (manager) => {
|
||||
const apiKey = manager.create(ApiKey, apiKeyFields);
|
||||
const savedApiKey = await manager.save(apiKey);
|
||||
|
||||
await this.apiKeyRoleService.assignRoleToApiKeyWithManager(manager, {
|
||||
apiKeyId: savedApiKey.id,
|
||||
roleId,
|
||||
workspaceId: savedApiKey.workspaceId,
|
||||
});
|
||||
|
||||
return savedApiKey;
|
||||
})
|
||||
.then(async (savedApiKey) => {
|
||||
await this.apiKeyRoleService.recomputeCache(savedApiKey.workspaceId);
|
||||
|
||||
return savedApiKey;
|
||||
});
|
||||
}
|
||||
|
||||
async findById(id: string, workspaceId: string): Promise<ApiKey | null> {
|
||||
|
||||
+1
@@ -55,6 +55,7 @@ export class ApiKeyController {
|
||||
? new Date(createApiKeyDto.revokedAt)
|
||||
: undefined,
|
||||
workspaceId: workspace.id,
|
||||
roleId: createApiKeyDto.roleId,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
|
||||
@InputType()
|
||||
@@ -22,4 +23,9 @@ export class CreateApiKeyDTO {
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
revokedAt?: string;
|
||||
|
||||
@Field()
|
||||
@IsNotEmpty()
|
||||
@IsUUID()
|
||||
roleId: string;
|
||||
}
|
||||
|
||||
+4
@@ -21,6 +21,10 @@ export const apiKeyGraphqlApiExceptionHandler = (error: Error) => {
|
||||
throw new UserInputError(error.message, {
|
||||
userFriendlyMessage: error.userFriendlyMessage,
|
||||
});
|
||||
case ApiKeyExceptionCode.API_KEY_NO_ROLE_ASSIGNED:
|
||||
throw new ForbiddenError(error.message, {
|
||||
userFriendlyMessage: error.userFriendlyMessage,
|
||||
});
|
||||
default: {
|
||||
const _exhaustiveCheck: never = error.code;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user