mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
fix(ql3): bind evolved reconciliation target
This commit is contained in:
+18
-5
@@ -223,12 +223,25 @@ function normalizeOptions(
|
|||||||
fail('authentication or Secret material must be below deploymentRoot');
|
fail('authentication or Secret material must be below deploymentRoot');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const targetRelative = path.relative(
|
||||||
|
normalized.deploymentRoot,
|
||||||
|
normalized.targetDatabasePath,
|
||||||
|
);
|
||||||
if (
|
if (
|
||||||
roots.some(
|
!targetRelative ||
|
||||||
(root) =>
|
targetRelative.startsWith('..') ||
|
||||||
overlaps(root, normalized.targetDatabasePath) ||
|
path.isAbsolute(targetRelative)
|
||||||
overlaps(normalized.targetDatabasePath, root),
|
) {
|
||||||
)
|
fail('targetDatabasePath must be below deploymentRoot');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
roots
|
||||||
|
.slice(1)
|
||||||
|
.some(
|
||||||
|
(root) =>
|
||||||
|
overlaps(root, normalized.targetDatabasePath) ||
|
||||||
|
overlaps(normalized.targetDatabasePath, root),
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
fail('targetDatabasePath overlaps an authority root');
|
fail('targetDatabasePath overlaps an authority root');
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-3
@@ -338,6 +338,14 @@ export async function applyLocalReconciliationSecretConfig(
|
|||||||
) {
|
) {
|
||||||
fail('apply command is detached from a ready signed decision');
|
fail('apply command is detached from a ready signed decision');
|
||||||
}
|
}
|
||||||
|
const targetSnapshotSha256 = terminal.context.planHeader.targetSnapshotSha256;
|
||||||
|
if (terminal.receipt.applyBindingCount > 0 && targetSnapshotSha256 === null) {
|
||||||
|
fail('active binding plan is missing evolved target authority');
|
||||||
|
}
|
||||||
|
const stoppedProofOptions =
|
||||||
|
targetSnapshotSha256 === null
|
||||||
|
? {}
|
||||||
|
: { expectedEvolvedTargetSha256: targetSnapshotSha256 };
|
||||||
const planTerminal = terminal.context.planTerminal;
|
const planTerminal = terminal.context.planTerminal;
|
||||||
const capture = readLocalReconciliationCaptureIntent(
|
const capture = readLocalReconciliationCaptureIntent(
|
||||||
planTerminal.intent.command.options.captureRoot,
|
planTerminal.intent.command.options.captureRoot,
|
||||||
@@ -375,7 +383,11 @@ export async function applyLocalReconciliationSecretConfig(
|
|||||||
fail('apply lost reviewed head compare-and-swap');
|
fail('apply lost reviewed head compare-and-swap');
|
||||||
}
|
}
|
||||||
discardUnpreparedLocalReconciliationSecretConfigMaterials(selected);
|
discardUnpreparedLocalReconciliationSecretConfigMaterials(selected);
|
||||||
const before = proveLocalReconciliationStoppedState(capture.command, uid);
|
const before = proveLocalReconciliationStoppedState(
|
||||||
|
capture.command,
|
||||||
|
uid,
|
||||||
|
stoppedProofOptions,
|
||||||
|
);
|
||||||
const materials: Readonly<PreparedReconciliationSecretConfigMaterial>[] =
|
const materials: Readonly<PreparedReconciliationSecretConfigMaterial>[] =
|
||||||
[];
|
[];
|
||||||
const openRequirements =
|
const openRequirements =
|
||||||
@@ -441,7 +453,11 @@ export async function applyLocalReconciliationSecretConfig(
|
|||||||
? {}
|
? {}
|
||||||
: { busyTimeoutMs: command.options.busyTimeoutMs }),
|
: { busyTimeoutMs: command.options.busyTimeoutMs }),
|
||||||
});
|
});
|
||||||
const after = proveLocalReconciliationStoppedState(capture.command, uid);
|
const after = proveLocalReconciliationStoppedState(
|
||||||
|
capture.command,
|
||||||
|
uid,
|
||||||
|
stoppedProofOptions,
|
||||||
|
);
|
||||||
if (after.proofDigest !== before.proofDigest) {
|
if (after.proofDigest !== before.proofDigest) {
|
||||||
fail('stopped target drifted across preparation');
|
fail('stopped target drifted across preparation');
|
||||||
}
|
}
|
||||||
@@ -522,7 +538,11 @@ export async function applyLocalReconciliationSecretConfig(
|
|||||||
fail('apply lost prepared head compare-and-swap');
|
fail('apply lost prepared head compare-and-swap');
|
||||||
}
|
}
|
||||||
if (!recoveringPreparedIntent) {
|
if (!recoveringPreparedIntent) {
|
||||||
const stopped = proveLocalReconciliationStoppedState(capture.command, uid);
|
const stopped = proveLocalReconciliationStoppedState(
|
||||||
|
capture.command,
|
||||||
|
uid,
|
||||||
|
stoppedProofOptions,
|
||||||
|
);
|
||||||
if (stopped.proofDigest !== intent.stoppedProofDigest) {
|
if (stopped.proofDigest !== intent.stoppedProofDigest) {
|
||||||
fail('stopped proof drifted before write');
|
fail('stopped proof drifted before write');
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -614,6 +614,7 @@ function publishPlan(
|
|||||||
authority: Readonly<SecretConfigReviewAuthority>,
|
authority: Readonly<SecretConfigReviewAuthority>,
|
||||||
dependencies: LocalReconciliationSecretConfigPlanDependencies,
|
dependencies: LocalReconciliationSecretConfigPlanDependencies,
|
||||||
uid: number,
|
uid: number,
|
||||||
|
targetSnapshotSha256: string | null,
|
||||||
automationTarget?: DatabaseSync,
|
automationTarget?: DatabaseSync,
|
||||||
): Readonly<LocalReconciliationSecretConfigPlanReceipt> {
|
): Readonly<LocalReconciliationSecretConfigPlanReceipt> {
|
||||||
let descriptor: number | undefined;
|
let descriptor: number | undefined;
|
||||||
@@ -649,6 +650,7 @@ function publishPlan(
|
|||||||
projectId: command.request.projectId,
|
projectId: command.request.projectId,
|
||||||
tableDisposition: authority.tableDisposition,
|
tableDisposition: authority.tableDisposition,
|
||||||
unadaptedLegacyConfigCount: authority.unadaptedLegacyConfigCount,
|
unadaptedLegacyConfigCount: authority.unadaptedLegacyConfigCount,
|
||||||
|
targetSnapshotSha256,
|
||||||
preparedHeadDigest: head.headDigest,
|
preparedHeadDigest: head.headDigest,
|
||||||
preparedAtMs: command.request.preparedAtMs,
|
preparedAtMs: command.request.preparedAtMs,
|
||||||
});
|
});
|
||||||
@@ -987,6 +989,7 @@ export async function planLocalReconciliationSecretConfig(
|
|||||||
authority,
|
authority,
|
||||||
dependencies,
|
dependencies,
|
||||||
identity.uid,
|
identity.uid,
|
||||||
|
automationTarget?.snapshotSha256 ?? null,
|
||||||
target,
|
target,
|
||||||
);
|
);
|
||||||
const receipt =
|
const receipt =
|
||||||
|
|||||||
+4
@@ -226,6 +226,7 @@ function header(
|
|||||||
'schemaVersion',
|
'schemaVersion',
|
||||||
'secretConfigId',
|
'secretConfigId',
|
||||||
'tableDisposition',
|
'tableDisposition',
|
||||||
|
'targetSnapshotSha256',
|
||||||
'unadaptedLegacyConfigCount',
|
'unadaptedLegacyConfigCount',
|
||||||
],
|
],
|
||||||
'header',
|
'header',
|
||||||
@@ -244,6 +245,9 @@ function header(
|
|||||||
record.projectId.length < 1 ||
|
record.projectId.length < 1 ||
|
||||||
(record.tableDisposition !== 'absent' &&
|
(record.tableDisposition !== 'absent' &&
|
||||||
record.tableDisposition !== 'manual_external') ||
|
record.tableDisposition !== 'manual_external') ||
|
||||||
|
(record.targetSnapshotSha256 !== null &&
|
||||||
|
(typeof record.targetSnapshotSha256 !== 'string' ||
|
||||||
|
!DIGEST_PATTERN.test(record.targetSnapshotSha256))) ||
|
||||||
!Number.isSafeInteger(record.unadaptedLegacyConfigCount) ||
|
!Number.isSafeInteger(record.unadaptedLegacyConfigCount) ||
|
||||||
(record.unadaptedLegacyConfigCount as number) < 0 ||
|
(record.unadaptedLegacyConfigCount as number) < 0 ||
|
||||||
![
|
![
|
||||||
|
|||||||
+4
-12
@@ -54,6 +54,7 @@ export interface LocalReconciliationSecretConfigPlanHeader {
|
|||||||
readonly projectId: string;
|
readonly projectId: string;
|
||||||
readonly tableDisposition: 'absent' | 'manual_external';
|
readonly tableDisposition: 'absent' | 'manual_external';
|
||||||
readonly unadaptedLegacyConfigCount: number;
|
readonly unadaptedLegacyConfigCount: number;
|
||||||
|
readonly targetSnapshotSha256: string | null;
|
||||||
readonly preparedHeadDigest: string;
|
readonly preparedHeadDigest: string;
|
||||||
readonly preparedAtMs: number;
|
readonly preparedAtMs: number;
|
||||||
readonly headerDigest: string;
|
readonly headerDigest: string;
|
||||||
@@ -350,11 +351,7 @@ function targetAutomationAdoptionProjection(
|
|||||||
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
|
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
|
||||||
),
|
),
|
||||||
planDigest: adoptionText(row, 'planDigest', DIGEST_PATTERN),
|
planDigest: adoptionText(row, 'planDigest', DIGEST_PATTERN),
|
||||||
inventoryDigest: adoptionText(
|
inventoryDigest: adoptionText(row, 'inventoryDigest', DIGEST_PATTERN),
|
||||||
row,
|
|
||||||
'inventoryDigest',
|
|
||||||
DIGEST_PATTERN,
|
|
||||||
),
|
|
||||||
decisionDigest: adoptionText(row, 'decisionDigest', DIGEST_PATTERN),
|
decisionDigest: adoptionText(row, 'decisionDigest', DIGEST_PATTERN),
|
||||||
receiptDigest: adoptionText(row, 'receiptDigest', DIGEST_PATTERN),
|
receiptDigest: adoptionText(row, 'receiptDigest', DIGEST_PATTERN),
|
||||||
authorizationFileDigest: adoptionText(
|
authorizationFileDigest: adoptionText(
|
||||||
@@ -372,11 +369,7 @@ function targetAutomationAdoptionProjection(
|
|||||||
adoptedTriggerCount: selectedAdoptedTriggerCount,
|
adoptedTriggerCount: selectedAdoptedTriggerCount,
|
||||||
skippedCount,
|
skippedCount,
|
||||||
auditEventId: adoptionText(row, 'auditEventId', UUID_V4_PATTERN),
|
auditEventId: adoptionText(row, 'auditEventId', UUID_V4_PATTERN),
|
||||||
createdAtMs: adoptionCount(
|
createdAtMs: adoptionCount(row, 'createdAtMs', Number.MAX_SAFE_INTEGER),
|
||||||
row,
|
|
||||||
'createdAtMs',
|
|
||||||
Number.MAX_SAFE_INTEGER,
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
if (payload.auditEventId !== payload.mutationId) {
|
if (payload.auditEventId !== payload.mutationId) {
|
||||||
fail('target Automation adoption audit binding drifted');
|
fail('target Automation adoption audit binding drifted');
|
||||||
@@ -1048,8 +1041,7 @@ export function buildLocalReconciliationSecretConfigPlanReceipt(
|
|||||||
adoptedLegacyTriggerCount: footer.adoptedLegacyTriggerCount,
|
adoptedLegacyTriggerCount: footer.adoptedLegacyTriggerCount,
|
||||||
adoptionProvenanceTaskCount: footer.adoptionProvenanceTaskCount,
|
adoptionProvenanceTaskCount: footer.adoptionProvenanceTaskCount,
|
||||||
adoptionProvenanceTriggerCount: footer.adoptionProvenanceTriggerCount,
|
adoptionProvenanceTriggerCount: footer.adoptionProvenanceTriggerCount,
|
||||||
automationAdoptionProvenanceState:
|
automationAdoptionProvenanceState: footer.automationAdoptionProvenanceState,
|
||||||
footer.automationAdoptionProvenanceState,
|
|
||||||
unadaptedLegacyConfigCount: footer.unadaptedLegacyConfigCount,
|
unadaptedLegacyConfigCount: footer.unadaptedLegacyConfigCount,
|
||||||
outcome: footer.outcome,
|
outcome: footer.outcome,
|
||||||
preparedAtMs: header.preparedAtMs,
|
preparedAtMs: header.preparedAtMs,
|
||||||
|
|||||||
@@ -342,14 +342,25 @@ function normalizeOptions(
|
|||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
secretConfig !== null &&
|
secretConfig !== null &&
|
||||||
roots.some(
|
roots
|
||||||
(root) =>
|
.slice(1)
|
||||||
overlaps(root, secretConfig.targetDatabasePath) ||
|
.some(
|
||||||
overlaps(secretConfig.targetDatabasePath, root),
|
(root) =>
|
||||||
)
|
overlaps(root, secretConfig.targetDatabasePath) ||
|
||||||
|
overlaps(secretConfig.targetDatabasePath, root),
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
fail('targetDatabasePath overlaps an authority root');
|
fail('targetDatabasePath overlaps an authority root');
|
||||||
}
|
}
|
||||||
|
if (secretConfig !== null) {
|
||||||
|
const relative = path.relative(
|
||||||
|
normalized.deploymentRoot,
|
||||||
|
secretConfig.targetDatabasePath,
|
||||||
|
);
|
||||||
|
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
|
||||||
|
fail('Secret/Config targetDatabasePath must be below deploymentRoot');
|
||||||
|
}
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
automation !== null &&
|
automation !== null &&
|
||||||
secretConfig !== null &&
|
secretConfig !== null &&
|
||||||
|
|||||||
@@ -28,9 +28,14 @@ const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
|
|||||||
export interface LocalReconciliationStoppedProof {
|
export interface LocalReconciliationStoppedProof {
|
||||||
readonly stoppedRecordDigest: string;
|
readonly stoppedRecordDigest: string;
|
||||||
readonly reconciliationEvidenceDigest: string;
|
readonly reconciliationEvidenceDigest: string;
|
||||||
|
readonly evolvedTargetSha256?: string;
|
||||||
readonly proofDigest: string;
|
readonly proofDigest: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LocalReconciliationStoppedProofOptions {
|
||||||
|
readonly expectedEvolvedTargetSha256?: string;
|
||||||
|
}
|
||||||
|
|
||||||
function configurationError(message: string): never {
|
function configurationError(message: string): never {
|
||||||
throw new LocalDeploymentConfigurationError(message);
|
throw new LocalDeploymentConfigurationError(message);
|
||||||
}
|
}
|
||||||
@@ -63,6 +68,61 @@ function exact(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sameFileStat(left: fs.BigIntStats, right: fs.BigIntStats): boolean {
|
||||||
|
return (
|
||||||
|
left.dev === right.dev &&
|
||||||
|
left.ino === right.ino &&
|
||||||
|
left.mode === right.mode &&
|
||||||
|
left.nlink === right.nlink &&
|
||||||
|
left.uid === right.uid &&
|
||||||
|
left.size === right.size &&
|
||||||
|
left.mtimeNs === right.mtimeNs &&
|
||||||
|
left.ctimeNs === right.ctimeNs
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stableTargetSha256(filePath: string, uid: number): string {
|
||||||
|
let descriptor: number | undefined;
|
||||||
|
const buffer = Buffer.allocUnsafe(64 * 1024);
|
||||||
|
try {
|
||||||
|
const pathStat = fs.lstatSync(filePath, { bigint: true });
|
||||||
|
descriptor = fs.openSync(
|
||||||
|
filePath,
|
||||||
|
fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW,
|
||||||
|
);
|
||||||
|
const before = fs.fstatSync(descriptor, { bigint: true });
|
||||||
|
if (
|
||||||
|
!pathStat.isFile() ||
|
||||||
|
pathStat.isSymbolicLink() ||
|
||||||
|
!sameFileStat(pathStat, before) ||
|
||||||
|
before.uid !== BigInt(uid) ||
|
||||||
|
before.nlink !== 1n ||
|
||||||
|
(before.mode & 0o077n) !== 0n ||
|
||||||
|
fs.realpathSync(filePath) !== filePath ||
|
||||||
|
before.size < 1n
|
||||||
|
) {
|
||||||
|
configurationError('evolved target database identity is invalid');
|
||||||
|
}
|
||||||
|
const hash = crypto.createHash('sha256');
|
||||||
|
for (;;) {
|
||||||
|
const count = fs.readSync(descriptor, buffer, 0, buffer.byteLength, null);
|
||||||
|
if (count === 0) break;
|
||||||
|
hash.update(buffer.subarray(0, count));
|
||||||
|
}
|
||||||
|
const after = fs.fstatSync(descriptor, { bigint: true });
|
||||||
|
if (!sameFileStat(before, after)) {
|
||||||
|
configurationError('evolved target database changed while hashing');
|
||||||
|
}
|
||||||
|
return hash.digest('hex');
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof LocalDeploymentConfigurationError) throw error;
|
||||||
|
return configurationError('evolved target database is unavailable');
|
||||||
|
} finally {
|
||||||
|
buffer.fill(0);
|
||||||
|
if (descriptor !== undefined) fs.closeSync(descriptor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function serviceManagerStoppedPath(
|
function serviceManagerStoppedPath(
|
||||||
command: Readonly<LocalReconciliationCapturePrepareCommand>,
|
command: Readonly<LocalReconciliationCapturePrepareCommand>,
|
||||||
): string {
|
): string {
|
||||||
@@ -177,7 +237,15 @@ function serviceManagerStoppedRecord(
|
|||||||
export function proveLocalReconciliationStoppedState(
|
export function proveLocalReconciliationStoppedState(
|
||||||
command: Readonly<LocalReconciliationCapturePrepareCommand>,
|
command: Readonly<LocalReconciliationCapturePrepareCommand>,
|
||||||
uid: number,
|
uid: number,
|
||||||
|
options: Readonly<LocalReconciliationStoppedProofOptions> = {},
|
||||||
): Readonly<LocalReconciliationStoppedProof> {
|
): Readonly<LocalReconciliationStoppedProof> {
|
||||||
|
const expectedEvolvedTargetSha256 = options.expectedEvolvedTargetSha256;
|
||||||
|
if (
|
||||||
|
expectedEvolvedTargetSha256 !== undefined &&
|
||||||
|
!DIGEST_PATTERN.test(expectedEvolvedTargetSha256)
|
||||||
|
) {
|
||||||
|
configurationError('evolved target snapshot digest is invalid');
|
||||||
|
}
|
||||||
const persisted =
|
const persisted =
|
||||||
command.request.stoppedAuthority === 'docker'
|
command.request.stoppedAuthority === 'docker'
|
||||||
? dockerStoppedEvidence(command)
|
? dockerStoppedEvidence(command)
|
||||||
@@ -198,11 +266,42 @@ export function proveLocalReconciliationStoppedState(
|
|||||||
},
|
},
|
||||||
uid,
|
uid,
|
||||||
);
|
);
|
||||||
if (
|
const exactStoppedData =
|
||||||
current.disposition !== 'reconciliation_required' ||
|
expectedEvolvedTargetSha256 === undefined &&
|
||||||
(persisted !== undefined &&
|
current.disposition === 'reconciliation_required' &&
|
||||||
persisted.evidenceDigest !== current.evidenceDigest)
|
(persisted === undefined ||
|
||||||
) {
|
persisted.evidenceDigest === current.evidenceDigest);
|
||||||
|
let evolvedStoppedData = false;
|
||||||
|
if (expectedEvolvedTargetSha256 !== undefined) {
|
||||||
|
const currentSha256 = stableTargetSha256(
|
||||||
|
command.request.targetDatabasePath,
|
||||||
|
uid,
|
||||||
|
);
|
||||||
|
const confirmed = readTargetDataReconciliationEvidenceForPaths(
|
||||||
|
{
|
||||||
|
profile: command.request.profile,
|
||||||
|
activationPath: command.request.activationPath,
|
||||||
|
legacySourcePath: command.request.legacySourcePath,
|
||||||
|
targetDatabasePath: command.request.targetDatabasePath,
|
||||||
|
expectedActivationDigest: command.request.expectedActivationDigest,
|
||||||
|
...(adoptedTargetBaseline === undefined
|
||||||
|
? {}
|
||||||
|
: { adoptedTargetBaseline }),
|
||||||
|
},
|
||||||
|
uid,
|
||||||
|
);
|
||||||
|
evolvedStoppedData =
|
||||||
|
current.disposition === 'reconciliation_required' &&
|
||||||
|
current.sourceMatchesActivation === true &&
|
||||||
|
current.sourceSidecarsClear === true &&
|
||||||
|
current.targetSidecarsClear === true &&
|
||||||
|
current.targetMatchesActivation === false &&
|
||||||
|
(current.baselineKind !== 'adopted_target' ||
|
||||||
|
current.targetMatchesBaseline === false) &&
|
||||||
|
currentSha256 === expectedEvolvedTargetSha256 &&
|
||||||
|
confirmed.evidenceDigest === current.evidenceDigest;
|
||||||
|
}
|
||||||
|
if (!exactStoppedData && !evolvedStoppedData) {
|
||||||
configurationError(
|
configurationError(
|
||||||
'stopped data does not have exact reconciliation-required evidence',
|
'stopped data does not have exact reconciliation-required evidence',
|
||||||
);
|
);
|
||||||
@@ -211,6 +310,9 @@ export function proveLocalReconciliationStoppedState(
|
|||||||
stoppedAuthority: command.request.stoppedAuthority,
|
stoppedAuthority: command.request.stoppedAuthority,
|
||||||
stoppedRecordDigest: command.request.expectedStoppedRecordDigest,
|
stoppedRecordDigest: command.request.expectedStoppedRecordDigest,
|
||||||
reconciliationEvidenceDigest: current.evidenceDigest,
|
reconciliationEvidenceDigest: current.evidenceDigest,
|
||||||
|
...(expectedEvolvedTargetSha256 === undefined
|
||||||
|
? {}
|
||||||
|
: { evolvedTargetSha256: expectedEvolvedTargetSha256 }),
|
||||||
});
|
});
|
||||||
return Object.freeze({ ...payload, proofDigest: cutoverDigest(payload) });
|
return Object.freeze({ ...payload, proofDigest: cutoverDigest(payload) });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1444,6 +1444,7 @@ async function secretConfigPlanFixture(t, options = {}) {
|
|||||||
options.applicationId ?? '00000000-0000-4000-8000-000000000423',
|
options.applicationId ?? '00000000-0000-4000-8000-000000000423',
|
||||||
reviewSuffix: `secret-config-${suffix}`,
|
reviewSuffix: `secret-config-${suffix}`,
|
||||||
createDefaultSidecars: false,
|
createDefaultSidecars: false,
|
||||||
|
targetInsideDeploymentRoot: true,
|
||||||
initializeDatabases: secretConfigDatabaseInitializer({
|
initializeDatabases: secretConfigDatabaseInitializer({
|
||||||
active: options.active === true,
|
active: options.active === true,
|
||||||
configs: options.configs === true,
|
configs: options.configs === true,
|
||||||
@@ -4278,6 +4279,129 @@ test('Secret/Config plan follows applied Automation and preserved Run History on
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
assert.equal(verified.status, 'verified');
|
assert.equal(verified.status, 'verified');
|
||||||
|
|
||||||
|
const secretConfigDecisionRoot = path.join(
|
||||||
|
path.dirname(state.captureRoot),
|
||||||
|
'cross-domain-secret-config-decision',
|
||||||
|
);
|
||||||
|
fs.mkdirSync(secretConfigDecisionRoot, { mode: 0o700 });
|
||||||
|
const candidates = fs
|
||||||
|
.readFileSync(
|
||||||
|
path.join(secretConfigRoot, secretConfigId, 'plan.ndjson'),
|
||||||
|
'utf8',
|
||||||
|
)
|
||||||
|
.trimEnd()
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => JSON.parse(line))
|
||||||
|
.filter(
|
||||||
|
(record) =>
|
||||||
|
record.kind ===
|
||||||
|
'qinglong3-local-reconciliation-secret-config-plan-candidate',
|
||||||
|
);
|
||||||
|
const decisionState = {
|
||||||
|
...state,
|
||||||
|
planned,
|
||||||
|
secretConfigRoot,
|
||||||
|
secretConfigId,
|
||||||
|
secretConfigCommand,
|
||||||
|
secretConfigDecisionRoot,
|
||||||
|
candidates,
|
||||||
|
};
|
||||||
|
const secretConfigDecisionId = '019b0000-0000-7000-8000-000000000432';
|
||||||
|
const decisionPrepareCommand = secretConfigDecisionPrepareCommand(
|
||||||
|
decisionState,
|
||||||
|
secretConfigDecisionId,
|
||||||
|
);
|
||||||
|
const decisionPrepared = await prepareLocalReconciliationSecretConfigDecision(
|
||||||
|
decisionPrepareCommand,
|
||||||
|
);
|
||||||
|
const decisionFile = secretConfigDecisionFile(
|
||||||
|
decisionState,
|
||||||
|
{ result: decisionPrepared },
|
||||||
|
[
|
||||||
|
{
|
||||||
|
disposition: 'apply_active_binding',
|
||||||
|
reason: 'reviewed_active_binding',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'cross-domain-active-binding',
|
||||||
|
);
|
||||||
|
const decisionCommit = secretConfigDecisionCommitFixture(
|
||||||
|
decisionState,
|
||||||
|
{
|
||||||
|
result: decisionPrepared,
|
||||||
|
commandOptions: decisionPrepareCommand.options,
|
||||||
|
},
|
||||||
|
decisionFile.filePath,
|
||||||
|
);
|
||||||
|
const decision = await commitLocalReconciliationSecretConfigDecision(
|
||||||
|
decisionCommit.command,
|
||||||
|
decisionCommit.dependencies,
|
||||||
|
);
|
||||||
|
assert.equal(decision.outcome, 'ready');
|
||||||
|
assert.equal(decision.applyBindingCount, 1);
|
||||||
|
|
||||||
|
const secretKeyringPath = path.join(
|
||||||
|
state.deploymentRoot,
|
||||||
|
'cross-domain-local-secret-keyring.json',
|
||||||
|
);
|
||||||
|
await provisionLocalSecretKeyring(secretKeyringPath);
|
||||||
|
const secretConfigApplyRoot = path.join(
|
||||||
|
path.dirname(state.captureRoot),
|
||||||
|
'cross-domain-secret-config-apply',
|
||||||
|
);
|
||||||
|
fs.mkdirSync(secretConfigApplyRoot, { mode: 0o700 });
|
||||||
|
const appliedAtMs = decisionCommit.command.request.committedAtMs + 1;
|
||||||
|
const applied = await applyLocalReconciliationSecretConfig(
|
||||||
|
{
|
||||||
|
schemaVersion: 1,
|
||||||
|
operation: 'local.deployment.reconciliation.secret-config.apply',
|
||||||
|
options: {
|
||||||
|
...decisionPrepareCommand.options,
|
||||||
|
secretConfigApplyRoot,
|
||||||
|
targetDatabasePath: state.targetDatabasePath,
|
||||||
|
secretKeyringPath,
|
||||||
|
ownerPepperKeyringDirectory:
|
||||||
|
state.command.options.ownerPepperKeyringDirectory,
|
||||||
|
credentialFilePath: state.command.options.credentialFilePath,
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
decisionId: secretConfigDecisionId,
|
||||||
|
secretConfigId,
|
||||||
|
expectedDecisionDigest: decision.decisionDigest,
|
||||||
|
expectedHeadDigest: decision.instanceHeadDigest,
|
||||||
|
mutationId: '00000000-0000-4000-8000-000000000433',
|
||||||
|
requestId: 'cross-domain-secret-config-apply',
|
||||||
|
appliedAtMs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
async openAuthenticationDatabase() {
|
||||||
|
return { async close() {} };
|
||||||
|
},
|
||||||
|
async authenticate(_database, authenticationOptions) {
|
||||||
|
const authenticatedAtMs = authenticationOptions.now();
|
||||||
|
return {
|
||||||
|
principal: {
|
||||||
|
subject: { type: 'user', id: 'review-owner' },
|
||||||
|
authenticationId: 'reconcile_secret_config_apply:test',
|
||||||
|
authenticatedAtMs,
|
||||||
|
expiresAtMs: authenticatedAtMs + 60 * 60 * 1_000,
|
||||||
|
assurance: 'local_console',
|
||||||
|
},
|
||||||
|
databaseFence: {
|
||||||
|
credentialId: 'review-owner',
|
||||||
|
credentialVersion: 1,
|
||||||
|
pepperKeyId: 'review-owner-v1',
|
||||||
|
pepperVersion: 1,
|
||||||
|
},
|
||||||
|
async confirm() {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert.equal(applied.state, 'reconciliation_secret_config_applied');
|
||||||
|
assert.equal(applied.activeBindingCount, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Secret/Config decision reauthenticates the same reviewer, seals exact candidates and verifies content-free', async (t) => {
|
test('Secret/Config decision reauthenticates the same reviewer, seals exact candidates and verifies content-free', async (t) => {
|
||||||
@@ -4515,10 +4639,7 @@ test('Secret/Config apply publishes encrypted material atomically and recovers e
|
|||||||
},
|
},
|
||||||
async authenticate(_database, options) {
|
async authenticate(_database, options) {
|
||||||
authentications += 1;
|
authentications += 1;
|
||||||
assert.match(
|
assert.match(options.authenticationNamespace, /^[a-z][a-z0-9_]{0,31}$/);
|
||||||
options.authenticationNamespace,
|
|
||||||
/^[a-z][a-z0-9_]{0,31}$/,
|
|
||||||
);
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
options.authenticationNamespace,
|
options.authenticationNamespace,
|
||||||
'reconcile_secret_config_apply',
|
'reconcile_secret_config_apply',
|
||||||
@@ -4561,6 +4682,22 @@ test('Secret/Config apply publishes encrypted material atomically and recovers e
|
|||||||
),
|
),
|
||||||
/authentication or Secret material must be below deploymentRoot/,
|
/authentication or Secret material must be below deploymentRoot/,
|
||||||
);
|
);
|
||||||
|
await assert.rejects(
|
||||||
|
applyLocalReconciliationSecretConfig(
|
||||||
|
{
|
||||||
|
...applyCommand,
|
||||||
|
options: {
|
||||||
|
...applyOptions,
|
||||||
|
targetDatabasePath: path.join(
|
||||||
|
path.dirname(state.deploymentRoot),
|
||||||
|
'outside-target.sqlite',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
applyDependencies,
|
||||||
|
),
|
||||||
|
/targetDatabasePath must be below deploymentRoot/,
|
||||||
|
);
|
||||||
for (const boundary of ['afterMaterialPublished']) {
|
for (const boundary of ['afterMaterialPublished']) {
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
applyLocalReconciliationSecretConfig(applyCommand, {
|
applyLocalReconciliationSecretConfig(applyCommand, {
|
||||||
|
|||||||
Reference in New Issue
Block a user