Files
tessera-ctl/docs/mandantentrennung-zugriffsklassifikation.md
T
schalli 7f08b27eea feat(jts-02): groups.service.ts binden, Transaktionen tragfaehig machen, Absicherung sehend machen
prisma-tenant.extension.ts bekommt withTenantTransaction(prisma, tenantId, fn)
— die interaktive Transaktion auf dem UNgebundenen Client mit set_config als
erster Anweisung direkt auf tx, die in Aufgabe 1 als einzige der drei
gemessenen Formen sowohl die Einzelmessung als auch eine Lastprobe unter
echter Nebenlaeufigkeit bestand (die Array-Form auf dem gebundenen Client
verteilt jede Operation auf eine eigene Teiltransaktion; die interaktive Form
auf dem gebundenen Client brach unter 40 parallelen Aufrufen mit P2028 ab).

rls-access-inventory.spec.ts bekommt eine dritte Erkennung fuer
Modellzugriffe ueber den Rueckgabeparameter einer interaktiven Transaktion
(zwei Formen: direkter Empfaenger.$transaction(async...) und das neue
Hilfsmittel withTenantTransaction(...)) — macht das Paar
(groups.service.ts, tenantModuleActivation) erstmals sichtbar, das bislang
keine Pruefung dieses Projekts je gesehen hat.

groups.service.ts: alle zwoelf Methoden inklusive der drei Transaktionen
(update() isDefault:true, reassignDefaultBeforeDelete(), ensureDefaultGroup())
laufen jetzt ueber den Mandantenkontext. Zaehler und Transaktion in
ensureDefaultGroup() sind gemeinsam gebunden (T-JTS-05). addUserToDefaultGroup()
prueft neu, dass der Zielbenutzer zum Mandanten gehoert (Befund E, T-JTS-02) —
die Regel auf GroupMembership prueft nachweislich nur die Gruppenseite.

groups.service.spec.ts bekommt zwei unterscheidbare Clients ueber demselben
Speicher-Fake (Muster aus 260909-ipc, auf die interaktive Form uebertragen)
und 13 neue Bindungsnachweise; alle 42 Bestandstests bleiben gruen.
Klassifikationsdokument nachgezogen. 737 Tests und die Typpruefung gruen.

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

