mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 00:38:14 +08:00
205 lines
6.2 KiB
JavaScript
205 lines
6.2 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { generateKeyPairSync } = require('node:crypto');
|
|
const { test } = require('node:test');
|
|
|
|
const {
|
|
createPluginPackagePublisherRevocationProposal,
|
|
PluginPackagePublisherRevocationProposalConflictError,
|
|
} = require('@qinglong/runtime-core/plugin-package-publisher-revocation-proposal');
|
|
const {
|
|
createPluginPackagePublisherTrustSnapshot,
|
|
} = require('@qinglong/runtime-core/plugin-package-publisher-trust');
|
|
const {
|
|
PostgresPluginPackagePublisherRevocationProposalRepository,
|
|
} = require('../dist/plugin-package/publisher/pluginPackagePublisherRevocationProposalRepository');
|
|
|
|
const SUBJECT = Object.freeze({ type: 'user', id: 'usr_owner' });
|
|
const FENCE = Object.freeze({ projectVersion: 4, bindingVersion: 7 });
|
|
|
|
function proposal() {
|
|
const { publicKey } = generateKeyPairSync('ed25519');
|
|
const trustSnapshot = createPluginPackagePublisherTrustSnapshot([
|
|
{
|
|
publisher: 'publisher-a.example',
|
|
keyId: 'key-a',
|
|
publicKeyPem: publicKey.export({ type: 'spki', format: 'pem' }),
|
|
notBeforeMs: 1,
|
|
notAfterMs: 10_000,
|
|
},
|
|
]);
|
|
return createPluginPackagePublisherRevocationProposal({
|
|
actionRef: 'publisher-revoke:publisher-a.example:key-a',
|
|
authorityProjectId: 'cluster-trust-authority',
|
|
trustAuthorityId: 'cluster',
|
|
trustGeneration: 1,
|
|
trustSnapshot,
|
|
publisher: 'publisher-a.example',
|
|
keyId: 'key-a',
|
|
authorizationMode: 'dual_control',
|
|
reasonCode: 'suspected_key_compromise',
|
|
proposedBy: SUBJECT,
|
|
proposerAssurance: 'multi_factor',
|
|
proposalFence: FENCE,
|
|
createdAtMs: 100,
|
|
});
|
|
}
|
|
|
|
function audit(candidate, overrides = {}) {
|
|
return {
|
|
eventId: '32000000-0000-4000-8000-000000000001',
|
|
requestId: candidate.actionRef,
|
|
operationId: 'plugin_package.publisher_revocation.propose',
|
|
projectId: candidate.projectId,
|
|
subject: candidate.proposedBy,
|
|
authenticationId: 'auth-owner',
|
|
outcome: 'allowed',
|
|
reasons: ['publisher_revocation_proposal'],
|
|
fence: candidate.proposalFence,
|
|
occurredAtMs: candidate.createdAtMs,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function fixture(candidate, trustOverrides = {}) {
|
|
let storedProposal = null;
|
|
let storedAudit = null;
|
|
let signerLocks = 0;
|
|
const query = async (text, values = []) => {
|
|
if (text.includes('pg_advisory_xact_lock(hashtextextended')) {
|
|
signerLocks += 1;
|
|
assert.deepEqual(values, [
|
|
JSON.stringify([
|
|
candidate.actionInput.publisher,
|
|
candidate.actionInput.keyId,
|
|
]),
|
|
774635229,
|
|
]);
|
|
return { rows: [{}], rowCount: 1 };
|
|
}
|
|
if (
|
|
text.includes(
|
|
'FROM "ql3"."plugin_package_publisher_revocation_proposals"',
|
|
)
|
|
) {
|
|
return {
|
|
rows: storedProposal
|
|
? [
|
|
{
|
|
proposalJson: storedProposal,
|
|
proposalDigest: storedProposal.proposalDigest,
|
|
},
|
|
]
|
|
: [],
|
|
};
|
|
}
|
|
if (
|
|
text.includes(
|
|
'FROM "ql3"."plugin_package_publisher_trust_heads"',
|
|
)
|
|
) {
|
|
return {
|
|
rows: [
|
|
{
|
|
generation:
|
|
trustOverrides.generation ??
|
|
candidate.actionInput.trustGeneration,
|
|
effectiveTrustDigest:
|
|
trustOverrides.effectiveTrustDigest ??
|
|
candidate.actionInput.previousTrustDigest,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
if (text.includes('lock_approval_policy_fence')) {
|
|
return { rows: [{ matches: true }] };
|
|
}
|
|
if (
|
|
text.includes(
|
|
'INSERT INTO\n "ql3"."plugin_package_publisher_revocation_proposals"',
|
|
)
|
|
) {
|
|
storedProposal = JSON.parse(values[20]);
|
|
return { rows: [], rowCount: 1 };
|
|
}
|
|
if (text.includes('INSERT INTO "ql3"."security_audit_events"')) {
|
|
storedAudit = audit(candidate);
|
|
return { rows: [], rowCount: 1 };
|
|
}
|
|
if (text.includes('FROM "ql3"."security_audit_events"')) {
|
|
return {
|
|
rows: storedAudit
|
|
? [
|
|
{
|
|
eventId: storedAudit.eventId,
|
|
requestId: storedAudit.requestId,
|
|
operationId: storedAudit.operationId,
|
|
projectId: storedAudit.projectId,
|
|
subjectType: storedAudit.subject.type,
|
|
subjectId: storedAudit.subject.id,
|
|
authenticationId: storedAudit.authenticationId,
|
|
outcome: storedAudit.outcome,
|
|
reasons: storedAudit.reasons,
|
|
projectVersion: storedAudit.fence.projectVersion,
|
|
bindingVersion: storedAudit.fence.bindingVersion,
|
|
occurredAtMs: storedAudit.occurredAtMs,
|
|
},
|
|
]
|
|
: [],
|
|
};
|
|
}
|
|
return { rows: [], rowCount: 0 };
|
|
};
|
|
const client = { query, release() {} };
|
|
const repository =
|
|
new PostgresPluginPackagePublisherRevocationProposalRepository({
|
|
query,
|
|
async connect() {
|
|
return client;
|
|
},
|
|
});
|
|
repository.signerLocks = () => signerLocks;
|
|
return repository;
|
|
}
|
|
|
|
test('persists and exactly replays a generation-fenced revocation proposal', async () => {
|
|
const candidate = proposal();
|
|
const repository = fixture(candidate);
|
|
const created = await repository.createProposal({
|
|
proposal: candidate,
|
|
audit: audit(candidate),
|
|
});
|
|
assert.equal(created.status, 'created');
|
|
assert.equal(repository.signerLocks(), 1);
|
|
const replay = await repository.createProposal({
|
|
proposal: candidate,
|
|
audit: audit(candidate),
|
|
});
|
|
assert.equal(replay.status, 'existing');
|
|
assert.equal(repository.signerLocks(), 1);
|
|
assert.deepEqual(
|
|
await repository.findProposalByActionRef(candidate.actionRef),
|
|
candidate,
|
|
);
|
|
});
|
|
|
|
test('rejects stale trust generations and mismatched audit authority', async () => {
|
|
const candidate = proposal();
|
|
await assert.rejects(
|
|
fixture(candidate, { generation: 2 }).createProposal({
|
|
proposal: candidate,
|
|
audit: audit(candidate),
|
|
}),
|
|
PluginPackagePublisherRevocationProposalConflictError,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
fixture(candidate).createProposal({
|
|
proposal: candidate,
|
|
audit: audit(candidate, {
|
|
reasons: ['client_supplied_transition'],
|
|
}),
|
|
}),
|
|
PluginPackagePublisherRevocationProposalConflictError,
|
|
);
|
|
});
|