Files
qinglong/packages/ql3-cluster-admin/test/workerCredentialFileDelivery.test.cjs

339 lines
12 KiB
JavaScript

const assert = require('node:assert/strict');
const { randomUUID } = require('node:crypto');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const {
WorkerCredentialDeliveryConflictError,
WorkerCredentialDeliveryUnavailableError,
workerCredentialDeliveryTokenDigest,
} = require('@qinglong/runtime-core/worker-credential-delivery');
const {
formatWorkerCredentialToken,
} = require('@qinglong/runtime-core/worker-credential-token');
const {
MAX_WORKER_CREDENTIAL_FILE_STAGES,
WorkerCredentialFileDeliveryAdapter,
} = require('@qinglong/cluster-admin/worker-credential-file-delivery');
const {
createRecoverableWorkerCredentialIssuer,
} = require('../dist/worker-credential/workerCredentialDelivery');
const DELIVERY_ID = '123e4567-e89b-42d3-a456-426614174901';
const CREDENTIAL_ID = 'worker_generation_2';
const PREVIOUS_CREDENTIAL_ID = 'worker_generation_1';
const PEPPER = Buffer.alloc(32, 7).toString('base64url');
function fixture(t) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-worker-delivery-'));
const stages = path.join(root, 'stages');
const target = path.join(root, 'target');
fs.mkdirSync(stages, { mode: 0o700 });
fs.mkdirSync(target, { mode: 0o700 });
const targetTokenFile = path.join(target, 'credential.token');
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const adapter = new WorkerCredentialFileDeliveryAdapter({
stageDirectory: stages,
targetTokenFile,
});
return { root, stages, target, targetTokenFile, adapter };
}
function token(credentialId, fill) {
return Buffer.from(formatWorkerCredentialToken(
credentialId,
Buffer.alloc(32, fill).toString('base64url'),
));
}
function intent(adapter, value, overrides = {}) {
return {
deliveryId: DELIVERY_ID,
workerId: 'edge-router-1',
credentialId: CREDENTIAL_ID,
credentialVersion: 1,
previousCredentialId: PREVIOUS_CREDENTIAL_ID,
secretDigest: 'a'.repeat(64),
tokenDigest: workerCredentialDeliveryTokenDigest(value),
deploymentTargetDigest: adapter.deploymentTargetDigest,
deploymentGeneration: 'secret-generation-2',
stagedAtMs: 1_000,
...overrides,
};
}
function committed(candidate, overrides = {}) {
return {
...candidate,
version: 1,
state: 'credential_committed',
credentialCommittedAtMs: 1_000,
publishedAtMs: null,
publicationDigest: null,
observedAtMs: null,
observedSessionId: null,
observedSessionVersion: null,
previousRevokedAtMs: null,
...overrides,
};
}
function deliveryAuthority() {
let resolved = null;
return {
port: {
async resolveMutation() { return resolved; },
async append() { throw new Error('raw append is forbidden'); },
async resolveDelivery() { return resolved?.delivery ?? null; },
async resolveDelivered() { return resolved; },
async commitDelivered(command) {
resolved = {
credential: command.credential.credential,
mutation: command.credential.mutation,
audit: command.credential.audit,
delivery: command.delivery,
};
return {
status: 'created',
credential: resolved.credential,
mutation: resolved.mutation,
};
},
async markPublished(command) {
resolved = {
...resolved,
delivery: {
...resolved.delivery,
version: 2,
state: 'published',
publishedAtMs: command.publishedAtMs,
publicationDigest: command.publicationDigest,
},
};
return resolved.delivery;
},
async listRecoveryPage() { throw new Error('not used'); },
async revokePreviousDelivered() { throw new Error('not used'); },
async authorizeStageDiscard() { throw new Error('not used'); },
async markStageDiscarded() { throw new Error('not used'); },
async listStageDiscardRecoveryPage() { throw new Error('not used'); },
},
current: () => resolved,
};
}
test('stages one private no-replace secret and inspects only its intent', async (t) => {
const { adapter, stages } = fixture(t);
const material = token(CREDENTIAL_ID, 2);
const candidate = intent(adapter, material);
await adapter.stage(candidate, material);
material.fill(0);
assert.deepEqual(await adapter.inspect(DELIVERY_ID), candidate);
const stagePath = path.join(stages, `${DELIVERY_ID}.stage`);
assert.equal(fs.statSync(stagePath).mode & 0o777, 0o600);
assert.deepEqual(fs.readdirSync(stages), [`${DELIVERY_ID}.stage`]);
const replay = token(CREDENTIAL_ID, 2);
await adapter.stage(candidate, replay);
await assert.rejects(
adapter.stage({ ...candidate, deploymentGeneration: 'other-generation' }, replay),
WorkerCredentialDeliveryConflictError,
);
replay.fill(0);
assert.equal((await adapter.inspect(DELIVERY_ID)).deploymentGeneration,
'secret-generation-2');
});
test('atomically replaces only the expected previous credential and replays publication', async (t) => {
const { adapter, targetTokenFile, stages, target } = fixture(t);
const previous = token(PREVIOUS_CREDENTIAL_ID, 1);
fs.writeFileSync(targetTokenFile, previous, { mode: 0o600 });
previous.fill(0);
const material = token(CREDENTIAL_ID, 2);
const candidate = intent(adapter, material);
await adapter.stage(candidate, material);
const publication = await adapter.publish(committed(candidate));
assert.match(publication.publicationDigest, /^[0-9a-f]{64}$/);
assert.deepEqual(fs.readFileSync(targetTokenFile), material);
assert.equal(fs.statSync(targetTokenFile).mode & 0o777, 0o600);
assert.deepEqual(fs.readdirSync(target), ['credential.token']);
assert.deepEqual(fs.readdirSync(stages), [`${DELIVERY_ID}.stage`]);
const restarted = new WorkerCredentialFileDeliveryAdapter({
stageDirectory: stages,
targetTokenFile,
});
assert.deepEqual(
await restarted.publish(committed(candidate)),
publication,
);
material.fill(0);
});
test('completes stage-before-commit issuance through the concrete file adapter', async (t) => {
const { adapter, targetTokenFile } = fixture(t);
const authority = deliveryAuthority();
const generated = Buffer.alloc(32, 9);
const service = createRecoverableWorkerCredentialIssuer(
authority.port,
adapter,
PEPPER,
{ now: () => 1_000, randomBytes: () => generated },
);
const result = await service.issue({
mutationId: DELIVERY_ID,
requestId: 'request-file-delivery-1',
expectedCurrentVersion: 0,
credentialId: CREDENTIAL_ID,
workerId: 'edge-router-1',
principal: {
subject: { type: 'user', id: 'usr_admin' },
authenticationId: 'session:admin:1',
authenticatedAtMs: 900,
expiresAtMs: 2_000,
assurance: 'multi_factor',
},
notBeforeAtMs: 1_000,
expiresAtMs: 2_000,
previousCredentialId: null,
deploymentTargetDigest: adapter.deploymentTargetDigest,
deploymentGeneration: 'secret-generation-2',
});
assert.equal(result.status, 'published');
assert.equal(result.delivery.state, 'published');
assert.match(fs.readFileSync(targetTokenFile, 'ascii'),
/^ql3w_worker_generation_2_[A-Za-z0-9_-]{43}$/);
assert.equal(JSON.stringify(authority.current()).includes('ql3w_'), false);
assert.equal(generated.every((byte) => byte === 0), true);
});
test('fails closed instead of overwriting an unexpected target generation', async (t) => {
const { adapter, targetTokenFile } = fixture(t);
const unexpected = token('worker_generation_other', 3);
fs.writeFileSync(targetTokenFile, unexpected, { mode: 0o600 });
const material = token(CREDENTIAL_ID, 2);
const candidate = intent(adapter, material);
await adapter.stage(candidate, material);
await assert.rejects(
adapter.publish(committed(candidate)),
WorkerCredentialDeliveryConflictError,
);
assert.deepEqual(fs.readFileSync(targetTokenFile), unexpected);
unexpected.fill(0);
material.fill(0);
});
test('discards only an unpublished exact orphan stage', async (t) => {
const { adapter, targetTokenFile } = fixture(t);
const material = token(CREDENTIAL_ID, 2);
const candidate = intent(adapter, material);
await adapter.stage(candidate, material);
await adapter.discard(candidate);
assert.equal(await adapter.inspect(DELIVERY_ID), null);
await adapter.discard(candidate);
await adapter.stage(candidate, material);
fs.writeFileSync(targetTokenFile, material, { mode: 0o600 });
await assert.rejects(
adapter.discard(candidate),
WorkerCredentialDeliveryConflictError,
);
assert.deepEqual(await adapter.inspect(DELIVERY_ID), candidate);
material.fill(0);
});
test('uses a durable target lock and leaves the old token untouched on contention', async (t) => {
const { adapter, target, targetTokenFile } = fixture(t);
const previous = token(PREVIOUS_CREDENTIAL_ID, 1);
fs.writeFileSync(targetTokenFile, previous, { mode: 0o600 });
fs.writeFileSync(
path.join(target, '.ql3-worker-credential-delivery.lock'),
`${JSON.stringify({ deliveryId: randomUUID() })}\n`,
{ mode: 0o600 },
);
const material = token(CREDENTIAL_ID, 2);
const candidate = intent(adapter, material);
await adapter.stage(candidate, material);
await assert.rejects(
adapter.publish(committed(candidate)),
WorkerCredentialDeliveryUnavailableError,
);
assert.deepEqual(fs.readFileSync(targetTokenFile), previous);
previous.fill(0);
material.fill(0);
});
test('lists only bounded ordered low-sensitive stage intents', async (t) => {
const { adapter, stages } = fixture(t);
const firstToken = token(CREDENTIAL_ID, 2);
const secondToken = token(CREDENTIAL_ID, 3);
const first = intent(adapter, firstToken);
const second = intent(adapter, secondToken, {
deliveryId: '123e4567-e89b-42d3-a456-426614174902',
tokenDigest: workerCredentialDeliveryTokenDigest(secondToken),
deploymentGeneration: 'secret-generation-3',
});
await adapter.stage(second, secondToken);
await adapter.stage(first, firstToken);
assert.deepEqual(await adapter.listStaged({ limit: 1 }), {
stages: [first],
truncated: true,
nextCursor: first.deliveryId,
});
assert.deepEqual(await adapter.listStaged({
afterDeliveryId: first.deliveryId,
limit: 1,
}), {
stages: [second],
truncated: false,
});
fs.writeFileSync(
path.join(stages, `.${DELIVERY_ID}.${randomUUID()}.tmp`),
'uncertain',
{ mode: 0o600 },
);
await assert.rejects(
adapter.listStaged(),
WorkerCredentialDeliveryUnavailableError,
);
firstToken.fill(0);
secondToken.fill(0);
});
test('enforces stage capacity, dedicated roots and live POSIX permissions', async (t) => {
const { adapter, stages, target, targetTokenFile } = fixture(t);
const material = token(CREDENTIAL_ID, 2);
const candidate = intent(adapter, material);
for (let index = 0; index < MAX_WORKER_CREDENTIAL_FILE_STAGES; index += 1) {
fs.writeFileSync(
path.join(stages, `${randomUUID()}.stage`),
'bounded',
{ mode: 0o600 },
);
}
await assert.rejects(
adapter.stage(candidate, material),
WorkerCredentialDeliveryUnavailableError,
);
fs.rmSync(stages, { recursive: true });
fs.mkdirSync(stages, { mode: 0o700 });
const changed = new WorkerCredentialFileDeliveryAdapter({
stageDirectory: stages,
targetTokenFile,
});
const changedCandidate = intent(changed, material);
await changed.stage(changedCandidate, material);
fs.chmodSync(target, 0o755);
await assert.rejects(
changed.publish(committed(changedCandidate)),
WorkerCredentialDeliveryUnavailableError,
);
fs.chmodSync(target, 0o700);
assert.throws(() => new WorkerCredentialFileDeliveryAdapter({
stageDirectory: target,
targetTokenFile,
}));
material.fill(0);
});