---
phase: quick-260910-exd
plan: 01
type: execute
wave: 1
depends_on: []
autonomous: true
requirements: [WINDOWS-18, ETAPPE-2-MODULE-REGISTRY]
files_modified:
- apps/api/scripts/rls-scratch-check.mjs
- docs/mandantentrennung-etappe2-fehlerrichtung.md
- apps/api/src/module-registry/module-access.service.ts
- apps/api/src/module-registry/module-access.service.spec.ts
- apps/api/src/module-registry/module.guard.spec.ts
- apps/api/src/module-registry/module-registry.service.ts
- apps/api/src/module-registry/module-registry.service.spec.ts
- docs/mandantentrennung-zugriffsklassifikation.md
- .planning/WINDOWS.md
estimate:
tokens: 195000
raw_tokens: 195000
tasks: 3
confidence: low
must_haves:
truths:
- "Der Entscheidungsweg fuer Modulzugriff laeuft nach diesem Durchlauf auf BEIDEN Stufen mandantengebunden: die Aktivierungsstufe (TenantModuleActivation) und die Freigabestufe (ModuleGrant). Keine Methode bindet die eine Stufe und laesst die andere ungebunden, und keine Methode erzeugt zwei Klienten fuer zwei verschiedene Mandanten in derselben Aufloesung."
- "Der plattformweite Modulkatalog (Modell `Module`) bleibt ungebunden, und die Begruendung dafuer ist GEMESSEN statt angenommen: die Tabelle traegt heute ueberhaupt keinen Zeilenschutz, eine Bindung waere heute wirkungslos und nicht etwa katastrophal — katastrophal wuerde sie erst, wenn Etappe 3 dieser Tabelle eine Regel gibt. Genau diese Bedingung steht als Bedingung im Dokument, nicht als Behauptung."
- "Die umgekehrte Fehlerrichtung dieses Bereichs — nach dem Scharfschalten liefert eine ungebunden gebliebene Abfrage nicht zu viel, sondern nichts, und aus 'nichts' wird 'kein Zugriff' — ist an der echten, ausgelieferten Regel gemessen und je Pfad mit dem konkreten Signal beschrieben, an dem man sie erkennen wuerde."
- "Das Kernproblem dieses Bereichs ist ausdruecklich behandelt: es gibt heute KEIN Signal, das 'wirklich keine Freigabe' von 'die Abfrage hat nichts gefunden' unterscheidet. Beide Faelle liefern dieselbe 403-Meldung, dieselbe leere Modulliste mit Status 200 und keinerlei Protokolleintrag. Diese Abwesenheit ist als Abwesenheit festgehalten, mit der konkreten Vorabpruefung, die sie in Etappe 4 aufdecken wuerde — nicht durch eine Laufzeitwarnung ueberdeckt, die im Normalbetrieb Dauerlaerm waere."
- "Die Umstellung folgt dem bereits umgestellten Nachbarn `groups/module-grants.service.ts`, der DIESELBEN Modelle beruehrt: ein Klient je Methode unter dem Namen `tenantPrisma`, die bestehenden `where`-Filter bleiben als zweites Netz stehen, und die anwendungsseitige Mandanten-Gegenpruefung wird nicht durch die Datenbank ersetzt. Zwei Dateien mit zwei verschiedenen Vorgehensweisen auf denselben Tabellen entstehen nicht."
- "Die Testlage ist VOR der Umstellung hergestellt, nicht danach: die vorhandene Testdatei bekommt den Zwei-Klienten-Nachweis, und die Datei mit den meisten Zugriffen des Bereichs bekommt ueberhaupt erst eine Testdatei. Ein vergessener Bindungsaufruf wird dadurch rot, statt aus einem anderen Grund zu scheitern."
- "Beide Kopfkommentare, die in diesem Bereich heute etwas Unwahres behaupten, sind an der Messung richtiggestellt — eine Behauptung im Kommentar ist keine Lieferung."
- "Baseline gehalten: 810 Tests gruen (55 Dateien) oder mehr, Typpruefung sauber, das Wegwerf-Werkzeug meldet alle Pruefungen bestanden — am Ende JEDER Aufgabe, nicht nur am Ende des Plans."
- "Der Schalter bleibt AUS: `DATABASE_URL` zeigt weiterhin auf die Rolle `tessera`, Schema und Migrationen sind unveraendert, T-JTS-02 und T-JTS-03 bleiben aufgezeichnet und ungefixt."
artifacts:
- "apps/api/scripts/rls-scratch-check.mjs — ein achter Abschnitt `runModuleRegistryAreaChecks` mit dreizehn namentlich benannten Pruefungen gegen die echten, aus den ausgelieferten Migrationen geschnittenen Regeln"
- "docs/mandantentrennung-etappe2-fehlerrichtung.md — Abschnitt `## Bereich module-registry` mit (m1) Messung, (m2) Signaltabelle, (m3) Leere-als-Abwesenheit samt eigener Behandlung des Problems 'leer heisst kein Recht', (m4) bewusst nicht geloest, (m5) bewusst nicht angefasst, plus Nachtrag"
- "apps/api/src/module-registry/module-access.service.ts — die drei Aktivierungs- und die zwei Freigabe-Zugriffe gebunden, der Katalogzugriff begruendet ungebunden"
- "apps/api/src/module-registry/module-access.service.spec.ts — Zwei-Klienten-Nachweis ueber `__makeBoundClient`, alle bestehenden Faelle erhalten"
- "apps/api/src/module-registry/module.guard.spec.ts — der Fall, der die Abwesenheit eines unterscheidenden Signals festnagelt"
- "apps/api/src/module-registry/module-registry.service.ts — die fuenf Aktivierungszugriffe gebunden, die sechs Katalogzugriffe begruendet ungebunden, beide falschen Kopfkommentare richtiggestellt"
- "apps/api/src/module-registry/module-registry.service.spec.ts — NEU, die Datei hatte bisher keine"
- "docs/mandantentrennung-zugriffsklassifikation.md — fuenf Bestandsaufnahme-Zeilen, Uebersichtszeile, Summenzeile, Klassen-Verteilung samt Ueberschrift, Abschnitt zur Hintergrunddienst-Falle"
- ".planning/WINDOWS.md — offener Eintrag zum fehlenden unterscheidenden Signal, in Tabelle UND JSON-Block"
key_links:
- "module.guard.ts haelt keinen eigenen Datenbankzugriff — seine Richtigkeit ist vollstaendig eine Funktion dessen, was ModuleAccessService zurueckgibt. Die Bindung muss deshalb im Dienst sitzen, nicht im Waechter."
- "dashboard.service.ts ruft dieselbe Aufloesung auf, obwohl der Bereich `dashboard` noch nicht umgestellt ist — weil die Bindung im Dienst sitzt, wird der Dashboard-Filter mit umgestellt, ohne dass eine Datei des Bereichs `dashboard` angefasst wird."
- "Der Gruppenpfad der Freigabe-Aufloesung laeuft ueber `Group` und `GroupMembership` — nach der Bindung schliesst er eine Freigabe mit fremder Gruppenkennung aus, die die Regel auf `ModuleGrant` allein nachweislich durchlaesst (T-JTS-03). Die Schreibseiten-Gegenpruefung in `module-grants.service.ts` bleibt trotzdem stehen: sie ist heute der einzige Schutz."
- "Beide Eindeutigkeitsschluessel dieses Bereichs fuehren mit der Mandantenkennung — die Kette 'unsichtbare Zeile, falsches frei, harter Eindeutigkeitsfehler' aus den Bereichen `tenders` und `user` kann hier strukturell nicht auftreten. Das ist die Entlastung dieses Bereichs und wird gemessen, nicht angenommen."
---
Etappe 2 der Mandantentrennung, sechster Bereich: `module-registry`. Die
siebzehn Datenbankzugriffe der beiden Dienste dieses Bereichs werden dort auf
`forTenant()` umgestellt, wo sie auf Rechnung genau eines Mandanten laufen, und
dort begruendet ungebunden gelassen, wo sie den plattformweiten Modulkatalog
betreffen.
Zweck: dieser Bereich ENTSCHEIDET bei jeder Anfrage, wer welches Modul benutzen
darf. Bleibt hier eine Abfrage ungebunden, sieht das nach dem Scharfschalten
nicht wie ein Fehler aus, sondern wie ein Rechteentzug — die Seitenleiste ist
leer, der Marktplatz zeigt nichts, jede Modulroute antwortet mit 403. Fuer den
Betroffenen ist das nicht von "ein Administrator hat mir das weggenommen" zu
unterscheiden, und niemand meldet einen Fehler fuer ein Recht, das er fuer
absichtlich entzogen haelt. Das ist die sichtbarste Auspraegung der umgekehrten
Fehlerrichtung im gesamten Vorhaben und zugleich die am wenigsten
meldungswahrscheinliche.
Ergebnis: gebundene Aktivierungs- und Freigabestufe, ein begruendet ungebundener
Katalog, eine gemessene Kritikschrift, eine Testlage, die einen vergessenen
Bindungsaufruf rot macht, und zwei Dokumente, die am Ende nachweislich mit dem
Quelltext uebereinstimmen.
**Der Schalter bleibt AUS.** `DATABASE_URL` zeigt weiterhin auf die Rolle
`tessera` mit `BYPASSRLS`. Das Scharfschalten ist Etappe 4 und findet hier NICHT
statt.
@~/.claude/gsd-core/workflows/execute-plan.md
@~/.claude/gsd-core/templates/summary.md
@.planning/STATE.md
@docs/mandantentrennung-zugriffsklassifikation.md
@docs/mandantentrennung-etappe2-fehlerrichtung.md
@apps/api/src/prisma/prisma-tenant.extension.ts
@apps/api/src/prisma/rls-access-inventory.spec.ts
@apps/api/scripts/rls-scratch-check.mjs
@apps/api/src/groups/module-grants.service.ts
@apps/api/src/groups/module-grants.service.spec.ts
@apps/api/src/module-registry/module-access.service.ts
@apps/api/src/module-registry/module-access.service.spec.ts
@apps/api/src/module-registry/module-registry.service.ts
@apps/api/src/module-registry/module.guard.ts
@apps/api/src/module-registry/module.guard.spec.ts
@apps/api/src/module-registry/module-registry.controller.ts
@.planning/quick/260910-das-mandantentrennung-etappe-2-bereich-user-/260910-das-PLAN.md
Alle Zahlen unten sind zur Planungszeit am 2026-09-10 gegen HEAD `f657e24`
GEMESSEN, mit der jeweils angegebenen Anweisung. Sie leiten die Untersuchung,
sie sind KEINE Bearbeitungsvollmacht — die Dateien werden vor jeder Aenderung
erneut gelesen.
**Befund A — die Zahl haelt.** Siebzehn Rohtreffer, genau wie die
Klassifikation sie fuehrt. Gemessen mit
`grep -ro "this\.prisma\.[a-zA-Z]*" apps/api/src/module-registry | grep -v spec | sort | uniq -c`:
`module-access.service.ts` sechs (`module` einmal, `moduleGrant` zweimal,
`tenantModuleActivation` dreimal), `module-registry.service.ts` elf (`module`
sechsmal, `tenantModuleActivation` fuenfmal), `module.guard.ts` und
`module-registry.controller.ts` je null. Nach `dkv` (21) und `user` (17) der
dritte Bereich in Folge, in dem beim Hineinschauen nichts schrumpft. Auch die
fuenf (Datei, Modell)-Paare der Bestandsaufnahme stimmen mit dem Quelltext
ueberein — anders als im Bereich `user` (eine sachlich falsche Zeile) und im
Bereich `dkv` (ein ganz fehlendes Paar) ist hier keine Korrektur noetig. Die
BEGRUENDUNGEN der beiden Katalogzeilen sind trotzdem zu duenn, siehe Befund E.
**Befund B — keine Transaktion in diesem Bereich.** Gemessen mit
`grep -rn '\$transaction(' apps/api/src/module-registry --include=*.ts | grep -v spec`:
null Treffer, Rueckgabewert 1. Der im Kopfkommentar von
`prisma-tenant.extension.ts` ausdruecklich verlangte erneute Test vor jedem
neuen `forTenant()`-Fall mit eigener Transaktion ist damit fuer diesen Bereich
beantwortet: es faellt kein neuer Fall an, `withTenantTransaction()` wird hier
nicht gebraucht und nicht eingefuehrt.
**Befund C — drei der vier bekannten Testformen liegen hier GLEICHZEITIG vor.**
Das ist die Form, die dieser Bereich der Liste hinzufuegt:
- `module-access.service.spec.ts` (262 Zeilen, 15 Faelle) hat einen
handgerollten Prisma-Nachbau mit `tenantModuleActivation`, `moduleGrant` und
`module`, aber KEINE Attrappe fuer das Bindungshilfsmittel. Das ist die
`groups`/`tenders`-Form: nach der Umstellung riefe das echte `forTenant()`
eine `$extends`-Methode auf einem schlichten Objekt auf, das keine hat — die
Tests wuerden scheitern, aber aus dem falschen Grund, und ein vergessener
Bindungsaufruf waere von einem vorhandenen nicht zu unterscheiden.
- `module-registry.service.ts` hat UEBERHAUPT KEINE Testdatei. Das ist die
`dkv`-Form, und sie trifft hier elf der siebzehn Zugriffe, darunter jeden
einzelnen Schreibpfad des Bereichs (Aktivieren, Deaktivieren, Katalogpflege
beim Start).
- `module.guard.spec.ts` (119 Zeilen) stellt beide Dienste als Attrappe und
beruehrt keine Datenbank. Von der Bindung nicht betroffen — aber genau diese
Datei ist der Ort, an dem sich festnageln laesst, dass der Waechter bei einer
leeren Menge nichts hat, woran er die Ursache erkennen koennte.
- `module-registry.controller.ts` hat ebenfalls keine Testdatei. Er haelt
keinen Datenbankzugriff und wird von diesem Plan nicht angefasst.
**Befund D — der Waechter haelt keinen eigenen Zugriff.** `module.guard.ts`:
null `this.prisma`-Treffer. Er ruft `moduleRegistryService.findBySlug(slug)`
(Katalog, bleibt ungebunden) und `moduleAccessService.getAccessibleModuleIds()`
(wird gebunden) und wirft bei leerer Menge
`ForbiddenException('Module ... is not accessible for this user')`. Seine
Richtigkeit ist damit vollstaendig eine Funktion dessen, was der Dienst
zurueckgibt — die Bindung gehoert in den Dienst, und der Waechter braucht
keine.
**Befund E — der Modulkatalog traegt ueberhaupt keinen Zeilenschutz, und die
naheliegende Begruendung fuer seine Nichtbindung ist deshalb FALSCH.** Gemessen
mit `grep -n 'ENABLE ROW LEVEL SECURITY' apps/api/prisma/migrations/*/migration.sql`:
23 Tabellen ueber alle ausgelieferten Migrationen, `"Module"` ist NICHT
darunter. Das Modell traegt im Schema auch kein `tenantId`
(`apps/api/prisma/schema.prisma`, Zeilen 97-110).
Daraus folgt eine Praezisierung, die dieser Plan ausdruecklich vornimmt statt
sie zu uebergehen: die Aussage "eine Bindung wuerde den Katalog fuer jeden
Mandanten unsichtbar machen" trifft HEUTE nicht zu. Ohne Regel auf der Tabelle
aendert eine Bindung an der Ergebnismenge nichts — sie setzt nur eine
Sitzungsvariable und kostet eine zusaetzliche Transaktion je Abfrage. Die
Nichtbindung ist trotzdem richtig, aber aus zwei anderen Gruenden, und beide
gehoeren so und nicht anders ins Dokument: erstens waere ein gebundener
Katalogzugriff eine Unwahrheit in der Aufzeichnung (er behauptete eine
Mandantendimension, die die Tabelle nicht hat); zweitens WUERDE die Bindung
genau dann katastrophal, wenn Etappe 3 dieser Tabelle eine Regel gaebe — dann
verschwaende der gesamte Katalog fuer jeden Mandanten. Der zweite Grund ist
eine BEDINGUNG und wird als Bedingung notiert, nicht als heute beobachtbare
Tatsache. Genau diese Unterscheidung ist Aufgabe 1 zu messen.
**Befund F — der zweite Verbraucher, in einem noch nicht umgestellten
Bereich.** `apps/api/src/dashboard/dashboard.service.ts:128` ruft ebenfalls
`getAccessibleModuleIds()`. Weil die Bindung im Dienst sitzt, wird der
Widget-Filter des Dashboards mit umgestellt, ohne dass eine Datei des Bereichs
`dashboard` angefasst wird — eine Reihenfolge-ENTLASTUNG, nicht eine Falle.
Heute ruht dieser Pfad: `apps/api/src/dashboard/widget-module-map.ts:24` fuehrt
`WIDGET_MODULE_MAP` als leeres Objekt (D-22 aus 15-05), weshalb
`dashboard.service.ts` vorher zurueckkehrt und die Aufloesung gar nicht
erreicht. Gemessen, nicht angenommen.
**Befund G — zwei Kopfkommentare behaupten etwas Unwahres, und beide Methoden
haben null Aufrufer.** Gemessen mit
`grep -rn "isModuleActive\|findActiveForTenant" apps/api/src apps/web/src packages`:
- `isModuleActive` — genau EIN Treffer, die Definition selbst. Ihr
Kopfkommentar sagt "Used by ModuleGuard to gate access to module-specific
endpoints". Der Waechter ruft sie nicht auf; er nimmt `findBySlug` plus
`getAccessibleModuleIds`.
- `findActiveForTenant` — zwei Treffer: die Definition und eine Prosa-Erwaehnung
im Kopfkommentar des Controllers ("stays unchanged for Plan 15-03's
marketplace catalog"). Der Marktplatz-Katalog wird von `getCatalogFlags`
bedient, nicht von dieser Methode.
Das ist Fehler 3 der Fehlerliste dieses Vorhabens in Reinform (eine Behauptung
ist keine Lieferung), hier in einem Kommentar statt in einem Bericht. Beide
Kommentare werden richtiggestellt, und beide Methoden werden TROTZDEM
umgestellt: heute toter, ungebunden gelassener Code ist die Falle fuer den, der
ihn morgen verdrahtet.
**Befund H — die Entlastung dieses Bereichs, gegen den Quelltext gehalten.**
`TenantModuleActivation` traegt `@@unique([tenantId, moduleId])`
(`schema.prisma:120`). Die beiden partiellen Eindeutigkeitsindizes auf
`ModuleGrant` fuehren ebenfalls mit der Mandantenkennung
(`20260804130130_add_groups_and_module_grants/migration.sql:105-108`:
`("tenantId","moduleId","groupId")` bzw. `("tenantId","moduleId","userId")`).
Die Kette, die die Bereiche `tenders` (`TenderTriage`) und `user` (`username`)
beide tragen — unsichtbare Zeile, falsches "frei", harter Eindeutigkeitsfehler
— kann in diesem Bereich strukturell NICHT auftreten, weil jeder
Eindeutigkeitsschluessel den Mandanten enthaelt und eine fremde Zeile deshalb
gar nicht in dieselbe Schluesselstelle faellt. Das ist der erste Bereich von
sechs, in dem die Kette nicht umgangen, sondern strukturell abwesend ist. Wird
in Aufgabe 1 gemessen, nicht behauptet.
**Befund I — der Gruppenpfad bekommt durch die Bindung eine Verteidigung, die
die Regel auf `ModuleGrant` allein nicht hat.** Die Gruppen-Abfrage in
`getAccessibleModuleIds` filtert
`{ tenantId, group: { memberships: { some: { userId } } } }`. Gebunden laeuft
diese Beziehungsdurchquerung ueber `Group` (Regel: `tenantId`) und
`GroupMembership` (Regel ueber den Join auf `Group`). Eine Freigabezeile mit
korrekter eigener Mandantenkennung, aber FREMDER Gruppenkennung — genau die
Zeile, von der T-JTS-03 gemessen hat, dass die Regel auf `ModuleGrant` sie
NICHT abweist — wird auf der Leseseite unsichtbar, sobald gebunden wird, weil
die verbundene Gruppe unsichtbar ist. Heute ist
`assertTargetBelongsToTenant` in `module-grants.service.ts` der EINZIGE Schutz
davor. Nach dem Scharfschalten kommt ein zweiter dazu, aber erst dann — die
Schreibseiten-Pruefung bleibt deshalb unangetastet und wird ausdruecklich nicht
als "macht jetzt die Datenbank" gestrichen. Beide Richtungen sind in Aufgabe 1
zu messen: gebunden ausgeschlossen, ueber die Wartungsrolle mit `BYPASSRLS`
enthalten.
**Befund J — der zweistufige Zugriff, im Code nachgesehen.** ADMIN und
SUPER_ADMIN ueberspringen die Freigabestufe, aber NICHT die Aktivierungsstufe:
der Kurzschlusszweig liest ausschliesslich `tenantModuleActivation`. Der
Vorgabezustand ist geschlossen (`grantedIds.length === 0` liefert eine leere
Menge, ohne die Aktivierungsstufe ueberhaupt zu befragen). Die Schnittmenge
(D-02) filtert die aktiven Aktivierungen auf die freigegebenen Modulkennungen.
Alle drei Mandantenkennungen stammen aus dem Sitzungsnachweis (T-15-10) und
stehen bereits heute in jedem `where` — die Bindung AENDERT deshalb an keinem
heutigen Ergebnis etwas, sie zieht nur die Grenze ein zweites Mal, in der
Datenbank. Eine Bindung an der falschen Stufe kann daher nicht oeffnen; sie
kann nur schliessen. Die einzige Form, die oeffnen wuerde, waere eine Bindung
an den falschen Mandanten, und die ist ausgeschlossen, solange die Kennung aus
dem Sitzungsnachweis kommt. Das ist zu pruefen, nicht zu glauben — Aufgabe 2
nagelt es in Tests fest.
**Befund K — der Bodensatz der Uebersichtszeile nach der Umstellung.** Aus
Befund A folgt rechnerisch: sieben ungebundene Rohtreffer (die Katalogzugriffe)
und zehn gebundene. Diese Zahlen stehen hier als Erwartung, NICHT als
Vorgabewert: die Pruefungen in Aufgabe 3 leiten sie zur Laufzeit erneut aus dem
Quelltext ab und halten das Dokument gegen das Gemessene, nicht gegen diese
Zeile.
**Gemessene Ausgangslage (2026-09-10, HEAD `f657e24`):**
`npm --prefix apps/api run test` meldet 810 Tests in 55 Dateien gruen (die
Aufgabenstellung nannte 54 Dateien — 55 ist der gemessene Wert);
`node apps/api/scripts/rls-scratch-check.mjs` meldet "Alle 53 Pruefungen
bestanden."; der Datenbankcontainer `tessera-ctl-db-1` war unter `172.19.0.2`
erreichbar (Adresse bei jedem Lauf neu ermitteln, nie abschreiben).
Aufgabe 1: Die Fehlerrichtung fuer diesen Bereich MESSEN und aufschreiben — an den echten, ausgelieferten Regeln
Der Container `tessera-ctl-db-1` laeuft und ist ueber seine Container-Adresse erreichbar; die Adresse wird zur Laufzeit ermittelt, nicht aus diesem Plan abgeschrieben.
apps/api/scripts/rls-scratch-check.mjs, docs/mandantentrennung-etappe2-fehlerrichtung.md
Diese Aufgabe ist der duenne, durchgehende Faden dieses Plans: sie beruehrt das
Messwerkzeug, die echte Datenbank und die Kritikschrift und beweist damit
end-to-end, dass die Aussage dieses Durchlaufs traegt, BEVOR eine Zeile
Produktivcode umgestellt wird. Sie aendert keinen Produktivcode.
Dreizehn neue Pruefungen in einem achten Abschnitt `runModuleRegistryAreaChecks`,
aufgerufen in `main()` NACH `runUserAreaChecks` und VOR
`runTransactionShapeMeasurement`. Der Abschnitt setzt auf den Tabellen auf, die
`runGroupsAreaChecks` bereits anlegt (`Group`, `GroupMembership`, `ModuleGrant`,
`TenantModuleActivation`, samt der wortgleich aus den ausgelieferten Migrationen
geschnittenen Regeln) — er legt sie NICHT neu an und tippt keine Regel nach.
Findet die Extraktion eine benoetigte Regel nicht, meldet der Abschnitt eine
FEHLGESCHLAGENE Pruefung und bricht ab, statt mit einer geratenen Regel
weiterzumessen; das ist die Form, die `runGroupsAreaChecks` bereits vormacht.
Vorbereitend legt der Abschnitt ueber die Verwaltungsrolle an:
(a) eine Tabelle `"Module"` OHNE Zeilenschutz — die zu messende Eigenschaft
selbst, nach dem Muster der Tabelle `"Tenant"` im Abschnitt des Bereichs `user`,
mit zwei Katalogzeilen (`mod-1`, `mod-2`);
(b) den Eindeutigkeitsindex auf `("tenantId","moduleId")` fuer
`"TenantModuleActivation"`, wortgleich zur Schemadefinition — ohne ihn liesse
sich Pruefung 12 nicht messen;
(c) je eine Direkt-Freigabezeile pro Mandant (`userId`, kein `groupId`);
(d) die Mitgliedschaft `(group-b, user-a)`, die `runGroupsAreaChecks` unter
gebundenem Kontext bewusst NICHT anlegen konnte (die Regel wies sie ab) — sie
wird hier ueber die Verwaltungsrolle gesetzt, weil Pruefung 6 sonst aus dem
falschen Grund bestuende: sie wuerde nichts ausschliessen, weil es nichts zu
finden gaebe. Der Aufbau muss so sein, dass Pruefung 6 FEHLSCHLUEGE, wenn die
Bindung nicht ausschloesse.
Die Zeile `grant-foreign-group` (Mandantenkennung TENANT-A, Gruppenkennung
group-b), die `runGroupsAreaChecks` bereits erfolgreich einfuegt, wird
wiederverwendet und nicht neu erzeugt.
Die dreizehn Pruefungen, je mit dieser Kennung, jede mit dem tatsaechlich
beobachteten Wert im Meldetext (nicht mit einer Wiederholung der Erwartung):
1. `modulegrant-ungebunden-null-zeilen` — die Belegzeile dieses Abschnitts: der
IDENTISCHE Lesezugriff auf `"ModuleGrant"` ohne vorheriges `set_config`
liefert 0 Zeilen, nicht die tatsaechlich vorhandenen. Die beobachtete Zahl
der tatsaechlich vorhandenen Zeilen gehoert in den Meldetext.
2. `tenantmoduleactivation-ungebunden-null-zeilen` — dasselbe fuer die
Aktivierungstabelle. Diese Tabelle ist die, die der Kurzschluss fuer
ADMIN/SUPER_ADMIN als EINZIGE liest — hier faellt der Verwaltungszugriff aus.
3. `admin-kurzschluss-gebunden-liefert-nur-eigene-aktivierungen` — gebunden an
TENANT-A liefert die aktive Aktivierungsliste ausschliesslich A-Zeilen. Der
Rollen-Kurzschluss kann durch die Bindung nicht ueber die Mandantengrenze
hinaus wachsen.
4. `direktfreigabe-gebunden-nur-eigene-zeile` — gebunden an TENANT-A liefert die
Direkt-Freigabe-Abfrage ueber die Benutzerkennung nur die A-Zeile.
5. `gruppenpfad-gebunden-folgt-der-gruppenregel` — gebunden an TENANT-A liefert
der Drei-Tabellen-Weg (Freigabe ueber Gruppe ueber Mitgliedschaft) fuer
`user-a` genau die A-Freigabe. Das bildet die verschachtelte Beziehungsabfrage
des Dienstes nach.
6. `gruppenpfad-gebunden-schliesst-die-fremde-gruppe-aus` — derselbe Weg,
gebunden an TENANT-A, liefert `grant-foreign-group` NICHT, obwohl die Regel
auf `ModuleGrant` diese Zeile nachweislich durchlaesst (T-JTS-03) und die
Mitgliedschaft `(group-b, user-a)` vorhanden ist. Der Meldetext benennt
T-JTS-03 und sagt, dass diese Verteidigung erst nach dem Scharfschalten
greift.
7. `gruppenpfad-ueber-die-wartungsrolle-liefert-die-fremde-gruppe-mit` — die
Gegenmessung: derselbe Weg, ausgefuehrt ueber die Verwaltungsrolle mit
`BYPASSRLS`, LIEFERT die fremde Zeile. Damit steht fest, dass der Ausschluss
in Pruefung 6 von der Bindung kommt und nicht vom Aufbau. Praezedenzfall fuer
diese Messform ueber die Wartungsrolle:
`user-fan-out-je-mandant-gebunden-liefert-alle-zeilen`.
8. `module-tabelle-traegt-keinen-zeilenschutz` — haelt zwei Quellen
gegeneinander: der ungebundene Lesezugriff liefert BEIDE Katalogzeilen, UND
`pg_class.relrowsecurity` fuer `"Module"` ist `false`. Die Aussage haengt
damit nicht allein daran, dass dieser Abschnitt selbst keinen Zeilenschutz
eingeschaltet hat.
9. `katalog-bindung-aendert-heute-nichts-an-der-ergebnismenge` — gebunden an
TENANT-A und ungebunden liefern identische Katalogzeilen. Das ist die
Praezisierung aus Befund E, gemessen: die Nichtbindung des Katalogs ist heute
keine Rettung vor Unsichtbarkeit, sondern eine Frage der Wahrhaftigkeit der
Aufzeichnung — und wird erst dann zur Rettung, wenn Etappe 3 dieser Tabelle
eine Regel gibt. Der Meldetext haelt genau diese Bedingung fest.
10. `gebundener-join-auf-den-katalog-liefert-den-modulnamen` — gebunden an
TENANT-A liefert der Verbund aus Aktivierung und Katalog die
Aktivierungszeile MIT ihrem Modulnamen. Das bildet die
`include`-Form nach, die `findActiveForTenant` benutzt und die
`module-grants.service.ts` bereits gebunden fuehrt: die Bindung verliert die
Katalogseite nicht.
11. `aktivierung-gebundenes-einfuegen-fremder-mandant-abgelehnt` — ein gebundenes
Einfuegen unter TENANT-A mit fremder Mandantenkennung wird abgewiesen. Der
Meldetext nennt die gemessene Fehlerklasse (Zeilenschutz-Ablehnung) und liest
sie mit derselben Hilfsfunktion aus, die der Abschnitt des Bereichs `user`
dafuer eingefuehrt hat (`sqlStateOf`, wegen des generischen Prisma-Codes bei
fehlgeschlagenem Rohzugriff).
12. `aktivierung-eindeutigkeit-traegt-den-mandanten-keine-unsichtbare-kollision`
— die Entlastung aus Befund H, gemessen: bei vorhandener, unter TENANT-A
unsichtbarer Zeile `(TENANT-B, mod-2)` GELINGT das gebundene Einfuegen von
`(TENANT-A, mod-2)`. Der Meldetext stellt das ausdruecklich der Kette aus den
Bereichen `tenders` und `user` gegenueber und benennt sie namentlich, damit
die Entlastung nicht als beilaeufig gelesen wird.
13. `freigabe-eindeutigkeitsindizes-fuehren-mit-der-mandantenkennung` — eine
Textmessung statt einer Datenbankmessung: die beiden partiellen
Eindeutigkeitsindizes auf `"ModuleGrant"` werden aus der ausgelieferten
Migration `20260804130130_add_groups_and_module_grants` gelesen, und es wird
geprueft, dass beide mit der Mandantenkennung beginnen. Damit gilt die
Entlastung aus Pruefung 12 belegt auch fuer die Freigabetabelle, ohne dass
beide partiellen Indizes im Wegwerf-Schema nachgebaut werden muessen. Findet
die Extraktion die Indizes nicht, ist die Pruefung FEHLGESCHLAGEN, nicht
uebersprungen.
TEIL 2, Beleg statt Behauptung fuer Befund B: die Anweisung aus Befund B wird
ausgefuehrt und ihre Ausgabe samt Rueckgabewert in die Kritikschrift
uebernommen, in derselben Form wie im Abschnitt des Bereichs `user`.
TEIL 3, Beleg statt Behauptung fuer Befund G: die Aufrufermessung fuer
`isModuleActive` und `findActiveForTenant` wird ausgefuehrt und ihre Ausgabe
uebernommen — sie traegt die Richtigstellung beider Kopfkommentare in Aufgabe 3.
Danach die Kritikschrift: `docs/mandantentrennung-etappe2-fehlerrichtung.md`
bekommt einen Abschnitt `## Bereich module-registry`, gesetzt NACH dem Abschnitt
`## Bereich user` und VOR `## Verweis`, mit denselben Unterabschnitten wie die
fuenf vorherigen Bereiche:
- `### (m1) Die Messung` — die TATSAECHLICH beobachtete Ausgabe des Laufs (nicht
eine abgeschriebene Erwartung), die Belegzeile namentlich benannt, dazu Teil 2
und Teil 3.
- `### (m2) Signaltabelle je umgestelltem Pfad` — je Pfad das Verhalten bei zu
wenig Ergebnis und das konkrete Signal mit seinem Ort. Mindestens: die
Aufloesung im Kurzschlusszweig, die Aufloesung im Benutzerzweig (Direktweg und
Gruppenweg getrennt), die Schnittmengenabfrage, die Modulliste fuer die
Seitenleiste, die Katalogflaggen fuer den Marktplatz, die Aktivierungsliste,
das Aktivieren, das Deaktivieren, die Aktiv-Abfrage, sowie die zwei bewusst
ungebundenen Katalogpfade als ausdrueckliche Grenze (nach dem Muster der
RSS-Zeile im Abschnitt `tenders`: sie beschreiben nicht heutiges Verhalten,
sondern was eine KUENFTIGE Bindung anrichten wuerde).
- `### (m3) Welcher Code Leere als Abwesenheit deutet` — die namentliche Liste,
nach Wirkung sortiert, MIT eigener Behandlung des Problems "leer heisst kein
Recht" (siehe unten).
- `### (m4) Was dieser Durchlauf bewusst nicht loest`
- `### (m5) Was dieser Durchlauf bewusst NICHT anfasst`
Der Unterabschnitt (m3) traegt diesen Abschnitt und muss namentlich benennen:
den Rueckgabepunkt bei leerer Freigabemenge, den Kurzschlusszweig bei leerer
Aktivierungsliste, den Rueckgabepunkt bei leerer Modulliste, die Katalogflaggen
(deren Leere im Controller zu ZWEI falschen Flaggen wird und damit eine ANDERE
Unwahrheit erzeugt als der Sperrhinweis: "nicht aktiviert" statt "nicht
freigegeben"), die 403-Stelle im Waechter, die Aktiv-Abfrage mit ihrem
Vorgabewert `false` (heute ohne Aufrufer, morgen eine Falle), und — als
Gegenrichtung, damit der Abschnitt nicht nur aus Alarm besteht — die
Deaktivierungs-Ausnahme, die bei Leere LAUT wird.
Und dann die Frage, die dieser Bereich vor allen anderen beantworten muss:
**welches Signal unterscheidet "wirklich keine Freigabe" von "die Abfrage hat
nichts gefunden"?** Die Untersuchung dazu ist Teil dieser Aufgabe, das Ergebnis
gehoert in den Text, und wenn die Antwort "keines" lautet, dann steht das so
da — schlicht, ohne Beschoenigung, mit den drei Stellen, die heute identisch
aussehen (dieselbe 403-Meldung, dieselbe leere Liste mit Status 200, kein
Protokolleintrag), und mit dem Zusatz, der diesen Bereich von allen vorherigen
unterscheidet: der Betroffene hat eine fertige, falsche Erklaerung zur Hand und
meldet deshalb keinen Fehler.
Die eine Asymmetrie, die sich zu einem Signal machen LIESSE, gehoert benannt und
an Etappe 4 uebergeben, nicht hier geloest: nach dem Scharfschalten ist ein zu
kleines Ergebnis in diesem Bereich TOTAL und nicht selektiv — jeder Benutzer
jedes Mandanten verliert gleichzeitig jedes Modul, waehrend die Aktivierungs- und
Freigabetabellen weiterhin Zeilen halten. Die Vorabpruefung von Etappe 4
(`rls-preflight.mjs`) kann genau das feststellen: aktive Aktivierungszeilen
vorhanden, aber die Aufloesung liefert fuer einen bekannten Administrator eine
leere Menge. Eine Laufzeitwarnung an den genannten Stellen wird ERWOGEN und
VERWORFEN, mit derselben Begruendung wie bei `getAllActiveConfigs` im Bereich
`ldap` und den fuenf Stellen im Bereich `tenders`: eine leere Menge ist fuer
einen Benutzer ohne Freigabe und auf einer frischen Installation der
Normalzustand, eine Warnung waere Dauerlaerm und verloere ihr Signal.
In (m4) gehoeren mindestens: T-JTS-02 und T-JTS-03 bleiben aufgezeichnet und
ungefixt (Befund I — die Leseseite bekommt eine zweite Verteidigung, aber erst
nach dem Scharfschalten, und die Schreibseiten-Gegenpruefung bleibt deshalb der
heutige Schutz); die Regel fuer den Modulkatalog in Etappe 3 (Befund E, als
Bedingung formuliert); die offene Architekturfrage `req.tenantPrisma`, die auch
dieser Bereich nicht entscheidet. In (m5): der Waechter, der Controller, das
Frontend, Schema und Migrationen — jeweils mit der Feststellung, dass sie
geprueft und bewusst gelassen sind.
Nichts in dieser Aufgabe wird behauptet, was nicht in derselben Aufgabe gemessen
und mit dem Werkzeug festgeschrieben wurde. Eine Messung, die nicht eingecheckt
ist, ist keine Messung — das ist Fehler 2 der Fehlerliste dieses Vorhabens.
Lies zuerst `apps/api/scripts/rls-scratch-check.mjs` vollstaendig, mindestens
aber `runGroupsAreaChecks` (Aufbau der vier Tabellen und ihrer Regeln),
`runUserAreaChecks` (die Form fuer eine Tabelle OHNE Zeilenschutz und die
Hilfsfunktion zum Auslesen der Fehlerklasse), `extractPolicySql`,
`readGroupsRlsPoliciesMigrationSql`, `readRemainingTenantTablesMigrationSql`,
`report`, `forTenantQuery`, `withAdminPrisma` und `main`. Uebernimm ihre Form,
erfinde keine zweite.
Ergaenze `runModuleRegistryAreaChecks(adminUrl, scratchRoleUrl, results)` mit dem
in `` beschriebenen Aufbau und den dreizehn dort namentlich
festgelegten Pruefungen, und rufe die Funktion in `main()` nach
`runUserAreaChecks` und vor `runTransactionShapeMeasurement` auf. Der Abschnitt
schneidet keine Regel neu — er benutzt die von `runGroupsAreaChecks` bereits
angelegten Tabellen und Regeln. Neu angelegt werden nur die Katalogtabelle ohne
Zeilenschutz, der Eindeutigkeitsindex auf der Aktivierungstabelle, die beiden
Direkt-Freigabezeilen und die Mitgliedschaft, die Pruefung 6 ueberhaupt erst
aussagekraeftig macht.
Fuehre den Lauf gegen die echte Datenbank aus (Container-Adresse zur Laufzeit
ermitteln) und uebernimm die TATSAECHLICHE Ausgabe in die Kritikschrift. Wenn
eine Pruefung anders ausfaellt als in `` erwartet, ist das ein BEFUND
und wird als Befund aufgeschrieben — die Erwartung wird nicht nachtraeglich an
das Ergebnis angepasst und das Ergebnis nicht an die Erwartung.
Fuehre die Anweisungen aus TEIL 2 und TEIL 3 aus und uebernimm ihre Ausgabe samt
Rueckgabewert woertlich.
Schreibe danach den Abschnitt `## Bereich module-registry` in
`docs/mandantentrennung-etappe2-fehlerrichtung.md`, mit den fuenf
Unterabschnitten aus ``. Setze ihn nach `## Bereich user` und vor
`## Verweis`. Schreibe die vorhandenen Abschnitte NICHT um — sie beschreiben
korrekt den Stand zum Zeitpunkt ihrer Messung.
Aendere in dieser Aufgabe KEINEN Produktivcode: kein `forTenant()` in
`apps/api/src/module-registry`, keine Aenderung an Schema, Migrationen,
Compose- oder Beispiel-Umgebungsdateien.
Wenn eine der dreizehn Pruefungen aus einem Grund nicht messbar ist, den dieser
Plan nicht vorhergesehen hat, dann melde das als Befund und miss ersatzweise die
naechstliegende Eigenschaft — aber lasse die Pruefung nicht schweigend aus und
ersetze sie nicht durch eine Behauptung im Text.
DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && OUT=$(TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs) && echo "$OUT" && for K in modulegrant-ungebunden-null-zeilen tenantmoduleactivation-ungebunden-null-zeilen admin-kurzschluss-gebunden-liefert-nur-eigene-aktivierungen direktfreigabe-gebunden-nur-eigene-zeile gruppenpfad-gebunden-folgt-der-gruppenregel gruppenpfad-gebunden-schliesst-die-fremde-gruppe-aus gruppenpfad-ueber-die-wartungsrolle-liefert-die-fremde-gruppe-mit module-tabelle-traegt-keinen-zeilenschutz katalog-bindung-aendert-heute-nichts-an-der-ergebnismenge gebundener-join-auf-den-katalog-liefert-den-modulnamen aktivierung-gebundenes-einfuegen-fremder-mandant-abgelehnt aktivierung-eindeutigkeit-traegt-den-mandanten-keine-unsichtbare-kollision freigabe-eindeutigkeitsindizes-fuehren-mit-der-mandantenkennung; do echo "$OUT" | grep -q "^$K: bestanden" || { echo "FEHLENDE ODER FEHLGESCHLAGENE PRUEFUNG: $K"; exit 1; }; done && echo "$OUT" | grep -qE '^Alle [0-9]+ Pruefungen bestanden\.$' && grep -q '^## Bereich module-registry$' docs/mandantentrennung-etappe2-fehlerrichtung.md && for S in m1 m2 m3 m4 m5; do grep -qE "^### \($S\) " docs/mandantentrennung-etappe2-fehlerrichtung.md || { echo "FEHLENDER UNTERABSCHNITT: ($S)"; exit 1; }; done && npm --prefix apps/api run test && npm --prefix apps/api run type-check && test -z "$(git diff --name-only HEAD -- apps/api/prisma apps/api/src docker-compose.yml docker-compose.dev.yml docker-compose.prod.yml docker-compose.ci.yml .env.example .env.prod.example)"
Das Wegwerf-Werkzeug meldet alle Pruefungen bestanden (die 53 bisherigen plus die dreizehn namentlich geprueften dieses Bereichs) mit Rueckgabewert 0; jede der dreizehn Kennungen steht einzeln als `bestanden` in der Ausgabe; die Belegzeile zur Unsichtbarkeit nennt in ihrem Meldetext die tatsaechlich vorhandene Zeilenzahl, die Zeile zur Katalogtabelle haelt Lesezugriff und Systemkatalog gegeneinander, die Zeile zum Gruppenpfad benennt T-JTS-03, und die Zeile zur Eindeutigkeit stellt die Entlastung den Bereichen `tenders` und `user` namentlich gegenueber; `docs/mandantentrennung-etappe2-fehlerrichtung.md` traegt einen Abschnitt `## Bereich module-registry` mit allen fuenf Unterabschnitten, der tatsaechlich beobachteten Ausgabe, den Belegen aus TEIL 2 und TEIL 3, der Signaltabelle einschliesslich der zwei bewusst ungebundenen Katalogpfade als Grenze, der namentlichen Liste der Leere-als-Abwesenheit-Stellen samt Gegenrichtung, und einer ausdruecklichen, unbeschoenigten Antwort auf die Frage nach dem unterscheidenden Signal samt der an Etappe 4 uebergebenen Vorabpruefung und der begruendeten Verwerfung einer Laufzeitwarnung; `npm --prefix apps/api run test` meldet weiterhin 810 Tests gruen und die Typpruefung ist sauber; unter `apps/api/src`, in Schema, Migrationen sowie in Compose- und Beispiel-Umgebungsdateien ist nichts geaendert.
Aufgabe 2: Den Entscheidungsweg binden — erst die Testlage, dann die Umstellung, beide Stufen an denselben Mandanten
Aufgabe 1 ist abgeschlossen und eingecheckt; der Container `tessera-ctl-db-1` laeuft (Adresse erneut ermitteln, nicht abschreiben).
apps/api/src/module-registry/module-access.service.spec.ts, apps/api/src/module-registry/module-access.service.ts, apps/api/src/module-registry/module.guard.spec.ts
Der Nachweis kommt VOR der Umstellung, sonst beweist er nichts. Die vorhandene
Testdatei hat keine Attrappe fuer das Bindungshilfsmittel (Befund C, erste Form)
— sie wird ZUERST auf den Zwei-Klienten-Nachweis umgebaut, danach wird
umgestellt.
Muster fuer die Testdatei, wortgleich uebernommen aus
`apps/api/src/groups/module-grants.service.spec.ts` (dem bereits umgestellten
Nachbarn auf denselben Modellen), nicht neu erfunden:
- Die Erweiterung `../prisma/prisma-tenant.extension` wird gemockt, sodass
`forTenant(prisma, tenantId)` an `prisma.__makeBoundClient(tenantId)`
delegiert. `withTenantTransaction` wird NICHT gemockt und nicht gebraucht —
dieser Bereich hat keine Transaktion (Befund B).
- Der gebundene Klient ist ein ZWEITES, vom ungebundenen unterscheidbares Objekt
ueber DEMSELBEN Speicher, das protokolliert, welche Aufrufe ueber ihn liefen
(Modellname, Methodenname, Mandantenkennung). Ein reiner Identitaets-Mock
koennte einen vergessenen Bindungsaufruf nicht von einem ungebundenen
unterscheiden — das ist der Grund, aus dem der Bereich `ldap` seinerzeit an
dieser Stelle nichts bewies.
- Alle 15 bestehenden Faelle der Datei bleiben inhaltlich erhalten. Der
bestehende Nachbau filtert bereits auf die Schnittmengen-Bedingung und
unterscheidet Direktweg von Gruppenweg; diese Logik wird ueber den gebundenen
Klienten hinweg beibehalten, nicht ersetzt.
Neue Faelle in dieser Datei (Nachweis VOR Umbau, also zuerst rot):
- Der Kurzschlusszweig bindet seinen Aktivierungs-Lesezugriff an die
Mandantenkennung aus dem Sitzungsnachweis.
- Der Benutzerzweig bindet BEIDE Freigabe-Lesezugriffe (Direktweg und
Gruppenweg) UND den Schnittmengen-Lesezugriff, und ALLE gebundenen Aufrufe
einer Aufloesung tragen DIESELBE Mandantenkennung — nie zwei verschiedene in
derselben Aufloesung.
- Der Vorgabezustand bleibt geschlossen und ueberlebt die Bindung: ein Benutzer
ohne jede Freigabe erhaelt eine leere Menge, UND der Schnittmengen-Lesezugriff
wird gar nicht erst ausgefuehrt (das Protokoll enthaelt ihn nicht). Diese
Reihenfolge ist eine Eigenschaft, die nicht verloren gehen darf.
- Der Rollen-Kurzschluss waechst durch die Bindung nicht: eine Aufloesung fuer
einen zweiten Mandanten leitet keine Module des ersten ab, und die gebundene
Kennung im Protokoll ist die des zweiten.
- Die Modulliste fuer die Seitenleiste erreicht den Katalog ueber den
UNGEBUNDENEN Klienten: der Katalog-Lesezugriff taucht im Bindungsprotokoll
NICHT auf, waehrend er auf dem ungebundenen Nachbau nachweislich stattfindet.
Das ist der Testfall, der jemanden erwischt, der spaeter den Katalog bindet.
- Die Katalogflaggen binden ihren eigenen Aktivierungs-Lesezugriff, und die
darin geschachtelte Aufloesung erzeugt ihren eigenen gebundenen Klienten mit
derselben Mandantenkennung — dieselbe Konvention, die
`module-grants.service.ts` mit seiner Mandanten-Gegenpruefung bereits
vormacht (eine geschachtelte Methode erzeugt ihren eigenen Klienten, gebundene
Klienten werden nicht zwischen Methoden weitergereicht).
In `module.guard.spec.ts` kommt genau EIN Fall dazu, und er hat einen anderen
Zweck als alle uebrigen: er nagelt die ABWESENHEIT eines unterscheidenden
Signals fest. Zwei Aufrufe des Waechters — einer, bei dem die Aufloesung eine
leere Menge liefert, weil der Benutzer tatsaechlich keine Freigabe hat, und
einer, bei dem sie eine leere Menge liefert, obwohl es Freigaben gibt — muessen
heute dieselbe Ausnahme mit derselben Meldung ergeben. Der Test haelt beide
Meldungen gegeneinander und ist damit die maschinelle Fassung des Satzes aus
Aufgabe 1: es gibt heute kein Signal. Sein Kopfkommentar sagt ausdruecklich, dass
dieser Test rot werden SOLL, sobald jemand ein Signal einbaut — und dass dann
sowohl er als auch der Abschnitt (m3) der Kritikschrift nachzuziehen sind.
Erst danach die Umstellung von `module-access.service.ts`:
- In der Aufloesung wird EIN gebundener Klient erzeugt, unter dem Namen
`tenantPrisma`, vor der Rollenverzweigung, und von allen vier
mandantengebundenen Zugriffen dieser Methode benutzt (Kurzschlusszweig,
Direktweg, Gruppenweg, Schnittmenge). Nicht ein Klient je Modellzugriff.
- Die bestehenden `where`-Filter mit der Mandantenkennung bleiben ALLE stehen.
Sie sind nicht redundant, sondern das zweite Netz — dieselbe Begruendung, die
`module-grants.service.ts` fuer seine Filter bereits im Code fuehrt, mit dem
Verweis auf die Messung, dass die Regel auf der Mitgliedschaftstabelle nur die
Gruppenseite prueft (T-JTS-02) und die Regel auf der Freigabetabelle nur die
Mandantenkennung der Zeile (T-JTS-03).
- Die Modulliste fuer die Seitenleiste erzeugt fuer ihren Katalogzugriff KEINEN
gebundenen Klienten. An dieser Stelle steht ein Kommentar, der die
Nichtbindung begruendet — mit der Messung aus Aufgabe 1 (die Tabelle traegt
heute keinen Zeilenschutz, eine Bindung waere heute wirkungslos) UND mit der
Bedingung, unter der die Entscheidung neu zu treffen waere (sobald Etappe 3
dieser Tabelle eine Regel gibt, verschwaende der Katalog fuer jeden
Mandanten). Die Bedingung wird als Bedingung formuliert, nicht als heutige
Tatsache.
- Die Katalogflaggen erzeugen ihren eigenen gebundenen Klienten fuer den
Aktivierungs-Lesezugriff; die geschachtelte Aufloesung erzeugt weiterhin ihren
eigenen. Beide laufen wie bisher nebenlaeufig — das ist genau die Form, die
`module-grants.service.ts` mit drei bzw. vier parallelen gebundenen
Teilabfragen bereits faehrt.
**Kommentar-Hygiene, mit Praezedenzfall — gilt fuer Aufgabe 2 UND Aufgabe 3.**
Die Verifikation beider Aufgaben zaehlt und sucht im Quelltext nach
Zugriffsformen, und beide Zaehlungen koennen von einem Kommentar verfaelscht
werden, der eine solche Form in Code-Schreibweise zitiert. Zwei Formen sind
betroffen: der Name des gebundenen Klienten unmittelbar gefolgt vom Namen des
Katalogmodells (Negativ-Gate: darf im Quelltext ueberhaupt nicht vorkommen), und
der Name des ungebundenen Klienten unmittelbar gefolgt vom Namen eines Modells
(Zaehl-Gate: die Zahl muss exakt der Zahl der tatsaechlichen Katalogzugriffe
entsprechen). Formuliere jede Begruendung deshalb in WORTEN — "der Katalogzugriff
laeuft bewusst ueber den ungebundenen Klienten" — und niemals in
Code-Schreibweise, auch nicht in einem Kommentar, auch nicht als Beispiel.
Praezedenzfall im Projekt: in `tenders.controller.ts` wurde die
Kommentarformulierung aus genau diesem Grund umgestellt. Schlaegt ein Gate an
einer Begruendung statt am Code an, ist die Begruendung umzuformulieren, nicht
das Gate abzuschwaechen.
**Falsifizierungsnachweis, Pflicht und im SUMMARY festzuhalten** (Fehler 5 der
Fehlerliste): baue nach der Umstellung EINEN Bindungsaufruf probeweise zurueck —
den Gruppenweg der Freigabe-Aufloesung, weil er der Pfad ist, dessen Ausfall die
groesste Wirkung haette — halte fest, welcher Test dadurch rot wird und mit
welcher Meldung, nimm den Rueckbau zurueck und halte fest, dass derselbe Lauf
danach wieder gruen ist. Ohne diesen Nachweis ist nicht belegt, dass die neuen
Tests ueberhaupt etwas pruefen.
Lies `apps/api/src/groups/module-grants.service.spec.ts` (Attrappen-Aufbau,
`__makeBoundClient`, Bindungsprotokoll, die Hilfsfunktion zum Pruefen eines
gebundenen Aufrufs, der Block der Bindungstests am Dateiende) und
`apps/api/src/module-registry/module-access.service.spec.ts` vollstaendig, bevor
du etwas aenderst.
Baue `module-access.service.spec.ts` auf den Zwei-Klienten-Nachweis um und
ergaenze die in `` genannten neuen Faelle. Fuehre den Lauf aus und
halte fest, welche der neuen Faelle VOR der Umstellung rot sind — das ist der
Nachweis, dass sie etwas pruefen.
Ergaenze in `module.guard.spec.ts` den einen Fall aus `` samt seinem
Kopfkommentar.
Stelle danach `module-access.service.ts` nach `` um. Halte dich an die
Konvention des bereits umgestellten Nachbarn `module-grants.service.ts`: ein
Klient je Methode unter dem Namen `tenantPrisma`, bestehende `where`-Filter
bleiben stehen, geschachtelte Methoden erzeugen ihren eigenen Klienten.
Fuehre den Falsifizierungsnachweis aus `` durch und halte sein
Ergebnis fuer das SUMMARY fest.
Fasse in dieser Aufgabe `module-registry.service.ts` NICHT an — sie gehoert
vollstaendig zu Aufgabe 3. Fasse `apps/api/src/groups`, `apps/api/src/dashboard`,
`apps/api/src/auth`, `apps/api/src/ldap`, `apps/api/src/user`, Schema,
Migrationen, Compose- und Beispiel-Umgebungsdateien an keiner Stelle an.
Wenn die Umstellung eine Signatur oder eine Aufrufstelle ausserhalb dieser drei
Dateien erzwingt, dann ist das ein Befund: halte ihn fest, entscheide begruendet,
und schreibe die Abweichung ins SUMMARY, statt sie stillschweigend zu machen.
DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs && npm --prefix apps/api run test && npm --prefix apps/api run type-check && grep -q '__makeBoundClient' apps/api/src/module-registry/module-access.service.spec.ts && grep -q "vi.mock('../prisma/prisma-tenant.extension'" apps/api/src/module-registry/module-access.service.spec.ts && grep -q "from '../prisma/prisma-tenant.extension'" apps/api/src/module-registry/module-access.service.ts && BOUND=$(grep -o "tenantPrisma\.[a-zA-Z]*\." apps/api/src/module-registry/module-access.service.ts | wc -l | tr -d ' ') && { test "$BOUND" -ge 5 || { echo "BINDUNG: nur $BOUND gebundene Modellzugriffe in module-access.service.ts, erwartet mindestens 5"; exit 1; }; } && U=$(grep -o "this\.prisma\.[a-zA-Z]*" apps/api/src/module-registry/module-access.service.ts | wc -l | tr -d ' ') && { test "$U" -eq 1 || { echo "KATALOG: $U ungebundene Rohtreffer in module-access.service.ts, erwartet genau 1 (nur der Katalogzugriff)"; exit 1; }; } && { if grep -qE 'tenantPrisma\.module\.' apps/api/src/module-registry/module-access.service.ts; then echo "KATALOG WURDE GEBUNDEN oder die Begruendung nennt die verbotene Zeichenfolge woertlich"; exit 1; fi; } && npm --prefix apps/api run test -- src/module-registry/module-access.service.spec.ts src/module-registry/module.guard.spec.ts && test -z "$(git diff --name-only HEAD -- apps/api/prisma apps/api/src/groups apps/api/src/dashboard apps/api/src/auth apps/api/src/ldap apps/api/src/user apps/api/src/module-registry/module-registry.service.ts apps/api/src/module-registry/module.guard.ts apps/api/src/module-registry/module-registry.controller.ts docker-compose.yml docker-compose.dev.yml docker-compose.prod.yml docker-compose.ci.yml .env.example .env.prod.example)"
`module-access.service.spec.ts` nutzt den Zwei-Klienten-Nachweis ueber `__makeBoundClient`, alle 15 bestehenden Faelle sind inhaltlich erhalten, und die in `` genannten neuen Faelle sind vorhanden und gruen — einschliesslich des Falls, der belegt, dass der Katalogzugriff NICHT im Bindungsprotokoll auftaucht; `module.guard.spec.ts` traegt den Fall, der die Abwesenheit eines unterscheidenden Signals festnagelt, samt Kopfkommentar zu seinem Zweck; in `module-access.service.ts` laufen der Kurzschlusszweig, beide Freigabewege, die Schnittmenge und der Aktivierungszugriff der Katalogflaggen ueber `forTenant()` mit dem Namen `tenantPrisma`, ein Klient je Methode, und ausschliesslich der Katalogzugriff bleibt ungebunden — mit einem Kommentar, der die Messung aus Aufgabe 1 UND die Bedingung fuer eine kuenftige Neubewertung nennt, ohne die vom Gate gesuchte Zeichenfolge zu enthalten; alle bestehenden `where`-Filter mit der Mandantenkennung stehen weiterhin und tragen die Begruendung (T-JTS-02/T-JTS-03), warum sie nicht redundant sind; die Testzahl liegt bei mindestens 810 und der Lauf ist gruen, die Typpruefung ist sauber, das Wegwerf-Werkzeug meldet weiterhin alle Pruefungen bestanden; der Falsifizierungsnachweis ist durchgefuehrt, zurueckgenommen und im SUMMARY mit Testnamen und Fehlermeldung festgehalten; `module-registry.service.ts`, der Waechter, der Controller, die Bereiche `groups`/`dashboard`/`auth`/`ldap`/`user`, Schema, Migrationen sowie Compose- und Beispiel-Umgebungsdateien sind unveraendert.
Aufgabe 3: Die Aktivierungsverwaltung binden, die zwei falschen Kopfkommentare richtigstellen und beide Dokumente samt allen Handzaehlungen nachziehen
Aufgabe 2 ist abgeschlossen und uebersetzt; der Container `tessera-ctl-db-1` laeuft (Adresse erneut ermitteln, nicht abschreiben).
apps/api/src/module-registry/module-registry.service.spec.ts, apps/api/src/module-registry/module-registry.service.ts, docs/mandantentrennung-zugriffsklassifikation.md, docs/mandantentrennung-etappe2-fehlerrichtung.md, .planning/WINDOWS.md
Wieder Nachweis vor Umbau. Diese Datei hatte bisher UEBERHAUPT KEINE Testdatei
(Befund C, zweite Form) und haelt elf der siebzehn Zugriffe des Bereichs,
darunter jeden Schreibpfad. `apps/api/src/module-registry/module-registry.service.spec.ts`
wird neu angelegt, mit demselben Zwei-Klienten-Nachweis und demselben
Bindungsprotokoll wie in Aufgabe 2.
Faelle der neuen Testdatei:
- Die Aktivierungsliste bindet ihren Lesezugriff an den uebergebenen Mandanten
UND liefert die Katalogseite weiterhin mit (die verbundene Modellform
ueberlebt die Bindung) — belegt in Aufgabe 1 durch
`gebundener-join-auf-den-katalog-liefert-den-modulnamen`.
- Die Aktivierungsliste eines zweiten Mandanten liefert keine Zeile des ersten.
- Das Aktivieren erreicht die Katalog-Existenzpruefung UNGEBUNDEN und schreibt
die Aktivierung GEBUNDEN, an den uebergebenen Mandanten.
- Das Aktivieren wirft die Nicht-gefunden-Ausnahme fuer eine unbekannte
Modulkennung, und zwar BEVOR irgendein gebundener Schreibzugriff im Protokoll
steht — die Reihenfolge ist Teil der Eigenschaft.
- Das Deaktivieren bindet alle DREI Aktivierungszugriffe (Lesen der Aktivierung,
Schreiben, sowie die Existenzpruefung der Aktivierung) an DENSELBEN Mandanten,
ueber EINEN Klienten.
- Das Deaktivieren wirft die Nicht-gefunden-Ausnahme, wenn keine Aktivierung
vorliegt — die LAUTE, harmlose Richtung dieses Bereichs, hier ausdruecklich
als solche festgenagelt, damit sie bei einem spaeteren Umbau nicht
versehentlich in ein stilles `false` verwandelt wird.
- Die Aktiv-Abfrage bindet ihren Aktivierungs-Lesezugriff und erreicht den
Katalog ungebunden; ohne Aktivierung liefert sie `false` — die STILLE Richtung,
ebenfalls festgenagelt, mit einem Kommentar, der sie als solche benennt.
- Kein Katalogzugriff des Dienstes — weder die Gesamtliste, noch die
Kennzeichen-Suche, noch die Existenzpruefungen, noch die Katalogpflege beim
Start — taucht im Bindungsprotokoll auf. Das ist derselbe Wachhund wie in
Aufgabe 2, hier fuer sechs Zugriffe statt fuer einen.
Erst danach die Umstellung von `module-registry.service.ts`, je Methode ein
Klient unter dem Namen `tenantPrisma`:
- Aktivierungsliste: Lesezugriff GEBUNDEN, verbundene Modellform bleibt.
- Aktivieren: Katalog-Existenzpruefung UNGEBUNDEN, Aktivierungsschreibzugriff
GEBUNDEN.
- Deaktivieren: Katalog-Existenzpruefung UNGEBUNDEN, beide Aktivierungszugriffe
GEBUNDEN, ueber einen Klienten.
- Aktiv-Abfrage: Katalogsuche UNGEBUNDEN, Aktivierungs-Lesezugriff GEBUNDEN.
- Gesamtliste, Kennzeichen-Suche, Katalogpflege beim Start: UNGEBUNDEN, mit
derselben Begruendungsform wie in Aufgabe 2 (Messung plus Bedingung), und bei
der Katalogpflege zusaetzlich mit dem Grund, den nur sie hat: sie laeuft beim
Anwendungsstart aus vier Seed-Dateien, ohne Anfrage und ohne Mandanten — ein
gebundener Aufruf haette dort strukturell keinen Kontext.
- Die Kennzeichen-Suche bekommt zusaetzlich einen Satz, den keine andere
Katalogstelle braucht: sie ist die Stelle, die der Waechter bei JEDER
Modulanfrage aufruft, und eine Bindung wuerde jede Modulanfrage mit einer
Meldung abweisen, die faelschlich von einer fehlenden Aktivierung spricht.
**Die zwei Richtigstellungen (Befund G), mit der Messung aus Aufgabe 1 belegt:**
der Kopfkommentar der Aktiv-Abfrage behauptet, der Waechter benutze sie — er tut
es nicht, sie hat null Aufrufer. Der Kopfkommentar der Aktivierungsliste wird
vom Controller-Kommentar als Lieferant des Marktplatz-Katalogs benannt — auch sie
hat null Aufrufer, der Marktplatz laeuft ueber die Katalogflaggen. Beide
Kommentare werden richtiggestellt und BEIDE Methoden trotzdem umgestellt, mit
dem ausgeschriebenen Grund: heute toter, ungebunden gelassener Code ist die
Falle fuer den, der ihn morgen verdrahtet. Der irrefuehrende Satz im
Kopfkommentar des Controllers wird NICHT mitgeaendert — der Controller ist nicht
Teil dieses Plans; stattdessen wird die Feststellung in (m5) der Kritikschrift
aufgenommen, damit sie nicht verloren geht.
**Die Dokumente.** In `docs/mandantentrennung-zugriffsklassifikation.md`:
- Fuenf Bestandsaufnahme-Zeilen. Die drei Aktivierungs-/Freigabe-Zeilen wechseln
auf `gebunden`, mit Begruendung im Stil der bereits umgestellten Bereiche
(Quick-Kennung, was umgestellt wurde, und warum die anwendungsseitigen Filter
stehen bleiben). Die zwei Katalog-Zeilen BLEIBEN auf `ungebunden`, bekommen
aber eine Begruendung, die den Unterschied zwischen "noch offen" und "bewusst
und dauerhaft so" traegt: die Messung aus Aufgabe 1 (keine Regel auf der
Tabelle), der Grund fuer die Nichtbindung (Wahrhaftigkeit der Aufzeichnung),
und die Bedingung, unter der neu zu entscheiden waere (eine Regel in Etappe 3).
Die Stand-Spalte kennt nur drei Werte — die Unterscheidung MUSS deshalb in der
Begruendung stehen, sie kann nirgends sonst stehen.
- Der Abschnitt zur Hintergrunddienst-Falle: dieser Bereich fuegt KEINEN
sechsten Fall hinzu, und das gehoert gemessen statt angenommen — die
Katalogpflege beim Start schreibt zwar ohne Mandantenkontext, iteriert aber
ueber nichts je Mandant und hat damit nicht die Bauform der fuenf gefuehrten
Faelle. Dieser Satz kommt in den Abschnitt, mit Begruendung, damit die
Abwesenheit belegt ist und nicht wie ein Uebersehen aussieht.
- Uebersichtszeile, Summenzeile, Klassen-Verteilung samt Ueberschrift: alle vier
Zahlen werden am Code bzw. an der Bestandsaufnahme NEU ERMITTELT und
eingetragen. Die Erwartung aus Befund K (sieben ungebunden, zehn gebunden)
wird NICHT abgeschrieben — sie wird gemessen und darf abweichen; weicht sie
ab, ist die Abweichung ein Befund fuers SUMMARY.
In `docs/mandantentrennung-etappe2-fehlerrichtung.md` kommt ans Ende des
Abschnitts `## Bereich module-registry` ein Nachtrag im Stil der vorherigen
Bereiche: der Text darueber wird NICHT umgeschrieben, der Nachtrag haelt fest,
was Aufgabe 2 und 3 tatsaechlich umgesetzt haben, haelt die tatsaechlich
umgestellten Pfade gegen die in (m2) angekuendigten, und nennt BEIDE
Falsifizierungsnachweise (aus Aufgabe 2 und aus dieser Aufgabe) mit Testname und
Fehlermeldung. Ein Nachweis, der nur in einer Commit-Meldung steht, gilt nicht
als festgehalten — das ist Fehler 5 der Fehlerliste.
In `.planning/WINDOWS.md` kommt ein neuer offener Eintrag der Klasse
`deviation` auf `apps/api/src/module-registry/module-access.service.ts`: es gibt
heute kein Signal, das "wirklich keine Freigabe" von "die Abfrage hat nichts
gefunden" unterscheidet; nach dem Scharfschalten ist der Ausfall total und
lautlos und sieht fuer den Betroffenen wie ein absichtlicher Entzug aus; die
konkrete Vorabpruefung fuer Etappe 4 (aktive Aktivierungszeilen vorhanden, aber
die Aufloesung liefert fuer einen bekannten Administrator eine leere Menge) ist
benannt; die Laufzeitwarnung ist erwogen und begruendet verworfen. Die Datei
fuehrt den Bestand ZWEIMAL — als Tabelle und als JSON-Block; beide muessen den
neuen Eintrag tragen, sonst ist der Bestand in sich widerspruechlich.
**Falsifizierungsnachweis, Pflicht und im SUMMARY festzuhalten:** baue einen
Bindungsaufruf dieser Aufgabe probeweise zurueck — den Schreibzugriff des
Deaktivierens, weil er der einzige gebundene Schreibzugriff des Bereichs mit
sichtbarer Wirkung ist — halte fest, welcher Test rot wird und mit welcher
Meldung, nimm den Rueckbau zurueck, halte fest, dass derselbe Lauf danach wieder
gruen ist.
Lies `apps/api/src/module-registry/module-registry.service.ts` vollstaendig und
die Attrappen-Form aus `apps/api/src/groups/module-grants.service.spec.ts`
erneut, falls sie nicht mehr im Kontext ist.
Lege `apps/api/src/module-registry/module-registry.service.spec.ts` neu an, mit
den in `` genannten Faellen, und fuehre den Lauf aus, BEVOR du den
Dienst umstellst — halte fest, welche Faelle rot sind.
Stelle danach `module-registry.service.ts` nach `` um, mit denselben
Konventionen wie in Aufgabe 2 (ein Klient je Methode unter dem Namen
`tenantPrisma`, bestehende Filter bleiben). Der Absatz zur Kommentar-Hygiene aus
Aufgabe 2 gilt hier unveraendert und ist hier sogar strenger wirksam: diese Datei
hat sechs Katalogzugriffe, und das Zaehl-Gate verlangt genau diese sechs — eine
Begruendung in Code-Schreibweise wuerde die Zaehlung verfaelschen.
Stelle beide Kopfkommentare aus Befund G richtig, mit Bezug auf die
Aufrufermessung aus Aufgabe 1.
Ziehe danach `docs/mandantentrennung-zugriffsklassifikation.md` nach: fuenf
Bestandsaufnahme-Zeilen, der Abschnitt zur Hintergrunddienst-Falle, und alle
Handzaehlungen. Ermittle die Rohtrefferzahlen fuer die Uebersichtszeile mit den
im Dokument selbst genannten Messanweisungen NEU, statt sie abzuschreiben, und
trage die gemessenen Werte ein. Laufe danach die maschinelle Pruefung
`rls-access-inventory.spec.ts` und ziehe nach, was sie beanstandet — sie ist die
Autoritaet fuer die Bestandsaufnahme, nicht dieser Plan.
Ergaenze den Nachtrag in der Kritikschrift und den neuen Eintrag in
`.planning/WINDOWS.md` (Tabelle UND JSON-Block).
Fuehre den Falsifizierungsnachweis aus `` durch und halte sein
Ergebnis fuer das SUMMARY fest.
Fasse `apps/api/src/groups`, `apps/api/src/dashboard`, `apps/api/src/auth`,
`apps/api/src/ldap`, `apps/api/src/user`, den Waechter, den Controller, Schema,
Migrationen sowie Compose- und Beispiel-Umgebungsdateien an keiner Stelle an.
`DATABASE_URL` bleibt unveraendert auf der Rolle `tessera` — das Scharfschalten
ist Etappe 4.
Weicht eine der vier Handzaehlungen von der Erwartung aus Befund K ab, dann ist
die Messung massgeblich und die Abweichung ein Befund fuers SUMMARY — nicht
umgekehrt.
DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs && npm --prefix apps/api run test && npm --prefix apps/api run type-check && npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts && test -f apps/api/src/module-registry/module-registry.service.spec.ts && grep -q '__makeBoundClient' apps/api/src/module-registry/module-registry.service.spec.ts && grep -q "from '../prisma/prisma-tenant.extension'" apps/api/src/module-registry/module-registry.service.ts && CU=$(grep -o "this\.prisma\.[a-zA-Z]*" apps/api/src/module-registry/module-registry.service.ts | wc -l | tr -d ' ') && { test "$CU" -eq 6 || { echo "KATALOG: $CU ungebundene Rohtreffer in module-registry.service.ts, erwartet genau 6 (nur die Katalogzugriffe)"; exit 1; }; } && for F in apps/api/src/module-registry/module-access.service.ts apps/api/src/module-registry/module-registry.service.ts; do if grep -qE 'tenantPrisma\.module\.' "$F"; then echo "KATALOG WURDE GEBUNDEN oder die Begruendung nennt die verbotene Zeichenfolge woertlich: $F"; exit 1; fi; done && test 0 -eq "$(grep -rn '\$transaction(' apps/api/src/module-registry --include=*.ts | grep -v spec | grep -vE '^[^:]*:[0-9]+:[[:space:]]*(\*|//|/\*)' | wc -l | tr -d ' ')" && grep -qE '^\| apps/api/src/module-registry/module-access\.service\.ts \| moduleGrant \| muss-mandantengebunden \| gebunden \|' docs/mandantentrennung-zugriffsklassifikation.md && grep -qE '^\| apps/api/src/module-registry/module-access\.service\.ts \| tenantModuleActivation \| muss-mandantengebunden \| gebunden \|' docs/mandantentrennung-zugriffsklassifikation.md && grep -qE '^\| apps/api/src/module-registry/module-registry\.service\.ts \| tenantModuleActivation \| muss-mandantengebunden \| gebunden \|' docs/mandantentrennung-zugriffsklassifikation.md && grep -qE '^\| apps/api/src/module-registry/module-access\.service\.ts \| module \| keine-mandantengebundene-tabelle \| ungebunden \|' docs/mandantentrennung-zugriffsklassifikation.md && grep -qE '^\| apps/api/src/module-registry/module-registry\.service\.ts \| module \| keine-mandantengebundene-tabelle \| ungebunden \|' docs/mandantentrennung-zugriffsklassifikation.md && U=$(grep -ro "this\.prisma\.[a-zA-Z]*" apps/api/src/module-registry | grep -v spec | wc -l | tr -d ' ') && B=$(grep -ro "tenantPrisma\.[a-zA-Z]*\." apps/api/src/module-registry | grep -v spec | wc -l | tr -d ' ') && { test "$U" -lt 17 || { echo "UEBERSICHTSZEILE: ungebundene Rohtreffer in apps/api/src/module-registry sind $U, also nicht gesunken — es wurde nichts umgestellt"; exit 1; }; } && { test "$B" -gt 0 || { echo "UEBERSICHTSZEILE: gebundene Rohtreffer in apps/api/src/module-registry sind $B"; exit 1; }; } && { grep -qE "^\| module-registry \| ${U} \| ${B} \| \*\*war 17/0\*\*" docs/mandantentrennung-zugriffsklassifikation.md || { echo "UEBERSICHTSZEILE module-registry nennt nicht die neu gemessenen Zahlen ${U}/${B} im etablierten Stil"; exit 1; }; } && awk -F'|' '$2 ~ /^ *[a-z][a-z-]* *$/ && $3 ~ /^ *[0-9]+ *$/ && $4 ~ /^ *[0-9]+ *$/ { su+=$3; sb+=$4; n++ } $2 ~ /^ *\*\*Summe\*\* *$/ && $4 !~ /^ *$/ { g3=$3; g4=$4; gsub(/[^0-9]/,"",g3); gsub(/[^0-9]/,"",g4); ru=g3+0; rb=g4+0; seen=1 } END { if (!seen || n+0 != 12) { print "UEBERSICHTSTABELLE nicht erkannt, Bereichszeilen: " n; exit 1 } if (su+0 != ru || sb+0 != rb) { print "SUMMENZEILE stimmt nicht: Bereichszeilen ergeben " su "/" sb ", Summenzeile nennt " ru "/" rb; exit 1 } }' docs/mandantentrennung-zugriffsklassifikation.md && awk -F'|' '$2 ~ /^ *apps\/api\/src\// { k=$4; gsub(/^ +| +$/,"",k); cls[k]++; pairs++ } $2 ~ /^ *(muss-mandantengebunden|keine-mandantengebundene-tabelle|beides|bewusst-uebergreifend) *$/ && $3 ~ /^ *[0-9]+ *$/ && $4 ~ /^ *$/ { k=$2; gsub(/^ +| +$/,"",k); v=$3; gsub(/[^0-9]/,"",v); tab[k]=v+0; tn++ } $2 ~ /^ *\*\*Summe\*\* *$/ && $4 ~ /^ *$/ { v=$3; gsub(/[^0-9]/,"",v); tsum=v+0; tseen=1 } /^## Klassen-Verteilung/ { h=$0; gsub(/[^0-9]/,"",h); hp=h+0; hseen=1 } END { if (tn+0 != 4 || !tseen || !hseen) { print "KLASSEN-VERTEILUNG nicht erkannt: Klassenzeilen " tn ", Summenzeile " tseen ", Ueberschrift " hseen; exit 1 } if (tsum != pairs+0) { print "KLASSEN-SUMME stimmt nicht: Bestandsaufnahme hat " pairs " Paare, Tabellensumme nennt " tsum; exit 1 } if (hp != pairs+0) { print "UEBERSCHRIFT der Klassen-Verteilung nennt " hp " Paare, Bestandsaufnahme hat " pairs; exit 1 } s=0; for (k in tab) { if (tab[k] != cls[k]+0) { print "KLASSE " k ": Tabelle nennt " tab[k] ", Bestandsaufnahme zaehlt " cls[k]+0; exit 1 } s+=tab[k] } if (s != pairs+0) { print "KLASSENZEILEN ergeben " s ", Bestandsaufnahme hat " pairs; exit 1 } }' docs/mandantentrennung-zugriffsklassifikation.md && awk 'BEGIN{split("ein zwei drei vier x sechs sieben acht neun",w," "); w[5]="fünf"} /^## Der Hintergrunddienst als Falle/{seen=1; head=$0; f=1; next} /^## /{f=0} f && /^- \*\*`/{n++} f && /^\*\*Der .* Fall, anderer Bauart/{n++} f && /module-registry/{m=1} END{ if(!seen){print "ABSCHNITT Hintergrunddienst nicht gefunden"; exit 1} want="## Der Hintergrunddienst als Falle — " w[n] " Fälle"; if(head != want){printf "HINTERGRUNDDIENST-UEBERSCHRIFT nennt \"%s\", gezaehlt wurden %d Faelle, erwartet \"%s\"\n", head, n, want; exit 1} if(!m){print "ABSCHNITT Hintergrunddienst nennt diesen Bereich nicht — die Abwesenheit eines sechsten Falls ist nicht belegt"; exit 1} }' docs/mandantentrennung-zugriffsklassifikation.md && python3 -c "
import re,sys
s=open('.planning/WINDOWS.md',encoding='utf-8').read()
rows=[l for l in s.splitlines() if re.match(r'^\| \d+ \|', l)]
ids=re.findall(r'\"id\": (\d+),', s)
if len(rows)!=len(ids): print('WINDOWS: %d Tabellenzeilen, %d JSON-Eintraege' % (len(rows), len(ids))); sys.exit(1)
if not any(re.match(r'^\| 23 \|', l) for l in rows): print('WINDOWS: Eintrag 23 fehlt in der Tabelle'); sys.exit(1)
if '23' not in ids: print('WINDOWS: Eintrag 23 fehlt im JSON-Block'); sys.exit(1)
if 'module-registry' not in s: print('WINDOWS: kein Eintrag nennt diesen Bereich'); sys.exit(1)
" && grep -q 'Nachtrag (260910-exd' docs/mandantentrennung-etappe2-fehlerrichtung.md && test -z "$(git diff --name-only HEAD -- apps/api/prisma apps/api/src/groups apps/api/src/dashboard apps/api/src/auth apps/api/src/ldap apps/api/src/user apps/api/src/module-registry/module.guard.ts apps/api/src/module-registry/module-registry.controller.ts docker-compose.yml docker-compose.dev.yml docker-compose.prod.yml docker-compose.ci.yml .env.example .env.prod.example)"
`apps/api/src/module-registry/module-registry.service.spec.ts` existiert, nutzt den Zwei-Klienten-Nachweis und deckt die in `` genannten Faelle ab, einschliesslich der beiden ausdruecklich festgenagelten Richtungen (die laute Nicht-gefunden-Ausnahme beim Deaktivieren, das stille `false` der Aktiv-Abfrage) und des Wachhunds, der jeden der sechs Katalogzugriffe aus dem Bindungsprotokoll heraushaelt; in `module-registry.service.ts` laufen die fuenf Aktivierungszugriffe ueber `forTenant()` unter dem Namen `tenantPrisma`, ein Klient je Methode, und die sechs Katalogzugriffe bleiben ungebunden — jeder mit einer Begruendung, die Messung und Bedingung trennt, die Katalogpflege zusaetzlich mit ihrem eigenen Startgrund und die Kennzeichen-Suche zusaetzlich mit der Folge, die eine Bindung fuer jede Modulanfrage haette; beide falschen Kopfkommentare sind mit Bezug auf die Aufrufermessung richtiggestellt und beide Methoden trotzdem umgestellt, mit ausgeschriebenem Grund; der Testlauf ist gruen mit mindestens 810 Tests, die Typpruefung sauber, das Wegwerf-Werkzeug meldet alle Pruefungen bestanden; `rls-access-inventory.spec.ts` laeuft gruen und stimmt mit den fuenf Bestandsaufnahme-Zeilen des Bereichs ueberein; ALLE FUENF handgepflegten Stellen sind nachgezogen UND einzeln maschinell gegatet — die Uebersichtszeile gegen die am Code neu ermittelten Rohtrefferzahlen, die Summenzeile gegen die Addition der zwoelf Bereichszeilen, die Klassen-Verteilung gegen die ueber die Bestandsaufnahme nachgezaehlten Klassen samt Summe, die Ueberschrift der Klassen-Verteilung gegen dieselbe Paarzahl, und der Abschnitt zur Hintergrunddienst-Falle gegen die aus seinen eigenen Faellen abgeleitete Zahl im Ueberschriftstext PLUS den Beleg, dass dieser Bereich dort ausdruecklich genannt ist; keine dieser fuenf Pruefungen kann von einer stehen gebliebenen Fassung bestanden werden; die Kritikschrift traegt den Nachtrag mit den tatsaechlich umgesetzten Pfaden und BEIDEN Falsifizierungsnachweisen samt Testname und Fehlermeldung; `.planning/WINDOWS.md` traegt den neuen offenen Eintrag in Tabelle UND JSON-Block, mit der benannten Vorabpruefung fuer Etappe 4 und der begruendeten Verwerfung einer Laufzeitwarnung; der Waechter, der Controller, die Bereiche `groups`/`dashboard`/`auth`/`ldap`/`user`, Schema, Migrationen sowie Compose- und Beispiel-Umgebungsdateien sind unveraendert und `DATABASE_URL` zeigt weiterhin auf die Rolle `tessera`.
Konfiguriert: ASVS-Stufe 1, blockierend ab `high`.
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Browser/Benutzer → Modul-API | Mandantenkennung, Benutzerkennung und Rolle stammen ausschliesslich aus dem validierten Sitzungsnachweis (`request.user`, gesetzt von der JWT-Pruefstrategie ueber das httpOnly-Cookie), nie aus Koerper oder Parametern (T-03-04/T-15-10). Alles jenseits dieser Grenze ist nicht vertrauenswuerdig. |
| Benutzer → Modul, das sein Mandant nicht aktiviert hat | Die Aktivierungsstufe. Heute ALLEIN vom Anwendungscode gezogen (`where` mit der Mandantenkennung), nach diesem Plan zusaetzlich von der Datenbank — aber erst wirksam nach Etappe 4. |
| Benutzer → Modul, das ihm nicht freigegeben ist | Die Freigabestufe, Vorgabezustand geschlossen. ADMIN und SUPER_ADMIN umgehen sie ausdruecklich (D-03), aber NICHT die Aktivierungsstufe. |
| Mandant A → Freigabe- und Aktivierungszeilen des Mandanten B | Die Grenze, um die es in diesem Plan geht. |
| Freigabezeile → referenzierte Gruppe eines fremden Mandanten | Die Grenze, die die Regel auf der Freigabetabelle NACHWEISLICH nicht zieht (T-JTS-03). Heute allein von der Schreibseiten-Gegenpruefung in `module-grants.service.ts` gezogen. |
| API → PostgreSQL | Die Zeilenschutz-Grenze. Heute wirkungslos, weil die Rolle `BYPASSRLS` traegt (WINDOWS #18) — dieser Plan bereitet sie vor, schaltet sie NICHT scharf. |
| API → plattformweiter Modulkatalog | Eine bewusst durchlaessige Grenze: der Katalog gehoert keinem Mandanten und traegt heute keinen Zeilenschutz. |
| Anwendungsstart → Datenbank | Die Katalogpflege laeuft ohne Anfrage, ohne Sitzungsnachweis und ohne Mandanten. |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-EXD-01 | Elevation of Privilege | `module-access.service.ts`, Aktivierungsstufe | high | mitigate | Ein Benutzer des Mandanten A erhaelt Zugriff auf ein nur fuer B aktiviertes Modul. Beide Aktivierungs-Lesezugriffe der Aufloesung werden gebunden; der `where`-Filter mit der Mandantenkennung bleibt zusaetzlich stehen. Gemessen in Aufgabe 1: `admin-kurzschluss-gebunden-liefert-nur-eigene-aktivierungen`. |
| T-EXD-02 | Tampering | `module-registry.service.ts`, Aktivieren/Deaktivieren | high | mitigate | Eine Aktivierung wird unter dem falschen Mandanten geschrieben oder gelesen. Alle fuenf Aktivierungszugriffe gebunden, je Methode EIN Klient. Gemessen: `aktivierung-gebundenes-einfuegen-fremder-mandant-abgelehnt`. |
| T-EXD-03 | Elevation of Privilege | `module-access.service.ts`, Rollen-Kurzschluss | high | mitigate | Der ADMIN/SUPER_ADMIN-Kurzschluss wird durch eine falsch gebundene Abfrage verbreitert. Die Kennung stammt aus dem Sitzungsnachweis und steht bereits im `where`; die Bindung kann deshalb nur einschraenken, nie oeffnen (Befund J). In Aufgabe 2 als Test festgenagelt: eine Aufloesung fuer einen zweiten Mandanten leitet keine Module des ersten ab. |
| T-EXD-04 | Elevation of Privilege | `module-access.service.ts`, Vorgabezustand | critical | mitigate | Der geschlossene Vorgabezustand faellt offen — eine leere Freigabemenge fuehrt versehentlich zur vollen Aktivierungsliste. In Aufgabe 2 als Test festgenagelt: ohne Freigaben leere Menge UND der Schnittmengen-Lesezugriff findet gar nicht erst statt. |
| T-EXD-05 | Denial of Service | `module-access.service.ts`, `module.guard.ts` | high | mitigate | Die umgekehrte Fehlerrichtung: ein zu kleines Ergebnis entzieht einem berechtigten Benutzer lautlos jeden Zugriff und liest sich als absichtlicher Entzug. Vollstaendige Bindung beider Stufen; Signaltabelle und namentliche Liste der Leere-als-Abwesenheit-Stellen in der Kritikschrift; offener Eintrag im Broken-Windows-Register mit der Vorabpruefung fuer Etappe 4. |
| T-EXD-06 | Repudiation | `module.guard.ts` | high | accept | Es gibt heute KEIN Signal, das "wirklich keine Freigabe" von "die Abfrage hat nichts gefunden" unterscheidet — beide erzeugen dieselbe 403-Meldung, dieselbe leere Liste mit Status 200 und keinen Protokolleintrag. Bewusst akzeptiert und AUFGEZEICHNET statt durch eine Laufzeitwarnung ueberdeckt, die im Normalbetrieb Dauerlaerm waere (dieselbe Begruendung wie bei `getAllActiveConfigs` im Bereich `ldap`). In Aufgabe 2 als Test festgenagelt, damit die Aufzeichnung rot wird, sobald jemand ein Signal einbaut. |
| T-EXD-07 | Tampering | `ModuleGrant`-Regel, referenzierte Gruppe | high | transfer | Die ausgelieferte Regel prueft nur die Mandantenkennung der Zeile, nicht die referenzierte Gruppe (T-JTS-03). Bleibt aufgezeichnet und ungefixt; der Schutz bleibt bei der Schreibseiten-Gegenpruefung in `module-grants.service.ts`, die dieser Plan nicht anfasst. Die Bindung fuegt auf der LESESEITE eine zweite Verteidigung hinzu, aber erst nach Etappe 4 — gemessen in Aufgabe 1, Pruefungen 6 und 7. |
| T-EXD-08 | Information Disclosure | Modulkatalog (`Module`) | medium | mitigate | Der plattformweite Katalog wird versehentlich gebunden und verschwindet, sobald Etappe 3 ihm eine Regel gibt. Negativ-Gate in beiden Verifikationen, plus ein Testfall je Datei, der jeden Katalogzugriff aus dem Bindungsprotokoll heraushaelt. |
| T-EXD-09 | Spoofing | Sitzungsnachweis | low | accept | Mandantenkennung aus Koerper oder Parametern statt aus dem Sitzungsnachweis. Bereits in Phase 15 behandelt (T-03-04/T-15-10); dieser Plan aendert daran nichts und schwaecht es nicht. |
| T-EXD-10 | Tampering | Eindeutigkeitsschluessel dieses Bereichs | low | accept | Die Kette aus den Bereichen `tenders` und `user` (unsichtbare Zeile, falsches "frei", harter Eindeutigkeitsfehler) kann hier strukturell nicht auftreten, weil alle drei betroffenen Eindeutigkeitsschluessel mit der Mandantenkennung fuehren. Gemessen in Aufgabe 1, Pruefungen 12 und 13 — akzeptiert auf Grundlage einer Messung, nicht einer Annahme. |
| T-EXD-SC | Tampering | Paketinstallation | low | accept | Dieser Plan installiert kein Paket (npm/pip/cargo) und fuegt keine Abhaengigkeit hinzu. Das Legitimitaets-Gate faellt damit nicht an; ausdruecklich festgehalten statt schweigend ausgelassen. |
Nach Abschluss aller drei Aufgaben:
1. `node apps/api/scripts/rls-scratch-check.mjs` meldet alle Pruefungen bestanden
(53 bisherige plus 13 neue), Rueckgabewert 0.
2. `npm --prefix apps/api run test` meldet mindestens 810 Tests gruen; die
Dateizahl ist um mindestens eine gestiegen (die neue Testdatei fuer die
Aktivierungsverwaltung).
3. `npm --prefix apps/api run type-check` ist sauber.
4. `npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts`
ist gruen — die Bestandsaufnahme stimmt mit dem Quelltext ueberein.
5. Alle fuenf handgepflegten Stellen der Klassifikation sind maschinell gegatet
und gruen.
6. `git diff --name-only HEAD` nennt keine Datei unter `apps/api/prisma`, keine
Compose- oder Beispiel-Umgebungsdatei, und keine Datei der Bereiche `groups`,
`dashboard`, `auth`, `ldap`, `user`.
7. `DATABASE_URL` zeigt unveraendert auf die Rolle `tessera`.
- Die siebzehn Zugriffe des Bereichs sind vollstaendig entschieden: zehn
gebunden, sieben begruendet ungebunden, keiner unentschieden.
- Die Begruendung fuer jeden ungebundenen Zugriff trennt Messung von Bedingung
— sie behauptet nichts, was heute nicht gilt, und sie verschweigt nicht, was
morgen gelten wuerde.
- Die umgekehrte Fehlerrichtung ist an der echten, ausgelieferten Regel gemessen
und je Pfad mit ihrem konkreten Signal beschrieben.
- Die Frage nach dem unterscheidenden Signal ist beantwortet, und wenn die
Antwort "keines" lautet, steht sie so im Text UND als Testfall UND als offener
Eintrag im Broken-Windows-Register.
- Beide Falsifizierungsnachweise sind durchgefuehrt, zurueckgenommen und im
SUMMARY festgehalten — mit Testname und Fehlermeldung, nicht als Behauptung.
- Baseline gehalten am Ende jeder Aufgabe, nicht nur am Ende des Plans.
- Der Schalter ist weiterhin AUS.
Nach diesem Bereich verbleiben in Etappe 2 fuenf Bereiche, nach heutiger
Rohtrefferzahl: `dashboard` (13), `calendar` (12), `tenant` (8), `favorites` (7),
`auth` (5, gemischt), `settings` (4).
Zwei Reihenfolgebedingungen aus diesem Durchlauf, fuer die Planung der
folgenden Bereiche festgehalten:
- `dashboard` bekommt seinen Modulzugriffs-Filter durch DIESEN Durchlauf bereits
richtig, weil die Bindung im Dienst sitzt (Befund F). Der Bereich `dashboard`
muss dafuer nichts nachholen; seine eigenen dreizehn Zugriffe bleiben davon
unberuehrt.
- `settings` ist weiterhin die offene Reihenfolgebedingung des Bereichs
`tenders` (Befund K dort, SMTP-Zugangsdaten) und ist mit vier Rohtreffern der
kleinste verbleibende Bereich.