Spicy Media sah die Zeilen der Modis -- drei Tabellen, ein Loch

GEMESSEN, NICHT VERMUTET, und es war live: Aufgaben, Bereichs-Eintraege
und Dateien eines Modis waren fuer Spicy Media sichtbar. Die NAMEN der
Modis waren ueberall sauber verborgen -- ihre ZEILEN nicht. Eine halbe
Verborgenheit ist keine.

WARUM ES PASSIEREN KONNTE: Fuer Personen gibt es die Regel EINMAL
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" (ohneDogFather). Ein Modi-Eintrag gehoert ihm nicht,
also fiel er durch.

Manager, Scout und Creator waren nie betroffen, ihre Regeln sind enger.
Der Kalender auch nicht: termineSichtbar() gibt jedem nur Eigenes.
Beides nachgesehen, nicht angenommen.

GEFUNDEN HAT ES KEINE UEBERLEGUNG, sondern eine Pruefung, die etwas
ANLEGT und danach mit fremden Augen nachsieht. Vorher hatte ich nur
Namenslisten geprueft -- und die waren die ganze Zeit gruen. Der Anlass
war nicht einmal Misstrauen gegen diese Stelle: Ich wollte ein
Ideen-Board auf die Eintraege setzen und dabei wissen, wer sie sieht.

BEHOBEN mit ohneModi() als Gegenstueck zu ohneDogFather -- und zwar als
UMHUELLUNG um die drei Regeln, nicht als Flicken darin. Ein Flicken
haette den einen bekannten Zweig geschlossen und den naechsten
Rollenzweig wieder offen gelassen; gemerkt haette es niemand, weil an
der geaenderten Stelle nichts davon steht.

Dazu die `fuerAlle`-Ausnahme bei den Eintraegen: Sie haengt ein ODER an
und haette die Bedingung sonst wieder aufgemacht. Heute hat kein Modi
eine Kachel in einen solchen Bereich -- ein Aufruf an der Oberflaeche
vorbei braucht sie aber nicht. Eine Regel, die nur im Formular gilt,
ist keine Regel.

Nachgesehen, dass die Umhuellung nirgends das falsche Tabellenkuerzel
setzt: Alle Aufrufstellen in workspace-hinweise.js und workspace-suche.js
fuehren die Tabellen als a, d und e -- genau so, wie es dasteht.

NEBENBEI: Das Modi-Team teilt sich jetzt auch die Bereichs-Eintraege,
nicht nur die Aufgaben (Entscheidung Filipe, 09.09.2026: "sie sind
untereinander ein Team"). Ohne diesen Zweig saehe jeder Modi nur, was er
selbst geschrieben hat -- eine gemeinsame Sammlung waere keine.

ZWEI EIGENE FEHLER AUF DEM WEG DAHIN, beide festgehalten:

  * Die neue Messung stand HINTER der Gegenprobe. Die macht eine Person
    absichtlich zur Creatorin -- die Messung bekam 403 und meldete
    "kann nichts anlegen". Gemessen wurde ein Zustand, den es im
    Betrieb nicht gibt. Genau davor warnt der Kommentar, den ich selbst
    zwei Tage vorher an diese Gegenprobe geschrieben hatte.
  * Das "konnte nicht nachsehen" nannte KEINEN Grund. Damit ist der
    dritte Ausgang nur dem Namen nach da -- man weiss danach so wenig
    wie vorher. Erst mit der Fehlermeldung im Text kam ich auf die Spur.

GEPRUEFT: pruef-modi-verborgen (75, davon 18 neu ueber drei Tabellen und
fuenf Rollen), pruef-spicy (60), pruef-bereiche-lesend,
pruef-aufgabenbrett, pruef-modi-katalog (29).

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-10 01:34:02 +02:00
co-authored by Claude Opus 5
parent 390f592967
commit 138bcce80b
5 changed files with 212 additions and 6 deletions
+77 -2
View File
@@ -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 */ }
+22 -1
View File
@@ -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. */
+46 -2
View File
@@ -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 = `
+22 -1
View File
@@ -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
+45
View File
@@ -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");