Files
tessera-ctl/.planning/quick/260910-jab-mandantentrennung-die-drei-zu-kurz-greif/260910-jab-SUMMARY.md
T

17 KiB
Raw Blame History

phase, plan, subsystem, tags, status, dependency-graph, tech-stack, key-files, decisions, metrics, actuals
phase plan subsystem tags status dependency-graph tech-stack key-files decisions metrics actuals
quick-260910-jab 01 mandantentrennung-datenbankrolle
rls
postgresql
multi-tenancy
security
module-grants
groups
tenders
complete
requires provides affects
T-JTS-02
T-JTS-03
WINDOWS-19
T-JAB-01..15-mitigations
rls-widen-migration-20260910120000
apps/api/prisma
apps/api/src/groups
apps/api/src/tenders
apps/api/src/module-registry
apps/api/scripts/rls-scratch-check.mjs
added patterns
Vier nach Befehl getrennte RLS-Policies (SELECT/INSERT/UPDATE/DELETE) statt einer permissiven USING-Klausel, wenn Lesen und Schreiben unterschiedliche Sichtbarkeitsregeln brauchen
Loch-behauptende Wegwerf-Pruefungen werden UMGEKEHRT statt geloescht, mit Verweis auf den alten Pruefungsnamen und die alte Befundkennung im Meldetext
Eine Zeile, auf der spaetere Pruefungen aufsetzen, wird nach einer Regelverschaerfung ueber die Wartungsrolle (BYPASSRLS) bereitgestellt, wenn der urspruengliche Erzeugungsweg (gebundenes INSERT) jetzt abgewiesen wird
created 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/groups/module-grants.service.spec.ts
apps/api/src/module-registry/module-access.service.ts
apps/api/src/prisma/rls-coverage.spec.ts
docs/mandantentrennung-zugriffsklassifikation.md
docs/mandantentrennung-etappe2-fehlerrichtung.md
docs/mandantentrennung-datenbankrolle.md
.planning/WINDOWS.md
GroupMembership-Regel prueft jetzt beide Seiten der Beziehung (Gruppe UND Benutzer, mit UND verknuepft), nach dem Join-Muster von PasswordResetToken
ModuleGrant-Regel prueft zusaetzlich beide moeglichen Ziele (Gruppe/Benutzer) mit Leer-Zulassung, weil D-04 Gruppe und Benutzer als Entweder-oder fuehrt
TenderRssFeedSource bekommt vier nach Befehl getrennte Policies statt einer, weil ein einzelner USING-Ausdruck auch UPDATE/DELETE mitregelt
SearchProvider bewusst NICHT angefasst — die WINDOWS-#19-Praemisse ist fuer dieses Modell widerlegt (kein Codeweg erzeugt eine mandantenlose Zeile)
TenderRssFeedSourceService.listForUser wird gebunden (einziger Anwendungscode-Pfad, den die Reparatur sonst still falsch gemacht haette); createPlatform/remove bleiben bewusst ungebunden
WINDOWS #24 neu angelegt: der Verwaltungsweg fuer plattformweite Zeilen unter der Anwendungsrolle fehlt weiterhin — verschwindet nicht mit der Schliessung von #19
duration completed
~70min 2026-09-10
tokens tasks commits plan_head_before
34770 3 3 93444aa91e

Quick 260910-jab: Die drei zu kurz greifenden Datenbankregeln schliessen — Summary

Eine handgeschriebene, lokal angewandte PostgreSQL-Migration schliesst T-JTS-02 (GroupMembership prueft nur die Gruppenseite), T-JTS-03 (ModuleGrant prueft nur die Mandantenkennung der Zeile, nicht wohin sie zeigt) und WINDOWS #19 (plattformweite TenderRssFeedSource-Zeilen waeren nach dem Scharfschalten fuer JEDEN Mandanten unsichtbar gewesen); das Wegwerf-Werkzeug misst alle drei Reparaturen an der lebenden Datenbank, mit den drei loch-behauptenden Pruefungen umgekehrt statt geloescht.

