Files
schalli ea003d44fe
Tessera CI/CD / Lint & Type Check (push) Successful in 45s
Tessera CI/CD / Tests (push) Successful in 52s
Tessera CI/CD / Build & Publish Images (push) Successful in 28s
docs(quick-260909-dgj): Mandantentrennung vorbereitet — Plan, Bericht, Befunde
Die Arbeit liefert Rolle, Verbindungstrennung, Pruefwerkzeug und Policies fuer
alle 16 offenen Tabellen, schaltet die Trennung aber bewusst NICHT scharf.

Grund, gemessen statt vermutet: im Code stehen 182 Datenbankzugriffe ohne
Mandantenkontext gegen 19 mit. Der Anmeldeweg ist zwingend darunter — er liest
die Benutzerzeile, bevor der Mandant bekannt ist, weil der Mandant erst aus
dieser Zeile kommt. Unter einer Rolle ohne Umgehungsrecht liefert diese Abfrage
nichts, und niemand koennte sich mehr anmelden. Das Scharfschalten ist damit
ein eigener Vorgang, kein Nebeneffekt dieser Arbeit.

Neu im Ledger als #19: SearchProvider und TenderRssFeedSource haben ein
nullable tenantId. Die einfache Policy vergleicht NULL nie gleich, wodurch die
plattformweiten Zeilen nach dem Scharfschalten fuer JEDEN Mandanten
verschwinden wuerden — nicht nur fuer fremde. Heute wirkungslos, beim
Scharfschalten zwingend mitzuloesen. Die richtige Semantik ist eine
Produktentscheidung, deshalb bewusst nicht eigenmaechtig anders geloest.

673/673 Tests gruen, Typpruefung sauber. #18 bleibt offen.

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

13 KiB

