62 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-260909-mir | 01 | execute | 1 | true |
|
|
|
|
Zweck: Dieser Bereich haelt die Tankkartenabrechnung eines Unternehmens — welche Fahrzeuge es faehrt, wer sie faehrt, was es tankt und was es dafuer zahlt — und die Zugangsdaten zu dem Postfach, in dem diese Rechnungen ankommen. Ein Quer-Lesen ist die Offenlegung von Fuhrpark und Ausgaben eines fremden Unternehmens.
Die diesem Bereich eigene Fehlerform ist eine andere als in allen drei Bereichen
davor: eine ungebundene Einzelabfrage liefert nach dem Scharfschalten nicht "zu
wenige Zeilen", sondern null — und null heisst an dieser Stelle im Code nicht
"Fehler", sondern "dieses Modul ist nicht eingerichtet". Ein eingerichtetes Modul
saehe danach aus wie ein nie eingerichtetes: ein leeres Formular, eine unauffaellige
Protokollzeile, kein Alarm.
Ergebnis: Die Kritikschrift bekommt einen dkv-Abschnitt samt dieser Fehlerform.
Das Messwerkzeug bekommt die drei Policies dieses Bereichs und drei Messungen, die
es bisher nirgends gab. Die drei Tabellen sind gebunden, der eine bewusst
uebergreifende Pfad ist benannt und markiert statt still gelassen, die Luecke der
ldap-Klasse (Ausfuhrdatei ueber den Dateinamen allein) ist geschlossen — und der
Bereich hat zum ersten Mal ueberhaupt Tests.
<execution_context>
@/.claude/gsd-core/workflows/execute-plan.md
@/.claude/gsd-core/templates/summary.md
</execution_context>
<planning_time_findings>
Alles Folgende wurde am 2026-09-09 zur Planungszeit am lebenden Baum gemessen. Die Zahlen und Zeilenangaben aus dem Auftrag waren Hinweise zum Aufschlagen, keine Aenderungsvollmacht — jede Fundstelle wurde einzeln aufgeschlagen. Auch die Zahlen in diesem Abschnitt sind Planungsstand: bei der Ausfuehrung neu messen, nicht abschreiben.
Ausgangsstand (jetzt gemessen, nicht aus einem Bericht zitiert):
npm --prefix apps/api run test-> 53 Dateien, 772 Tests, gruen, 5,19 s, Rueckgabewert 0.git rev-parse --short HEAD->6464ccb, Arbeitsbaum sauber. Deckt sich mit dem Auftrag.apps/api/package.jsonfuehrttest(vitest run) undtype-check(tsc --noEmit) — die beiden Befehle, auf denen jede Pruefung dieses Plans aufsetzt.- Die Adresse von
tessera-ctl-db-1ist bei der Ausfuehrung neu zu ermitteln; eine Container-Adresse ist veraenderlich und darf nicht aus einem Plan abgeschrieben werden.
Befund A — die 21 sind echt, und sie liegen alle in EINER Datei.
grep -ro "this\.prisma\.[a-zA-Z]*" apps/api/src/dkv | grep -v spec liefert 21
Treffer, aufgeteilt in 10 auf dkvVehicleMaster, 7 auf dkvModuleConfig, 4 auf
dkvInvoiceHistory — alle in apps/api/src/dkv/dkv.service.ts, kein einziger
Treffer ist ein Nicht-Modellzugriff. Dieses eine Mal schrumpft die
Ueberschriftszahl bei der Nachschau NICHT; sie ist der tatsaechliche Arbeitsvorrat.
Das ist eine Feststellung, keine Erlaubnis, beim naechsten Bereich wieder zu
schaetzen.
Befund B — die uebrigen Dateien des Bereichs erreichen die Datenbank nicht, und
das ist nachgesehen statt geglaubt. Der Auftrag verlangt ausdruecklich, dem
Erkenner nicht zu vertrauen (der Bereich tenders hatte eine blinde Stelle).
grep -rn "prisma\.\|PrismaService\|\$transaction\|forTenant\|withTenantTransaction" apps/api/src/dkv --include=*.ts | grep -v '\.spec\.'
zeigt: nur dkv.service.ts injiziert PrismaService. dkv-scheduler.service.ts
geht ueber DkvService, dkv-mail.service.ts ueber SettingsService,
dkv.seed.ts ueber ModuleRegistryService, und dkv-export.service.ts,
dkv-parser.service.ts, dkv.controller.ts, dkv.module.ts beruehren die
Datenbank ueberhaupt nicht. Die blinde Stelle des tenders-Erkenners war der
Transaktionsparameter — hier gibt es keine Transaktion (Befund C), also auch keine
solche Stelle. Zwei Uebergaben in noch nicht umgestellte Bereiche folgen daraus und
gehoeren in die Kritikschrift, nicht in diesen Umbau: dkv-mail.service.ts haengt
an settings.service.ts (dieselbe Reihenfolgebedingung, die der tenders-Durchlauf
als Befund K festhielt), dkv.seed.ts an module-registry.
Befund C — dieser Bereich hat KEINE Transaktion.
grep -rn '\$transaction(' apps/api/src/dkv --include=*.ts | grep -v spec liefert
null Treffer. Der Kopfkommentar von prisma-tenant.extension.ts verlangt
woertlich, vor jedem NEUEN mandantengebundenen Fall mit eigener Transaktion erneut
zu messen — fuer diesen Bereich faellt kein solcher Fall an, withTenantTransaction()
wird hier nicht gebraucht und darf nicht eingefuehrt werden. Zwei Stellen sind
mehrschrittig, aber HEUTE schon nicht atomar: der Ersetzen-Modus des Fahrzeug-Imports
(deleteMany gefolgt von createMany) und die Zugangsdaten-Erhaltung in
saveConfig (lesen, entschluesseln, neu verschluesseln, schreiben). Sie in eine
Transaktion zu heben waere eine Verhaltensaenderung jenseits dieses Auftrags —
dieselbe Grenze, die der tenders-Durchlauf fuer saveConfig/createForUser zog.
Was dieser Bereich statt einer Transaktion hat, ist eine bisher NICHT gemessene
Nebenlaeufigkeitsform: getHistory fuehrt zwei Abfragen ueber Promise.all parallel
aus. Nach der Umstellung sind das zwei parallele Einzeloperationen auf EINEM
gebundenen Klienten. Die vorhandene Lastprobe misst die interaktiven Formen (ii) und
(iii), nicht diese. Sie gehoert deshalb gemessen (Aufgabe 1, Teil 2) — nicht, weil
ein Problem vermutet wird, sondern weil sie die Form ist, auf die sich dieser
Bereich stuetzt.
Befund D — der Planer, und warum er die eigentliche Aufgabe dieses Plans ist. Die Altlast aus 07-04 ist in vier Stellen aufgeschlagen und bestaetigt:
dkv-scheduler.service.ts,onModuleInit(): ruftthis.dkvService.loadConfig()OHNE Mandanten auf und uebernimmtconfig.tenantIdals den einen Mandanten, den der eine benannte Cron-Auftragdkv-inbox-pollfortan bedient.dkv.service.ts,loadConfig(tenantId?): ohne Mandant einfindFirstganz ohne Bedingung, mit Mandant einfindUniqueuebertenantId. EINE Methode, ZWEI gegensaetzliche Bindungsanforderungen hinter einem optionalen Parameter.- Die Auswahl greift auf
CONFIG_SAFE_SELECTzu, dasencryptedInboxCredsausschliesst. Der uebergreifende Pfad sieht also KEINE Zugangsdaten — festgehalten als Entlastung, weil man beim Lesen des Auftrags das Gegenteil vermuten koennte. - Die Pruefung lautet
config?.isActive && config.tenantId. Bei zwei Mandanten, von denen der erste inaktiv ist, registriert der Planer gar nichts — obwohl der zweite aktiv waere. Die Altlast ist damit nicht nur "bedient einen beliebigen", sondern "kann ganz ausfallen".
Nach dem Scharfschalten liefert dieselbe ungebundene Abfrage null. Der Planer
protokolliert dann DKV scheduler: no active config found — cron job not registered
und beendet die Einrichtung — eine Zeile, die auf einer frischen Installation der
Normalfall ist und deshalb niemanden alarmiert. Die zweite stille Stelle liegt in
_runPipeline: no config for tenant ... als Warnung, dann return.
Die Entscheidung dieses Plans, ausgeschrieben statt still getroffen. Von den drei im Auftrag genannten Formen:
- (a) An einen konkret aufgeloesten Mandanten binden — nicht moeglich.
onModuleInit()hat keine Anfrage, keinen Sitzungsnachweis und keinen Konfigurationswert, aus dem ein Mandant kaeme. Einen einzufuehren waere eine neue Einstellung, also eine Funktionsaenderung. - (b) Umbau auf einmal-abfragen-viele-bedienen — abgelehnt, mit Begruendung. Das
ist genau die Mehrmandanten-Planung, die 07-04 zurueckgestellt hat: alle aktiven
Konfigurationen lesen, je Mandant einen Auftrag fuehren, deren Lebenszyklus bei
jeder Konfigurationsaenderung nachziehen (heute verwaltet
setIntervalGENAU EINEN Auftrag unter einem festen Namen), und entscheiden, was bei unterschiedlichen Intervallen je Mandant gilt. Das ist eine Funktion, kein Bindungsumbau. Der Auftrag sagt selbst: wenn das die Schlussfolgerung ist, dann klar sagen statt hineinzuschlittern. Sie ist es. - (c) Als benannte Altlast weiterfuehren, mit Markierung — gewaehlt. Es gibt
dafuer einen unmittelbaren Praezedenzfall:
getAllActiveConfigsim Bereichldap(Befund B/E) ist derselbe Fall — ein bewusst uebergreifender Planer-Lesezugriff, der ungebunden bleibt, einen eigenen Kopfkommentar traegt, und dessen Verstummen nach dem Scharfschalten an die Vorabpruefung von Etappe 4 uebergeben wird.
Die eine Unsymmetrie, die dabei NICHT verschwiegen werden darf und die den
Praezedenzfall nicht deckt: getAllActiveConfigs ist heute RICHTIG und verstummt
erst spaeter. Der DKV-Planer ist heute schon FALSCH — er bedient bei mehreren
Mandanten einen beliebigen und die uebrigen nie — und verstummt zusaetzlich spaeter.
Beides muss die Markierung sagen, sonst liest sie sich wie eine Entwarnung. Die
gewaehlte Form hat deshalb drei Teile: die uebergreifende Abfrage wird eine EIGENE,
benannte Methode (kein Zweig hinter einem optionalen Parameter, den jemand spaeter
"mitrepariert"), sie und der Planer tragen einen Kopfkommentar, der beide Zustaende
benennt, und die Altlast wird als Eintrag im Broken-Windows-Register gefuehrt.
Befund E — die Luecke der ldap-Klasse dieses Bereichs, und sie ist heute
ausnutzbar. DkvService.getExportFile(tenantId, filename) nimmt einen Mandanten
entgegen und benutzt ihn nirgends. Die Methode prueft den Dateinamen gegen ein
Muster (Wegverzeichnis-Schutz, T-07-09), setzt ihn auf das GEMEINSAME Verzeichnis
user-files/ und liest. DkvExportService.writeAndPrune schreibt alle Mandanten in
dasselbe Verzeichnis. Ein Administrator eines beliebigen Mandanten kann ueber
GET /dkv/exports/:filename die Abrechnungsdatei eines fremden Mandanten
herunterladen, sobald er den Namen kennt oder raet — und der Name ist halb
vorhersagbar (RG-DKV-{Rechnungsnummer}-{JJMMTT}.xlsx). Das ist dieselbe Klasse wie
der ldap-Fund (Aufloesung ueber die Kennung allein), nur ueber einen Dateinamen
statt eine Datensatzkennung.
Die Reparatur passt genau in diesen Auftrag, statt daneben zu liegen: die einzige
mandantengebundene Aussage darueber, wem eine Datei gehoert, ist
DkvInvoiceHistory.exportFilename — und dieser Lesezugriff wird in diesem Plan
ohnehin gebunden. Am Frontend nachgesehen statt aus dem Backend geschlossen:
InvoiceHistoryTable.tsx und ExportFileList.tsx beziehen JEDEN angebotenen
Dateinamen aus den Historienzeilen (h.exportFilename). Ein Riegel ueber die
gebundene Historie ist fuer die tatsaechliche Benutzung folgenlos und schliesst
genau den Weg, der daran vorbeigeht.
Die Verhaltensaenderung, die dabei entsteht, gehoert benannt statt uebersehen: eine Datei, die auf der Platte liegt, aber zu KEINER Historienzeile gehoert, ist danach nicht mehr herunterladbar. Das ist die Absicht.
Befund F — die zweite, nicht reparierbare Haelfte derselben Beobachtung.
writeAndPrune behaelt die letzten zehn Dateien des GEMEINSAMEN Verzeichnisses.
Verarbeitet ein Mandant zehn Rechnungen, verdraengt er damit die Dateien aller
anderen; deren Historienzeilen nennen dann einen Dateinamen, der nicht mehr
existiert. Das ist keine Bindungsfrage — es ist die Ablagestruktur, und sie zu
aendern (Unterverzeichnisse je Mandant, Umzug der Bestandsdateien) ist ein eigener
Auftrag. Festhalten, nicht hier loesen.
Befund G — die Besitzpruefungen bei Fahrzeugen, mit einer echten Beobachtung.
updateVehicle und deleteVehicle lesen zuerst ueber findFirst({ id, tenantId })
— mit Mandantenbedingung, also heute korrekt geschuetzt — und schreiben danach ueber
update({ where: { id } }) bzw. delete({ where: { id } }), also ueber die Kennung
ALLEIN. Heute deckt die vorgeschaltete Pruefung das ab; es ist keine Luecke wie
Befund E. Nach der Umstellung muessen aber BEIDE Anweisungen gebunden sein: eine
gebundene Vorpruefung mit einem ungebundenen Schreibzugriff dahinter waere genau der
Riss, den der Auftrag als Klasse benennt. Die Mandantenbedingung im findFirst
bleibt dabei erhalten — nicht mit dem Argument "macht jetzt die Datenbank"
entfernen, dieselbe Regel, die der tenders-Durchlauf fuer die
Benutzerfilterung aufstellte.
Befund H — dieser Bereich hat NICHT die Eindeutigkeitsfalle des Bereichs
tenders, und das ist zu messen statt zu unterstellen. Im Schema nachgelesen:
DkvVehicleMaster traegt @@unique([tenantId, kennzeichen]) — der Mandant ist Teil
des Schluessels. DkvModuleConfig.tenantId ist selbst @unique. Ein gebundenes
upsert kann hier also nicht auf eine unsichtbare fremde Zeile treffen, wie es der
tenders-Befund F beschrieb. Das ist der Gegenbefund, und weil er eine Entscheidung
traegt (keine P2002-Uebersetzung noetig), wird er gemessen und nicht aus dem
Schematext geschlossen. Alle drei Tabellen haben ein NICHT nullbares tenantId —
WINDOWS #19 (nullbare Mandantenkennung) faellt in diesem Bereich nicht an.
Befund I — die Policies dieses Bereichs, wortgleich nachgelesen. Alle drei in
20260909140000_rls_remaining_tenant_tables lauten
USING ("tenantId" = current_tenant_id()), mit ENABLE und FORCE ROW LEVEL SECURITY, ohne FOR-Einschraenkung und ohne eigene WITH CHECK-Klausel. Was
PostgreSQL daraus fuer ein INSERT macht, ist eine Eigenschaft der Datenbank und
keine des Textes — deshalb wird das Schreibverhalten gemessen (Aufgabe 1) und nicht
gelesen. Eine Benutzerdimension haben diese Policies wie die des Bereichs tenders
nicht; hier faellt das weniger ins Gewicht, weil die DKV-Daten mandantenweit und
nicht je Nutzer geschnitten sind — festhalten, nicht ausbauen.
Befund J — die Testlage, und sie ist die dritte Form. Der Auftrag verlangt,
beide bisher beobachteten Formen zu pruefen. Gemessen: find apps/api -name "*.spec.ts"
liefert 53 Dateien und darunter KEINE EINZIGE fuer den Bereich dkv. Es gibt
also weder den Identitaets-Mock von ldap noch das Fehlen eines Mocks bei
vorhandenen Tests wie in groups/tenders — es gibt gar keine Tests. Der Bereich
kann heute auf keinen Fehler rot werden, nicht nur auf keinen Bindungsfehler. Eine
Testdatei anzulegen ist damit keine Zugabe, sondern die Voraussetzung dafuer, dass
irgendeine Aussage dieses Plans nachpruefbar ist. Das Muster liegt vor:
apps/api/src/groups/groups.service.spec.ts mockt die Erweiterung und liefert
__makeBoundClient(tenantId) als protokollierenden Wrapper um DIESELBEN Maps.
Befund K — welcher Code Leere als Abwesenheit deutet (Vorarbeit fuer Aufgabe 1, dort auszuformulieren und zu ergaenzen, nicht abzuschreiben).
Die Form, die diesen Bereich von den drei vorherigen unterscheidet: nicht "eine
Liste ist leer", sondern "ein einzelnes Objekt ist null, und null bedeutet hier
etwas Harmloses".
loadConfig()ohne Mandant, gefolgt vonconfig?.isActiveim Planer —nullheisst "kein aktives Modul", der Planer richtet nichts ein und protokolliert das als Normalfall. Kein Fehler, keine Warnung, keine sichtbare Aenderung._runPipeline,if (!config) { warn; return; }—nullheisst "dieser Mandant hat DKV nicht eingerichtet". Die Rechnungsverarbeitung stellt die Arbeit ein. Rechnungen laufen weiter im Postfach auf, es entsteht keine Historienzeile, keine Ausfuhrdatei, keine E-Mail — und keine Fehlermeldung.getConfigForApi,if (!safe) return null— die Oberflaeche zeigt daraufhin ein leeres Einrichtungsformular. Ein Administrator sieht "noch nicht eingerichtet" fuer ein Modul, das eingerichtet IST, und wuerde beim Neu-Ausfuellen die vorhandenen Zugangsdaten ueberschreiben.getConfigForApi, dertry/catchum die Entschluesselung — faengt heute Entschluesselungsfehler ab und liefert einen leeren Benutzernamen. Nach dem Scharfschalten faellt der Lesezugriff selbst leer aus,rawistnull, und der Zweig laeuft ohne Fehler durch:hasPasswordbleibtfalse. Die Oberflaeche meldet "kein Passwort hinterlegt" fuer ein hinterlegtes Passwort.saveConfig, die Erhaltung der nicht ausgefuellten Zugangsdaten — liest die bestehende Zeile, um Benutzername oder Passwort zu uebernehmen. Laeuft dieser Lesezugriff leer, wird das Feld mit dem LEEREN Wert neu verschluesselt: aus einem gespeicherten Passwort wird ein leeres. Zerstoerend und still, die gefaehrlichste Stelle des Bereichs. Sie liegt hinter einemtry/catch, das ausdruecklich sagt, dass es Fehler ignoriert und mit dem Uebergebenen ueberschreibt.testConnection, der Rueckgriff auf das gespeicherte Passwort — laeuft leer, der Test schlaegt mit einem Anmeldefehler des Postfachs fehl. Harmlos in der Richtung, aber irrefuehrend: die Meldung zeigt auf das Postfach, nicht auf die Datenbank._buildExportRows— die Fahrzeugstammdaten werden gebuendelt geladen und ueber eine Zuordnungstabelle gesucht; ein fehlender Treffer ist per D-13 ein GUELTIGER Zustand (unbekanntes Kennzeichen, leeres Fahrerfeld). Laeuft der Lesezugriff ganz leer, entsteht eine vollstaendige Ausfuhrdatei OHNE einen einzigen Fahrer — kein Fehler, keine Warnung, eine Datei, die plausibel aussieht und falsch ist. Diese Stelle ist der Grund, warum es nicht genuegt, nur die Anzeigepfade zu binden.
Gegenrichtung, ebenfalls nachgesehen: updateVehicle/deleteVehicle werfen bei
Leere LAUT (NotFoundException), importVehiclesCsv wirft bei leerem CSV laut, und
getExportFile wirft bei fehlender Datei laut. Das sind die harmlosen Stellen.
Befund L — was in diesem Bereich NICHT umzustellen ist. Ausser dem in Befund D
behandelten Planer-Startpfad: nichts. Es gibt keine Klasse
keine-mandantengebundene-tabelle in diesem Bereich; alle drei Paare sind
muss-mandantengebunden und alle drei tragen ein nicht nullbares tenantId. Der
Bereich endet damit im Stand gebunden fuer dkvInvoiceHistory und
dkvVehicleMaster und gemischt fuer dkvModuleConfig — die eine Mischung ist der
benannte Planer-Pfad, nicht eine uebrig gebliebene Fundstelle.
</planning_time_findings>
Aufgabe 1: Die Fehlerform dieses Bereichs messen und die Kritikschrift fuer dkv schreiben Der Container `tessera-ctl-db-1` laeuft; seine Adresse per `docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}'` NEU ermitteln (eine Container-Adresse ist veraenderlich und darf nicht aus diesem Plan abgeschrieben werden). apps/api/scripts/rls-scratch-check.mjs, docs/mandantentrennung-etappe2-fehlerrichtung.md Zuerst messen, dann die Kritik aus der Messung schreiben — nicht umgekehrt. Kein Dienstcode in dieser Aufgabe.TEIL 1, apps/api/scripts/rls-scratch-check.mjs: einen sechsten Abschnitt
runDkvAreaChecks(adminUrl, scratchRoleUrl, results) nach dem Vorbild des
vorhandenen runTendersAreaChecks ergaenzen und in main() NACH diesem, aber VOR
runTransactionShapeMeasurement aufrufen — die Transaktionsmessung und die
Lastprobe setzen auf den von runGroupsAreaChecks angelegten Tabellen auf und
duerfen ihre Voraussetzung nicht verlieren; das ist beim Einhaengen zu pruefen, nicht
anzunehmen.
Die drei Policies werden NICHT im Werkzeug neu getippt. Sie kommen aus derselben
Migration, die readRemainingTenantTablesMigrationSql() bereits liest;
extractPolicySql() schneidet je Tabelle heraus. Gebraucht werden
DkvInvoiceHistory, DkvModuleConfig, DkvVehicleMaster. Findet die Extraktion
eine der drei nicht, meldet der Abschnitt eine FEHLGESCHLAGENE Pruefung
dkv-policies-aus-migration-gefunden und bricht ab — das Werkzeug darf nicht still
mit einer geratenen Policy weitermessen.
Der Abschnitt legt in der Wegwerf-Datenbank schlanke Tabellen an, die genau die
Spalten und Bedingungen tragen, die die Policies und die Messungen brauchen. Die
Eindeutigkeitsbedingungen sind dabei KEIN Beiwerk, sondern Gegenstand von Befund H:
DkvModuleConfig."tenantId" eindeutig, DkvVehicleMaster("tenantId","kennzeichen")
zusammengesetzt eindeutig, DkvInvoiceHistory ohne Eindeutigkeit mit einer Spalte
exportFilename. Alle drei tenantId-Spalten NICHT NULL. Danach ENABLE plus FORCE
ROW LEVEL SECURITY, die drei extrahierten Policies, die Rechtevergabe an die
Wegwerf-Rolle und Testzeilen: je Mandant (TENANT-A, TENANT-B) je eine
Konfigurationszeile, je zwei Fahrzeugzeilen mit einem KENNZEICHEN, das in BEIDEN
Mandanten vorkommt, und je eine Historienzeile mit einem gesetzten exportFilename.
Gemessen wird unter der Rolle ohne BYPASSRLS ueber das vorhandene
forTenantQuery-Hilfsmittel, mit diesen Kennungen — jede Kennung genau so
geschrieben, weil die Pruefung dieser Aufgabe sie einzeln in der Ausgabe sucht:
dkvmoduleconfig-gebunden-nur-eigene-zeile— der gebundene SELECT unter TENANT-A liefert die Zeile von A und keine von B.dkvmoduleconfig-ungebunden-null-zeilen— DERSELBE SELECT ohne vorher gesetzten Kontext liefert null Zeilen. Die Belegzeile, die den ganzen Abschnitt der Kritikschrift traegt; sie muss an der echten, ausgelieferten Policy haengen.dkvmoduleconfig-ungebundene-einzelabfrage-liefert-keine-zeile— die eigene, diesem Bereich vorbehaltene Messung: ein ungebundenesSELECT ... LIMIT 1ohne jede Bedingung, also die Form, die der Planer-Startpfad heute benutzt. Bestanden, wenn KEINE Zeile zurueckkommt, obwohl zwei existieren. Der Meldetext sagt ausdruecklich, was das bedeutet: aus einer beliebigen-aber-vorhandenen Zeile wird keine Zeile, und der aufrufende Code liest das als "nicht eingerichtet". Ohne diesen Zusatz ist die Zeile nicht von der vorherigen zu unterscheiden.dkvinvoicehistory-gebunden-nur-eigener-mandantunddkvvehiclemaster-gebunden-nur-eigener-mandant— je eine Pruefung nach dem Muster der ersten.dkvvehiclemaster-gebundenes-einfuegen-fremder-mandant-abgelehnt— ein gebundenes INSERT unter TENANT-A mittenantIdvon TENANT-B. Die Abweisung ist das bestandene Ergebnis. Diese Pruefung existiert, weil die ausgelieferten Policies KEINE eigene WITH-CHECK-Klausel tragen und was PostgreSQL daraus fuer ein INSERT ableitet eine Eigenschaft der Datenbank ist, keine des Policy-Textes (Befund I). Faellt sie anders aus als erwartet, ist das ein Ergebnis und kein Grund, sie umzuschreiben: dann gilt die Messung, und die Abweichung wird in der Kritikschrift ausgeschrieben, bevor Aufgabe 2 beginnt.dkvvehiclemaster-gebundenes-update-ueber-kennung-allein-trifft-null-zeilen— ein gebundenes UPDATE unter TENANT-A, das eine Zeile von TENANT-B allein ueber deren Kennung anspricht. Bestanden, wenn null Zeilen betroffen sind. Der Meldetext nennt die Folge fuer Aufgabe 3 (Befund G): ein Schreibzugriff ueber die Kennung allein scheitert gebunden nicht laut, sondern trifft still nichts — die vorgeschaltete Besitzpruefung bleibt deshalb erhalten und wird nicht durch die Datenbank ersetzt.dkvvehiclemaster-schluessel-traegt-mandant-keine-fremdkollision— unter TENANT-A ein gebundenes INSERT auf ein Kennzeichen, das es unter TENANT-B bereits gibt. Bestanden, wenn es GELINGT. Das ist der Gegenbefund zu Befund F des Bereichstenders: weil der Mandant Teil des zusammengesetzten Schluessels ist, gibt es hier keine Kollision auf einer unsichtbaren fremden Zeile und keine P2002-Uebersetzung zu bauen. Der Meldetext sagt genau das.
TEIL 2, die Nebenlaeufigkeitsform, auf die sich dieser Bereich stuetzt (Befund C):
getHistory fuehrt zwei Abfragen ueber Promise.all parallel aus, nach der
Umstellung also zwei parallele Einzeloperationen auf EINEM gebundenen Klienten. Die
vorhandene runConcurrencyProbe misst die interaktiven Formen, nicht diese. Den
Abschnitt deshalb um dkv-zwei-parallele-gebundene-einzelabfragen-je-eigener-kontext
erweitern: zwei ueber Promise.all gleichzeitig gestartete gebundene Abfragen ueber
DENSELBEN Klienten, eine unter TENANT-A und eine unter TENANT-B, jeweils mit
pg_backend_pid() und current_tenant_id() in der Abfrage. Bestanden, wenn jede
den Kontext sieht, unter dem sie gestartet wurde, und jede die richtige Zeilenzahl
liefert. Als Verletzung zaehlt beides: ein fremder oder fehlender Kontext und ein
Abbruch.
TEIL 3, Beleg statt Behauptung fuer Befund C:
grep -rn '\$transaction(' apps/api/src/dkv --include=*.ts | grep -v spec ausfuehren
und das Ergebnis (Trefferzahl) in der Kritikschrift festhalten, samt der
Feststellung, dass der im Kopf von prisma-tenant.extension.ts verlangte erneute
Test fuer diesen Bereich damit beantwortet ist: kein neuer Fall,
withTenantTransaction() wird nicht gebraucht und nicht eingefuehrt, und die beiden
mehrschrittigen Stellen bleiben so unatomar wie heute. Faellt das Ergebnis anders aus
als in Befund C beschrieben, gilt die MESSUNG, und die Abweichung wird ausgeschrieben,
bevor Aufgabe 2 beginnt.
Das Werkzeug raeumt weiterhin ausschliesslich seine fest verdrahtete Wegwerf-Datenbank ab und bekommt keine steuerbaren Namen (T-EOR-07 bleibt gueltig). Kein bestehender Abschnitt wird veraendert; alle 32 bisherigen Pruefungen muessen unveraendert weiterlaufen.
TEIL 4, docs/mandantentrennung-etappe2-fehlerrichtung.md um einen Abschnitt
## Bereich dkv ERWEITERN, nicht ein zweites Dokument anlegen. Die Leitfrage aus
Abschnitt (a) gilt unveraendert weiter und wird nicht wiederholt; der neue Abschnitt
verweist darauf und haelt im Kopf fest, dass er den Bereich dkv zum Zeitpunkt
seiner Umstellung beschreibt (Quick-Task 260909-mir). In ganzen Saetzen auf Deutsch,
mit derselben Gliederung wie der tenders-Abschnitt:
(d1) Die Messung — die TATSAECHLICH beobachtete Ausgabe des Laufs, hineinkopiert,
nicht nacherzaehlt, mit Datum und der bei der Ausfuehrung ermittelten Adresse. Die
Belegzeile ausdruecklich benennen, und daneben die zweite tragende Zeile
(dkvmoduleconfig-ungebundene-einzelabfrage-liefert-keine-zeile) mit dem Hinweis,
warum dieser Bereich sie zusaetzlich braucht: er liest an der entscheidenden Stelle
kein Mengenergebnis, sondern ein Einzelobjekt. Die Ergebnisse aus Teil 2 und Teil 3
gehoeren ebenfalls hierher.
(d2) Signaltabelle je umgestelltem Pfad: Pfad, Verhalten bei zu wenig Ergebnis, konkretes Signal mit Ort. Es muessen alle in Aufgabe 2 und 3 umgestellten Pfade vorkommen, ausserdem der bewusst ungebunden bleibende Planer-Startpfad und der neue Riegel vor dem Ausfuhrdatei-Download (dessen Signal bei zu kleinem Leseergebnis eine 404 auf eine tatsaechlich vorhandene, tatsaechlich eigene Datei ist).
(d3) Welcher Code Leere als Abwesenheit deutet — der Kern dieses Abschnitts, weil die
Form hier eine andere ist als in den drei Bereichen davor. Ausdruecklich ausfuehren:
ein Einzelobjekt, das null wird, wo null bereits eine gueltige, harmlose
Bedeutung hat ("dieses Modul ist nicht eingerichtet"), sieht nach dem Scharfschalten
identisch aus wie ein nie eingerichtetes Modul — die Oberflaeche zeigt ein normales,
unalarmiertes leeres Formular. Die sieben Stellen aus Befund K namentlich benennen,
getrennt nach zerstoerend / lautlos / irrefuehrend, mit der Zugangsdaten-Erhaltung in
saveConfig als der zerstoerenden. Die Rechnungsverarbeitung bekommt eigenen Raum:
bei still verschwundener Konfiguration laufen Rechnungen im Postfach auf, ohne
Historienzeile, ohne Ausfuhrdatei, ohne Versand und ohne Fehlermeldung. Die
Gegenrichtung (die laut werfenden Stellen) ebenfalls nennen, damit der Abschnitt
nicht nur Alarm ist.
(d4) Was dieser Durchlauf bewusst nicht loest: der Planer-Startpfad mit der
VOLLSTAENDIGEN Begruendung aus Befund D — beide Zustaende (heute beliebig, kuenftig
leer), die drei erwogenen Formen und warum (c) gewaehlt wurde, der Verweis auf den
Praezedenzfall getAllActiveConfigs und die Unsymmetrie, die dieser Praezedenzfall
NICHT deckt; die gemeinsame Ablage der Ausfuhrdateien samt Verdraengung ueber
Mandantengrenzen (Befund F); die Uebergaben in die noch nicht umgestellten Bereiche
settings (ueber dkv-mail.service.ts, dieselbe Reihenfolgebedingung, die der
tenders-Durchlauf als Befund K fuehrt) und module-registry (ueber dkv.seed.ts);
und die offene Architekturfrage req.tenantPrisma, die auch dieser Bereich nicht
entscheidet.
(d5) Was dieser Durchlauf bewusst NICHT anfasst: die beiden mehrschrittigen Stellen
bleiben unatomar (Befund C), und die Ablagestruktur der Ausfuhrdateien bleibt
unveraendert (Befund F). Beide mit der Feststellung, dass sie geprueft und bewusst
gelassen sind — nicht uebersehen.
DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && OUT=$(TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs) && echo "$OUT" && for K in dkvmoduleconfig-gebunden-nur-eigene-zeile dkvmoduleconfig-ungebunden-null-zeilen dkvmoduleconfig-ungebundene-einzelabfrage-liefert-keine-zeile dkvinvoicehistory-gebunden-nur-eigener-mandant dkvvehiclemaster-gebunden-nur-eigener-mandant dkvvehiclemaster-gebundenes-einfuegen-fremder-mandant-abgelehnt dkvvehiclemaster-gebundenes-update-ueber-kennung-allein-trifft-null-zeilen dkvvehiclemaster-schluessel-traegt-mandant-keine-fremdkollision dkv-zwei-parallele-gebundene-einzelabfragen-je-eigener-kontext; do echo "$OUT" | grep -q "^$K: bestanden" || { echo "FEHLENDE ODER FEHLGESCHLAGENE PRUEFUNG: $K"; exit 1; }; done && echo "$OUT" | grep -qE '^Alle [0-9]+ Pruefungen bestanden.$' && grep -q '^## Bereich dkv$' docs/mandantentrennung-etappe2-fehlerrichtung.md && npm --prefix apps/api run test && npm --prefix apps/api run type-check && test -z "$(git diff --name-only HEAD -- apps/api/prisma docker-compose.yml docker-compose.dev.yml docker-compose.prod.yml docker-compose.ci.yml .env.example .env.prod.example)"
Das Wegwerf-Werkzeug meldet alle Pruefungen bestanden (die 32 bisherigen plus die neun namentlich gepruefte des dkv-Abschnitts) mit Rueckgabewert 0; jede der neun Kennungen steht einzeln als bestanden in der Ausgabe; npm --prefix apps/api run test meldet weiterhin 772 Tests gruen und die Typpruefung ist sauber; docs/mandantentrennung-etappe2-fehlerrichtung.md traegt einen Abschnitt ## Bereich dkv mit der tatsaechlich beobachteten Ausgabe, einer Signaltabelle, dem eigenen Unterabschnitt zur null-als-Abwesenheit-Fehlerform mit sieben namentlich benannten Stellen, der ausgeschriebenen Planer-Entscheidung samt der drei erwogenen Formen, und den beiden Abschnitten zu dem, was bewusst offen bzw. unangetastet bleibt; Schema, Migrationen, Compose- und Beispiel-Umgebungsdateien sind unveraendert.
apps/api/src/dkv/dkv.service.spec.ts neu anlegen, nach dem Muster aus
apps/api/src/groups/groups.service.spec.ts:
- Die Erweiterung
../prisma/prisma-tenant.extensionwird gemockt, sodassforTenant(prisma, tenantId)anprisma.__makeBoundClient(tenantId)delegiert.withTenantTransactionwird NICHT gemockt und nicht gebraucht — dieser Bereich hat keine Transaktion (Befund C). - Ein handgeschriebener In-Memory-Fake mit Maps fuer
dkvModuleConfig,dkvVehicleMasterunddkvInvoiceHistory.__makeBoundClient(tenantId)liefert je Modell einen protokollierenden Wrapper um DIESELBEN Maps und schreibt jeden Aufruf als{ tenantId, model, method }in ein gemeinsames Protokoll. Zwei unterscheidbare Klienten ueber einen Speicher — der ungebundene Fake protokolliert nicht, der gebundene schon. Genau daran wird eine vergessene Bindung sichtbar. - Die uebrigen Abhaengigkeiten des Dienstes (Verschluesselung, Zerleger, Ausfuhr, Versand, die beiden Postfach-Anbieter) als schlichte Attrappen.
Erwartete Testfaelle dieser Aufgabe:
- Test 1:
getConfigForApi(tenantId)— beide Lesezugriffe aufdkvModuleConfigstehen im Bindungsprotokoll unter genau diesem Mandanten. - Test 2:
getConfigForApieines Mandanten liefert NICHT die Konfiguration eines zweiten Mandanten, wenn beide im Speicher liegen. - Test 3:
saveConfig(tenantId, dto)mit gesetztem Benutzernamen und LEEREM Passwort — der erhaltende Lesezugriff UND der Schreibzugriff stehen gebunden im Protokoll, und das gespeicherte Passwort ist danach unveraendert. Das ist die zerstoerende Stelle aus Befund K; sie braucht einen eigenen Test, nicht nur eine Bindungszaehlung. - Test 4:
testConnection(tenantId, dto)mit leerem Passwort — der Rueckgriff auf die gespeicherten Zugangsdaten steht gebunden im Protokoll. - Test 5: die Verarbeitungsstrecke liest ihre Konfiguration gebunden; bei fehlender Konfiguration bricht sie wie bisher still ab (das Verhalten wird NICHT geaendert, nur festgeschrieben, damit eine spaetere Aenderung sichtbar wird).
- Test 6: der bewusst uebergreifende Planer-Startpfad steht NICHT im Bindungsprotokoll — die einzige Stelle des Bereichs, an der das Fehlen einer Bindung die bestandene Erwartung ist. Der Testname sagt das ausdruecklich, damit niemand ihn spaeter als vergessene Bindung "repariert".
- Test 7: der Planer-Startpfad liefert die verschluesselten Zugangsdaten NICHT mit — die Entlastung aus Befund D wird festgeschrieben, nicht geglaubt.
Falsifizierungsnachweis, verlangt und zu belegen: nach der Umstellung eine der
gebundenen Stellen probeweise zurueckbauen, beobachten, dass GENAU der erwartete Test
rot wird, den Rueckbau zuruecknehmen, und beides im SUMMARY festhalten. Ein Test, von
dem nur behauptet wird, dass er rot werden koennte, ist kein Nachweis.
apps/api/src/dkv/dkv.service.ts, alle sieben Zugriffe auf dkvModuleConfig:
Die Methode loadConfig(tenantId?) wird in ZWEI Methoden geteilt. Das ist der Kern
dieser Aufgabe und nicht kosmetisch: ein optionaler Parameter, hinter dem der eine
Zweig gebunden werden MUSS und der andere gebunden werden DARF NICHT, ist genau die
Form, die spaeter jemand versehentlich vereinheitlicht.
loadConfig(tenantId)bekommt einen PFLICHT-Mandanten und laeuft ueber einen gebundenen Klienten ausforTenant(this.prisma, tenantId).- Der uebergreifende Zweig wird eine eigene, benannte Methode, deren Name sagt, was
sie tut (sie zieht IRGENDEINE aktive Konfiguration fuer die Einrichtung des
Planers, nicht die eines bestimmten Mandanten). Sie bleibt bewusst UNGEBUNDEN und
behaelt die sichere Feldauswahl, die die verschluesselten Zugangsdaten
ausschliesst. Ihr Kopfkommentar benennt BEIDE Zustaende: dass sie heute schon eine
beliebige Zeile zieht und bei mehreren Mandanten die uebrigen nie bedient, UND dass
sie nach dem Scharfschalten gar keine Zeile mehr zieht und der Planer daraufhin
still nichts einrichtet. Er benennt ausserdem, warum sie NICHT gebunden wird
(binden hiesse garantiert leer laufen), warum sie NICHT auf
einmal-abfragen-viele-bedienen umgebaut wird (das ist die in 07-04
zurueckgestellte Mehrmandanten-Planung, also eine Funktionsaenderung), und wohin
das Signal gehoert (Vorabpruefung von Etappe 4,
apps/api/scripts/rls-preflight.mjs). Der Kommentar verweist auf dendkv-Abschnitt der Kritikschrift statt die Begruendung zu wiederholen.
getConfigForApi, saveConfig, testConnection und der Konfigurations-Lesezugriff
der Verarbeitungsstrecke binden vollstaendig: je Methode EIN gebundener Klient, nicht
einer je Modellzugriff. Die bestehenden where-Bedingungen ueber tenantId bleiben
erhalten — nicht mit dem Argument entfernen, das mache jetzt die Datenbank;
dieselbe Regel, die der tenders-Durchlauf fuer die Benutzerfilterung aufstellte.
Das upsert in saveConfig braucht keine Kollisionsbehandlung, weil der
Mandantenschluessel selbst eindeutig ist (Befund H, in Aufgabe 1 gemessen).
apps/api/src/dkv/dkv-scheduler.service.ts: den vorhandenen Kopfkommentar zur
Einmandanten-Fassung fortschreiben statt ersetzen. Er benennt danach zusaetzlich, dass
der Planer nach dem Scharfschalten gar nichts mehr einrichtet, dass die Protokollzeile
ueber die fehlende aktive Konfiguration auf einer frischen Installation der Normalfall
ist und deshalb nicht alarmiert, und verweist auf den dkv-Abschnitt der
Kritikschrift und auf den Registereintrag aus dieser Aufgabe. Der Aufruf wird auf die
neue, benannte Methode umgestellt. An der Ablauflogik des Planers wird NICHTS
geaendert: keine zweite Konfiguration, kein zweiter Auftrag, kein Fan-out.
Broken-Windows-Register: die Altlast als offenen Eintrag anlegen, mit
node ~/.claude/gsd-core/bin/gsd-tools.cjs windows append (die Aufrufform ohne
Argumente ausgeben lassen, wenn die erwarteten Felder unklar sind — nicht raten). Der
Text nennt beide Zustaende, den betroffenen Pfad, die Reihenfolgebedingung fuer
Etappe 4 und die Entscheidung samt Begruendung. Die Verwaltungsfelder des Registers
werden dem Werkzeug ueberlassen, nicht von Hand geschrieben.
Nichts anderes wird in dieser Aufgabe angefasst: keine Fahrzeug- oder
Historienpfade (Aufgabe 3), kein Schema, keine Migration, keine Compose- oder
Umgebungsdatei.
DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs && npm --prefix apps/api run test && npm --prefix apps/api run type-check && test -f apps/api/src/dkv/dkv.service.spec.ts && grep -q '__makeBoundClient' apps/api/src/dkv/dkv.service.spec.ts && test -z "$(git diff --name-only HEAD -- apps/api/prisma docker-compose.yml docker-compose.dev.yml docker-compose.prod.yml docker-compose.ci.yml .env.example .env.prod.example)"
apps/api/src/dkv/dkv.service.spec.ts existiert, nutzt den Zwei-Klienten-Nachweis ueber __makeBoundClient, und deckt die sieben in <behavior> genannten Faelle ab; die Testzahl liegt ueber 772 und der Lauf ist gruen; die Typpruefung ist sauber; das Wegwerf-Werkzeug meldet weiterhin alle Pruefungen bestanden; alle sieben Zugriffe auf dkvModuleConfig ausser dem einen benannten Planer-Startpfad laufen ueber forTenant(); der Planer-Startpfad ist eine eigene, benannte Methode mit einem Kopfkommentar, der beide Zustaende, die getroffene Entscheidung und den Ort des Signals nennt; dkv-scheduler.service.ts traegt den fortgeschriebenen Kommentar; die Altlast steht als offener Eintrag im Broken-Windows-Register; der Falsifizierungsnachweis (probeweiser Rueckbau, erwarteter Test rot, Rueckbau zurueckgenommen) ist im SUMMARY festgehalten; Schema, Migrationen, Compose- und Beispiel-Umgebungsdateien sind unveraendert.
- Test 1:
listVehiclesundcreateVehiclestehen gebunden im Protokoll und liefern bzw. schreiben nur unter dem uebergebenen Mandanten. - Test 2:
updateVehicleunddeleteVehicle— BEIDE Anweisungen je Methode (Besitzpruefung und Schreibzugriff) stehen gebunden im Protokoll. Das ist Befund G: eine gebundene Vorpruefung mit einem ungebundenen Schreibzugriff dahinter waere die Luecke, nicht die Loesung. - Test 3:
updateVehicle/deleteVehicleauf ein Fahrzeug eines FREMDEN Mandanten werfen weiterhin die vorhandene Nicht-gefunden-Ausnahme; die Mandantenbedingung in der Besitzpruefung bleibt erhalten und wird nicht durch die Datenbank ersetzt. - Test 4:
importVehiclesCsvin beiden Modi — Ersetzen (loeschen und anlegen) und Zusammenfuehren (je Zeile aktualisieren-oder-anlegen) — steht in JEDER Anweisung gebunden im Protokoll, und der Ersetzen-Modus loescht ausschliesslich Fahrzeuge des eigenen Mandanten, waehrend die eines zweiten Mandanten unberuehrt bleiben. - Test 5:
getHistory— beide parallel gestarteten Abfragen stehen gebunden im Protokoll, und die Gesamtzahl zaehlt nur die eigenen Zeilen. - Test 6: die beiden Historien-Schreibzugriffe der Verarbeitungsstrecke (Erfolgsfall und Zerlegungsfehler) stehen gebunden im Protokoll.
- Test 7: der gebuendelte Lesezugriff auf die Fahrzeugstammdaten beim Aufbau der Ausfuhrzeilen steht gebunden im Protokoll und zieht keine Fahrzeuge eines zweiten Mandanten in die Ausfuhrdatei.
- Test 8:
getExportFileliefert eine Datei, zu der eine Historienzeile DIESES Mandanten mit passendem Dateinamen existiert. - Test 9:
getExportFileverweigert dieselbe Datei einem ZWEITEN Mandanten mit der vorhandenen Nicht-gefunden-Ausnahme, obwohl die Datei existiert und das Namensmuster besteht. Das ist der Beleg fuer die geschlossene Luecke aus Befund E und der wichtigste Test dieser Aufgabe. - Test 10: der Riegel laeuft ueber einen GEBUNDENEN Lesezugriff auf die Historie — im Bindungsprotokoll nachweisbar, nicht nur am Ergebnis.
Falsifizierungsnachweis wie in Aufgabe 2: eine gebundene Stelle probeweise
zurueckbauen, beobachten, dass genau der erwartete Test rot wird, zuruecknehmen, im
SUMMARY festhalten.
apps/api/src/dkv/dkv.service.ts, die zehn Zugriffe auf dkvVehicleMaster und die
vier auf dkvInvoiceHistory:
Alle binden ueber forTenant(this.prisma, tenantId), je Methode EIN gebundener
Klient. Betroffen sind die Fahrzeugliste, das Anlegen, die beiden Paare aus
Besitzpruefung und Schreibzugriff bei Aendern und Loeschen, beide Zweige des
CSV-Imports, die beiden Historien-Schreibzugriffe der Verarbeitungsstrecke, die
beiden parallelen Abfragen der Historienseite und der gebuendelte Lesezugriff beim
Aufbau der Ausfuhrzeilen. Die bestehenden where-Bedingungen ueber tenantId und
die vorgeschalteten Besitzpruefungen bleiben unveraendert erhalten. Fuer den
Zusammenfuehren-Modus ist keine Kollisionsbehandlung noetig, weil der Mandant Teil
des zusammengesetzten Schluessels ist (Befund H, in Aufgabe 1 gemessen) — falls die
Messung in Aufgabe 1 anders ausgefallen ist, gilt sie und nicht dieser Satz.
Die Ausfuhrdatei-Luecke (Befund E) schliessen: getExportFile nimmt bereits einen
Mandanten entgegen und verwirft ihn. Kuenftig entscheidet ein GEBUNDENER Lesezugriff
auf die Rechnungshistorie ueber den hinterlegten Dateinamen, ob diese Datei zu diesem
Mandanten gehoert; ohne Treffer wird dieselbe Nicht-gefunden-Ausnahme geworfen, die
die Methode heute bei fehlender Datei wirft — Fehlen und Fremdbesitz kollabieren
bewusst zu derselben Antwort, damit die Antwort selbst nichts ueber fremde Mandanten
verraet. Der vorhandene Musterabgleich des Dateinamens bleibt als erste Stufe
unveraendert stehen; der neue Riegel kommt DAHINTER und ersetzt ihn nicht. Ein
Kommentar an der Methode haelt die dabei entstehende Verhaltensaenderung fest: eine
Datei ohne zugehoerige Historienzeile ist danach nicht mehr abrufbar, und das ist die
Absicht.
docs/mandantentrennung-zugriffsklassifikation.md nachziehen:
- Die Bereichszeile
dkvder Uebersichtstabelle mit den bei der Ausfuehrung NEU gemessenen Zahlen fortschreiben, im Stil der bereits fortgeschriebenen Zeilen (war X/0plus eine Begruendung, welche Zugriffe umgestellt wurden und welche bewusst nicht). Die Summenzeile mitziehen. - Die drei Bestandsaufnahme-Zeilen des Bereichs auf den maschinell gemessenen Stand
setzen:
dkvInvoiceHistoryunddkvVehicleMastergebunden,dkvModuleConfiggemischt, jeweils mit einer Begruendung, die beidkvModuleConfigausdruecklich den einen benannten Planer-Startpfad als Grund der Mischung nennt — damit niemand ihn spaeter fuer eine uebersehene Fundstelle haelt. - Den Abschnitt zum Hintergrunddienst als Falle um den DKV-Planer erweitern, mit der Feststellung, dass er die entartete Form dieser Falle ist: er iteriert nicht ueber alle Mandanten, sondern zieht EINEN beliebigen — die uebrigen bekommen nicht zu wenig, sondern gar nichts.
Sollte rls-access-inventory.spec.ts nach der Umstellung einen anderen Stand messen
als hier beschrieben, gilt die MESSUNG: dann wird das Dokument auf den gemessenen
Stand gesetzt und die Abweichung im SUMMARY ausgeschrieben, statt die Pruefung
passend zu machen.
docs/mandantentrennung-etappe2-fehlerrichtung.md abschliessen: den in Aufgabe 1
angelegten dkv-Abschnitt um einen Nachtrag ergaenzen, der die tatsaechlich
umgesetzten Pfade gegen die dort angekuendigten haelt, und die geschlossene
Ausfuhrdatei-Luecke festhalten. Wie in den vorherigen Durchlaeufen wird der
urspruengliche Text NICHT umgeschrieben — er beschreibt korrekt den Zustand zum
Zeitpunkt der Umstellung; der Nachtrag steht daneben.
DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs && npm --prefix apps/api run test && npm --prefix apps/api run type-check && npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts && grep -qE '^| apps/api/src/dkv/dkv.service.ts | dkvVehicleMaster | muss-mandantengebunden | gebunden |' docs/mandantentrennung-zugriffsklassifikation.md && grep -qE '^| apps/api/src/dkv/dkv.service.ts | dkvInvoiceHistory | muss-mandantengebunden | gebunden |' docs/mandantentrennung-zugriffsklassifikation.md && grep -qE '^| apps/api/src/dkv/dkv.service.ts | dkvModuleConfig | muss-mandantengebunden | gemischt |' docs/mandantentrennung-zugriffsklassifikation.md && test -z "$(git diff --name-only HEAD -- apps/api/prisma docker-compose.yml docker-compose.dev.yml docker-compose.prod.yml docker-compose.ci.yml .env.example .env.prod.example)"
Alle 21 Zugriffe des Bereichs ausser dem einen benannten Planer-Startpfad laufen ueber forTenant(); getExportFile gibt eine Datei nur heraus, wenn eine gebundene Historienzeile dieses Mandanten sie nennt, und verweigert sie einem zweiten Mandanten mit derselben Nicht-gefunden-Ausnahme; die Testdatei deckt die zehn in <behavior> genannten Faelle ab und der Lauf ist gruen mit mehr als 772 Tests; rls-access-inventory.spec.ts laeuft gruen und stimmt mit den drei Bestandsaufnahme-Zeilen des Klassifikationsdokuments ueberein (gebunden/gebunden/gemischt); die Bereichs- und Summenzeile der Uebersichtstabelle sind mit neu gemessenen Zahlen fortgeschrieben; der Abschnitt zum Hintergrunddienst als Falle nennt den DKV-Planer als entartete Form; die Kritikschrift traegt den Nachtrag mit den tatsaechlich umgesetzten Pfaden und der geschlossenen Ausfuhrdatei-Luecke; die Typpruefung ist sauber; das Wegwerf-Werkzeug meldet alle Pruefungen bestanden; Schema, Migrationen, Compose- und Beispiel-Umgebungsdateien sind unveraendert und DATABASE_URL zeigt weiterhin auf die Rolle tessera.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| Browser/Admin → DKV-API | Der Mandant kommt ausschliesslich aus req.tenantId (gesetzt von TenantMiddleware nach den Auth-Guards); _requireTenant wirft, wenn er fehlt. Alles jenseits dieser Grenze ist nicht vertrauenswuerdig. |
| API → PostgreSQL | Die RLS-Grenze. Heute wirkungslos, weil DATABASE_URL auf die Rolle tessera mit BYPASSRLS zeigt (WINDOWS #18) — dieser Plan bereitet die Grenze vor, schaltet sie aber NICHT scharf. |
API → gemeinsames Ablageverzeichnis user-files/ |
Die einzige Grenze dieses Bereichs, die die Datenbank NICHT ziehen kann: alle Mandanten schreiben Ausfuhrdateien in dasselbe Verzeichnis, die Datei traegt keinen Mandanten. |
| API → fremdes Postfach (IMAP/Exchange) | Ueber Zugangsdaten, die verschluesselt in DkvModuleConfig liegen und im Klartext nur innerhalb einer Methode existieren (T-05-13). |
API → SMTP (ueber SettingsService) |
Bereich settings ist noch nicht umgestellt — Reihenfolgebedingung fuer Etappe 4, nicht Gegenstand dieses Plans. |
STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|---|---|---|---|---|---|
| T-MIR-01 | Information Disclosure | dkv.service.ts — Lesepfade auf dkvInvoiceHistory und dkvVehicleMaster (Fuhrpark, Fahrer, Rechnungs- und Ausgabenhistorie) |
high | mitigate | Aufgabe 3 bindet jeden dieser Lesepfade ueber forTenant(); Aufgabe 1 misst an der ausgelieferten Policy, dass ein gebundener SELECT nur die eigene Zeile liefert (dkvinvoicehistory-gebunden-nur-eigener-mandant, dkvvehiclemaster-gebunden-nur-eigener-mandant); die vorhandenen where-Bedingungen ueber tenantId bleiben als zweite Schicht erhalten. |
| T-MIR-02 | Information Disclosure | DkvService.getExportFile — Aufloesung einer Ausfuhrdatei allein ueber den Dateinamen, der uebergebene Mandant wird verworfen (Befund E, heute ausnutzbar) |
high | mitigate | Aufgabe 3 setzt einen gebundenen Lesezugriff auf DkvInvoiceHistory.exportFilename als Riegel hinter den bestehenden Musterabgleich; Fehlen und Fremdbesitz kollabieren zu derselben Nicht-gefunden-Antwort, damit die Antwort nichts ueber fremde Mandanten verraet. Test 9 der Aufgabe 3 belegt die Verweigerung gegenueber einem zweiten Mandanten. |
| T-MIR-03 | Tampering | updateVehicle/deleteVehicle — Schreibzugriff ueber die Datensatzkennung allein hinter einer Besitzpruefung (Befund G) |
medium | mitigate | Aufgabe 3 bindet BEIDE Anweisungen je Methode und laesst die Mandantenbedingung der Besitzpruefung stehen; Aufgabe 1 misst, dass ein gebundenes UPDATE ueber die Kennung allein auf eine fremde Zeile null Zeilen trifft (dkvvehiclemaster-gebundenes-update-ueber-kennung-allein-trifft-null-zeilen) — es scheitert also still statt laut, weshalb die Vorpruefung nicht entfallen darf. |
| T-MIR-04 | Tampering | Gebundenes Einfuegen mit fremder Mandantenkennung — die ausgelieferten Policies tragen keine eigene WITH-CHECK-Klausel (Befund I) | medium | mitigate | Aufgabe 1 misst das Schreibverhalten an der echten Policy statt es aus dem Policy-Text zu schliessen (dkvvehiclemaster-gebundenes-einfuegen-fremder-mandant-abgelehnt). Faellt die Messung anders aus als erwartet, gilt sie und die Abweichung wird ausgeschrieben, bevor umgebaut wird. |
| T-MIR-05 | Information Disclosure | Postfach-Zugangsdaten in DkvModuleConfig.encryptedInboxCreds — Auswahl der Konfigurationszeile durch eine Abfrage ohne Mandantenbedingung |
medium | mitigate | Der uebergreifende Planer-Startpfad behaelt die sichere Feldauswahl, die die verschluesselten Zugangsdaten ausschliesst; Test 7 der Aufgabe 2 schreibt das fest statt es zu glauben. Alle Pfade, die die Zugangsdaten tatsaechlich lesen (Anzeige, Speichern, Verbindungstest, Verarbeitungsstrecke), binden in Aufgabe 2 vollstaendig. |
| T-MIR-06 | Denial of Service | Umgekehrte Fehlerrichtung: nach dem Scharfschalten liefert der ungebundene Planer-Startpfad null, der Planer richtet still nichts ein, und die Verarbeitungsstrecke liest null als "Modul nicht eingerichtet" — ein eingerichtetes Modul sieht aus wie ein nie eingerichtetes |
medium | accept | Bewusst nicht in diesem Durchlauf geloest, mit voller Begruendung (Befund D): eine Bindung liesse den Pfad garantiert leer laufen, ein Umbau auf einmal-abfragen-viele-bedienen waere die in 07-04 zurueckgestellte Mehrmandanten-Planung und damit eine Funktionsaenderung. Stattdessen dreifach markiert: eigene benannte Methode mit Kopfkommentar, der beide Zustaende nennt (Aufgabe 2), Abschnitt (d4) der Kritikschrift (Aufgabe 1), offener Eintrag im Broken-Windows-Register (Aufgabe 2). Wirkung erst nach Etappe 4, die ihre eigene Vorabpruefung (rls-preflight.mjs) hat; kein Vertraulichkeits- oder Integritaetsschaden, kein Datenverlust, selbstanzeigend beim naechsten Rechnungslauf. |
| T-MIR-07 | Tampering | Zerstoerende Fehlerrichtung: laeuft der erhaltende Lesezugriff in saveConfig leer, wird ein gespeichertes Passwort mit dem leeren Wert neu verschluesselt (Befund K, Stelle 5) |
high | mitigate | Aufgabe 2 bindet Lese- UND Schreibzugriff dieser Methode gemeinsam an denselben Mandanten, sodass beide dieselbe Sichtbarkeit haben und ein leerer Lesezugriff nicht mit einem erfolgreichen Schreibzugriff kombiniert werden kann; Test 3 der Aufgabe 2 prueft die Erhaltung des Passworts ausdruecklich statt nur die Bindung zu zaehlen. Die Stelle ist zusaetzlich in Abschnitt (d3) der Kritikschrift als die zerstoerende Stelle des Bereichs benannt. |
| T-MIR-08 | Denial of Service | Gemeinsames Ablageverzeichnis: writeAndPrune behaelt die letzten zehn Dateien ueber alle Mandanten hinweg und verdraengt damit die Dateien fremder Mandanten (Befund F) |
low | accept | Keine Bindungsfrage, sondern die Ablagestruktur; eine Loesung (Unterverzeichnisse je Mandant plus Umzug der Bestandsdateien) ist ein eigener Auftrag. In Abschnitt (d4) der Kritikschrift festgehalten. Auswirkung ist ein fehlgeschlagener Download bei erhaltener Historienzeile, kein Datenverlust an den Abrechnungsdaten selbst. |
| T-MIR-SC | Tampering | npm/pip/cargo-Installationen | n/a | accept | Dieser Plan installiert kein Paket — keine Aufgabe fuehrt einen Paketmanager aus, alle benutzten Bausteine (vitest, @prisma/client, forTenant) sind bereits Abhaengigkeiten. Das Legitimitaets-Gate faellt damit nicht an; sollte bei der Ausfuehrung wider Erwarten eine Installation noetig werden, ist das ein Abbruchgrund und keine Nebensache. |
| </threat_model> |
<success_criteria>
- Alle 21 Zugriffe des Bereichs
dkvsind entweder ueberforTenant()gebunden oder gehoeren zu dem EINEN benannten, kommentierten und im Register gefuehrten Planer-Startpfad. Es bleibt keine Fundstelle ohne Zuordnung. - Die Entscheidung zum Planer steht ausgeschrieben an drei Orten (Code, Kritikschrift, Register) und nennt beide Zustaende — heute beliebig, kuenftig leer — statt nur den zweiten.
- Die Luecke der ldap-Klasse dieses Bereichs (Ausfuhrdatei ueber den Dateinamen allein) ist geschlossen und der Riegel ist durch einen Test belegt, der einem zweiten Mandanten den Zugriff verweigert.
- Der Bereich hat zum ersten Mal Tests, und diese Tests koennen auf eine vergessene Bindung rot werden — nachgewiesen durch probeweisen Rueckbau, nicht behauptet.
- Beide Dokumente sind fortgeschrieben, nicht umgeschrieben: der urspruengliche Text bleibt lesbar, Nachtraege stehen daneben. </success_criteria>