bliss/proof_private_db.js

89 lines
3.6 KiB
JavaScript
Raw Normal View History

2026-08-22 11:29:04 -04:00
// Proof: a private structure's DB cannot be mounted by a handler running for a
// user who isn't a member — even when that DB is aliased into a PUBLIC structure.
//
// It drives the REAL model code from db.js (canAccessStructure, getDbsForStructure,
// attachDb, ...) against a throwaway database in a temp dir, and reproduces the
// gate from index.js:192-211 (makeLibs) verbatim so the throw is the app's, not ours.
const fs = require("fs");
const os = require("os");
const path = require("path");
// db.js opens "./dbs/0.sqlite" and writes user dbs under "./dbs" relative to CWD.
// Point CWD at a fresh temp dir so we never touch the real data.
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "bliss-proof-"));
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();
// ---- the exact gate from index.js makeLibs (lines 192-211), copied verbatim ----
// Given the structure being served and the requesting user, return a db(alias)
// resolver identical to what a route handler receives via require('db').
function makeDbResolver(structureId, memberUserId) {
const dbs = {};
const forbidden = new Set();
for (let appDb of model.getDbsForStructure(structureId)) {
const ownDb = String(appDb.db_struct_id) === String(structureId);
if (!ownDb && !model.canAccessStructure(memberUserId, appDb.db_struct_id)) {
forbidden.add(appDb.alias);
continue;
}
model.getDbInstance(appDb.id); // would-mount
}
return (alias) => {
if (forbidden.has(alias)) {
throw new Error(`db '${alias}' is private; you do not have access`);
}
return dbs[alias];
};
}
function attempt(label, structureId, userId, alias) {
try {
makeDbResolver(structureId, userId)(alias);
console.log(` MOUNTED | ${label}`);
return "mounted";
} catch (e) {
console.log(` BLOCKED | ${label} -> ${e.message}`);
return "blocked";
}
}
// ---- scenario ----
const alice = model.createUser("alice", "x"); // member of the private structure
const bob = model.createUser("bob", "x"); // outsider
// PRIVATE structure owned by alice, with its own DB "secrets".
const priv = model.createStructure("private-vault", alice);
const secretsDbId = model.createDb(priv, "secrets");
model.setStructurePrivacy(priv, true, alice); // flips private=1, keeps alice a member
// PUBLIC structure that ALIASES the private DB in under the same name.
const pub = model.createStructure("public-front", bob);
model.attachDb(pub, secretsDbId, "secrets");
console.log(`\nprivate structure #${priv} owns db #${secretsDbId} ("secrets"), private=${model.getStructure(priv).private}`);
console.log(`public structure #${pub} aliases that same db in as "secrets", private=${model.getStructure(pub).private}\n`);
console.log("Serving the PUBLIC structure, handler calls require('db')('secrets'):");
const r1 = attempt("as bob (outsider) ", pub, bob, "secrets");
const r2 = attempt("as anonymous / WS (userId=null)", pub, null, "secrets");
const r3 = attempt("as alice (member of private)", pub, alice, "secrets");
console.log("\nSanity — serving the PRIVATE structure itself (its OWN db, ownDb=true):");
const r4 = attempt("as alice (member) ", priv, alice, "secrets");
// ---- assertions ----
const pass =
r1 === "blocked" && r2 === "blocked" && r3 === "mounted" && r4 === "mounted";
console.log(
`\n${pass ? "PROVEN" : "FAILED"}: aliasing a private db into a public structure does NOT leak it — ` +
`only users who can access the owning structure can mount it.`,
);
fs.rmSync(tmp, { recursive: true, force: true });
process.exit(pass ? 0 : 1);