phase, plan, subsystem, tags, requires, provides, affects, actuals, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, coverage, duration, completed, status
phase plan subsystem tags requires provides affects actuals tech-stack key-files key-decisions patterns-established requirements-completed coverage duration completed status
quick-260909-dgj 01 database
postgresql
rls
prisma
multi-tenancy
docker
security
phase provides
02-authentication-multi-tenancy current_tenant_id(), forTenant() extension, sieben Ausgangs-Policies
phase provides
15-modul-berechtigungen-gruppen-user-grants Group/GroupMembership/ModuleGrant RLS-Muster (20260804130918)
Datenbankrolle tessera_app ohne Superuser-/BYPASSRLS-Recht (Migration 20260909130000)
Getrennte Migrations-/Laufzeitverbindung ueber TESSERA_MIGRATE_DATABASE_URL, Vorgabe unveraendert
Pruefwerkzeug apps/api/scripts/rls-preflight.mjs (fuenf Nachweise, transaktionssicher)
Vollstaendige RLS-Abdeckung aller 20 Tabellen mit tenantId (Migration 20260909140000)
Betriebsanleitung docs/mandantentrennung-datenbankrolle.md mit Sperrgrund und Rueckweg
datenbank
betrieb
mandantenfaehigkeit
WINDOWS-18
tokens tasks commits
12875 4 4
added patterns
DO $$ ... $$ Konvergenz-Migration statt CREATE ROLE IF NOT EXISTS — noetig, weil Rolleneigenschaften (SUPERUSER/BYPASSRLS) sich nicht per IF NOT EXISTS setzen lassen
sh-Skript mit --print-plan-Betriebsart, damit ein Ablaufskript ohne Seiteneffekte in der CI testbar ist
Node-ES-Modul mit --print-plan-Betriebsart fuer dasselbe Muster in einem Pruefwerkzeug
Abdeckungstest liest Schema + Migrationen zur Laufzeit statt Text zu vergleichen — bleibt fuer kuenftige Modelle gueltig
created modified
apps/api/prisma/migrations/20260909130000_rls_app_role/migration.sql
apps/api/prisma/migrations/20260909140000_rls_remaining_tenant_tables/migration.sql
apps/api/scripts/migrate-and-start.sh
apps/api/scripts/rls-preflight.mjs
apps/api/src/prisma/rls-app-role.spec.ts
apps/api/src/prisma/start-script.spec.ts
apps/api/src/prisma/rls-preflight.spec.ts
apps/api/src/prisma/rls-coverage.spec.ts
docs/mandantentrennung-datenbankrolle.md
apps/api/Dockerfile
docker-compose.yml
docker-compose.prod.yml
.env.example
docs/README.md
Umstellung bleibt standardmaessig aus — 182 gemessene unskalierte Prisma-Zugriffe (inkl. Anmeldeweg) wuerden bei sofortiger Aktivierung jeden Login und mehrere Hintergrunddienste lahmlegen
D-03-Korrektur (Tender-Tabellen) steht im Kopf der NEUEN Migration, nicht in der angewendeten 20260804130918 — Prisma-Pruefsumme darf nicht brechen
SearchProvider und TenderRssFeedSource behalten die einfache tenantId = current_tenant_id()-Policy trotz nullable tenantId; NULL-Zeilen (plattformweite Vorgaben/Feeds) werden dadurch nach einer Aktivierung nicht ausgeliefert — dokumentiert als Threat Flag, nicht behoben, da die Umstellung ohnehin nicht aktiv ist
Rollen-Migrationen konvergieren statt zu scheitern: DO $$-Block prueft Existenz UND Eigenschaften separat, bricht mit ausfuehrbarer Anleitung ab, wenn current_user nicht genug Rechte hat
Skripte mit --print-plan-Betriebsart sind der Weg, um Ablauflogik ohne Datenbank/Netzwerk in Vitest zu pruefen
WINDOWS-18
id description requirement verification human_judgment
D1 Datenbankrolle tessera_app wird wiederholbar angelegt, traegt weder Superuser- noch BYPASSRLS-Recht, kein Kennwort im SQL WINDOWS-18
kind ref status
unit apps/api/src/prisma/rls-app-role.spec.ts (7 Tests) pass
false
id description requirement verification human_judgment
D2 Migrations- und Laufzeitverbindung sind getrennt (TESSERA_MIGRATE_DATABASE_URL); ohne gesetzte Variable bleibt der Ablauf exakt der bisherige WINDOWS-18
kind ref status
unit apps/api/src/prisma/start-script.spec.ts (5 Tests) pass
false
id description requirement verification human_judgment
D3 Pruefwerkzeug misst fuenf benannte Eigenschaften transaktionssicher, verbindet in --print-plan nicht, schreibt in keiner Betriebsart WINDOWS-18
kind ref status
unit apps/api/src/prisma/rls-preflight.spec.ts (5 Tests) pass
false
id description requirement verification human_judgment
D4 Alle 20 Modelle mit tenantId tragen eine RLS-Policy; 5 bewusste Ausnahmen und 3 Join-Muster sind namentlich mit Begruendung festgehalten WINDOWS-18
kind ref status
unit apps/api/src/prisma/rls-coverage.spec.ts (5 Tests) pass
false
id description verification human_judgment rationale
D5 Die neuen Policies wirken erst nach einer manuellen, vom Betreiber ausgefuehrten Umstellung — ein Live-Nachweis am tatsaechlich umgestellten System ist ausserhalb dieses Vorgangs
true Konstraint dieses Plans: nichts anwenden, nichts umstellen. Der Live-Nachweis (rls-preflight.mjs gegen die neue Rolle, danach Anmeldung pruefen) folgt in einem eigenen, spaeteren Vorgang, sobald die 182 unskalierten Zugriffe behandelt sind.
15min 2026-09-09 complete

Quick Task 260909-dgj: Mandantentrennung auf Datenbankebene — Rolle, Nachweis, Abdeckung (Umstellung selbst bleibt aus) Summary

Legt die Datenbankrolle tessera_app ohne RLS-Umgehungsrecht an, trennt Migrations- von Laufzeitverbindung, liefert ein Pruefwerkzeug fuer den Nachweis und deckt alle 20 Tabellen mit tenantId per Policy ab — schaltet die Verbindung selbst aber bewusst NICHT um, weil 182 gemessene unskalierte Prisma-Zugriffe (darunter der Anmeldeweg) das sofort verhindern wuerden.

Performance

  • Duration: ~15 min
  • Started: 2026-09-09T07:56:00Z
  • Completed: 2026-09-09T08:06:00Z
  • Tasks: 4/4
  • Files modified: 14

