docs(quick-260910-jab): Plan fuer die drei zu kurz greifenden Datenbankregeln

Schliesst T-JTS-02, T-JTS-03 und WINDOWS #19 in einer handgeschriebenen
Migration. Drei zur Planungszeit gemessene Funde praegen den Zuschnitt:
eine DRITTE loch-behauptende Pruefung im Wegwerf-Werkzeug (nicht zwei), eine
Zeile, auf der zwei spaetere Pruefungen aufsetzen und die nach der Reparatur
nicht mehr entsteht, und genau ein Anwendungspfad, den die Reparatur still
falsch machen wuerde.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
2026-09-10 14:11:39 +02:00
parent 48430589e7
commit 93444aa91e
@@ -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."
---
<objective>
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.
</objective>
<execution_context>
@~/.claude/gsd-core/workflows/execute-plan.md
@~/.claude/gsd-core/templates/summary.md
</execution_context>
<context>
@.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
</context>
<planning_time_findings>
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 "<Tabelle>"` 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.
</planning_time_findings>
<tasks>
<task type="tracer">
<name>Aufgabe 1: Die drei Regeln schreiben, lokal anwenden und an der echten Datenbank messen</name>
<precondition>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.</precondition>
<files>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</files>
<behavior>
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.
</behavior>
<action>
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 `<planning_time_findings>` 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 `<behavior>` 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.
</action>
<verify>
<automated>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)"</automated>
</verify>
<done>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.</done>
</task>
<task type="auto" tdd="true">
<name>Aufgabe 2: Der eine Anwendungspfad, den die Reparatur still falsch machen wuerde — und die vier Aufzeichnungen im Quelltext</name>
<files>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</files>
<behavior>
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.
</behavior>
<action>
Zuerst die Ausgangslage erneut messen (Testzahl, Typpruefung, Wegwerf-Werkzeug),
dann die Testerwartungen aus `<behavior>` 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.
</action>
<verify>
<automated>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)"</automated>
</verify>
<done>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.</done>
</task>
<task type="auto">
<name>Aufgabe 3: Den Aktenstand kohaerent machen — Ledger, Klassifikation, Kritikschrift, Betriebsanleitung</name>
<files>.planning/WINDOWS.md, docs/mandantentrennung-zugriffsklassifikation.md, docs/mandantentrennung-etappe2-fehlerrichtung.md, docs/mandantentrennung-datenbankrolle.md</files>
<action>
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.
</action>
<verify>
<automated>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)"</automated>
</verify>
<done>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.</done>
</task>
</tasks>
<!-- planner-discipline-allow: IS NULL -->
<!-- Die Negativ-Pruefung auf die Nullwert-Zulassung laeuft ausschliesslich ueber
die Ausgabe des Systemkatalogs der laufenden Datenbank, nicht ueber eine
Quelldatei — ein Kommentar im Quelltext kann sie deshalb nicht entwerten. -->
<threat_model>
## 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 |
</threat_model>
<verification>
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.
</verification>
<success_criteria>
- 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.
</success_criteria>
<output>
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.
</output>