Files
tessera-ctl/docs/mandantentrennung-zugriffsklassifikation.md
T
schalli f68beb379a docs(quick-260911-fh9): Mandantenquelle im auth-Controller festnageln, Klassifikation nachziehen, Ledger-Eintraege anlegen
- auth.controller.spec.ts: NEU. Mandantenquelle je Handler (das Claim fuer
  me/changePassword; fuer die oberste Rolle der Mandant des Ziels aus dem
  Fan-out), unbekanntes Ziel, null-Durchreichung von me, Rollen-Metadaten
  (ROLES_KEY) und Public-Metadaten (IS_PUBLIC_KEY) fuer alle sieben
  Handler — 23 Faelle; Falsifizierungsnachweis am Fan-out-Zweig
  durchgefuehrt und zurueckgenommen
- docs/mandantentrennung-zugriffsklassifikation.md: Uebersichtszeile auth
  auf 3/10 (war 8/5), Summenzeile 78/167, Bestandsaufnahme-Zeile
  auth.service.ts/user auf gebunden (keine gemischt-Zeile mehr fuer diese
  Datei), Klassen-Verteilung unveraendert bei 64 Paaren mit
  Stand-Vermerk, Hintergrunddienst-Abschnitt ohne sechsten Fall,
  Etappe-3-Anmeldeweg-Punkt in "Was diese Etappe NICHT entscheidet";
  beide Dokument-Falsifizierungen durchgefuehrt und zurueckgenommen
- .planning/WINDOWS.md: zwei neue offene Eintraege (#28 verschluckte Leere
  im Frontend, Familie #23/#25/#26; #29 Rechteausweitung ADMIN->SUPER_ADMIN
  im Schwesterweg PATCH /users/:id, T-FH9-05)

Baseline wiederhergestellt: 951/951 Tests gruen in 60 Dateien (927+23
neue Faelle plus der in Aufgabe 2 erwartungsgemaess rote Test), Werkzeug
120/120, Typpruefung sauber.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
2026-09-11 12:01:47 +02:00

