feat(selfhost): show sanitized update logs in admin UI
All checks were successful
Build and Release FEDEO / verify (push) Successful in 48s
Build and Release FEDEO / build-and-release (push) Successful in 5m45s

This commit is contained in:
2026-09-25 18:42:26 +02:00
parent 8f06d06037
commit 29155f916a
9 changed files with 398 additions and 9 deletions

View File

@@ -30,11 +30,22 @@ test("all update endpoints require a current global admin, including after revoc
assert.equal((await f.app.inject({ url: "/admin/updates/status" })).statusCode, 202);
f.revoke();
for (const [method, url] of [["GET", "/admin/updates/status"], ["GET", `/admin/updates/requests/${id}`],
["GET", `/admin/updates/requests/${id}/logs`],
["POST", "/admin/updates/check"], ["POST", "/admin/updates/apply"]] as const) {
assert.equal((await f.app.inject({ method, url, headers, payload: method === "POST" ? { requestId: id } : undefined })).statusCode, 403);
}
assert.equal(f.calls.length, 1);
});
test("admin log access uses the fixed request route and does not cache responses", async t => {
const f = await fixture(t);
const response = await f.app.inject({ url: `/admin/updates/requests/${id}/logs` });
assert.equal(response.statusCode, 202);
assert.equal(response.headers["cache-control"], "no-store");
assert.deepEqual(f.calls[0], ["GET", `/v1/requests/${id}/logs`, undefined]);
assert.equal((await f.app.inject({ url: "/admin/updates/requests/bad.id/logs" })).statusCode, 400);
const anonymous = await fixture(t, true, false);
assert.equal((await anonymous.app.inject({ url: `/admin/updates/requests/${id}/logs` })).statusCode, 401);
});
test("unauthenticated and cookie-only requests cannot start jobs", async t => {
const anonymous = await fixture(t, true, false);
assert.equal((await anonymous.app.inject({ url: "/admin/updates/status" })).statusCode, 401);
@@ -117,3 +128,19 @@ test("Unix client bounds response size, deadline, and protocol shape", async t =
});
}
});
test("Unix client validates bounded log responses and their request association", async t => {
let payload: any = { requestId: id, logs: [{ source: "commands", content: "pull access denied", truncated: true,
path: "/private/commands.log" }], private: "omitted" };
const client = await socketFixture(t, (_req, res) => res.end(JSON.stringify(payload)));
const response = await client("GET", `/v1/requests/${id}/logs`);
assert.deepEqual(response.body, { requestId: id, logs: [{ source: "commands", content: "pull access denied", truncated: true }] });
for (const invalid of [
{ ...payload, requestId: "different-request" },
{ ...payload, logs: [{ source: "env", content: "secret", truncated: false }] },
{ ...payload, logs: [{ source: "commands", content: "x".repeat(6001), truncated: false }] },
]) {
payload = invalid;
await assert.rejects(client("GET", `/v1/requests/${id}/logs`), { code: "UPDATER_PROTOCOL" });
}
await assert.rejects(client("GET", `/v1/requests/${id}/logs?path=/etc/passwd`), { code: "INVALID_REQUEST" });
});