Files
tessera-ctl/.planning/quick/260910-exd-mandantentrennung-etappe-2-bereich-modul/260910-exd-PLAN.md
T

77 KiB

phase, plan, type, wave, depends_on, autonomous, requirements, files_modified, estimate, must_haves
phase plan type wave depends_on autonomous requirements files_modified estimate must_haves
quick-260910-exd 01 execute 1
true
WINDOWS-18
ETAPPE-2-MODULE-REGISTRY
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
tokens raw_tokens tasks confidence
195000 195000 3 low
truths artifacts key_links
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.
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
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.

<execution_context> @/.claude/gsd-core/workflows/execute-plan.md @/.claude/gsd-core/templates/summary.md </execution_context>

@.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

<planning_time_findings>

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).

</planning_time_findings>

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 <behavior> 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 <behavior> 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 <behavior>. 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 <behavior> 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 <behavior> samt seinem Kopfkommentar.

Stelle danach module-access.service.ts nach <behavior> 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 <behavior> 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 <behavior> 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 <behavior> 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 <behavior> 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 <behavior> 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)"</automated> </verify> <done>apps/api/src/module-registry/module-registry.service.spec.tsexistiert, nutzt den Zwei-Klienten-Nachweis und deckt die ingenannten Faelle ab, einschliesslich der beiden ausdruecklich festgenagelten Richtungen (die laute Nicht-gefunden-Ausnahme beim Deaktivieren, das stillefalseder Aktiv-Abfrage) und des Wachhunds, der jeden der sechs Katalogzugriffe aus dem Bindungsprotokoll heraushaelt; inmodule-registry.service.tslaufen die fuenf Aktivierungszugriffe ueberforTenant()unter dem NamentenantPrisma, 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.tslaeuft 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.mdtraegt 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 Bereichegroups/dashboard/auth/ldap/user, Schema, Migrationen sowie Compose- und Beispiel-Umgebungsdateien sind unveraendert und DATABASE_URLzeigt weiterhin auf die Rolletessera`.

<threat_model>

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.

</threat_model>

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.

<success_criteria>

  • 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.

</success_criteria>

<next_stages>

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.

</next_stages>

Erstelle `.planning/quick/260910-exd-mandantentrennung-etappe-2-bereich-modul/260910-exd-SUMMARY.md`, wenn alle drei Aufgaben abgeschlossen sind.