- Neuer Abschnitt "Regelschluss Benutzerdimension (Etappe 3b, 260911-nke)"
in der Kritikschrift mit b1 (woertliche Werkzeugausgabe + pg_policies-Liste),
b2 (Signaltabelle beide Fehlerrichtungen), b3 (NotFound-statt-Forbidden
je Methode), b4/b5 (bewusst nicht geloest/angefasst). Zehn datierte
Nachtraege an allen Stellen, die zuvor "keine Benutzerdimension" als
Stand beschrieben (t1/t4/r4/w1/w4/k1/k4/f1/f4/Abschluss) — historische
Messung bleibt lesbar.
- Klassifikation: drei Bestandsaufnahme-Zeilen (calendarSource,
widgetInstance, favoriteLink) mit Zusatz "Benutzerdimension seit
20260911120000 (260911-nke)"; neuer Punkt "Aufgelöst (260911-nke)" im
Abschnitt "Was diese Etappe NICHT entscheidet"; neuer Stand-Absatz —
Paarzahl (72) und Klassen-Verteilung bleiben unveraendert.
- Betriebsanleitung: `forTenant(prisma, tenantId, userId?)` und die zehn/
vier-Tabellen-Aufteilung nachgezogen.
- Datenbankrolle: neuer Absatz zu `app.current_user`/`current_user_id()`
neben `app.current_tenant`; SECURITY-DEFINER-Kopfkommentare unangetastet.
- Auftrag: 3b als erledigt markiert (Migrationsname, sechs statt drei
Umkehrungen, Endzahlen); 3a/3c unveraendert.
- WINDOWS.md: neuer Eintrag #34 (open, deviation) fuer die bewusst offene
Flanke — Aufrufer ohne userId sieht den ganzen Mandanten, kein Waechter
gebaut.
- Baseline: 1020/62 Tests, Typpruefung sauber, Werkzeug 203/203 bestanden.
Erlaubnisliste gegen 8829999 eingehalten, schema.prisma/Compose/.env/3a
unveraendert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
81 KiB
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 mittenantId(oder eine Tabelle, die ihre Mandantenregel über einen Join auf eine solche Tabelle bezieht). Muss in Etappe 2 aufforTenant()umgestellt werden.bewusst-uebergreifend— muss über Mandanten hinweg sehen, mit ausgeschriebenem Grund. Braucht in Etappe 3 eine sichtbare Kennzeichnung (Systemkontext), aber keinenforTenant()-Umbau.keine-mandantengebundene-tabelle— betrifft eines der acht Modelle ohnetenantIdbzw. 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).
| Bereich | Ungebunden | Gebunden | Hinweis |
|---|---|---|---|
| tenders | 35 | 27 | war 62/0, dann 36/26 nach 260909-laa — 260910-jab (Aufgabe 2) hat tender-rss-feed.service.ts/listForUser zusätzlich auf forTenant() umgestellt (WINDOWS #19 geschlossen, Befund F: ungebunden hätte die Reparatur den Pfad sonst still auf nur die plattformweiten Zeilen reduziert): ein Rohtreffer wandert von ungebunden nach gebunden (36→35, 26→27). Die 35 verbleibenden ungebundenen Treffer sind die zwölf bewusst nicht angefassten Paare (D-03-Katalog, zwei Fan-out-Adapter) plus die zwei bewusst ungebundenen RSS-Pfade (createPlatform/remove, WINDOWS #24) plus die übergreifenden Hälften der beiden Hintergrunddienste (Etappe-3-Übergabe) |
| groups | 0 | 31 | war 37/0 — Aufgabe 2/3 (260909-jts) haben groups.service.ts (12 Methoden) und module-grants.service.ts (5 Methoden) vollständig auf forTenant()/withTenantTransaction() umgestellt. Die neun zusätzlichen, über tx gebundenen Zugriffe innerhalb der drei Transaktionen zählt dieses einfache Muster nicht mit (siehe Methodenhinweis oben) |
| ldap | 4 | 26 | war 21/0 — Aufgabe 2/3 (260909-ipc) haben ldap-config.service.ts (5 Methoden) und ldap.service.ts (6 Methoden, 11 Abfragen) auf forTenant() umgestellt. Die 4 verbleibenden ungebundenen Treffer sind bewusst: getAllActiveConfigs/onApplicationBootstrap (Befund B) und resolveEmailForWrite (Befund A, T-IPC-04) |
| dkv | 1 | 22 | war 21/0 — Aufgabe 2/3 (260909-mir) haben dkv.service.ts vollständig auf forTenant() umgestellt: Konfigurationspfade (loadConfig, getConfigForApi, saveConfig, testConnection), Historie, Fahrzeugstammdaten und der neue Besitzriegel vor dem Ausfuhrdatei-Download. Gebunden sind es 22 statt 21, weil der Riegel einen zusätzlichen Lesezugriff auf dkvInvoiceHistory einführt (T-MIR-03). Der eine verbleibende ungebundene Treffer ist der benannte Planer-Startpfad loadAnyActiveConfigForScheduler() (Befund D, WINDOWS #21) — bewusst, mit dreifacher Markierung |
| user | 8 | 14 | war 17/0 — Aufgabe 2/3 (260910-das) haben user.service.ts (findById/create/update/deactivate/delete sowie die zwei neuen Plattform-Administratorsicht-Methoden), admin-seed.service.ts (Erstanlage des Administrators) und user.controller.ts (Benutzerliste des ADMIN-Zweigs, alle drei Kennungswege ueber die Dienstmethoden, alle fuenf Selbstbedienungszugriffe) auf forTenant() umgestellt. Die 8 verbleibenden ungebundenen Rohtreffer sind bewusst: findByUsername in user.service.ts (plattformweit eindeutiger Schluessel, derselbe Fall wie resolveEmailForWrite im Bereich ldap), die Erstanlage-Pruefung und beide Zugriffe auf tenant in admin-seed.service.ts, sowie der neue Schleifentreiber this.prisma.tenant.findMany der beiden Plattform-Administratorsicht-Methoden in user.service.ts (Tenant traegt keinen Zeilenschutz) |
| module-registry | 7 | 10 | war 17/0 — Aufgabe 2/3 (260910-exd) haben module-access.service.ts (getAccessibleModuleIds: Kurzschlusszweig, Direktweg, Gruppenweg, Schnittmenge; getCatalogFlags: eigener Aktivierungs-Lesezugriff) und module-registry.service.ts (findActiveForTenant, activateForTenant, deactivateForTenant, isModuleActive) auf forTenant() umgestellt. Die 7 verbleibenden ungebundenen Rohtreffer sind bewusst: der eine Katalogzugriff in module-access.service.ts (findAccessibleModules) und die sechs Katalogzugriffe in module-registry.service.ts (findAll, findBySlug, die beiden Katalog-Existenzpruefungen in activateForTenant/deactivateForTenant, die Katalogsuche in isModuleActive, seedModule) — der Modulkatalog (Module) traegt heute keinen Zeilenschutz, eine Bindung waere heute wirkungslos, nicht katastrophal; katastrophal wuerde sie erst, WENN Etappe 3 dieser Tabelle eine Regel gibt (Befund E) |
| dashboard | 1 | 12 | war 13/0 — Aufgabe 2/3 (260910-krx) haben dashboard.service.ts vollständig umgestellt: getLayout/saveLayout (gemeinsam gebunden), getWidgets/addWidget/updateWidgetConfig/removeWidget sowie getSearchProviders/addSearchProvider/removeSearchProvider laufen über forTenant(), je Methode ein Klient. Der eine verbleibende ungebundene Rohtreffer ist bewusst: der Modulkatalog (Module) trägt heute keinen Zeilenschutz, eine Bindung wäre heute wirkungslos, nicht katastrophal — katastrophal würde sie erst, WENN Etappe 3 dieser Tabelle eine Regel gibt (Befund E aus module-registry, hier übernommen) |
| auth | 3 | 10 | 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 | 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 | 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 | 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 |
| settings | 1 | 3 | war 4/0 — 260911-gwh (Aufgabe 2) hat getSmtpConfig, saveSmtpConfig, getDecryptedSmtpConfig auf forTenant() umgestellt (3 gebundene smtpConfig-Rohtreffer). Der eine verbleibende ungebundene Rohtreffer ist der umbenannte Planer-Startpfad loadAnySmtpConfigForStartupTransport() (Befund D, WINDOWS #30) — bewusst, mit dreifacher Markierung; Befund K (tenders/dkv hängen an getDecryptedSmtpConfig) ist damit erfüllt |
| Summe | 68 | 178 | 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, 72 Paare)
Stand 260909-jts (Aufgabe 3): 61 Paare aus dem vorherigen Durchlauf
(260909-ipc) plus ein bisher vollstaendig unsichtbares Paar
(groups.service.ts/tenantModuleActivation), das erst die um
Transaktionsparameter erweiterte Erkennung (Befund B, Aufgabe 2 dieses
Plans) sichtbar macht — der einzige Zugriff auf dieses Modell in dieser
Datei lief bis dahin ausschliesslich ueber den Rueckgabeparameter der
interaktiven Transaktion in ensureDefaultGroup und war weder ueber
this.prisma.<Modell> noch ueber <gebundener Client>.<Modell> erfassbar.
Die Zahl ist der Ausgabe der Pruefung in
apps/api/src/prisma/rls-access-inventory.spec.ts entnommen, nicht
geschaetzt.
Stand 260909-laa (Aufgabe 2): dieselben 62 Paare, keine neue Fundstelle
hinzugekommen oder verschwunden — nur EINE Klasse hat sich verschoben:
tender-rss-feed.service.ts/tenderRssFeedSource wechselt von
muss-mandantengebunden auf beides (WINDOWS #19 — createForUser bindet,
listForUser/createPlatform/remove bleiben bewusst uebergreifend), der
exakte Praezedenzfall aus ldapConfig in 260909-ipc.
Stand 260910-das (Aufgabe 3): 63 Paare — 62 aus dem vorherigen
Durchlauf plus EIN neues Paar (user.service.ts/tenant, Klasse
keine-mandantengebundene-tabelle, Stand ungebunden): der Schleifentreiber
der neu eingefuehrten Plattform-Administratorsicht (Befund F/N, Aufgabe 1/2).
Zusaetzlich ZWEI Klassenkorrekturen, jede mit eigener Begruendung an der
Fundstelle in der Bestandsaufnahme oben: user.service.ts/user wechselt
von muss-mandantengebunden auf beides — wortgleich derselbe
Praezedenzfall wie ldap.service.ts/user in 260909-ipc
(resolveEmailForWrite, hier findByUsername); admin-seed.service.ts/user
wechselt von bewusst-uebergreifend auf beides, weil die bisherige
Begruendung nachweislich falsch war (Befund J: der Mandant ist bei der
Erstanlage des Administrators bereits bekannt, nicht strukturell fehlend).
Alle drei Zahlen sind der Ausgabe von rls-access-inventory.spec.ts
entnommen, nicht geschaetzt.
Stand 260910-krx (Aufgabe 3): unveraendert, ausdruecklich festgehalten
statt uebersprungen. Weiterhin 63 Paare, keine Klasse verschoben sich.
Die vier Paare des Bereichs dashboard
(dashboard.service.ts/dashboardLayout, /module, /searchProvider,
/widgetInstance) waren bereits vor diesem Durchlauf korrekt klassifiziert
(drei muss-mandantengebunden, eines keine-mandantengebundene-tabelle) —
dieser Plan aendert nur ihre Stand-Spalte (ungebunden auf gebunden
fuer drei der vier Paare), keine ihrer Klassen. Eine unveraenderte Tabelle
ohne diesen Vermerk waere von einer vergessenen Nachziehung nicht zu
unterscheiden — deshalb steht die Abwesenheit einer Aenderung hier
ausdruecklich, statt stillschweigend uebersprungen zu werden.
Stand 260911-cwh (Aufgabe 3): unveraendert, ausdruecklich festgehalten
statt uebersprungen. Weiterhin 63 Paare, keine Klasse verschoben sich. Das
eine Paar des Bereichs calendar (calendar.service.ts/calendarSource)
war bereits vor diesem Durchlauf korrekt klassifiziert (muss-mandantengebunden)
— Aufgabe 2 (260911-cwh) aendert nur seine Stand-Spalte (ungebunden auf
gebunden), nicht seine Klasse. Eine unveraenderte Tabelle ohne diesen
Vermerk waere von einer vergessenen Nachziehung nicht zu unterscheiden —
deshalb steht die Abwesenheit einer Aenderung hier ausdruecklich, statt
stillschweigend uebersprungen zu werden.
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".
| Klasse | Anzahl Paare |
|---|---|
| muss-mandantengebunden | 35 |
| keine-mandantengebundene-tabelle | 21 |
| beides | 14 |
| bewusst-uebergreifend | 2 |
| Summe | 72 |
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 aufgroup,ldapConfigunduserinnerhalb der Sync-Methoden teils ungebunden, obwohl der Mandant zu diesem Zeitpunkt bereits bekannt war — nur 4 der inzwischen 11forTenant()-Aufrufstellen deckten die Lese-/Schreibpfade ab. Jetzt sindgroupundldapConfigvollständig gebunden; beiuserbleibt ausschließlichresolveEmailForWritebewusst 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)undensureDefaultGroup(tenantId)ingroups.service.ts) — ist mit 260909-jts (Aufgabe 2) GESCHLOSSEN: beide Methoden laufen seither überforTenant()/withTenantTransaction()(siehe Bestandsaufnahme unten,groups.service.ts/group, Standgebunden). 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 auchdocs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt (e), Befund D.Nachtrag (260911-mkj): WINDOWS #27 geschlossen — derselbe Bereich
ldaptraegt einen zweiten, bislang unsichtbaren Planer-Lesezugriff:LdapConfigService.getAllActiveConfigs()(ldap-config.service.ts) reicht ueberinclude: { tenant: true, fieldMappings: true }sowohl inTenant(schutzlos, harmlos) als auch inLdapFieldMapping(geschuetzt ueber Join aufLdapConfig) hinein. Nach dem Scharfschalten verstummt die Feldzuordnung mit der Elternzeile, nicht getrennt von ihr — der Etappe-3-Systemkontext muss BEIDE Tabellen sehen. Die Bestandsaufnahme fuehrtldapFieldMappingdeshalb seither alsbeides/gemischtstattmuss-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. LiesttenderMatch(Kandidatenabfrage,findManymitdistinct: ['userId']) bewusst über ALLE Mandanten in einem einzigenfindMany(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 denormalisiertetenantIdder Treffer-Zeile mit aus, damit die Schleife binden kann; der Sonderfall eines Nutzers mit Treffern unter zwei verschiedenen Mandanten (Mandantenwechsel) ist NICHT gelöst, siehedocs/mandantentrennung-etappe2-fehlerrichtung.md. Innerhalb der Schleife laufentenderNotificationPref.findUnique,tenderMatch.findMany/updateManyunduser.findUniqueje Kandidatenzeile überforTenant(), 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 plattformweitentender-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/updateManysowieuser.findUniqueüberforTenant(), 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. Liesttenant.findMany()bewusst über ALLE Mandanten (Treiber, ungebunden —Tenantträgt keinen Zeilenschutz, Aufgabe 1 gemessen,tenant-tabelle-ohne-zeilenschutz-bleibt-lesbar) und ruft je MandantgroupsService.ensureDefaultGroup(tenant.id)auf — dieser Rumpf ist seit 260909-jts vollständig überforTenant()/withTenantTransaction()gebunden (siehe Bestandsaufnahme,groups.service.ts/group, Standgebunden). 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 Bereichuserüberhaupt an der Reihe war. Einschränkung, bewusst nicht verschwiegen: der äußeretry/catchinensureDefaultGroupsForAllTenants()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.tss 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".
Bestandsaufnahme
Maschinell ermittelt, rls-access-inventory.spec.ts hält Vollständigkeit
nach. Spalten: Datei, Modell (Prisma-Modellname wie in this.prisma.<Modell>
oder — seit 260909-ipc, Befund G — in <gebundener Client>.<Modell>
verwendet), Klasse, Stand (gebunden/ungebunden/gemischt, seit
260909-ipc maschinell gegen den Quelltext geprüft), Begründung.
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
vier Erkennungsformen (this.prisma, eine const X = forTenant(-Zuweisung,
ein Transaktionsparameter, withTenantTransaction() — 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/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.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 | gemischt | Postfach-/Zugangsdaten des DKV-Moduls je Mandant. Seit 260909-mir (Aufgabe 2) laufen loadConfig, getConfigForApi, saveConfig, testConnection und der Konfigurations-Lesezugriff der Verarbeitungsstrecke ueber forTenant(). Die Mischung stammt ausschliesslich vom einen benannten, bewusst ungebundenen Planer-Startpfad loadAnyActiveConfigForScheduler() (WINDOWS #21) — keine uebersehene Fundstelle. |
| apps/api/src/dkv/dkv.service.ts | dkvVehicleMaster | muss-mandantengebunden | gebunden | Fahrzeugstammdaten des DKV-Moduls je Mandant. Seit 260909-mir (Aufgabe 3) laufen Fahrzeugliste, Anlegen, beide Paare aus Besitzpruefung und Schreibzugriff (Aendern/Loeschen), beide Zweige des CSV-Imports und der gebuendelte Lesezugriff beim Aufbau der Ausfuhrzeilen vollstaendig ueber forTenant(); die vorgeschalteten Besitzpruefungen bei Aendern/Loeschen bleiben zusaetzlich bestehen (Befund G — ein gebundenes UPDATE ueber die Kennung allein trifft eine fremde Zeile still, nicht laut). |
| apps/api/src/favorites/favorites.service.ts | favoriteLink | muss-mandantengebunden | 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). |
| 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 | gemischt | getConfig, createConfig und updateConfig laufen ueber forTenant(), gebunden an den aus der Anfrage bereits bekannten Mandanten. getAllActiveConfigs() (Planer-Lesezugriff ueber ALLE Mandanten) und die Start-Nachverschluesselung in onApplicationBootstrap bleiben bewusst uebergreifend: beide laufen, bevor bzw. unabhaengig davon, ob ein einzelner Mandantenkontext feststeht (Befund B, 260909-ipc-PLAN.md). Korrektur der Klasse von muss-mandantengebunden: das Paar ist tatsaechlich beides, keine Verhaltensaenderung. |
| apps/api/src/ldap/ldap-config.service.ts | ldapFieldMapping | beides | gemischt | 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). |
| apps/api/src/ldap/ldap-config.service.ts | tenant | keine-mandantengebundene-tabelle | ungebunden | NEUE Fundstelle (260911-mkj, WINDOWS #27): getAllActiveConfigs() (Zeile 311) include: { tenant: true, fieldMappings: true } auf this.prisma.ldapConfig.findMany; Tenant traegt keinen Zeilenschutz (260911-e2s Aufgabe 1, tenant-tabelle-ohne-zeilenschutz-bleibt-lesbar). |
| 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 | gemischt | SMTP-Zugangsdaten je Mandant, tenantId-Spalte vorhanden. Seit 260911-gwh (Aufgabe 2) laufen getSmtpConfig, saveSmtpConfig, getDecryptedSmtpConfig ueber forTenant(). Die Mischung stammt ausschliesslich vom einen benannten, bewusst ungebundenen Planer-Startpfad loadAnySmtpConfigForStartupTransport() (WINDOWS #30, sechster Fall der Hintergrunddienst-Falle) — keine uebersehene Fundstelle, dieselbe Form wie dkv.service.ts/dkvModuleConfig. Befund K (tender-mail.service.ts/dkv-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 | gemischt | Ein einziger globaler Cron-Job liest über ALLE Mandanten (bewusst übergreifend, Pitfall-1-Kommentar im Dateikopf) — seit 260909-laa (Aufgabe 3) bleibt die Kandidatenabfrage (findMany mit distinct) bewusst ungebunden, die Je-Treffer-Abfrage (findMany nach Mandant) und die Stempelung (updateMany) je Kandidatenzeile laufen über forTenant(). |
| apps/api/src/tenders/tender-digest.scheduler.ts | tenderNotificationPref | beides | gebunden | Präferenzen werden je Kandidatenzeile gelesen. Seit 260909-laa (Aufgabe 3) läuft der einzige Zugriff über forTenant(), gebunden an den Mandanten der jeweiligen Zeile. |
| apps/api/src/tenders/tender-digest.scheduler.ts | 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 | ungebunden | Gespeicherte Suchprofile ALLER Mandanten werden gegen neue Treffer geprüft — bewusst übergreifend, Etappe-3-Übergabe (260909-laa, Aufgabe 3, unverändert). |
| apps/api/src/tenders/tender-matching.service.ts | user | beides | gebunden | E-Mail-Adressen für die Sofortmeldung. Seit 260909-laa (Aufgabe 3) läuft der einzige Zugriff über forTenant(), gebunden an den Mandanten des jeweiligen Profils. |
| apps/api/src/tenders/tender-notification-pref.service.ts | tenderNotificationPref | muss-mandantengebunden | gebunden | Nutzer-CRUD für die eigenen Benachrichtigungseinstellungen — Mandant aus der Anfrage bekannt. Seit 260909-laa (Aufgabe 2) laufen getForUser/setForUser vollständig über forTenant(); setForUser übersetzt eine P2002-Verletzung (Befund F) in eine verständliche deutsche Meldung. |
| apps/api/src/tenders/tender-rss-feed.service.ts | tenderRssFeedSource | beides | gemischt | Nutzer-CRUD für die eigenen RSS-Quellen (anders als der Fan-out in adapters/rss.adapter.ts). Seit 260909-laa (Aufgabe 2) bindet createForUser (Zähler + Anlage, beide ausschließlich auf persönlichen Zeilen mit gesetztem Mandanten) über forTenant(). Seit 260910-jab (Aufgabe 2, WINDOWS #19 geschlossen) bindet zusätzlich listForUser — die neue Leseregel (tenant_platform_read_policy, 20260910120000_rls_widen_membership_grant_and_platform_read) schließt die plattformweite Zeile ausdrücklich ein, ungebunden hätte die Reparatur den Pfad sonst still auf nur die plattformweiten Zeilen reduziert (Befund F). createPlatform/remove bleiben bewusst ungebunden — beide Pfade lassen sich unter der Anwendungsrolle grundsätzlich nicht anlegen/entfernen, weil jede Schreibregel einen Mandanten verlangt (WINDOWS #24, eigener offener Punkt). Klasse beides bleibt korrekt: zwei gebundene, zwei bewusst ungebundene Zugriffe in derselben Datei. |
| apps/api/src/tenders/tender-saved-search.service.ts | tenderSavedSearch | muss-mandantengebunden | gebunden | Nutzer-CRUD für die eigenen gespeicherten Suchprofile — Mandant aus der Anfrage bekannt. Seit 260909-laa (Aufgabe 2) laufen list/create/update/remove vollständig über forTenant(). |
| apps/api/src/tenders/tender-scheduler.service.ts | tenderSourcePollConfig | keine-mandantengebundene-tabelle | ungebunden | Plattformweiter DÖE-Poll-Status, kein tenantId — im Dateikopf explizit als "genuine platform-wide singleton" begründet. |
| apps/api/src/tenders/tender-triage.service.ts | tenderTriage | muss-mandantengebunden | gebunden | Favorisierungs-/Ablehnungsstatus eines Nutzers, tenantId-Spalte vorhanden. Seit 260909-laa (Aufgabe 2) laufen setTriage/listForUser/favoriteIds vollständig über forTenant(). |
| apps/api/src/tenders/tenders.controller.ts | tender | keine-mandantengebundene-tabelle | ungebunden | Lesezugriff auf den plattformweiten Katalog (D-03). |
| apps/api/src/tenders/tenders.controller.ts | 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). |
| 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 überAufgelöst (260911-e2s): die Frage ist für ALLE Bereiche entschieden, nicht nur fürreq.tenantPrismastatt eines erneutenforTenant()-Aufrufs im Service gehen (offener Befund oben). Der Bereichldap(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 inldap.service.tsund die drei inauth.service.tsbereits vormachten. Der Bereichgroups(260909-jts) hat sich für denselben dienst-internen Weg entschieden — jede Methode ingroups.service.tsundmodule-grants.service.tserzeugt ihren eigenenforTenant()- bzw.withTenantTransaction()-Aufruf, gebundene Clients werden nicht zwischen Methoden weitergereicht. Der Bereichdashboard(260910-krx) hat sich für denselben dienst-internen Weg entschieden — jede der neun umgestellten Methoden indashboard.service.tserzeugt ihren eigenenforTenant()-Aufruf, wie alle sieben Bereiche vor ihm. Der Bereichcalendar(260911-cwh) hat sich für denselben dienst-internen Weg entschieden — jede der sechs umgestellten Methoden incalendar.service.tserzeugt ihren eigenenforTenant()-Aufruf, wie alle acht Bereiche vor ihm. Die Frage bleibt für alle übrigen Bereiche der Etappe 2 offen.tenant— dienst-intern, ein Klient je Methode, ist die Konvention. Die Anfrageobjekt-Eigenschaft existiert nicht mehr:tenant.guard.tssetzt nur nochreq.tenantId,tenant.middleware.ts(der nie verdrahtete Zwilling mit identischer Logik) ist gelöscht. Siehe Abschnitt "Zwei belegte Befunde" oben unddocs/mandantentrennung-etappe2-fehlerrichtung.md, "## Bereich tenant", (n4)(a).Wie die WINDOWS-#19-Policy fürAufgelöst (260910-jab):SearchProvider/TenderRssFeedSourceam Ende genau lautet — nur, dass sie vor dem Scharfschalten gelöst sein muss.TenderRssFeedSourcebekommt vier nach Befehl getrennte Regeln,SearchProviderbleibt 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 Plan260909-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 aufusername @uniqueplattformweit 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.tsbraucht dann eine Mandantenangabe VOR der Suche. Die Bindung der drei Nach-Anmeldungs-Methoden dieses Bereichs (getMe,changePassword,adminResetPassword) ist davon NEUTRAL — sie haengt am ClaimtenantIdund anUser.id(plattformweite UUID), nicht anusername/email. Siehedocs/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) bringtapp.current_user/current_user_id()und dieIS 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. Siehedocs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt "Regelschluss Benutzerdimension (Etappe 3b, 260911-nke)". Was weiterhin offen ist: ein Aufrufer, deruserIdvergisst, sieht den ganzen Mandanten (kein Wächter gebaut, siehe.planning/WINDOWS.md); Systemkontext (Etappe 3c) und Anmeldenamen pro Mandant (Etappe 3a) bleiben offen. - Wie das Mailmodul künftig je Mandant versendet (260911-gwh). Der
Startpfad
loadAnySmtpConfigForStartupTransport()bleibt bewusst ungebunden (sechster Fall der Hintergrunddienst-Falle, WINDOWS #30, siehe oben) — ein Umbau auf Transport je Versand ausgetDecryptedSmtpConfig(tenantId), die Form, dieDkvMailService/TenderMailServicebereits haben, ist eine Funktionsänderung (Umbau des Mailmoduls), kein Bindungsumbau, und deshalb NICHT Gegenstand dieser Etappe. Siehedocs/mandantentrennung-etappe2-fehlerrichtung.md, "## Bereich settings", (s4)(a).