diff --git a/tests/unit/authz/public-policy.test.ts b/tests/unit/authz/public-policy.test.ts index 45e816dcce..527801476e 100644 --- a/tests/unit/authz/public-policy.test.ts +++ b/tests/unit/authz/public-policy.test.ts @@ -47,11 +47,13 @@ function cliCtx(overrides: Partial = {}): PolicyContex }; } -test("publicPolicy stamps the local-CLI subject for a valid loopback machine token", async (t) => { - if (!getMachineTokenSync()) { - t.skip("machine-id unavailable in this environment"); - return; - } +test("publicPolicy stamps the local-CLI subject for a valid loopback machine token", async () => { + // No environment guard here on purpose: this and the negative control below are + // the only tests covering the loopback branch added for check:pack-boot, and a + // conditional skip would silence them exactly where the coverage matters. The + // sibling management-policy tests in authz/routeGuard.test.ts call this same + // helper unguarded, so an empty token is a broken environment worth failing on. + assert.ok(getMachineTokenSync(), "machine token must resolve for this suite to mean anything"); const out = await publicPolicy.evaluate(cliCtx()); assert.equal(out.allow, true); if (out.allow) { @@ -60,11 +62,8 @@ test("publicPolicy stamps the local-CLI subject for a valid loopback machine tok } }); -test("publicPolicy keeps anonymous for a non-loopback peer carrying the token", async (t) => { - if (!getMachineTokenSync()) { - t.skip("machine-id unavailable in this environment"); - return; - } +test("publicPolicy keeps anonymous for a non-loopback peer carrying the token", async () => { + assert.ok(getMachineTokenSync(), "machine token must resolve for this suite to mean anything"); const out = await publicPolicy.evaluate(cliCtx({ ip: "203.0.113.7" })); assert.equal(out.allow, true); if (out.allow) assert.equal(out.subject.kind, "anonymous"); diff --git a/tests/unit/route-guard-private-lan.test.ts b/tests/unit/route-guard-private-lan.test.ts index a19bed6d5f..f70074c59f 100644 --- a/tests/unit/route-guard-private-lan.test.ts +++ b/tests/unit/route-guard-private-lan.test.ts @@ -95,17 +95,38 @@ test("management policy must NOT derive locality from the spoofable Host header" join(import.meta.dirname, "../../src/server/authz/policies/management.ts"), "utf8" ); + // `requestPeerAddress` and friends moved to authz/peerContext.ts when the PUBLIC + // policy had to share the very same verdict (check:pack-boot / #11040 follow-up). + // The guard follows the implementation instead of the filename: neither module may + // read the Host header, and the module that OWNS peer resolution must resolve the + // token-stamped peer IP. + const peerSrc = readFileSync( + join(import.meta.dirname, "../../src/server/authz/peerContext.ts"), + "utf8" + ); // Regression guard: a prior fix read the client-controlled Host header for the // LOCAL_ONLY decision, letting `Host: 127.0.0.1` bypass the gate. Locality must // come from the token-stamped peer IP instead. + for (const [name, text] of [ + ["management.ts", src], + ["peerContext.ts", peerSrc], + ] as const) { + assert.ok( + !text.includes('get?.("host")') && !text.includes('get("host")'), + `${name} must NOT read the Host header for locality` + ); + } assert.ok( - !src.includes('get?.("host")') && !src.includes('get("host")'), - "requestPeerAddress must NOT read the Host header" - ); - assert.ok( - src.includes("resolveStampedPeer") && src.includes("PEER_IP_HEADER"), + peerSrc.includes("resolveStampedPeer") && peerSrc.includes("PEER_IP_HEADER"), "requestPeerAddress must resolve the trusted token-stamped peer IP" ); + // Positive anchor: management.ts must still route its locality decision through + // the shared helpers, so this guard cannot pass by the policy quietly growing its + // own Host-based path again. + assert.ok( + src.includes("peerContext") && src.includes("isLoopbackRequest"), + "management policy must delegate locality to authz/peerContext" + ); }); // ── resolveStampedPeer: the auth boundary that replaces Host-header trust ──