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
@@ -1182,6 +1182,72 @@ entscheidet sie nicht. Er bindet dienst-intern, wie `ldap`, `groups`,
Jeweils mit der Feststellung, dass sie geprüft und bewusst gelassen sind —
nicht übersehen.
**Nachtrag (260910-das, Aufgabe 3).** Wie in den vorherigen Durchläufen wird
der Text oben NICHT umgeschrieben — er beschreibt korrekt den Stand zum
Zeitpunkt der Messung (Aufgabe 1); dieser Nachtrag hält fest, was Aufgabe 2/3
tatsächlich umgesetzt haben.
*Tatsächlich umgesetzte Pfade gegen die angekündigten gehalten:* alle in (u2)
genannten Pfade sind wie beschrieben umgestellt. `UserService.findById`,
`create`, `update`, `deactivate`, `delete` laufen über `forTenant()`;
`create`/`update` übersetzen die plattformweite Eindeutigkeitsverletzung in
eine deutsche Konfliktmeldung, die weder Halter noch Mandant nennt.
`AdminSeedService.seedAdmin()` bindet die Erstanlage des Administrators an
den unmittelbar zuvor angelegten Mandanten und entschärft die Startsperre
(P2002 wird wie „Administrator existiert bereits" behandelt, jeder andere
Fehler bricht weiterhin ab). `UserController` bindet alle sieben eigenen
Zugriffe (ADMIN-Zweig der Benutzerliste, alle fünf Selbstbedienungswege) und
löst die drei Wege über die Kennung rollenabhängig auf. `findByUsername`
bleibt wie angekündigt ungebunden, mit richtiggestelltem Kopfkommentar.
*Die geschlossene Lücke im Selbstlösch-Riegel (Befund H):* der Vergleich
`user.id === currentUser.sub` griff nie, weil der Sitzungsnachweis kein Feld
`sub` trägt (`JwtStrategy.validate()` liefert exakt `{ id, username, role,
tenantId }`). Der Nachweis kommt aus der Reihenfolge selbst, nicht aus einer
Behauptung: `user.controller.spec.ts`, Test 6, wurde zuerst gegen die alte
Fassung ausgeführt — die Zeile `if (user.id === currentUser.sub)` wurde
probeweise wiederhergestellt, der Testlauf zeigte den erwarteten roten Test
(„promise resolved … instead of rejecting"), danach wurde auf
`currentUser.id` repariert und derselbe Testlauf grün. Die Wirkung ist eine
Verhaltensänderung: ein Administrator kann sein eigenes Konto seither nicht
mehr löschen — das ist die ursprüngliche, im Code bereits formulierte
Absicht, nicht neu erfunden.
*Die gewählte Form der Plattform-Administratorsicht, samt Beleg aus der
Messung:* wie in (u3)/Befund F angekündigt, laufen
`UserService.findAllForPlatformAdmin()` und `findByIdForPlatformAdmin()` als
Schleife über alle Mandanten (`this.prisma.tenant.findMany`, ungebunden, weil
`Tenant` keinen Zeilenschutz trägt) mit je EINEM gebundenen Lesezugriff im
Rumpf — dieselbe Form wie `AdminSeedService.ensureDefaultGroupsForAllTenants()`.
Der Beleg, dass diese Form die heutige Sicht erhält statt sie zu mindern,
kommt aus Aufgabe 1: `user-fan-out-je-mandant-gebunden-liefert-alle-zeilen`
maß die Vereinigung der je-Mandant gebundenen `SELECT`s gegen eine über die
Wartungsrolle (mit `BYPASSRLS`) gemessene Gesamtmenge — beide Mengen waren
identisch (`["alice","bob","carol","dave"]`). In `user.service.spec.ts`
bestätigen Test 6 und Test 7 dasselbe am Code: je Mandant genau EIN
Protokolleintrag, die Sortierung nach Benutzername bleibt über die
zusammengeführten Teilmengen hinweg korrekt, und eine Kennungsauflösung für
einen Benutzer eines fremden Mandanten gelingt nachweislich über einen
gebundenen Lesezugriff.
*Falsifizierungsnachweise (Aufgabe 2 und 3), je einmal durchgeführt und
zurückgenommen:* in Aufgabe 2 wurde `UserService.findById` probeweise auf
den ungebundenen Klienten zurückgebaut — genau `user.service.spec.ts`, Test
4, wurde rot, mit der Meldung „erwarteter gebundener Aufruf
user.findUnique(tenant=t1) fehlt im Protokoll: []"; der Rückbau wurde
zurückgenommen, derselbe Testlauf danach wieder grün. In derselben Aufgabe
wurde zusätzlich `AdminSeedService.seedAdmin()`s gebundener
`tenantPrisma.user.create`-Aufruf probeweise auf den ungebundenen Basisclient
zurückgebaut — sechs Tests wurden rot (u. a. Test 9–12), alle mit der
Meldung „tenantPrisma.user.create is not a function", weil der ungebundene
Basisclient in der Testattrappe keine `create`-Methode auf `user` trägt; der
Rückbau wurde zurückgenommen, alle zehn Tests danach wieder grün. In Aufgabe
3 wurde `UserController.uploadAvatar`s gebundener Schreibzugriff probeweise
auf den ungebundenen Basisclient zurückgebaut — genau
`user.controller.spec.ts`, Test 7, wurde rot, mit der Meldung „Cannot read
properties of undefined (reading 'update')"; der Rückbau wurde
zurückgenommen, derselbe Testlauf danach wieder grün.
## Verweis
Die Bestandsaufnahme, welche Fundstelle den hier beschriebenen Übergang