mirror of
https://github.com/immich-app/immich.git
synced 2025-06-14 21:38:26 +02:00
feat(web): granular api access controls (#18179)
* feat: api access control * feat(web): granular api access controls * fix test * fix e2e test * fix: lint * pr feedback * merge main + new design * finalize styling --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
parent
f0d881b4f8
commit
b054e9dc2c
12 changed files with 311 additions and 37 deletions
server/src
controllers
dtos
services
|
@ -1,4 +1,5 @@
|
|||
import { APIKeyController } from 'src/controllers/api-key.controller';
|
||||
import { Permission } from 'src/enum';
|
||||
import { ApiKeyService } from 'src/services/api-key.service';
|
||||
import request from 'supertest';
|
||||
import { factory } from 'test/small.factory';
|
||||
|
@ -52,7 +53,9 @@ describe(APIKeyController.name, () => {
|
|||
});
|
||||
|
||||
it('should require a valid uuid', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).put(`/api-keys/123`).send({ name: 'new name' });
|
||||
const { status, body } = await request(ctx.getHttpServer())
|
||||
.put(`/api-keys/123`)
|
||||
.send({ name: 'new name', permissions: [Permission.ALL] });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest(['id must be a UUID']));
|
||||
});
|
||||
|
|
|
@ -18,6 +18,11 @@ export class APIKeyUpdateDto {
|
|||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name!: string;
|
||||
|
||||
@IsEnum(Permission, { each: true })
|
||||
@ApiProperty({ enum: Permission, enumName: 'Permission', isArray: true })
|
||||
@ArrayMinSize(1)
|
||||
permissions!: Permission[];
|
||||
}
|
||||
|
||||
export class APIKeyCreateResponseDto {
|
||||
|
|
|
@ -69,7 +69,9 @@ describe(ApiKeyService.name, () => {
|
|||
|
||||
mocks.apiKey.getById.mockResolvedValue(void 0);
|
||||
|
||||
await expect(sut.update(auth, id, { name: 'New Name' })).rejects.toBeInstanceOf(BadRequestException);
|
||||
await expect(sut.update(auth, id, { name: 'New Name', permissions: [Permission.ALL] })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
|
||||
expect(mocks.apiKey.update).not.toHaveBeenCalledWith(id);
|
||||
});
|
||||
|
@ -82,9 +84,28 @@ describe(ApiKeyService.name, () => {
|
|||
mocks.apiKey.getById.mockResolvedValue(apiKey);
|
||||
mocks.apiKey.update.mockResolvedValue(apiKey);
|
||||
|
||||
await sut.update(auth, apiKey.id, { name: newName });
|
||||
await sut.update(auth, apiKey.id, { name: newName, permissions: [Permission.ALL] });
|
||||
|
||||
expect(mocks.apiKey.update).toHaveBeenCalledWith(auth.user.id, apiKey.id, { name: newName });
|
||||
expect(mocks.apiKey.update).toHaveBeenCalledWith(auth.user.id, apiKey.id, {
|
||||
name: newName,
|
||||
permissions: [Permission.ALL],
|
||||
});
|
||||
});
|
||||
|
||||
it('should update permissions', async () => {
|
||||
const auth = factory.auth();
|
||||
const apiKey = factory.apiKey({ userId: auth.user.id });
|
||||
const newPermissions = [Permission.ACTIVITY_CREATE, Permission.ACTIVITY_READ, Permission.ACTIVITY_UPDATE];
|
||||
|
||||
mocks.apiKey.getById.mockResolvedValue(apiKey);
|
||||
mocks.apiKey.update.mockResolvedValue(apiKey);
|
||||
|
||||
await sut.update(auth, apiKey.id, { name: apiKey.name, permissions: newPermissions });
|
||||
|
||||
expect(mocks.apiKey.update).toHaveBeenCalledWith(auth.user.id, apiKey.id, {
|
||||
name: apiKey.name,
|
||||
permissions: newPermissions,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ export class ApiKeyService extends BaseService {
|
|||
throw new BadRequestException('API Key not found');
|
||||
}
|
||||
|
||||
const key = await this.apiKeyRepository.update(auth.user.id, id, { name: dto.name });
|
||||
const key = await this.apiKeyRepository.update(auth.user.id, id, { name: dto.name, permissions: dto.permissions });
|
||||
|
||||
return this.map(key);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue