docs(quick-260910-jab): drei Datenbankregeln geschlossen und verifiziert
This commit is contained in:
+320
@@ -0,0 +1,320 @@
|
||||
---
|
||||
phase: quick-260910-jab
|
||||
plan: 01
|
||||
subsystem: mandantentrennung-datenbankrolle
|
||||
tags: [rls, postgresql, multi-tenancy, security, module-grants, groups, tenders]
|
||||
status: complete
|
||||
dependency-graph:
|
||||
requires: [T-JTS-02, T-JTS-03, WINDOWS-19]
|
||||
provides: [T-JAB-01..15-mitigations, rls-widen-migration-20260910120000]
|
||||
affects: [apps/api/prisma, apps/api/src/groups, apps/api/src/tenders, apps/api/src/module-registry, apps/api/scripts/rls-scratch-check.mjs]
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "Vier nach Befehl getrennte RLS-Policies (SELECT/INSERT/UPDATE/DELETE) statt einer permissiven USING-Klausel, wenn Lesen und Schreiben unterschiedliche Sichtbarkeitsregeln brauchen"
|
||||
- "Loch-behauptende Wegwerf-Pruefungen werden UMGEKEHRT statt geloescht, mit Verweis auf den alten Pruefungsnamen und die alte Befundkennung im Meldetext"
|
||||
- "Eine Zeile, auf der spaetere Pruefungen aufsetzen, wird nach einer Regelverschaerfung ueber die Wartungsrolle (BYPASSRLS) bereitgestellt, wenn der urspruengliche Erzeugungsweg (gebundenes INSERT) jetzt abgewiesen wird"
|
||||
key-files:
|
||||
created:
|
||||
- apps/api/prisma/migrations/20260910120000_rls_widen_membership_grant_and_platform_read/migration.sql
|
||||
modified:
|
||||
- apps/api/scripts/rls-scratch-check.mjs
|
||||
- apps/api/src/groups/migration-sql.spec.ts
|
||||
- apps/api/src/tenders/tender-rss-feed.service.ts
|
||||
- apps/api/src/tenders/tender-rss-feed.service.spec.ts
|
||||
- apps/api/src/tenders/tenders.controller.ts
|
||||
- apps/api/src/tenders/tenders.controller.spec.ts
|
||||
- apps/api/src/groups/groups.service.ts
|
||||
- apps/api/src/groups/module-grants.service.ts
|
||||
- apps/api/src/groups/module-grants.service.spec.ts
|
||||
- apps/api/src/module-registry/module-access.service.ts
|
||||
- apps/api/src/prisma/rls-coverage.spec.ts
|
||||
- docs/mandantentrennung-zugriffsklassifikation.md
|
||||
- docs/mandantentrennung-etappe2-fehlerrichtung.md
|
||||
- docs/mandantentrennung-datenbankrolle.md
|
||||
- .planning/WINDOWS.md
|
||||
decisions:
|
||||
- "GroupMembership-Regel prueft jetzt beide Seiten der Beziehung (Gruppe UND Benutzer, mit UND verknuepft), nach dem Join-Muster von PasswordResetToken"
|
||||
- "ModuleGrant-Regel prueft zusaetzlich beide moeglichen Ziele (Gruppe/Benutzer) mit Leer-Zulassung, weil D-04 Gruppe und Benutzer als Entweder-oder fuehrt"
|
||||
- "TenderRssFeedSource bekommt vier nach Befehl getrennte Policies statt einer, weil ein einzelner USING-Ausdruck auch UPDATE/DELETE mitregelt"
|
||||
- "SearchProvider bewusst NICHT angefasst — die WINDOWS-#19-Praemisse ist fuer dieses Modell widerlegt (kein Codeweg erzeugt eine mandantenlose Zeile)"
|
||||
- "TenderRssFeedSourceService.listForUser wird gebunden (einziger Anwendungscode-Pfad, den die Reparatur sonst still falsch gemacht haette); createPlatform/remove bleiben bewusst ungebunden"
|
||||
- "WINDOWS #24 neu angelegt: der Verwaltungsweg fuer plattformweite Zeilen unter der Anwendungsrolle fehlt weiterhin — verschwindet nicht mit der Schliessung von #19"
|
||||
metrics:
|
||||
duration: "~70min"
|
||||
completed: 2026-09-10
|
||||
actuals:
|
||||
tokens: 34770
|
||||
tasks: 3
|
||||
commits: 3
|
||||
plan_head_before: 93444aa91e8fb0739ee5bbf023f7ccbbd3cee38e
|
||||
---
|
||||
|
||||
# Quick 260910-jab: Die drei zu kurz greifenden Datenbankregeln schliessen — Summary
|
||||
|
||||
Eine handgeschriebene, lokal angewandte PostgreSQL-Migration schliesst
|
||||
T-JTS-02 (GroupMembership prueft nur die Gruppenseite), T-JTS-03 (ModuleGrant
|
||||
prueft nur die Mandantenkennung der Zeile, nicht wohin sie zeigt) und
|
||||
WINDOWS #19 (plattformweite TenderRssFeedSource-Zeilen waeren nach dem
|
||||
Scharfschalten fuer JEDEN Mandanten unsichtbar gewesen); das Wegwerf-Werkzeug
|
||||
misst alle drei Reparaturen an der lebenden Datenbank, mit den drei
|
||||
loch-behauptenden Pruefungen umgekehrt statt geloescht.
|
||||
|
||||
## Ausgangslage — selbst gemessen (nicht uebernommen)
|
||||
|
||||
Zu Beginn von Aufgabe 1 frisch gemessen, gegen HEAD `93444aa`:
|
||||
|
||||
- **Tests:** 833 bestanden, 56 Dateien (`npm --prefix apps/api run test`)
|
||||
- **Typpruefung:** sauber (`npm --prefix apps/api run type-check`)
|
||||
- **Wegwerf-Werkzeug:** 66/66 Pruefungen bestanden
|
||||
(`rls-scratch-check.mjs` gegen `tessera-ctl-db-1`, Adresse `172.19.0.2`,
|
||||
zur Laufzeit ermittelt)
|
||||
|
||||
Diese drei Zahlen decken sich mit der in der Aufgabenstellung genannten
|
||||
Baseline — hier trotzdem selbst nachgemessen, nicht abgeschrieben (die
|
||||
Vorgabe der Aufgabenstellung verlangt genau das).
|
||||
|
||||
## Am Ende gemessen
|
||||
|
||||
- **Tests:** 839 bestanden, 56 Dateien (+6, alle aus dem neuen
|
||||
`migration-sql.spec.ts`-Beschreibungsblock)
|
||||
- **Typpruefung:** sauber
|
||||
- **Wegwerf-Werkzeug:** 74/74 Pruefungen bestanden (+8: siehe Aufschlüsselung
|
||||
unten unter "Neue/umgekehrte Pruefungen")
|
||||
|
||||
## Bestandszaehlung ueber Mandantengrenzen (Befund H, vor Anwendung der Migration)
|
||||
|
||||
Gegen die lokale Datenbank `tessera` ausgefuehrt, vor dem Anwenden der neuen
|
||||
Migration:
|
||||
|
||||
```
|
||||
memberships-cross-tenant | 0
|
||||
grants-cross-tenant-group | 0
|
||||
grants-cross-tenant-user | 0
|
||||
total-memberships | 3
|
||||
total-grants | 4
|
||||
total-tenants | 1
|
||||
tenderrssfeed-rows | 2
|
||||
tenderrssfeed-platform-rows | 1
|
||||
searchprovider-rows | 0
|
||||
searchprovider-null-tenant | 0
|
||||
```
|
||||
|
||||
Ergebnis null — es gibt lokal keine Zeile, die von der verschärften Regel
|
||||
unsichtbar würde. Für das Testsystem ist das NICHT gemessen und muss vor
|
||||
Etappe 4 als eigene Vorabprüfung wiederholt werden (siehe Befund H im Plan).
|
||||
|
||||
## Was gebaut wurde
|
||||
|
||||
### Aufgabe 1 — Migration + Wegwerf-Werkzeug
|
||||
|
||||
Neue Migration `20260910120000_rls_widen_membership_grant_and_platform_read`
|
||||
(lokal angewandt via `prisma migrate deploy` — **nicht** über `npx prisma`,
|
||||
das versuchte ungefragt Prisma 8.0.0-rc.13 herunterzuladen und wurde
|
||||
abgebrochen; stattdessen `apps/api/node_modules/.bin/prisma`, die im Projekt
|
||||
gepinnte Version 6.19.3, dieselbe, die `rls-scratch-check.mjs` selbst
|
||||
verwendet):
|
||||
|
||||
1. **`GroupMembership`** — die Regel prüft jetzt `groupId IN (...)` UND
|
||||
`userId IN (...)`, beide gegen den laufenden Mandanten.
|
||||
2. **`ModuleGrant`** — die Regel prüft weiterhin `tenantId = current_tenant_id()`
|
||||
UND zusätzlich `groupId IS NULL OR groupId IN (...)` UND
|
||||
`userId IS NULL OR userId IN (...)`.
|
||||
3. **`TenderRssFeedSource`** — vier Regeln statt einer:
|
||||
`tenant_platform_read_policy` (SELECT, schließt `tenantId IS NULL` ein),
|
||||
`tenant_insert_policy`/`tenant_update_policy`/`tenant_delete_policy`
|
||||
(verlangen ausnahmslos einen Mandanten).
|
||||
4. **`SearchProvider`** — unverändert, nur der erklärende Absatz im
|
||||
Migrationskopf.
|
||||
|
||||
Regelliste der lebenden Datenbank nach Anwendung (`pg_policies`, Auszug):
|
||||
|
||||
```
|
||||
GroupMembership#tenant_isolation_policy#ALL#(groupId IN (...) AND userId IN (...))
|
||||
ModuleGrant#tenant_isolation_policy#ALL#(tenantId = ... AND (groupId IS NULL OR ...) AND (userId IS NULL OR ...))
|
||||
SearchProvider#tenant_isolation_policy#ALL#(tenantId = current_tenant_id())
|
||||
TenderRssFeedSource#tenant_delete_policy#DELETE#(tenantId = current_tenant_id())
|
||||
TenderRssFeedSource#tenant_insert_policy#INSERT##WITH CHECK (tenantId = current_tenant_id())
|
||||
TenderRssFeedSource#tenant_platform_read_policy#SELECT#(tenantId = current_tenant_id() OR tenantId IS NULL)
|
||||
TenderRssFeedSource#tenant_update_policy#UPDATE#USING(...)#WITH CHECK(...)
|
||||
```
|
||||
|
||||
**Neue/umgekehrte Pruefungen im Wegwerf-Werkzeug** (66 → 74):
|
||||
|
||||
| Kennung | Art | Ergebnis |
|
||||
|---|---|---|
|
||||
| `groupmembership-schreiben-fremder-benutzer-abgelehnt` | Umkehr (T-JTS-02) | bestanden — Insert mit echtem `user-b` abgewiesen |
|
||||
| `groupmembership-fremder-benutzer-ueber-die-wartungsrolle-weiterhin-moeglich` | neu | bestanden — Gegenmessung über Wartungsrolle gelingt |
|
||||
| `modulegrant-fremde-gruppe-abgelehnt` | Umkehr (T-JTS-03) | bestanden |
|
||||
| `modulegrant-fremder-benutzer-abgelehnt` | neu (zweiter D-04-Zweig) | bestanden |
|
||||
| `modulegrant-fremde-gruppe-ueber-die-wartungsrolle-weiterhin-moeglich` | neu (legt `grant-foreign-group` an) | bestanden |
|
||||
| `tenderrssfeed-plattformzeile-gebunden-sichtbar` | Umkehr (WINDOWS #19) | bestanden |
|
||||
| `tenderrssfeed-eigene-zeile-gebunden-weiterhin-sichtbar` | neu | bestanden |
|
||||
| `tenderrssfeed-ungebunden-nur-die-plattformzeile` | neu (Befund F) | bestanden |
|
||||
| `tenderrssfeed-gebundenes-aendern-der-plattformzeile-abgelehnt` | neu | bestanden |
|
||||
| `tenderrssfeed-gebundenes-loeschen-der-plattformzeile-abgelehnt` | neu | bestanden |
|
||||
| `searchprovider-mandantenlose-zeile-bleibt-unter-jedem-kontext-unsichtbar` | neu (eigener Bereich) | bestanden |
|
||||
|
||||
Extraktion umgeleitet: `readRlsWidenMigrationSql()` liest die neue Datei
|
||||
für `GroupMembership`/`ModuleGrant`; `extractAllPolicySql()` (neu, mit
|
||||
globalem Regex-Flag) liefert alle vier `TenderRssFeedSource`-Policies. Die
|
||||
Meldung von `gruppenpfad-gebunden-schliesst-die-fremde-gruppe-aus` im
|
||||
Bereich `module-registry` ist richtiggestellt (behauptete vorher fälschlich,
|
||||
die Regel lasse die fremde Zeile durch).
|
||||
|
||||
### Aufgabe 2 — `listForUser` binden
|
||||
|
||||
`TenderRssFeedSourceService.listForUser(userId, tenantId)` läuft jetzt über
|
||||
`forTenant()`. `TendersController.listRssFeeds` reicht die Mandantenkennung
|
||||
aus dem Sitzungsnachweis durch (Aufrufform geprüft, nicht die Antwort).
|
||||
`createPlatform`/`remove` bleiben bewusst ungebunden. Fünf Aufzeichnungen im
|
||||
Quelltext (drei Kopfkommentare in `tender-rss-feed.service.ts`,
|
||||
`module-access.service.ts`, `groups.service.ts`, `module-grants.service.ts`,
|
||||
`rls-coverage.spec.ts`) nennen die neue Migration und beschreiben die neue
|
||||
Regel statt der alten. Die Mandanten-Gegenprüfung
|
||||
(`assertTargetBelongsToTenant`) bleibt unverändert bestehen — zwei bestehende
|
||||
Testfälle in `module-grants.service.spec.ts` wurden um einen Kommentar
|
||||
ergänzt, der festhält, warum sie nach der Regeländerung NICHT entbehrlich
|
||||
geworden sind.
|
||||
|
||||
**Deviation, gemessen statt geglaubt (siehe `<output>`-Vorgabe des Plans):**
|
||||
die Behauptung "kein Anwendungscode muss sich ändern" trifft für GENAU EINEN
|
||||
Pfad nicht zu — `TenderRssFeedSourceService.listForUser`. Vor der
|
||||
Regeländerung lieferte der ungebundene Pfad nach dem Scharfschalten NICHTS
|
||||
(gemessen: `tenderrssfeed-ungebunden-nur-die-plattformzeile` würde ohne
|
||||
Bindung 0 statt 1 Zeile liefern). Nach der Regeländerung liefert derselbe
|
||||
ungebundene Pfad NUR die plattformweiten Zeilen — eine kurze, glaubhafte
|
||||
Teilantwort statt einer schreienden Leere. Das Binden gehört deshalb zur
|
||||
Reparatur selbst, nicht daneben: ohne diese Bindung hätte 260910-jab einen
|
||||
Pfad still von "meldet sich laut" auf "täuscht Vollständigkeit vor"
|
||||
verschlechtert.
|
||||
|
||||
**Falsifizierungsnachweis** (Bindungsaufruf zurückgenommen, Test rot
|
||||
gesehen, Rücknahme zurückgenommen):
|
||||
|
||||
```
|
||||
Backup genommen → const tenantPrisma = ... entfernt, this.prisma direkt verwendet
|
||||
npm --prefix apps/api run test -- src/tenders/tender-rss-feed.service.spec.ts
|
||||
|
||||
× listForUser() bindet — forTenant() wird mit der uebergebenen Mandantenkennung aufgerufen
|
||||
AssertionError: expected "spy" to be called with arguments: [ {…(4)}, 'tenant-a' ]
|
||||
Number of calls: 0
|
||||
|
||||
Test Files 1 failed (1)
|
||||
Tests 1 failed | 30 passed (31)
|
||||
|
||||
→ Rücknahme rückgängig gemacht (Backup wiederhergestellt), erneuter Lauf: 31/31 grün
|
||||
```
|
||||
|
||||
Genau EIN Test wurde rot, wie erwartet — kein anderer Test hing versehentlich
|
||||
an dieser Bindung.
|
||||
|
||||
**Rule 1 (Auto-Fix):** `listRssFeeds` destrukturierte `feeds.map(({userId, ...rest}) => ...)`.
|
||||
Da `listForUser` jetzt über `forTenant(...) as any` läuft, wurde `feeds`
|
||||
implizit `any`, und TypeScript meldete `TS7031: Binding element 'ownerUserId'
|
||||
implicitly has an 'any' type`. Behoben mit expliziter `(... : any)`-Annotation,
|
||||
Muster aus anderen `.map((g: any) => ...)`-Stellen derselben Datei.
|
||||
Typpruefung danach wieder sauber.
|
||||
|
||||
### Aufgabe 3 — Aktenstand kohärent
|
||||
|
||||
- **`.planning/WINDOWS.md`**: #19 auf `fixed` (Tabelle + JSON-Block,
|
||||
`resolved_at: 2026-09-10T12:35:40.000Z`), Beleg nennt die neue Migration
|
||||
und die Prüfungen für beide Fehlerrichtungen des Lese-/Schreibsplits.
|
||||
#18/#20/#21/#22/#23 unverändert offen. Neuer Eintrag **#24** (offen, Kind
|
||||
`deviation`): der Verwaltungsweg für plattformweite Zeilen unter der
|
||||
Anwendungsrolle fehlt — weder Anlegen noch Entfernen ist unter der
|
||||
Anwendungsrolle möglich, in der alten wie in der neuen Regel. Kopfzahlen
|
||||
aus dem JSON-Block abgeleitet und mit einem eigenen Python-Skript
|
||||
gegengeprüft (identisch zum Verify-Skript des Plans): 6 offen, 17 behoben,
|
||||
1 zurückgestellt, 24 gesamt.
|
||||
- **Klassifikation**: #19-Block beantwortet, die vier betroffenen
|
||||
Bestandsaufnahme-Zeilen nachgezogen, Übersichtszeile `tenders` (35/27,
|
||||
gemessen mit den beiden im Dokument selbst geführten grep-Anweisungen) und
|
||||
Summenzeile (107/135) neu aus dem Quelltext abgeleitet — beide mit dem
|
||||
exakten `awk`-Prüfskript des Plans gegengeprüft.
|
||||
- **Kritikschrift**: neuer Abschnitt `## Regelschluss T-JTS-02, T-JTS-03 und
|
||||
WINDOWS #19` mit (r1) tatsächlich beobachteter Ausgabe + Regelliste aus
|
||||
der lebenden Datenbank, (r2) Signaltabelle mit beiden Fehlerrichtungen je
|
||||
Regel, (r3) den Stellen, an denen Leere weiterhin als Abwesenheit gedeutet
|
||||
wird (inklusive der neuen Stelle aus Befund F), (r4) was dieser Durchlauf
|
||||
nicht löst, (r5) was er nicht anfasst. Fünf überholte Bestandsstellen mit
|
||||
Nachträgen versehen (der Punkt in (g4), der Punkt in (t4), die
|
||||
Signaltabellenzeile der drei RSS-Pfade, DREI aufgezeichnete
|
||||
Werkzeugausgaben — eine mehr als die im Plan als Minimum genannten zwei,
|
||||
weil beim Durchsuchen eine dritte literale Zitatstelle im Abschnitt
|
||||
`module-registry` gefunden wurde — und der Punkt in (m4)). Alle alten
|
||||
Messprotokolle bleiben wörtlich stehen, die Nachträge stehen daneben.
|
||||
- **Selbst ausgeführte Messung zur fehlenden Benutzerdimension** (Aufgabe 3,
|
||||
wie vom Plan verlangt): `grep -rn "current_setting\|set_config" apps/api/src
|
||||
apps/api/prisma/migrations` findet GENAU EINE Sitzungsvariable,
|
||||
`app.current_tenant` (`prisma-tenant.extension.ts`,
|
||||
`20260618112133_rls_policies`). Es gibt KEINE zweite Sitzungsvariable für
|
||||
den Benutzer — Tabellen wie `TenderSavedSearch` haben deshalb strukturell
|
||||
keine Möglichkeit, eine Benutzerdimension auf Datenbankebene durchzusetzen,
|
||||
ohne eine solche Variable erst einzuführen. Ergebnis in (r4) der
|
||||
Kritikschrift festgehalten, nicht gebaut.
|
||||
- **Betriebsanleitung**: die eine Stelle, die #19 als offen führte, nennt
|
||||
jetzt den Auflösungsstand und WINDOWS #24.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 1 - Bug] `TenderRssFeedSource`-Wegwerf-UPDATE-Prüfung zielte auf
|
||||
eine nicht existierende Spalte `label`**
|
||||
- **Gefunden während:** Aufgabe 1, erster Lauf des Wegwerf-Werkzeugs nach
|
||||
dem Hinzufügen der neuen UPDATE-Prüfung
|
||||
- **Problem:** `tenderrssfeed-gebundenes-aendern-der-plattformzeile-abgelehnt`
|
||||
versuchte `SET label = ...`, aber die im selben Abschnitt angelegte
|
||||
Wegwerf-Tabelle hat keine `label`-Spalte (`id, url, userId, tenantId`) —
|
||||
die Prüfung "bestand", aber aus dem FALSCHEN Grund (SQL-Fehler `42703`
|
||||
statt der beabsichtigten RLS-Abweisung).
|
||||
- **Fix:** Spalte auf `url` geändert, die tatsächlich existiert; danach
|
||||
bestand die Prüfung aus dem richtigen Grund (`tenant_update_policy`
|
||||
filtert die Zielzeile heraus, 0 betroffene Zeilen).
|
||||
- **Dateien:** `apps/api/scripts/rls-scratch-check.mjs`
|
||||
- **Commit:** `f4f3115`
|
||||
|
||||
**2. [Rule 1 - Bug] Implizites `any` beim Destrukturieren in `listRssFeeds`**
|
||||
- **Gefunden während:** Aufgabe 2, Typprüfung nach dem Binden von
|
||||
`listForUser`
|
||||
- **Problem:** `feeds.map(({ userId: ownerUserId, ...rest }) => ...)` löste
|
||||
`TS7031` aus, weil `feeds` durch die neue `forTenant(...) as any`-Bindung
|
||||
implizit `any` wurde.
|
||||
- **Fix:** explizite `(... : any)`-Annotation am Destrukturierungsparameter.
|
||||
- **Dateien:** `apps/api/src/tenders/tenders.controller.ts`
|
||||
- **Commit:** `6b23735`
|
||||
|
||||
### Package-Legitimitätsgatter
|
||||
|
||||
**3. [Rule 3 – ausgeschlossen, keine Installation]** `npx prisma migrate
|
||||
deploy` versuchte beim Ausführen ungefragt, `prisma@8.0.0-rc.13`
|
||||
herunterzuladen (weiter als die im Projekt gepinnte 6.19.3) — abgebrochen
|
||||
(`pkill`), stattdessen `apps/api/node_modules/.bin/prisma` verwendet, die
|
||||
lokale, im Lockfile gepinnte Version. Keine Installation fand statt, daher
|
||||
kein Checkpoint nötig — dokumentiert, weil es beinahe eine unbeabsichtigte
|
||||
Fremdversion in den Migrationslauf eingeschleust hätte.
|
||||
|
||||
Keine weiteren Abweichungen — alle übrigen Aufgaben wie im Plan beschrieben
|
||||
ausgeführt.
|
||||
|
||||
## Known Stubs
|
||||
|
||||
Keine.
|
||||
|
||||
## Threat Flags
|
||||
|
||||
Keine neuen — das Threat-Model des Plans (T-JAB-01 bis T-JAB-15) deckt alle
|
||||
in diesem Durchlauf berührten Flächen bereits ab; keine neue, dort nicht
|
||||
erfasste Angriffsfläche entstanden.
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
- `apps/api/prisma/migrations/20260910120000_rls_widen_membership_grant_and_platform_read/migration.sql` — FOUND
|
||||
- Commit `f4f3115` (Aufgabe 1) — FOUND (`git log --oneline --all | grep f4f3115`)
|
||||
- Commit `6b23735` (Aufgabe 2) — FOUND
|
||||
- Commit `03fb3bf` (Aufgabe 3) — FOUND
|
||||
- `rls-scratch-check.mjs` meldet 74/74 bestanden gegen die lebende Datenbank — bestätigt (letzter Lauf vor diesem Bericht)
|
||||
- `.planning/WINDOWS.md` besteht das plan-eigene Python-Kohärenzskript — bestätigt
|
||||
- `npm --prefix apps/api run test` — 839/839 grün, `type-check` sauber — bestätigt
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
---
|
||||
phase: quick-260910-jab
|
||||
verified: 2026-09-10T14:55:00Z
|
||||
status: passed
|
||||
score: 11/11 must-haves verified
|
||||
covered_files:
|
||||
- ".planning/WINDOWS.md"
|
||||
- ".planning/quick/260910-jab-mandantentrennung-die-drei-zu-kurz-greif/260910-jab-PLAN.md"
|
||||
- ".planning/quick/260910-jab-mandantentrennung-die-drei-zu-kurz-greif/260910-jab-SUMMARY.md"
|
||||
- "apps/api/prisma/migrations/20260910120000_rls_widen_membership_grant_and_platform_read/migration.sql"
|
||||
- "apps/api/scripts/rls-scratch-check.mjs"
|
||||
- "apps/api/src/groups/groups.service.ts"
|
||||
- "apps/api/src/groups/migration-sql.spec.ts"
|
||||
- "apps/api/src/groups/module-grants.service.spec.ts"
|
||||
- "apps/api/src/groups/module-grants.service.ts"
|
||||
- "apps/api/src/module-registry/module-access.service.ts"
|
||||
- "apps/api/src/prisma/rls-coverage.spec.ts"
|
||||
- "apps/api/src/tenders/tender-rss-feed.service.spec.ts"
|
||||
- "apps/api/src/tenders/tender-rss-feed.service.ts"
|
||||
- "apps/api/src/tenders/tenders.controller.spec.ts"
|
||||
- "apps/api/src/tenders/tenders.controller.ts"
|
||||
- "docs/mandantentrennung-datenbankrolle.md"
|
||||
- "docs/mandantentrennung-etappe2-fehlerrichtung.md"
|
||||
- "docs/mandantentrennung-zugriffsklassifikation.md"
|
||||
covered_digest: "v1:sha256:4ba76478ddf3d04462ca28c23051e03f469ffd731ae9db235c5cd3e25c803e6f"
|
||||
behavior_unverified: 0
|
||||
overrides_applied: 0
|
||||
---
|
||||
|
||||
# Quick 260910-jab: Die drei zu kurz greifenden Datenbankregeln — Verification Report
|
||||
|
||||
**Task Goal:** Close T-JTS-02 (`GroupMembership` checked only the group side),
|
||||
T-JTS-03 (`ModuleGrant` checked its own tenant but not the group/user it
|
||||
points at) and WINDOWS #19 (nullable `tenantId` would hide platform-wide rows
|
||||
from every tenant after cutover) — while leaving the written record coherent.
|
||||
|
||||
**Verified:** 2026-09-10, independently, against the live database container
|
||||
`tessera-ctl-db-1` (address `172.19.0.2`, resolved fresh via `docker inspect`)
|
||||
and the working tree at commits `f4f3115`, `6b23735`, `03fb3bf` (base `93444aa`).
|
||||
|
||||
**Status:** passed
|
||||
|
||||
## Summary
|
||||
|
||||
Every priority item in the verification brief was independently re-checked,
|
||||
including three destructive falsification experiments (temporarily reverting
|
||||
each of the three fixed policies in the migration file and re-running the
|
||||
scratch tool against a throwaway database) to prove the inverted checks
|
||||
would actually fail if the fix regressed. All three did. Full test suite
|
||||
(839/839), type-check (clean), and the scratch tool (74/74) were re-run
|
||||
independently and match the SUMMARY's claims exactly. No discrepancy found.
|
||||
|
||||
## Goal Achievement
|
||||
|
||||
### Observable Truths
|
||||
|
||||
| # | Truth | Status | Evidence |
|
||||
|---|-------|--------|----------|
|
||||
| 1 | `GroupMembership` policy checks both sides (group AND user) | ✓ VERIFIED | Live `pg_policies`: `("groupId" IN (...Group...)) AND ("userId" IN (...User...))`. Falsification: reverted to group-only text in migration file, reran scratch tool → `groupmembership-schreiben-fremder-benutzer-abgelehnt` FAILED as expected, then restored (file byte-identical after restore) |
|
||||
| 2 | `ModuleGrant` policy checks both possible targets (group OR user) with null-allowance, both D-04 branches measured | ✓ VERIFIED | Live `pg_policies` shows `tenantId = ... AND (groupId IS NULL OR ...) AND (userId IS NULL OR ...)`. Falsification: reverted to tenant-only text → both `modulegrant-fremde-gruppe-abgelehnt` and `modulegrant-fremder-benutzer-abgelehnt` FAILED as expected, then restored |
|
||||
| 3 | `assertTargetBelongsToTenant` in `module-grants.service.ts` unchanged, held by a test that goes red if removed | ✓ VERIFIED | 3 call sites present (`grep -n`); two dedicated spec cases in `module-grants.service.spec.ts:290-312` with an explicit comment (lines 281-289) stating why they'd go red if the app-level guard were dropped |
|
||||
| 4 | `TenderRssFeedSource` read/write split: bound read returns own + platform rows; write (insert/update/delete) still requires a tenant; both error directions (too strict / too loose) separately measured | ✓ VERIFIED | Live `pg_policies`: 4 command-separated policies; only `tenant_platform_read_policy` (SELECT) contains `IS NULL`, none of insert/update/delete do. Scratch tool: `tenderrssfeed-plattformzeile-gebunden-sichtbar` + `tenderrssfeed-eigene-zeile-gebunden-weiterhin-sichtbar` (too-strict direction) and `tenderrssfeed-gebundenes-aendern-...-abgelehnt` + `...-loeschen-...-abgelehnt` (too-loose direction), all passed independently |
|
||||
| 5 | The three hole-claiming scratch-tool checks are INVERTED, not deleted/loosened, named for the new truth, referencing the old finding | ✓ VERIFIED | Old check names (`...-nicht-verhindert`, `...-trotz-eigener-mandantenkennung-erlaubt`, `...-unter-jedem-mandanten-unsichtbar`) appear only as backward-references inside the new checks' message text, not as separate passing assertions (`grep` over the whole tool file). New names assert rejection/visibility, matching the fix |
|
||||
| 6 | `grant-foreign-group` row (dependency for two `module-registry` checks) still gets created, now via the maintenance role, and neither dependent check passes for the wrong reason | ✓ VERIFIED | `runGroupsAreaChecks` creates it via `withAdminPrisma`/BYPASSRLS (line ~913-917) before `runModuleRegistryAreaChecks` runs (both share the same scratch DB in `main()`); `gruppenpfad-gebunden-schliesst-die-fremde-gruppe-aus` message text now correctly says the row was excluded because the maintenance-role insert bypassed a rule that would otherwise reject it — not because the rule "lets it through" |
|
||||
| 7 | The claim "no application code needs to change" was measured, not assumed; the one path that does (`listForUser`) is bound | ✓ VERIFIED | `listForUser` uses `forTenant(this.prisma, tenantId)`; controller passes `tenantId` from session context (`tenders.controller.ts:267-268`). Falsification: reverted the binding, ran `tender-rss-feed.service.spec.ts` → exactly 1 test failed (`listForUser() bindet...`), 30 passed, matching SUMMARY's claim precisely; restored |
|
||||
| 8 | Every record describing one of the three old rules now tells the truth, naming the new migration | ✓ VERIFIED | All 4 in-source header comments (`module-access.service.ts`, `groups.service.ts`, `module-grants.service.ts`, `tender-rss-feed.service.ts` x3) and `rls-coverage.spec.ts` description line updated and name `20260910120000_rls_widen_membership_grant_and_platform_read`; classification doc `tenders` row (35/27) and sum row (107/135) independently re-derived from source via the same grep the doc cites and matched exactly; `rls-access-inventory.spec.ts` (10/10) passes |
|
||||
| 9 | WINDOWS #19 closed with evidence naming the migration and both error-direction checks; #18/#20/#21/#23 remain open; what's NOT solved recorded as its own open entry that doesn't vanish with #19 | ✓ VERIFIED | #19 `status: fixed`, `resolved_at` set, description names the migration and 5 named checks, explicitly frames SearchProvider half as REFUTED PREMISE not solved problem. #18/#20/#21/#23 byte-identical to base commit. New entry #24 (open, `deviation`) names both blocked paths (`createPlatform`/`remove`) and states explicitly this predates 260910-jab. Header counts (6 open/17 fixed/1 waived/24 total) match a fresh count of the JSON-equivalent table rows |
|
||||
| 10 | Switch stays OFF; `DATABASE_URL` still points at role `tessera` | ✓ VERIFIED | `docker-compose.yml:33` and `.env.example:2` both show role `tessera` (no `_app` suffix), unchanged from base |
|
||||
| 11 | Baseline held: test count doesn't drop, type-check clean, scratch tool all-passed, at every task boundary | ✓ VERIFIED | Independently re-ran: 839/839 tests (56 files), `tsc --noEmit` clean, `rls-scratch-check.mjs` 74/74 passed — all match SUMMARY's claimed end-state exactly |
|
||||
|
||||
**Score:** 11/11 truths verified (0 present-but-behavior-unverified)
|
||||
|
||||
### Falsification Experiments (independently performed by this verifier)
|
||||
|
||||
Three destructive experiments were run directly against the scratch tool /
|
||||
migration file, each reverted afterward and confirmed byte-identical to the
|
||||
original:
|
||||
|
||||
1. Reverted `GroupMembership` policy text to group-only → `groupmembership-schreiben-fremder-benutzer-abgelehnt` FAILED (1/74). Restored.
|
||||
2. Reverted `ModuleGrant` policy text to tenant-only → `modulegrant-fremde-gruppe-abgelehnt` and `modulegrant-fremder-benutzer-abgelehnt` both FAILED (2/74). Restored.
|
||||
3. Reverted `TenderRssFeedSource` to the old single-policy form → the tenders-area extraction guard correctly aborted with `tenders-policies-aus-migration-gefunden: FEHLGESCHLAGEN` (fail-closed, not a silent wrong measurement) rather than measuring the old text as if it were current. Restored.
|
||||
4. Reverted `TenderRssFeedSourceService.listForUser`'s `forTenant()` binding → exactly 1 test failed (`listForUser() bindet — forTenant() wird mit der uebergebenen Mandantenkennung aufgerufen`), 30 passed. Restored.
|
||||
|
||||
All four confirm the guarding assertions (tests and scratch checks) genuinely
|
||||
detect the regression they claim to detect — not passing by construction.
|
||||
|
||||
### Required Artifacts
|
||||
|
||||
| Artifact | Expected | Status | Details |
|
||||
|----------|----------|--------|---------|
|
||||
| `apps/api/prisma/migrations/20260910120000_rls_widen_membership_grant_and_platform_read/migration.sql` | New migration, applied locally | ✓ VERIFIED | Exists, `prisma migrate status` reports up to date, live `pg_policies` text matches file text exactly (diffed by hand for GroupMembership/ModuleGrant/TenderRssFeedSource/SearchProvider) |
|
||||
| `apps/api/scripts/rls-scratch-check.mjs` | 3 inverted checks + gegenmessungen + 4 command directions + extraction redirect | ✓ VERIFIED | `readRlsWidenMigrationSql()`/`extractAllPolicySql()` wired into `runGroupsAreaChecks`/`runTendersAreaChecks`; old extraction (`readGroupsRlsPoliciesMigrationSql`) no longer used for GroupMembership/ModuleGrant |
|
||||
| `apps/api/src/groups/migration-sql.spec.ts` | New describe block, text-only | ✓ VERIFIED | `rls_widen_membership_grant_and_platform_read migration.sql` block present, 6 tests, all pure regex/string assertions, no DB |
|
||||
| `apps/api/src/tenders/tender-rss-feed.service.ts` | `listForUser` bound, 3 header comments corrected | ✓ VERIFIED | Confirmed by reading; `createPlatform`/`remove` deliberately still unbound with corrected reasoning pointing at WINDOWS #24 |
|
||||
| `apps/api/src/tenders/tenders.controller.ts` + both spec files | tenant pass-through, two-client proof | ✓ VERIFIED | `extractTriageContext(req)` supplies `tenantId`; two-client proof in spec uses `__makeBoundClient`/`boundCallLog`, not an identity mock (unbound fake doesn't log, bound one does) |
|
||||
| `apps/api/src/groups/groups.service.ts`, `module-grants.service.ts`, `module-access.service.ts`, `rls-coverage.spec.ts` | 4 in-source records corrected | ✓ VERIFIED | Read all 4 — each names the new migration and describes the new rule while explicitly retaining the app-level guard as "second net" |
|
||||
| `docs/mandantentrennung-zugriffsklassifikation.md` | #19 block answered, 4 inventory rows updated, overview/sum rows re-derived | ✓ VERIFIED | tenders row 35/27 matches independently re-run grep exactly; sum row 107/135 matches; `rls-access-inventory.spec.ts` passes (machine-checked binding to this doc) |
|
||||
| `docs/mandantentrennung-etappe2-fehlerrichtung.md` | New `## Regelschluss...` section, 5 subsections (r1-r5), annotations at superseded spots | ✓ VERIFIED | All 5 subsections present; r1 quotes an actual 74-check run (verified to match reality); r2 signal table covers both error directions per rule; old measurement blocks preserved verbatim with adjacent `NACHTRAG (260910-jab)` annotations (6+ locations found, not rewritten) |
|
||||
| `docs/mandantentrennung-datenbankrolle.md` | The one #19-as-open spot updated | ✓ VERIFIED | Line 91-94 now says GESCHLOSSEN with migration name |
|
||||
| `.planning/WINDOWS.md` | #19 fixed, #24 new, counters derived | ✓ VERIFIED | Header counts (6/1/17/24) match row-by-row count; #18/#20/#21/#23 byte-identical to base |
|
||||
|
||||
### Key Link Verification
|
||||
|
||||
| From | To | Via | Status | Details |
|
||||
|------|-----|-----|--------|---------|
|
||||
| `rls-scratch-check.mjs` extraction | `20260910120000_...` migration file | `readRlsWidenMigrationSql()` + `extractAllPolicySql()` | ✓ WIRED | Confirmed by falsification #3 above: reverting the migration file's TenderRssFeedSource text made the tool's own extraction guard fire, proving it reads the live file, not a cached/hardcoded value |
|
||||
| Groups-area `grant-foreign-group` provisioning | `module-registry`-area dependent checks | shared scratch DB across `main()`'s sequential area-check calls | ✓ WIRED | Confirmed both dependent checks (`gruppenpfad-gebunden-schliesst-die-fremde-gruppe-aus`, `gruppenpfad-ueber-die-wartungsrolle-liefert-die-fremde-gruppe-mit`) pass in the live run and read code confirming ordering |
|
||||
| `TendersController.listRssFeeds` | `TenderRssFeedSourceService.listForUser` | `extractTriageContext(req).tenantId` passthrough | ✓ WIRED | Read at `tenders.controller.ts:266-268` |
|
||||
| `TenderRssFeedSource` command-separated policies | live database | migration applied via `prisma migrate deploy`, not `migrate dev`/`reset` | ✓ WIRED | `prisma migrate status` → up to date; `pg_policies` text matches file text |
|
||||
|
||||
### Behavioral Spot-Checks / Probe Execution
|
||||
|
||||
| Behavior | Command | Result | Status |
|
||||
|----------|---------|--------|--------|
|
||||
| Scratch tool passes fully | `node apps/api/scripts/rls-scratch-check.mjs` against live container | `Alle 74 Pruefungen bestanden.` | ✓ PASS |
|
||||
| Full test suite | `npm --prefix apps/api run test` | 839/839, 56 files | ✓ PASS |
|
||||
| Type check | `npm --prefix apps/api run type-check` | clean, no output | ✓ PASS |
|
||||
| `rls-access-inventory.spec.ts` (machine clamp doc↔code) | `npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts` | 10/10 | ✓ PASS |
|
||||
| Falsification: GroupMembership reverted | scratch tool against reverted migration text | 1/74 failed (correct check) | ✓ PASS (as regression detector) |
|
||||
| Falsification: ModuleGrant reverted | scratch tool against reverted migration text | 2/74 failed (correct checks) | ✓ PASS (as regression detector) |
|
||||
| Falsification: TenderRssFeedSource reverted | scratch tool against reverted migration text | extraction guard fired, 1/62 failed | ✓ PASS (fail-closed, not silently wrong) |
|
||||
| Falsification: listForUser binding reverted | `npm --prefix apps/api run test -- src/tenders/tender-rss-feed.service.spec.ts` | 1/31 failed (correct test) | ✓ PASS (as regression detector) |
|
||||
|
||||
### Requirements Coverage
|
||||
|
||||
| Requirement | Source Plan | Description | Status | Evidence |
|
||||
|-------------|-------------|-------------|--------|----------|
|
||||
| WINDOWS-19 | 260910-jab-PLAN.md | Platform-wide rows visible to every tenant, not just their own, after cutover | ✓ SATISFIED | 4-policy split live, both error directions measured and independently falsified |
|
||||
| T-JTS-02 | 260910-jab-PLAN.md | `GroupMembership` policy checks user side too | ✓ SATISFIED | Live policy text confirmed, falsified |
|
||||
| T-JTS-03 | 260910-jab-PLAN.md | `ModuleGrant` policy checks referenced group/user, not just own tenant | ✓ SATISFIED | Live policy text confirmed, falsified, both D-04 branches measured |
|
||||
|
||||
### Anti-Patterns Found
|
||||
|
||||
None. Searched all 16 files in `files_modified` for `TBD|FIXME|XXX|TODO|HACK|PLACEHOLDER` — zero hits. No stray stub/empty-return patterns found in the reviewed code.
|
||||
|
||||
### Scope Discipline
|
||||
|
||||
- `apps/api/prisma/schema.prisma` — untouched (`git diff --stat` empty against base)
|
||||
- `docker-compose*.yml`, `.env.example`, `.env.prod.example` — untouched
|
||||
- `DATABASE_URL` still role `tessera` (no `_app` suffix) — switch remains OFF
|
||||
- `assertTargetBelongsToTenant` present with 3 call/definition sites, unchanged behavior
|
||||
- WINDOWS #21, #23 — byte-identical to base commit (not touched by this task)
|
||||
- Full diff scope (16 files changed) matches the plan's declared `files_modified` list exactly, plus `module-grants.service.spec.ts` (listed in SUMMARY key-files, consistent with Aufgabe 2's test-first requirement)
|
||||
|
||||
### Human Verification Required
|
||||
|
||||
None. All must-haves are verifiable via the live database, the scratch tool,
|
||||
and static analysis, and were independently re-derived rather than accepted
|
||||
from the SUMMARY.
|
||||
|
||||
### Gaps Summary
|
||||
|
||||
No gaps found. This is an unusually well-evidenced quick task: every claim in
|
||||
the SUMMARY that could be independently re-measured was re-measured (not
|
||||
re-read), including four destructive falsification experiments this verifier
|
||||
ran fresh (beyond the two the executor already ran), and every number
|
||||
matched exactly (74/74, 839/839, 35/27, 107/135, 6/17/1/24). The framing of
|
||||
the SearchProvider half of WINDOWS #19 as a refuted premise (rather than a
|
||||
solved problem) is accurate and consistent across the migration header, the
|
||||
ledger entry, and the classification document.
|
||||
|
||||
---
|
||||
|
||||
_Verified: 2026-09-10T14:55:00Z_
|
||||
_Verifier: Claude (gsd-verifier)_
|
||||
Reference in New Issue
Block a user