mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(local): apply signed automation decisions atomically
This commit is contained in:
@@ -48,6 +48,20 @@ export interface PublishReviewedLegacyCrontabAdoptionOptions {
|
||||
) => void | Promise<void>;
|
||||
}
|
||||
|
||||
export type PublishVerifiedLegacyCrontabAdoptionOptions = Pick<
|
||||
PublishReviewedLegacyCrontabAdoptionOptions,
|
||||
| 'sourceClient'
|
||||
| 'targetPath'
|
||||
| 'profile'
|
||||
| 'timezone'
|
||||
| 'projectId'
|
||||
| 'mutationId'
|
||||
| 'requestId'
|
||||
| 'observedAtMs'
|
||||
| 'confirmSourceIdentity'
|
||||
| 'confirmReviewerAuthority'
|
||||
>;
|
||||
|
||||
export class LegacyCrontabPublicationAuthorizationError extends Error {
|
||||
readonly code = 'LEGACY_CRONTAB_PUBLICATION_NOT_AUTHORIZED';
|
||||
constructor() {
|
||||
@@ -65,7 +79,7 @@ export class LegacyCrontabPublicationUnavailableError extends Error {
|
||||
}
|
||||
|
||||
function auditRecord(
|
||||
options: PublishReviewedLegacyCrontabAdoptionOptions,
|
||||
options: PublishVerifiedLegacyCrontabAdoptionOptions,
|
||||
scope: VerifiedLegacyCrontabDecisionAuthorizationFileScope,
|
||||
decision: Readonly<SecurityPolicyDecision> | null,
|
||||
outcome: SecurityAuditRecord['outcome'],
|
||||
@@ -157,7 +171,27 @@ export async function publishReviewedLegacyCrontabAdoption(
|
||||
options.observedAtMs,
|
||||
),
|
||||
},
|
||||
async (scope) => {
|
||||
(scope) => publishVerifiedLegacyCrontabAdoption(options, scope),
|
||||
);
|
||||
}
|
||||
|
||||
export async function publishVerifiedLegacyCrontabAdoption(
|
||||
options: PublishVerifiedLegacyCrontabAdoptionOptions,
|
||||
scope: VerifiedLegacyCrontabDecisionAuthorizationFileScope,
|
||||
): Promise<PublishLocalLegacyAdoptionResult> {
|
||||
if (
|
||||
!options ||
|
||||
typeof options !== 'object' ||
|
||||
Array.isArray(options) ||
|
||||
typeof options.confirmSourceIdentity !== 'function' ||
|
||||
!scope ||
|
||||
typeof scope !== 'object' ||
|
||||
typeof scope.confirmIdentity !== 'function'
|
||||
) {
|
||||
throw new LegacyCrontabPublicationUnavailableError(
|
||||
'Verified Legacy publication scope is invalid',
|
||||
);
|
||||
}
|
||||
options.confirmSourceIdentity();
|
||||
await options.confirmReviewerAuthority?.(scope.result.receipt.reviewer);
|
||||
const target = await openLocalSqliteAdoptionDatabase({
|
||||
@@ -247,6 +281,4 @@ export async function publishReviewedLegacyCrontabAdoption(
|
||||
} finally {
|
||||
await target.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
import type { DatabaseSync } from 'node:sqlite';
|
||||
import type { LocalSecretKeyProvider } from '@qinglong/runtime-core/local-secret';
|
||||
import type { SecurityPrincipal } from '@qinglong/runtime-core/security';
|
||||
import {
|
||||
publishVerifiedLegacyCrontabAdoption,
|
||||
type PublishReviewedLegacyCrontabAdoptionResult,
|
||||
type PublishVerifiedLegacyCrontabAdoptionOptions,
|
||||
} from './legacyCrontabPublisher';
|
||||
import {
|
||||
publishLegacyCrontabDecisionAuthorizationFile,
|
||||
withVerifiedLegacyCrontabDecisionAuthorizationFile,
|
||||
@@ -35,7 +40,7 @@ export interface ReconciliationAutomationDecisionIdentity {
|
||||
readonly inventoryDigest: string;
|
||||
}
|
||||
|
||||
interface ReconciliationAutomationDecisionVerificationOptions
|
||||
export interface ReconciliationAutomationDecisionVerificationOptions
|
||||
extends ReconciliationAutomationDecisionIdentity {
|
||||
readonly authorizationPath: string;
|
||||
readonly sourceClient: DatabaseSync;
|
||||
@@ -47,6 +52,13 @@ interface ReconciliationAutomationDecisionVerificationOptions
|
||||
readonly allowedParentModes?: readonly (0o500 | 0o700)[];
|
||||
}
|
||||
|
||||
export interface ApplyReconciliationAutomationDecisionOptions
|
||||
extends ReconciliationAutomationDecisionVerificationOptions,
|
||||
Omit<
|
||||
PublishVerifiedLegacyCrontabAdoptionOptions,
|
||||
'sourceClient' | 'profile' | 'timezone'
|
||||
> {}
|
||||
|
||||
export interface IssueReconciliationAutomationDecisionOptions
|
||||
extends ReconciliationAutomationDecisionVerificationOptions {
|
||||
readonly reviewFilePath: string;
|
||||
@@ -409,3 +421,29 @@ export async function verifyReconciliationAutomationDecision(
|
||||
return scope.result;
|
||||
});
|
||||
}
|
||||
|
||||
export async function applyReconciliationAutomationDecision(
|
||||
options: ApplyReconciliationAutomationDecisionOptions,
|
||||
): Promise<PublishReviewedLegacyCrontabAdoptionResult> {
|
||||
return withVerifiedReconciliationAutomationDecision(options, (scope) =>
|
||||
publishVerifiedLegacyCrontabAdoption(
|
||||
{
|
||||
sourceClient: options.sourceClient,
|
||||
targetPath: options.targetPath,
|
||||
profile: options.profile,
|
||||
timezone: options.timezone,
|
||||
projectId: options.projectId,
|
||||
mutationId: options.mutationId,
|
||||
requestId: options.requestId,
|
||||
observedAtMs: options.observedAtMs,
|
||||
confirmSourceIdentity: options.confirmSourceIdentity,
|
||||
...(options.confirmReviewerAuthority === undefined
|
||||
? {}
|
||||
: {
|
||||
confirmReviewerAuthority: options.confirmReviewerAuthority,
|
||||
}),
|
||||
},
|
||||
scope,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,9 @@ const { migrateLocalSqlitePath } = require('@qinglong/local-sqlite/migration');
|
||||
const {
|
||||
visitLegacyCrontabAdoptionInspections,
|
||||
} = require('../dist/legacy-adoption/legacyCrontabAdoption');
|
||||
const {
|
||||
applyReconciliationAutomationDecision,
|
||||
} = require('../dist/legacy-adoption/reconciliationAutomationDecision');
|
||||
|
||||
const REVIEWED_AT_MS = 1_760_000_000_000;
|
||||
const REVIEWER = Object.freeze({
|
||||
@@ -442,6 +445,7 @@ test('publishes one reviewed legacy task set into the target atomically', async
|
||||
authorizationDirectory,
|
||||
`${decisionId}.ndjson`,
|
||||
);
|
||||
const authorization =
|
||||
await publishReviewedLegacyCrontabAdoptionDecisionAuthorizationFile({
|
||||
sourcePath: value.sourcePath,
|
||||
profile: 'edge',
|
||||
@@ -492,13 +496,42 @@ test('publishes one reviewed legacy task set into the target atomically', async
|
||||
reviewerAuthorityChecks += 1;
|
||||
},
|
||||
};
|
||||
const inserted = await publishReviewedLegacyCrontabAdoption(
|
||||
publicationOptions,
|
||||
);
|
||||
const publicationSource = new DatabaseSync(value.sourcePath, {
|
||||
readOnly: true,
|
||||
});
|
||||
let sourceIdentityChecks = 0;
|
||||
const inserted = await applyReconciliationAutomationDecision({
|
||||
authorizationPath,
|
||||
decisionId,
|
||||
profile: 'edge',
|
||||
automationPlanDigest: plan.planDigest,
|
||||
inventoryDigest: authorization.receipt.inventoryDigest,
|
||||
sourceClient: publicationSource,
|
||||
timezone: 'UTC',
|
||||
keyProvider: authorizationKeyProvider(),
|
||||
observedAtMs: REVIEWED_AT_MS + 1,
|
||||
openRequirements: () =>
|
||||
page.diagnostics.map((diagnostic) => ({
|
||||
rowOrdinal: diagnostic.rowOrdinal,
|
||||
sourceDigest: diagnostic.sourceDigest,
|
||||
classification: diagnostic.classification,
|
||||
requirement: 'review_adopt',
|
||||
})),
|
||||
targetPath: value.targetPath,
|
||||
projectId: 'default',
|
||||
mutationId,
|
||||
requestId: 'reconciliation-automation-publication-test',
|
||||
confirmSourceIdentity() {
|
||||
sourceIdentityChecks += 1;
|
||||
},
|
||||
confirmReviewerAuthority: publicationOptions.confirmReviewerAuthority,
|
||||
});
|
||||
publicationSource.close();
|
||||
assert.equal(inserted.status, 'inserted');
|
||||
assert.equal(inserted.adoption.adoptedTaskCount, 1);
|
||||
assert.equal(inserted.adoption.adoptedTriggerCount, 1);
|
||||
assert.equal(reviewerAuthorityChecks, 2);
|
||||
assert.equal(sourceIdentityChecks, 2);
|
||||
assert.equal(
|
||||
(await publishReviewedLegacyCrontabAdoption(publicationOptions)).status,
|
||||
'existing',
|
||||
|
||||
Reference in New Issue
Block a user