# 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. **WINDOWS #19 — nullbares `tenantId` bei `SearchProvider` und `TenderRssFeedSource`.** Beide Modelle tragen ein nullbares `tenantId` (`SearchProvider` für admin-gepflegte Vorgabe-Suchmaschinen, wobei laut 05-02-Entscheidung die tatsächlichen Vorgaben als Konstanten und nicht als DB-Zeilen mit `tenantId = NULL` geführt werden — die Spalte ist nullbar, ob es heute tatsächlich `NULL`-Zeilen gibt, ist damit eine offene Frage für Etappe 3, nicht eine hier beantwortete; `TenderRssFeedSource` für plattformweite RSS-Quellen wie den geseedeten `service.bund.de`-Feed, D-06). Die aktuelle Policy `"tenantId" = current_tenant_id()` vergleicht `NULL` nie gleich — nach dem Scharfschalten wären plattformweite Zeilen für JEDEN Mandanten unsichtbar, nicht nur für fremde. Das ist heute ohne Wirkung (Schalter aus, #18), muss aber in Etappe 3 zusammen mit den restlichen Fundstellen gelöst werden: die Policy braucht für den Lesezugriff `tenantId IS NULL OR tenantId = current_tenant_id()`, während Schreibzugriffe weiterhin einen Mandanten verlangen. ## Übersicht je Bereich (Zeilentreffer je Bereich, ungebunden vs. gebunden) **Wichtig, seit 260909-ipc (Aufgabe 3):** die Spalte "Ungebunden" zählt NUR noch `this.prisma.`-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.`, Konvention dieses Codes — siehe `rls-access-inventory.spec.ts` für die allgemeinere, namensunabhängige Erkennung über die `const = 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/ | grep -v spec | wc -l` bzw. `grep -ro "tenantPrisma\.[a-zA-Z]*\." apps/api/src/ | 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 | 36 | 26 | **war 62/0** — Aufgabe 2/3 (260909-laa) haben die fünf Nutzer-CRUD-Dienste (vier vollständig, `tender-rss-feed.service.ts` teilweise mit im Code begründeter WINDOWS-#19-Grenze) und die Je-Treffer-Hälften der beiden Hintergrunddienste (`tender-digest.scheduler.ts`, `tender-matching.service.ts`) auf `forTenant()` umgestellt. Die 36 verbleibenden ungebundenen Treffer sind die zwölf bewusst nicht angefassten Paare (D-03-Katalog, zwei Fan-out-Adapter) plus die drei bewusst ungebundenen RSS-Pfade 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 | 17 | 0 | unverändert | | 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 | | calendar | 12 | 0 | unverändert | | 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 | ## Klassen-Verteilung (nach (Datei, Modell)-Fundstellen, 62 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.` noch ueber `.` 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. | Klasse | Anzahl Paare | |---|---| | muss-mandantengebunden | 32 | | keine-mandantengebundene-tabelle | 16 | | beides | 11 | | bewusst-uebergreifend | 3 | | **Summe** | **62** | ## Der Hintergrunddienst als Falle — drei `beides`-Fälle Ein Planer, der über alle Mandanten iteriert, liest zu Recht übergreifend — muss aber *innerhalb* der Schleife je Mandant binden. Drei Dateien sind betroffen: - **`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. ## Bestandsaufnahme Maschinell ermittelt, `rls-access-inventory.spec.ts` hält Vollständigkeit nach. Spalten: Datei, Modell (Prisma-Modellname wie in `this.prisma.` oder — seit 260909-ipc, Befund G — in `.` verwendet), Klasse, Stand (`gebunden`/`ungebunden`/`gemischt`, seit 260909-ipc maschinell gegen den Quelltext geprüft), Begründung. | 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 | gemischt | `getMe`, `changePassword`, `adminResetPassword` suchen über die Benutzerkennung aus dem Sitzungsnachweis — der Mandant ist dort bereits bekannt (Aufgabe 2 fasst sie bewusst nicht an, siehe SUMMARY). | | apps/api/src/calendar/calendar.service.ts | calendarSource | muss-mandantengebunden | ungebunden | Kalenderquellen eines Nutzers, `tenantId`-Spalte vorhanden. | | apps/api/src/dashboard/dashboard.service.ts | dashboardLayout | muss-mandantengebunden | ungebunden | Widget-Anordnung eines Nutzers, `tenantId`-Spalte vorhanden. | | apps/api/src/dashboard/dashboard.service.ts | module | keine-mandantengebundene-tabelle | ungebunden | Modulkatalog ist plattformweit, kein `tenantId` (Migration 20260909140000, Gruppe b). | | apps/api/src/dashboard/dashboard.service.ts | searchProvider | muss-mandantengebunden | ungebunden | `tenantId` nullbar (WINDOWS #19) — heutige, tatsächlich gespeicherte Zeilen sind nutzerangelegt und tragen einen Mandanten; Vorgabe-Anbieter kommen laut 05-02 aus Konstanten, nicht aus der DB. | | apps/api/src/dashboard/dashboard.service.ts | widgetInstance | muss-mandantengebunden | ungebunden | Platzierte Dashboard-Widgets eines Nutzers, `tenantId`-Spalte vorhanden. | | 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.` noch `.` 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) — die Regel auf `GroupMembership` prueft nachweislich nur die Gruppenseite. | | 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, weil die Regel auf dieser Tabelle nur die Mandantenkennung der Zeile prüft, nicht die referenzierte Gruppe (Befund F, T-JTS-03, Aufgabe 1). | | 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`. | | apps/api/src/module-registry/module-access.service.ts | moduleGrant | muss-mandantengebunden | ungebunden | Modulfreigaben je Mandant. | | apps/api/src/module-registry/module-access.service.ts | tenantModuleActivation | muss-mandantengebunden | ungebunden | Aktivierung je Mandant, `tenantId`-Spalte vorhanden. | | apps/api/src/module-registry/module-registry.service.ts | module | keine-mandantengebundene-tabelle | ungebunden | Modulkatalog ist plattformweit. | | apps/api/src/module-registry/module-registry.service.ts | tenantModuleActivation | muss-mandantengebunden | ungebunden | Aktivierung je Mandant. | | 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). | | apps/api/src/tenant/tenant.service.ts | tenant | keine-mandantengebundene-tabelle | ungebunden | Dieselbe Begründung. | | 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()`; `listForUser`/`createPlatform`/`remove` bleiben bewusst ungebunden, weil sie (auch) die nullbare, plattformweite Zeile berühren (WINDOWS #19, Aufgabe 1 gemessen: eine gebundene Zeile wäre unter jedem Mandanten unsichtbar bzw. ein gebundenes Einfügen ohne Mandant würde abgewiesen). Korrektur der Klasse von `muss-mandantengebunden` auf `beides`, keine Verhaltensänderung — derselbe Präzedenzfall wie `ldapConfig` in 260909-ipc. | | 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 — `Tenant` hat keine `tenantId`-Spalte. | | apps/api/src/user/admin-seed.service.ts | user | bewusst-uebergreifend | ungebunden | Erstanlage des Administrators beim ersten Start: läuft einmalig beim Boot, BEVOR irgendein Mandantenkontext existiert, um den allerersten Mandanten samt Admin-Nutzer anzulegen — es gibt zu diesem Zeitpunkt strukturell keinen Mandanten, an den gebunden werden könnte. | | apps/api/src/user/user.controller.ts | user | muss-mandantengebunden | ungebunden | Nutzerverwaltung innerhalb des Mandanten des anfragenden Admins. | | apps/api/src/user/user.service.ts | user | muss-mandantengebunden | ungebunden | Dieselbe Begründung. | ## 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. Die Frage bleibt für alle übrigen Bereiche der Etappe 2 offen. - Wie die WINDOWS-#19-Policy für `SearchProvider`/`TenderRssFeedSource` am Ende genau lautet — nur, dass sie vor dem Scharfschalten gelöst sein muss. - Die Reihenfolge und Zuschnitt der Etappe-2-Pläne — dafür ist die Klassen-Verteilung oben der Arbeitsvorrat, siehe `` im Plan `260909-eor-PLAN.md`.