Files
tessera-ctl/docs/mandantentrennung-zugriffsklassifikation.md
T
schalli abb6c8bea3 feat(jts-03): module-grants.service.ts binden, beide Dokumente schliessen
Alle fuenf Methoden von ModuleGrantsService (assertTargetBelongsToTenant,
grant, revoke, getMatrix, getUserAccess) laufen jetzt ueber forTenant(); bei
den beiden Datenlieferungen teilen sich alle parallel abgesetzten
Teilabfragen denselben gebundenen Client. Die Mandanten-Gegenpruefung vor
jedem Erteilen bleibt ausdruecklich bestehen und bekommt einen Verweis auf
Befund F/T-JTS-03: die Regel auf ModuleGrant prueft nur die
Mandantenkennung der Zeile, nicht die referenzierte Gruppe. Der veraltete
Kommentar ueber der Mitgliedschaftsabfrage im Benutzer-Detail ("kein
forTenant hier") ist durch den neuen Stand ersetzt.

module-grants.service.spec.ts bekommt denselben Bindungsnachweis-Mock wie
groups.service.spec.ts (zwei unterscheidbare Clients ueber demselben
Speicher) und sechs neue Bindungsnachweise; alle 28 Bestandstests bleiben
gruen.

Beide Dokumente geschlossen: die Bereichsuebersicht fuer groups ist neu
gemessen (0 ungebunden, 31 gebunden — ein dokumentierter methodischer
Bodensatz, da die einfache Rohtrefferzaehlung die neun ueber `tx` gebundenen
Zugriffe innerhalb der drei Transaktionen nicht sieht), die
Klassen-Verteilung auf 62 Paare aktualisiert, und der als offen gefuehrte
Befund D aus dem ldap-Abschnitt der Fehlerrichtung ist mit Verweis auf
diesen Durchlauf als erledigt vermerkt (Nachtrag, nicht Neuschrieb). 743
Tests und die Typpruefung gruen; Schema, Migrationen und alle vier
Compose-Dateien unveraendert.

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

266 lines
28 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.
**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.<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 | 62 | 0 | unverändert |
| 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 | 21 | 0 | unverändert |
| 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** | **173** | **62** | Ungebunden: war 210 vor dieser Etappe (260909-ipc-Stand), Delta = die 37 in Aufgabe 2/3 (260909-jts) umgestellten `groups`-Rohtreffer. Gebunden: war 31, jetzt zusätzlich 31 in `groups` (Bodensatz — siehe Methodenhinweis oben) |
## 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.<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.
| Klasse | Anzahl Paare |
|---|---|
| muss-mandantengebunden | 33 |
| keine-mandantengebundene-tabelle | 16 |
| beides | 10 |
| 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): liest
`tenderMatch`/`tenderNotificationPref`/`user` bewusst über ALLE Mandanten
in einem `findMany` (ein einziger globaler Cron-Job, kein Mandant im
Job selbst — so von Anfang an entworfen, Pitfall 1 in den Kommentaren der
Datei). Innerhalb der Verteilung je Treffer ist der Mandant aus der Zeile
bekannt und der Versand muss darauf gebunden laufen.
- **`tender-matching.service.ts`** (Ausschreibungs-Sofortmeldung): dieselbe
Form — `tenderMatch`/`tenderSavedSearch`/`user` werden für die
Sofort-Benachrichtigung über alle betroffenen Mandanten hinweg gelesen,
der Versand je Treffer ist mandantengebunden.
## 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.
| 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 | ungebunden | DKV-Rechnungshistorie je Mandant, `tenantId`-Spalte vorhanden. |
| apps/api/src/dkv/dkv.service.ts | dkvModuleConfig | muss-mandantengebunden | ungebunden | Postfach-/Zugangsdaten des DKV-Moduls je Mandant. |
| apps/api/src/dkv/dkv.service.ts | dkvVehicleMaster | muss-mandantengebunden | ungebunden | Fahrzeugstammdaten des DKV-Moduls je Mandant. |
| 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) — 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 | ungebunden | Ein einziger globaler Cron-Job liest über ALLE Mandanten (bewusst übergreifend, Pitfall-1-Kommentar im Dateikopf), der Versand je Treffer ist an dessen Mandanten gebunden. |
| apps/api/src/tenders/tender-digest.scheduler.ts | tenderNotificationPref | beides | ungebunden | Dieselbe Begründung — Präferenzen werden über alle Mandanten gelesen, aber je Zeile mandantenbezogen ausgewertet. |
| apps/api/src/tenders/tender-digest.scheduler.ts | user | beides | ungebunden | E-Mail-Adressen für den Versand werden über alle Mandanten gelesen, der eigentliche Versand ist je Treffer mandantengebunden. |
| apps/api/src/tenders/tender-email-config.service.ts | tenderEmailConfig | muss-mandantengebunden | ungebunden | 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. |
| 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 | ungebunden | Sofortmeldung: Treffer über alle betroffenen Mandanten gelesen, Versand je Treffer mandantengebunden — siehe Abschnitt "Der Hintergrunddienst als Falle". |
| apps/api/src/tenders/tender-matching.service.ts | tenderSavedSearch | beides | ungebunden | Dieselbe Begründung — gespeicherte Suchprofile aller Mandanten werden gegen neue Treffer geprüft, der Versand ist je Profil mandantengebunden. |
| apps/api/src/tenders/tender-matching.service.ts | user | beides | ungebunden | Dieselbe Begründung — E-Mail-Adressen für die Sofortmeldung. |
| apps/api/src/tenders/tender-notification-pref.service.ts | tenderNotificationPref | muss-mandantengebunden | ungebunden | Nutzer-CRUD für die eigenen Benachrichtigungseinstellungen — Mandant aus der Anfrage bekannt. |
| apps/api/src/tenders/tender-rss-feed.service.ts | tenderRssFeedSource | muss-mandantengebunden | ungebunden | Nutzer-CRUD für die eigenen RSS-Quellen (anders als der Fan-out in `adapters/rss.adapter.ts`) — Mandant aus der Anfrage bekannt. WINDOWS #19 betrifft die nullbaren plattformweiten Zeilen, nicht diesen CRUD-Pfad. |
| apps/api/src/tenders/tender-saved-search.service.ts | tenderSavedSearch | muss-mandantengebunden | ungebunden | Nutzer-CRUD für die eigenen gespeicherten Suchprofile — Mandant aus der Anfrage bekannt. |
| 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 | ungebunden | Favorisierungs-/Ablehnungsstatus eines Nutzers, `tenantId`-Spalte vorhanden. |
| 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 `<next_stages>` im
Plan `260909-eor-PLAN.md`.