- 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
8.1 KiB
Tessera – Mandantentrennung auf Datenbankebene (WINDOWS #18)
Diese Anleitung richtet sich an dieselben Kolleg:innen wie das Betriebshandbuch 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
- Der Befund
- Was bereits vorbereitet ist
- Der Sperrgrund — warum die Umstellung noch nicht erfolgt ist
- Die Umstellung, Schritt fuer Schritt
- Die Vorher-Pruefung
- 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 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_appohne Superuser- und ohne BYPASSRLS-Recht (Migration20260909130000_rls_app_role). - Getrennte Verbindungen fuer Migration und Laufzeit.
prisma migrate deploybraucht die Rechte des Tabelleneigentuemers, die Anwendung soll sie nicht haben —apps/api/scripts/migrate-and-start.shtrennt beide Schritte ueber die neue VariableTESSERA_MIGRATE_DATABASE_URL. - Ein Pruefwerkzeug, das den Nachweis fuehrt statt ihn zu behaupten:
apps/api/scripts/rls-preflight.mjsmisst 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_tablesergaenzt die bislang fehlenden 16 Tabellen; alle 20 Tabellen mittenantIdtragen 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.
-
Kennwort der Rolle einmalig setzen, als Datenbank-Superuser:
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
psqluebergeben und danach aus der Shell-Historie entfernt. -
In der
.envdes ServersTESSERA_MIGRATE_DATABASE_URLauf die bisherige Verbindung (Rolletessera, Tabelleneigentuemer) setzen undDATABASE_URLauf die neue Rolletessera_appumbiegen — in dieser Reihenfolge, dennprisma migrate deploymuss weiterhin mit Eigentuemerrechten laufen, waehrend die Anwendung selbst die eingeschraenkte Rolle bekommt. -
Vor dem Neustart die Vorher-Pruefung aus Abschnitt 5 gegen die neue Rolle ausfuehren.
-
API-Dienst neu erstellen (
docker compose up -d --build --force-recreate api, siehe Betriebshandbuch Abschnitt 4).
5. Die Vorher-Pruefung
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:
DATABASE_URLin der.envwieder auf die bisherige Verbindung (Rolletessera) setzen.docker compose up -d --build --force-recreate apiausfuehren.
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).