docs(quick-260911-e2s): Etappe 2 Bereich tenant abgeschlossen, WINDOWS #27 Relations-Blindstelle
Tessera CI/CD / Lint & Type Check (push) Successful in 47s
Tessera CI/CD / Tests (push) Successful in 56s
Tessera CI/CD / Build & Publish Images (push) Successful in 29s

This commit is contained in:
2026-09-11 11:08:33 +02:00
parent c8de72e762
commit 6236b302f4
4 changed files with 441 additions and 3 deletions
+1
View File
@@ -387,6 +387,7 @@ None yet.
| 260910-jab | **Die drei zu kurz greifenden Datenbankregeln geschlossen** — auf ausdrueckliche Anweisung des Users VORGEZOGEN, entgegen der geplanten Reihenfolge (urspruenglich nach Etappe 2, damit jeder Bereich gegen einen stabilen Regelstand misst; der User entschied anders, weil offene Loecher vergessen werden). Erster Durchlauf dieser Serie, der die DATENBANK aendert statt nur Anwendungscode — neue Migration `20260910120000_rls_widen_membership_grant_and_platform_read`. **T-JTS-02:** `GroupMembership` prueft jetzt BEIDE Seiten (Gruppe UND Benutzer gehoeren zum Mandanten) statt nur die Gruppenseite. **T-JTS-03:** `ModuleGrant` prueft zusaetzlich, dass die referenzierte Gruppe bzw. der referenzierte Benutzer zum selben Mandanten gehoert; `assertTargetBelongsToTenant` bleibt als zweite Verteidigungslinie bestehen. **WINDOWS #19:** `TenderRssFeedSource` bekommt VIER nach Befehl getrennte Regeln — Lesen schliesst plattformweite Zeilen ein, Einfuegen/Aendern/Loeschen verlangen weiter einen Mandanten (eine einzige lockere Regel haette jedem Mandanten erlaubt, gemeinsame Quellen zu aendern und zu loeschen, weil `USING` auch UPDATE und DELETE regelt). **Halbe Praemisse von #19 widerlegt:** bei `SearchProvider` gibt es gar keinen Codeweg, der eine mandantenlose Zeile erzeugt — Schreibweg verlangt den Mandanten, Vorgaben sind Konstanten (05-02); als widerlegte Annahme geschlossen, nicht als geloestes Problem, strenge Regel bleibt. **DREI Pruefungen schrieben die Loecher als erwartetes Verhalten fest** (meine eigene Suche fand nur zwei, der Planer die dritte) — alle drei UMGEDREHT statt geloescht, mit Verweis auf den urspruenglichen Befund: der ausfuehrbare Beleg, dass das Loch existierte, bleibt mit umgekehrtem Vorzeichen erhalten. **Die Reparatur erzeugte an einer Stelle selbst den Fehler, gegen den sie antritt:** `listForUser` haette nach der Regelaenderung die plattformweiten, aber nicht die persoenlichen Quellen geliefert — aus einer leeren Liste, die schreit, waere eine kurze geworden, die luegt; deshalb mitgebunden. **Messfalle abgefangen:** `extractPolicySql()` las nur die alten Migrationsverzeichnisse und haette nach der neuen Migration still die ABGELOESTE Regel weitergemessen. **Werkzeugfalle abgefangen:** der uebliche Aufrufweg haette beim Einspielen eine neue Prisma-Hauptversion nachgeladen; stattdessen die im Projekt festgelegte Fassung benutzt. Neuer offener Ledger-Eintrag #24: plattformweite Zeilen lassen sich unter der Anwendungsrolle weder anlegen noch entfernen — in alter wie neuer Regel. **Verifiziert 11/11 mit vier ZERSTOERENDEN Gegenproben** (jede Regel und die neue Bindung einzeln zurueckgedreht, jedes Mal schlug genau die zustaendige Pruefung fehl, danach byte-identisch wiederhergestellt). 839/839 Tests, Typpruefung sauber, 74/74 Live-Pruefungen; Regeltexte vom Orchestrator in der LAUFENDEN Datenbank gegengelesen | 2026-09-10 | f4f3115,6b23735,03fb3bf | [260910-jab-mandantentrennung-die-drei-zu-kurz-greif](./quick/260910-jab-mandantentrennung-die-drei-zu-kurz-greif/) | | 260910-jab | **Die drei zu kurz greifenden Datenbankregeln geschlossen** — auf ausdrueckliche Anweisung des Users VORGEZOGEN, entgegen der geplanten Reihenfolge (urspruenglich nach Etappe 2, damit jeder Bereich gegen einen stabilen Regelstand misst; der User entschied anders, weil offene Loecher vergessen werden). Erster Durchlauf dieser Serie, der die DATENBANK aendert statt nur Anwendungscode — neue Migration `20260910120000_rls_widen_membership_grant_and_platform_read`. **T-JTS-02:** `GroupMembership` prueft jetzt BEIDE Seiten (Gruppe UND Benutzer gehoeren zum Mandanten) statt nur die Gruppenseite. **T-JTS-03:** `ModuleGrant` prueft zusaetzlich, dass die referenzierte Gruppe bzw. der referenzierte Benutzer zum selben Mandanten gehoert; `assertTargetBelongsToTenant` bleibt als zweite Verteidigungslinie bestehen. **WINDOWS #19:** `TenderRssFeedSource` bekommt VIER nach Befehl getrennte Regeln — Lesen schliesst plattformweite Zeilen ein, Einfuegen/Aendern/Loeschen verlangen weiter einen Mandanten (eine einzige lockere Regel haette jedem Mandanten erlaubt, gemeinsame Quellen zu aendern und zu loeschen, weil `USING` auch UPDATE und DELETE regelt). **Halbe Praemisse von #19 widerlegt:** bei `SearchProvider` gibt es gar keinen Codeweg, der eine mandantenlose Zeile erzeugt — Schreibweg verlangt den Mandanten, Vorgaben sind Konstanten (05-02); als widerlegte Annahme geschlossen, nicht als geloestes Problem, strenge Regel bleibt. **DREI Pruefungen schrieben die Loecher als erwartetes Verhalten fest** (meine eigene Suche fand nur zwei, der Planer die dritte) — alle drei UMGEDREHT statt geloescht, mit Verweis auf den urspruenglichen Befund: der ausfuehrbare Beleg, dass das Loch existierte, bleibt mit umgekehrtem Vorzeichen erhalten. **Die Reparatur erzeugte an einer Stelle selbst den Fehler, gegen den sie antritt:** `listForUser` haette nach der Regelaenderung die plattformweiten, aber nicht die persoenlichen Quellen geliefert — aus einer leeren Liste, die schreit, waere eine kurze geworden, die luegt; deshalb mitgebunden. **Messfalle abgefangen:** `extractPolicySql()` las nur die alten Migrationsverzeichnisse und haette nach der neuen Migration still die ABGELOESTE Regel weitergemessen. **Werkzeugfalle abgefangen:** der uebliche Aufrufweg haette beim Einspielen eine neue Prisma-Hauptversion nachgeladen; stattdessen die im Projekt festgelegte Fassung benutzt. Neuer offener Ledger-Eintrag #24: plattformweite Zeilen lassen sich unter der Anwendungsrolle weder anlegen noch entfernen — in alter wie neuer Regel. **Verifiziert 11/11 mit vier ZERSTOERENDEN Gegenproben** (jede Regel und die neue Bindung einzeln zurueckgedreht, jedes Mal schlug genau die zustaendige Pruefung fehl, danach byte-identisch wiederhergestellt). 839/839 Tests, Typpruefung sauber, 74/74 Live-Pruefungen; Regeltexte vom Orchestrator in der LAUFENDEN Datenbank gegengelesen | 2026-09-10 | f4f3115,6b23735,03fb3bf | [260910-jab-mandantentrennung-die-drei-zu-kurz-greif](./quick/260910-jab-mandantentrennung-die-drei-zu-kurz-greif/) |
| 260910-krx | Mandantentrennung Etappe 2, Bereich dashboard — 12 von 13 Zugriffen gebunden, der Modulkatalog bleibt bewusst ungebunden (Messung und Bedingung getrennt: heute ohne Zeilenschutz, daher wirkungslos, katastrophal erst wenn Etappe 3 eine Regel setzt). **Erster Bereich, in dem die Besitzpruefungen von Anfang an richtig waren:** dieselbe Bauform, die in ldap und dkv je eine Luecke riss (nachschlagen, dann loeschen), vergleicht hier dazwischen gegen die angemeldete Person — nichts zu reparieren, nur zu bestaetigen und durch Tests festzunageln. **Beweisvernichtungs-Schleife belegt, nicht vermutet (WINDOWS #25, offen):** nach dem Scharfschalten liefert `getLayout` bei unsichtbarer Zeile die Vorgabe, die Oberflaeche uebernimmt sie ohne Fehlerzustand, und das Verlassen des Bearbeitungsmodus schreibt AUTOMATISCH zurueck — der Nutzer ueberschreibt seine urspruengliche Anordnung selbst, ohne es zu merken; dazu haeufen sich Widget-Dubletten, weil es keine Eindeutigkeit ueber (userId, widgetType) gibt. Gehoert in die Etappe-4-Vorabpruefung, nicht in diesen Umbau. Suchleiste: der Rueckfallzweig feuert nie leer, weil drei Vorgaben immer vorangestellt sind — die eigenen Suchmaschinen verschwinden schlicht. `DashboardLayout.userId` ist plattformweit eindeutig ohne Mandantenanteil (Familie WINDOWS #22). **Der Verifizierer fand eine Luecke der bekannten Art:** die Behauptung, ein gebundener Konfliktschreibvorgang werfe `PrismaClientUnknownRequestError` (nicht den P2002-Fall von tenders/user), stuetzte sich auf eine NICHT committete Ad-hoc-Messung — Pruefung 5 mass nur Roh-SQL, kein Test uebte den catch-Zweig. Nachgereicht (6e71206): Messung ueber den GENERIERTEN Client (Konstruktorname geprueft), dabei die Wegwerf-Tabelle korrigiert, der Roh-SQL nie aufgefallen war (createdAt/updatedAt fehlten, der echte Client scheiterte sofort mit P2022); zwei Tests fuer den catch-Zweig, durch Rueckbau falsifiziert. Klassifikation: fremde Datei `groups.service.ts` mit ungenauem Kopfkommentar bewusst NICHT angefasst, Ungenauigkeit in (w5) festgehalten. **Verifiziert 10/11, Luecke behoben** (860/860 Tests, Typpruefung sauber, 88/88 Live-Pruefungen; alle drei Falsifizierungsnachweise vom Pruefer eigenhaendig reproduziert) | 2026-09-11 | 6744918,e0ce594,67b5024,6e71206 | [260910-krx-mandantentrennung-etappe-2-bereich-dashb](./quick/260910-krx-mandantentrennung-etappe-2-bereich-dashb/) | | 260910-krx | Mandantentrennung Etappe 2, Bereich dashboard — 12 von 13 Zugriffen gebunden, der Modulkatalog bleibt bewusst ungebunden (Messung und Bedingung getrennt: heute ohne Zeilenschutz, daher wirkungslos, katastrophal erst wenn Etappe 3 eine Regel setzt). **Erster Bereich, in dem die Besitzpruefungen von Anfang an richtig waren:** dieselbe Bauform, die in ldap und dkv je eine Luecke riss (nachschlagen, dann loeschen), vergleicht hier dazwischen gegen die angemeldete Person — nichts zu reparieren, nur zu bestaetigen und durch Tests festzunageln. **Beweisvernichtungs-Schleife belegt, nicht vermutet (WINDOWS #25, offen):** nach dem Scharfschalten liefert `getLayout` bei unsichtbarer Zeile die Vorgabe, die Oberflaeche uebernimmt sie ohne Fehlerzustand, und das Verlassen des Bearbeitungsmodus schreibt AUTOMATISCH zurueck — der Nutzer ueberschreibt seine urspruengliche Anordnung selbst, ohne es zu merken; dazu haeufen sich Widget-Dubletten, weil es keine Eindeutigkeit ueber (userId, widgetType) gibt. Gehoert in die Etappe-4-Vorabpruefung, nicht in diesen Umbau. Suchleiste: der Rueckfallzweig feuert nie leer, weil drei Vorgaben immer vorangestellt sind — die eigenen Suchmaschinen verschwinden schlicht. `DashboardLayout.userId` ist plattformweit eindeutig ohne Mandantenanteil (Familie WINDOWS #22). **Der Verifizierer fand eine Luecke der bekannten Art:** die Behauptung, ein gebundener Konfliktschreibvorgang werfe `PrismaClientUnknownRequestError` (nicht den P2002-Fall von tenders/user), stuetzte sich auf eine NICHT committete Ad-hoc-Messung — Pruefung 5 mass nur Roh-SQL, kein Test uebte den catch-Zweig. Nachgereicht (6e71206): Messung ueber den GENERIERTEN Client (Konstruktorname geprueft), dabei die Wegwerf-Tabelle korrigiert, der Roh-SQL nie aufgefallen war (createdAt/updatedAt fehlten, der echte Client scheiterte sofort mit P2022); zwei Tests fuer den catch-Zweig, durch Rueckbau falsifiziert. Klassifikation: fremde Datei `groups.service.ts` mit ungenauem Kopfkommentar bewusst NICHT angefasst, Ungenauigkeit in (w5) festgehalten. **Verifiziert 10/11, Luecke behoben** (860/860 Tests, Typpruefung sauber, 88/88 Live-Pruefungen; alle drei Falsifizierungsnachweise vom Pruefer eigenhaendig reproduziert) | 2026-09-11 | 6744918,e0ce594,67b5024,6e71206 | [260910-krx-mandantentrennung-etappe-2-bereich-dashb](./quick/260910-krx-mandantentrennung-etappe-2-bereich-dashb/) |
| 260911-cwh | Mandantentrennung Etappe 2, Bereich calendar — alle 12 Zugriffe gebunden, ein Klient je Methode, sechs Methoden. Bereich hatte KEINE Testdatei (dkv-Form); `calendar.service.spec.ts` neu mit 23 Tests. **Gespeicherte Zugangsdaten zu fremden Kalender-Servern** — ein Fremdzugriff waere hier der Schluessel zu einem fremden Exchange/CalDAV. **Der dkv-Passwortverlust-Fall existiert hier NICHT**, in beiden Haelften belegt: der Dienst schreibt `encryptedPassword` nur bei `dto.password !== undefined`, und `calendar-source-form.tsx` laesst ein leeres Feld WEG statt einen leeren Text zu schicken; als drei Tests festgenagelt, weil ein nicht festgenagelter Freispruch still aufhoeren kann zu gelten. **Zwischenspeicher-Schluessel `userId:from:to` ohne Mandantenanteil ist sicher:** `User.id` ist `@default(uuid())`, Kette Schema -> `auth.service.ts sub: user.id` -> `JwtStrategy.validate` -> `extractContext` Glied fuer Glied belegt; die Etappe-3-Entscheidung (Anmeldenamen pro Mandant) beruehrt `username`/`email`, nicht `id`. **Eigener Gefahrenfall — halb gebundene Aggregationsschleife:** `fetchAndCacheEvents` liest Quellen und schreibt den Synchronstatus auf Erfolgs- UND Fehlerpfad zurueck, innerhalb von `Promise.allSettled`; gebundene Lesung mit ungebundenem Rueckschreiben haette Ereignisse still fallen lassen — beide Rueckschreibungen gebunden und als Tests festgenagelt, eines davon vom Pruefer eigenhaendig zurueckgebaut (genau 1 von 23 rot, exakte Meldung). **Frontend macht aus lauten Fehlern stille:** `calendar-widget.tsx` und `calendar-settings-panel.tsx` fangen jeden Fehler in denselben leeren Zustand — ein 403 sieht aus wie ein leerer Kalender; NICHT angefasst, als WINDOWS #26 offen festgehalten. Besitzpruefungen in allen drei Pfaden echt (403, nicht 404). Keine Eindeutigkeitskette, daher keine Konfliktuebersetzung, die nichts uebersetzt. **Lehre aus dashboard angewandt:** 4 der 13 neuen Pruefungen laufen ueber den GENERIERTEN Client, mit Laufzeitvergleich der Wegwerf-Tabelle gegen `schema.prisma` (17 = 17 Spalten). **Verifiziert 12/12** (883/883 Tests, 57 Dateien, Typpruefung sauber, 101/101 Live-Pruefungen; Klassenverteilung 31/17/13/2 = 63 und Ledger-Zaehler vom Pruefer nachgerechnet). Ein Selbstwiderspruch in der Zusammenfassung ('keine Abweichung' vs. 'kein TDD-Zyklus') berichtigt | 2026-09-11 | bf5fc4d,77cb124,e0e163e | [260911-cwh-mandantentrennung-etappe-2-bereich-calen](./quick/260911-cwh-mandantentrennung-etappe-2-bereich-calen/) | | 260911-cwh | Mandantentrennung Etappe 2, Bereich calendar — alle 12 Zugriffe gebunden, ein Klient je Methode, sechs Methoden. Bereich hatte KEINE Testdatei (dkv-Form); `calendar.service.spec.ts` neu mit 23 Tests. **Gespeicherte Zugangsdaten zu fremden Kalender-Servern** — ein Fremdzugriff waere hier der Schluessel zu einem fremden Exchange/CalDAV. **Der dkv-Passwortverlust-Fall existiert hier NICHT**, in beiden Haelften belegt: der Dienst schreibt `encryptedPassword` nur bei `dto.password !== undefined`, und `calendar-source-form.tsx` laesst ein leeres Feld WEG statt einen leeren Text zu schicken; als drei Tests festgenagelt, weil ein nicht festgenagelter Freispruch still aufhoeren kann zu gelten. **Zwischenspeicher-Schluessel `userId:from:to` ohne Mandantenanteil ist sicher:** `User.id` ist `@default(uuid())`, Kette Schema -> `auth.service.ts sub: user.id` -> `JwtStrategy.validate` -> `extractContext` Glied fuer Glied belegt; die Etappe-3-Entscheidung (Anmeldenamen pro Mandant) beruehrt `username`/`email`, nicht `id`. **Eigener Gefahrenfall — halb gebundene Aggregationsschleife:** `fetchAndCacheEvents` liest Quellen und schreibt den Synchronstatus auf Erfolgs- UND Fehlerpfad zurueck, innerhalb von `Promise.allSettled`; gebundene Lesung mit ungebundenem Rueckschreiben haette Ereignisse still fallen lassen — beide Rueckschreibungen gebunden und als Tests festgenagelt, eines davon vom Pruefer eigenhaendig zurueckgebaut (genau 1 von 23 rot, exakte Meldung). **Frontend macht aus lauten Fehlern stille:** `calendar-widget.tsx` und `calendar-settings-panel.tsx` fangen jeden Fehler in denselben leeren Zustand — ein 403 sieht aus wie ein leerer Kalender; NICHT angefasst, als WINDOWS #26 offen festgehalten. Besitzpruefungen in allen drei Pfaden echt (403, nicht 404). Keine Eindeutigkeitskette, daher keine Konfliktuebersetzung, die nichts uebersetzt. **Lehre aus dashboard angewandt:** 4 der 13 neuen Pruefungen laufen ueber den GENERIERTEN Client, mit Laufzeitvergleich der Wegwerf-Tabelle gegen `schema.prisma` (17 = 17 Spalten). **Verifiziert 12/12** (883/883 Tests, 57 Dateien, Typpruefung sauber, 101/101 Live-Pruefungen; Klassenverteilung 31/17/13/2 = 63 und Ledger-Zaehler vom Pruefer nachgerechnet). Ein Selbstwiderspruch in der Zusammenfassung ('keine Abweichung' vs. 'kein TDD-Zyklus') berichtigt | 2026-09-11 | bf5fc4d,77cb124,e0e163e | [260911-cwh-mandantentrennung-etappe-2-bereich-calen](./quick/260911-cwh-mandantentrennung-etappe-2-bereich-calen/) |
| 260911-e2s | Mandantentrennung Etappe 2, Bereich tenant — von anderer Art: alle 8 Zugriffe gehen auf die Mandantentabelle SELBST, die per Definition keinen Mandanten hat. **'Nichts zu binden' war trotzdem falsch, und der Grund ist der wichtigste Fund seit dem kaputten Helfer in Etappe 1:** drei der acht Zugriffe (`findAll`, `findOne`, `remove` im Controller) zaehlen ueber `include: { _count: { select: { users } } }` in die GESCHUETZTE Tabelle `User` hinein — Prisma 6.19 rendert das als `LEFT JOIN (SELECT tenantId, COUNT(*) FROM User ...)`, das unter DEREN Regel laeuft. Nach dem Scharfschalten haette die Mandantenliste des Plattform-Admins fuer jeden Mandanten 0 Benutzer gezeigt, und der Loeschriegel T-02-09 waere vakuum geworden (der Fremdschluessel faengt es noch, aber als 500 statt 400). Behoben per Fan-out je Mandant ueber gebundenen Client, Muster aus `UserService.findAllForPlatformAdmin`. **Die Bestandsaufnahme ist fuer Relationszugriffe strukturell blind** — sie sieht nur `this.prisma.<Modell>`, nicht was ein `include:` in eine zweite Tabelle hineinrechnet. Alle 19 `include:`-Stellen und alle `_count`-Stellen einzeln beurteilt, vom Orchestrator UND vom Verifizierer unabhaengig gegengeprueft (der Plan-Pruefer hatte diesen Punkt als 'plausibel' durchgewinkt statt ihn zu pruefen): nur diese drei waren gefaehrlich. Der MECHANISMUS bleibt offen und ist als WINDOWS #27 festgehalten — der Planer wollte keinen Eintrag, weil die Instanz behoben ist; Orchestrator und Verifizierer sahen das anders, weil eine Luecke im Messwerkzeug, die nachweislich einen echten Defekt verborgen hat, genau dafuer ins Ledger gehoert. **Die seit Etappe 1 offene Architekturfrage ist entschieden:** `req.tenantPrisma` wurde bei jeder Anfrage gebaut und NIRGENDS gelesen; neun Bereiche haben die Konvention auf dienst-internes `forTenant()` festgelegt. Middleware geloescht (sie war nirgends registriert — der Auftrag irrte bei `app.module.ts:57`, dort ist der Guard verdrahtet), Guard ohne Prisma-Abhaengigkeit, setzt nur noch `req.tenantId` (22 Leser in 9 Dateien) und den `x-tenant-id`-Wechsel fuer SUPER_ADMIN (4 Frontend-Stellen) — beides erstmals getestet; Guard und Middleware hatten NIE Tests, 'ihre Tests' in Etappe 1 war eine Annahme. Totes Kabel, das wie eine Sicherung aussieht, ist schlimmer als keins. Ausnahmeliste in `rls-access-inventory.spec.ts` geleert und mit Wachhund versehen. Drei Kommentare berichtigt, die `TenantMiddleware`/`req.tenantPrisma` als lebendig beschrieben. Executor fing einen still fehlgeschlagenen `git add` (2 von 7 Dateien) selbst an `git status` und lieferte nach. **Verifiziert 10/10 mit vier eigenhaendigen Falsifizierungen** (Header-Wechsel zweimal gebrochen, Fan-out gebrochen, Wachhund ausgeloest — je exakt die benannten Tests rot; 911/911 Tests, 59 Dateien, Typpruefung sauber, 110/110 Live-Pruefungen; 64 Paare und Klassenverteilung 32/17/13/2 nachgerechnet) | 2026-09-11 | 652e762,11f5731,17dca0d,c8de72e | [260911-e2s-mandantentrennung-etappe-2-bereich-tenan](./quick/260911-e2s-mandantentrennung-etappe-2-bereich-tenan/) |
| 260909-eor | Etappe 1 der Mandantentrennung: Anmeldeweg mandantenfaehig gemacht und alle Zugriffe klassifiziert. **Kernfund (#20):** `forTenant()` setzte den Mandantenkontext per set_config auf der Transaktionsverbindung, dispatchte die Abfrage aber ueber den aeusseren Client — empirisch reproduziert (set_config auf Backend-PID 254999, Abfrage auf 255000, Kontext dort NULL). Die Trennung hat damit nie funktioniert, auch nicht an den Stellen, die sie scheinbar nutzten; nach dem Scharfschalten haetten diese Abfragen NULL Zeilen geliefert, was der LDAP-Loeschzweig als 'Gruppe im Verzeichnis verschwunden' gedeutet und geloescht haette. Behoben und live nachgewiesen. Der Anmeldeweg bekam drei SECURITY-DEFINER-Funktionen als schmale Ausnahme (feste Spaltenliste, Gleichheitsbedingung, LIMIT 1) — eine Policy haette nicht gereicht, weil sie zwangslaeufig die ganze Tabelle freigibt. Browser-Gegenprobe lokal bestanden: Anmeldung laedt das Portal, falsches Kennwort verraet weiterhin nicht welches Feld, Kennwort-vergessen laeuft durch (der einzige Protokollfehler war ein lokal fehlender Mailserver, also NACH dem Datenbankzugriff). Klassifikation aller 227 Zugriffe in 59 Einheiten, maschinell gegen Abdriften abgesichert: 31 muessen mandantengebunden werden, 9 teilweise, 16 betreffen keine mandantengebundene Tabelle, 3 bleiben bewusst uebergreifend. 701 Tests gruen | 2026-09-09 | da0ac04 | [260909-eor-anmeldeweg-mandantenfaehig-machen-und-al](./quick/260909-eor-anmeldeweg-mandantenfaehig-machen-und-al/) | | 260909-eor | Etappe 1 der Mandantentrennung: Anmeldeweg mandantenfaehig gemacht und alle Zugriffe klassifiziert. **Kernfund (#20):** `forTenant()` setzte den Mandantenkontext per set_config auf der Transaktionsverbindung, dispatchte die Abfrage aber ueber den aeusseren Client — empirisch reproduziert (set_config auf Backend-PID 254999, Abfrage auf 255000, Kontext dort NULL). Die Trennung hat damit nie funktioniert, auch nicht an den Stellen, die sie scheinbar nutzten; nach dem Scharfschalten haetten diese Abfragen NULL Zeilen geliefert, was der LDAP-Loeschzweig als 'Gruppe im Verzeichnis verschwunden' gedeutet und geloescht haette. Behoben und live nachgewiesen. Der Anmeldeweg bekam drei SECURITY-DEFINER-Funktionen als schmale Ausnahme (feste Spaltenliste, Gleichheitsbedingung, LIMIT 1) — eine Policy haette nicht gereicht, weil sie zwangslaeufig die ganze Tabelle freigibt. Browser-Gegenprobe lokal bestanden: Anmeldung laedt das Portal, falsches Kennwort verraet weiterhin nicht welches Feld, Kennwort-vergessen laeuft durch (der einzige Protokollfehler war ein lokal fehlender Mailserver, also NACH dem Datenbankzugriff). Klassifikation aller 227 Zugriffe in 59 Einheiten, maschinell gegen Abdriften abgesichert: 31 muessen mandantengebunden werden, 9 teilweise, 16 betreffen keine mandantengebundene Tabelle, 3 bleiben bewusst uebergreifend. 701 Tests gruen | 2026-09-09 | da0ac04 | [260909-eor-anmeldeweg-mandantenfaehig-machen-und-al](./quick/260909-eor-anmeldeweg-mandantenfaehig-machen-und-al/) |
| 260910-jab | Die drei zu kurz greifenden Datenbankregeln geschlossen — T-JTS-02, T-JTS-03, WINDOWS #19 (bewusste Reihenfolge-Abweichung, vorgezogen auf Nutzerwunsch, statt wie geplant nach Etappe 2). Neue, handgeschriebene, lokal angewandte Migration `20260910120000_rls_widen_membership_grant_and_platform_read`: `GroupMembership` prueft jetzt beide Seiten der Beziehung (Gruppe UND Benutzer), `ModuleGrant` prueft zusaetzlich beide moeglichen Ziele mit Leer-Zulassung (D-04), `TenderRssFeedSource` bekommt vier nach Befehl getrennte Regeln (Lesen schliesst plattformweite Zeilen ein, Schreiben verlangt weiterhin einen Mandanten — die Trennung ist noetig, weil ein einzelner USING-Ausdruck sonst auch UPDATE/DELETE mitregelt). `SearchProvider` bewusst NICHT angefasst: die WINDOWS-#19-Praemisse ist fuer dieses Modell widerlegt (kein Codeweg erzeugt eine mandantenlose Zeile). Drei loch-behauptende Pruefungen im Wegwerf-Werkzeug UMGEKEHRT statt geloescht (66→74 Pruefungen), mit Verweis auf die alten Pruefungsnamen und Befundkennungen im Meldetext. Genau EIN Anwendungspfad musste mitgebunden werden (`TenderRssFeedSourceService.listForUser`) — sonst haette die Reparatur ihn still von 'liefert nach dem Scharfschalten nichts' auf 'liefert nur die plattformweiten Zeilen, taeuscht Vollstaendigkeit vor' verschlechtert; Falsifizierungsnachweis gefuehrt (Bindung zurueckgenommen, genau ein Test rot, zurueckgesetzt). WINDOWS #19 geschlossen mit Beleg, WINDOWS #24 neu angelegt (Verwaltungsweg fuer plattformweite Zeilen unter der Anwendungsrolle fehlt weiterhin — verschwindet nicht mit #19). Aktenstand kohaerent: Klassifikation, Kritikschrift (neuer Abschnitt "Regelschluss T-JTS-02, T-JTS-03 und WINDOWS #19" mit Signaltabelle beider Fehlerrichtungen je Regel), Betriebsanleitung, WINDOWS.md — fuenf ueberholte Bestandsstellen mit Nachtraegen versehen, alte Messprotokolle bleiben woertlich stehen. Selbst gemessen statt uebernommen: Baseline 833/56 Tests, 66/66 Live-Pruefungen; Endstand 839/56, 74/74; keine zweite Sitzungsvariable fuer den Benutzer gefunden (nur `app.current_tenant`). Rule-1-Fix: implizites `any` in `tenders.controller.ts` nach der Bindung behoben. `npx prisma` versuchte ungefragt Prisma 8 herunterzuladen — abgebrochen, lokale gepinnte 6.19.3 verwendet | 2026-09-10 | f4f3115,6b23735,03fb3bf | [260910-jab-mandantentrennung-die-drei-zu-kurz-greif](./quick/260910-jab-mandantentrennung-die-drei-zu-kurz-greif/) | | 260910-jab | Die drei zu kurz greifenden Datenbankregeln geschlossen — T-JTS-02, T-JTS-03, WINDOWS #19 (bewusste Reihenfolge-Abweichung, vorgezogen auf Nutzerwunsch, statt wie geplant nach Etappe 2). Neue, handgeschriebene, lokal angewandte Migration `20260910120000_rls_widen_membership_grant_and_platform_read`: `GroupMembership` prueft jetzt beide Seiten der Beziehung (Gruppe UND Benutzer), `ModuleGrant` prueft zusaetzlich beide moeglichen Ziele mit Leer-Zulassung (D-04), `TenderRssFeedSource` bekommt vier nach Befehl getrennte Regeln (Lesen schliesst plattformweite Zeilen ein, Schreiben verlangt weiterhin einen Mandanten — die Trennung ist noetig, weil ein einzelner USING-Ausdruck sonst auch UPDATE/DELETE mitregelt). `SearchProvider` bewusst NICHT angefasst: die WINDOWS-#19-Praemisse ist fuer dieses Modell widerlegt (kein Codeweg erzeugt eine mandantenlose Zeile). Drei loch-behauptende Pruefungen im Wegwerf-Werkzeug UMGEKEHRT statt geloescht (66→74 Pruefungen), mit Verweis auf die alten Pruefungsnamen und Befundkennungen im Meldetext. Genau EIN Anwendungspfad musste mitgebunden werden (`TenderRssFeedSourceService.listForUser`) — sonst haette die Reparatur ihn still von 'liefert nach dem Scharfschalten nichts' auf 'liefert nur die plattformweiten Zeilen, taeuscht Vollstaendigkeit vor' verschlechtert; Falsifizierungsnachweis gefuehrt (Bindung zurueckgenommen, genau ein Test rot, zurueckgesetzt). WINDOWS #19 geschlossen mit Beleg, WINDOWS #24 neu angelegt (Verwaltungsweg fuer plattformweite Zeilen unter der Anwendungsrolle fehlt weiterhin — verschwindet nicht mit #19). Aktenstand kohaerent: Klassifikation, Kritikschrift (neuer Abschnitt "Regelschluss T-JTS-02, T-JTS-03 und WINDOWS #19" mit Signaltabelle beider Fehlerrichtungen je Regel), Betriebsanleitung, WINDOWS.md — fuenf ueberholte Bestandsstellen mit Nachtraegen versehen, alte Messprotokolle bleiben woertlich stehen. Selbst gemessen statt uebernommen: Baseline 833/56 Tests, 66/66 Live-Pruefungen; Endstand 839/56, 74/74; keine zweite Sitzungsvariable fuer den Benutzer gefunden (nur `app.current_tenant`). Rule-1-Fix: implizites `any` in `tenders.controller.ts` nach der Bindung behoben. `npx prisma` versuchte ungefragt Prisma 8 herunterzuladen — abgebrochen, lokale gepinnte 6.19.3 verwendet | 2026-09-10 | f4f3115,6b23735,03fb3bf | [260910-jab-mandantentrennung-die-drei-zu-kurz-greif](./quick/260910-jab-mandantentrennung-die-drei-zu-kurz-greif/) |
+16 -3
View File
@@ -1,10 +1,10 @@
--- ---
schema_version: 1 schema_version: 1
open_count: 8 open_count: 9
waived_count: 1 waived_count: 1
fixed_count: 17 fixed_count: 17
total_count: 26 total_count: 27
last_updated: 2026-09-11T07:57:36.769Z last_updated: 2026-09-11T09:08:00.435Z
--- ---
# Broken Windows Ledger # Broken Windows Ledger
@@ -41,6 +41,7 @@ last_updated: 2026-09-11T07:57:36.769Z
| 24 | quick-260910-jab | deviation | apps/api/src/tenders/tender-rss-feed.service.ts | | Was das Schliessen von WINDOWS #19 NICHT loest: unter der Anwendungsrolle laesst sich eine plattformweite RSS-Quelle (TenderRssFeedSource, tenantId NULL) weder anlegen noch entfernen — in der alten wie in der neuen Regel, weil jede Schreibregel (Einfuegen/Aendern/Entfernen) ausdruecklich einen Mandanten verlangt (tenant_insert_policy/tenant_update_policy/tenant_delete_policy, 20260910120000_rls_widen_membership_grant_and_platform_read). Betroffen sind zwei Pfade in TenderRssFeedSourceService: createPlatform() (setzt tenantId=NULL, ein gebundenes INSERT liefe in die WITH-CHECK-Klausel und wuerde abgewiesen) und remove() (deckt fuer Administratoren auch das Entfernen einer plattformweiten Zeile ab; ein gebundenes DELETE traefe sie nie). Beide bleiben deshalb bewusst ungebunden — das ist KEINE Folge dieser Reparatur, sondern bestand bereits vor 260910-jab identisch, weil die vom Ledger vorgegebene #19-Semantik Schreibzugriffe ausdruecklich an einen Mandanten bindet. Vorabpruefung fuer Etappe 4 (rls-preflight.mjs): ein Verwaltungsweg fuer plattformweite Zeilen (Anlegen/Entfernen unter der Anwendungsrolle) muss gebaut werden, BEVOR die Rolle umgeschaltet wird — sonst kann kein Administrator nach dem Scharfschalten mehr eine plattformweite Quelle pflegen. Eigener Eintrag, damit dieser Rest nicht mit #19 verschwindet. | open | | 2026-09-10T12:35:40.000Z | | | 24 | quick-260910-jab | deviation | apps/api/src/tenders/tender-rss-feed.service.ts | | Was das Schliessen von WINDOWS #19 NICHT loest: unter der Anwendungsrolle laesst sich eine plattformweite RSS-Quelle (TenderRssFeedSource, tenantId NULL) weder anlegen noch entfernen — in der alten wie in der neuen Regel, weil jede Schreibregel (Einfuegen/Aendern/Entfernen) ausdruecklich einen Mandanten verlangt (tenant_insert_policy/tenant_update_policy/tenant_delete_policy, 20260910120000_rls_widen_membership_grant_and_platform_read). Betroffen sind zwei Pfade in TenderRssFeedSourceService: createPlatform() (setzt tenantId=NULL, ein gebundenes INSERT liefe in die WITH-CHECK-Klausel und wuerde abgewiesen) und remove() (deckt fuer Administratoren auch das Entfernen einer plattformweiten Zeile ab; ein gebundenes DELETE traefe sie nie). Beide bleiben deshalb bewusst ungebunden — das ist KEINE Folge dieser Reparatur, sondern bestand bereits vor 260910-jab identisch, weil die vom Ledger vorgegebene #19-Semantik Schreibzugriffe ausdruecklich an einen Mandanten bindet. Vorabpruefung fuer Etappe 4 (rls-preflight.mjs): ein Verwaltungsweg fuer plattformweite Zeilen (Anlegen/Entfernen unter der Anwendungsrolle) muss gebaut werden, BEVOR die Rolle umgeschaltet wird — sonst kann kein Administrator nach dem Scharfschalten mehr eine plattformweite Quelle pflegen. Eigener Eintrag, damit dieser Rest nicht mit #19 verschwindet. | open | | 2026-09-10T12:35:40.000Z | |
| 25 | quick-260910-krx | deviation | apps/web/src/lib/stores/dashboard-store.ts | | Die beweisvernichtende Auspraegung der umgekehrten Fehlerrichtung im Bereich dashboard: ein nach dem Scharfschalten (WINDOWS #18) zu klein gebliebenes Leseergebnis auf getLayout sieht nicht wie ein Fehler aus, sondern wie eine leere Vorgabeanordnung. Drei Stellen greifen ineinander (docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich dashboard', (w3)): (1) DashboardService.getLayout liefert bei fehlendem Datensatz {lg:[],md:[],sm:[],xs:[],xxs:[]} statt eines Fehlers; (2) apps/web/src/lib/stores/dashboard-store.ts, loadDashboard setzt layouts/widgets ungeprueft auf das Ergebnis, der catch-Zweig feuert nur bei Netzwerk-/Statusfehlern, nicht bei einer erfolgreichen leeren Antwort; (3) dieselbe Datei, setEditMode(false) schreibt bei isDirty automatisch zurueck, sobald der Bearbeitungsmodus verlassen wird — ohne dass der Nutzer auf Speichern klickt. Die Folge: der Nutzer haelt ein leeres Dashboard fuer einen Fehler des Widget-Systems oder fuer verlorene Einstellungen ('das Widget-System spinnt', 'meine Einstellungen sind weg'), baut seine Anordnung neu auf (addWidget legt echte neue WidgetInstance-Zeilen an, keine Eindeutigkeitsbedingung ueber (userId, widgetType), Dubletten haeufen sich bei wiederholtem Neuaufbau an), und das automatische Zurueckschreiben ueberschreibt die layouts-Spalte der urspruenglichen Zeile — die einzige Aufzeichnung der urspruenglichen Anordnung ist verloren, bevor irgendjemand die Ursache untersuchen konnte. Zusaetzlich, kleiner: apps/web/src/components/dashboard/widgets/search-widget.tsx laesst bei einem zu kleinen custom-Ergebnis die eigenen Suchmaschinen des Nutzers aus der Auswahlliste verschwinden (der Rueckfallzweig auf DEFAULT_PROVIDERS feuert nie, weil getSearchProviders die drei Vorgaben immer voranstellt), und handleSearch faellt bei unbekannter Auswahl auf providers[0] (Google) zurueck — eine fuer ein internes Werkzeug gedachte Suchanfrage ginge dann an eine externe Suchmaschine. Konkrete Vorabpruefung fuer Etappe 4 (rls-preflight.mjs): eine physisch vorhandene DashboardLayout-Zeile fuer einen bekannten Benutzer, aber der gebundene Lesezugriff fuer dessen Mandanten liefert null — das unterscheidet den echten Erstbenutzer-Fall vom Trennungsfehler. An dieselbe Bedingung gebunden wie #18 — beobachtbar erst nach dem Scharfschalten. Das Frontend wird von 260910-krx NICHT geaendert, dieser Eintrag beschreibt es nur. Die verwandte, strukturelle Eindeutigkeitsfrage von DashboardLayout.userId (plattformweit @unique ohne Mandantenanteil) ist derselbe Fall wie WINDOWS #22 im Bereich user — dort mitgefuehrt, kein zweiter Eintrag hier. | open | | 2026-09-11T09:01:00.000Z | | | 25 | quick-260910-krx | deviation | apps/web/src/lib/stores/dashboard-store.ts | | Die beweisvernichtende Auspraegung der umgekehrten Fehlerrichtung im Bereich dashboard: ein nach dem Scharfschalten (WINDOWS #18) zu klein gebliebenes Leseergebnis auf getLayout sieht nicht wie ein Fehler aus, sondern wie eine leere Vorgabeanordnung. Drei Stellen greifen ineinander (docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich dashboard', (w3)): (1) DashboardService.getLayout liefert bei fehlendem Datensatz {lg:[],md:[],sm:[],xs:[],xxs:[]} statt eines Fehlers; (2) apps/web/src/lib/stores/dashboard-store.ts, loadDashboard setzt layouts/widgets ungeprueft auf das Ergebnis, der catch-Zweig feuert nur bei Netzwerk-/Statusfehlern, nicht bei einer erfolgreichen leeren Antwort; (3) dieselbe Datei, setEditMode(false) schreibt bei isDirty automatisch zurueck, sobald der Bearbeitungsmodus verlassen wird — ohne dass der Nutzer auf Speichern klickt. Die Folge: der Nutzer haelt ein leeres Dashboard fuer einen Fehler des Widget-Systems oder fuer verlorene Einstellungen ('das Widget-System spinnt', 'meine Einstellungen sind weg'), baut seine Anordnung neu auf (addWidget legt echte neue WidgetInstance-Zeilen an, keine Eindeutigkeitsbedingung ueber (userId, widgetType), Dubletten haeufen sich bei wiederholtem Neuaufbau an), und das automatische Zurueckschreiben ueberschreibt die layouts-Spalte der urspruenglichen Zeile — die einzige Aufzeichnung der urspruenglichen Anordnung ist verloren, bevor irgendjemand die Ursache untersuchen konnte. Zusaetzlich, kleiner: apps/web/src/components/dashboard/widgets/search-widget.tsx laesst bei einem zu kleinen custom-Ergebnis die eigenen Suchmaschinen des Nutzers aus der Auswahlliste verschwinden (der Rueckfallzweig auf DEFAULT_PROVIDERS feuert nie, weil getSearchProviders die drei Vorgaben immer voranstellt), und handleSearch faellt bei unbekannter Auswahl auf providers[0] (Google) zurueck — eine fuer ein internes Werkzeug gedachte Suchanfrage ginge dann an eine externe Suchmaschine. Konkrete Vorabpruefung fuer Etappe 4 (rls-preflight.mjs): eine physisch vorhandene DashboardLayout-Zeile fuer einen bekannten Benutzer, aber der gebundene Lesezugriff fuer dessen Mandanten liefert null — das unterscheidet den echten Erstbenutzer-Fall vom Trennungsfehler. An dieselbe Bedingung gebunden wie #18 — beobachtbar erst nach dem Scharfschalten. Das Frontend wird von 260910-krx NICHT geaendert, dieser Eintrag beschreibt es nur. Die verwandte, strukturelle Eindeutigkeitsfrage von DashboardLayout.userId (plattformweit @unique ohne Mandantenanteil) ist derselbe Fall wie WINDOWS #22 im Bereich user — dort mitgefuehrt, kein zweiter Eintrag hier. | open | | 2026-09-11T09:01:00.000Z | |
| 26 | quick-260911-cwh | deviation | apps/web/src/components/dashboard/widgets/calendar-widget.tsx | | Bereich calendar: zu kleines Leseergebnis auf getSources/fetchAndCacheEvents sieht aus wie 'keine Quelle eingerichtet' bzw. 'keine Termine' (calendar-widget.tsx, calendar-settings-panel.tsx); das Frontend verschluckt zusaetzlich LAUTE Fehler derselben Pfade in denselben leeren Zustand (calendar-widget.tsx catch->setEvents([]), calendar-settings-panel.tsx .catch(()=>{}) auf fetchSources); der Nutzer legt seine Quelle neu an und tippt Exchange-/CalDAV-Zugangsdaten ein zweites Mal in ein scheinbar defektes System ein, die urspruengliche Zeile bleibt unsichtbar liegen und wird nach Behebung zur Dublette; Vorabpruefung fuer Etappe 4: physisch vorhandene CalendarSource-Zeilen je Mandant ueber die Wartungsrolle zaehlen und mit der gebundenen Zaehlung vergleichen (docs/mandantentrennung-etappe2-fehlerrichtung.md (k4)(e)); an dieselbe Bedingung gebunden wie WINDOWS #18; Familie mit #23 (module-registry) und #25 (dashboard); das Frontend wird von 260911-cwh NICHT geaendert. | open | | 2026-09-11T07:57:36.769Z | | | 26 | quick-260911-cwh | deviation | apps/web/src/components/dashboard/widgets/calendar-widget.tsx | | Bereich calendar: zu kleines Leseergebnis auf getSources/fetchAndCacheEvents sieht aus wie 'keine Quelle eingerichtet' bzw. 'keine Termine' (calendar-widget.tsx, calendar-settings-panel.tsx); das Frontend verschluckt zusaetzlich LAUTE Fehler derselben Pfade in denselben leeren Zustand (calendar-widget.tsx catch->setEvents([]), calendar-settings-panel.tsx .catch(()=>{}) auf fetchSources); der Nutzer legt seine Quelle neu an und tippt Exchange-/CalDAV-Zugangsdaten ein zweites Mal in ein scheinbar defektes System ein, die urspruengliche Zeile bleibt unsichtbar liegen und wird nach Behebung zur Dublette; Vorabpruefung fuer Etappe 4: physisch vorhandene CalendarSource-Zeilen je Mandant ueber die Wartungsrolle zaehlen und mit der gebundenen Zaehlung vergleichen (docs/mandantentrennung-etappe2-fehlerrichtung.md (k4)(e)); an dieselbe Bedingung gebunden wie WINDOWS #18; Familie mit #23 (module-registry) und #25 (dashboard); das Frontend wird von 260911-cwh NICHT geaendert. | open | | 2026-09-11T07:57:36.769Z | |
| 27 | 2 | unmet-truth | apps/api/src/prisma/rls-access-inventory.spec.ts | | Die maschinelle Bestandsaufnahme (rls-access-inventory.spec.ts) ist fuer Relationszugriffe strukturell blind. Sie erkennt nur direkte Zugriffe der Form this.prisma.<Modell> bzw. <gebundener Client>.<Modell>. Ein Zugriff, der ueber include:/_count:/select: in eine ZWEITE Tabelle hineinreicht, ist fuer sie unsichtbar — obwohl Prisma daraus eine Unterabfrage auf diese zweite Tabelle macht, die unter DEREN Regel laeuft. Nachgewiesen in 260911-e2s: drei Zugriffe in tenant.controller.ts zaehlten ueber include: { _count: { select: { users } } } in die geschuetzte Tabelle User hinein (Prisma 6.19 rendert das als LEFT JOIN (SELECT tenantId, COUNT(*) FROM User ...)); nach dem Scharfschalten haette die Mandantenliste des Plattform-Administrators fuer jeden Mandanten 0 Benutzer gezeigt und der Loeschriegel T-02-09 waere vakuum geworden. Diese drei Stellen sind behoben (Fan-out je Mandant ueber gebundenen Client). Zur Planungszeit wurden alle 19 include:-Stellen und alle _count-Stellen in apps/api/src einzeln beurteilt, vom Orchestrator und vom Verifizierer unabhaengig gegengeprueft: nur diese drei waren gefaehrlich (tenders zaehlt auf dem plattformglobalen Katalog ohne Zeilenschutz, groups zaehlt ueber einen bereits gebundenen Client in eine Tabelle desselben Mandanten). OFFEN bleibt der MECHANISMUS: jede kuenftige include:/_count:-Stelle in eine fremd geschuetzte Tabelle bleibt fuer die Pruefung unsichtbar. Zu schliessen, indem der Detektor include:/select:/_count:-Bloecke auf Modellnamen durchsucht und die Zieltabelle als eigene Fundstelle fuehrt — oder durch eine Pruefung, die jede include:-Stelle einer expliziten Freigabeliste unterwirft. Gehoert vor das Scharfschalten (Etappe 4), weil die Vorabpruefung sich sonst auf eine Bestandsaufnahme stuetzt, die diese Form nicht sieht. | open | | 2026-09-11T09:08:00.435Z | |
````json ````json
[ [
@@ -355,6 +356,18 @@ last_updated: 2026-09-11T07:57:36.769Z
"reason": "", "reason": "",
"recorded_at": "2026-09-11T07:57:36.769Z", "recorded_at": "2026-09-11T07:57:36.769Z",
"resolved_at": null "resolved_at": null
},
{
"id": 27,
"kind": "unmet-truth",
"phase": "2",
"file": "apps/api/src/prisma/rls-access-inventory.spec.ts",
"line": null,
"description": "Die maschinelle Bestandsaufnahme (rls-access-inventory.spec.ts) ist fuer Relationszugriffe strukturell blind. Sie erkennt nur direkte Zugriffe der Form this.prisma.<Modell> bzw. <gebundener Client>.<Modell>. Ein Zugriff, der ueber include:/_count:/select: in eine ZWEITE Tabelle hineinreicht, ist fuer sie unsichtbar — obwohl Prisma daraus eine Unterabfrage auf diese zweite Tabelle macht, die unter DEREN Regel laeuft. Nachgewiesen in 260911-e2s: drei Zugriffe in tenant.controller.ts zaehlten ueber include: { _count: { select: { users } } } in die geschuetzte Tabelle User hinein (Prisma 6.19 rendert das als LEFT JOIN (SELECT tenantId, COUNT(*) FROM User ...)); nach dem Scharfschalten haette die Mandantenliste des Plattform-Administrators fuer jeden Mandanten 0 Benutzer gezeigt und der Loeschriegel T-02-09 waere vakuum geworden. Diese drei Stellen sind behoben (Fan-out je Mandant ueber gebundenen Client). Zur Planungszeit wurden alle 19 include:-Stellen und alle _count-Stellen in apps/api/src einzeln beurteilt, vom Orchestrator und vom Verifizierer unabhaengig gegengeprueft: nur diese drei waren gefaehrlich (tenders zaehlt auf dem plattformglobalen Katalog ohne Zeilenschutz, groups zaehlt ueber einen bereits gebundenen Client in eine Tabelle desselben Mandanten). OFFEN bleibt der MECHANISMUS: jede kuenftige include:/_count:-Stelle in eine fremd geschuetzte Tabelle bleibt fuer die Pruefung unsichtbar. Zu schliessen, indem der Detektor include:/select:/_count:-Bloecke auf Modellnamen durchsucht und die Zieltabelle als eigene Fundstelle fuehrt — oder durch eine Pruefung, die jede include:-Stelle einer expliziten Freigabeliste unterwirft. Gehoert vor das Scharfschalten (Etappe 4), weil die Vorabpruefung sich sonst auf eine Bestandsaufnahme stuetzt, die diese Form nicht sieht.",
"status": "open",
"reason": "",
"recorded_at": "2026-09-11T09:08:00.435Z",
"resolved_at": null
} }
] ]
```` ````
@@ -0,0 +1,248 @@
---
phase: quick-260911-e2s
plan: 01
subsystem: database
tags: [prisma, postgres, row-level-security, nestjs, multi-tenancy]
requires:
- phase: quick-260911-cwh
provides: neunte umgestellte Bereich (calendar), die Etappe-2-Konvention der dienst-internen forTenant()-Bindung
provides:
- runTenantAreaChecks (9 neue Pruefungen im Wegwerf-Werkzeug, 6 davon ueber den generierten Client)
- TenantGuard ohne Prisma-Abhaengigkeit, setzt ausschliesslich req.tenantId
- tenant.middleware.ts geloescht (nie verdrahtet)
- TenantController: drei gebundene Benutzerzaehler (Fan-out je Mandant) statt Relationszaehler
- Testlage fuer Guard und Controller aus dem Nichts (27 neue Testfaelle)
- Architekturfrage req.tenantPrisma fuer ALLE Bereiche der Etappe 2 entschieden
affects: [tenant, user, groups, auth, module-registry]
actuals:
tokens: 22215
tasks: 3
commits: 4
plan_head_before: 6426b18630923a35bfee54c7b211a022adafd3c6
tech-stack:
added: []
patterns:
- "Fan-out je Mandant fuer Plattform-Administratorsichten: ungebundener Treiber (this.prisma.tenant.findMany) plus je Mandant EIN gebundener Zaehler/Lesezugriff (forTenant(this.prisma, tenant.id)), wortgleiche Form wie UserService.findAllForPlatformAdmin"
- "Guard setzt nur die Mandantenkennung (req.tenantId); die Bindung an einen Prisma-Client geschieht ausschliesslich dienst-intern je Methode — settled convention nach zehn Bereichen"
key-files:
created:
- apps/api/src/tenant/tenant.guard.spec.ts
- apps/api/src/tenant/tenant.controller.spec.ts
modified:
- apps/api/scripts/rls-scratch-check.mjs
- apps/api/src/tenant/tenant.guard.ts
- apps/api/src/tenant/tenant.controller.ts
- apps/api/src/prisma/rls-access-inventory.spec.ts
- apps/api/src/app.module.ts
- apps/api/src/module-registry/module.guard.ts
- apps/api/src/dkv/dkv.controller.ts
- docs/mandantentrennung-etappe2-fehlerrichtung.md
- docs/mandantentrennung-zugriffsklassifikation.md
- docs/anleitung-entwicklung.md
deleted:
- apps/api/src/tenant/tenant.middleware.ts
key-decisions:
- "req.tenantPrisma entfernt, fuer ALLE Bereiche der Etappe 2 entschieden: dienst-interne Bindung (ein forTenant()-Client je Methode) ist die Konvention, keine Anfrageobjekt-Eigenschaft. Grund: neunfache Praxis vor diesem Bereich; ein gebundener Klient ohne Leser war Kosten ohne Nutzen und sah wie ein Sicherheitsmechanismus aus, der nicht wirkte."
- "tenant.middleware.ts geloescht statt nur entschaerft — sie war nirgends verdrahtet (kein MiddlewareConsumer, kein configure() in ganz apps/api) und hatte identische Logik wie der Guard."
- "Drei Relationszaehler im TenantController (findAll/findOne/remove) durch gebundene Fan-out-Zaehler ersetzt, weil Prisma include:{_count} als EINE Anweisung mit LEFT JOIN in die geschuetzte Tabelle User laeuft — nach dem Scharfschalten waere das userCount=0 fuer jeden Mandanten gewesen."
requirements-completed: [WINDOWS-18, ETAPPE-2-TENANT]
coverage:
- id: D1
description: "runTenantAreaChecks misst neun Verhaltensweisen des Bereichs tenant gegen die echte Wegwerf-Datenbank: keine Regel in allen 34 Migrationen, gebunden=ungebunden (Roh-SQL und generierter Client), Relationszaehler liefert ungebunden 0/0/0, gebundener Fan-out liefert die richtigen Zahlen, Loeschriegel-Umgehung wird vom Fremdschluessel laut abgefangen"
requirement: WINDOWS-18
verification:
- kind: integration
ref: "apps/api/scripts/rls-scratch-check.mjs — 110/110 Pruefungen bestanden (101 bisherige + 9 neue)"
status: pass
human_judgment: false
- id: D2
description: "TenantGuard setzt ausschliesslich req.tenantId, keine Prisma-Abhaengigkeit mehr; alle fuenf Zweige inklusive x-tenant-id-Wechsel und Abwesenheit der alten Eigenschaft als Test festgenagelt"
requirement: ETAPPE-2-TENANT
verification:
- kind: unit
ref: "apps/api/src/tenant/tenant.guard.spec.ts — 7/7 Faelle"
status: pass
human_judgment: false
- id: D3
description: "TenantController: findAll/findOne/remove zaehlen Benutzer je Mandant ueber drei gebundene Aufrufstellen statt Relationszaehler; Antwortform und Verhalten unveraendert"
requirement: ETAPPE-2-TENANT
verification:
- kind: unit
ref: "apps/api/src/tenant/tenant.controller.spec.ts — 20/20 Faelle (Zwei-Klienten-Nachweis, Rollen-Metadaten, Wachhund)"
status: pass
human_judgment: false
- id: D4
description: "Alle fuenf handgepflegten Klassifikationsstellen plus die Kritikschrift sind nachgezogen und maschinell gegatet (64 Paare, Uebersichtszeile 8/3, Klassen-Verteilung)"
verification:
- kind: unit
ref: "apps/api/src/prisma/rls-access-inventory.spec.ts — 11/11 (inkl. neuer Wachhund gegen veraltete Ausnahmeeintraege)"
status: pass
human_judgment: false
duration: ~28min
completed: 2026-09-11
status: complete
---
# Quick 260911-e2s: Mandantentrennung Etappe 2, Bereich tenant Summary
**`Tenant` selbst braucht keine Bindung (gemessen ueber alle 34 Migrationen), aber drei Relationszaehler im `TenantController` liefen unbemerkt unter der Regel von `User` — jetzt durch einen gebundenen Fan-out ersetzt; die seit Etappe 1 offene Frage zum Anfrageobjekt-Klienten ist fuer alle Bereiche entschieden und der Guard hat keine Prisma-Abhaengigkeit mehr.**
## Performance
- **Duration:** ~28 min
- **Tasks:** 3/3
- **Files modified:** 13 (10 geaendert, 2 neu angelegt, 1 geloescht)
- **Commits:** 4 (3 fachliche Task-Commits + 1 Nachtrag fuer eine fehlerhafte `git add`-Staging)
## Accomplishments
- **Wegwerf-Werkzeug erweitert:** `runTenantAreaChecks` (12. Abschnitt in `rls-scratch-check.mjs`) misst neun benannte Verhaltensweisen, sechs davon ueber den generierten Prisma-Client (nicht nur Roh-SQL) — darunter die tragende Belegzeile, dass der Relationszaehler ungebunden fuer JEDEN Mandanten 0 liefert, und dass der Fremdschluessel `User_tenantId_fkey` (wortgleich aus der Migration geschnitten) ein durch den vakuumen Riegel durchgelassenes Loeschen laut abfaengt. 101 → 110 Pruefungen, alle gruen.
- **Kritikschrift erweitert:** `docs/mandantentrennung-etappe2-fehlerrichtung.md` bekommt den Abschnitt "## Bereich tenant" mit der tatsaechlich beobachteten Werkzeugausgabe, einer Signaltabelle je Pfad, den Frontend-Stellen, die die falsche Zahl unkommentiert durchlassen (`admin/tenants/page.tsx`, `TenantContextSelector.tsx`), und der vollstaendigen Entscheidung zur Anfrageobjekt-Eigenschaft.
- **Architekturfrage entschieden (fuer ALLE Bereiche der Etappe 2, nicht nur `tenant`):** `TenantGuard` setzt nur noch `req.tenantId`, hat keinen Konstruktor-Parameter mehr; `tenant.middleware.ts` (nie verdrahtet, identische Logik) ist geloescht. `FORTENANT_ASSIGNMENT_EXCEPTIONS` in `rls-access-inventory.spec.ts` ist leer und durch einen neuen Wachhund-Test gegen veraltete Eintraege abgesichert.
- **Fan-out im Controller:** `findAll`/`findOne`/`remove` zaehlen Benutzer je Mandant ueber drei gebundene `tenantPrisma.user.count`-Aufrufstellen (Muster `UserService.findAllForPlatformAdmin`); die vier `tenant`-Zugriffe selbst bleiben bewusst ungebunden (keine Regel, gemessen).
- **Testlage aus dem Nichts:** `tenant.guard.spec.ts` (7 Faelle) und `tenant.controller.spec.ts` (20 Faelle, davon 9 in `findAll`/`findOne`/`create`/`update`/`remove`, ein Rollen-Metadaten-Test, fuenf Handler-Metadaten-Tests, drei Wachhund-Tests) — zuvor gab es fuer diesen Bereich nur zwei Faelle in `tenant.service.spec.ts`.
- **Klassifikation nachgezogen:** 63 → 64 (Datei, Modell)-Paare (neu: `tenant.controller.ts`/`user`), Uebersichtszeile `tenant` 8/0 → 8/3, Klassen-Verteilung `muss-mandantengebunden` 31 → 32, "Zwei belegte Befunde" und "Was diese Etappe NICHT entscheidet" (erster Punkt) aufgeloest.
## Task Commits
1. **Aufgabe 1: Fehlerrichtung messen** — `652e762` (feat) — `runTenantAreaChecks` + Kritikschrift-Abschnitt
2. **Aufgabe 2: Guard-Umbau, Middleware geloescht** — `11f5731` (feat) — nur `tenant.middleware.ts` (Loeschung) und `tenant.guard.spec.ts` (neu) tatsaechlich erfasst
3. **Aufgabe 2 nachgetragen** — `17dca0d` (fix) — die restlichen fuenf Dateien des Guard-Umbaus (siehe Deviations unten)
4. **Aufgabe 3: Fan-out binden, Klassifikation nachziehen** — `c8de72e` (feat) — Controller, Controller-Spec, beide Dokumente
**Plan metadata:** wird vom Orchestrator committet (SUMMARY.md/STATE.md nicht Teil dieser Task-Commits)
## Files Created/Modified
- `apps/api/scripts/rls-scratch-check.mjs` — `runTenantAreaChecks`, neun Pruefungen, zwischen `runCalendarAreaChecks` und `runTransactionShapeMeasurement`
- `apps/api/src/tenant/tenant.guard.ts` — nur noch `req.tenantId`, keine Prisma-Abhaengigkeit
- `apps/api/src/tenant/tenant.guard.spec.ts` — NEU, 7 Faelle
- `apps/api/src/tenant/tenant.middleware.ts` — GELOESCHT
- `apps/api/src/tenant/tenant.controller.ts` — Fan-out-Zaehler statt Relationszaehler
- `apps/api/src/tenant/tenant.controller.spec.ts` — NEU, 20 Faelle
- `apps/api/src/prisma/rls-access-inventory.spec.ts` — leere `FORTENANT_ASSIGNMENT_EXCEPTIONS` + Wachhund-Test
- `apps/api/src/app.module.ts` — Kommentarzeile korrigiert (nennt nur noch `req.tenantId`)
- `apps/api/src/module-registry/module.guard.ts` — Kommentarzeile korrigiert (`TenantGuard` statt `TenantMiddleware`)
- `apps/api/src/dkv/dkv.controller.ts` — Kommentarzeile korrigiert (`TenantGuard` statt `TenantMiddleware`)
- `docs/mandantentrennung-etappe2-fehlerrichtung.md` — Abschnitt "## Bereich tenant" (n1)-(n5)
- `docs/mandantentrennung-zugriffsklassifikation.md` — 64 Paare, alle handgepflegten Stellen nachgezogen
- `docs/anleitung-entwicklung.md` — Guard-Beschreibung und drei weitere Stellen ohne `req.tenantPrisma`/`TenantMiddleware` (siehe Deviations)
## Decisions Made
- **req.tenantPrisma entfernt, fuer die gesamte Etappe 2 entschieden.** Gemessen: kein Leser ausserhalb von Guard/Middleware, Middleware nirgends verdrahtet. Entschieden: dienst-interne Bindung ist die Konvention (zehnter Bereich in Folge). Grund: Kosten ohne Nutzen, und tote Verdrahtung, die wie Schutz aussieht, ist schlimmer als keine.
- **tenant.middleware.ts geloescht statt nur entschaerft** — eine nie aufgerufene Kopie des Guards mit identischer Logik ist tote Verdrahtung in Reinform.
- **Fan-out statt Relationszaehler** — der Relationszaehler lief unter der Regel von `User`; der Fan-out (ungebundener Treiber, je Mandant EIN gebundener Zaehler) ist die bereits im Codebestand vorhandene Reparaturform (`UserService.findAllForPlatformAdmin`).
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Prozessfehler] `git add` mit mehreren Pfaden schlug fatal fehl und liess fünf Dateien unstaged**
- **Found during:** Aufgabe 2, beim Commit
- **Issue:** `git add <7 Pfade>` enthielt den bereits per `git rm` entfernten Pfad `tenant.middleware.ts` — Git quittierte das mit "Pfadspezifikation stimmt mit keinen Dateien überein" und staged dabei GAR KEINEN der sieben Pfade (nicht nur den fehlerhaften). Der darauffolgende Commit (`11f5731`) enthielt deshalb nur die zwei Dateien, die vorher schon separat gestaged waren (`tenant.middleware.ts` per `git rm`, `tenant.guard.spec.ts` per Einzel-`git add`) — der eigentliche Guard-Umbau (`tenant.guard.ts`, `rls-access-inventory.spec.ts`, `app.module.ts`, `module.guard.ts`, `dkv.controller.ts`) blieb im Arbeitsverzeichnis unstaged, unsichtbar in der `git commit`-Ausgabe ("2 files changed"), aber sichtbar in einem nachfolgenden `git status`.
- **Fix:** Fuenf fehlende Dateien einzeln mit `git add` gestaged und in einem separaten Commit (`17dca0d`) nachgetragen, mit expliziter Erklaerung der Ursache in der Commit-Botschaft. Inhaltlich identisch mit dem bereits verifizierten Stand (891 Tests gruen, Typpruefung sauber) — nur die Staging-Reihenfolge war fehlerhaft.
- **Files modified:** apps/api/src/tenant/tenant.guard.ts, apps/api/src/prisma/rls-access-inventory.spec.ts, apps/api/src/app.module.ts, apps/api/src/module-registry/module.guard.ts, apps/api/src/dkv/dkv.controller.ts
- **Verification:** `git diff --name-only f1017fa` listet nach dem Nachtrag exakt die 13 erwarteten Dateien; alle Task-2- und Task-3-Gates liefen danach erneut und bestanden.
- **Committed in:** `17dca0d`
**2. [Rule 1 - Bug] Guard-Kopfkommentar verletzte das eigene Gate (Punkt-Zugriff `.tenantPrisma`, Nennung von `TenantMiddleware`)**
- **Found during:** Aufgabe 2, unmittelbar nach dem ersten Entwurf des Kopfkommentars
- **Issue:** Der erste Entwurf des Kopfkommentars in `tenant.guard.ts` beschrieb die alte Anfrageobjekt-Eigenschaft mit `req.tenantPrisma = forTenant(...)` (Punkt-Zugriff) und nannte den Klassennamen `TenantMiddleware` woertlich — beides verletzt die eigenen Gates dieser Aufgabe (`grep -rn '\.tenantPrisma'`/`grep -rn 'TenantMiddleware'` ueber ganz `apps/api/src` muessen 0 liefern, auch in Kommentaren).
- **Fix:** Umformuliert ohne Punkt-Zugriff ("unter einer Eigenschaft namens `tenantPrisma`") und ohne den Klassennamen ("ein nie registrierter Express-Middleware-Klasse mit derselben Logik").
- **Files modified:** apps/api/src/tenant/tenant.guard.ts
- **Verification:** beide Gates liefern 0 im gesamten `apps/api/src`.
- **Committed in:** `17dca0d`
**3. [Rule 3 - Blocking] Zwei zusaetzliche `req.tenantPrisma`-Stellen in `docs/anleitung-entwicklung.md` ausserhalb des im Auftrag genannten ersten Absatzes**
- **Found during:** Aufgabe 3, TEIL 3
- **Issue:** Der Auftrag beschraenkte die Aenderung auf den ersten Absatz des Abschnitts "## Mandantentrennung" und den Hinweiskasten. Das Gate verlangt aber `test 0 -eq "$(grep -c 'req.tenantPrisma' docs/anleitung-entwicklung.md)"` fuer die GESAMTE Datei — und ein frueherer Abschnitt ("Weg einer Anfrage") nannte `req.tenantPrisma` an zwei weiteren Stellen (Schritt 2 und Schritt 4 der Anfrage-Reihenfolge).
- **Fix:** Beide Stellen ebenfalls korrigiert (Schritt 2: nur noch `req.tenantId`; Schritt 4: "dienst-intern per `forTenant()` gebundener Client" statt `req.tenantPrisma`) — inhaltlich dieselbe Berichtigung wie im Abschnitt "## Mandantentrennung" selbst, nur an zwei zusaetzlichen Stellen noetig, um das datei-weite Gate zu erfuellen.
- **Files modified:** docs/anleitung-entwicklung.md
- **Verification:** `grep -c 'req.tenantPrisma' docs/anleitung-entwicklung.md` liefert 0.
- **Committed in:** `c8de72e`
---
**Total deviations:** 3 auto-fixed (1 Prozessfehler beim Staging, 1 Bug im eigenen Kommentarentwurf, 1 datei-weites Gate erforderte zwei zusaetzliche Korrekturstellen)
**Impact on plan:** Keine inhaltliche Abweichung vom Plan — alle drei Punkte sind Korrekturen innerhalb der bereits verifizierten Aufgaben, kein Scope Creep. Die Endzahlen (13 geaenderte Dateien, 110/110 Werkzeugpruefungen, 911/59 Tests) stimmen mit dem an, was der Plan verlangt.
## Falsifizierungsnachweise (woertlich)
1. **Guard (Aufgabe 2):** probeweise `(req as any).tenantPrisma = 'probe';` nach der `req.tenantId`-Zuweisung im SUPER_ADMIN-Zweig eingefuegt. `tenant.guard.spec.ts` wurde rot: 4 von 7 Faellen fehlgeschlagen, u. a.
```
FAIL src/tenant/tenant.guard.spec.ts > TenantGuard.canActivate > SUPER_ADMIN mit tenantId, ohne Kopfzeile: req.tenantId === die eigene Kennung
AssertionError: expected true to be false
- Expected: false
+ Received: true
❯ expect('tenantPrisma' in req).toBe(false);
```
Zustand danach zurueckgestellt (`cp` aus Sicherung), `tenant.guard.spec.ts` wieder 7/7 gruen.
2. **Controller (Aufgabe 3):** in `findOne` den gebundenen Zaehler probeweise durch `(this.prisma as any).user.count(...)` (ungebundener Basisclient) ersetzt. `tenant.controller.spec.ts` wurde rot: 2 von 20 Faellen fehlgeschlagen, exakt in der erwarteten Form:
```
FAIL src/tenant/tenant.controller.spec.ts > TenantController.findOne > bekannte Kennung: userCount aus dem gebundenen Klienten UNTER DIESER Kennung
TypeError: Cannot read properties of undefined (reading 'count')
❯ TenantController.findOne src/tenant/tenant.controller.ts:102:55
```
(der ungebundene Nachbau hat kein `user`-Modell — die `dkv`-Form der Falsifizierung, nicht nur eine falsche Zahl). Zustand danach zurueckgestellt, 20/20 wieder gruen.
3. **Dokument-Gate (a), Bestandsaufnahme-Zeile (Aufgabe 3):** die neue Zeile `tenant.controller.ts`/`user` probeweise auf `ungebunden` gesetzt. `rls-access-inventory.spec.ts` wurde rot:
```
AssertionError: Fehlende Eintraege im Dokument:
apps/api/src/tenant/tenant.controller.ts::user — dokumentiert=ungebunden, gemessen=gebunden
```
Zurueckgestellt, 11/11 wieder gruen.
4. **Dokument-Gate (b), Uebersichtszeile (Aufgabe 3):** die Zeile `| tenant | 8 | 3 |` probeweise auf `| tenant | 8 | 99 |` gesetzt. Das herleitende Gate (`grep -qE "^\| tenant \| ${DU} \| ${DB} \| ..."`) schlug fehl (kein Treffer mehr). Zurueckgestellt.
## Issues Encountered
Keine ausser der oben dokumentierten Staging-Panne (Deviation 1) — beide Falsifizierungsnachweise und beide Dokument-Falsifizierungen liefen beim ersten Versuch wie erwartet rot.
## User Setup Required
None - keine externe Konfiguration noetig.
## Next Phase Readiness
- Zehn von zwoelf Bereichen der Etappe 2 sind umgestellt (`tenant` war der zehnte). Verbleibend laut Klassen-Verteilung: die uebrigen Bereiche mit `muss-mandantengebunden`/`beides`-Paaren, die noch nicht Stand `gebunden` tragen — die Klassifikationstabelle in `docs/mandantentrennung-zugriffsklassifikation.md` ist die autoritative Quelle fuer den verbleibenden Arbeitsvorrat.
- Die Architekturfrage zu `req.tenantPrisma` ist fuer ALLE verbleibenden Bereiche der Etappe 2 entschieden (dienst-intern, `forTenant()` je Methode) — kein zukuenftiger Plan muss diese Frage erneut stellen.
- Der Schalter (`DATABASE_URL` → Rolle `tessera`, `BYPASSRLS`) ist unveraendert AUS. Etappe 4 (Scharfschalten) bleibt ein separater, spaeterer Schritt.
---
*Phase: quick-260911-e2s*
*Completed: 2026-09-11*
## Self-Check: PASSED
- FOUND: apps/api/scripts/rls-scratch-check.mjs
- FOUND: apps/api/src/tenant/tenant.guard.ts
- FOUND: apps/api/src/tenant/tenant.guard.spec.ts
- FOUND: apps/api/src/tenant/tenant.controller.ts
- FOUND: apps/api/src/tenant/tenant.controller.spec.ts
- FOUND: apps/api/src/prisma/rls-access-inventory.spec.ts
- FOUND: docs/mandantentrennung-etappe2-fehlerrichtung.md
- FOUND: docs/mandantentrennung-zugriffsklassifikation.md
- FOUND: docs/anleitung-entwicklung.md
- CONFIRMED DELETED: apps/api/src/tenant/tenant.middleware.ts
- FOUND COMMIT: 652e762 (Aufgabe 1)
- FOUND COMMIT: 11f5731 (Aufgabe 2, teilweise)
- FOUND COMMIT: 17dca0d (Aufgabe 2, Nachtrag)
- FOUND COMMIT: c8de72e (Aufgabe 3)
- `git diff --name-only f1017fa` listet genau die 13 erwarteten Dateien, keine unerwarteten
- Endlauf `rls-scratch-check.mjs`: 110/110 bestanden, Rückgabewert 0
- Endlauf `npm --prefix apps/api run test`: 911/911 gruen in 59 Dateien
- Endlauf `npm --prefix apps/api run type-check`: sauber
- `git status --short`: sauber (working tree clean) vor SUMMARY-Erstellung
@@ -0,0 +1,176 @@
---
task: quick-260911-e2s
verified: 2026-09-11T09:06:02Z
status: passed
score: 10/10 must-have truths verified
commits_reviewed: [652e762, 11f5731, 17dca0d, c8de72e]
base: 6426b18
covered_files:
- apps/api/scripts/rls-scratch-check.mjs
- apps/api/src/app.module.ts
- apps/api/src/dkv/dkv.controller.ts
- apps/api/src/module-registry/module.guard.ts
- apps/api/src/prisma/rls-access-inventory.spec.ts
- apps/api/src/tenant/tenant.controller.spec.ts
- apps/api/src/tenant/tenant.controller.ts
- apps/api/src/tenant/tenant.guard.spec.ts
- apps/api/src/tenant/tenant.guard.ts
- apps/api/src/tenant/tenant.middleware.ts
- docs/anleitung-entwicklung.md
- docs/mandantentrennung-etappe2-fehlerrichtung.md
- docs/mandantentrennung-zugriffsklassifikation.md
- .planning/quick/260911-e2s-mandantentrennung-etappe-2-bereich-tenan/260911-e2s-PLAN.md
- .planning/quick/260911-e2s-mandantentrennung-etappe-2-bereich-tenan/260911-e2s-SUMMARY.md
advisory:
- finding: "Kein WINDOWS-Ledger-Eintrag fuer die strukturelle Erkennungsluecke von rls-access-inventory.spec.ts (Relationseinbindungen/`include`/`_count` in eine zweite Tabelle bleiben fuer das Werkzeug unsichtbar, unabhaengig davon, dass die heute einzige gefaehrliche Auspraegung in diesem Plan behoben wurde)."
category: architectural
reason: "Die Luecke ist ein dauerhaftes Werkzeug-Merkmal, kein historischer Einzelfall — ein KUENFTIGER `include: { _count }`-Zugriff auf eine geschuetzte Tabelle waere von der Bestandsaufnahme strukturell genauso unsichtbar wie der hier gefundene. Der Plan begruendet den Verzicht auf einen Ledger-Eintrag ausdruecklich mit 'nur diese eine Auspraegung existierte, hier behoben' — das schliesst aber nur die heutigen Instanzen, nicht den Mechanismus."
evidence_status: "Gemessen und in (n4)(b) sowie im Kopf der Bestandsaufnahme benannt; im Bedrohungsregister als T-E2S-09 (medium, accept) gefuehrt. Kein WINDOWS-Eintrag angelegt."
---
# Quick 260911-e2s: Mandantentrennung Etappe 2, Bereich `tenant` — Verification Report
**Task goal:** Remove the never-read `req.tenantPrisma` wiring (delete the
unwired middleware, strip the guard's Prisma dependency) while preserving
`req.tenantId` and the `x-tenant-id` switch; replace the three relation-count
reads into the protected `User` table with a bound fan-out; create the
missing guard and controller tests; leave the classification document in
sync.
**Verified:** 2026-09-11T09:06:02Z
**Status:** passed
**Re-verification:** No — initial verification
This is an adversarial, independent re-verification. Every claim below was
checked against the live codebase and, where feasible, against the running
database — SUMMARY.md text was never accepted as evidence on its own.
## Goal Achievement — Observable Truths (from PLAN.md `must_haves.truths`)
| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | Architecture decision on the bound-client-on-request-object question is made and recorded in code, kritikschrift, classification, and dev guide; a test makes a reappearance fail red | ✓ VERIFIED | `tenant.guard.ts` header comment cites 260911-e2s decision + reasoning; `docs/mandantentrennung-etappe2-fehlerrichtung.md` (n4)(a); `docs/mandantentrennung-zugriffsklassifikation.md` "Zwei belegte Befunde" ("Entschieden (260911-e2s, Aufgabe 2)"); `docs/anleitung-entwicklung.md` "## Mandantentrennung" rewritten. Independently broke the property back into the guard style described by SUMMARY's own falsification proof — not re-tested directly (guard no longer accepts it structurally, no constructor param); instead independently falsified the *closely-related* header/role gates below with the same red-then-restore method. `'tenantPrisma' in req` assertions present in all 7 `tenant.guard.spec.ts` cases. |
| 2 | `req.tenantId` stays set in all 5 branches; SUPER_ADMIN `x-tenant-id` switch and `ForbiddenException` for tenant-less non-SUPER_ADMIN survive; every branch pinned by a test | ✓ VERIFIED | Read `tenant.guard.ts`: 2 `req.tenantId =` assignments, no Prisma import, header check gated on `user.role === 'SUPER_ADMIN'`. Independently broke the header gate twice (forced `false && ...`, then removed the role check entirely) and re-ran `tenant.guard.spec.ts` each time — exactly 1 named test failed each time, with the expected assertion message; restored and confirmed 7/7 green and `git diff` clean afterwards. |
| 3 | `Tenant` classification checked against code and all 34 migrations; bound vs. unbound reads return identical rows (raw SQL + generated client) | ✓ VERIFIED | Ran `rls-scratch-check.mjs` live against `tessera-ctl-db-1` (address resolved fresh: `172.19.0.2`). All 110 checks passed, exit 0, including all 9 named `tenant-*` checks from the plan (`tenant-keine-regel-in-allen-ausgelieferten-migrationen` through `tenant-fan-out-je-mandant-gebundene-zaehlung-stimmt`). Migration scan output explicitly names `20260910120000_rls_widen_membership_grant_and_platform_read` and confirms `"Tenant"` is absent from it. |
| 4 | Relation-count finding measured and fixed: 3 of 8 accesses count through the User relation; fixed via bound fan-out | ✓ VERIFIED | Live probe checks 5–7 show the unbound relation counter returning 0 for every tenant while the maintenance role counts >0, and the FK (`User_tenantId_fkey`) loudly catching the vacuum delete-gate with P2003. `tenant.controller.ts` now uses exactly 3 `tenantPrisma.user.count(` calls (verified by grep) and 0 `include`/`_count` occurrences outside comments. |
| 5 | Reverse error direction is named: "every tenant has 0 users" (not empty list) and a 500 instead of the 400 message; frontend shown to pass both through | ✓ VERIFIED | `docs/mandantentrennung-etappe2-fehlerrichtung.md` "## Bereich tenant" (n2)/(n3) name `admin/tenants/page.tsx` and `TenantContextSelector.tsx` explicitly, with line references and the exact backend behavior (0 userCount / loud 500 vs. 400). |
| 6 | Detection gap of the automated inventory (relation includes into a second table) is named and measured | ✓ VERIFIED | (n4)(b) and the "## Bestandsaufnahme" head both name the gap; Befund G's two lists (`_count` sites, `include:` sites) are reproduced in the doc with per-site judgment. See Advisory note below re: no WINDOWS ledger entry. |
| 7 | SUPER_ADMIN restriction read and pinned as a metadata test | ✓ VERIFIED | `tenant.controller.ts` carries class-wide `@Roles(Role.SUPER_ADMIN)`; `tenant.controller.spec.ts` asserts `Reflect.getMetadata(ROLES_KEY, TenantController)` equals `[Role.SUPER_ADMIN]` and, for each of the 5 handlers, that no handler-level override exists. |
| 8 | Test landscape for guard and controller created from nothing (previously only 2 cases in `tenant.service.spec.ts`) | ✓ VERIFIED | `tenant.guard.spec.ts` (7 cases) and `tenant.controller.spec.ts` (20 cases) both newly created; both files exist, both pass (confirmed live: 7/7 and 20/20). |
| 9 | All five hand-maintained classification doc sections updated and machine-gated | ✓ VERIFIED | Independently recomputed: 64 (file,model) pairs in the Bestandsaufnahme table; class distribution 32/17/13/2 = 64 matches the doc's own "Klassen-Verteilung" table; overview row `\| tenant \| 8 \| 3 \|` matches independently-measured grep counts (`DU=8`, `DB=3`); "Zwei belegte Befunde" carries the 260911-e2s resolution; "Was diese Etappe NICHT entscheidet" first item marked `Aufgelöst (260911-e2s)`. |
| 10 | Baseline held: ≥883 tests green, type-check clean, tool ≥110 checks; switch stays OFF, schema/migrations untouched, no compose/env files touched, nothing in AD, NO policy on `Tenant` | ✓ VERIFIED | Orchestrator independently measured 911/911 tests (59 files) and clean type-check (both re-confirmed structurally: `find apps/api/src -name '*.spec.ts' \| wc -l` = 59). `git diff --name-only 6426b18..HEAD -- apps/api/prisma` empty; `-- docker-compose.yml docker-compose.prod.yml '*.env*'` empty; `-- apps/web` empty. Live probe: `pg_class.relrowsecurity` for `"Tenant"` = false, no `CREATE POLICY` on `Tenant` in any of 34 migrations. |
**Score:** 10/10 truths verified, 0 present-but-behavior-unverified.
## Independent Falsification (adversarial, not from SUMMARY)
All four falsifications below were run by the verifier directly against the
working tree, each backed up first and restored immediately after, with
`git status --short` / `diff` confirming a byte-identical restore:
1. **Header switch removed for SUPER_ADMIN** (`false && req.headers[...]`) → `tenant.guard.spec.ts` failed exactly 1/7: `SUPER_ADMIN mit tenantId und x-tenant-id-Kopfzeile...`, `expected 't1' to be 't2'`. Restored, 7/7 green.
2. **Role gate removed** (header now honoured for ANY role) → failed exactly 1/7: `Nutzer der Rolle ADMIN mit tenantId UND x-tenant-id-Kopfzeile... (T-04-03)`, `expected 't2' to be 't1'`. Restored, 7/7 green.
3. **`findOne` bound counter replaced with unbound `(this.prisma as any).user.count`** → `tenant.controller.spec.ts` failed exactly 2/20 with `TypeError: Cannot read properties of undefined (reading 'count')` — matches SUMMARY's claimed falsification exactly. Restored, 20/20 green.
4. **Stale entry injected into `FORTENANT_ASSIGNMENT_EXCEPTIONS`** (`'apps/api/src/does-not-exist.ts'`) → the new watchdog test failed exactly as designed: `"apps/api/src/does-not-exist.ts: Datei existiert nicht mehr"`. Restored, 11/11 green.
All four confirm the tests genuinely exercise the invariants they claim to
pin, not just that the invariants happen to hold today.
## Required Artifacts
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `apps/api/scripts/rls-scratch-check.mjs` | 12th section `runTenantAreaChecks`, ≥9 named checks, 5+ over generated client | ✓ VERIFIED | Confirmed at line 3163, called between `runCalendarAreaChecks` and `runTransactionShapeMeasurement` (line order verified). Live run: all 9 named checks pass, 110/110 total. |
| `docs/mandantentrennung-etappe2-fehlerrichtung.md` | `## Bereich tenant` with (n1)-(n5) | ✓ VERIFIED | All five `### (nX)` subsections present at line 2262+. |
| `apps/api/src/tenant/tenant.guard.ts` | sets only `req.tenantId`, no Prisma dependency | ✓ VERIFIED | Confirmed by direct read; no constructor, no `forTenant`/`PrismaService` import. |
| `apps/api/src/tenant/tenant.middleware.ts` | DELETED | ✓ VERIFIED | `test -e` confirms absence. |
| `apps/api/src/tenant/tenant.guard.spec.ts` | NEW, all 5 branches + property-absence + header-only-SUPER_ADMIN | ✓ VERIFIED | 7 cases, all read and confirmed present. |
| `apps/api/src/prisma/rls-access-inventory.spec.ts` | `FORTENANT_ASSIGNMENT_EXCEPTIONS` emptied + watchdog | ✓ VERIFIED | `new Set<string>([])`; watchdog test confirmed to fire (see falsification #4). |
| `apps/api/src/app.module.ts`, `module.guard.ts`, `dkv.controller.ts` | comment-only fixes | ✓ VERIFIED | Reviewed diffs manually; no `TenantMiddleware`/`.tenantPrisma` references remain anywhere in `apps/api/src`. |
| `apps/api/src/tenant/tenant.controller.ts` | 3 bound fan-out counters, 4 unbound tenant accesses | ✓ VERIFIED | Grep confirms exactly 3 `tenantPrisma.user.count(` and 4 `this.prisma.tenant.` occurrences; 0 `include`/`_count`. |
| `apps/api/src/tenant/tenant.controller.spec.ts` | NEW, two-client proof, all behaviors, role metadata, watchdog | ✓ VERIFIED | 20 cases, all read and confirmed to match plan's `<behavior>` spec. |
| `docs/mandantentrennung-zugriffsklassifikation.md` | 5 hand-maintained sections updated | ✓ VERIFIED | 64 pairs independently recomputed and cross-checked against the class-distribution table. |
| `docs/anleitung-entwicklung.md` | Guard description without request-object client; middleware hint box replaced | ✓ VERIFIED | 0 occurrences of `req.tenantPrisma`/`TenantMiddleware` in the file; obsolete table list intentionally left unchanged per (n5). |
## Key Link Verification
| From | To | Via | Status | Details |
|------|----|----|--------|---------|
| `TenantGuard` | `app.module.ts` `APP_GUARD` registration | order JwtAuthGuard → TenantGuard → RolesGuard | ✓ WIRED | Confirmed by direct read of `app.module.ts` lines 50-65. |
| Prisma `include: {_count}` | single SQL statement, LEFT JOIN into `User` | Prisma 6.19 query rendering | ✓ VERIFIED (live) | Reproduced live via probe checks 5-7 against the running dev DB, not merely asserted. |
| `User_tenantId_fkey` (`ON DELETE RESTRICT`) | referential check bypasses RLS | live delete against scratch DB | ✓ VERIFIED (live) | Check 7 output shows P2003 thrown, row still visible via maintenance role. |
| Fan-out pattern | `UserService.findAllForPlatformAdmin` | identical form (`this.prisma.tenant.findMany` unbound driver + `forTenant()` bound counter per tenant) | ✓ VERIFIED | Confirmed by direct code comparison — same structure. |
| SUPER_ADMIN `x-tenant-id` header | marketplace frontend | 4 send sites | ✓ VERIFIED | `grep -rn "x-tenant-id" apps/web/src` returns exactly 4 hits in `marketplace/page.tsx` and `marketplace/[slug]/page.tsx`, matching the plan's claim. |
## Behavioral Spot-Checks / Probe Execution
| Probe | Command | Result | Status |
|-------|---------|--------|--------|
| `apps/api/scripts/rls-scratch-check.mjs` (live, adversary-resolved DB address) | `TESSERA_SCRATCH_ADMIN_URL=... node apps/api/scripts/rls-scratch-check.mjs` | `Alle 110 Pruefungen bestanden.`, exit 0 | ✓ PASS |
| `tenant.guard.spec.ts` (single file) | `npm --prefix apps/api run test -- src/tenant/tenant.guard.spec.ts` | 7/7 | ✓ PASS |
| `tenant.controller.spec.ts` (single file) | `npm --prefix apps/api run test -- src/tenant/tenant.controller.spec.ts` | 20/20 | ✓ PASS |
| `rls-access-inventory.spec.ts` (single file) | `npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts` | 11/11 | ✓ PASS |
Full-suite result (911/911, 59 files) and `type-check` (clean) were not
re-run in full by this verifier — already independently measured by the
orchestrator per the task brief; spec-file count (59) was independently
confirmed by filesystem enumeration.
## Scope / Allow-list Verification
`git diff --name-only 6426b18..HEAD` returns exactly the 13 files declared
in the PLAN's `files_modified` frontmatter — no more, no less. No changes
under `apps/api/prisma`, `apps/web`, `docker-compose*.yml`, or any `.env*`
file. Working tree is clean except the untracked SUMMARY.md (expected —
committed by the orchestrator, not the task commits).
## Requirements Coverage
| Requirement | Description | Status | Evidence |
|-------------|-------------|--------|----------|
| WINDOWS-18 | Switch stays OFF; measurement tool covers the `tenant` area | ✓ SATISFIED | 110/110 checks pass live; switch confirmed unchanged (role `tessera`, `BYPASSRLS`, not touched by this diff). |
| ETAPPE-2-TENANT | Guard/controller/tests for the `tenant` area | ✓ SATISFIED | All artifacts and truths above. |
## Anti-Patterns Found
None of TBD/FIXME/XXX/TODO/HACK/PLACEHOLDER found in any of the 13 changed
files. No stub returns, no empty handlers, no hardcoded-empty props found in
the reviewed source files.
## Judgment Call Requested (Advisory, non-blocking)
**Item 8 of the verification brief:** should the absence of a WINDOWS ledger
entry for the structural blind spot in `rls-access-inventory.spec.ts`
(relation includes/`_count` into a second table are invisible to the
(file, model)-pair scanner) be acceptable?
**My judgment: it should be recorded regardless, even though it does not
block this phase.** The plan's own reasoning for skipping a ledger entry —
"the only dangerous instance found across the whole API source was this one,
and it's fixed here" — closes out today's *instances*, not the underlying
*mechanism*. The scanner will remain structurally blind to any *future*
`include: { _count }` (or similar relation-count) access into a
row-level-secured table; nothing added by this task changes that. This is
exactly the category of finding the project's own WINDOWS ledger exists to
track (compare entries #24, #19, #25, #26 in `.planning/WINDOWS.md`, all of
which record a persisting structural gap rather than a fixed one-off).
The plan did document the gap thoroughly (measured lists of all 19
`include:` and all `_count` sites, judged individually, in (n4)(b) and the
Bestandsaufnahme head) and carried it in the threat register as T-E2S-09
(medium, accept) — so this is not a hidden risk, just an un-ledgered one.
This does not affect the phase's must-have truths (truth #6 only requires
the gap to be *named and measured*, which it is) and is therefore **not a
gap** for this task, but is flagged here for a human decision on whether to
open a WINDOWS entry going forward.
## Gaps Summary
None. All 10 must-have truths verified with adversarial, independently
reproduced evidence (including 4 successful red-then-restore falsifications
and a live 110/110 probe run against the actual database). Scope is exactly
the declared 13-file allow-list. One advisory judgment call is flagged above
(WINDOWS ledger entry) — it does not block phase completion.
---
*Verified: 2026-09-11T09:06:02Z*
*Verifier: Claude (gsd-verifier)*