// What holds influxdb to the `influxdb-api` provision (grants.ts): one v1 authorization per consumer, // under the username and password the mesh gave, allowed only what the consumer contributed; made // once and brought back on every apply; buckets created when missing and never deleted; and an // authorization the mesh did not make — same name or not — never adopted, changed or deleted. // // InfluxDB is a fake: the routes the module touches, answering with the status codes and shapes // InfluxDB 2.9 gives (a filter matching nothing is a 404, a password outside 8–72 characters a 400, // an inactive authorization or a wrong password a 401 on /query). Run against the compiled module // (npm test builds first), the way the runtime loads it. import { test, after, beforeEach } from "node:test"; import assert from "node:assert/strict"; import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; import { InfluxDBClient } from "../dist/client.js"; import { ApiGrants, MARK, askedFor, marked } from "../dist/grants.js"; type Rec = Record; const ADMIN = "operator-token"; const orgs = new Map([["zurag", "org1"]]); let buckets: Rec[] = []; let auths: Rec[] = []; let calls: string[] = []; let seq = 0; function body(req: IncomingMessage): Promise { return new Promise((resolve) => { let raw = ""; req.on("data", (c) => (raw += c)); req.on("end", () => resolve(raw ? JSON.parse(raw) : undefined)); }); } function send(res: ServerResponse, status: number, value?: unknown): void { res.writeHead(status, { "Content-Type": "application/json" }); res.end(value === undefined ? "" : JSON.stringify(value)); } const server = createServer(async (req, res) => { const url = new URL(req.url!, "http://fake"); const p = url.pathname; calls.push(`${req.method} ${p}`); if (p === "/query") { const basic = (req.headers.authorization ?? "").replace(/^Basic /, ""); const [u, pw] = Buffer.from(basic, "base64").toString().split(":"); const a = auths.find((x) => x.token === u); if (!a || a.status !== "active" || a.password === undefined || a.password !== pw) { return send(res, 401, { code: "unauthorized", message: "Unauthorized" }); } return send(res, 200, { results: [{ statement_id: 0 }] }); } if (req.headers.authorization !== `Token ${ADMIN}`) return send(res, 401, { code: "unauthorized" }); if (p === "/api/v2/orgs") { const id = orgs.get(url.searchParams.get("org") ?? ""); if (!id) return send(res, 404, { code: "not found", message: "organization name not found" }); return send(res, 200, { orgs: [{ id, name: url.searchParams.get("org") }] }); } if (p === "/api/v2/buckets" && req.method === "GET") { const found = buckets.filter((b) => b.orgID === url.searchParams.get("orgID") && b.name === url.searchParams.get("name")); if (found.length === 0) return send(res, 404, { code: "not found", message: "bucket not found" }); return send(res, 200, { buckets: found }); } if (p === "/api/v2/buckets" && req.method === "POST") { const b = { ...(await body(req)), id: `b${++seq}` }; buckets.push(b); return send(res, 201, b); } if (p === "/private/legacy/authorizations" && req.method === "GET") { const found = auths.filter((a) => a.token === url.searchParams.get("token")); if (found.length === 0) return send(res, 404, { code: "not found", message: "authorization not found" }); // Never answers with the password: InfluxDB keeps only its hash. return send(res, 200, { authorizations: found.map(({ password, ...a }) => ({ ...a, links: {} })) }); } if (p === "/private/legacy/authorizations" && req.method === "POST") { const a = await body(req); if (auths.some((x) => x.token === a.token)) return send(res, 409, { code: "conflict", message: "token already exists" }); const made = { ...a, id: `a${++seq}`, status: a.status ?? "active" }; auths.push(made); return send(res, 201, made); } const m = /^\/private\/legacy\/authorizations\/([^/]+)(\/password)?$/.exec(p); const a = m && auths.find((x) => x.id === m[1]); if (!a) return send(res, 404, { code: "not found" }); if (m![2] && req.method === "POST") { const { password } = await body(req); if (typeof password !== "string" || password.length < 8 || password.length > 72) { return send(res, 400, { code: "invalid", message: "passwords must be between 8 and 72 characters long" }); } a.password = password; return send(res, 204); } if (req.method === "PATCH") { Object.assign(a, await body(req)); return send(res, 200, a); } if (req.method === "DELETE") { auths = auths.filter((x) => x !== a); return send(res, 204); } send(res, 405); }); await new Promise((r) => server.listen(0, "127.0.0.1", r)); after(() => server.close()); const port = (server.address() as { port: number }).port; const grants = new ApiGrants(new InfluxDBClient(`http://127.0.0.1:${port}`, ADMIN, "zurag"), "zurag"); const PW = "mesh-minted-password-of-forty-characters"; /** Grafana on ace, as the mesh hands it to the provisioner. */ function grafana(password = PW, values: Record = { access: "read" }) { return { as: "mesh_ace_grafana", password, consumer: "ace", values }; } /** Node-RED on ace: writes one bucket. */ function nodered(password = PW, values: Record = { access: "write", buckets: ["zurag"] }) { return { as: "mesh_ace_nodered", password, consumer: "ace", values }; } function only(token: string): Rec { const found = auths.filter((a) => a.token === token); assert.equal(found.length, 1, `exactly one authorization ${token}, found ${found.length}`); return found[0]; } function perms(a: Rec): string[] { return a.permissions.map((p: Rec) => `${p.action}:${p.resource.type}:${p.resource.id ?? "*"}`).sort(); } beforeEach(() => { buckets = [{ id: "zb", orgID: "org1", name: "zurag" }]; auths = []; calls = []; }); test("what a contribution may ask for, and what is refused", () => { assert.deepEqual(askedFor({}), { access: "read", buckets: [] }); assert.deepEqual(askedFor({ access: "read-write", buckets: ["b", "a", "a"] }), { access: "read-write", buckets: ["a", "b"] }); assert.throws(() => askedFor({ access: "admin" }), /access/); assert.throws(() => askedFor({ access: "write" }), /names no bucket/); assert.throws(() => askedFor({ buckets: "zurag" }), /list of bucket names/); assert.throws(() => askedFor({ access: "write", buckets: ["_monitoring"] }), /system bucket/); }); test("a reader is given one authorization, reading every bucket of the org, under the mesh's password", async () => { assert.equal(await grants.ensure(grafana()), "created"); const a = only("mesh_ace_grafana"); assert.equal(a.orgID, "org1"); assert.equal(a.status, "active"); assert.ok(a.description.startsWith(MARK)); assert.deepEqual(perms(a), ["read:buckets:*"]); assert.equal(a.password, PW); assert.equal(await grants.holds(grafana()), true); }); test("a writer is allowed its own buckets only, and a missing one is made — never deleted", async () => { assert.equal(await grants.ensure(nodered(PW, { access: "write", buckets: ["zurag", "printer"] })), "created"); const made = buckets.find((b) => b.name === "printer"); assert.ok(made, "the missing bucket was created"); assert.deepEqual(made!.retentionRules, [], "kept for ever: retention is the operator's choice"); assert.deepEqual(perms(only("mesh_ace_nodered")), [`write:buckets:${made!.id}`, "write:buckets:zb"]); assert.equal(await grants.remove("mesh_ace_nodered"), "removed"); assert.equal(buckets.length, 2, "withdrawing the consumer leaves every bucket and its data"); }); test("applying the same grant again writes nothing", async () => { await grants.ensure(grafana()); calls = []; assert.equal(await grants.ensure(grafana()), "unchanged"); assert.ok(calls.every((c) => c.startsWith("GET")), `only reads: ${calls.join(", ")}`); only("mesh_ace_grafana"); }); test("a rotated password is set in place; a changed access remakes only the mesh's own", async () => { await grants.ensure(nodered()); const id = only("mesh_ace_nodered").id; assert.equal(await grants.holds(nodered("rotated-password-0123456789")), false); assert.equal(await grants.ensure(nodered("rotated-password-0123456789")), "updated"); assert.equal(only("mesh_ace_nodered").id, id, "updated, not replaced"); assert.equal(await grants.holds(nodered("rotated-password-0123456789")), true); await grants.ensure(nodered(PW, { access: "read-write", buckets: ["zurag"] })); assert.deepEqual(perms(only("mesh_ace_nodered")), ["read:buckets:zb", "write:buckets:zb"]); assert.equal(await grants.holds(nodered(PW, { access: "read-write", buckets: ["zurag"] })), true); }); test("an authorization disabled, re-passworded or deleted behind the mesh's back is not held, and is made whole", async () => { await grants.ensure(grafana()); only("mesh_ace_grafana").status = "inactive"; assert.equal(await grants.holds(grafana()), false); assert.equal(await grants.ensure(grafana()), "updated"); assert.equal(await grants.holds(grafana()), true); only("mesh_ace_grafana").password = "somebody-else-set-this"; assert.equal(await grants.holds(grafana()), false); await grants.ensure(grafana()); assert.equal(await grants.holds(grafana()), true); auths = []; assert.equal(await grants.holds(grafana()), false); assert.equal(await grants.ensure(grafana()), "created"); }); test("holds only reads, and a bucket gone missing is not held rather than made", async () => { await grants.ensure(nodered()); buckets = []; calls = []; assert.equal(await grants.holds(nodered()), false); assert.ok(calls.every((c) => c.startsWith("GET")), `only reads: ${calls.join(", ")}`); assert.equal(buckets.length, 0); }); test("an authorization of the same name the mesh did not make is refused, and left exactly as it was", async () => { auths = [{ id: "theirs", token: "mesh_ace_grafana", orgID: "org1", status: "active", description: "hand-made", permissions: [{ action: "write", resource: { type: "buckets", orgID: "org1" } }], password: "their-password" }]; const before = JSON.stringify(auths); await assert.rejects(grants.ensure(grafana()), /did not make/); assert.equal(JSON.stringify(auths), before); assert.ok(calls.every((c) => c.startsWith("GET")), `only reads: ${calls.join(", ")}`); assert.equal(await grants.holds(grafana()), false); assert.equal(await grants.remove("mesh_ace_grafana"), "not ours"); assert.equal(auths.length, 1, "never deleted"); }); test("the predecessor's own v1 users and tokens are never touched", async () => { auths = [{ id: "hal", token: "grafana", orgID: "org1", status: "active", description: "", permissions: [{ action: "read", resource: { type: "buckets", orgID: "org1" } }], password: "old-password" }]; await grants.ensure(grafana()); assert.equal(auths.find((a) => a.id === "hal")!.password, "old-password"); assert.equal(await grants.remove("grafana"), "not ours"); assert.equal(marked({ token: "grafana", description: `${MARK} x` }), false, "the mark needs the mesh's name too"); }); test("an org the instance does not have, or a non-mesh name, makes nothing", async () => { const elsewhere = new ApiGrants(new InfluxDBClient(`http://127.0.0.1:${port}`, ADMIN, "nope"), "nope"); await assert.rejects(elsewhere.ensure(grafana()), /no org "nope"/); await assert.rejects(grants.ensure({ ...grafana(), as: "grafana" }), /not a mesh identity/); assert.equal(auths.length, 0); }); test("a withdrawn consumer's authorization is removed, and an absent one is not an error", async () => { await grants.ensure(grafana()); assert.equal(await grants.remove("mesh_ace_grafana"), "removed"); assert.equal(auths.length, 0); assert.equal(await grants.remove("mesh_ace_grafana"), "absent"); });