fix(tokens): tighten machine credential issuance
This commit is contained in:
parent
57abfa5b03
commit
473b7310d5
@ -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',
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user