118 lines
7.6 KiB
JavaScript
118 lines
7.6 KiB
JavaScript
"use strict";
|
|
|
|
const test = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const crypto = require("node:crypto");
|
|
const fs = require("node:fs");
|
|
const os = require("node:os");
|
|
const path = require("node:path");
|
|
const { createApp } = require("./server");
|
|
|
|
async function withServer(run, extra = {}) {
|
|
const mail = [];
|
|
const app = createApp({
|
|
requestToken: "request-only-secret",
|
|
ownerEmail: "owner@example.invalid",
|
|
publicBaseUrl: "https://example.invalid/authz",
|
|
targets: ["JD-FD-PRIMARY", "BS-GZ-006"],
|
|
actions: { "server-login": ["read-navigation-map", "inspect-services"], "server-ops": ["read-navigation-map", "inspect-services"], "repo-push": ["read-navigation-map", "push-repository"] },
|
|
stateFile: "",
|
|
sendEmail: async (message) => { mail.push(message); return true; },
|
|
...extra,
|
|
});
|
|
await new Promise((resolve) => app.listen(0, "127.0.0.1", resolve));
|
|
const base = `http://127.0.0.1:${app.address().port}`;
|
|
try { await run({ base, mail }); } finally { await new Promise((resolve) => app.close(resolve)); }
|
|
}
|
|
|
|
test("work order sends an opaque approval link and can be claimed once", async () => {
|
|
await withServer(async ({ base, mail }) => {
|
|
const recipientFingerprint = crypto.createHash("sha256").update("owner@example.invalid").digest("hex");
|
|
const requested = await fetch(`${base}/api/workorders`, {
|
|
method: "POST",
|
|
headers: { authorization: "Bearer request-only-secret", "content-type": "application/json" },
|
|
body: JSON.stringify({ persona_id: "ICE-GL-ZY001", persona_name: "铸渊", target: "JD-FD-PRIMARY", scope: "server-login", action: "read-navigation-map", recipient_fingerprint: recipientFingerprint }),
|
|
});
|
|
assert.equal(requested.status, 201);
|
|
const order = await requested.json();
|
|
assert.equal(mail.length, 1);
|
|
assert.equal(mail[0].to, "owner@example.invalid");
|
|
assert.doesNotMatch(JSON.stringify(order), /approvalToken/i);
|
|
|
|
const approvalPath = new URL(mail[0].approvalUrl).pathname.replace("/authz", "");
|
|
const page = await fetch(`${base}${approvalPath}`);
|
|
assert.equal(page.status, 200);
|
|
assert.match(await page.text(), /JD-FD-PRIMARY/);
|
|
|
|
const approved = await fetch(`${base}${approvalPath}`, { method: "POST" });
|
|
assert.equal(approved.status, 200);
|
|
const claimed = await fetch(`${base}/api/workorders/${order.workorder_id}/claim`, {
|
|
method: "POST",
|
|
headers: { authorization: `Bearer ${order.claim_token}` },
|
|
});
|
|
assert.equal(claimed.status, 200);
|
|
const session = await claimed.json();
|
|
assert.equal(session.expires_in, 3600);
|
|
|
|
const secondClaim = await fetch(`${base}/api/workorders/${order.workorder_id}/claim`, { method: "POST", headers: { authorization: `Bearer ${order.claim_token}` } });
|
|
assert.equal(secondClaim.status, 403);
|
|
});
|
|
});
|
|
|
|
test("navigation map acknowledgement is mandatory before other registered actions", async () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "lake-lamp-server-map-"));
|
|
const mapsDir = path.join(dir, "maps"); fs.mkdirSync(mapsDir);
|
|
fs.writeFileSync(path.join(mapsDir, "JD-FD-PRIMARY.json"), JSON.stringify({ node_id: "JD-FD-PRIMARY", modules: [{ code: "JD-GTW-01" }] }));
|
|
try {
|
|
await withServer(async ({ base, mail }) => {
|
|
const fingerprint = crypto.createHash("sha256").update("owner@example.invalid").digest("hex");
|
|
const requested = await fetch(`${base}/api/workorders`, { method: "POST", headers: { authorization: "Bearer request-only-secret", "content-type": "application/json" }, body: JSON.stringify({ persona_id: "ICE-GL-ZY001", target: "JD-FD-PRIMARY", scope: "server-login", action: "read-navigation-map", recipient_fingerprint: fingerprint }) });
|
|
const order = await requested.json();
|
|
const approvalPath = new URL(mail[0].approvalUrl).pathname.replace("/authz", "");
|
|
await fetch(`${base}${approvalPath}`, { method: "POST" });
|
|
const claimed = await fetch(`${base}/api/workorders/${order.workorder_id}/claim`, { method: "POST", headers: { authorization: `Bearer ${order.claim_token}` } });
|
|
const session = await claimed.json();
|
|
const common = { persona_id: "ICE-GL-ZY001", target: "JD-FD-PRIMARY", scope: "server-login" };
|
|
const locked = await fetch(`${base}/api/session/verify`, { method: "POST", headers: { authorization: `Bearer ${session.session_token}`, "content-type": "application/json" }, body: JSON.stringify({ ...common, action: "inspect-services" }) });
|
|
assert.equal(locked.status, 423);
|
|
const mapResponse = await fetch(`${base}/api/navigation-map/read`, { method: "POST", headers: { authorization: `Bearer ${session.session_token}`, "content-type": "application/json" }, body: JSON.stringify(common) });
|
|
const map = await mapResponse.json();
|
|
const ack = await fetch(`${base}/api/navigation-map/ack`, { method: "POST", headers: { authorization: `Bearer ${session.session_token}`, "content-type": "application/json" }, body: JSON.stringify({ ...common, map_hash: map.map_hash }) });
|
|
assert.equal(ack.status, 200);
|
|
const unlocked = await fetch(`${base}/api/session/verify`, { method: "POST", headers: { authorization: `Bearer ${session.session_token}`, "content-type": "application/json" }, body: JSON.stringify({ ...common, action: "inspect-services" }) });
|
|
assert.equal(unlocked.status, 200);
|
|
}, { mapsDir, mapStateFile: path.join(dir, "acks.json") });
|
|
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test("request endpoint rejects direct email target switching and unknown actions", async () => {
|
|
await withServer(async ({ base }) => {
|
|
const common = { method: "POST", headers: { authorization: "Bearer request-only-secret", "content-type": "application/json" } };
|
|
const directEmail = await fetch(`${base}/api/workorders`, { ...common, body: JSON.stringify({ persona_id: "ICE-GL-ZY001", target: "JD-FD-PRIMARY", scope: "server-login", action: "read-navigation-map", email: "attacker@example.invalid" }) });
|
|
assert.equal(directEmail.status, 400);
|
|
const unknown = await fetch(`${base}/api/workorders`, { ...common, body: JSON.stringify({ persona_id: "ICE-GL-ZY001", target: "JD-FD-PRIMARY", scope: "server-login", action: "shell" }) });
|
|
assert.equal(unknown.status, 400);
|
|
});
|
|
});
|
|
|
|
test("session verification rejects switching servers without new approval", async () => {
|
|
await withServer(async ({ base, mail }) => {
|
|
const fingerprint = crypto.createHash("sha256").update("owner@example.invalid").digest("hex");
|
|
const requested = await fetch(`${base}/api/workorders`, {
|
|
method: "POST", headers: { authorization: "Bearer request-only-secret", "content-type": "application/json" },
|
|
body: JSON.stringify({ persona_id: "ICE-GL-ZY001", target: "BS-GZ-006", scope: "server-ops", action: "read-navigation-map", recipient_fingerprint: fingerprint }),
|
|
});
|
|
const order = await requested.json();
|
|
const approvalPath = new URL(mail[0].approvalUrl).pathname.replace("/authz", "");
|
|
await fetch(`${base}${approvalPath}`, { method: "POST" });
|
|
const claimed = await fetch(`${base}/api/workorders/${order.workorder_id}/claim`, { method: "POST", headers: { authorization: `Bearer ${order.claim_token}` } });
|
|
const session = await claimed.json();
|
|
const switched = await fetch(`${base}/api/session/verify`, {
|
|
method: "POST", headers: { authorization: `Bearer ${session.session_token}`, "content-type": "application/json" },
|
|
body: JSON.stringify({ persona_id: "ICE-GL-ZY001", target: "JD-FD-PRIMARY", scope: "server-ops", action: "read-navigation-map" }),
|
|
});
|
|
assert.equal(switched.status, 403);
|
|
assert.equal((await switched.json()).error, "target_mismatch");
|
|
});
|
|
});
|