Files

321 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: quick-260910-jab
plan: 01
subsystem: mandantentrennung-datenbankrolle
tags: [rls, postgresql, multi-tenancy, security, module-grants, groups, tenders]
status: complete
dependency-graph:
requires: [T-JTS-02, T-JTS-03, WINDOWS-19]
provides: [T-JAB-01..15-mitigations, rls-widen-migration-20260910120000]
affects: [apps/api/prisma, apps/api/src/groups, apps/api/src/tenders, apps/api/src/module-registry, apps/api/scripts/rls-scratch-check.mjs]
tech-stack:
added: []
patterns:
- "Vier nach Befehl getrennte RLS-Policies (SELECT/INSERT/UPDATE/DELETE) statt einer permissiven USING-Klausel, wenn Lesen und Schreiben unterschiedliche Sichtbarkeitsregeln brauchen"
- "Loch-behauptende Wegwerf-Pruefungen werden UMGEKEHRT statt geloescht, mit Verweis auf den alten Pruefungsnamen und die alte Befundkennung im Meldetext"
- "Eine Zeile, auf der spaetere Pruefungen aufsetzen, wird nach einer Regelverschaerfung ueber die Wartungsrolle (BYPASSRLS) bereitgestellt, wenn der urspruengliche Erzeugungsweg (gebundenes INSERT) jetzt abgewiesen wird"
key-files:
created:
- apps/api/prisma/migrations/20260910120000_rls_widen_membership_grant_and_platform_read/migration.sql
modified:
- apps/api/scripts/rls-scratch-check.mjs
- apps/api/src/groups/migration-sql.spec.ts
- apps/api/src/tenders/tender-rss-feed.service.ts
- apps/api/src/tenders/tender-rss-feed.service.spec.ts
- apps/api/src/tenders/tenders.controller.ts
- apps/api/src/tenders/tenders.controller.spec.ts
- apps/api/src/groups/groups.service.ts
- apps/api/src/groups/module-grants.service.ts
- apps/api/src/groups/module-grants.service.spec.ts
- apps/api/src/module-registry/module-access.service.ts
- apps/api/src/prisma/rls-coverage.spec.ts
- docs/mandantentrennung-zugriffsklassifikation.md
- docs/mandantentrennung-etappe2-fehlerrichtung.md
- docs/mandantentrennung-datenbankrolle.md
- .planning/WINDOWS.md
decisions:
- "GroupMembership-Regel prueft jetzt beide Seiten der Beziehung (Gruppe UND Benutzer, mit UND verknuepft), nach dem Join-Muster von PasswordResetToken"
- "ModuleGrant-Regel prueft zusaetzlich beide moeglichen Ziele (Gruppe/Benutzer) mit Leer-Zulassung, weil D-04 Gruppe und Benutzer als Entweder-oder fuehrt"
- "TenderRssFeedSource bekommt vier nach Befehl getrennte Policies statt einer, weil ein einzelner USING-Ausdruck auch UPDATE/DELETE mitregelt"
- "SearchProvider bewusst NICHT angefasst — die WINDOWS-#19-Praemisse ist fuer dieses Modell widerlegt (kein Codeweg erzeugt eine mandantenlose Zeile)"
- "TenderRssFeedSourceService.listForUser wird gebunden (einziger Anwendungscode-Pfad, den die Reparatur sonst still falsch gemacht haette); createPlatform/remove bleiben bewusst ungebunden"
- "WINDOWS #24 neu angelegt: der Verwaltungsweg fuer plattformweite Zeilen unter der Anwendungsrolle fehlt weiterhin — verschwindet nicht mit der Schliessung von #19"
metrics:
duration: "~70min"
completed: 2026-09-10
actuals:
tokens: 34770
tasks: 3
commits: 3
plan_head_before: 93444aa91e8fb0739ee5bbf023f7ccbbd3cee38e
---
# Quick 260910-jab: Die drei zu kurz greifenden Datenbankregeln schliessen — Summary
Eine handgeschriebene, lokal angewandte PostgreSQL-Migration schliesst
T-JTS-02 (GroupMembership prueft nur die Gruppenseite), T-JTS-03 (ModuleGrant
prueft nur die Mandantenkennung der Zeile, nicht wohin sie zeigt) und
WINDOWS #19 (plattformweite TenderRssFeedSource-Zeilen waeren nach dem
Scharfschalten fuer JEDEN Mandanten unsichtbar gewesen); das Wegwerf-Werkzeug
misst alle drei Reparaturen an der lebenden Datenbank, mit den drei
loch-behauptenden Pruefungen umgekehrt statt geloescht.
## Ausgangslage — selbst gemessen (nicht uebernommen)
Zu Beginn von Aufgabe 1 frisch gemessen, gegen HEAD `93444aa`:
- **Tests:** 833 bestanden, 56 Dateien (`npm --prefix apps/api run test`)
- **Typpruefung:** sauber (`npm --prefix apps/api run type-check`)
- **Wegwerf-Werkzeug:** 66/66 Pruefungen bestanden
(`rls-scratch-check.mjs` gegen `tessera-ctl-db-1`, Adresse `172.19.0.2`,
zur Laufzeit ermittelt)
Diese drei Zahlen decken sich mit der in der Aufgabenstellung genannten
Baseline — hier trotzdem selbst nachgemessen, nicht abgeschrieben (die
Vorgabe der Aufgabenstellung verlangt genau das).
## Am Ende gemessen
- **Tests:** 839 bestanden, 56 Dateien (+6, alle aus dem neuen
`migration-sql.spec.ts`-Beschreibungsblock)
- **Typpruefung:** sauber
- **Wegwerf-Werkzeug:** 74/74 Pruefungen bestanden (+8: siehe Aufschlüsselung
unten unter "Neue/umgekehrte Pruefungen")
## Bestandszaehlung ueber Mandantengrenzen (Befund H, vor Anwendung der Migration)
Gegen die lokale Datenbank `tessera` ausgefuehrt, vor dem Anwenden der neuen
Migration:
```
memberships-cross-tenant | 0
grants-cross-tenant-group | 0
grants-cross-tenant-user | 0
total-memberships | 3
total-grants | 4
total-tenants | 1
tenderrssfeed-rows | 2
tenderrssfeed-platform-rows | 1
searchprovider-rows | 0
searchprovider-null-tenant | 0
```
Ergebnis null — es gibt lokal keine Zeile, die von der verschärften Regel
unsichtbar würde. Für das Testsystem ist das NICHT gemessen und muss vor
Etappe 4 als eigene Vorabprüfung wiederholt werden (siehe Befund H im Plan).
## Was gebaut wurde
### Aufgabe 1 — Migration + Wegwerf-Werkzeug
Neue Migration `20260910120000_rls_widen_membership_grant_and_platform_read`
(lokal angewandt via `prisma migrate deploy` — **nicht** über `npx prisma`,
das versuchte ungefragt Prisma 8.0.0-rc.13 herunterzuladen und wurde
abgebrochen; stattdessen `apps/api/node_modules/.bin/prisma`, die im Projekt
gepinnte Version 6.19.3, dieselbe, die `rls-scratch-check.mjs` selbst
verwendet):
1. **`GroupMembership`** — die Regel prüft jetzt `groupId IN (...)` UND
`userId IN (...)`, beide gegen den laufenden Mandanten.
2. **`ModuleGrant`** — die Regel prüft weiterhin `tenantId = current_tenant_id()`
UND zusätzlich `groupId IS NULL OR groupId IN (...)` UND
`userId IS NULL OR userId IN (...)`.
3. **`TenderRssFeedSource`** — vier Regeln statt einer:
`tenant_platform_read_policy` (SELECT, schließt `tenantId IS NULL` ein),
`tenant_insert_policy`/`tenant_update_policy`/`tenant_delete_policy`
(verlangen ausnahmslos einen Mandanten).
4. **`SearchProvider`** — unverändert, nur der erklärende Absatz im
Migrationskopf.
Regelliste der lebenden Datenbank nach Anwendung (`pg_policies`, Auszug):
```
GroupMembership#tenant_isolation_policy#ALL#(groupId IN (...) AND userId IN (...))
ModuleGrant#tenant_isolation_policy#ALL#(tenantId = ... AND (groupId IS NULL OR ...) AND (userId IS NULL OR ...))
SearchProvider#tenant_isolation_policy#ALL#(tenantId = current_tenant_id())
TenderRssFeedSource#tenant_delete_policy#DELETE#(tenantId = current_tenant_id())
TenderRssFeedSource#tenant_insert_policy#INSERT##WITH CHECK (tenantId = current_tenant_id())
TenderRssFeedSource#tenant_platform_read_policy#SELECT#(tenantId = current_tenant_id() OR tenantId IS NULL)
TenderRssFeedSource#tenant_update_policy#UPDATE#USING(...)#WITH CHECK(...)
```
**Neue/umgekehrte Pruefungen im Wegwerf-Werkzeug** (66 → 74):
| Kennung | Art | Ergebnis |
|---|---|---|
| `groupmembership-schreiben-fremder-benutzer-abgelehnt` | Umkehr (T-JTS-02) | bestanden — Insert mit echtem `user-b` abgewiesen |
| `groupmembership-fremder-benutzer-ueber-die-wartungsrolle-weiterhin-moeglich` | neu | bestanden — Gegenmessung über Wartungsrolle gelingt |
| `modulegrant-fremde-gruppe-abgelehnt` | Umkehr (T-JTS-03) | bestanden |
| `modulegrant-fremder-benutzer-abgelehnt` | neu (zweiter D-04-Zweig) | bestanden |
| `modulegrant-fremde-gruppe-ueber-die-wartungsrolle-weiterhin-moeglich` | neu (legt `grant-foreign-group` an) | bestanden |
| `tenderrssfeed-plattformzeile-gebunden-sichtbar` | Umkehr (WINDOWS #19) | bestanden |
| `tenderrssfeed-eigene-zeile-gebunden-weiterhin-sichtbar` | neu | bestanden |
| `tenderrssfeed-ungebunden-nur-die-plattformzeile` | neu (Befund F) | bestanden |
| `tenderrssfeed-gebundenes-aendern-der-plattformzeile-abgelehnt` | neu | bestanden |
| `tenderrssfeed-gebundenes-loeschen-der-plattformzeile-abgelehnt` | neu | bestanden |
| `searchprovider-mandantenlose-zeile-bleibt-unter-jedem-kontext-unsichtbar` | neu (eigener Bereich) | bestanden |
Extraktion umgeleitet: `readRlsWidenMigrationSql()` liest die neue Datei
für `GroupMembership`/`ModuleGrant`; `extractAllPolicySql()` (neu, mit
globalem Regex-Flag) liefert alle vier `TenderRssFeedSource`-Policies. Die
Meldung von `gruppenpfad-gebunden-schliesst-die-fremde-gruppe-aus` im
Bereich `module-registry` ist richtiggestellt (behauptete vorher fälschlich,
die Regel lasse die fremde Zeile durch).
### Aufgabe 2 — `listForUser` binden
`TenderRssFeedSourceService.listForUser(userId, tenantId)` läuft jetzt über
`forTenant()`. `TendersController.listRssFeeds` reicht die Mandantenkennung
aus dem Sitzungsnachweis durch (Aufrufform geprüft, nicht die Antwort).
`createPlatform`/`remove` bleiben bewusst ungebunden. Fünf Aufzeichnungen im
Quelltext (drei Kopfkommentare in `tender-rss-feed.service.ts`,
`module-access.service.ts`, `groups.service.ts`, `module-grants.service.ts`,
`rls-coverage.spec.ts`) nennen die neue Migration und beschreiben die neue
Regel statt der alten. Die Mandanten-Gegenprüfung
(`assertTargetBelongsToTenant`) bleibt unverändert bestehen — zwei bestehende
Testfälle in `module-grants.service.spec.ts` wurden um einen Kommentar
ergänzt, der festhält, warum sie nach der Regeländerung NICHT entbehrlich
geworden sind.
**Deviation, gemessen statt geglaubt (siehe `<output>`-Vorgabe des Plans):**
die Behauptung "kein Anwendungscode muss sich ändern" trifft für GENAU EINEN
Pfad nicht zu — `TenderRssFeedSourceService.listForUser`. Vor der
Regeländerung lieferte der ungebundene Pfad nach dem Scharfschalten NICHTS
(gemessen: `tenderrssfeed-ungebunden-nur-die-plattformzeile` würde ohne
Bindung 0 statt 1 Zeile liefern). Nach der Regeländerung liefert derselbe
ungebundene Pfad NUR die plattformweiten Zeilen — eine kurze, glaubhafte
Teilantwort statt einer schreienden Leere. Das Binden gehört deshalb zur
Reparatur selbst, nicht daneben: ohne diese Bindung hätte 260910-jab einen
Pfad still von "meldet sich laut" auf "täuscht Vollständigkeit vor"
verschlechtert.
**Falsifizierungsnachweis** (Bindungsaufruf zurückgenommen, Test rot
gesehen, Rücknahme zurückgenommen):
```
Backup genommen → const tenantPrisma = ... entfernt, this.prisma direkt verwendet
npm --prefix apps/api run test -- src/tenders/tender-rss-feed.service.spec.ts
× listForUser() bindet — forTenant() wird mit der uebergebenen Mandantenkennung aufgerufen
AssertionError: expected "spy" to be called with arguments: [ {…(4)}, 'tenant-a' ]
Number of calls: 0
Test Files 1 failed (1)
Tests 1 failed | 30 passed (31)
→ Rücknahme rückgängig gemacht (Backup wiederhergestellt), erneuter Lauf: 31/31 grün
```
Genau EIN Test wurde rot, wie erwartet — kein anderer Test hing versehentlich
an dieser Bindung.
**Rule 1 (Auto-Fix):** `listRssFeeds` destrukturierte `feeds.map(({userId, ...rest}) => ...)`.
Da `listForUser` jetzt über `forTenant(...) as any` läuft, wurde `feeds`
implizit `any`, und TypeScript meldete `TS7031: Binding element 'ownerUserId'
implicitly has an 'any' type`. Behoben mit expliziter `(... : any)`-Annotation,
Muster aus anderen `.map((g: any) => ...)`-Stellen derselben Datei.
Typpruefung danach wieder sauber.
### Aufgabe 3 — Aktenstand kohärent
- **`.planning/WINDOWS.md`**: #19 auf `fixed` (Tabelle + JSON-Block,
`resolved_at: 2026-09-10T12:35:40.000Z`), Beleg nennt die neue Migration
und die Prüfungen für beide Fehlerrichtungen des Lese-/Schreibsplits.
#18/#20/#21/#22/#23 unverändert offen. Neuer Eintrag **#24** (offen, Kind
`deviation`): der Verwaltungsweg für plattformweite Zeilen unter der
Anwendungsrolle fehlt — weder Anlegen noch Entfernen ist unter der
Anwendungsrolle möglich, in der alten wie in der neuen Regel. Kopfzahlen
aus dem JSON-Block abgeleitet und mit einem eigenen Python-Skript
gegengeprüft (identisch zum Verify-Skript des Plans): 6 offen, 17 behoben,
1 zurückgestellt, 24 gesamt.
- **Klassifikation**: #19-Block beantwortet, die vier betroffenen
Bestandsaufnahme-Zeilen nachgezogen, Übersichtszeile `tenders` (35/27,
gemessen mit den beiden im Dokument selbst geführten grep-Anweisungen) und
Summenzeile (107/135) neu aus dem Quelltext abgeleitet — beide mit dem
exakten `awk`-Prüfskript des Plans gegengeprüft.
- **Kritikschrift**: neuer Abschnitt `## Regelschluss T-JTS-02, T-JTS-03 und
WINDOWS #19` mit (r1) tatsächlich beobachteter Ausgabe + Regelliste aus
der lebenden Datenbank, (r2) Signaltabelle mit beiden Fehlerrichtungen je
Regel, (r3) den Stellen, an denen Leere weiterhin als Abwesenheit gedeutet
wird (inklusive der neuen Stelle aus Befund F), (r4) was dieser Durchlauf
nicht löst, (r5) was er nicht anfasst. Fünf überholte Bestandsstellen mit
Nachträgen versehen (der Punkt in (g4), der Punkt in (t4), die
Signaltabellenzeile der drei RSS-Pfade, DREI aufgezeichnete
Werkzeugausgaben — eine mehr als die im Plan als Minimum genannten zwei,
weil beim Durchsuchen eine dritte literale Zitatstelle im Abschnitt
`module-registry` gefunden wurde — und der Punkt in (m4)). Alle alten
Messprotokolle bleiben wörtlich stehen, die Nachträge stehen daneben.
- **Selbst ausgeführte Messung zur fehlenden Benutzerdimension** (Aufgabe 3,
wie vom Plan verlangt): `grep -rn "current_setting\|set_config" apps/api/src
apps/api/prisma/migrations` findet GENAU EINE Sitzungsvariable,
`app.current_tenant` (`prisma-tenant.extension.ts`,
`20260618112133_rls_policies`). Es gibt KEINE zweite Sitzungsvariable für
den Benutzer — Tabellen wie `TenderSavedSearch` haben deshalb strukturell
keine Möglichkeit, eine Benutzerdimension auf Datenbankebene durchzusetzen,
ohne eine solche Variable erst einzuführen. Ergebnis in (r4) der
Kritikschrift festgehalten, nicht gebaut.
- **Betriebsanleitung**: die eine Stelle, die #19 als offen führte, nennt
jetzt den Auflösungsstand und WINDOWS #24.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] `TenderRssFeedSource`-Wegwerf-UPDATE-Prüfung zielte auf
eine nicht existierende Spalte `label`**
- **Gefunden während:** Aufgabe 1, erster Lauf des Wegwerf-Werkzeugs nach
dem Hinzufügen der neuen UPDATE-Prüfung
- **Problem:** `tenderrssfeed-gebundenes-aendern-der-plattformzeile-abgelehnt`
versuchte `SET label = ...`, aber die im selben Abschnitt angelegte
Wegwerf-Tabelle hat keine `label`-Spalte (`id, url, userId, tenantId`) —
die Prüfung "bestand", aber aus dem FALSCHEN Grund (SQL-Fehler `42703`
statt der beabsichtigten RLS-Abweisung).
- **Fix:** Spalte auf `url` geändert, die tatsächlich existiert; danach
bestand die Prüfung aus dem richtigen Grund (`tenant_update_policy`
filtert die Zielzeile heraus, 0 betroffene Zeilen).
- **Dateien:** `apps/api/scripts/rls-scratch-check.mjs`
- **Commit:** `f4f3115`
**2. [Rule 1 - Bug] Implizites `any` beim Destrukturieren in `listRssFeeds`**
- **Gefunden während:** Aufgabe 2, Typprüfung nach dem Binden von
`listForUser`
- **Problem:** `feeds.map(({ userId: ownerUserId, ...rest }) => ...)` löste
`TS7031` aus, weil `feeds` durch die neue `forTenant(...) as any`-Bindung
implizit `any` wurde.
- **Fix:** explizite `(... : any)`-Annotation am Destrukturierungsparameter.
- **Dateien:** `apps/api/src/tenders/tenders.controller.ts`
- **Commit:** `6b23735`
### Package-Legitimitätsgatter
**3. [Rule 3 – ausgeschlossen, keine Installation]** `npx prisma migrate
deploy` versuchte beim Ausführen ungefragt, `prisma@8.0.0-rc.13`
herunterzuladen (weiter als die im Projekt gepinnte 6.19.3) — abgebrochen
(`pkill`), stattdessen `apps/api/node_modules/.bin/prisma` verwendet, die
lokale, im Lockfile gepinnte Version. Keine Installation fand statt, daher
kein Checkpoint nötig — dokumentiert, weil es beinahe eine unbeabsichtigte
Fremdversion in den Migrationslauf eingeschleust hätte.
Keine weiteren Abweichungen — alle übrigen Aufgaben wie im Plan beschrieben
ausgeführt.
## Known Stubs
Keine.
## Threat Flags
Keine neuen — das Threat-Model des Plans (T-JAB-01 bis T-JAB-15) deckt alle
in diesem Durchlauf berührten Flächen bereits ab; keine neue, dort nicht
erfasste Angriffsfläche entstanden.
## Self-Check: PASSED
- `apps/api/prisma/migrations/20260910120000_rls_widen_membership_grant_and_platform_read/migration.sql` — FOUND
- Commit `f4f3115` (Aufgabe 1) — FOUND (`git log --oneline --all | grep f4f3115`)
- Commit `6b23735` (Aufgabe 2) — FOUND
- Commit `03fb3bf` (Aufgabe 3) — FOUND
- `rls-scratch-check.mjs` meldet 74/74 bestanden gegen die lebende Datenbank — bestätigt (letzter Lauf vor diesem Bericht)
- `.planning/WINDOWS.md` besteht das plan-eigene Python-Kohärenzskript — bestätigt
- `npm --prefix apps/api run test` — 839/839 grün, `type-check` sauber — bestätigt