diff --git a/server/pruef-modi-verborgen.mjs b/server/pruef-modi-verborgen.mjs index add60da4..7c2dbb36 100644 --- a/server/pruef-modi-verborgen.mjs +++ b/server/pruef-modi-verborgen.mjs @@ -393,13 +393,87 @@ console.log("\n=== 7. Dieselbe Seite mit einem echten Browser ===\n"); await browser.close(); } } -/* ---------- 8. Die Gegenprobe ---------------------------------------- +/* ---------- 8. Was einem Modi GEHOERT, sieht sonst niemand ----------- */ +console.log("\n=== 8. Aufgaben, Einträge und Dateien eines Modis ===\n"); +{ + /* DREI TABELLEN, DREI EIGENE SICHTBARKEITSREGELN. Fuer Personen ist + die Regel zentral (verborgeneIds); fuer ZEILEN gibt es sie dreimal + -- in workspace-aufgaben.js, workspace-bereiche.js und + workspace-dateien.js. Alle drei geben Spicy Media "alles ausser dem, + was DogFather gehoert" -- und ein Modi-Eintrag gehoert ihm nicht. + + Der Kalender ist bewusst nicht dabei: termineSichtbar() gibt jedem + nur Eigenes, dort kann nichts durchsickern. Nachgesehen, nicht + angenommen. */ + const an = await anmelden("creator", "CODE-MODI-0001"); + const MERKMAL = "GEHEIMER MODI-EINTRAG"; + + /* (a) Eine Aufgabe -- ueber den normalen Weg. */ + const t1 = await fetch(BASIS + "/workspace/api/aufgaben", { + method: "POST", headers: { "Content-Type": "application/json", cookie: an.keks }, + body: JSON.stringify({ titel: MERKMAL + " Aufgabe" }), + }); + ok(t1.status === 201, `der Modi legt eine Aufgabe an (${t1.status})`); + + /* (b) Ein Bereichs-Eintrag -- ebenfalls ueber den normalen Weg. */ + const t2 = await fetch(BASIS + "/workspace/api/bereich/live", { + method: "POST", headers: { "Content-Type": "application/json", cookie: an.keks }, + body: JSON.stringify({ art: "vorbereitung", titel: MERKMAL + " Eintrag", text: "nur für das Team" }), + }); + const t2daten = await t2.json().catch(() => ({})); + if (t2.status !== 201 && t2.status !== 200) { + unklar(`kein Bereichs-Eintrag moeglich (${t2.status}: ${JSON.stringify(t2daten)})`); + } else { + ok(true, `und einen Bereichs-Eintrag (${t2.status})`); + } + + /* (c) Eine Datei -- direkt in die Tabelle. Der normale Weg braeuchte + einen echten Datei-Upload; hier geht es nicht um das Hochladen, + sondern allein um die Frage, WER die Zeile danach sieht. */ + { + const dd = new DatabaseSync(process.env.WORKSPACE_DB); + dd.prepare(`INSERT INTO dateien + (name_original, name_datei, groesse, typ, status, creator_id, hochgeladen_von, erstellt) + VALUES (?,?,?,?,'entwurf',NULL,?,?)`) + .run(MERKMAL + " Datei", "modi-probe.txt", 12, "text/plain", idModi1, jetzt); + dd.close(); + ok(true, "und eine Datei liegt auf seinen Namen"); + } + + const WEGE2 = [ + ["Aufgaben", "/workspace/api/aufgaben", (x) => x.aufgaben], + ["Einträge", "/workspace/api/bereich/live", (x) => x.eintraege || x.liste], + ["Dateien", "/workspace/api/dateien", (x) => x.dateien], + ]; + for (const [name, kachel, code, darf] of [ + ["DogFather", "admin", "CODE-DOGI-0001", true], + ["Spicy Media", "spicy", "CODE-SPIC-0001", false], + ["Manager", "manager", "CODE-MAN-0001", false], + ["Scout", "scout", "CODE-SCOU-0001", false], + ["Creator", "creator", "CODE-ANNA-0001", false], + ]) { + const w = await anmelden(kachel, code); + for (const [was, pfad, ziehen] of WEGE2) { + const r = await hole(w.keks, pfad); + if (r.status !== 200) { unklar(`${name.padEnd(12)} ${was.padEnd(9)} kein Zugriff (${r.status})`); continue; } + let liste = []; + try { liste = ziehen(r.daten) || []; } catch { liste = []; } + const drin = liste.some((z) => + String(z.titel || z.name_original || z.name || "").includes(MERKMAL)); + if (darf) ok(true, `${name.padEnd(12)} ${was.padEnd(9)} sieht es (${drin ? "ja" : "nein"})`); + else ok(!drin, `${name.padEnd(12)} ${was.padEnd(9)} ` + + (drin ? "SIEHT ES -- LECK" : "sieht es nicht")); + } + } +} + +/* ---------- 9. Die Gegenprobe ---------------------------------------- Sie steht ABSICHTLICH GANZ UNTEN: Sie haengt eine Person absichtlich aus der Regel aus. Lief sie weiter oben, tauchte diese Person in jeder Liste DANACH auf -- und jeder spaetere Abschnitt maesse einen Zustand, den es im Betrieb nicht gibt. -------------------------------------------------------------------- */ -console.log("\n=== 8. Gegenprobe: kann diese Pruefung ueberhaupt anschlagen? ===\n"); +console.log("\n=== 9. Gegenprobe: kann diese Pruefung ueberhaupt anschlagen? ===\n"); { const d2 = new DatabaseSync(process.env.WORKSPACE_DB); d2.prepare("UPDATE personen SET rolle = 'creator' WHERE id = ?").run(idModi1); @@ -423,6 +497,7 @@ console.log("\n=== 8. Gegenprobe: kann diese Pruefung ueberhaupt anschlagen? === } } + console.log(`\n${fehler ? "FEHLGESCHLAGEN" : "BESTANDEN"} -- ` + `${geprueft} geprueft, ${fehler} Fehler, ${offen} nicht nachsehbar`); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } diff --git a/server/workspace-aufgaben.js b/server/workspace-aufgaben.js index 5c9763f3..865780b4 100644 --- a/server/workspace-aufgaben.js +++ b/server/workspace-aufgaben.js @@ -13,6 +13,7 @@ import { db, protokolliere, echteIp, sitzungLesen, betreutWo, istLeitung, ohneDogFather, heuteLokal, ROLLEN_SORTIERUNG, betreuteIds, externPruefen, externSql, sichtbarePersonenIds, sichtbareCreatorIds, MODI_KATEGORIEN, kategorienFuer, kategoriePersonen, + ohneModi, siehtModis, } from "./workspace.js"; export const aufgabenRouter = express.Router(); @@ -51,9 +52,29 @@ aufgabenRouter.use("/workspace/api", angemeldet); /* ---------- Sichtbarkeit ------------------------------------------------ */ +/* ===================================================================== + DIE UMHUELLUNG: was einem Modi gehoert, faellt hier heraus. + + Sie sitzt UM die eigentliche Regel und nicht darin -- damit sie auch + fuer Regeln gilt, die es heute noch nicht gibt. Beim ersten Anlauf + war nur der eine bekannte Fall geflickt (Spicy Media); das haette den + naechsten Rollenzweig wieder offen gelassen, und niemand haette es + gemerkt, weil an der geaenderten Stelle nichts davon steht. + + DogFather und die Modis gehen unveraendert durch. + ===================================================================== */ +export function sichtbar(person) { + const regel = sichtbarRoh(person); + if (!regel || siehtModis(person)) return regel; + return { + wo: `(${regel.wo}) AND ${ohneModi("a", ["creator_id", "verantwortlich_id", "erstellt_von"])}`, + werte: regel.werte, + }; +} + /* Liefert WHERE-Bedingung und Werte, passend zur Rolle. An genau einer Stelle definiert, damit keine Abfrage sie versehentlich vergisst. */ -export function sichtbar(person) { +function sichtbarRoh(person) { switch (person.rolle) { /* NUR DogFather sieht alles. Ausdruecklich so gewuenscht (01.09.2026) -- und ausdruecklich NUR er. */ diff --git a/server/workspace-bereiche.js b/server/workspace-bereiche.js index b8ae0463..c6fcf737 100644 --- a/server/workspace-bereiche.js +++ b/server/workspace-bereiche.js @@ -18,6 +18,7 @@ import express from "express"; import { db, protokolliere, echteIp, sitzungLesen, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather, siehtAlles, istSpicy, ohneDogFather, externPruefen, externSql, + ohneModi, siehtModis, } from "./workspace.js"; export const bereicheRouter = express.Router(); @@ -249,9 +250,32 @@ for (const weg of ["post", "patch", "delete", "put"]) { bereicheRouter[weg]("/workspace/api/bereich/*", nichtSchreibendFuerCreator); } +/* ===================================================================== + DIE UMHUELLUNG: was einem Modi gehoert, faellt hier heraus. + + Sie sitzt UM die eigentliche Regel und nicht darin -- damit sie auch + fuer Regeln gilt, die es heute noch nicht gibt. Beim ersten Anlauf + war nur der eine bekannte Fall geflickt (Spicy Media); das haette den + naechsten Rollenzweig wieder offen gelassen, und niemand haette es + gemerkt, weil an der geaenderten Stelle nichts davon steht. + + DogFather und die Modis gehen unveraendert durch. Fuer alle anderen + kommt die Bedingung dazu -- auch fuer die, deren Regel ohnehin nichts + Fremdes trifft. Das kostet eine Unterabfrage und spart die Frage, + ob es diesmal wirklich niemand treffen kann. + ===================================================================== */ +export function sichtbar(person) { + const regel = sichtbarRoh(person); + if (!regel || siehtModis(person)) return regel; + return { + wo: `(${regel.wo}) AND ${ohneModi("e", ["creator_id", "erstellt_von"])}`, + werte: regel.werte, + }; +} + /* Scouts haben mit der Creator-Betreuung nichts zu tun -- sie sehen hier nichts. Creator sehen ihren eigenen Bereich, das Management alles. */ -export function sichtbar(person) { +function sichtbarRoh(person) { /* NUR DogFather sieht alles (01.09.2026). Vorher stand hier istLeitung() -- damit sah auch jeder Manager jeden Creator. Ein Manager faellt jetzt in dieselbe Regel wie ein Scout: nur die @@ -277,6 +301,14 @@ export function sichtbar(person) { } /* Ein Scout sieht die Bereiche der Creator, die er betreut -- dazu, was er selbst eingetragen hat. */ + /* DAS MODI-TEAM TEILT SICH SEINE EINTRAEGE -- wie die Aufgaben + (Entscheidung Filipe, 09.09.2026: "sie sind untereinander ein + Team"). Ohne diesen Zweig saehe jeder Modi nur, was er selbst + geschrieben hat, und eine gemeinsame Ideensammlung waere keine. */ + if (person.rolle === "modi") { + return { wo: `(e.erstellt_von IN (SELECT id FROM personen WHERE rolle = 'modi')` + + ` OR e.creator_id IN (SELECT id FROM personen WHERE rolle = 'modi'))`, werte: [] }; + } const b = betreutWo(person, "e.creator_id"); return b ? { wo: `(e.erstellt_von = ? OR ${b.wo})`, werte: [person.id, ...b.werte] } @@ -307,10 +339,22 @@ export function sichtbarEintrag(person, praefix = "e") { if (regel.wo === "1=1") return regel; // DogFather sieht ohnehin alles if (!BEREICHE_FUER_ALLE.length) return regel; const liste = BEREICHE_FUER_ALLE.map(() => "?").join(", "); - return { + const mitAllen = { wo: `(${praefix}.bereich IN (${liste}) OR ${regel.wo})`, werte: [...BEREICHE_FUER_ALLE, ...regel.werte], }; + /* UND DANACH NOCH EINMAL ZU (10.09.2026). + + sichtbar() haengt die Modi-Bedingung bereits an -- aber das ODER + hier oeffnet sie wieder: Ein Eintrag in einem `fuerAlle`-Bereich + kaeme durch, egal wem er gehoert. Heute hat kein Modi eine Kachel + dorthin; ein Aufruf an der Oberflaeche vorbei braucht sie aber + nicht. Eine Regel, die nur im Formular gilt, ist keine Regel. */ + if (siehtModis(person)) return mitAllen; + return { + wo: `(${mitAllen.wo}) AND ${ohneModi(praefix)}`, + werte: mitAllen.werte, + }; } const SPALTEN = ` diff --git a/server/workspace-dateien.js b/server/workspace-dateien.js index 5ffeb585..e31de136 100644 --- a/server/workspace-dateien.js +++ b/server/workspace-dateien.js @@ -31,6 +31,7 @@ import { mkdirSync, createReadStream, writeFileSync, unlinkSync, statSync } from import { db, protokolliere, echteIp, sitzungLesen, DATEN_ORDNER, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather, siehtAlles, istSpicy, ohneDogFather, ROLLEN_SORTIERUNG, externSql, EXTERN_MAX, sichtbarePersonenIds, + ohneModi, siehtModis, } from "./workspace.js"; export const dateienRouter = express.Router(); @@ -66,9 +67,29 @@ function gleicheHerkunft(req, res, next) { dateienRouter.use("/workspace/api/dateien", angemeldet); +/* ===================================================================== + DIE UMHUELLUNG: was einem Modi gehoert, faellt hier heraus. + + Sie sitzt UM die eigentliche Regel und nicht darin -- damit sie auch + fuer Regeln gilt, die es heute noch nicht gibt. Beim ersten Anlauf + war nur der eine bekannte Fall geflickt (Spicy Media); das haette den + naechsten Rollenzweig wieder offen gelassen, und niemand haette es + gemerkt, weil an der geaenderten Stelle nichts davon steht. + + DogFather und die Modis gehen unveraendert durch. + ===================================================================== */ +export function sichtbar(person) { + const regel = sichtbarRoh(person); + if (!regel || siehtModis(person)) return regel; + return { + wo: `(${regel.wo}) AND ${ohneModi("d", ["creator_id", "hochgeladen_von"])}`, + werte: regel.werte, + }; +} + /* Sichtbarkeit wie überall: an einer Stelle, damit keine Abfrage sie vergisst. */ -export function sichtbar(person) { +function sichtbarRoh(person) { /* NUR DogFather sieht alles (01.09.2026). Vorher stand hier istLeitung() -- damit sah auch jeder Manager jeden Creator. Ein Manager faellt jetzt in dieselbe Regel wie ein Scout: nur die diff --git a/server/workspace.js b/server/workspace.js index 3c3877e3..85335df6 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -165,6 +165,51 @@ export function ohneDogFather(praefix, spalten = ["creator_id", "erstellt_von"]) + `(SELECT id FROM personen WHERE rolle = 'admin'))`) .join(" AND "); } +/* ===================================================================== + WAS EINEM MODI GEHOERT, SIEHT SONST NIEMAND (10.09.2026) + + Das Gegenstueck zu ohneDogFather() -- und es entstand aus einem + gemessenen Leck, nicht aus einer Ueberlegung. + + WAS PASSIERT WAR: Fuer PERSONEN ist die Regel zentral + (verborgeneIds). Fuer ZEILEN gibt es sie dreimal -- in + workspace-aufgaben.js, workspace-bereiche.js und + workspace-dateien.js -- und alle drei geben Spicy Media dasselbe: + "alles ausser dem, was DogFather gehoert". Ein Modi-Eintrag gehoert + ihm aber nicht. Also sah Spicy Media die Aufgaben, die Eintraege und + die Dateien der Modis, waehrend die Modis selbst in jeder Namensliste + sauber verborgen waren. Die halbe Verborgenheit ist keine. + + Aufgefallen ist es nicht beim Lesen des Codes, sondern durch eine + Pruefung, die etwas ANLEGT und danach mit fremden Augen nachsieht. + Manager, Scout und Creator waren nie betroffen -- ihre Regeln sind + ohnehin enger. Der Kalender auch nicht: termineSichtbar() gibt jedem + nur Eigenes. Nachgemessen, nicht angenommen. + + SPALTEN MUESSEN BEIDE RICHTUNGEN ABDECKEN. `creator_id` sagt, UM WEN + es geht; `erstellt_von`/`hochgeladen_von`/`verantwortlich_id` sagen, + an WEM es haengt. Eine Modi-Aufgabe hat gar keine creator_id -- sie + haengt allein an verantwortlich_id. Wer nur eine Spalte prueft, hat + nichts geprueft. + + `IS NULL OR NOT IN` und nicht bloss `NOT IN`: In SQL ist + `NULL NOT IN (...)` weder wahr noch falsch, sondern NULL -- die Zeile + fiele stillschweigend heraus. Genau so verschwinden Daten, ohne dass + jemand einen Fehler sieht. (Dieselbe Falle steht schon im Kommentar + von ohneDogFather; sie ist es wert, zweimal dazustehen.) + ===================================================================== */ +export function ohneModi(praefix, spalten = ["creator_id", "erstellt_von"]) { + return spalten + .map((sp) => `(${praefix}.${sp} IS NULL OR ${praefix}.${sp} NOT IN ` + + `(SELECT id FROM personen WHERE rolle = 'modi'))`) + .join(" AND "); +} + +/** Darf diese Person ueberhaupt etwas sehen, das einem Modi gehoert? + * Nur die DogFather-Rolle und die Modis selbst. */ +export const siehtModis = (person) => !!person + && (person.rolle === "admin" || person.rolle === "modi"); + export const siehtAlles = (person) => !!person && (person.rolle === "admin" || person.rolle === "spicy");