mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
227 lines
6.8 KiB
JavaScript
227 lines
6.8 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { createHmac } = require('node:crypto');
|
|
const { test } = require('node:test');
|
|
const {
|
|
ApiCredentialUnavailableError,
|
|
} = require('@qinglong/runtime-core/api-credential');
|
|
const {
|
|
ClusterControlApiCredentialConfigurationError,
|
|
ClusterControlApiCredentialUnavailableError,
|
|
apiCredentialSecretDigest,
|
|
createClusterControlApiCredentialAuthenticator,
|
|
} = require('@qinglong/cluster-control/api-credential');
|
|
|
|
const NOW = 10_000;
|
|
const PEPPER = Buffer.alloc(32, 1).toString('base64url');
|
|
const NEXT_PEPPER = Buffer.alloc(32, 3).toString('base64url');
|
|
const SECRET = Buffer.alloc(32, 2).toString('base64url');
|
|
const CREDENTIAL_ID = 'app_primary';
|
|
|
|
function metadata(authorization = `Bearer ql3c_${CREDENTIAL_ID}_${SECRET}`) {
|
|
return {
|
|
requestId: 'request-1',
|
|
method: 'POST',
|
|
path: '/api/v3/projects/default/runs',
|
|
query: Object.freeze({}),
|
|
headers: Object.freeze({ authorization }),
|
|
signal: new AbortController().signal,
|
|
};
|
|
}
|
|
|
|
function credential(overrides = {}) {
|
|
return {
|
|
credentialId: CREDENTIAL_ID,
|
|
version: 2,
|
|
pepperKeyId: 'legacy-v1',
|
|
state: 'active',
|
|
subject: { type: 'api_app', id: 'app_primary' },
|
|
subjectStatus: 'active',
|
|
secretDigest: apiCredentialSecretDigest(PEPPER, CREDENTIAL_ID, SECRET),
|
|
createdAtMs: 1,
|
|
notBeforeAtMs: 1,
|
|
expiresAtMs: 100_000,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function authenticator(value = credential(), overrides = {}) {
|
|
return createClusterControlApiCredentialAuthenticator(
|
|
{
|
|
async resolve(credentialId) {
|
|
assert.equal(credentialId, CREDENTIAL_ID);
|
|
return value;
|
|
},
|
|
},
|
|
PEPPER,
|
|
{ now: () => NOW, ...overrides },
|
|
);
|
|
}
|
|
|
|
test('authenticates a high-entropy service bearer as a short-lived principal', async () => {
|
|
const principal = await authenticator().authenticate(metadata());
|
|
assert.deepEqual(principal, {
|
|
subject: { type: 'api_app', id: 'app_primary' },
|
|
authenticationId: 'api_credential:app_primary:2',
|
|
authenticatedAtMs: NOW,
|
|
expiresAtMs: NOW + 60_000,
|
|
assurance: 'service',
|
|
});
|
|
assert.equal(Object.isFrozen(principal), true);
|
|
assert.equal(JSON.stringify(principal).includes(SECRET), false);
|
|
});
|
|
|
|
test('derives a domain-separated HMAC digest and user assurance', async () => {
|
|
const expected = createHmac('sha256', Buffer.from(PEPPER, 'base64url'))
|
|
.update(Buffer.from('qinglong-api-credential-v1\0', 'utf8'))
|
|
.update(CREDENTIAL_ID, 'utf8')
|
|
.update('\0', 'utf8')
|
|
.update(Buffer.from(SECRET, 'base64url'))
|
|
.digest('hex');
|
|
assert.equal(
|
|
apiCredentialSecretDigest(PEPPER, CREDENTIAL_ID, SECRET),
|
|
expected,
|
|
);
|
|
const principal = await authenticator(
|
|
credential({ subject: { type: 'user', id: 'usr_primary' } }),
|
|
).authenticate(metadata());
|
|
assert.equal(principal.assurance, 'single_factor');
|
|
});
|
|
|
|
test('authenticates overlap generations by exact stored key id without fallback', async () => {
|
|
const keyring = {
|
|
schemaVersion: 1,
|
|
activePepperKeyId: 'rotation-2026-08',
|
|
keys: [
|
|
{ pepperKeyId: 'legacy-v1', pepper: PEPPER },
|
|
{ pepperKeyId: 'rotation-2026-08', pepper: NEXT_PEPPER },
|
|
],
|
|
};
|
|
const records = new Map([
|
|
[
|
|
'legacy',
|
|
credential({
|
|
credentialId: 'legacy',
|
|
secretDigest: apiCredentialSecretDigest(PEPPER, 'legacy', SECRET),
|
|
}),
|
|
],
|
|
[
|
|
'next',
|
|
credential({
|
|
credentialId: 'next',
|
|
pepperKeyId: 'rotation-2026-08',
|
|
secretDigest: apiCredentialSecretDigest(
|
|
NEXT_PEPPER,
|
|
'next',
|
|
SECRET,
|
|
),
|
|
}),
|
|
],
|
|
['unknown', credential({ credentialId: 'unknown', pepperKeyId: 'missing' })],
|
|
]);
|
|
const verifier = createClusterControlApiCredentialAuthenticator(
|
|
{ async resolve(credentialId) { return records.get(credentialId) ?? null; } },
|
|
keyring,
|
|
{ now: () => NOW },
|
|
);
|
|
const request = (credentialId) =>
|
|
metadata(`Bearer ql3c_${credentialId}_${SECRET}`);
|
|
|
|
assert.equal((await verifier.authenticate(request('legacy'))).subject.id, 'app_primary');
|
|
assert.equal((await verifier.authenticate(request('next'))).subject.id, 'app_primary');
|
|
await assert.rejects(
|
|
verifier.authenticate(request('unknown')),
|
|
ClusterControlApiCredentialUnavailableError,
|
|
);
|
|
});
|
|
|
|
test('rejects missing, malformed, wrong, inactive and disabled credentials', async () => {
|
|
let repositoryCalls = 0;
|
|
const strict = createClusterControlApiCredentialAuthenticator(
|
|
{
|
|
async resolve() {
|
|
repositoryCalls += 1;
|
|
return credential();
|
|
},
|
|
},
|
|
PEPPER,
|
|
{ now: () => NOW },
|
|
);
|
|
for (const header of [
|
|
undefined,
|
|
'bearer token',
|
|
`Bearer ql3c_${CREDENTIAL_ID}_short`,
|
|
`Bearer ql3c_${CREDENTIAL_ID}_${Buffer.alloc(32, 3).toString('base64url')}`,
|
|
]) {
|
|
const request = metadata();
|
|
request.headers = Object.freeze(
|
|
header === undefined ? {} : { authorization: header },
|
|
);
|
|
assert.equal(await strict.authenticate(request), null);
|
|
}
|
|
assert.equal(
|
|
repositoryCalls,
|
|
1,
|
|
'only a structurally valid token reaches SQL',
|
|
);
|
|
|
|
for (const value of [
|
|
credential({ state: 'revoked' }),
|
|
credential({ subjectStatus: 'disabled' }),
|
|
credential({ notBeforeAtMs: NOW + 1 }),
|
|
credential({ expiresAtMs: NOW }),
|
|
null,
|
|
]) {
|
|
assert.equal(await authenticator(value).authenticate(metadata()), null);
|
|
}
|
|
});
|
|
|
|
test('maps storage, corrupt record and cancellation failures to unavailable', async () => {
|
|
const unavailable = createClusterControlApiCredentialAuthenticator(
|
|
{
|
|
async resolve() {
|
|
throw new ApiCredentialUnavailableError();
|
|
},
|
|
},
|
|
PEPPER,
|
|
{ now: () => NOW },
|
|
);
|
|
await assert.rejects(
|
|
unavailable.authenticate(metadata()),
|
|
ClusterControlApiCredentialUnavailableError,
|
|
);
|
|
await assert.rejects(
|
|
authenticator(credential({ secretDigest: 'corrupt' })).authenticate(
|
|
metadata(),
|
|
),
|
|
ClusterControlApiCredentialUnavailableError,
|
|
);
|
|
await assert.rejects(
|
|
authenticator(credential({ pepperKeyId: 'other-v1' })).authenticate(
|
|
metadata(),
|
|
),
|
|
ClusterControlApiCredentialUnavailableError,
|
|
);
|
|
|
|
const controller = new AbortController();
|
|
controller.abort();
|
|
await assert.rejects(
|
|
authenticator().authenticate({ ...metadata(), signal: controller.signal }),
|
|
ClusterControlApiCredentialUnavailableError,
|
|
);
|
|
});
|
|
|
|
test('rejects weak pepper and unbounded principal lifetime at construction', () => {
|
|
const repository = { async resolve() {} };
|
|
assert.throws(
|
|
() => createClusterControlApiCredentialAuthenticator(repository, 'weak'),
|
|
ClusterControlApiCredentialConfigurationError,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
createClusterControlApiCredentialAuthenticator(repository, PEPPER, {
|
|
principalTtlMs: 300_001,
|
|
}),
|
|
ClusterControlApiCredentialConfigurationError,
|
|
);
|
|
});
|