3a9391d9c8
- user.controller.ts: alle sieben Zugriffe binden. ADMIN-Zweig der Benutzerliste laeuft ueber forTenant() mit weiterhin bestehender Mandantenbedingung im where; SUPER_ADMIN-Zweig ueber die neue UserService.findAllForPlatformAdmin(). Die drei Wege ueber die Kennung loesen den Zielbenutzer rollenabhaengig ueber resolveTargetUser() auf (ADMIN gebunden an eigenen Mandanten, SUPER_ADMIN uebergreifend); der Schreibzugriff bei update/delete bindet an den Mandanten des Zielbenutzers, nicht des Aufrufers, damit die uebergreifende Verwaltung durch die oberste Rolle erhalten bleibt - Selbstloesch-Riegel (Befund H) repariert: verglich bisher gegen currentUser.sub, ein Feld, das der Sitzungsnachweis nicht traegt -- der Riegel griff nie. Jetzt gegen currentUser.id. Verhaltensaenderung: ein Administrator kann sein eigenes Konto nun nicht mehr loeschen - Alle fuenf Selbstbedienungszugriffe (Bild hochladen/loeschen/ ausliefern, Akzentfarbe) binden an die Mandantenkennung aus dem Sitzungsnachweis - user.controller.spec.ts (neu): Zwei-Klienten-Nachweis fuer die vorher testlose Steuerungsschicht, 8 Testfaelle, Falsifizierungsnachweis fuer eine gebundene Stelle sowie Rot-vor-Reparatur-Nachweis fuer den Selbstloesch-Riegel (siehe SUMMARY) - docs/mandantentrennung-zugriffsklassifikation.md: alle vier handgepflegten Stellen nachgezogen (Uebersichtszeile 8/14, Summenzeile 118/124, Klassen-Verteilung 63 Paare, Hintergrunddienst-Abschnitt auf fuenf Faelle inkl. admin-seed.service.ts als erster beidseitig korrekter Fall) sowie zwei Klassenkorrekturen (user.service.ts/user und admin-seed.service.ts/user je auf "beides") - docs/mandantentrennung-etappe2-fehlerrichtung.md: Nachtrag zum user-Abschnitt mit den tatsaechlich umgesetzten Pfaden, der geschlossenen Luecke und den Falsifizierungsnachweisen - 810 Tests gruen (8 neue in user.controller.spec.ts), Typpruefung sauber, Wegwerf-Werkzeug meldet weiterhin alle 53 Pruefungen bestanden Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
347 lines
40 KiB
Markdown
347 lines
40 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 | 36 | 26 | **war 62/0** — Aufgabe 2/3 (260909-laa) haben die fünf Nutzer-CRUD-Dienste (vier vollständig, `tender-rss-feed.service.ts` teilweise mit im Code begründeter WINDOWS-#19-Grenze) und die Je-Treffer-Hälften der beiden Hintergrunddienste (`tender-digest.scheduler.ts`, `tender-matching.service.ts`) auf `forTenant()` umgestellt. Die 36 verbleibenden ungebundenen Treffer sind die zwölf bewusst nicht angefassten Paare (D-03-Katalog, zwei Fan-out-Adapter) plus die drei bewusst ungebundenen RSS-Pfade plus die übergreifenden Hälften der beiden Hintergrunddienste (Etappe-3-Übergabe) |
|
||
| groups | 0 | 31 | **war 37/0** — Aufgabe 2/3 (260909-jts) haben `groups.service.ts` (12 Methoden) und `module-grants.service.ts` (5 Methoden) vollständig auf `forTenant()`/`withTenantTransaction()` umgestellt. Die neun zusätzlichen, über `tx` gebundenen Zugriffe innerhalb der drei Transaktionen zählt dieses einfache Muster nicht mit (siehe Methodenhinweis oben) |
|
||
| ldap | 4 | 26 | **war 21/0** — Aufgabe 2/3 (260909-ipc) haben `ldap-config.service.ts` (5 Methoden) und `ldap.service.ts` (6 Methoden, 11 Abfragen) auf `forTenant()` umgestellt. Die 4 verbleibenden ungebundenen Treffer sind bewusst: `getAllActiveConfigs`/`onApplicationBootstrap` (Befund B) und `resolveEmailForWrite` (Befund A, T-IPC-04) |
|
||
| dkv | 1 | 22 | **war 21/0** — Aufgabe 2/3 (260909-mir) haben `dkv.service.ts` vollständig auf `forTenant()` umgestellt: Konfigurationspfade (`loadConfig`, `getConfigForApi`, `saveConfig`, `testConnection`), Historie, Fahrzeugstammdaten und der neue Besitzriegel vor dem Ausfuhrdatei-Download. Gebunden sind es 22 statt 21, weil der Riegel einen zusätzlichen Lesezugriff auf `dkvInvoiceHistory` einführt (T-MIR-03). Der eine verbleibende ungebundene Treffer ist der benannte Planer-Startpfad `loadAnyActiveConfigForScheduler()` (Befund D, WINDOWS #21) — bewusst, mit dreifacher Markierung |
|
||
| user | 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 | 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** | **118** | **124** | Ungebunden: war 127 nach 260909-mir, Delta = die 9 in Aufgabe 2/3 (260910-das) gesunkenen `user`-Rohtreffer (17→8). Gebunden: war 110, jetzt zusätzlich 14 in `user`. 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.
|
||
|
||
| 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`).
|
||
|
||
## 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 | 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) — die Regel auf `GroupMembership` prueft nachweislich nur die Gruppenseite. |
|
||
| apps/api/src/groups/module-grants.service.ts | group | muss-mandantengebunden | gebunden | Wie groups.service.ts. Seit 260909-jts (Aufgabe 3) gebunden — `assertTargetBelongsToTenant` erzeugt seinen eigenen Kontext, `getMatrix` teilt sich einen Kontext mit den beiden anderen parallelen Teilabfragen. |
|
||
| apps/api/src/groups/module-grants.service.ts | groupMembership | muss-mandantengebunden | gebunden | Kein eigenes `tenantId`, RLS über Join auf `Group`. Seit 260909-jts gebunden; der `where`-Filter über die Beziehung zur Gruppe bleibt zusätzlich stehen, weil die Regel auf dieser Tabelle nachweislich nur die Gruppenseite prüft (Aufgabe 1). |
|
||
| apps/api/src/groups/module-grants.service.ts | moduleGrant | muss-mandantengebunden | gebunden | Modulfreigaben je Mandant. Seit 260909-jts gebunden; die Mandanten-Gegenprüfung vor jedem Erteilen (`assertTargetBelongsToTenant`) bleibt zusätzlich bestehen, weil die Regel auf dieser Tabelle nur die Mandantenkennung der Zeile prüft, nicht die referenzierte Gruppe (Befund F, T-JTS-03, Aufgabe 1). |
|
||
| apps/api/src/groups/module-grants.service.ts | tenantModuleActivation | muss-mandantengebunden | gebunden | Welche Module ein Mandant aktiviert hat, `tenantId`-Spalte vorhanden. Seit 260909-jts gebunden. |
|
||
| apps/api/src/groups/module-grants.service.ts | user | muss-mandantengebunden | gebunden | Zielbenutzer eines Grants innerhalb des Mandanten. Seit 260909-jts gebunden; die Mandanten-Gegenprüfung bleibt bestehen. |
|
||
| apps/api/src/ldap/ldap-config.service.ts | ldapConfig | beides | gemischt | `getConfig`, `createConfig` und `updateConfig` laufen ueber `forTenant()`, gebunden an den aus der Anfrage bereits bekannten Mandanten. `getAllActiveConfigs()` (Planer-Lesezugriff ueber ALLE Mandanten) und die Start-Nachverschluesselung in `onApplicationBootstrap` bleiben bewusst uebergreifend: beide laufen, bevor bzw. unabhaengig davon, ob ein einzelner Mandantenkontext feststeht (Befund B, 260909-ipc-PLAN.md). Korrektur der Klasse von `muss-mandantengebunden`: das Paar ist tatsaechlich `beides`, keine Verhaltensaenderung. |
|
||
| apps/api/src/ldap/ldap-config.service.ts | ldapFieldMapping | muss-mandantengebunden | gebunden | Kein eigenes `tenantId`, RLS ueber Join auf `LdapConfig`. `addFieldMapping` und `removeFieldMapping` nehmen den Mandanten jetzt als Parameter entgegen und laufen vollstaendig ueber `forTenant()` (T-IPC-01, 260909-ipc-PLAN.md) — schliesst zugleich die Fremdzugriffsluecke beim Loeschen einer Feldzuordnung ueber ihre Kennung. |
|
||
| apps/api/src/ldap/ldap.service.ts | group | beides | gebunden | AD-Abgleich: `listGroups` (die "bereits importiert"-Markierung) und `importGroupsByDn` (die Idempotenzpruefung ueber `ldapObjectGuid`) sind mit Aufgabe 3 (260909-ipc) auf `forTenant()` umgestellt — zusammen mit den bereits vorher gebundenen Stellen (Anlage, Mitgliedschafts- und Gruppenabgleich) ist damit jeder `group`-Zugriff dieser Datei gebunden. Die Klasse bleibt `beides`, weil ein zukuenftiger uebergreifender Lesezugriff (z. B. ein neuer Planer-Pfad) hier ebenso legitim waere wie bei `ldapConfig` unten — nicht, weil heute noch ein ungebundener Zugriff bestuende. |
|
||
| apps/api/src/ldap/ldap.service.ts | groupMembership | muss-mandantengebunden | gebunden | Kein eigenes `tenantId`, RLS ueber Join auf `Group`. `syncGroupMembershipsForTenant` laeuft vollstaendig ueber `forTenant()` (Plan 16-03/16-05) — von der alten, nur `this.prisma.*` erkennenden Suche nie erfasst, weil bereits gebunden; die erweiterte Erkennung aus Aufgabe 2 (260909-ipc) macht diese Fundstelle erstmals sichtbar. |
|
||
| apps/api/src/ldap/ldap.service.ts | ldapConfig | beides | gebunden | Die `lastSyncAt`-Fortschreibung am Ende von `syncUsersForTenant` ist mit Aufgabe 3 (260909-ipc) auf den in derselben Methode bereits vorhandenen `forTenant()`-Client umgestellt — es entsteht kein zweiter. Damit ist der einzige `ldapConfig`-Zugriff dieser Datei gebunden. |
|
||
| apps/api/src/ldap/ldap.service.ts | user | beides | gemischt | Mit Aufgabe 3 (260909-ipc) sind `upsertMappedUser` (Identitaetssuche und Aktualisierung), `searchUsers` (die "bereits importiert"-Markierung), `importUsersByDn` (Dedup und ldapDn-Nachtrag) und die Deaktivierungsschleife in `syncUsersForTenant` auf `forTenant()` umgestellt. `resolveEmailForWrite` bleibt ausdruecklich UNGEBUNDEN (Befund A, T-IPC-04): `email`/`username` sind plattformweit eindeutig, eine mandantengebundene Suche saehe einen fremden Halter nicht mehr und meldete faelschlich "frei" — die geloeste Klasse waere `muss-mandantengebunden` gewesen, bleibt wegen dieser einen bewusst uebergreifenden Abfrage `beides`. Der Loeschzweig um `syncBoundGroupsForTenant` (WINDOWS #20) ist bereits seit Etappe 1 gebunden und war nie Teil dieses Befunds. |
|
||
| apps/api/src/module-registry/module-access.service.ts | module | keine-mandantengebundene-tabelle | ungebunden | Modulkatalog ist plattformweit, kein `tenantId`. |
|
||
| apps/api/src/module-registry/module-access.service.ts | moduleGrant | muss-mandantengebunden | ungebunden | Modulfreigaben je Mandant. |
|
||
| apps/api/src/module-registry/module-access.service.ts | tenantModuleActivation | muss-mandantengebunden | ungebunden | Aktivierung je Mandant, `tenantId`-Spalte vorhanden. |
|
||
| apps/api/src/module-registry/module-registry.service.ts | module | keine-mandantengebundene-tabelle | ungebunden | Modulkatalog ist plattformweit. |
|
||
| apps/api/src/module-registry/module-registry.service.ts | tenantModuleActivation | muss-mandantengebunden | ungebunden | Aktivierung je Mandant. |
|
||
| apps/api/src/settings/settings.service.ts | smtpConfig | muss-mandantengebunden | ungebunden | SMTP-Zugangsdaten je Mandant, `tenantId`-Spalte vorhanden. |
|
||
| apps/api/src/tenant/tenant.controller.ts | tenant | keine-mandantengebundene-tabelle | ungebunden | `Tenant` ist die Mandantentabelle selbst — hat keine eigene `tenantId`-Spalte, kann sie per Definition nicht haben (Migration 20260909140000, Gruppe b). |
|
||
| apps/api/src/tenant/tenant.service.ts | tenant | keine-mandantengebundene-tabelle | ungebunden | Dieselbe Begründung. |
|
||
| apps/api/src/tenders/adapters/email-alert.adapter.ts | tenderEmailConfig | bewusst-uebergreifend | ungebunden | `fetchTenders()` liest bewusst jede aktive `TenderEmailConfig`-Zeile über ALLE Mandanten in einer Abfrage (Plattform-Scheduler, ein Tick pro Postfach, D-13/D-01) — ausführlich im Dateikopf begründet, darf laut Kommentar niemals in `forTenant()` verpackt werden. |
|
||
| apps/api/src/tenders/adapters/rss.adapter.ts | tenderRssFeedSource | bewusst-uebergreifend | ungebunden | Fan-out über jeden aktiven Feed, plattformweit UND persönlich, in einer Abfrage (Zeilen 55–83 im Dateikopf begründet) — dieselbe Scheduler-Ebene wie beim E-Mail-Adapter. |
|
||
| apps/api/src/tenders/tender-dedup.service.ts | tender | keine-mandantengebundene-tabelle | ungebunden | Explizit im Dateikopf: "platform-global, RLS-exempt tables. Never wrap these queries in forTenant()." (D-03) |
|
||
| apps/api/src/tenders/tender-dedup.service.ts | tenderSource | keine-mandantengebundene-tabelle | ungebunden | Dieselbe Begründung. |
|
||
| apps/api/src/tenders/tender-digest.scheduler.ts | tenderMatch | beides | gemischt | Ein einziger globaler Cron-Job liest über ALLE Mandanten (bewusst übergreifend, Pitfall-1-Kommentar im Dateikopf) — seit 260909-laa (Aufgabe 3) bleibt die Kandidatenabfrage (`findMany` mit `distinct`) bewusst ungebunden, die Je-Treffer-Abfrage (`findMany` nach Mandant) und die Stempelung (`updateMany`) je Kandidatenzeile laufen über `forTenant()`. |
|
||
| apps/api/src/tenders/tender-digest.scheduler.ts | tenderNotificationPref | beides | gebunden | Präferenzen werden je Kandidatenzeile gelesen. Seit 260909-laa (Aufgabe 3) läuft der einzige Zugriff über `forTenant()`, gebunden an den Mandanten der jeweiligen Zeile. |
|
||
| apps/api/src/tenders/tender-digest.scheduler.ts | user | beides | gebunden | E-Mail-Adressen für den Versand werden je Kandidatenzeile gelesen. Seit 260909-laa (Aufgabe 3) läuft der einzige Zugriff über `forTenant()`. |
|
||
| apps/api/src/tenders/tender-email-config.service.ts | tenderEmailConfig | muss-mandantengebunden | gebunden | Nutzer-CRUD für die eigene Postfachanbindung (Phase 17, D-01) — anders als der Fan-out-Adapter oben, hier ist der Mandant aus der Anfrage bekannt. Seit 260909-laa (Aufgabe 2) laufen `getConfigForApi` (beide Lesezugriffe), `saveConfig` und `testConnection` vollständig über `forTenant()`. |
|
||
| apps/api/src/tenders/tender-fingerprint-backfill.service.ts | tender | keine-mandantengebundene-tabelle | ungebunden | Einmaliges Backfill-Skript über den plattformweiten `Tender`-Katalog (D-03). |
|
||
| apps/api/src/tenders/tender-ingestion.service.ts | tender | keine-mandantengebundene-tabelle | ungebunden | Explizit im Dateikopf: "Multi-tenant safety (D-03, T-10-09): uses the plain, non-tenant-scoped ... queries ... these are platform-global". |
|
||
| apps/api/src/tenders/tender-ingestion.service.ts | tenderSourcePollConfig | keine-mandantengebundene-tabelle | ungebunden | Plattformweiter Poll-Status, kein `tenantId` (Migration 20260909140000, Gruppe b). |
|
||
| apps/api/src/tenders/tender-matching.service.ts | tender | keine-mandantengebundene-tabelle | ungebunden | Liest den plattformweiten Katalog (D-03), um Treffer zu berechnen — kein `tenantId`. |
|
||
| apps/api/src/tenders/tender-matching.service.ts | tenderMatch | beides | gebunden | Sofortmeldung — siehe Abschnitt "Der Hintergrunddienst als Falle". Seit 260909-laa (Aufgabe 3) laufen sowohl die Treffer-Anlage (`upsert`) als auch der Instant-Dispatch (`findMany`/`updateMany`) vollständig über `forTenant()`, EIN gebundener Client je Profil. |
|
||
| apps/api/src/tenders/tender-matching.service.ts | tenderSavedSearch | beides | ungebunden | Gespeicherte Suchprofile ALLER Mandanten werden gegen neue Treffer geprüft — bewusst übergreifend, Etappe-3-Übergabe (260909-laa, Aufgabe 3, unverändert). |
|
||
| apps/api/src/tenders/tender-matching.service.ts | user | beides | gebunden | E-Mail-Adressen für die Sofortmeldung. Seit 260909-laa (Aufgabe 3) läuft der einzige Zugriff über `forTenant()`, gebunden an den Mandanten des jeweiligen Profils. |
|
||
| apps/api/src/tenders/tender-notification-pref.service.ts | tenderNotificationPref | muss-mandantengebunden | gebunden | Nutzer-CRUD für die eigenen Benachrichtigungseinstellungen — Mandant aus der Anfrage bekannt. Seit 260909-laa (Aufgabe 2) laufen `getForUser`/`setForUser` vollständig über `forTenant()`; `setForUser` übersetzt eine P2002-Verletzung (Befund F) in eine verständliche deutsche Meldung. |
|
||
| apps/api/src/tenders/tender-rss-feed.service.ts | tenderRssFeedSource | beides | gemischt | Nutzer-CRUD für die eigenen RSS-Quellen (anders als der Fan-out in `adapters/rss.adapter.ts`). Seit 260909-laa (Aufgabe 2) bindet `createForUser` (Zähler + Anlage, beide ausschließlich auf persönlichen Zeilen mit gesetztem Mandanten) über `forTenant()`; `listForUser`/`createPlatform`/`remove` bleiben bewusst ungebunden, weil sie (auch) die nullbare, plattformweite Zeile berühren (WINDOWS #19, Aufgabe 1 gemessen: eine gebundene Zeile wäre unter jedem Mandanten unsichtbar bzw. ein gebundenes Einfügen ohne Mandant würde abgewiesen). Korrektur der Klasse von `muss-mandantengebunden` auf `beides`, keine Verhaltensänderung — derselbe Präzedenzfall wie `ldapConfig` in 260909-ipc. |
|
||
| apps/api/src/tenders/tender-saved-search.service.ts | tenderSavedSearch | muss-mandantengebunden | gebunden | Nutzer-CRUD für die eigenen gespeicherten Suchprofile — Mandant aus der Anfrage bekannt. Seit 260909-laa (Aufgabe 2) laufen `list`/`create`/`update`/`remove` vollständig über `forTenant()`. |
|
||
| apps/api/src/tenders/tender-scheduler.service.ts | tenderSourcePollConfig | keine-mandantengebundene-tabelle | ungebunden | Plattformweiter DÖE-Poll-Status, kein `tenantId` — im Dateikopf explizit als "genuine platform-wide singleton" begründet. |
|
||
| apps/api/src/tenders/tender-triage.service.ts | tenderTriage | muss-mandantengebunden | gebunden | Favorisierungs-/Ablehnungsstatus eines Nutzers, `tenantId`-Spalte vorhanden. Seit 260909-laa (Aufgabe 2) laufen `setTriage`/`listForUser`/`favoriteIds` vollständig über `forTenant()`. |
|
||
| apps/api/src/tenders/tenders.controller.ts | tender | keine-mandantengebundene-tabelle | ungebunden | Lesezugriff auf den plattformweiten Katalog (D-03). |
|
||
| apps/api/src/tenders/tenders.controller.ts | tenderSourcePollConfig | keine-mandantengebundene-tabelle | ungebunden | Plattformweiter Poll-Status, admin-verwaltet, kein `tenantId`. |
|
||
| apps/api/src/tenders/tenders.module.ts | tenderSourcePollConfig | keine-mandantengebundene-tabelle | ungebunden | Singleton-Bestückung beim Boot — im Dateikopf explizit als "global, RLS-exempt (D-03)" begründet. |
|
||
| apps/api/src/user/admin-seed.service.ts | tenant | keine-mandantengebundene-tabelle | ungebunden | Legt beim ersten Start den Standard-Mandanten selbst an 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. 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`.
|