Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
90 KiB
phase, plan, type, wave, depends_on, autonomous, requirements, files_modified, estimate, must_haves
| phase | plan | type | wave | depends_on | autonomous | requirements | files_modified | estimate | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| quick-260914-eym | 01 | execute | 1 | true |
|
|
|
|
Purpose: Nach dem Scharfschalten darf kein Hintergrunddienst stumm die Arbeit einstellen, und schon heute darf kein Mandant den SMTP-Server oder den Postfach-Planer eines anderen benutzen. Der Schalter bleibt AUS; morgen (2026-09-15) geht alpha mit EINEM Mandanten live — jeder der sechs Pfade muss sich dort beobachtbar identisch zu heute verhalten, und das ist je Pfad ein Test, keine Zusicherung.
Output: eine handgeschriebene Migration (lokal angewendet), der erweiterte Helfer samt Spec, sechs umgestellte Stellen samt Specs (zwei davon Funktionsausbau), der erweiterte Detektor, das erweiterte Wegwerf-Werkzeug, kohaerenter Aktenstand (Kritikschrift, Klassifikation, Auftrag, Datenbankrolle, Ledger), drei Commits, gepusht.
<execution_context>
@/.claude/gsd-core/workflows/execute-plan.md
@/.claude/gsd-core/templates/summary.md
</execution_context>
Zur Planungszeit gemessen (2026-09-14, HEAD 5e0e408, Arbeitsbaum sauber) — vom Executor gegen den lebenden Baum ERNEUT zu pruefen, bevor er es als Bearbeitungsgrundlage nimmt (#3786). Zeilennummern sind Orientierung, keine Bearbeitungsgrundlage.
- Datenbank: Container
tessera-ctl-db-1war beim Einstieg 33 StundenExited(nurgitea-dblief); der Planer hat ihn mitdocker start tessera-ctl-db-1gestartet (lokaler Dev-Container, kein Server). IP perdocker inspect172.19.0.2, Rolletessera:tessera_dev, Binaryapps/api/node_modules/.bin/prisma(NICHTnpx prisma).migrate status= up to date, 35 Migrationen angewendet.pg_proc:current_user_id1,is_system_context0.pg_policies(public): 29 Regeln, 0 malsystem_read_policy; auf DkvModuleConfig, LdapConfig, LdapFieldMapping, SmtpConfig, TenderMatch, TenderSavedSearch je GENAU EINEtenant_isolation_policy,cmd = ALL, PERMISSIVE. Rollen:tesserarolsuper+rolbypassrls,tessera_appweder noch. Zeilen: Tenant 1, DkvModuleConfig 1, SmtpConfig 0, LdapConfig 0, TenderMatch 0, TenderSavedSearch 0.TenantundTendertragen in KEINER MigrationENABLE ROW LEVEL SECURITY. - Baseline frisch gemessen:
npm --prefix apps/api run test->Test Files 62 passed (62),Tests 1028 passed (1028);npm --prefix apps/api run type-checkExit 0;TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@172.19.0.2:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs->Alle 203 Pruefungen bestanden.Keinmailhoglokal:ENOTFOUND mailhogin Testausgaben ist Umgebung. - Live-Probe der Regelsemantik (Wegwerf-Datenbank, Rolle ohne BYPASSRLS, TenderSavedSearch mit
tenant_isolation_policyALL +system_read_policyFOR SELECT, generierter Client): (a) ungebunden 0 Zeilen; (b) forSystem beide Mandanten; (c) INSERT ->PrismaClientUnknownRequestError, SQLSTATE 42501new row violates row-level security policy;updateManycount 0;deleteManycount 0;updateper id ->PrismaClientKnownRequestErrorP2025; (d) forTenant(A) unmittelbar nach forSystem auf DEMSELBEN Client -> nur A;is_system_context()innerhalb der forTenant-Transaktionfalse(Rohwert''); in einer Transaktion mitset_config('app.system_context','true',true)-> true, danach ausserhalb -> false; (e)pg_policiesunter der Wegwerf-Rolle lesbar:system_read_policy/SELECT,qual = is_system_context(),with_check = null. - Helfer
prisma-tenant.extension.ts(192 Zeilen):forTenantab Zeile 162 (EINE getaggte Anweisung mit zweiset_config,$transaction([setContext, query(args)])),withTenantTransactionab 184 (setzt nurapp.current_tenantdirekt auftx). Specprisma-tenant.extension.spec.ts(11 Tests) prueft u. a. 'genau zwei Eintraege' (51), Leerstring an zweiter Parameterstelle (138) — bleibt gueltig, wennapp.system_contextals LITERAL''im Template-Text steht, nicht als Parameter. - Detektor
rls-access-inventory.spec.ts(914 Zeilen, 24 Tests): Zuweisungsform Zeile 370 (/const\s+(\w+)\s*=\s*forTenant\(/g),STAND_TOKENS112,FileAnalysis115,scanRelationKeys(region, ctx, isBound, unbound, bound)298, Anker-EmpfaengerallReceiverNames/boundReceiverNames461–467,computeStandByKey560, Doc-Zeilenmuster 594 (Stand[a-z-]+—system-gebundenparst), Test 'einer der drei gueltigen Stand-Werte' 637. Ausnahmelisten-Muster mit Veraltet-Wachhund: 82/94/110 und Tests 681/729. - Die sechs Faelle, gemessen: (1)
dkv.service.ts:148loadAnyActiveConfigForScheduler()=this.prisma.dkvModuleConfig.findFirst({ select: CONFIG_SAFE_SELECT })(nur skalare Felder); Aufruferdkv-scheduler.service.ts:75unddkv.service.spec.tsTests 6/7 (417, 435);dkv-scheduler.service.ts:JOB_NAME = 'dkv-inbox-poll'(60), FeldactiveTenantId(66),setInterval(intervalMin, tenantId?)(108),stopJob()(161);dkv.controller.ts:84setInterval(dto.pollIntervalMin, tenantId),:86stopJob(). Der Auftragsname steht ausserhalb der Datei nur indocs/mandantentrennung-etappe2-fehlerrichtung.md:901.dkv.service.ts:337–346: Single-Flight-Riegelthis.processingist EIN Boolean fuer den ganzen Prozess, nicht je Mandant.cron4.4.0 liegt inapps/api/node_modules/cron(fireOnTick(),cronTime.source,isActivevorhanden — im Node-REPL bestaetigt). (2)settings.service.ts:240loadAnySmtpConfigForStartupTransport()=this.prisma.smtpConfig.findFirst(); einziger Aufrufermail.module.ts:38(Mailer-Fabrik); Spec-Blocksettings.service.spec.ts:419–498(vier Tests).MailService(134 Zeilen) hat GENAU EINEN produktiven Aufrufer:auth.service.ts:246sendPasswordResetEmail(email, token)— der Mandant ist dort bekannt (:241forTenant(this.prisma, user.tenantId));sendWelcomeEmailhat NULL Aufrufer. Vorlage fuer Transport je Versand:dkv-mail.service.ts(getDecryptedSmtpConfig(tenantId),nodemailer.createTransport,transport.close()imfinally).auth.service.spec.ts:372/390mocktsendPasswordResetEmailund prueft die Argumente. (3)ldap-config.service.ts:this.prisma.ldapConfig.findManyZeile 66 (Nachverschluesselung,select: { id, tenantId, encryptedBindPassword }),this.prisma.ldapConfig.updateZeile 78 (je Altzeile — ein SCHREIBZUGRIFF, der unter Systemkontext scheitern wuerde),this.prisma.ldapConfig.findManyZeile 309 (getAllActiveConfigs,include: { tenant: true, fieldMappings: true }); Aufruferldap-sync.scheduler.ts:31; Spec-Tests 301/306 sichern 'forTenant nicht aufgerufen'. (4)tender-digest.scheduler.ts:121this.prisma.tenderMatch.findMany({ where: { notifiedAt: null }, select: { userId, tenantId }, distinct: ['userId'] }); Spec 364 sichert 'forTenant genau einmal'. (5)tender-matching.service.ts:71this.prisma.tenderSavedSearch.findMany();:84this.prisma.tender.findMany(D-03, bleibt); Spec 433 sichert 'forTenant genau einmal'. (6)admin-seed.service.ts:172this.prisma.tenant.findMany({ select: { id, slug } })—Tenantohne Regel; ausserhalb der Schleife KEIN geschuetzter Lesezugriff -> nur dokumentieren. - Spec-Dateien, die die Erweiterung mocken und deren Prueflinge kuenftig
forSystemimportieren (der Mock-Fabrik mussforSystemhinzugefuegt werden, sonst wirft vitest beim Zugriff 'No "forSystem" export is defined on the mock'):dkv.service.spec.ts:22,settings.service.spec.ts:19(nur solange settingsforSystembraeuchte — nach Loeschung des Startpfads NICHT noetig),ldap-config.service.spec.ts:9,tender-digest.scheduler.spec.ts:30,tender-matching.service.spec.ts:25,tender-notifications.integration.spec.ts:12. - Werkzeug
rls-scratch-check.mjs(5531 Zeilen):setupScratchDatabase100–168 (tipptcurrent_tenant_id(), SCHNEIDETcurrent_user_id()aus der 3b-Migration, 148–166),forTenantQuery192,report180,extractPolicySql441,extractAllPolicySql478,readRlsUserDimensionMigrationSql500,extractCurrentUserIdFunctionSql509,readRemainingTenantTablesMigrationSql682,readRlsPoliciesMigrationSql428,readSchemaModelScalarFieldNames2992,sqlStateOf1862,runSingleRulePersonalTableCheck4607 (Muster: DROP/CREATE, Spaltenvergleich, generierter Client, 42501),buildInlineExtendedClient5281,main5483–5526 (Reihenfolge;runUserDimensionChecksvorrunConcurrencyProbe). Vorhandene Wegwerf-Tabellen:SmtpConfig4375 undTenderSavedSearch4924 VOLLSTAENDIG;LdapConfig544,LdapFieldMapping551,DkvModuleConfig1608 UNVOLLSTAENDIG (Roh-SQL-Abschnitte);TenderMatchhat KEINE. Die Wegwerf-Tabellen des neuen Abschnitts sind deshalb vollstaendig NEU aus schema.prisma zu bauen (DkvModuleConfig 16 skalare Felder inkl.encryptedInboxCreds, LdapConfig 15 inkl.groupFilterDns text[]/userExcludeList text[], LdapFieldMapping 6, TenderMatch 8, TenderSavedSearch 8). - Regelquellen fuer die Mandantenregeln der fuenf Tabellen (WORTGLEICH schneiden): DkvModuleConfig und TenderMatch aus
20260909140000_rls_remaining_tenant_tables; LdapConfig und LdapFieldMapping aus20260618112133_rls_policies; TenderSavedSearch aus20260911120000_rls_user_dimension_personal_tables. rls-coverage.spec.ts: Test 2 verlangt 'mindestens eine Policy' je RLS-Tabelle, Test 5 zaehlt nur in 20260909140000 — beide bleiben durch eine zweite Regel je Tabelle unberuehrt.rls-preflight.mjs(getPolicyTablesperSELECT DISTINCT tablename FROM pg_policies,ohne-kontext-leerzaehlt ohne jede Variable) bleibt gueltig, weilis_system_context()ohne Variable false ist — die Datei wird NICHT angefasst.- Klassifikation
docs/mandantentrennung-zugriffsklassifikation.md(727 Zeilen): Uebersichtstabelle ab 143 (Spalten Ungebunden/Gebunden, Summe 68/178, Zeilen tenders 35/27, ldap 4/26, dkv 1/22, settings 1/3), Klassen-Verteilung ab 169 (72 Paare, 35/21/14/2), Hintergrunddienst-Abschnitt ab 298 (Ueberschrift '## Der Hintergrunddienst als Falle — sechs Fälle' bleibt), Bestandsaufnahme-Header 532–560 (nennt drei Stand-Werte und vier Erkennungsformen), betroffene Zeilen 590 (dkv/dkvModuleConfig gemischt), 605/606/607 (ldap-config ldapConfig gemischt, ldapFieldMapping gemischt, tenant ungebunden), 619 (settings/smtpConfig gemischt), 628 (digest/tenderMatch gemischt), 638 (matching/tenderSavedSearch ungebunden), 649 (admin-seed/tenant ungebunden — bleibt, Begruendung Nachtrag). Abgeleitete Erwartung NACH Aufgabe 2 (Executor rechnet mit der Schleife nach): Rohtreffer ungebunden 68 -> 61 (ldap −3, dkv −1, settings −1, tenders −2), gebunden 178 -> 179 (ldap +1), neue Spalte System 5 (dkv 1, ldap 2, tenders 2); Paare 72 und Klassen unveraendert. - Kritikschrift
docs/mandantentrennung-etappe2-fehlerrichtung.md(3360 Zeilen): (d4) ab 896 (nenntdkv-inbox-pollin 901), (s4) ab 3025, (b4) ab 3201 (erster Punkt 'Systemkontext fuer Hintergrunddienste (Etappe 3c)'), '## Etappe 2 — Abschluss' ab 3229, '## Verweis' 3356; Form-Vorlage '## Regelschluss Benutzerdimension (Etappe 3b, 260911-nke)' 3117–3228 mit (b1)–(b5).docs/mandantentrennung-datenbankrolle.md(220 Zeilen): Aufzaehlung 'was gebaut wurde' 55–88 (3b-Punkt ab 70), Absatz 'Diese Stellen brauchen einen ausdruecklichen, benannten Systemkontext' ab 131, Vorher-Pruefung Abschnitt 5 ab 176.docs/mandantentrennung-etappe3-auftrag.md: Erledigt-Form fuer 3b in 41–52, 3c-Abschnitt 143–165. - Ledger
.planning/WINDOWS.md: Frontmatter open 16 / waived 1 / fixed 19 / total 36; #21 Zeile 38 (open), #30 Zeile 47 (open). Signaturen:node /home/vicolab/.claude/gsd-core/bin/gsd-tools.cjs windows fixed <id>undwindows append --kind deviation --phase quick-260914-eym --file <Datei> --description "<ein Absatz ASCII>". - Planer-Beitraege geprueft:
api-coverageDetektor auf dem Umfangstext ->detected: false(keine neue externe API; nodemailer und SMTP sind Bestand) — keine COVERAGE-Matrix noetig.assumption-deltaDetektor ->detected: false(englisches Vokabular greift auf den deutschen Umfang nicht), INHALTLICH liegt aber eine Einzahl-zu-Mehrzahl-Wende vor (ein Cron-Auftrag -> einer je Mandant; ein Start-Transport -> einer je Versand) — Entscheidung unten im Block<assumption_delta_decision>festgehalten.schema-gateFEUERT (neue Prisma-Migration): Aufgabe 1 traegt den [BLOCKING]-Schrittmigrate deploygegen die lokale Datenbank, danachmigrate statussauber undpg_policiesgelesen.security:<threat_model>unten, ASVS Level 1, Schwelle high. - Testkommandos:
npm --prefix apps/api run test(ZusammenfassungszeilenTest Files N passed (N)/Tests N passed (N)),npm --prefix apps/api run type-check. Werkzeug:TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs, SchlusszeileAlle N Pruefungen bestanden.; Zielzahlen abgeleitet: 203 + 4 Funktionsfaelle + 9 (DkvModuleConfig) = 216 nach Aufgabe 1; + 4 x 9 + 1 (ldapConfig->fieldMappings) = 253 nach Aufgabe 2 (Gate >= 250, tatsaechliche Zahl in die SUMMARY).
<assumption_delta_decision>
Nomen, das primaer wird: der Mandant als Auftragsidentitaet. Vorher war der DKV-Planer EIN Auftrag mit einem Feld activeTenantId (Einzahl als Grundannahme, der Mandant ein Detail des einen Auftrags); der Mail-Transport war EIN Objekt beim Start. Entscheidung: promote — der Auftrag je Mandant (Registry-Name dkv-inbox-poll:<tenantId>) wird die primaere Form, das Einzahl-Feld verschwindet ersatzlos (nicht daneben behalten); der Transport je Versand wird die einzige Form, die Mailer-Fabrik beim Start verschwindet. Begruendung: 'add-alongside' (Einzelauftrag behalten, Mehrzahl daneben) haette zwei Wahrheiten ueber denselben Zustand erzeugt — genau die Form, die #21 heute falsch macht. Mit einem Mandanten ist die Mehrzahl-Form beobachtbar identisch (Gate in Aufgabe 1/2). Invariantentest angeboten und aufgenommen: dkv-scheduler.service.spec.ts 'zwei Mandanten -> zwei Auftraege, Aenderung des einen laesst den anderen unberuehrt'.
</assumption_delta_decision>
5e0e408), `git status --short` leer, DB-IP per `docker inspect`, `migrate status` up to date (35 angewendet), `pg_proc` ohne `is_system_context`, Baseline-Testlauf mit Zahlen in die SUMMARY (erwartet 62/1028, Typpruefung 0, Werkzeug 203).
(1) Helfer apps/api/src/prisma/prisma-tenant.extension.ts. Neue Funktion export function forSystem(prisma: PrismaClient) in exakt der Bauart von forTenant(): $extends mit $allOperations, EINE getaggte $executeRaw-Anweisung SELECT set_config('app.system_context', 'true', true), set_config('app.current_tenant', '', true), set_config('app.current_user', '', true) (alle drei Werte als Literale im Template-Text, keine Parameter — es fliesst nichts Variables ein), dann $transaction([setContext, query(args)]) mit results[1]. In forTenant() die bestehende Anweisung um , set_config('app.system_context', '', true) als LITERAL erweitern (die Parameterliste bleibt [tenantId, userId ?? ''], damit die bestehenden Spec-Zusicherungen 51/138/159/181 unveraendert gelten); dasselbe Literal in withTenantTransaction() an dessen tx.$executeRaw-Anweisung anhaengen. Kopfkommentar um einen Abschnitt 'SYSTEMKONTEXT (Etappe 3c, 260914-eym)' ergaenzen: warum ein SCHWESTERHELFER statt eines vierten Parameters (Systemkontext ist eine EIGENE Klasse; der Detektor bekommt dafuer eine eigene Erkennungsform — die Umkehrung der 3b-Begruendung, ausdruecklich so benannt), warum alle drei Variablen in jeder Form gesetzt werden (kein Erben zwischen den Kontexten, local=true ist das erste Netz, der Reset das zweite), dass unter Systemkontext NUR gelesen werden kann (die Regel ist FOR SELECT; Schreiben scheitert an der Mandantenregel — gemessen 42501 / count 0 / P2025), und wer ihn rufen darf: ausschliesslich die in FORSYSTEM_ALLOWED_CALL_SITES (Detektor) genannten Stellen.
(2) Spec prisma-tenant.extension.spec.ts: mindestens drei neue Tests im Stil der bestehenden — forSystem(): das Template nennt app.system_context, app.current_tenant, app.current_user, die Werte 'true', '', '' stehen im Text, values ist leer, das $transaction-Feld hat genau zwei Eintraege; forTenant(): der Template-Text enthaelt app.system_context und setzt es auf den Leerstring; withTenantTransaction(): dito auf tx.
(3) Migration apps/api/prisma/migrations/20260914120000_rls_system_context_read/migration.sql, Kopfform wie 20260911120000 (Kennung 260914-eym, welche Dateien UNVERAENDERT bleiben und warum — Pruefsumme —, der Satz zum Schalter). Inhalt in dieser Reihenfolge: CREATE OR REPLACE FUNCTION is_system_context() RETURNS BOOLEAN AS $$ SELECT COALESCE(current_setting('app.system_context', true) = 'true', false); $$ LANGUAGE sql STABLE; mit Kommentar (COALESCE, weil current_setting(..., true) ohne Variable NULL liefert und NULL = 'true' NULL waere — die Regel muss dann FALSE sehen, nicht NULL; kein GRANT, Begruendung wie 3b). Dann fuer GENAU diese fuenf Tabellen, jede mit einem Satz, WELCHER Aufrufer sie ueber den Systemkontext liest: CREATE POLICY system_read_policy ON "DkvModuleConfig" FOR SELECT USING (is_system_context()); (DkvService.loadActiveConfigsForScheduler), "LdapConfig" (LdapConfigService.getAllActiveConfigs und onApplicationBootstrap), "LdapFieldMapping" (dieselbe Methode ueber include: { fieldMappings } — WINDOWS-#27-Form), "TenderMatch" (TenderDigestScheduler.runDigest, Kandidatenabfrage), "TenderSavedSearch" (TenderMatchingService.matchDelta). Kein DROP POLICY, keine Aenderung an bestehenden Regeln — permissive Regeln werden ODER-verknuepft, deshalb erweitert FOR SELECT nur das Lesen. Abschluss 'Was diese Migration bewusst NICHT tut': kein SmtpConfig (der Startpfad des Mailmoduls wird in Aufgabe 2 ENTFERNT, nicht umgestellt — Transport je Versand nach Mandant), kein Tenant/Tender (keine Regel, nichts zu oeffnen), keine Schreibregel unter Systemkontext (Schreiben bleibt je Mandant), keine Aenderung am Schalter.
[BLOCKING] Migration lokal anwenden: cd apps/api && DATABASE_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/tessera" ./node_modules/.bin/prisma migrate deploy, dann migrate status (up to date, 36 angewendet), dann pg_proc (is_system_context = 1) und pg_policies lesen: genau fuenf system_read_policy, alle cmd = SELECT, permissive = PERMISSIVE, qual = is_system_context(), with_check NULL; Gesamtzahl 34. Ausgabe woertlich in die SUMMARY. schema.prisma, Compose, .env bleiben unangetastet.
(4) Migrations-Spec apps/api/src/groups/migration-sql.spec.ts: neuer describe-Block rls_system_context_read migration.sql (Etappe 3c, 260914-eym) nach dem 3b-Block, mit readMigrationSql('_rls_system_context_read') und nonCommentLines: Funktion mit COALESCE vorhanden; genau fuenf CREATE POLICY system_read_policy, je eine fuer die fuenf Tabellen; jede FOR SELECT; jede USING (is_system_context()); kein DROP POLICY; kein SmtpConfig/Tenant/Tender ausserhalb von Kommentaren; der Kopf nennt SmtpConfig als bewusst nicht enthalten.
(5) Werkzeug apps/api/scripts/rls-scratch-check.mjs:
- Leser
readRlsSystemContextMigrationSql()(Endung_rls_system_context_read, MusterreadRlsUserDimensionMigrationSql),extractIsSystemContextFunctionSql()(Regex bisLANGUAGE sql STABLE;),extractSystemReadPolicySql(migrationSql, tableName)(RegexCREATE POLICY system_read_policy ON "<T>"[\s\S]*?;). InsetupScratchDatabasedie Funktion SCHNEIDEN (Abbruch mitfail(...), wenn Migration oder Funktion fehlt — nicht tippen) undGRANT EXECUTEan die Wegwerf-Rolle. forTenantQueryundbuildInlineExtendedClientspiegelbildlich zum Helfer um das Literalset_config('app.system_context', '', true)erweitern; neueforSystemQuery(prisma, queryFn)undbuildInlineSystemClient(prisma)spiegelbildlich zuforSystem().- Neuer Abschnitt
runSystemContextChecks(adminUrl, scratchRoleUrl, results), im Hauptlauf NACHrunUserDimensionChecksund VORrunConcurrencyProbe. Zuerst vier Funktionsfaelle ueber die Wegwerf-Rolle in je einer Transaktion:is-system-context-ungesetzt-false,is-system-context-leer-false(nachset_config('app.system_context','',true)),is-system-context-true-true,is-system-context-fremdwert-false(Wert'yes'). Dann eine innere RoutinerunSystemContextTableCheck(config)(MusterrunSingleRulePersonalTableCheck), in DIESER Aufgabe nur fuerDkvModuleConfig(slugdkvmoduleconfig, Mandantenregel ausreadRemainingTenantTablesMigrationSql(), Systemregel aus der neuen Migration, Wegwerf-Tabelle mit ALLEN 16 skalaren Spalten, SpaltenvergleichreadSchemaModelScalarFieldNames('DkvModuleConfig')als erste Pruefung mit Abbruch; Zeilencfg-a/TENANT-A undcfg-b/TENANT-B ueber die Wartungsrolle). Neun Kennungen ueber den GENERIERTEN Client:<slug>-wegwerftabelle-deckt-alle-spalten-des-generierten-clients,<slug>-ungebunden-null-zeilen(findMany auf dem rohen Client, 0),<slug>-systemkontext-sieht-beide-mandanten(buildInlineSystemClient, tenantIds beider),<slug>-systemkontext-insert-abgewiesen-42501(create mit tenantId TENANT-A; erwartetsqlStateOf(err) === '42501', jedes Gelingen und jeder andere Fehler FEHLGESCHLAGEN mit Meldung),<slug>-systemkontext-updatemany-count-0,<slug>-systemkontext-deletemany-count-0,<slug>-fortenant-a-nach-systemkontext-nur-a(buildInlineExtendedClient(prisma,'TENANT-A') auf DEMSELBENprismaunmittelbar nach dem System-Aufruf; nur TENANT-A),<slug>-is-system-context-unter-fortenant-false($transaction([setContextA, $queryRaw SELECT is_system_context()])-> false),<slug>-pg-policies-genau-eine-system-read-policy-select(unter der Wegwerf-Rollepg_policieslesen: genau eine Zeilesystem_read_policymitcmd='SELECT'fuer die Tabelle). Meldetexte nennen die gemessenen Werte.
(6) Detektor apps/api/src/prisma/rls-access-inventory.spec.ts — fuenfte Erkennungsform: FileAnalysis um systemModels: Set<string>, totalForSystemCalls, systemAssignmentFormCalls; in analyzeSource Zuweisungen const <Name> = forSystem( sammeln, <Name>.<Modell> in systemModels, totalForSystemCalls per /(?<!function )forSystem\(/g; Systemnamen als Anker-Empfaenger der vierten Form (Relationsziele auf System-Klienten in systemModels) — dafuer scanRelationKeys so umbauen, dass es die Ziel-Menge direkt bekommt statt isBound. STAND_TOKENS um system-gebunden; computeStandByKey mit der Vorrangregel aus den truths (ungebunden+anderes -> gemischt; nur ungebunden -> ungebunden; system ohne ungebunden -> system-gebunden; sonst gebunden); findAccessSites nimmt systemModels mit. Neue Konstante FORSYSTEM_ALLOWED_CALL_SITES = new Map<string, number>([['apps/api/src/dkv/dkv.service.ts', 1]]) mit Kopfkommentar (in Aufgabe 2 auf vier Dateien erweitert) und drei Tests: (i) jede Datei mit totalForSystemCalls > 0 steht in der Map und die Zahl stimmt EXAKT (Fremddatei oder Abweichung -> rot, Meldung nennt Datei und beide Zahlen); (ii) veraltete Eintraege: jede Datei der Map existiert und hat genau die genannte Zahl (Muster 681); (iii) jedes forSystem( folgt der Zuweisungsform (totalForSystemCalls === systemAssignmentFormCalls, keine Ausnahmeliste — es gibt keinen begruendeten Fall). Testname 637 auf 'vier gueltigen Stand-Werte' und den Kopfkommentar der Datei fortschreiben. Drei Proben im describe der vierten Erkennung: Probe C (const systemPrisma = forSystem(this.prisma) + systemPrisma.ldapConfig.findMany({ include: { fieldMappings: true } }) -> systemModels enthaelt ldapConfig UND ldapFieldMapping, beide weder in bound noch unbound, Stand system-gebunden), Probe D (dieselbe Datei mit einem forTenant-Zugriff auf dasselbe Modell -> Stand bleibt system-gebunden; mit einem this.prisma-Zugriff auf dasselbe Modell -> gemischt), Probe E (forSystem(this.prisma).x.findMany() ohne Zuweisung -> totalForSystemCalls 1, systemAssignmentFormCalls 0).
(7) DKV je Mandant. dkv.service.ts: loadAnyActiveConfigForScheduler() durch async loadActiveConfigsForScheduler() ersetzen — const systemPrisma = forSystem(this.prisma) as any; return systemPrisma.dkvModuleConfig.findMany({ where: { isActive: true }, select: CONFIG_SAFE_SELECT, orderBy: { tenantId: 'asc' } }); — Import forSystem ergaenzen, Kopfkommentar der Methode NEU (systemgebunden, warum viele statt einer, Verweis auf #21 als geschlossen, das Verstummen nach dem Scharfschalten ist damit strukturell ausgeschlossen, weil system_read_policy diese Tabelle oeffnet), den Klassen-Kopfkommentar ('Multi-tenant note (v1)') nachziehen. Der Single-Flight-Riegel processing bleibt unangetastet (Kommentar an Ort und Stelle: prozessweit, nicht je Mandant — Ledger-Eintrag in Aufgabe 3). dkv-scheduler.service.ts: Kopfkommentar neu (Auftrag je Mandant, Einmal-abfragen-viele-bedienen, Registry-Name dkv-inbox-poll:<tenantId>, was mit einem Mandanten identisch bleibt); JOB_NAME_PREFIX = 'dkv-inbox-poll', private jobNameFor(tenantId); Feld activeTenantId ERSATZLOS entfernen; onModuleInit: alle aktiven Configs laden, je Config this.setInterval(config.pollIntervalMin, config.tenantId), bei leerer Liste weiterhin die Protokollzeile 'DKV scheduler: no active config found — cron job not registered', sonst 'DKV scheduler initialized: N tenant(s)'; Fehler weiter fangen und protokollieren; setInterval(intervalMin: number, tenantId: string) (tenantId PFLICHT) ersetzt/erzeugt nur den Auftrag dieses Mandanten (stop+delete unter dem Mandantennamen, Cron-Expression-Berechnung UNVERAENDERT, Tick processInbox(tenantId)), stopJob(tenantId: string) entfernt nur diesen; neue oeffentliche registeredTenantIds(): string[] aus schedulerRegistry.getCronJobs() gefiltert nach Praefix (fuer Tests und Diagnose). dkv.controller.ts:86: stopJob(tenantId). NEUE dkv-scheduler.service.spec.ts mit Fake-Registry (Map-basiert: addCronJob, getCronJob wirft bei Unbekannt wie @nestjs/schedule, deleteCronJob, getCronJobs), Fake-DkvService (loadActiveConfigsForScheduler, processInbox als vi.fn), echtem cron (liegt unter apps/api/node_modules), afterEach stoppt jeden registrierten Auftrag — mindestens sechs Tests: (1) EIN aktiver Mandant, pollIntervalMin 15 -> genau ein Auftrag dkv-inbox-poll:<t> mit cronTime.source === '*/15 * * * *' (Identitaet zu heute), (2) pollIntervalMin 120 -> 0 */2 * * *, (3) fireOnTick() ruft processInbox genau mit dieser tenantId, (4) inaktive/keine Config -> kein Auftrag, Protokollzeile, (5) ZWEI Mandanten -> zwei Auftraege; setInterval(30, t2) ersetzt nur t2 (t1 behaelt seine Expression), stopJob(t1) entfernt nur t1 (Invariantentest aus dem assumption-delta-Block), (6) loadActiveConfigsForScheduler wirft -> Fehler gefangen, kein Auftrag, Start nicht blockiert. dkv.service.spec.ts: Mock-Fabrik um forSystem: vi.fn((prisma) => prisma.__makeSystemClient()) ergaenzen, Fake um __makeSystemClient() mit __systemCallLog (Muster __makeBoundClient, dkvModuleConfig.findMany mit where.isActive-Filter ueber die Map); Test 6 umdrehen: der Planer-Startpfad erzeugt GENAU EINEN System-Aufruf (dkvModuleConfig.findMany) und KEINEN gebundenen; Test 7 bleibt (kein encryptedInboxCreds im Ergebnis) und prueft zusaetzlich: zwei aktive plus eine inaktive Config -> genau die zwei aktiven, nach tenantId sortiert.
(8) Klassifikation, nur die Zeile, die sonst die Inventar-Spec rot macht: Bestandsaufnahme-Zeile apps/api/src/dkv/dkv.service.ts | dkvModuleConfig auf Stand system-gebunden, Begruendung fortschreiben (seit 260914-eym liest der Planer-Startpfad ueber forSystem(), alle uebrigen Zugriffe gebunden); im Bestandsaufnahme-Header die Stand-Werte um system-gebunden und die Erkennungsformen um die fuenfte ergaenzen (mit der Vorrangregel). Alles Weitere in Aufgabe 3.
Baseline am Ende: Tests >= 1040 (1028 + mindestens 3 + 6 + 3 + 6 = 1046 erwartet; die Zahl 1040 laesst Spielraum fuer zusammengelegte Faelle), Typpruefung, Werkzeug Alle N Pruefungen bestanden mit N >= 216. Commit: feat(quick-260914-eym): forSystem(), is_system_context(), Systemleseregel auf fuenf Tabellen, DKV-Planer je Mandant — ein Pfad (WINDOWS #21). git status --short danach leer.
cd /home/vicolab/projects/tessera-ctl && M=apps/api/prisma/migrations/20260914120000_rls_system_context_read/migration.sql && test -f "$M" && grep -q 'CREATE OR REPLACE FUNCTION is_system_context() RETURNS BOOLEAN' "$M" && grep -qF "COALESCE(current_setting('app.system_context', true) = 'true', false)" "$M" && for T in DkvModuleConfig LdapConfig LdapFieldMapping TenderMatch TenderSavedSearch; do grep -qF "CREATE POLICY system_read_policy ON "$T"" "$M" || { echo "FEHLT: Regel fuer $T"; exit 1; }; done && test 5 -eq "$(grep -v '^\s*--' "$M" | grep -c 'CREATE POLICY system_read_policy')" && test 5 -eq "$(grep -v '^\s*--' "$M" | grep -c 'FOR SELECT')" && test 0 -eq "$(grep -v '^\s*--' "$M" | grep -c 'DROP POLICY')" && test 0 -eq "$(grep -v '^\s*--' "$M" | grep -c 'SmtpConfig')" && test -z "$(git diff --name-only 5e0e408 -- apps/api/prisma/schema.prisma docker-compose.yml docker-compose.prod.yml docker-compose.dev.yml apps/api/package.json pnpm-lock.yaml apps/api/scripts/rls-preflight.mjs apps/api/src/user/admin-seed.service.ts)" && CH=$(git diff --name-only 5e0e408) || { echo "git diff fehlgeschlagen"; exit 1; } && test 0 -eq "$(printf '%s\n' "$CH" | grep -c '^.env')" && E=apps/api/src/prisma/prisma-tenant.extension.ts && grep -q 'export function forSystem(prisma: PrismaClient)' "$E" && grep -qF "set_config('app.system_context', 'true', true)" "$E" && test 2 -eq "$(grep -cF "set_config('app.system_context', '', true)" "$E")" && test 1 -eq "$(grep -c 'const systemPrisma = forSystem(this.prisma)' apps/api/src/dkv/dkv.service.ts)" && test 0 -eq "$(cat apps/api/src/dkv/dkv.service.ts apps/api/src/dkv/dkv-scheduler.service.ts apps/api/src/dkv/dkv.controller.ts apps/api/src/dkv/dkv.service.spec.ts | grep -c 'loadAnyActiveConfigForScheduler')" && test 0 -eq "$(grep -v '^\s*//|^\s**' apps/api/src/dkv/dkv-scheduler.service.ts | grep -c 'activeTenantId')" && grep -q 'stopJob(tenantId)' apps/api/src/dkv/dkv.controller.ts && grep -q 'registeredTenantIds' apps/api/src/dkv/dkv-scheduler.service.ts && test -f apps/api/src/dkv/dkv-scheduler.service.spec.ts && test 6 -le "$(grep -c '^\sit(' apps/api/src/dkv/dkv-scheduler.service.spec.ts)" && S=apps/api/src/prisma/rls-access-inventory.spec.ts && grep -q 'FORSYSTEM_ALLOWED_CALL_SITES' "$S" && grep -qF 'forSystem(' "$S" && grep -q "'system-gebunden'" "$S" && grep -q 'systemModels' "$S" && grep -q 'runSystemContextChecks' apps/api/scripts/rls-scratch-check.mjs && grep -q 'extractSystemReadPolicySql' apps/api/scripts/rls-scratch-check.mjs && grep -q 'buildInlineSystemClient' apps/api/scripts/rls-scratch-check.mjs && grep -q '_rls_system_context_read' apps/api/src/groups/migration-sql.spec.ts && grep -qE '^| apps/api/src/dkv/dkv.service.ts | dkvModuleConfig | muss-mandantengebunden | system-gebunden |' docs/mandantentrennung-zugriffsklassifikation.md && DB_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' tessera-ctl-db-1) && test -n "$DB_IP" && (cd apps/api && DATABASE_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/tessera" ./node_modules/.bin/prisma migrate status 2>&1 | grep -q 'Database schema is up to date') && (cd apps/api && DATABASE_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/tessera" node -e "const {PrismaClient}=require('@prisma/client');const p=new PrismaClient();(async()=>{const f=(await p.$queryRawUnsafe("SELECT count()::int AS c FROM pg_proc WHERE proname='is_system_context'"))[0].c;const r=await p.$queryRawUnsafe("SELECT tablename, cmd, permissive, qual, with_check FROM pg_policies WHERE policyname='system_read_policy' ORDER BY tablename");const ok=f===1&&r.length===5&&r.every(x=>x.cmd==='SELECT'&&x.permissive==='PERMISSIVE'&&x.qual==='is_system_context()'&&x.with_check===null)&&r.map(x=>x.tablename).join(',')==='DkvModuleConfig,LdapConfig,LdapFieldMapping,TenderMatch,TenderSavedSearch';const n=(await p.$queryRawUnsafe("SELECT count(*)::int AS c FROM pg_policies WHERE schemaname='public'"))[0].c;console.log('pg_proc',f,'system_read_policy',r.length,'policies',n);await p.$disconnect();process.exit(ok&&n===34?0:1)})()") && T=$(npm --prefix apps/api run test 2>&1 | grep -oE 'Tests [0-9]+ passed' | grep -oE '[0-9]+') && echo "TESTS=$T" && test "$T" -ge 1040 && npm --prefix apps/api run type-check >/dev/null 2>&1 && N=$(TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs 2>&1 | tee /tmp/claude-1000/-home-vicolab-projects-tessera-ctl/d06a4a73-407a-4ea6-b686-d1f72c209902/scratchpad/eym-t1-scratch.log | grep -oE 'Alle [0-9]+ Pruefungen bestanden' | grep -oE '[0-9]+') && echo "WERKZEUG=$N" && test "$N" -ge 216 && for K in is-system-context-ungesetzt-false is-system-context-leer-false is-system-context-true-true is-system-context-fremdwert-false dkvmoduleconfig-wegwerftabelle-deckt-alle-spalten-des-generierten-clients dkvmoduleconfig-ungebunden-null-zeilen dkvmoduleconfig-systemkontext-sieht-beide-mandanten dkvmoduleconfig-systemkontext-insert-abgewiesen-42501 dkvmoduleconfig-systemkontext-updatemany-count-0 dkvmoduleconfig-systemkontext-deletemany-count-0 dkvmoduleconfig-fortenant-a-nach-systemkontext-nur-a dkvmoduleconfig-is-system-context-unter-fortenant-false dkvmoduleconfig-pg-policies-genau-eine-system-read-policy-select; do grep -q "^${K}: bestanden" /tmp/claude-1000/-home-vicolab-projects-tessera-ctl/d06a4a73-407a-4ea6-b686-d1f72c209902/scratchpad/eym-t1-scratch.log || { echo "KENNUNG FEHLT ODER ROT: $K"; exit 1; }; done && test -z "$(git status --short)" && echo AUFGABE-1-OK
Migration existiert, ist lokal angewendet (pg_proc kennt is_system_context, pg_policies zeigt genau fuenf system_read_policy/SELECT/PERMISSIVE auf den fuenf Tabellen, 34 Regeln gesamt), schema.prisma/Compose/.env/package.json/Lockfile/preflight/admin-seed unveraendert; forSystem() existiert, forTenant() und withTenantTransaction() setzen app.system_context auf den Leerstring; der DKV-Planer fuehrt einen Auftrag je aktivem Mandanten und verhaelt sich mit einem Mandanten identisch (Cron-Expression, Tick, Protokollzeile — sechs Tests); der Detektor kennt die fuenfte Form mit exakter Erlaubnisliste und vier Stand-Werten; das Werkzeug misst die vier Funktionsfaelle und die neun Wahrheiten fuer DkvModuleConfig; Tests >= 1040, Typpruefung sauber, Werkzeug N >= 216; committet, git status leer.
(2) ldap ldap-config.service.ts: Import forSystem. getAllActiveConfigs(): const systemPrisma = forSystem(this.prisma) as any; und systemPrisma.ldapConfig.findMany({ where: { isActive: true }, include: { tenant: true, fieldMappings: true } }) — Kopfkommentar neu (systemgebunden; LdapFieldMapping ueber include mit abgedeckt, WINDOWS-#27-Form; Tenant ohne Regel; Verstummen nach dem Scharfschalten strukturell ausgeschlossen). onApplicationBootstrap(): das Lesen ueber const systemPrisma = forSystem(this.prisma) as any; (zweite Zuweisung, eigene Methode — der Detektor zaehlt zwei), die Schreibzeile je Altzeile ueber const tenantPrisma = forTenant(this.prisma, config.tenantId) as any; await tenantPrisma.ldapConfig.update(...) — Kommentar: unter Systemkontext ist Schreiben abgewiesen (gemessen P2025/42501), der Mandant steht in der gelesenen Zeile. ldap-config.service.spec.ts: Mock-Fabrik um forSystem: vi.fn((p) => p.__systemClient ?? p) ergaenzen (oder ein zweites unterscheidbares Objekt wie im Bindungsblock); Tests 301/306 UMDREHEN statt loeschen: getAllActiveConfigs() ruft forSystem genau einmal und forTenant nie; onApplicationBootstrap() mit leerer Liste ruft forSystem einmal, forTenant nie; NEU: mit einer Altzeile (Klartext-Kennwort, tenantId t1) ruft forTenant genau einmal mit t1 und update traegt das verschluesselte Kennwort.
(3) digest tender-digest.scheduler.ts: Import forSystem; die Kandidatenabfrage ueber const systemPrisma = forSystem(this.prisma) as any; — Kommentar (Etappe-3c-Uebergabe eingeloest: Systemkontext liest TenderMatch aller Mandanten NUR lesend; die Schleife bleibt je Kandidatenzeile gebunden, bewusst ohne Benutzer wie in 3b; der Sonderfall 'Nutzer mit Treffern unter zwei Mandanten' bleibt wie in (t4) beschrieben ungeloest). tender-digest.scheduler.spec.ts: Mock um forSystem (Fake __makeSystemClient() mit __systemCallLog, tenderMatch.findMany unveraendert), Test 364 bleibt (forTenant genau einmal), NEU: die Kandidatenabfrage laeuft ueber den System-Klienten (__systemCallLog enthaelt genau tenderMatch.findMany, nichts aus der Schleife).
(4) matching tender-matching.service.ts: Import forSystem; tenderSavedSearch.findMany() ueber const systemPrisma = forSystem(this.prisma) as any; — Kommentar; this.prisma.tender.findMany (D-03) BLEIBT ungebunden mit unveraendertem Kommentar. tender-matching.service.spec.ts: Mock um forSystem, Test 433 bleibt (forTenant genau einmal, Katalog ungebunden), NEU: Profilabfrage ueber den System-Klienten, Katalogabfrage NICHT (weiter roher Client). tender-notifications.integration.spec.ts: Mock um forSystem (Fake nach demselben Muster), bestehende zwei Tests gruen.
(5) Werkzeug: runSystemContextChecks um vier Tabellen ueber die innere Routine erweitern — ldapconfig (Mandantenregel aus readRlsPoliciesMigrationSql(); Wegwerf-Tabelle mit allen 15 skalaren Spalten, groupFilterDns text[] NOT NULL DEFAULT '{}', userExcludeList text[] NOT NULL DEFAULT '{}'), ldapfieldmapping (Regel aus derselben Datei — Join auf LdapConfig, deshalb NACH ldapconfig und ohne DROP der Elternzeile; 6 Spalten; Seed je Mandant eine Zuordnung an cfg-a/cfg-b), tendermatch (Regel aus readRemainingTenantTablesMigrationSql(); 8 Spalten; Seed je Mandant eine Zeile mit notifiedAt NULL), tendersavedsearch (Regel aus readRlsUserDimensionMigrationSql(); 8 Spalten). Je Tabelle dieselben neun Kennungen. PLUS eine Relations-Kennung ldapconfig-systemkontext-include-fieldmappings-beider-mandanten: buildInlineSystemClient(prisma).ldapConfig.findMany({ where: { isActive: true }, include: { fieldMappings: true } }) liefert beide Mandanten und je genau eine Zuordnung (die #27-Form unter Systemkontext). Erwartete Schlusszeile: Alle 253 Pruefungen bestanden. (Gate >= 250; die tatsaechliche Zahl in die SUMMARY).
(6) Detektor: FORSYSTEM_ALLOWED_CALL_SITES auf [['apps/api/src/dkv/dkv.service.ts', 1], ['apps/api/src/ldap/ldap-config.service.ts', 2], ['apps/api/src/tenders/tender-digest.scheduler.ts', 1], ['apps/api/src/tenders/tender-matching.service.ts', 1]] — Summe 5; Kopfkommentar nennt die sechs Faelle und warum settings/mail und admin-seed NICHT darin stehen.
(7) Klassifikation, nur die Zeilen, die sonst die Inventar-Spec rot machen: Stand system-gebunden fuer ldap-config.service.ts/ldapConfig, /ldapFieldMapping, /tenant, tender-digest.scheduler.ts/tenderMatch, tender-matching.service.ts/tenderSavedSearch; Stand gebunden fuer settings.service.ts/smtpConfig (Startpfad entfernt); Begruendungen fortschreiben (je ein Satz mit 260914-eym); Klassen UNVERAENDERT. Alles Weitere in Aufgabe 3.
(8) Falsifizierung durch Rueckbau — jede Messung woertlich in die SUMMARY, danach git checkout -- <Datei> und erneut gruen: (a) in der Migrationsdatei bei "TenderMatch" das FOR SELECT voruebergehend entfernen (Regel wird ALL) -> Werkzeug: tendermatch-systemkontext-insert-abgewiesen-42501 FEHLGESCHLAGEN (Insert gelingt), Schlusszeile nennt Fehlschlaege; (b) die system_read_policy fuer "TenderSavedSearch" voruebergehend aus der Migrationsdatei entfernen -> tendersavedsearch-systemkontext-sieht-beide-mandanten und …-pg-policies-genau-eine-… FEHLGESCHLAGEN (das Werkzeug misst die geschnittene Regel, nicht die lebende Datenbank — die Datenbank bleibt bei 34 Regeln, per pg_policies belegen); (c) im Werkzeug forSystemQuery/buildInlineSystemClient auf set_config(..., false) stellen -> alle …-fortenant-a-nach-systemkontext-nur-a BLEIBEN gruen (der Reset in buildInlineExtendedClient traegt); zusaetzlich den Reset dort entfernen -> rot (Erben sichtbar); (d) Detektor: Zahl fuer tender-matching.service.ts auf 0 -> Spec rot mit Meldung, die Datei und beide Zahlen nennt; eine Fremddatei (apps/api/src/user/admin-seed.service.ts) voruebergehend mit const systemPrisma = forSystem(this.prisma) versehen -> Spec rot. Alle Rueckbauten zuruecknehmen, git status --short leer ausser den gewollten Aenderungen.
Baseline am Ende: Tests >= 1044 (Aufgabe 1 plus mindestens 4 Mail, 2 ldap, 1 digest, 1 matching, minus 4 settings), Typpruefung, Werkzeug N >= 250, keine FEHLGESCHLAGEN. Commit: feat(quick-260914-eym): Mail-Transport je Versand nach Mandant (WINDOWS #30), ldap/digest/matching ueber Systemkontext, vier Tabellen im Werkzeug, Erlaubnisliste vollstaendig. git status --short danach leer.
cd /home/vicolab/projects/tessera-ctl && test 0 -eq "$(cat apps/api/src/settings/settings.service.ts apps/api/src/settings/settings.service.spec.ts apps/api/src/mail/mail.module.ts apps/api/src/mail/mail.service.ts | grep -c 'loadAnySmtpConfigForStartupTransport')" && test 0 -eq "$(grep -v '^\s*//|^\s**' apps/api/src/settings/settings.service.ts | grep -c 'this.prisma.smtpConfig')" && test 0 -eq "$(cat apps/api/src/mail/mail.module.ts apps/api/src/mail/mail.service.ts | grep -v '^\s*//|^\s**' | grep -c 'MailerModule|MailerService')" && grep -q 'getDecryptedSmtpConfig(tenantId)' apps/api/src/mail/mail.service.ts && grep -q 'transport.close()' apps/api/src/mail/mail.service.ts && grep -q 'resolveTransport' apps/api/src/mail/mail.service.ts && grep -q 'sendPasswordResetEmail(email, token, user.tenantId)' apps/api/src/auth/auth.service.ts && test -f apps/api/src/mail/mail.service.spec.ts && test 4 -le "$(grep -c '^\sit(' apps/api/src/mail/mail.service.spec.ts)" && test 2 -eq "$(grep -c 'const systemPrisma = forSystem(this.prisma)' apps/api/src/ldap/ldap-config.service.ts)" && test 0 -eq "$(grep -v '^\s//|^\s**' apps/api/src/ldap/ldap-config.service.ts | grep -c 'this.prisma.ldapConfig')" && grep -q 'forTenant(this.prisma, config.tenantId)' apps/api/src/ldap/ldap-config.service.ts && test 1 -eq "$(grep -c 'const systemPrisma = forSystem(this.prisma)' apps/api/src/tenders/tender-digest.scheduler.ts)" && test 0 -eq "$(grep -v '^\s*//|^\s**' apps/api/src/tenders/tender-digest.scheduler.ts | grep -c 'this.prisma.tenderMatch')" && test 1 -eq "$(grep -c 'const systemPrisma = forSystem(this.prisma)' apps/api/src/tenders/tender-matching.service.ts)" && test 0 -eq "$(grep -v '^\s*//|^\s**' apps/api/src/tenders/tender-matching.service.ts | grep -c 'this.prisma.tenderSavedSearch')" && grep -q 'this.prisma.tender.findMany' apps/api/src/tenders/tender-matching.service.ts && test 4 -eq "$(grep -rl --include=.ts 'forSystem(' apps/api/src | grep -v '.spec.ts$' | grep -v 'prisma-tenant.extension.ts$' | wc -l | tr -d ' ')" && test 5 -eq "$(grep -rh --include=.ts 'const systemPrisma = forSystem(this.prisma)' apps/api/src | grep -v spec | wc -l | tr -d ' ')" && git diff --quiet 5e0e408 -- apps/api/src/user/admin-seed.service.ts apps/api/prisma/schema.prisma apps/api/package.json pnpm-lock.yaml apps/api/scripts/rls-preflight.mjs && for F in ldap-config.service ldap-config.service tender-digest.scheduler tender-matching.service; do :; done && for P in 'apps/api/src/ldap/ldap-config.service.ts | ldapConfig | beides | system-gebunden' 'apps/api/src/ldap/ldap-config.service.ts | ldapFieldMapping | beides | system-gebunden' 'apps/api/src/ldap/ldap-config.service.ts | tenant | keine-mandantengebundene-tabelle | system-gebunden' 'apps/api/src/tenders/tender-digest.scheduler.ts | tenderMatch | beides | system-gebunden' 'apps/api/src/tenders/tender-matching.service.ts | tenderSavedSearch | beides | system-gebunden' 'apps/api/src/settings/settings.service.ts | smtpConfig | muss-mandantengebunden | gebunden'; do grep -qE "^| ${P} |" docs/mandantentrennung-zugriffsklassifikation.md || { echo "KLASSIFIKATION: Zeile fehlt oder falscher Stand: $P"; exit 1; }; done && DB_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' tessera-ctl-db-1) && test -n "$DB_IP" && T=$(npm --prefix apps/api run test 2>&1 | grep -oE 'Tests [0-9]+ passed' | grep -oE '[0-9]+') && echo "TESTS=$T" && test "$T" -ge 1044 && npm --prefix apps/api run type-check >/dev/null 2>&1 && N=$(TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs 2>&1 | tee /tmp/claude-1000/-home-vicolab-projects-tessera-ctl/d06a4a73-407a-4ea6-b686-d1f72c209902/scratchpad/eym-t2-scratch.log | grep -oE 'Alle [0-9]+ Pruefungen bestanden' | grep -oE '[0-9]+') && echo "WERKZEUG=$N" && test "$N" -ge 250 && test 0 -eq "$(grep -c FEHLGESCHLAGEN /tmp/claude-1000/-home-vicolab-projects-tessera-ctl/d06a4a73-407a-4ea6-b686-d1f72c209902/scratchpad/eym-t2-scratch.log)" && for S in dkvmoduleconfig ldapconfig ldapfieldmapping tendermatch tendersavedsearch; do for K in ungebunden-null-zeilen systemkontext-sieht-beide-mandanten systemkontext-insert-abgewiesen-42501 systemkontext-updatemany-count-0 systemkontext-deletemany-count-0 fortenant-a-nach-systemkontext-nur-a is-system-context-unter-fortenant-false pg-policies-genau-eine-system-read-policy-select; do grep -q "^${S}-${K}: bestanden" /tmp/claude-1000/-home-vicolab-projects-tessera-ctl/d06a4a73-407a-4ea6-b686-d1f72c209902/scratchpad/eym-t2-scratch.log || { echo "KENNUNG FEHLT: ${S}-${K}"; exit 1; }; done; done && grep -q '^ldapconfig-systemkontext-include-fieldmappings-beider-mandanten: bestanden' /tmp/claude-1000/-home-vicolab-projects-tessera-ctl/d06a4a73-407a-4ea6-b686-d1f72c209902/scratchpad/eym-t2-scratch.log && test -z "$(git status --short)" && echo AUFGABE-2-OK
Mailmodul ohne Startpfad und ohne Mailer-Fabrik, MailService baut je Versand einen Transport aus der SmtpConfig des Empfaenger-Mandanten mit Umgebungs-Rueckfall, requestPasswordReset reicht den Mandanten durch, vier Mail-Tests; ldap liest beide Pfade ueber den Systemkontext und schreibt je Altzeile gebunden; digest und matching lesen ueber den Systemkontext, ihre Schleifen bleiben gebunden, der Katalog bleibt ungebunden; genau vier Dateien mit genau fuenf forSystem-Zuweisungen, Erlaubnisliste exakt; Werkzeug misst alle fuenf Tabellen plus die Relations-Kennung ohne FEHLGESCHLAGEN, N >= 250; sieben Bestandsaufnahme-Zeilen mit neuem Stand; Rueckbau-Belege (a)–(d) in der SUMMARY; Tests >= 1044, Typpruefung sauber; committet, git status leer.
(2) Klassifikation docs/mandantentrennung-zugriffsklassifikation.md: Uebersichtstabelle bekommt eine DRITTE Spalte System (Rohtreffer systemPrisma\.[a-zA-Z]*\. je Bereich, gleiche Grep-Form wie die beiden anderen, mit datiertem Absatz vor der Tabelle, der die neue Spalte und ihre Grenzen erklaert — Relationsziele zaehlt auch sie nicht); Zeilen tenders/ldap/dkv/settings mit nachgerechneten Werten (Erwartung 33/27/2, 1/27/2, 0/22/1, 0/3/0 — NACHRECHNEN) und je einem datierten Satz im Hinweis; alle uebrigen Zeilen bekommen 0 in der neuen Spalte; Summenzeile ABGELEITET (Erwartung 61/179/5). Klassen-Verteilung: Absatz **Stand 260914-eym:** — Paarzahl 72 und Klassen UNVERAENDERT, sieben Paare aendern nur ihren Stand (alle sieben aufzaehlen, sechs auf system-gebunden, eines auf gebunden), der neue Stand-Wert und seine Vorrangregel erklaert. Hintergrunddienst-Abschnitt (Ueberschrift 'sechs Fälle' bleibt): je Fall ein **Regelschluss (260914-eym):** — ldap (beide Methoden, Nachverschluesselung liest system, schreibt gebunden), digest, matching, admin-seed (gemessen: nur Tenant, nichts umgebaut, Datei unveraendert), fuenfter Fall (#21 geschlossen, Auftrag je Mandant), sechster Fall (#30 geschlossen durch Entfernen — 'der sechste Fall existiert nicht mehr'). Bestandsaufnahme-Header: die fuenf Erkennungsformen und die vier Stand-Werte (aus Aufgabe 1 pruefen, ggf. vervollstaendigen). Zeile admin-seed.service.ts/tenant: Begruendung um den 3c-Befund ergaenzen (Stand bleibt ungebunden). Abschnitt 'Was diese Etappe NICHT entscheidet': den 3c-Punkt als erledigt vermerken.
(3) Auftrag docs/mandantentrennung-etappe3-auftrag.md: im 3c-Abschnitt einen Absatz **Erledigt (260914-eym, <Commit-Kuerzel der drei Commits>):** in der Form des 3b-Vermerks: Migration, Helfer, fuenf Tabellen (SmtpConfig nicht — warum), DKV je Mandant, Mail je Versand, ldap/digest/matching, admin-seed nur dokumentiert, Endzahlen (Tests/Typpruefung/Werkzeug), Verweis auf den (y)-Abschnitt; der urspruengliche Text bleibt stehen. Im Abschnitt 'Was NICHT ohne den User geht' nichts aendern; unter 'Werkzeuge und Fallen' einen Punkt ergaenzen: 'Ein Mock-Fabrik ohne den neuen Export wirft erst beim ZUGRIFF (vitest-Proxy) — jede Spec, deren Pruefling forSystem importiert, braucht den Export im Mock.'
(4) Datenbankrolle docs/mandantentrennung-datenbankrolle.md: in der Aufzaehlung 'was gebaut wurde' nach dem 3b-Punkt einen Punkt 'Eine dritte Sitzungsvariable fuer den Systemkontext' (Migration, Funktion mit COALESCE, FOR SELECT, fuenf Tabellen, forSystem() setzt die beiden anderen Variablen ausdruecklich leer, forTenant() umgekehrt); den Absatz ab 'Diese Stellen brauchen einen ausdruecklichen, benannten Systemkontext' um einen datierten Nachtrag ergaenzen (gebaut in 3c; die Vorher-Pruefung ohne-kontext-leer bleibt gueltig, weil is_system_context() ohne Variable false ist — Beleg is-system-context-ungesetzt-false; Etappe 4 ergaenzt die Vorher-Pruefung um mit-systemkontext-sichtbar). Die SECURITY-DEFINER-Kopfkommentare NICHT anfassen.
(5) Ledger .planning/WINDOWS.md ausschliesslich ueber das Werkzeug: node /home/vicolab/.claude/gsd-core/bin/gsd-tools.cjs windows fixed 21, windows fixed 30, dann windows append --kind deviation --phase quick-260914-eym --file apps/api/src/dkv/dkv.service.ts --description "<ein Absatz ASCII>" — Text: der Single-Flight-Riegel processing in DkvService.processInbox ist EIN prozessweites Boolean, seit 260914-eym laeuft je aktivem Mandanten ein eigener Cron-Auftrag; ueberschneiden sich zwei Ticks verschiedener Mandanten, bricht der zweite still ab und der Mandant wartet bis zum naechsten Intervall (kein Datenverlust, Verzoegerung; mit einem Mandanten unveraendert). Zu schliessen: Riegel je Mandant (Set) mit Test 'zwei Mandanten gleichzeitig, beide werden bedient'. Danach Frontmatter-Zaehler gegen die Zeilen pruefen (erwartet open 15 / waived 1 / fixed 21 / total 37 — aus den Zeilen ZAEHLEN, nicht abschreiben).
Baseline pruefen (Tests >= 1044, Typpruefung, Werkzeug N >= 250 — der Werkzeuglauf aus Aufgabe 2 gilt nur, wenn seither keine Datei unter apps/ geaendert wurde; git diff --stat HEAD~1 -- apps leer, sonst frisch laufen lassen). Erlaubnisliste gegen HEAD: git diff --stat 5e0e408 -- . ':!.planning' nennt genau 29 Dateien (die 30 aus files_modified ohne .planning/WINDOWS.md). Commit: docs(quick-260914-eym): Etappe 3c abgeschlossen — Kritikschrift, Klassifikation, Auftrag, Datenbankrolle, WINDOWS #21/#30 geschlossen, Single-Flight-Riegel als Eintrag. Dann git push (Push-URL zeigt auf localhost:3002; nicht ueber git.vicolab.de), git status --short leer, git rev-parse HEAD == git rev-parse origin/main. SUMMARY schreiben: Messungen, die drei Rueckbau-Belege (Werkzeug (a)/(b)/(c), Detektor (d)) WOERTLICH, die Abweichungen zum Auftrag (fuenf statt sechs Tabellen; ldap hat zwei Systemkontext-Leser; Mail-Startpfad entfernt statt umgestellt; Container musste gestartet werden), was ohne den User nicht geht (nichts Neues — Etappe 4 bleibt Rueckfrage).
cd /home/vicolab/projects/tessera-ctl && K=docs/mandantentrennung-zugriffsklassifikation.md && P=$(grep -cE '^| apps/api/src/[^|]+ | [a-zA-Z]+ | (muss-mandantengebunden|bewusst-uebergreifend|keine-mandantengebundene-tabelle|beides) | (gebunden|ungebunden|gemischt|system-gebunden) |' "$K") && { test "$P" -eq 72 || { echo "PAARE: $P, erwartet 72"; exit 1; }; } && grep -qE "^## Klassen-Verteilung (nach (Datei, Modell)-Fundstellen, ${P} Paare)" "$K" && grep -qE "^| **Summe** | **${P}** |$" "$K" && for C in muss-mandantengebunden keine-mandantengebundene-tabelle beides bewusst-uebergreifend; do N=$(grep -cE "^| apps/api/src/[^|]+ | [a-zA-Z]+ | ${C} |" "$K"); grep -qE "^| ${C} | ${N} |$" "$K" || { echo "KLASSEN-VERTEILUNG: ${C} nennt nicht ${N}"; exit 1; }; done && test 6 -eq "$(grep -cE '^| apps/api/src/[^|]+ | [a-zA-Z]+ | [a-z-]+ | system-gebunden |' "$K")" && grep -q '^**Stand 260914-eym' "$K" && TU=0 && TB=0 && TS=0 && for d in apps/api/src//; do u=$(grep -ro "this.prisma.[a-zA-Z]" "$d" 2>/dev/null | grep -v spec | wc -l | tr -d ' '); b=$(grep -ro "tenantPrisma.[a-zA-Z]." "$d" 2>/dev/null | grep -v spec | wc -l | tr -d ' '); s=$(grep -ro "systemPrisma.[a-zA-Z]." "$d" 2>/dev/null | grep -v spec | wc -l | tr -d ' '); TU=$((TU+u)); TB=$((TB+b)); TS=$((TS+s)); done && echo "ABGELEITET ${TU}/${TB}/${TS}" && { grep -qE "^| **Summe** | **${TU}** | **${TB}** | **${TS}** |" "$K" || { echo "SUMMENZEILE nennt nicht ${TU}/${TB}/${TS}"; exit 1; }; } && grep -qE '^| Bereich | Ungebunden | Gebunden | System |' "$K" && grep -q '^## Der Hintergrunddienst als Falle — sechs Fälle$' "$K" && test 6 -le "$(awk '/^## Der Hintergrunddienst als Falle/{f=1; next} /^## /{f=0} f' "$K" | grep -c 'Regelschluss (260914-eym)')" && D=docs/mandantentrennung-etappe2-fehlerrichtung.md && grep -q '^## Systemkontext (Etappe 3c, 260914-eym)$' "$D" && for Y in y1 y2 y3 y4 y5; do grep -qE "^### (${Y})" "$D" || { echo "ABSCHNITT (${Y}) fehlt"; exit 1; }; done && awk '/^### (y1)/{f=1} /^### (y2)/{f=0} f' "$D" | grep -q 'is-system-context-ungesetzt-false: bestanden' && awk '/^### (y1)/{f=1} /^### (y2)/{f=0} f' "$D" | grep -q 'tendermatch-systemkontext-insert-abgewiesen-42501: bestanden' && awk '/^### (y1)/{f=1} /^### (y2)/{f=0} f' "$D" | grep -q '#system_read_policy#SELECT#is_system_context()#' && awk '/^### (y3)/{f=1} /^### (y4)/{f=0} f' "$D" | grep -q 'ldap.service.ts' && for SEC in '(d4)' '(s4)' '(b4)'; do awk -v s="### $SEC" 'index($0,s)==1{f=1; next} /^### /{f=0} /^## /{f=0} f' "$D" | grep -q 'Nachtrag (260914-eym)' || { echo "NACHTRAG fehlt in $SEC"; exit 1; }; done && awk '/^## Etappe 2 — Abschluss$/{f=1; next} /^## /{f=0} f' "$D" | grep -q '260914-eym' && grep -q 'Erledigt (260914-eym' docs/mandantentrennung-etappe3-auftrag.md && grep -q 'app.system_context' docs/mandantentrennung-datenbankrolle.md && grep -q 'is-system-context-ungesetzt-false' docs/mandantentrennung-datenbankrolle.md && DR=$(git diff 5e0e408 -- docs/mandantentrennung-datenbankrolle.md) || { echo "git diff datenbankrolle fehlgeschlagen"; exit 1; } && test -n "$DR" && test 0 -eq "$(printf '%s\n' "$DR" | grep '^[-+]' | grep -v '^[-+][-+]' | grep -ci 'SECURITY DEFINER')" && W=.planning/WINDOWS.md && grep -E '^| 21 | ' "$W" | grep -q '| fixed |' && grep -E '^| 30 | ' "$W" | grep -q '| fixed |' && grep -E '^| 37 | quick-260914-eym | deviation | apps/api/src/dkv/dkv.service.ts' "$W" | grep -q '| open |' && O=$(grep -cE '^| [0-9]+ | .* | open | ' "$W") && Fx=$(grep -cE '^| [0-9]+ | .* | fixed | ' "$W") && Wv=$(grep -cE '^| [0-9]+ | .* | waived | ' "$W") && grep -q "^open_count: ${O}$" "$W" && grep -q "^fixed_count: ${Fx}$" "$W" && grep -q "^waived_count: ${Wv}$" "$W" && grep -q "^total_count: $((O+Fx+Wv))$" "$W" && echo "LEDGER open=${O} fixed=${Fx} waived=${Wv}" && ST=$(git diff --stat 5e0e408 -- . ':!.planning') || { echo "git diff --stat fehlgeschlagen"; exit 1; } && test 29 -eq "$(printf '%s\n' "$ST" | grep -c '|')" && T=$(npm --prefix apps/api run test 2>&1 | grep -oE 'Tests [0-9]+ passed' | grep -oE '[0-9]+') && echo "TESTS=$T" && test "$T" -ge 1044 && npm --prefix apps/api run type-check >/dev/null 2>&1 && test -z "$(git status --short)" && test "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" && echo AUFGABE-3-OK
Neuer Abschnitt (y1)–(y5) mit woertlicher Werkzeugausgabe und pg_policies-Liste; Nachtraege in (d4)/(s4)/(b4)/Abschluss; Klassifikation mit dritter Spalte, abgeleiteter Summenzeile, Stand-Absatz 260914-eym, sechs Regelschluesse im Hintergrunddienst-Abschnitt, 72 Paare / Klassen unveraendert / sechs Zeilen system-gebunden; Auftrag (3c erledigt), Datenbankrolle (dritte Variable, preflight-Aussage, SECURITY-DEFINER-Kommentare unangetastet), Ledger (#21/#30 fixed, #37 neu, Zaehler aus Zeilen); Erlaubnisliste 29 Dateien gegen 5e0e408; Baseline gruen; committet, gepusht, git status leer, HEAD == origin/main; SUMMARY mit Rueckbau-Belegen und Abweichungen zum Auftrag.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| Anfrageweg -> Systemkontext | Ein Request-Pfad, der forSystem() ruft, laese an JEDER Mandantenregel vorbei — die Grenze, die die Erlaubnisliste des Detektors zieht |
| Systemkontext -> Datenbank (Schreiben) | Nur Lesen ist geoeffnet; Schreiben bleibt hinter der Mandantenregel |
| forSystem -> forTenant auf gepoolten Verbindungen | Kein Kontext darf vom anderen erben |
| Hintergrunddienst -> Mandant | Einmal ueber alle lesen, je Mandant gebunden handeln — die Bauform aller sechs Faelle |
| Mandant A -> SMTP-Zugangsdaten Mandant B | Der Startpfad des Mailmoduls benutzte bisher EINEN beliebigen Server fuer alle |
| Wegwerf-Werkzeug -> ausgelieferte Migration | Das Werkzeug misst nur, was es aus der Migration schneidet |
STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|---|---|---|---|---|---|
| T-EYM-01 | Elevation of Privilege / Information Disclosure (ein Anfrageweg ruft faelschlich forSystem und liest alle Mandanten) |
forSystem(), jeder Dienst |
critical | mitigate | Aufgabe 1/2: FORSYSTEM_ALLOWED_CALL_SITES mit EXAKTER Zahl je Datei (4 Dateien, 5 Aufrufe), Fremddatei/Abweichung/veralteter Eintrag -> Spec rot; Zuweisungsform erzwungen; durch Rueckbau (d) belegt; Kopfkommentar des Helfers nennt die Liste |
| T-EYM-02 | Tampering (Schreiben unter Systemkontext an der Mandantenregel vorbei) | system_read_policy |
critical | mitigate | Regel FOR SELECT; je Tabelle …-insert-abgewiesen-42501, …-updatemany-count-0, …-deletemany-count-0 ueber den generierten Client; Rueckbau (a) FOR SELECT entfernt -> rot; ldap-Nachverschluesselung schreibt je Altzeile ueber forTenant |
| T-EYM-03 | Information Disclosure (Kontext-Erben: eine Verbindung traegt app.system_context in eine spaetere forTenant-Abfrage) |
Helfer, Pool | high | mitigate | set_config(..., true) transaktionslokal (erstes Netz) UND ausdruecklicher Reset aller drei Variablen in jeder Form (zweites Netz); …-fortenant-a-nach-systemkontext-nur-a, …-is-system-context-unter-fortenant-false; Rueckbau (c) belegt, dass der Reset traegt |
| T-EYM-04 | Denial of Service (zu wenig statt zu viel: ein Systemleser ohne Regel liefert nach dem Scharfschalten 0 Zeilen und schweigt) | fuenf Tabellen, sechs Pfade | high | mitigate | Tabellen GEZAEHLT ueber Aufrufe und include (LdapFieldMapping ueber #27-Form, Relations-Kennung); je Tabelle …-ungebunden-null-zeilen UND …-sieht-beide-mandanten als Paar; Rueckbau (b) Regel entfernt -> rot; (y3) beantwortet je Pfad, dass Leere nie loescht |
| T-EYM-05 | Denial of Service (Live-Gehen morgen mit einem Mandanten durch diese Aenderung gestoert) | DKV-Planer, MailService, ldap/digest/matching | critical | mitigate | Schalter AUS (Gate: Compose/.env/DATABASE_URL unveraendert); je Pfad Identitaetstest fuer EINEN Mandanten (Cron-Expression, Tick, Protokollzeile; Transport aus derselben Config, gleicher Absender; ldap/digest/matching bestehende Tests unveraendert); Gesamtsuite >= Baseline nach jeder Aufgabe |
| T-EYM-06 | Information Disclosure / Spoofing (Kennwort-Zuruecksetzung ueber den SMTP-Server und Absender eines FREMDEN Mandanten, T-GWH-03) | MailService |
high | mitigate | Startpfad entfernt; Transport je Versand aus getDecryptedSmtpConfig(tenantId) (gebunden); Test 'zwei Mandanten -> zwei Transporte, keiner sieht die Zugangsdaten des anderen'; Kennwort nur im Methodenrumpf, nie protokolliert |
| T-EYM-07 | Repudiation (Werkzeug misst ein getipptes Wunschbild statt der ausgelieferten Regel) | rls-scratch-check.mjs |
high | mitigate | Funktion und Systemregeln aus der NEUEN Migration geschnitten, Mandantenregeln aus ihren jeweiligen Migrationen, Abbruch bei Fehlschlag; pg_policies live in Scratch UND lebender Datenbank gelesen; Rueckbau (b) zeigt, dass das Werkzeug der Datei folgt |
| T-EYM-08 | Tampering (Pruefsummen ausgelieferter Migrationen) | bestehende Migrationen | high | mitigate | Nur eine NEUE Migrationsdatei; Gate git diff 5e0e408 -- apps/api/prisma ausser der neuen Datei leer; kein DROP POLICY |
| T-EYM-09 | Denial of Service (zwei Mandanten-Ticks ueberschneiden sich, der zweite bricht am prozessweiten Single-Flight-Riegel still ab) | DkvService.processInbox |
medium | accept (mit Aufzeichnung) | Verzoegerung bis zum naechsten Intervall, kein Datenverlust; mit einem Mandanten unveraendert; neuer WINDOWS-Eintrag #37 mit Loesungsweg (Riegel je Mandant) — der Tick bleibt in diesem Durchlauf unangetastet (Auftrag) |
| T-EYM-SC | Tampering | npm/pip/cargo installs | low | accept | Keine Paketinstallation; package.json/Lockfile in jedem Gate gegen 5e0e408 unveraendert; @nestjs-modules/mailer bleibt installiert, nur unbenutzt |
| </threat_model> |
<success_criteria>
forSystem(prisma)existiert, setzt alle drei Variablen,forTenant()/withTenantTransaction()setzenapp.system_contextleer; kein Erben, gemessen und durch Rueckbau falsifiziert.- Migration mit
is_system_context()und fuenfsystem_read_policy … FOR SELECTlokal angewendet; Schreiben unter Systemkontext abgewiesen (42501 / count 0 / P2025), gemessen; bestehende Migrationen, Schema, Compose, .env, Lockfile unveraendert; Schalter AUS. - Alle sechs Faelle behandelt: DKV je Mandant (#21 fixed), Mail je Versand mit entferntem Startpfad (#30 fixed), ldap (zwei Leser, ein gebundener Schreiber), digest, matching, admin-seed dokumentiert; je Pfad ein Identitaetstest fuer einen Mandanten.
- Detektor: fuenfte Form, Stand
system-gebunden, Erlaubnisliste mit exakten Zahlen, falsifiziert; Bestandsaufnahme und alle abgeleiteten Abschnitte konsistent, Inventar-Spec gruen. - Werkzeug:
runSystemContextChecksmit >= 50 neuen Kennungen, Schlusszeile >= 250, keine FEHLGESCHLAGEN. - Baseline: Tests >= 1044 / 62 Dateien (plus zwei neue Spec-Dateien = 64), Typpruefung 0, drei Commits mit Scope
quick-260914-eym, gepusht,git statusleer. - Alles, was bewusst offen bleibt, steht im Ledger (#37) oder in (y4) — nichts still weggelassen. </success_criteria>