Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 52 additions & 18 deletions src/api-key/api-key.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { TestClient } from 'test/test-client';
import { APIKeyPermission } from './api-key.entity';
import { APIKeyPermissionType } from './api-key.entity';

describe('APIKeyController (e2e)', () => {
let client: TestClient;
Expand All @@ -19,9 +19,15 @@ describe('APIKeyController (e2e)', () => {
namespace_id: client.namespace.id,
attrs: {
root_resource_id: client.namespace.root_resource_id,
permissions: {
resources: [APIKeyPermission.READ, APIKeyPermission.CREATE],
},
permissions: [
{
target: 'resources',
permissions: [
APIKeyPermissionType.READ,
APIKeyPermissionType.CREATE,
],
},
],
},
};

Expand All @@ -37,9 +43,15 @@ describe('APIKeyController (e2e)', () => {
namespace_id: client.namespace.id,
attrs: {
root_resource_id: client.namespace.root_resource_id,
permissions: {
resources: [APIKeyPermission.READ, APIKeyPermission.CREATE],
},
permissions: [
{
target: 'resources',
permissions: [
APIKeyPermissionType.READ,
APIKeyPermissionType.CREATE,
],
},
],
},
});
expect(body.id).toBeDefined();
Expand Down Expand Up @@ -111,9 +123,15 @@ describe('APIKeyController (e2e)', () => {
id: apiKeyId,
attrs: {
root_resource_id: client.namespace.root_resource_id,
permissions: {
resources: [APIKeyPermission.READ, APIKeyPermission.CREATE],
},
permissions: [
{
target: 'resources',
permissions: [
APIKeyPermissionType.READ,
APIKeyPermissionType.CREATE,
],
},
],
},
});
});
Expand All @@ -122,7 +140,12 @@ describe('APIKeyController (e2e)', () => {
const updateData = {
attrs: {
root_resource_id: client.namespace.root_resource_id,
permissions: { resources: [APIKeyPermission.READ] },
permissions: [
{
target: 'resources',
permissions: [APIKeyPermissionType.READ],
},
],
},
};

Expand All @@ -135,7 +158,12 @@ describe('APIKeyController (e2e)', () => {
id: apiKeyId,
attrs: {
root_resource_id: client.namespace.root_resource_id,
permissions: { resources: [APIKeyPermission.READ] },
permissions: [
{
target: 'resources',
permissions: [APIKeyPermissionType.READ],
},
],
},
});
});
Expand All @@ -157,9 +185,12 @@ describe('APIKeyController (e2e)', () => {
namespace_id: 'non-existent-namespace',
attrs: {
root_resource_id: client.namespace.root_resource_id,
permissions: {
resources: [APIKeyPermission.READ],
},
permissions: [
{
target: 'resources',
permissions: [APIKeyPermissionType.READ],
},
],
},
};

Expand All @@ -172,9 +203,12 @@ describe('APIKeyController (e2e)', () => {
namespace_id: client.namespace.id,
attrs: {
root_resource_id: 'non-existent-resource',
permissions: {
resources: [APIKeyPermission.READ],
},
permissions: [
{
target: 'resources',
permissions: [APIKeyPermissionType.READ],
},
],
},
};

Expand Down
13 changes: 9 additions & 4 deletions src/api-key/api-key.entity.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Base } from 'omniboxd/common/base.entity';
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

export enum APIKeyPermission {
export enum APIKeyPermissionType {
CREATE = 'create',
READ = 'read',
UPDATE = 'update',
Expand All @@ -12,10 +12,15 @@ export enum APIKeyPermissionTarget {
RESOURCES = 'resources',
}

export class APIKeyAttrs {
export type APIKeyPermission = {
target: APIKeyPermissionTarget;
permissions: APIKeyPermissionType[];
};

export type APIKeyAttrs = {
root_resource_id: string;
permissions: Record<APIKeyPermissionTarget, APIKeyPermission[]>;
}
permissions: APIKeyPermission[];
};

@Entity('api_keys')
export class APIKey extends Base {
Expand Down
29 changes: 19 additions & 10 deletions src/api-key/api-key.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { ForbiddenException } from '@nestjs/common';
import { APIKeyService } from './api-key.service';
import {
APIKey,
APIKeyPermission,
APIKeyPermissionType,
APIKeyPermissionTarget,
} from './api-key.entity';
import { PermissionsService } from 'omniboxd/permissions/permissions.service';
Expand All @@ -27,9 +27,12 @@ describe('APIKeyService', () => {
namespaceId: 'test-namespace-id',
attrs: {
root_resource_id: 'test-resource-id',
permissions: {
[APIKeyPermissionTarget.RESOURCES]: [APIKeyPermission.READ],
},
permissions: [
{
target: APIKeyPermissionTarget.RESOURCES,
permissions: [APIKeyPermissionType.READ],
},
],
},
createdAt: new Date(),
updatedAt: new Date(),
Expand Down Expand Up @@ -87,9 +90,12 @@ describe('APIKeyService', () => {
namespace_id: 'test-namespace-id',
attrs: {
root_resource_id: 'test-resource-id',
permissions: {
[APIKeyPermissionTarget.RESOURCES]: [APIKeyPermission.READ],
},
permissions: [
{
target: APIKeyPermissionTarget.RESOURCES,
permissions: [APIKeyPermissionType.READ],
},
],
},
};

Expand Down Expand Up @@ -192,9 +198,12 @@ describe('APIKeyService', () => {
user_id: 'test-user-id',
namespace_id: 'test-namespace-id',
attrs: {
permissions: {
[APIKeyPermissionTarget.RESOURCES]: [APIKeyPermission.READ],
},
permissions: [
{
target: APIKeyPermissionTarget.RESOURCES,
permissions: [APIKeyPermissionType.READ],
},
],
} as any,
};

Expand Down
74 changes: 68 additions & 6 deletions src/auth/api-key/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,36 @@ export class ProtectedController {
}
```

### Permission-Based Authentication

To require specific permissions for an endpoint, use the `permissions` option:

```typescript
import { Controller, Post } from '@nestjs/common';
import { APIKeyAuth } from 'omniboxd/auth/decorators';
import {
APIKeyPermissionTarget,
APIKeyPermissionType
} from 'omniboxd/api-key/api-key.entity';

@Controller('api/v1/resources')
export class ResourcesController {

@Post()
@APIKeyAuth({
permissions: [
{
target: APIKeyPermissionTarget.RESOURCES,
permissions: [APIKeyPermissionType.CREATE]
}
]
})
createResource() {
return { message: 'Resource created' };
}
}
```

### Accessing API Key Data

To access the API key data in your controller, use the `@APIKey()` parameter decorator:
Expand Down Expand Up @@ -94,18 +124,50 @@ interface APIKey {

interface APIKeyAttrs {
root_resource_id: string;
permissions: Record<APIKeyPermissionTarget, APIKeyPermission[]>;
permissions: APIKeyPermission[];
}

interface APIKeyPermission {
target: APIKeyPermissionTarget;
permissions: APIKeyPermissionType[];
}
```

## Permission Validation

When using the `permissions` option, the guard validates that the API key has all required permissions:

1. **Target Matching**: The API key must have permissions for the specified target (e.g., `RESOURCES`)
2. **Permission Checking**: The API key must have all required permission types for each target
3. **Validation Logic**: If any required permission is missing, a `ForbiddenException` is thrown

### Available Permission Types

```typescript
enum APIKeyPermissionType {
CREATE = 'create',
READ = 'read',
UPDATE = 'update',
DELETE = 'delete',
}

enum APIKeyPermissionTarget {
RESOURCES = 'resources',
}
```

## Error Handling

The `APIKeyAuthGuard` throws `UnauthorizedException` in the following cases:
The `APIKeyAuthGuard` throws exceptions in the following cases:

- **Missing Authorization Header**: No `Authorization` header is provided
- **Invalid Format**: API key doesn't start with `sk-`
- **Not Found**: API key is not found in the database
- **Parameter Decorator**: `@APIKey()` decorator throws if no API key data is available
- **UnauthorizedException**:
- Missing Authorization Header: No `Authorization` header is provided
- Invalid Format: API key doesn't start with `sk-`
- Not Found: API key is not found in the database
- **ForbiddenException**:
- Missing Target Permission: API key doesn't have permission for the required target
- Missing Permission Type: API key doesn't have the specific permission type required
- **Parameter Decorator**: `@APIKey()` decorator throws `UnauthorizedException` if no API key data is available

## Integration with JWT Authentication

Expand Down
Loading