/* ===================================================================== pruef-verteilen.mjs — Wer darf Aufgaben an andere geben? Filipe, 20.09.2026: "jeder soll sich selber aufgabe vergeben koennen aber nur die rechte hand und dogfather aufgaben an andere verteilen." --------------------------------------------------------------------- WARUM DAS EINE EIGENE PRUEFUNG BRAUCHT Die Regel hat ZWEI Seiten, und beide muessen dasselbe sagen: * Der Server lehnt ab, wer nicht darf. * Die Seite zeigt die Felder nur dem, der darf. Stimmt nur die erste, sieht die rechte Hand Felder, die nichts bewirken. Stimmt nur die zweite, kommt jeder durch, der die Schnittstelle direkt anspricht. Beides ist schon vorgekommen. UND DIE GEFAEHRLICHE STELLE: `hand` steht bewusst NICHT in `istLeitung` -- diese Menge wird an 57 Stellen gefragt, unter anderem im vertraulichen Meldeweg. Die Pruefung haelt fest, dass das Verteilen eine EIGENE Regel ist und nicht versehentlich zur Leitung wird. AUFRUF: node server/pruef-verteilen.mjs ===================================================================== */ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { request as httpAnfrage } from "node:http"; import { portMussFreiSein } from "./helfer-port.mjs"; import { notbremse } from "./helfer-notbremse.mjs"; const PORT = await portMussFreiSein(4433, "pruef-verteilen"); const ordner = mkdtempSync(join(tmpdir(), "ws-verteilen-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); process.env.PORT = String(PORT); process.env.SITE_ACCESS_SECRET = "lokaler-test"; process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; const express = (await import("express")).default; const ec = express.response.cookie; express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; await import("./index.js"); notbremse(180_000, "pruef-verteilen"); await new Promise((r) => setTimeout(r, 900)); process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); }); process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); }); let fehler = 0, geprueft = 0; const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; const melde = (t) => console.log(t); console.log("\n=== Wer darf Aufgaben an andere geben? ==="); /* ---- 1. Die Regel selbst, ohne Server ----------------------------- */ melde("\n=== 1. Die Regel ==="); { const { darfAufgabenVerteilen, istLeitung } = await import("./workspace.js"); const darf = ["admin", "hand", "spicy", "manager"]; const nicht = ["modi", "scout", "creator", "gast"]; ok(darf.every((r) => darfAufgabenVerteilen({ rolle: r })), `verteilen duerfen: ${darf.join(", ")}`); ok(nicht.every((r) => !darfAufgabenVerteilen({ rolle: r })), `und nicht: ${nicht.join(", ")}`); /* DIE WICHTIGE ABGRENZUNG: Verteilen ist NICHT Leitung. Waere die rechte Hand in `istLeitung` gerutscht, saehe sie damit auch vertrauliche Meldungen ueber Menschen -- eine Entscheidung, die niemand getroffen hat. */ ok(!istLeitung({ rolle: "hand" }), "die rechte Hand darf verteilen, ist aber NICHT „Leitung“ — das bleibt getrennt"); ok(darfAufgabenVerteilen({ rolle: undefined }) === false && darfAufgabenVerteilen(null) === false, "Gegenprobe: ohne Rolle darf niemand"); } /* ---- 2. Durch die Schnittstelle ----------------------------------- */ const { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); const jetzt = new Date().toISOString(); let idDogi, idHand, idModi, idModi2; { const d = new DatabaseSync(process.env.WORKSPACE_DB); const k = randomBytes(32).toString("hex"); d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von) VALUES (?,?,?,NULL)" + " ON CONFLICT(schluessel) DO NOTHING").run("code_kennung_schluessel", k, jetzt); const anlegen = (name, rolle, code) => { const salt = randomBytes(16).toString("hex"); const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)" + " VALUES (?,?,?,?,?,?,1,?)") .run(name, rolle, hash, salt, 32768, createHmac("sha256", k).update(code).digest("hex"), jetzt); return d.prepare("SELECT last_insert_rowid() AS id").get().id; }; idDogi = anlegen("Filipe", "admin", "CODE-A"); idHand = anlegen("VanVan", "hand", "CODE-H"); idModi = anlegen("Diene", "modi", "CODE-M"); idModi2 = anlegen("Ghost", "modi", "CODE-M2"); d.close(); } const CREW = "crew.dogfather-universe.com"; function ruf(pfad, keks, methode = "GET", koerper = null) { return new Promise((fertig, schief) => { const rumpf = koerper ? JSON.stringify(koerper) : null; const a = httpAnfrage({ host: "127.0.0.1", port: PORT, path: pfad, method: methode, headers: { Host: CREW, ...(keks ? { Cookie: keks } : {}), ...(rumpf ? { "Content-Type": "application/json", Origin: `https://${CREW}`, "Content-Length": Buffer.byteLength(rumpf) } : {}), }, }, (antwort) => { let text = ""; antwort.on("data", (x) => { text += x; }); antwort.on("end", () => fertig({ code: antwort.statusCode, text, keks: [].concat(antwort.headers["set-cookie"] || []).map((c) => c.split(";")[0]).join("; "), })); }); a.on("error", schief); if (rumpf) a.write(rumpf); a.end(); }); } const json = (a) => { try { return JSON.parse(a.text); } catch { return {}; } }; const anmelden = async (rolle, code) => (await ruf("/workspace/api/anmelden", null, "POST", { rolle, code })).keks; melde("\n=== 2. Durch die Schnittstelle ==="); const kDogi = await anmelden("admin", "CODE-A"); const kHand = await anmelden("hand", "CODE-H"); const kModi = await anmelden("modi", "CODE-M"); ok(!!kDogi && !!kHand && !!kModi, "DogFather, rechte Hand und ein Modi sind angemeldet"); /* Sagt der Server es auch der Seite? */ for (const [name, keks, soll] of [["DogFather", kDogi, true], ["rechte Hand", kHand, true], ["Modi", kModi, false]]) { const ich = json(await ruf("/workspace/api/ich", keks)); ok(ich.darf_verteilen === soll, `„${name}“ bekommt darf_verteilen = ${ich.darf_verteilen} (erwartet ${soll})`); } /* Und die Tat: eine Aufgabe FUER JEMAND ANDEREN. */ async function anlegenFuer(keks, zielId, titel) { const a = await ruf("/workspace/api/aufgaben", keks, "POST", { titel, verantwortlich_id: zielId, prioritaet: "mittel" }); if (a.code !== 201) return { code: a.code, verant: null }; const d = new DatabaseSync(process.env.WORKSPACE_DB); const z = d.prepare("SELECT verantwortlich_id FROM aufgaben WHERE id = ?").get(json(a).id); d.close(); return { code: a.code, verant: z?.verantwortlich_id ?? null }; } const vonHand = await anlegenFuer(kHand, idModi, "Von der rechten Hand an Diene"); ok(vonHand.code === 201 && vonHand.verant === idModi, `die rechte Hand gibt eine Aufgabe an Diene (${vonHand.code}, liegt bei ${vonHand.verant})`); const vonDogi = await anlegenFuer(kDogi, idModi2, "Von DogFather an Ghost"); ok(vonDogi.code === 201 && vonDogi.verant === idModi2, `DogFather ebenso (${vonDogi.code}, liegt bei ${vonDogi.verant})`); /* DIE ENTSCHEIDENDE GEGENPROBE: Ein Modi versucht dasselbe. Die Aufgabe wird NICHT abgelehnt -- sie landet bei ihm selbst. Das ist freundlicher als ein Fehler und genau das, was Filipe wollte: "jeder soll sich selber aufgabe vergeben koennen". */ const vonModi = await anlegenFuer(kModi, idModi2, "Versuch eines Modi"); ok(vonModi.code === 201 && vonModi.verant === idModi, `Gegenprobe: ein Modi kann es versuchen — die Aufgabe landet bei ihm selbst ` + `(${vonModi.verant}, nicht ${idModi2})`); console.log(`\n${fehler ? fehler + " FEHLER" : "Alles in Ordnung"} — ${geprueft} Prüfungen\n`); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } process.exit(fehler ? 1 : 0);