feat(quick-260911-nke): current_user_id(), Benutzerdimension in den Regeln, forTenant() mit userId — ein Pfad
- Neue Migration 20260911120000_rls_user_dimension_personal_tables: current_user_id() (NULLIF-gefaltet), zehn persoenliche Tabellen umgestellt (acht als eine Regel, SearchProvider/TenderRssFeedSource als je vier befehlsgetrennte Regeln), vier Verwaltungstabellen bewusst unveraendert. Lokal angewendet (migrate deploy, Prisma-Binary aus apps/api/node_modules/.bin), schema.prisma unveraendert. - forTenant(prisma, tenantId, userId?): beide set_config in EINER getaggten Anweisung, $transaction-Array bleibt bei zwei Eintraegen (WINDOWS #20), Leerstring ohne Benutzer statt Weglassen. - tender-saved-search.service.ts: alle vier forTenant()-Aufrufe reichen userId durch; Detektor-Regex bestaetigt 4 Treffer. - rls-scratch-check.mjs: current_user_id() aus der neuen Migration geschnitten (nicht getippt), drei Funktionsfaelle gemessen, neue runUserDimensionChecks() mit generiertem Client fuer TenderSavedSearch (vier Wahrheiten + Spaltenabgleich), die alte Loch-Pruefung tendersavedsearch-fremder-nutzer-desselben-mandanten-sichtbar umgedreht (alte Messung unter neuem Namen erhalten, neue Umkehrung MIT Benutzer). sqlStateOf() um Message-Fallback ergaenzt (RLS-Ablehnung ueber generierten Client traegt den SQLSTATE nur im Fehlertext, nicht in .meta.code). - Baseline: 1020/62 Tests, Typpruefung sauber, Werkzeug 146/146 bestanden. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
+221
@@ -0,0 +1,221 @@
|
||||
-- 260911-nke, Etappe 3b — die Benutzerdimension in den Regeln der zehn
|
||||
-- persoenlichen Tabellen. Schliesst die Klasse von Befunden, die in sieben
|
||||
-- Bereichs-Kritiken als "Policy hat keine Benutzerdimension" festgehalten
|
||||
-- wurde (docs/mandantentrennung-etappe2-fehlerrichtung.md).
|
||||
--
|
||||
-- Die betroffenen Dateien (20260618112133_rls_policies fuer die urspruengliche
|
||||
-- Tabellenform der acht Ein-Regel-Tabellen, 20260909140000_rls_remaining_
|
||||
-- tenant_tables fuer deren zuletzt ausgelieferte Fassung, 20260910120000_rls_
|
||||
-- widen_membership_grant_and_platform_read fuer TenderRssFeedSource) bleiben
|
||||
-- UNVERAENDERT stehen — Prisma fuehrt ihre Pruefsumme, eine Aenderung braechte
|
||||
-- "prisma migrate deploy" zum Abbruch. Praezedenzfall und Kopfform:
|
||||
-- 20260910120000_rls_widen_membership_grant_and_platform_read.
|
||||
--
|
||||
-- WICHTIG: wie alle bisherigen RLS-Migrationen wirken diese Regeln erst,
|
||||
-- wenn die Anwendung als Rolle ohne Umgehungsrecht verbindet (siehe
|
||||
-- 20260909130000_rls_app_role und docs/mandantentrennung-datenbankrolle.md).
|
||||
-- Die Verbindung ist zum Zeitpunkt dieser Migration weiterhin NICHT
|
||||
-- umgestellt — `DATABASE_URL` zeigt unveraendert auf die Rolle `tessera`
|
||||
-- (BYPASSRLS). Der Schalter bleibt AUS: diese Regeln sind fuer jeden
|
||||
-- heutigen Aufrufer wirkungslos, bis Etappe 4 scharfschaltet.
|
||||
|
||||
-- Zweite Sitzungsvariable `app.current_user`, Funktion nach dem Muster von
|
||||
-- `current_tenant_id()` (20260618112133). NULLIF ist Pflicht: `forTenant()`
|
||||
-- sendet "kein Benutzer" ausdruecklich als Leerstring (nicht als
|
||||
-- weggelassene Variable) — ohne NULLIF wuerde current_user_id() bei einem
|
||||
-- Aufruf ohne Benutzer den Leerstring statt NULL liefern, und
|
||||
-- "userId" = '' waere fuer jede Zeile falsch, nicht gleichbedeutend mit
|
||||
-- "kein Benutzer gesetzt". Kein GRANT EXECUTE noetig — wie bei
|
||||
-- current_tenant_id() (20260909130000_rls_app_role vergibt dafuer keines):
|
||||
-- PostgreSQL vergibt EXECUTE auf Funktionen standardmaessig an PUBLIC.
|
||||
CREATE OR REPLACE FUNCTION current_user_id() RETURNS TEXT AS $$
|
||||
SELECT NULLIF(current_setting('app.current_user', true), '');
|
||||
$$ LANGUAGE sql STABLE;
|
||||
|
||||
-- Vierzehn Tabellen tragen eine `userId`-Spalte, zehn davon sind
|
||||
-- persoenliche Daten und bekommen unten eine Regel. Die vier Ausnahmen
|
||||
-- bekommen KEINE Anweisung in dieser Migration:
|
||||
--
|
||||
-- - GroupMembership: Verwaltungsobjekt — ein Admin muss Mitgliedschaften
|
||||
-- anderer Nutzer sehen und pflegen koennen, das ist keine persoenliche
|
||||
-- Zeile des referenzierten Benutzers.
|
||||
-- - ModuleGrant: Verwaltungsobjekt — dieselbe Begruendung, ein Admin
|
||||
-- vergibt und sieht Freigaben fuer andere.
|
||||
-- - PasswordResetToken: Anmelde-Artefakt — wird gelesen, BEVOR ein
|
||||
-- Benutzer im Sinne von `app.current_user` bekannt ist (der Token IST
|
||||
-- der Weg, den Benutzer erst zu ermitteln); eine Benutzerdimension hier
|
||||
-- waere zirkulaer.
|
||||
-- - TenderMatch: wird vom Hintergrunddienst (tender-matching.service.ts)
|
||||
-- je Treffer geschrieben, nicht von einem eingeloggten Benutzer direkt;
|
||||
-- Etappe 3c behandelt Hintergrunddienste gesondert (Systemkontext).
|
||||
|
||||
-- Acht Tabellen mit NOT-NULL-`userId`: ein einzelner USING-Ausdruck genuegt,
|
||||
-- weil Lesen und Schreiben dieselbe Bedingung haben sollen — WITH CHECK
|
||||
-- folgt USING bei einer Policy ohne FOR-Klausel, ein Einfuegen/Aendern als
|
||||
-- Benutzer A mit fremder Kennung B faellt damit durch. Die `IS NULL OR`-Form
|
||||
-- macht die Aenderung fuer jeden Aufruf OHNE gesetzten Benutzer (Admin,
|
||||
-- Hintergrunddienst) wirkungslos: der sieht weiterhin den ganzen Mandanten,
|
||||
-- exakt wie vor dieser Migration (bewusste, offene Flanke — siehe
|
||||
-- .planning/WINDOWS.md). Die Regelnamen bleiben `tenant_isolation_policy`
|
||||
-- (wie jab bei GroupMembership/ModuleGrant): `extractPolicySql` und
|
||||
-- `pg_policies` behalten je Tabelle genau eine Regel.
|
||||
|
||||
DROP POLICY tenant_isolation_policy ON "CalendarSource";
|
||||
CREATE POLICY tenant_isolation_policy ON "CalendarSource"
|
||||
USING (
|
||||
"tenantId" = current_tenant_id()
|
||||
AND (current_user_id() IS NULL OR "userId" = current_user_id())
|
||||
);
|
||||
|
||||
DROP POLICY tenant_isolation_policy ON "DashboardLayout";
|
||||
CREATE POLICY tenant_isolation_policy ON "DashboardLayout"
|
||||
USING (
|
||||
"tenantId" = current_tenant_id()
|
||||
AND (current_user_id() IS NULL OR "userId" = current_user_id())
|
||||
);
|
||||
|
||||
DROP POLICY tenant_isolation_policy ON "FavoriteLink";
|
||||
CREATE POLICY tenant_isolation_policy ON "FavoriteLink"
|
||||
USING (
|
||||
"tenantId" = current_tenant_id()
|
||||
AND (current_user_id() IS NULL OR "userId" = current_user_id())
|
||||
);
|
||||
|
||||
DROP POLICY tenant_isolation_policy ON "TenderEmailConfig";
|
||||
CREATE POLICY tenant_isolation_policy ON "TenderEmailConfig"
|
||||
USING (
|
||||
"tenantId" = current_tenant_id()
|
||||
AND (current_user_id() IS NULL OR "userId" = current_user_id())
|
||||
);
|
||||
|
||||
DROP POLICY tenant_isolation_policy ON "TenderNotificationPref";
|
||||
CREATE POLICY tenant_isolation_policy ON "TenderNotificationPref"
|
||||
USING (
|
||||
"tenantId" = current_tenant_id()
|
||||
AND (current_user_id() IS NULL OR "userId" = current_user_id())
|
||||
);
|
||||
|
||||
DROP POLICY tenant_isolation_policy ON "TenderSavedSearch";
|
||||
CREATE POLICY tenant_isolation_policy ON "TenderSavedSearch"
|
||||
USING (
|
||||
"tenantId" = current_tenant_id()
|
||||
AND (current_user_id() IS NULL OR "userId" = current_user_id())
|
||||
);
|
||||
|
||||
DROP POLICY tenant_isolation_policy ON "TenderTriage";
|
||||
CREATE POLICY tenant_isolation_policy ON "TenderTriage"
|
||||
USING (
|
||||
"tenantId" = current_tenant_id()
|
||||
AND (current_user_id() IS NULL OR "userId" = current_user_id())
|
||||
);
|
||||
|
||||
DROP POLICY tenant_isolation_policy ON "WidgetInstance";
|
||||
CREATE POLICY tenant_isolation_policy ON "WidgetInstance"
|
||||
USING (
|
||||
"tenantId" = current_tenant_id()
|
||||
AND (current_user_id() IS NULL OR "userId" = current_user_id())
|
||||
);
|
||||
|
||||
-- SearchProvider — `userId` ist NULL-faehig (eine gemeinsame, mandanten-
|
||||
-- gebundene Zeile ohne Besitzer ist erlaubt), die Mandantenhaelfte ist NICHT
|
||||
-- gelockert: `SearchProvider` bleibt mandantenstreng (260910-jab (4),
|
||||
-- widerlegte Praemisse aus WINDOWS #19 — es gibt keinen Codeweg, der eine
|
||||
-- mandantenlose Zeile erzeugt). Vier nach Befehl getrennte Regeln
|
||||
-- (Praezedenz 260910-jab (3)): ein einzelner USING-Ausdruck, der die
|
||||
-- gemeinsame Zeile (`userId IS NULL`) zum Lesen einschliesst, wuerde sie
|
||||
-- ohne Trennung auch zum Aendern/Entfernen freigeben.
|
||||
DROP POLICY tenant_isolation_policy ON "SearchProvider";
|
||||
|
||||
CREATE POLICY tenant_user_read_policy ON "SearchProvider"
|
||||
FOR SELECT
|
||||
USING (
|
||||
"tenantId" = current_tenant_id()
|
||||
AND (
|
||||
current_user_id() IS NULL
|
||||
OR "userId" IS NULL
|
||||
OR "userId" = current_user_id()
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY tenant_user_insert_policy ON "SearchProvider"
|
||||
FOR INSERT
|
||||
WITH CHECK (
|
||||
"tenantId" = current_tenant_id()
|
||||
AND (current_user_id() IS NULL OR "userId" = current_user_id())
|
||||
);
|
||||
|
||||
CREATE POLICY tenant_user_update_policy ON "SearchProvider"
|
||||
FOR UPDATE
|
||||
USING (
|
||||
"tenantId" = current_tenant_id()
|
||||
AND (current_user_id() IS NULL OR "userId" = current_user_id())
|
||||
)
|
||||
WITH CHECK (
|
||||
"tenantId" = current_tenant_id()
|
||||
AND (current_user_id() IS NULL OR "userId" = current_user_id())
|
||||
);
|
||||
|
||||
CREATE POLICY tenant_user_delete_policy ON "SearchProvider"
|
||||
FOR DELETE
|
||||
USING (
|
||||
"tenantId" = current_tenant_id()
|
||||
AND (current_user_id() IS NULL OR "userId" = current_user_id())
|
||||
);
|
||||
|
||||
-- TenderRssFeedSource — loest die vier Regeln aus 20260910120000 ab (WINDOWS
|
||||
-- #19), unter DENSELBEN NAMEN neu angelegt. Die plattformweite Lesezulassung
|
||||
-- (`tenantId IS NULL`) und die Mandantenpflicht beim Schreiben aus jener
|
||||
-- Migration bleiben unveraendert bestehen — hier kommt ausschliesslich die
|
||||
-- Benutzerdimension hinzu. WINDOWS #24 (Admin-Erstellung/-Entfernen
|
||||
-- plattformweiter Zeilen bleibt ungebunden) ist von dieser Migration
|
||||
-- UNBERUEHRT.
|
||||
DROP POLICY tenant_platform_read_policy ON "TenderRssFeedSource";
|
||||
DROP POLICY tenant_insert_policy ON "TenderRssFeedSource";
|
||||
DROP POLICY tenant_update_policy ON "TenderRssFeedSource";
|
||||
DROP POLICY tenant_delete_policy ON "TenderRssFeedSource";
|
||||
|
||||
CREATE POLICY tenant_platform_read_policy ON "TenderRssFeedSource"
|
||||
FOR SELECT
|
||||
USING (
|
||||
("tenantId" = current_tenant_id() OR "tenantId" IS NULL)
|
||||
AND (
|
||||
current_user_id() IS NULL
|
||||
OR "userId" IS NULL
|
||||
OR "userId" = current_user_id()
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY tenant_insert_policy ON "TenderRssFeedSource"
|
||||
FOR INSERT
|
||||
WITH CHECK (
|
||||
"tenantId" = current_tenant_id()
|
||||
AND (current_user_id() IS NULL OR "userId" = current_user_id())
|
||||
);
|
||||
|
||||
CREATE POLICY tenant_update_policy ON "TenderRssFeedSource"
|
||||
FOR UPDATE
|
||||
USING (
|
||||
"tenantId" = current_tenant_id()
|
||||
AND (current_user_id() IS NULL OR "userId" = current_user_id())
|
||||
)
|
||||
WITH CHECK (
|
||||
"tenantId" = current_tenant_id()
|
||||
AND (current_user_id() IS NULL OR "userId" = current_user_id())
|
||||
);
|
||||
|
||||
CREATE POLICY tenant_delete_policy ON "TenderRssFeedSource"
|
||||
FOR DELETE
|
||||
USING (
|
||||
"tenantId" = current_tenant_id()
|
||||
AND (current_user_id() IS NULL OR "userId" = current_user_id())
|
||||
);
|
||||
|
||||
-- Was diese Migration bewusst NICHT tut:
|
||||
-- - Kein Systemkontext fuer Hintergrunddienste (Etappe 3c) — die `IS NULL
|
||||
-- OR`-Form macht das fuer heutige Aufrufer unnoetig.
|
||||
-- - Keine SECURITY-DEFINER-Funktion (Etappe 3a, Anmeldenamen pro Mandant).
|
||||
-- - Ein Aufrufer, der den Benutzer vergisst (drittes Argument an
|
||||
-- `forTenant()` nicht setzt), sieht den ganzen Mandanten — heute exakt
|
||||
-- der Stand VOR dieser Migration, also keine Verschlechterung, aber auch
|
||||
-- kein Netz dagegen. Siehe .planning/WINDOWS.md fuer den Nachweis, dass
|
||||
-- dieser Zustand aufgezeichnet, nicht uebersehen wurde.
|
||||
Reference in New Issue
Block a user