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:
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user