feat(quick-260910-das): Steuerungsschicht binden, Selbstloesch-Riegel schliessen

- user.controller.ts: alle sieben Zugriffe binden. ADMIN-Zweig der
  Benutzerliste laeuft ueber forTenant() mit weiterhin bestehender
  Mandantenbedingung im where; SUPER_ADMIN-Zweig ueber die neue
  UserService.findAllForPlatformAdmin(). Die drei Wege ueber die Kennung
  loesen den Zielbenutzer rollenabhaengig ueber resolveTargetUser() auf
  (ADMIN gebunden an eigenen Mandanten, SUPER_ADMIN uebergreifend); der
  Schreibzugriff bei update/delete bindet an den Mandanten des
  Zielbenutzers, nicht des Aufrufers, damit die uebergreifende
  Verwaltung durch die oberste Rolle erhalten bleibt
- Selbstloesch-Riegel (Befund H) repariert: verglich bisher gegen
  currentUser.sub, ein Feld, das der Sitzungsnachweis nicht traegt --
  der Riegel griff nie. Jetzt gegen currentUser.id. Verhaltensaenderung:
  ein Administrator kann sein eigenes Konto nun nicht mehr loeschen
- Alle fuenf Selbstbedienungszugriffe (Bild hochladen/loeschen/
  ausliefern, Akzentfarbe) binden an die Mandantenkennung aus dem
  Sitzungsnachweis
- user.controller.spec.ts (neu): Zwei-Klienten-Nachweis fuer die
  vorher testlose Steuerungsschicht, 8 Testfaelle, Falsifizierungsnachweis
  fuer eine gebundene Stelle sowie Rot-vor-Reparatur-Nachweis fuer den
  Selbstloesch-Riegel (siehe SUMMARY)
- docs/mandantentrennung-zugriffsklassifikation.md: alle vier
  handgepflegten Stellen nachgezogen (Uebersichtszeile 8/14, Summenzeile
  118/124, Klassen-Verteilung 63 Paare, Hintergrunddienst-Abschnitt auf
  fuenf Faelle inkl. admin-seed.service.ts als erster beidseitig
  korrekter Fall) sowie zwei Klassenkorrekturen (user.service.ts/user
  und admin-seed.service.ts/user je auf "beides")
- docs/mandantentrennung-etappe2-fehlerrichtung.md: Nachtrag zum
  user-Abschnitt mit den tatsaechlich umgesetzten Pfaden, der
  geschlossenen Luecke und den Falsifizierungsnachweisen
