Files
tessera-ctl/docs/mandantentrennung-zugriffsklassifikation.md
T
schalli e0e163ec63 docs(quick-260911-cwh): Bereich calendar Etappe 2 abgeschlossen — restliche Dokumentstellen nachgezogen (Aufgabe 3)
- docs/mandantentrennung-zugriffsklassifikation.md: Uebersichtszeile
  calendar auf 0/12 gezogen (war 12/0, kein ungebundener Rest — erster
  Bereich in Folge ohne begruendeten Rest), Summenzeile auf 83/159
  fortgeschrieben, Klassen-Verteilung unveraendert mit ausdruecklichem
  Stand-260911-cwh-Vermerk, Hintergrunddienst-Abschnitt um den
  Sonderfall refreshCacheInBackground (abgekoppelte Fortsetzung, kein
  sechster Fall) ergaenzt, Abschnitt "Was diese Etappe NICHT entscheidet"
  um calendar ergaenzt
- .planning/WINDOWS.md: neuer offener Eintrag (#26) zur lautlosen
  Auspraegung der umgekehrten Fehlerrichtung im Bereich calendar, ueber
  gsd-tools windows append angelegt
- Beide Falsifizierungsnachweise durchgefuehrt (falscher Stand macht
  rls-access-inventory.spec.ts rot; falsche Uebersichtszahl macht das
  herleitende Gate rot), zurueckgenommen
- Etappe 2, neunter Bereich (calendar) abgeschlossen: alle zwoelf
  Zugriffe gebunden, Baseline gehalten (883 Tests, 57 Dateien, 101
  Werkzeugpruefungen, Typpruefung sauber)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
2026-09-11 09:59:17 +02:00

53 KiB
Raw Blame History

Mandantentrennung — Zugriffsklassifikation

Dieses Dokument gehört zusammen mit docs/mandantentrennung-datenbankrolle.md zur Vorbereitung der Mandantentrennung auf Datenbankebene (WINDOWS #18/#20). Während die Datenbankrolle-Anleitung beschreibt, wie die Umstellung technisch abläuft, hält dieses Dokument fest, welche der 227 this.prisma.*-Fundstellen in apps/api/src beim Umbau in den folgenden Etappen angefasst werden müssen und welche bewusst unverändert bleiben.

Die Bestandsaufnahme unten wird maschinell aus dem Quelltext ermittelt (nicht von Hand zusammengeschrieben) und durch apps/api/src/prisma/rls-access-inventory.spec.ts bei jedem Testlauf gegen den tatsächlichen Stand geprüft. Verschiebt sich eine Zeile, bleibt die Prüfung grün — Vergleichsschlüssel sind Datei und Prisma-Modellname, keine Zeilennummer. Kommt eine neue Fundstelle hinzu oder verschwindet eine bestehende, schlägt die Prüfung fehl, bis dieses Dokument nachgezogen wird.

Die drei Klassen

  • muss-mandantengebunden — berührt auf Rechnung genau eines Mandanten eine Tabelle mit tenantId (oder eine Tabelle, die ihre Mandantenregel über einen Join auf eine solche Tabelle bezieht). Muss in Etappe 2 auf forTenant() umgestellt werden.
  • bewusst-uebergreifend — muss über Mandanten hinweg sehen, mit ausgeschriebenem Grund. Braucht in Etappe 3 eine sichtbare Kennzeichnung (Systemkontext), aber keinen forTenant()-Umbau.
  • keine-mandantengebundene-tabelle — betrifft eines der acht Modelle ohne tenantId bzw. eine bewusst plattformweite Tabelle (D-03). Kein Umbau nötig.
  • beides — ein vierter, im Plan ausdrücklich verlangter Sonderfall: ein Hintergrunddienst, der zurecht über alle Mandanten hinweg eine Liste aufbaut (bewusst übergreifend), aber innerhalb der Schleife je Mandant binden muss (muss mandantengebunden werden). Beide Anteile sind in derselben Datei vorhanden; die Fundstelle bekommt hier eine Sammelklassifikation, die Aufteilung auf Zeilenebene steht in der Begründung.

Zwei belegte Befunde

req.tenantPrisma wird gesetzt, aber nirgends gelesen. tenant.middleware.ts:44 und tenant.guard.ts:41 setzen req.tenantPrisma = forTenant(this.prisma, tenantId). Eine Volltextsuche über apps/api/src nach tenantPrisma außerhalb dieser beiden Dateien und ihrer Tests findet keine lesende Stelle — kein Controller greift darauf zu. Die Verdrahtung besteht, wird aber nicht genutzt. Für Etappe 2 ist zu entscheiden, ob die Controller künftig darüber gehen (dann bräuchte es keinen zweiten forTenant()-Aufruf je Service-Methode) oder ob der Weg ersatzlos entfällt. Dieser Plan entscheidet das nicht, hält den Befund nur fest.

WINDOWS #19 — nullbares tenantId bei SearchProvider und TenderRssFeedSource — GESCHLOSSEN (260910-jab, Aufgabe 1/2). Beide Modelle tragen ein nullbares tenantId (SearchProvider für admin-gepflegte Vorgabe-Suchmaschinen; TenderRssFeedSource für plattformweite RSS-Quellen wie den geseedeten service.bund.de-Feed, D-06). Die ausgelieferte Policy "tenantId" = current_tenant_id() verglich NULL nie gleich — nach dem Scharfschalten wären plattformweite Zeilen für JEDEN Mandanten unsichtbar gewesen, nicht nur für fremde.

Geschlossen durch Migration 20260910120000_rls_widen_membership_grant_and_platform_read, lokal angewandt und gegen den Systemkatalog der lebenden Datenbank gemessen: TenderRssFeedSource bekommt vier nach Befehl getrennte Regeln — tenant_platform_read_policy (SELECT) schließt Zeilen ohne Mandant ausdrücklich ein, tenant_insert_policy/tenant_update_policy/ tenant_delete_policy verlangen weiterhin ausnahmslos einen Mandanten. Die Trennung nach Befehl ist notwendig, weil ein einzelner USING-Ausdruck auch bestimmt, welche Zeilen UPDATE/DELETE erreichen — eine Leseregel, die plattformweite Zeilen einschließt, hätte ohne Trennung jedem Mandanten auch das Ändern/Entfernen dieser Zeilen erlaubt.

Die Hälfte zur Suchanbietertabelle (SearchProvider) schließt NICHT als gelöstes Problem, sondern als widerlegte Prämisse: lokal gemessen gibt es keinen Codeweg, der eine mandantenlose SearchProvider-Zeile erzeugt — der einzige Schreibweg (dashboard.service.ts) verlangt die Mandantenkennung als Pflichtparameter, und die Vorgabe-Suchmaschinen kommen laut 05-02-Entscheidung aus Konstanten, nicht aus der Datenbank. Die Regel bleibt deshalb bewusst unverändert streng; eine Lockerung wäre hier die falsche Richtung, weil sie eine künftige mandantenlose Zeile jedem Mandanten zeigen würde.

Was diese Reparatur NICHT löst: unter der Anwendungsrolle lässt sich eine plattformweite TenderRssFeedSource-Zeile weder anlegen noch entfernen, in der alten wie in der neuen Regel, weil jede Schreibregel einen Mandanten verlangt. Als eigener offener Ledger-Eintrag festgehalten (WINDOWS #24), damit dieser Rest nicht mit #19 verschwindet.

Übersicht je Bereich (Zeilentreffer je Bereich, ungebunden vs. gebunden)

Wichtig, seit 260909-ipc (Aufgabe 3): die Spalte "Ungebunden" zählt NUR noch this.prisma.<Modell>-Rohtreffer — eine unveränderte Spaltenüberschrift über einer veränderten Bedeutung wäre die nächste stille Falle, seit ein Bereich (ldap) tatsächlich gebundene Zugriffe hat, die aus dieser Zählung verschwinden. Die neue Spalte "Gebunden" zählt daneben die forTenant()-gebundenen Rohtreffer (tenantPrisma.<Modell>, Konvention dieses Codes — siehe rls-access-inventory.spec.ts für die allgemeinere, namensunabhängige Erkennung über die const <Name> = forTenant(-Zuweisungsform). Beide Spalten sind Rohtreffer (mehrere Vorkommen desselben Modells in derselben Datei zählen mehrfach), nicht (Datei, Modell)-Paare wie in der Bestandsaufnahme unten.

Gemessen mit grep -ro "this\.prisma\.[a-zA-Z]*" apps/api/src/<bereich> | grep -v spec | wc -l bzw. grep -ro "tenantPrisma\.[a-zA-Z]*\." apps/api/src/<bereich> | grep -v spec | wc -l am 2026-09-09, nach den Änderungen aus Aufgabe 2/3 dieses Plans (260909-jts):

Methodische Lücke, seit 260909-jts sichtbar: die zweite Zählung sucht ausschließlich den Namen tenantPrisma — die Konvention, die ldap und (bis auf die drei Transaktionen) auch groups verwenden. Die drei withTenantTransaction()-Aufrufe in groups.service.ts binden zusätzliche neun Modellzugriffe über den Namen tx (den Transaktionsparameter), die diese einfache Rohtrefferzählung strukturell NICHT sieht — anders als die maschinelle, namensunabhängige Erkennung in rls-access-inventory.spec.ts (Befund B), die auch diese Form erfasst. Die Zahl 31 unten ist deshalb der Bodensatz, nicht die vollständige Zahl gebundener Zugriffe in groups; die Bestandsaufnahme unten (Spalte "Stand", je (Datei, Modell)-Paar) ist die autoritative Quelle.

Bereich Ungebunden Gebunden Hinweis
tenders 35 27 war 62/0, dann 36/26 nach 260909-laa — 260910-jab (Aufgabe 2) hat tender-rss-feed.service.ts/listForUser zusätzlich auf forTenant() umgestellt (WINDOWS #19 geschlossen, Befund F: ungebunden hätte die Reparatur den Pfad sonst still auf nur die plattformweiten Zeilen reduziert): ein Rohtreffer wandert von ungebunden nach gebunden (36→35, 26→27). Die 35 verbleibenden ungebundenen Treffer sind die zwölf bewusst nicht angefassten Paare (D-03-Katalog, zwei Fan-out-Adapter) plus die zwei bewusst ungebundenen RSS-Pfade (createPlatform/remove, WINDOWS #24) plus die übergreifenden Hälften der beiden Hintergrunddienste (Etappe-3-Übergabe)
groups 0 31 war 37/0 — Aufgabe 2/3 (260909-jts) haben groups.service.ts (12 Methoden) und module-grants.service.ts (5 Methoden) vollständig auf forTenant()/withTenantTransaction() umgestellt. Die neun zusätzlichen, über tx gebundenen Zugriffe innerhalb der drei Transaktionen zählt dieses einfache Muster nicht mit (siehe Methodenhinweis oben)
ldap 4 26 war 21/0 — Aufgabe 2/3 (260909-ipc) haben ldap-config.service.ts (5 Methoden) und ldap.service.ts (6 Methoden, 11 Abfragen) auf forTenant() umgestellt. Die 4 verbleibenden ungebundenen Treffer sind bewusst: getAllActiveConfigs/onApplicationBootstrap (Befund B) und resolveEmailForWrite (Befund A, T-IPC-04)
dkv 1 22 war 21/0 — Aufgabe 2/3 (260909-mir) haben dkv.service.ts vollständig auf forTenant() umgestellt: Konfigurationspfade (loadConfig, getConfigForApi, saveConfig, testConnection), Historie, Fahrzeugstammdaten und der neue Besitzriegel vor dem Ausfuhrdatei-Download. Gebunden sind es 22 statt 21, weil der Riegel einen zusätzlichen Lesezugriff auf dkvInvoiceHistory einführt (T-MIR-03). Der eine verbleibende ungebundene Treffer ist der benannte Planer-Startpfad loadAnyActiveConfigForScheduler() (Befund D, WINDOWS #21) — bewusst, mit dreifacher Markierung
user 8 14 war 17/0 — Aufgabe 2/3 (260910-das) haben user.service.ts (findById/create/update/deactivate/delete sowie die zwei neuen Plattform-Administratorsicht-Methoden), admin-seed.service.ts (Erstanlage des Administrators) und user.controller.ts (Benutzerliste des ADMIN-Zweigs, alle drei Kennungswege ueber die Dienstmethoden, alle fuenf Selbstbedienungszugriffe) auf forTenant() umgestellt. Die 8 verbleibenden ungebundenen Rohtreffer sind bewusst: findByUsername in user.service.ts (plattformweit eindeutiger Schluessel, derselbe Fall wie resolveEmailForWrite im Bereich ldap), die Erstanlage-Pruefung und beide Zugriffe auf tenant in admin-seed.service.ts, sowie der neue Schleifentreiber this.prisma.tenant.findMany der beiden Plattform-Administratorsicht-Methoden in user.service.ts (Tenant traegt keinen Zeilenschutz)
module-registry 7 10 war 17/0 — Aufgabe 2/3 (260910-exd) haben module-access.service.ts (getAccessibleModuleIds: Kurzschlusszweig, Direktweg, Gruppenweg, Schnittmenge; getCatalogFlags: eigener Aktivierungs-Lesezugriff) und module-registry.service.ts (findActiveForTenant, activateForTenant, deactivateForTenant, isModuleActive) auf forTenant() umgestellt. Die 7 verbleibenden ungebundenen Rohtreffer sind bewusst: der eine Katalogzugriff in module-access.service.ts (findAccessibleModules) und die sechs Katalogzugriffe in module-registry.service.ts (findAll, findBySlug, die beiden Katalog-Existenzpruefungen in activateForTenant/deactivateForTenant, die Katalogsuche in isModuleActive, seedModule) — der Modulkatalog (Module) traegt heute keinen Zeilenschutz, eine Bindung waere heute wirkungslos, nicht katastrophal; katastrophal wuerde sie erst, WENN Etappe 3 dieser Tabelle eine Regel gibt (Befund E)
dashboard 1 12 war 13/0 — Aufgabe 2/3 (260910-krx) haben dashboard.service.ts vollständig umgestellt: getLayout/saveLayout (gemeinsam gebunden), getWidgets/addWidget/updateWidgetConfig/removeWidget sowie getSearchProviders/addSearchProvider/removeSearchProvider laufen über forTenant(), je Methode ein Klient. Der eine verbleibende ungebundene Rohtreffer ist bewusst: der Modulkatalog (Module) trägt heute keinen Zeilenschutz, eine Bindung wäre heute wirkungslos, nicht katastrophal — katastrophal würde sie erst, WENN Etappe 3 dieser Tabelle eine Regel gibt (Befund E aus module-registry, hier übernommen)
auth 8 5 unverändert gegenüber dem in Etappe 1 (260909-eor) gemessenen Stand
calendar 0 12 war 12/0 — Aufgabe 2 (260911-cwh) hat calendar.service.ts vollständig auf forTenant() umgestellt: getSources, addSource, beide Abfragen von updateSource/deleteSource, alle drei Abfragen von testConnection, Laden plus beide Synchronstatus-Rückschreibungen von fetchAndCacheEvents — je Methode ein Klient. Anders als bei den sieben Bereichen davor bleibt KEIN ungebundener Rest übrig: CalendarSource trägt eine Pflicht-Mandantenkennung, und kein Pfad dieses Bereichs liest über Mandanten hinweg
tenant 8 0 unverändert
favorites 7 0 unverändert
settings 4 0 unverändert
Summe 83 159 Ungebunden: war 118 nach 260910-das, dann 108 nach 260910-exd (module-registry 17→7), dann 107 nach 260910-jab (tenders 36→35, listForUser gebunden), dann 95 nach 260910-krx (dashboard 13→1), jetzt 83 nach 260911-cwh (calendar 12→0). Gebunden: war 124, dann 134 nach 260910-exd (zusätzlich 10 in module-registry), dann 135 nach 260910-jab (zusätzlich 1 in tenders), dann 147 nach 260910-krx (zusätzlich 12 in dashboard), jetzt 159 nach 260911-cwh (zusätzlich 12 in calendar). Diese Übersicht ist eine Buchführungshilfe; autoritativ ist die Fundstellentabelle unten, die rls-access-inventory.spec.ts bei jedem Lauf gegen den Quelltext prüft

Klassen-Verteilung (nach (Datei, Modell)-Fundstellen, 63 Paare)

Stand 260909-jts (Aufgabe 3): 61 Paare aus dem vorherigen Durchlauf (260909-ipc) plus ein bisher vollstaendig unsichtbares Paar (groups.service.ts/tenantModuleActivation), das erst die um Transaktionsparameter erweiterte Erkennung (Befund B, Aufgabe 2 dieses Plans) sichtbar macht — der einzige Zugriff auf dieses Modell in dieser Datei lief bis dahin ausschliesslich ueber den Rueckgabeparameter der interaktiven Transaktion in ensureDefaultGroup und war weder ueber this.prisma.<Modell> noch ueber <gebundener Client>.<Modell> erfassbar. Die Zahl ist der Ausgabe der Pruefung in apps/api/src/prisma/rls-access-inventory.spec.ts entnommen, nicht geschaetzt.

Stand 260909-laa (Aufgabe 2): dieselben 62 Paare, keine neue Fundstelle hinzugekommen oder verschwunden — nur EINE Klasse hat sich verschoben: tender-rss-feed.service.ts/tenderRssFeedSource wechselt von muss-mandantengebunden auf beides (WINDOWS #19 — createForUser bindet, listForUser/createPlatform/remove bleiben bewusst uebergreifend), der exakte Praezedenzfall aus ldapConfig in 260909-ipc.

Stand 260910-das (Aufgabe 3): 63 Paare — 62 aus dem vorherigen Durchlauf plus EIN neues Paar (user.service.ts/tenant, Klasse keine-mandantengebundene-tabelle, Stand ungebunden): der Schleifentreiber der neu eingefuehrten Plattform-Administratorsicht (Befund F/N, Aufgabe 1/2). Zusaetzlich ZWEI Klassenkorrekturen, jede mit eigener Begruendung an der Fundstelle in der Bestandsaufnahme oben: user.service.ts/user wechselt von muss-mandantengebunden auf beides — wortgleich derselbe Praezedenzfall wie ldap.service.ts/user in 260909-ipc (resolveEmailForWrite, hier findByUsername); admin-seed.service.ts/user wechselt von bewusst-uebergreifend auf beides, weil die bisherige Begruendung nachweislich falsch war (Befund J: der Mandant ist bei der Erstanlage des Administrators bereits bekannt, nicht strukturell fehlend). Alle drei Zahlen sind der Ausgabe von rls-access-inventory.spec.ts entnommen, nicht geschaetzt.

Stand 260910-krx (Aufgabe 3): unveraendert, ausdruecklich festgehalten statt uebersprungen. Weiterhin 63 Paare, keine Klasse verschoben sich. Die vier Paare des Bereichs dashboard (dashboard.service.ts/dashboardLayout, /module, /searchProvider, /widgetInstance) waren bereits vor diesem Durchlauf korrekt klassifiziert (drei muss-mandantengebunden, eines keine-mandantengebundene-tabelle) — dieser Plan aendert nur ihre Stand-Spalte (ungebunden auf gebunden fuer drei der vier Paare), keine ihrer Klassen. Eine unveraenderte Tabelle ohne diesen Vermerk waere von einer vergessenen Nachziehung nicht zu unterscheiden — deshalb steht die Abwesenheit einer Aenderung hier ausdruecklich, statt stillschweigend uebersprungen zu werden.

Stand 260911-cwh (Aufgabe 3): unveraendert, ausdruecklich festgehalten statt uebersprungen. Weiterhin 63 Paare, keine Klasse verschoben sich. Das eine Paar des Bereichs calendar (calendar.service.ts/calendarSource) war bereits vor diesem Durchlauf korrekt klassifiziert (muss-mandantengebunden) — Aufgabe 2 (260911-cwh) aendert nur seine Stand-Spalte (ungebunden auf gebunden), nicht seine Klasse. Eine unveraenderte Tabelle ohne diesen Vermerk waere von einer vergessenen Nachziehung nicht zu unterscheiden — deshalb steht die Abwesenheit einer Aenderung hier ausdruecklich, statt stillschweigend uebersprungen zu werden.

Klasse Anzahl Paare
muss-mandantengebunden 31
keine-mandantengebundene-tabelle 17
beides 13
bewusst-uebergreifend 2
Summe 63

Der Hintergrunddienst als Falle — fünf Fälle

Ein Planer, der über alle Mandanten iteriert, liest zu Recht übergreifend — muss aber innerhalb der Schleife je Mandant binden. Vier Dateien sind in diesem Sinne beides-Fälle, davon einer (260910-das) der bislang EINZIGE, der auf BEIDEN Hälften bereits richtig ist; der fünfte, seit 260909-mir bekannte Fall ist von anderer Art und deshalb unten getrennt aufgeführt — er iteriert gar nicht, sondern greift sich eine beliebige Zeile heraus:

  • ldap.service.ts (AD-Abgleich) — Stand 260909-ipc, Aufgaben 2/3: geschlossen. Iteriert nicht selbst über alle Mandanten (der Sync läuft je Aufruf für einen übergebenen Mandanten). Vor dieser Etappe blieben Lesezugriffe auf group, ldapConfig und user innerhalb der Sync-Methoden teils ungebunden, obwohl der Mandant zu diesem Zeitpunkt bereits bekannt war — nur 4 der inzwischen 11 forTenant()-Aufrufstellen deckten die Lese-/Schreibpfade ab. Jetzt sind group und ldapConfig vollständig gebunden; bei user bleibt ausschließlich resolveEmailForWrite bewusst ungebunden (Befund A, T-IPC-04 — siehe Bestandsaufnahme unten). Der als gefährlichster Punkt benannte Löschzweig (syncBoundGroupsForTenant, WINDOWS #20) war bereits seit Etappe 1 gebunden. Die seinerzeit offen geführte Reihenfolgebedingung für Etappe 4 — die Übergabe unmittelbar vor der Löschung (this.groupsService.reassignDefaultBeforeDelete(tenantId, group.id) und ensureDefaultGroup(tenantId) in groups.service.ts) — ist mit 260909-jts (Aufgabe 2) GESCHLOSSEN: beide Methoden laufen seither über forTenant()/withTenantTransaction() (siehe Bestandsaufnahme unten, groups.service.ts/group, Stand gebunden). Nachtrag, nicht Neuschrieb: der ursprüngliche Befund bleibt oben lesbar, weil er den Zustand zum Zeitpunkt der ldap-Umstellung korrekt beschreibt und für spätere Etappen als Beleg dient, siehe auch docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt (e), Befund D.
  • tender-digest.scheduler.ts (Ausschreibungs-Digest) — Stand 260909-laa, Aufgabe 3: Je-Treffer-Hälfte geschlossen, übergreifende Hälfte an Etappe 3 übergeben. Liest tenderMatch (Kandidatenabfrage, findMany mit distinct: ['userId']) bewusst über ALLE Mandanten in einem einzigen findMany (ein einziger globaler Cron-Job, kein Mandant im Job selbst — so von Anfang an entworfen, Pitfall 1 in den Kommentaren der Datei) — diese Kandidatenabfrage bleibt UNGEBUNDEN und ist im Code als Etappe-3-Übergabe kommentiert. Sie wählt zusätzlich das denormalisierte tenantId der Treffer-Zeile mit aus, damit die Schleife binden kann; der Sonderfall eines Nutzers mit Treffern unter zwei verschiedenen Mandanten (Mandantenwechsel) ist NICHT gelöst, siehe docs/mandantentrennung-etappe2-fehlerrichtung.md. Innerhalb der Schleife laufen tenderNotificationPref.findUnique, tenderMatch.findMany/updateMany und user.findUnique je Kandidatenzeile über forTenant(), gebunden an deren Mandanten.
  • tender-matching.service.ts (Ausschreibungs-Sofortmeldung) — Stand 260909-laa, Aufgabe 3: Je-Treffer-Hälfte geschlossen, übergreifende Hälfte an Etappe 3 übergeben. tenderSavedSearch.findMany (Profile ALLER Mandanten) und der Lesezugriff auf den plattformweiten tender-Katalog (D-03) bleiben UNGEBUNDEN, beide im Code als Etappe-3-Übergabe bzw. D-03 kommentiert. Innerhalb der Profilschleife laufen die Treffer-Anlage (tenderMatch.upsert) und — im nachgelagerten Instant-Dispatch — tenderMatch.findMany/updateMany sowie user.findUnique über forTenant(), EIN gebundener Client je Profil, gebunden an dessen Mandanten.
  • admin-seed.service.ts (ensureDefaultGroupsForAllTenants) — Stand 260910-das, Aufgabe 2: der vierte Fall dieser Art und der bislang EINZIGE, der auf BEIDEN Hälften bereits richtig ist. Liest tenant.findMany() bewusst über ALLE Mandanten (Treiber, ungebunden — Tenant trägt keinen Zeilenschutz, Aufgabe 1 gemessen, tenant-tabelle-ohne-zeilenschutz-bleibt-lesbar) und ruft je Mandant groupsService.ensureDefaultGroup(tenant.id) auf — dieser Rumpf ist seit 260909-jts vollständig über forTenant()/ withTenantTransaction() gebunden (siehe Bestandsaufnahme, groups.service.ts/group, Stand gebunden). Anders als bei den drei Fällen oben musste hier in Aufgabe 2/3 (260910-das) NICHTS umgestellt werden — der Rumpf war es bereits, bevor der Bereich user überhaupt an der Reihe war. Einschränkung, bewusst nicht verschwiegen: der äußere try/catch in ensureDefaultGroupsForAllTenants() verschluckt jeden Fehler des Treibers (tenant.findMany) in eine Protokollzeile — läuft die Mandantenliste nach dem Scharfschalten aus irgendeinem Grund leer, entsteht keine Fehlermeldung, sondern gar keine Ausgabe (Befund K).

Der fünfte Fall, anderer Bauart — dkv-scheduler.service.ts / DkvService.loadAnyActiveConfigForScheduler() (260909-mir, Befund D, WINDOWS #21): offen, als benannte Altlast weitergeführt. Dieser Dienst gehört nicht in dieselbe Klasse wie die drei oben. Jene lesen bewusst über alle Mandanten und müssen lediglich innerhalb der Schleife binden — sie sind heute korrekt und verstummen erst nach dem Scharfschalten. Der DKV-Planer iteriert überhaupt nicht: findFirst() ohne jede Bedingung zieht bei mehreren Mandanten EINEN beliebigen und bedient die übrigen NIE. Ist ausgerechnet die gezogene Zeile inaktiv, bedient er niemanden, obwohl ein zweiter Mandant aktiv wäre. Er ist damit heute bereits falsch und verstummt nach dem Scharfschalten zusätzlich (null statt einer beliebigen Zeile; der Planer protokolliert das als Normalfall und richtet für JEDEN Mandanten nichts ein, ohne Alarm).

Binden ist hier keine Lösung: onModuleInit() hat beim Start strukturell keinen Mandantenkontext, ein gebundener Aufruf liefe garantiert leer. Der Umbau auf einmal-abfragen-viele-bedienen ist die in Phase 07-04 zurückgestellte Mehrmandanten-Planung — eine Funktionsänderung, kein Bindungsumbau, und deshalb in Etappe 2 nicht vorgenommen. Der Zugriff bleibt deshalb ungebunden, aber dreifach markiert: eigene benannte Methode mit Kopfkommentar (bewusst KEINE Verzweigung hinter einem optionalen Parameter, die jemand später „vereinheitlicht"), fortgeschriebener Kopfkommentar in dkv-scheduler.service.ts, Ledger-Eintrag WINDOWS #21. Das Signal für das Verstummen gehört in die Vorabprüfung von Etappe 4 (rls-preflight.mjs).

Stand 260910-exd — kein sechster Fall, gemessen statt angenommen. Der Bereich module-registry fügt diesem Abschnitt KEINEN sechsten Fall hinzu. ModuleRegistryService.seedModule() (die Katalogpflege beim Start, aufgerufen aus vier Seed-Dateien) schreibt zwar ohne Mandantenkontext — sie iteriert aber über NICHTS je Mandant, sondern schreibt genau eine Zeile je Aufruf auf den plattformweiten Katalog Module. Damit fehlt ihr die Bauform der fünf oben geführten Fälle (übergreifend LESEN über alle Mandanten, dann je Mandant BINDEN) — sie ist deshalb kein Kandidat für diese Liste. Dieser Satz hält die Abwesenheit fest, damit sie nicht wie ein Übersehen aussieht.

Stand 260910-krx — auch der Bereich dashboard fügt diesem Abschnitt keinen sechsten Fall hinzu, gemessen statt angenommen. Anweisung (260910-krx, Aufgabe 1): grep -rn "onModuleInit\|onApplicationBootstrap\|@Cron\|setInterval\|Scheduler" apps/api/src/dashboard --include=*.ts liefert null Treffer außerhalb von Testdateien — der einzige Dienst dieses Bereichs, dashboard.service.ts, enthält keinen Hintergrunddienst, keinen Planer und keinen Start-Hook. Jede seiner neun mandantengebundenen Methoden wird ausschließlich synchron aus einer Anfrage eines einzelnen, bereits bekannten Nutzers heraus aufgerufen — die Bauform dieses Abschnitts (übergreifend LESEN über alle Mandanten, dann je Mandant BINDEN) kommt in diesem Bereich an keiner Stelle vor.

Stand 260911-cwh — auch der Bereich calendar fügt diesem Abschnitt keinen sechsten Fall hinzu, gemessen statt angenommen (260911-cwh, Aufgabe 1, Befund B). grep -rn "@Cron\|onModuleInit\|onApplicationBootstrap\|setInterval\|setTimeout" apps/api/src/calendar --include=*.ts liefert außerhalb von Testdateien genau einen Treffer, providers/ics.provider.ts:100 — ein setTimeout für den Abbruch eines HTTP-Abrufs nach acht Sekunden, kein Planer. Der einzige Dienst dieses Bereichs, calendar.service.ts, enthält keinen Hintergrunddienst, keinen Planer und keinen Start-Hook — die Bauform dieses Abschnitts (übergreifend LESEN über alle Mandanten, dann je Mandant BINDEN) kommt an keiner Stelle vor. Es gibt aber einen benannten Sonderfall, anderer Bauart als die fünf Fälle oben: refreshCacheInBackground (private Methode) ist eine ABGEKOPPELTE FORTSETZUNG einer Anfrage — aggregateEvents stößt sie an, wartet nicht auf sie, und sie ruft fetchAndCacheEvents mit den Parametern der ursprünglichen Anfrage auf. Sie iteriert NICHT über mehrere Mandanten und hat deshalb keine übergreifende Hälfte zu binden — sie trägt die Mandantenkennung der Anfrage, die sie ausgelöst hat, und kann strukturell keine andere haben. Kein Kandidat für diese Liste; dieser Absatz hält die Abwesenheit fest, damit sie nicht wie ein Übersehen aussieht.

Bestandsaufnahme

Maschinell ermittelt, rls-access-inventory.spec.ts hält Vollständigkeit nach. Spalten: Datei, Modell (Prisma-Modellname wie in this.prisma.<Modell> oder — seit 260909-ipc, Befund G — in <gebundener Client>.<Modell> verwendet), Klasse, Stand (gebunden/ungebunden/gemischt, seit 260909-ipc maschinell gegen den Quelltext geprüft), Begründung.

Datei Modell Klasse Stand Begründung
apps/api/src/auth/auth.service.ts passwordResetToken muss-mandantengebunden gebunden Kein eigenes tenantId, RLS ueber Join auf User (Migration 20260618112133). requestPasswordReset/resetPassword laufen vollstaendig ueber forTenant() (Etappe 1, WINDOWS #20, Aufgabe 1) — von der alten, nur this.prisma.* erkennenden Suche nie erfasst, weil bereits gebunden; die erweiterte Erkennung aus Aufgabe 2 (260909-ipc) macht diese Fundstelle erstmals sichtbar.
apps/api/src/auth/auth.service.ts user muss-mandantengebunden gemischt getMe, changePassword, adminResetPassword suchen über die Benutzerkennung aus dem Sitzungsnachweis — der Mandant ist dort bereits bekannt (Aufgabe 2 fasst sie bewusst nicht an, siehe SUMMARY).
apps/api/src/calendar/calendar.service.ts calendarSource muss-mandantengebunden gebunden Kalenderquellen eines Nutzers je Mandant gebunden (encryptedPassword traegt Zugangsdaten zu externen Exchange-/CalDAV-Servern), tenantId-Spalte vorhanden. Seit 260911-cwh (Aufgabe 2) laufen alle zwoelf Zugriffe (getSources, addSource, beide Abfragen von updateSource/deleteSource, alle drei Abfragen von testConnection, Laden plus beide Synchronstatus-Rueckschreibungen von fetchAndCacheEvents) ueber forTenant(), ein Klient je Methode; fetchAndCacheEvents/refreshCacheInBackground nehmen die Mandantenkennung als Parameter, Letztere traegt die Kennung der urspruenglichen Anfrage. Die drei Besitzpruefungen (updateSource/deleteSource/testConnection, Vergleich gegen userId aus dem Sitzungsnachweis) bleiben zusaetzlich bestehen — die Regel auf CalendarSource kennt keine Benutzerdimension (260911-cwh, Aufgabe 1, gemessen), sie sind bis zur Etappe-3-Entscheidung (2) der einzige Schutz zwischen Kollegen DESSELBEN Mandanten.
apps/api/src/dashboard/dashboard.service.ts dashboardLayout muss-mandantengebunden gebunden Widget-Anordnung eines Nutzers, tenantId-Spalte vorhanden. Seit 260910-krx (Aufgabe 2) laufen getLayout/saveLayout GEMEINSAM ueber forTenant(), ein Klient je Methode; saveLayout uebersetzt eine PrismaClientUnknownRequestError (RLS-Konflikt auf der plattformweit eindeutigen userId, gemessen in Aufgabe 1 — NICHT die P2002-Form, die der Bereich tenders abfaengt) in eine deutsche Konfliktmeldung.
apps/api/src/dashboard/dashboard.service.ts module keine-mandantengebundene-tabelle ungebunden Modulkatalog ist plattformweit, kein tenantId (Migration 20260909140000, Gruppe b). MESSUNG (260910-krx, Aufgabe 1, uebernommen aus module-registry-Pruefung module-tabelle-traegt-keinen-zeilenschutz): die Tabelle traegt heute keinen Zeilenschutz, eine Bindung waere heute wirkungslos, nicht katastrophal. BEDINGUNG: katastrophal wuerde sie erst, WENN Etappe 3 dieser Tabelle eine Regel gibt. Die Katalogaufloesung fuer den Widget-Modulfilter (ModuleAccessService.getAccessibleModuleIds) bindet bereits seit 260910-exd in ihrem eigenen Dienst — hier NICHT ein zweites Mal gebunden.
apps/api/src/dashboard/dashboard.service.ts searchProvider muss-mandantengebunden gebunden tenantId nullbar. WINDOWS #19 geschlossen (260910-jab) als widerlegte Prämisse für dieses Modell. In diesem Durchlauf (260910-krx, Aufgabe 1) EIGENSTAENDIG nachgeprueft, nicht aus 260910-jab abgeschrieben: grep -rn "searchProvider|SearchProvider" apps packages prisma --include=*.ts --include=*.mjs --include=*.js --include=*.sql --include=*.json (ohne node_modules, dist/, .next/) findet weiterhin genau einen Schreibweg, dashboard.service.ts:addSearchProvider (create), mit tenantId: string als Pflichtparameter — keine Seed-Datei, kein Skript. Seit Aufgabe 2/3 laufen getSearchProviders/addSearchProvider/removeSearchProvider ueber forTenant(); die Regel auf SearchProvider bleibt UNVERAENDERT streng, zusaetzlich datenbankseitig verteidigt durch searchprovider-gebundenes-einfuegen-ohne-mandant-abgelehnt (Aufgabe 1).
apps/api/src/dashboard/dashboard.service.ts widgetInstance muss-mandantengebunden gebunden Platzierte Dashboard-Widgets eines Nutzers, tenantId-Spalte vorhanden. Seit 260910-krx (Aufgabe 2) laufen getWidgets, addWidget sowie beide Paare aus Besitzpruefung und Schreibzugriff (updateWidgetConfig/removeWidget) ueber forTenant(); die vorgeschalteten Besitzpruefungen ueber die Benutzerkennung bleiben zusaetzlich bestehen (die Regeln dieses Bereichs kennen keine Benutzerdimension).
apps/api/src/dkv/dkv.service.ts dkvInvoiceHistory muss-mandantengebunden gebunden DKV-Rechnungshistorie je Mandant, tenantId-Spalte vorhanden. Seit 260909-mir (Aufgabe 3) laufen beide Historien-Schreibzugriffe der Verarbeitungsstrecke, beide parallelen Lesezugriffe von getHistory und der neue Riegel vor dem Ausfuhrdatei-Download vollstaendig ueber forTenant().
apps/api/src/dkv/dkv.service.ts dkvModuleConfig muss-mandantengebunden gemischt Postfach-/Zugangsdaten des DKV-Moduls je Mandant. Seit 260909-mir (Aufgabe 2) laufen loadConfig, getConfigForApi, saveConfig, testConnection und der Konfigurations-Lesezugriff der Verarbeitungsstrecke ueber forTenant(). Die Mischung stammt ausschliesslich vom einen benannten, bewusst ungebundenen Planer-Startpfad loadAnyActiveConfigForScheduler() (WINDOWS #21) — keine uebersehene Fundstelle.
apps/api/src/dkv/dkv.service.ts dkvVehicleMaster muss-mandantengebunden gebunden Fahrzeugstammdaten des DKV-Moduls je Mandant. Seit 260909-mir (Aufgabe 3) laufen Fahrzeugliste, Anlegen, beide Paare aus Besitzpruefung und Schreibzugriff (Aendern/Loeschen), beide Zweige des CSV-Imports und der gebuendelte Lesezugriff beim Aufbau der Ausfuhrzeilen vollstaendig ueber forTenant(); die vorgeschalteten Besitzpruefungen bei Aendern/Loeschen bleiben zusaetzlich bestehen (Befund G — ein gebundenes UPDATE ueber die Kennung allein trifft eine fremde Zeile still, nicht laut).
apps/api/src/favorites/favorites.service.ts favoriteLink muss-mandantengebunden ungebunden Favoriten-Links eines Nutzers, tenantId-Spalte vorhanden.
apps/api/src/groups/groups.service.ts group muss-mandantengebunden gebunden Gruppen sind je Mandant, tenantId-Spalte vorhanden. Alle 12 Methoden laufen seit 260909-jts (Aufgabe 2) ueber forTenant() bzw. withTenantTransaction().
apps/api/src/groups/groups.service.ts groupMembership muss-mandantengebunden gebunden Kein eigenes tenantId, RLS über Join auf Group (Migration 20260618112133-Nachfolger) — braucht trotzdem forTenant(), damit der Join-Kontext gesetzt ist. Seit 260909-jts gebunden, einschliesslich der drei Zugriffe innerhalb des Standardgruppen-Aufbaus (ensureDefaultGroup), die zuvor ueber den Transaktionsparameter liefen und fuer keine Pruefung dieses Projekts sichtbar waren (Befund B).
apps/api/src/groups/groups.service.ts moduleGrant muss-mandantengebunden gebunden Modulfreigaben je Mandant, tenantId-Spalte vorhanden. Seit 260909-jts gebunden, einschliesslich des Zugriffs innerhalb des Standardgruppen-Aufbaus (Befund B).
apps/api/src/groups/groups.service.ts tenantModuleActivation muss-mandantengebunden gebunden Welche Module ein Mandant aktiviert hat. Bis 260909-jts vollstaendig unsichtbar (Befund B, 260909-jts-PLAN.md): der einzige Zugriff dieser Datei lief innerhalb der interaktiven Transaktion von ensureDefaultGroup ueber den Rueckgabeparameter (tx.tenantModuleActivation.findMany) — weder this.prisma.<Modell> noch <gebundener Client>.<Modell> sahen das, weil tx weder this.prisma noch aus einer forTenant(-Zuweisung stammte. Die um Transaktionsparameter erweiterte Erkennung aus Aufgabe 2 macht dieses Paar erstmals sichtbar; die Stelle ist seit derselben Aufgabe gebunden (ueber withTenantTransaction()).
apps/api/src/groups/groups.service.ts user muss-mandantengebunden gebunden Nutzerverwaltung innerhalb eines Mandanten. Seit 260909-jts gebunden; addUserToDefaultGroup prueft seither zusaetzlich, dass der Zielbenutzer zum Mandanten gehoert (Befund E, T-JTS-02). Bis 20260910120000_rls_widen_membership_grant_and_platform_read pruefte die Regel auf GroupMembership nachweislich nur die Gruppenseite — seit 260910-jab prueft sie beide Seiten, die Anwendungspruefung bleibt trotzdem bestehen (Schalter weiterhin aus, #18).
apps/api/src/groups/module-grants.service.ts group muss-mandantengebunden gebunden Wie groups.service.ts. Seit 260909-jts (Aufgabe 3) gebunden — assertTargetBelongsToTenant erzeugt seinen eigenen Kontext, getMatrix teilt sich einen Kontext mit den beiden anderen parallelen Teilabfragen.
apps/api/src/groups/module-grants.service.ts groupMembership muss-mandantengebunden gebunden Kein eigenes tenantId, RLS über Join auf Group. Seit 260909-jts gebunden; der where-Filter über die Beziehung zur Gruppe bleibt zusätzlich stehen, weil die Regel auf dieser Tabelle nachweislich nur die Gruppenseite prüft (Aufgabe 1).
apps/api/src/groups/module-grants.service.ts moduleGrant muss-mandantengebunden gebunden Modulfreigaben je Mandant. Seit 260909-jts gebunden; die Mandanten-Gegenprüfung vor jedem Erteilen (assertTargetBelongsToTenant) bleibt zusätzlich bestehen. Bis 20260910120000_rls_widen_membership_grant_and_platform_read prüfte die Regel auf dieser Tabelle nur die Mandantenkennung der Zeile, nicht die referenzierte Gruppe/den referenzierten Benutzer (Befund F, T-JTS-03, Aufgabe 1) — seit 260910-jab prüft sie beide Ziele zusätzlich, die Anwendungsprüfung bleibt trotzdem der erste Schutz (Schalter weiterhin aus, #18).
apps/api/src/groups/module-grants.service.ts tenantModuleActivation muss-mandantengebunden gebunden Welche Module ein Mandant aktiviert hat, tenantId-Spalte vorhanden. Seit 260909-jts gebunden.
apps/api/src/groups/module-grants.service.ts user muss-mandantengebunden gebunden Zielbenutzer eines Grants innerhalb des Mandanten. Seit 260909-jts gebunden; die Mandanten-Gegenprüfung bleibt bestehen.
apps/api/src/ldap/ldap-config.service.ts ldapConfig beides gemischt getConfig, createConfig und updateConfig laufen ueber forTenant(), gebunden an den aus der Anfrage bereits bekannten Mandanten. getAllActiveConfigs() (Planer-Lesezugriff ueber ALLE Mandanten) und die Start-Nachverschluesselung in onApplicationBootstrap bleiben bewusst uebergreifend: beide laufen, bevor bzw. unabhaengig davon, ob ein einzelner Mandantenkontext feststeht (Befund B, 260909-ipc-PLAN.md). Korrektur der Klasse von muss-mandantengebunden: das Paar ist tatsaechlich beides, keine Verhaltensaenderung.
apps/api/src/ldap/ldap-config.service.ts ldapFieldMapping muss-mandantengebunden gebunden Kein eigenes tenantId, RLS ueber Join auf LdapConfig. addFieldMapping und removeFieldMapping nehmen den Mandanten jetzt als Parameter entgegen und laufen vollstaendig ueber forTenant() (T-IPC-01, 260909-ipc-PLAN.md) — schliesst zugleich die Fremdzugriffsluecke beim Loeschen einer Feldzuordnung ueber ihre Kennung.
apps/api/src/ldap/ldap.service.ts group beides gebunden AD-Abgleich: listGroups (die "bereits importiert"-Markierung) und importGroupsByDn (die Idempotenzpruefung ueber ldapObjectGuid) sind mit Aufgabe 3 (260909-ipc) auf forTenant() umgestellt — zusammen mit den bereits vorher gebundenen Stellen (Anlage, Mitgliedschafts- und Gruppenabgleich) ist damit jeder group-Zugriff dieser Datei gebunden. Die Klasse bleibt beides, weil ein zukuenftiger uebergreifender Lesezugriff (z. B. ein neuer Planer-Pfad) hier ebenso legitim waere wie bei ldapConfig unten — nicht, weil heute noch ein ungebundener Zugriff bestuende.
apps/api/src/ldap/ldap.service.ts groupMembership muss-mandantengebunden gebunden Kein eigenes tenantId, RLS ueber Join auf Group. syncGroupMembershipsForTenant laeuft vollstaendig ueber forTenant() (Plan 16-03/16-05) — von der alten, nur this.prisma.* erkennenden Suche nie erfasst, weil bereits gebunden; die erweiterte Erkennung aus Aufgabe 2 (260909-ipc) macht diese Fundstelle erstmals sichtbar.
apps/api/src/ldap/ldap.service.ts ldapConfig beides gebunden Die lastSyncAt-Fortschreibung am Ende von syncUsersForTenant ist mit Aufgabe 3 (260909-ipc) auf den in derselben Methode bereits vorhandenen forTenant()-Client umgestellt — es entsteht kein zweiter. Damit ist der einzige ldapConfig-Zugriff dieser Datei gebunden.
apps/api/src/ldap/ldap.service.ts user beides gemischt Mit Aufgabe 3 (260909-ipc) sind upsertMappedUser (Identitaetssuche und Aktualisierung), searchUsers (die "bereits importiert"-Markierung), importUsersByDn (Dedup und ldapDn-Nachtrag) und die Deaktivierungsschleife in syncUsersForTenant auf forTenant() umgestellt. resolveEmailForWrite bleibt ausdruecklich UNGEBUNDEN (Befund A, T-IPC-04): email/username sind plattformweit eindeutig, eine mandantengebundene Suche saehe einen fremden Halter nicht mehr und meldete faelschlich "frei" — die geloeste Klasse waere muss-mandantengebunden gewesen, bleibt wegen dieser einen bewusst uebergreifenden Abfrage beides. Der Loeschzweig um syncBoundGroupsForTenant (WINDOWS #20) ist bereits seit Etappe 1 gebunden und war nie Teil dieses Befunds.
apps/api/src/module-registry/module-access.service.ts module keine-mandantengebundene-tabelle ungebunden Modulkatalog ist plattformweit, kein tenantId. Bleibt bewusst ungebunden (260910-exd, Aufgabe 1, Befund E): keine Regel auf der Tabelle, eine Bindung waere heute wirkungslos, nicht katastrophal — katastrophal erst, WENN Etappe 3 dieser Tabelle eine Regel gibt.
apps/api/src/module-registry/module-access.service.ts moduleGrant muss-mandantengebunden gebunden Modulfreigaben je Mandant. Seit 260910-exd (Aufgabe 2) laufen Direktweg und Gruppenweg von getAccessibleModuleIds ueber forTenant(), EIN Klient je Methode.
apps/api/src/module-registry/module-access.service.ts tenantModuleActivation muss-mandantengebunden gebunden Aktivierung je Mandant, tenantId-Spalte vorhanden. Seit 260910-exd (Aufgabe 2) laufen Kurzschlusszweig, Schnittmengenabfrage und der eigene Lesezugriff von getCatalogFlags ueber forTenant().
apps/api/src/module-registry/module-registry.service.ts module keine-mandantengebundene-tabelle ungebunden Modulkatalog ist plattformweit. Bleibt bewusst ungebunden (260910-exd, Aufgabe 1, Befund E): keine Regel auf der Tabelle, eine Bindung waere heute wirkungslos, nicht katastrophal — katastrophal erst, WENN Etappe 3 dieser Tabelle eine Regel gibt. findBySlug ist zusaetzlich die Stelle, die ModuleGuard bei JEDER Modulanfrage aufruft.
apps/api/src/module-registry/module-registry.service.ts tenantModuleActivation muss-mandantengebunden gebunden Aktivierung je Mandant. Seit 260910-exd (Aufgabe 3) laufen findActiveForTenant, activateForTenant, beide Zugriffe von deactivateForTenant (ueber EINEN Klienten) und isModuleActive ueber forTenant(), je Methode EIN Klient.
apps/api/src/settings/settings.service.ts smtpConfig muss-mandantengebunden ungebunden SMTP-Zugangsdaten je Mandant, tenantId-Spalte vorhanden.
apps/api/src/tenant/tenant.controller.ts tenant keine-mandantengebundene-tabelle ungebunden Tenant ist die Mandantentabelle selbst — hat keine eigene tenantId-Spalte, kann sie per Definition nicht haben (Migration 20260909140000, Gruppe b).
apps/api/src/tenant/tenant.service.ts tenant keine-mandantengebundene-tabelle ungebunden Dieselbe Begründung.
apps/api/src/tenders/adapters/email-alert.adapter.ts tenderEmailConfig bewusst-uebergreifend ungebunden fetchTenders() liest bewusst jede aktive TenderEmailConfig-Zeile über ALLE Mandanten in einer Abfrage (Plattform-Scheduler, ein Tick pro Postfach, D-13/D-01) — ausführlich im Dateikopf begründet, darf laut Kommentar niemals in forTenant() verpackt werden.
apps/api/src/tenders/adapters/rss.adapter.ts tenderRssFeedSource bewusst-uebergreifend ungebunden Fan-out über jeden aktiven Feed, plattformweit UND persönlich, in einer Abfrage (Zeilen 55–83 im Dateikopf begründet) — dieselbe Scheduler-Ebene wie beim E-Mail-Adapter.
apps/api/src/tenders/tender-dedup.service.ts tender keine-mandantengebundene-tabelle ungebunden Explizit im Dateikopf: "platform-global, RLS-exempt tables. Never wrap these queries in forTenant()." (D-03)
apps/api/src/tenders/tender-dedup.service.ts tenderSource keine-mandantengebundene-tabelle ungebunden Dieselbe Begründung.
apps/api/src/tenders/tender-digest.scheduler.ts tenderMatch beides gemischt Ein einziger globaler Cron-Job liest über ALLE Mandanten (bewusst übergreifend, Pitfall-1-Kommentar im Dateikopf) — seit 260909-laa (Aufgabe 3) bleibt die Kandidatenabfrage (findMany mit distinct) bewusst ungebunden, die Je-Treffer-Abfrage (findMany nach Mandant) und die Stempelung (updateMany) je Kandidatenzeile laufen über forTenant().
apps/api/src/tenders/tender-digest.scheduler.ts tenderNotificationPref beides gebunden Präferenzen werden je Kandidatenzeile gelesen. Seit 260909-laa (Aufgabe 3) läuft der einzige Zugriff über forTenant(), gebunden an den Mandanten der jeweiligen Zeile.
apps/api/src/tenders/tender-digest.scheduler.ts user beides gebunden E-Mail-Adressen für den Versand werden je Kandidatenzeile gelesen. Seit 260909-laa (Aufgabe 3) läuft der einzige Zugriff über forTenant().
apps/api/src/tenders/tender-email-config.service.ts tenderEmailConfig muss-mandantengebunden gebunden Nutzer-CRUD für die eigene Postfachanbindung (Phase 17, D-01) — anders als der Fan-out-Adapter oben, hier ist der Mandant aus der Anfrage bekannt. Seit 260909-laa (Aufgabe 2) laufen getConfigForApi (beide Lesezugriffe), saveConfig und testConnection vollständig über forTenant().
apps/api/src/tenders/tender-fingerprint-backfill.service.ts tender keine-mandantengebundene-tabelle ungebunden Einmaliges Backfill-Skript über den plattformweiten Tender-Katalog (D-03).
apps/api/src/tenders/tender-ingestion.service.ts tender keine-mandantengebundene-tabelle ungebunden Explizit im Dateikopf: "Multi-tenant safety (D-03, T-10-09): uses the plain, non-tenant-scoped ... queries ... these are platform-global".
apps/api/src/tenders/tender-ingestion.service.ts tenderSourcePollConfig keine-mandantengebundene-tabelle ungebunden Plattformweiter Poll-Status, kein tenantId (Migration 20260909140000, Gruppe b).
apps/api/src/tenders/tender-matching.service.ts tender keine-mandantengebundene-tabelle ungebunden Liest den plattformweiten Katalog (D-03), um Treffer zu berechnen — kein tenantId.
apps/api/src/tenders/tender-matching.service.ts tenderMatch beides gebunden Sofortmeldung — siehe Abschnitt "Der Hintergrunddienst als Falle". Seit 260909-laa (Aufgabe 3) laufen sowohl die Treffer-Anlage (upsert) als auch der Instant-Dispatch (findMany/updateMany) vollständig über forTenant(), EIN gebundener Client je Profil.
apps/api/src/tenders/tender-matching.service.ts tenderSavedSearch beides ungebunden Gespeicherte Suchprofile ALLER Mandanten werden gegen neue Treffer geprüft — bewusst übergreifend, Etappe-3-Übergabe (260909-laa, Aufgabe 3, unverändert).
apps/api/src/tenders/tender-matching.service.ts user beides gebunden E-Mail-Adressen für die Sofortmeldung. Seit 260909-laa (Aufgabe 3) läuft der einzige Zugriff über forTenant(), gebunden an den Mandanten des jeweiligen Profils.
apps/api/src/tenders/tender-notification-pref.service.ts tenderNotificationPref muss-mandantengebunden gebunden Nutzer-CRUD für die eigenen Benachrichtigungseinstellungen — Mandant aus der Anfrage bekannt. Seit 260909-laa (Aufgabe 2) laufen getForUser/setForUser vollständig über forTenant(); setForUser übersetzt eine P2002-Verletzung (Befund F) in eine verständliche deutsche Meldung.
apps/api/src/tenders/tender-rss-feed.service.ts tenderRssFeedSource beides gemischt Nutzer-CRUD für die eigenen RSS-Quellen (anders als der Fan-out in adapters/rss.adapter.ts). Seit 260909-laa (Aufgabe 2) bindet createForUser (Zähler + Anlage, beide ausschließlich auf persönlichen Zeilen mit gesetztem Mandanten) über forTenant(). Seit 260910-jab (Aufgabe 2, WINDOWS #19 geschlossen) bindet zusätzlich listForUser — die neue Leseregel (tenant_platform_read_policy, 20260910120000_rls_widen_membership_grant_and_platform_read) schließt die plattformweite Zeile ausdrücklich ein, ungebunden hätte die Reparatur den Pfad sonst still auf nur die plattformweiten Zeilen reduziert (Befund F). createPlatform/remove bleiben bewusst ungebunden — beide Pfade lassen sich unter der Anwendungsrolle grundsätzlich nicht anlegen/entfernen, weil jede Schreibregel einen Mandanten verlangt (WINDOWS #24, eigener offener Punkt). Klasse beides bleibt korrekt: zwei gebundene, zwei bewusst ungebundene Zugriffe in derselben Datei.
apps/api/src/tenders/tender-saved-search.service.ts tenderSavedSearch muss-mandantengebunden gebunden Nutzer-CRUD für die eigenen gespeicherten Suchprofile — Mandant aus der Anfrage bekannt. Seit 260909-laa (Aufgabe 2) laufen list/create/update/remove vollständig über forTenant().
apps/api/src/tenders/tender-scheduler.service.ts tenderSourcePollConfig keine-mandantengebundene-tabelle ungebunden Plattformweiter DÖE-Poll-Status, kein tenantId — im Dateikopf explizit als "genuine platform-wide singleton" begründet.
apps/api/src/tenders/tender-triage.service.ts tenderTriage muss-mandantengebunden gebunden Favorisierungs-/Ablehnungsstatus eines Nutzers, tenantId-Spalte vorhanden. Seit 260909-laa (Aufgabe 2) laufen setTriage/listForUser/favoriteIds vollständig über forTenant().
apps/api/src/tenders/tenders.controller.ts tender keine-mandantengebundene-tabelle ungebunden Lesezugriff auf den plattformweiten Katalog (D-03).
apps/api/src/tenders/tenders.controller.ts tenderSourcePollConfig keine-mandantengebundene-tabelle ungebunden Plattformweiter Poll-Status, admin-verwaltet, kein tenantId.
apps/api/src/tenders/tenders.module.ts tenderSourcePollConfig keine-mandantengebundene-tabelle ungebunden Singleton-Bestückung beim Boot — im Dateikopf explizit als "global, RLS-exempt (D-03)" begründet.
apps/api/src/user/admin-seed.service.ts tenant keine-mandantengebundene-tabelle ungebunden Legt beim ersten Start den Standard-Mandanten selbst an und liest beim Start alle Mandanten fuer die Standardgruppen-Reparatur — Tenant hat keine tenantId-Spalte und traegt keinen Zeilenschutz (Aufgabe 1, tenant-tabelle-ohne-zeilenschutz-bleibt-lesbar). Fuenfter und bislang einziger bereits vollstaendig richtiger Fall der Hintergrunddienst-Falle (Befund K, siehe Abschnitt unten).
apps/api/src/user/admin-seed.service.ts user beides gemischt Klassenkorrektur (260910-das, Aufgabe 3): wechselt von bewusst-uebergreifend auf beides, weil die bisherige Begruendung ("es gibt strukturell keinen Mandanten zum Binden") nachweislich FALSCH war (Befund J) — der Mandant wird eine Anweisung vorher angelegt und ist bekannt. Die Erstanlage-Pruefung bleibt bewusst ungebunden (kein Mandant existiert zu diesem Zeitpunkt, username ist plattformweit eindeutig); die Erstanlage des Administrators selbst laeuft seit Aufgabe 2 ueber forTenant(), gebunden an den unmittelbar zuvor angelegten Mandanten. Eine P2002-Kollision beim Anlegen wird wie "Administrator existiert bereits" behandelt statt den Start abzubrechen (Befund I).
apps/api/src/user/user.controller.ts user muss-mandantengebunden gebunden Nutzerverwaltung innerhalb des Mandanten des anfragenden Admins (260910-das, Aufgabe 3): die Benutzerliste des ADMIN-Zweigs, alle drei Kennungswege (rollenabhaengig ueber UserService.findById/findByIdForPlatformAdmin) und alle fuenf Selbstbedienungszugriffe (Bild hochladen/loeschen/ausliefern, Akzentfarbe) laufen ueber forTenant(); die Rollenverzweigung zwischen mandantengebundener ADMIN-Sicht und der uebergreifenden SUPER_ADMIN-Sicht (ueber UserService.findAllForPlatformAdmin) bleibt bestehen. Der wirkungslose Selbstloesch-Riegel (Befund H, verglich gegen currentUser.sub, ein im Sitzungsnachweis nicht existierendes Feld) ist auf currentUser.id korrigiert.
apps/api/src/user/user.service.ts tenant keine-mandantengebundene-tabelle ungebunden Schleifentreiber der neuen Plattform-Administratorsicht (findAllForPlatformAdmin/findByIdForPlatformAdmin, 260910-das, Aufgabe 2, Befund F/N) — Tenant hat keine tenantId-Spalte und traegt keinen Zeilenschutz (Aufgabe 1, tenant-tabelle-ohne-zeilenschutz-bleibt-lesbar).
apps/api/src/user/user.service.ts user beides gemischt Klassenkorrektur (260910-das, Aufgabe 3): wechselt von muss-mandantengebunden auf beides wegen der einen bewusst ungebundenen Suche — wortgleich derselbe Praezedenzfall wie ldap.service.ts/user in 260909-ipc (resolveEmailForWrite). findById/create/update/deactivate/delete sowie die beiden neuen Plattform-Administratorsicht-Methoden laufen ueber forTenant(); create/update uebersetzen eine plattformweite Eindeutigkeitsverletzung (P2002) in eine deutsche Konfliktmeldung ohne Halter/Mandant zu nennen. findByUsername bleibt bewusst UNGEBUNDEN: der Anmeldeweg laeuft seit Etappe 1 ueber die drei SECURITY-DEFINER-Funktionen und hat diese Methode nicht mehr als Aufrufer (260910-das, Aufgabe 1, Teil 3: genau ein Treffer, die eigene Definition); eine gebundene Suche saehe einen fremden Halter des plattformweit eindeutigen username nicht und meldete faelschlich "frei".

Was diese Etappe NICHT entscheidet

  • Ob Controller künftig über req.tenantPrisma statt eines erneuten forTenant()-Aufrufs im Service gehen (offener Befund oben). Der Bereich ldap (260909-ipc) hat sich für den Dienst-internen Weg entschieden — forTenant(this.prisma, tenantId) wird in jeder umgestellten Methode neu erzeugt, wie es die vier Bestandsstellen in ldap.service.ts und die drei in auth.service.ts bereits vormachten. Der Bereich groups (260909-jts) hat sich für denselben dienst-internen Weg entschieden — jede Methode in groups.service.ts und module-grants.service.ts erzeugt ihren eigenen forTenant()- bzw. withTenantTransaction()-Aufruf, gebundene Clients werden nicht zwischen Methoden weitergereicht. Der Bereich dashboard (260910-krx) hat sich für denselben dienst-internen Weg entschieden — jede der neun umgestellten Methoden in dashboard.service.ts erzeugt ihren eigenen forTenant()-Aufruf, wie alle sieben Bereiche vor ihm. Der Bereich calendar (260911-cwh) hat sich für denselben dienst-internen Weg entschieden — jede der sechs umgestellten Methoden in calendar.service.ts erzeugt ihren eigenen forTenant()-Aufruf, wie alle acht Bereiche vor ihm. Die Frage bleibt für alle übrigen Bereiche der Etappe 2 offen.
  • Wie die WINDOWS-#19-Policy für SearchProvider/TenderRssFeedSource am Ende genau lautet — nur, dass sie vor dem Scharfschalten gelöst sein muss. Aufgelöst (260910-jab): TenderRssFeedSource bekommt vier nach Befehl getrennte Regeln, SearchProvider bleibt unverändert streng (widerlegte Prämisse). Siehe Abschnitt "Zwei belegte Befunde" oben. Was weiterhin offen ist: der Verwaltungsweg für plattformweite Zeilen unter der Anwendungsrolle (WINDOWS #24).
  • Die Reihenfolge und Zuschnitt der Etappe-2-Pläne — dafür ist die Klassen-Verteilung oben der Arbeitsvorrat, siehe <next_stages> im Plan 260909-eor-PLAN.md.