26 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. Beide Modelle tragen ein nullbares tenantId (SearchProvider für admin-gepflegte Vorgabe-Suchmaschinen, wobei laut 05-02-Entscheidung die tatsächlichen Vorgaben als Konstanten und nicht als DB-Zeilen mit tenantId = NULL geführt werden — die Spalte ist nullbar, ob es heute tatsächlich NULL-Zeilen gibt, ist damit eine offene Frage für Etappe 3, nicht eine hier beantwortete; TenderRssFeedSource für plattformweite RSS-Quellen wie den geseedeten service.bund.de-Feed, D-06). Die aktuelle Policy "tenantId" = current_tenant_id() vergleicht NULL nie gleich — nach dem Scharfschalten wären plattformweite Zeilen für JEDEN Mandanten unsichtbar, nicht nur für fremde. Das ist heute ohne Wirkung (Schalter aus, #18), muss aber in Etappe 3 zusammen mit den restlichen Fundstellen gelöst werden: die Policy braucht für den Lesezugriff tenantId IS NULL OR tenantId = current_tenant_id(), während Schreibzugriffe weiterhin einen Mandanten verlangen.

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

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

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

Bereich Ungebunden Gebunden Hinweis
tenders 62 0 unverändert
groups 37 0 unverändert
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 21 0 unverändert
user 17 0 unverändert
module-registry 17 0 unverändert
dashboard 13 0 unverändert
auth 8 5 unverändert gegenüber dem in Etappe 1 (260909-eor) gemessenen Stand
calendar 12 0 unverändert
tenant 8 0 unverändert
favorites 7 0 unverändert
settings 4 0 unverändert
Summe 210 31 Ungebunden: war 227 vor dieser Etappe (260909-eor-Stand), Delta = die 17 in Aufgabe 2/3 umgestellten ldap-Rohtreffer. Gebunden: war 5 (nur auth), jetzt zusätzlich 26 in ldap

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

Stand 260909-ipc (Aufgabe 2): 59 Paare aus der urspruenglichen Zaehlung plus zwei bisher unentdeckte, weil bereits gebundene Fundstellen (auth.service.ts/passwordResetToken, ldap.service.ts/groupMembership), die erst die um gebundene Zugriffe erweiterte Erkennung (Befund G) sichtbar macht — sie waren nie Teil der 227 this.prisma.*-Rohtrefferzahl, weil sie schon vor diesem Plan über forTenant() liefen. Dazu die Korrektur von (ldap-config.service.ts, ldapConfig) von muss-mandantengebunden auf beides (Befund B).

Klasse Anzahl Paare
muss-mandantengebunden 32
keine-mandantengebundene-tabelle 16
beides 10
bewusst-uebergreifend 3
Summe 61

Der Hintergrunddienst als Falle — drei beides-Fälle

Ein Planer, der über alle Mandanten iteriert, liest zu Recht übergreifend — muss aber innerhalb der Schleife je Mandant binden. Drei Dateien sind betroffen:

  • 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. Offen bleibt eine Reihenfolgebedingung für Etappe 4, NICHT Teil dieser Umstellung: die Übergabe unmittelbar vor der Löschung — this.groupsService.reassignDefaultBeforeDelete(tenantId, group.id) und ensureDefaultGroup(tenantId) — liegt in groups.service.ts und ist nicht gebunden. Nach dem Scharfschalten würde reassignDefaultBeforeDelete still false melden (kein Ersatzkandidat sichtbar), der Standard-Marker wandert nicht mit, und der Mandant bliebe nach einer Gruppenlöschung ohne Standardgruppe zurück — der Bereich groups muss deshalb vor Etappe 4 umgestellt sein (siehe docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt (e), Befund D).
  • tender-digest.scheduler.ts (Ausschreibungs-Digest): liest tenderMatch/tenderNotificationPref/user bewusst über ALLE Mandanten in einem findMany (ein einziger globaler Cron-Job, kein Mandant im Job selbst — so von Anfang an entworfen, Pitfall 1 in den Kommentaren der Datei). Innerhalb der Verteilung je Treffer ist der Mandant aus der Zeile bekannt und der Versand muss darauf gebunden laufen.
  • tender-matching.service.ts (Ausschreibungs-Sofortmeldung): dieselbe Form — tenderMatch/tenderSavedSearch/user werden für die Sofort-Benachrichtigung über alle betroffenen Mandanten hinweg gelesen, der Versand je Treffer ist mandantengebunden.

Bestandsaufnahme

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

Datei Modell Klasse Stand Begründung
apps/api/src/auth/auth.service.ts passwordResetToken muss-mandantengebunden gebunden Kein eigenes tenantId, RLS ueber Join auf User (Migration 20260618112133). requestPasswordReset/resetPassword laufen vollstaendig ueber forTenant() (Etappe 1, WINDOWS #20, Aufgabe 1) — von der alten, nur this.prisma.* erkennenden Suche nie erfasst, weil bereits gebunden; die erweiterte Erkennung aus Aufgabe 2 (260909-ipc) macht diese Fundstelle erstmals sichtbar.
apps/api/src/auth/auth.service.ts user muss-mandantengebunden gemischt getMe, changePassword, adminResetPassword suchen über die Benutzerkennung aus dem Sitzungsnachweis — der Mandant ist dort bereits bekannt (Aufgabe 2 fasst sie bewusst nicht an, siehe SUMMARY).
apps/api/src/calendar/calendar.service.ts calendarSource muss-mandantengebunden ungebunden Kalenderquellen eines Nutzers, tenantId-Spalte vorhanden.
apps/api/src/dashboard/dashboard.service.ts dashboardLayout muss-mandantengebunden ungebunden Widget-Anordnung eines Nutzers, tenantId-Spalte vorhanden.
apps/api/src/dashboard/dashboard.service.ts module keine-mandantengebundene-tabelle ungebunden Modulkatalog ist plattformweit, kein tenantId (Migration 20260909140000, Gruppe b).
apps/api/src/dashboard/dashboard.service.ts searchProvider muss-mandantengebunden ungebunden tenantId nullbar (WINDOWS #19) — heutige, tatsächlich gespeicherte Zeilen sind nutzerangelegt und tragen einen Mandanten; Vorgabe-Anbieter kommen laut 05-02 aus Konstanten, nicht aus der DB.
apps/api/src/dashboard/dashboard.service.ts widgetInstance muss-mandantengebunden ungebunden Platzierte Dashboard-Widgets eines Nutzers, tenantId-Spalte vorhanden.
apps/api/src/dkv/dkv.service.ts dkvInvoiceHistory muss-mandantengebunden ungebunden DKV-Rechnungshistorie je Mandant, tenantId-Spalte vorhanden.
apps/api/src/dkv/dkv.service.ts dkvModuleConfig muss-mandantengebunden ungebunden Postfach-/Zugangsdaten des DKV-Moduls je Mandant.
apps/api/src/dkv/dkv.service.ts dkvVehicleMaster muss-mandantengebunden ungebunden Fahrzeugstammdaten des DKV-Moduls je Mandant.
apps/api/src/favorites/favorites.service.ts favoriteLink muss-mandantengebunden ungebunden Favoriten-Links eines Nutzers, tenantId-Spalte vorhanden.
apps/api/src/groups/groups.service.ts group muss-mandantengebunden gebunden Gruppen sind je Mandant, tenantId-Spalte vorhanden. Alle 12 Methoden laufen seit 260909-jts (Aufgabe 2) ueber forTenant() bzw. withTenantTransaction().
apps/api/src/groups/groups.service.ts groupMembership muss-mandantengebunden gebunden Kein eigenes tenantId, RLS über Join auf Group (Migration 20260618112133-Nachfolger) — braucht trotzdem forTenant(), damit der Join-Kontext gesetzt ist. Seit 260909-jts gebunden, einschliesslich der drei Zugriffe innerhalb des Standardgruppen-Aufbaus (ensureDefaultGroup), die zuvor ueber den Transaktionsparameter liefen und fuer keine Pruefung dieses Projekts sichtbar waren (Befund B).
apps/api/src/groups/groups.service.ts moduleGrant muss-mandantengebunden gebunden Modulfreigaben je Mandant, tenantId-Spalte vorhanden. Seit 260909-jts gebunden, einschliesslich des Zugriffs innerhalb des Standardgruppen-Aufbaus (Befund B).
apps/api/src/groups/groups.service.ts tenantModuleActivation muss-mandantengebunden gebunden Welche Module ein Mandant aktiviert hat. Bis 260909-jts vollstaendig unsichtbar (Befund B, 260909-jts-PLAN.md): der einzige Zugriff dieser Datei lief innerhalb der interaktiven Transaktion von ensureDefaultGroup ueber den Rueckgabeparameter (tx.tenantModuleActivation.findMany) — weder this.prisma.<Modell> noch <gebundener Client>.<Modell> sahen das, weil tx weder this.prisma noch aus einer forTenant(-Zuweisung stammte. Die um Transaktionsparameter erweiterte Erkennung aus Aufgabe 2 macht dieses Paar erstmals sichtbar; die Stelle ist seit derselben Aufgabe gebunden (ueber withTenantTransaction()).
apps/api/src/groups/groups.service.ts user muss-mandantengebunden gebunden Nutzerverwaltung innerhalb eines Mandanten. Seit 260909-jts gebunden; addUserToDefaultGroup prueft seither zusaetzlich, dass der Zielbenutzer zum Mandanten gehoert (Befund E, T-JTS-02) — die Regel auf GroupMembership prueft nachweislich nur die Gruppenseite.
apps/api/src/groups/module-grants.service.ts group muss-mandantengebunden ungebunden Wie groups.service.ts.
apps/api/src/groups/module-grants.service.ts groupMembership muss-mandantengebunden ungebunden Kein eigenes tenantId, RLS über Join auf Group.
apps/api/src/groups/module-grants.service.ts moduleGrant muss-mandantengebunden ungebunden Modulfreigaben je Mandant.
apps/api/src/groups/module-grants.service.ts tenantModuleActivation muss-mandantengebunden ungebunden Welche Module ein Mandant aktiviert hat, tenantId-Spalte vorhanden.
apps/api/src/groups/module-grants.service.ts user muss-mandantengebunden ungebunden Zielbenutzer eines Grants innerhalb des Mandanten.
apps/api/src/ldap/ldap-config.service.ts ldapConfig beides gemischt getConfig, createConfig und updateConfig laufen ueber forTenant(), gebunden an den aus der Anfrage bereits bekannten Mandanten. getAllActiveConfigs() (Planer-Lesezugriff ueber ALLE Mandanten) und die Start-Nachverschluesselung in onApplicationBootstrap bleiben bewusst uebergreifend: beide laufen, bevor bzw. unabhaengig davon, ob ein einzelner Mandantenkontext feststeht (Befund B, 260909-ipc-PLAN.md). Korrektur der Klasse von muss-mandantengebunden: das Paar ist tatsaechlich beides, keine Verhaltensaenderung.
apps/api/src/ldap/ldap-config.service.ts ldapFieldMapping muss-mandantengebunden gebunden Kein eigenes tenantId, RLS ueber Join auf LdapConfig. addFieldMapping und removeFieldMapping nehmen den Mandanten jetzt als Parameter entgegen und laufen vollstaendig ueber forTenant() (T-IPC-01, 260909-ipc-PLAN.md) — schliesst zugleich die Fremdzugriffsluecke beim Loeschen einer Feldzuordnung ueber ihre Kennung.
apps/api/src/ldap/ldap.service.ts group beides gebunden AD-Abgleich: listGroups (die "bereits importiert"-Markierung) und importGroupsByDn (die Idempotenzpruefung ueber ldapObjectGuid) sind mit Aufgabe 3 (260909-ipc) auf forTenant() umgestellt — zusammen mit den bereits vorher gebundenen Stellen (Anlage, Mitgliedschafts- und Gruppenabgleich) ist damit jeder group-Zugriff dieser Datei gebunden. Die Klasse bleibt beides, weil ein zukuenftiger uebergreifender Lesezugriff (z. B. ein neuer Planer-Pfad) hier ebenso legitim waere wie bei ldapConfig unten — nicht, weil heute noch ein ungebundener Zugriff bestuende.
apps/api/src/ldap/ldap.service.ts groupMembership muss-mandantengebunden gebunden Kein eigenes tenantId, RLS ueber Join auf Group. syncGroupMembershipsForTenant laeuft vollstaendig ueber forTenant() (Plan 16-03/16-05) — von der alten, nur this.prisma.* erkennenden Suche nie erfasst, weil bereits gebunden; die erweiterte Erkennung aus Aufgabe 2 (260909-ipc) macht diese Fundstelle erstmals sichtbar.
apps/api/src/ldap/ldap.service.ts ldapConfig beides gebunden Die lastSyncAt-Fortschreibung am Ende von syncUsersForTenant ist mit Aufgabe 3 (260909-ipc) auf den in derselben Methode bereits vorhandenen forTenant()-Client umgestellt — es entsteht kein zweiter. Damit ist der einzige ldapConfig-Zugriff dieser Datei gebunden.
apps/api/src/ldap/ldap.service.ts user beides gemischt Mit Aufgabe 3 (260909-ipc) sind upsertMappedUser (Identitaetssuche und Aktualisierung), searchUsers (die "bereits importiert"-Markierung), importUsersByDn (Dedup und ldapDn-Nachtrag) und die Deaktivierungsschleife in syncUsersForTenant auf forTenant() umgestellt. resolveEmailForWrite bleibt ausdruecklich UNGEBUNDEN (Befund A, T-IPC-04): email/username sind plattformweit eindeutig, eine mandantengebundene Suche saehe einen fremden Halter nicht mehr und meldete faelschlich "frei" — die geloeste Klasse waere muss-mandantengebunden gewesen, bleibt wegen dieser einen bewusst uebergreifenden Abfrage beides. Der Loeschzweig um syncBoundGroupsForTenant (WINDOWS #20) ist bereits seit Etappe 1 gebunden und war nie Teil dieses Befunds.
apps/api/src/module-registry/module-access.service.ts module keine-mandantengebundene-tabelle ungebunden Modulkatalog ist plattformweit, kein tenantId.
apps/api/src/module-registry/module-access.service.ts moduleGrant muss-mandantengebunden ungebunden Modulfreigaben je Mandant.
apps/api/src/module-registry/module-access.service.ts tenantModuleActivation muss-mandantengebunden ungebunden Aktivierung je Mandant, tenantId-Spalte vorhanden.
apps/api/src/module-registry/module-registry.service.ts module keine-mandantengebundene-tabelle ungebunden Modulkatalog ist plattformweit.
apps/api/src/module-registry/module-registry.service.ts tenantModuleActivation muss-mandantengebunden ungebunden Aktivierung je Mandant.
apps/api/src/settings/settings.service.ts smtpConfig muss-mandantengebunden ungebunden SMTP-Zugangsdaten je Mandant, tenantId-Spalte vorhanden.
apps/api/src/tenant/tenant.controller.ts tenant keine-mandantengebundene-tabelle ungebunden Tenant ist die Mandantentabelle selbst — hat keine eigene tenantId-Spalte, kann sie per Definition nicht haben (Migration 20260909140000, Gruppe b).
apps/api/src/tenant/tenant.service.ts tenant keine-mandantengebundene-tabelle ungebunden Dieselbe Begründung.
apps/api/src/tenders/adapters/email-alert.adapter.ts tenderEmailConfig bewusst-uebergreifend ungebunden fetchTenders() liest bewusst jede aktive TenderEmailConfig-Zeile über ALLE Mandanten in einer Abfrage (Plattform-Scheduler, ein Tick pro Postfach, D-13/D-01) — ausführlich im Dateikopf begründet, darf laut Kommentar niemals in forTenant() verpackt werden.
apps/api/src/tenders/adapters/rss.adapter.ts tenderRssFeedSource bewusst-uebergreifend ungebunden Fan-out über jeden aktiven Feed, plattformweit UND persönlich, in einer Abfrage (Zeilen 55–83 im Dateikopf begründet) — dieselbe Scheduler-Ebene wie beim E-Mail-Adapter.
apps/api/src/tenders/tender-dedup.service.ts tender keine-mandantengebundene-tabelle ungebunden Explizit im Dateikopf: "platform-global, RLS-exempt tables. Never wrap these queries in forTenant()." (D-03)
apps/api/src/tenders/tender-dedup.service.ts tenderSource keine-mandantengebundene-tabelle ungebunden Dieselbe Begründung.
apps/api/src/tenders/tender-digest.scheduler.ts tenderMatch beides ungebunden Ein einziger globaler Cron-Job liest über ALLE Mandanten (bewusst übergreifend, Pitfall-1-Kommentar im Dateikopf), der Versand je Treffer ist an dessen Mandanten gebunden.
apps/api/src/tenders/tender-digest.scheduler.ts tenderNotificationPref beides ungebunden Dieselbe Begründung — Präferenzen werden über alle Mandanten gelesen, aber je Zeile mandantenbezogen ausgewertet.
apps/api/src/tenders/tender-digest.scheduler.ts user beides ungebunden E-Mail-Adressen für den Versand werden über alle Mandanten gelesen, der eigentliche Versand ist je Treffer mandantengebunden.
apps/api/src/tenders/tender-email-config.service.ts tenderEmailConfig muss-mandantengebunden ungebunden 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.
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 ungebunden Sofortmeldung: Treffer über alle betroffenen Mandanten gelesen, Versand je Treffer mandantengebunden — siehe Abschnitt "Der Hintergrunddienst als Falle".
apps/api/src/tenders/tender-matching.service.ts tenderSavedSearch beides ungebunden Dieselbe Begründung — gespeicherte Suchprofile aller Mandanten werden gegen neue Treffer geprüft, der Versand ist je Profil mandantengebunden.
apps/api/src/tenders/tender-matching.service.ts user beides ungebunden Dieselbe Begründung — E-Mail-Adressen für die Sofortmeldung.
apps/api/src/tenders/tender-notification-pref.service.ts tenderNotificationPref muss-mandantengebunden ungebunden Nutzer-CRUD für die eigenen Benachrichtigungseinstellungen — Mandant aus der Anfrage bekannt.
apps/api/src/tenders/tender-rss-feed.service.ts tenderRssFeedSource muss-mandantengebunden ungebunden Nutzer-CRUD für die eigenen RSS-Quellen (anders als der Fan-out in adapters/rss.adapter.ts) — Mandant aus der Anfrage bekannt. WINDOWS #19 betrifft die nullbaren plattformweiten Zeilen, nicht diesen CRUD-Pfad.
apps/api/src/tenders/tender-saved-search.service.ts tenderSavedSearch muss-mandantengebunden ungebunden Nutzer-CRUD für die eigenen gespeicherten Suchprofile — Mandant aus der Anfrage bekannt.
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 ungebunden Favorisierungs-/Ablehnungsstatus eines Nutzers, tenantId-Spalte vorhanden.
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 — Tenant hat keine tenantId-Spalte.
apps/api/src/user/admin-seed.service.ts user bewusst-uebergreifend ungebunden Erstanlage des Administrators beim ersten Start: läuft einmalig beim Boot, BEVOR irgendein Mandantenkontext existiert, um den allerersten Mandanten samt Admin-Nutzer anzulegen — es gibt zu diesem Zeitpunkt strukturell keinen Mandanten, an den gebunden werden könnte.
apps/api/src/user/user.controller.ts user muss-mandantengebunden ungebunden Nutzerverwaltung innerhalb des Mandanten des anfragenden Admins.
apps/api/src/user/user.service.ts user muss-mandantengebunden ungebunden Dieselbe Begründung.

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. Die Frage bleibt für alle übrigen Bereiche der Etappe 2 offen.
  • Wie die WINDOWS-#19-Policy für SearchProvider/TenderRssFeedSource am Ende genau lautet — nur, dass sie vor dem Scharfschalten gelöst sein muss.
  • Die Reihenfolge und Zuschnitt der Etappe-2-Pläne — dafür ist die Klassen-Verteilung oben der Arbeitsvorrat, siehe <next_stages> im Plan 260909-eor-PLAN.md.