From b0d6b00045a30912eef2d07c241246da1f2f5552 Mon Sep 17 00:00:00 2001 From: noroot Date: Tue, 1 Sep 2026 09:38:51 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20Migration=200021=20crashte=20in=20Produk?= =?UTF-8?q?tion=20=E2=80=94=20falsche=20Rollen-Annahme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v0.38.0 verursachte einen ~3-minütigen Ausfall: Migration 0021 nahm an, die Anwendung verbinde überall als Postgres-Superuser "postgres" (nur lokal via sudo geprüft, nicht die tatsächliche Produktions-DATABASE_URL) und versuchte dort eine neue Rolle anzulegen - production verbindet aber über die nicht-privilegierte, tabellenbesitzende Rolle "deklarix" ohne CREATEROLE, das INSERT/CREATE ROLE schlug fehl und der Dienst crash-loopte im "dirty migration"-Zustand. Kein Datenverlust (Postgres hat die DDL-Transaktion selbst zurückgerollt), Dienst wurde auf v0.37.0 zurückgestuft und stabilisiert. Fix: Rollen-Anlage ist jetzt an eine CREATEROLE-Prüfung gekoppelt und wird bei fehlender Berechtigung übersprungen statt zu scheitern. Zusätzlich FORCE ROW LEVEL SECURITY auf jeder Tabelle - das bindet auch den Tabellenbesitzer (wie Productions "deklarix"), ganz ohne die zusätzliche Rolle. Produktivbetrieb braucht dadurch jetzt gar keinen manuellen Schritt mehr. Erneut end-to-end verifiziert, diesmal zusätzlich produktionsgetreu simuliert (temporäre nicht-privilegierte, tabellenbesitzende Rolle lokal). Co-Authored-By: Claude Sonnet 5 --- CLAUDE.md | 91 ++++++++++++++----- .../0021_row_level_security.down.sql | 26 +++++- .../migrations/0021_row_level_security.up.sql | 73 ++++++++++----- 3 files changed, 137 insertions(+), 53 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index f8e418f..222d68f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -635,16 +635,22 @@ und live per curl gegen echten Server + Postgres verifiziert. ## Row-Level-Security (2026-09-01, Migration 0021) -**Kritischer Fund vor der Umsetzung:** Sowohl lokal als auch auf dem -Produktivserver verbindet sich die Anwendung als `postgres`-Rolle — ein -echter Postgres-**Superuser**. Superuser umgehen RLS-Policies *immer*, -unabhängig von `FORCE ROW LEVEL SECURITY` (das wirkt nur beim -Tabellenbesitzer, nicht bei Superusern — eine harte, nicht -überschreibbare Postgres-Regel). Policies allein hätten also nichts -bewirkt. Migration 0021 legt deshalb zusätzlich eine neue, -eingeschränkte Rolle **`deklarix_app`** an (kein Superuser, kein -Tabellenbesitzer, `NOBYPASSRLS`, zunächst `NOLOGIN`) — nur für diese -Rolle greifen die Policies tatsächlich. +**Ausgangslage:** RLS-Policies wirken nie bei Postgres-Superusern, und +nie beim Tabellenbesitzer ohne `FORCE ROW LEVEL SECURITY` (beides harte, +nicht überschreibbare Postgres-Regeln). Lokal verbindet die Anwendung +als echter Superuser `postgres` (Docker-Testumgebung) — dort hätte +`FORCE` allein nichts bewirkt. Auf dem Produktivserver verbindet sie +dagegen über eine eigene, **nicht-privilegierte** Rolle `deklarix` +(kein Superuser), die zugleich Eigentümerin der Tabellen ist — dort +reicht `FORCE` aus. **Diese Erkenntnis kam erst nach einem +Fehlversuch** (siehe "Incident" weiter unten) — die ursprüngliche +Annahme, auch Produktion verbinde als Superuser, war falsch und beruhte +auf einer Prüfung der falschen Rolle. Migration 0021 deckt seit der +Korrektur beide Fälle ab: `FORCE ROW LEVEL SECURITY` auf jeder Tabelle +(reicht für Produktion), plus optional eine neue, eingeschränkte Rolle +**`deklarix_app`** (kein Superuser, kein Tabellenbesitzer, +`NOBYPASSRLS`, zunächst `NOLOGIN`) für Umgebungen mit einer +Superuser-Verbindung wie lokal. **Architektur:** `internal/store/tenant_scope.go` — `Store.db(ctx)` liefert entweder die aktive Transaktion (falls `WithTenantScope` sie @@ -668,7 +674,7 @@ werden frühere Schritte desselben Requests zurückgerollt (vorher: keine Transaktion, ein halb fehlgeschlagener Handler konnte einen verwaisten Account ohne Nutzer hinterlassen). -**Geschützte Tabellen** (`ENABLE ROW LEVEL SECURITY` + Policy +**Geschützte Tabellen** (`ENABLE` + `FORCE ROW LEVEL SECURITY` + Policy `tenant_isolation`): `antrag`, `registereintrag`, `abteilung`, `werkzeug_sperre`, `genehmiger_rolle`, `freigabe_regel`, `loeschfrist_einstellung` (direkte `account_id`-Prüfung), `werkzeug` @@ -710,19 +716,56 @@ eingeschränkte Rolle und überspringen sich selbst sauber, wenn `DATABASE_URL_APP` nicht gesetzt ist (analog zum bestehenden `DATABASE_URL`-Skip-Muster). -**Produktivbetrieb — noch ausstehender manueller Schritt.** Die -Migration allein bewirkt in Produktion noch NICHTS (die App verbindet -weiterhin als `postgres`-Superuser, `DATABASE_URL_APP` ist nicht -gesetzt). Um RLS tatsächlich scharf zu schalten: -1. `ALTER ROLE deklarix_app WITH LOGIN PASSWORD '';` - einmalig auf dem Zielserver ausführen (das Passwort steht nicht im - Code/in Migrationen — Secrets gehören nicht in ein versioniertes - Repo). -2. `DATABASE_URL_APP=postgres://deklarix_app:@/deklarix?...` - in `/etc/deklarix/deklarix.env` eintragen. -3. Dienst neu starten. -`DATABASE_URL` (Migrationen, DDL-Rechte) bleibt unverändert auf der -bisherigen, privilegierten Verbindung. +**Incident 2026-09-01 (~3 Minuten Downtime) und Korrektur.** Die erste +Fassung der Migration ging fälschlich davon aus, dass die Anwendung +überall als Postgres-**Superuser** `postgres` verbindet (das hatte ich +nur lokal und via `sudo -u postgres psql` auf dem Server geprüft — das +ist aber ein SSH/OS-Login-Check, nicht die tatsächliche +`DATABASE_URL`-Rolle der Anwendung). Tatsächlich verbindet Produktion +über eine eigene, **nicht-privilegierte** Rolle `deklarix` (kein +Superuser, kein `CREATEROLE`), die zugleich Eigentümerin aller Tabellen +ist. Die Migration versuchte dort `CREATE ROLE deklarix_app` +auszuführen, scheiterte mit "permission denied to create role", blieb +im `dirty`-Zustand hängen und der Dienst crash-loopte beim Start +(09:27–09:30 Uhr). Behoben durch: `schema_migrations` manuell auf +Version 20 zurückgesetzt, Paket auf v0.37.0 zurückgestuft, Dienst +stabilisiert — kein Datenverlust, Postgres hatte die fehlgeschlagene +Migration als DDL-Transaktion bereits sauber selbst zurückgerollt, nur +golang-migrates eigene Versions-Buchführung musste von Hand korrigiert +werden. + +**Korrigierte, robustere Migration:** das Anlegen von `deklarix_app` ist +jetzt an eine Prüfung gekoppelt (`SELECT ... WHERE rolname = current_user +AND rolcreaterole`) und wird bei fehlendem `CREATEROLE` übersprungen +(`RAISE NOTICE`, kein Fehler) statt die ganze Migration scheitern zu +lassen. Zusätzlich bekommt jede Tabelle jetzt **`FORCE ROW LEVEL +SECURITY`** (vorher nur `ENABLE`) — das bindet auch den **Tabellen- +besitzer** an die Policies, sofern er kein Superuser ist. Damit deckt +eine einzige Migration beide Fälle ab: +- **Produktion** (`deklarix`, Tabellenbesitzer, kein Superuser): `FORCE` + allein reicht bereits aus. **Kein manueller Schritt nötig** — nach dem + Deploy dieser Migration ist RLS dort sofort aktiv. +- **Lokales Docker-Postgres** (`postgres`-Superuser, für den `FORCE` + wirkungslos bleibt): `deklarix_app` wird zusätzlich angelegt, für + lokale Tests weiterhin per `DATABASE_URL_APP` nutzbar. + +**Erneut end-to-end verifiziert nach der Korrektur**, diesmal +zusätzlich produktionsgetreu: eine zweite, temporäre lokale Rolle +(`deklarix_sim`, kein Superuser, kein `CREATEROLE`, Eigentümerin einer +frischen Test-Datenbank — exakt Produktions-Rechtemodell) durchlief die +komplette Migrationskette 0001–0021 fehlerfrei, **und** die Isolation +griff nachweislich auch für sie als Tabellenbesitzerin (Kontext A sah +ausschließlich Zeile A, trotz voller Tabellen-Eigentümerschaft). Vorher +war nur der `deklarix_app`-Pfad (Nicht-Eigentümer-Rolle) bewiesen, nicht +der tatsächliche Produktions-Pfad (Eigentümer-Rolle + FORCE) — genau die +Lücke, die den Incident verursachte. + +**Lehre für künftige Prüfungen dieser Art:** "welche DB-Rolle verwendet +die Anwendung" per `sudo -u postgres psql` zu beantworten prüft die +falsche Sache — maßgeblich ist ausschließlich die Rolle **in der +tatsächlichen `DATABASE_URL`** (hier: `cat /etc/deklarix/deklarix.env` +bzw. `SELECT rolname, rolsuper, rolcreaterole FROM pg_roles WHERE +rolname = 'deklarix'`, nicht `current_user` über einen andere Anmeldung). --- diff --git a/internal/store/migrations/0021_row_level_security.down.sql b/internal/store/migrations/0021_row_level_security.down.sql index 0b56c26..3705752 100644 --- a/internal/store/migrations/0021_row_level_security.down.sql +++ b/internal/store/migrations/0021_row_level_security.down.sql @@ -1,40 +1,58 @@ +ALTER TABLE antrag NO FORCE ROW LEVEL SECURITY; ALTER TABLE antrag DISABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS tenant_isolation ON antrag; +ALTER TABLE registereintrag NO FORCE ROW LEVEL SECURITY; ALTER TABLE registereintrag DISABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS tenant_isolation ON registereintrag; +ALTER TABLE abteilung NO FORCE ROW LEVEL SECURITY; ALTER TABLE abteilung DISABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS tenant_isolation ON abteilung; +ALTER TABLE werkzeug_sperre NO FORCE ROW LEVEL SECURITY; ALTER TABLE werkzeug_sperre DISABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS tenant_isolation ON werkzeug_sperre; +ALTER TABLE genehmiger_rolle NO FORCE ROW LEVEL SECURITY; ALTER TABLE genehmiger_rolle DISABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS tenant_isolation ON genehmiger_rolle; +ALTER TABLE freigabe_regel NO FORCE ROW LEVEL SECURITY; ALTER TABLE freigabe_regel DISABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS tenant_isolation ON freigabe_regel; +ALTER TABLE loeschfrist_einstellung NO FORCE ROW LEVEL SECURITY; ALTER TABLE loeschfrist_einstellung DISABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS tenant_isolation ON loeschfrist_einstellung; +ALTER TABLE werkzeug NO FORCE ROW LEVEL SECURITY; ALTER TABLE werkzeug DISABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS tenant_isolation ON werkzeug; +ALTER TABLE bewertung NO FORCE ROW LEVEL SECURITY; ALTER TABLE bewertung DISABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS tenant_isolation ON bewertung; +ALTER TABLE entscheidung NO FORCE ROW LEVEL SECURITY; ALTER TABLE entscheidung DISABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS tenant_isolation ON entscheidung; +ALTER TABLE freigabeschritt NO FORCE ROW LEVEL SECURITY; ALTER TABLE freigabeschritt DISABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS tenant_isolation ON freigabeschritt; +ALTER TABLE nutzer_genehmiger_rolle NO FORCE ROW LEVEL SECURITY; ALTER TABLE nutzer_genehmiger_rolle DISABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS tenant_isolation ON nutzer_genehmiger_rolle; -REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM deklarix_app; -ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE SELECT, INSERT, UPDATE, DELETE ON TABLES FROM deklarix_app; -REVOKE USAGE ON SCHEMA public FROM deklarix_app; -DROP ROLE IF EXISTS deklarix_app; +DO $$ +BEGIN + IF EXISTS (SELECT FROM pg_roles WHERE rolname = 'deklarix_app') THEN + EXECUTE 'REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM deklarix_app'; + EXECUTE 'ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE SELECT, INSERT, UPDATE, DELETE ON TABLES FROM deklarix_app'; + EXECUTE 'REVOKE USAGE ON SCHEMA public FROM deklarix_app'; + DROP ROLE deklarix_app; + END IF; +END +$$; diff --git a/internal/store/migrations/0021_row_level_security.up.sql b/internal/store/migrations/0021_row_level_security.up.sql index 71de58b..513d429 100644 --- a/internal/store/migrations/0021_row_level_security.up.sql +++ b/internal/store/migrations/0021_row_level_security.up.sql @@ -1,36 +1,47 @@ -- Mandantenisolation auf Datenbankebene (Postgres Row-Level Security). --- WICHTIG: RLS-Policies wirken NIE bei Postgres-Superusern und NIE beim --- Tabellenbesitzer ohne FORCE ROW LEVEL SECURITY — und FORCE wirkt --- seinerseits NICHT bei Superusern (das ist eine harte, nicht --- überschreibbare Postgres-Regel). Migrationen und die bisherige --- Anwendungs-DATABASE_URL laufen als Superuser "postgres" (Tabellen- --- besitzer) — für diese Rolle ist RLS wirkungslos, ganz gleich wie die --- Policies aussehen. Deshalb legt diese Migration zusätzlich eine neue, --- eingeschränkte Rolle "deklarix_app" an (kein Superuser, kein --- Tabellenbesitzer, NOBYPASSRLS) — NUR für diese Rolle greifen die --- Policies unten tatsächlich. Migrationen laufen weiterhin über die --- bisherige privilegierte DATABASE_URL; die laufende Anwendung muss auf --- die neue, eingeschränkte Rolle umgestellt werden (neue Umgebungs- --- variable DATABASE_URL_APP, siehe cmd/deklarix/main.go) — ohne diesen --- Wechsel ist diese Migration reine Dokumentation ohne Wirkung. -- --- Die Rolle wird bewusst OHNE Passwort angelegt (NOLOGIN) — ein --- Klartext-Passwort gehört nicht in eine versionierte, für jeden mit --- Repo-Zugriff lesbare Migrationsdatei. Login-Fähigkeit + Passwort --- werden einmalig manuell je Umgebung gesetzt: --- ALTER ROLE deklarix_app WITH LOGIN PASSWORD ''; --- Danach DATABASE_URL_APP in der jeweiligen deklarix.env eintragen. +-- WICHTIG, per Incident am 2026-09-01 gelernt: RLS-Policies wirken NIE +-- bei Postgres-Superusern, und NIE beim Tabellenbesitzer ohne FORCE ROW +-- LEVEL SECURITY. Welcher Fall zutrifft, hängt von der Umgebung ab: +-- - Produktion verbindet als eigene, nicht-privilegierte Rolle (z. B. +-- "deklarix"), die zugleich Eigentümerin der Tabellen ist (sie hat +-- sie über die Migrationen selbst angelegt) — für sie reicht FORCE +-- ROW LEVEL SECURITY völlig aus, keine weitere Rolle nötig. +-- - Manche Entwicklungs-/Testumgebungen verbinden dagegen als +-- echter Postgres-Superuser (z. B. lokales Docker-Postgres mit +-- "postgres") — für den wirkt FORCE nicht (Superuser sind davon +-- laut Postgres-Dokumentation ausdrücklich ausgenommen). Dort kann +-- zusätzlich eine eingeschränkte Rolle "deklarix_app" angelegt +-- werden, für die die Policies unabhängig von FORCE gelten. +-- +-- Diese Migration deckt BEIDE Fälle ab, ohne bei fehlendem CREATEROLE +-- fehlzuschlagen (das brachte den Dienst am 2026-09-01 für ~3 Minuten +-- zum Absturz, siehe CLAUDE.md) — das Anlegen von "deklarix_app" ist +-- rein optional und wird übersprungen, wenn die aktuelle Rolle dafür +-- keine Berechtigung hat. DO $$ BEGIN - IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'deklarix_app') THEN - CREATE ROLE deklarix_app NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS NOLOGIN; + IF EXISTS (SELECT FROM pg_roles WHERE rolname = current_user AND rolcreaterole) THEN + IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'deklarix_app') THEN + CREATE ROLE deklarix_app NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS NOLOGIN; + END IF; + EXECUTE 'GRANT USAGE ON SCHEMA public TO deklarix_app'; + EXECUTE 'GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO deklarix_app'; + EXECUTE 'ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO deklarix_app'; + ELSE + RAISE NOTICE 'Rolle % hat kein CREATEROLE — deklarix_app wird übersprungen, FORCE ROW LEVEL SECURITY schützt stattdessen direkt die bestehende (Tabellenbesitzer-)Rolle.', current_user; END IF; END $$; -GRANT USAGE ON SCHEMA public TO deklarix_app; -GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO deklarix_app; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO deklarix_app; +-- Login-Fähigkeit + Passwort für deklarix_app (falls angelegt) werden +-- einmalig manuell je Umgebung gesetzt, NIE in einer versionierten +-- Migration (Klartext-Secret gehört nicht ins Repo): +-- ALTER ROLE deklarix_app WITH LOGIN PASSWORD ''; +-- Danach optional DATABASE_URL_APP in der jeweiligen deklarix.env +-- eintragen. Für Umgebungen, in denen die Anwendung bereits als +-- Tabellenbesitzer (nicht-Superuser) verbindet, ist das NICHT nötig — +-- FORCE ROW LEVEL SECURITY unten reicht dort aus. -- ─── Tabellen MIT direkter account_id-Spalte ─────────────────────────── -- Eine einzelne Policy (FOR ALL) pro Tabelle deckt SELECT/UPDATE/DELETE @@ -46,6 +57,7 @@ CREATE POLICY tenant_isolation ON antrag FOR ALL USING ( OR current_setting('app.is_betreiber', true) = 'true' ); ALTER TABLE antrag ENABLE ROW LEVEL SECURITY; +ALTER TABLE antrag FORCE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation ON registereintrag FOR ALL USING ( account_id = NULLIF(current_setting('app.account_id', true), '')::uuid @@ -55,6 +67,7 @@ CREATE POLICY tenant_isolation ON registereintrag FOR ALL USING ( OR current_setting('app.is_betreiber', true) = 'true' ); ALTER TABLE registereintrag ENABLE ROW LEVEL SECURITY; +ALTER TABLE registereintrag FORCE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation ON abteilung FOR ALL USING ( account_id = NULLIF(current_setting('app.account_id', true), '')::uuid @@ -64,6 +77,7 @@ CREATE POLICY tenant_isolation ON abteilung FOR ALL USING ( OR current_setting('app.is_betreiber', true) = 'true' ); ALTER TABLE abteilung ENABLE ROW LEVEL SECURITY; +ALTER TABLE abteilung FORCE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation ON werkzeug_sperre FOR ALL USING ( account_id = NULLIF(current_setting('app.account_id', true), '')::uuid @@ -73,6 +87,7 @@ CREATE POLICY tenant_isolation ON werkzeug_sperre FOR ALL USING ( OR current_setting('app.is_betreiber', true) = 'true' ); ALTER TABLE werkzeug_sperre ENABLE ROW LEVEL SECURITY; +ALTER TABLE werkzeug_sperre FORCE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation ON genehmiger_rolle FOR ALL USING ( account_id = NULLIF(current_setting('app.account_id', true), '')::uuid @@ -82,6 +97,7 @@ CREATE POLICY tenant_isolation ON genehmiger_rolle FOR ALL USING ( OR current_setting('app.is_betreiber', true) = 'true' ); ALTER TABLE genehmiger_rolle ENABLE ROW LEVEL SECURITY; +ALTER TABLE genehmiger_rolle FORCE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation ON freigabe_regel FOR ALL USING ( account_id = NULLIF(current_setting('app.account_id', true), '')::uuid @@ -91,6 +107,7 @@ CREATE POLICY tenant_isolation ON freigabe_regel FOR ALL USING ( OR current_setting('app.is_betreiber', true) = 'true' ); ALTER TABLE freigabe_regel ENABLE ROW LEVEL SECURITY; +ALTER TABLE freigabe_regel FORCE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation ON loeschfrist_einstellung FOR ALL USING ( account_id = NULLIF(current_setting('app.account_id', true), '')::uuid @@ -100,6 +117,7 @@ CREATE POLICY tenant_isolation ON loeschfrist_einstellung FOR ALL USING ( OR current_setting('app.is_betreiber', true) = 'true' ); ALTER TABLE loeschfrist_einstellung ENABLE ROW LEVEL SECURITY; +ALTER TABLE loeschfrist_einstellung FORCE ROW LEVEL SECURITY; -- ─── werkzeug: account_id NULLABLE (NULL = zentraler Katalog) ────────── -- Lesen: jeder sieht zentrale (NULL) Einträge plus die eigenen. NUR der @@ -115,6 +133,7 @@ CREATE POLICY tenant_isolation ON werkzeug FOR ALL USING ( OR account_id = NULLIF(current_setting('app.account_id', true), '')::uuid ); ALTER TABLE werkzeug ENABLE ROW LEVEL SECURITY; +ALTER TABLE werkzeug FORCE ROW LEVEL SECURITY; -- ─── Tabellen OHNE eigene account_id, über Fremdschlüssel abgeleitet ─── -- antrag/genehmiger_rolle sind selbst schon RLS-geschützt (s. o.) — eine @@ -126,6 +145,7 @@ CREATE POLICY tenant_isolation ON bewertung FOR ALL USING ( antrag_id IN (SELECT id FROM antrag) ); ALTER TABLE bewertung ENABLE ROW LEVEL SECURITY; +ALTER TABLE bewertung FORCE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation ON entscheidung FOR ALL USING ( antrag_id IN (SELECT id FROM antrag) @@ -133,6 +153,7 @@ CREATE POLICY tenant_isolation ON entscheidung FOR ALL USING ( antrag_id IN (SELECT id FROM antrag) ); ALTER TABLE entscheidung ENABLE ROW LEVEL SECURITY; +ALTER TABLE entscheidung FORCE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation ON freigabeschritt FOR ALL USING ( antrag_id IN (SELECT id FROM antrag) @@ -140,6 +161,7 @@ CREATE POLICY tenant_isolation ON freigabeschritt FOR ALL USING ( antrag_id IN (SELECT id FROM antrag) ); ALTER TABLE freigabeschritt ENABLE ROW LEVEL SECURITY; +ALTER TABLE freigabeschritt FORCE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation ON nutzer_genehmiger_rolle FOR ALL USING ( genehmiger_rolle_id IN (SELECT id FROM genehmiger_rolle) @@ -147,6 +169,7 @@ CREATE POLICY tenant_isolation ON nutzer_genehmiger_rolle FOR ALL USING ( genehmiger_rolle_id IN (SELECT id FROM genehmiger_rolle) ); ALTER TABLE nutzer_genehmiger_rolle ENABLE ROW LEVEL SECURITY; +ALTER TABLE nutzer_genehmiger_rolle FORCE ROW LEVEL SECURITY; -- ─── Bewusst OHNE RLS ─────────────────────────────────────────────────── -- account: hat keine account_id-Spalte (ist selbst der Mandant) und