feat(quick-260914-eym): forSystem(), is_system_context(), Systemleseregel auf fuenf Tabellen, DKV-Planer je Mandant — ein Pfad (WINDOWS #21)
- Helfer forSystem(prisma) in prisma-tenant.extension.ts (Array-Form, setzt app.system_context='true' und die beiden anderen Variablen ausdruecklich leer); forTenant()/withTenantTransaction() setzen app.system_context='' als Literal (4 neue Spec-Tests) - Migration 20260914120000_rls_system_context_read: is_system_context() (COALESCE, STABLE) und system_read_policy FOR SELECT auf DkvModuleConfig, LdapConfig, LdapFieldMapping, TenderMatch, TenderSavedSearch — lokal angewendet (36 Migrationen, pg_proc 1, 5 system_read_policy, 34 Regeln) - migration-sql.spec.ts: describe-Block fuer die neue Migration (6 Tests) - rls-scratch-check.mjs: Funktion aus der Migration geschnitten, forSystemQuery/buildInlineSystemClient, Reset in forTenantQuery/ buildInlineExtendedClient, runSystemContextChecks (4 Funktionsfaelle + 9 Kennungen DkvModuleConfig) -> Alle 216 Pruefungen bestanden - rls-access-inventory.spec.ts: fuenfte Erkennungsform const X = forSystem(, Stand system-gebunden mit Vorrangregel, FORSYSTEM_ALLOWED_CALL_SITES (exakte Zahl je Datei, 3 Tests), Proben C/D/E - DKV: loadActiveConfigsForScheduler() ueber forSystem (findMany isActive, CONFIG_SAFE_SELECT, orderBy tenantId); DkvSchedulerService mit Auftrag je Mandant dkv-inbox-poll:<tenantId>, activeTenantId ersatzlos entfernt, setInterval/stopJob je Mandant, registeredTenantIds(); Controller stopJob(tenantId); neue dkv-scheduler.service.spec.ts (7 Tests), dkv.service.spec.ts Tests 6/7 umgestellt - Klassifikation: dkv.service.ts/dkvModuleConfig system-gebunden, Header mit fuenfter Erkennungsform und viertem Stand-Wert - Baseline: 63 Dateien / 1051 Tests, tsc 0, Werkzeug 216 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
-- 260914-eym, Etappe 3c — der benannte Systemkontext fuer die
|
||||
-- Hintergrunddienste. Sechs Stellen lesen absichtlich ueber ALLE Mandanten
|
||||
-- (docs/mandantentrennung-zugriffsklassifikation.md, Abschnitt "Der
|
||||
-- Hintergrunddienst als Falle"); ohne diese Migration saehen sie nach dem
|
||||
-- Scharfschalten NULL Zeilen und wuerden stumm die Arbeit einstellen
|
||||
-- (zu-wenig-statt-zu-viel, docs/mandantentrennung-etappe2-fehlerrichtung.md).
|
||||
--
|
||||
-- Die betroffenen Dateien (20260618112133_rls_policies fuer LdapConfig und
|
||||
-- LdapFieldMapping, 20260909140000_rls_remaining_tenant_tables fuer
|
||||
-- DkvModuleConfig und TenderMatch, 20260911120000_rls_user_dimension_
|
||||
-- personal_tables fuer TenderSavedSearch) bleiben UNVERAENDERT stehen —
|
||||
-- Prisma fuehrt ihre Pruefsumme, eine Aenderung braechte "prisma migrate
|
||||
-- deploy" zum Abbruch. Kopfform: 20260911120000_rls_user_dimension_personal_tables.
|
||||
--
|
||||
-- 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.
|
||||
|
||||
-- Dritte Sitzungsvariable `app.system_context`. Der Helfer `forSystem()`
|
||||
-- (apps/api/src/prisma/prisma-tenant.extension.ts) setzt sie auf 'true'
|
||||
-- und die beiden anderen Variablen AUSDRUECKLICH auf den Leerstring;
|
||||
-- `forTenant()` setzt sie umgekehrt ausdruecklich auf den Leerstring.
|
||||
--
|
||||
-- COALESCE ist Pflicht: `current_setting(..., true)` liefert ohne gesetzte
|
||||
-- Variable NULL, und `NULL = 'true'` waere NULL, nicht FALSE. Eine Regel
|
||||
-- mit USING (NULL) laesst zwar keine Zeile durch, aber die Funktion soll
|
||||
-- fuer jeden Aufrufer eine klare Antwort liefern: ohne Variable, mit
|
||||
-- Leerstring und mit jedem anderen Wert als 'true' ist sie FALSE. Damit
|
||||
-- bleibt die Vorher-Pruefung `ohne-kontext-leer` in rls-preflight.mjs
|
||||
-- gueltig (ohne Variable sieht niemand etwas). Kein GRANT EXECUTE noetig —
|
||||
-- wie bei current_tenant_id() und current_user_id(): PostgreSQL vergibt
|
||||
-- EXECUTE auf Funktionen standardmaessig an PUBLIC.
|
||||
CREATE OR REPLACE FUNCTION is_system_context() RETURNS BOOLEAN AS $$
|
||||
SELECT COALESCE(current_setting('app.system_context', true) = 'true', false);
|
||||
$$ LANGUAGE sql STABLE;
|
||||
|
||||
-- Je betroffener Tabelle EINE zusaetzliche PERMISSIVE Regel, NUR FOR SELECT.
|
||||
-- Permissive Regeln werden ODER-verknuepft: fuer SELECT gilt danach
|
||||
-- (Mandantenregel ODER Systemregel), fuer INSERT/UPDATE/DELETE gilt weiter
|
||||
-- NUR die bestehende `tenant_isolation_policy` — unter Systemkontext ist
|
||||
-- `current_tenant_id()` der Leerstring, kein Mandant passt, jedes Schreiben
|
||||
-- faellt durch (gemessen: INSERT -> SQLSTATE 42501, updateMany/deleteMany
|
||||
-- -> count 0, update per id -> P2025). Kein DROP POLICY, keine Aenderung an
|
||||
-- bestehenden Regeln. Genau die fuenf Tabellen, die die Systemkontext-Leser
|
||||
-- tatsaechlich lesen (gezaehlt in Aufrufe und include/select hinein):
|
||||
|
||||
-- DkvModuleConfig — DkvService.loadActiveConfigsForScheduler() liest beim
|
||||
-- Start des Planers ALLE aktiven Konfigurationen und registriert je Mandant
|
||||
-- einen eigenen Cron-Auftrag (WINDOWS #21).
|
||||
CREATE POLICY system_read_policy ON "DkvModuleConfig"
|
||||
FOR SELECT USING (is_system_context());
|
||||
|
||||
-- LdapConfig — LdapConfigService.getAllActiveConfigs() (Sync-Planer) und
|
||||
-- LdapConfigService.onApplicationBootstrap() (Nachverschluesselung alter
|
||||
-- Klartext-Kennwoerter, liest ueber alle, schreibt je Zeile gebunden).
|
||||
CREATE POLICY system_read_policy ON "LdapConfig"
|
||||
FOR SELECT USING (is_system_context());
|
||||
|
||||
-- LdapFieldMapping — dieselbe Methode getAllActiveConfigs() ueber
|
||||
-- `include: { fieldMappings: true }` (die WINDOWS-#27-Form: ein Relationsziel
|
||||
-- wird ueber den Klienten der Elternabfrage gelesen und braucht dieselbe
|
||||
-- Oeffnung).
|
||||
CREATE POLICY system_read_policy ON "LdapFieldMapping"
|
||||
FOR SELECT USING (is_system_context());
|
||||
|
||||
-- TenderMatch — TenderDigestScheduler.runDigest(), Kandidatenabfrage
|
||||
-- (unbenachrichtigte Treffer aller Mandanten, danach je Kandidat gebunden).
|
||||
CREATE POLICY system_read_policy ON "TenderMatch"
|
||||
FOR SELECT USING (is_system_context());
|
||||
|
||||
-- TenderSavedSearch — TenderMatchingService.matchDelta(), alle gespeicherten
|
||||
-- Suchprofile aller Mandanten (Treffer-Anlage danach je Profil gebunden).
|
||||
CREATE POLICY system_read_policy ON "TenderSavedSearch"
|
||||
FOR SELECT USING (is_system_context());
|
||||
|
||||
-- Was diese Migration bewusst NICHT tut:
|
||||
--
|
||||
-- - Keine Regel auf SmtpConfig: der Startpfad des Mailmoduls (findFirst()
|
||||
-- beim Boot, WINDOWS #30) wird in 260914-eym nicht auf den Systemkontext
|
||||
-- umgestellt, sondern ENTFERNT — MailService baut je Versand einen
|
||||
-- Transport aus der SmtpConfig des Empfaenger-Mandanten (gebunden). Der
|
||||
-- sechste Fall der Hintergrunddienst-Falle existiert damit nicht mehr.
|
||||
-- - Keine Regel auf Tenant und Tender: beide Tabellen tragen in KEINER
|
||||
-- Migration ENABLE ROW LEVEL SECURITY — es gibt nichts zu oeffnen
|
||||
-- (admin-seed.service.ts liest nur Tenant; der Katalog-Lesezugriff in
|
||||
-- tender-matching.service.ts bleibt nach D-03 bewusst ungebunden).
|
||||
-- - Keine Schreibregel unter Systemkontext: Schreiben bleibt je Mandant
|
||||
-- ueber forTenant() — der Systemkontext liest, er handelt nicht.
|
||||
-- - Keine Aenderung am Schalter: DATABASE_URL, Compose- und
|
||||
-- Umgebungsdateien bleiben unangetastet.
|
||||
Reference in New Issue
Block a user