Files
qinglong/packages/ql3-cluster-admin/test/pluginPackagePublisherRevocationApprovedAction.test.cjs
T

234 lines
6.9 KiB
JavaScript

const assert = require('node:assert/strict');
const { generateKeyPairSync } = require('node:crypto');
const { test } = require('node:test');
const {
consumeApprovalRequest,
createApprovalRequest,
decideApprovalRequest,
} = require('@qinglong/runtime-core/approved-action');
const {
createApprovedActionExecution,
claimApprovedActionExecution,
startApprovedActionExecution,
} = require('@qinglong/runtime-core/approved-action-execution');
const {
createPluginPackagePublisherRevocationProposal,
} = require('@qinglong/runtime-core/plugin-package-publisher-revocation-proposal');
const {
createPluginPackagePublisherTrustSnapshot,
} = require('@qinglong/runtime-core/plugin-package-publisher-trust');
const {
ClusterPluginPackagePublisherRevocationApprovedActionHandler,
} = require('../dist/plugin-package/publisher/pluginPackagePublisherRevocationApprovedAction');
const REQUESTER = Object.freeze({ type: 'user', id: 'usr_owner' });
const REVIEWER = Object.freeze({ type: 'user', id: 'usr_security' });
const SYSTEM = Object.freeze({ type: 'system', id: 'cluster_package_executor' });
const FENCE = Object.freeze({ projectVersion: 4, bindingVersion: 7 });
function proposal() {
const { publicKey } = generateKeyPairSync('ed25519');
return createPluginPackagePublisherRevocationProposal({
actionRef: 'publisher-revoke:publisher-a.example:key-a',
authorityProjectId: 'cluster-trust-authority',
trustAuthorityId: 'cluster',
trustGeneration: 1,
trustSnapshot: createPluginPackagePublisherTrustSnapshot([
{
publisher: 'publisher-a.example',
keyId: 'key-a',
publicKeyPem: publicKey.export({ type: 'spki', format: 'pem' }),
notBeforeMs: 1,
notAfterMs: 10_000,
},
]),
publisher: 'publisher-a.example',
keyId: 'key-a',
authorizationMode: 'dual_control',
reasonCode: 'suspected_key_compromise',
proposedBy: REQUESTER,
proposerAssurance: 'multi_factor',
proposalFence: FENCE,
createdAtMs: 5,
});
}
function dispatch(candidate) {
const action = {
permission: candidate.permission,
actionType: candidate.actionType,
actionRef: candidate.actionRef,
actionDigest: candidate.actionDigest,
previewDigest: candidate.previewDigest,
};
const pending = createApprovalRequest({
id: 'approval-publisher-revoke',
projectId: candidate.projectId,
action,
risk: 'critical',
decisionMode: 'separation_of_duty',
requestedBy: REQUESTER,
requestedAtMs: 10,
expiresAtMs: 1_000,
requestFence: FENCE,
});
const approved = decideApprovalRequest(pending, {
expectedVersion: 1,
decisionId: 'decision-publisher-revoke',
decision: 'approved',
reasonCode: 'reviewed',
principal: {
subject: REVIEWER,
authenticationId: 'auth-reviewer',
authenticatedAtMs: 15,
expiresAtMs: 500,
assurance: 'multi_factor',
},
decidedAtMs: 20,
authorizationFence: FENCE,
});
return consumeApprovalRequest(approved, {
expectedVersion: 2,
consumptionId: 'consume-publisher-revoke',
dispatchId: 'dispatch-publisher-revoke',
action,
requestedBy: REQUESTER,
consumedBy: SYSTEM,
consumedAtMs: 30,
authorizationFence: FENCE,
}).dispatch;
}
function execution(approvedDispatch) {
const baseline = createApprovedActionExecution(approvedDispatch, 5);
const claimed = claimApprovedActionExecution(baseline, {
owner: 'publisher-executor',
leaseToken: 'lease-publisher-revoke',
nowMs: 31,
leaseDurationMs: 1_000,
});
assert.equal(claimed.status, 'leased');
return startApprovedActionExecution(
{ dispatch: approvedDispatch, execution: claimed },
{
dispatchId: approvedDispatch.id,
approvalRequestId: approvedDispatch.approvalRequestId,
actionDigest: approvedDispatch.action.actionDigest,
owner: 'publisher-executor',
leaseToken: 'lease-publisher-revoke',
expectedVersion: claimed.version,
startedAtMs: 40,
},
);
}
test('inspects authority and executes the exact approved revocation receipt', async () => {
const candidate = proposal();
const approvedDispatch = dispatch(candidate);
const started = execution(approvedDispatch);
const receipts = [];
const handler =
new ClusterPluginPackagePublisherRevocationApprovedActionHandler(
{
async findProposalByActionRef() {
return candidate;
},
async createProposal() {
throw new Error('must not create');
},
},
{
async run(receipt) {
receipts.push(receipt);
return {
safeToAdmit: true,
receiptDigest: receipt.receiptDigest,
impactDigest: 'a'.repeat(64),
};
},
},
);
assert.deepEqual(await handler.inspect(approvedDispatch), {
status: 'ready',
actionDigest: candidate.actionDigest,
});
const result = await handler.execute({
dispatch: approvedDispatch,
execution: started,
idempotencyKey: 'publisher-revoke-attempt',
fence: {
owner: started.leaseOwner,
leaseToken: started.leaseToken,
version: started.version,
},
});
assert.deepEqual(result, {
outcome: 'succeeded',
resultCode: 'publisher_revocation_converged',
resultDigest: 'a'.repeat(64),
});
assert.equal(receipts.length, 1);
assert.equal(receipts[0].mutationId, approvedDispatch.id);
assert.equal(receipts[0].revokedAtMs, started.startedAtMs);
});
test('blocks missing proposals and incomplete quarantine convergence', async () => {
const candidate = proposal();
const approvedDispatch = dispatch(candidate);
const missing =
new ClusterPluginPackagePublisherRevocationApprovedActionHandler(
{
async findProposalByActionRef() {
return null;
},
async createProposal() {
throw new Error('must not create');
},
},
{ async run() { throw new Error('must not run'); } },
);
assert.deepEqual(await missing.inspect(approvedDispatch), {
status: 'blocked',
resultCode: 'publisher_revocation_proposal_missing',
});
const started = execution(approvedDispatch);
const incomplete =
new ClusterPluginPackagePublisherRevocationApprovedActionHandler(
{
async findProposalByActionRef() {
return candidate;
},
async createProposal() {
throw new Error('must not create');
},
},
{
async run(receipt) {
return {
safeToAdmit: false,
receiptDigest: receipt.receiptDigest,
impactDigest: 'b'.repeat(64),
};
},
},
);
assert.deepEqual(
await incomplete.execute({
dispatch: approvedDispatch,
execution: started,
idempotencyKey: 'publisher-revoke-attempt',
fence: {
owner: started.leaseOwner,
leaseToken: started.leaseToken,
version: started.version,
},
}),
{
outcome: 'indeterminate',
resultCode: 'publisher_revocation_convergence_incomplete',
},
);
});