Ausgangslage — selbst gemessen (nicht uebernommen)

Zu Beginn von Aufgabe 1 frisch gemessen, gegen HEAD 93444aa:

  • Tests: 833 bestanden, 56 Dateien (npm --prefix apps/api run test)
  • Typpruefung: sauber (npm --prefix apps/api run type-check)
  • Wegwerf-Werkzeug: 66/66 Pruefungen bestanden (rls-scratch-check.mjs gegen tessera-ctl-db-1, Adresse 172.19.0.2, zur Laufzeit ermittelt)

Diese drei Zahlen decken sich mit der in der Aufgabenstellung genannten Baseline — hier trotzdem selbst nachgemessen, nicht abgeschrieben (die Vorgabe der Aufgabenstellung verlangt genau das).

Am Ende gemessen

  • Tests: 839 bestanden, 56 Dateien (+6, alle aus dem neuen migration-sql.spec.ts-Beschreibungsblock)
  • Typpruefung: sauber
  • Wegwerf-Werkzeug: 74/74 Pruefungen bestanden (+8: siehe Aufschlüsselung unten unter "Neue/umgekehrte Pruefungen")

Bestandszaehlung ueber Mandantengrenzen (Befund H, vor Anwendung der Migration)

Gegen die lokale Datenbank tessera ausgefuehrt, vor dem Anwenden der neuen Migration:

memberships-cross-tenant   | 0
grants-cross-tenant-group  | 0
grants-cross-tenant-user   | 0
total-memberships          | 3
total-grants                | 4
total-tenants                | 1
tenderrssfeed-rows          | 2
tenderrssfeed-platform-rows | 1
searchprovider-rows          | 0
searchprovider-null-tenant  | 0

Ergebnis null — es gibt lokal keine Zeile, die von der verschärften Regel unsichtbar würde. Für das Testsystem ist das NICHT gemessen und muss vor Etappe 4 als eigene Vorabprüfung wiederholt werden (siehe Befund H im Plan).

Was gebaut wurde

Aufgabe 1 — Migration + Wegwerf-Werkzeug

Neue Migration 20260910120000_rls_widen_membership_grant_and_platform_read (lokal angewandt via prisma migrate deploy — nicht über npx prisma, das versuchte ungefragt Prisma 8.0.0-rc.13 herunterzuladen und wurde abgebrochen; stattdessen apps/api/node_modules/.bin/prisma, die im Projekt gepinnte Version 6.19.3, dieselbe, die rls-scratch-check.mjs selbst verwendet):

  1. GroupMembership — die Regel prüft jetzt groupId IN (...) UND userId IN (...), beide gegen den laufenden Mandanten.
  2. ModuleGrant — die Regel prüft weiterhin tenantId = current_tenant_id() UND zusätzlich groupId IS NULL OR groupId IN (...) UND userId IS NULL OR userId IN (...).
  3. TenderRssFeedSource — vier Regeln statt einer: tenant_platform_read_policy (SELECT, schließt tenantId IS NULL ein), tenant_insert_policy/tenant_update_policy/tenant_delete_policy (verlangen ausnahmslos einen Mandanten).
  4. SearchProvider — unverändert, nur der erklärende Absatz im Migrationskopf.

Regelliste der lebenden Datenbank nach Anwendung (pg_policies, Auszug):

GroupMembership#tenant_isolation_policy#ALL#(groupId IN (...) AND userId IN (...))
ModuleGrant#tenant_isolation_policy#ALL#(tenantId = ... AND (groupId IS NULL OR ...) AND (userId IS NULL OR ...))
SearchProvider#tenant_isolation_policy#ALL#(tenantId = current_tenant_id())
TenderRssFeedSource#tenant_delete_policy#DELETE#(tenantId = current_tenant_id())
TenderRssFeedSource#tenant_insert_policy#INSERT##WITH CHECK (tenantId = current_tenant_id())
TenderRssFeedSource#tenant_platform_read_policy#SELECT#(tenantId = current_tenant_id() OR tenantId IS NULL)
TenderRssFeedSource#tenant_update_policy#UPDATE#USING(...)#WITH CHECK(...)

