diff --git a/SOURCE_MANIFEST.txt b/SOURCE_MANIFEST.txt index 922a3d5..3e00822 100644 --- a/SOURCE_MANIFEST.txt +++ b/SOURCE_MANIFEST.txt @@ -114,8 +114,8 @@ abe5dd6fd68f2970cd19ef134094907c67219061d8fe9a1a08324c78de4ad437 484 f018383f755352ca448e2ebb1e19b1dba412a3eb793d61e64b02953e300754fd 10538 Publish-ForgeFlow-Release.ps1 688fff7d2c989adb97ebb7fae38962656b70304a0aa5d27433c56adf7f136de0 4196 Publish-Missing-Binary-Release.ps1 42e2f375c2fa9bea138428b85eb2bb93fe8bf6e4a5fc0cd728f2c6713a019df7 10315 README.md -76f3dc50b3cd60f6a078f3e37ac8fb1c7e5d5dd746000d9458c1e7189dc54804 14947 reports/architecture-audit.json -94915d088fcb8f0e66e31006283de5a43749c4028c64887305a420fa8162565e 1343 reports/architecture-audit.md +64d4c37123a87704e584cd40b3cdadbbece768ca36cc6f10c6666f266be056c7 14947 reports/architecture-audit.json +1ed7d86361db5adbb71f1dd042f66e62e6a2d0b774db59bcb5f215b5996c17b5 1343 reports/architecture-audit.md 509c7bcff5280349bd9f45ed6151f70372bad7010a9ea582c13e2ccab91fe0cd 6272 scripts/acceptance.mjs d0745072321aca2c80f44460974a7926715a9f429164aaf7660dced40b52c736 4790 scripts/apply-binary-update.ps1 404863bcbe7292355662e3a326455df864d7279badc29f90866a3b837420df54 10745 scripts/apply-source-update.ps1 @@ -161,7 +161,7 @@ c629efa832a806409954ab5a4986bf88606ce5fa5278bb1885a76222d1cb6faf 6441 52b6d88ed1f5c904a13cdde92e5f96d1e2b5971ceef49862152197353cdc6490 27928 src/main/server-inventory.cjs c7921a2380e305abdabb94dc0d04c6060eaa09bbe01f18994786f20bfa02d0c3 15107 src/main/ssh-service.cjs 19538a3c40ea3489bbaee9a23af36a5e99962af6bb3d04259f05ece6588cbeb2 25901 src/main/unraid-access-methods.cjs -5ba5a90ad9b7a0d05dd64fa64968cbdd7271bec4df6971f9685a9dd411e4fad4 9536 src/main/unraid-deploy-key-host.cjs +5621e35323e4f81fb14a05670f81579ec1e66bea3a55fa6457ece0f807421424 9801 src/main/unraid-deploy-key-host.cjs 6d9910dace52625f88e066a8485af2663c3735ff15e9ce9031441ce742710a21 30793 src/main/unraid-deployment-methods.cjs 673b1692e7c2b5197545df98750b5d048bddf44206263e25be4f17d9bf900e2c 17208 src/main/unraid-deployment-service.cjs a2fa253bb2664ad7e430d7a4cd4c4434b89e6c54c19c7b9e4f53fe08485339d3 42652 src/main/unraid-inventory-methods.cjs @@ -209,7 +209,7 @@ a4e5947204ff6878e601e32477bc85b53cd0153baf95a161c8935b6e5466c257 1155 ac17f8bbe9e388b80abef7792c8b184a1fd482c93f13d23a478e433961020f75 17214 tests/config-store.test.mjs f1463326aee79842d265687ae628189ce54e92544600f2bd14073780287cfb14 2502 tests/configuration-backup.test.mjs 144c8e217a334cd69f502938d944e0f2dac61703d5df47e287b9ed542918c779 8129 tests/dependency-wiring.test.mjs -caab598cd563454ed4281bfa89785eb4795f39da7fdb92b82ca73112714406f0 10033 tests/deploy-key-host.test.mjs +aad5948ea374d1e56e777005c73639654c96a90364dd398c949052cf5ae343a2 11130 tests/deploy-key-host.test.mjs b7e009fed4171d6dd6b4c3154ba1d3f7198e98f5b79b298687841fc8169447cd 9354 tests/deploy-key-lifecycle.test.mjs 49bf9cf9842e7899015013675208f83a95402065a082320927a677ee4bab0766 24875 tests/deployment-operations.test.mjs 1dc6477bd07de78be189e6e8195ec339eb9d75820c4dbd5b073b8520ee21f6b5 1938 tests/deployment-policy.test.mjs diff --git a/src/main/unraid-deploy-key-host.cjs b/src/main/unraid-deploy-key-host.cjs index 705f8af..3add84f 100644 --- a/src/main/unraid-deploy-key-host.cjs +++ b/src/main/unraid-deploy-key-host.cjs @@ -50,7 +50,10 @@ class UnraidDeployKeyHost { const f = parseMarker((await this.execute(server, script, { timeout: 45_000, maxOutput: 256 * 1024 })).stdout, marker); return { ready: /^[0-9a-f]{40}$/i.test(f.remoteSha || ""), remoteSha: f.remoteSha || null, fingerprint: f.fingerprint || null, hostFingerprint: f.hostFingerprint || null }; } - async preflightCandidate(context) { const proof = await this.verifyCandidate(context); if (!proof.ready) throw new Error("Candidate preflight did not prove the remote branch."); return proof; } + // A caller that just verified this candidate passes its proof in. Re-running + // `git ls-remote` would open a second SSH connection to ask the same question, + // with nothing in between that could change the answer. + async preflightCandidate(context) { const proof = context?.proof?.remoteSha ? context.proof : await this.verifyCandidate(context); if (!proof.ready) throw new Error("Candidate preflight did not prove the remote branch."); return proof; } async promote({ repository, server, candidate }) { const p = this.paths(repository, server); const c = candidate.paths; await this.execute(server, `test -s ${shellQuote(c.privateKey)}; test -s ${shellQuote(c.publicKey)}; test -s ${shellQuote(c.knownHosts)}; cp -p ${shellQuote(c.privateKey)} ${shellQuote(p.privateKey)}.new; cp -p ${shellQuote(c.publicKey)} ${shellQuote(p.publicKey)}.new; cp -p ${shellQuote(c.knownHosts)} ${shellQuote(p.knownHosts)}.new; mv ${shellQuote(p.privateKey)}.new ${shellQuote(p.privateKey)}; mv ${shellQuote(p.publicKey)}.new ${shellQuote(p.publicKey)}; mv ${shellQuote(p.knownHosts)}.new ${shellQuote(p.knownHosts)}`); diff --git a/tests/deploy-key-host.test.mjs b/tests/deploy-key-host.test.mjs index 1a78fb5..1e54772 100644 --- a/tests/deploy-key-host.test.mjs +++ b/tests/deploy-key-host.test.mjs @@ -105,6 +105,28 @@ test("candidate verification only reports ready on a real remote commit", async await assert.rejects(() => unproven.host.preflightCandidate(context), /did not prove the remote branch/); }); +test("a preflight reuses a proof it was handed instead of asking the server again", async () => { + const reused = keyHost("__FORGEFLOW_KEY_PROOF__\nremoteSha=\nfingerprint=\nhostFingerprint=\n"); + const proof = { ready: true, remoteSha: "f".repeat(40), fingerprint: "SHA256:new", hostFingerprint: "SHA256:host" }; + const context = { + repository: { ...REPOSITORY, sshUrl: "git@gitea.example:Jens/Portfolio.git" }, + profile: { branch: "main" }, + server: SERVER, + candidate: { paths: { privateKey: "/k/deploy-key", publicKey: "/k/deploy-key.pub", knownHosts: "/k/known_hosts" } }, + proof, + }; + + assert.deepEqual(await reused.host.preflightCandidate(context), proof); + assert.equal(reused.scripts.length, 0, "no second connection is opened"); + + // A proof that never established a remote commit is not a shortcut. + await assert.rejects( + () => reused.host.preflightCandidate({ ...context, proof: { ready: false } }), + /did not prove the remote branch/, + ); + assert.equal(reused.scripts.length, 1, "an unusable proof falls back to verifying"); +}); + test("verifying the active key uses the repository-scoped paths rather than a candidate", async () => { const { host, scripts } = keyHost(`__FORGEFLOW_KEY_PROOF__\nremoteSha=${"e".repeat(40)}\nfingerprint=SHA256:active\nhostFingerprint=SHA256:host\n`); const paths = host.paths(REPOSITORY, SERVER);