feat(quick-260909-mir): dkv-Fehlerform messen und Kritikschrift erweitern
- rls-scratch-check.mjs: runDkvAreaChecks() misst die drei ausgelieferten Policies (DkvInvoiceHistory/DkvModuleConfig/DkvVehicleMaster) wortgleich aus der Migration, plus die Nebenlaeufigkeitsform von getHistory() (Promise.all ueber zwei gebundene Einzelabfragen); alle 9 neuen plus 32 bestehende Pruefungen bestehen (41 gesamt) - Belegt Befund H (kein P2002-Fall, Mandant ist Teil des zusammengesetzten Schluessels) und Befund I (gebundenes INSERT mit fremder tenantId wird ohne eigene WITH-CHECK-Klausel trotzdem abgewiesen) an der echten Datenbank statt am Policy-Text - docs/mandantentrennung-etappe2-fehlerrichtung.md: neuer Abschnitt "Bereich dkv" mit der dritten Fehlerform der Etappe (ein Einzelobjekt wird null, wo null bereits "nicht eingerichtet" bedeutet), der Signaltabelle je umzustellendem Pfad, den sieben Stellen aus Befund K (zerstoerend/lautlos/irrefuehrend) und der ausgeschriebenen Planer-Entscheidung (Form c, mit Unsymmetrie zum ldap-Praezedenzfall) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
@@ -617,6 +617,301 @@ nennen — die Datei selbst steht ohnehin auf der Nicht-Anfassen-Liste, hier
|
||||
nur festgehalten, damit niemand sie beim Nachziehen der Begründungen
|
||||
"freundlich kommentiert" und den Lauf rot macht.
|
||||
|
||||
## Bereich dkv
|
||||
|
||||
Dieser Abschnitt erweitert die Kritikschrift um den Bereich `dkv`
|
||||
(Quick-Task 260909-mir) und beschreibt ihn zum Zeitpunkt seiner Umstellung.
|
||||
Die Leitfrage aus Abschnitt (a) gilt unverändert weiter — dieser Abschnitt
|
||||
beantwortet sie erneut, für einen Bereich mit einer dritten, eigenen
|
||||
Fehlerform: nicht "eine Liste ist leer" (wie bei `ldap`/`groups`) und nicht
|
||||
"ein Benachrichtigungsweg handelt gar nicht" (wie bei `tenders`), sondern
|
||||
"ein einzelnes Objekt wird `null`, und `null` hat an dieser Stelle bereits
|
||||
eine gültige, harmlose Bedeutung". Ein eingerichtetes Modul sieht danach
|
||||
aus wie ein nie eingerichtetes.
|
||||
|
||||
### (d1) Die Messung
|
||||
|
||||
Aufgabe 1 hat `apps/api/scripts/rls-scratch-check.mjs` um einen sechsten
|
||||
Abschnitt (`runDkvAreaChecks`) erweitert, mit den drei Policies für
|
||||
`DkvInvoiceHistory`, `DkvModuleConfig` und `DkvVehicleMaster` (alle aus der
|
||||
ausgelieferten Migration `20260909140000_rls_remaining_tenant_tables`)
|
||||
WORTGLEICH extrahiert, nicht im Werkzeug nachgetippt. Tatsächlich
|
||||
beobachtete Ausgabe dieses Laufs (2026-09-09, gegen `tessera-ctl-db-1`,
|
||||
Adresse `172.19.0.2`):
|
||||
|
||||
```
|
||||
dkvmoduleconfig-gebunden-nur-eigene-zeile: bestanden — forTenant(TENANT-A) liefert 1 Zeile(n): ["TENANT-A"]
|
||||
dkvmoduleconfig-ungebunden-null-zeilen: bestanden — ungebundener SELECT auf "DkvModuleConfig" liefert 0 Zeile(n)
|
||||
dkvmoduleconfig-ungebundene-einzelabfrage-liefert-keine-zeile: bestanden — ungebundenes SELECT ... LIMIT 1 ohne jede Bedingung liefert 0 Zeile(n), obwohl 2 existieren — die Form, die der Planer-Startpfad heute benutzt: aus einer beliebigen-aber-vorhandenen Zeile wird KEINE Zeile, und der aufrufende Code liest das als "dieses Modul ist nicht eingerichtet"
|
||||
dkvinvoicehistory-gebunden-nur-eigener-mandant: bestanden — forTenant(TENANT-A) liefert 1 Zeile(n): ["TENANT-A"]
|
||||
dkvvehiclemaster-gebunden-nur-eigener-mandant: bestanden — forTenant(TENANT-A) liefert 2 Zeile(n): ["TENANT-A","TENANT-A"]
|
||||
dkvvehiclemaster-gebundenes-einfuegen-fremder-mandant-abgelehnt: bestanden — gebundenes INSERT unter TENANT-A mit tenantId=TENANT-B abgewiesen: ERROR: new row violates row-level security policy for table "DkvVehicleMaster"
|
||||
dkvvehiclemaster-gebundenes-update-ueber-kennung-allein-trifft-null-zeilen: bestanden — gebundenes UPDATE unter TENANT-A ueber die Kennung 'veh-b1' (gehoert TENANT-B) allein betrifft 0 Zeile(n) — Folge fuer Aufgabe 3 (Befund G): die vorgeschaltete Besitzpruefung bleibt deshalb erhalten und wird nicht durch die Datenbank ersetzt
|
||||
dkvvehiclemaster-schluessel-traegt-mandant-keine-fremdkollision: bestanden — gebundenes INSERT unter TENANT-A auf das bereits unter TENANT-B vorhandene Kennzeichen 'B-ONLY-1' gelingt — der Mandant ist Teil des zusammengesetzten Schluessels, keine Kollision auf einer unsichtbaren fremden Zeile, keine P2002-Uebersetzung noetig
|
||||
dkv-zwei-parallele-gebundene-einzelabfragen-je-eigener-kontext: bestanden — TENANT-A: pid=286680, t="TENANT-A", rows=1; TENANT-B: pid=286679, t="TENANT-B", rows=1 — Nebenlaeufigkeitsform von getHistory(): zwei ueber Promise.all gleichzeitig gestartete gebundene Einzelabfragen ueber denselben Klienten, jede unter ihrem eigenen Kontext
|
||||
Alle 41 Pruefungen bestanden.
|
||||
```
|
||||
|
||||
Die Belegzeile, die diesen Abschnitt der Kritikschrift trägt, ist
|
||||
`dkvmoduleconfig-ungebunden-null-zeilen`: der IDENTISCHE `SELECT "tenantId"
|
||||
FROM "DkvModuleConfig"` ohne vorheriges `set_config` liefert **0 Zeilen**,
|
||||
nicht etwa die 2 tatsächlich vorhandenen — an der echten, ausgelieferten
|
||||
Policy gemessen, nicht an einer im Werkzeug nachgebauten Hilfstabelle.
|
||||
|
||||
Daneben trägt `dkvmoduleconfig-ungebundene-einzelabfrage-liefert-keine-zeile`
|
||||
diesen Abschnitt zusätzlich, weil dieser Bereich an der entscheidenden
|
||||
Stelle kein Mengenergebnis liest, sondern ein Einzelobjekt: dieselbe
|
||||
Tabelle, dasselbe fehlende `set_config`, aber diesmal ein `SELECT ...
|
||||
LIMIT 1` ohne jede Bedingung — exakt die Form, die `loadConfig()` ohne
|
||||
Mandant (der Planer-Startpfad) heute über `findFirst()` benutzt. Auch
|
||||
diese Abfrage liefert **0 Zeilen**, obwohl 2 existieren. Der Unterschied
|
||||
zur ersten Belegzeile ist nicht die Zahl (beide sind 0), sondern die
|
||||
Lesart: ein leeres `findMany`-Ergebnis ist im aufrufenden Code sichtbar
|
||||
leer, ein leeres `findFirst`-Ergebnis wird zu `null`, und `null` hat in
|
||||
`loadConfig()`/`onModuleInit()` bereits eine gültige, harmlose Bedeutung
|
||||
("kein aktives Modul konfiguriert") — siehe (d3).
|
||||
|
||||
TEIL 2 hat zusätzlich die Nebenläufigkeitsform gemessen, auf die sich
|
||||
`getHistory()` stützt (Befund C): zwei über `Promise.all` gleichzeitig
|
||||
gestartete gebundene Einzelabfragen über DENSELBEN Klienten, hier für zwei
|
||||
verschiedene Mandanten nachgebaut
|
||||
(`dkv-zwei-parallele-gebundene-einzelabfragen-je-eigener-kontext`). Jede
|
||||
Abfrage sah den Kontext, unter dem sie gestartet wurde (`TENANT-A`/
|
||||
`TENANT-B`), und jede lieferte die richtige Zeilenzahl (je 1) — keine
|
||||
Verletzung, kein Abbruch.
|
||||
|
||||
TEIL 3 hat nachgemessen, dass dieser Bereich keine mandantengebundene
|
||||
Transaktion enthält (Befund C):
|
||||
`grep -rn '\$transaction(' apps/api/src/dkv --include=*.ts | grep -v spec`
|
||||
liefert **null Treffer** (Rückgabewert 1, keine Ausgabe). Der im Kopf von
|
||||
`prisma-tenant.extension.ts` verlangte erneute Test vor jedem neuen
|
||||
`forTenant()`-Fall mit eigener Transaktion ist damit für diesen Bereich
|
||||
beantwortet: es fällt kein neuer Fall an, `withTenantTransaction()` wird
|
||||
hier nicht gebraucht und in Aufgabe 2/3 nicht eingeführt. Die beiden
|
||||
mehrschrittigen Stellen des Bereichs — der Ersetzen-Modus des
|
||||
Fahrzeug-Imports (`deleteMany` gefolgt von `createMany`) und die
|
||||
Zugangsdaten-Erhaltung in `saveConfig` (lesen, entschlüsseln, neu
|
||||
verschlüsseln, schreiben) — bleiben deshalb so unatomar wie heute; sie in
|
||||
eine Transaktion zu heben wäre eine Verhaltensänderung jenseits dieses
|
||||
Auftrags, siehe (d5).
|
||||
|
||||
Zwei Messungen dieses Laufs tragen eine Entscheidung, die kein bisheriger
|
||||
Bereich in dieser Form brauchte:
|
||||
|
||||
- `dkvvehiclemaster-gebundenes-einfuegen-fremder-mandant-abgelehnt`
|
||||
(Befund I) — dieselbe Frage wie bei `TenderRssFeedSource` in `tenders`,
|
||||
hier mit demselben Ergebnis: die ausgelieferten Policies dieses Bereichs
|
||||
tragen keine eigene `WITH CHECK`-Klausel, also verwendet PostgreSQL
|
||||
denselben `USING`-Ausdruck auch für neu geschriebene Zeilen — ein
|
||||
gebundenes `INSERT` unter TENANT-A mit `tenantId = TENANT-B` wird
|
||||
abgewiesen. Was PostgreSQL daraus für ein `INSERT` ableitet, ist eine
|
||||
Eigenschaft der Datenbank, keine des Policy-Textes, deshalb gemessen und
|
||||
nicht aus dem Text geschlossen.
|
||||
- `dkvvehiclemaster-schluessel-traegt-mandant-keine-fremdkollision`
|
||||
(Befund H) — der Gegenbefund zu `tenders`-Befund F: weil
|
||||
`DkvVehicleMaster` die zusammengesetzte Eindeutigkeit `@@unique([tenantId,
|
||||
kennzeichen])` trägt, gibt es hier KEINE Kollision auf einer unsichtbaren
|
||||
fremden Zeile. Ein gebundenes `INSERT` unter TENANT-A auf ein
|
||||
Kennzeichen, das unter TENANT-B bereits existiert, GELINGT — das
|
||||
bestandene Ergebnis ist das Gelingen, nicht die Abweisung. Aufgabe 2/3
|
||||
bauen deshalb keine P2002-Übersetzung für diesen Bereich; anders als bei
|
||||
`TenderTriage` in `tenders` ist hier keine gebaut, weil keine gebraucht
|
||||
wird — nachgemessen statt unterstellt.
|
||||
|
||||
### (d2) Signaltabelle je umgestelltem Pfad
|
||||
|
||||
| Pfad | Verhalten bei zu wenig Ergebnis | Konkretes Signal |
|
||||
|---|---|---|
|
||||
| `DkvService.getConfigForApi` | Der gebundene erste Lesezugriff (`loadConfig(tenantId)`) liefert `null` statt der eigenen Konfiguration | `GET /dkv/config` liefert `404 DKV module not yet configured`; die Oberfläche zeigt das Einrichtungsformular für ein Modul, das tatsächlich eingerichtet ist |
|
||||
| `DkvService.getConfigForApi`, der zweite (rohe) Lesezugriff auf die Zugangsdaten | Läuft dieser gebundene `findUnique` leer (während der erste — sicher ausgewählte — noch träfe), bleibt `raw` `null`, der `try`-Block liefert `hasPassword=false` | Die Oberfläche meldet "kein Passwort hinterlegt" für ein Modul mit tatsächlich hinterlegtem Passwort — ohne Fehlermeldung, siehe (d3) Stelle 4 |
|
||||
| `DkvService.saveConfig`, die Zugangsdaten-Erhaltung | Der gebundene erhaltende Lesezugriff liefert `null` statt der bestehenden Zeile, der `try/catch` schluckt das | Ein gespeichertes Passwort wird mit dem LEEREN Wert neu verschlüsselt — die zerstoerende Stelle, siehe (d3) Stelle 5 und T-MIR-07 |
|
||||
| `DkvService.testConnection`, der Rückgriff auf gespeicherte Zugangsdaten | Der gebundene Lesezugriff liefert `null` statt der bestehenden Zeile | Der Verbindungstest schlägt mit einem Anmeldefehler des Postfachs fehl — die Meldung zeigt auf das Postfach, nicht auf die Datenbank |
|
||||
| `DkvService._runPipeline`, der Konfigurations-Lesezugriff | Der gebundene `findUnique` liefert `null` statt der bestehenden Konfiguration | `_runPipeline` protokolliert `no config for tenant ...` als Warnung und `return`et — die Rechnungsverarbeitung stellt für diesen Mandanten die Arbeit ein, ohne Fehlermeldung |
|
||||
| `DkvService.listVehicles`/`createVehicle` | Der gebundene Zugriff liefert 0 Zeilen statt der tatsächlich vorhandenen bzw. schreibt nicht | Die Fahrzeugliste ist leer für einen Mandanten mit tatsächlich vorhandenen Fahrzeugen |
|
||||
| `DkvService.updateVehicle`/`deleteVehicle`, die Besitzprüfung | Der gebundene `findFirst` liefert `null` statt der eigenen Zeile | `PUT`/`DELETE /dkv/vehicles/:id` scheitert mit der vorhandenen `NotFoundException`, obwohl das Fahrzeug existiert — siehe (d2)-Zeile zum neuen Riegel unten für die spiegelbildliche Fehlerform |
|
||||
| `DkvService.importVehiclesCsv` (Ersetzen-Modus) | Der gebundene `deleteMany` löscht 0 Zeilen statt der tatsächlich vorhandenen (harmlos: dann bleiben Alt-Fahrzeuge stehen, `createMany` legt zusätzlich an) | Nach einem Ersetzen-Import bestehen alte UND neue Fahrzeugzeilen nebeneinander — kein Datenverlust, aber ein Zustand, der als "Ersetzen" nicht mehr stimmt |
|
||||
| `DkvService.getHistory` | Beide gebundenen Parallelabfragen liefern 0 Zeilen bzw. Zählung 0 statt der tatsächlich vorhandenen | Die Rechnungshistorie-Tabelle ist leer für einen Mandanten mit tatsächlich vorhandener Historie |
|
||||
| `DkvService._buildExportRows`, der gebündelte Lesezugriff auf die Fahrzeugstammdaten | Der gebundene `findMany` liefert 0 Zeilen statt der tatsächlich vorhandenen | Eine vollständige Ausfuhrdatei OHNE einen einzigen Fahrer entsteht — kein Fehler, keine Warnung, eine Datei, die plausibel aussieht und falsch ist, siehe (d3) Stelle 7 |
|
||||
| `DkvService.getExportFile`, neuer Riegel (Aufgabe 3, Befund E) | Der gebundene Lesezugriff auf `DkvInvoiceHistory.exportFilename` liefert keinen Treffer, obwohl die Datei existiert und das Namensmuster besteht | `GET /dkv/exports/:filename` liefert `404`, obwohl die Datei auf der Platte liegt — die Absicht der Umstellung: Fehlen und Fremdbesitz kollabieren bewusst zur selben Antwort |
|
||||
| `loadConfig(tenantId)` ohne Mandant (bewusst ungebunden, Planer-Startpfad) | Betrifft nicht die Bindung selbst — die Methode bindet niemals. Nach dem Scharfschalten liefert dieselbe Abfrage `null` statt einer beliebigen Zeile | `onModuleInit()` protokolliert `DKV scheduler: no active config found — cron job not registered` und richtet für JEDEN Mandanten nichts ein — siehe (d4) |
|
||||
|
||||
### (d3) Welcher Code Leere als Abwesenheit deutet
|
||||
|
||||
Die Form, die diesen Bereich von `ldap`, `groups` und `tenders`
|
||||
unterscheidet: nicht "eine Liste ist leer" und nicht "ein
|
||||
Benachrichtigungsweg handelt gar nicht", sondern "ein einzelnes Objekt
|
||||
wird `null`, und `null` hat an dieser Stelle bereits eine gültige,
|
||||
harmlose Bedeutung". Ein eingerichtetes Modul sieht danach aus wie ein nie
|
||||
eingerichtetes: ein leeres Formular, eine unauffällige Protokollzeile,
|
||||
kein Alarm.
|
||||
|
||||
**Zerstörend (eine Stelle, der gefährlichste Punkt des Bereichs):**
|
||||
|
||||
5. `DkvService.saveConfig`, die Erhaltung der nicht ausgefüllten
|
||||
Zugangsdaten — liest die bestehende Zeile, um Benutzername oder
|
||||
Passwort zu übernehmen, wenn das Formularfeld leer gelassen wurde.
|
||||
Läuft dieser Lesezugriff nach dem Scharfschalten leer (weil ungebunden
|
||||
oder unter falschem Kontext gebunden), wird das Feld mit dem LEEREN
|
||||
Wert neu verschlüsselt: aus einem gespeicherten Passwort wird ein
|
||||
leeres. Die Stelle liegt hinter einem `try/catch`, das ausdrücklich
|
||||
sagt, dass es Fehler ignoriert und mit dem Übergebenen überschreibt —
|
||||
T-MIR-07 im Bedrohungsregister dieses Plans. Aufgabe 2 bindet Lese- UND
|
||||
Schreibzugriff dieser Methode gemeinsam an denselben Mandanten, sodass
|
||||
ein leerer Lesezugriff nicht mit einem erfolgreichen Schreibzugriff
|
||||
unter einem anderen Kontext kombiniert werden kann.
|
||||
|
||||
**Lautlos (drei Stellen, die Rechnungsverarbeitung bekommt eigenen
|
||||
Raum):**
|
||||
|
||||
1. `DkvSchedulerService.onModuleInit()`, gefolgt von
|
||||
`config?.isActive && config.tenantId` — `null` heißt "kein aktives
|
||||
Modul konfiguriert", der Planer richtet nichts ein und protokolliert
|
||||
das als Normalfall (`DKV scheduler: no active config found — cron job
|
||||
not registered`). Kein Fehler, keine Warnung, keine sichtbare
|
||||
Änderung — siehe (d4) für die volle Begründung, warum dieser Pfad
|
||||
bewusst ungebunden bleibt.
|
||||
2. `DkvService._runPipeline`, `if (!config) { warn; return; }` — `null`
|
||||
heißt "dieser Mandant hat DKV nicht eingerichtet". Die
|
||||
Rechnungsverarbeitung stellt die Arbeit ein: Rechnungen laufen im
|
||||
Postfach weiter auf, es entsteht keine Historienzeile, keine
|
||||
Ausfuhrdatei, kein Versand — und keine Fehlermeldung. Anders als beim
|
||||
Planer-Startpfad ist dieser Lesezugriff in Aufgabe 2 vollständig
|
||||
gebunden; die Stelle bleibt hier festgehalten, weil sie die Folge einer
|
||||
still verschwundenen Konfiguration ist, nicht weil sie ungebunden
|
||||
bliebe.
|
||||
7. `DkvService._buildExportRows`, der gebündelte Lesezugriff auf die
|
||||
Fahrzeugstammdaten — ein fehlender Treffer je Kennzeichen ist nach D-13
|
||||
bereits ein GÜLTIGER Zustand (unbekanntes Kennzeichen, leeres
|
||||
Fahrerfeld). Läuft der Lesezugriff selbst ganz leer (0 Fahrzeuge statt
|
||||
der tatsächlich vorhandenen), entsteht eine vollständige 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 genügt, nur die Anzeigepfade zu binden.
|
||||
|
||||
**Irreführend (drei Stellen, die auf die falsche Ursache zeigen oder eine
|
||||
falsche Vergangenheit nahelegen):**
|
||||
|
||||
3. `DkvService.getConfigForApi`, `if (!safe) return null` — die
|
||||
Oberfläche zeigt daraufhin ein leeres Einrichtungsformular. Ein
|
||||
Administrator sieht "noch nicht eingerichtet" für ein Modul, das
|
||||
eingerichtet IST — und würde beim Neu-Ausfüllen die vorhandenen
|
||||
Zugangsdaten überschreiben. Diese Stelle bleibt bewusst unter
|
||||
"irreführend" und nicht unter "zerstörend": die Zerstörung selbst
|
||||
passiert erst in `saveConfig` (Stelle 5), falls der Administrator
|
||||
tatsächlich neu ausfüllt und speichert — hier liegt nur die
|
||||
irreführende Voraussetzung dafür.
|
||||
4. `DkvService.getConfigForApi`, der `try/catch` um die Entschlüsselung —
|
||||
fängt heute Entschlüsselungsfehler ab und liefert einen leeren
|
||||
Benutzernamen. Nach dem Scharfschalten fällt der Lesezugriff selbst
|
||||
leer aus, `raw` ist `null`, und der Zweig läuft ohne Fehler durch:
|
||||
`hasPassword` bleibt `false`. Die Oberfläche meldet "kein Passwort
|
||||
hinterlegt" für ein hinterlegtes Passwort.
|
||||
6. `DkvService.testConnection`, der Rückgriff auf das gespeicherte
|
||||
Passwort — läuft leer, der Test schlägt mit einem Anmeldefehler des
|
||||
Postfachs fehl. Harmlos in der Richtung, aber irreführend: die Meldung
|
||||
zeigt auf das Postfach, nicht auf die Datenbank.
|
||||
|
||||
**Gegenrichtung, ebenfalls nachgesehen statt geschlossen behauptet:** die
|
||||
laut werfenden Stellen sind `updateVehicle`/`deleteVehicle`
|
||||
(`NotFoundException` bei Leere), `importVehiclesCsv` (wirft bei leerem CSV)
|
||||
und `getExportFile` (wirft bei fehlender Datei, jetzt zusätzlich bei
|
||||
fehlendem gebundenen Historientreffer, Aufgabe 3). Diese Stellen sind
|
||||
harmlos, weil ein zu kleines Ergebnis dort bereits heute einen Fehler
|
||||
auslöst, der nicht mit dem Scharfschalten neu entsteht.
|
||||
|
||||
### (d4) Was dieser Durchlauf bewusst nicht löst
|
||||
|
||||
**Der Planer-Startpfad — die eigentliche Aufgabe dieses Plans, ausgeschrieben statt still getroffen.**
|
||||
`DkvSchedulerService.onModuleInit()` ruft `DkvService.loadConfig()` ohne
|
||||
Mandant auf und übernimmt `config.tenantId` als den einen Mandanten, den
|
||||
der eine benannte Cron-Auftrag `dkv-inbox-poll` fortan bedient
|
||||
(`this.dkvService.loadConfig()` → `findFirst()` ganz ohne Bedingung). Zwei
|
||||
Zustände, beide gehören benannt, sonst liest sich die Markierung wie eine
|
||||
Entwarnung:
|
||||
|
||||
- **Heute** ist die Abfrage bereits FALSCH, nicht nur ungenau: bei mehreren
|
||||
Mandanten bedient sie einen BELIEBIGEN und die übrigen NIE. Die Prüfung
|
||||
`config?.isActive && config.tenantId` verschärft das — ist ausgerechnet
|
||||
die gezogene beliebige Zeile inaktiv, registriert der Planer gar nichts,
|
||||
obwohl ein zweiter Mandant aktiv wäre.
|
||||
- **Nach dem Scharfschalten** verstummt sie zusätzlich: dieselbe Abfrage
|
||||
liefert `null`, der Planer protokolliert `DKV scheduler: no active
|
||||
config found — cron job not registered` und richtet für JEDEN Mandanten
|
||||
nichts ein — eine Zeile, die auf einer frischen Installation der
|
||||
Normalfall ist und deshalb niemanden alarmiert.
|
||||
|
||||
Von den drei im Auftrag genannten Formen wurde geprüft:
|
||||
|
||||
- **(a) An einen konkret aufgelösten Mandanten binden** — nicht möglich.
|
||||
`onModuleInit()` hat keine Anfrage, keinen Sitzungsnachweis und keinen
|
||||
Konfigurationswert, aus dem ein Mandant käme. Einen einzuführen wäre eine
|
||||
neue Einstellung, also eine Funktionsänderung.
|
||||
- **(b) Umbau auf einmal-abfragen-viele-bedienen** — abgelehnt, mit
|
||||
Begründung. Das ist genau die Mehrmandanten-Planung, die 07-04
|
||||
zurückgestellt hat: alle aktiven Konfigurationen lesen, je Mandant einen
|
||||
Auftrag führen, deren Lebenszyklus bei jeder Konfigurationsänderung
|
||||
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.
|
||||
- **(c) Als benannte Altlast weiterführen, mit Markierung** — GEWÄHLT. Der
|
||||
unmittelbare Präzedenzfall ist `getAllActiveConfigs` im Bereich `ldap`
|
||||
(260909-ipc, Befund B): ein bewusst übergreifender Planer-Lesezugriff,
|
||||
der ungebunden bleibt, einen eigenen Kopfkommentar trägt, und dessen
|
||||
Verstummen nach dem Scharfschalten an die Vorabprüfung von Etappe 4
|
||||
übergeben wird.
|
||||
|
||||
**Die Unsymmetrie, die dieser Präzedenzfall NICHT deckt:**
|
||||
`getAllActiveConfigs` ist HEUTE korrekt und verstummt erst später. Der
|
||||
DKV-Planer ist HEUTE bereits falsch — er bedient bei mehreren Mandanten
|
||||
einen beliebigen und die übrigen nie — und verstummt zusätzlich später.
|
||||
Die Markierung in Aufgabe 2 sagt beides, sonst läse sie sich wie eine
|
||||
Entwarnung. Die gewählte Form hat drei Teile, alle umgesetzt: die
|
||||
übergreifende Abfrage ist eine EIGENE, benannte Methode (kein Zweig hinter
|
||||
einem optionalen Parameter), sie und der Planer tragen einen Kopfkommentar,
|
||||
der beide Zustände benennt, und die Altlast steht als offener Eintrag im
|
||||
Broken-Windows-Register (siehe SUMMARY dieses Plans für die genaue
|
||||
Eintragskennung). Das Signal für das Verstummen gehört in die
|
||||
Vorabprüfung von Etappe 4 (`apps/api/scripts/rls-preflight.mjs`), NICHT in
|
||||
diesen Durchlauf.
|
||||
|
||||
**Die gemeinsame Ablage der Ausfuhrdateien samt Verdrängung über
|
||||
Mandantengrenzen (Befund F).** `DkvExportService.writeAndPrune` behält die
|
||||
letzten zehn Dateien des GEMEINSAMEN Verzeichnisses `user-files/`.
|
||||
Verarbeitet ein Mandant zehn Rechnungen, verdrängt 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 ändern (Unterverzeichnisse je Mandant, Umzug der
|
||||
Bestandsdateien) ist ein eigener Auftrag. Siehe auch (d5) und T-MIR-08.
|
||||
|
||||
**Die Übergaben in die noch nicht umgestellten Bereiche.**
|
||||
`dkv-mail.service.ts` hängt an `SettingsService.getDecryptedSmtpConfig`
|
||||
(`settings.service.ts` ist noch vollständig unbound, siehe
|
||||
`docs/mandantentrennung-zugriffsklassifikation.md`) — dieselbe
|
||||
Reihenfolgebedingung, die der `tenders`-Durchlauf für dieselbe Abhängigkeit
|
||||
als Befund K festhielt. `dkv.seed.ts` hängt an `module-registry`, ebenfalls
|
||||
noch unbound. Nach dem Scharfschalten fände die ungebundene SMTP-Abfrage
|
||||
keine Zeile mehr — Ergebnis: kein Versand für niemanden, mit Wiederholung
|
||||
bei jedem Lauf (die Datei bleibt lokal verfügbar, D-16). Reihenfolgebedingung
|
||||
für Etappe 4, hier festgehalten, nicht gelöst.
|
||||
|
||||
**Die offene Architekturfrage `req.tenantPrisma`.** Auch der Bereich `dkv`
|
||||
entscheidet sie nicht — er bindet dienst-intern, wie `ldap`, `groups` und
|
||||
`tenders` es vormachen.
|
||||
|
||||
### (d5) Was dieser Durchlauf bewusst nicht anfasst
|
||||
|
||||
- **Die beiden mehrschrittigen Stellen bleiben unatomar (Befund C, TEIL
|
||||
3).** Der Ersetzen-Modus des Fahrzeug-Imports (`deleteMany` gefolgt von
|
||||
`createMany`) und die Zugangsdaten-Erhaltung in `saveConfig` (lesen,
|
||||
entschlüsseln, neu verschlüsseln, schreiben) werden NICHT in eine
|
||||
Transaktion gehoben — geprüft und bewusst gelassen, nicht übersehen. Der
|
||||
im Kopf von `prisma-tenant.extension.ts` verlangte erneute Test ist mit
|
||||
TEIL 3 dieser Aufgabe beantwortet: kein neuer Transaktionsfall,
|
||||
`withTenantTransaction()` bleibt für diesen Bereich ungenutzt.
|
||||
- **Die Ablagestruktur der Ausfuhrdateien bleibt unverändert (Befund F).**
|
||||
Geprüft und bewusst gelassen — eine Lösung (Unterverzeichnisse je
|
||||
Mandant, Umzug der Bestandsdateien) ist ein eigener Auftrag, siehe (d4).
|
||||
|
||||
## Verweis
|
||||
|
||||
Die Bestandsaufnahme, welche Fundstelle den hier beschriebenen Übergang
|
||||
|
||||
Reference in New Issue
Block a user