/** * **A module that takes a shared-cache grant presents the login it was granted** (novox/hq 081). * * The cache provider scopes each consumer to an ACL user of its own, confined to keys under its * login (novox/hq 080). A consumer that hands its software the password and not the login logs in * as the server's default user: the grant is honoured by the provider and ignored by the consumer, * and nothing notices while the default user is open. The grant bed proves the provider's half * against a consumer written to the contract; this holds every catalogue module that asks for the * cache to its half — the login, as `${bound:redis-cache:as}`, somewhere it hands its software. * * What it cannot see is whether the software also keeps its keys under that login: that is the * software's, and a module whose software cannot (fixed key or channel names in its code) does not * take the shared cache at all — baserow runs its own, and n8n in its shipped mode needs none. */ import { test } from "node:test"; import assert from "node:assert/strict"; import { existsSync, readdirSync, readFileSync } from "node:fs"; import { resolve } from "node:path"; import { catalogueIsPresent, catalogueDir } from "./integration/harness.ts"; const CACHE = "redis-cache"; /** What a module hands its software: every file it writes, and every container's environment and * arguments. A comment, a `why`, or a declared exception is not handed to anything. */ function handedToSoftware(manifest: string): string[] { const m = JSON.parse(manifest) as { resources?: Record[] }; const out: string[] = []; for (const r of m.resources ?? []) { if (typeof r["content"] === "string") out.push(r["content"] as string); if (r["env"] && typeof r["env"] === "object") out.push(...Object.values(r["env"] as Record).map(String)); if (Array.isArray(r["args"])) out.push(...(r["args"] as unknown[]).map(String)); } return out; } /** Whether a manifest hands its software the login it is granted for the cache. */ function presentsTheLogin(manifest: string): boolean { const login = `\${bound:${CACHE}:as}`; return handedToSoftware(manifest).some((given) => given.includes(login)); } test("the check sees a module that hands its software the password and not the login", () => { const passwordOnly = JSON.stringify({ module: "m", requires: [CACHE], resources: [ { id: "env", type: "file", path: "/x", content: `HOST=\${bound:${CACHE}:at}\nPASSWORD=\${secret:${CACHE}}\n` }] }); const withLogin = JSON.stringify({ module: "m", requires: [CACHE], resources: [ { id: "env", type: "file", path: "/x", content: `USER=\${bound:${CACHE}:as}\nPASSWORD=\${secret:${CACHE}}\n` }] }); assert.equal(presentsTheLogin(passwordOnly), false); assert.equal(presentsTheLogin(withLogin), true); // Named only where no software reads it — a declared reason — is not presenting it. const onlyInAReason = JSON.stringify({ module: "m", requires: [CACHE], resources: [ { id: "srv", type: "container", name: "s", image: "x", env: { PASSWORD: `\${secret:${CACHE}}` }, "secrets-in-environment": `the login is \${bound:${CACHE}:as}` }] }); assert.equal(presentsTheLogin(onlyInAReason), false); }); test("every catalogue module that takes the shared cache presents the login it was granted", (t) => { const absent = catalogueIsPresent(); if (absent) { t.skip(`cannot check — ${absent}`); return; } const offences: string[] = []; for (const d of readdirSync(catalogueDir(), { withFileTypes: true })) { const file = resolve(catalogueDir(), d.name, "module.json"); if (!d.isDirectory() || !existsSync(file)) continue; const text = readFileSync(file, "utf8"); const m = JSON.parse(text) as { requires?: string[] }; if (!(m.requires ?? []).includes(CACHE)) continue; if (!presentsTheLogin(text)) offences.push(d.name); } assert.deepEqual(offences, [], `these take the shared cache and never hand their software the login (\${bound:${CACHE}:as}), so they ` + `log in as the server's default user — present the login, or run a cache of their own:\n ${offences.join("\n ")}`); });