9039cea686
- Bytes liegen unter user-files/dashboard-images/<userId>/<id>.<ext>, die Zeile haelt nur noch storagePath (Muster User.avatarPath) - Dateiname immer servergeneriert: UUID der Zeile + Endung aus dem ERKANNTEN Mime-Typ, originalName kommt in keinem Pfad vor (T-HK4-01) - Migration 20260922120000: storagePath dazu, data wird NULLbar, kein DROP (zweistufig, T-HK4-03); system_read_policy fuer den Umzug - onApplicationBootstrap zieht Altbestand automatisch um: systemgebunden lesen, je Zeile mandantengebunden schreiben (Muster DKV-Planer) - Upload nimmt die Zeile bei fehlgeschlagenem Schreiben zurueck, Loeschen entfernt die Datei mit, fehlende Datei -> 404 (T-HK4-04) - 11 neue Dienst-Tests gegen ein echtes Temp-Verzeichnis (kein fs-Mock) - Zugriffsklassifikation: Stand system-gebunden, Zahlen nachgemessen Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
831 lines
94 KiB
Markdown
831 lines
94 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.
|
||
|
||
**Entschieden (260911-e2s, Aufgabe 2):** ersatzloser Entfall, fuer ALLE
|
||
Bereiche der Etappe 2, nicht nur fuer `tenant`. Gemessen: außerhalb von
|
||
`tenant.middleware.ts` und `tenant.guard.ts` gab es KEINEN Leser (Suchumfang
|
||
oben bestaetigt, ebenso erneut gemessen in 260911-e2s Aufgabe 1, Befund B);
|
||
`tenant.middleware.ts` war zudem NIRGENDS verdrahtet (kein
|
||
`MiddlewareConsumer`, kein `configure(` in ganz `apps/api`, gemessen)
|
||
und hatte — anders als der urspruengliche Befund oben suggerierte — auch
|
||
KEINE eigenen Tests, ebenso wenig wie der Guard (`ls apps/api/src/tenant/`
|
||
vor 260911-e2s: einzige Testdatei war `tenant.service.spec.ts`). Entscheidung:
|
||
`tenant.middleware.ts` ist GELOESCHT, `tenant.guard.ts` setzt nur noch
|
||
`req.tenantId` und hat keine Prisma-Abhaengigkeit mehr. Grund: alle neun vor
|
||
diesem Bereich umgestellten Bereiche binden ausnahmslos dienst-intern (ein
|
||
Klient je Methode) — die Konvention ist durch Praxis entschieden, und tote
|
||
Verdrahtung, die wie ein Sicherheitsmechanismus aussieht, ist schlimmer als
|
||
keine. Siehe `docs/mandantentrennung-etappe2-fehlerrichtung.md`, Abschnitt
|
||
"## Bereich tenant", (n4)(a), fuer Messung und Begruendung im Volltext.
|
||
|
||
**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.
|
||
|
||
**Zweite methodische Lücke, seit 260911-mkj geschlossen — aber nur in der
|
||
Bestandsaufnahme:** die beiden Rohtreffer-Greps dieser Tabelle
|
||
(`this\.prisma\.[a-zA-Z]*` bzw. `tenantPrisma\.[a-zA-Z]*\.`) zählen
|
||
Relationsziele (`include:`, `select:`, `_count:`, Relationsfilter in
|
||
`where:`/`orderBy:`) strukturell NICHT — ein `include: { module: true }`
|
||
auf `tenantPrisma.tenantModuleActivation` erzeugt keinen Rohtreffer auf
|
||
`module`, weil `module` in der Datei nie als `tenantPrisma.module` steht.
|
||
Die Spalten und die Summenzeile behalten deshalb ihre bisherige Bedeutung
|
||
und ihre bisherigen Werte (weiterhin 68 ungebunden / 178 gebunden, NACH-
|
||
GERECHNET, nicht abgeschrieben) — sie zählen weiterhin nur direkte
|
||
Modellaufrufe. Autoritativ für Relationszugriffe ist allein die
|
||
Bestandsaufnahme unten, deren vierte Erkennungsform
|
||
(`rls-access-inventory.spec.ts`, `analyzeSource`) die Ziele als eigene
|
||
Paare führt: sieben der 72 Paare dort tauchen in KEINER Rohtrefferzahl
|
||
dieser Übersicht auf, zum Beispiel
|
||
`module-registry/module-access.service.ts`/`group` (nur über den
|
||
Relationsfilter `group: { memberships: { some: { userId } } }` sichtbar,
|
||
niemals als `tenantPrisma.group` im Quelltext).
|
||
|
||
**Dritte Spalte `System` (260914-eym, Etappe 3c):** Rohtreffer
|
||
`systemPrisma\.[a-zA-Z]*\.` je Bereich, gleiche Grep-Form wie die beiden
|
||
anderen Spalten (nur `.ts` ohne `.spec.ts`) — die direkten Modellaufrufe
|
||
über den Systemkontext-Klienten `forSystem()`. Dieselbe Grenze wie die
|
||
anderen beiden Spalten: Relationsziele (`include: { fieldMappings }` in
|
||
`ldap-config.service.ts`) zählt auch sie NICHT; autoritativ bleibt die
|
||
Bestandsaufnahme unten (Stand `system-gebunden`). Die Werte aller drei
|
||
Spalten sind mit der Schleife aus dem Gate von 260914-eym nachgerechnet
|
||
(`for d in apps/api/src/*/`), nicht abgeschrieben.
|
||
|
||
| Bereich | Ungebunden | Gebunden | System | Hinweis |
|
||
|---|---|---|---|---|
|
||
| tenders | 33 | 27 | 2 | **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). **260914-eym:** diese beiden Hälften (Kandidatenabfrage des Digest, Profilabfrage des Abgleichs) lesen jetzt über `forSystem()` — 35→33 ungebunden, 2 System |
|
||
| groups | 0 | 31 | 0 | **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 | 1 | 27 | 2 | **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 waren bewusst: `getAllActiveConfigs`/`onApplicationBootstrap` (Befund B) und `resolveEmailForWrite` (Befund A, T-IPC-04). **260914-eym:** die beiden Leser in `ldap-config.service.ts` laufen über `forSystem()` (4→1 ungebunden, 2 System), die Schreibzeile der Nachverschlüsselung über `forTenant()` (26→27 gebunden); der eine verbleibende ungebundene Rohtreffer ist `resolveEmailForWrite` |
|
||
| dkv | 0 | 22 | 1 | **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 war der benannte Planer-Startpfad `loadAnyActiveConfigForScheduler()` (Befund D, WINDOWS #21). **260914-eym:** ersetzt durch `loadActiveConfigsForScheduler()` über `forSystem()` (1→0 ungebunden, 1 System) — WINDOWS #21 geschlossen |
|
||
| user | 8 | 14 | 0 | **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 | 0 | **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 | 21 | 1 | **260922-hk4:** 18→21 gebunden, 0→1 System — die Bilderrahmen-Bilder liegen jetzt im Dateibereich statt in der Spalte `data`. Drei zusätzliche gebundene Rohtreffer in `dashboard-images.service.ts`: das Nachtragen von `storagePath` nach dem Upload (die UUID steht erst nach `create` fest), das Zurücknehmen der Zeile bei fehlgeschlagenem Schreiben, und das Nachtragen im Umzug beim Start. Der eine System-Rohtreffer ist die Lesehälfte dieses Umzugs (`onApplicationBootstrap`, Zeilen ohne `storagePath` über ALLE Mandanten, Muster DKV-Planer) — geschrieben wird auch dort je Zeile mandantengebunden. Nachgemessen mit der Gate-Schleife. Vorher: **260921-pi9:** 12→18 gebunden — `dashboard-images.service.ts` (Bilderrahmen) bringt sechs gebundene `dashboardImage`-Rohtreffer (`findMany`, `count`, `create`, zweimal `findUnique`, `delete`), nachgemessen mit der Gate-Schleife. Vorher: **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 | 3 | 10 | 0 | **war 8/5** — 260911-fh9 (Aufgabe 2) hat `getMe`, `changePassword`, `adminResetPassword` (fünf Rohtreffer auf `user`, drei Methoden) auf `forTenant()` umgestellt. Die 3 verbleibenden ungebundenen Rohtreffer sind die `$queryRaw`-Aufrufe der drei Anmeldefunktionen (`validateUser`, `requestPasswordReset`, `resetPassword`) — KEINE Modellzugriffe (`$` liegt nicht in `[a-zA-Z]`, die Bestandsaufnahme führt sie deshalb nicht als (Datei, Modell)-Paar), bewusst und dauerhaft ungebunden, siehe `20260909160000_auth_lookup_functions` und `docs/mandantentrennung-etappe2-fehlerrichtung.md`, Abschnitt "## Bereich auth", (h1) |
|
||
| calendar | 0 | 12 | 0 | **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 | 3 | 0 | **war 8/0** — 260911-e2s (Aufgabe 3) hat drei gebundene Benutzerzähler in `tenant.controller.ts` eingeführt (Fan-out je Mandant nach dem Muster von `UserService.findAllForPlatformAdmin`, ersetzt die drei vorherigen Relationszähler); die acht `tenant`-Zugriffe selbst BLEIBEN ungebunden — `Tenant` trägt keine Regel in irgendeiner ausgelieferten Migration (260911-e2s Aufgabe 1, Prüfung 1/2), hier ist Ungebundenheit richtig, nicht geduldet |
|
||
| favorites | 0 | 8 | 0 | **war 7/0** — 260911-gwh (Aufgabe 2) hat `favorites.service.ts` vollständig auf `forTenant()` umgestellt: `list`, `create`, `update`, `remove`, `getIconBytes` laufen je über EINEN Klienten `tenantPrisma` (7 gebundene `favoriteLink`-Rohtreffer); `create` prüft zusätzlich über einen gebundenen `widgetInstance.findUnique`, dass das Ziel-Widget dem Aufrufer gehört (T-GWH-05, Befund F aus Aufgabe 1: der Fremdschlüssel prüft am Zeilenschutz vorbei) — der achte gebundene Rohtreffer dieser Zeile |
|
||
| bug-reports | 0 | 1 | 0 | neu (260914-m97), ein gebundener Zugriff |
|
||
| settings | 0 | 4 | 0 | **Nachgemessen 260921-pi9: 4 gebundene Rohtreffer** (die Tabelle nannte 3; der vierte `smtpConfig`-Zugriff kam mit 260914-m97/`bugReportRecipient` hinzu, ohne dass die Zeile nachgezogen wurde). **war 4/0** — 260911-gwh (Aufgabe 2) hat `getSmtpConfig`, `saveSmtpConfig`, `getDecryptedSmtpConfig` auf `forTenant()` umgestellt (3 gebundene `smtpConfig`-Rohtreffer). Der eine verbleibende ungebundene Rohtreffer war der umbenannte Planer-Startpfad `loadAnySmtpConfigForStartupTransport()` (Befund D, WINDOWS #30). **260914-eym:** GELÖSCHT — `MailService` baut je Versand einen Transport über `getDecryptedSmtpConfig(tenantId)` (1→0 ungebunden, 0 System, kein Systemkontext nötig); Befund K (`tenders`/`dkv`/`mail` hängen an `getDecryptedSmtpConfig`) ist damit erfüllt — WINDOWS #30 geschlossen |
|
||
| **Summe** | **61** | **190** | **6** | **260922-hk4:** Gebunden 187→190, System 5→6 (beides `dashboard`, siehe dortige Zeile), Ungebunden unverändert — nachgerechnet mit derselben Gate-Schleife, nicht abgeschrieben. **260921-pi9:** Gebunden 179→187, nachgerechnet mit der Gate-Schleife: +6 in `dashboard` (Bilderrahmen), +1 in `settings` (Zeile war seit 260914-m97 um eins zu niedrig), +1 fuer `bug-reports` (Zeile seit 260914-m97 vorhanden, in der Summe aber nie mitgezaehlt) — die Summe stimmt damit wieder mit den Bereichszeilen ueberein. **260914-eym:** Ungebunden 68→61 (`tenders` −2, `ldap` −3, `dkv` −1, `settings` −1), Gebunden 178→179 (`ldap` +1), System 5 (`dkv` 1, `ldap` 2, `tenders` 2) — nachgerechnet mit der Gate-Schleife, nicht abgeschrieben. Vorgeschichte: 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), dann 83 nach 260911-cwh (`calendar` 12→0), unverändert nach 260911-e2s (`tenant` bleibt bei 8 ungebundenen Rohtreffern), dann 78 nach 260911-fh9 (`auth` 8→3), jetzt 68 nach 260911-gwh (`favorites` 7→0, `settings` 4→1). 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`), dann 159 nach 260911-cwh (zusätzlich 12 in `calendar`), dann 162 nach 260911-e2s (zusätzlich 3 in `tenant`), dann 167 nach 260911-fh9 (zusätzlich 5 in `auth`), jetzt 178 nach 260911-gwh (zusätzlich 8 in `favorites`, 3 in `settings`). Dies ist der ENDSTAND der Etappe 2: jeder verbleibende ungebundene Rohtreffer ist einer der in diesem Dokument benannten, bewusst ungebundenen Fälle. 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, 74 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.
|
||
|
||
**Nachtrag 260921-pi9:** 74 Paare — ein neues Paar
|
||
`dashboard/dashboard-images.service.ts`/`dashboardImage`
|
||
(muss-mandantengebunden, gebunden) fuer die hochgeladenen Bilder des
|
||
Bilderrahmen-Widgets. Nachgezaehlt mit `grep -cE` ueber die Fundstellen-
|
||
tabelle: vor diesem Eintrag standen dort bereits 73 Zeilen, nicht 72 — das
|
||
Paar `bug-reports/bug-reports.service.ts`/`user` (260914-m97,
|
||
muss-mandantengebunden) war in der Tabelle eingetragen, in dieser
|
||
Verteilung aber nie mitgezaehlt. Beide Korrekturen (72→74, 35→37) sind
|
||
gemessen, 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.
|
||
|
||
**Stand 260911-e2s (Aufgabe 3):** 64 Paare — 63 aus dem vorherigen
|
||
Durchlauf plus EIN neues Paar (`tenant.controller.ts`/`user`, Klasse
|
||
`muss-mandantengebunden`, Stand `gebunden`): die drei gebundenen
|
||
Benutzerzähler des Fan-outs (Befund F/G aus 260911-e2s Aufgabe 1). Keine
|
||
bestehende Klasse verschiebt sich — die beiden `tenant`-Paare
|
||
(`tenant.controller.ts`/`tenant`, `tenant.service.ts`/`tenant`) bleiben
|
||
`keine-mandantengebundene-tabelle`/`ungebunden`, nur ihre Begründung wird
|
||
fortgeschrieben (siehe Fundstellentabelle unten).
|
||
|
||
**Stand 260911-fh9 (Aufgabe 3): unveraendert, ausdruecklich festgehalten
|
||
statt uebersprungen.** Weiterhin 64 Paare, keine Klasse verschiebt sich. Das
|
||
Paar `apps/api/src/auth/auth.service.ts`/`user` war bereits vor diesem
|
||
Durchlauf korrekt klassifiziert (`muss-mandantengebunden`) — Aufgabe 2
|
||
(260911-fh9) aendert nur seine `Stand`-Spalte (`gemischt` auf `gebunden`),
|
||
nicht seine Klasse. Das Paar `apps/api/src/auth/auth.service.ts`/
|
||
`passwordResetToken` bleibt `muss-mandantengebunden`/`gebunden`,
|
||
unveraendert. 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-gwh (Aufgabe 3):** 65 Paare — 64 aus dem vorherigen Durchlauf
|
||
plus EIN neues Paar (`favorites.service.ts`/`widgetInstance`, Klasse
|
||
`muss-mandantengebunden`, Stand `gebunden`): der Besitzriegel in `create()`
|
||
(T-GWH-05, Aufgabe 1 Pruefung 7 hat den Fremdschluessel-Durchgriff
|
||
bestaetigt). Zwei Paare aendern nur ihre `Stand`-Spalte, keine ihrer Klasse:
|
||
`favorites.service.ts`/`favoriteLink` (`ungebunden` auf `gebunden`) und
|
||
`settings.service.ts`/`smtpConfig` (`ungebunden` auf `gemischt`). Das ist
|
||
der ENDSTAND der Etappe 2 — die Zahl ist der Ausgabe von
|
||
`rls-access-inventory.spec.ts` entnommen, nicht geschaetzt.
|
||
|
||
**Stand 260911-mkj (Aufgabe 1/2):** 72 Paare — 65 aus dem Endstand der
|
||
Etappe 2 plus SIEBEN neue Paare der vierten Erkennungsform (WINDOWS #27):
|
||
`groups/module-grants.service.ts`/`module` (`keine-mandantengebundene-tabelle`,
|
||
`gebunden`), `ldap/ldap-config.service.ts`/`tenant`
|
||
(`keine-mandantengebundene-tabelle`, `ungebunden`),
|
||
`module-registry/module-access.service.ts`/`group` UND `/groupMembership`
|
||
(beide `muss-mandantengebunden`, `gebunden`),
|
||
`tenders/tender-digest.scheduler.ts`/`tender`
|
||
(`keine-mandantengebundene-tabelle`, `gebunden`) UND `/tenderSavedSearch`
|
||
(`muss-mandantengebunden`, `gebunden`),
|
||
`tenders/tenders.controller.ts`/`tenderSource`
|
||
(`keine-mandantengebundene-tabelle`, `ungebunden`). Drei Paare aendern ihren
|
||
Stand, eines davon zusaetzlich die Klasse:
|
||
`ldap-config.service.ts`/`ldapFieldMapping` wechselt von
|
||
`muss-mandantengebunden`/`gebunden` auf `beides`/`gemischt` — der
|
||
uebergreifende Planer-Lesepfad `getAllActiveConfigs()` reicht ueber
|
||
`include: { fieldMappings: true }` in `LdapFieldMapping` hinein, dieselbe
|
||
Unterabfrage-Form, die WINDOWS #27 aufgedeckt hat.
|
||
`module-registry.service.ts`/`module` und `tender-matching.service.ts`/
|
||
`tender` wechseln je von `ungebunden` auf `gemischt`, ihre Klasse bleibt.
|
||
Die Zahl 72 ist der Ausgabe von `rls-access-inventory.spec.ts` entnommen,
|
||
nicht geschaetzt.
|
||
|
||
**Stand 260911-nke:** die Paarzahl (72) und die Klassen-Verteilung sind
|
||
UNVERÄNDERT — Etappe 3b (Benutzerdimension in den Regeln, `forTenant()`
|
||
bekommt ein drittes Argument) fügt keine neue Fundstelle hinzu und ändert
|
||
keine bestehende von mandanten-gebunden auf mandanten-ungebunden oder
|
||
umgekehrt; benutzer-gebunden ist keine eigene Klasse in diesem Schema. Die
|
||
Benutzerdimension steht stattdessen in der Begründungsspalte der drei
|
||
betroffenen Bestandsaufnahme-Zeilen (`calendarSource`, `widgetInstance`,
|
||
`favoriteLink`) oben und im Abschnitt "Was diese Etappe NICHT entscheidet".
|
||
|
||
|
||
**Stand 260914-eym:** die Paarzahl (72) und die Klassen-Verteilung sind
|
||
UNVERÄNDERT — die fünfte Erkennungsform (`const X = forSystem(`) bringt
|
||
keine neue Fundstelle und lässt keine verschwinden. Sieben Paare ändern nur
|
||
ihren Stand: SECHS auf den neuen Wert `system-gebunden` —
|
||
`dkv/dkv.service.ts`/`dkvModuleConfig`,
|
||
`ldap/ldap-config.service.ts`/`ldapConfig`, `/ldapFieldMapping`, `/tenant`,
|
||
`tenders/tender-digest.scheduler.ts`/`tenderMatch`,
|
||
`tenders/tender-matching.service.ts`/`tenderSavedSearch` — und EINES auf
|
||
`gebunden` (`settings/settings.service.ts`/`smtpConfig`, Startpfad
|
||
gelöscht). Der neue Stand-Wert bedeutet: mindestens ein Zugriff dieses
|
||
Paars läuft über den Systemkontext-Klienten und KEIN Zugriff ist ungebunden;
|
||
Vorrang: ungebunden vorhanden UND anderes → `gemischt`, nur ungebunden →
|
||
`ungebunden`, System ohne ungebunden → `system-gebunden` (auch neben
|
||
mandantengebundenen Zugriffen — die Begründungsspalte nennt sie), sonst
|
||
`gebunden`. Die Zahlen sind der Ausgabe von `rls-access-inventory.spec.ts`
|
||
entnommen (30 Zusicherungen, darunter der Wachhund
|
||
`FORSYSTEM_ALLOWED_CALL_SITES`).
|
||
|
||
| Klasse | Anzahl Paare |
|
||
|---|---|
|
||
| muss-mandantengebunden | 37 |
|
||
| keine-mandantengebundene-tabelle | 21 |
|
||
| beides | 14 |
|
||
| bewusst-uebergreifend | 2 |
|
||
| **Summe** | **74** |
|
||
|
||
## Der Hintergrunddienst als Falle — sechs 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. Der
|
||
sechste Fall (seit 260911-gwh) ist von DERSELBEN Bauart wie der fünfte —
|
||
kein Iterieren, eine beliebige-aber-vorhandene Zeile, kein Mandantenkontext
|
||
beim Start — mit einer zusätzlichen Verdeckungsschicht, siehe unten:
|
||
|
||
- **`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.
|
||
|
||
**Nachtrag (260911-mkj):** WINDOWS #27 geschlossen — derselbe Bereich
|
||
`ldap` traegt
|
||
einen zweiten, bislang unsichtbaren Planer-Lesezugriff:
|
||
`LdapConfigService.getAllActiveConfigs()` (`ldap-config.service.ts`)
|
||
reicht ueber `include: { tenant: true, fieldMappings: true }` sowohl in
|
||
`Tenant` (schutzlos, harmlos) als auch in `LdapFieldMapping` (geschuetzt
|
||
ueber Join auf `LdapConfig`) hinein. Nach dem Scharfschalten verstummt
|
||
die Feldzuordnung mit der Elternzeile, nicht getrennt von ihr — der
|
||
Etappe-3-Systemkontext muss BEIDE Tabellen sehen. Die Bestandsaufnahme
|
||
fuehrt `ldapFieldMapping` deshalb seither als `beides`/`gemischt` statt
|
||
`muss-mandantengebunden`/`gebunden` (siehe Bestandsaufnahme unten,
|
||
`ldap-config.service.ts`/`ldapFieldMapping`).
|
||
- **`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`).
|
||
|
||
**Der sechste Fall, gleicher Bauart wie der fünfte — `mail.module.ts` /
|
||
`SettingsService.loadAnySmtpConfigForStartupTransport()`** (260911-gwh,
|
||
Befund D, WINDOWS #30): offen, als benannte Altlast weitergeführt. Dieselbe
|
||
Form wie der fünfte Fall — `findFirst()` ohne jede Bedingung zieht bei
|
||
mehreren Mandanten EINEN beliebigen und bedient die übrigen NIE; **heute
|
||
bereits falsch**, weil der SMTP-Server und die Absenderadresse EINES
|
||
beliebigen Mandanten die Kennwort-Zurücksetzungs- und Willkommensmails
|
||
ALLER Mandanten tragen (T-GWH-03, Nutzung fremder Zugangsdaten). Binden ist
|
||
auch hier keine Lösung: `useFactory` hat beim Start strukturell keinen
|
||
Mandantenkontext. Der Umbau auf Transport je Versand aus
|
||
`getDecryptedSmtpConfig(tenantId)` — die Form, die `DkvMailService`/
|
||
`TenderMailService` bereits haben — ist eine Funktionsänderung
|
||
(Umbau des Mailmoduls), kein Bindungsumbau, deshalb NICHT in Etappe 2
|
||
vorgenommen.
|
||
|
||
Die UNSYMMETRIE zu BEIDEN Präzedenzfällen: `ldap.service.ts`/
|
||
`getAllActiveConfigs` ist heute korrekt und verstummt erst später; der
|
||
DKV-Planer (fünfter Fall) ist heute bereits falsch und verstummt zusätzlich
|
||
später, ABER MIT einer Protokollzeile ("no active config found"). Der
|
||
Mail-Startpfad ist heute bereits falsch UND verstummt später OHNE
|
||
Protokollzeile, weil `mail.module.ts`s Rückfallkette (Priorität 2 `MAIL_*`,
|
||
3 `TESSERA_SMTP_*`, 4 `localhost:1025`) einen FALSCHEN, aber vorhandenen
|
||
Transport an die Stelle der Leere setzt — `MailService` fängt den
|
||
Transportfehler (T-02-12), der Controller antwortet `200`. Das Verstummen
|
||
ist damit DOPPELT verdeckt, eine dritte Ausprägung, die keiner der beiden
|
||
Vorlagen (`ldap`, `dkv`) vollständig entspricht.
|
||
|
||
Dreifache Markierung: eigene benannte Methode mit Kopfkommentar (dkv-
|
||
Präzedenzfall, ein Name, den niemand für einen Anfrageweg hält), Modul-
|
||
kommentar in `mail.module.ts`, Ledger-Eintrag WINDOWS #30 (EIGENER Eintrag
|
||
statt Anschluss an #21: andere Datei, andere Reparatur, andere
|
||
Verdeckungsform). Das Signal für das Verstummen gehört ebenfalls in die
|
||
Vorabprüfung von Etappe 4 (`rls-preflight.mjs`).
|
||
|
||
Befund K ist mit dieser Bindung ERFÜLLT: `getDecryptedSmtpConfig(tenantId)`
|
||
— der einzige Versandpfad von `tender-mail.service.ts` und
|
||
`dkv-mail.service.ts` — läuft seit 260911-gwh über `forTenant()`
|
||
(siehe Bestandsaufnahme-Zeile `settings.service.ts`/`smtpConfig` oben und
|
||
`docs/mandantentrennung-etappe2-fehlerrichtung.md`, Abschnitte "## Bereich
|
||
tenders" (t4) und "## Bereich dkv" (d4), jeweils Nachtrag 260911-gwh); die
|
||
Etappe-4-Vorabprüfung muss diese Reihenfolgebedingung ab jetzt NICHT mehr
|
||
führen.
|
||
|
||
**Stand 260911-gwh — der Bereich `favorites` fügt diesem Abschnitt keinen
|
||
weiteren Fall hinzu, gemessen statt angenommen (Befund K).** Anweisung:
|
||
`grep -rn "@Cron\|onModuleInit\|onApplicationBootstrap\|setInterval\|setTimeout" apps/api/src/favorites apps/api/src/settings --include=*.ts | grep -v spec`
|
||
liefert außerhalb von Testdateien genau EINEN Treffer,
|
||
`icon-discovery.service.ts:255` — ein `setTimeout` für den Abbruch eines
|
||
HTTP-Abrufs, kein Planer (dieselbe Form wie `ics.provider.ts:100` in
|
||
260911-cwh). Die Bauform dieses Abschnitts (übergreifend LESEN über alle
|
||
Mandanten, dann je Mandant BINDEN) kommt in `favorites` an keiner Stelle
|
||
vor; der einzige Hintergrund-Zugriff des Bereichspaares ist der oben
|
||
beschriebene sechste Fall, und der lebt nicht in `favorites`, sondern in
|
||
`mail.module.ts`/`settings.service.ts`.
|
||
|
||
**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.
|
||
|
||
Auch der Bereich `tenant` fügt diesem Abschnitt keinen sechsten Fall hinzu,
|
||
gemessen statt angenommen (260911-e2s, Aufgabe 1, Befund J). Anweisung:
|
||
`grep -rn "@Cron\|onModuleInit\|onApplicationBootstrap\|setInterval\|setTimeout\|Scheduler" apps/api/src/tenant --include=*.ts`
|
||
und `grep -rn '\$transaction(\|\$queryRaw\|\$executeRaw' apps/api/src/tenant --include=*.ts`
|
||
liefern je null Treffer — kein Hintergrunddienst, kein Roh-SQL, keine
|
||
Transaktion in diesem Bereich. `TenantService.create` ruft nach dem Anlegen
|
||
eines Mandanten `groupsService.ensureDefaultGroup(tenant.id)` auf; dieser
|
||
Aufruf läuft seit 260909-jts bereits vollständig über den gebundenen Weg des
|
||
Bereichs `groups` (`forTenant()`/`withTenantTransaction()`, siehe
|
||
Fundstellentabelle unten, `groups.service.ts`/`group`, Stand `gebunden`) —
|
||
nichts an dieser Übergabe musste in 260911-e2s umgestellt werden.
|
||
|
||
**Stand 260911-fh9 — auch der Bereich `auth` fügt diesem Abschnitt keinen
|
||
sechsten Fall hinzu, gemessen statt angenommen (Befund J).** Anweisung:
|
||
`grep -rn "@Cron\|onModuleInit\|onApplicationBootstrap\|setInterval\|setTimeout" apps/api/src/auth --include=*.ts`
|
||
und `grep -rn '\$transaction(' apps/api/src/auth --include=*.ts` liefern je
|
||
null Treffer außerhalb von Testdateien — kein Hintergrunddienst, keine
|
||
Transaktion in diesem Bereich. `grep -rn "include:\|_count\|select:"
|
||
apps/api/src/auth --include=*.ts | grep -v spec` findet genau EINEN
|
||
Treffer, das `select` in `getMe` — ausschließlich skalare Felder von
|
||
`User`, keine Relation (WINDOWS #27 hier ohne Ausprägung). Die einzige
|
||
Stelle, an der dieser Bereich ohne Mandantenkontext liest, ist der
|
||
Anmeldeweg (`validateUser`, `requestPasswordReset`, `resetPassword`) —
|
||
geloest durch die drei SECURITY-DEFINER-Funktionen, nicht durch die Bauform
|
||
"übergreifend lesen, dann je Mandant binden".
|
||
|
||
**Regelschluss (260914-eym) — Etappe 3c hat den Systemkontext gebaut; je Fall:**
|
||
|
||
- **Regelschluss (260914-eym), Fall ldap** (`getAllActiveConfigs()` und die
|
||
Nachverschlüsselung in `onApplicationBootstrap()`): beide Methoden lesen
|
||
über `forSystem()` (`system_read_policy … FOR SELECT` auf LdapConfig und —
|
||
für `include: { fieldMappings }` — auf LdapFieldMapping); die
|
||
Nachverschlüsselung schreibt je Altzeile GEBUNDEN über
|
||
`forTenant(this.prisma, config.tenantId)`, weil Schreiben unter
|
||
Systemkontext abgewiesen wird (gemessen P2025/42501). `Tenant` braucht
|
||
keine Regel. Der Detektor zählt zwei `forSystem(`-Aufrufe in dieser Datei.
|
||
- **Regelschluss (260914-eym), Fall tender-digest** (`runDigest`): die
|
||
Kandidatenabfrage liest über `forSystem()` (Regel auf TenderMatch), die
|
||
Schleife bleibt je Kandidatenzeile gebunden, bewusst ohne Benutzer.
|
||
- **Regelschluss (260914-eym), Fall tender-matching** (`matchDelta`): die
|
||
Profilabfrage liest über `forSystem()` (Regel auf TenderSavedSearch);
|
||
Treffer-Anlage und Sofortmeldung bleiben je Profil gebunden, der
|
||
Katalog-Lesezugriff (`tender`, D-03) bleibt ungebunden.
|
||
- **Regelschluss (260914-eym), Fall admin-seed**: GEMESSEN — der einzige
|
||
Lesezugriff außerhalb der Schleife ist `tenant.findMany` auf `Tenant`, das
|
||
in keiner Migration `ENABLE ROW LEVEL SECURITY` trägt. Nichts umgebaut,
|
||
Datei unverändert (Gate gegen `5e0e408`), nur dokumentiert; Stand bleibt
|
||
`ungebunden`.
|
||
- **Regelschluss (260914-eym), fünfter Fall (DKV-Planer, WINDOWS #21
|
||
GESCHLOSSEN)**: `loadActiveConfigsForScheduler()` liest über `forSystem()`
|
||
ALLE aktiven Konfigurationen (Regel auf DkvModuleConfig), der Planer
|
||
registriert je Mandant einen eigenen Auftrag `dkv-inbox-poll:<tenantId>`
|
||
(einmal-abfragen-viele-bedienen); mit einem Mandanten beobachtbar
|
||
identisch (Cron-Expression, Tick, Protokollzeile — je ein Test).
|
||
- **Regelschluss (260914-eym), sechster Fall (Mail-Startpfad, WINDOWS #30
|
||
GESCHLOSSEN)**: der sechste Fall EXISTIERT NICHT MEHR — der Startpfad
|
||
(`findFirst()` beim Boot) ist entfernt, nicht umgestellt; `MailService`
|
||
baut je Versand einen Transport aus `getDecryptedSmtpConfig(tenantId)`
|
||
des Empfänger-Mandanten. Deshalb trägt SmtpConfig keine
|
||
`system_read_policy`.
|
||
|
||
## 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>`
|
||
oder — seit 260914-eym — in `<System-Client>.<Modell>` verwendet), Klasse,
|
||
Stand (`gebunden`/`ungebunden`/`gemischt`/`system-gebunden`, seit
|
||
260909-ipc maschinell gegen den Quelltext geprüft), Begründung.
|
||
|
||
**Fünfte Erkennungsform und vierter Stand-Wert (260914-eym, Etappe 3c):**
|
||
`const <Name> = forSystem(` und danach `<Name>.<Modell>` — der benannte
|
||
Systemkontext der Hintergrunddienste (liest über ALLE Mandanten, nur lesend,
|
||
Regel `system_read_policy … FOR SELECT`). Relationsziele über `include`/
|
||
`select` auf einem System-Klienten zählen ebenfalls als system-gebunden.
|
||
Vorrang der Stände je Paar: ungebunden vorhanden UND anderes → `gemischt`;
|
||
nur ungebunden → `ungebunden`; Systemkontext vorhanden und KEIN ungebundener
|
||
Zugriff → `system-gebunden` (auch wenn daneben mandantengebundene Zugriffe
|
||
stehen — die Begründungsspalte nennt sie); nur mandantengebunden →
|
||
`gebunden`. Wer `forSystem(` rufen darf, steht mit EXAKTER Zahl je Datei in
|
||
`FORSYSTEM_ALLOWED_CALL_SITES` (Detektor) — jede Fremddatei, jede Abweichung
|
||
der Zahl und jeder veraltete Eintrag machen die Spec rot.
|
||
|
||
**Erkennungslücke GESCHLOSSEN (260911-mkj, WINDOWS #27):** bis 260911-e2s sah
|
||
die Bestandsaufnahme ausschließlich (Datei, Modell)-Paare über
|
||
`this.prisma.<Modell>` bzw. `<gebundener Client>.<Modell>` — eine
|
||
Relationseinbindung (`include:`, `select:`, Relationszähler `_count`) in
|
||
eine ZWEITE Tabelle erzeugte kein eigenes Paar und war für das Werkzeug
|
||
strukturell unsichtbar, obwohl Prisma sie als Unterabfrage/Join auf die
|
||
zweite Tabelle unter DEREN Regel rendert. Eine vierte Erkennungsform in
|
||
`rls-access-inventory.spec.ts` (`analyzeSource`, `SCHEMA_RELATIONS`) löst
|
||
Relationsfelder über `schema.prisma` auf ihr Zielmodell auf und trägt das
|
||
Zielmodell seither als eigene Fundstelle derselben Datei — gebunden, wenn
|
||
der Empfänger des erkannten Modellaufrufs gebunden ist, sonst ungebunden.
|
||
Das erfasst `include:`, `select:`, `_count: { select: ... }`, `_count:
|
||
true`, Relationsfilter in `where:`, `orderBy:` über Relationen und
|
||
verschachtelte Schreibzugriffe in `data:`.
|
||
|
||
Bewusst NICHT gesehen, und wie das begrenzt ist: ein Empfänger außerhalb der
|
||
fünf Erkennungsformen (`this.prisma`, eine `const X = forTenant(`-Zuweisung,
|
||
ein Transaktionsparameter, `withTenantTransaction(`, seit 260914-eym eine
|
||
`const X = forSystem(`-Zuweisung) — begrenzt durch den
|
||
Waechter Rohzahl (`include|select|_count` über den gesamten kommentarfreien
|
||
Quelltext) gegen die innerhalb erkannter Aufrufe gezählte Zahl, mit
|
||
begründeter Ausnahmeliste `RELATION_SPEC_EXCEPTIONS`; ein `include:`/
|
||
`select:`-Wert aus einer fremden Datei oder ohne aufzulösende Konstante —
|
||
begrenzt durch den Wertform-Waechter (`unresolvedRelationSpecValues`); die
|
||
Kurzschreibweise `include: { x }` — gemessen null Vorkommen im gesamten
|
||
API-Quelltext, kein Prisma-Idiom für diese drei Schlüssel; eine
|
||
Vorlagen-Interpolation (`${tx.user.count()}`) — zählt in der Rohzahl und
|
||
fällt damit laut, statt still zu verschwinden.
|
||
|
||
Gemessen (260911-mkj, Ausgabe von `rls-access-inventory.spec.ts`): SIEBEN
|
||
neue Paare, DREI fortgeschriebene Stände, EINE Ausnahmedatei
|
||
(`RELATION_SPEC_EXCEPTIONS`). Die einzige zur Planungszeit bereits bekannte
|
||
gefährliche Ausprägung (ungebundener äußerer Aufruf auf einer
|
||
UNGESCHÜTZTEN Tabelle, Einbindung in eine GESCHÜTZTE Tabelle) war
|
||
`tenant.controller.ts` — bereits in 260911-e2s behoben (Fan-out ersetzt den
|
||
Relationszähler), von der vierten Erkennung erneut bestätigt: kein
|
||
Relationszugriff mehr dort. Seit 260911-mkj führt die Spalte `Modell` auch
|
||
RELATIONSZIELE, die in der Datei selbst nie als `<Klient>.<Modell>` stehen,
|
||
sondern nur über den Schlüsselpfad eines erkannten Modellaufrufs sichtbar
|
||
werden.
|
||
|
||
| 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 | gebunden | Klassenkorrektur (260911-fh9, Aufgabe 2/3): wechselt von `gemischt` auf `gebunden` — `getMe`, `changePassword`, `adminResetPassword` binden seit Aufgabe 2 je über GENAU EINEN Klienten `tenantPrisma` an den Mandanten aus dem Sitzungsnachweis (`@CurrentUser().tenantId`); für die oberste Rolle (SUPER_ADMIN) löst der Controller den Mandanten des ZIELS über den gebundenen Fan-out `UserService.findByIdForPlatformAdmin` auf. `adminResetPassword` verweigert zusätzlich einem Nicht-SUPER_ADMIN das Kennwort eines SUPER_ADMIN (T-FH9-04). Die drei Anmeldesuchen (`validateUser`, `requestPasswordReset`, `resetPassword`) laufen weiterhin über die drei SECURITY-DEFINER-Funktionen (`$queryRaw`, keine Modellzugriffe — `$` liegt nicht in `[a-zA-Z]`) und bleiben unverändert auf dem ungebundenen Klienten. Etappe-3-Vorbehalt: die Bindung hängt am Claim `tenantId` und an `User.id` (plattformweite UUID), nicht an `username`/`email` — der Anmeldeweg-Umbau für je Mandant eindeutige Anmeldenamen betrifft diese Bindung nicht, siehe `docs/mandantentrennung-etappe2-fehlerrichtung.md`, Abschnitt "## Bereich auth", (h4)(a). |
|
||
| apps/api/src/bug-reports/bug-reports.service.ts | user | muss-mandantengebunden | gebunden | Fehler-melden-Knopf (quick-260914-m97): eine gebundene Leseoperation auf die Zeile des angemeldeten Benutzers (Anzeigename, E-Mail, Rolle fuer den Bericht), Mandant ausschliesslich aus dem Sitzungsnachweis. |
|
||
| 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. Benutzerdimension seit 20260911120000 (260911-nke). |
|
||
| apps/api/src/dashboard/dashboard-images.service.ts | dashboardImage | muss-mandantengebunden | system-gebunden | **260922-hk4:** Stand von `gebunden` auf `system-gebunden` — NICHT weil ein Anfrageweg aufgeweicht wurde, sondern weil EIN Startpfad dazugekommen ist: `onApplicationBootstrap()` zieht die Bilder einmalig aus der Spalte `data` in den Dateibereich (`user-files/dashboard-images/<userId>/<id>.<ext>`) und muss dafür die noch nicht umgezogenen Zeilen ALLER Mandanten sehen (`const systemPrisma = forSystem(this.prisma)`, ein Aufruf, Erlaubnisliste in `rls-access-inventory.spec.ts`; Leserecht über `system_read_policy … FOR SELECT` auf "DashboardImage", Migration 20260922120000). GESCHRIEBEN wird auch dort je Zeile über `forTenant(prisma, row.tenantId, row.userId)` — einmal-lesen-viele-bedienen, Muster DKV-Planer. Die Bytes selbst liegen seither auf der Platte, die Zeile hält nur noch `storagePath` (Muster `User.avatarPath`); der Dateiname ist IMMER servergeneriert (UUID der Zeile + Endung aus dem ERKANNTEN Mime-Typ), `originalName` kommt in keinem Pfad vor (T-HK4-01). Alle vier Anfragewege sind unverändert mandantengebunden: Hochgeladene Bilder des Bilderrahmen-Widgets (quick-260921-pi9), gehoeren dem hochladenden Benutzer; `tenantId`-Spalte vorhanden, Regel `tenant_isolation_policy` MIT Benutzerdimension von Anfang an (Migration 20260921120000, Form aus 20260911120000). Alle vier Methoden (`list`, `upload`, `getBytes`, `remove`) holen je einen Klienten `const tenantPrisma = forTenant(this.prisma, tenantId, userId)`; Liste und Zaehler filtern zusaetzlich explizit `where: { tenantId, userId }`, `getBytes`/`remove` pruefen den Besitz anwendungsseitig (`row.userId !== userId || row.tenantId !== tenantId` -> 404, nie 403) — zweites Netz, kein Ersatz, weil der RLS-Schalter heute aus ist. `select` der Liste/Upload-Antwort ohne `data` (Bytes nur ueber `GET :id`). |
|
||
| 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). Benutzerdimension seit 20260911120000 (260911-nke). |
|
||
| 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 | system-gebunden | 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()`. Seit 260914-eym liest der Planer-Startpfad `loadActiveConfigsForScheduler()` ueber `forSystem()` (alle aktiven Konfigurationen, nur lesend, `system_read_policy`) — kein ungebundener Zugriff mehr, WINDOWS #21 geschlossen; alle uebrigen Zugriffe bleiben mandantengebunden (Stand-Vorrang: system ohne ungebunden = `system-gebunden`). |
|
||
| 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 | gebunden | Favoriten-Links eines Nutzers, `tenantId`-Spalte vorhanden. Seit 260911-gwh (Aufgabe 2) laufen `list`, `create`, `update`, `remove`, `getIconBytes` vollstaendig ueber `forTenant()`, je Methode EIN Klient `tenantPrisma`; die Besitzpruefungen (`findUnique`, Vergleich `link.userId !== userId`, dann Schreibzugriff auf DEMSELBEN Klienten) bleiben zusaetzlich bestehen — die Regel auf `FavoriteLink` kennt keine Benutzerdimension (Aufgabe 1, Pruefung 4), die `userId`-Filter sind bis zur Etappe-3-Entscheidung (2) der einzige Schutz gegen Quer-Lesen zwischen Nutzern DESSELBEN Mandanten. Die Mandantenquelle ist dieselbe wie bei `dashboard` (`extractContext` im Controller), nicht das Claim wie bei `auth`. Benutzerdimension seit 20260911120000 (260911-nke). Nachtrag (260917-jdd): `reorder()` laeuft als Mehrschritt ueber `withTenantTransaction()` (einzige gemessene atomare Form, siehe prisma-tenant.extension.ts) — diese Form setzt KEINE Benutzerdimension in der Sitzung, deshalb traegt jede Bedingung innerhalb der Transaktion `userId` UND `widgetId`; der Stand bleibt `gebunden` (Erkennungsform 2 des Detektors). |
|
||
| apps/api/src/favorites/favorites.service.ts | widgetInstance | muss-mandantengebunden | gebunden | NEUE Fundstelle (260911-gwh, Aufgabe 2): `create()` prueft ueber einen gebundenen `widgetInstance.findUnique` (`select: { userId: true }`), dass das Ziel-Widget (`dto.widgetId`) dem Aufrufer gehoert, BEVOR die Zeile angelegt wird — der Fremdschluessel `FavoriteLink.widgetId` prueft an der Zeilenschutz-Regel von `WidgetInstance` VORBEI (dokumentiertes PostgreSQL-Verhalten, Aufgabe 1 Pruefung 7 hat das GELINGEN eines gebundenen `create` mit einer fremdmandantigen `widgetId` bestaetigt); ohne den Riegel waere der Unterschied zwischen "Widget existiert nicht" (FK-Verletzung) und "gehoert einem fremden Mandanten" (gelingt) ein Existenzorakel ueber Mandantengrenzen (T-GWH-05). |
|
||
| 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 | module | keine-mandantengebundene-tabelle | gebunden | NEUE Fundstelle (260911-mkj, WINDOWS #27): nur ueber `include: { module: true }` auf `tenantPrisma.tenantModuleActivation.findMany` erreicht (`getMatrix`, Zeilen 196 und 253). `Module` traegt plattformweit keinen Zeilenschutz (Migration 20260909140000, Gruppe b) — die Bindung des Elternaufrufs ist fuer die Unterabfrage wirkungslos, nicht schaedlich, dieselbe Einordnung wie `module-registry.service.ts`/`module` unten. |
|
||
| 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 | system-gebunden | `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` laufen, bevor bzw. unabhaengig davon, ob ein einzelner Mandantenkontext feststeht (Befund B, 260909-ipc-PLAN.md) — seit 260914-eym lesen beide ueber `forSystem()` (`system_read_policy`), die Schreibzeile je Altzeile der Nachverschluesselung laeuft ueber `forTenant(this.prisma, config.tenantId)`; kein ungebundener Zugriff mehr. Korrektur der Klasse von `muss-mandantengebunden`: das Paar ist tatsaechlich `beides`, keine Verhaltensaenderung. |
|
||
| apps/api/src/ldap/ldap-config.service.ts | ldapFieldMapping | beides | system-gebunden | Klassenkorrektur (260911-mkj, WINDOWS #27): wechselt von `muss-mandantengebunden` auf `beides`. Kein eigenes `tenantId`, RLS ueber Join auf `LdapConfig`. `addFieldMapping` und `removeFieldMapping` nehmen den Mandanten 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. Die vierte Erkennung (260911-mkj) macht sichtbar, dass der bewusst uebergreifende Planer-Lesepfad `getAllActiveConfigs()` ueber `include: { fieldMappings: true }` in `LdapFieldMapping` hineinreicht — dieselbe Unterabfrage-Form wie die drei `tenant`-Zaehler aus WINDOWS #27, hier aber KEIN neuer Befund: der Elternpfad ist als erster Fall der Hintergrunddienst-Falle bereits an Etappe 3 uebergeben, die Feldzuordnungen teilen nach dem Scharfschalten sein Schicksal (Regel auf `LdapFieldMapping` ueber Join auf `LdapConfig`, Migration 20260618112133). Die Klasse folgt der Elternzeile `ldapConfig` (`beides`). Seit 260914-eym liest der Elternpfad ueber `forSystem()`, das Relationsziel ist damit `system-gebunden` und traegt eine eigene `system_read_policy` (Migration 20260914120000). |
|
||
| apps/api/src/ldap/ldap-config.service.ts | tenant | keine-mandantengebundene-tabelle | system-gebunden | NEUE Fundstelle (260911-mkj, WINDOWS #27): `getAllActiveConfigs()` `include: { tenant: true, fieldMappings: true }` auf `ldapConfig.findMany`; `Tenant` traegt keinen Zeilenschutz (260911-e2s Aufgabe 1, `tenant-tabelle-ohne-zeilenschutz-bleibt-lesbar`). Seit 260914-eym laeuft der Ankeraufruf ueber `forSystem()` — der Stand folgt dem Empfaenger (`system-gebunden`), eine Regel braucht `Tenant` weiterhin nicht. |
|
||
| 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 | group | muss-mandantengebunden | gebunden | NEUE Fundstelle (260911-mkj, WINDOWS #27): Relationsfilter `where: { tenantId, group: { memberships: { some: { userId } } } }` auf `tenantPrisma.moduleGrant.findMany` (Zeile 73, `getAccessibleModuleIds`, Gruppenweg). Prisma rendert das als Unterabfrage auf `Group` unter DEREN Regel; der Klient ist gebunden, `Group` gehoert demselben Mandanten. |
|
||
| apps/api/src/module-registry/module-access.service.ts | groupMembership | muss-mandantengebunden | gebunden | NEUE Fundstelle (260911-mkj, WINDOWS #27): derselbe Relationsfilter wie bei `group` oben, zwei Ebenen tief (`group > memberships`) auf `tenantPrisma.moduleGrant.findMany` (Zeile 73). Prisma rendert das als Unterabfrage auf `GroupMembership` unter DEREN Regel; gebunden, derselbe Mandant. |
|
||
| 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 | gemischt | Stand-Aenderung (260911-mkj, WINDOWS #27): `ungebunden` -> `gemischt`, Klasse bleibt. Modulkatalog ist plattformweit. Der ungebundene Anteil bleibt bewusst so (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. Die neu sichtbare gebundene Haelfte stammt ausschliesslich aus `include: { module: true }` auf `tenantPrisma.tenantModuleActivation` (Zeilen 56, 97, 148) — fuer die schutzlose Katalogtabelle wirkungslos, nicht schaedlich. |
|
||
| 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 | gebunden | SMTP-Zugangsdaten je Mandant, `tenantId`-Spalte vorhanden. Seit 260911-gwh (Aufgabe 2) laufen `getSmtpConfig`, `saveSmtpConfig`, `getDecryptedSmtpConfig` ueber `forTenant()`. Der bis 260914-eym einzige ungebundene Zugriff — der Startpfad des Mailmoduls (`findFirst()` beim Boot, WINDOWS #30, sechster Fall der Hintergrunddienst-Falle) — ist GELOESCHT: `MailService` baut je Versand einen Transport aus `getDecryptedSmtpConfig(tenantId)` des Empfaenger-Mandanten. Befund K (`tender-mail.service.ts`/`dkv-mail.service.ts`/`mail.service.ts` haengen an `getDecryptedSmtpConfig`) ist damit erfuellt. |
|
||
| 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). Gemessen 260911-e2s (Aufgabe 1, Prüfung 1/2): keine Regel auf `Tenant` in irgendeiner der 34 ausgelieferten Migrationen einschließlich `20260910120000_rls_widen_membership_grant_and_platform_read`; gebunden und ungebunden liefern über Roh-SQL UND generierten Client dieselben Zeilen. |
|
||
| apps/api/src/tenant/tenant.controller.ts | user | muss-mandantengebunden | gebunden | Seit 260911-e2s (Aufgabe 3): `findAll`/`findOne`/`remove` zählen Benutzer je Mandant über drei gebundene Aufrufstellen (`tenantPrisma.user.count`, Fan-out-Muster aus `UserService.findAllForPlatformAdmin`) statt über den früheren Relationszähler (`include: { _count: { select: { users } } }`), der nach dem Scharfschalten unter der Regel von `User` unbemerkt null geliefert hätte (260911-e2s Aufgabe 1, Prüfungen 5-7). `where: { tenantId }` bleibt heute (Rolle mit BYPASSRLS, WINDOWS #18) der einzige wirksame Filter. |
|
||
| apps/api/src/tenant/tenant.service.ts | tenant | keine-mandantengebundene-tabelle | ungebunden | Dieselbe Begründung. Gemessen 260911-e2s (Aufgabe 1, Prüfung 1/2): keine Regel auf `Tenant` in irgendeiner der 34 ausgelieferten Migrationen einschließlich `20260910120000_rls_widen_membership_grant_and_platform_read`. |
|
||
| 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 | tender | keine-mandantengebundene-tabelle | gebunden | NEUE Fundstelle (260911-mkj, WINDOWS #27): `include: { tender: true, savedSearch: true }` auf `tenantPrisma.tenderMatch.findMany` (Zeile 149) innerhalb der Mandantenschleife des Planers. `Tender` ist der plattformglobale Katalog (D-03) — die Bindung des Elternaufrufs ist fuer die Unterabfrage wirkungslos, nicht schaedlich. |
|
||
| apps/api/src/tenders/tender-digest.scheduler.ts | tenderMatch | beides | system-gebunden | Ein einziger globaler Cron-Job liest über ALLE Mandanten (bewusst übergreifend, Pitfall-1-Kommentar im Dateikopf) — seit 260909-laa (Aufgabe 3) laufen die Je-Treffer-Abfrage (`findMany` nach Mandant) und die Stempelung (`updateMany`) je Kandidatenzeile über `forTenant()`; seit 260914-eym liest die Kandidatenabfrage (`findMany` mit `distinct`) über `forSystem()` (`system_read_policy`, nur lesend) statt ungebunden. |
|
||
| 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 | tenderSavedSearch | muss-mandantengebunden | gebunden | NEUE Fundstelle (260911-mkj, WINDOWS #27): `include: { savedSearch: true }` plus `orderBy` ueber `savedSearch` auf `tenantPrisma.tenderMatch.findMany` (Zeile 149) innerhalb der Mandantenschleife des Planers. `TenderSavedSearch` ist mandantengebunden und ueber denselben gebundenen Klienten erreicht. |
|
||
| 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 | gemischt | Stand-Aenderung (260911-mkj, WINDOWS #27): `ungebunden` -> `gemischt`, Klasse bleibt. Liest den plattformweiten Katalog (D-03), um Treffer zu berechnen — kein `tenantId`. Die neu sichtbare gebundene Haelfte stammt aus `include: { tender: true }` auf `tenantPrisma.tenderMatch.findMany` (Zeile 139) im Sofortmeldungs-Dispatch — fuer die schutzlose Katalogtabelle wirkungslos, nicht schaedlich. |
|
||
| 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 | system-gebunden | Gespeicherte Suchprofile ALLER Mandanten werden gegen neue Treffer geprüft — bewusst übergreifend; seit 260914-eym über `forSystem()` (`system_read_policy`, nur lesend), die Etappe-3-Übergabe aus 260909-laa ist damit eingelöst. Treffer-Anlage und Sofortmeldung bleiben je Profil gebunden. |
|
||
| 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 | tenderSource | keine-mandantengebundene-tabelle | ungebunden | NEUE Fundstelle (260911-mkj, WINDOWS #27): `getTender` (Zeile 612) `include: { sources: { select: ... } }` auf `this.prisma.tender.findUnique`; `TenderSource` plattformweit ohne Zeilenschutz (dieselbe Einordnung wie `tender-dedup.service.ts`/`tenderSource`). |
|
||
| 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). 3c-Befund (260914-eym): einziger Lesezugriff außerhalb der Schleife, `Tenant` ohne Regel — kein Systemkontext nötig, Datei unverändert, Stand bleibt `ungebunden`. |
|
||
| 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.~~
|
||
**Aufgelöst (260911-e2s):** die Frage ist für ALLE Bereiche entschieden,
|
||
nicht nur für `tenant` — dienst-intern, ein Klient je Methode, ist die
|
||
Konvention. Die Anfrageobjekt-Eigenschaft existiert nicht mehr:
|
||
`tenant.guard.ts` setzt nur noch `req.tenantId`, `tenant.middleware.ts`
|
||
(der nie verdrahtete Zwilling mit identischer Logik) ist gelöscht. Siehe
|
||
Abschnitt "Zwei belegte Befunde" oben und
|
||
`docs/mandantentrennung-etappe2-fehlerrichtung.md`, "## Bereich tenant",
|
||
(n4)(a).
|
||
- ~~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`.
|
||
- **Wie der Anmeldeweg unter je Mandant eindeutigen Anmeldenamen (Etappe-3-
|
||
Entscheidung (1)) den Mandanten VOR der Benutzersuche erfaehrt (260911-fh9).**
|
||
`auth_lookup_user_by_username(p_username)` stuetzt sich heute auf
|
||
`username @unique` plattformweit und braucht kuenftig
|
||
`(p_tenant_id, p_username)` — die drei SECURITY-DEFINER-Funktionen aus
|
||
Etappe 1 werden dabei ENGER (zwei Gleichheitsbedingungen statt einer),
|
||
nicht weiter; `local.strategy.ts` braucht dann eine Mandantenangabe VOR
|
||
der Suche. Die Bindung der drei Nach-Anmeldungs-Methoden dieses Bereichs
|
||
(`getMe`, `changePassword`, `adminResetPassword`) ist davon NEUTRAL — sie
|
||
haengt am Claim `tenantId` und an `User.id` (plattformweite UUID), nicht
|
||
an `username`/`email`. Siehe
|
||
`docs/mandantentrennung-etappe2-fehlerrichtung.md`, "## Bereich auth",
|
||
(h4)(a).
|
||
- **Wie die Benutzerdimension in die Regeln der zehn persönlichen Tabellen
|
||
kommt (Etappe-3-Entscheidung (2)).** **Aufgelöst (260911-nke):** Migration
|
||
`20260911120000_rls_user_dimension_personal_tables` (Etappe 3b) bringt
|
||
`app.current_user`/`current_user_id()` und die `IS NULL OR`-Form in die
|
||
Regeln aller zehn persönlichen Tabellen; `forTenant(prisma, tenantId,
|
||
userId?)` bekommt den optionalen dritten Parameter, 34 Nutzer-CRUD-
|
||
Aufrufstellen reichen ihn durch. Siehe
|
||
`docs/mandantentrennung-etappe2-fehlerrichtung.md`, Abschnitt
|
||
"Regelschluss Benutzerdimension (Etappe 3b, 260911-nke)". Was weiterhin
|
||
offen ist: ein Aufrufer, der `userId` vergisst, sieht den ganzen
|
||
Mandanten (kein Wächter gebaut, siehe `.planning/WINDOWS.md`);
|
||
Anmeldenamen pro Mandant (Etappe 3a) bleibt offen. **Systemkontext
|
||
(Etappe 3c) — erledigt (260914-eym):** Migration
|
||
`20260914120000_rls_system_context_read` (`is_system_context()`,
|
||
`system_read_policy … FOR SELECT` auf fünf Tabellen; seit 260922-hk4
|
||
kommt "DashboardImage" als sechste dazu, angelegt in der Migration
|
||
20260922120000 für den Bootstrap-Umzug der Bilderrahmen-Bilder),
|
||
Schwesterhelfer
|
||
`forSystem()`, fünfte Erkennungsform des Detektors mit Erlaubnisliste;
|
||
siehe `docs/mandantentrennung-etappe2-fehlerrichtung.md`, Abschnitt
|
||
"## Systemkontext (Etappe 3c, 260914-eym)" und den Regelschluss je Fall
|
||
im Hintergrunddienst-Abschnitt oben.
|
||
- **Wie das Mailmodul künftig je Mandant versendet (260911-gwh).** ~~Der
|
||
Startpfad bleibt bewusst ungebunden (sechster Fall der
|
||
Hintergrunddienst-Falle, WINDOWS #30, siehe oben) — ein Umbau auf
|
||
Transport je Versand aus `getDecryptedSmtpConfig(tenantId)`, die Form,
|
||
die `DkvMailService`/`TenderMailService` bereits haben, ist eine
|
||
Funktionsänderung (Umbau des Mailmoduls), kein Bindungsumbau, und deshalb
|
||
NICHT Gegenstand dieser Etappe.~~ **Aufgelöst (260914-eym):** genau dieser
|
||
Umbau ist gebaut — Startpfad und Mailer-Fabrik entfernt, `MailService`
|
||
baut je Versand einen Transport nach Mandant des Empfängers, WINDOWS #30
|
||
geschlossen. Siehe
|
||
`docs/mandantentrennung-etappe2-fehlerrichtung.md`, "## Bereich settings",
|
||
(s4)(a) mit Nachtrag.
|