e0e163ec63
- docs/mandantentrennung-zugriffsklassifikation.md: Uebersichtszeile calendar auf 0/12 gezogen (war 12/0, kein ungebundener Rest — erster Bereich in Folge ohne begruendeten Rest), Summenzeile auf 83/159 fortgeschrieben, Klassen-Verteilung unveraendert mit ausdruecklichem Stand-260911-cwh-Vermerk, Hintergrunddienst-Abschnitt um den Sonderfall refreshCacheInBackground (abgekoppelte Fortsetzung, kein sechster Fall) ergaenzt, Abschnitt "Was diese Etappe NICHT entscheidet" um calendar ergaenzt - .planning/WINDOWS.md: neuer offener Eintrag (#26) zur lautlosen Auspraegung der umgekehrten Fehlerrichtung im Bereich calendar, ueber gsd-tools windows append angelegt - Beide Falsifizierungsnachweise durchgefuehrt (falscher Stand macht rls-access-inventory.spec.ts rot; falsche Uebersichtszahl macht das herleitende Gate rot), zurueckgenommen - Etappe 2, neunter Bereich (calendar) abgeschlossen: alle zwoelf Zugriffe gebunden, Baseline gehalten (883 Tests, 57 Dateien, 101 Werkzeugpruefungen, Typpruefung sauber) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
439 lines
53 KiB
Markdown
439 lines
53 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` — GESCHLOSSEN (260910-jab, Aufgabe 1/2).** Beide Modelle
|
||
tragen ein nullbares `tenantId` (`SearchProvider` für admin-gepflegte
|
||
Vorgabe-Suchmaschinen; `TenderRssFeedSource` für plattformweite RSS-Quellen
|
||
wie den geseedeten `service.bund.de`-Feed, D-06). Die ausgelieferte Policy
|
||
`"tenantId" = current_tenant_id()` verglich `NULL` nie gleich — nach dem
|
||
Scharfschalten wären plattformweite Zeilen für JEDEN Mandanten unsichtbar
|
||
gewesen, nicht nur für fremde.
|
||
|
||
Geschlossen durch Migration
|
||
`20260910120000_rls_widen_membership_grant_and_platform_read`, lokal
|
||
angewandt und gegen den Systemkatalog der lebenden Datenbank gemessen:
|
||
`TenderRssFeedSource` bekommt vier nach Befehl getrennte Regeln —
|
||
`tenant_platform_read_policy` (SELECT) schließt Zeilen ohne Mandant
|
||
ausdrücklich ein, `tenant_insert_policy`/`tenant_update_policy`/
|
||
`tenant_delete_policy` verlangen weiterhin ausnahmslos einen Mandanten. Die
|
||
Trennung nach Befehl ist notwendig, weil ein einzelner `USING`-Ausdruck auch
|
||
bestimmt, welche Zeilen `UPDATE`/`DELETE` erreichen — eine Leseregel, die
|
||
plattformweite Zeilen einschließt, hätte ohne Trennung jedem Mandanten auch
|
||
das Ändern/Entfernen dieser Zeilen erlaubt.
|
||
|
||
Die Hälfte zur Suchanbietertabelle (`SearchProvider`) schließt NICHT als
|
||
gelöstes Problem, sondern als **widerlegte Prämisse**: lokal gemessen gibt es
|
||
keinen Codeweg, der eine mandantenlose `SearchProvider`-Zeile erzeugt — der
|
||
einzige Schreibweg (`dashboard.service.ts`) verlangt die Mandantenkennung als
|
||
Pflichtparameter, und die Vorgabe-Suchmaschinen kommen laut 05-02-Entscheidung
|
||
aus Konstanten, nicht aus der Datenbank. Die Regel bleibt deshalb bewusst
|
||
unverändert streng; eine Lockerung wäre hier die falsche Richtung, weil sie
|
||
eine künftige mandantenlose Zeile jedem Mandanten zeigen würde.
|
||
|
||
Was diese Reparatur NICHT löst: unter der Anwendungsrolle lässt sich eine
|
||
plattformweite `TenderRssFeedSource`-Zeile weder anlegen noch entfernen, in
|
||
der alten wie in der neuen Regel, weil jede Schreibregel einen Mandanten
|
||
verlangt. Als eigener offener Ledger-Eintrag festgehalten (WINDOWS #24),
|
||
damit dieser Rest nicht mit #19 verschwindet.
|
||
|
||
## Übersicht je Bereich (Zeilentreffer je Bereich, ungebunden vs. gebunden)
|
||
|
||
**Wichtig, seit 260909-ipc (Aufgabe 3):** die Spalte "Ungebunden" zählt NUR
|
||
noch `this.prisma.<Modell>`-Rohtreffer — eine unveränderte Spaltenüberschrift
|
||
über einer veränderten Bedeutung wäre die nächste stille Falle, seit ein
|
||
Bereich (`ldap`) tatsächlich gebundene Zugriffe hat, die aus dieser Zählung
|
||
verschwinden. Die neue Spalte "Gebunden" zählt daneben die
|
||
`forTenant()`-gebundenen Rohtreffer (`tenantPrisma.<Modell>`, Konvention
|
||
dieses Codes — siehe `rls-access-inventory.spec.ts` für die allgemeinere,
|
||
namensunabhängige Erkennung über die `const <Name> = forTenant(`-Zuweisungsform).
|
||
Beide Spalten sind Rohtreffer (mehrere Vorkommen desselben Modells in
|
||
derselben Datei zählen mehrfach), nicht (Datei, Modell)-Paare wie in der
|
||
Bestandsaufnahme unten.
|
||
|
||
Gemessen mit
|
||
`grep -ro "this\.prisma\.[a-zA-Z]*" apps/api/src/<bereich> | grep -v spec | wc -l`
|
||
bzw. `grep -ro "tenantPrisma\.[a-zA-Z]*\." apps/api/src/<bereich> | grep -v spec | wc -l`
|
||
am 2026-09-09, **nach** den Änderungen aus Aufgabe 2/3 dieses Plans (260909-jts):
|
||
|
||
**Methodische Lücke, seit 260909-jts sichtbar:** die zweite Zählung sucht
|
||
ausschließlich den Namen `tenantPrisma` — die Konvention, die `ldap` und
|
||
(bis auf die drei Transaktionen) auch `groups` verwenden. Die drei
|
||
`withTenantTransaction()`-Aufrufe in `groups.service.ts` binden zusätzliche
|
||
neun Modellzugriffe über den Namen `tx` (den Transaktionsparameter), die
|
||
diese einfache Rohtrefferzählung strukturell NICHT sieht — anders als die
|
||
maschinelle, namensunabhängige Erkennung in `rls-access-inventory.spec.ts`
|
||
(Befund B), die auch diese Form erfasst. Die Zahl 31 unten ist deshalb der
|
||
Bodensatz, nicht die vollständige Zahl gebundener Zugriffe in `groups`; die
|
||
Bestandsaufnahme unten (Spalte "Stand", je (Datei, Modell)-Paar) ist die
|
||
autoritative Quelle.
|
||
|
||
| Bereich | Ungebunden | Gebunden | Hinweis |
|
||
|---|---|---|---|
|
||
| tenders | 35 | 27 | **war 62/0**, dann 36/26 nach 260909-laa — 260910-jab (Aufgabe 2) hat `tender-rss-feed.service.ts`/`listForUser` zusätzlich auf `forTenant()` umgestellt (WINDOWS #19 geschlossen, Befund F: ungebunden hätte die Reparatur den Pfad sonst still auf nur die plattformweiten Zeilen reduziert): ein Rohtreffer wandert von ungebunden nach gebunden (36→35, 26→27). Die 35 verbleibenden ungebundenen Treffer sind die zwölf bewusst nicht angefassten Paare (D-03-Katalog, zwei Fan-out-Adapter) plus die zwei bewusst ungebundenen RSS-Pfade (`createPlatform`/`remove`, WINDOWS #24) plus die übergreifenden Hälften der beiden Hintergrunddienste (Etappe-3-Übergabe) |
|
||
| groups | 0 | 31 | **war 37/0** — Aufgabe 2/3 (260909-jts) haben `groups.service.ts` (12 Methoden) und `module-grants.service.ts` (5 Methoden) vollständig auf `forTenant()`/`withTenantTransaction()` umgestellt. Die neun zusätzlichen, über `tx` gebundenen Zugriffe innerhalb der drei Transaktionen zählt dieses einfache Muster nicht mit (siehe Methodenhinweis oben) |
|
||
| ldap | 4 | 26 | **war 21/0** — Aufgabe 2/3 (260909-ipc) haben `ldap-config.service.ts` (5 Methoden) und `ldap.service.ts` (6 Methoden, 11 Abfragen) auf `forTenant()` umgestellt. Die 4 verbleibenden ungebundenen Treffer sind bewusst: `getAllActiveConfigs`/`onApplicationBootstrap` (Befund B) und `resolveEmailForWrite` (Befund A, T-IPC-04) |
|
||
| dkv | 1 | 22 | **war 21/0** — Aufgabe 2/3 (260909-mir) haben `dkv.service.ts` vollständig auf `forTenant()` umgestellt: Konfigurationspfade (`loadConfig`, `getConfigForApi`, `saveConfig`, `testConnection`), Historie, Fahrzeugstammdaten und der neue Besitzriegel vor dem Ausfuhrdatei-Download. Gebunden sind es 22 statt 21, weil der Riegel einen zusätzlichen Lesezugriff auf `dkvInvoiceHistory` einführt (T-MIR-03). Der eine verbleibende ungebundene Treffer ist der benannte Planer-Startpfad `loadAnyActiveConfigForScheduler()` (Befund D, WINDOWS #21) — bewusst, mit dreifacher Markierung |
|
||
| user | 8 | 14 | **war 17/0** — Aufgabe 2/3 (260910-das) haben `user.service.ts` (`findById`/`create`/`update`/`deactivate`/`delete` sowie die zwei neuen Plattform-Administratorsicht-Methoden), `admin-seed.service.ts` (Erstanlage des Administrators) und `user.controller.ts` (Benutzerliste des ADMIN-Zweigs, alle drei Kennungswege ueber die Dienstmethoden, alle fuenf Selbstbedienungszugriffe) auf `forTenant()` umgestellt. Die 8 verbleibenden ungebundenen Rohtreffer sind bewusst: `findByUsername` in `user.service.ts` (plattformweit eindeutiger Schluessel, derselbe Fall wie `resolveEmailForWrite` im Bereich `ldap`), die Erstanlage-Pruefung und beide Zugriffe auf `tenant` in `admin-seed.service.ts`, sowie der neue Schleifentreiber `this.prisma.tenant.findMany` der beiden Plattform-Administratorsicht-Methoden in `user.service.ts` (`Tenant` traegt keinen Zeilenschutz) |
|
||
| module-registry | 7 | 10 | **war 17/0** — Aufgabe 2/3 (260910-exd) haben `module-access.service.ts` (`getAccessibleModuleIds`: Kurzschlusszweig, Direktweg, Gruppenweg, Schnittmenge; `getCatalogFlags`: eigener Aktivierungs-Lesezugriff) und `module-registry.service.ts` (`findActiveForTenant`, `activateForTenant`, `deactivateForTenant`, `isModuleActive`) auf `forTenant()` umgestellt. Die 7 verbleibenden ungebundenen Rohtreffer sind bewusst: der eine Katalogzugriff in `module-access.service.ts` (`findAccessibleModules`) und die sechs Katalogzugriffe in `module-registry.service.ts` (`findAll`, `findBySlug`, die beiden Katalog-Existenzpruefungen in `activateForTenant`/`deactivateForTenant`, die Katalogsuche in `isModuleActive`, `seedModule`) — der Modulkatalog (`Module`) traegt heute keinen Zeilenschutz, eine Bindung waere heute wirkungslos, nicht katastrophal; katastrophal wuerde sie erst, WENN Etappe 3 dieser Tabelle eine Regel gibt (Befund E) |
|
||
| dashboard | 1 | 12 | **war 13/0** — Aufgabe 2/3 (260910-krx) haben `dashboard.service.ts` vollständig umgestellt: `getLayout`/`saveLayout` (gemeinsam gebunden), `getWidgets`/`addWidget`/`updateWidgetConfig`/`removeWidget` sowie `getSearchProviders`/`addSearchProvider`/`removeSearchProvider` laufen über `forTenant()`, je Methode ein Klient. Der eine verbleibende ungebundene Rohtreffer ist bewusst: der Modulkatalog (`Module`) trägt heute keinen Zeilenschutz, eine Bindung wäre heute wirkungslos, nicht katastrophal — katastrophal würde sie erst, WENN Etappe 3 dieser Tabelle eine Regel gibt (Befund E aus `module-registry`, hier übernommen) |
|
||
| auth | 8 | 5 | unverändert gegenüber dem in Etappe 1 (260909-eor) gemessenen Stand |
|
||
| calendar | 0 | 12 | **war 12/0** — Aufgabe 2 (260911-cwh) hat `calendar.service.ts` vollständig auf `forTenant()` umgestellt: `getSources`, `addSource`, beide Abfragen von `updateSource`/`deleteSource`, alle drei Abfragen von `testConnection`, Laden plus beide Synchronstatus-Rückschreibungen von `fetchAndCacheEvents` — je Methode ein Klient. Anders als bei den sieben Bereichen davor bleibt KEIN ungebundener Rest übrig: `CalendarSource` trägt eine Pflicht-Mandantenkennung, und kein Pfad dieses Bereichs liest über Mandanten hinweg |
|
||
| tenant | 8 | 0 | unverändert |
|
||
| favorites | 7 | 0 | unverändert |
|
||
| settings | 4 | 0 | unverändert |
|
||
| **Summe** | **83** | **159** | Ungebunden: war 118 nach 260910-das, dann 108 nach 260910-exd (module-registry 17→7), dann 107 nach 260910-jab (`tenders` 36→35, `listForUser` gebunden), dann 95 nach 260910-krx (`dashboard` 13→1), jetzt 83 nach 260911-cwh (`calendar` 12→0). Gebunden: war 124, dann 134 nach 260910-exd (zusätzlich 10 in `module-registry`), dann 135 nach 260910-jab (zusätzlich 1 in `tenders`), dann 147 nach 260910-krx (zusätzlich 12 in `dashboard`), jetzt 159 nach 260911-cwh (zusätzlich 12 in `calendar`). 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, 63 Paare)
|
||
|
||
Stand 260909-jts (Aufgabe 3): 61 Paare aus dem vorherigen Durchlauf
|
||
(260909-ipc) plus ein bisher vollstaendig unsichtbares Paar
|
||
(`groups.service.ts`/`tenantModuleActivation`), das erst die um
|
||
Transaktionsparameter erweiterte Erkennung (Befund B, Aufgabe 2 dieses
|
||
Plans) sichtbar macht — der einzige Zugriff auf dieses Modell in dieser
|
||
Datei lief bis dahin ausschliesslich ueber den Rueckgabeparameter der
|
||
interaktiven Transaktion in `ensureDefaultGroup` und war weder ueber
|
||
`this.prisma.<Modell>` noch ueber `<gebundener Client>.<Modell>` erfassbar.
|
||
Die Zahl ist der Ausgabe der Pruefung in
|
||
`apps/api/src/prisma/rls-access-inventory.spec.ts` entnommen, nicht
|
||
geschaetzt.
|
||
|
||
**Stand 260909-laa (Aufgabe 2):** dieselben 62 Paare, keine neue Fundstelle
|
||
hinzugekommen oder verschwunden — nur EINE Klasse hat sich verschoben:
|
||
`tender-rss-feed.service.ts`/`tenderRssFeedSource` wechselt von
|
||
`muss-mandantengebunden` auf `beides` (WINDOWS #19 — `createForUser` bindet,
|
||
`listForUser`/`createPlatform`/`remove` bleiben bewusst uebergreifend), der
|
||
exakte Praezedenzfall aus `ldapConfig` in 260909-ipc.
|
||
|
||
**Stand 260910-das (Aufgabe 3):** 63 Paare — 62 aus dem vorherigen
|
||
Durchlauf plus EIN neues Paar (`user.service.ts`/`tenant`, Klasse
|
||
`keine-mandantengebundene-tabelle`, Stand `ungebunden`): der Schleifentreiber
|
||
der neu eingefuehrten Plattform-Administratorsicht (Befund F/N, Aufgabe 1/2).
|
||
Zusaetzlich ZWEI Klassenkorrekturen, jede mit eigener Begruendung an der
|
||
Fundstelle in der Bestandsaufnahme oben: `user.service.ts`/`user` wechselt
|
||
von `muss-mandantengebunden` auf `beides` — wortgleich derselbe
|
||
Praezedenzfall wie `ldap.service.ts`/`user` in 260909-ipc
|
||
(`resolveEmailForWrite`, hier `findByUsername`); `admin-seed.service.ts`/`user`
|
||
wechselt von `bewusst-uebergreifend` auf `beides`, weil die bisherige
|
||
Begruendung nachweislich falsch war (Befund J: der Mandant ist bei der
|
||
Erstanlage des Administrators bereits bekannt, nicht strukturell fehlend).
|
||
Alle drei Zahlen sind der Ausgabe von `rls-access-inventory.spec.ts`
|
||
entnommen, nicht geschaetzt.
|
||
|
||
**Stand 260910-krx (Aufgabe 3): unveraendert, ausdruecklich festgehalten
|
||
statt uebersprungen.** Weiterhin 63 Paare, keine Klasse verschoben sich.
|
||
Die vier Paare des Bereichs `dashboard`
|
||
(`dashboard.service.ts`/`dashboardLayout`, `/module`, `/searchProvider`,
|
||
`/widgetInstance`) waren bereits vor diesem Durchlauf korrekt klassifiziert
|
||
(drei `muss-mandantengebunden`, eines `keine-mandantengebundene-tabelle`) —
|
||
dieser Plan aendert nur ihre `Stand`-Spalte (`ungebunden` auf `gebunden`
|
||
fuer drei der vier Paare), keine ihrer Klassen. Eine unveraenderte Tabelle
|
||
ohne diesen Vermerk waere von einer vergessenen Nachziehung nicht zu
|
||
unterscheiden — deshalb steht die Abwesenheit einer Aenderung hier
|
||
ausdruecklich, statt stillschweigend uebersprungen zu werden.
|
||
|
||
**Stand 260911-cwh (Aufgabe 3): unveraendert, ausdruecklich festgehalten
|
||
statt uebersprungen.** Weiterhin 63 Paare, keine Klasse verschoben sich. Das
|
||
eine Paar des Bereichs `calendar` (`calendar.service.ts`/`calendarSource`)
|
||
war bereits vor diesem Durchlauf korrekt klassifiziert (`muss-mandantengebunden`)
|
||
— Aufgabe 2 (260911-cwh) aendert nur seine `Stand`-Spalte (`ungebunden` auf
|
||
`gebunden`), nicht seine Klasse. Eine unveraenderte Tabelle ohne diesen
|
||
Vermerk waere von einer vergessenen Nachziehung nicht zu unterscheiden —
|
||
deshalb steht die Abwesenheit einer Aenderung hier ausdruecklich, statt
|
||
stillschweigend uebersprungen zu werden.
|
||
|
||
| Klasse | Anzahl Paare |
|
||
|---|---|
|
||
| muss-mandantengebunden | 31 |
|
||
| keine-mandantengebundene-tabelle | 17 |
|
||
| beides | 13 |
|
||
| bewusst-uebergreifend | 2 |
|
||
| **Summe** | **63** |
|
||
|
||
## Der Hintergrunddienst als Falle — fünf Fälle
|
||
|
||
Ein Planer, der über alle Mandanten iteriert, liest zu Recht übergreifend —
|
||
muss aber *innerhalb* der Schleife je Mandant binden. Vier Dateien sind in
|
||
diesem Sinne `beides`-Fälle, davon einer (260910-das) der bislang EINZIGE,
|
||
der auf BEIDEN Hälften bereits richtig ist; der fünfte, seit 260909-mir
|
||
bekannte Fall ist von anderer Art und deshalb unten getrennt aufgeführt — er
|
||
iteriert gar nicht, sondern greift sich eine beliebige Zeile heraus:
|
||
|
||
- **`ldap.service.ts`** (AD-Abgleich) — **Stand 260909-ipc, Aufgaben 2/3:
|
||
geschlossen.** Iteriert nicht selbst über alle Mandanten (der Sync läuft
|
||
je Aufruf für einen übergebenen Mandanten). Vor dieser Etappe blieben
|
||
Lesezugriffe auf `group`, `ldapConfig` und `user` innerhalb der
|
||
Sync-Methoden teils ungebunden, obwohl der Mandant zu diesem Zeitpunkt
|
||
bereits bekannt war — nur 4 der inzwischen 11 `forTenant()`-Aufrufstellen
|
||
deckten die Lese-/Schreibpfade ab. Jetzt sind `group` und `ldapConfig`
|
||
vollständig gebunden; bei `user` bleibt ausschließlich
|
||
`resolveEmailForWrite` bewusst ungebunden (Befund A, T-IPC-04 — siehe
|
||
Bestandsaufnahme unten). Der als gefährlichster Punkt benannte
|
||
Löschzweig (`syncBoundGroupsForTenant`, WINDOWS #20) war bereits seit
|
||
Etappe 1 gebunden. **Die seinerzeit offen geführte Reihenfolgebedingung
|
||
für Etappe 4 — die Übergabe unmittelbar vor der Löschung
|
||
(`this.groupsService.reassignDefaultBeforeDelete(tenantId, group.id)` und
|
||
`ensureDefaultGroup(tenantId)` in `groups.service.ts`) — ist mit
|
||
260909-jts (Aufgabe 2) GESCHLOSSEN:** beide Methoden laufen seither über
|
||
`forTenant()`/`withTenantTransaction()` (siehe Bestandsaufnahme unten,
|
||
`groups.service.ts`/`group`, Stand `gebunden`). Nachtrag, nicht
|
||
Neuschrieb: der ursprüngliche Befund bleibt oben lesbar, weil er den
|
||
Zustand zum Zeitpunkt der ldap-Umstellung korrekt beschreibt und für
|
||
spätere Etappen als Beleg dient, siehe auch
|
||
`docs/mandantentrennung-etappe2-fehlerrichtung.md`, Abschnitt (e), Befund D.
|
||
- **`tender-digest.scheduler.ts`** (Ausschreibungs-Digest) — **Stand
|
||
260909-laa, Aufgabe 3: Je-Treffer-Hälfte geschlossen, übergreifende
|
||
Hälfte an Etappe 3 übergeben.** Liest `tenderMatch` (Kandidatenabfrage,
|
||
`findMany` mit `distinct: ['userId']`) bewusst über ALLE Mandanten in
|
||
einem einzigen `findMany` (ein einziger globaler Cron-Job, kein Mandant
|
||
im Job selbst — so von Anfang an entworfen, Pitfall 1 in den Kommentaren
|
||
der Datei) — diese Kandidatenabfrage bleibt UNGEBUNDEN und ist im Code
|
||
als Etappe-3-Übergabe kommentiert. Sie wählt zusätzlich das
|
||
denormalisierte `tenantId` der Treffer-Zeile mit aus, damit die Schleife
|
||
binden kann; der Sonderfall eines Nutzers mit Treffern unter zwei
|
||
verschiedenen Mandanten (Mandantenwechsel) ist NICHT gelöst, siehe
|
||
`docs/mandantentrennung-etappe2-fehlerrichtung.md`. Innerhalb der
|
||
Schleife laufen `tenderNotificationPref.findUnique`,
|
||
`tenderMatch.findMany`/`updateMany` und `user.findUnique` je
|
||
Kandidatenzeile über `forTenant()`, gebunden an deren Mandanten.
|
||
- **`tender-matching.service.ts`** (Ausschreibungs-Sofortmeldung) —
|
||
**Stand 260909-laa, Aufgabe 3: Je-Treffer-Hälfte geschlossen, übergreifende
|
||
Hälfte an Etappe 3 übergeben.** `tenderSavedSearch.findMany` (Profile ALLER
|
||
Mandanten) und der Lesezugriff auf den plattformweiten `tender`-Katalog
|
||
(D-03) bleiben UNGEBUNDEN, beide im Code als Etappe-3-Übergabe bzw. D-03
|
||
kommentiert. Innerhalb der Profilschleife laufen die Treffer-Anlage
|
||
(`tenderMatch.upsert`) und — im nachgelagerten Instant-Dispatch —
|
||
`tenderMatch.findMany`/`updateMany` sowie `user.findUnique` über
|
||
`forTenant()`, EIN gebundener Client je Profil, gebunden an dessen
|
||
Mandanten.
|
||
- **`admin-seed.service.ts`** (`ensureDefaultGroupsForAllTenants`) — **Stand
|
||
260910-das, Aufgabe 2: der vierte Fall dieser Art und der bislang EINZIGE,
|
||
der auf BEIDEN Hälften bereits richtig ist.** Liest `tenant.findMany()`
|
||
bewusst über ALLE Mandanten (Treiber, ungebunden — `Tenant` trägt keinen
|
||
Zeilenschutz, Aufgabe 1 gemessen, `tenant-tabelle-ohne-zeilenschutz-bleibt-lesbar`)
|
||
und ruft je Mandant `groupsService.ensureDefaultGroup(tenant.id)` auf —
|
||
dieser Rumpf ist seit 260909-jts vollständig über `forTenant()`/
|
||
`withTenantTransaction()` gebunden (siehe Bestandsaufnahme,
|
||
`groups.service.ts`/`group`, Stand `gebunden`). Anders als bei den drei
|
||
Fällen oben musste hier in Aufgabe 2/3 (260910-das) NICHTS umgestellt
|
||
werden — der Rumpf war es bereits, bevor der Bereich `user` überhaupt an
|
||
der Reihe war. Einschränkung, bewusst nicht verschwiegen: der äußere
|
||
`try/catch` in `ensureDefaultGroupsForAllTenants()` verschluckt jeden
|
||
Fehler des Treibers (`tenant.findMany`) in eine Protokollzeile — läuft die
|
||
Mandantenliste nach dem Scharfschalten aus irgendeinem Grund leer,
|
||
entsteht keine Fehlermeldung, sondern gar keine Ausgabe (Befund K).
|
||
|
||
**Der fünfte Fall, anderer Bauart — `dkv-scheduler.service.ts` /
|
||
`DkvService.loadAnyActiveConfigForScheduler()`** (260909-mir, Befund D,
|
||
WINDOWS #21): **offen, als benannte Altlast weitergeführt.** Dieser Dienst
|
||
gehört nicht in dieselbe Klasse wie die drei oben. Jene lesen bewusst über alle
|
||
Mandanten und müssen lediglich innerhalb der Schleife binden — sie sind heute
|
||
korrekt und verstummen erst nach dem Scharfschalten. Der DKV-Planer iteriert
|
||
überhaupt nicht: `findFirst()` ohne jede Bedingung zieht bei mehreren Mandanten
|
||
EINEN beliebigen und bedient die übrigen NIE. Ist ausgerechnet die gezogene
|
||
Zeile inaktiv, bedient er niemanden, obwohl ein zweiter Mandant aktiv wäre. Er
|
||
ist damit **heute bereits falsch** und verstummt nach dem Scharfschalten
|
||
zusätzlich (`null` statt einer beliebigen Zeile; der Planer protokolliert das als
|
||
Normalfall und richtet für JEDEN Mandanten nichts ein, ohne Alarm).
|
||
|
||
Binden ist hier keine Lösung: `onModuleInit()` hat beim Start strukturell keinen
|
||
Mandantenkontext, ein gebundener Aufruf liefe garantiert leer. Der Umbau auf
|
||
einmal-abfragen-viele-bedienen ist die in Phase 07-04 zurückgestellte
|
||
Mehrmandanten-Planung — eine Funktionsänderung, kein Bindungsumbau, und deshalb
|
||
in Etappe 2 nicht vorgenommen. Der Zugriff bleibt deshalb ungebunden, aber
|
||
dreifach markiert: eigene benannte Methode mit Kopfkommentar (bewusst KEINE
|
||
Verzweigung hinter einem optionalen Parameter, die jemand später
|
||
„vereinheitlicht"), fortgeschriebener Kopfkommentar in
|
||
`dkv-scheduler.service.ts`, Ledger-Eintrag WINDOWS #21. Das Signal für das
|
||
Verstummen gehört in die Vorabprüfung von Etappe 4 (`rls-preflight.mjs`).
|
||
|
||
**Stand 260910-exd — kein sechster Fall, gemessen statt angenommen.** Der
|
||
Bereich `module-registry` fügt diesem Abschnitt KEINEN sechsten Fall hinzu.
|
||
`ModuleRegistryService.seedModule()` (die Katalogpflege beim Start,
|
||
aufgerufen aus vier Seed-Dateien) schreibt zwar ohne Mandantenkontext — sie
|
||
iteriert aber über NICHTS je Mandant, sondern schreibt genau eine Zeile je
|
||
Aufruf auf den plattformweiten Katalog `Module`. Damit fehlt ihr die Bauform
|
||
der fünf oben geführten Fälle (übergreifend LESEN über alle Mandanten, dann
|
||
je Mandant BINDEN) — sie ist deshalb kein Kandidat für diese Liste. Dieser
|
||
Satz hält die Abwesenheit fest, damit sie nicht wie ein Übersehen aussieht.
|
||
|
||
**Stand 260910-krx — auch der Bereich `dashboard` fügt diesem Abschnitt
|
||
keinen sechsten Fall hinzu, gemessen statt angenommen.** Anweisung (260910-krx,
|
||
Aufgabe 1): `grep -rn "onModuleInit\|onApplicationBootstrap\|@Cron\|setInterval\|Scheduler" apps/api/src/dashboard --include=*.ts`
|
||
liefert null Treffer außerhalb von Testdateien — der einzige Dienst dieses
|
||
Bereichs, `dashboard.service.ts`, enthält keinen Hintergrunddienst, keinen
|
||
Planer und keinen Start-Hook. Jede seiner neun mandantengebundenen Methoden
|
||
wird ausschließlich synchron aus einer Anfrage eines einzelnen, bereits
|
||
bekannten Nutzers heraus aufgerufen — die Bauform dieses Abschnitts
|
||
(übergreifend LESEN über alle Mandanten, dann je Mandant BINDEN) kommt in
|
||
diesem Bereich an keiner Stelle vor.
|
||
|
||
**Stand 260911-cwh — auch der Bereich `calendar` fügt diesem Abschnitt
|
||
keinen sechsten Fall hinzu, gemessen statt angenommen (260911-cwh, Aufgabe 1,
|
||
Befund B).** `grep -rn "@Cron\|onModuleInit\|onApplicationBootstrap\|setInterval\|setTimeout" apps/api/src/calendar --include=*.ts`
|
||
liefert außerhalb von Testdateien genau einen Treffer,
|
||
`providers/ics.provider.ts:100` — ein `setTimeout` für den Abbruch eines
|
||
HTTP-Abrufs nach acht Sekunden, kein Planer. Der einzige Dienst dieses
|
||
Bereichs, `calendar.service.ts`, enthält keinen Hintergrunddienst, keinen
|
||
Planer und keinen Start-Hook — die Bauform dieses Abschnitts (übergreifend
|
||
LESEN über alle Mandanten, dann je Mandant BINDEN) kommt an keiner Stelle
|
||
vor. Es gibt aber einen benannten Sonderfall, anderer Bauart als die fünf
|
||
Fälle oben: `refreshCacheInBackground` (private Methode) ist eine
|
||
ABGEKOPPELTE FORTSETZUNG einer Anfrage — `aggregateEvents` stößt sie an,
|
||
wartet nicht auf sie, und sie ruft `fetchAndCacheEvents` mit den Parametern
|
||
der ursprünglichen Anfrage auf. Sie iteriert NICHT über mehrere Mandanten
|
||
und hat deshalb keine übergreifende Hälfte zu binden — sie trägt die
|
||
Mandantenkennung der Anfrage, die sie ausgelöst hat, und kann strukturell
|
||
keine andere haben. Kein Kandidat für diese Liste; dieser Absatz hält die
|
||
Abwesenheit fest, damit sie nicht wie ein Übersehen aussieht.
|
||
|
||
## 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 | gebunden | Kalenderquellen eines Nutzers je Mandant gebunden (encryptedPassword traegt Zugangsdaten zu externen Exchange-/CalDAV-Servern), `tenantId`-Spalte vorhanden. Seit 260911-cwh (Aufgabe 2) laufen alle zwoelf Zugriffe (`getSources`, `addSource`, beide Abfragen von `updateSource`/`deleteSource`, alle drei Abfragen von `testConnection`, Laden plus beide Synchronstatus-Rueckschreibungen von `fetchAndCacheEvents`) ueber `forTenant()`, ein Klient je Methode; `fetchAndCacheEvents`/`refreshCacheInBackground` nehmen die Mandantenkennung als Parameter, Letztere traegt die Kennung der urspruenglichen Anfrage. Die drei Besitzpruefungen (`updateSource`/`deleteSource`/`testConnection`, Vergleich gegen `userId` aus dem Sitzungsnachweis) bleiben zusaetzlich bestehen — die Regel auf `CalendarSource` kennt keine Benutzerdimension (260911-cwh, Aufgabe 1, gemessen), sie sind bis zur Etappe-3-Entscheidung (2) der einzige Schutz zwischen Kollegen DESSELBEN Mandanten. |
|
||
| apps/api/src/dashboard/dashboard.service.ts | dashboardLayout | muss-mandantengebunden | gebunden | Widget-Anordnung eines Nutzers, `tenantId`-Spalte vorhanden. Seit 260910-krx (Aufgabe 2) laufen `getLayout`/`saveLayout` GEMEINSAM ueber `forTenant()`, ein Klient je Methode; `saveLayout` uebersetzt eine `PrismaClientUnknownRequestError` (RLS-Konflikt auf der plattformweit eindeutigen `userId`, gemessen in Aufgabe 1 — NICHT die `P2002`-Form, die der Bereich `tenders` abfaengt) in eine deutsche Konfliktmeldung. |
|
||
| apps/api/src/dashboard/dashboard.service.ts | module | keine-mandantengebundene-tabelle | ungebunden | Modulkatalog ist plattformweit, kein `tenantId` (Migration 20260909140000, Gruppe b). MESSUNG (260910-krx, Aufgabe 1, uebernommen aus `module-registry`-Pruefung `module-tabelle-traegt-keinen-zeilenschutz`): die Tabelle traegt heute keinen Zeilenschutz, eine Bindung waere heute wirkungslos, nicht katastrophal. BEDINGUNG: katastrophal wuerde sie erst, WENN Etappe 3 dieser Tabelle eine Regel gibt. Die Katalogaufloesung fuer den Widget-Modulfilter (`ModuleAccessService.getAccessibleModuleIds`) bindet bereits seit 260910-exd in ihrem eigenen Dienst — hier NICHT ein zweites Mal gebunden. |
|
||
| apps/api/src/dashboard/dashboard.service.ts | searchProvider | muss-mandantengebunden | gebunden | `tenantId` nullbar. WINDOWS #19 geschlossen (260910-jab) als **widerlegte Prämisse** für dieses Modell. In diesem Durchlauf (260910-krx, Aufgabe 1) EIGENSTAENDIG nachgeprueft, nicht aus 260910-jab abgeschrieben: `grep -rn "searchProvider\|SearchProvider" apps packages prisma --include=*.ts --include=*.mjs --include=*.js --include=*.sql --include=*.json` (ohne `node_modules`, `dist/`, `.next/`) findet weiterhin genau einen Schreibweg, `dashboard.service.ts:addSearchProvider` (`create`), mit `tenantId: string` als Pflichtparameter — keine Seed-Datei, kein Skript. Seit Aufgabe 2/3 laufen `getSearchProviders`/`addSearchProvider`/`removeSearchProvider` ueber `forTenant()`; die Regel auf `SearchProvider` bleibt UNVERAENDERT streng, zusaetzlich datenbankseitig verteidigt durch `searchprovider-gebundenes-einfuegen-ohne-mandant-abgelehnt` (Aufgabe 1). |
|
||
| apps/api/src/dashboard/dashboard.service.ts | widgetInstance | muss-mandantengebunden | gebunden | Platzierte Dashboard-Widgets eines Nutzers, `tenantId`-Spalte vorhanden. Seit 260910-krx (Aufgabe 2) laufen `getWidgets`, `addWidget` sowie beide Paare aus Besitzpruefung und Schreibzugriff (`updateWidgetConfig`/`removeWidget`) ueber `forTenant()`; die vorgeschalteten Besitzpruefungen ueber die Benutzerkennung bleiben zusaetzlich bestehen (die Regeln dieses Bereichs kennen keine Benutzerdimension). |
|
||
| apps/api/src/dkv/dkv.service.ts | dkvInvoiceHistory | muss-mandantengebunden | gebunden | DKV-Rechnungshistorie je Mandant, `tenantId`-Spalte vorhanden. Seit 260909-mir (Aufgabe 3) laufen beide Historien-Schreibzugriffe der Verarbeitungsstrecke, beide parallelen Lesezugriffe von `getHistory` und der neue Riegel vor dem Ausfuhrdatei-Download vollstaendig ueber `forTenant()`. |
|
||
| apps/api/src/dkv/dkv.service.ts | dkvModuleConfig | muss-mandantengebunden | gemischt | Postfach-/Zugangsdaten des DKV-Moduls je Mandant. Seit 260909-mir (Aufgabe 2) laufen `loadConfig`, `getConfigForApi`, `saveConfig`, `testConnection` und der Konfigurations-Lesezugriff der Verarbeitungsstrecke ueber `forTenant()`. Die Mischung stammt ausschliesslich vom einen benannten, bewusst ungebundenen Planer-Startpfad `loadAnyActiveConfigForScheduler()` (WINDOWS #21) — keine uebersehene Fundstelle. |
|
||
| apps/api/src/dkv/dkv.service.ts | dkvVehicleMaster | muss-mandantengebunden | gebunden | Fahrzeugstammdaten des DKV-Moduls je Mandant. Seit 260909-mir (Aufgabe 3) laufen Fahrzeugliste, Anlegen, beide Paare aus Besitzpruefung und Schreibzugriff (Aendern/Loeschen), beide Zweige des CSV-Imports und der gebuendelte Lesezugriff beim Aufbau der Ausfuhrzeilen vollstaendig ueber `forTenant()`; die vorgeschalteten Besitzpruefungen bei Aendern/Loeschen bleiben zusaetzlich bestehen (Befund G — ein gebundenes UPDATE ueber die Kennung allein trifft eine fremde Zeile still, nicht laut). |
|
||
| apps/api/src/favorites/favorites.service.ts | favoriteLink | muss-mandantengebunden | ungebunden | Favoriten-Links eines Nutzers, `tenantId`-Spalte vorhanden. |
|
||
| apps/api/src/groups/groups.service.ts | group | muss-mandantengebunden | gebunden | Gruppen sind je Mandant, `tenantId`-Spalte vorhanden. Alle 12 Methoden laufen seit 260909-jts (Aufgabe 2) ueber `forTenant()` bzw. `withTenantTransaction()`. |
|
||
| apps/api/src/groups/groups.service.ts | groupMembership | muss-mandantengebunden | gebunden | Kein eigenes `tenantId`, RLS über Join auf `Group` (Migration 20260618112133-Nachfolger) — braucht trotzdem `forTenant()`, damit der Join-Kontext gesetzt ist. Seit 260909-jts gebunden, einschliesslich der drei Zugriffe innerhalb des Standardgruppen-Aufbaus (`ensureDefaultGroup`), die zuvor ueber den Transaktionsparameter liefen und fuer keine Pruefung dieses Projekts sichtbar waren (Befund B). |
|
||
| apps/api/src/groups/groups.service.ts | moduleGrant | muss-mandantengebunden | gebunden | Modulfreigaben je Mandant, `tenantId`-Spalte vorhanden. Seit 260909-jts gebunden, einschliesslich des Zugriffs innerhalb des Standardgruppen-Aufbaus (Befund B). |
|
||
| apps/api/src/groups/groups.service.ts | tenantModuleActivation | muss-mandantengebunden | gebunden | Welche Module ein Mandant aktiviert hat. Bis 260909-jts vollstaendig unsichtbar (Befund B, 260909-jts-PLAN.md): der einzige Zugriff dieser Datei lief innerhalb der interaktiven Transaktion von `ensureDefaultGroup` ueber den Rueckgabeparameter (`tx.tenantModuleActivation.findMany`) — weder `this.prisma.<Modell>` noch `<gebundener Client>.<Modell>` sahen das, weil `tx` weder `this.prisma` noch aus einer `forTenant(`-Zuweisung stammte. Die um Transaktionsparameter erweiterte Erkennung aus Aufgabe 2 macht dieses Paar erstmals sichtbar; die Stelle ist seit derselben Aufgabe gebunden (ueber `withTenantTransaction()`). |
|
||
| apps/api/src/groups/groups.service.ts | user | muss-mandantengebunden | gebunden | Nutzerverwaltung innerhalb eines Mandanten. Seit 260909-jts gebunden; `addUserToDefaultGroup` prueft seither zusaetzlich, dass der Zielbenutzer zum Mandanten gehoert (Befund E, T-JTS-02). Bis 20260910120000_rls_widen_membership_grant_and_platform_read pruefte die Regel auf `GroupMembership` nachweislich nur die Gruppenseite — seit 260910-jab prueft sie beide Seiten, die Anwendungspruefung bleibt trotzdem bestehen (Schalter weiterhin aus, #18). |
|
||
| apps/api/src/groups/module-grants.service.ts | group | muss-mandantengebunden | gebunden | Wie groups.service.ts. Seit 260909-jts (Aufgabe 3) gebunden — `assertTargetBelongsToTenant` erzeugt seinen eigenen Kontext, `getMatrix` teilt sich einen Kontext mit den beiden anderen parallelen Teilabfragen. |
|
||
| apps/api/src/groups/module-grants.service.ts | groupMembership | muss-mandantengebunden | gebunden | Kein eigenes `tenantId`, RLS über Join auf `Group`. Seit 260909-jts gebunden; der `where`-Filter über die Beziehung zur Gruppe bleibt zusätzlich stehen, weil die Regel auf dieser Tabelle nachweislich nur die Gruppenseite prüft (Aufgabe 1). |
|
||
| apps/api/src/groups/module-grants.service.ts | moduleGrant | muss-mandantengebunden | gebunden | Modulfreigaben je Mandant. Seit 260909-jts gebunden; die Mandanten-Gegenprüfung vor jedem Erteilen (`assertTargetBelongsToTenant`) bleibt zusätzlich bestehen. Bis 20260910120000_rls_widen_membership_grant_and_platform_read prüfte die Regel auf dieser Tabelle nur die Mandantenkennung der Zeile, nicht die referenzierte Gruppe/den referenzierten Benutzer (Befund F, T-JTS-03, Aufgabe 1) — seit 260910-jab prüft sie beide Ziele zusätzlich, die Anwendungsprüfung bleibt trotzdem der erste Schutz (Schalter weiterhin aus, #18). |
|
||
| apps/api/src/groups/module-grants.service.ts | tenantModuleActivation | muss-mandantengebunden | gebunden | Welche Module ein Mandant aktiviert hat, `tenantId`-Spalte vorhanden. Seit 260909-jts gebunden. |
|
||
| apps/api/src/groups/module-grants.service.ts | user | muss-mandantengebunden | gebunden | Zielbenutzer eines Grants innerhalb des Mandanten. Seit 260909-jts gebunden; die Mandanten-Gegenprüfung bleibt bestehen. |
|
||
| apps/api/src/ldap/ldap-config.service.ts | ldapConfig | beides | gemischt | `getConfig`, `createConfig` und `updateConfig` laufen ueber `forTenant()`, gebunden an den aus der Anfrage bereits bekannten Mandanten. `getAllActiveConfigs()` (Planer-Lesezugriff ueber ALLE Mandanten) und die Start-Nachverschluesselung in `onApplicationBootstrap` bleiben bewusst uebergreifend: beide laufen, bevor bzw. unabhaengig davon, ob ein einzelner Mandantenkontext feststeht (Befund B, 260909-ipc-PLAN.md). Korrektur der Klasse von `muss-mandantengebunden`: das Paar ist tatsaechlich `beides`, keine Verhaltensaenderung. |
|
||
| apps/api/src/ldap/ldap-config.service.ts | ldapFieldMapping | muss-mandantengebunden | gebunden | Kein eigenes `tenantId`, RLS ueber Join auf `LdapConfig`. `addFieldMapping` und `removeFieldMapping` nehmen den Mandanten jetzt als Parameter entgegen und laufen vollstaendig ueber `forTenant()` (T-IPC-01, 260909-ipc-PLAN.md) — schliesst zugleich die Fremdzugriffsluecke beim Loeschen einer Feldzuordnung ueber ihre Kennung. |
|
||
| apps/api/src/ldap/ldap.service.ts | group | beides | gebunden | AD-Abgleich: `listGroups` (die "bereits importiert"-Markierung) und `importGroupsByDn` (die Idempotenzpruefung ueber `ldapObjectGuid`) sind mit Aufgabe 3 (260909-ipc) auf `forTenant()` umgestellt — zusammen mit den bereits vorher gebundenen Stellen (Anlage, Mitgliedschafts- und Gruppenabgleich) ist damit jeder `group`-Zugriff dieser Datei gebunden. Die Klasse bleibt `beides`, weil ein zukuenftiger uebergreifender Lesezugriff (z. B. ein neuer Planer-Pfad) hier ebenso legitim waere wie bei `ldapConfig` unten — nicht, weil heute noch ein ungebundener Zugriff bestuende. |
|
||
| apps/api/src/ldap/ldap.service.ts | groupMembership | muss-mandantengebunden | gebunden | Kein eigenes `tenantId`, RLS ueber Join auf `Group`. `syncGroupMembershipsForTenant` laeuft vollstaendig ueber `forTenant()` (Plan 16-03/16-05) — von der alten, nur `this.prisma.*` erkennenden Suche nie erfasst, weil bereits gebunden; die erweiterte Erkennung aus Aufgabe 2 (260909-ipc) macht diese Fundstelle erstmals sichtbar. |
|
||
| apps/api/src/ldap/ldap.service.ts | ldapConfig | beides | gebunden | Die `lastSyncAt`-Fortschreibung am Ende von `syncUsersForTenant` ist mit Aufgabe 3 (260909-ipc) auf den in derselben Methode bereits vorhandenen `forTenant()`-Client umgestellt — es entsteht kein zweiter. Damit ist der einzige `ldapConfig`-Zugriff dieser Datei gebunden. |
|
||
| apps/api/src/ldap/ldap.service.ts | user | beides | gemischt | Mit Aufgabe 3 (260909-ipc) sind `upsertMappedUser` (Identitaetssuche und Aktualisierung), `searchUsers` (die "bereits importiert"-Markierung), `importUsersByDn` (Dedup und ldapDn-Nachtrag) und die Deaktivierungsschleife in `syncUsersForTenant` auf `forTenant()` umgestellt. `resolveEmailForWrite` bleibt ausdruecklich UNGEBUNDEN (Befund A, T-IPC-04): `email`/`username` sind plattformweit eindeutig, eine mandantengebundene Suche saehe einen fremden Halter nicht mehr und meldete faelschlich "frei" — die geloeste Klasse waere `muss-mandantengebunden` gewesen, bleibt wegen dieser einen bewusst uebergreifenden Abfrage `beides`. Der Loeschzweig um `syncBoundGroupsForTenant` (WINDOWS #20) ist bereits seit Etappe 1 gebunden und war nie Teil dieses Befunds. |
|
||
| apps/api/src/module-registry/module-access.service.ts | module | keine-mandantengebundene-tabelle | ungebunden | Modulkatalog ist plattformweit, kein `tenantId`. Bleibt bewusst ungebunden (260910-exd, Aufgabe 1, Befund E): keine Regel auf der Tabelle, eine Bindung waere heute wirkungslos, nicht katastrophal — katastrophal erst, WENN Etappe 3 dieser Tabelle eine Regel gibt. |
|
||
| apps/api/src/module-registry/module-access.service.ts | moduleGrant | muss-mandantengebunden | gebunden | Modulfreigaben je Mandant. Seit 260910-exd (Aufgabe 2) laufen Direktweg und Gruppenweg von `getAccessibleModuleIds` ueber `forTenant()`, EIN Klient je Methode. |
|
||
| apps/api/src/module-registry/module-access.service.ts | tenantModuleActivation | muss-mandantengebunden | gebunden | Aktivierung je Mandant, `tenantId`-Spalte vorhanden. Seit 260910-exd (Aufgabe 2) laufen Kurzschlusszweig, Schnittmengenabfrage und der eigene Lesezugriff von `getCatalogFlags` ueber `forTenant()`. |
|
||
| apps/api/src/module-registry/module-registry.service.ts | module | keine-mandantengebundene-tabelle | ungebunden | Modulkatalog ist plattformweit. Bleibt bewusst ungebunden (260910-exd, Aufgabe 1, Befund E): keine Regel auf der Tabelle, eine Bindung waere heute wirkungslos, nicht katastrophal — katastrophal erst, WENN Etappe 3 dieser Tabelle eine Regel gibt. `findBySlug` ist zusaetzlich die Stelle, die `ModuleGuard` bei JEDER Modulanfrage aufruft. |
|
||
| apps/api/src/module-registry/module-registry.service.ts | tenantModuleActivation | muss-mandantengebunden | gebunden | Aktivierung je Mandant. Seit 260910-exd (Aufgabe 3) laufen `findActiveForTenant`, `activateForTenant`, beide Zugriffe von `deactivateForTenant` (ueber EINEN Klienten) und `isModuleActive` ueber `forTenant()`, je Methode EIN Klient. |
|
||
| apps/api/src/settings/settings.service.ts | smtpConfig | muss-mandantengebunden | ungebunden | SMTP-Zugangsdaten je Mandant, `tenantId`-Spalte vorhanden. |
|
||
| apps/api/src/tenant/tenant.controller.ts | tenant | keine-mandantengebundene-tabelle | ungebunden | `Tenant` ist die Mandantentabelle selbst — hat keine eigene `tenantId`-Spalte, kann sie per Definition nicht haben (Migration 20260909140000, Gruppe b). |
|
||
| 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()`. Seit 260910-jab (Aufgabe 2, WINDOWS #19 geschlossen) bindet zusätzlich `listForUser` — die neue Leseregel (`tenant_platform_read_policy`, 20260910120000_rls_widen_membership_grant_and_platform_read) schließt die plattformweite Zeile ausdrücklich ein, ungebunden hätte die Reparatur den Pfad sonst still auf nur die plattformweiten Zeilen reduziert (Befund F). `createPlatform`/`remove` bleiben bewusst ungebunden — beide Pfade lassen sich unter der Anwendungsrolle grundsätzlich nicht anlegen/entfernen, weil jede Schreibregel einen Mandanten verlangt (WINDOWS #24, eigener offener Punkt). Klasse `beides` bleibt korrekt: zwei gebundene, zwei bewusst ungebundene Zugriffe in derselben Datei. |
|
||
| apps/api/src/tenders/tender-saved-search.service.ts | tenderSavedSearch | muss-mandantengebunden | gebunden | Nutzer-CRUD für die eigenen gespeicherten Suchprofile — Mandant aus der Anfrage bekannt. Seit 260909-laa (Aufgabe 2) laufen `list`/`create`/`update`/`remove` vollständig über `forTenant()`. |
|
||
| apps/api/src/tenders/tender-scheduler.service.ts | tenderSourcePollConfig | keine-mandantengebundene-tabelle | ungebunden | Plattformweiter DÖE-Poll-Status, kein `tenantId` — im Dateikopf explizit als "genuine platform-wide singleton" begründet. |
|
||
| apps/api/src/tenders/tender-triage.service.ts | tenderTriage | muss-mandantengebunden | gebunden | Favorisierungs-/Ablehnungsstatus eines Nutzers, `tenantId`-Spalte vorhanden. Seit 260909-laa (Aufgabe 2) laufen `setTriage`/`listForUser`/`favoriteIds` vollständig über `forTenant()`. |
|
||
| apps/api/src/tenders/tenders.controller.ts | tender | keine-mandantengebundene-tabelle | ungebunden | Lesezugriff auf den plattformweiten Katalog (D-03). |
|
||
| apps/api/src/tenders/tenders.controller.ts | tenderSourcePollConfig | keine-mandantengebundene-tabelle | ungebunden | Plattformweiter Poll-Status, admin-verwaltet, kein `tenantId`. |
|
||
| apps/api/src/tenders/tenders.module.ts | tenderSourcePollConfig | keine-mandantengebundene-tabelle | ungebunden | Singleton-Bestückung beim Boot — im Dateikopf explizit als "global, RLS-exempt (D-03)" begründet. |
|
||
| apps/api/src/user/admin-seed.service.ts | tenant | keine-mandantengebundene-tabelle | ungebunden | Legt beim ersten Start den Standard-Mandanten selbst an und liest beim Start alle Mandanten fuer die Standardgruppen-Reparatur — `Tenant` hat keine `tenantId`-Spalte und traegt keinen Zeilenschutz (Aufgabe 1, `tenant-tabelle-ohne-zeilenschutz-bleibt-lesbar`). Fuenfter und bislang einziger bereits vollstaendig richtiger Fall der Hintergrunddienst-Falle (Befund K, siehe Abschnitt unten). |
|
||
| apps/api/src/user/admin-seed.service.ts | user | beides | gemischt | Klassenkorrektur (260910-das, Aufgabe 3): wechselt von `bewusst-uebergreifend` auf `beides`, weil die bisherige Begruendung ("es gibt strukturell keinen Mandanten zum Binden") nachweislich FALSCH war (Befund J) — der Mandant wird eine Anweisung vorher angelegt und ist bekannt. Die Erstanlage-Pruefung bleibt bewusst ungebunden (kein Mandant existiert zu diesem Zeitpunkt, `username` ist plattformweit eindeutig); die Erstanlage des Administrators selbst laeuft seit Aufgabe 2 ueber `forTenant()`, gebunden an den unmittelbar zuvor angelegten Mandanten. Eine P2002-Kollision beim Anlegen wird wie "Administrator existiert bereits" behandelt statt den Start abzubrechen (Befund I). |
|
||
| apps/api/src/user/user.controller.ts | user | muss-mandantengebunden | gebunden | Nutzerverwaltung innerhalb des Mandanten des anfragenden Admins (260910-das, Aufgabe 3): die Benutzerliste des ADMIN-Zweigs, alle drei Kennungswege (rollenabhaengig ueber `UserService.findById`/`findByIdForPlatformAdmin`) und alle fuenf Selbstbedienungszugriffe (Bild hochladen/loeschen/ausliefern, Akzentfarbe) laufen ueber `forTenant()`; die Rollenverzweigung zwischen mandantengebundener ADMIN-Sicht und der uebergreifenden `SUPER_ADMIN`-Sicht (ueber `UserService.findAllForPlatformAdmin`) bleibt bestehen. Der wirkungslose Selbstloesch-Riegel (Befund H, verglich gegen `currentUser.sub`, ein im Sitzungsnachweis nicht existierendes Feld) ist auf `currentUser.id` korrigiert. |
|
||
| apps/api/src/user/user.service.ts | tenant | keine-mandantengebundene-tabelle | ungebunden | Schleifentreiber der neuen Plattform-Administratorsicht (`findAllForPlatformAdmin`/`findByIdForPlatformAdmin`, 260910-das, Aufgabe 2, Befund F/N) — `Tenant` hat keine `tenantId`-Spalte und traegt keinen Zeilenschutz (Aufgabe 1, `tenant-tabelle-ohne-zeilenschutz-bleibt-lesbar`). |
|
||
| apps/api/src/user/user.service.ts | user | beides | gemischt | Klassenkorrektur (260910-das, Aufgabe 3): wechselt von `muss-mandantengebunden` auf `beides` wegen der einen bewusst ungebundenen Suche — wortgleich derselbe Praezedenzfall wie `ldap.service.ts`/`user` in 260909-ipc (`resolveEmailForWrite`). `findById`/`create`/`update`/`deactivate`/`delete` sowie die beiden neuen Plattform-Administratorsicht-Methoden laufen ueber `forTenant()`; `create`/`update` uebersetzen eine plattformweite Eindeutigkeitsverletzung (P2002) in eine deutsche Konfliktmeldung ohne Halter/Mandant zu nennen. `findByUsername` bleibt bewusst UNGEBUNDEN: der Anmeldeweg laeuft seit Etappe 1 ueber die drei SECURITY-DEFINER-Funktionen und hat diese Methode nicht mehr als Aufrufer (260910-das, Aufgabe 1, Teil 3: genau ein Treffer, die eigene Definition); eine gebundene Suche saehe einen fremden Halter des plattformweit eindeutigen `username` nicht und meldete faelschlich "frei". |
|
||
|
||
## Was diese Etappe NICHT entscheidet
|
||
|
||
- Ob Controller künftig über `req.tenantPrisma` statt eines erneuten
|
||
`forTenant()`-Aufrufs im Service gehen (offener Befund oben). Der Bereich
|
||
`ldap` (260909-ipc) hat sich für den Dienst-internen Weg entschieden —
|
||
`forTenant(this.prisma, tenantId)` wird in jeder umgestellten Methode neu
|
||
erzeugt, wie es die vier Bestandsstellen in `ldap.service.ts` und die drei
|
||
in `auth.service.ts` bereits vormachten. Der Bereich `groups` (260909-jts)
|
||
hat sich für denselben dienst-internen Weg entschieden — jede Methode in
|
||
`groups.service.ts` und `module-grants.service.ts` erzeugt ihren eigenen
|
||
`forTenant()`- bzw. `withTenantTransaction()`-Aufruf, gebundene Clients
|
||
werden nicht zwischen Methoden weitergereicht. Der Bereich `dashboard`
|
||
(260910-krx) hat sich für denselben dienst-internen Weg entschieden — jede
|
||
der neun umgestellten Methoden in `dashboard.service.ts` erzeugt ihren
|
||
eigenen `forTenant()`-Aufruf, wie alle sieben Bereiche vor ihm. Der Bereich
|
||
`calendar` (260911-cwh) hat sich für denselben dienst-internen Weg
|
||
entschieden — jede der sechs umgestellten Methoden in `calendar.service.ts`
|
||
erzeugt ihren eigenen `forTenant()`-Aufruf, wie alle acht Bereiche vor ihm.
|
||
Die Frage bleibt für alle übrigen Bereiche der Etappe 2 offen.
|
||
- ~~Wie die WINDOWS-#19-Policy für `SearchProvider`/`TenderRssFeedSource`
|
||
am Ende genau lautet — nur, dass sie vor dem Scharfschalten gelöst sein
|
||
muss.~~ Aufgelöst (260910-jab): `TenderRssFeedSource` bekommt vier nach
|
||
Befehl getrennte Regeln, `SearchProvider` bleibt unverändert streng
|
||
(widerlegte Prämisse). Siehe Abschnitt "Zwei belegte Befunde" oben. Was
|
||
weiterhin offen ist: der Verwaltungsweg für plattformweite Zeilen unter
|
||
der Anwendungsrolle (WINDOWS #24).
|
||
- Die Reihenfolge und Zuschnitt der Etappe-2-Pläne — dafür ist die
|
||
Klassen-Verteilung oben der Arbeitsvorrat, siehe `<next_stages>` im
|
||
Plan `260909-eor-PLAN.md`.
|