3336a6e419
tender-saved-search.service.ts, tender-triage.service.ts, tender-notification-pref.service.ts und tender-email-config.service.ts laufen jetzt vollstaendig ueber forTenant() — vier neue Parameter (list, update, remove, listForUser, favoriteIds, getForUser, getConfigForApi, testConnection bekommen tenantId), die anwendungsseitige userId-Filterung bleibt unveraendert (Befund E: die Policies haben keine Benutzerdimension). tender-rss-feed.service.ts bindet nur createForUser (Zaehler + Anlage, beide ausschliesslich auf persoenlichen Zeilen); listForUser, createPlatform und remove bleiben mit Codekommentar bewusst ungebunden (WINDOWS #19 — eine gebundene plattformweite Zeile waere unter jedem Mandanten unsichtbar, ein gebundenes Einfuegen ohne Mandant wuerde abgewiesen). tender-notification-pref.service.ts und tender-email-config.service.ts uebersetzen eine P2002-Verletzung auf dem tenantlosen upsert-Schluessel (Befund F) in eine verstaendliche deutsche Meldung statt eines rohen Fehlers. tenders.controller.ts reicht tenantId an den acht betroffenen Aufrufstellen durch extractTriageContext() durch (kein neuer Aufloesungsweg); die drei RSS-Aufrufstellen bleiben unveraendert, da ihre Dienstmethoden nicht binden. Alle sieben angefassten Testdateien bekommen den Zwei-Client-Nachweis (__makeBoundClient ueber demselben Speicher) und Bindungstests je umgestellter Methode; tender-rss-feed.service.spec.ts zusaetzlich den Gegentest, dass die drei unveraendert bleibenden Pfade forTenant() NICHT aufrufen. Falsifiziert: ein probeweiser Rueckbau der list()-Bindung in tender-saved-search.service.ts machte genau den erwarteten Bindungstest rot, danach zurueckgenommen. docs/mandantentrennung-zugriffsklassifikation.md: Stand der fuenf Paare auf gebunden bzw. gemischt nachgezogen; tenderRssFeedSource von muss-mandantengebunden auf beides umklassifiziert (derselbe Praezedenzfall wie ldapConfig in 260909-ipc). 761 Tests gruen (743 + 18 neue Bindungsnachweise), Typpruefung sauber, Wegwerf-Werkzeug 32/32, kein Schema-/Migrations-/Compose-/ Umgebungsdatei-Diff. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
266 lines
29 KiB
Markdown
266 lines
29 KiB
Markdown
# 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 | 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 | 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 | 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 `<next_stages>` im
|
||
Plan `260909-eor-PLAN.md`.
|