docs(quick-260909-mir): Plan fuer Etappe 2, Bereich dkv
This commit is contained in:
+791
@@ -0,0 +1,791 @@
|
||||
---
|
||||
phase: quick-260909-mir
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
autonomous: true
|
||||
requirements: [WINDOWS-20, ETAPPE-2-DKV]
|
||||
|
||||
files_modified:
|
||||
- apps/api/scripts/rls-scratch-check.mjs
|
||||
- docs/mandantentrennung-etappe2-fehlerrichtung.md
|
||||
- apps/api/src/dkv/dkv.service.ts
|
||||
- apps/api/src/dkv/dkv.service.spec.ts
|
||||
- apps/api/src/dkv/dkv-scheduler.service.ts
|
||||
- docs/mandantentrennung-zugriffsklassifikation.md
|
||||
- .planning/WINDOWS.md
|
||||
|
||||
estimate:
|
||||
tokens: 170000
|
||||
raw_tokens: 170000
|
||||
tasks: 3
|
||||
confidence: low
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Jeder Zugriff des Bereichs `dkv`, der auf Rechnung genau eines Mandanten eine der drei DKV-Tabellen beruehrt, laeuft ueber einen gebundenen Client — Postfach-/Modulkonfiguration, Rechnungshistorie und Fahrzeugstammdaten vollstaendig."
|
||||
- "Der eine bewusst uebergreifende Zugriff des Bereichs — der Planer-Startpfad, der heute eine BELIEBIGE Konfigurationszeile zieht — ist eine eigene, benannte Methode mit eigenem Kopfkommentar, nicht ein Zweig hinter einem optionalen Parameter. Er bleibt ungebunden, weil ihn zu binden ihn garantiert leer laufen liesse."
|
||||
- "Die Entscheidung zum Planer ist ausgeschrieben, nicht stillschweigend getroffen: Weiterfuehrung als benannte Altlast mit Markierung im Code, im Fehlerrichtungs-Dokument und im Broken-Windows-Register — ausdruecklich NICHT der Umbau auf einmal-abfragen-viele-bedienen, weil das die in 07-04 zurueckgestellte Mehrmandanten-Planung ist und damit eine Funktionsaenderung, kein Bindungsumbau."
|
||||
- "Die Fehlerrichtung dieses Bereichs ist GEMESSEN, nicht behauptet: dass eine ungebundene Einzelabfrage auf die Modulkonfiguration nach dem Scharfschalten keine beliebige Zeile mehr liefert, sondern gar keine, haengt an der echten, ausgelieferten Policy und steht als Ausgabezeile im Werkzeug."
|
||||
- "Die Luecke der ldap-Klasse dieses Bereichs ist gefunden und geschlossen: der Download einer Ausfuhrdatei loest heute allein ueber den Dateinamen auf und wirft den uebergebenen Mandanten weg — kuenftig entscheidet ein gebundener Lesezugriff auf die Rechnungshistorie, ob die Datei zu diesem Mandanten gehoert."
|
||||
- "Es existiert ein `dkv`-Abschnitt der Kritikschrift, der je umgestelltem Pfad das konkrete Signal nennt UND die diesem Bereich eigene Fehlerform abdeckt: eine Abfrage, die heute eine beliebige-aber-richtige Zeile liefert und kuenftig `null`, wobei `null` an dieser Stelle als 'das Modul ist nicht eingerichtet' gelesen wird — ein Zustand, den die Oberflaeche als ganz normales leeres Formular zeigt."
|
||||
- "Die Testlage dieses Bereichs ist repariert: vor diesem Durchlauf gab es fuer `dkv` KEINE einzige Testdatei, der Bereich konnte also auf keinen Fehler rot werden. Es existiert jetzt eine Testdatei mit Zwei-Klienten-Nachweis, die rot wird, sobald eine Fundstelle ungebunden bleibt — nachgewiesen ueber einen probeweisen Rueckbau, nicht behauptet."
|
||||
- "Dass dieser Bereich keine mandantengebundene Transaktion enthaelt, ist nachgemessen und damit der im Kopf von `prisma-tenant.extension.ts` verlangte erneute Test fuer diesen Bereich beantwortet; die eine mehrschrittige Stelle (Ersetzen-Modus des Fahrzeug-Imports) bleibt so unatomar wie heute, statt unter dem Deckmantel der Umstellung atomar gemacht zu werden."
|
||||
- "Klassifikationsdokument und `rls-access-inventory.spec.ts` zeigen fuer alle drei Paare des Bereichs denselben, maschinell gemessenen Stand."
|
||||
- "772+ Tests und die Typpruefung sind gruen, das Wegwerf-Werkzeug meldet alle Pruefungen bestanden; Schema, Migrationen, alle Compose-Dateien und beide Beispiel-Umgebungsdateien sind unveraendert; der Schalter bleibt AUS."
|
||||
artifacts:
|
||||
- apps/api/scripts/rls-scratch-check.mjs
|
||||
- docs/mandantentrennung-etappe2-fehlerrichtung.md
|
||||
- apps/api/src/dkv/dkv.service.ts
|
||||
- apps/api/src/dkv/dkv.service.spec.ts
|
||||
- apps/api/src/dkv/dkv-scheduler.service.ts
|
||||
- docs/mandantentrennung-zugriffsklassifikation.md
|
||||
- .planning/WINDOWS.md
|
||||
key_links:
|
||||
- "gebundener Klient <-> die drei Policies `tenant_isolation_policy` auf DkvInvoiceHistory/DkvModuleConfig/DkvVehicleMaster, wortgleich aus der ausgelieferten Migration `20260909140000_rls_remaining_tenant_tables` herausgeschnitten statt im Werkzeug nachgetippt"
|
||||
- "der Planer-Startpfad <-> die eine Abfrage ohne Mandantenbedingung, deren Ergebnis heute beliebig und kuenftig leer ist — die Stelle, an der ein Bindungsumbau zur Funktionsaenderung wuerde, wenn man sie 'mitrepariert'"
|
||||
- "Dateiname der Ausfuhrdatei <-> `DkvInvoiceHistory.exportFilename` — die einzige mandantengebundene Aussage darueber, wem eine Datei im gemeinsamen Ablageverzeichnis gehoert"
|
||||
- "verschluesselte Postfachzugangsdaten in DkvModuleConfig <-> der gebundene Lesezugriff der Verarbeitungsstrecke — der Pfad, der nach dem Scharfschalten aus 'Postfach nicht erreichbar' ein stilles 'kein Postfach eingerichtet' machen wuerde"
|
||||
- "zusammengesetzte Eindeutigkeit (tenantId, kennzeichen) <-> gebundenes `upsert` im Fahrzeug-Import — die Stelle, an der dieser Bereich NICHT die Falle des Bereichs `tenders` hat, was zu messen und nicht zu unterstellen ist"
|
||||
- "`rls-access-inventory.spec.ts` <-> Stand-Spalte des Klassifikationsdokuments fuer alle drei Paare des Bereichs"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Der Bereich `dkv` ist der vierte Bereich der Etappe 2 — und der erste, dessen
|
||||
Hauptschwierigkeit nicht der Umbau ist, sondern eine Entscheidung. 21 Zugriffe auf
|
||||
drei Tabellen, alle in einer einzigen Datei, alle mandantengebunden zu machen: das
|
||||
ist die kleinere Haelfte. Die groessere ist eine bereits dokumentierte Altlast aus
|
||||
07-04 — der Planer dieses Moduls zieht seine Konfiguration ueber eine Abfrage OHNE
|
||||
Mandantenbedingung und bekommt damit eine BELIEBIGE Zeile. Bei einem Mandanten ist
|
||||
die beliebige Zeile immer die richtige, weshalb es nie jemandem auffiel.
|
||||
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/gsd-core/workflows/execute-plan.md
|
||||
@~/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/STATE.md
|
||||
@docs/mandantentrennung-zugriffsklassifikation.md
|
||||
@docs/mandantentrennung-etappe2-fehlerrichtung.md
|
||||
@apps/api/src/prisma/prisma-tenant.extension.ts
|
||||
@apps/api/src/prisma/rls-access-inventory.spec.ts
|
||||
@apps/api/scripts/rls-scratch-check.mjs
|
||||
@apps/api/src/groups/groups.service.spec.ts
|
||||
@apps/api/src/tenders/tender-saved-search.service.ts
|
||||
@apps/api/prisma/migrations/20260909140000_rls_remaining_tenant_tables/migration.sql
|
||||
@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-export.service.ts
|
||||
@CLAUDE.md
|
||||
</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.json` fuehrt `test` (`vitest run`) und `type-check`
|
||||
(`tsc --noEmit`) — die beiden Befehle, auf denen jede Pruefung dieses Plans
|
||||
aufsetzt.
|
||||
- Die Adresse von `tessera-ctl-db-1` ist 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()`: ruft `this.dkvService.loadConfig()`
|
||||
OHNE Mandanten auf und uebernimmt `config.tenantId` als den einen Mandanten, den
|
||||
der eine benannte Cron-Auftrag `dkv-inbox-poll` fortan bedient.
|
||||
- `dkv.service.ts`, `loadConfig(tenantId?)`: ohne Mandant ein `findFirst` ganz ohne
|
||||
Bedingung, mit Mandant ein `findUnique` ueber `tenantId`. EINE Methode, ZWEI
|
||||
gegensaetzliche Bindungsanforderungen hinter einem optionalen Parameter.
|
||||
- Die Auswahl greift auf `CONFIG_SAFE_SELECT` zu, das `encryptedInboxCreds`
|
||||
ausschliesst. 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 `setInterval` GENAU 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: `getAllActiveConfigs` im Bereich `ldap`
|
||||
(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".
|
||||
|
||||
1. `loadConfig()` ohne Mandant, gefolgt von `config?.isActive` im Planer — `null`
|
||||
heisst "kein aktives Modul", der Planer richtet nichts ein und protokolliert das
|
||||
als Normalfall. Kein Fehler, keine Warnung, keine sichtbare Aenderung.
|
||||
2. `_runPipeline`, `if (!config) { warn; return; }` — `null` heisst "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.
|
||||
3. `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.
|
||||
4. `getConfigForApi`, der `try/catch` um die Entschluesselung — faengt heute
|
||||
Entschluesselungsfehler ab und liefert einen leeren Benutzernamen. Nach dem
|
||||
Scharfschalten faellt der Lesezugriff selbst leer aus, `raw` ist `null`, und der
|
||||
Zweig laeuft ohne Fehler durch: `hasPassword` bleibt `false`. Die Oberflaeche
|
||||
meldet "kein Passwort hinterlegt" fuer ein hinterlegtes Passwort.
|
||||
5. `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 einem `try/catch`, das ausdruecklich sagt,
|
||||
dass es Fehler ignoriert und mit dem Uebergebenen ueberschreibt.
|
||||
6. `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.
|
||||
7. `_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>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="tracer">
|
||||
<name>Aufgabe 1: Die Fehlerform dieses Bereichs messen und die Kritikschrift fuer dkv schreiben</name>
|
||||
<precondition>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).</precondition>
|
||||
<files>apps/api/scripts/rls-scratch-check.mjs, docs/mandantentrennung-etappe2-fehlerrichtung.md</files>
|
||||
<action>
|
||||
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 ungebundenes `SELECT ... LIMIT 1` ohne
|
||||
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-mandant` und
|
||||
`dkvvehiclemaster-gebunden-nur-eigener-mandant` — je eine Pruefung nach dem
|
||||
Muster der ersten.
|
||||
- `dkvvehiclemaster-gebundenes-einfuegen-fremder-mandant-abgelehnt` — ein gebundenes
|
||||
INSERT unter TENANT-A mit `tenantId` von 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 Bereichs
|
||||
`tenders`: 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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>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)"</automated>
|
||||
</verify>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Aufgabe 2: Die Testlage herstellen, die Konfigurationspfade binden und den Planer-Pfad benennen statt still lassen</name>
|
||||
<precondition>Aufgabe 1 ist abgeschlossen und uebersetzt; der Container `tessera-ctl-db-1` laeuft (Adresse erneut ermitteln, nicht abschreiben).</precondition>
|
||||
<files>apps/api/src/dkv/dkv.service.spec.ts, apps/api/src/dkv/dkv.service.ts, apps/api/src/dkv/dkv-scheduler.service.ts, .planning/WINDOWS.md</files>
|
||||
<behavior>
|
||||
Der Nachweis kommt VOR der Umstellung, sonst beweist er nichts. Dieser Bereich hatte
|
||||
bisher KEINE einzige Testdatei (Befund J) — der erste Schritt ist deshalb, ueberhaupt
|
||||
eine Stelle zu schaffen, an der etwas rot werden kann.
|
||||
|
||||
`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.extension` wird gemockt, sodass
|
||||
`forTenant(prisma, tenantId)` an `prisma.__makeBoundClient(tenantId)` delegiert.
|
||||
`withTenantTransaction` wird NICHT gemockt und nicht gebraucht — dieser Bereich hat
|
||||
keine Transaktion (Befund C).
|
||||
- Ein handgeschriebener In-Memory-Fake mit Maps fuer `dkvModuleConfig`,
|
||||
`dkvVehicleMaster` und `dkvInvoiceHistory`. `__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 auf `dkvModuleConfig`
|
||||
stehen im Bindungsprotokoll unter genau diesem Mandanten.
|
||||
- Test 2: `getConfigForApi` eines 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.
|
||||
</behavior>
|
||||
<action>
|
||||
`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 aus `forTenant(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 den `dkv`-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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>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)"</automated>
|
||||
</verify>
|
||||
<done>`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.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Aufgabe 3: Fahrzeugstammdaten und Rechnungshistorie binden, die Ausfuhrdatei-Luecke schliessen und beide Dokumente nachziehen</name>
|
||||
<precondition>Aufgabe 2 ist abgeschlossen und uebersetzt; der Container `tessera-ctl-db-1` laeuft (Adresse erneut ermitteln, nicht abschreiben).</precondition>
|
||||
<files>apps/api/src/dkv/dkv.service.ts, apps/api/src/dkv/dkv.service.spec.ts, docs/mandantentrennung-zugriffsklassifikation.md, docs/mandantentrennung-etappe2-fehlerrichtung.md</files>
|
||||
<behavior>
|
||||
Wieder Nachweis vor Umbau, im selben Fake und demselben Bindungsprotokoll aus
|
||||
Aufgabe 2 — der Fake wird erweitert, nicht ersetzt.
|
||||
|
||||
- Test 1: `listVehicles` und `createVehicle` stehen gebunden im Protokoll und liefern
|
||||
bzw. schreiben nur unter dem uebergebenen Mandanten.
|
||||
- Test 2: `updateVehicle` und `deleteVehicle` — 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`/`deleteVehicle` auf 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: `importVehiclesCsv` in 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: `getExportFile` liefert eine Datei, zu der eine Historienzeile DIESES
|
||||
Mandanten mit passendem Dateinamen existiert.
|
||||
- Test 9: `getExportFile` verweigert 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.
|
||||
</behavior>
|
||||
<action>
|
||||
`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 `dkv` der Uebersichtstabelle mit den bei der Ausfuehrung NEU
|
||||
gemessenen Zahlen fortschreiben, im Stil der bereits fortgeschriebenen Zeilen
|
||||
(`war X/0` plus 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: `dkvInvoiceHistory` und `dkvVehicleMaster` gebunden, `dkvModuleConfig`
|
||||
gemischt, jeweils mit einer Begruendung, die bei `dkvModuleConfig` ausdruecklich
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>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)"</automated>
|
||||
</verify>
|
||||
<done>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`.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<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>
|
||||
|
||||
<verification>
|
||||
- Das Wegwerf-Werkzeug meldet nach jeder Aufgabe alle Pruefungen bestanden, mit
|
||||
Rueckgabewert 0, und die 32 vorbestehenden Pruefungen laufen unveraendert mit.
|
||||
- `npm --prefix apps/api run test` ist nach jeder Aufgabe gruen und liegt nach
|
||||
Aufgabe 2 ueber 772 Tests, weil der Bereich zum ersten Mal eine Testdatei hat.
|
||||
- `npm --prefix apps/api run type-check` gibt nach jeder Aufgabe 0 zurueck.
|
||||
- `rls-access-inventory.spec.ts` und die Stand-Spalte des Klassifikationsdokuments
|
||||
stimmen fuer alle drei Paare des Bereichs ueberein.
|
||||
- `git diff --name-only HEAD -- apps/api/prisma docker-compose*.yml .env.example .env.prod.example`
|
||||
ist nach jeder Aufgabe leer: kein Schema, keine Migration, keine Compose-Datei,
|
||||
keine Umgebungsdatei angefasst. `DATABASE_URL` zeigt unveraendert auf die Rolle
|
||||
`tessera` — der Schalter bleibt AUS.
|
||||
- Der Falsifizierungsnachweis ist je Aufgabe (2 und 3) im SUMMARY ausgeschrieben:
|
||||
welche Bindung probeweise zurueckgebaut wurde, welcher Test daraufhin rot wurde,
|
||||
und dass der Rueckbau zurueckgenommen ist.
|
||||
- Im Verzeichnisdienst wurde nichts geaendert; dieser Plan beruehrt Active Directory
|
||||
an keiner Stelle.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Alle 21 Zugriffe des Bereichs `dkv` sind entweder ueber `forTenant()` 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>
|
||||
|
||||
<output>
|
||||
Create `.planning/quick/260909-mir-mandantentrennung-etappe-2-bereich-dkv-a/260909-mir-SUMMARY.md` when done
|
||||
</output>
|
||||
Reference in New Issue
Block a user