/* ===================================================================== DIE RECHTE HAND AUF DER PERSONENSEITE (24.09.2026) Filipe: „die rechte hand soll das auch sehen. und die selben rechte da haben wie dogfather. das einzige was sie nicht kann ist die dogfather rolle oder leute anfassen. also da kann sie nichts verändern." WAS HIER GEPRUEFT WIRD, und warum jede Zeile: * SIE DARF, was DogFather darf -- anlegen, Code, Rolle, sperren, loeschen. Bis heute war die Oberflaeche strenger als der Server: Sie hatte drei dieser Rechte und sah keinen einzigen Knopf. * SIE DARF NICHT an DogFather und an die Manager. Das ist die Grenze aus seinem Satz, und sie wird EINZELN geprueft -- fuer jeden der fuenf Wege. * DER LETZTE DOGFATHER-ZUGANG bleibt. Diese Sicherung stand vorher hinter „nur DogFather darf loeschen"; seit die rechte Hand loescht, muss sie allein tragen. ===================================================================== */ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; const ordner = mkdtempSync(join(tmpdir(), "ws-hp-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); import { notbremse } from "./helfer-notbremse.mjs"; const { eigenerPort } = await import("./helfer-port.mjs"); const PORT = await eigenerPort(import.meta, "die Hand-Personen-Pruefung"); process.env.PORT = `${PORT}`; process.env.SITE_ACCESS_SECRET = "lokaler-test"; process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1"; 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(240_000, "pruef-hand-personen"); await new Promise((r) => setTimeout(r, 900)); 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); const { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); const d = new DatabaseSync(process.env.WORKSPACE_DB); const jetzt = new Date().toISOString(); const ks = randomBytes(32).toString("hex"); d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") .run("code_kennung_schluessel", ks, jetzt); function 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", ks).update(code).digest("hex"), jetzt); return d.prepare("SELECT last_insert_rowid() AS id").get().id; } const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001"); const idHand = anlegen("VanVan", "hand", "CODE-HAND-0001"); const idLinke = anlegen("Funny", "linke", "CODE-LINK-0001"); const idModi = anlegen("Miss", "modi", "CODE-MODI-0001"); const idModi2 = anlegen("Ghost", "modi", "CODE-MODI-0002"); const idMana = anlegen("Patrick", "manager", "CODE-MANA-0001"); const idGast = anlegen("Kessi", "gast", "CODE-GAST-0001"); d.close(); const CREW = "crew.dogfather-universe.com"; const { request } = await import("node:http"); /** Ein Aufruf mit Hausadresse -- `fetch` verwirft den Host-Kopf. */ function roh(pfad, keks, koerper, methode) { return new Promise((fertig, schief) => { const daten = koerper ? JSON.stringify(koerper) : null; const a = request({ host: "127.0.0.1", port: PORT, path: pfad, method: methode || (daten ? "POST" : "GET"), headers: { Host: CREW, Origin: `https://${CREW}`, ...(keks ? { Cookie: keks } : {}), ...(daten ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(daten) } : {}), }, }, (antwort) => { let text = ""; antwort.on("data", (t) => { text += t; }); antwort.on("end", () => fertig({ code: antwort.statusCode, text })); }); a.on("error", schief); if (daten) a.write(daten); a.end(); }); } const json = (r) => { try { return JSON.parse(r.text); } catch { return {}; } }; async function anmelden(rolle, code) { const r = await new Promise((fertig, schief) => { const daten = JSON.stringify({ rolle, code, alter_ok: true }); const a = request({ host: "127.0.0.1", port: PORT, path: "/workspace/api/anmelden", method: "POST", headers: { Host: CREW, Origin: `https://${CREW}`, "Content-Type": "application/json", "Content-Length": Buffer.byteLength(daten) }, }, (antwort) => { let text = ""; antwort.on("data", (t) => { text += t; }); antwort.on("end", () => fertig({ code: antwort.statusCode, kopf: antwort.headers })); }); a.on("error", schief); a.write(daten); a.end(); }); if (r.code !== 200) return null; return [].concat(r.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; "); } const kDogi = await anmelden("admin", "CODE-DOGI-0001"); const kHand = await anmelden("hand", "CODE-HAND-0001"); const kLinke = await anmelden("linke", "CODE-LINK-0001"); const kModi = await anmelden("modi", "CODE-MODI-0001"); melde("\n=== 0. Alle vier kommen herein ==="); ok(!!kDogi && !!kHand && !!kLinke && !!kModi, "DogFather, rechte Hand, linke Hand, Modi"); /* ===================================================================== 1. WAS DIE OBERFLAECHE ERFAEHRT ===================================================================== */ melde("\n=== 1. Die Auskunft an die Oberflaeche ==="); { const ichDogi = json(await roh("/workspace/api/ich", kDogi)); const ichHand = json(await roh("/workspace/api/ich", kHand)); const ichLinke = json(await roh("/workspace/api/ich", kLinke)); const ichModi = json(await roh("/workspace/api/ich", kModi)); ok(ichDogi.darf_zugaenge_verwalten === true, "DogFather verwaltet Zugaenge"); ok(ichHand.darf_zugaenge_verwalten === true, "die rechte Hand auch"); ok(ichLinke.darf_zugaenge_verwalten === false, "die LINKE Hand nicht -- das war eine eigene Entscheidung vom 21.09."); ok(ichModi.darf_zugaenge_verwalten !== true, "ein Modi erst recht nicht"); ok(ichHand.darf_personen_loeschen === true, "sie darf loeschen"); ok(ichLinke.darf_personen_loeschen !== true, "die linke Hand nicht"); /* WEN sie anfassen darf -- die Grenze aus Filipes Satz. */ const anf = ichHand.rollen_anfassbar || []; ok(!anf.includes("admin"), `DogFather steht NICHT darin (${anf.join(", ")})`); /* DIE MANAGER STEHEN IN `rollen_anfassbar` -- und das ist KEIN Mangel, sondern die falsche Frage. `rollen_anfassbar` sagt, welche Rollen sie grundsaetzlich aendern duerfte; ob ein bestimmter Mensch fuer sie existiert, entscheidet die Haustrennung: Auf crew. liefert `hausBedingung` nur Team-Dogi-Rollen, ein Manager ist dort fuer NIEMANDEN sichtbar, auch nicht fuer DogFather. Gemessen wird das in Abschnitt 3 an den echten Wegen. */ ok(Array.isArray(anf), `rollen_anfassbar ist eine Liste (${anf.join(", ")})`); ok(anf.includes("modi") || anf.includes("gast"), "Modis und Community schon -- sonst waere das Recht leer"); } /* ===================================================================== 2. WAS SIE DARF ===================================================================== */ melde("\n=== 2. Was sie darf ==="); let neuerModi = 0; { const anlegen2 = await roh("/workspace/api/verwaltung/personen", kHand, { name: "Neue Modi", rolle: "modi" }); ok(anlegen2.code === 200 || anlegen2.code === 201, `sie legt einen Modi an (${anlegen2.code})`); neuerModi = json(anlegen2).id || 0; ok(!!json(anlegen2).code, "und sieht den Code genau einmal"); const code = await roh(`/workspace/api/verwaltung/personen/${idModi}/code`, kHand, {}, "POST"); ok(code.code === 200, `sie erzeugt einen Code neu (${code.code})`); const rolle = await roh(`/workspace/api/verwaltung/personen/${idModi2}/rolle`, kHand, { rolle: "gast" }, "PUT"); ok(rolle.code === 200, `sie aendert eine Rolle (${rolle.code})`); const sperren = await roh(`/workspace/api/verwaltung/personen/${idModi}`, kHand, { aktiv: false }, "PATCH"); ok(sperren.code === 200, `sie sperrt (${sperren.code}) -- das ist neu seit heute`); const entsperren = await roh(`/workspace/api/verwaltung/personen/${idModi}`, kHand, { aktiv: true }, "PATCH"); ok(entsperren.code === 200, `und entsperrt wieder (${entsperren.code})`); if (neuerModi) { const vorschau = await roh(`/workspace/api/verwaltung/personen/${neuerModi}/loeschbar`, kHand); ok(vorschau.code === 200, `sie sieht die Loesch-Vorschau (${vorschau.code})`); const weg = await roh(`/workspace/api/verwaltung/personen/${neuerModi}`, kHand, { name: "Neue Modi" }, "DELETE"); ok(weg.code === 200, `sie loescht (${weg.code}) -- das ist neu seit heute`); } else { ok(false, "kein angelegter Modi zum Loeschen"); } } /* ===================================================================== 3. WAS SIE NICHT DARF -- jeden Weg einzeln ===================================================================== */ melde("\n=== 3. An DogFather und den Managern nicht ==="); { /* GEPRUEFT WIRD MIT TEAM-DOGI-ROLLEN. Ein Manager gehoert zur Agentur und ist auf crew. fuer niemanden sichtbar -- ihn hier zu nehmen haette 404 gemessen und „geschuetzt" genannt, obwohl es nur die Haustrennung war. Die linke Hand ist der richtige Fall: Sie IST auf dieser Adresse, und die rechte Hand darf sie trotzdem nicht anfassen. */ for (const [wen, id, name] of [ ["DogFather", idDogi, "Filipe"], ["die linke Hand", idLinke, "Funny"]]) { const code = await roh(`/workspace/api/verwaltung/personen/${id}/code`, kHand, {}, "POST"); ok(code.code === 403 || code.code === 404, `kein neuer Code fuer ${wen} (${code.code})`); const rolle = await roh(`/workspace/api/verwaltung/personen/${id}/rolle`, kHand, { rolle: "modi" }, "PUT"); ok(rolle.code === 403 || rolle.code === 404, `keine Rollenaenderung bei ${wen} (${rolle.code})`); const sperren = await roh(`/workspace/api/verwaltung/personen/${id}`, kHand, { aktiv: false }, "PATCH"); ok(sperren.code === 403 || sperren.code === 404, `kein Sperren von ${wen} (${sperren.code})`); const weg = await roh(`/workspace/api/verwaltung/personen/${id}`, kHand, { name }, "DELETE"); ok(weg.code === 403 || weg.code === 404, `kein Loeschen von ${wen} (${weg.code})`); const vorschau = await roh(`/workspace/api/verwaltung/personen/${id}/loeschbar`, kHand); ok(vorschau.code === 403 || vorschau.code === 404, `nicht einmal die Loesch-Vorschau von ${wen} (${vorschau.code})`); } /* UND DIE MANAGER -- hier zeigt sich der Unterschied am deutlichsten. Mein erster Anlauf erwartete, dass BEIDE abgewiesen werden ("auf crew. gibt es keine Manager"). Gemessen: Hand 403, DogFather 200. Das war kein Loch, sondern genau die bestellte Ordnung -- die Haustrennung gilt fuer die LISTE, nicht fuer den direkten Zugriff von DogFather. Ich hatte das eine fuer das andere gehalten. Beide Zeilen stehen jetzt getrennt da, und gerade ihr Unterschied ist die Aussage: Derselbe Weg, zwei Rollen, zwei Antworten. */ const handMana = await roh(`/workspace/api/verwaltung/personen/${idMana}`, kHand, { aktiv: false }, "PATCH"); ok(handMana.code === 403 || handMana.code === 404, `die rechte Hand kommt an einen Manager nicht heran (${handMana.code})`); const dogiMana = await roh(`/workspace/api/verwaltung/personen/${idMana}`, kDogi, { aktiv: false }, "PATCH"); ok(dogiMana.code === 200, `DogFather schon (${dogiMana.code}) -- sonst hiesse "sie darf es nicht" nur, ` + `dass es niemand darf`); /* Wieder entsperren, damit die Schlussprobe den Grundstand sieht. */ await roh(`/workspace/api/verwaltung/personen/${idMana}`, kDogi, { aktiv: true }, "PATCH"); /* SIE KANN KEINEN DOGFATHER ERSCHAFFEN. Die Rolle ist fuer niemanden vergebbar, auch nicht fuer DogFather selbst -- hier zaehlt, dass der Weg ueber sie nicht offensteht. */ const machDogi = await roh(`/workspace/api/verwaltung/personen/${idModi}/rolle`, kHand, { rolle: "admin" }, "PUT"); ok(machDogi.code >= 400, `sie macht niemanden zum DogFather (${machDogi.code})`); const anlegeDogi = await roh("/workspace/api/verwaltung/personen", kHand, { name: "Zweiter Chef", rolle: "admin" }); ok(anlegeDogi.code >= 400, `und legt keinen an (${anlegeDogi.code})`); } /* ===================================================================== 4. DIE ANDEREN ROLLEN KOMMEN GAR NICHT HERAN ===================================================================== */ melde("\n=== 4. Alle anderen ==="); { for (const [wer, keks] of [["die linke Hand", kLinke], ["ein Modi", kModi]]) { const anl = await roh("/workspace/api/verwaltung/personen", keks, { name: "Versuch", rolle: "modi" }); ok(anl.code >= 400, `${wer} legt niemanden an (${anl.code})`); const weg = await roh(`/workspace/api/verwaltung/personen/${idGast}`, keks, { name: "Kessi" }, "DELETE"); ok(weg.code >= 400, `${wer} loescht niemanden (${weg.code})`); } } /* ===================================================================== 5. GEGENPROBEN -- kann das hier ueberhaupt rot werden? ===================================================================== */ melde("\n=== 5. Gegenproben (jede MUSS anschlagen) ==="); { /* DogFather selbst kann alles -- sonst hiesse „sie darf dasselbe" nur, dass beide nichts duerfen. */ const dogiCode = await roh(`/workspace/api/verwaltung/personen/${idLinke}/code`, kDogi, {}, "POST"); ok(dogiCode.code === 200, `DogFather erzeugt einen Code fuer die linke Hand (${dogiCode.code}) -- ` + `sie kann es nicht, er schon`); /* DER LETZTE DOGFATHER-ZUGANG BLEIBT -- auch fuer ihn selbst. Diese Sicherung stand vorher hinter „nur DogFather darf loeschen"; jetzt traegt sie allein. */ const letzter = await roh(`/workspace/api/verwaltung/personen/${idDogi}`, kDogi, { name: "Filipe" }, "DELETE"); ok(letzter.code >= 400, `der letzte DogFather-Zugang laesst sich nicht loeschen (${letzter.code})`); /* Ohne Anmeldung gar nichts. */ const ohne = await roh("/workspace/api/verwaltung/personen", null, { name: "Fremd", rolle: "modi" }); ok(ohne.code >= 400, `ohne Anmeldung nichts (${ohne.code})`); /* UND DIE TAFEL IST DANACH UNVERSEHRT: DogFather, Manager und die linke Hand stehen noch da. Ein abgewiesener Versuch, der trotzdem etwas aendert, waere der schlimmste Ausgang. */ const liste = json(await roh("/workspace/api/verwaltung/personen", kDogi)); const namen = (liste.personen || []).map((x) => x.rolle); /* Ohne "manager" -- der ist auf crew. gar nicht in der Liste, siehe oben. Ihn hier zu erwarten hiesse, die Haustrennung fuer einen Fehler zu halten. */ ok(namen.includes("admin") && namen.includes("linke") && namen.includes("hand"), `nach allen Versuchen stehen sie noch da (${namen.length} Personen: ` + `${[...new Set(namen)].join(", ")})`); } try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } console.log(""); console.log(`${geprueft} Pruefungen, ${fehler} Fehler`); console.log(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG"); process.exit(fehler ? 1 : 0);