Accomplishments

  • tessera_app-Rolle (Migration 20260909130000_rls_app_role) wird wiederholbar angelegt bzw. auf NOSUPERUSER/NOBYPASSRLS konvergiert, ohne Kennwort im SQL, mit lautem Abbruch samt Handlungsanweisung, falls current_user die Rechte dafuer fehlen
  • apps/api/scripts/migrate-and-start.sh trennt den Migrationsschritt (TESSERA_MIGRATE_DATABASE_URL) vom Laufzeitschritt (DATABASE_URL); ohne gesetzte Variable bleibt der Containerstart identisch zum bisherigen CMD — verifiziert per Test, nicht behauptet
  • apps/api/scripts/rls-preflight.mjs fuehrt fuenf transaktionssichere Nachweise (Rollenrechte, Kontext-Setzbarkeit, keine Zeilen ohne Kontext, Zeilen mit Kontext, vollstaendige Schreibrechte) gegen eine beliebige Verbindung aus, ohne etwas zu veraendern
  • Migration 20260909140000_rls_remaining_tenant_tables ergaenzt Policies fuer die 16 zuvor offenen Tabellen; ein Abdeckungstest liest Schema und Migrationen zur Laufzeit und faellt kuenftig bei jedem neuen Modell ohne bewusste Entscheidung
  • docs/mandantentrennung-datenbankrolle.md benennt den Sperrgrund (182 unskalierte Zugriffe, Anmeldeweg als struktureller Grund) unmissverstaendlich, dazu die Handgriffe des Betreibers und den Rueckweg bei einer nicht mehr verbindenden API

Task Commits

Each task was committed atomically:

  1. Task 1: Anwendungsrolle ohne RLS-Umgehungsrecht anlegen (Migration) - b3375ae (feat)
  2. Task 2: Migrationsverbindung von der Laufzeitverbindung trennen - a5f99e5 (feat)
  3. Task 3: Pruefwerkzeug fuer den Nachweis plus Betriebsanleitung mit Rueckweg - 44a90e5 (feat)
  4. Task 4: Policies fuer die 16 fehlenden Tabellen - efaabc9 (feat)

Alle vier Aufgaben folgten TDD: Testdatei zuerst geschrieben, rot bestaetigt, dann die Implementierung, dann gruen bestaetigt — jeweils im selben Commit (Test + Implementierung gehoerten inhaltlich zusammen, keine separate RED-Phase committet).

Files Created/Modified

  • apps/api/prisma/migrations/20260909130000_rls_app_role/migration.sql - legt tessera_app an, konvergiert wiederholbar
  • apps/api/prisma/migrations/20260909140000_rls_remaining_tenant_tables/migration.sql - 16 fehlende Policies, D-03-Korrektur im Kopf
  • apps/api/scripts/migrate-and-start.sh - trennt Migrations-/Laufzeitverbindung, POSIX sh, --print-plan-Betriebsart
  • apps/api/scripts/rls-preflight.mjs - fuenf transaktionssichere Nachweise, --print-plan-Betriebsart
  • apps/api/src/prisma/rls-app-role.spec.ts / start-script.spec.ts / rls-preflight.spec.ts / rls-coverage.spec.ts - 22 Tests insgesamt, alle rot vor der jeweiligen Implementierung
  • apps/api/Dockerfile - kopiert apps/api/scripts, CMD ruft das neue Skript auf statt der alten &&-Kette
  • docker-compose.yml / docker-compose.prod.yml - reichen TESSERA_MIGRATE_DATABASE_URL mit leerem Vorgabewert durch
  • .env.example - erklaert beide Variablen, Umstellungszeile bleibt auskommentiert
  • docs/mandantentrennung-datenbankrolle.md - Befund, Vorbereitetes, Sperrgrund, Handgriffe, Vorher-Pruefung, Rueckweg
  • docs/README.md - verlinkt die neue Anleitung

Decisions Made

  • Umstellung bleibt standardmaessig aus (siehe key-decisions oben) — WINDOWS #18 bleibt bewusst offen, nicht fixed.
  • Die D-03-Korrektur zur pauschalen Tender-RLS-Aussage aus 20260804130918 steht ausschliesslich im Kopf der neuen Migration 20260909140000; die angewendete Datei blieb unangetastet (Prisma-Pruefsumme).
  • SearchProvider und TenderRssFeedSource (beide mit nullable tenantId) bekamen dieselbe einfache Policy wie die uebrigen 14 Tabellen — siehe „Threat Flags" unten fuer die damit verbundene, noch offene Nebenwirkung.

Deviations from Plan

Keine Abweichung von Rule 1-4 noetig — der Plan war bereits sehr praezise vorgemessen (28/20/7/16-Zaehlung, 182-vs-19-Zugriffszaehlung, exakte Zeilennummern in docker-compose.yml) und stimmte beim Ausfuehren mit dem Codebestand ueberein.

