mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
314 lines
8.2 KiB
JavaScript
314 lines
8.2 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { test } = require('node:test');
|
|
|
|
const {
|
|
createLocalPresenceProofManager,
|
|
} = require('../dist/authentication/localPresenceProof.js');
|
|
const {
|
|
createLocalApiSecretListRoute,
|
|
createLocalApiSecretPutRoute,
|
|
} = require('../dist/secret/secretRoutes.js');
|
|
|
|
const PRINCIPAL = Object.freeze({
|
|
subject: Object.freeze({ type: 'user', id: 'owner' }),
|
|
authenticationId: 'local_credential:owner-console:1',
|
|
authenticatedAtMs: 9_000,
|
|
expiresAtMs: 60_000,
|
|
assurance: 'single_factor',
|
|
});
|
|
|
|
const FENCE = Object.freeze({
|
|
credentialId: 'owner-console',
|
|
credentialVersion: 1,
|
|
pepperKeyId: 'owner-v1',
|
|
materialDigest: 'a'.repeat(64),
|
|
subjectType: 'user',
|
|
subjectId: 'owner',
|
|
secretDigest: 'b'.repeat(64),
|
|
notBeforeAtMs: 1,
|
|
expiresAtMs: 60_000,
|
|
});
|
|
|
|
function uuidFactory() {
|
|
let sequence = 400;
|
|
return () => {
|
|
sequence += 1;
|
|
return `019f9200-0000-4000-8000-${String(sequence).padStart(12, '0')}`;
|
|
};
|
|
}
|
|
|
|
function body(overrides = {}) {
|
|
return Object.freeze({
|
|
name: 'github-token',
|
|
plaintext: 'never-return-this-value',
|
|
mutationId: '019f9200-0000-4000-8000-000000000101',
|
|
expectedCurrentVersion: 0,
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
function fixture(t) {
|
|
const deploymentRoot = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'ql3-secret-put-'),
|
|
);
|
|
fs.chmodSync(deploymentRoot, 0o700);
|
|
t.after(() => fs.rmSync(deploymentRoot, { recursive: true, force: true }));
|
|
const calls = [];
|
|
const presenceProof = createLocalPresenceProofManager({
|
|
deploymentRoot,
|
|
profile: 'edge',
|
|
now: () => 10_000,
|
|
randomUuid: uuidFactory(),
|
|
randomSecret: () => Buffer.alloc(32, 17),
|
|
});
|
|
t.after(() => presenceProof.close());
|
|
const projectPolicy = {
|
|
async resolve(projectId, subject) {
|
|
calls.push(['policy', projectId, subject]);
|
|
return {
|
|
project: {
|
|
id: projectId,
|
|
name: 'Default',
|
|
slug: 'default',
|
|
status: 'active',
|
|
version: 3,
|
|
createdAtMs: 1,
|
|
updatedAtMs: 2,
|
|
},
|
|
binding: {
|
|
projectId,
|
|
subject,
|
|
version: 5,
|
|
state: 'active',
|
|
role: 'owner',
|
|
mutationId: 'owner-binding',
|
|
changedBy: { type: 'user', id: 'bootstrap-owner' },
|
|
createdAtMs: 2,
|
|
},
|
|
};
|
|
},
|
|
async append() {
|
|
throw new Error('not used');
|
|
},
|
|
};
|
|
const route = createLocalApiSecretPutRoute({
|
|
projectPolicy,
|
|
async secretAdministrationForCredential(fence) {
|
|
calls.push(['credential-fence', fence]);
|
|
return {
|
|
async resolveLocalSecretAdministrationMutation() {
|
|
return null;
|
|
},
|
|
async appendAuthorizedLocalSecretEnvelope(command) {
|
|
calls.push(['mutation', command]);
|
|
return {
|
|
status: 'inserted',
|
|
envelope: command.envelope,
|
|
audit: command.audit,
|
|
};
|
|
},
|
|
async record() {
|
|
throw new Error('not used');
|
|
},
|
|
};
|
|
},
|
|
securityAudit: {
|
|
async record(record) {
|
|
calls.push(['audit', record]);
|
|
},
|
|
},
|
|
secretKeys: {
|
|
async active() {
|
|
calls.push(['active-key']);
|
|
return { keyId: 'active-key', key: Buffer.alloc(32, 23) };
|
|
},
|
|
async resolve() {
|
|
return null;
|
|
},
|
|
},
|
|
presenceProof,
|
|
now: () => 10_000,
|
|
randomUuid: uuidFactory(),
|
|
});
|
|
const authenticated = Object.freeze({
|
|
principal: PRINCIPAL,
|
|
credentialFence: FENCE,
|
|
async confirm() {
|
|
calls.push(['confirm']);
|
|
},
|
|
});
|
|
return { route, calls, deploymentRoot, authenticated };
|
|
}
|
|
|
|
function request(state, requestBody, overrides = {}) {
|
|
return Object.freeze({
|
|
requestId: 'local:019f9200-0000-4000-8000-000000000301',
|
|
projectId: 'default',
|
|
body: requestBody,
|
|
presence: null,
|
|
authenticated: state.authenticated,
|
|
signal: new AbortController().signal,
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
function readProof(state, challenge) {
|
|
return JSON.parse(
|
|
fs.readFileSync(
|
|
path.join(
|
|
state.deploymentRoot,
|
|
'console-presence',
|
|
challenge.body.proofFileName,
|
|
),
|
|
'utf8',
|
|
),
|
|
).proof;
|
|
}
|
|
|
|
test('lists bounded Secret metadata without storage or mutation material', async () => {
|
|
const route = createLocalApiSecretListRoute({
|
|
async listLocalSecretMetadata(options) {
|
|
assert.deepEqual(options, { projectId: 'default', limit: 1 });
|
|
return {
|
|
secrets: [
|
|
{
|
|
projectId: 'default',
|
|
name: 'github-token',
|
|
currentVersion: 2,
|
|
createdAtMs: 10_000,
|
|
},
|
|
],
|
|
truncated: true,
|
|
next: { name: 'github-token' },
|
|
};
|
|
},
|
|
});
|
|
const result = await route.handle({ projectId: 'default', limit: 1 });
|
|
assert.equal(result.statusCode, 200);
|
|
assert.deepEqual(Object.keys(result.body.secrets[0]).sort(), [
|
|
'createdAtMs',
|
|
'currentVersion',
|
|
'name',
|
|
'secretRef',
|
|
]);
|
|
assert.equal(
|
|
result.body.secrets[0].secretRef.startsWith('qlsecret:v1:'),
|
|
true,
|
|
);
|
|
assert.equal(result.body.next.after, 'Z2l0aHViLXRva2Vu');
|
|
assert.doesNotMatch(
|
|
JSON.stringify(result.body),
|
|
/cipher|plaintext|mutation|keyId/u,
|
|
);
|
|
});
|
|
|
|
test('fails closed on widened, cross-Project and over-budget metadata', async () => {
|
|
for (const page of [
|
|
{
|
|
secrets: [
|
|
{
|
|
projectId: 'other',
|
|
name: 'github-token',
|
|
currentVersion: 2,
|
|
createdAtMs: 10_000,
|
|
},
|
|
],
|
|
truncated: false,
|
|
},
|
|
{
|
|
secrets: [
|
|
{
|
|
projectId: 'default',
|
|
name: 'github-token',
|
|
currentVersion: 2,
|
|
createdAtMs: 10_000,
|
|
ciphertext: 'forbidden',
|
|
},
|
|
],
|
|
truncated: false,
|
|
},
|
|
{
|
|
secrets: [
|
|
{
|
|
projectId: 'default',
|
|
name: 'first',
|
|
currentVersion: 1,
|
|
createdAtMs: 10_000,
|
|
},
|
|
{
|
|
projectId: 'default',
|
|
name: 'second',
|
|
currentVersion: 1,
|
|
createdAtMs: 10_001,
|
|
},
|
|
],
|
|
truncated: true,
|
|
next: { name: 'second' },
|
|
},
|
|
]) {
|
|
const route = createLocalApiSecretListRoute({
|
|
async listLocalSecretMetadata() {
|
|
return page;
|
|
},
|
|
});
|
|
assert.deepEqual(await route.handle({ projectId: 'default', limit: 1 }), {
|
|
statusCode: 503,
|
|
body: { code: 'secret_query_unavailable' },
|
|
});
|
|
}
|
|
});
|
|
|
|
test('requires exact local presence and returns no Secret plaintext', async (t) => {
|
|
const state = fixture(t);
|
|
const command = body();
|
|
const challenge = await state.route.handle(request(state, command));
|
|
assert.equal(challenge.statusCode, 428);
|
|
const result = await state.route.handle(
|
|
request(state, command, { presence: readProof(state, challenge) }),
|
|
);
|
|
assert.equal(result.statusCode, 201);
|
|
assert.deepEqual(result.body, {
|
|
status: 'inserted',
|
|
secret: {
|
|
name: 'github-token',
|
|
currentVersion: 1,
|
|
secretRef: result.body.secret.secretRef,
|
|
},
|
|
});
|
|
assert.equal(JSON.stringify(result).includes(command.plaintext), false);
|
|
const mutation = state.calls.find(([kind]) => kind === 'mutation')[1];
|
|
assert.equal(
|
|
Buffer.from(mutation.envelope.ciphertext, 'base64url').includes(
|
|
Buffer.from(command.plaintext),
|
|
),
|
|
false,
|
|
);
|
|
assert.deepEqual(
|
|
state.calls
|
|
.filter(([kind]) => kind === 'audit')
|
|
.map(([, audit]) => [audit.operationId, audit.outcome, audit.reasons[0]]),
|
|
[['secret.create', 'approval_required', 'local_presence_required']],
|
|
);
|
|
});
|
|
|
|
test('binds proof to exact plaintext digest and rejects widened bodies', async (t) => {
|
|
const state = fixture(t);
|
|
assert.deepEqual(
|
|
await state.route.handle(request(state, { ...body(), extra: true })),
|
|
{ statusCode: 400, body: { code: 'invalid_secret' } },
|
|
);
|
|
const command = body();
|
|
const challenge = await state.route.handle(request(state, command));
|
|
const proof = readProof(state, challenge);
|
|
assert.deepEqual(
|
|
await state.route.handle(
|
|
request(state, body({ plaintext: 'changed-value' }), { presence: proof }),
|
|
),
|
|
{ statusCode: 401, body: { code: 'local_presence_rejected' } },
|
|
);
|
|
assert.equal(state.calls.filter(([kind]) => kind === 'mutation').length, 0);
|
|
});
|