docs(quick-260910-jab): drei Datenbankregeln geschlossen und verifiziert

This commit is contained in:
2026-09-10 14:57:20 +02:00
parent 03fb3bf9c7
commit f2051202d8
3 changed files with 495 additions and 7 deletions
+10 -7
View File
@@ -4,10 +4,10 @@ milestone: v1.2
current_phase: 17 current_phase: 17
current_phase_name: eigene-ausschreibungs-quellen-je-nutzer current_phase_name: eigene-ausschreibungs-quellen-je-nutzer
status: verified status: verified
stopped_at: "Etappe 2 Bereich ldap abgeschlossen (Quick 260909-ipc). Naechster Bereich laut .continue-here.md: groups (37 Zugriffe)." stopped_at: "Quick 260910-jab abgeschlossen: T-JTS-02/T-JTS-03/WINDOWS #19 geschlossen (Migration 20260910120000). WINDOWS #24 neu offen (Verwaltungsweg fuer plattformweite Zeilen fehlt)."
last_updated: "2026-09-09T12:13:06.239Z" last_updated: "2026-09-10T12:35:40.000Z"
last_activity: 2026-09-09 last_activity: 2026-09-10
last_activity_desc: Etappe 1 der Mandantentrennung fertig — forTenant repariert, Anmeldeweg geloest, alle Zugriffe klassifiziert last_activity_desc: Quick 260910-jab — drei zu kurz greifende RLS-Regeln geschlossen (GroupMembership beide Seiten, ModuleGrant beide Ziele, TenderRssFeedSource Lese-/Schreibsplit), listForUser gebunden, Aktenstand kohaerent
state_head: e1586a41dd58432c489486abd408b406841e1a7f state_head: e1586a41dd58432c489486abd408b406841e1a7f
progress: progress:
total_phases: 17 total_phases: 17
@@ -117,6 +117,7 @@ Progress: [██████████] 100%
| Phase 17 P02 | 58min | 3 tasks | 14 files | | Phase 17 P02 | 58min | 3 tasks | 14 files |
| Phase 17 P03 | 62min | 3 tasks | 13 files | | Phase 17 P03 | 62min | 3 tasks | 13 files |
| Phase quick-260909-ipc P01 | 55min | 3 tasks | 8 files | | Phase quick-260909-ipc P01 | 55min | 3 tasks | 8 files |
| Phase quick-260910-jab P01 | 70min | 3 tasks | 16 files |
## Accumulated Context ## Accumulated Context
@@ -379,7 +380,9 @@ None yet.
| 260909-mir | Mandantentrennung Etappe 2, Bereich dkv — Tankkarten-Modul. Alle 21 klassifizierten Zugriffe in `dkv.service.ts` gebunden (am Ende 22, weil der neue Besitzriegel einen Lesezugriff hinzufuegt); genau einer bleibt bewusst ungebunden. **Bereits bestehende Fremdzugriffsluecke geschlossen (T-MIR-03):** `getExportFile(tenantId, filename)` nahm die Mandantenkennung entgegen und benutzte sie nie — die Datei kam allein ueber ihren Namen aus dem gemeinsamen `user-files/`-Verzeichnis, ein Administrator eines beliebigen Mandanten konnte die Tankkarten-Auswertung eines anderen herunterladen. Der Riegel leitet die Zugehoerigkeit jetzt aus `DkvInvoiceHistory.exportFilename` ab; in der Oberflaeche gegengeprueft, dass jeder angebotene Dateiname aus einer Historienzeile stammt, die regulaere Nutzung aendert sich also nicht. Die Luecke ist keine Folge des Umbaus, sie bestand seit jeher. **Zerstoerender Fehler in umgekehrter Richtung behoben:** `saveConfig` verschluckte im Zweig, der ein gespeichertes Passwort erhalten soll, Lese- und Entschluesselungsfehler und machte mit leeren Werten weiter — nach dem Scharfschalten haette er ein vorhandenes Passwort durch ein leeres ersetzt und verschluesselt abgelegt, ohne Meldung, nicht rekonstruierbar. **Dritte Variante der Testluecke:** der Bereich hatte gar keine Testdatei (nicht wie ldap eine, die nichts prueft, nicht wie groups/tenders eine, die abstuerzen wuerde); `dkv.service.spec.ts` neu angelegt. **WINDOWS #21, bewusste Entscheidung:** der Planer-Startpfad bleibt ungebunden und wird als benannte Altlast weitergefuehrt — binden ist unmoeglich (`onModuleInit` hat strukturell keinen Mandanten), Umbau auf einmal-abfragen-viele-bedienen waere die in 07-04 zurueckgestellte Mehrmandanten-Planung. Unsymmetrie zum ldap-Praezedenzfall ausgeschrieben: jener ist heute korrekt und verstummt spaeter, dieser ist HEUTE bereits falsch (bedient einen willkuerlichen Mandanten, bei inaktiver Zeile niemanden) UND verstummt zusaetzlich. Dreifach markiert. Erster Bereich, dessen Kopfzahl beim Hineinsehen NICHT kleiner wurde. **Ausfuehrung brach am 2026-09-09 gegen Ende von Aufgabe 3 an einem Sitzungslimit ab** — zwei Aufgaben committet, die dritte vollstaendig im Arbeitsbaum; am 2026-09-10 nachgetragen. Aufgefallen durch `git status`, nicht durch den Bericht. **Verifiziert 9/9, zwei Dokumentationsluecken danach behoben** (Hintergrunddienst-Abschnitt um den vierten Fall erweitert, Falsifizierungsnachweise nachgetragen). 789/789 Tests, Typpruefung sauber, 41/41 Live-Pruefungen | 2026-09-10 | 761e5e2,222f453,5e8237d | [260909-mir-mandantentrennung-etappe-2-bereich-dkv-a](./quick/260909-mir-mandantentrennung-etappe-2-bereich-dkv-a/) | | 260909-mir | Mandantentrennung Etappe 2, Bereich dkv — Tankkarten-Modul. Alle 21 klassifizierten Zugriffe in `dkv.service.ts` gebunden (am Ende 22, weil der neue Besitzriegel einen Lesezugriff hinzufuegt); genau einer bleibt bewusst ungebunden. **Bereits bestehende Fremdzugriffsluecke geschlossen (T-MIR-03):** `getExportFile(tenantId, filename)` nahm die Mandantenkennung entgegen und benutzte sie nie — die Datei kam allein ueber ihren Namen aus dem gemeinsamen `user-files/`-Verzeichnis, ein Administrator eines beliebigen Mandanten konnte die Tankkarten-Auswertung eines anderen herunterladen. Der Riegel leitet die Zugehoerigkeit jetzt aus `DkvInvoiceHistory.exportFilename` ab; in der Oberflaeche gegengeprueft, dass jeder angebotene Dateiname aus einer Historienzeile stammt, die regulaere Nutzung aendert sich also nicht. Die Luecke ist keine Folge des Umbaus, sie bestand seit jeher. **Zerstoerender Fehler in umgekehrter Richtung behoben:** `saveConfig` verschluckte im Zweig, der ein gespeichertes Passwort erhalten soll, Lese- und Entschluesselungsfehler und machte mit leeren Werten weiter — nach dem Scharfschalten haette er ein vorhandenes Passwort durch ein leeres ersetzt und verschluesselt abgelegt, ohne Meldung, nicht rekonstruierbar. **Dritte Variante der Testluecke:** der Bereich hatte gar keine Testdatei (nicht wie ldap eine, die nichts prueft, nicht wie groups/tenders eine, die abstuerzen wuerde); `dkv.service.spec.ts` neu angelegt. **WINDOWS #21, bewusste Entscheidung:** der Planer-Startpfad bleibt ungebunden und wird als benannte Altlast weitergefuehrt — binden ist unmoeglich (`onModuleInit` hat strukturell keinen Mandanten), Umbau auf einmal-abfragen-viele-bedienen waere die in 07-04 zurueckgestellte Mehrmandanten-Planung. Unsymmetrie zum ldap-Praezedenzfall ausgeschrieben: jener ist heute korrekt und verstummt spaeter, dieser ist HEUTE bereits falsch (bedient einen willkuerlichen Mandanten, bei inaktiver Zeile niemanden) UND verstummt zusaetzlich. Dreifach markiert. Erster Bereich, dessen Kopfzahl beim Hineinsehen NICHT kleiner wurde. **Ausfuehrung brach am 2026-09-09 gegen Ende von Aufgabe 3 an einem Sitzungslimit ab** — zwei Aufgaben committet, die dritte vollstaendig im Arbeitsbaum; am 2026-09-10 nachgetragen. Aufgefallen durch `git status`, nicht durch den Bericht. **Verifiziert 9/9, zwei Dokumentationsluecken danach behoben** (Hintergrunddienst-Abschnitt um den vierten Fall erweitert, Falsifizierungsnachweise nachgetragen). 789/789 Tests, Typpruefung sauber, 41/41 Live-Pruefungen | 2026-09-10 | 761e5e2,222f453,5e8237d | [260909-mir-mandantentrennung-etappe-2-bereich-dkv-a](./quick/260909-mir-mandantentrennung-etappe-2-bereich-dkv-a/) |
| 260910-das | Mandantentrennung Etappe 2, Bereich user — Benutzerverwaltung, die schwerste Fehlerklasse des Vorhabens (Fremdzugriff hier ist Rechteausweitung ueber Mandantengrenzen, nicht blosse Sichtbarkeit). Alle 17 Zugriffe eingeordnet: Verwaltungswege gebunden, Eindeutigkeits- und Suchwege bewusst ungebunden, jede Entscheidung mit Begruendung am Ort. **Startsperre entschaerft — der schwerwiegendste Fund:** nach dem Scharfschalten haette eine FRISCHE Installation ihren ersten Administrator nicht anlegen koennen und die Anwendung waere gar nicht erst gestartet. Kette (Glied fuer Glied belegt): die Startpruefung liefert `null` — nicht weil der Admin fehlt, sondern weil ohne Mandantenkontext keine Zeile sichtbar ist — also wird angelegt, das laeuft in den plattformweit eindeutigen Anmeldenamen, und weil `seedAdmin()` ungekapselt in `onApplicationBootstrap` haengt, bricht der Start ab. Betroffen waere jede Installation mit gesetzten Admin-Umgebungswerten gewesen; auf dem bestehenden System nie aufgefallen, weil dort der Admin laengst existiert. Jetzt bindet die Erstanlage an den eine Anweisung zuvor angelegten Mandanten und faengt GENAU den Doppelanlage-Fall ab — jeder andere Fehler bricht den Start weiterhin ab (beide Haelften einzeln nachgewiesen). **Riegel repariert, der seit seiner Entstehung wirkungslos war:** die Sperre gegen das Loeschen des eigenen Kontos verglich gegen ein Feld, das der Sitzungsnachweis gar nicht traegt (`sub`; er traegt `id`, `username`, `role`, `tenantId`) — ein Administrator konnte sein eigenes Konto loeschen. Kein Mandantenproblem, gefunden weil dieser Durchlauf jede Zeile aufschlaegt. **Zwei Falschaussagen in eigenen Artefakten berichtigt:** der Kopfkommentar von `findByUsername` behauptete, sie muesse fuer den mandantenuebergreifenden Anmeldeweg ungebunden bleiben — der laeuft seit Etappe 1 ueber die SECURITY-DEFINER-Funktionen, und die Methode hat gemessen NULL Aufrufer; und die Klassifikationszeile der Erstanlage behauptete, es gebe strukturell keinen Mandanten zum Binden, obwohl er eine Anweisung vorher entsteht (Klasse auf `beides` korrigiert). **SUPER_ADMIN-Sicht** war nach dem Scharfschalten in JEDER heutigen Form kaputt (ungebunden null Zeilen, gebunden stille Funktionsminderung) — jetzt Schleife ueber alle Mandanten mit gebundenem Rumpf, neues Fundstellenpaar `(user.service.ts, tenant)`. **Steuerungsschicht hatte gar keine Tests** (7 der 17 Zugriffe plus die gesamte Rollenlogik) — `user.controller.spec.ts` neu. Plan-Pruefer fand einen Blocker: vier handgepflegte Dokumentstellen benannt, nur zwei abgesichert — also derselbe Fehler, den der Plan verhindern sollte; nachgebessert mit herleitenden statt fest verdrahteten Pruefungen, in vier Einzelmutationen falsifiziert. **Verifiziert 10/10** (810/810 Tests, Typpruefung sauber, 53/53 Live-Pruefungen; Selbstloesch-Riegel und Klassenverteilung vom Pruefer eigenhaendig nachgerechnet) | 2026-09-10 | b848ba6,888f660,3a9391d | [260910-das-mandantentrennung-etappe-2-bereich-user-](./quick/260910-das-mandantentrennung-etappe-2-bereich-user-/) | | 260910-das | Mandantentrennung Etappe 2, Bereich user — Benutzerverwaltung, die schwerste Fehlerklasse des Vorhabens (Fremdzugriff hier ist Rechteausweitung ueber Mandantengrenzen, nicht blosse Sichtbarkeit). Alle 17 Zugriffe eingeordnet: Verwaltungswege gebunden, Eindeutigkeits- und Suchwege bewusst ungebunden, jede Entscheidung mit Begruendung am Ort. **Startsperre entschaerft — der schwerwiegendste Fund:** nach dem Scharfschalten haette eine FRISCHE Installation ihren ersten Administrator nicht anlegen koennen und die Anwendung waere gar nicht erst gestartet. Kette (Glied fuer Glied belegt): die Startpruefung liefert `null` — nicht weil der Admin fehlt, sondern weil ohne Mandantenkontext keine Zeile sichtbar ist — also wird angelegt, das laeuft in den plattformweit eindeutigen Anmeldenamen, und weil `seedAdmin()` ungekapselt in `onApplicationBootstrap` haengt, bricht der Start ab. Betroffen waere jede Installation mit gesetzten Admin-Umgebungswerten gewesen; auf dem bestehenden System nie aufgefallen, weil dort der Admin laengst existiert. Jetzt bindet die Erstanlage an den eine Anweisung zuvor angelegten Mandanten und faengt GENAU den Doppelanlage-Fall ab — jeder andere Fehler bricht den Start weiterhin ab (beide Haelften einzeln nachgewiesen). **Riegel repariert, der seit seiner Entstehung wirkungslos war:** die Sperre gegen das Loeschen des eigenen Kontos verglich gegen ein Feld, das der Sitzungsnachweis gar nicht traegt (`sub`; er traegt `id`, `username`, `role`, `tenantId`) — ein Administrator konnte sein eigenes Konto loeschen. Kein Mandantenproblem, gefunden weil dieser Durchlauf jede Zeile aufschlaegt. **Zwei Falschaussagen in eigenen Artefakten berichtigt:** der Kopfkommentar von `findByUsername` behauptete, sie muesse fuer den mandantenuebergreifenden Anmeldeweg ungebunden bleiben — der laeuft seit Etappe 1 ueber die SECURITY-DEFINER-Funktionen, und die Methode hat gemessen NULL Aufrufer; und die Klassifikationszeile der Erstanlage behauptete, es gebe strukturell keinen Mandanten zum Binden, obwohl er eine Anweisung vorher entsteht (Klasse auf `beides` korrigiert). **SUPER_ADMIN-Sicht** war nach dem Scharfschalten in JEDER heutigen Form kaputt (ungebunden null Zeilen, gebunden stille Funktionsminderung) — jetzt Schleife ueber alle Mandanten mit gebundenem Rumpf, neues Fundstellenpaar `(user.service.ts, tenant)`. **Steuerungsschicht hatte gar keine Tests** (7 der 17 Zugriffe plus die gesamte Rollenlogik) — `user.controller.spec.ts` neu. Plan-Pruefer fand einen Blocker: vier handgepflegte Dokumentstellen benannt, nur zwei abgesichert — also derselbe Fehler, den der Plan verhindern sollte; nachgebessert mit herleitenden statt fest verdrahteten Pruefungen, in vier Einzelmutationen falsifiziert. **Verifiziert 10/10** (810/810 Tests, Typpruefung sauber, 53/53 Live-Pruefungen; Selbstloesch-Riegel und Klassenverteilung vom Pruefer eigenhaendig nachgerechnet) | 2026-09-10 | b848ba6,888f660,3a9391d | [260910-das-mandantentrennung-etappe-2-bereich-user-](./quick/260910-das-mandantentrennung-etappe-2-bereich-user-/) |
| 260910-exd | Mandantentrennung Etappe 2, Bereich module-registry — der Berechtigungs-Anfrageweg. `module.guard.ts` laeuft bei JEDER Modulanfrage und hat null eigene Datenbankzugriffe; seine Richtigkeit ist vollstaendig eine Funktion dessen, was dieser Bereich liefert. Endstand 7 ungebunden / 10 gebunden. **Eine Vorgabe des Auftrags war falsch und wurde widerlegt:** angeblich wuerde eine Bindung des Modulkatalogs ihn fuer jeden Mandanten unsichtbar machen — gemessen ueber alle 23 `ENABLE ROW LEVEL SECURITY`-Zeilen steht `Module` auf keiner, die Tabelle traegt gar keinen Zeilenschutz und kein `tenantId`. Eine Bindung waere heute WIRKUNGSLOS, nicht katastrophal; katastrophal wird sie erst, wenn Etappe 3 der Tabelle eine Regel gibt. Handlung unveraendert (Katalog bleibt ungebunden), aber Messung und Bedingung sind in Code und Dokument jetzt getrennt — eine richtige Handlung mit falscher Begruendung haelt nur, bis sich jemand auf die Begruendung verlaesst. **Die Kernfrage ehrlich beantwortet:** es gibt KEIN Signal, das 'wirklich keine Freigabe' von 'die Abfrage hat nichts gefunden' unterscheidet. Nach dem Scharfschalten saehe ein Unterlauf nicht wie ein Fehler aus, sondern wie 'du hast keine Module' — leere Seitenleiste, leerer Marktplatz, jeder Modulaufruf abgewiesen, fuer den Betroffenen nicht von einem absichtlichen Entzug zu unterscheiden. Dreifach festgehalten: im Text, als Testfall in `module.guard.spec.ts` (zwei Aufrufe, identische Meldung, gegeneinander gehalten) und als WINDOWS #23 mit konkreter Etappe-4-Vorabpruefung. **Erstmals eine Entlastung, die strukturell haelt:** die Kette unsichtbare Zeile -> falsches 'frei' -> 23505 kann hier nicht auftreten, weil die Eindeutigkeitsschluessel die Mandantenkennung fuehren — erster von sechs Bereichen, in dem sie abwesend statt umgangen ist; entsprechend wurde KEINE Absicherung eingebaut, die nichts absichert. **Wieder zwei Kopfkommentare mit Falschaussagen** (`isModuleActive` 'Used by ModuleGuard', `findActiveForTenant` als Marktplatz-Lieferant), beide Methoden mit null Aufrufern — berichtigt. `module-registry.service.ts` hatte trotz 11 der 17 Zugriffe und aller Schreibwege GAR KEINE Testdatei. Reihenfolge-Entlastung: der Dashboard-Filter wird mitgebunden, ohne dass eine `dashboard`-Datei angefasst wird. Zwei Abweichungen, beide vom eigenen Pruefgatter erzwungen und geprueft: eine Identitaets-Attrappe in `tender-scheduler.service.spec.ts` (zulaessig — jene Datei prueft Planer-Verhalten, die Bindung ist in `module-registry.service.spec.ts` mit zwei Klienten belegt) und eine Stand-Spalte, die eine Aufgabe frueher nachgezogen werden musste. **Verifiziert 9/9** (833/833 Tests, Typpruefung sauber, 66/66 Live-Pruefungen; Klassenverteilung und Summenzeile vom Pruefer eigenhaendig nachgerechnet). Eine Zahl in der Zusammenfassung (7 statt 6 neue Bindungsnachweise) vom Pruefer nachgezaehlt und berichtigt | 2026-09-10 | 7d45e2f,3df7268,9c0eefe | [260910-exd-mandantentrennung-etappe-2-bereich-modul](./quick/260910-exd-mandantentrennung-etappe-2-bereich-modul/) | | 260910-exd | Mandantentrennung Etappe 2, Bereich module-registry — der Berechtigungs-Anfrageweg. `module.guard.ts` laeuft bei JEDER Modulanfrage und hat null eigene Datenbankzugriffe; seine Richtigkeit ist vollstaendig eine Funktion dessen, was dieser Bereich liefert. Endstand 7 ungebunden / 10 gebunden. **Eine Vorgabe des Auftrags war falsch und wurde widerlegt:** angeblich wuerde eine Bindung des Modulkatalogs ihn fuer jeden Mandanten unsichtbar machen — gemessen ueber alle 23 `ENABLE ROW LEVEL SECURITY`-Zeilen steht `Module` auf keiner, die Tabelle traegt gar keinen Zeilenschutz und kein `tenantId`. Eine Bindung waere heute WIRKUNGSLOS, nicht katastrophal; katastrophal wird sie erst, wenn Etappe 3 der Tabelle eine Regel gibt. Handlung unveraendert (Katalog bleibt ungebunden), aber Messung und Bedingung sind in Code und Dokument jetzt getrennt — eine richtige Handlung mit falscher Begruendung haelt nur, bis sich jemand auf die Begruendung verlaesst. **Die Kernfrage ehrlich beantwortet:** es gibt KEIN Signal, das 'wirklich keine Freigabe' von 'die Abfrage hat nichts gefunden' unterscheidet. Nach dem Scharfschalten saehe ein Unterlauf nicht wie ein Fehler aus, sondern wie 'du hast keine Module' — leere Seitenleiste, leerer Marktplatz, jeder Modulaufruf abgewiesen, fuer den Betroffenen nicht von einem absichtlichen Entzug zu unterscheiden. Dreifach festgehalten: im Text, als Testfall in `module.guard.spec.ts` (zwei Aufrufe, identische Meldung, gegeneinander gehalten) und als WINDOWS #23 mit konkreter Etappe-4-Vorabpruefung. **Erstmals eine Entlastung, die strukturell haelt:** die Kette unsichtbare Zeile -> falsches 'frei' -> 23505 kann hier nicht auftreten, weil die Eindeutigkeitsschluessel die Mandantenkennung fuehren — erster von sechs Bereichen, in dem sie abwesend statt umgangen ist; entsprechend wurde KEINE Absicherung eingebaut, die nichts absichert. **Wieder zwei Kopfkommentare mit Falschaussagen** (`isModuleActive` 'Used by ModuleGuard', `findActiveForTenant` als Marktplatz-Lieferant), beide Methoden mit null Aufrufern — berichtigt. `module-registry.service.ts` hatte trotz 11 der 17 Zugriffe und aller Schreibwege GAR KEINE Testdatei. Reihenfolge-Entlastung: der Dashboard-Filter wird mitgebunden, ohne dass eine `dashboard`-Datei angefasst wird. Zwei Abweichungen, beide vom eigenen Pruefgatter erzwungen und geprueft: eine Identitaets-Attrappe in `tender-scheduler.service.spec.ts` (zulaessig — jene Datei prueft Planer-Verhalten, die Bindung ist in `module-registry.service.spec.ts` mit zwei Klienten belegt) und eine Stand-Spalte, die eine Aufgabe frueher nachgezogen werden musste. **Verifiziert 9/9** (833/833 Tests, Typpruefung sauber, 66/66 Live-Pruefungen; Klassenverteilung und Summenzeile vom Pruefer eigenhaendig nachgerechnet). Eine Zahl in der Zusammenfassung (7 statt 6 neue Bindungsnachweise) vom Pruefer nachgezaehlt und berichtigt | 2026-09-10 | 7d45e2f,3df7268,9c0eefe | [260910-exd-mandantentrennung-etappe-2-bereich-modul](./quick/260910-exd-mandantentrennung-etappe-2-bereich-modul/) |
| 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/) |
| 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/) |
## Deferred Items ## Deferred Items
@@ -419,8 +422,8 @@ sind. Kein Anlass, sie vorher erneut vorzulegen.
## Session Continuity ## Session Continuity
Last session: 2026-09-09T12:13:05.681Z Last session: 2026-09-10T12:35:40.000Z
Resumed: 2026-09-09 — Sitzung ueber /gsd-resume-work fortgesetzt; Einstiegspunkt Etappe 2 (Bereich ldap), Auswahl des Nutzers steht aus. Resumed: 2026-09-09 — Sitzung ueber /gsd-resume-work fortgesetzt; Einstiegspunkt Etappe 2 (Bereich ldap), Auswahl des Nutzers steht aus.
Stopped at: Etappe 2, sieben von ZWOELF Bereichen durch (ldap 260909-ipc, groups 260909-jts, tenders 260909-laa, dkv 260909-mir, user 260910-das). KORREKTUR der Bereichsliste: es sind zwoelf, nicht elf — `auth` fehlte in der Uebergabe und hat noch 5 ungebundene Zugriffe (getMe/changePassword/adminResetPassword, in Etappe 1 bewusst ausgelassen). Gegengeprueft: ausserhalb der zwoelf beruehrt KEIN Dienst die Datenbank. SIEBEN von zwoelf durch (zuletzt module-registry 260910-exd). Naechster Bereich: dashboard (13), danach calendar (12), tenant (8), favorites (7), auth (5), settings (4). ACHTUNG settings: Befund K macht ihn zur Reihenfolgebedingung fuer Etappe 4. ACHTUNG settings: Befund K aus 260909-laa macht ihn zur Reihenfolgebedingung fuer Etappe 4 — ohne ihn kein Mailversand nach dem Scharfschalten. Der User hat am 2026-09-09 gesagt, die restlichen Bereiche sollen ohne Rueckfrage durchlaufen; beim Scharfschalten (Etappe 4) wird ausdruecklich angehalten. Stopped at: Quick 260910-jab abgeschlossen — die drei zu kurz greifenden Datenbankregeln (T-JTS-02, T-JTS-03, WINDOWS #19) sind geschlossen, VORGEZOGEN auf ausdruecklichen Nutzerwunsch vor die restlichen fuenf offenen Bereiche der Etappe 2 (dashboard 13, calendar 12, tenant 8, favorites 7, auth 5, settings 4 — sechs, nicht fuenf, `settings` war bereits vorher als Reihenfolgebedingung fuer Etappe 4 markiert, Befund K). Diese fuenf/sechs Bereiche messen ab jetzt gegen die NEUEN Regeln aus 20260910120000_rls_widen_membership_grant_and_platform_read. WINDOWS #19 fixed, WINDOWS #24 neu offen (Verwaltungsweg fuer plattformweite Zeilen unter der Anwendungsrolle). Naechster Schritt laut vorheriger Uebergabe: Bereich dashboard (13 Zugriffe), danach wie zuvor geplant. Der User hat am 2026-09-09 gesagt, die restlichen Bereiche sollen ohne Rueckfrage durchlaufen; beim Scharfschalten (Etappe 4) wird ausdruecklich angehalten.
Resume file: None Resume file: None
Last activity: 2026-09-10 - Completed quick task 260910-exd: Mandantentrennung Etappe 2, Bereich module-registry (verifiziert 9/9) Last activity: 2026-09-10 - Completed quick task 260910-jab: Die drei zu kurz greifenden Datenbankregeln (T-JTS-02, T-JTS-03, WINDOWS #19) geschlossen
@@ -0,0 +1,320 @@
---
phase: quick-260910-jab
plan: 01
subsystem: mandantentrennung-datenbankrolle
tags: [rls, postgresql, multi-tenancy, security, module-grants, groups, tenders]
status: complete
dependency-graph:
requires: [T-JTS-02, T-JTS-03, WINDOWS-19]
provides: [T-JAB-01..15-mitigations, rls-widen-migration-20260910120000]
affects: [apps/api/prisma, apps/api/src/groups, apps/api/src/tenders, apps/api/src/module-registry, apps/api/scripts/rls-scratch-check.mjs]
tech-stack:
added: []
patterns:
- "Vier nach Befehl getrennte RLS-Policies (SELECT/INSERT/UPDATE/DELETE) statt einer permissiven USING-Klausel, wenn Lesen und Schreiben unterschiedliche Sichtbarkeitsregeln brauchen"
- "Loch-behauptende Wegwerf-Pruefungen werden UMGEKEHRT statt geloescht, mit Verweis auf den alten Pruefungsnamen und die alte Befundkennung im Meldetext"
- "Eine Zeile, auf der spaetere Pruefungen aufsetzen, wird nach einer Regelverschaerfung ueber die Wartungsrolle (BYPASSRLS) bereitgestellt, wenn der urspruengliche Erzeugungsweg (gebundenes INSERT) jetzt abgewiesen wird"
key-files:
created:
- apps/api/prisma/migrations/20260910120000_rls_widen_membership_grant_and_platform_read/migration.sql
modified:
- apps/api/scripts/rls-scratch-check.mjs
- apps/api/src/groups/migration-sql.spec.ts
- apps/api/src/tenders/tender-rss-feed.service.ts
- apps/api/src/tenders/tender-rss-feed.service.spec.ts
- apps/api/src/tenders/tenders.controller.ts
- apps/api/src/tenders/tenders.controller.spec.ts
- apps/api/src/groups/groups.service.ts
- 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/prisma/rls-coverage.spec.ts
- docs/mandantentrennung-zugriffsklassifikation.md
- docs/mandantentrennung-etappe2-fehlerrichtung.md
- docs/mandantentrennung-datenbankrolle.md
- .planning/WINDOWS.md
decisions:
- "GroupMembership-Regel prueft jetzt beide Seiten der Beziehung (Gruppe UND Benutzer, mit UND verknuepft), nach dem Join-Muster von PasswordResetToken"
- "ModuleGrant-Regel prueft zusaetzlich beide moeglichen Ziele (Gruppe/Benutzer) mit Leer-Zulassung, weil D-04 Gruppe und Benutzer als Entweder-oder fuehrt"
- "TenderRssFeedSource bekommt vier nach Befehl getrennte Policies statt einer, weil ein einzelner USING-Ausdruck auch UPDATE/DELETE mitregelt"
- "SearchProvider bewusst NICHT angefasst — die WINDOWS-#19-Praemisse ist fuer dieses Modell widerlegt (kein Codeweg erzeugt eine mandantenlose Zeile)"
- "TenderRssFeedSourceService.listForUser wird gebunden (einziger Anwendungscode-Pfad, den die Reparatur sonst still falsch gemacht haette); createPlatform/remove bleiben bewusst ungebunden"
- "WINDOWS #24 neu angelegt: der Verwaltungsweg fuer plattformweite Zeilen unter der Anwendungsrolle fehlt weiterhin — verschwindet nicht mit der Schliessung von #19"
metrics:
duration: "~70min"
completed: 2026-09-10
actuals:
tokens: 34770
tasks: 3
commits: 3
plan_head_before: 93444aa91e8fb0739ee5bbf023f7ccbbd3cee38e
---
# Quick 260910-jab: Die drei zu kurz greifenden Datenbankregeln schliessen — Summary
Eine handgeschriebene, lokal angewandte PostgreSQL-Migration schliesst
T-JTS-02 (GroupMembership prueft nur die Gruppenseite), T-JTS-03 (ModuleGrant
prueft nur die Mandantenkennung der Zeile, nicht wohin sie zeigt) und
WINDOWS #19 (plattformweite TenderRssFeedSource-Zeilen waeren nach dem
Scharfschalten fuer JEDEN Mandanten unsichtbar gewesen); das Wegwerf-Werkzeug
misst alle drei Reparaturen an der lebenden Datenbank, mit den drei
loch-behauptenden Pruefungen umgekehrt statt geloescht.
## Ausgangslage — selbst gemessen (nicht uebernommen)
Zu Beginn von Aufgabe 1 frisch gemessen, gegen HEAD `93444aa`:
- **Tests:** 833 bestanden, 56 Dateien (`npm --prefix apps/api run test`)
- **Typpruefung:** sauber (`npm --prefix apps/api run type-check`)
- **Wegwerf-Werkzeug:** 66/66 Pruefungen bestanden
(`rls-scratch-check.mjs` gegen `tessera-ctl-db-1`, Adresse `172.19.0.2`,
zur Laufzeit ermittelt)
Diese drei Zahlen decken sich mit der in der Aufgabenstellung genannten
Baseline — hier trotzdem selbst nachgemessen, nicht abgeschrieben (die
Vorgabe der Aufgabenstellung verlangt genau das).
## Am Ende gemessen
- **Tests:** 839 bestanden, 56 Dateien (+6, alle aus dem neuen
`migration-sql.spec.ts`-Beschreibungsblock)
- **Typpruefung:** sauber
- **Wegwerf-Werkzeug:** 74/74 Pruefungen bestanden (+8: siehe Aufschlüsselung
unten unter "Neue/umgekehrte Pruefungen")
## Bestandszaehlung ueber Mandantengrenzen (Befund H, vor Anwendung der Migration)
Gegen die lokale Datenbank `tessera` ausgefuehrt, vor dem Anwenden der neuen
Migration:
```
memberships-cross-tenant | 0
grants-cross-tenant-group | 0
grants-cross-tenant-user | 0
total-memberships | 3
total-grants | 4
total-tenants | 1
tenderrssfeed-rows | 2
tenderrssfeed-platform-rows | 1
searchprovider-rows | 0
searchprovider-null-tenant | 0
```
Ergebnis null — es gibt lokal keine Zeile, die von der verschärften Regel
unsichtbar würde. Für das Testsystem ist das NICHT gemessen und muss vor
Etappe 4 als eigene Vorabprüfung wiederholt werden (siehe Befund H im Plan).
## Was gebaut wurde
### Aufgabe 1 — Migration + Wegwerf-Werkzeug
Neue Migration `20260910120000_rls_widen_membership_grant_and_platform_read`
(lokal angewandt via `prisma migrate deploy` — **nicht** über `npx prisma`,
das versuchte ungefragt Prisma 8.0.0-rc.13 herunterzuladen und wurde
abgebrochen; stattdessen `apps/api/node_modules/.bin/prisma`, die im Projekt
gepinnte Version 6.19.3, dieselbe, die `rls-scratch-check.mjs` selbst
verwendet):
1. **`GroupMembership`** — die Regel prüft jetzt `groupId IN (...)` UND
`userId IN (...)`, beide gegen den laufenden Mandanten.
2. **`ModuleGrant`** — die Regel prüft weiterhin `tenantId = current_tenant_id()`
UND zusätzlich `groupId IS NULL OR groupId IN (...)` UND
`userId IS NULL OR userId IN (...)`.
3. **`TenderRssFeedSource`** — vier Regeln statt einer:
`tenant_platform_read_policy` (SELECT, schließt `tenantId IS NULL` ein),
`tenant_insert_policy`/`tenant_update_policy`/`tenant_delete_policy`
(verlangen ausnahmslos einen Mandanten).
4. **`SearchProvider`** — unverändert, nur der erklärende Absatz im
Migrationskopf.
Regelliste der lebenden Datenbank nach Anwendung (`pg_policies`, Auszug):
```
GroupMembership#tenant_isolation_policy#ALL#(groupId IN (...) AND userId IN (...))
ModuleGrant#tenant_isolation_policy#ALL#(tenantId = ... AND (groupId IS NULL OR ...) AND (userId IS NULL OR ...))
SearchProvider#tenant_isolation_policy#ALL#(tenantId = current_tenant_id())
TenderRssFeedSource#tenant_delete_policy#DELETE#(tenantId = current_tenant_id())
TenderRssFeedSource#tenant_insert_policy#INSERT##WITH CHECK (tenantId = current_tenant_id())
TenderRssFeedSource#tenant_platform_read_policy#SELECT#(tenantId = current_tenant_id() OR tenantId IS NULL)
TenderRssFeedSource#tenant_update_policy#UPDATE#USING(...)#WITH CHECK(...)
```
**Neue/umgekehrte Pruefungen im Wegwerf-Werkzeug** (66 → 74):
| Kennung | Art | Ergebnis |
|---|---|---|
| `groupmembership-schreiben-fremder-benutzer-abgelehnt` | Umkehr (T-JTS-02) | bestanden — Insert mit echtem `user-b` abgewiesen |
| `groupmembership-fremder-benutzer-ueber-die-wartungsrolle-weiterhin-moeglich` | neu | bestanden — Gegenmessung über Wartungsrolle gelingt |
| `modulegrant-fremde-gruppe-abgelehnt` | Umkehr (T-JTS-03) | bestanden |
| `modulegrant-fremder-benutzer-abgelehnt` | neu (zweiter D-04-Zweig) | bestanden |
| `modulegrant-fremde-gruppe-ueber-die-wartungsrolle-weiterhin-moeglich` | neu (legt `grant-foreign-group` an) | bestanden |
| `tenderrssfeed-plattformzeile-gebunden-sichtbar` | Umkehr (WINDOWS #19) | bestanden |
| `tenderrssfeed-eigene-zeile-gebunden-weiterhin-sichtbar` | neu | bestanden |
| `tenderrssfeed-ungebunden-nur-die-plattformzeile` | neu (Befund F) | bestanden |
| `tenderrssfeed-gebundenes-aendern-der-plattformzeile-abgelehnt` | neu | bestanden |
| `tenderrssfeed-gebundenes-loeschen-der-plattformzeile-abgelehnt` | neu | bestanden |
| `searchprovider-mandantenlose-zeile-bleibt-unter-jedem-kontext-unsichtbar` | neu (eigener Bereich) | bestanden |
Extraktion umgeleitet: `readRlsWidenMigrationSql()` liest die neue Datei
für `GroupMembership`/`ModuleGrant`; `extractAllPolicySql()` (neu, mit
globalem Regex-Flag) liefert alle vier `TenderRssFeedSource`-Policies. Die
Meldung von `gruppenpfad-gebunden-schliesst-die-fremde-gruppe-aus` im
Bereich `module-registry` ist richtiggestellt (behauptete vorher fälschlich,
die Regel lasse die fremde Zeile durch).
### Aufgabe 2 — `listForUser` binden
`TenderRssFeedSourceService.listForUser(userId, tenantId)` läuft jetzt über
`forTenant()`. `TendersController.listRssFeeds` reicht die Mandantenkennung
aus dem Sitzungsnachweis durch (Aufrufform geprüft, nicht die Antwort).
`createPlatform`/`remove` bleiben bewusst ungebunden. Fünf Aufzeichnungen im
Quelltext (drei Kopfkommentare in `tender-rss-feed.service.ts`,
`module-access.service.ts`, `groups.service.ts`, `module-grants.service.ts`,
`rls-coverage.spec.ts`) nennen die neue Migration und beschreiben die neue
Regel statt der alten. Die Mandanten-Gegenprüfung
(`assertTargetBelongsToTenant`) bleibt unverändert bestehen — zwei bestehende
Testfälle in `module-grants.service.spec.ts` wurden um einen Kommentar
ergänzt, der festhält, warum sie nach der Regeländerung NICHT entbehrlich
geworden sind.
**Deviation, gemessen statt geglaubt (siehe `<output>`-Vorgabe des Plans):**
die Behauptung "kein Anwendungscode muss sich ändern" trifft für GENAU EINEN
Pfad nicht zu — `TenderRssFeedSourceService.listForUser`. Vor der
Regeländerung lieferte der ungebundene Pfad nach dem Scharfschalten NICHTS
(gemessen: `tenderrssfeed-ungebunden-nur-die-plattformzeile` würde ohne
Bindung 0 statt 1 Zeile liefern). Nach der Regeländerung liefert derselbe
ungebundene Pfad NUR die plattformweiten Zeilen — eine kurze, glaubhafte
Teilantwort statt einer schreienden Leere. Das Binden gehört deshalb zur
Reparatur selbst, nicht daneben: ohne diese Bindung hätte 260910-jab einen
Pfad still von "meldet sich laut" auf "täuscht Vollständigkeit vor"
verschlechtert.
**Falsifizierungsnachweis** (Bindungsaufruf zurückgenommen, Test rot
gesehen, Rücknahme zurückgenommen):
```
Backup genommen → const tenantPrisma = ... entfernt, this.prisma direkt verwendet
npm --prefix apps/api run test -- src/tenders/tender-rss-feed.service.spec.ts
× listForUser() bindet — forTenant() wird mit der uebergebenen Mandantenkennung aufgerufen
AssertionError: expected "spy" to be called with arguments: [ {…(4)}, 'tenant-a' ]
Number of calls: 0
Test Files 1 failed (1)
Tests 1 failed | 30 passed (31)
→ Rücknahme rückgängig gemacht (Backup wiederhergestellt), erneuter Lauf: 31/31 grün
```
Genau EIN Test wurde rot, wie erwartet — kein anderer Test hing versehentlich
an dieser Bindung.
**Rule 1 (Auto-Fix):** `listRssFeeds` destrukturierte `feeds.map(({userId, ...rest}) => ...)`.
Da `listForUser` jetzt über `forTenant(...) as any` läuft, wurde `feeds`
implizit `any`, und TypeScript meldete `TS7031: Binding element 'ownerUserId'
implicitly has an 'any' type`. Behoben mit expliziter `(... : any)`-Annotation,
Muster aus anderen `.map((g: any) => ...)`-Stellen derselben Datei.
Typpruefung danach wieder sauber.
### Aufgabe 3 — Aktenstand kohärent
- **`.planning/WINDOWS.md`**: #19 auf `fixed` (Tabelle + JSON-Block,
`resolved_at: 2026-09-10T12:35:40.000Z`), Beleg nennt die neue Migration
und die Prüfungen für beide Fehlerrichtungen des Lese-/Schreibsplits.
#18/#20/#21/#22/#23 unverändert offen. Neuer Eintrag **#24** (offen, Kind
`deviation`): der Verwaltungsweg für plattformweite Zeilen unter der
Anwendungsrolle fehlt — weder Anlegen noch Entfernen ist unter der
Anwendungsrolle möglich, in der alten wie in der neuen Regel. Kopfzahlen
aus dem JSON-Block abgeleitet und mit einem eigenen Python-Skript
gegengeprüft (identisch zum Verify-Skript des Plans): 6 offen, 17 behoben,
1 zurückgestellt, 24 gesamt.
- **Klassifikation**: #19-Block beantwortet, die vier betroffenen
Bestandsaufnahme-Zeilen nachgezogen, Übersichtszeile `tenders` (35/27,
gemessen mit den beiden im Dokument selbst geführten grep-Anweisungen) und
Summenzeile (107/135) neu aus dem Quelltext abgeleitet — beide mit dem
exakten `awk`-Prüfskript des Plans gegengeprüft.
- **Kritikschrift**: neuer Abschnitt `## Regelschluss T-JTS-02, T-JTS-03 und
WINDOWS #19` mit (r1) tatsächlich beobachteter Ausgabe + Regelliste aus
der lebenden Datenbank, (r2) Signaltabelle mit beiden Fehlerrichtungen je
Regel, (r3) den Stellen, an denen Leere weiterhin als Abwesenheit gedeutet
wird (inklusive der neuen Stelle aus Befund F), (r4) was dieser Durchlauf
nicht löst, (r5) was er nicht anfasst. Fünf überholte Bestandsstellen mit
Nachträgen versehen (der Punkt in (g4), der Punkt in (t4), die
Signaltabellenzeile der drei RSS-Pfade, DREI aufgezeichnete
Werkzeugausgaben — eine mehr als die im Plan als Minimum genannten zwei,
weil beim Durchsuchen eine dritte literale Zitatstelle im Abschnitt
`module-registry` gefunden wurde — und der Punkt in (m4)). Alle alten
Messprotokolle bleiben wörtlich stehen, die Nachträge stehen daneben.
- **Selbst ausgeführte Messung zur fehlenden Benutzerdimension** (Aufgabe 3,
wie vom Plan verlangt): `grep -rn "current_setting\|set_config" apps/api/src
apps/api/prisma/migrations` findet GENAU EINE Sitzungsvariable,
`app.current_tenant` (`prisma-tenant.extension.ts`,
`20260618112133_rls_policies`). Es gibt KEINE zweite Sitzungsvariable für
den Benutzer — Tabellen wie `TenderSavedSearch` haben deshalb strukturell
keine Möglichkeit, eine Benutzerdimension auf Datenbankebene durchzusetzen,
ohne eine solche Variable erst einzuführen. Ergebnis in (r4) der
Kritikschrift festgehalten, nicht gebaut.
- **Betriebsanleitung**: die eine Stelle, die #19 als offen führte, nennt
jetzt den Auflösungsstand und WINDOWS #24.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] `TenderRssFeedSource`-Wegwerf-UPDATE-Prüfung zielte auf
eine nicht existierende Spalte `label`**
- **Gefunden während:** Aufgabe 1, erster Lauf des Wegwerf-Werkzeugs nach
dem Hinzufügen der neuen UPDATE-Prüfung
- **Problem:** `tenderrssfeed-gebundenes-aendern-der-plattformzeile-abgelehnt`
versuchte `SET label = ...`, aber die im selben Abschnitt angelegte
Wegwerf-Tabelle hat keine `label`-Spalte (`id, url, userId, tenantId`) —
die Prüfung "bestand", aber aus dem FALSCHEN Grund (SQL-Fehler `42703`
statt der beabsichtigten RLS-Abweisung).
- **Fix:** Spalte auf `url` geändert, die tatsächlich existiert; danach
bestand die Prüfung aus dem richtigen Grund (`tenant_update_policy`
filtert die Zielzeile heraus, 0 betroffene Zeilen).
- **Dateien:** `apps/api/scripts/rls-scratch-check.mjs`
- **Commit:** `f4f3115`
**2. [Rule 1 - Bug] Implizites `any` beim Destrukturieren in `listRssFeeds`**
- **Gefunden während:** Aufgabe 2, Typprüfung nach dem Binden von
`listForUser`
- **Problem:** `feeds.map(({ userId: ownerUserId, ...rest }) => ...)` löste
`TS7031` aus, weil `feeds` durch die neue `forTenant(...) as any`-Bindung
implizit `any` wurde.
- **Fix:** explizite `(... : any)`-Annotation am Destrukturierungsparameter.
- **Dateien:** `apps/api/src/tenders/tenders.controller.ts`
- **Commit:** `6b23735`
### Package-Legitimitätsgatter
**3. [Rule 3 – ausgeschlossen, keine Installation]** `npx prisma migrate
deploy` versuchte beim Ausführen ungefragt, `prisma@8.0.0-rc.13`
herunterzuladen (weiter als die im Projekt gepinnte 6.19.3) — abgebrochen
(`pkill`), stattdessen `apps/api/node_modules/.bin/prisma` verwendet, die
lokale, im Lockfile gepinnte Version. Keine Installation fand statt, daher
kein Checkpoint nötig — dokumentiert, weil es beinahe eine unbeabsichtigte
Fremdversion in den Migrationslauf eingeschleust hätte.
Keine weiteren Abweichungen — alle übrigen Aufgaben wie im Plan beschrieben
ausgeführt.
## Known Stubs
Keine.
## Threat Flags
Keine neuen — das Threat-Model des Plans (T-JAB-01 bis T-JAB-15) deckt alle
in diesem Durchlauf berührten Flächen bereits ab; keine neue, dort nicht
erfasste Angriffsfläche entstanden.
## Self-Check: PASSED
- `apps/api/prisma/migrations/20260910120000_rls_widen_membership_grant_and_platform_read/migration.sql` — FOUND
- Commit `f4f3115` (Aufgabe 1) — FOUND (`git log --oneline --all | grep f4f3115`)
- Commit `6b23735` (Aufgabe 2) — FOUND
- Commit `03fb3bf` (Aufgabe 3) — FOUND
- `rls-scratch-check.mjs` meldet 74/74 bestanden gegen die lebende Datenbank — bestätigt (letzter Lauf vor diesem Bericht)
- `.planning/WINDOWS.md` besteht das plan-eigene Python-Kohärenzskript — bestätigt
- `npm --prefix apps/api run test` — 839/839 grün, `type-check` sauber — bestätigt
@@ -0,0 +1,165 @@
---
phase: quick-260910-jab
verified: 2026-09-10T14:55:00Z
status: passed
score: 11/11 must-haves verified
covered_files:
- ".planning/WINDOWS.md"
- ".planning/quick/260910-jab-mandantentrennung-die-drei-zu-kurz-greif/260910-jab-PLAN.md"
- ".planning/quick/260910-jab-mandantentrennung-die-drei-zu-kurz-greif/260910-jab-SUMMARY.md"
- "apps/api/prisma/migrations/20260910120000_rls_widen_membership_grant_and_platform_read/migration.sql"
- "apps/api/scripts/rls-scratch-check.mjs"
- "apps/api/src/groups/groups.service.ts"
- "apps/api/src/groups/migration-sql.spec.ts"
- "apps/api/src/groups/module-grants.service.spec.ts"
- "apps/api/src/groups/module-grants.service.ts"
- "apps/api/src/module-registry/module-access.service.ts"
- "apps/api/src/prisma/rls-coverage.spec.ts"
- "apps/api/src/tenders/tender-rss-feed.service.spec.ts"
- "apps/api/src/tenders/tender-rss-feed.service.ts"
- "apps/api/src/tenders/tenders.controller.spec.ts"
- "apps/api/src/tenders/tenders.controller.ts"
- "docs/mandantentrennung-datenbankrolle.md"
- "docs/mandantentrennung-etappe2-fehlerrichtung.md"
- "docs/mandantentrennung-zugriffsklassifikation.md"
covered_digest: "v1:sha256:4ba76478ddf3d04462ca28c23051e03f469ffd731ae9db235c5cd3e25c803e6f"
behavior_unverified: 0
overrides_applied: 0
---
# Quick 260910-jab: Die drei zu kurz greifenden Datenbankregeln — Verification Report
**Task Goal:** Close T-JTS-02 (`GroupMembership` checked only the group side),
T-JTS-03 (`ModuleGrant` checked its own tenant but not the group/user it
points at) and WINDOWS #19 (nullable `tenantId` would hide platform-wide rows
from every tenant after cutover) — while leaving the written record coherent.
**Verified:** 2026-09-10, independently, against the live database container
`tessera-ctl-db-1` (address `172.19.0.2`, resolved fresh via `docker inspect`)
and the working tree at commits `f4f3115`, `6b23735`, `03fb3bf` (base `93444aa`).
**Status:** passed
## Summary
Every priority item in the verification brief was independently re-checked,
including three destructive falsification experiments (temporarily reverting
each of the three fixed policies in the migration file and re-running the
scratch tool against a throwaway database) to prove the inverted checks
would actually fail if the fix regressed. All three did. Full test suite
(839/839), type-check (clean), and the scratch tool (74/74) were re-run
independently and match the SUMMARY's claims exactly. No discrepancy found.
## Goal Achievement
### Observable Truths
| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | `GroupMembership` policy checks both sides (group AND user) | ✓ VERIFIED | Live `pg_policies`: `("groupId" IN (...Group...)) AND ("userId" IN (...User...))`. Falsification: reverted to group-only text in migration file, reran scratch tool → `groupmembership-schreiben-fremder-benutzer-abgelehnt` FAILED as expected, then restored (file byte-identical after restore) |
| 2 | `ModuleGrant` policy checks both possible targets (group OR user) with null-allowance, both D-04 branches measured | ✓ VERIFIED | Live `pg_policies` shows `tenantId = ... AND (groupId IS NULL OR ...) AND (userId IS NULL OR ...)`. Falsification: reverted to tenant-only text → both `modulegrant-fremde-gruppe-abgelehnt` and `modulegrant-fremder-benutzer-abgelehnt` FAILED as expected, then restored |
| 3 | `assertTargetBelongsToTenant` in `module-grants.service.ts` unchanged, held by a test that goes red if removed | ✓ VERIFIED | 3 call sites present (`grep -n`); two dedicated spec cases in `module-grants.service.spec.ts:290-312` with an explicit comment (lines 281-289) stating why they'd go red if the app-level guard were dropped |
| 4 | `TenderRssFeedSource` read/write split: bound read returns own + platform rows; write (insert/update/delete) still requires a tenant; both error directions (too strict / too loose) separately measured | ✓ VERIFIED | Live `pg_policies`: 4 command-separated policies; only `tenant_platform_read_policy` (SELECT) contains `IS NULL`, none of insert/update/delete do. Scratch tool: `tenderrssfeed-plattformzeile-gebunden-sichtbar` + `tenderrssfeed-eigene-zeile-gebunden-weiterhin-sichtbar` (too-strict direction) and `tenderrssfeed-gebundenes-aendern-...-abgelehnt` + `...-loeschen-...-abgelehnt` (too-loose direction), all passed independently |
| 5 | The three hole-claiming scratch-tool checks are INVERTED, not deleted/loosened, named for the new truth, referencing the old finding | ✓ VERIFIED | Old check names (`...-nicht-verhindert`, `...-trotz-eigener-mandantenkennung-erlaubt`, `...-unter-jedem-mandanten-unsichtbar`) appear only as backward-references inside the new checks' message text, not as separate passing assertions (`grep` over the whole tool file). New names assert rejection/visibility, matching the fix |
| 6 | `grant-foreign-group` row (dependency for two `module-registry` checks) still gets created, now via the maintenance role, and neither dependent check passes for the wrong reason | ✓ VERIFIED | `runGroupsAreaChecks` creates it via `withAdminPrisma`/BYPASSRLS (line ~913-917) before `runModuleRegistryAreaChecks` runs (both share the same scratch DB in `main()`); `gruppenpfad-gebunden-schliesst-die-fremde-gruppe-aus` message text now correctly says the row was excluded because the maintenance-role insert bypassed a rule that would otherwise reject it — not because the rule "lets it through" |
| 7 | The claim "no application code needs to change" was measured, not assumed; the one path that does (`listForUser`) is bound | ✓ VERIFIED | `listForUser` uses `forTenant(this.prisma, tenantId)`; controller passes `tenantId` from session context (`tenders.controller.ts:267-268`). Falsification: reverted the binding, ran `tender-rss-feed.service.spec.ts` → exactly 1 test failed (`listForUser() bindet...`), 30 passed, matching SUMMARY's claim precisely; restored |
| 8 | Every record describing one of the three old rules now tells the truth, naming the new migration | ✓ VERIFIED | All 4 in-source header comments (`module-access.service.ts`, `groups.service.ts`, `module-grants.service.ts`, `tender-rss-feed.service.ts` x3) and `rls-coverage.spec.ts` description line updated and name `20260910120000_rls_widen_membership_grant_and_platform_read`; classification doc `tenders` row (35/27) and sum row (107/135) independently re-derived from source via the same grep the doc cites and matched exactly; `rls-access-inventory.spec.ts` (10/10) passes |
| 9 | WINDOWS #19 closed with evidence naming the migration and both error-direction checks; #18/#20/#21/#23 remain open; what's NOT solved recorded as its own open entry that doesn't vanish with #19 | ✓ VERIFIED | #19 `status: fixed`, `resolved_at` set, description names the migration and 5 named checks, explicitly frames SearchProvider half as REFUTED PREMISE not solved problem. #18/#20/#21/#23 byte-identical to base commit. New entry #24 (open, `deviation`) names both blocked paths (`createPlatform`/`remove`) and states explicitly this predates 260910-jab. Header counts (6 open/17 fixed/1 waived/24 total) match a fresh count of the JSON-equivalent table rows |
| 10 | Switch stays OFF; `DATABASE_URL` still points at role `tessera` | ✓ VERIFIED | `docker-compose.yml:33` and `.env.example:2` both show role `tessera` (no `_app` suffix), unchanged from base |
| 11 | Baseline held: test count doesn't drop, type-check clean, scratch tool all-passed, at every task boundary | ✓ VERIFIED | Independently re-ran: 839/839 tests (56 files), `tsc --noEmit` clean, `rls-scratch-check.mjs` 74/74 passed — all match SUMMARY's claimed end-state exactly |
**Score:** 11/11 truths verified (0 present-but-behavior-unverified)
### Falsification Experiments (independently performed by this verifier)
Three destructive experiments were run directly against the scratch tool /
migration file, each reverted afterward and confirmed byte-identical to the
original:
1. Reverted `GroupMembership` policy text to group-only → `groupmembership-schreiben-fremder-benutzer-abgelehnt` FAILED (1/74). Restored.
2. Reverted `ModuleGrant` policy text to tenant-only → `modulegrant-fremde-gruppe-abgelehnt` and `modulegrant-fremder-benutzer-abgelehnt` both FAILED (2/74). Restored.
3. Reverted `TenderRssFeedSource` to the old single-policy form → the tenders-area extraction guard correctly aborted with `tenders-policies-aus-migration-gefunden: FEHLGESCHLAGEN` (fail-closed, not a silent wrong measurement) rather than measuring the old text as if it were current. Restored.
4. Reverted `TenderRssFeedSourceService.listForUser`'s `forTenant()` binding → exactly 1 test failed (`listForUser() bindet — forTenant() wird mit der uebergebenen Mandantenkennung aufgerufen`), 30 passed. Restored.
All four confirm the guarding assertions (tests and scratch checks) genuinely
detect the regression they claim to detect — not passing by construction.
### Required Artifacts
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `apps/api/prisma/migrations/20260910120000_rls_widen_membership_grant_and_platform_read/migration.sql` | New migration, applied locally | ✓ VERIFIED | Exists, `prisma migrate status` reports up to date, live `pg_policies` text matches file text exactly (diffed by hand for GroupMembership/ModuleGrant/TenderRssFeedSource/SearchProvider) |
| `apps/api/scripts/rls-scratch-check.mjs` | 3 inverted checks + gegenmessungen + 4 command directions + extraction redirect | ✓ VERIFIED | `readRlsWidenMigrationSql()`/`extractAllPolicySql()` wired into `runGroupsAreaChecks`/`runTendersAreaChecks`; old extraction (`readGroupsRlsPoliciesMigrationSql`) no longer used for GroupMembership/ModuleGrant |
| `apps/api/src/groups/migration-sql.spec.ts` | New describe block, text-only | ✓ VERIFIED | `rls_widen_membership_grant_and_platform_read migration.sql` block present, 6 tests, all pure regex/string assertions, no DB |
| `apps/api/src/tenders/tender-rss-feed.service.ts` | `listForUser` bound, 3 header comments corrected | ✓ VERIFIED | Confirmed by reading; `createPlatform`/`remove` deliberately still unbound with corrected reasoning pointing at WINDOWS #24 |
| `apps/api/src/tenders/tenders.controller.ts` + both spec files | tenant pass-through, two-client proof | ✓ VERIFIED | `extractTriageContext(req)` supplies `tenantId`; two-client proof in spec uses `__makeBoundClient`/`boundCallLog`, not an identity mock (unbound fake doesn't log, bound one does) |
| `apps/api/src/groups/groups.service.ts`, `module-grants.service.ts`, `module-access.service.ts`, `rls-coverage.spec.ts` | 4 in-source records corrected | ✓ VERIFIED | Read all 4 — each names the new migration and describes the new rule while explicitly retaining the app-level guard as "second net" |
| `docs/mandantentrennung-zugriffsklassifikation.md` | #19 block answered, 4 inventory rows updated, overview/sum rows re-derived | ✓ VERIFIED | tenders row 35/27 matches independently re-run grep exactly; sum row 107/135 matches; `rls-access-inventory.spec.ts` passes (machine-checked binding to this doc) |
| `docs/mandantentrennung-etappe2-fehlerrichtung.md` | New `## Regelschluss...` section, 5 subsections (r1-r5), annotations at superseded spots | ✓ VERIFIED | All 5 subsections present; r1 quotes an actual 74-check run (verified to match reality); r2 signal table covers both error directions per rule; old measurement blocks preserved verbatim with adjacent `NACHTRAG (260910-jab)` annotations (6+ locations found, not rewritten) |
| `docs/mandantentrennung-datenbankrolle.md` | The one #19-as-open spot updated | ✓ VERIFIED | Line 91-94 now says GESCHLOSSEN with migration name |
| `.planning/WINDOWS.md` | #19 fixed, #24 new, counters derived | ✓ VERIFIED | Header counts (6/1/17/24) match row-by-row count; #18/#20/#21/#23 byte-identical to base |
### Key Link Verification
| From | To | Via | Status | Details |
|------|-----|-----|--------|---------|
| `rls-scratch-check.mjs` extraction | `20260910120000_...` migration file | `readRlsWidenMigrationSql()` + `extractAllPolicySql()` | ✓ WIRED | Confirmed by falsification #3 above: reverting the migration file's TenderRssFeedSource text made the tool's own extraction guard fire, proving it reads the live file, not a cached/hardcoded value |
| Groups-area `grant-foreign-group` provisioning | `module-registry`-area dependent checks | shared scratch DB across `main()`'s sequential area-check calls | ✓ WIRED | Confirmed both dependent checks (`gruppenpfad-gebunden-schliesst-die-fremde-gruppe-aus`, `gruppenpfad-ueber-die-wartungsrolle-liefert-die-fremde-gruppe-mit`) pass in the live run and read code confirming ordering |
| `TendersController.listRssFeeds` | `TenderRssFeedSourceService.listForUser` | `extractTriageContext(req).tenantId` passthrough | ✓ WIRED | Read at `tenders.controller.ts:266-268` |
| `TenderRssFeedSource` command-separated policies | live database | migration applied via `prisma migrate deploy`, not `migrate dev`/`reset` | ✓ WIRED | `prisma migrate status` → up to date; `pg_policies` text matches file text |
### Behavioral Spot-Checks / Probe Execution
| Behavior | Command | Result | Status |
|----------|---------|--------|--------|
| Scratch tool passes fully | `node apps/api/scripts/rls-scratch-check.mjs` against live container | `Alle 74 Pruefungen bestanden.` | ✓ PASS |
| Full test suite | `npm --prefix apps/api run test` | 839/839, 56 files | ✓ PASS |
| Type check | `npm --prefix apps/api run type-check` | clean, no output | ✓ PASS |
| `rls-access-inventory.spec.ts` (machine clamp doc↔code) | `npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts` | 10/10 | ✓ PASS |
| Falsification: GroupMembership reverted | scratch tool against reverted migration text | 1/74 failed (correct check) | ✓ PASS (as regression detector) |
| Falsification: ModuleGrant reverted | scratch tool against reverted migration text | 2/74 failed (correct checks) | ✓ PASS (as regression detector) |
| Falsification: TenderRssFeedSource reverted | scratch tool against reverted migration text | extraction guard fired, 1/62 failed | ✓ PASS (fail-closed, not silently wrong) |
| Falsification: listForUser binding reverted | `npm --prefix apps/api run test -- src/tenders/tender-rss-feed.service.spec.ts` | 1/31 failed (correct test) | ✓ PASS (as regression detector) |
### Requirements Coverage
| Requirement | Source Plan | Description | Status | Evidence |
|-------------|-------------|-------------|--------|----------|
| WINDOWS-19 | 260910-jab-PLAN.md | Platform-wide rows visible to every tenant, not just their own, after cutover | ✓ SATISFIED | 4-policy split live, both error directions measured and independently falsified |
| T-JTS-02 | 260910-jab-PLAN.md | `GroupMembership` policy checks user side too | ✓ SATISFIED | Live policy text confirmed, falsified |
| T-JTS-03 | 260910-jab-PLAN.md | `ModuleGrant` policy checks referenced group/user, not just own tenant | ✓ SATISFIED | Live policy text confirmed, falsified, both D-04 branches measured |
### Anti-Patterns Found
None. Searched all 16 files in `files_modified` for `TBD|FIXME|XXX|TODO|HACK|PLACEHOLDER` — zero hits. No stray stub/empty-return patterns found in the reviewed code.
### Scope Discipline
- `apps/api/prisma/schema.prisma` — untouched (`git diff --stat` empty against base)
- `docker-compose*.yml`, `.env.example`, `.env.prod.example` — untouched
- `DATABASE_URL` still role `tessera` (no `_app` suffix) — switch remains OFF
- `assertTargetBelongsToTenant` present with 3 call/definition sites, unchanged behavior
- WINDOWS #21, #23 — byte-identical to base commit (not touched by this task)
- Full diff scope (16 files changed) matches the plan's declared `files_modified` list exactly, plus `module-grants.service.spec.ts` (listed in SUMMARY key-files, consistent with Aufgabe 2's test-first requirement)
### Human Verification Required
None. All must-haves are verifiable via the live database, the scratch tool,
and static analysis, and were independently re-derived rather than accepted
from the SUMMARY.
### Gaps Summary
No gaps found. This is an unusually well-evidenced quick task: every claim in
the SUMMARY that could be independently re-measured was re-measured (not
re-read), including four destructive falsification experiments this verifier
ran fresh (beyond the two the executor already ran), and every number
matched exactly (74/74, 839/839, 35/27, 107/135, 6/17/1/24). The framing of
the SearchProvider half of WINDOWS #19 as a refuted premise (rather than a
solved problem) is accurate and consistent across the migration header, the
ledger entry, and the classification document.
---
_Verified: 2026-09-10T14:55:00Z_
_Verifier: Claude (gsd-verifier)_