536 lines
63 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Mandantentrennung — Zugriffsklassifikation
Dieses Dokument gehört zusammen mit `docs/mandantentrennung-datenbankrolle.md`
zur Vorbereitung der Mandantentrennung auf Datenbankebene (WINDOWS #18/#20).
Während die Datenbankrolle-Anleitung beschreibt, **wie** die Umstellung
technisch abläuft, hält dieses Dokument fest, **welche** der 227
`this.prisma.*`-Fundstellen in `apps/api/src` beim Umbau in den folgenden
Etappen angefasst werden müssen und welche bewusst unverändert bleiben.
Die Bestandsaufnahme unten wird **maschinell aus dem Quelltext ermittelt**
(nicht von Hand zusammengeschrieben) und durch
`apps/api/src/prisma/rls-access-inventory.spec.ts` bei jedem Testlauf gegen
den tatsächlichen Stand geprüft. Verschiebt sich eine Zeile, bleibt die
Prüfung grün — Vergleichsschlüssel sind Datei und Prisma-Modellname, keine
Zeilennummer. Kommt eine neue Fundstelle hinzu oder verschwindet eine
bestehende, schlägt die Prüfung fehl, bis dieses Dokument nachgezogen wird.
## Die drei Klassen
- **`muss-mandantengebunden`** — berührt auf Rechnung genau eines Mandanten
eine Tabelle mit `tenantId` (oder eine Tabelle, die ihre Mandantenregel
über einen Join auf eine solche Tabelle bezieht). Muss in Etappe 2 auf
`forTenant()` umgestellt werden.
- **`bewusst-uebergreifend`** — muss über Mandanten hinweg sehen, mit
ausgeschriebenem Grund. Braucht in Etappe 3 eine sichtbare Kennzeichnung
(Systemkontext), aber keinen `forTenant()`-Umbau.
- **`keine-mandantengebundene-tabelle`** — betrifft eines der acht Modelle
ohne `tenantId` bzw. eine bewusst plattformweite Tabelle (D-03). Kein
Umbau nötig.
- **`beides`** — ein vierter, im Plan ausdrücklich verlangter Sonderfall:
ein Hintergrunddienst, der zurecht über alle Mandanten hinweg eine Liste
aufbaut (bewusst übergreifend), aber *innerhalb* der Schleife je Mandant
binden muss (muss mandantengebunden werden). Beide Anteile sind in
derselben Datei vorhanden; die Fundstelle bekommt hier eine
Sammelklassifikation, die Aufteilung auf Zeilenebene steht in der
Begründung.
## Zwei belegte Befunde
**`req.tenantPrisma` wird gesetzt, aber nirgends gelesen.**
`tenant.middleware.ts:44` und `tenant.guard.ts:41` setzen
`req.tenantPrisma = forTenant(this.prisma, tenantId)`. Eine Volltextsuche
über `apps/api/src` nach `tenantPrisma` außerhalb dieser beiden Dateien und
ihrer Tests findet keine lesende Stelle — kein Controller greift darauf zu.
Die Verdrahtung besteht, wird aber nicht genutzt. Für Etappe 2 ist zu
entscheiden, ob die Controller künftig darüber gehen (dann bräuchte es keinen
zweiten `forTenant()`-Aufruf je Service-Methode) oder ob der Weg ersatzlos
entfällt. Dieser Plan entscheidet das nicht, hält den Befund nur fest.
**Entschieden (260911-e2s, Aufgabe 2):** ersatzloser Entfall, fuer ALLE
Bereiche der Etappe 2, nicht nur fuer `tenant`. Gemessen: außerhalb von
`tenant.middleware.ts` und `tenant.guard.ts` gab es KEINEN Leser (Suchumfang
oben bestaetigt, ebenso erneut gemessen in 260911-e2s Aufgabe 1, Befund B);
`tenant.middleware.ts` war zudem NIRGENDS verdrahtet (kein
`MiddlewareConsumer`, kein `configure(` in ganz `apps/api`, gemessen)
und hatte — anders als der urspruengliche Befund oben suggerierte — auch
KEINE eigenen Tests, ebenso wenig wie der Guard (`ls apps/api/src/tenant/`
vor 260911-e2s: einzige Testdatei war `tenant.service.spec.ts`). Entscheidung:
`tenant.middleware.ts` ist GELOESCHT, `tenant.guard.ts` setzt nur noch
`req.tenantId` und hat keine Prisma-Abhaengigkeit mehr. Grund: alle neun vor
diesem Bereich umgestellten Bereiche binden ausnahmslos dienst-intern (ein
Klient je Methode) — die Konvention ist durch Praxis entschieden, und tote
Verdrahtung, die wie ein Sicherheitsmechanismus aussieht, ist schlimmer als
keine. Siehe `docs/mandantentrennung-etappe2-fehlerrichtung.md`, Abschnitt
"## Bereich tenant", (n4)(a), fuer Messung und Begruendung im Volltext.
**WINDOWS #19 — nullbares `tenantId` bei `SearchProvider` und
`TenderRssFeedSource` — GESCHLOSSEN (260910-jab, Aufgabe 1/2).** Beide Modelle
tragen ein nullbares `tenantId` (`SearchProvider` für admin-gepflegte
Vorgabe-Suchmaschinen; `TenderRssFeedSource` für plattformweite RSS-Quellen
wie den geseedeten `service.bund.de`-Feed, D-06). Die ausgelieferte Policy
`"tenantId" = current_tenant_id()` verglich `NULL` nie gleich — nach dem
Scharfschalten wären plattformweite Zeilen für JEDEN Mandanten unsichtbar
gewesen, nicht nur für fremde.
Geschlossen durch Migration
`20260910120000_rls_widen_membership_grant_and_platform_read`, lokal
angewandt und gegen den Systemkatalog der lebenden Datenbank gemessen:
`TenderRssFeedSource` bekommt vier nach Befehl getrennte Regeln —
`tenant_platform_read_policy` (SELECT) schließt Zeilen ohne Mandant
ausdrücklich ein, `tenant_insert_policy`/`tenant_update_policy`/
`tenant_delete_policy` verlangen weiterhin ausnahmslos einen Mandanten. Die
Trennung nach Befehl ist notwendig, weil ein einzelner `USING`-Ausdruck auch
bestimmt, welche Zeilen `UPDATE`/`DELETE` erreichen — eine Leseregel, die
plattformweite Zeilen einschließt, hätte ohne Trennung jedem Mandanten auch
das Ändern/Entfernen dieser Zeilen erlaubt.
Die Hälfte zur Suchanbietertabelle (`SearchProvider`) schließt NICHT als
gelöstes Problem, sondern als **widerlegte Prämisse**: lokal gemessen gibt es
keinen Codeweg, der eine mandantenlose `SearchProvider`-Zeile erzeugt — der
einzige Schreibweg (`dashboard.service.ts`) verlangt die Mandantenkennung als
Pflichtparameter, und die Vorgabe-Suchmaschinen kommen laut 05-02-Entscheidung
aus Konstanten, nicht aus der Datenbank. Die Regel bleibt deshalb bewusst
unverändert streng; eine Lockerung wäre hier die falsche Richtung, weil sie
eine künftige mandantenlose Zeile jedem Mandanten zeigen würde.
Was diese Reparatur NICHT löst: unter der Anwendungsrolle lässt sich eine
plattformweite `TenderRssFeedSource`-Zeile weder anlegen noch entfernen, in
der alten wie in der neuen Regel, weil jede Schreibregel einen Mandanten
verlangt. Als eigener offener Ledger-Eintrag festgehalten (WINDOWS #24),
damit dieser Rest nicht mit #19 verschwindet.
## Übersicht je Bereich (Zeilentreffer je Bereich, ungebunden vs. gebunden)
**Wichtig, seit 260909-ipc (Aufgabe 3):** die Spalte "Ungebunden" zählt NUR
noch `this.prisma.<Modell>`-Rohtreffer — eine unveränderte Spaltenüberschrift
über einer veränderten Bedeutung wäre die nächste stille Falle, seit ein
Bereich (`ldap`) tatsächlich gebundene Zugriffe hat, die aus dieser Zählung
verschwinden. Die neue Spalte "Gebunden" zählt daneben die
`forTenant()`-gebundenen Rohtreffer (`tenantPrisma.<Modell>`, Konvention
dieses Codes — siehe `rls-access-inventory.spec.ts` für die allgemeinere,
namensunabhängige Erkennung über die `const <Name> = forTenant(`-Zuweisungsform).
Beide Spalten sind Rohtreffer (mehrere Vorkommen desselben Modells in
derselben Datei zählen mehrfach), nicht (Datei, Modell)-Paare wie in der
Bestandsaufnahme unten.
Gemessen mit
`grep -ro "this\.prisma\.[a-zA-Z]*" apps/api/src/<bereich> | grep -v spec | wc -l`
bzw. `grep -ro "tenantPrisma\.[a-zA-Z]*\." apps/api/src/<bereich> | grep -v spec | wc -l`
am 2026-09-09, **nach** den Änderungen aus Aufgabe 2/3 dieses Plans (260909-jts):
**Methodische Lücke, seit 260909-jts sichtbar:** die zweite Zählung sucht
ausschließlich den Namen `tenantPrisma` — die Konvention, die `ldap` und
(bis auf die drei Transaktionen) auch `groups` verwenden. Die drei
`withTenantTransaction()`-Aufrufe in `groups.service.ts` binden zusätzliche
neun Modellzugriffe über den Namen `tx` (den Transaktionsparameter), die
diese einfache Rohtrefferzählung strukturell NICHT sieht — anders als die
maschinelle, namensunabhängige Erkennung in `rls-access-inventory.spec.ts`
(Befund B), die auch diese Form erfasst. Die Zahl 31 unten ist deshalb der
Bodensatz, nicht die vollständige Zahl gebundener Zugriffe in `groups`; die
Bestandsaufnahme unten (Spalte "Stand", je (Datei, Modell)-Paar) ist die
autoritative Quelle.
| Bereich | Ungebunden | Gebunden | Hinweis |
|---|---|---|---|
| tenders | 35 | 27 | **war 62/0**, dann 36/26 nach 260909-laa — 260910-jab (Aufgabe 2) hat `tender-rss-feed.service.ts`/`listForUser` zusätzlich auf `forTenant()` umgestellt (WINDOWS #19 geschlossen, Befund F: ungebunden hätte die Reparatur den Pfad sonst still auf nur die plattformweiten Zeilen reduziert): ein Rohtreffer wandert von ungebunden nach gebunden (36→35, 26→27). Die 35 verbleibenden ungebundenen Treffer sind die zwölf bewusst nicht angefassten Paare (D-03-Katalog, zwei Fan-out-Adapter) plus die zwei bewusst ungebundenen RSS-Pfade (`createPlatform`/`remove`, WINDOWS #24) plus die übergreifenden Hälften der beiden Hintergrunddienste (Etappe-3-Übergabe) |
| 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 | 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 | 7 | 10 | **war 17/0** — Aufgabe 2/3 (260910-exd) haben `module-access.service.ts` (`getAccessibleModuleIds`: Kurzschlusszweig, Direktweg, Gruppenweg, Schnittmenge; `getCatalogFlags`: eigener Aktivierungs-Lesezugriff) und `module-registry.service.ts` (`findActiveForTenant`, `activateForTenant`, `deactivateForTenant`, `isModuleActive`) auf `forTenant()` umgestellt. Die 7 verbleibenden ungebundenen Rohtreffer sind bewusst: der eine Katalogzugriff in `module-access.service.ts` (`findAccessibleModules`) und die sechs Katalogzugriffe in `module-registry.service.ts` (`findAll`, `findBySlug`, die beiden Katalog-Existenzpruefungen in `activateForTenant`/`deactivateForTenant`, die Katalogsuche in `isModuleActive`, `seedModule`) — der Modulkatalog (`Module`) traegt heute keinen Zeilenschutz, eine Bindung waere heute wirkungslos, nicht katastrophal; katastrophal wuerde sie erst, WENN Etappe 3 dieser Tabelle eine Regel gibt (Befund E) |
| dashboard | 1 | 12 | **war 13/0** — Aufgabe 2/3 (260910-krx) haben `dashboard.service.ts` vollständig umgestellt: `getLayout`/`saveLayout` (gemeinsam gebunden), `getWidgets`/`addWidget`/`updateWidgetConfig`/`removeWidget` sowie `getSearchProviders`/`addSearchProvider`/`removeSearchProvider` laufen über `forTenant()`, je Methode ein Klient. Der eine verbleibende ungebundene Rohtreffer ist bewusst: der Modulkatalog (`Module`) trägt heute keinen Zeilenschutz, eine Bindung wäre heute wirkungslos, nicht katastrophal — katastrophal würde sie erst, WENN Etappe 3 dieser Tabelle eine Regel gibt (Befund E aus `module-registry`, hier übernommen) |
| auth | 3 | 10 | **war 8/5** — 260911-fh9 (Aufgabe 2) hat `getMe`, `changePassword`, `adminResetPassword` (fünf Rohtreffer auf `user`, drei Methoden) auf `forTenant()` umgestellt. Die 3 verbleibenden ungebundenen Rohtreffer sind die `$queryRaw`-Aufrufe der drei Anmeldefunktionen (`validateUser`, `requestPasswordReset`, `resetPassword`) — KEINE Modellzugriffe (`$` liegt nicht in `[a-zA-Z]`, die Bestandsaufnahme führt sie deshalb nicht als (Datei, Modell)-Paar), bewusst und dauerhaft ungebunden, siehe `20260909160000_auth_lookup_functions` und `docs/mandantentrennung-etappe2-fehlerrichtung.md`, Abschnitt "## Bereich auth", (h1) |
| calendar | 0 | 12 | **war 12/0** — Aufgabe 2 (260911-cwh) hat `calendar.service.ts` vollständig auf `forTenant()` umgestellt: `getSources`, `addSource`, beide Abfragen von `updateSource`/`deleteSource`, alle drei Abfragen von `testConnection`, Laden plus beide Synchronstatus-Rückschreibungen von `fetchAndCacheEvents` — je Methode ein Klient. Anders als bei den sieben Bereichen davor bleibt KEIN ungebundener Rest übrig: `CalendarSource` trägt eine Pflicht-Mandantenkennung, und kein Pfad dieses Bereichs liest über Mandanten hinweg |
| tenant | 8 | 3 | **war 8/0** — 260911-e2s (Aufgabe 3) hat drei gebundene Benutzerzähler in `tenant.controller.ts` eingeführt (Fan-out je Mandant nach dem Muster von `UserService.findAllForPlatformAdmin`, ersetzt die drei vorherigen Relationszähler); die acht `tenant`-Zugriffe selbst BLEIBEN ungebunden — `Tenant` trägt keine Regel in irgendeiner ausgelieferten Migration (260911-e2s Aufgabe 1, Prüfung 1/2), hier ist Ungebundenheit richtig, nicht geduldet |
| favorites | 7 | 0 | unverändert |
| settings | 4 | 0 | unverändert |
| **Summe** | **78** | **167** | Ungebunden: war 118 nach 260910-das, dann 108 nach 260910-exd (module-registry 17→7), dann 107 nach 260910-jab (`tenders` 36→35, `listForUser` gebunden), dann 95 nach 260910-krx (`dashboard` 13→1), dann 83 nach 260911-cwh (`calendar` 12→0), unverändert nach 260911-e2s (`tenant` bleibt bei 8 ungebundenen Rohtreffern), jetzt 78 nach 260911-fh9 (`auth` 8→3). Gebunden: war 124, dann 134 nach 260910-exd (zusätzlich 10 in `module-registry`), dann 135 nach 260910-jab (zusätzlich 1 in `tenders`), dann 147 nach 260910-krx (zusätzlich 12 in `dashboard`), dann 159 nach 260911-cwh (zusätzlich 12 in `calendar`), dann 162 nach 260911-e2s (zusätzlich 3 in `tenant`), jetzt 167 nach 260911-fh9 (zusätzlich 5 in `auth`). 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, 64 Paare)
Stand 260909-jts (Aufgabe 3): 61 Paare aus dem vorherigen Durchlauf
(260909-ipc) plus ein bisher vollstaendig unsichtbares Paar
(`groups.service.ts`/`tenantModuleActivation`), das erst die um
Transaktionsparameter erweiterte Erkennung (Befund B, Aufgabe 2 dieses
Plans) sichtbar macht — der einzige Zugriff auf dieses Modell in dieser
Datei lief bis dahin ausschliesslich ueber den Rueckgabeparameter der
interaktiven Transaktion in `ensureDefaultGroup` und war weder ueber
`this.prisma.<Modell>` noch ueber `<gebundener Client>.<Modell>` erfassbar.
Die Zahl ist der Ausgabe der Pruefung in
`apps/api/src/prisma/rls-access-inventory.spec.ts` entnommen, nicht
geschaetzt.
**Stand 260909-laa (Aufgabe 2):** dieselben 62 Paare, keine neue Fundstelle
hinzugekommen oder verschwunden — nur EINE Klasse hat sich verschoben:
`tender-rss-feed.service.ts`/`tenderRssFeedSource` wechselt von
`muss-mandantengebunden` auf `beides` (WINDOWS #19 — `createForUser` bindet,
`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.
**Stand 260910-krx (Aufgabe 3): unveraendert, ausdruecklich festgehalten
statt uebersprungen.** Weiterhin 63 Paare, keine Klasse verschoben sich.
Die vier Paare des Bereichs `dashboard`
(`dashboard.service.ts`/`dashboardLayout`, `/module`, `/searchProvider`,
`/widgetInstance`) waren bereits vor diesem Durchlauf korrekt klassifiziert
(drei `muss-mandantengebunden`, eines `keine-mandantengebundene-tabelle`) —
dieser Plan aendert nur ihre `Stand`-Spalte (`ungebunden` auf `gebunden`
fuer drei der vier Paare), keine ihrer Klassen. Eine unveraenderte Tabelle
ohne diesen Vermerk waere von einer vergessenen Nachziehung nicht zu
unterscheiden — deshalb steht die Abwesenheit einer Aenderung hier
ausdruecklich, statt stillschweigend uebersprungen zu werden.
**Stand 260911-cwh (Aufgabe 3): unveraendert, ausdruecklich festgehalten
statt uebersprungen.** Weiterhin 63 Paare, keine Klasse verschoben sich. Das
eine Paar des Bereichs `calendar` (`calendar.service.ts`/`calendarSource`)
war bereits vor diesem Durchlauf korrekt klassifiziert (`muss-mandantengebunden`)
— Aufgabe 2 (260911-cwh) aendert nur seine `Stand`-Spalte (`ungebunden` auf
`gebunden`), nicht seine Klasse. Eine unveraenderte Tabelle ohne diesen
Vermerk waere von einer vergessenen Nachziehung nicht zu unterscheiden —
deshalb steht die Abwesenheit einer Aenderung hier ausdruecklich, statt
stillschweigend uebersprungen zu werden.
**Stand 260911-e2s (Aufgabe 3):** 64 Paare — 63 aus dem vorherigen
Durchlauf plus EIN neues Paar (`tenant.controller.ts`/`user`, Klasse
`muss-mandantengebunden`, Stand `gebunden`): die drei gebundenen
Benutzerzähler des Fan-outs (Befund F/G aus 260911-e2s Aufgabe 1). Keine
bestehende Klasse verschiebt sich — die beiden `tenant`-Paare
(`tenant.controller.ts`/`tenant`, `tenant.service.ts`/`tenant`) bleiben
`keine-mandantengebundene-tabelle`/`ungebunden`, nur ihre Begründung wird
fortgeschrieben (siehe Fundstellentabelle unten).
**Stand 260911-fh9 (Aufgabe 3): unveraendert, ausdruecklich festgehalten
statt uebersprungen.** Weiterhin 64 Paare, keine Klasse verschiebt sich. Das
Paar `apps/api/src/auth/auth.service.ts`/`user` war bereits vor diesem
Durchlauf korrekt klassifiziert (`muss-mandantengebunden`) — Aufgabe 2
(260911-fh9) aendert nur seine `Stand`-Spalte (`gemischt` auf `gebunden`),
nicht seine Klasse. Das Paar `apps/api/src/auth/auth.service.ts`/
`passwordResetToken` bleibt `muss-mandantengebunden`/`gebunden`,
unveraendert. Eine unveraenderte Tabelle ohne diesen Vermerk waere von
einer vergessenen Nachziehung nicht zu unterscheiden — deshalb steht die
Abwesenheit einer Aenderung hier ausdruecklich, statt stillschweigend
uebersprungen zu werden.
| Klasse | Anzahl Paare |
|---|---|
| muss-mandantengebunden | 32 |
| keine-mandantengebundene-tabelle | 17 |
| beides | 13 |
| bewusst-uebergreifend | 2 |
| **Summe** | **64** |
## 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. 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
je Aufruf für einen übergebenen Mandanten). Vor dieser Etappe blieben
Lesezugriffe auf `group`, `ldapConfig` und `user` innerhalb der
Sync-Methoden teils ungebunden, obwohl der Mandant zu diesem Zeitpunkt
bereits bekannt war — nur 4 der inzwischen 11 `forTenant()`-Aufrufstellen
deckten die Lese-/Schreibpfade ab. Jetzt sind `group` und `ldapConfig`
vollständig gebunden; bei `user` bleibt ausschließlich
`resolveEmailForWrite` bewusst ungebunden (Befund A, T-IPC-04 — siehe
Bestandsaufnahme unten). Der als gefährlichster Punkt benannte
Löschzweig (`syncBoundGroupsForTenant`, WINDOWS #20) war bereits seit
Etappe 1 gebunden. **Die seinerzeit offen geführte Reihenfolgebedingung
für Etappe 4 — die Übergabe unmittelbar vor der Löschung
(`this.groupsService.reassignDefaultBeforeDelete(tenantId, group.id)` und
`ensureDefaultGroup(tenantId)` in `groups.service.ts`) — ist mit
260909-jts (Aufgabe 2) GESCHLOSSEN:** beide Methoden laufen seither über
`forTenant()`/`withTenantTransaction()` (siehe Bestandsaufnahme unten,
`groups.service.ts`/`group`, Stand `gebunden`). Nachtrag, nicht
Neuschrieb: der ursprüngliche Befund bleibt oben lesbar, weil er den
Zustand zum Zeitpunkt der ldap-Umstellung korrekt beschreibt und für
spätere Etappen als Beleg dient, siehe auch
`docs/mandantentrennung-etappe2-fehlerrichtung.md`, Abschnitt (e), Befund D.
- **`tender-digest.scheduler.ts`** (Ausschreibungs-Digest) — **Stand
260909-laa, Aufgabe 3: Je-Treffer-Hälfte geschlossen, übergreifende
Hälfte an Etappe 3 übergeben.** Liest `tenderMatch` (Kandidatenabfrage,
`findMany` mit `distinct: ['userId']`) bewusst über ALLE Mandanten in
einem einzigen `findMany` (ein einziger globaler Cron-Job, kein Mandant
im Job selbst — so von Anfang an entworfen, Pitfall 1 in den Kommentaren
der Datei) — diese Kandidatenabfrage bleibt UNGEBUNDEN und ist im Code
als Etappe-3-Übergabe kommentiert. Sie wählt zusätzlich das
denormalisierte `tenantId` der Treffer-Zeile mit aus, damit die Schleife
binden kann; der Sonderfall eines Nutzers mit Treffern unter zwei
verschiedenen Mandanten (Mandantenwechsel) ist NICHT gelöst, siehe
`docs/mandantentrennung-etappe2-fehlerrichtung.md`. Innerhalb der
Schleife laufen `tenderNotificationPref.findUnique`,
`tenderMatch.findMany`/`updateMany` und `user.findUnique` je
Kandidatenzeile über `forTenant()`, gebunden an deren Mandanten.
- **`tender-matching.service.ts`** (Ausschreibungs-Sofortmeldung) —
**Stand 260909-laa, Aufgabe 3: Je-Treffer-Hälfte geschlossen, übergreifende
Hälfte an Etappe 3 übergeben.** `tenderSavedSearch.findMany` (Profile ALLER
Mandanten) und der Lesezugriff auf den plattformweiten `tender`-Katalog
(D-03) bleiben UNGEBUNDEN, beide im Code als Etappe-3-Übergabe bzw. D-03
kommentiert. Innerhalb der Profilschleife laufen die Treffer-Anlage
(`tenderMatch.upsert`) und — im nachgelagerten Instant-Dispatch —
`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 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
Mandanten und müssen lediglich innerhalb der Schleife binden — sie sind heute
korrekt und verstummen erst nach dem Scharfschalten. Der DKV-Planer iteriert
überhaupt nicht: `findFirst()` ohne jede Bedingung zieht bei mehreren Mandanten
EINEN beliebigen und bedient die übrigen NIE. Ist ausgerechnet die gezogene
Zeile inaktiv, bedient er niemanden, obwohl ein zweiter Mandant aktiv wäre. Er
ist damit **heute bereits falsch** und verstummt nach dem Scharfschalten
zusätzlich (`null` statt einer beliebigen Zeile; der Planer protokolliert das als
Normalfall und richtet für JEDEN Mandanten nichts ein, ohne Alarm).
Binden ist hier keine Lösung: `onModuleInit()` hat beim Start strukturell keinen
Mandantenkontext, ein gebundener Aufruf liefe garantiert leer. Der Umbau auf
einmal-abfragen-viele-bedienen ist die in Phase 07-04 zurückgestellte
Mehrmandanten-Planung — eine Funktionsänderung, kein Bindungsumbau, und deshalb
in Etappe 2 nicht vorgenommen. Der Zugriff bleibt deshalb ungebunden, aber
dreifach markiert: eigene benannte Methode mit Kopfkommentar (bewusst KEINE
Verzweigung hinter einem optionalen Parameter, die jemand später
„vereinheitlicht"), fortgeschriebener Kopfkommentar in
`dkv-scheduler.service.ts`, Ledger-Eintrag WINDOWS #21. Das Signal für das
Verstummen gehört in die Vorabprüfung von Etappe 4 (`rls-preflight.mjs`).
**Stand 260910-exd — kein sechster Fall, gemessen statt angenommen.** Der
Bereich `module-registry` fügt diesem Abschnitt KEINEN sechsten Fall hinzu.
`ModuleRegistryService.seedModule()` (die Katalogpflege beim Start,
aufgerufen aus vier Seed-Dateien) schreibt zwar ohne Mandantenkontext — sie
iteriert aber über NICHTS je Mandant, sondern schreibt genau eine Zeile je
Aufruf auf den plattformweiten Katalog `Module`. Damit fehlt ihr die Bauform
der fünf oben geführten Fälle (übergreifend LESEN über alle Mandanten, dann
je Mandant BINDEN) — sie ist deshalb kein Kandidat für diese Liste. Dieser
Satz hält die Abwesenheit fest, damit sie nicht wie ein Übersehen aussieht.
**Stand 260910-krx — auch der Bereich `dashboard` fügt diesem Abschnitt
keinen sechsten Fall hinzu, gemessen statt angenommen.** Anweisung (260910-krx,
Aufgabe 1): `grep -rn "onModuleInit\|onApplicationBootstrap\|@Cron\|setInterval\|Scheduler" apps/api/src/dashboard --include=*.ts`
liefert null Treffer außerhalb von Testdateien — der einzige Dienst dieses
Bereichs, `dashboard.service.ts`, enthält keinen Hintergrunddienst, keinen
Planer und keinen Start-Hook. Jede seiner neun mandantengebundenen Methoden
wird ausschließlich synchron aus einer Anfrage eines einzelnen, bereits
bekannten Nutzers heraus aufgerufen — die Bauform dieses Abschnitts
(übergreifend LESEN über alle Mandanten, dann je Mandant BINDEN) kommt in
diesem Bereich an keiner Stelle vor.
**Stand 260911-cwh — auch der Bereich `calendar` fügt diesem Abschnitt
keinen sechsten Fall hinzu, gemessen statt angenommen (260911-cwh, Aufgabe 1,
Befund B).** `grep -rn "@Cron\|onModuleInit\|onApplicationBootstrap\|setInterval\|setTimeout" apps/api/src/calendar --include=*.ts`
liefert außerhalb von Testdateien genau einen Treffer,
`providers/ics.provider.ts:100` — ein `setTimeout` für den Abbruch eines
HTTP-Abrufs nach acht Sekunden, kein Planer. Der einzige Dienst dieses
Bereichs, `calendar.service.ts`, enthält keinen Hintergrunddienst, keinen
Planer und keinen Start-Hook — die Bauform dieses Abschnitts (übergreifend
LESEN über alle Mandanten, dann je Mandant BINDEN) kommt an keiner Stelle
vor. Es gibt aber einen benannten Sonderfall, anderer Bauart als die fünf
Fälle oben: `refreshCacheInBackground` (private Methode) ist eine
ABGEKOPPELTE FORTSETZUNG einer Anfrage — `aggregateEvents` stößt sie an,
wartet nicht auf sie, und sie ruft `fetchAndCacheEvents` mit den Parametern
der ursprünglichen Anfrage auf. Sie iteriert NICHT über mehrere Mandanten
und hat deshalb keine übergreifende Hälfte zu binden — sie trägt die
Mandantenkennung der Anfrage, die sie ausgelöst hat, und kann strukturell
keine andere haben. Kein Kandidat für diese Liste; dieser Absatz hält die
Abwesenheit fest, damit sie nicht wie ein Übersehen aussieht.
Auch der Bereich `tenant` fügt diesem Abschnitt keinen sechsten Fall hinzu,
gemessen statt angenommen (260911-e2s, Aufgabe 1, Befund J). Anweisung:
`grep -rn "@Cron\|onModuleInit\|onApplicationBootstrap\|setInterval\|setTimeout\|Scheduler" apps/api/src/tenant --include=*.ts`
und `grep -rn '\$transaction(\|\$queryRaw\|\$executeRaw' apps/api/src/tenant --include=*.ts`
liefern je null Treffer — kein Hintergrunddienst, kein Roh-SQL, keine
Transaktion in diesem Bereich. `TenantService.create` ruft nach dem Anlegen
eines Mandanten `groupsService.ensureDefaultGroup(tenant.id)` auf; dieser
Aufruf läuft seit 260909-jts bereits vollständig über den gebundenen Weg des
Bereichs `groups` (`forTenant()`/`withTenantTransaction()`, siehe
Fundstellentabelle unten, `groups.service.ts`/`group`, Stand `gebunden`) —
nichts an dieser Übergabe musste in 260911-e2s umgestellt werden.
**Stand 260911-fh9 — auch der Bereich `auth` fügt diesem Abschnitt keinen
sechsten Fall hinzu, gemessen statt angenommen (Befund J).** Anweisung:
`grep -rn "@Cron\|onModuleInit\|onApplicationBootstrap\|setInterval\|setTimeout" apps/api/src/auth --include=*.ts`
und `grep -rn '\$transaction(' apps/api/src/auth --include=*.ts` liefern je
null Treffer außerhalb von Testdateien — kein Hintergrunddienst, keine
Transaktion in diesem Bereich. `grep -rn "include:\|_count\|select:"
apps/api/src/auth --include=*.ts | grep -v spec` findet genau EINEN
Treffer, das `select` in `getMe` — ausschließlich skalare Felder von
`User`, keine Relation (WINDOWS #27 hier ohne Ausprägung). Die einzige
Stelle, an der dieser Bereich ohne Mandantenkontext liest, ist der
Anmeldeweg (`validateUser`, `requestPasswordReset`, `resetPassword`) —
geloest durch die drei SECURITY-DEFINER-Funktionen, nicht durch die Bauform
"übergreifend lesen, dann je Mandant binden".
## Bestandsaufnahme
Maschinell ermittelt, `rls-access-inventory.spec.ts` hält Vollständigkeit
nach. Spalten: Datei, Modell (Prisma-Modellname wie in `this.prisma.<Modell>`
oder — seit 260909-ipc, Befund G — in `<gebundener Client>.<Modell>`
verwendet), Klasse, Stand (`gebunden`/`ungebunden`/`gemischt`, seit
260909-ipc maschinell gegen den Quelltext geprüft), Begründung.
**Erkennungslücke, seit 260911-e2s vermessen (Aufgabe 1, (n4)(b)):** die
Bestandsaufnahme sieht ausschließlich (Datei, Modell)-Paare über
`this.prisma.<Modell>` bzw. `<gebundener Client>.<Modell>` — eine
Relationseinbindung (`include:`, Relationszähler `_count`) in eine ZWEITE
Tabelle erzeugt kein eigenes Paar und ist für das Werkzeug strukturell
unsichtbar. Alle 19 `include:`-Stellen und alle `_count`-Stellen des
API-Quelltexts wurden einzeln nachgesehen; die einzige gefährliche
Ausprägung (ungebundener äußerer Aufruf auf einer UNGESCHÜTZTEN Tabelle,
Einbindung in eine GESCHÜTZTE Tabelle) war `tenant.controller.ts` — hier in
Aufgabe 3 behoben (Fan-out ersetzt den Relationszähler).
| Datei | Modell | Klasse | Stand | Begründung |
|---|---|---|---|---|
| apps/api/src/auth/auth.service.ts | passwordResetToken | muss-mandantengebunden | gebunden | Kein eigenes `tenantId`, RLS ueber Join auf `User` (Migration 20260618112133). `requestPasswordReset`/`resetPassword` laufen vollstaendig ueber `forTenant()` (Etappe 1, WINDOWS #20, Aufgabe 1) — von der alten, nur `this.prisma.*` erkennenden Suche nie erfasst, weil bereits gebunden; die erweiterte Erkennung aus Aufgabe 2 (260909-ipc) macht diese Fundstelle erstmals sichtbar. |
| apps/api/src/auth/auth.service.ts | user | muss-mandantengebunden | gebunden | Klassenkorrektur (260911-fh9, Aufgabe 2/3): wechselt von `gemischt` auf `gebunden` — `getMe`, `changePassword`, `adminResetPassword` binden seit Aufgabe 2 je über GENAU EINEN Klienten `tenantPrisma` an den Mandanten aus dem Sitzungsnachweis (`@CurrentUser().tenantId`); für die oberste Rolle (SUPER_ADMIN) löst der Controller den Mandanten des ZIELS über den gebundenen Fan-out `UserService.findByIdForPlatformAdmin` auf. `adminResetPassword` verweigert zusätzlich einem Nicht-SUPER_ADMIN das Kennwort eines SUPER_ADMIN (T-FH9-04). Die drei Anmeldesuchen (`validateUser`, `requestPasswordReset`, `resetPassword`) laufen weiterhin über die drei SECURITY-DEFINER-Funktionen (`$queryRaw`, keine Modellzugriffe — `$` liegt nicht in `[a-zA-Z]`) und bleiben unverändert auf dem ungebundenen Klienten. Etappe-3-Vorbehalt: die Bindung hängt am Claim `tenantId` und an `User.id` (plattformweite UUID), nicht an `username`/`email` — der Anmeldeweg-Umbau für je Mandant eindeutige Anmeldenamen betrifft diese Bindung nicht, siehe `docs/mandantentrennung-etappe2-fehlerrichtung.md`, Abschnitt "## Bereich auth", (h4)(a). |
| apps/api/src/calendar/calendar.service.ts | calendarSource | muss-mandantengebunden | gebunden | Kalenderquellen eines Nutzers je Mandant gebunden (encryptedPassword traegt Zugangsdaten zu externen Exchange-/CalDAV-Servern), `tenantId`-Spalte vorhanden. Seit 260911-cwh (Aufgabe 2) laufen alle zwoelf Zugriffe (`getSources`, `addSource`, beide Abfragen von `updateSource`/`deleteSource`, alle drei Abfragen von `testConnection`, Laden plus beide Synchronstatus-Rueckschreibungen von `fetchAndCacheEvents`) ueber `forTenant()`, ein Klient je Methode; `fetchAndCacheEvents`/`refreshCacheInBackground` nehmen die Mandantenkennung als Parameter, Letztere traegt die Kennung der urspruenglichen Anfrage. Die drei Besitzpruefungen (`updateSource`/`deleteSource`/`testConnection`, Vergleich gegen `userId` aus dem Sitzungsnachweis) bleiben zusaetzlich bestehen — die Regel auf `CalendarSource` kennt keine Benutzerdimension (260911-cwh, Aufgabe 1, gemessen), sie sind bis zur Etappe-3-Entscheidung (2) der einzige Schutz zwischen Kollegen DESSELBEN Mandanten. |
| apps/api/src/dashboard/dashboard.service.ts | dashboardLayout | muss-mandantengebunden | gebunden | Widget-Anordnung eines Nutzers, `tenantId`-Spalte vorhanden. Seit 260910-krx (Aufgabe 2) laufen `getLayout`/`saveLayout` GEMEINSAM ueber `forTenant()`, ein Klient je Methode; `saveLayout` uebersetzt eine `PrismaClientUnknownRequestError` (RLS-Konflikt auf der plattformweit eindeutigen `userId`, gemessen in Aufgabe 1 — NICHT die `P2002`-Form, die der Bereich `tenders` abfaengt) in eine deutsche Konfliktmeldung. |
| apps/api/src/dashboard/dashboard.service.ts | module | keine-mandantengebundene-tabelle | ungebunden | Modulkatalog ist plattformweit, kein `tenantId` (Migration 20260909140000, Gruppe b). MESSUNG (260910-krx, Aufgabe 1, uebernommen aus `module-registry`-Pruefung `module-tabelle-traegt-keinen-zeilenschutz`): die Tabelle traegt heute keinen Zeilenschutz, eine Bindung waere heute wirkungslos, nicht katastrophal. BEDINGUNG: katastrophal wuerde sie erst, WENN Etappe 3 dieser Tabelle eine Regel gibt. Die Katalogaufloesung fuer den Widget-Modulfilter (`ModuleAccessService.getAccessibleModuleIds`) bindet bereits seit 260910-exd in ihrem eigenen Dienst — hier NICHT ein zweites Mal gebunden. |
| apps/api/src/dashboard/dashboard.service.ts | searchProvider | muss-mandantengebunden | gebunden | `tenantId` nullbar. WINDOWS #19 geschlossen (260910-jab) als **widerlegte Prämisse** für dieses Modell. In diesem Durchlauf (260910-krx, Aufgabe 1) EIGENSTAENDIG nachgeprueft, nicht aus 260910-jab abgeschrieben: `grep -rn "searchProvider\|SearchProvider" apps packages prisma --include=*.ts --include=*.mjs --include=*.js --include=*.sql --include=*.json` (ohne `node_modules`, `dist/`, `.next/`) findet weiterhin genau einen Schreibweg, `dashboard.service.ts:addSearchProvider` (`create`), mit `tenantId: string` als Pflichtparameter — keine Seed-Datei, kein Skript. Seit Aufgabe 2/3 laufen `getSearchProviders`/`addSearchProvider`/`removeSearchProvider` ueber `forTenant()`; die Regel auf `SearchProvider` bleibt UNVERAENDERT streng, zusaetzlich datenbankseitig verteidigt durch `searchprovider-gebundenes-einfuegen-ohne-mandant-abgelehnt` (Aufgabe 1). |
| apps/api/src/dashboard/dashboard.service.ts | widgetInstance | muss-mandantengebunden | gebunden | Platzierte Dashboard-Widgets eines Nutzers, `tenantId`-Spalte vorhanden. Seit 260910-krx (Aufgabe 2) laufen `getWidgets`, `addWidget` sowie beide Paare aus Besitzpruefung und Schreibzugriff (`updateWidgetConfig`/`removeWidget`) ueber `forTenant()`; die vorgeschalteten Besitzpruefungen ueber die Benutzerkennung bleiben zusaetzlich bestehen (die Regeln dieses Bereichs kennen keine Benutzerdimension). |
| apps/api/src/dkv/dkv.service.ts | dkvInvoiceHistory | muss-mandantengebunden | gebunden | DKV-Rechnungshistorie je Mandant, `tenantId`-Spalte vorhanden. Seit 260909-mir (Aufgabe 3) laufen beide Historien-Schreibzugriffe der Verarbeitungsstrecke, beide parallelen Lesezugriffe von `getHistory` und der neue Riegel vor dem Ausfuhrdatei-Download vollstaendig ueber `forTenant()`. |
| apps/api/src/dkv/dkv.service.ts | dkvModuleConfig | muss-mandantengebunden | gemischt | Postfach-/Zugangsdaten des DKV-Moduls je Mandant. Seit 260909-mir (Aufgabe 2) laufen `loadConfig`, `getConfigForApi`, `saveConfig`, `testConnection` und der Konfigurations-Lesezugriff der Verarbeitungsstrecke ueber `forTenant()`. Die Mischung stammt ausschliesslich vom einen benannten, bewusst ungebundenen Planer-Startpfad `loadAnyActiveConfigForScheduler()` (WINDOWS #21) — keine uebersehene Fundstelle. |
| apps/api/src/dkv/dkv.service.ts | dkvVehicleMaster | muss-mandantengebunden | gebunden | Fahrzeugstammdaten des DKV-Moduls je Mandant. Seit 260909-mir (Aufgabe 3) laufen Fahrzeugliste, Anlegen, beide Paare aus Besitzpruefung und Schreibzugriff (Aendern/Loeschen), beide Zweige des CSV-Imports und der gebuendelte Lesezugriff beim Aufbau der Ausfuhrzeilen vollstaendig ueber `forTenant()`; die vorgeschalteten Besitzpruefungen bei Aendern/Loeschen bleiben zusaetzlich bestehen (Befund G — ein gebundenes UPDATE ueber die Kennung allein trifft eine fremde Zeile still, nicht laut). |
| apps/api/src/favorites/favorites.service.ts | favoriteLink | muss-mandantengebunden | ungebunden | Favoriten-Links eines Nutzers, `tenantId`-Spalte vorhanden. |
| apps/api/src/groups/groups.service.ts | group | muss-mandantengebunden | gebunden | Gruppen sind je Mandant, `tenantId`-Spalte vorhanden. Alle 12 Methoden laufen seit 260909-jts (Aufgabe 2) ueber `forTenant()` bzw. `withTenantTransaction()`. |
| apps/api/src/groups/groups.service.ts | groupMembership | muss-mandantengebunden | gebunden | Kein eigenes `tenantId`, RLS über Join auf `Group` (Migration 20260618112133-Nachfolger) — braucht trotzdem `forTenant()`, damit der Join-Kontext gesetzt ist. Seit 260909-jts gebunden, einschliesslich der drei Zugriffe innerhalb des Standardgruppen-Aufbaus (`ensureDefaultGroup`), die zuvor ueber den Transaktionsparameter liefen und fuer keine Pruefung dieses Projekts sichtbar waren (Befund B). |
| apps/api/src/groups/groups.service.ts | moduleGrant | muss-mandantengebunden | gebunden | Modulfreigaben je Mandant, `tenantId`-Spalte vorhanden. Seit 260909-jts gebunden, einschliesslich des Zugriffs innerhalb des Standardgruppen-Aufbaus (Befund B). |
| apps/api/src/groups/groups.service.ts | tenantModuleActivation | muss-mandantengebunden | gebunden | Welche Module ein Mandant aktiviert hat. Bis 260909-jts vollstaendig unsichtbar (Befund B, 260909-jts-PLAN.md): der einzige Zugriff dieser Datei lief innerhalb der interaktiven Transaktion von `ensureDefaultGroup` ueber den Rueckgabeparameter (`tx.tenantModuleActivation.findMany`) — weder `this.prisma.<Modell>` noch `<gebundener Client>.<Modell>` sahen das, weil `tx` weder `this.prisma` noch aus einer `forTenant(`-Zuweisung stammte. Die um Transaktionsparameter erweiterte Erkennung aus Aufgabe 2 macht dieses Paar erstmals sichtbar; die Stelle ist seit derselben Aufgabe gebunden (ueber `withTenantTransaction()`). |
| apps/api/src/groups/groups.service.ts | user | muss-mandantengebunden | gebunden | Nutzerverwaltung innerhalb eines Mandanten. Seit 260909-jts gebunden; `addUserToDefaultGroup` prueft seither zusaetzlich, dass der Zielbenutzer zum Mandanten gehoert (Befund E, T-JTS-02). Bis 20260910120000_rls_widen_membership_grant_and_platform_read pruefte die Regel auf `GroupMembership` nachweislich nur die Gruppenseite — seit 260910-jab prueft sie beide Seiten, die Anwendungspruefung bleibt trotzdem bestehen (Schalter weiterhin aus, #18). |
| apps/api/src/groups/module-grants.service.ts | group | muss-mandantengebunden | gebunden | Wie groups.service.ts. Seit 260909-jts (Aufgabe 3) gebunden — `assertTargetBelongsToTenant` erzeugt seinen eigenen Kontext, `getMatrix` teilt sich einen Kontext mit den beiden anderen parallelen Teilabfragen. |
| apps/api/src/groups/module-grants.service.ts | groupMembership | muss-mandantengebunden | gebunden | Kein eigenes `tenantId`, RLS über Join auf `Group`. Seit 260909-jts gebunden; der `where`-Filter über die Beziehung zur Gruppe bleibt zusätzlich stehen, weil die Regel auf dieser Tabelle nachweislich nur die Gruppenseite prüft (Aufgabe 1). |
| apps/api/src/groups/module-grants.service.ts | moduleGrant | muss-mandantengebunden | gebunden | Modulfreigaben je Mandant. Seit 260909-jts gebunden; die Mandanten-Gegenprüfung vor jedem Erteilen (`assertTargetBelongsToTenant`) bleibt zusätzlich bestehen. Bis 20260910120000_rls_widen_membership_grant_and_platform_read prüfte die Regel auf dieser Tabelle nur die Mandantenkennung der Zeile, nicht die referenzierte Gruppe/den referenzierten Benutzer (Befund F, T-JTS-03, Aufgabe 1) — seit 260910-jab prüft sie beide Ziele zusätzlich, die Anwendungsprüfung bleibt trotzdem der erste Schutz (Schalter weiterhin aus, #18). |
| apps/api/src/groups/module-grants.service.ts | tenantModuleActivation | muss-mandantengebunden | gebunden | Welche Module ein Mandant aktiviert hat, `tenantId`-Spalte vorhanden. Seit 260909-jts gebunden. |
| apps/api/src/groups/module-grants.service.ts | user | muss-mandantengebunden | gebunden | Zielbenutzer eines Grants innerhalb des Mandanten. Seit 260909-jts gebunden; die Mandanten-Gegenprüfung bleibt bestehen. |
| apps/api/src/ldap/ldap-config.service.ts | ldapConfig | beides | gemischt | `getConfig`, `createConfig` und `updateConfig` laufen ueber `forTenant()`, gebunden an den aus der Anfrage bereits bekannten Mandanten. `getAllActiveConfigs()` (Planer-Lesezugriff ueber ALLE Mandanten) und die Start-Nachverschluesselung in `onApplicationBootstrap` bleiben bewusst uebergreifend: beide laufen, bevor bzw. unabhaengig davon, ob ein einzelner Mandantenkontext feststeht (Befund B, 260909-ipc-PLAN.md). Korrektur der Klasse von `muss-mandantengebunden`: das Paar ist tatsaechlich `beides`, keine Verhaltensaenderung. |
| apps/api/src/ldap/ldap-config.service.ts | ldapFieldMapping | muss-mandantengebunden | gebunden | Kein eigenes `tenantId`, RLS ueber Join auf `LdapConfig`. `addFieldMapping` und `removeFieldMapping` nehmen den Mandanten jetzt als Parameter entgegen und laufen vollstaendig ueber `forTenant()` (T-IPC-01, 260909-ipc-PLAN.md) — schliesst zugleich die Fremdzugriffsluecke beim Loeschen einer Feldzuordnung ueber ihre Kennung. |
| apps/api/src/ldap/ldap.service.ts | group | beides | gebunden | AD-Abgleich: `listGroups` (die "bereits importiert"-Markierung) und `importGroupsByDn` (die Idempotenzpruefung ueber `ldapObjectGuid`) sind mit Aufgabe 3 (260909-ipc) auf `forTenant()` umgestellt — zusammen mit den bereits vorher gebundenen Stellen (Anlage, Mitgliedschafts- und Gruppenabgleich) ist damit jeder `group`-Zugriff dieser Datei gebunden. Die Klasse bleibt `beides`, weil ein zukuenftiger uebergreifender Lesezugriff (z. B. ein neuer Planer-Pfad) hier ebenso legitim waere wie bei `ldapConfig` unten — nicht, weil heute noch ein ungebundener Zugriff bestuende. |
| apps/api/src/ldap/ldap.service.ts | groupMembership | muss-mandantengebunden | gebunden | Kein eigenes `tenantId`, RLS ueber Join auf `Group`. `syncGroupMembershipsForTenant` laeuft vollstaendig ueber `forTenant()` (Plan 16-03/16-05) — von der alten, nur `this.prisma.*` erkennenden Suche nie erfasst, weil bereits gebunden; die erweiterte Erkennung aus Aufgabe 2 (260909-ipc) macht diese Fundstelle erstmals sichtbar. |
| apps/api/src/ldap/ldap.service.ts | ldapConfig | beides | gebunden | Die `lastSyncAt`-Fortschreibung am Ende von `syncUsersForTenant` ist mit Aufgabe 3 (260909-ipc) auf den in derselben Methode bereits vorhandenen `forTenant()`-Client umgestellt — es entsteht kein zweiter. Damit ist der einzige `ldapConfig`-Zugriff dieser Datei gebunden. |
| apps/api/src/ldap/ldap.service.ts | user | beides | gemischt | Mit Aufgabe 3 (260909-ipc) sind `upsertMappedUser` (Identitaetssuche und Aktualisierung), `searchUsers` (die "bereits importiert"-Markierung), `importUsersByDn` (Dedup und ldapDn-Nachtrag) und die Deaktivierungsschleife in `syncUsersForTenant` auf `forTenant()` umgestellt. `resolveEmailForWrite` bleibt ausdruecklich UNGEBUNDEN (Befund A, T-IPC-04): `email`/`username` sind plattformweit eindeutig, eine mandantengebundene Suche saehe einen fremden Halter nicht mehr und meldete faelschlich "frei" — die geloeste Klasse waere `muss-mandantengebunden` gewesen, bleibt wegen dieser einen bewusst uebergreifenden Abfrage `beides`. Der Loeschzweig um `syncBoundGroupsForTenant` (WINDOWS #20) ist bereits seit Etappe 1 gebunden und war nie Teil dieses Befunds. |
| apps/api/src/module-registry/module-access.service.ts | module | keine-mandantengebundene-tabelle | ungebunden | Modulkatalog ist plattformweit, kein `tenantId`. Bleibt bewusst ungebunden (260910-exd, Aufgabe 1, Befund E): keine Regel auf der Tabelle, eine Bindung waere heute wirkungslos, nicht katastrophal — katastrophal erst, WENN Etappe 3 dieser Tabelle eine Regel gibt. |
| apps/api/src/module-registry/module-access.service.ts | moduleGrant | muss-mandantengebunden | gebunden | Modulfreigaben je Mandant. Seit 260910-exd (Aufgabe 2) laufen Direktweg und Gruppenweg von `getAccessibleModuleIds` ueber `forTenant()`, EIN Klient je Methode. |
| apps/api/src/module-registry/module-access.service.ts | tenantModuleActivation | muss-mandantengebunden | gebunden | Aktivierung je Mandant, `tenantId`-Spalte vorhanden. Seit 260910-exd (Aufgabe 2) laufen Kurzschlusszweig, Schnittmengenabfrage und der eigene Lesezugriff von `getCatalogFlags` ueber `forTenant()`. |
| apps/api/src/module-registry/module-registry.service.ts | module | keine-mandantengebundene-tabelle | ungebunden | Modulkatalog ist plattformweit. Bleibt bewusst ungebunden (260910-exd, Aufgabe 1, Befund E): keine Regel auf der Tabelle, eine Bindung waere heute wirkungslos, nicht katastrophal — katastrophal erst, WENN Etappe 3 dieser Tabelle eine Regel gibt. `findBySlug` ist zusaetzlich die Stelle, die `ModuleGuard` bei JEDER Modulanfrage aufruft. |
| apps/api/src/module-registry/module-registry.service.ts | tenantModuleActivation | muss-mandantengebunden | gebunden | Aktivierung je Mandant. Seit 260910-exd (Aufgabe 3) laufen `findActiveForTenant`, `activateForTenant`, beide Zugriffe von `deactivateForTenant` (ueber EINEN Klienten) und `isModuleActive` ueber `forTenant()`, je Methode EIN Klient. |
| apps/api/src/settings/settings.service.ts | smtpConfig | muss-mandantengebunden | ungebunden | SMTP-Zugangsdaten je Mandant, `tenantId`-Spalte vorhanden. |
| apps/api/src/tenant/tenant.controller.ts | tenant | keine-mandantengebundene-tabelle | ungebunden | `Tenant` ist die Mandantentabelle selbst — hat keine eigene `tenantId`-Spalte, kann sie per Definition nicht haben (Migration 20260909140000, Gruppe b). Gemessen 260911-e2s (Aufgabe 1, Prüfung 1/2): keine Regel auf `Tenant` in irgendeiner der 34 ausgelieferten Migrationen einschließlich `20260910120000_rls_widen_membership_grant_and_platform_read`; gebunden und ungebunden liefern über Roh-SQL UND generierten Client dieselben Zeilen. |
| apps/api/src/tenant/tenant.controller.ts | user | muss-mandantengebunden | gebunden | Seit 260911-e2s (Aufgabe 3): `findAll`/`findOne`/`remove` zählen Benutzer je Mandant über drei gebundene Aufrufstellen (`tenantPrisma.user.count`, Fan-out-Muster aus `UserService.findAllForPlatformAdmin`) statt über den früheren Relationszähler (`include: { _count: { select: { users } } }`), der nach dem Scharfschalten unter der Regel von `User` unbemerkt null geliefert hätte (260911-e2s Aufgabe 1, Prüfungen 5-7). `where: { tenantId }` bleibt heute (Rolle mit BYPASSRLS, WINDOWS #18) der einzige wirksame Filter. |
| apps/api/src/tenant/tenant.service.ts | tenant | keine-mandantengebundene-tabelle | ungebunden | Dieselbe Begründung. Gemessen 260911-e2s (Aufgabe 1, Prüfung 1/2): keine Regel auf `Tenant` in irgendeiner der 34 ausgelieferten Migrationen einschließlich `20260910120000_rls_widen_membership_grant_and_platform_read`. |
| apps/api/src/tenders/adapters/email-alert.adapter.ts | tenderEmailConfig | bewusst-uebergreifend | ungebunden | `fetchTenders()` liest bewusst jede aktive `TenderEmailConfig`-Zeile über ALLE Mandanten in einer Abfrage (Plattform-Scheduler, ein Tick pro Postfach, D-13/D-01) — ausführlich im Dateikopf begründet, darf laut Kommentar niemals in `forTenant()` verpackt werden. |
| apps/api/src/tenders/adapters/rss.adapter.ts | tenderRssFeedSource | bewusst-uebergreifend | ungebunden | Fan-out über jeden aktiven Feed, plattformweit UND persönlich, in einer Abfrage (Zeilen 55–83 im Dateikopf begründet) — dieselbe Scheduler-Ebene wie beim E-Mail-Adapter. |
| apps/api/src/tenders/tender-dedup.service.ts | tender | keine-mandantengebundene-tabelle | ungebunden | Explizit im Dateikopf: "platform-global, RLS-exempt tables. Never wrap these queries in forTenant()." (D-03) |
| apps/api/src/tenders/tender-dedup.service.ts | tenderSource | keine-mandantengebundene-tabelle | ungebunden | Dieselbe Begründung. |
| apps/api/src/tenders/tender-digest.scheduler.ts | tenderMatch | beides | gemischt | Ein einziger globaler Cron-Job liest über ALLE Mandanten (bewusst übergreifend, Pitfall-1-Kommentar im Dateikopf) — seit 260909-laa (Aufgabe 3) bleibt die Kandidatenabfrage (`findMany` mit `distinct`) bewusst ungebunden, die Je-Treffer-Abfrage (`findMany` nach Mandant) und die Stempelung (`updateMany`) je Kandidatenzeile laufen über `forTenant()`. |
| apps/api/src/tenders/tender-digest.scheduler.ts | tenderNotificationPref | beides | gebunden | Präferenzen werden je Kandidatenzeile gelesen. Seit 260909-laa (Aufgabe 3) läuft der einzige Zugriff über `forTenant()`, gebunden an den Mandanten der jeweiligen Zeile. |
| apps/api/src/tenders/tender-digest.scheduler.ts | user | beides | gebunden | E-Mail-Adressen für den Versand werden je Kandidatenzeile gelesen. Seit 260909-laa (Aufgabe 3) läuft der einzige Zugriff über `forTenant()`. |
| apps/api/src/tenders/tender-email-config.service.ts | tenderEmailConfig | muss-mandantengebunden | gebunden | Nutzer-CRUD für die eigene Postfachanbindung (Phase 17, D-01) — anders als der Fan-out-Adapter oben, hier ist der Mandant aus der Anfrage bekannt. Seit 260909-laa (Aufgabe 2) laufen `getConfigForApi` (beide Lesezugriffe), `saveConfig` und `testConnection` vollständig über `forTenant()`. |
| apps/api/src/tenders/tender-fingerprint-backfill.service.ts | tender | keine-mandantengebundene-tabelle | ungebunden | Einmaliges Backfill-Skript über den plattformweiten `Tender`-Katalog (D-03). |
| apps/api/src/tenders/tender-ingestion.service.ts | tender | keine-mandantengebundene-tabelle | ungebunden | Explizit im Dateikopf: "Multi-tenant safety (D-03, T-10-09): uses the plain, non-tenant-scoped ... queries ... these are platform-global". |
| apps/api/src/tenders/tender-ingestion.service.ts | tenderSourcePollConfig | keine-mandantengebundene-tabelle | ungebunden | Plattformweiter Poll-Status, kein `tenantId` (Migration 20260909140000, Gruppe b). |
| apps/api/src/tenders/tender-matching.service.ts | tender | keine-mandantengebundene-tabelle | ungebunden | Liest den plattformweiten Katalog (D-03), um Treffer zu berechnen — kein `tenantId`. |
| apps/api/src/tenders/tender-matching.service.ts | tenderMatch | beides | gebunden | Sofortmeldung — siehe Abschnitt "Der Hintergrunddienst als Falle". Seit 260909-laa (Aufgabe 3) laufen sowohl die Treffer-Anlage (`upsert`) als auch der Instant-Dispatch (`findMany`/`updateMany`) vollständig über `forTenant()`, EIN gebundener Client je Profil. |
| apps/api/src/tenders/tender-matching.service.ts | tenderSavedSearch | beides | ungebunden | Gespeicherte Suchprofile ALLER Mandanten werden gegen neue Treffer geprüft — bewusst übergreifend, Etappe-3-Übergabe (260909-laa, Aufgabe 3, unverändert). |
| apps/api/src/tenders/tender-matching.service.ts | user | beides | gebunden | E-Mail-Adressen für die Sofortmeldung. Seit 260909-laa (Aufgabe 3) läuft der einzige Zugriff über `forTenant()`, gebunden an den Mandanten des jeweiligen Profils. |
| apps/api/src/tenders/tender-notification-pref.service.ts | tenderNotificationPref | muss-mandantengebunden | gebunden | Nutzer-CRUD für die eigenen Benachrichtigungseinstellungen — Mandant aus der Anfrage bekannt. Seit 260909-laa (Aufgabe 2) laufen `getForUser`/`setForUser` vollständig über `forTenant()`; `setForUser` übersetzt eine P2002-Verletzung (Befund F) in eine verständliche deutsche Meldung. |
| apps/api/src/tenders/tender-rss-feed.service.ts | tenderRssFeedSource | beides | gemischt | Nutzer-CRUD für die eigenen RSS-Quellen (anders als der Fan-out in `adapters/rss.adapter.ts`). Seit 260909-laa (Aufgabe 2) bindet `createForUser` (Zähler + Anlage, beide ausschließlich auf persönlichen Zeilen mit gesetztem Mandanten) über `forTenant()`. Seit 260910-jab (Aufgabe 2, WINDOWS #19 geschlossen) bindet zusätzlich `listForUser` — die neue Leseregel (`tenant_platform_read_policy`, 20260910120000_rls_widen_membership_grant_and_platform_read) schließt die plattformweite Zeile ausdrücklich ein, ungebunden hätte die Reparatur den Pfad sonst still auf nur die plattformweiten Zeilen reduziert (Befund F). `createPlatform`/`remove` bleiben bewusst ungebunden — beide Pfade lassen sich unter der Anwendungsrolle grundsätzlich nicht anlegen/entfernen, weil jede Schreibregel einen Mandanten verlangt (WINDOWS #24, eigener offener Punkt). Klasse `beides` bleibt korrekt: zwei gebundene, zwei bewusst ungebundene Zugriffe in derselben Datei. |
| apps/api/src/tenders/tender-saved-search.service.ts | tenderSavedSearch | muss-mandantengebunden | gebunden | Nutzer-CRUD für die eigenen gespeicherten Suchprofile — Mandant aus der Anfrage bekannt. Seit 260909-laa (Aufgabe 2) laufen `list`/`create`/`update`/`remove` vollständig über `forTenant()`. |
| apps/api/src/tenders/tender-scheduler.service.ts | tenderSourcePollConfig | keine-mandantengebundene-tabelle | ungebunden | Plattformweiter DÖE-Poll-Status, kein `tenantId` — im Dateikopf explizit als "genuine platform-wide singleton" begründet. |
| apps/api/src/tenders/tender-triage.service.ts | tenderTriage | muss-mandantengebunden | gebunden | Favorisierungs-/Ablehnungsstatus eines Nutzers, `tenantId`-Spalte vorhanden. Seit 260909-laa (Aufgabe 2) laufen `setTriage`/`listForUser`/`favoriteIds` vollständig über `forTenant()`. |
| 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 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 | 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
- ~~Ob Controller künftig über `req.tenantPrisma` statt eines erneuten
`forTenant()`-Aufrufs im Service gehen (offener Befund oben). Der Bereich
`ldap` (260909-ipc) hat sich für den Dienst-internen Weg entschieden —
`forTenant(this.prisma, tenantId)` wird in jeder umgestellten Methode neu
erzeugt, wie es die vier Bestandsstellen in `ldap.service.ts` und die drei
in `auth.service.ts` bereits vormachten. Der Bereich `groups` (260909-jts)
hat sich für denselben dienst-internen Weg entschieden — jede Methode in
`groups.service.ts` und `module-grants.service.ts` erzeugt ihren eigenen
`forTenant()`- bzw. `withTenantTransaction()`-Aufruf, gebundene Clients
werden nicht zwischen Methoden weitergereicht. Der Bereich `dashboard`
(260910-krx) hat sich für denselben dienst-internen Weg entschieden — jede
der neun umgestellten Methoden in `dashboard.service.ts` erzeugt ihren
eigenen `forTenant()`-Aufruf, wie alle sieben Bereiche vor ihm. Der Bereich
`calendar` (260911-cwh) hat sich für denselben dienst-internen Weg
entschieden — jede der sechs umgestellten Methoden in `calendar.service.ts`
erzeugt ihren eigenen `forTenant()`-Aufruf, wie alle acht Bereiche vor ihm.
Die Frage bleibt für alle übrigen Bereiche der Etappe 2 offen.~~
**Aufgelöst (260911-e2s):** die Frage ist für ALLE Bereiche entschieden,
nicht nur für `tenant` — dienst-intern, ein Klient je Methode, ist die
Konvention. Die Anfrageobjekt-Eigenschaft existiert nicht mehr:
`tenant.guard.ts` setzt nur noch `req.tenantId`, `tenant.middleware.ts`
(der nie verdrahtete Zwilling mit identischer Logik) ist gelöscht. Siehe
Abschnitt "Zwei belegte Befunde" oben und
`docs/mandantentrennung-etappe2-fehlerrichtung.md`, "## Bereich tenant",
(n4)(a).
- ~~Wie die WINDOWS-#19-Policy für `SearchProvider`/`TenderRssFeedSource`
am Ende genau lautet — nur, dass sie vor dem Scharfschalten gelöst sein
muss.~~ Aufgelöst (260910-jab): `TenderRssFeedSource` bekommt vier nach
Befehl getrennte Regeln, `SearchProvider` bleibt unverändert streng
(widerlegte Prämisse). Siehe Abschnitt "Zwei belegte Befunde" oben. Was
weiterhin offen ist: der Verwaltungsweg für plattformweite Zeilen unter
der Anwendungsrolle (WINDOWS #24).
- Die Reihenfolge und Zuschnitt der Etappe-2-Pläne — dafür ist die
Klassen-Verteilung oben der Arbeitsvorrat, siehe `<next_stages>` im
Plan `260909-eor-PLAN.md`.
- **Wie der Anmeldeweg unter je Mandant eindeutigen Anmeldenamen (Etappe-3-
Entscheidung (1)) den Mandanten VOR der Benutzersuche erfaehrt (260911-fh9).**
`auth_lookup_user_by_username(p_username)` stuetzt sich heute auf
`username @unique` plattformweit und braucht kuenftig
`(p_tenant_id, p_username)` — die drei SECURITY-DEFINER-Funktionen aus
Etappe 1 werden dabei ENGER (zwei Gleichheitsbedingungen statt einer),
nicht weiter; `local.strategy.ts` braucht dann eine Mandantenangabe VOR
der Suche. Die Bindung der drei Nach-Anmeldungs-Methoden dieses Bereichs
(`getMe`, `changePassword`, `adminResetPassword`) ist davon NEUTRAL — sie
haengt am Claim `tenantId` und an `User.id` (plattformweite UUID), nicht
an `username`/`email`. Siehe
`docs/mandantentrennung-etappe2-fehlerrichtung.md`, "## Bereich auth",
(h4)(a).