diff --git a/services/platform-service/src/modules/tokens/routes.test.ts b/services/platform-service/src/modules/tokens/routes.test.ts index d670ec97..c0fbe468 100644 --- a/services/platform-service/src/modules/tokens/routes.test.ts +++ b/services/platform-service/src/modules/tokens/routes.test.ts @@ -129,9 +129,7 @@ describe('tokenRoutes', () => { expect(repoMock.hashToken).toHaveBeenCalled(); }); - it('POST /tokens respects explicit environment overrides', async () => { - repoMock.hashToken.mockResolvedValue('hashed_token'); - repoMock.create.mockResolvedValue(baseToken); + it('POST /tokens rejects unsupported user_api tokens', async () => { const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', @@ -139,6 +137,83 @@ describe('tokenRoutes', () => { email: 'admin@example.com', }); + const res = await app.inject({ + method: 'POST', + url: '/api/tokens', + payload: { + name: 'legacy-user-token', + tokenType: 'user_api', + scopes: ['read'], + expiresInDays: 30, + }, + }); + + expect(res.statusCode).toBe(400); + expect(repoMock.create).not.toHaveBeenCalled(); + }); + + it('POST /tokens rejects service_api creation for non-super-admins', async () => { + const app = await buildApp({ + sub: 'admin_1', + productId: 'lysnrai', + role: 'admin', + email: 'admin@example.com', + }); + + const res = await app.inject({ + method: 'POST', + url: '/api/tokens', + payload: { + name: 'service-token', + tokenType: 'service_api', + scopes: ['jobs:read'], + expiresInDays: 30, + }, + }); + + expect(res.statusCode).toBe(403); + expect(repoMock.create).not.toHaveBeenCalled(); + }); + + it('POST /tokens allows service_api creation for super_admin', async () => { + repoMock.hashToken.mockResolvedValue('hashed_token'); + repoMock.create.mockResolvedValue(baseToken); + const app = await buildApp({ + sub: 'root_1', + productId: 'lysnrai', + role: 'super_admin', + email: 'root@example.com', + }); + + const res = await app.inject({ + method: 'POST', + url: '/api/tokens', + payload: { + name: 'service-token', + tokenType: 'service_api', + scopes: ['jobs:read'], + expiresInDays: 30, + }, + }); + + expect(res.statusCode).toBe(201); + expect(repoMock.create).toHaveBeenCalledWith( + expect.objectContaining({ + tokenType: 'service_api', + }) + ); + }); + + it('POST /tokens respects explicit environment overrides', async () => { + repoMock.hashToken.mockResolvedValue('hashed_token'); + repoMock.create.mockResolvedValue(baseToken); + const app = await buildApp({ + sub: 'root_1', + productId: 'lysnrai', + role: 'super_admin', + email: 'root@example.com', + }); + const res = await app.inject({ method: 'POST', url: '/api/tokens', diff --git a/services/platform-service/src/modules/tokens/routes.ts b/services/platform-service/src/modules/tokens/routes.ts index 890566c5..e7cb5758 100644 --- a/services/platform-service/src/modules/tokens/routes.ts +++ b/services/platform-service/src/modules/tokens/routes.ts @@ -69,6 +69,14 @@ export async function tokenRoutes(app: FastifyInstance) { const { name, tokenType, environment, scopes, expiresInDays } = parsed.data; const resolvedEnvironment = environment ?? getRuntimeEnvironment(); + if (tokenType === 'user_api') { + throw new BadRequestError('user_api tokens are not supported for machine credential routes'); + } + + if (tokenType === 'service_api' && payload.role !== 'super_admin') { + throw new ForbiddenError('Super admin access required for service_api tokens'); + } + const rawToken = `wai_${crypto.randomBytes(32).toString('hex')}`; const prefix = rawToken.slice(0, 12); const tokenHash = await repo.hashToken(rawToken);