bliss/proof_plumbing_owner_checks.js

84 lines
3.9 KiB
JavaScript
Raw Normal View History

2026-08-22 11:29:04 -04:00
// Proof: the /plumbing read API only ever hands out an artifact to a caller who
// may see the structure it BELONGS TO — including via the id-only endpoints the
// live editor uses, and even when a private db is aliased into a public one.
//
// It mounts the REAL plumbing.js router on a throwaway express app (temp db),
// with a tiny middleware that fakes a logged-in session from an ?as= query, and
// makes real HTTP requests. So the assertions exercise the shipped access logic.
const fs = require("fs");
const os = require("os");
const path = require("path");
const express = require("express");
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "bliss-plumbing-"));
fs.mkdirSync(path.join(tmp, "dbs"));
process.chdir(tmp);
const PROJECT = "/home/happy/code/bliss";
const model = require(path.join(PROJECT, "db.js"));
model.applyMigrations();
// ---- scenario ------------------------------------------------------------
const alice = model.createUser("alice", "x"); // member of the private structure
const bob = model.createUser("bob", "x"); // outsider
// PRIVATE structure owned by alice: its own route, template, and db.
const priv = model.createStructure("private-vault", alice);
const privRoute = model.createRoute("GET", "/secret", priv, "handler(){}");
const privTemplate = model.createTemplate(priv, "secret_tpl", "top secret", "");
const secretsDbId = model.createDb(priv, "secrets");
model.setStructurePrivacy(priv, true, alice);
// PUBLIC structure owned by bob that ALIASES alice's private db under "secrets".
const pub = model.createStructure("public-front", bob);
model.attachDb(pub, secretsDbId, "secrets");
// ---- app: real plumbing router, faked session from ?as= ------------------
const app = express();
app.use((req, _res, next) => {
const as = req.query.as;
req.session = { userId: as === "alice" ? alice : as === "bob" ? bob : null };
next();
});
app.use("/plumbing", require(path.join(PROJECT, "plumbing.js")));
const server = app.listen(0);
const port = server.address().port;
async function get(url) {
const res = await fetch(`http://127.0.0.1:${port}${url}`);
return res.status;
}
function expect(label, actual, wanted) {
const ok = actual === wanted;
console.log(` ${ok ? "PASS" : "FAIL"} | ${label} (got ${actual}, want ${wanted})`);
return ok;
}
(async () => {
const results = [];
console.log("\nid-only endpoints (what the live editor calls):");
results.push(expect("alice reads her private route ", await get(`/plumbing/routes/${privRoute}?as=alice`), 200));
results.push(expect("bob reads alice's private route ", await get(`/plumbing/routes/${privRoute}?as=bob`), 404));
results.push(expect("anon reads alice's private route ", await get(`/plumbing/routes/${privRoute}`), 404));
results.push(expect("alice reads her private template ", await get(`/plumbing/templates/${privTemplate}?as=alice`), 200));
results.push(expect("bob reads alice's private template", await get(`/plumbing/templates/${privTemplate}?as=bob`), 404));
console.log("\nstructure-scoped endpoints reject id-laundering:");
// bob CAN see his public structure, but the route id belongs to the private one.
results.push(expect("bob: pub structure + private routeId", await get(`/plumbing/structures/${pub}/routes/${privRoute}?as=bob`), 404));
console.log("\naliased private db is not leaked through a public structure:");
results.push(expect("bob reads aliased private db ", await get(`/plumbing/structures/${pub}/dbs/${secretsDbId}?as=bob`), 404));
results.push(expect("alice reads that db via her structure", await get(`/plumbing/structures/${priv}/dbs/${secretsDbId}?as=alice`), 200));
const pass = results.every(Boolean);
console.log(`\n${pass ? "PROVEN" : "FAILED"}: plumbing gates every artifact against its own structure's access rules.`);
server.close();
fs.rmSync(tmp, { recursive: true, force: true });
process.exit(pass ? 0 : 1);
})();