feat(api-key): restrict job operations to service tokens
This commit is contained in:
parent
95261acb92
commit
2f7163b856
@ -26,7 +26,10 @@ vi.mock('./repository.js', () => repoMock);
|
||||
vi.mock('./registry.js', () => registryMock);
|
||||
vi.mock('./runner.js', () => runnerMock);
|
||||
|
||||
async function seedApiKey(scopes: string[]) {
|
||||
async function seedApiKey(
|
||||
scopes: string[],
|
||||
tokenType: 'product_api' | 'service_api' = 'service_api'
|
||||
) {
|
||||
const provider = new MemoryDatastoreProvider();
|
||||
setProvider(provider);
|
||||
const collection = provider.getCollection('api_tokens', '/id');
|
||||
@ -35,7 +38,7 @@ async function seedApiKey(scopes: string[]) {
|
||||
productId: 'lysnrai',
|
||||
userId: 'svc_jobs',
|
||||
userName: 'Jobs Service',
|
||||
tokenType: 'service_api',
|
||||
tokenType,
|
||||
environment: 'dev',
|
||||
prefix: rawApiKey.slice(0, 12),
|
||||
tokenHash: await bcrypt.hash(rawApiKey, 10),
|
||||
@ -97,4 +100,23 @@ describe('jobRoutes api key integration', () => {
|
||||
expect(repoMock.getJobDefinition).toHaveBeenCalledWith('job_reindex', 'lysnrai');
|
||||
expect(runnerMock.executeJob).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects product_api keys on job routes', async () => {
|
||||
await seedApiKey(['jobs:read'], 'product_api');
|
||||
repoMock.listJobDefinitions.mockResolvedValue([{ id: 'job_sync', productId: 'lysnrai' }]);
|
||||
|
||||
const { jobRoutes } = await import('./routes.js');
|
||||
const app = Fastify();
|
||||
await registerOptionalApiKeyContext(app);
|
||||
await app.register(jobRoutes, { prefix: '/api' });
|
||||
|
||||
const res = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/jobs',
|
||||
headers: { 'x-api-key': rawApiKey },
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(403);
|
||||
expect(repoMock.listJobDefinitions).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@ -11,6 +11,7 @@ export async function jobRoutes(app: FastifyInstance) {
|
||||
const access = requireJwtOrApiKey(req, {
|
||||
allowJwt: true,
|
||||
apiKeyScopes: ['jobs:read'],
|
||||
apiKeyTokenTypes: ['service_api'],
|
||||
rateLimitKey: 'jobs:read',
|
||||
});
|
||||
return access.productId;
|
||||
@ -20,6 +21,7 @@ export async function jobRoutes(app: FastifyInstance) {
|
||||
const access = requireJwtOrApiKey(req, {
|
||||
jwtRoles: ['super_admin', 'admin'],
|
||||
apiKeyScopes: ['jobs:write'],
|
||||
apiKeyTokenTypes: ['service_api'],
|
||||
rateLimitKey: 'jobs:write',
|
||||
});
|
||||
return access.productId;
|
||||
|
||||
@ -16,6 +16,7 @@ export async function runRoutes(app: FastifyInstance) {
|
||||
const access = requireJwtOrApiKey(req, {
|
||||
jwtRoles: ['super_admin', 'admin'],
|
||||
apiKeyScopes: ['jobs:read'],
|
||||
apiKeyTokenTypes: ['service_api'],
|
||||
rateLimitKey: 'jobs:read',
|
||||
});
|
||||
return access.productId;
|
||||
@ -27,6 +28,7 @@ export async function runRoutes(app: FastifyInstance) {
|
||||
} {
|
||||
const access = requireJwtOrApiKey(req, {
|
||||
jwtRoles: ['super_admin', 'admin'],
|
||||
apiKeyTokenTypes: ['service_api'],
|
||||
rateLimitKey: 'jobs:write',
|
||||
});
|
||||
return { productId: access.productId, actorId: access.actorId };
|
||||
|
||||
@ -15,7 +15,10 @@ const repoMock = {
|
||||
|
||||
vi.mock('./repository.js', () => repoMock);
|
||||
|
||||
async function seedApiKey(scopes: string[]) {
|
||||
async function seedApiKey(
|
||||
scopes: string[],
|
||||
tokenType: 'product_api' | 'service_api' = 'service_api'
|
||||
) {
|
||||
const provider = new MemoryDatastoreProvider();
|
||||
setProvider(provider);
|
||||
const collection = provider.getCollection('api_tokens', '/id');
|
||||
@ -24,7 +27,7 @@ async function seedApiKey(scopes: string[]) {
|
||||
productId: 'lysnrai',
|
||||
userId: 'svc_runs',
|
||||
userName: 'Runs Service',
|
||||
tokenType: 'service_api',
|
||||
tokenType,
|
||||
environment: 'dev',
|
||||
prefix: rawApiKey.slice(0, 12),
|
||||
tokenHash: await bcrypt.hash(rawApiKey, 10),
|
||||
@ -59,4 +62,23 @@ describe('runRoutes api key integration', () => {
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(repoMock.listRuns).toHaveBeenCalledWith('lysnrai', { limit: 10 });
|
||||
});
|
||||
|
||||
it('rejects product_api keys on run routes', async () => {
|
||||
await seedApiKey(['jobs:read'], 'product_api');
|
||||
repoMock.listRuns.mockResolvedValue([{ id: 'run_1', productId: 'lysnrai' }]);
|
||||
|
||||
const { runRoutes } = await import('./routes.js');
|
||||
const app = Fastify();
|
||||
await registerOptionalApiKeyContext(app);
|
||||
await app.register(runRoutes, { prefix: '/api' });
|
||||
|
||||
const res = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/runs?limit=10',
|
||||
headers: { 'x-api-key': rawApiKey },
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(403);
|
||||
expect(repoMock.listRuns).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user