From b123beae38b952c4b713f9e5a1df0528f7605ddf Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 19 Jan 2026 10:20:06 -0500 Subject: [PATCH] fix(server): api key update checks (#25363) --- server/src/services/api-key.service.spec.ts | 72 +++++++++++++++++++++ server/src/services/api-key.service.ts | 8 +++ 2 files changed, 80 insertions(+) diff --git a/server/src/services/api-key.service.spec.ts b/server/src/services/api-key.service.spec.ts index 8d48b47f1e..14544f454f 100644 --- a/server/src/services/api-key.service.spec.ts +++ b/server/src/services/api-key.service.spec.ts @@ -107,6 +107,78 @@ describe(ApiKeyService.name, () => { permissions: newPermissions, }); }); + + describe('api key auth', () => { + it('should prevent adding Permission.all', async () => { + const permissions = [Permission.ApiKeyCreate, Permission.ApiKeyUpdate, Permission.AssetRead]; + const auth = factory.auth({ apiKey: { permissions } }); + const apiKey = factory.apiKey({ userId: auth.user.id, permissions }); + + mocks.apiKey.getById.mockResolvedValue(apiKey); + + await expect(sut.update(auth, apiKey.id, { permissions: [Permission.All] })).rejects.toThrow( + 'Cannot grant permissions you do not have', + ); + + expect(mocks.apiKey.update).not.toHaveBeenCalled(); + }); + + it('should prevent adding a new permission', async () => { + const permissions = [Permission.ApiKeyCreate, Permission.ApiKeyUpdate, Permission.AssetRead]; + const auth = factory.auth({ apiKey: { permissions } }); + const apiKey = factory.apiKey({ userId: auth.user.id, permissions }); + + mocks.apiKey.getById.mockResolvedValue(apiKey); + + await expect(sut.update(auth, apiKey.id, { permissions: [Permission.AssetCopy] })).rejects.toThrow( + 'Cannot grant permissions you do not have', + ); + + expect(mocks.apiKey.update).not.toHaveBeenCalled(); + }); + + it('should allow removing permissions', async () => { + const auth = factory.auth({ apiKey: { permissions: [Permission.ApiKeyUpdate, Permission.AssetRead] } }); + const apiKey = factory.apiKey({ + userId: auth.user.id, + permissions: [Permission.AssetRead, Permission.AssetDelete], + }); + + mocks.apiKey.getById.mockResolvedValue(apiKey); + mocks.apiKey.update.mockResolvedValue(apiKey); + + // remove Permission.AssetDelete + await sut.update(auth, apiKey.id, { permissions: [Permission.AssetRead] }); + + expect(mocks.apiKey.update).toHaveBeenCalledWith( + auth.user.id, + apiKey.id, + expect.objectContaining({ permissions: [Permission.AssetRead] }), + ); + }); + + it('should allow adding new permissions', async () => { + const auth = factory.auth({ + apiKey: { permissions: [Permission.ApiKeyUpdate, Permission.AssetRead, Permission.AssetUpdate] }, + }); + const apiKey = factory.apiKey({ userId: auth.user.id, permissions: [Permission.AssetRead] }); + + mocks.apiKey.getById.mockResolvedValue(apiKey); + mocks.apiKey.update.mockResolvedValue(apiKey); + + // add Permission.AssetUpdate + await sut.update(auth, apiKey.id, { + name: apiKey.name, + permissions: [Permission.AssetRead, Permission.AssetUpdate], + }); + + expect(mocks.apiKey.update).toHaveBeenCalledWith( + auth.user.id, + apiKey.id, + expect.objectContaining({ permissions: [Permission.AssetRead, Permission.AssetUpdate] }), + ); + }); + }); }); describe('delete', () => { diff --git a/server/src/services/api-key.service.ts b/server/src/services/api-key.service.ts index 96671daab1..492ee9c0fd 100644 --- a/server/src/services/api-key.service.ts +++ b/server/src/services/api-key.service.ts @@ -32,6 +32,14 @@ export class ApiKeyService extends BaseService { throw new BadRequestException('API Key not found'); } + if ( + auth.apiKey && + dto.permissions && + !isGranted({ requested: dto.permissions, current: auth.apiKey.permissions }) + ) { + throw new BadRequestException('Cannot grant permissions you do not have'); + } + const key = await this.apiKeyRepository.update(auth.user.id, id, { name: dto.name, permissions: dto.permissions }); return this.map(key);