127 lines
4.0 KiB
TypeScript
127 lines
4.0 KiB
TypeScript
|
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||
|
|
import { requireOpsAuth } from '../ts/opsserver/helpers/auth.js';
|
||
|
|
import * as interfaces from '../ts_interfaces/index.js';
|
||
|
|
|
||
|
|
type TScope = interfaces.data.TApiTokenScope;
|
||
|
|
|
||
|
|
const makeIdentity = (role: string = 'user'): interfaces.data.IIdentity => ({
|
||
|
|
jwt: `jwt-${role}`,
|
||
|
|
userId: `${role}-user`,
|
||
|
|
name: role,
|
||
|
|
expiresAt: Date.now() + 3600000,
|
||
|
|
role,
|
||
|
|
});
|
||
|
|
|
||
|
|
const makeOpsServer = (options: {
|
||
|
|
identityRole?: string | null;
|
||
|
|
tokenScopes?: TScope[];
|
||
|
|
tokenPolicy?: interfaces.data.IApiTokenPolicy;
|
||
|
|
}) => {
|
||
|
|
const token = {
|
||
|
|
id: 'token-1',
|
||
|
|
name: 'test-token',
|
||
|
|
tokenHash: 'hash',
|
||
|
|
scopes: options.tokenScopes || [],
|
||
|
|
policy: options.tokenPolicy,
|
||
|
|
createdAt: Date.now(),
|
||
|
|
expiresAt: null,
|
||
|
|
lastUsedAt: null,
|
||
|
|
createdBy: 'token-user',
|
||
|
|
enabled: true,
|
||
|
|
} as interfaces.data.IStoredApiToken;
|
||
|
|
|
||
|
|
return {
|
||
|
|
adminHandler: {
|
||
|
|
validateIdentity: async (identityArg?: interfaces.data.IIdentity) => {
|
||
|
|
if (!identityArg || options.identityRole === null) return null;
|
||
|
|
return { ...identityArg, role: options.identityRole || identityArg.role || 'user' };
|
||
|
|
},
|
||
|
|
},
|
||
|
|
dcRouterRef: {
|
||
|
|
apiTokenManager: {
|
||
|
|
validateToken: async (rawTokenArg: string) => rawTokenArg === 'valid-token' ? token : null,
|
||
|
|
hasScope: (storedTokenArg: interfaces.data.IStoredApiToken, scopeArg: TScope) => {
|
||
|
|
if (storedTokenArg.policy?.role === 'admin') return true;
|
||
|
|
return storedTokenArg.scopes.includes('*') || storedTokenArg.scopes.includes(scopeArg) || Boolean(storedTokenArg.policy?.scopes?.includes(scopeArg));
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
} as any;
|
||
|
|
};
|
||
|
|
|
||
|
|
const getErrorText = (errorArg: unknown) => {
|
||
|
|
return (errorArg as any).errorText || (errorArg as any).text || (errorArg as Error).message;
|
||
|
|
};
|
||
|
|
|
||
|
|
tap.test('requireOpsAuth accepts valid JWT identity for read endpoints', async () => {
|
||
|
|
const auth = await requireOpsAuth(
|
||
|
|
makeOpsServer({ identityRole: 'user' }),
|
||
|
|
{ identity: makeIdentity('user') },
|
||
|
|
{ scope: 'config:read' },
|
||
|
|
);
|
||
|
|
expect(auth.type).toEqual('identity');
|
||
|
|
expect(auth.userId).toEqual('user-user');
|
||
|
|
expect(auth.isAdmin).toEqual(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('requireOpsAuth rejects non-admin JWT identity for admin identity requirements', async () => {
|
||
|
|
let errorText = '';
|
||
|
|
try {
|
||
|
|
await requireOpsAuth(
|
||
|
|
makeOpsServer({ identityRole: 'user' }),
|
||
|
|
{ identity: makeIdentity('user') },
|
||
|
|
{ scope: 'routes:write', requireAdminIdentity: true },
|
||
|
|
);
|
||
|
|
} catch (error) {
|
||
|
|
errorText = getErrorText(error);
|
||
|
|
}
|
||
|
|
expect(errorText).toEqual('admin identity required');
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('requireOpsAuth accepts scoped API tokens', async () => {
|
||
|
|
const auth = await requireOpsAuth(
|
||
|
|
makeOpsServer({ identityRole: null, tokenScopes: ['logs:read'] }),
|
||
|
|
{ apiToken: 'valid-token' },
|
||
|
|
{ scope: 'logs:read' },
|
||
|
|
);
|
||
|
|
expect(auth.type).toEqual('apiToken');
|
||
|
|
expect(auth.userId).toEqual('token-user');
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('requireOpsAuth rejects API tokens without the required scope', async () => {
|
||
|
|
let errorText = '';
|
||
|
|
try {
|
||
|
|
await requireOpsAuth(
|
||
|
|
makeOpsServer({ identityRole: null, tokenScopes: ['logs:read'] }),
|
||
|
|
{ apiToken: 'valid-token' },
|
||
|
|
{ scope: 'stats:read' },
|
||
|
|
);
|
||
|
|
} catch (error) {
|
||
|
|
errorText = getErrorText(error);
|
||
|
|
}
|
||
|
|
expect(errorText).toEqual('insufficient scope');
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('requireOpsAuth requires admin policy for sensitive API-token operations', async () => {
|
||
|
|
let errorText = '';
|
||
|
|
try {
|
||
|
|
await requireOpsAuth(
|
||
|
|
makeOpsServer({ identityRole: null, tokenScopes: ['tokens:manage'] }),
|
||
|
|
{ apiToken: 'valid-token' },
|
||
|
|
{ scope: 'tokens:manage', requireAdminToken: true },
|
||
|
|
);
|
||
|
|
} catch (error) {
|
||
|
|
errorText = getErrorText(error);
|
||
|
|
}
|
||
|
|
expect(errorText).toEqual('admin API token required');
|
||
|
|
|
||
|
|
const auth = await requireOpsAuth(
|
||
|
|
makeOpsServer({ identityRole: null, tokenPolicy: { role: 'admin' } }),
|
||
|
|
{ apiToken: 'valid-token' },
|
||
|
|
{ scope: 'tokens:manage', requireAdminToken: true },
|
||
|
|
);
|
||
|
|
expect(auth.isAdmin).toEqual(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
export default tap.start();
|