Files
qinglong/packages/ql3-cluster-admin/test/administration.test.cjs
T

211 lines
6.1 KiB
JavaScript

const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
ApiCredentialAdministrationMutationConflictError,
} = require('@qinglong/runtime-core/api-credential-administration');
const {
apiCredentialSecretDigest,
} = require('@qinglong/runtime-core/api-credential-token');
const {
ClusterAdministrationAuthenticationError,
createClusterAdministrationService,
} = require('@qinglong/cluster-admin/administration');
const NOW = 1_000;
const PEPPER = 'A'.repeat(43);
const SUBJECT = { type: 'api_app', id: 'app_primary' };
const PRINCIPAL = {
subject: { type: 'user', id: 'usr_admin' },
authenticationId: 'session:admin:1',
authenticatedAtMs: 900,
expiresAtMs: 2_000,
assurance: 'multi_factor',
};
function request(overrides = {}) {
return {
mutationId: '123e4567-e89b-42d3-a456-426614174301',
requestId: 'request-credential-issue-1',
expectedCurrentVersion: 0,
credentialId: 'credential_primary',
subject: SUBJECT,
principal: PRINCIPAL,
notBeforeAtMs: NOW,
expiresAtMs: 2_000,
...overrides,
};
}
function repositories() {
let credentialMutation = null;
const credentialCommands = [];
const identityCommands = [];
const identity = {
subject: SUBJECT,
status: 'active',
version: 1,
createdAtMs: 100,
updatedAtMs: 100,
};
return {
identityCommands,
credentialCommands,
identities: {
async resolve() {
return identity;
},
async resolveMutation() {
return null;
},
async append(command) {
identityCommands.push(command);
return {
status: 'inserted',
identity: {
subject: command.mutation.subject,
status: command.mutation.status,
version: command.mutation.subjectVersion,
createdAtMs: command.mutation.createdAtMs,
updatedAtMs: command.mutation.createdAtMs,
},
mutation: command.mutation,
};
},
},
credentials: {
async resolveMutation() {
return credentialMutation;
},
async append(command) {
credentialCommands.push(command);
credentialMutation = {
credential: command.credential,
mutation: command.mutation,
audit: command.audit,
};
return {
status: 'inserted',
credential: command.credential,
mutation: command.mutation,
};
},
conflictWith(stored) {
credentialMutation = stored;
},
},
};
}
test('issues one token, stores only its digest and clears mutable secret bytes', async () => {
const repos = repositories();
const generated = Buffer.alloc(32, 7);
const expectedSecret = generated.toString('base64url');
const service = createClusterAdministrationService(
repos.identities,
repos.credentials,
PEPPER,
{ now: () => NOW, randomBytes: () => generated },
);
const result = await service.issueCredential(request());
assert.equal(result.status, 'inserted');
assert.equal(result.token, `ql3c_credential_primary_${expectedSecret}`);
assert.equal(repos.credentialCommands.length, 1);
assert.equal(
repos.credentialCommands[0].credential.secretDigest,
apiCredentialSecretDigest(PEPPER, 'credential_primary', expectedSecret),
);
assert.equal(repos.credentialCommands[0].audit.eventId, request().mutationId);
assert.equal(
generated.every((value) => value === 0),
true,
);
});
test('binds newly issued credentials to the selected active pepper key', async () => {
const repos = repositories();
const nextPepper = Buffer.alloc(32, 2).toString('base64url');
const generated = Buffer.alloc(32, 9);
const service = createClusterAdministrationService(
repos.identities,
repos.credentials,
{ pepperKeyId: 'rotation-2026-08', pepper: nextPepper },
{ now: () => NOW, randomBytes: () => generated },
);
const result = await service.issueCredential(request());
const secret = result.token.split('_').at(-1);
assert.equal(
repos.credentialCommands[0].credential.pepperKeyId,
'rotation-2026-08',
);
assert.equal(
repos.credentialCommands[0].credential.secretDigest,
apiCredentialSecretDigest(nextPepper, 'credential_primary', secret),
);
});
test('semantic mutation replay returns no token and does not generate a new secret', async () => {
const repos = repositories();
let randomCalls = 0;
const service = createClusterAdministrationService(
repos.identities,
repos.credentials,
PEPPER,
{
now: () => NOW,
randomBytes() {
randomCalls += 1;
return Buffer.alloc(32, randomCalls);
},
},
);
assert.ok((await service.issueCredential(request())).token);
const replay = await service.issueCredential(request());
assert.equal(replay.status, 'existing');
assert.equal(replay.token, null);
assert.equal(randomCalls, 1);
await assert.rejects(
service.issueCredential(request({ requestId: 'different-request' })),
ApiCredentialAdministrationMutationConflictError,
);
});
test('rejects weak principals and widened requests before repository mutation', async () => {
const repos = repositories();
const service = createClusterAdministrationService(
repos.identities,
repos.credentials,
PEPPER,
{ now: () => NOW, randomBytes: () => Buffer.alloc(32, 1) },
);
await assert.rejects(
service.issueCredential(
request({ principal: { ...PRINCIPAL, assurance: 'single_factor' } }),
),
ClusterAdministrationAuthenticationError,
);
await assert.rejects(
service.issueCredential(request({ debug: true })),
/request shape is invalid/,
);
assert.equal(repos.credentialCommands.length, 0);
});
test('maps malformed entropy output to a stable configuration error', async () => {
const repos = repositories();
const service = createClusterAdministrationService(
repos.identities,
repos.credentials,
PEPPER,
{ now: () => NOW, randomBytes: () => 'not-a-buffer' },
);
await assert.rejects(
service.issueCredential(request()),
/randomBytes returned invalid secret material/,
);
assert.equal(repos.credentialCommands.length, 0);
});