- 810 Tests gruen (8 neue in user.controller.spec.ts), Typpruefung
  sauber, Wegwerf-Werkzeug meldet weiterhin alle 53 Pruefungen bestanden

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
2026-09-10 10:35:43 +02:00
parent 888f66003c
commit 3a9391d9c8
4 changed files with 468 additions and 54 deletions
@@ -100,7 +100,7 @@ autoritative Quelle.
| groups | 0 | 31 | **war 37/0** — Aufgabe 2/3 (260909-jts) haben `groups.service.ts` (12 Methoden) und `module-grants.service.ts` (5 Methoden) vollständig auf `forTenant()`/`withTenantTransaction()` umgestellt. Die neun zusätzlichen, über `tx` gebundenen Zugriffe innerhalb der drei Transaktionen zählt dieses einfache Muster nicht mit (siehe Methodenhinweis oben) |
| ldap | 4 | 26 | **war 21/0** — Aufgabe 2/3 (260909-ipc) haben `ldap-config.service.ts` (5 Methoden) und `ldap.service.ts` (6 Methoden, 11 Abfragen) auf `forTenant()` umgestellt. Die 4 verbleibenden ungebundenen Treffer sind bewusst: `getAllActiveConfigs`/`onApplicationBootstrap` (Befund B) und `resolveEmailForWrite` (Befund A, T-IPC-04) |
| dkv | 1 | 22 | **war 21/0** — Aufgabe 2/3 (260909-mir) haben `dkv.service.ts` vollständig auf `forTenant()` umgestellt: Konfigurationspfade (`loadConfig`, `getConfigForApi`, `saveConfig`, `testConnection`), Historie, Fahrzeugstammdaten und der neue Besitzriegel vor dem Ausfuhrdatei-Download. Gebunden sind es 22 statt 21, weil der Riegel einen zusätzlichen Lesezugriff auf `dkvInvoiceHistory` einführt (T-MIR-03). Der eine verbleibende ungebundene Treffer ist der benannte Planer-Startpfad `loadAnyActiveConfigForScheduler()` (Befund D, WINDOWS #21) — bewusst, mit dreifacher Markierung |
| user | 17 | 0 | unverändert |
| user | 8 | 14 | **war 17/0** — Aufgabe 2/3 (260910-das) haben `user.service.ts` (`findById`/`create`/`update`/`deactivate`/`delete` sowie die zwei neuen Plattform-Administratorsicht-Methoden), `admin-seed.service.ts` (Erstanlage des Administrators) und `user.controller.ts` (Benutzerliste des ADMIN-Zweigs, alle drei Kennungswege ueber die Dienstmethoden, alle fuenf Selbstbedienungszugriffe) auf `forTenant()` umgestellt. Die 8 verbleibenden ungebundenen Rohtreffer sind bewusst: `findByUsername` in `user.service.ts` (plattformweit eindeutiger Schluessel, derselbe Fall wie `resolveEmailForWrite` im Bereich `ldap`), die Erstanlage-Pruefung und beide Zugriffe auf `tenant` in `admin-seed.service.ts`, sowie der neue Schleifentreiber `this.prisma.tenant.findMany` der beiden Plattform-Administratorsicht-Methoden in `user.service.ts` (`Tenant` traegt keinen Zeilenschutz) |
| module-registry | 17 | 0 | unverändert |
| dashboard | 13 | 0 | unverändert |
| auth | 8 | 5 | unverändert gegenüber dem in Etappe 1 (260909-eor) gemessenen Stand |
@@ -108,9 +108,9 @@ autoritative Quelle.
| tenant | 8 | 0 | unverändert |
| favorites | 7 | 0 | unverändert |
| settings | 4 | 0 | unverändert |
| **Summe** | **127** | **110** | Ungebunden: war 147 nach 260909-laa, Delta = die 20 in Aufgabe 2/3 (260909-mir) umgestellten `dkv`-Rohtreffer. Gebunden: war 88, jetzt zusätzlich 22 in `dkv` (20 umgestellte plus 2 neue Zugriffe des Besitzriegels). Quergemessen beim Abschluss von 260909-mir: ein roher `grep` über `apps/api/src` zählt 126 statt 127 ungebundene Treffer — die Differenz stammt aus einer geringfügig anderen Ausschlussregel für Testdateien, nicht aus einer offenen Fundstelle. Diese Übersicht ist eine Buchführungshilfe; **autoritativ ist die Fundstellentabelle unten**, die `rls-access-inventory.spec.ts` bei jedem Lauf gegen den Quelltext prüft |
| **Summe** | **118** | **124** | Ungebunden: war 127 nach 260909-mir, Delta = die 9 in Aufgabe 2/3 (260910-das) gesunkenen `user`-Rohtreffer (17→8). Gebunden: war 110, jetzt zusätzlich 14 in `user`. Diese Übersicht ist eine Buchführungshilfe; **autoritativ ist die Fundstellentabelle unten**, die `rls-access-inventory.spec.ts` bei jedem Lauf gegen den Quelltext prüft |
## Klassen-Verteilung (nach (Datei, Modell)-Fundstellen, 62 Paare)
## Klassen-Verteilung (nach (Datei, Modell)-Fundstellen, 63 Paare)
Stand 260909-jts (Aufgabe 3): 61 Paare aus dem vorherigen Durchlauf
(260909-ipc) plus ein bisher vollstaendig unsichtbares Paar
@@ -131,21 +131,37 @@ hinzugekommen oder verschwunden — nur EINE Klasse hat sich verschoben:
`listForUser`/`createPlatform`/`remove` bleiben bewusst uebergreifend), der
exakte Praezedenzfall aus `ldapConfig` in 260909-ipc.
**Stand 260910-das (Aufgabe 3):** 63 Paare — 62 aus dem vorherigen
Durchlauf plus EIN neues Paar (`user.service.ts`/`tenant`, Klasse
`keine-mandantengebundene-tabelle`, Stand `ungebunden`): der Schleifentreiber
der neu eingefuehrten Plattform-Administratorsicht (Befund F/N, Aufgabe 1/2).
Zusaetzlich ZWEI Klassenkorrekturen, jede mit eigener Begruendung an der
Fundstelle in der Bestandsaufnahme oben: `user.service.ts`/`user` wechselt
von `muss-mandantengebunden` auf `beides` — wortgleich derselbe
Praezedenzfall wie `ldap.service.ts`/`user` in 260909-ipc
(`resolveEmailForWrite`, hier `findByUsername`); `admin-seed.service.ts`/`user`
wechselt von `bewusst-uebergreifend` auf `beides`, weil die bisherige
Begruendung nachweislich falsch war (Befund J: der Mandant ist bei der
Erstanlage des Administrators bereits bekannt, nicht strukturell fehlend).
Alle drei Zahlen sind der Ausgabe von `rls-access-inventory.spec.ts`
entnommen, nicht geschaetzt.
| Klasse | Anzahl Paare |
|---|---|
| muss-mandantengebunden | 32 |
| keine-mandantengebundene-tabelle | 16 |
| beides | 11 |
| bewusst-uebergreifend | 3 |
| **Summe** | **62** |
| muss-mandantengebunden | 31 |
| keine-mandantengebundene-tabelle | 17 |
| beides | 13 |
| bewusst-uebergreifend | 2 |
| **Summe** | **63** |
## Der Hintergrunddienst als Falle — vier Fälle
## Der Hintergrunddienst als Falle — fünf Fälle
Ein Planer, der über alle Mandanten iteriert, liest zu Recht übergreifend —
muss aber *innerhalb* der Schleife je Mandant binden. Drei Dateien sind in
diesem Sinne `beides`-Fälle; der vierte, seit 260909-mir bekannte Fall ist von
anderer Art und deshalb unten getrennt aufgeführt — er iteriert gar nicht,
sondern greift sich eine beliebige Zeile heraus:
muss aber *innerhalb* der Schleife je Mandant binden. Vier Dateien sind in
diesem Sinne `beides`-Fälle, davon einer (260910-das) der bislang EINZIGE,
der auf BEIDEN Hälften bereits richtig ist; der fünfte, seit 260909-mir
bekannte Fall ist von anderer Art und deshalb unten getrennt aufgeführt — er
iteriert gar nicht, sondern greift sich eine beliebige Zeile heraus:
- **`ldap.service.ts`** (AD-Abgleich) — **Stand 260909-ipc, Aufgaben 2/3:
geschlossen.** Iteriert nicht selbst über alle Mandanten (der Sync läuft
@@ -194,8 +210,24 @@ sondern greift sich eine beliebige Zeile heraus:
`tenderMatch.findMany`/`updateMany` sowie `user.findUnique` über
`forTenant()`, EIN gebundener Client je Profil, gebunden an dessen
Mandanten.
- **`admin-seed.service.ts`** (`ensureDefaultGroupsForAllTenants`) — **Stand
260910-das, Aufgabe 2: der vierte Fall dieser Art und der bislang EINZIGE,
der auf BEIDEN Hälften bereits richtig ist.** Liest `tenant.findMany()`
bewusst über ALLE Mandanten (Treiber, ungebunden — `Tenant` trägt keinen
Zeilenschutz, Aufgabe 1 gemessen, `tenant-tabelle-ohne-zeilenschutz-bleibt-lesbar`)
und ruft je Mandant `groupsService.ensureDefaultGroup(tenant.id)` auf —
dieser Rumpf ist seit 260909-jts vollständig über `forTenant()`/
`withTenantTransaction()` gebunden (siehe Bestandsaufnahme,
`groups.service.ts`/`group`, Stand `gebunden`). Anders als bei den drei
Fällen oben musste hier in Aufgabe 2/3 (260910-das) NICHTS umgestellt
werden — der Rumpf war es bereits, bevor der Bereich `user` überhaupt an
der Reihe war. Einschränkung, bewusst nicht verschwiegen: der äußere
`try/catch` in `ensureDefaultGroupsForAllTenants()` verschluckt jeden
Fehler des Treibers (`tenant.findMany`) in eine Protokollzeile — läuft die
Mandantenliste nach dem Scharfschalten aus irgendeinem Grund leer,
entsteht keine Fehlermeldung, sondern gar keine Ausgabe (Befund K).
**Der vierte Fall, anderer Bauart — `dkv-scheduler.service.ts` /
**Der fünfte Fall, anderer Bauart — `dkv-scheduler.service.ts` /
`DkvService.loadAnyActiveConfigForScheduler()`** (260909-mir, Befund D,
WINDOWS #21): **offen, als benannte Altlast weitergeführt.** Dieser Dienst
gehört nicht in dieselbe Klasse wie die drei oben. Jene lesen bewusst über alle
@@ -287,11 +319,11 @@ verwendet), Klasse, Stand (`gebunden`/`ungebunden`/`gemischt`, seit
| apps/api/src/tenders/tenders.controller.ts | tender | keine-mandantengebundene-tabelle | ungebunden | Lesezugriff auf den plattformweiten Katalog (D-03). |
| apps/api/src/tenders/tenders.controller.ts | tenderSourcePollConfig | keine-mandantengebundene-tabelle | ungebunden | Plattformweiter Poll-Status, admin-verwaltet, kein `tenantId`. |
| apps/api/src/tenders/tenders.module.ts | tenderSourcePollConfig | keine-mandantengebundene-tabelle | ungebunden | Singleton-Bestückung beim Boot — im Dateikopf explizit als "global, RLS-exempt (D-03)" begründet. |
| apps/api/src/user/admin-seed.service.ts | tenant | keine-mandantengebundene-tabelle | ungebunden | Legt beim ersten Start den Standard-Mandanten selbst an — `Tenant` hat keine `tenantId`-Spalte. |
| apps/api/src/user/admin-seed.service.ts | user | bewusst-uebergreifend | gemischt | ZWISCHENSTAND nach Aufgabe 2 (260910-das): die Erstanlage-Pruefung bleibt bewusst ungebunden, die Erstanlage des Administrators selbst laeuft seit Aufgabe 2 ueber `forTenant()`, gebunden an den unmittelbar zuvor angelegten Mandanten (Befund J). Die Klassenkorrektur auf `beides` samt Begruendung folgt in Aufgabe 3. |
| apps/api/src/user/user.controller.ts | user | muss-mandantengebunden | ungebunden | Nutzerverwaltung innerhalb des Mandanten des anfragenden Admins. Wird in Aufgabe 3 (260910-das) gebunden. |
| apps/api/src/user/admin-seed.service.ts | tenant | keine-mandantengebundene-tabelle | ungebunden | Legt beim ersten Start den Standard-Mandanten selbst an und liest beim Start alle Mandanten fuer die Standardgruppen-Reparatur — `Tenant` hat keine `tenantId`-Spalte und traegt keinen Zeilenschutz (Aufgabe 1, `tenant-tabelle-ohne-zeilenschutz-bleibt-lesbar`). Fuenfter und bislang einziger bereits vollstaendig richtiger Fall der Hintergrunddienst-Falle (Befund K, siehe Abschnitt unten). |
| apps/api/src/user/admin-seed.service.ts | user | beides | gemischt | Klassenkorrektur (260910-das, Aufgabe 3): wechselt von `bewusst-uebergreifend` auf `beides`, weil die bisherige Begruendung ("es gibt strukturell keinen Mandanten zum Binden") nachweislich FALSCH war (Befund J) — der Mandant wird eine Anweisung vorher angelegt und ist bekannt. Die Erstanlage-Pruefung bleibt bewusst ungebunden (kein Mandant existiert zu diesem Zeitpunkt, `username` ist plattformweit eindeutig); die Erstanlage des Administrators selbst laeuft seit Aufgabe 2 ueber `forTenant()`, gebunden an den unmittelbar zuvor angelegten Mandanten. Eine P2002-Kollision beim Anlegen wird wie "Administrator existiert bereits" behandelt statt den Start abzubrechen (Befund I). |
| apps/api/src/user/user.controller.ts | user | muss-mandantengebunden | gebunden | Nutzerverwaltung innerhalb des Mandanten des anfragenden Admins (260910-das, Aufgabe 3): die Benutzerliste des ADMIN-Zweigs, alle drei Kennungswege (rollenabhaengig ueber `UserService.findById`/`findByIdForPlatformAdmin`) und alle fuenf Selbstbedienungszugriffe (Bild hochladen/loeschen/ausliefern, Akzentfarbe) laufen ueber `forTenant()`; die Rollenverzweigung zwischen mandantengebundener ADMIN-Sicht und der uebergreifenden `SUPER_ADMIN`-Sicht (ueber `UserService.findAllForPlatformAdmin`) bleibt bestehen. Der wirkungslose Selbstloesch-Riegel (Befund H, verglich gegen `currentUser.sub`, ein im Sitzungsnachweis nicht existierendes Feld) ist auf `currentUser.id` korrigiert. |
| apps/api/src/user/user.service.ts | tenant | keine-mandantengebundene-tabelle | ungebunden | Schleifentreiber der neuen Plattform-Administratorsicht (`findAllForPlatformAdmin`/`findByIdForPlatformAdmin`, 260910-das, Aufgabe 2, Befund F/N) — `Tenant` hat keine `tenantId`-Spalte und traegt keinen Zeilenschutz (Aufgabe 1, `tenant-tabelle-ohne-zeilenschutz-bleibt-lesbar`). |
| apps/api/src/user/user.service.ts | user | muss-mandantengebunden | gemischt | ZWISCHENSTAND nach Aufgabe 2 (260910-das): `findById`/`create`/`update`/`deactivate`/`delete` sowie die beiden neuen Plattform-Administratorsicht-Methoden laufen seither ueber `forTenant()`; ausschliesslich `findByUsername` bleibt bewusst ungebunden (plattformweit eindeutiger Schluessel, derselbe Fall wie `resolveEmailForWrite` im Bereich `ldap`). Die Klassenkorrektur auf `beides` samt Begruendung folgt in Aufgabe 3. |
| apps/api/src/user/user.service.ts | user | beides | gemischt | Klassenkorrektur (260910-das, Aufgabe 3): wechselt von `muss-mandantengebunden` auf `beides` wegen der einen bewusst ungebundenen Suche — wortgleich derselbe Praezedenzfall wie `ldap.service.ts`/`user` in 260909-ipc (`resolveEmailForWrite`). `findById`/`create`/`update`/`deactivate`/`delete` sowie die beiden neuen Plattform-Administratorsicht-Methoden laufen ueber `forTenant()`; `create`/`update` uebersetzen eine plattformweite Eindeutigkeitsverletzung (P2002) in eine deutsche Konfliktmeldung ohne Halter/Mandant zu nennen. `findByUsername` bleibt bewusst UNGEBUNDEN: der Anmeldeweg laeuft seit Etappe 1 ueber die drei SECURITY-DEFINER-Funktionen und hat diese Methode nicht mehr als Aufrufer (260910-das, Aufgabe 1, Teil 3: genau ein Treffer, die eigene Definition); eine gebundene Suche saehe einen fremden Halter des plattformweit eindeutigen `username` nicht und meldete faelschlich "frei". |
## Was diese Etappe NICHT entscheidet