Eine Test-Iteration war noetig: Der erste Entwurf der Rollen-Migration verwendete in Kopfkommentaren zweimal die Formulierung „BYPASSRLS-Rollen" bzw. „SUPERUSER/BYPASSRLS", was den eigenen Test 2 (kein bares BYPASSRLS ohne vorangestelltes NO) verletzte. Umformuliert auf „Rollen ohne NOBYPASSRLS" bzw. „SUPERUSER/NOBYPASSRLS" — inhaltlich identisch, textuell konform. Kein Rule-Fall, da innerhalb desselben ungetesteten ersten Entwurfs vor dem ersten gruenen Lauf behoben.

Total deviations: 0 (Rule 1-4) Impact on plan: Keine — Plan exakt wie geschrieben umgesetzt.

Issues Encountered

Keine blockierenden Probleme. Alle vier TDD-Zyklen liefen rot -> gruen ohne Iterationsbedarf jenseits der oben genannten Testfeinschliff-Korrektur.

User Setup Required

Keine sofortige Handlung noetig — die Umstellung ist bewusst nicht Teil dieses Vorgangs. Wenn die 182 unskalierten Zugriffe (siehe Sperrgrund in docs/mandantentrennung-datenbankrolle.md, Abschnitt 3) in einem eigenen, spaeteren Vorgang behandelt sind, folgen die vier Handgriffe aus Abschnitt 4 derselben Anleitung: Kennwort setzen, .env auf dem Server umstellen, Vorher-Pruefung ausfuehren, Container neu erstellen — ausdruecklich vom Betreiber, nicht automatisiert.

Known Stubs

Keine — alle vier Bausteine sind vollstaendig implementiert und getestet (keine leeren Rueckgabewerte, kein "coming soon").

Next Phase Readiness

Bereit fuer einen Folge-Vorgang, der die 182 unskalierten Prisma-Zugriffe behandelt (Anmeldeweg, AD-Abgleich, Ausschreibungs-Digest, Modulzugriffspruefung, Treffersuche, Admin-Erstanlage) — erst danach ist die Umstellung selbst (Kennwort setzen, .env umbiegen) sinnvoll und sicher. docs/mandantentrennung-datenbankrolle.md beschreibt den vollstaendigen Weg.

Blocker: keiner fuer diesen Vorgang selbst. Die Umstellung bleibt fuer den Produktivbetrieb blockiert, bis der Folge-Vorgang abgeschlossen ist — genau wie in Abschnitt 3 der neuen Anleitung dokumentiert.

WINDOWS #18 bleibt open (nicht fixed, nicht waived) — dieser Vorgang bereitet die Behebung vor, vollzieht sie aber nicht.

Threat Flags

Flag File Description
threat_flag: nullable-tenantId-policy-gap apps/api/prisma/migrations/20260909140000_rls_remaining_tenant_tables/migration.sql (SearchProvider, TenderRssFeedSource) Beide Modelle haben tenantId String? (nullable) fuer plattformweite Zeilen (Vorgabe-Suchanbieter bzw. plattformweite RSS-Feeds mit userId = null). Die neue Policy "tenantId" = current_tenant_id() liefert fuer NULL-Zeilen laut PostgreSQL-Dreiwertlogik nie true — sobald die Rolle aktiv verbindet, wuerden diese plattformweiten Zeilen fuer JEDEN Mandanten unsichtbar, nicht nur fuer den falschen. Derzeit ohne Wirkung, da die Umstellung nicht aktiv ist (siehe Sperrgrund). Zu klaeren im Folge-Vorgang, der die Umstellung tatsaechlich vollzieht: entweder eine OR "tenantId" IS NULL-Klausel ergaenzen oder die plattformweiten Zeilen ueber einen anderen Mechanismus ausliefern.

Self-Check: PASSED

  • FOUND: apps/api/prisma/migrations/20260909130000_rls_app_role/migration.sql
  • FOUND: apps/api/prisma/migrations/20260909140000_rls_remaining_tenant_tables/migration.sql
  • FOUND: apps/api/scripts/migrate-and-start.sh
  • FOUND: apps/api/scripts/rls-preflight.mjs
  • FOUND: apps/api/src/prisma/rls-app-role.spec.ts
  • FOUND: apps/api/src/prisma/start-script.spec.ts
  • FOUND: apps/api/src/prisma/rls-preflight.spec.ts
  • FOUND: apps/api/src/prisma/rls-coverage.spec.ts
  • FOUND: docs/mandantentrennung-datenbankrolle.md
  • FOUND commit b3375ae, a5f99e5, 44a90e5, efaabc9

Quick Task: 260909-dgj Completed: 2026-09-09