Aufgabe 3 der Etappe 2: elf Abfragen in sechs Methoden (listGroups, upsertMappedUser, searchUsers, importUsersByDn, importGroupsByDn, syncUsersForTenant) laufen jetzt ueber forTenant(), teils mit einem neu erzeugten, teils mit dem in derselben Methode bereits vorhandenen gebundenen Client. resolveEmailForWrite bleibt ausdruecklich ungebunden (Befund A, T-IPC-04): email/username sind plattformweit eindeutig, eine Bindung wuerde einen fremden Halter uebersehen und eine saubere Kollisionsmeldung in einen P2002-Abbruch verwandeln. Ein neuer Testblock biegt forTenant() auf ein zweites, unterscheidbares Client-Objekt um (der bisherige Identitaets-Mock haette die Umstellung nicht bemerkt, Befund F) und belegt damit, dass die Adressabfrage weiterhin am ungebundenen und der Rest am gebundenen Client landet. Alle 67 Bestandstests bleiben unveraendert gruen. docs/mandantentrennung-zugriffsklassifikation.md ist fuer den Bereich ldap geschlossen: gemessener Stand je Fundstelle, neu gerechnete Bereichsuebersicht (gebunden getrennt von ungebunden gezaehlt) und die Uebergabe des Standardgruppen-Punkts an den Bereich groups vor Etappe 4 dokumentiert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
24 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.
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 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. 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)undensureDefaultGroup(tenantId)— liegt ingroups.service.tsund ist nicht gebunden. Nach dem Scharfschalten würdereassignDefaultBeforeDeletestillfalsemelden (kein Ersatzkandidat sichtbar), der Standard-Marker wandert nicht mit, und der Mandant bliebe nach einer Gruppenlöschung ohne Standardgruppe zurück — der Bereichgroupsmuss deshalb vor Etappe 4 umgestellt sein (siehedocs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt (e), Befund D).tender-digest.scheduler.ts(Ausschreibungs-Digest): liesttenderMatch/tenderNotificationPref/userbewusst über ALLE Mandanten in einemfindMany(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/userwerden 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 | ungebunden | Gruppen sind je Mandant, tenantId-Spalte vorhanden. |
| apps/api/src/groups/groups.service.ts | groupMembership | muss-mandantengebunden | ungebunden | Kein eigenes tenantId, RLS über Join auf Group (Migration 20260618112133-Nachfolger) — braucht trotzdem forTenant(), damit der Join-Kontext gesetzt ist. |
| apps/api/src/groups/groups.service.ts | moduleGrant | muss-mandantengebunden | ungebunden | Modulfreigaben je Mandant, tenantId-Spalte vorhanden. |
| apps/api/src/groups/groups.service.ts | user | muss-mandantengebunden | ungebunden | Nutzerverwaltung innerhalb eines Mandanten. |
| 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.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. Die Frage bleibt für alle übrigen Bereiche der Etappe 2 offen. - Wie die WINDOWS-#19-Policy für
SearchProvider/TenderRssFeedSourceam 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 Plan260909-eor-PLAN.md.