mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 00:38:14 +08:00
426 lines
13 KiB
JavaScript
426 lines
13 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const crypto = require('node:crypto');
|
|
const { spawnSync } = require('node:child_process');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { afterEach, test } = require('node:test');
|
|
|
|
const {
|
|
WorkerCredentialManagementDurableAuditEvidenceError,
|
|
databaseSql,
|
|
parseArguments,
|
|
runWorkerCredentialManagementDurableAuditEvidence,
|
|
validateWorkerCredentialManagementDurableAuditEvidence,
|
|
} = require('../../scripts/ql3-worker-credential-management-durable-audit-evidence.cjs');
|
|
|
|
const NOW_MS = 1_700_000_000_000;
|
|
const temporaryDirectories = [];
|
|
|
|
function digest(domain, value) {
|
|
return `sha256:${crypto
|
|
.createHash('sha256')
|
|
.update(domain)
|
|
.update('\0')
|
|
.update(value)
|
|
.digest('hex')}`;
|
|
}
|
|
|
|
function ceremony() {
|
|
return {
|
|
schemaVersion: 1,
|
|
planRequest: {
|
|
actionRef: 'worker-credential:worker-a:generation-2',
|
|
authorityProjectId: 'cluster-authority',
|
|
action: 'rotate',
|
|
deliveryId: '123e4567-e89b-42d3-a456-426614174901',
|
|
workerId: 'worker-a',
|
|
credentialId: 'credential-generation-2',
|
|
previousCredentialId: 'credential-generation-1',
|
|
credentialNotBeforeAtMs: NOW_MS + 1_000,
|
|
credentialExpiresAtMs: NOW_MS + 10 * 60_000,
|
|
deploymentTargetDigest: 'd'.repeat(64),
|
|
deploymentGeneration: 'generation-2',
|
|
},
|
|
approvalRequestId: 'approval-worker-a-generation-2',
|
|
approvalAuditEventId: '123e4567-e89b-42d3-a456-426614174902',
|
|
requesterDecisionId: 'requester-self-decision',
|
|
requesterDecisionAuditEventId: '123e4567-e89b-42d3-a456-426614174903',
|
|
reviewerDecisionId: 'reviewer-decision',
|
|
reviewerDecisionAuditEventId: '123e4567-e89b-42d3-a456-426614174904',
|
|
decisionReasonCode: 'reviewed',
|
|
inspectionId: 'inspection-worker-a-generation-2',
|
|
};
|
|
}
|
|
|
|
function ceremonyReport() {
|
|
const value = ceremony();
|
|
return {
|
|
schemaVersion: 1,
|
|
fixture: 'qinglong/worker-credential-management-live-ceremony@v1',
|
|
observedAt: new Date(NOW_MS).toISOString(),
|
|
identity: {
|
|
providerKind: 'external_oidc',
|
|
issuer: 'https://identity.production.example.org/',
|
|
discoveryDocumentSha256: `sha256:${'c'.repeat(64)}`,
|
|
jwksSha256: `sha256:${'d'.repeat(64)}`,
|
|
audience: 'qinglong3-worker-credential-management',
|
|
type: 'ql3-worker-credential-management+jwt',
|
|
purpose: 'worker-credential-management',
|
|
requesterSubjectSha256: digest(
|
|
'qinglong3.worker-management.subject.v1',
|
|
'operator-a',
|
|
),
|
|
reviewerSubjectSha256: digest(
|
|
'qinglong3.worker-management.subject.v1',
|
|
'reviewer-b',
|
|
),
|
|
requesterKeyIdSha256: `sha256:${'1'.repeat(64)}`,
|
|
reviewerKeyIdSha256: `sha256:${'2'.repeat(64)}`,
|
|
},
|
|
ceremony: {
|
|
actionRefSha256: digest(
|
|
'qinglong3.worker-management.action-ref.v1',
|
|
value.planRequest.actionRef,
|
|
),
|
|
authorityProjectIdSha256: digest(
|
|
'qinglong3.worker-management.project.v1',
|
|
value.planRequest.authorityProjectId,
|
|
),
|
|
planStatus: 'created',
|
|
approvalStatus: 'created',
|
|
requesterSelfDecisionStatus: 403,
|
|
requesterSelfDecisionCode: 'forbidden',
|
|
reviewerDecisionStatus: 'decided',
|
|
approvalState: 'approved',
|
|
inspectionStale: false,
|
|
dispatchCreated: false,
|
|
approvalConsumed: false,
|
|
planDigest: 'a'.repeat(64),
|
|
previewDigest: 'b'.repeat(64),
|
|
requestIdSha256: [1, 2, 3, 4, 5].map(
|
|
(value) => `sha256:${String(value).repeat(64)}`,
|
|
),
|
|
},
|
|
gates: {
|
|
externalIdentity: true,
|
|
workerPurposeBound: true,
|
|
requesterAndReviewerDistinct: true,
|
|
requesterSelfDecisionRejected: true,
|
|
reviewerDecisionAccepted: true,
|
|
inspectionAuthorized: true,
|
|
noExecutionOrConsumption: true,
|
|
passed: true,
|
|
},
|
|
};
|
|
}
|
|
|
|
function snapshot(overrides = {}) {
|
|
const value = ceremony();
|
|
const base = {
|
|
serverVersionNumber: 180004,
|
|
transactionReadOnly: true,
|
|
role: {
|
|
name: 'ql3_worker_management_evidence',
|
|
canLogin: true,
|
|
superuser: false,
|
|
createDatabase: false,
|
|
createRole: false,
|
|
replication: false,
|
|
bypassRls: false,
|
|
schemaUsage: true,
|
|
selectableTables: [
|
|
'approval_requests',
|
|
'security_audit_events',
|
|
'worker_credential_management_plans',
|
|
],
|
|
writableTables: [],
|
|
privilegedMemberships: [],
|
|
},
|
|
plan: {
|
|
action_ref: value.planRequest.actionRef,
|
|
authority_project_id: value.planRequest.authorityProjectId,
|
|
action: value.planRequest.action,
|
|
plan_digest: 'a'.repeat(64),
|
|
preview_digest: 'b'.repeat(64),
|
|
requested_by_type: 'user',
|
|
requested_by_id: 'operator-a',
|
|
planned_at_ms: NOW_MS,
|
|
},
|
|
approval: {
|
|
request_id: value.approvalRequestId,
|
|
project_id: value.planRequest.authorityProjectId,
|
|
version: 2,
|
|
state: 'approved',
|
|
action_type: 'worker_credential.delivery.rotate',
|
|
action_ref: value.planRequest.actionRef,
|
|
action_digest: 'a'.repeat(64),
|
|
preview_digest: 'b'.repeat(64),
|
|
requested_by_type: 'user',
|
|
requested_by_id: 'operator-a',
|
|
decision_id: value.reviewerDecisionId,
|
|
consumption_id: null,
|
|
dispatch_id: null,
|
|
decided_by_type: 'user',
|
|
decided_by_id: 'reviewer-b',
|
|
decision_reason_code: 'reviewed',
|
|
decision_authentication_id: 'authentication-reviewer',
|
|
},
|
|
audits: [
|
|
{
|
|
kind: 'proposal',
|
|
event_id: value.approvalAuditEventId,
|
|
request_id: value.approvalRequestId,
|
|
operation_id: 'approval.request',
|
|
project_id: value.planRequest.authorityProjectId,
|
|
subject_type: 'user',
|
|
subject_id: 'operator-a',
|
|
authentication_id: 'authentication-requester',
|
|
outcome: 'approval_required',
|
|
reasons: ['worker_credential_review'],
|
|
project_version: 1,
|
|
binding_version: 1,
|
|
occurred_at_ms: NOW_MS + 10,
|
|
},
|
|
{
|
|
kind: 'reviewer_decision',
|
|
event_id: value.reviewerDecisionAuditEventId,
|
|
request_id: value.approvalRequestId,
|
|
operation_id: 'approval.decide',
|
|
project_id: value.planRequest.authorityProjectId,
|
|
subject_type: 'user',
|
|
subject_id: 'reviewer-b',
|
|
authentication_id: 'authentication-reviewer',
|
|
outcome: 'allowed',
|
|
reasons: ['worker_credential_review'],
|
|
project_version: 1,
|
|
binding_version: 2,
|
|
occurred_at_ms: NOW_MS + 20,
|
|
},
|
|
],
|
|
requesterDecisionAuditRows: 0,
|
|
};
|
|
return { ...base, ...overrides };
|
|
}
|
|
|
|
function writePrivate(directory, name, value) {
|
|
const filePath = path.join(directory, name);
|
|
fs.writeFileSync(filePath, `${JSON.stringify(value)}\n`, { mode: 0o600 });
|
|
return filePath;
|
|
}
|
|
|
|
function fixture() {
|
|
const directory = fs.realpathSync(
|
|
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-worker-audit-evidence-test-')),
|
|
);
|
|
fs.chmodSync(directory, 0o700);
|
|
temporaryDirectories.push(directory);
|
|
return {
|
|
ceremonyReportFile: writePrivate(
|
|
directory,
|
|
'ceremony-report.json',
|
|
ceremonyReport(),
|
|
),
|
|
ceremonyFile: writePrivate(directory, 'ceremony.json', ceremony()),
|
|
pgServiceFile: writePrivate(directory, 'pg_service.conf', {
|
|
placeholder: true,
|
|
}),
|
|
pgService: 'ql3_worker_management_evidence',
|
|
outputFile: path.join(directory, 'durable-evidence.json'),
|
|
};
|
|
}
|
|
|
|
function dependencies(value = snapshot()) {
|
|
return {
|
|
now: () => NOW_MS + 30,
|
|
runPsql(serviceFile, args, sql) {
|
|
assert.match(serviceFile, /pg_service\.conf$/);
|
|
assert.deepEqual(args, [
|
|
'--no-psqlrc',
|
|
'--quiet',
|
|
'--no-align',
|
|
'--tuples-only',
|
|
'--set=ON_ERROR_STOP=1',
|
|
'--dbname=service=ql3_worker_management_evidence',
|
|
'--file=-',
|
|
]);
|
|
assert.match(sql, /^BEGIN TRANSACTION READ ONLY;/);
|
|
assert.match(sql, /current_setting\('transaction_read_only'\)/);
|
|
assert.match(sql, /requesterDecisionAuditRows/);
|
|
assert.doesNotMatch(sql, /worker-credential\.execute/);
|
|
return { status: 0, stdout: `${JSON.stringify(value)}\n`, stderr: '' };
|
|
},
|
|
};
|
|
}
|
|
|
|
afterEach(() => {
|
|
for (const directory of temporaryDirectories.splice(0)) {
|
|
fs.rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('collects two durable audit rows through an exact read-only role', () => {
|
|
const paths = fixture();
|
|
const report = runWorkerCredentialManagementDurableAuditEvidence(
|
|
paths,
|
|
dependencies(),
|
|
);
|
|
|
|
assert.equal(report.gates.passed, true);
|
|
assert.equal(report.database.transactionReadOnly, true);
|
|
assert.equal(report.durableState.auditRows.length, 2);
|
|
assert.equal(report.durableState.requesterSelfDecisionAuditAbsent, true);
|
|
assert.equal(report.durableState.dispatchCreated, false);
|
|
assert.equal(report.durableState.approvalConsumed, false);
|
|
assert.equal(
|
|
validateWorkerCredentialManagementDurableAuditEvidence(report).compatible,
|
|
true,
|
|
);
|
|
const serialized = JSON.stringify(report);
|
|
assert.doesNotMatch(
|
|
serialized,
|
|
/operator-a|reviewer-b|cluster-authority|approval-worker|authentication-|postgresql:\/\//,
|
|
);
|
|
assert.equal(fs.statSync(paths.outputFile).mode & 0o777, 0o600);
|
|
|
|
const audit = spawnSync(
|
|
process.execPath,
|
|
[
|
|
path.resolve(
|
|
__dirname,
|
|
'../../scripts/ql3-worker-credential-management-durable-audit-evidence-audit.cjs',
|
|
),
|
|
`--report=${paths.outputFile}`,
|
|
],
|
|
{ encoding: 'utf8' },
|
|
);
|
|
assert.equal(audit.status, 0, audit.stderr);
|
|
assert.equal(JSON.parse(audit.stdout).compatible, true);
|
|
});
|
|
|
|
test('fails closed for write authority, privileged membership and extra table reads', () => {
|
|
for (const role of [
|
|
{ writableTables: ['approval_requests'] },
|
|
{ privilegedMemberships: ['ql3_worker_credential_manager'] },
|
|
{
|
|
selectableTables: [
|
|
'approval_requests',
|
|
'security_audit_events',
|
|
'worker_credential_management_plans',
|
|
'worker_credentials',
|
|
],
|
|
},
|
|
]) {
|
|
const paths = fixture();
|
|
assert.throws(
|
|
() =>
|
|
runWorkerCredentialManagementDurableAuditEvidence(
|
|
paths,
|
|
dependencies(snapshot({ role: { ...snapshot().role, ...role } })),
|
|
),
|
|
/exact read-only authority/,
|
|
);
|
|
assert.equal(fs.existsSync(paths.outputFile), false);
|
|
}
|
|
});
|
|
|
|
test('requires PostgreSQL 18.4 or later within the reviewed major', () => {
|
|
for (const serverVersionNumber of [180003, 190000]) {
|
|
const paths = fixture();
|
|
assert.throws(
|
|
() =>
|
|
runWorkerCredentialManagementDurableAuditEvidence(
|
|
paths,
|
|
dependencies(snapshot({ serverVersionNumber })),
|
|
),
|
|
WorkerCredentialManagementDurableAuditEvidenceError,
|
|
);
|
|
assert.equal(fs.existsSync(paths.outputFile), false);
|
|
}
|
|
});
|
|
|
|
test('fails closed when self-decision has an audit row or reviewed state drifts', () => {
|
|
for (const changed of [
|
|
{ requesterDecisionAuditRows: 1 },
|
|
{
|
|
approval: { ...snapshot().approval, dispatch_id: 'unexpected-dispatch' },
|
|
},
|
|
{ approval: { ...snapshot().approval, decided_by_id: 'operator-a' } },
|
|
{ audits: snapshot().audits.slice(0, 1) },
|
|
]) {
|
|
const paths = fixture();
|
|
assert.throws(
|
|
() =>
|
|
runWorkerCredentialManagementDurableAuditEvidence(
|
|
paths,
|
|
dependencies(snapshot(changed)),
|
|
),
|
|
WorkerCredentialManagementDurableAuditEvidenceError,
|
|
);
|
|
assert.equal(fs.existsSync(paths.outputFile), false);
|
|
}
|
|
});
|
|
|
|
test('binds private ceremony to the low-sensitive ceremony report', () => {
|
|
const paths = fixture();
|
|
const changed = ceremony();
|
|
changed.planRequest.actionRef = 'worker-credential:other:generation-2';
|
|
fs.writeFileSync(paths.ceremonyFile, `${JSON.stringify(changed)}\n`, {
|
|
mode: 0o600,
|
|
});
|
|
assert.throws(
|
|
() =>
|
|
runWorkerCredentialManagementDurableAuditEvidence(paths, dependencies()),
|
|
WorkerCredentialManagementDurableAuditEvidenceError,
|
|
);
|
|
assert.equal(fs.existsSync(paths.outputFile), false);
|
|
});
|
|
|
|
test('offline audit rejects widened, false-gate and sensitive reports', () => {
|
|
const paths = fixture();
|
|
const report = runWorkerCredentialManagementDurableAuditEvidence(
|
|
paths,
|
|
dependencies(),
|
|
);
|
|
for (const candidate of [
|
|
{ ...report, debug: true },
|
|
{ ...report, gates: { ...report.gates, passed: false } },
|
|
{ ...report, database: { ...report.database, password: 'forbidden' } },
|
|
]) {
|
|
assert.equal(
|
|
validateWorkerCredentialManagementDurableAuditEvidence(candidate)
|
|
.compatible,
|
|
false,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('uses path-only CLI arguments and SQL contains no domain mutation', () => {
|
|
assert.deepEqual(
|
|
parseArguments([
|
|
'--ceremony-report=/private/ceremony-report.json',
|
|
'--ceremony=/private/ceremony.json',
|
|
'--pg-service-file=/private/pg_service.conf',
|
|
'--pg-service=ql3_worker_management_evidence',
|
|
'--output=/private/durable-evidence.json',
|
|
]),
|
|
{
|
|
ceremonyReportFile: '/private/ceremony-report.json',
|
|
ceremonyFile: '/private/ceremony.json',
|
|
pgServiceFile: '/private/pg_service.conf',
|
|
pgService: 'ql3_worker_management_evidence',
|
|
outputFile: '/private/durable-evidence.json',
|
|
},
|
|
);
|
|
assert.throws(
|
|
() => parseArguments(['--pg-service=host=attacker']),
|
|
WorkerCredentialManagementDurableAuditEvidenceError,
|
|
);
|
|
const sql = databaseSql({ value: ceremony() });
|
|
assert.match(sql, /^BEGIN TRANSACTION READ ONLY;/);
|
|
assert.doesNotMatch(
|
|
sql,
|
|
/\b(?:INSERT\s+INTO|UPDATE\s+"?ql3|DELETE\s+FROM|TRUNCATE\s+TABLE|CALL\s|DO\s+\$)/i,
|
|
);
|
|
});
|