diff --git a/.planning/quick/260910-jab-mandantentrennung-die-drei-zu-kurz-greif/260910-jab-PLAN.md b/.planning/quick/260910-jab-mandantentrennung-die-drei-zu-kurz-greif/260910-jab-PLAN.md new file mode 100644 index 0000000..3cb2e59 --- /dev/null +++ b/.planning/quick/260910-jab-mandantentrennung-die-drei-zu-kurz-greif/260910-jab-PLAN.md @@ -0,0 +1,728 @@ +--- +phase: quick-260910-jab +plan: 01 +type: execute +wave: 1 +depends_on: [] +autonomous: true +requirements: [WINDOWS-19, T-JTS-02, T-JTS-03] + +files_modified: + - 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/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/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 + +estimate: + tokens: 210000 + raw_tokens: 210000 + tasks: 3 + confidence: low + +must_haves: + truths: + - "Die Regel auf `GroupMembership` prueft nach diesem Durchlauf BEIDE Seiten der Beziehung: die Gruppenseite wie bisher UND die Benutzerseite ueber denselben Join-Praezedenzfall, den `PasswordResetToken` seit 20260618112133 vormacht. Eine Mitgliedschaft, die eine Gruppe des einen Mandanten mit einem Benutzer eines anderen verbindet, wird von der Datenbank abgewiesen — gemessen mit einem Benutzer, den es im anderen Mandanten TATSAECHLICH gibt, nicht mit einer erfundenen Kennung." + - "Die Regel auf `ModuleGrant` prueft nach diesem Durchlauf nicht mehr nur die Mandantenkennung der Zeile, sondern auch, wohin die Zeile zeigt: eine Freigabe mit korrekter eigener Mandantenkennung, aber fremder Gruppen- ODER fremder Benutzerkennung, wird abgewiesen. Beide Zweige des Entweder-oder (D-04) sind einzeln gemessen." + - "`assertTargetBelongsToTenant` in `module-grants.service.ts` steht am Ende dieses Durchlaufs unveraendert und wird von einem Test gehalten, der rot wird, wenn jemand sie als 'macht jetzt die Datenbank' entfernt. Die Datenbankregel ist ein ZWEITES Netz, kein Ersatz." + - "Fuer `TenderRssFeedSource` ist der Lesezugriff vom Schreibzugriff getrennt: ein gebundener Lesezugriff liefert die eigenen UND die plattformweiten Zeilen, waehrend Einfuegen, Aendern und Loeschen weiterhin einen Mandanten verlangen. BEIDE Fehlerrichtungen sind einzeln gemessen — zu streng (plattformweite Zeile bleibt unsichtbar) und zu locker (ein Mandant koennte eine plattformweite Zeile aendern oder entfernen)." + - "Die drei Pruefungen des Wegwerf-Werkzeugs, die die Loecher bisher als erwartetes Verhalten festhielten, sind UMGEKEHRT — nicht geloescht und nicht gelockert. Jede traegt einen Namen, der die neue Wahrheit sagt, und in ihrem Meldetext einen Verweis auf den alten Befund und den alten Pruefungsnamen, damit der Nachweis, dass das Loch existierte, nicht verloren geht." + - "Die Zeile `grant-foreign-group`, die bisher als Nebenwirkung einer der umgekehrten Pruefungen entstand und auf der ZWEI spaetere Pruefungen des Bereichs `module-registry` aufsetzen, wird weiterhin angelegt — jetzt ueber die Wartungsrolle. Keine spaetere Pruefung besteht dadurch aus dem falschen Grund (weil es nichts zu finden gaebe)." + - "Die Behauptung 'kein Anwendungscode muss sich aendern' ist geprueft statt geglaubt worden, und ihr gemessenes Ergebnis steht im Bericht: sie trifft fuer GENAU EINEN Pfad nicht zu. `TenderRssFeedSourceService.listForUser` liefert nach der Reparatur ungebunden nur noch die plattformweiten Zeilen statt gar keiner — aus einer schreienden Leere wuerde eine glaubhafte Teilantwort. Dieser Pfad ist deshalb gebunden." + - "Jede Aufzeichnung, die eine der drei alten Regeln beschreibt, sagt am Ende die Wahrheit: die vier Kopfkommentare im Quelltext, die Beschreibungszeile im Abdeckungstest, die betroffenen Zeilen der Klassifikation samt Uebersichts- und Summenzeile, und die vier ueberholten Stellen der Kritikschrift — jede mit dem Namen der neuen Migration. Eine Aufzeichnung, die ein geschlossenes Loch beschreibt, ohne zu sagen, dass es geschlossen wurde, ist die Luege durch Auslassung, die dieser Durchlauf zu vermeiden hat." + - "WINDOWS #19 ist mit Beleg geschlossen (Migrationsname plus die namentlich benannten Pruefungen). #18, #20, #21 und #23 bleiben offen. Was die Reparatur NICHT loest — plattformweite Zeilen lassen sich unter der Anwendungsrolle weder anlegen noch entfernen, in der alten wie in der neuen Regel — ist als eigener offener Eintrag aufgezeichnet und verschwindet nicht mit #19." + - "Der Schalter bleibt AUS: `DATABASE_URL` zeigt weiterhin auf die Rolle `tessera`. Die Regeln sind heute wirkungslos — genau das macht sie jetzt sicher aenderbar und bedeutet zugleich, dass die laufende Anwendung sie nicht bestaetigen kann. Das Wegwerf-Werkzeug und die Regelliste der lebenden Datenbank sind die einzigen Zeugen." + - "Baseline gehalten: die Testzahl faellt nicht unter den zu Beginn JEDER Aufgabe frisch gemessenen Stand, die Typpruefung ist sauber, das Wegwerf-Werkzeug meldet alle Pruefungen bestanden — am Ende JEDER Aufgabe, nicht nur am Ende des Plans. 'Gruen' bedeutet nach dieser Aufgabe etwas anderes als vorher, und genau das ist der Zweck." + artifacts: + - "apps/api/prisma/migrations/20260910120000_rls_widen_membership_grant_and_platform_read/migration.sql — NEU, handgeschrieben nach dem Muster von 20260909140000: die beiden zu kurz greifenden Regeln ersetzt, die vier befehlsgetrennten Regeln fuer die plattformweiten Zeilen angelegt, `SearchProvider` mit gemessener Begruendung ausdruecklich NICHT angefasst" + - "apps/api/scripts/rls-scratch-check.mjs — drei umgekehrte Pruefungen, die Wartungsrollen-Gegenmessungen dazu, die vier Befehlsrichtungen des Lese-/Schreibsplits, die Bereitstellung von `grant-foreign-group` ueber die Wartungsrolle, und die Extraktion, die die abgeloesten Regeln aus der NEUEN Migration liest" + - "apps/api/src/groups/migration-sql.spec.ts — ein neuer Beschreibungsblock fuer die neue Migration, im Stil der beiden bestehenden Bloecke: reiner Textabgleich ohne Datenbank" + - "apps/api/src/tenders/tender-rss-feed.service.ts — `listForUser` gebunden, alle drei Kopfkommentare an der neuen Regel richtiggestellt" + - "apps/api/src/tenders/tenders.controller.ts + beide Testdateien des Bereichs — die Durchreichung des Mandanten und der Zwei-Klienten-Nachweis" + - "apps/api/src/groups/groups.service.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 — die vier Aufzeichnungen im Quelltext, die die alten Regeln beschreiben" + - "docs/mandantentrennung-zugriffsklassifikation.md — der #19-Block beantwortet statt offen, die vier betroffenen Bestandsaufnahme-Zeilen, die Uebersichtszeile `tenders`, die Summenzeile und der Punkt in 'Was diese Etappe NICHT entscheidet'" + - "docs/mandantentrennung-etappe2-fehlerrichtung.md — ein neuer Abschnitt `## Regelschluss T-JTS-02, T-JTS-03 und WINDOWS #19` mit den fuenf ueblichen Unterabschnitten, plus Nachtraege an den vier ueberholten Bestandsstellen" + - "docs/mandantentrennung-datenbankrolle.md — die eine Stelle, die #19 als offen fuehrt" + - ".planning/WINDOWS.md — #19 geschlossen mit Beleg, ein neuer offener Eintrag fuer den Schreibweg plattformweiter Zeilen, Zaehler aus dem JSON-Block abgeleitet statt getippt" + key_links: + - "Das Wegwerf-Werkzeug schneidet die Regeln WORTGLEICH aus den ausgelieferten Migrationsdateien. Sobald eine Regel abgeloest wird, misst die Extraktion die falsche Datei weiter — die Umleitung auf die neue Migration ist deshalb keine Kosmetik, sondern die Bedingung dafuer, dass die umgekehrten Pruefungen ueberhaupt das messen, was sie behaupten." + - "`grant-foreign-group` entstand bisher als Nebenwirkung der Pruefung, die T-JTS-03 festhielt. Zwei Pruefungen des Bereichs `module-registry` setzen darauf auf: eine schliesst die Zeile aus, die andere weist ueber die Wartungsrolle nach, dass der Ausschluss von der Bindung kommt. Faellt die Zeile weg, besteht die erste aus dem falschen Grund und die zweite scheitert." + - "Ein `USING`-Ausdruck allein bestimmt auch, welche Zeilen ein UPDATE oder DELETE ueberhaupt erreicht. Ein einziger permissiver Ausdruck, der die plattformweiten Zeilen fuer das Lesen einschliesst, gaebe damit jedem Mandanten das Recht, sie zu aendern und zu entfernen. Die Trennung nach Befehl ist deshalb die Sache selbst, nicht eine Stilfrage." + - "Nach der Reparatur kehrt sich die Fehlerrichtung fuer `listForUser` um: heute liefert der ungebundene Pfad nach dem Scharfschalten NICHTS, danach die plattformweiten Zeilen. Eine leere Liste faellt auf, eine kurze Liste nicht — die Reparatur erzeugt genau die stille Falschantwort, gegen die dieses ganze Vorhaben laeuft, wenn dieser eine Pfad nicht mitgebunden wird." + - "Die Praemisse von #19 stimmt nur zur Haelfte: fuer `TenderRssFeedSource` gibt es plattformweite Zeilen und sie werden benutzt (D-06, der geseedete Feed), fuer `SearchProvider` gibt es keinen Codeweg, der eine mandantenlose Zeile erzeugt — die Vorgaben sind Konstanten (05-02). Die ausgelieferte Migration und die Klassifikation sagen das bereits; der Ledger-Eintrag sagt es nicht. Die Schliessung muss diese Haelfte als widerlegte Praemisse schliessen, nicht als geloestes Problem." +--- + + +Die drei Datenbankregeln schliessen, die kuerzer greifen als sie sollen: +`GroupMembership` prueft nur die Gruppen-, nicht die Benutzerseite (T-JTS-02); +`ModuleGrant` prueft die Zeile, aber nicht, wohin sie zeigt (T-JTS-03); und die +Regel fuer die Tabellen mit nullbarer Mandantenkennung wuerde die +plattformweiten Zeilen nach dem Scharfschalten fuer JEDEN Mandanten unsichtbar +machen statt nur fuer fremde (WINDOWS #19). + +Zweck: alle drei sind heute wirkungslos, weil die Anwendung als Rolle mit +Umgehungsrecht verbindet (#18). Genau das macht sie jetzt gefahrlos aenderbar — +und bedeutet zugleich, dass die laufende Anwendung die Reparatur nicht +bestaetigen kann. Das Wegwerf-Werkzeug und die Regelliste der lebenden +Datenbank sind die einzigen Zeugen, die es gibt. + +Dieser Durchlauf weicht bewusst von der geplanten Reihenfolge ab, auf +ausdrueckliche Anweisung des Nutzers. Die Folge ist einzukalkulieren, nicht zu +verschweigen: die fuenf noch offenen Bereiche der Etappe 2 messen ab jetzt gegen +die NEUEN Regeln, und mehrere bereits abgeschlossene Bereiche haben gegen die +alten gemessen. Diese Messungen stehen aufgezeichnet. Sie muessen am Ende +stimmen. + +Ergebnis: eine handgeschriebene, lokal angewandte Migration; ein Messwerkzeug, +dessen drei loch-behauptende Pruefungen umgekehrt statt entfernt sind; genau ein +mitgebundener Anwendungspfad, weil die Reparatur ihn sonst still falsch machen +wuerde; und ein Aktenstand, der nirgends mehr ein Loch beschreibt, das es nicht +mehr gibt. + +**Der Schalter bleibt AUS.** `DATABASE_URL` zeigt weiterhin auf die Rolle +`tessera`. Das Scharfschalten ist Etappe 4 und findet hier NICHT statt. + + + +@~/.claude/gsd-core/workflows/execute-plan.md +@~/.claude/gsd-core/templates/summary.md + + + +@.planning/STATE.md +@.planning/WINDOWS.md +@docs/mandantentrennung-zugriffsklassifikation.md +@docs/mandantentrennung-etappe2-fehlerrichtung.md +@docs/mandantentrennung-datenbankrolle.md +@apps/api/prisma/migrations/20260618112133_rls_policies/migration.sql +@apps/api/prisma/migrations/20260804130918_groups_rls_policies/migration.sql +@apps/api/prisma/migrations/20260909140000_rls_remaining_tenant_tables/migration.sql +@apps/api/scripts/rls-scratch-check.mjs +@apps/api/src/groups/migration-sql.spec.ts +@apps/api/src/prisma/rls-coverage.spec.ts +@apps/api/src/groups/module-grants.service.ts +@apps/api/src/tenders/tender-rss-feed.service.ts +@.planning/quick/260910-exd-mandantentrennung-etappe-2-bereich-modul/260910-exd-PLAN.md + + + + +Alle Zahlen und Fundstellen unten sind zur Planungszeit am 2026-09-10 gegen HEAD +`4843058` gemessen, mit der jeweils genannten Anweisung. Sie leiten die +Untersuchung, sie sind KEINE Bearbeitungsvollmacht — jede Datei wird vor jeder +Aenderung erneut gelesen, und jede Zeilenangabe erneut aufgesucht statt +abgeschrieben. + +**Befund A — die drei Regeln, im ausgelieferten Text nachgelesen.** +`20260804130918_groups_rls_policies/migration.sql` fuehrt drei Regeln unter dem +Namen `tenant_isolation_policy`: `Group` vergleicht direkt gegen +`current_tenant_id()`; `GroupMembership` prueft ausschliesslich, ob `groupId` in +den Gruppen des Mandanten liegt; `ModuleGrant` vergleicht ausschliesslich die +eigene `tenantId`. `20260909140000_rls_remaining_tenant_tables/migration.sql` +legt sechzehn weitere Regeln an, darunter `SearchProvider` und +`TenderRssFeedSource`, beide in der einfachen Gleichheitsform. Ihr Kopf sagt +ausdruecklich, dass keine getrennte Schreibbedingung angelegt wurde, weil +PostgreSQL dann denselben Ausdruck auch fuer neu geschriebene Zeilen verwendet — +genau diese Vereinfachung ist es, die fuer die nullbaren Spalten nicht traegt. + +**Befund B — es sind DREI loch-behauptende Pruefungen im Werkzeug, nicht zwei.** +Die Aufgabenstellung nennt zwei und sagt ausdruecklich, dass der Fund einer +Planungszeit-Suche keine Vollstaendigkeitsgarantie ist. Die Suche ist gemacht +worden. Die dritte ist `tenderrssfeed-plattformzeile-unter-jedem-mandanten-unsichtbar` +in `runTendersAreaChecks` (`rls-scratch-check.mjs`, im Bereich der Zeilen +1041-1061 zu suchen, nicht abzuschreiben): sie besteht, WEIL die plattformweite +Zeile unter beiden Mandantenkontexten fehlt, und haelt in ihrem Meldetext +zusaetzlich die Handlungsanweisung fest, drei RSS-Pfade deshalb nicht zu binden. +Sie faellt mit der Reparatur und ist wie die beiden anderen umzukehren. +Gemessen mit `grep -n "GELUNGEN\|nicht-verhindert\|erlaubt\|unsichtbar"` ueber +die Werkzeugdatei; ausserdem wurde die Datei nach weiteren Pruefungen +durchgesehen, deren bestandenes Ergebnis das ALTE Verhalten ist — es gibt keine +vierte. + +**Befund C — eine der beiden benannten Pruefungen LIEFERT eine Zeile, auf der +zwei spaetere Pruefungen aufsetzen. Das ist die gefaehrlichste Einzelheit +dieses Durchlaufs.** Die Pruefung, die T-JTS-03 festhaelt, fuegt dabei die Zeile +`grant-foreign-group` ein (Mandant TENANT-A, Gruppe `group-b`). Zwei Pruefungen +des Bereichs `module-registry` bauen darauf auf: eine weist nach, dass der +gebundene Drei-Tabellen-Weg diese Zeile ausschliesst, die andere weist ueber die +Wartungsrolle nach, dass der Ausschluss von der Bindung kommt und nicht vom +Aufbau. Nach der Reparatur wird das Einfuegen abgewiesen, die Zeile entsteht +nicht mehr — die erste Pruefung bestuende dann aus dem falschen Grund (es gibt +nichts auszuschliessen) und die zweite SCHEITERT. Gemessen mit +`grep -n "grant-foreign-group" apps/api/scripts/rls-scratch-check.mjs`: fuenf +Fundstellen, eine Einfuegestelle und vier Verwendungen. Die Gegenprobe fuer die +zweite loch-behauptende Zeile faellt anders aus: `membership-foreign-user` hat +ausser ihrer Einfuegestelle keine Verwendung. + +**Befund D — die Extraktion liest die Regel aus der Migrationsdatei, und sie +kennt nur einen Namen.** `extractPolicySql()` sucht woertlich nach +`CREATE POLICY tenant_isolation_policy ON ""` und liefert GENAU EINEN +Treffer. Daraus folgen zwei Dinge, die sonst still schiefgehen: erstens muss die +Extraktion fuer die abgeloesten Regeln auf die NEUE Migrationsdatei zeigen, +sonst misst das Werkzeug weiter die abgeloeste Regel und die umgekehrten +Pruefungen scheitern aus einem verwirrenden Grund; zweitens braucht die Tabelle +mit vier befehlsgetrennten Regeln eine Extraktion, die mehr als einen Treffer +liefern kann. + +**Befund E — die Praemisse von #19 stimmt nur zur Haelfte.** Fuer +`TenderRssFeedSource` gibt es plattformweite Zeilen, sie sind gewollt (D-06) und +sie sind da: in der lokalen Datenbank zwei Zeilen insgesamt, davon eine mit +leerem Benutzer UND leerem Mandanten. Fuer `SearchProvider` gilt das nicht. +Gemessen: `dashboard.service.ts` ist der einzige Ort, der die Tabelle beruehrt +(vier Zugriffe), und der einzige Schreibweg dorthin nimmt die Mandantenkennung +als Pflichtparameter entgegen und setzt sie. Die Vorgabe-Suchmaschinen kommen +aus der Konstanten `DEFAULT_SEARCH_PROVIDERS` und werden erst nach dem Lesen +davorgehaengt — sie sind keine Datenbankzeilen (Entscheidung 05-02). Lokal +gemessen: null Zeilen insgesamt, null mit leerer Mandantenkennung. Sowohl der +Kopfkommentar der ausgelieferten Migration als auch die Klassifikation sagen das +bereits; allein der Ledger-Eintrag #19 behauptet "von der Administration +gepflegte Suchanbieter". Folge fuer diesen Plan: `SearchProvider` behaelt seine +strenge Regel, und die Schliessung von #19 schliesst diese Haelfte als +WIDERLEGTE PRAEMISSE, nicht als geloestes Problem. Eine Lockerung waere hier die +falsche Richtung: sie wuerde eine kuenftige mandantenlose Zeile jedem Mandanten +zeigen. + +**Befund F — die Behauptung "kein Anwendungscode muss sich aendern" trifft fuer +GENAU EINEN Pfad nicht zu.** `TenderRssFeedSourceService.listForUser` ist heute +bewusst ungebunden, mit einem Kopfkommentar, der als Begruendung die alte Regel +nennt. Nach der Reparatur kehrt sich die Lage um. Heute liefert dieser Pfad +ungebunden nach dem Scharfschalten NICHTS (die Gleichheitsbedingung vergleicht +den leeren Kontext mit nichts). Danach liefert er die plattformweiten Zeilen — +und nur die. Aus einer leeren Liste, die schreit, wird eine kurze Liste, die +luegt: der Nutzer saehe die plattformweiten Feeds und haette keinen Anlass zu +melden, dass seine eigenen fehlen. Gebunden liefert derselbe Pfad genau das +Richtige, eigene plus plattformweite. Die Reparatur ERZEUGT hier also die stille +Falschantwort, gegen die dieses Vorhaben laeuft, wenn dieser Pfad nicht +mitgebunden wird. Der aufrufende Endpunkt hat den Mandanten bereits zur Hand (er +reicht ihn eine Methode weiter beim Anlegen eines eigenen Feeds durch), die +Aenderung ist deshalb klein und ohne neue Aufloesung. + +**Befund G — zwei Pfade bleiben unter der Anwendungsrolle unmoeglich, in der +alten wie in der neuen Regel.** Das Anlegen einer plattformweiten Zeile setzt +die Mandantenkennung leer; die Schreibbedingung verlangt einen Mandanten — vor +wie nach der Reparatur abgewiesen. Das Entfernen einer plattformweiten Zeile +laeuft ungebunden und trifft nach dem Scharfschalten nichts. Beides ist keine +Folge dieses Plans und wird von ihm auch nicht geloest: die vom Ledger +vorgegebene Semantik lautet ausdruecklich, dass Schreibzugriffe weiterhin einen +Mandanten verlangen. Es ist damit ein Verwaltungsweg, der in Etappe 4 gebaut +werden muss, und es darf nicht mit #19 zusammen verschwinden. + +**Befund H — der Bestand ist sauber, lokal gemessen.** Ueber die lokale +Datenbank gezaehlt: keine Mitgliedschaft, deren Gruppe und Benutzer zu +verschiedenen Mandanten gehoeren; keine Freigabe, deren Gruppe oder Benutzer zu +einem anderen Mandanten gehoert als die Zeile selbst; drei Mitgliedschaften, +vier Freigaben, ein Mandant. Die schaerferen Regeln machen also lokal keine +vorhandene Zeile unsichtbar. Fuer das Testsystem ist das NICHT gemessen und darf +nicht angenommen werden — dieselbe Zaehlung gehoert als Vorabpruefung in Etappe 4, +denn eine Zeile, die die neue Regel nicht mehr sieht, waere danach weder +sichtbar noch loeschbar. + +**Befund I — zwei Buchhaltungszeilen bewegen sich.** Wird `listForUser` +gebunden, sinkt die ungebundene Rohtrefferzahl des Bereichs `tenders` um eins +und die gebundene steigt um eins; die Uebersichtszeile und die Summenzeile der +Klassifikation nennen beide Zahlen. Gemessen mit den beiden Anweisungen, die die +Klassifikation selbst als maszgeblich fuehrt: `tenders` steht heute auf 36 +ungebunden und 26 gebunden, die Summenzeile auf 108 und 134. Diese Zahlen stehen +hier als ERWARTUNG, nicht als Vorgabewert — die Pruefung leitet sie zur Laufzeit +erneut aus dem Quelltext ab. + +**Befund J — welche Aufzeichnungen die Reparatur unwahr macht.** Vollstaendig +gesucht mit `grep -rn "T-JTS-02\|T-JTS-03"` und `grep -rn "#19"` ueber Quelltext +und Dokumente. Im Quelltext vier Stellen: der Kopfkommentar zur +Zugriffsaufloesung in `module-access.service.ts`, der Kommentar an der +Benutzerpruefung in `groups.service.ts`, der Kommentar an der +Mandanten-Gegenpruefung in `module-grants.service.ts` und die +Beschreibungszeile fuer `GroupMembership` in der Ausnahmeliste von +`rls-coverage.spec.ts`. Dazu die drei Kopfkommentare in +`tender-rss-feed.service.ts`. In der Klassifikation: der #19-Block, die +Bestandsaufnahme-Zeilen fuer `searchProvider`, `groups.service.ts`/`user`, +`module-grants.service.ts`/`moduleGrant` und +`tender-rss-feed.service.ts`/`tenderRssFeedSource`, die Uebersichtszeile +`tenders`, die Summenzeile und der Punkt in "Was diese Etappe NICHT +entscheidet", der die #19-Regel ausdruecklich als offen fuehrt. In der +Kritikschrift: der Punkt in `(g4)`, der beide Regeln als einseitig beschreibt; +der Punkt in `(t4)`, der #19 als nicht geloest fuehrt; die Zeile der +Signaltabelle des Bereichs `tenders` zu den drei RSS-Pfaden; die beiden +aufgezeichneten Werkzeugausgaben, die die alten Meldetexte woertlich zitieren; +und der Punkt im Abschnitt `module-registry`, der beide Befunde als offen +fuehrt. Dazu eine Stelle in der Betriebsanleitung zur Datenbankrolle. + +**Befund K — bei getrennter Lese- und Schreibbedingung entscheidet der Befehl, +nicht der Ausdruck.** Ein einziger permissiver Ausdruck, der die plattformweiten +Zeilen einschliesst, gilt in PostgreSQL auch fuer UPDATE und DELETE — ein +Mandant duerfte eine plattformweite Zeile dann aendern und entfernen. Deshalb +wird die Regel dieser Tabelle nach BEFEHL getrennt: eine Leseregel, die die +plattformweiten Zeilen einschliesst, und je eine Regel fuer Einfuegen, Aendern +und Entfernen, die einen Mandanten verlangen. Vier ausdrueckliche Regeln statt +einer mit stillschweigender Wirkung; beide Fehlerrichtungen werden gemessen, +nicht erschlossen. + +**Gemessene Ausgangslage (2026-09-10, HEAD `4843058`):** Arbeitsbaum sauber; +Datenbankcontainer `tessera-ctl-db-1` unter `172.19.0.2` erreichbar (Adresse bei +JEDEM Lauf neu ermitteln, nie abschreiben), Zugang `tessera:tessera_dev`, +Datenbank `tessera`. Die Aufgabenstellung nennt als Baseline 833 Tests in 56 +Dateien, eine saubere Typpruefung und 66 bestandene Pruefungen des +Wegwerf-Werkzeugs; diese drei Zahlen sind zu Beginn von Aufgabe 1 SELBST zu +messen und im Bericht mit dem gemessenen Wert zu nennen, nicht zu uebernehmen — +eine Zahl aus zweiter Hand ist in diesem Vorhaben schon viermal geschrumpft. + + + + + + + Aufgabe 1: Die drei Regeln schreiben, lokal anwenden und an der echten Datenbank messen + Der Container `tessera-ctl-db-1` laeuft und ist ueber seine zur Laufzeit ermittelte Container-Adresse mit `tessera:tessera_dev` erreichbar; der Arbeitsbaum ist sauber. + 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/migration-sql.spec.ts + +Diese Aufgabe ist der duenne, durchgehende Faden: sie beruehrt die +Migrationsdatei, die lebende Datenbank, das Messwerkzeug und den Textabgleich +und beweist damit von Ende zu Ende, dass die Regelaenderung traegt, bevor ein +einziger Anwendungspfad angefasst wird. Sie aendert keinen Anwendungscode. + +Die neuen und geaenderten Pruefungen des Wegwerf-Werkzeugs, jede mit dieser +Kennung und jede mit dem tatsaechlich beobachteten Wert im Meldetext: + +- `groupmembership-schreiben-fremder-benutzer-abgelehnt` — die Umkehr der ersten + loch-behauptenden Pruefung. Gemessen mit einem Benutzer, den es im anderen + Mandanten TATSAECHLICH gibt (`user-b`), nicht mit einer erfundenen Kennung: + nur so misst sie den Fall, den T-JTS-02 benannt hat, statt eines + Fremdschluessel-Nichts. Der Meldetext nennt den alten Pruefungsnamen und den + Befund, damit der Nachweis, dass das Loch existierte, erhalten bleibt. +- `groupmembership-fremder-benutzer-ueber-die-wartungsrolle-weiterhin-moeglich` — + die Gegenmessung: dasselbe Einfuegen ueber die Verwaltungsrolle mit + Umgehungsrecht GELINGT. Damit steht fest, dass die Abweisung von der Regel + kommt und nicht vom Aufbau. Praezedenzfall fuer diese Messform: + `gruppenpfad-ueber-die-wartungsrolle-liefert-die-fremde-gruppe-mit`. +- `modulegrant-fremde-gruppe-abgelehnt` — die Umkehr der zweiten + loch-behauptenden Pruefung, mit demselben Verweis-Erfordernis. +- `modulegrant-fremder-benutzer-abgelehnt` — NEU, der zweite Zweig des + Entweder-oder (D-04): eine Freigabe mit eigener Mandantenkennung und fremder + Benutzerkennung. T-JTS-03 hat nur den Gruppenzweig gemessen; die Regel muss + beide decken, sonst bleibt die Haelfte des Lochs offen. +- `modulegrant-fremde-gruppe-ueber-die-wartungsrolle-weiterhin-moeglich` — die + Gegenmessung dazu. Diese Pruefung legt ZUGLEICH die Zeile `grant-foreign-group` + an, die bisher als Nebenwirkung der loch-behauptenden Pruefung entstand und auf + der die beiden Pruefungen des Bereichs `module-registry` aufsetzen (Befund C). + Der Aufbau muss so sein, dass diese Zeile weiterhin vorhanden ist, wenn der + Bereich `module-registry` gemessen wird. +- `tenderrssfeed-plattformzeile-gebunden-sichtbar` — die Umkehr der dritten + loch-behauptenden Pruefung: unter BEIDEN Mandantenkontexten ist die + plattformweite Zeile jetzt sichtbar. Der Meldetext nennt den alten + Pruefungsnamen, WINDOWS #19 und die beobachteten Kennungen beider Ergebnisse. +- `tenderrssfeed-eigene-zeile-gebunden-weiterhin-sichtbar` — die andere + Fehlerrichtung derselben Aenderung: der gebundene Lesezugriff liefert + ZUSAETZLICH weiterhin die eigene Zeile des Mandanten und NICHT die des anderen. + Ohne diese Pruefung waere eine zu weit gefasste Leseregel unbemerkt. +- `tenderrssfeed-ungebunden-nur-die-plattformzeile` — die Belegzeile fuer Befund + F: ohne gesetzten Mandantenkontext liefert der Lesezugriff jetzt genau die + plattformweiten Zeilen und keine persoenliche. Das ist die neue Fehlerrichtung, + die Aufgabe 2 traegt; sie gehoert gemessen, nicht behauptet. +- `tenderrssfeed-gebundenes-einfuegen-ohne-mandant-abgelehnt` — BESTEHT BEREITS + und muss bestanden bleiben. Ihr Meldetext ist an die neue, jetzt ausdrueckliche + Schreibbedingung anzupassen. +- `tenderrssfeed-gebundenes-aendern-der-plattformzeile-abgelehnt` — NEU: ein + gebundenes UPDATE auf die plattformweite Zeile wird abgewiesen. +- `tenderrssfeed-gebundenes-loeschen-der-plattformzeile-abgelehnt` — NEU: ein + gebundenes DELETE auf die plattformweite Zeile wird abgewiesen. Diese beiden + sind die Messung der Richtung "zu locker" und damit der eigentliche Grund fuer + die Trennung nach Befehl (Befund K). +- `searchprovider-mandantenlose-zeile-bleibt-unter-jedem-kontext-unsichtbar` — + NEU, mit einer eigenen Wegwerf-Tabelle und der UNVERAENDERT ausgelieferten + Regel: eine mandantenlose Zeile bleibt hier bewusst unsichtbar. Der Meldetext + haelt die Begruendung aus Befund E fest — es gibt keinen Codeweg, der eine + solche Zeile erzeugt, die Vorgaben sind Konstanten — und benennt die + Bedingung, unter der diese Entscheidung neu zu bewerten waere. + +Die Extraktion der abgeloesten Regeln muss auf die NEUE Migrationsdatei zeigen +(Befund D). Findet sie eine benoetigte Regel nicht, meldet der Abschnitt eine +FEHLGESCHLAGENE Pruefung und bricht ab, statt mit einer geratenen Regel +weiterzumessen — die Form, die die bestehenden Abschnitte bereits vormachen. +Der Meldetext der Pruefung `gruppenpfad-gebunden-schliesst-die-fremde-gruppe-aus` +im Bereich `module-registry` behauptet heute, die Regel lasse die fremde Zeile +durch; das ist nach dieser Aufgabe unwahr und im selben Zug richtigzustellen. + + +Zuerst die Ausgangslage SELBST messen und die drei Zahlen notieren (Testlauf, +Typpruefung, Wegwerf-Werkzeug). Erst danach anfangen. + +Dann die drei ausgelieferten Migrationen und die vier Modelle im Schema erneut +lesen — die Angaben aus `` leiten nur die Suche. + +Eine EINZIGE neue Migration anlegen, Verzeichnisname mit dem Zeitstempel +`20260910120000` und dem Namensteil `rls_widen_membership_grant_and_platform_read`; +der Zeitstempel muss hinter dem juengsten vorhandenen liegen. Die bestehenden +Migrationsdateien bleiben UNVERAENDERT — Prisma fuehrt ihre Pruefsummen, eine +Aenderung braechte den Migrationslauf zum Abbruch; der Praezedenzfall dafuer und +die Form des erklaerenden Kopfes stehen im Kopf von 20260909140000. Der Kopf der +neuen Datei nennt: welche Regeln sie abloest und warum, dass die alten Dateien +deshalb stehen bleiben, dass der Schalter weiterhin aus ist und die Regeln damit +heute wirkungslos sind, sowie die Begruendung aus Befund E dafuer, dass +`SearchProvider` ausdruecklich NICHT angefasst wird. + +Inhalt der Migration, in dieser Reihenfolge: + +(1) `GroupMembership`: die bestehende Regel entfernen und unter demselben Namen +neu anlegen, mit einer zweiten Bedingung fuer die Benutzerseite nach dem +Join-Muster, das `PasswordResetToken` in 20260618112133 vormacht — die +Benutzerkennung muss unter den Benutzern des laufenden Mandanten liegen, ebenso +wie die Gruppenkennung unter dessen Gruppen. Beide Bedingungen mit UND +verknuepft. + +(2) `ModuleGrant`: die bestehende Regel entfernen und unter demselben Namen neu +anlegen. Die eigene Mandantenkennung wird wie bisher verglichen; zusaetzlich +muss die Gruppenkennung entweder leer sein oder unter den Gruppen des laufenden +Mandanten liegen, und die Benutzerkennung entweder leer sein oder unter dessen +Benutzern. Die Leer-Zulassung ist zwingend, weil das Modell Gruppe und Benutzer +als Entweder-oder fuehrt (D-04). + +(3) `TenderRssFeedSource`: die bestehende Regel entfernen und durch VIER nach +Befehl getrennte Regeln ersetzen (Befund K), mit den Namen +`tenant_platform_read_policy`, `tenant_insert_policy`, `tenant_update_policy` +und `tenant_delete_policy`. Die Leseregel gilt fuer SELECT und laesst zusaetzlich +zur Gleichheit mit dem laufenden Mandanten die Zeilen ohne Mandantenkennung zu. +Die drei Schreibregeln verlangen ausnahmslos Gleichheit mit dem laufenden +Mandanten — die Einfuegeregel als Schreibbedingung, die Aenderungsregel in beiden +Klauseln, die Loeschregel als Lesebedingung ihres Befehls. + +(4) `SearchProvider`: keine Anweisung, nur der erklaerende Absatz im Kopf. + +Danach die Migration LOKAL ANWENDEN — ohne diesen Schritt ist nichts gemessen, +und Bau wie Typpruefung liefen auch ohne ihn gruen. Die Container-Adresse frisch +ermitteln, nicht abschreiben. Anwenden ausschliesslich mit dem Befehl, der +ausstehende Migrationen anwendet, NIEMALS mit der Entwicklungsvariante und +niemals mit dem Ruecksetzbefehl — diese Datenbank traegt den lokalen Bestand. +Danach den Status abfragen und die Regelliste der lebenden Datenbank aus dem +Systemkatalog auslesen; diese Liste ist der Beleg, dass die Regel wirklich +angekommen ist. + +Zusaetzlich die Bestandszaehlung aus Befund H erneut ausfuehren (Mitgliedschaften +und Freigaben, die ueber Mandantengrenzen zeigen) und das Ergebnis fuer den +Bericht festhalten. Ist es nicht null, HALTEN und melden statt weitermachen — +solche Zeilen waeren nach dem Scharfschalten weder sichtbar noch loeschbar. + +Dann das Wegwerf-Werkzeug wie unter `` beschrieben umbauen. Die +Extraktion auf die neue Datei umleiten, eine Extraktionsform ergaenzen, die +mehrere Regeln je Tabelle liefern kann, die drei loch-behauptenden Pruefungen +UMKEHREN statt zu entfernen oder zu lockern, die Gegenmessungen und die vier +Befehlsrichtungen ergaenzen, und die Bereitstellung von `grant-foreign-group` +so umbauen, dass die beiden aufsetzenden Pruefungen weiterhin messen, was sie +behaupten. + +Zuletzt einen neuen Beschreibungsblock in `apps/api/src/groups/migration-sql.spec.ts` +fuer die neue Migration, im Stil der beiden bestehenden Bloecke: reiner +Textabgleich ohne Datenbank. Er prueft, dass die neue Datei die abgeloesten +Regeln entfernt und neu anlegt, dass die Benutzerseite in beiden neuen +Bedingungen vorkommt, dass die Tabelle mit nullbarer Mandantenkennung genau vier +nach Befehl getrennte Regeln bekommt, und dass ausschliesslich die Leseregel die +Zeilen ohne Mandant zulaesst. + + + DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && DATABASE_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/tessera" npx prisma migrate status --schema apps/api/prisma/schema.prisma | tee /tmp/jab-migrate-status.txt && grep -qiE 'up to date|schema is up to date|keine ausstehenden' /tmp/jab-migrate-status.txt && POL=$(docker exec tessera-ctl-db-1 psql -U tessera -d tessera -At -c "SELECT tablename||'#'||policyname||'#'||coalesce(cmd,'')||'#'||coalesce(qual,'')||'#'||coalesce(with_check,'') FROM pg_policies WHERE tablename IN ('GroupMembership','ModuleGrant','TenderRssFeedSource','SearchProvider') ORDER BY tablename, policyname") && echo "$POL" && echo "$POL" | grep '^GroupMembership#' | grep -q '"User"' && echo "$POL" | grep '^ModuleGrant#' | grep -q '"User"' && echo "$POL" | grep '^ModuleGrant#' | grep -q '"Group"' && test 4 -eq "$(echo "$POL" | grep -c '^TenderRssFeedSource#')" && test 1 -eq "$(echo "$POL" | grep -c '^SearchProvider#')" && echo "$POL" | grep '^TenderRssFeedSource#' | grep '#SELECT#' | grep -q 'IS NULL' && test 0 -eq "$(echo "$POL" | grep '^TenderRssFeedSource#' | grep -vE '#SELECT#' | grep -c 'IS NULL')" && test 0 -eq "$(echo "$POL" | grep '^SearchProvider#' | grep -c 'IS NULL')" && OUT=$(TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs) && echo "$OUT" && for K in groupmembership-schreiben-fremder-benutzer-abgelehnt groupmembership-fremder-benutzer-ueber-die-wartungsrolle-weiterhin-moeglich modulegrant-fremde-gruppe-abgelehnt modulegrant-fremder-benutzer-abgelehnt modulegrant-fremde-gruppe-ueber-die-wartungsrolle-weiterhin-moeglich tenderrssfeed-plattformzeile-gebunden-sichtbar tenderrssfeed-eigene-zeile-gebunden-weiterhin-sichtbar tenderrssfeed-ungebunden-nur-die-plattformzeile tenderrssfeed-gebundenes-einfuegen-ohne-mandant-abgelehnt tenderrssfeed-gebundenes-aendern-der-plattformzeile-abgelehnt tenderrssfeed-gebundenes-loeschen-der-plattformzeile-abgelehnt searchprovider-mandantenlose-zeile-bleibt-unter-jedem-kontext-unsichtbar gruppenpfad-gebunden-schliesst-die-fremde-gruppe-aus gruppenpfad-ueber-die-wartungsrolle-liefert-die-fremde-gruppe-mit; do echo "$OUT" | grep -q "^$K: bestanden" || { echo "FEHLENDE ODER FEHLGESCHLAGENE PRUEFUNG: $K"; exit 1; }; done && echo "$OUT" | grep -qE '^Alle [0-9]+ Pruefungen bestanden\.$' && for A in T-JTS-02 T-JTS-03; do echo "$OUT" | grep -q "$A" || { echo "VERWEIS AUF DEN ALTEN BEFUND FEHLT IM MELDETEXT: $A"; exit 1; }; done && ls -d apps/api/prisma/migrations/*_rls_widen_membership_grant_and_platform_read >/dev/null && npm --prefix apps/api run test -- src/groups/migration-sql.spec.ts src/prisma/rls-coverage.spec.ts && npm --prefix apps/api run test && npm --prefix apps/api run type-check && test -z "$(git diff --name-only HEAD -- apps/api/prisma/schema.prisma apps/api/src/tenders apps/api/src/module-registry apps/api/src/dashboard apps/api/src/user apps/api/src/ldap apps/api/src/auth docker-compose.yml docker-compose.dev.yml docker-compose.prod.yml docker-compose.ci.yml .env.example .env.prod.example)" + + Eine neue Migration mit dem Namensteil `rls_widen_membership_grant_and_platform_read` existiert, ist LOKAL angewandt und der Migrationsstatus meldet keinen Rueckstand; die Regelliste der lebenden Datenbank zeigt fuer die Mitgliedschaftstabelle und die Freigabetabelle eine Bedingung, die die Benutzertabelle nennt, fuer die Freigabetabelle zusaetzlich die Gruppentabelle, fuer die RSS-Quellentabelle genau vier nach Befehl getrennte Regeln, von denen ausschliesslich die Leseregel die Zeilen ohne Mandantenkennung zulaesst, und fuer die Suchanbietertabelle unveraendert genau eine strenge Regel; die Bestandszaehlung ueber Mandantengrenzen zeigende Zeilen ist ausgefuehrt und ihr Ergebnis im Bericht genannt; das Wegwerf-Werkzeug meldet alle Pruefungen bestanden mit Rueckgabewert 0, die zwoelf neuen bzw. umgekehrten Kennungen stehen einzeln als bestanden in der Ausgabe, und die Meldetexte nennen beide alten Befundkennungen, damit der Nachweis ihrer Existenz erhalten bleibt; die beiden aufsetzenden Pruefungen des Bereichs `module-registry` stehen weiterhin auf bestanden, ihre Grundlage wird ueber die Wartungsrolle bereitgestellt und die Meldetexte behaupten nicht mehr, die Regel lasse die fremde Zeile durch; der neue Beschreibungsblock in der Migrations-Textpruefung ist vorhanden und gruen; der Gesamttestlauf ist gruen und die Typpruefung sauber; Schema, Anwendungscode und die Compose- und Beispiel-Umgebungsdateien sind unveraendert. + + + + Aufgabe 2: Der eine Anwendungspfad, den die Reparatur still falsch machen wuerde — und die vier Aufzeichnungen im Quelltext + 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/module-registry/module-access.service.ts, apps/api/src/prisma/rls-coverage.spec.ts + +Die Testerwartungen VOR der Umstellung, damit ein vergessener Bindungsaufruf rot +wird statt aus einem anderen Grund zu scheitern: + +- Die Feed-Auflistung erzeugt genau EINEN gebundenen Klienten mit der + Mandantenkennung aus dem Aufrufzusammenhang und fuehrt die Abfrage ueber ihn + aus — nachgewiesen mit dem im Vorhaben etablierten Zwei-Klienten-Nachweis + (zwei unterscheidbare Klienten, der ungebundene darf die Abfrage nicht sehen), + nicht mit einer Identitaets-Attrappe. Eine Attrappe, die den Helfer als + Identitaet abbildet, wuerde die Umstellung in KEINER Richtung bemerken; dieser + Fehler ist in diesem Vorhaben bereits dreimal aufgetreten. +- Der aufrufende Endpunkt reicht die Mandantenkennung aus dem Sitzungsnachweis + durch — nachgewiesen an der Aufrufform, nicht an der Antwort. +- Die Abbildung der Antwort bleibt unveraendert: plattformweite Zeilen werden + weiterhin als solche gekennzeichnet und die Besitzerkennung weiterhin + entfernt. Der bestehende Fall dazu bleibt inhaltlich erhalten. +- Die beiden Pfade, die eine plattformweite Zeile anlegen bzw. entfernen, + bleiben UNGEBUNDEN. Ein Fall haelt das fest, damit ein spaeterer Leser sie + nicht "der Vollstaendigkeit halber" mitbindet — gebunden koennte niemand mehr + eine plattformweite Quelle anlegen oder entfernen. +- Die Mandanten-Gegenpruefung vor jedem Erteilen einer Modulfreigabe bleibt + bestehen. Ein Fall haelt fest, dass ein Erteilen auf ein fremdes Ziel weiterhin + im Anwendungscode scheitert — nicht erst in der Datenbank, die heute ohnehin + nichts durchsetzt. Existiert ein solcher Fall bereits, wird er um einen + Kommentar ergaenzt, der sagt, warum er nach dieser Regelaenderung NICHT + entbehrlich geworden ist. + + +Zuerst die Ausgangslage erneut messen (Testzahl, Typpruefung, Wegwerf-Werkzeug), +dann die Testerwartungen aus `` schreiben und rot sehen, dann +umstellen. + +Die Auflistung der Feeds nimmt statt der blossen Benutzerkennung einen +Aufrufzusammenhang mit Benutzer- UND Mandantenkennung entgegen und fuehrt ihre +Abfrage ueber einen gebundenen Klienten aus, benannt wie in den bereits +umgestellten Bereichen. Der bestehende Filter auf eigene und plattformweite +Zeilen bleibt stehen — er ist nach der Bindung nicht ueberfluessig, sondern das +zweite Netz. Der aufrufende Endpunkt reicht die Mandantenkennung aus dem +Sitzungsnachweis durch; er hat sie bereits zur Hand. + +Die Begruendung fuer diese Bindung gehoert in den Kopfkommentar der Methode, und +zwar als das, was sie ist: die Regelaenderung dreht die Fehlerrichtung dieses +Pfades um. Ungebunden lieferte er nach dem Scharfschalten frueher gar nichts und +liefert jetzt die plattformweiten Zeilen — eine kurze, glaubhafte Liste statt +einer leeren. Der Kommentar nennt die neue Migration und die Pruefung, die das +belegt. + +Die beiden Kopfkommentare der Pfade, die eine plattformweite Zeile anlegen bzw. +entfernen, werden an der neuen Regel richtiggestellt: sie bleiben ungebunden, +aber aus dem jetzt ausdruecklichen Grund (die Schreibregeln verlangen einen +Mandanten), und sie halten fest, dass beide Faelle unter der Anwendungsrolle +ueberhaupt nicht mehr durchgehen — vor wie nach dieser Aenderung — und deshalb +in Etappe 4 einen Verwaltungsweg brauchen. Ein Verweis auf den dafuer in Aufgabe +3 angelegten Ledger-Eintrag gehoert dazu. + +Die vier Aufzeichnungen im Quelltext, die die alten Regeln beschreiben, werden +an der Messung aus Aufgabe 1 richtiggestellt: der Kopfkommentar zur +Zugriffsaufloesung im Modulbereich, der Kommentar an der Benutzerpruefung in der +Gruppenverwaltung, der Kommentar an der Mandanten-Gegenpruefung bei den +Modulfreigaben und die Beschreibungszeile fuer die Mitgliedschaftstabelle in der +Ausnahmeliste des Abdeckungstests, die die Absicherung heute nur ueber die +Gruppenseite beschreibt und jetzt beide Seiten nennen muss. Jede dieser vier +Stellen nennt die neue Migration. + +Die Mandanten-Gegenpruefung selbst wird NICHT entfernt und nicht abgeschwaecht. +Ihr Kommentar sagt kuenftig, dass die Datenbank die Grenze inzwischen ebenfalls +zieht, dass diese zweite Ziehung aber erst nach dem Scharfschalten wirkt und die +Pruefung im Anwendungscode bis dahin der einzige und danach der erste Schutz +bleibt. + +Zum Abschluss den Falsifizierungsnachweis: den Bindungsaufruf der Feed-Auflistung +versuchsweise zuruecknehmen, den roten Testnamen samt Fehlermeldung notieren, +die Ruecknahme rueckgaengig machen. Ohne diesen Nachweis ist nicht belegt, dass +der neue Test die Umstellung ueberhaupt bemerken wuerde. Er gehoert in den +Bericht. + + + DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs | grep -qE '^Alle [0-9]+ Pruefungen bestanden\.$' && grep -q "from '../prisma/prisma-tenant.extension'" apps/api/src/tenders/tender-rss-feed.service.ts && B=$(grep -c 'tenantPrisma\.tenderRssFeedSource\.' apps/api/src/tenders/tender-rss-feed.service.ts) && { test "$B" -ge 3 || { echo "BINDUNG: nur $B gebundene Zugriffe in tender-rss-feed.service.ts, erwartet mindestens 3 (zwei bestehende plus die Auflistung)"; exit 1; }; } && U=$(grep -c 'this\.prisma\.tenderRssFeedSource\.' apps/api/src/tenders/tender-rss-feed.service.ts) && { test "$U" -eq 2 || { echo "UNGEBUNDEN: $U ungebundene Zugriffe in tender-rss-feed.service.ts, erwartet genau 2 (Anlegen und Entfernen plattformweiter Zeilen)"; exit 1; }; } && A=$(grep -c 'assertTargetBelongsToTenant' apps/api/src/groups/module-grants.service.ts) && { test "$A" -ge 3 || { echo "GEGENPRUEFUNG: nur $A Vorkommen von assertTargetBelongsToTenant, die Pruefung wurde entfernt oder ihre Aufrufe reduziert"; exit 1; }; } && MIG=$(basename $(ls -d apps/api/prisma/migrations/*_rls_widen_membership_grant_and_platform_read)) && for F in apps/api/src/tenders/tender-rss-feed.service.ts apps/api/src/groups/groups.service.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; do grep -q "$MIG" "$F" || { echo "AUFZEICHNUNG NENNT DIE NEUE MIGRATION NICHT: $F"; exit 1; }; done && grep -q 'tenantId' apps/api/src/tenders/tenders.controller.ts && npm --prefix apps/api run test -- src/tenders/tender-rss-feed.service.spec.ts src/tenders/tenders.controller.spec.ts src/groups/module-grants.service.spec.ts src/prisma/rls-coverage.spec.ts && npm --prefix apps/api run test && npm --prefix apps/api run type-check && test -z "$(git diff --name-only HEAD -- apps/api/prisma apps/api/src/dashboard apps/api/src/user apps/api/src/ldap apps/api/src/auth apps/api/src/dkv docker-compose.yml docker-compose.dev.yml docker-compose.prod.yml docker-compose.ci.yml .env.example .env.prod.example)" + + Die Feed-Auflistung laeuft ueber einen gebundenen Klienten, nimmt die Mandantenkennung aus dem Aufrufzusammenhang entgegen und wird vom aufrufenden Endpunkt entsprechend bedient; die beiden Pfade fuer plattformweite Zeilen bleiben ungebunden und tragen die richtiggestellte Begruendung samt Verweis auf den in Aufgabe 3 angelegten offenen Eintrag; der Zwei-Klienten-Nachweis liegt in der Testdatei des Dienstes vor, keine Identitaets-Attrappe; die Mandanten-Gegenpruefung bei den Modulfreigaben steht unveraendert und ist durch einen Fall gehalten, der beim Rueckbau rot wird; alle fuenf Aufzeichnungen im Quelltext nennen die neue Migration und beschreiben die neue Regel statt der alten; der Falsifizierungsnachweis ist durchgefuehrt, zurueckgenommen und im Bericht mit Testnamen und Fehlermeldung festgehalten; der Gesamttestlauf ist gruen mit mindestens der zu Beginn dieser Aufgabe gemessenen Testzahl, die Typpruefung sauber, das Wegwerf-Werkzeug weiterhin vollstaendig bestanden; Schema, Migrationen und die Bereiche `dashboard`, `user`, `ldap`, `auth`, `dkv` sowie die Compose- und Beispiel-Umgebungsdateien sind unveraendert. + + + + Aufgabe 3: Den Aktenstand kohaerent machen — Ledger, Klassifikation, Kritikschrift, Betriebsanleitung + .planning/WINDOWS.md, docs/mandantentrennung-zugriffsklassifikation.md, docs/mandantentrennung-etappe2-fehlerrichtung.md, docs/mandantentrennung-datenbankrolle.md + +Der Ledger. WINDOWS #19 wird auf `fixed` gesetzt, in der Markdown-Tabelle UND im +JSON-Block, mit Aufloesungszeitpunkt und einem Beleg, der die neue Migration +namentlich nennt und die Pruefungen benennt, die beide Fehlerrichtungen des +Lese-/Schreibsplits messen. Der Eintrag haelt zusaetzlich fest, dass seine +Praemisse fuer die Suchanbietertabelle WIDERLEGT wurde (Befund E) und diese +Haelfte deshalb nicht geloest, sondern richtiggestellt ist. #18, #20, #21 und #23 +bleiben unveraendert offen. + +Ein NEUER offener Eintrag kommt hinzu, ebenfalls in Tabelle und JSON-Block: unter +der Anwendungsrolle laesst sich eine plattformweite RSS-Quelle weder anlegen noch +entfernen — in der alten wie in der neuen Regel, weil Schreibzugriffe +ausdruecklich einen Mandanten verlangen. Der Eintrag benennt die beiden +betroffenen Pfade, sagt, dass dies KEINE Folge dieser Reparatur ist, und benennt +die konkrete Vorabpruefung fuer Etappe 4. Er ist der Grund, warum die Schliessung +von #19 nichts verschwinden laesst. + +Die Zaehler im Kopf der Datei werden aus dem JSON-Block ABGELEITET, nicht +weitergezaehlt — vier von vier Kopfzahlen dieses Vorhabens sind bei genauerem +Hinsehen schon einmal geschrumpft. + +Die Klassifikation. Der Block zu #19 wird von einer offenen Frage zu einer +beantworteten: er nennt die neue Migration, die getroffene Semantik (Lesen +schliesst die plattformweiten Zeilen ein, Schreiben verlangt einen Mandanten, +getrennt nach Befehl weil ein Lesebedingung allein auch Aendern und Entfernen +regelt) und die Widerlegung fuer die Suchanbietertabelle. Der Punkt in "Was +diese Etappe NICHT entscheidet", der genau diese Regelform als offen fuehrt, +wird entsprechend aufgeloest statt stehengelassen. + +In der Bestandsaufnahme werden die vier betroffenen Zeilen nachgezogen: die +Zeile zur Suchanbietertabelle (die Begruendung bleibt richtig, bekommt aber die +Messung und den Verweis), die beiden Zeilen, die die einseitigen Regeln als +Grund fuer eine zusaetzliche Anwendungspruefung nennen, und die Zeile zu den +RSS-Quellen, deren Stand-Begruendung jetzt einen gebundenen Pfad mehr und zwei +ungebundene mit neuer Begruendung nennt. Uebersichtszeile und Summenzeile werden +aus dem Quelltext neu abgeleitet, nicht aus diesem Plan uebernommen. + +Die Kritikschrift bekommt einen neuen Abschnitt `## Regelschluss T-JTS-02, +T-JTS-03 und WINDOWS #19`, gesetzt nach dem Abschnitt zum Bereich +`module-registry` und vor den Verweis am Ende, mit den fuenf im Dokument +ueblichen Unterabschnitten unter den Kennungen `(r1)` bis `(r5)`: die +TATSAECHLICH beobachtete Ausgabe des Werkzeuglaufs und der Regelliste aus der +lebenden Datenbank; eine Signaltabelle, die fuer jede der drei Regeln BEIDE +Fehlerrichtungen fuehrt (zu streng und zu locker) samt dem Signal, an dem man sie +erkennen wuerde; die namentliche Liste der Stellen, an denen Leere weiterhin als +Abwesenheit gedeutet wird, einschliesslich der neuen Stelle aus Befund F und der +Begruendung, warum sie gebunden wurde; was dieser Durchlauf bewusst nicht loest +(der Verwaltungsweg fuer plattformweite Zeilen, die fehlende Benutzerdimension +der Ausschreibungsregeln, die plattformweite Eindeutigkeit von Anmeldename und +Adresse); und was er bewusst nicht anfasst. + +Zur fehlenden Benutzerdimension gehoert eine eigene Messung statt einer +Uebernahme: es ist selbst nachzusehen, ob es irgendwo eine zweite +Sitzungsvariable fuer den Benutzer gibt, und das Ergebnis mit der ausgefuehrten +Anweisung festzuhalten. Gebaut wird sie hier nicht. + +Ausserdem werden die vier ueberholten Bestandsstellen der Kritikschrift mit +einem Nachtrag versehen — die Form dafuer macht der Abschnitt zur Fortschreibung +des ldap-Abschnitts bereits vor: der Punkt, der beide Regeln als einseitig +beschreibt; der Punkt, der #19 als nicht geloest fuehrt; die Zeile der +Signaltabelle des Bereichs `tenders` zu den drei RSS-Pfaden; und die beiden +aufgezeichneten Werkzeugausgaben, die die alten Meldetexte woertlich zitieren. +Die zitierten alten Ausgaben werden NICHT umgeschrieben — sie sind ein +Messprotokoll und bleiben, was sie waren; der Nachtrag steht daneben und sagt, +seit wann und wodurch die Aussage ueberholt ist. Ebenso die eine Stelle in der +Betriebsanleitung zur Datenbankrolle, die #19 als offen fuehrt. + + + DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs | grep -qE '^Alle [0-9]+ Pruefungen bestanden\.$' && MIG=$(basename $(ls -d apps/api/prisma/migrations/*_rls_widen_membership_grant_and_platform_read)) && python3 - "$MIG" <<'PY' +import json, re, sys +mig = sys.argv[1] +t = open('.planning/WINDOWS.md', encoding='utf-8').read() +m = re.search(r'`{3,}json\s*\n(\[.*?\])\s*\n`{3,}', t, re.S) +if not m: + sys.exit('JSON-Block in WINDOWS.md nicht gefunden') +rows = json.loads(m.group(1)) +by_id = {r['id']: r for r in rows} +if by_id[19]['status'] != 'fixed': + sys.exit('WINDOWS #19 steht nicht auf fixed') +if not by_id[19].get('resolved_at'): + sys.exit('WINDOWS #19 hat keinen Aufloesungszeitpunkt') +for i in (18, 20, 21, 23): + if by_id[i]['status'] != 'open': + sys.exit(f'WINDOWS #{i} ist nicht mehr offen') +new = [r for r in rows if r['id'] > 23] +if len(new) != 1: + sys.exit(f'genau ein neuer Eintrag erwartet, gefunden: {len(new)}') +if new[0]['status'] != 'open': + sys.exit('der neue Eintrag ist nicht offen') +open_n = sum(1 for r in rows if r['status'] == 'open') +fixed_n = sum(1 for r in rows if r['status'] == 'fixed') +waived_n = sum(1 for r in rows if r['status'] == 'waived') +head = t.split('---')[1] +for key, want in (('open_count', open_n), ('fixed_count', fixed_n), + ('waived_count', waived_n), ('total_count', len(rows))): + got = re.search(rf'^{key}:\s*(\d+)\s*$', head, re.M) + if not got or int(got.group(1)) != want: + sys.exit(f'{key} im Kopf stimmt nicht mit dem JSON-Block: {got and got.group(1)} statt {want}') +for r in rows: + line = re.search(rf'^\|\s*{r["id"]}\s*\|.*$', t, re.M) + if not line: + sys.exit(f'Eintrag {r["id"]} fehlt in der Markdown-Tabelle') + if f'| {r["status"]} |' not in line.group(0): + sys.exit(f'Status von Eintrag {r["id"]} weicht zwischen Tabelle und JSON-Block ab') +if mig not in (by_id[19].get('reason') or '') + (by_id[19].get('description') or ''): + sys.exit('der Beleg zu #19 nennt die neue Migration nicht') +print(f'WINDOWS.md kohaerent: {open_n} offen, {fixed_n} behoben, {waived_n} zurueckgestellt, {len(rows)} gesamt') +PY + && grep -q '^## Regelschluss T-JTS-02, T-JTS-03 und WINDOWS #19$' docs/mandantentrennung-etappe2-fehlerrichtung.md && for S in r1 r2 r3 r4 r5; do grep -qE "^### \($S\) " docs/mandantentrennung-etappe2-fehlerrichtung.md || { echo "FEHLENDER UNTERABSCHNITT: ($S)"; exit 1; }; done && for F in docs/mandantentrennung-etappe2-fehlerrichtung.md docs/mandantentrennung-zugriffsklassifikation.md docs/mandantentrennung-datenbankrolle.md .planning/WINDOWS.md; do grep -q "$MIG" "$F" || { echo "DOKUMENT NENNT DIE NEUE MIGRATION NICHT: $F"; exit 1; }; done && U=$(grep -ro "this\.prisma\.[a-zA-Z]*" apps/api/src/tenders | grep -v spec | wc -l | tr -d ' ') && B=$(grep -ro "tenantPrisma\.[a-zA-Z]*\." apps/api/src/tenders | grep -v spec | wc -l | tr -d ' ') && { grep -qE "^\| tenders \| ${U} \| ${B} \|" docs/mandantentrennung-zugriffsklassifikation.md || { echo "UEBERSICHTSZEILE tenders nennt nicht die neu gemessenen Zahlen ${U}/${B}"; exit 1; }; } && awk -F'|' '$2 ~ /^ *[a-z][a-z-]* *$/ && $3 ~ /^ *[0-9]+ *$/ && $4 ~ /^ *[0-9]+ *$/ { su+=$3; sb+=$4; n++ } $2 ~ /^ *\*\*Summe\*\* *$/ && $4 !~ /^ *$/ { g3=$3; g4=$4; gsub(/[^0-9]/,"",g3); gsub(/[^0-9]/,"",g4); ru=g3+0; rb=g4+0; seen=1 } END { if (!seen || n+0 != 12) { print "UEBERSICHTSTABELLE nicht erkannt, Bereichszeilen: " n; exit 1 } if (su+0 != ru || sb+0 != rb) { print "SUMMENZEILE stimmt nicht: Bereichszeilen ergeben " su "/" sb ", Summenzeile nennt " ru "/" rb; exit 1 } }' docs/mandantentrennung-zugriffsklassifikation.md && npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts && npm --prefix apps/api run test && npm --prefix apps/api run type-check && test -z "$(git diff --name-only HEAD -- apps/api/prisma/schema.prisma docker-compose.yml docker-compose.dev.yml docker-compose.prod.yml docker-compose.ci.yml .env.example .env.prod.example)" + + WINDOWS #19 steht in Tabelle UND JSON-Block auf behoben, mit Zeitpunkt, mit dem Namen der neuen Migration im Beleg und mit der ausdruecklichen Feststellung, dass die Haelfte zur Suchanbietertabelle als widerlegte Praemisse und nicht als geloestes Problem schliesst; #18, #20, #21 und #23 sind unveraendert offen; genau ein neuer offener Eintrag beschreibt den fehlenden Verwaltungsweg fuer plattformweite Zeilen samt Vorabpruefung fuer Etappe 4; die vier Kopfzahlen stimmen mit dem JSON-Block ueberein und sind daraus abgeleitet; die Klassifikation fuehrt den #19-Block als beantwortet, hat den zugehoerigen Punkt in "Was diese Etappe NICHT entscheidet" aufgeloest, die vier Bestandsaufnahme-Zeilen nachgezogen und Uebersichts- wie Summenzeile aus dem Quelltext neu abgeleitet; die Kritikschrift traegt den neuen Abschnitt mit allen fuenf Unterabschnitten, der tatsaechlich beobachteten Ausgabe, einer Signaltabelle mit BEIDEN Fehlerrichtungen je Regel, der selbst ausgefuehrten Messung zur fehlenden Benutzerdimension und Nachtraegen an den vier ueberholten Bestandsstellen, ohne die alten Messprotokolle umzuschreiben; die Betriebsanleitung nennt #19 nicht mehr als offen; die Inventarpruefung, der Gesamttestlauf und die Typpruefung sind gruen; Schema und die Compose- und Beispiel-Umgebungsdateien sind unveraendert. + + + + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Mandant A → Datenzeilen des Mandanten B | Die Grenze, die dieser Plan an zwei Stellen von einseitig auf beidseitig zieht | +| Mandant → plattformweite Zeile (leere Mandantenkennung) | Neue, ausdrueckliche Grenze: lesen ja, schreiben nein — und sie muss nach BEIDEN Seiten stimmen | +| Anwendungscode → Datenbankregel | Die Regel ist ein ZWEITES Netz. Wird der Anwendungscode im Vertrauen darauf abgebaut, faellt der einzige heute wirksame Schutz weg (Schalter ist aus) | +| Aufzeichnung → spaeterer Leser | Eine Aufzeichnung, die ein geschlossenes Loch als offen fuehrt oder eine ueberholte Handlungsanweisung gibt, ist ein Angriffsweg auf den naechsten Durchlauf | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-JAB-01 | Elevation of Privilege | Regel auf `GroupMembership` | high | mitigate | Die Regel prueft nach Aufgabe 1 beide Seiten der Beziehung; gemessen mit einem Benutzer, den es im fremden Mandanten TATSAECHLICH gibt, samt Wartungsrollen-Gegenmessung, die belegt, dass die Abweisung von der Regel kommt | +| T-JAB-02 | Elevation of Privilege | Regel auf `ModuleGrant`, referenzierte Gruppe | high | mitigate | Die Regel prueft zusaetzlich die referenzierte Gruppe; die anwendungsseitige Gegenpruefung bleibt bestehen und ist durch einen Fall gehalten, der beim Rueckbau rot wird | +| T-JAB-03 | Elevation of Privilege | Regel auf `ModuleGrant`, referenzierter Benutzer | high | mitigate | Der zweite Zweig des Entweder-oder wird ausdruecklich mitgeprueft — T-JTS-03 hatte nur den Gruppenzweig gemessen, eine Reparatur nur dieses Zweigs liesse die Haelfte des Lochs offen | +| T-JAB-04 | Elevation of Privilege | Lese-/Schreibsplit zu locker gefasst | high | mitigate | Vier nach Befehl getrennte Regeln statt einer permissiven; drei Pruefungen messen, dass Einfuegen, Aendern und Entfernen einer plattformweiten Zeile unter gebundenem Kontext abgewiesen werden, und die Regelliste der lebenden Datenbank belegt, dass ausschliesslich die Leseregel die Zeilen ohne Mandant zulaesst | +| T-JAB-05 | Denial of Service | Lese-/Schreibsplit zu streng gefasst | high | mitigate | Zwei Pruefungen messen die Gegenrichtung: die plattformweite Zeile ist unter beiden Mandantenkontexten sichtbar UND die eigene Zeile des Mandanten bleibt es ebenfalls. Ohne die zweite waere eine Leseregel, die nur noch die plattformweiten Zeilen liefert, unbemerkt | +| T-JAB-06 | Denial of Service | Bestandszeilen, die die schaerfere Regel nicht mehr sieht | medium | mitigate | Aufgabe 1 zaehlt vor der Anwendung, ob es Mitgliedschaften oder Freigaben ueber Mandantengrenzen gibt, und HAELT bei einem Ergebnis ungleich null; dieselbe Zaehlung wird als Vorabpruefung an Etappe 4 uebergeben, weil das Testsystem hier nicht gemessen ist | +| T-JAB-07 | Information Disclosure | Suchanbietertabelle faelschlich gelockert | medium | mitigate | Die Tabelle behaelt ihre strenge Regel; die Praemisse von #19 ist fuer sie gemessen widerlegt. Eine Lockerung wuerde eine kuenftige mandantenlose Zeile jedem Mandanten zeigen — genau die falsche Richtung | +| T-JAB-08 | Tampering | Abbau der anwendungsseitigen Gegenpruefung im Vertrauen auf die neue Regel | high | mitigate | Die Gegenpruefung bleibt unveraendert, ihr Kommentar sagt ausdruecklich, dass die zweite Ziehung erst nach dem Scharfschalten wirkt, und eine Zaehlung ihrer Vorkommen ist Teil der Abnahme von Aufgabe 2 | +| T-JAB-09 | Repudiation | Der Nachweis, dass die Loecher existierten, geht verloren | medium | mitigate | Die drei Pruefungen werden UMGEKEHRT statt geloescht; jede traegt im Meldetext den alten Pruefungsnamen und die Befundkennung, und die Abnahme prueft, dass beide Kennungen in der Ausgabe des Werkzeugs vorkommen | +| T-JAB-10 | Spoofing | Eine Pruefung besteht aus dem falschen Grund, weil ihre Grundlage weggefallen ist | high | mitigate | Die Zeile, auf der zwei spaetere Pruefungen aufsetzen, wird ueber die Wartungsrolle bereitgestellt; die Abnahme fordert beide aufsetzenden Pruefungen namentlich als bestanden, und die Gegenmessung ueber die Wartungsrolle wuerde scheitern, wenn die Zeile fehlte | +| T-JAB-11 | Tampering | Die Regelaenderung wird geschrieben, aber nie angewandt | high | mitigate | Blockierender Schritt in Aufgabe 1: Migrationsstatus ohne Rueckstand UND Auslesen der Regelliste aus dem Systemkatalog der laufenden Datenbank. Bau und Typpruefung liefen auch ohne die Anwendung gruen | +| T-JAB-12 | Information Disclosure | Zugangsdaten im versionierten Text | low | accept | Die lokalen Entwicklungszugangsdaten stehen bereits als Vorgabewert in der Compose-Datei; es entsteht kein neues Geheimnis. Die Migration selbst vergibt kein Kennwort — derselbe Vorsatz wie in 20260909130000 | +| T-JAB-13 | Denial of Service | Der Verwaltungsweg fuer plattformweite Zeilen fehlt nach dem Scharfschalten | medium | transfer | Nicht durch diesen Plan geloest, weil die vorgegebene Semantik Schreibzugriffe an einen Mandanten bindet. Als eigener offener Ledger-Eintrag mit Vorabpruefung an Etappe 4 uebergeben, damit er nicht mit #19 verschwindet | +| T-JAB-14 | Repudiation | Handgepflegte Dokumentstellen werden still uebersprungen | high | mitigate | Vollstaendige Fundstellenliste in Befund J, abgeleitete Pruefungen statt fest verdrahteter Zahlen fuer Uebersichts- und Summenzeile, und eine Pruefung, die fordert, dass jede der betroffenen Dateien die neue Migration namentlich nennt | +| T-JAB-15 | Tampering | npm/pip/cargo-Installationen | low | accept | Dieser Plan installiert kein Paket; `package.json` und die Sperrdatei werden nicht angefasst. Das Paket-Legitimitaetsgatter faellt damit nicht an | + + + + +1. Der Migrationsstatus der lokalen Datenbank meldet keinen Rueckstand, und die + Regelliste aus dem Systemkatalog zeigt die neuen Regeln so, wie die Migration + sie beschreibt — nicht nur die Datei auf der Platte. +2. `node apps/api/scripts/rls-scratch-check.mjs` meldet alle Pruefungen + bestanden, einschliesslich der zwoelf namentlich geforderten neuen bzw. + umgekehrten und der beiden aufsetzenden Pruefungen des Bereichs + `module-registry`. +3. `npm --prefix apps/api run test` ist gruen, mit mindestens der zu Beginn von + Aufgabe 1 selbst gemessenen Testzahl. +4. `npm --prefix apps/api run type-check` ist sauber. +5. `npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts` + ist gruen — die maschinelle Klammer zwischen Quelltext und Klassifikation. +6. `.planning/WINDOWS.md` ist in Kopf, Tabelle und JSON-Block widerspruchsfrei; + #19 behoben, ein neuer Eintrag offen, #18/#20/#21/#23 unveraendert offen. +7. `DATABASE_URL` in allen Compose-Dateien und Beispiel-Umgebungsdateien zeigt + unveraendert auf die Rolle ohne `_app`-Zusatz; `prisma/schema.prisma` ist + unveraendert. +8. Manuell zu lesen, nicht maschinell zu pruefen: der neue Abschnitt der + Kritikschrift fuehrt fuer jede der drei Regeln BEIDE Fehlerrichtungen und + nicht nur die, die repariert wurde. + + + + + +- Die drei Regeln greifen so weit, wie sie sollen, und das ist an der lebenden + Datenbank gemessen statt aus der Migrationsdatei erschlossen. +- Kein Test und keine Pruefung behauptet noch, eines der drei Loecher sei + erwartetes Verhalten — und keiner ist dabei verloren gegangen. +- Der eine Anwendungspfad, den die Reparatur still falsch gemacht haette, ist + gebunden, und die Messung, die das belegt, laeuft bei jedem Werkzeuglauf mit. +- Die anwendungsseitige Mandanten-Gegenpruefung steht unveraendert. +- Keine Aufzeichnung beschreibt mehr ein Loch, das es nicht mehr gibt; keine + gibt mehr eine Handlungsanweisung, die nach der Reparatur falsch waere. +- Was die Reparatur nicht loest, ist als eigener offener Punkt aufgeschrieben + statt mit dem geschlossenen zu verschwinden. +- Der Schalter ist weiterhin aus. + + + + +Bericht nach `.planning/quick/260910-jab-mandantentrennung-die-drei-zu-kurz-greif/260910-jab-SUMMARY.md`. + +Er nennt ausdruecklich: die drei zu Beginn SELBST gemessenen Ausgangszahlen und +die drei am Ende gemessenen; das Ergebnis der Bestandszaehlung ueber +Mandantengrenzen zeigender Zeilen; den Falsifizierungsnachweis aus Aufgabe 2 mit +Testnamen und Fehlermeldung; das Ergebnis der eigenen Messung zur fehlenden +Benutzerdimension; und — als eigener Abschnitt — die Abweichung von der +Aufgabenstellung, dass die Behauptung "kein Anwendungscode muss sich aendern" +gemessen fuer genau einen Pfad nicht zutrifft, mit der Begruendung, warum das +Binden dieses Pfades zur Reparatur gehoert und nicht daneben. +