docs(quick-260911-gwh): Etappe 2 auf Endstand bringen -- sechster Fall, Befund K erfuellt, Ledger, Anleitung

- .planning/WINDOWS.md: drei neue offene Eintraege (#30 Startpfad des
  Mailmoduls, #31 verschluckte Leere favorites, #32 verschluckte Leere
  settings); Platzhalter WINDOWS #TBD-GWH in settings.service.ts und
  mail.module.ts durch #30 ersetzt
- docs/mandantentrennung-zugriffsklassifikation.md: Uebersichtszeilen
  favorites (0/8, war 7/0) und settings (1/3, war 4/0) neu gemessen;
  Summenzeile 68/178 (Endstand Etappe 2); Bestandsaufnahme (favoriteLink
  gebunden, smtpConfig gemischt, neue Zeile widgetInstance/gebunden);
  Klassen-Verteilung 65 Paare (33/17/13/2); Hintergrunddienst-Abschnitt mit
  sechstem Fall (mail.module.ts, WINDOWS #30) und erfuellter
  Befund-K-Bedingung; neuer Punkt in "Was diese Etappe NICHT entscheidet"
- docs/mandantentrennung-etappe2-fehlerrichtung.md: Nachtraege unter Befund
  K in (t4) und im Uebergaben-Absatz von (d4) -- Reihenfolgebedingung
  erfuellt; ## Etappe 2 -- Abschluss mit den derivierten Endzahlen
- docs/anleitung-entwicklung.md: 23 RLS-Tabellen statt sieben, FavoriteLink
  nicht mehr als Tabelle ohne Regel, tenantPrisma statt manuellem
  tenantId-Filter als gelebter Stil
- rls-access-inventory.spec.ts wieder gruen (11/11), volle Suite 994/994,
  Werkzeug 137/137; zwei Dokument-Falsifizierungen durchgefuehrt und
  zurueckgenommen (siehe SUMMARY)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
2026-09-11 14:05:06 +02:00
parent b5f22e2c4a
commit 12409322f5
6 changed files with 165 additions and 35 deletions
+18 -14
View File
@@ -308,21 +308,25 @@ Prisma-Client mehr und veröffentlicht keinen auf dem Anfrageobjekt.
> Express-Middleware mit identischer Logik — beides wurde mit 260911-e2s entfernt, nachdem eine
> Volltextsuche keinen Leser dieser Eigenschaft außerhalb der beiden Dateien fand.
`app.current_tenant` wird von **Postgres Row-Level-Security** ausgewertet. RLS-Policies sind aber
**nicht** auf allen Tabellen aktiv — aktuell nur auf `User`, `PasswordResetToken`, `LdapConfig`,
`LdapFieldMapping`, `Group`, `GroupMembership` und `ModuleGrant` (siehe die Migrationen
`20260618112133_rls_policies` und `20260804130918_groups_rls_policies`). Alle übrigen
mandantenbezogenen Tabellen — u. a. `DkvVehicleMaster`, `DkvInvoiceHistory`, `CalendarSource`,
`Tender`, `TenderSavedSearch`, `FavoriteLink` — tragen zwar eine `tenantId`-Spalte, aber **keine**
RLS-Policy.
`app.current_tenant` wird von **Postgres Row-Level-Security** ausgewertet. RLS-Policies liegen
seit `20260909140000_rls_remaining_tenant_tables` auf 23 Tabellen (4 aus
`20260618112133_rls_policies`, 3 aus `20260804130918_groups_rls_policies`, 16 aus der
`_rls_remaining_tenant_tables`-Migration selbst — `grep -c "ENABLE ROW LEVEL SECURITY"` über die
drei Migrationen, zur Ausführungszeit nachzählen), darunter `FavoriteLink` und `SmtpConfig`. Ohne
eigene `tenantId`-Spalte bzw. bewusst plattformweit bleiben `Module`, `Tenant`, `Tender`,
`TenderSource` und `TenderSourcePollConfig` (siehe die Bestandsaufnahme in
`docs/mandantentrennung-zugriffsklassifikation.md`, Klasse `keine-mandantengebundene-tabelle`, für
die vollständige, maschinell geprüfte Liste — von dort ableiten, nicht raten).
**Was ein Entwickler nie vergessen darf:** Bei jeder Query gegen eine Tabelle ohne RLS-Policy muss
`tenantId` **manuell** in die `where`-Klausel — die Datenbank filtert hier nichts von selbst. Das
ist im Code auch der gelebte Stil: `DkvService.loadConfig()`
(`apps/api/src/dkv/dkv.service.ts`) etwa nutzt den plain, UNGEBUNDENEN `PrismaService` und
filtert explizit mit `where: { tenantId }`. Wer bei einer solchen Tabelle das `tenantId`-Filter
vergisst, liest oder schreibt mandantenübergreifend — ohne dass RLS das auffängt. Bei den sieben
RLS-geschützten Tabellen greift die DB-seitige Absicherung zusätzlich, vorausgesetzt die Query
**Was ein Entwickler nie vergessen darf:** jeder Zugriff auf eine mandantengebundene Tabelle läuft
dienst-intern über einen mit `forTenant()` gebundenen Klienten `tenantPrisma`
(`apps/api/src/prisma/prisma-tenant.extension.ts`) — die zusätzlichen `where`-Filter über
`userId` bleiben bestehen, wo die Regel selbst keine Benutzerdimension kennt (siehe
`docs/mandantentrennung-etappe2-fehlerrichtung.md`). Bei den Tabellen ohne eigene `tenantId`
(oben) filtert die Anwendung stattdessen — wo relevant — über den zutreffenden Bezug (z. B.
plattformweiter Katalog, kein Mandantenfilter nötig); siehe
`docs/mandantentrennung-zugriffsklassifikation.md` für den vollständigen Stand je Datei/Modell.
Bei den RLS-geschützten Tabellen greift die DB-seitige Absicherung zusätzlich, vorausgesetzt die Query
läuft tatsächlich über einen dienst-intern per `forTenant()` gebundenen Client und nicht über
den globalen, ungebundenen `PrismaService`.