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
This commit is contained in:
+7
-6
@@ -4,10 +4,10 @@ milestone: v1.2
|
||||
current_phase: 17
|
||||
current_phase_name: eigene-ausschreibungs-quellen-je-nutzer
|
||||
status: verified
|
||||
stopped_at: "Quick Task 260909-cx0 abgeschlossen: user-files-Volume ergaenzt (WINDOWS #17 bleibt offen bis Server-Uebernahme), CLAUDE.md-Technik-Block auf installierten Stand korrigiert"
|
||||
last_updated: "2026-09-09T10:00:00.000Z"
|
||||
stopped_at: "Drei Vorhaben am 2026-09-09 abgearbeitet: Dateisicherung fuer user-files (benanntes Volume), Versionsangaben in CLAUDE.md auf den installierten Stand samt Abschnitt ueber sechs nie eingebaute Empfehlungen, und die Vorbereitung der Mandantentrennung. Bei letzterer kam der schwerste Befund der Sitzung heraus: die vorhandene Datenbank-Trennung wirkt gar nicht, weil die Anwendungsrolle sie umgeht (#18) — praktisch gemessen. Gebaut sind Rolle, Verbindungstrennung, Pruefwerkzeug und Policies fuer alle 16 offenen Tabellen; das Scharfschalten bleibt aus, weil 182 Zugriffe ohne Mandantenkontext im Code stehen und der Anmeldeweg darunter zwingend ist. OFFEN: #17 (Volume-Zeile auf dem Server nachtragen), #18 (Scharfschalten, braucht die 182 Stellen), #19 (nullable tenantId bei SearchProvider und TenderRssFeedSource)."
|
||||
last_updated: "2026-09-09T10:10:00.000Z"
|
||||
last_activity: 2026-09-09
|
||||
last_activity_desc: Dateisicherung nachgeruestet, Versionsangaben in CLAUDE.md auf den installierten Stand gebracht
|
||||
last_activity_desc: Mandantentrennung vorbereitet — Rolle, Policies und Pruefwerkzeug gebaut, Umschalten bewusst offen
|
||||
state_head: c80704957a5518e316a53edc0b8d7e98052a9750
|
||||
progress:
|
||||
total_phases: 17
|
||||
@@ -368,6 +368,7 @@ None yet.
|
||||
| 260909-ab3 | Zwei Befunde aus der Live-Pruefung behoben. **#14:** Die Suche in der Freigaben-Matrix filterte beide Achsen mit demselben Begriff und leerte dadurch die jeweils andere — jetzt bleibt die nicht getroffene Achse vollstaendig stehen, die Gruppensuche unter internem UND AD-Namen (#6c) ist per Regressionstest gesichert. **#15:** AD-Konten mit bereits vergebener Mailadresse werden nun angelegt, nur ohne Adresse (Produktentscheidung des Users vom 2026-09-09; der erste Anspruch behaelt die Adresse), auf BEIDEN Wegen — Sync und Einzelimport. Rohe Prisma-Texte gehen nur noch ins Log, der Bericht zeigt drei verstaendliche deutsche Abschnitte. **Sicherheitsfund nebenbei geschlossen (T-Q3-01):** der Update-Zweig schrieb die Mailadresse bedingungslos um, ein Verzeichniseintrag haette so die Adresse einer echten Person uebernehmen und deren Passwort-Reset empfangen koennen. `User.email` ist jetzt optional (Migration geschrieben, laeuft beim naechsten API-Start automatisch mit). Verifiziert 8/8: 651/651 API- und 233/233 Web-Tests, beide Typpruefungen sauber; die Sicherheitspruefung wurde durch Rueckbau falsifiziert (ohne Besitzpruefung schlaegt der Test fehl). **Am 2026-09-09 im Browser abgenommen, beide Ledger-Punkte geschlossen** (Bericht: 260909-ab3-UAT-2026-09-09.md) | 2026-09-09 | 2167046 | [260909-ab3-matrix-suche-und-sync-meldungen-reparier](./quick/260909-ab3-matrix-suche-und-sync-meldungen-reparier/) |
|
||||
| 260909-cx0 | Hochgeladene Dateien (Avatare, DKV-Exporte) ueberlebten kein `--force-recreate` des api-Containers (WINDOWS #17) — lagen nur in der fluechtigen Container-Schicht, keine Compose-Datei mountete `/app/user-files`. Jetzt benanntes Docker-Volume `user-files` in `docker-compose.yml` und `docker-compose.prod.yml` (Eigentuemerschaft uid 1001 aus dem Image, kein Bind-Mount), Betriebshandbuch Kapitel 6/7 entsprechend nachgezogen. Zweiter, unabhaengiger Punkt: CLAUDE.md nannte fuer die Technik-Tabelle noch die 2026-06/07-Empfehlung (Next.js 16.2.x, Prisma 7.8.x, Keycloak, Redis, TanStack Query, shadcn/ui, Playwright, Husky, lint-staged) statt des installierten Stands — jetzt korrigiert auf Next.js 15.5.19, Prisma 6.19.3 etc., nie uebernommene Empfehlungen in eigenem Abschnitt "Recommended But Not Adopted", `.planning/research/STACK.md` nur mit Hinweiszeile ergaenzt. Keine Abhaengigkeit aktualisiert. **WINDOWS #17 bleibt offen** — die Aenderung erreicht die laufende Installation auf alpha nicht, `/opt/tessera/docker-compose.yml` weicht vom Repository ab und muss vom Nutzer selbst ergaenzt werden | 2026-09-09 | dab72eb,c807049 | [260909-cx0-dateisicherung-nachruesten-und-versionsa](./quick/260909-cx0-dateisicherung-nachruesten-und-versionsa/) |
|
||||
| 260909-cx0 | Dateisicherung nachgeruestet und Versionsangaben geradegezogen. **user-files** liegt jetzt in einem benannten Volume (docker-compose.yml und .prod.yml) — vorher lag der Ordner nur in der fluechtigen Container-Schicht, hochgeladene Profilbilder und DKV-Exporte waeren bei jedem --force-recreate weg gewesen. Benanntes Volume statt Bind-Mount, weil das Image /app/user-files an uid 1001 uebereignet; ein frisch angelegtes Host-Verzeichnis gehoert root und haette aus dem Datenverlust einen kaputten Upload gemacht. docker-compose.dev.yml blieb bewusst unveraendert (Compose fuehrt Mount-Listen ueber das Ziel zusammen). **CLAUDE.md** nennt jetzt die installierten Fassungen statt der urspruenglich empfohlenen (Next.js 15.5.19 statt 16, Prisma 6.19.3 statt 7); neu ist ein Abschnitt 'Recommended But Not Adopted', der sechs nie eingebaute Empfehlungen benennt — darunter Keycloak, Redis und shadcn/ui. Der Block ist generiert, deshalb traegt er einen Herkunftsvermerk und die Recherchedatei eine datierte Hinweiszeile; ihre Zahlen blieben unangetastet. Keine Abhaengigkeit angefasst (per git diff gegengeprueft). **WINDOWS #17 bleibt offen**, bis der User dieselbe Volume-Zeile in /opt/tessera/docker-compose.yml nachtraegt — die Serverdatei weicht vom Repository ab | 2026-09-09 | c807049 | [260909-cx0-dateisicherung-nachruesten-und-versionsa](./quick/260909-cx0-dateisicherung-nachruesten-und-versionsa/) |
|
||||
| 260909-dgj | Mandantentrennung auf Datenbankebene vorbereitet (WINDOWS #18). Ausloeser war ein gemessener Befund: die Anwendung verbindet als Rolle mit Superuser- und BYPASSRLS-Recht, daher greifen die sieben vorhandenen Policies gar nicht — ohne gesetzten Mandantenkontext lieferte 'SELECT count(*) FROM Group' zwei statt null Zeilen. Gebaut wurden: Rolle `tessera_app` ohne Umgehungsrecht (wiederholbare Migration, kein Kennwort im SQL), Trennung von Migrations- und Laufzeitverbindung ueber TESSERA_MIGRATE_DATABASE_URL, ein Pruefwerkzeug mit fuenf transaktionssicheren Nachweisen, Policies fuer die 16 fehlenden Tabellen und eine Betriebsanleitung. **Der Schalter bleibt bewusst aus, #18 bleibt offen:** im Code stehen 182 Datenbankzugriffe ohne Mandantenkontext gegen 19 mit — darunter zwingend der Anmeldeweg, der die Benutzerzeile liest, bevor der Mandant bekannt ist (er kommt erst aus dieser Zeile). Ein Umschalten wuerde die Anmeldung fuer alle sperren. Dabei fiel #19 an: SearchProvider und TenderRssFeedSource haben ein nullable tenantId; die einfache Policy wuerde die plattformweiten Zeilen nach dem Scharfschalten fuer jeden Mandanten unsichtbar machen. 673/673 Tests gruen | 2026-09-09 | efaabc9 | [260909-dgj-mandantentrennung-auf-alle-tabellen-mit-](./quick/260909-dgj-mandantentrennung-auf-alle-tabellen-mit-/) |
|
||||
|
||||
## Deferred Items
|
||||
|
||||
@@ -407,7 +408,7 @@ sind. Kein Anlass, sie vorher erneut vorzulegen.
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-09-09T07:38:54.774Z
|
||||
Stopped at: Quick Task 260909-cx0 abgeschlossen: user-files-Volume ergaenzt (WINDOWS #17 bleibt offen bis Server-Uebernahme), CLAUDE.md-Technik-Block auf installierten Stand korrigiert
|
||||
Last session: 2026-09-09T10:10:00.000Z
|
||||
Stopped at: Alles Beauftragte erledigt. Offen im Ledger: #17 (der User traegt die Volume-Zeile in /opt/tessera/docker-compose.yml nach), #18 (Mandantentrennung scharf schalten — braucht die 182 unskalierten Zugriffe, eigener Vorgang) und #19 (nullable tenantId, zusammen mit #18 zu loesen). Zurueckgestellt bleiben #12, Abnahmeplan 02-05, Mandanten-Branding und die Lizenzpruefung.
|
||||
Resume file: None
|
||||
Last activity: 2026-09-09 - Ledger geschlossen, Mandanten-Branding zurueckgestellt
|
||||
Last activity: 2026-09-09 - Dateisicherung, Versionsangaben und Vorbereitung der Mandantentrennung
|
||||
|
||||
+16
-3
@@ -1,10 +1,10 @@
|
||||
---
|
||||
schema_version: 1
|
||||
open_count: 2
|
||||
open_count: 3
|
||||
waived_count: 1
|
||||
fixed_count: 15
|
||||
total_count: 18
|
||||
last_updated: 2026-09-09T07:42:13.878Z
|
||||
total_count: 19
|
||||
last_updated: 2026-09-09T08:08:19.293Z
|
||||
---
|
||||
|
||||
# Broken Windows Ledger
|
||||
@@ -33,6 +33,7 @@ last_updated: 2026-09-09T07:42:13.878Z
|
||||
| 16 | 14 | unmet-truth | apps/api/src/tenders/tenders.controller.ts | | Das Postfach im Ausschreibungs-Radar hat keinen Verbindungstest, obwohl die Faehigkeit fertig vorliegt. Beide Inbox-Provider bringen testConnection() mit (imap.provider.ts:343, exchange-inbox.provider.ts:294), und das DKV-Modul nutzt sie ueber POST /dkv/test-connection samt Knopf 'Verbindung testen' im InboxConfigForm. Beim Ausschreibungs-Radar fehlt beides: der Controller kennt zu email-config nur GET (:343) und PUT (:357), kein Test-Endpunkt, und das Formular unter Meine Quellen hat keinen Knopf. Historie geprueft: der Knopf war nie vorhanden (git log -S testConnection im tender-radar-Frontend ist leer), es ist also eine Luecke, keine Regression. Folge fuer den Betrieb: ein Tippfehler in der EWS-Endpunkt-URL oder falsche Zugangsdaten fallen erst auf, wenn dauerhaft nichts ankommt — und dann ist nicht unterscheidbar, ob die Verbindung scheitert oder schlicht keine Alarm-Mail da war. Das trifft besonders WINDOWS #12, dessen ganzer Zweck der Beleg des handgeschriebenen NTLM/SOAP-Wegs ist. Aufgefallen am 2026-09-07 beim Einrichten des Postfachs. | fixed | | 2026-09-07T13:22:33.916Z | 2026-09-09T05:20:09.851Z |
|
||||
| 17 | 6 | unmet-truth | docker-compose.yml | | Hochgeladene Dateien ueberleben kein Neuerstellen der Container. Der Code legt sie unter user-files/ ab (user.controller.ts:40 und :266 fuer Profilbilder, dazu die DKV-Exporte), aber KEINE der Compose-Dateien mountet dieses Verzeichnis — weder im Repository (docker-compose.yml, .prod.yml, .dev.yml haben nur das Volume pgdata) noch in der abweichenden Datei auf dem Server /opt/tessera/docker-compose.yml. Am 2026-09-09 gemessen: 'docker inspect' auf tessera-api-1 meldet ueberhaupt keinen Mount, /app/user-files liegt damit nur in der beschreibbaren Container-Schicht und ist bei jedem 'up -d --force-recreate' weg. Aufgefallen beim Schreiben des Betriebshandbuchs. KEIN Schaden entstanden: aktuell hat kein Nutzer ein Profilbild hinterlegt (avatarPath ueberall NULL), und die bisherigen Neuerstellungen trafen einen leeren Ordner. Die Luecke schlaegt zu, sobald der erste Nutzer ein Bild hochlaedt oder ein DKV-Export aufgehoben werden soll. Behebung: ein benanntes Volume oder Bind-Mount fuer user-files in beiden Compose-Dateien; die Server-Datei muss zusaetzlich von Hand ergaenzt werden, weil sie vom Repository abweicht. | open | | 2026-09-09T06:42:22.801Z | |
|
||||
| 18 | 2 | unmet-truth | docker-compose.yml | | Die Mandantentrennung auf Datenbankebene ist wirkungslos, weil die Anwendungsrolle sie umgeht. Die API verbindet laut docker-compose.yml:33 als Rolle 'tessera'; diese Rolle hat auf alpha rolsuper=t UND rolbypassrls=t. PostgreSQL wendet Row-Level-Security auf solche Rollen grundsaetzlich nicht an — auch FORCE ROW LEVEL SECURITY aendert daran nichts, das erzwingt nur die Anwendung auf den Tabelleneigentuemer, nicht auf BYPASSRLS-Rollen. Am 2026-09-09 praktisch gemessen: ohne gesetztes app.current_tenant liefert 'SELECT count(*) FROM "Group"' zwei Zeilen, waehrend die Policy USING ("tenantId" = current_tenant_id()) bei NULL-Kontext null Zeilen liefern muesste. Damit sind alle sieben bisher mit RLS ausgestatteten Tabellen (User, Group, GroupMembership, LdapConfig, LdapFieldMapping, ModuleGrant, PasswordResetToken) faktisch ungeschuetzt; die Trennung haengt allein am manuellen 'where tenantId' im Anwendungscode. Die Migration 20260804130918 nennt RLS ausdruecklich 'ein zweites Sicherheitsnetz' — dieses Netz existiert derzeit nicht. Reihenfolge der Behebung: ZUERST eine eigene Anwendungsrolle ohne Superuser- und BYPASSRLS-Recht einrichten und die Anwendung darauf umstellen, DANN greifen die vorhandenen Policies, und ERST DANN lohnt es, fehlende Tabellen zu ergaenzen. Vorher gebaute Policies waeren wirkungslos und wuerden eine Sicherheit vortaeuschen. Kein akutes Risiko, solange Tessera nur intern und einmandantig laeuft (ein einziger Mandant 'default'), aber vor jedem Kundeneinsatz zwingend. | open | | 2026-09-09T07:42:13.878Z | |
|
||||
| 19 | 2 | unmet-truth | apps/api/prisma/migrations/20260909140000_rls_remaining_tenant_tables/migration.sql | | Zwei der neuen Policies wuerden plattformweite Zeilen unsichtbar machen, sobald die Mandantentrennung scharf geschaltet wird. SearchProvider und TenderRssFeedSource haben ein nullable tenantId: Zeilen mit tenantId = NULL gelten fuer alle Mandanten (die von der Administration gepflegten Feeds und Suchanbieter). Die einfache Policy 'tenantId = current_tenant_id()' vergleicht NULL niemals gleich, diese Zeilen waeren nach der Aktivierung fuer JEDEN Mandanten weg — nicht nur fuer fremde. Heute ohne Wirkung, weil die Anwendung weiter als BYPASSRLS-Rolle verbindet (#18, Schalter bewusst aus). Beim Scharfschalten zwingend mitzuloesen, zusammen mit den 182 unskalierten Zugriffen: die Policy muss die plattformweiten Zeilen ausdruecklich einschliessen, etwa ueber 'tenantId IS NULL OR tenantId = current_tenant_id()' fuer den Lesezugriff, waehrend Schreibzugriffe weiterhin einen Mandanten verlangen. Beim Schreiben der Migration am 2026-09-09 aufgefallen und bewusst nicht eigenmaechtig anders geloest, weil die richtige Semantik eine Produktentscheidung ist. | open | | 2026-09-09T08:08:19.293Z | |
|
||||
|
||||
````json
|
||||
[
|
||||
@@ -251,6 +252,18 @@ last_updated: 2026-09-09T07:42:13.878Z
|
||||
"reason": "",
|
||||
"recorded_at": "2026-09-09T07:42:13.878Z",
|
||||
"resolved_at": null
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"kind": "unmet-truth",
|
||||
"phase": "2",
|
||||
"file": "apps/api/prisma/migrations/20260909140000_rls_remaining_tenant_tables/migration.sql",
|
||||
"line": null,
|
||||
"description": "Zwei der neuen Policies wuerden plattformweite Zeilen unsichtbar machen, sobald die Mandantentrennung scharf geschaltet wird. SearchProvider und TenderRssFeedSource haben ein nullable tenantId: Zeilen mit tenantId = NULL gelten fuer alle Mandanten (die von der Administration gepflegten Feeds und Suchanbieter). Die einfache Policy 'tenantId = current_tenant_id()' vergleicht NULL niemals gleich, diese Zeilen waeren nach der Aktivierung fuer JEDEN Mandanten weg — nicht nur fuer fremde. Heute ohne Wirkung, weil die Anwendung weiter als BYPASSRLS-Rolle verbindet (#18, Schalter bewusst aus). Beim Scharfschalten zwingend mitzuloesen, zusammen mit den 182 unskalierten Zugriffen: die Policy muss die plattformweiten Zeilen ausdruecklich einschliessen, etwa ueber 'tenantId IS NULL OR tenantId = current_tenant_id()' fuer den Lesezugriff, waehrend Schreibzugriffe weiterhin einen Mandanten verlangen. Beim Schreiben der Migration am 2026-09-09 aufgefallen und bewusst nicht eigenmaechtig anders geloest, weil die richtige Semantik eine Produktentscheidung ist.",
|
||||
"status": "open",
|
||||
"reason": "",
|
||||
"recorded_at": "2026-09-09T08:08:19.293Z",
|
||||
"resolved_at": null
|
||||
}
|
||||
]
|
||||
````
|
||||
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
---
|
||||
phase: quick-260909-dgj
|
||||
plan: 01
|
||||
subsystem: database
|
||||
tags: [postgresql, rls, prisma, multi-tenancy, docker, security]
|
||||
|
||||
requires:
|
||||
- phase: 02-authentication-multi-tenancy
|
||||
provides: current_tenant_id(), forTenant() extension, sieben Ausgangs-Policies
|
||||
- phase: 15-modul-berechtigungen-gruppen-user-grants
|
||||
provides: Group/GroupMembership/ModuleGrant RLS-Muster (20260804130918)
|
||||
|
||||
provides:
|
||||
- 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
|
||||
|
||||
affects: [datenbank, betrieb, mandantenfaehigkeit, WINDOWS-18]
|
||||
|
||||
actuals:
|
||||
tokens: 12875
|
||||
tasks: 4
|
||||
commits: 4
|
||||
|
||||
tech-stack:
|
||||
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"
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- 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
|
||||
modified:
|
||||
- apps/api/Dockerfile
|
||||
- docker-compose.yml
|
||||
- docker-compose.prod.yml
|
||||
- .env.example
|
||||
- docs/README.md
|
||||
|
||||
key-decisions:
|
||||
- "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"
|
||||
|
||||
patterns-established:
|
||||
- "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"
|
||||
|
||||
requirements-completed: [WINDOWS-18]
|
||||
|
||||
coverage:
|
||||
- id: D1
|
||||
description: "Datenbankrolle tessera_app wird wiederholbar angelegt, traegt weder Superuser- noch BYPASSRLS-Recht, kein Kennwort im SQL"
|
||||
requirement: "WINDOWS-18"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "apps/api/src/prisma/rls-app-role.spec.ts (7 Tests)"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D2
|
||||
description: "Migrations- und Laufzeitverbindung sind getrennt (TESSERA_MIGRATE_DATABASE_URL); ohne gesetzte Variable bleibt der Ablauf exakt der bisherige"
|
||||
requirement: "WINDOWS-18"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "apps/api/src/prisma/start-script.spec.ts (5 Tests)"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D3
|
||||
description: "Pruefwerkzeug misst fuenf benannte Eigenschaften transaktionssicher, verbindet in --print-plan nicht, schreibt in keiner Betriebsart"
|
||||
requirement: "WINDOWS-18"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "apps/api/src/prisma/rls-preflight.spec.ts (5 Tests)"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D4
|
||||
description: "Alle 20 Modelle mit tenantId tragen eine RLS-Policy; 5 bewusste Ausnahmen und 3 Join-Muster sind namentlich mit Begruendung festgehalten"
|
||||
requirement: "WINDOWS-18"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "apps/api/src/prisma/rls-coverage.spec.ts (5 Tests)"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D5
|
||||
description: "Die neuen Policies wirken erst nach einer manuellen, vom Betreiber ausgefuehrten Umstellung — ein Live-Nachweis am tatsaechlich umgestellten System ist ausserhalb dieses Vorgangs"
|
||||
verification: []
|
||||
human_judgment: true
|
||||
rationale: "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."
|
||||
|
||||
duration: 15min
|
||||
completed: 2026-09-09
|
||||
status: 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*
|
||||
Reference in New Issue
Block a user