fix(tokens): tighten machine credential issuance

This commit is contained in:
root 2026-03-15 06:28:50 +00:00
parent 57abfa5b03
commit 473b7310d5
2 changed files with 86 additions and 3 deletions

View File

@ -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',

View File

@ -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);