feat(ql3): resolve secret action recovery manually

This commit is contained in:
whyour
2026-08-14 12:11:50 +08:00
parent d8489d8a0b
commit 3ca9901055
29 changed files with 3143 additions and 50 deletions
@@ -1,11 +1,18 @@
import {
PostgresApprovalManagementIdentityKeysetLedgerRepository,
PostgresApprovedActionManualRecoveryRepository,
PostgresApprovalRequestRepository,
PostgresApprovalRequestSource,
PostgresProjectPolicyRepository,
PostgresSecurityAuditRepository,
} from '@qinglong/cluster-postgres/approval-manager';
import type { PostgresPool } from '@qinglong/runtime-core';
import {
createApprovedActionManualRecoveryService,
type ApprovedActionManualRecoveryInspectRequest,
type ApprovedActionManualRecoveryResolveRequest,
type ApprovedActionManualRecoveryService,
} from '@qinglong/runtime-core/approved-action-manual-recovery';
import {
createApprovalDecisionService,
type ApprovalDecisionRequest,
@@ -28,6 +35,14 @@ export interface ClusterApprovalManagementService {
request: ApprovalDecisionRequest,
confirmAuthorization: () => void | Promise<void>,
): ReturnType<ApprovalDecisionService['decide']>;
inspectRecovery(
request: ApprovedActionManualRecoveryInspectRequest,
confirmAuthorization: () => void | Promise<void>,
): ReturnType<ApprovedActionManualRecoveryService['inspect']>;
resolveRecovery(
request: ApprovedActionManualRecoveryResolveRequest,
confirmAuthorization: () => void | Promise<void>,
): ReturnType<ApprovedActionManualRecoveryService['resolve']>;
recordFailure(record: SecurityAuditRecord): Promise<void>;
}
@@ -69,6 +84,12 @@ export function createClusterApprovalManagementService(
const approvals = new PostgresApprovalRequestRepository(options.pool);
const source = new PostgresApprovalRequestSource(options.pool);
const audit = new PostgresSecurityAuditRepository(options.pool);
const recovery = createApprovedActionManualRecoveryService({
repository: new PostgresApprovedActionManualRecoveryRepository(options.pool),
policy,
audit,
...(options.now === undefined ? {} : { now: options.now }),
});
return Object.freeze({
inspect(
request: ApprovalInspectionRequest,
@@ -93,6 +114,18 @@ export function createClusterApprovalManagementService(
...(options.now === undefined ? {} : { now: options.now }),
}).decide(request);
},
inspectRecovery(
request: ApprovedActionManualRecoveryInspectRequest,
confirmAuthorization: () => void | Promise<void>,
) {
return recovery.inspect(request, confirmAuthorization);
},
resolveRecovery(
request: ApprovedActionManualRecoveryResolveRequest,
confirmAuthorization: () => void | Promise<void>,
) {
return recovery.resolve(request, confirmAuthorization);
},
recordFailure(record: SecurityAuditRecord) {
return audit.record(record);
},
@@ -142,15 +142,139 @@ function decisionApproval(
safeTime(approval.decidedAtMs);
}
function recoveryResult(
value: unknown,
command: Extract<
ClusterApprovalManagementCommand,
{ operation: 'approval.recover.inspect' | 'approval.recover.resolve' }
>,
): void {
const recovery = exact(value, [
'projectId',
'dispatchId',
'approvalRequestId',
'expectedAction',
'execution',
'resolution',
]);
if (
identifier(recovery.projectId) !== command.request.projectId ||
identifier(recovery.dispatchId) !== command.request.dispatchId
) {
invalid();
}
identifier(recovery.approvalRequestId);
action(recovery.expectedAction);
const execution = exact(recovery.execution, [
'status',
'version',
'executionDigest',
'attemptCount',
'maxAttempts',
'startedAtMs',
'leaseExpiresAtMs',
'resultMutationId',
'resultCode',
'resultDigest',
'completedAtMs',
'createdAtMs',
'updatedAtMs',
]);
if (
![
'pending',
'leased',
'executing',
'recovery_required',
'retry_wait',
'succeeded',
'failed',
'blocked',
].includes(execution.status as string) ||
!Number.isSafeInteger(execution.version) ||
Number(execution.version) < 1 ||
typeof execution.executionDigest !== 'string' ||
!/^[0-9a-f]{64}$/.test(execution.executionDigest) ||
!Number.isSafeInteger(execution.attemptCount) ||
Number(execution.attemptCount) < 0 ||
!Number.isSafeInteger(execution.maxAttempts) ||
Number(execution.maxAttempts) < 1
) {
invalid();
}
safeTime(execution.startedAtMs, true);
safeTime(execution.leaseExpiresAtMs, true);
safeTime(execution.completedAtMs, true);
safeTime(execution.createdAtMs);
safeTime(execution.updatedAtMs);
for (const field of ['resultMutationId', 'resultCode']) {
const item = execution[field];
if (item !== null) identifier(item);
}
if (
execution.resultDigest !== null &&
(typeof execution.resultDigest !== 'string' ||
!/^[0-9a-f]{64}$/.test(execution.resultDigest))
) {
invalid();
}
if (recovery.resolution !== null) {
const resolution = exact(recovery.resolution, [
'mutationId',
'decision',
'evidenceDigest',
'reasonCode',
'resolvedBy',
'resolvedAtMs',
'resolutionDigest',
]);
identifier(resolution.mutationId);
if (
(resolution.decision !== 'confirm_failed' &&
resolution.decision !== 'abandon_unknown') ||
typeof resolution.evidenceDigest !== 'string' ||
!/^[0-9a-f]{64}$/.test(resolution.evidenceDigest) ||
typeof resolution.reasonCode !== 'string' ||
!/^[a-z][a-z0-9_]{0,63}$/.test(resolution.reasonCode) ||
typeof resolution.resolutionDigest !== 'string' ||
!/^[0-9a-f]{64}$/.test(resolution.resolutionDigest)
) {
invalid();
}
subject(resolution.resolvedBy);
safeTime(resolution.resolvedAtMs);
if (
command.operation === 'approval.recover.resolve' &&
(resolution.mutationId !== command.request.mutationId ||
resolution.decision !== command.request.decision ||
resolution.evidenceDigest !== command.request.evidenceDigest ||
resolution.reasonCode !== command.request.reasonCode)
) {
invalid();
}
}
if (
command.operation === 'approval.recover.resolve' &&
(recovery.resolution === null ||
Number(execution.version) !== command.request.expectedExecutionVersion + 1 ||
(execution.status !== 'failed' && execution.status !== 'blocked'))
) {
invalid();
}
}
export function validateClusterApprovalManagementClientResult(
value: unknown,
command: Readonly<ClusterApprovalManagementCommand>,
): Readonly<ClusterApprovalManagementTransportResult> {
const recoveryOperation =
command.operation === 'approval.recover.inspect' ||
command.operation === 'approval.recover.resolve';
const envelope = exact(value, [
'schemaVersion',
'operation',
'status',
'approval',
recoveryOperation ? 'recovery' : 'approval',
]);
if (
envelope.schemaVersion !== 1 ||
@@ -158,7 +282,22 @@ export function validateClusterApprovalManagementClientResult(
) {
invalid();
}
if (command.operation === 'approval.inspect') {
if (command.operation === 'approval.recover.inspect') {
if (
(envelope.status !== 'found' && envelope.status !== 'absent') ||
(envelope.status === 'absent') !== (envelope.recovery === null)
) {
invalid();
}
if (envelope.recovery !== null) {
recoveryResult(envelope.recovery, command);
}
} else if (command.operation === 'approval.recover.resolve') {
if (envelope.status !== 'resolved' && envelope.status !== 'existing') {
invalid();
}
recoveryResult(envelope.recovery, command);
} else if (command.operation === 'approval.inspect') {
if (
(envelope.status !== 'found' && envelope.status !== 'absent') ||
(envelope.status === 'absent') !== (envelope.approval === null)
@@ -7,6 +7,15 @@ import {
normalizeApprovedActionBinding,
type ApprovedActionBinding,
} from '@qinglong/runtime-core/approved-action';
import {
ApprovedActionManualRecoveryAuthorizationError,
ApprovedActionManualRecoveryFenceConflictError,
ApprovedActionManualRecoveryTargetUnavailableError,
ApprovedActionManualRecoveryUnavailableError,
ApprovedActionManualRecoveryUnsupportedError,
type ApprovedActionManualRecoverySnapshot,
} from '@qinglong/runtime-core/approved-action-manual-recovery';
import { approvedActionExecutionEffectiveStatus } from '@qinglong/runtime-core/approved-action-execution';
import {
ApprovalDecisionAuthorizationError,
ApprovalDecisionBindingConflictError,
@@ -30,30 +39,54 @@ const UUID_V4_PATTERN =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
const STRONG_ASSURANCES = new Set(['multi_factor', 'hardware']);
interface BaseRequest {
interface AuditBaseRequest {
readonly projectId: string;
readonly approvalRequestId: string;
readonly requestId: string;
readonly auditEventId: string;
readonly failureAuditEventId: string;
}
interface ApprovalBaseRequest extends AuditBaseRequest {
readonly approvalRequestId: string;
}
interface RecoveryBaseRequest extends AuditBaseRequest {
readonly dispatchId: string;
}
export type ClusterApprovalManagementCommand =
| Readonly<{
schemaVersion: 1;
operation: 'approval.inspect';
request: BaseRequest;
request: ApprovalBaseRequest;
}>
| Readonly<{
schemaVersion: 1;
operation: 'approval.decide';
request: BaseRequest & {
request: ApprovalBaseRequest & {
readonly expectedVersion: 1;
readonly expectedAction: Readonly<ApprovedActionBinding>;
readonly decisionId: string;
readonly decision: 'approved' | 'rejected';
readonly reasonCode: string;
};
}>
| Readonly<{
schemaVersion: 1;
operation: 'approval.recover.inspect';
request: RecoveryBaseRequest;
}>
| Readonly<{
schemaVersion: 1;
operation: 'approval.recover.resolve';
request: RecoveryBaseRequest & {
readonly expectedExecutionVersion: number;
readonly expectedExecutionDigest: string;
readonly mutationId: string;
readonly decision: 'confirm_failed' | 'abandon_unknown';
readonly evidenceDigest: string;
readonly reasonCode: string;
};
}>;
export type ClusterApprovalManagementTransportResult = Readonly<
@@ -163,45 +196,109 @@ export function normalizeClusterApprovalManagementCommand(
const envelope = exact(value, ['schemaVersion', 'operation', 'request']);
if (
envelope.schemaVersion !== 1 ||
(envelope.operation !== 'approval.inspect' &&
envelope.operation !== 'approval.decide')
![
'approval.inspect',
'approval.decide',
'approval.recover.inspect',
'approval.recover.resolve',
].includes(envelope.operation as string)
) {
invalid();
}
const operation = envelope.operation;
const base = [
const operation = envelope.operation as ClusterApprovalManagementCommand['operation'];
const auditBase = [
'projectId',
'approvalRequestId',
'requestId',
'auditEventId',
'failureAuditEventId',
];
const recoveryOperation =
operation === 'approval.recover.inspect' ||
operation === 'approval.recover.resolve';
const base = [
...auditBase,
recoveryOperation ? 'dispatchId' : 'approvalRequestId',
];
const request = exact(
envelope.request,
operation === 'approval.inspect'
operation === 'approval.inspect' || operation === 'approval.recover.inspect'
? base
: [
: operation === 'approval.decide'
? [
...base,
'expectedVersion',
'expectedAction',
'decisionId',
'decision',
'reasonCode',
]
: [
...base,
'expectedExecutionVersion',
'expectedExecutionDigest',
'mutationId',
'decision',
'evidenceDigest',
'reasonCode',
],
);
const normalizedBase = {
const normalizedAuditBase = {
projectId: identifier(request.projectId),
approvalRequestId: identifier(request.approvalRequestId),
requestId: identifier(request.requestId),
auditEventId: uuid(request.auditEventId),
failureAuditEventId: uuid(request.failureAuditEventId),
};
if (normalizedBase.auditEventId === normalizedBase.failureAuditEventId) invalid();
if (normalizedAuditBase.auditEventId === normalizedAuditBase.failureAuditEventId) {
invalid();
}
if (operation === 'approval.inspect') {
return Object.freeze({
schemaVersion: 1,
operation,
request: Object.freeze(normalizedBase),
operation: 'approval.inspect',
request: Object.freeze({
...normalizedAuditBase,
approvalRequestId: identifier(request.approvalRequestId),
}),
});
}
if (operation === 'approval.recover.inspect') {
return Object.freeze({
schemaVersion: 1,
operation: 'approval.recover.inspect',
request: Object.freeze({
...normalizedAuditBase,
dispatchId: identifier(request.dispatchId),
}),
});
}
if (operation === 'approval.recover.resolve') {
if (
!Number.isSafeInteger(request.expectedExecutionVersion) ||
Number(request.expectedExecutionVersion) < 1 ||
typeof request.expectedExecutionDigest !== 'string' ||
!/^[0-9a-f]{64}$/.test(request.expectedExecutionDigest) ||
(request.decision !== 'confirm_failed' &&
request.decision !== 'abandon_unknown') ||
typeof request.evidenceDigest !== 'string' ||
!/^[0-9a-f]{64}$/.test(request.evidenceDigest) ||
typeof request.reasonCode !== 'string' ||
!REASON_PATTERN.test(request.reasonCode)
) {
invalid();
}
return Object.freeze({
schemaVersion: 1,
operation: 'approval.recover.resolve',
request: Object.freeze({
...normalizedAuditBase,
dispatchId: identifier(request.dispatchId),
expectedExecutionVersion: Number(request.expectedExecutionVersion),
expectedExecutionDigest: request.expectedExecutionDigest,
mutationId: identifier(request.mutationId),
decision: request.decision,
evidenceDigest: request.evidenceDigest,
reasonCode: request.reasonCode,
}),
});
}
if (
@@ -222,9 +319,10 @@ export function normalizeClusterApprovalManagementCommand(
}
return Object.freeze({
schemaVersion: 1,
operation,
operation: 'approval.decide',
request: Object.freeze({
...normalizedBase,
...normalizedAuditBase,
approvalRequestId: identifier(request.approvalRequestId),
expectedVersion: 1,
expectedAction,
decisionId: identifier(request.decisionId),
@@ -284,11 +382,15 @@ function failureReason(error: unknown, authenticated: boolean): Readonly<{
}
if (
error instanceof ApprovalInspectionAuthorizationError ||
error instanceof ApprovalDecisionAuthorizationError
error instanceof ApprovalDecisionAuthorizationError ||
error instanceof ApprovedActionManualRecoveryAuthorizationError
) {
return Object.freeze({ outcome: 'denied', reason: 'policy_rejected' });
}
if (error instanceof ApprovalDecisionTargetUnavailableError) {
if (
error instanceof ApprovalDecisionTargetUnavailableError ||
error instanceof ApprovedActionManualRecoveryTargetUnavailableError
) {
return Object.freeze({ outcome: 'denied', reason: 'approval_target_unavailable' });
}
if (error instanceof ApprovalDecisionBindingConflictError) {
@@ -299,7 +401,9 @@ function failureReason(error: unknown, authenticated: boolean): Readonly<{
error instanceof ApprovalRequestStateConflictError ||
error instanceof ApprovalRequestExpiredError ||
error instanceof ApprovalMutationConflictError ||
error instanceof ApprovalPolicyFenceConflictError
error instanceof ApprovalPolicyFenceConflictError ||
error instanceof ApprovedActionManualRecoveryFenceConflictError ||
error instanceof ApprovedActionManualRecoveryUnsupportedError
) {
return Object.freeze({ outcome: 'denied', reason: 'approval_state_or_fence_conflict' });
}
@@ -320,11 +424,15 @@ function observedTime(now: () => number): number {
function mapped(error: unknown): Error {
if (
error instanceof ApprovalInspectionAuthorizationError ||
error instanceof ApprovalDecisionAuthorizationError
error instanceof ApprovalDecisionAuthorizationError ||
error instanceof ApprovedActionManualRecoveryAuthorizationError
) {
return new ClusterApprovalManagementTransportAuthorizationError();
}
if (error instanceof ApprovalDecisionTargetUnavailableError) {
if (
error instanceof ApprovalDecisionTargetUnavailableError ||
error instanceof ApprovedActionManualRecoveryTargetUnavailableError
) {
return new ClusterApprovalManagementTransportTargetUnavailableError();
}
if (
@@ -333,13 +441,16 @@ function mapped(error: unknown): Error {
error instanceof ApprovalRequestStateConflictError ||
error instanceof ApprovalRequestExpiredError ||
error instanceof ApprovalMutationConflictError ||
error instanceof ApprovalPolicyFenceConflictError
error instanceof ApprovalPolicyFenceConflictError ||
error instanceof ApprovedActionManualRecoveryFenceConflictError ||
error instanceof ApprovedActionManualRecoveryUnsupportedError
) {
return new ClusterApprovalManagementTransportConflictError();
}
if (
error instanceof ApprovalInspectionUnavailableError ||
error instanceof ApprovalDecisionUnavailableError
error instanceof ApprovalDecisionUnavailableError ||
error instanceof ApprovedActionManualRecoveryUnavailableError
) {
return new ClusterApprovalManagementTransportUnavailableError();
}
@@ -348,6 +459,47 @@ function mapped(error: unknown): Error {
: new ClusterApprovalManagementTransportUnavailableError();
}
function recoveryProjection(
snapshot: Readonly<ApprovedActionManualRecoverySnapshot>,
nowMs: number,
): Readonly<Record<string, unknown>> {
const execution = snapshot.execution.execution;
const resolution = snapshot.resolution;
return Object.freeze({
projectId: snapshot.execution.dispatch.projectId,
dispatchId: snapshot.execution.dispatch.id,
approvalRequestId: snapshot.execution.dispatch.approvalRequestId,
expectedAction: snapshot.execution.dispatch.action,
execution: Object.freeze({
status: approvedActionExecutionEffectiveStatus(execution, nowMs),
version: execution.version,
executionDigest: execution.executionDigest,
attemptCount: execution.attemptCount,
maxAttempts: execution.maxAttempts,
startedAtMs: execution.startedAtMs,
leaseExpiresAtMs: execution.leaseExpiresAtMs,
resultMutationId: execution.resultMutationId,
resultCode: execution.resultCode,
resultDigest: execution.resultDigest,
completedAtMs: execution.completedAtMs,
createdAtMs: execution.createdAtMs,
updatedAtMs: execution.updatedAtMs,
}),
resolution:
resolution === null
? null
: Object.freeze({
mutationId: resolution.mutationId,
decision: resolution.decision,
evidenceDigest: resolution.evidenceDigest,
reasonCode: resolution.reasonCode,
resolvedBy: resolution.resolvedBy,
resolvedAtMs: resolution.resolvedAtMs,
resolutionDigest: resolution.resolutionDigest,
}),
});
}
export function createClusterApprovalManagementTransport(options: Readonly<{
service: ClusterApprovalManagementService;
now?: () => number;
@@ -359,6 +511,8 @@ export function createClusterApprovalManagementTransport(options: Readonly<{
Object.keys(options).some((key) => key !== 'service' && key !== 'now') ||
typeof options.service?.inspect !== 'function' ||
typeof options.service?.decide !== 'function' ||
typeof options.service?.inspectRecovery !== 'function' ||
typeof options.service?.resolveRecovery !== 'function' ||
typeof options.service?.recordFailure !== 'function' ||
(options.now !== undefined && typeof options.now !== 'function')
) {
@@ -407,6 +561,52 @@ export function createClusterApprovalManagementTransport(options: Readonly<{
throw new ClusterApprovalManagementTransportAuthenticationError();
}
};
if (command.operation === 'approval.recover.inspect') {
const snapshot = await options.service.inspectRecovery(
{
projectId: command.request.projectId,
dispatchId: command.request.dispatchId,
auditEventId: command.request.auditEventId,
requestId: command.request.requestId,
principal,
},
confirmAuthorization,
);
return Object.freeze({
schemaVersion: 1 as const,
operation: command.operation,
status: snapshot === null ? ('absent' as const) : ('found' as const),
recovery:
snapshot === null
? null
: recoveryProjection(snapshot, observedTime(now)),
});
}
if (command.operation === 'approval.recover.resolve') {
const result = await options.service.resolveRecovery(
{
projectId: command.request.projectId,
dispatchId: command.request.dispatchId,
expectedExecutionVersion:
command.request.expectedExecutionVersion,
expectedExecutionDigest: command.request.expectedExecutionDigest,
mutationId: command.request.mutationId,
decision: command.request.decision,
evidenceDigest: command.request.evidenceDigest,
reasonCode: command.request.reasonCode,
auditEventId: command.request.auditEventId,
requestId: command.request.requestId,
principal,
},
confirmAuthorization,
);
return Object.freeze({
schemaVersion: 1 as const,
operation: command.operation,
status: result.status,
recovery: recoveryProjection(result.snapshot, observedTime(now)),
});
}
if (command.operation === 'approval.inspect') {
const detail = await options.service.inspect(
{
@@ -39,6 +39,13 @@ const BASE_REQUEST = Object.freeze({
auditEventId: '60000000-0000-4000-8000-000000000001',
failureAuditEventId: '60000000-0000-4000-8000-000000000002',
});
const RECOVERY_BASE_REQUEST = Object.freeze({
projectId: 'default',
dispatchId: 'dispatch-1',
requestId: 'recovery-command-1',
auditEventId: '60000000-0000-4000-8000-000000000003',
failureAuditEventId: '60000000-0000-4000-8000-000000000004',
});
function privateWrite(filePath, value) {
writeFileSync(filePath, value, { mode: 0o600 });
@@ -204,6 +211,117 @@ test('validates the durable decision tuple and rejects server-side drift', () =>
}
});
test('validates recovery inspection and binds terminal resolution to the command fence', () => {
const recoveryAction = {
...ACTION,
permission: 'secret.manage',
actionType: 'plugin_package.secret_binding.bind',
actionRef: 'secret-binding:1',
};
const inspectCommand = {
schemaVersion: 1,
operation: 'approval.recover.inspect',
request: RECOVERY_BASE_REQUEST,
};
const recovery = {
projectId: 'default',
dispatchId: 'dispatch-1',
approvalRequestId: 'approval-1',
expectedAction: recoveryAction,
execution: {
status: 'recovery_required',
version: 3,
executionDigest: 'c'.repeat(64),
attemptCount: 1,
maxAttempts: 3,
startedAtMs: 1_400,
leaseExpiresAtMs: 1_800,
resultMutationId: null,
resultCode: null,
resultDigest: null,
completedAtMs: null,
createdAtMs: 1_200,
updatedAtMs: 1_400,
},
resolution: null,
};
assert.deepEqual(
validateClusterApprovalManagementClientResult(
{
schemaVersion: 1,
operation: inspectCommand.operation,
status: 'found',
recovery,
},
inspectCommand,
).recovery,
recovery,
);
const resolveCommand = {
schemaVersion: 1,
operation: 'approval.recover.resolve',
request: {
...RECOVERY_BASE_REQUEST,
expectedExecutionVersion: 3,
expectedExecutionDigest: 'c'.repeat(64),
mutationId: 'manual-recovery-1',
decision: 'abandon_unknown',
evidenceDigest: 'e'.repeat(64),
reasonCode: 'orphan_absence_verified',
},
};
const resolved = {
...recovery,
execution: {
...recovery.execution,
status: 'blocked',
version: 4,
executionDigest: 'd'.repeat(64),
leaseExpiresAtMs: null,
resultMutationId: 'manual-recovery-1',
resultCode: 'manual_recovery_abandoned_unknown',
completedAtMs: 2_000,
updatedAtMs: 2_000,
},
resolution: {
mutationId: 'manual-recovery-1',
decision: 'abandon_unknown',
evidenceDigest: 'e'.repeat(64),
reasonCode: 'orphan_absence_verified',
resolvedBy: { type: 'user', id: 'owner-1' },
resolvedAtMs: 2_000,
resolutionDigest: 'f'.repeat(64),
},
};
const result = {
schemaVersion: 1,
operation: resolveCommand.operation,
status: 'resolved',
recovery: resolved,
};
assert.deepEqual(
validateClusterApprovalManagementClientResult(result, resolveCommand),
result,
);
for (const changed of [
{ ...resolved, execution: { ...resolved.execution, version: 3 } },
{
...resolved,
resolution: { ...resolved.resolution, evidenceDigest: '0'.repeat(64) },
},
]) {
assert.throws(
() =>
validateClusterApprovalManagementClientResult(
{ ...result, recovery: changed },
resolveCommand,
),
ClusterPluginPackageManagementClientRequestError,
);
}
});
test('accepts only the exact Approval route before opening one mTLS connection', async () => {
let connects = 0;
await assert.rejects(
@@ -2,9 +2,16 @@ const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
consumeApprovalRequest,
createApprovalRequest,
decideApprovalRequest,
} = require('@qinglong/runtime-core/approved-action');
const {
claimApprovedActionExecution,
completeApprovedActionExecution,
createApprovedActionExecution,
startApprovedActionExecution,
} = require('@qinglong/runtime-core/approved-action-execution');
const {
ClusterApprovalManagementTransportAuthenticationError,
ClusterApprovalManagementTransportRequestError,
@@ -32,6 +39,13 @@ const BASE_REQUEST = Object.freeze({
auditEventId: '40000000-0000-4000-8000-000000000001',
failureAuditEventId: '40000000-0000-4000-8000-000000000002',
});
const RECOVERY_BASE_REQUEST = Object.freeze({
projectId: 'default',
dispatchId: 'dispatch-1',
requestId: 'recovery-command-1',
auditEventId: '40000000-0000-4000-8000-000000000003',
failureAuditEventId: '40000000-0000-4000-8000-000000000004',
});
function pending() {
return createApprovalRequest({
@@ -70,6 +84,64 @@ function decideCommand() {
};
}
function executingRecoverySnapshot() {
const action = {
...ACTION,
permission: 'secret.manage',
actionType: 'plugin_package.secret_binding.bind',
actionRef: 'secret-binding:1',
};
const recoveryPending = createApprovalRequest({
id: 'approval-1',
projectId: 'default',
action,
risk: 'high',
decisionMode: 'human_confirmation',
requestedBy: { type: 'agent', id: 'agent-1' },
requestedAtMs: 900,
expiresAtMs: 10_000,
requestFence: { projectVersion: 1, bindingVersion: 2 },
});
const approved = decideApprovalRequest(recoveryPending, {
expectedVersion: 1,
decisionId: 'decision-recovery-1',
decision: 'approved',
reasonCode: 'reviewed',
principal: PRINCIPAL,
decidedAtMs: 1_100,
authorizationFence: { projectVersion: 1, bindingVersion: 2 },
});
const dispatch = consumeApprovalRequest(approved, {
expectedVersion: 2,
consumptionId: 'consumption-1',
dispatchId: 'dispatch-1',
action,
requestedBy: approved.requestedBy,
consumedBy: { type: 'system', id: 'package-executor' },
consumedAtMs: 1_200,
authorizationFence: { projectVersion: 1, bindingVersion: 2 },
}).dispatch;
const leased = claimApprovedActionExecution(createApprovedActionExecution(dispatch), {
owner: 'executor-1',
leaseToken: 'lease-1',
nowMs: 1_300,
leaseDurationMs: 500,
});
const execution = startApprovedActionExecution(
{ dispatch, execution: leased },
{
dispatchId: dispatch.id,
approvalRequestId: dispatch.approvalRequestId,
actionDigest: dispatch.action.actionDigest,
owner: leased.leaseOwner,
leaseToken: leased.leaseToken,
expectedVersion: leased.version,
startedAtMs: 1_400,
},
);
return { execution: { dispatch, execution }, resolution: null };
}
test('inspects and decides through fresh strong authentication without leaking principal facts', async () => {
const calls = [];
const failures = [];
@@ -104,6 +176,12 @@ test('inspects and decides through fresh strong authentication without leaking p
}),
};
},
async inspectRecovery() {
throw new Error('not used');
},
async resolveRecovery() {
throw new Error('not used');
},
async recordFailure(record) {
failures.push(record);
},
@@ -147,6 +225,12 @@ test('records unauthenticated and reauthentication failures with schema-valid id
async decide() {
throw new Error('not used');
},
async inspectRecovery() {
throw new Error('not used');
},
async resolveRecovery() {
throw new Error('not used');
},
async recordFailure(record) {
failures.push(record);
},
@@ -191,6 +275,8 @@ test('rejects widened or ambiguously audited commands before authentication', as
service: {
async inspect() {},
async decide() {},
async inspectRecovery() {},
async resolveRecovery() {},
async recordFailure() {},
},
});
@@ -229,3 +315,90 @@ test('rejects widened or ambiguously audited commands before authentication', as
);
assert.equal(authenticationCalls, 0);
});
test('inspects and resolves recovery without exposing execution lease or authentication facts', async () => {
const source = executingRecoverySnapshot();
const nextExecution = completeApprovedActionExecution(source.execution.execution, {
owner: source.execution.execution.leaseOwner,
leaseToken: source.execution.execution.leaseToken,
expectedVersion: source.execution.execution.version,
resultMutationId: 'manual-recovery-1',
outcome: 'indeterminate',
resultCode: 'manual_recovery_abandoned_unknown',
completedAtMs: 2_000,
});
const resolution = {
mutationId: 'manual-recovery-1',
decision: 'abandon_unknown',
evidenceDigest: 'e'.repeat(64),
reasonCode: 'orphan_absence_verified',
resolvedBy: { type: 'user', id: 'owner-1' },
resolvedAtMs: 2_000,
resolutionDigest: 'f'.repeat(64),
};
const transport = createClusterApprovalManagementTransport({
service: {
async inspect() {},
async decide() {},
async inspectRecovery(_request, confirmAuthorization) {
await confirmAuthorization();
return source;
},
async resolveRecovery(_request, confirmAuthorization) {
await confirmAuthorization();
return {
status: 'resolved',
snapshot: {
execution: { dispatch: source.execution.dispatch, execution: nextExecution },
resolution: {
schema: 'qinglong/approved-action-manual-recovery@v1',
dispatchId: 'dispatch-1',
dispatchDigest: source.execution.execution.dispatchDigest,
projectId: 'default',
actionType: source.execution.dispatch.action.actionType,
actionDigest: source.execution.dispatch.action.actionDigest,
executionVersion: source.execution.execution.version,
executionDigest: source.execution.execution.executionDigest,
authenticationId: PRINCIPAL.authenticationId,
assurance: PRINCIPAL.assurance,
authenticatedAtMs: PRINCIPAL.authenticatedAtMs,
authorizationFence: { projectVersion: 1, bindingVersion: 2 },
auditEventId: RECOVERY_BASE_REQUEST.auditEventId,
...resolution,
},
},
};
},
async recordFailure() {},
},
now: () => 2_000,
});
const authentication = { async authenticate() { return PRINCIPAL; } };
const inspected = await transport.execute(
{ schemaVersion: 1, operation: 'approval.recover.inspect', request: RECOVERY_BASE_REQUEST },
authentication,
);
const resolved = await transport.execute(
{
schemaVersion: 1,
operation: 'approval.recover.resolve',
request: {
...RECOVERY_BASE_REQUEST,
expectedExecutionVersion: source.execution.execution.version,
expectedExecutionDigest: source.execution.execution.executionDigest,
mutationId: resolution.mutationId,
decision: resolution.decision,
evidenceDigest: resolution.evidenceDigest,
reasonCode: resolution.reasonCode,
},
},
authentication,
);
assert.equal(inspected.recovery.execution.status, 'recovery_required');
assert.equal(resolved.recovery.execution.status, 'blocked');
assert.equal(resolved.recovery.resolution.decision, 'abandon_unknown');
assert.doesNotMatch(
JSON.stringify([inspected, resolved]),
/leaseOwner|leaseToken|authenticationId|authenticatedAtMs|assurance/,
);
});
@@ -188,6 +188,7 @@ function database(serverVersionNum = '160014') {
'create_plugin_package_secret_binding_approval_plan',
'create_plugin_package_secret_transition_plan',
'plugin_package_secret_binding_transition_snapshot',
'resolve_approved_action_manual_recovery',
].includes(functionName),
isOwner: false,
})),
@@ -220,6 +220,7 @@ function database(serverVersionNum = '160014') {
'plugin_package_secret_binding_planning_snapshot',
'create_plugin_package_secret_transition_plan',
'plugin_package_secret_binding_transition_snapshot',
'resolve_approved_action_manual_recovery',
].includes(functionName),
isOwner: false,
})),