Files
tessera-ctl/docs/mandantentrennung-datenbankrolle.md
T
schalli 44a90e5bfd feat(quick-260909-dgj): Pruefwerkzeug fuer den Nachweis plus Betriebsanleitung (Task 3/4)
- apps/api/scripts/rls-preflight.mjs misst fuenf benannte Eigenschaften
  (rollenrechte, kontext-setzbar, ohne-kontext-leer, mit-kontext-sichtbar,
  schreibrechte) jeweils in einer eigenen Transaktion gegen eine per
  TESSERA_PREFLIGHT_DATABASE_URL angegebene Verbindung; --print-plan
  verbindet nicht, das Werkzeug schreibt in keiner Betriebsart
- 5 Tests in rls-preflight.spec.ts (rot vor dem Werkzeug, jetzt gruen)
- docs/mandantentrennung-datenbankrolle.md: Befund, Sperrgrund (182
  unskalierte Zugriffe, Anmeldeweg), Handgriffe des Betreibers samt
  Kennwortsetzung, Freigabebedingung und Rueckweg; docs/README.md verweist
  darauf

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
2026-09-09 10:03:25 +02:00

174 lines
8.1 KiB
Markdown
Raw 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.
<!-- generated-by: gsd-doc-writer -->
# Tessera – Mandantentrennung auf Datenbankebene (WINDOWS #18)
Diese Anleitung richtet sich an dieselben Kolleg:innen wie das
[Betriebshandbuch](./anleitung-betrieb.md) und behandelt nur einen einzigen
Punkt: die Datenbankrolle, mit der Tessera verbindet, und warum die
bestehenden Sicherheitsregeln (Row-Level Security) heute keine Wirkung
haben.
## Inhaltsverzeichnis
1. [Der Befund](#1-der-befund)
2. [Was bereits vorbereitet ist](#2-was-bereits-vorbereitet-ist)
3. [Der Sperrgrund — warum die Umstellung noch nicht erfolgt ist](#3-der-sperrgrund--warum-die-umstellung-noch-nicht-erfolgt-ist)
4. [Die Umstellung, Schritt fuer Schritt](#4-die-umstellung-schritt-fuer-schritt)
5. [Die Vorher-Pruefung](#5-die-vorher-pruefung)
6. [Der Rueckweg](#6-der-rueckweg)
---
## 1. Der Befund
Tessera trennt die Daten mehrerer Mandanten auf zwei Ebenen: im Anwendungscode
(`where: { tenantId }`) und zusaetzlich auf Datenbankebene ueber PostgreSQL
Row-Level Security (RLS) — ein „zweites Sicherheitsnetz" fuer den Fall, dass
im Code einmal ein `tenantId`-Filter vergessen wird.
Dieses zweite Netz existiert seit Juni 2026 auf sieben Tabellen und wurde am
2026-09-09 mit einer wichtigen Einschraenkung nachgemessen: **es greift
derzeit nicht.** Die API verbindet laut `docker-compose.yml` als Rolle
`tessera`. Diese Rolle entsteht aus der Umgebungsvariable `POSTGRES_USER` und
ist damit automatisch Superuser des PostgreSQL-Clusters. PostgreSQL wendet
Row-Level Security auf Superuser-Rollen grundsaetzlich nicht an — auch nicht
auf Rollen mit dem Recht `BYPASSRLS`, das Superuser automatisch mitbringen.
Der Zusatz `FORCE ROW LEVEL SECURITY` in den bestehenden Migrationen aendert
daran nichts: er erzwingt Policies nur gegenueber dem *Tabelleneigentuemer*,
nicht gegenueber Rollen mit Umgehungsrecht — und die Rolle `tessera` ist
beides zugleich.
Praktisch nachgemessen am 2026-09-09: ein `SELECT count(*) FROM "Group"` ohne
gesetzten Mandantenkontext liefert zwei Zeilen, obwohl die vorhandene Policy
`USING ("tenantId" = current_tenant_id())` bei fehlendem Kontext null Zeilen
liefern muesste. Die Isolation zwischen Mandanten haengt heute vollstaendig
am `where`-Filter im Anwendungscode — nicht an der Datenbank.
Dieser Befund ist als [WINDOWS #18](../.planning/WINDOWS.md) im
Fehler-Ledger festgehalten. Kein akutes Risiko, solange Tessera nur intern
und einmandantig laeuft — aber zwingend zu beheben, bevor Tessera an einen
zweiten, externen Mandanten geht.
## 2. Was bereits vorbereitet ist
Diese Aenderung liefert die vier Bausteine, die fuer eine wirksame Trennung
noetig sind:
- **Eine eigene Datenbankrolle** `tessera_app` ohne Superuser- und ohne
BYPASSRLS-Recht (Migration `20260909130000_rls_app_role`).
- **Getrennte Verbindungen fuer Migration und Laufzeit.** `prisma migrate
deploy` braucht die Rechte des Tabelleneigentuemers, die Anwendung soll
sie nicht haben — `apps/api/scripts/migrate-and-start.sh` trennt beide
Schritte ueber die neue Variable `TESSERA_MIGRATE_DATABASE_URL`.
- **Ein Pruefwerkzeug**, das den Nachweis fuehrt statt ihn zu behaupten:
`apps/api/scripts/rls-preflight.mjs` misst gegen eine angegebene
Verbindung Rollenrechte, Setzbarkeit des Mandantenkontexts, sichtbare
Zeilen mit und ohne Kontext sowie vorhandene Zugriffsrechte.
- **Vollstaendige Policy-Abdeckung.** Migration
`20260909140000_rls_remaining_tenant_tables` ergaenzt die bislang
fehlenden 16 Tabellen; alle 20 Tabellen mit `tenantId` tragen jetzt eine
Regel.
## 3. Der Sperrgrund — warum die Umstellung noch nicht erfolgt ist
**Die Verbindung ist bewusst noch nicht umgestellt, und sie darf noch nicht
umgestellt werden.**
Am 2026-09-09 wurde im Quelltext von `apps/api/src` gezaehlt, wie oft die
Anwendung ueber den unskalierten Prisma-Client zugreift (also ohne den
Mandantenkontext zu setzen) gegenueber der Anzahl der Verwendungen des
mandantengebundenen `tenantPrisma`: **182 unskalierte Zugriffe** — 84 auf die
sieben bereits mit Policies versehenen Tabellen, 98 auf die 16 neu
hinzugekommenen — gegenueber lediglich **19 Verwendungen** von `tenantPrisma`
im gesamten API-Quelltext.
Darunter ist der Anmeldeweg selbst, und der kann strukturell nicht anders
funktionieren: `apps/api/src/auth/auth.service.ts` sucht den Benutzer anhand
des Benutzernamens, **bevor** der Mandant bekannt ist — der Mandant wird ja
erst aus dem gefundenen Benutzer bestimmt. Unter der Rolle `tessera_app`
liefert genau diese Suche null Zeilen. **Niemand koennte sich mehr
anmelden.**
Ebenso betroffen sind Hintergrunddienste, die von Natur aus ohne
Mandantenkontext laufen: der AD-Abgleich, der Ausschreibungs-Digest, die
Modulzugriffspruefung, die Treffersuche im Ausschreibungs-Radar und die
Erstanlage des Administrators beim ersten Start.
Diese 182 Stellen brauchen einen ausdruecklichen, benannten Systemkontext
(oder eine gezielte Umstellung auf `tenantPrisma`/`forTenant()`), bevor der
Schalter umgelegt werden darf. Das ist **eigene Arbeit und nicht Teil dieser
Aenderung.** Ein gruener Bericht des Pruefwerkzeugs aus Abschnitt 5 belegt
ausschliesslich, dass die Datenbankseite stimmt — er sagt nichts ueber diese
182 Zugriffe aus.
## 4. Die Umstellung, Schritt fuer Schritt
Erst durchfuehren, wenn Abschnitt 3 abgearbeitet ist. Reihenfolge einhalten
— die Migration muss weiterhin als Tabelleneigentuemer laufen.
1. **Kennwort der Rolle einmalig setzen**, als Datenbank-Superuser:
```sql
ALTER ROLE tessera_app WITH PASSWORD '<hier ein starkes, generiertes Kennwort einsetzen>';
```
Dieser Befehl gehoert in keine Datei und in kein Protokoll — er wird
direkt an `psql` uebergeben und danach aus der Shell-Historie entfernt.
2. **In der `.env` des Servers** `TESSERA_MIGRATE_DATABASE_URL` auf die
**bisherige** Verbindung (Rolle `tessera`, Tabelleneigentuemer) setzen und
`DATABASE_URL` auf die **neue** Rolle `tessera_app` umbiegen — in dieser
Reihenfolge, denn `prisma migrate deploy` muss weiterhin mit
Eigentuemerrechten laufen, waehrend die Anwendung selbst die eingeschraenkte
Rolle bekommt.
3. Vor dem Neustart die Vorher-Pruefung aus Abschnitt 5 gegen die neue Rolle
ausfuehren.
4. API-Dienst neu erstellen (`docker compose up -d --build --force-recreate
api`, siehe Betriebshandbuch Abschnitt 4).
## 5. Die Vorher-Pruefung
```bash
TESSERA_PREFLIGHT_DATABASE_URL="postgresql://tessera_app:<kennwort>@<host>:5432/tessera" \
node apps/api/scripts/rls-preflight.mjs
```
Das Werkzeug meldet fuenf Pruefungen: Rollenrechte, Setzbarkeit des
Mandantenkontexts, keine sichtbaren Zeilen ohne Kontext, sichtbare Zeilen mit
Kontext, vollstaendige Zugriffsrechte. Es veraendert nichts — es liest
ausschliesslich.
**Ein gruener Bericht ist die Freigabebedingung fuer Schritt 4 aus Abschnitt
4 — aber er ist keine Freigabe fuer die Umstellung insgesamt.** Er bestaetigt
nur, dass die Datenbankseite stimmt. Die 182 unskalierten Zugriffe aus
Abschnitt 3 misst er nicht und kann sie nicht messen — dafuer muesste er den
Anwendungscode lesen, nicht die Datenbank.
## 6. Der Rueckweg
**Wenn die API nach einer Umstellung nicht mehr hochkommt:**
Das sichtbare Bild: der `api`-Container bleibt ungesund (Healthcheck
schlaegt fehl), und das Protokoll (`docker compose logs api`) zeigt einen
Authentifizierungs- oder Rechtefehler von PostgreSQL — etwa eine
fehlgeschlagene Anmeldung oder eine verweigerte Berechtigung.
Weg zurueck, zwei Schritte:
1. `DATABASE_URL` in der `.env` wieder auf die bisherige Verbindung (Rolle
`tessera`) setzen.
2. `docker compose up -d --build --force-recreate api` ausfuehren.
Die Rolle `tessera_app` darf dabei unangetastet bestehen bleiben — sie
schadet nicht, solange niemand mit ihr verbindet.
**Wichtiger Hinweis fuer den Server:** `/opt/tessera/docker-compose.yml` ist
keine Arbeitskopie dieses Repositorys. Aenderungen an `docker-compose.yml`
oder `docker-compose.prod.yml` in diesem Repository erreichen den Server
nicht automatisch. Wer `TESSERA_MIGRATE_DATABASE_URL` dort verfuegbar haben
will, traegt die Zeile selbst in `/opt/tessera/docker-compose.yml` ein
(vorher sichern) — genau wie es bereits fuer andere nachtraegliche
Variablen dokumentiert ist (Betriebshandbuch, Abschnitt zur
Server-Konfiguration).