Neue/umgekehrte Pruefungen im Wegwerf-Werkzeug (66 → 74):

Kennung Art Ergebnis
groupmembership-schreiben-fremder-benutzer-abgelehnt Umkehr (T-JTS-02) bestanden — Insert mit echtem user-b abgewiesen
groupmembership-fremder-benutzer-ueber-die-wartungsrolle-weiterhin-moeglich neu bestanden — Gegenmessung über Wartungsrolle gelingt
modulegrant-fremde-gruppe-abgelehnt Umkehr (T-JTS-03) bestanden
modulegrant-fremder-benutzer-abgelehnt neu (zweiter D-04-Zweig) bestanden
modulegrant-fremde-gruppe-ueber-die-wartungsrolle-weiterhin-moeglich neu (legt grant-foreign-group an) bestanden
tenderrssfeed-plattformzeile-gebunden-sichtbar Umkehr (WINDOWS #19) bestanden
tenderrssfeed-eigene-zeile-gebunden-weiterhin-sichtbar neu bestanden
tenderrssfeed-ungebunden-nur-die-plattformzeile neu (Befund F) bestanden
tenderrssfeed-gebundenes-aendern-der-plattformzeile-abgelehnt neu bestanden
tenderrssfeed-gebundenes-loeschen-der-plattformzeile-abgelehnt neu bestanden
searchprovider-mandantenlose-zeile-bleibt-unter-jedem-kontext-unsichtbar neu (eigener Bereich) bestanden

Extraktion umgeleitet: readRlsWidenMigrationSql() liest die neue Datei für GroupMembership/ModuleGrant; extractAllPolicySql() (neu, mit globalem Regex-Flag) liefert alle vier TenderRssFeedSource-Policies. Die Meldung von gruppenpfad-gebunden-schliesst-die-fremde-gruppe-aus im Bereich module-registry ist richtiggestellt (behauptete vorher fälschlich, die Regel lasse die fremde Zeile durch).

Aufgabe 2 — listForUser binden

TenderRssFeedSourceService.listForUser(userId, tenantId) läuft jetzt über forTenant(). TendersController.listRssFeeds reicht die Mandantenkennung aus dem Sitzungsnachweis durch (Aufrufform geprüft, nicht die Antwort). createPlatform/remove bleiben bewusst ungebunden. Fünf Aufzeichnungen im Quelltext (drei Kopfkommentare in tender-rss-feed.service.ts, module-access.service.ts, groups.service.ts, module-grants.service.ts, rls-coverage.spec.ts) nennen die neue Migration und beschreiben die neue Regel statt der alten. Die Mandanten-Gegenprüfung (assertTargetBelongsToTenant) bleibt unverändert bestehen — zwei bestehende Testfälle in module-grants.service.spec.ts wurden um einen Kommentar ergänzt, der festhält, warum sie nach der Regeländerung NICHT entbehrlich geworden sind.

Deviation, gemessen statt geglaubt (siehe <output>-Vorgabe des Plans): die Behauptung "kein Anwendungscode muss sich ändern" trifft für GENAU EINEN Pfad nicht zu — TenderRssFeedSourceService.listForUser. Vor der Regeländerung lieferte der ungebundene Pfad nach dem Scharfschalten NICHTS (gemessen: tenderrssfeed-ungebunden-nur-die-plattformzeile würde ohne Bindung 0 statt 1 Zeile liefern). Nach der Regeländerung liefert derselbe ungebundene Pfad NUR die plattformweiten Zeilen — eine kurze, glaubhafte Teilantwort statt einer schreienden Leere. Das Binden gehört deshalb zur Reparatur selbst, nicht daneben: ohne diese Bindung hätte 260910-jab einen Pfad still von "meldet sich laut" auf "täuscht Vollständigkeit vor" verschlechtert.

Falsifizierungsnachweis (Bindungsaufruf zurückgenommen, Test rot gesehen, Rücknahme zurückgenommen):

Backup genommen → const tenantPrisma = ... entfernt, this.prisma direkt verwendet
npm --prefix apps/api run test -- src/tenders/tender-rss-feed.service.spec.ts

× listForUser() bindet — forTenant() wird mit der uebergebenen Mandantenkennung aufgerufen
  AssertionError: expected "spy" to be called with arguments: [ {…(4)}, 'tenant-a' ]
  Number of calls: 0

Test Files  1 failed (1)
     Tests  1 failed | 30 passed (31)

→ Rücknahme rückgängig gemacht (Backup wiederhergestellt), erneuter Lauf: 31/31 grün

Genau EIN Test wurde rot, wie erwartet — kein anderer Test hing versehentlich an dieser Bindung.

Rule 1 (Auto-Fix): listRssFeeds destrukturierte feeds.map(({userId, ...rest}) => ...). Da listForUser jetzt über forTenant(...) as any läuft, wurde feeds implizit any, und TypeScript meldete TS7031: Binding element 'ownerUserId' implicitly has an 'any' type. Behoben mit expliziter (... : any)-Annotation, Muster aus anderen .map((g: any) => ...)-Stellen derselben Datei. Typpruefung danach wieder sauber.

Aufgabe 3 — Aktenstand kohärent

  • .planning/WINDOWS.md: #19 auf fixed (Tabelle + JSON-Block, resolved_at: 2026-09-10T12:35:40.000Z), Beleg nennt die neue Migration und die Prüfungen für beide Fehlerrichtungen des Lese-/Schreibsplits. #18/#20/#21/#22/#23 unverändert offen. Neuer Eintrag #24 (offen, Kind deviation): der Verwaltungsweg für plattformweite Zeilen unter der Anwendungsrolle fehlt — weder Anlegen noch Entfernen ist unter der Anwendungsrolle möglich, in der alten wie in der neuen Regel. Kopfzahlen aus dem JSON-Block abgeleitet und mit einem eigenen Python-Skript gegengeprüft (identisch zum Verify-Skript des Plans): 6 offen, 17 behoben, 1 zurückgestellt, 24 gesamt.
  • Klassifikation: #19-Block beantwortet, die vier betroffenen Bestandsaufnahme-Zeilen nachgezogen, Übersichtszeile tenders (35/27, gemessen mit den beiden im Dokument selbst geführten grep-Anweisungen) und Summenzeile (107/135) neu aus dem Quelltext abgeleitet — beide mit dem exakten awk-Prüfskript des Plans gegengeprüft.
  • Kritikschrift: neuer Abschnitt ## Regelschluss T-JTS-02, T-JTS-03 und WINDOWS #19 mit (r1) tatsächlich beobachteter Ausgabe + Regelliste aus der lebenden Datenbank, (r2) Signaltabelle mit beiden Fehlerrichtungen je Regel, (r3) den Stellen, an denen Leere weiterhin als Abwesenheit gedeutet wird (inklusive der neuen Stelle aus Befund F), (r4) was dieser Durchlauf nicht löst, (r5) was er nicht anfasst. Fünf überholte Bestandsstellen mit Nachträgen versehen (der Punkt in (g4), der Punkt in (t4), die Signaltabellenzeile der drei RSS-Pfade, DREI aufgezeichnete Werkzeugausgaben — eine mehr als die im Plan als Minimum genannten zwei, weil beim Durchsuchen eine dritte literale Zitatstelle im Abschnitt module-registry gefunden wurde — und der Punkt in (m4)). Alle alten Messprotokolle bleiben wörtlich stehen, die Nachträge stehen daneben.
  • Selbst ausgeführte Messung zur fehlenden Benutzerdimension (Aufgabe 3, wie vom Plan verlangt): grep -rn "current_setting\|set_config" apps/api/src apps/api/prisma/migrations findet GENAU EINE Sitzungsvariable, app.current_tenant (prisma-tenant.extension.ts, 20260618112133_rls_policies). Es gibt KEINE zweite Sitzungsvariable für den Benutzer — Tabellen wie TenderSavedSearch haben deshalb strukturell keine Möglichkeit, eine Benutzerdimension auf Datenbankebene durchzusetzen, ohne eine solche Variable erst einzuführen. Ergebnis in (r4) der Kritikschrift festgehalten, nicht gebaut.
  • Betriebsanleitung: die eine Stelle, die #19 als offen führte, nennt jetzt den Auflösungsstand und WINDOWS #24.

Deviations from Plan

Auto-fixed Issues

1. [Rule 1 - Bug] TenderRssFeedSource-Wegwerf-UPDATE-Prüfung zielte auf eine nicht existierende Spalte label

  • Gefunden während: Aufgabe 1, erster Lauf des Wegwerf-Werkzeugs nach dem Hinzufügen der neuen UPDATE-Prüfung
  • Problem: tenderrssfeed-gebundenes-aendern-der-plattformzeile-abgelehnt versuchte SET label = ..., aber die im selben Abschnitt angelegte Wegwerf-Tabelle hat keine label-Spalte (id, url, userId, tenantId) — die Prüfung "bestand", aber aus dem FALSCHEN Grund (SQL-Fehler 42703 statt der beabsichtigten RLS-Abweisung).
  • Fix: Spalte auf url geändert, die tatsächlich existiert; danach bestand die Prüfung aus dem richtigen Grund (tenant_update_policy filtert die Zielzeile heraus, 0 betroffene Zeilen).
  • Dateien: apps/api/scripts/rls-scratch-check.mjs
  • Commit: f4f3115

2. [Rule 1 - Bug] Implizites any beim Destrukturieren in listRssFeeds

  • Gefunden während: Aufgabe 2, Typprüfung nach dem Binden von listForUser
  • Problem: feeds.map(({ userId: ownerUserId, ...rest }) => ...) löste TS7031 aus, weil feeds durch die neue forTenant(...) as any-Bindung implizit any wurde.
  • Fix: explizite (... : any)-Annotation am Destrukturierungsparameter.
  • Dateien: apps/api/src/tenders/tenders.controller.ts
  • Commit: 6b23735

Package-Legitimitätsgatter

3. [Rule 3 – ausgeschlossen, keine Installation] npx prisma migrate deploy versuchte beim Ausführen ungefragt, prisma@8.0.0-rc.13 herunterzuladen (weiter als die im Projekt gepinnte 6.19.3) — abgebrochen (pkill), stattdessen apps/api/node_modules/.bin/prisma verwendet, die lokale, im Lockfile gepinnte Version. Keine Installation fand statt, daher kein Checkpoint nötig — dokumentiert, weil es beinahe eine unbeabsichtigte Fremdversion in den Migrationslauf eingeschleust hätte.

Keine weiteren Abweichungen — alle übrigen Aufgaben wie im Plan beschrieben ausgeführt.

Known Stubs

Keine.

Threat Flags

Keine neuen — das Threat-Model des Plans (T-JAB-01 bis T-JAB-15) deckt alle in diesem Durchlauf berührten Flächen bereits ab; keine neue, dort nicht erfasste Angriffsfläche entstanden.

Self-Check: PASSED

  • apps/api/prisma/migrations/20260910120000_rls_widen_membership_grant_and_platform_read/migration.sql — FOUND
  • Commit f4f3115 (Aufgabe 1) — FOUND (git log --oneline --all | grep f4f3115)
  • Commit 6b23735 (Aufgabe 2) — FOUND
  • Commit 03fb3bf (Aufgabe 3) — FOUND
  • rls-scratch-check.mjs meldet 74/74 bestanden gegen die lebende Datenbank — bestätigt (letzter Lauf vor diesem Bericht)
  • .planning/WINDOWS.md besteht das plan-eigene Python-Kohärenzskript — bestätigt
  • npm --prefix apps/api run test — 839/839 grün, type-check sauber — bestätigt