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:
2026-09-14 11:32:54 +02:00
parent 02016e19eb
commit 3d645674f0
12 changed files with 1272 additions and 139 deletions
+72 -2
View File
@@ -146,13 +146,83 @@ import { PrismaClient } from '@prisma/client';
* KEINEN dritten Parameter: kein Nutzer-CRUD-Aufrufer nutzt diese Funktion
* (nur `groups`, ein Verwaltungsweg) — ein unbenutzter Parameter waere
* Spekulation ohne heutigen Aufrufer.
*
* SYSTEMKONTEXT (Etappe 3c, 260914-eym):
*
* `forSystem(prisma)` ist ein SCHWESTERHELFER von `forTenant()`, kein
* vierter Parameter — die UMKEHRUNG der 3b-Begruendung oben, ausdruecklich
* so gewollt: der Systemkontext ist eine EIGENE Zugriffsklasse (liest ueber
* ALLE Mandanten), und genau deshalb bekommt der Detektor der
* Bestandsaufnahme (`rls-access-inventory.spec.ts`) fuer ihn eine EIGENE,
* fuenfte Erkennungsform (`const X = forSystem(`) mit dem Stand
* `system-gebunden`. Ein vierter Parameter an `forTenant()` haette diese
* Klasse fuer den Detektor UNSICHTBAR gemacht — ein ueber alle Mandanten
* lesender Zugriff waere als `gebunden` gezaehlt worden.
*
* Alle DREI Sitzungsvariablen werden in JEDER Form gesetzt:
* `forSystem()` setzt `app.system_context = 'true'` und AUSDRUECKLICH
* `app.current_tenant = ''` und `app.current_user = ''`; `forTenant()` und
* `withTenantTransaction()` setzen umgekehrt AUSDRUECKLICH
* `app.system_context = ''`. Kein Kontext darf vom anderen erben.
* `set_config(..., true)` (transaktionslokal) ist das ERSTE Netz — deshalb
* sieht `forTenant(A)` unmittelbar nach `forSystem` auf demselben Client
* nur A (gemessen im Werkzeug: `<slug>-fortenant-a-nach-systemkontext-nur-a`,
* `<slug>-is-system-context-unter-fortenant-false`). Der ausdrueckliche
* Reset ist das ZWEITE Netz fuer eine hypothetische `local=false`-Aenderung
* — durch Rueckbau falsifiziert (Reset entfernt UND local=false -> rot).
* Alle Werte von `forSystem()` stehen als LITERALE im Template-Text (es
* fliesst nichts Variables ein); in `forTenant()` bleibt die Parameterliste
* `[tenantId, userId ?? '']` unveraendert.
*
* Unter Systemkontext kann NUR GELESEN werden: die Regel
* `system_read_policy` (Migration 20260914120000_rls_system_context_read)
* ist `FOR SELECT`; permissive Regeln werden ODER-verknuepft, fuer
* INSERT/UPDATE/DELETE gilt weiter NUR die Mandantenregel, und unter
* Systemkontext ist `current_tenant_id()` der Leerstring — kein Mandant
* passt. Gemessen: INSERT -> SQLSTATE 42501, `updateMany`/`deleteMany` ->
* count 0, `update` per id -> P2025.
*
* Wer `forSystem()` rufen darf: AUSSCHLIESSLICH die in
* `FORSYSTEM_ALLOWED_CALL_SITES` (rls-access-inventory.spec.ts) genannten
* Stellen mit der dort genannten EXAKTEN Zahl je Datei. Jeder weitere
* Aufruf — in einer fremden Datei oder als zweiter in einer erlaubten —
* macht die Spec rot. Ein Anfrageweg darf diesen Helfer NIE rufen.
*/
export function forTenant(prisma: PrismaClient, tenantId: string, userId?: string) {
return prisma.$extends({
query: {
$allOperations({ args, query }: { args: any; query: (args: any) => any }) {
const setContext = (prisma as any)
.$executeRaw`SELECT set_config('app.current_tenant', ${tenantId}, true), set_config('app.current_user', ${userId ?? ''}, true)`;
.$executeRaw`SELECT set_config('app.current_tenant', ${tenantId}, true), set_config('app.current_user', ${userId ?? ''}, true), set_config('app.system_context', '', true)`;
return (prisma as any)
.$transaction([setContext, query(args)])
.then((results: any[]) => results[1]);
},
},
});
}
/**
* Systemkontext (Etappe 3c, 260914-eym): ein Client, der ueber ALLE
* Mandanten LIEST — fuer die Hintergrunddienste, die einmal ueber alles
* lesen und dann je Mandant gebunden handeln (DKV-Planer, ldap,
* tender-digest, tender-matching). Gleiche Array-Form-`$transaction`-Bauart
* wie `forTenant()` (Kontext und Abfrage auf EINER Verbindung, WINDOWS #20).
*
* EINE getaggte Anweisung setzt `app.system_context = 'true'` und
* AUSDRUECKLICH `app.current_tenant = ''` und `app.current_user = ''` —
* alle drei als Literale im Template-Text, es fliesst nichts Variables ein.
* Nur Lesen ist geoeffnet (`system_read_policy ... FOR SELECT`); jedes
* Schreiben scheitert an der Mandantenregel. Aufrufer: ausschliesslich die
* Stellen aus `FORSYSTEM_ALLOWED_CALL_SITES` (siehe Kopfkommentar).
*/
export function forSystem(prisma: PrismaClient) {
return prisma.$extends({
query: {
$allOperations({ args, query }: { args: any; query: (args: any) => any }) {
const setContext = (prisma as any)
.$executeRaw`SELECT set_config('app.system_context', 'true', true), set_config('app.current_tenant', '', true), set_config('app.current_user', '', true)`;
return (prisma as any)
.$transaction([setContext, query(args)])
@@ -186,7 +256,7 @@ export function withTenantTransaction<T>(
fn: (tx: any) => Promise<T>,
): Promise<T> {
return (prisma as any).$transaction(async (tx: any) => {
await tx.$executeRaw`SELECT set_config('app.current_tenant', ${tenantId}, true)`;
await tx.$executeRaw`SELECT set_config('app.current_tenant', ${tenantId}, true), set_config('app.system_context', '', true)`;
return fn(tx);
});
}