diff --git a/.planning/WINDOWS.md b/.planning/WINDOWS.md index 5ac5f80..ff7eecd 100644 --- a/.planning/WINDOWS.md +++ b/.planning/WINDOWS.md @@ -2,9 +2,9 @@ schema_version: 1 open_count: 14 waived_count: 1 -fixed_count: 17 -total_count: 32 -last_updated: 2026-09-11T11:57:50.484Z +fixed_count: 18 +total_count: 33 +last_updated: 2026-09-11T14:48:15.447Z --- # Broken Windows Ledger @@ -41,12 +41,13 @@ last_updated: 2026-09-11T11:57:50.484Z | 24 | quick-260910-jab | deviation | apps/api/src/tenders/tender-rss-feed.service.ts | | Was das Schliessen von WINDOWS #19 NICHT loest: unter der Anwendungsrolle laesst sich eine plattformweite RSS-Quelle (TenderRssFeedSource, tenantId NULL) weder anlegen noch entfernen — in der alten wie in der neuen Regel, weil jede Schreibregel (Einfuegen/Aendern/Entfernen) ausdruecklich einen Mandanten verlangt (tenant_insert_policy/tenant_update_policy/tenant_delete_policy, 20260910120000_rls_widen_membership_grant_and_platform_read). Betroffen sind zwei Pfade in TenderRssFeedSourceService: createPlatform() (setzt tenantId=NULL, ein gebundenes INSERT liefe in die WITH-CHECK-Klausel und wuerde abgewiesen) und remove() (deckt fuer Administratoren auch das Entfernen einer plattformweiten Zeile ab; ein gebundenes DELETE traefe sie nie). Beide bleiben deshalb bewusst ungebunden — das ist KEINE Folge dieser Reparatur, sondern bestand bereits vor 260910-jab identisch, weil die vom Ledger vorgegebene #19-Semantik Schreibzugriffe ausdruecklich an einen Mandanten bindet. Vorabpruefung fuer Etappe 4 (rls-preflight.mjs): ein Verwaltungsweg fuer plattformweite Zeilen (Anlegen/Entfernen unter der Anwendungsrolle) muss gebaut werden, BEVOR die Rolle umgeschaltet wird — sonst kann kein Administrator nach dem Scharfschalten mehr eine plattformweite Quelle pflegen. Eigener Eintrag, damit dieser Rest nicht mit #19 verschwindet. | open | | 2026-09-10T12:35:40.000Z | | | 25 | quick-260910-krx | deviation | apps/web/src/lib/stores/dashboard-store.ts | | Die beweisvernichtende Auspraegung der umgekehrten Fehlerrichtung im Bereich dashboard: ein nach dem Scharfschalten (WINDOWS #18) zu klein gebliebenes Leseergebnis auf getLayout sieht nicht wie ein Fehler aus, sondern wie eine leere Vorgabeanordnung. Drei Stellen greifen ineinander (docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich dashboard', (w3)): (1) DashboardService.getLayout liefert bei fehlendem Datensatz {lg:[],md:[],sm:[],xs:[],xxs:[]} statt eines Fehlers; (2) apps/web/src/lib/stores/dashboard-store.ts, loadDashboard setzt layouts/widgets ungeprueft auf das Ergebnis, der catch-Zweig feuert nur bei Netzwerk-/Statusfehlern, nicht bei einer erfolgreichen leeren Antwort; (3) dieselbe Datei, setEditMode(false) schreibt bei isDirty automatisch zurueck, sobald der Bearbeitungsmodus verlassen wird — ohne dass der Nutzer auf Speichern klickt. Die Folge: der Nutzer haelt ein leeres Dashboard fuer einen Fehler des Widget-Systems oder fuer verlorene Einstellungen ('das Widget-System spinnt', 'meine Einstellungen sind weg'), baut seine Anordnung neu auf (addWidget legt echte neue WidgetInstance-Zeilen an, keine Eindeutigkeitsbedingung ueber (userId, widgetType), Dubletten haeufen sich bei wiederholtem Neuaufbau an), und das automatische Zurueckschreiben ueberschreibt die layouts-Spalte der urspruenglichen Zeile — die einzige Aufzeichnung der urspruenglichen Anordnung ist verloren, bevor irgendjemand die Ursache untersuchen konnte. Zusaetzlich, kleiner: apps/web/src/components/dashboard/widgets/search-widget.tsx laesst bei einem zu kleinen custom-Ergebnis die eigenen Suchmaschinen des Nutzers aus der Auswahlliste verschwinden (der Rueckfallzweig auf DEFAULT_PROVIDERS feuert nie, weil getSearchProviders die drei Vorgaben immer voranstellt), und handleSearch faellt bei unbekannter Auswahl auf providers[0] (Google) zurueck — eine fuer ein internes Werkzeug gedachte Suchanfrage ginge dann an eine externe Suchmaschine. Konkrete Vorabpruefung fuer Etappe 4 (rls-preflight.mjs): eine physisch vorhandene DashboardLayout-Zeile fuer einen bekannten Benutzer, aber der gebundene Lesezugriff fuer dessen Mandanten liefert null — das unterscheidet den echten Erstbenutzer-Fall vom Trennungsfehler. An dieselbe Bedingung gebunden wie #18 — beobachtbar erst nach dem Scharfschalten. Das Frontend wird von 260910-krx NICHT geaendert, dieser Eintrag beschreibt es nur. Die verwandte, strukturelle Eindeutigkeitsfrage von DashboardLayout.userId (plattformweit @unique ohne Mandantenanteil) ist derselbe Fall wie WINDOWS #22 im Bereich user — dort mitgefuehrt, kein zweiter Eintrag hier. | open | | 2026-09-11T09:01:00.000Z | | | 26 | quick-260911-cwh | deviation | apps/web/src/components/dashboard/widgets/calendar-widget.tsx | | Bereich calendar: zu kleines Leseergebnis auf getSources/fetchAndCacheEvents sieht aus wie 'keine Quelle eingerichtet' bzw. 'keine Termine' (calendar-widget.tsx, calendar-settings-panel.tsx); das Frontend verschluckt zusaetzlich LAUTE Fehler derselben Pfade in denselben leeren Zustand (calendar-widget.tsx catch->setEvents([]), calendar-settings-panel.tsx .catch(()=>{}) auf fetchSources); der Nutzer legt seine Quelle neu an und tippt Exchange-/CalDAV-Zugangsdaten ein zweites Mal in ein scheinbar defektes System ein, die urspruengliche Zeile bleibt unsichtbar liegen und wird nach Behebung zur Dublette; Vorabpruefung fuer Etappe 4: physisch vorhandene CalendarSource-Zeilen je Mandant ueber die Wartungsrolle zaehlen und mit der gebundenen Zaehlung vergleichen (docs/mandantentrennung-etappe2-fehlerrichtung.md (k4)(e)); an dieselbe Bedingung gebunden wie WINDOWS #18; Familie mit #23 (module-registry) und #25 (dashboard); das Frontend wird von 260911-cwh NICHT geaendert. | open | | 2026-09-11T07:57:36.769Z | | -| 27 | 2 | unmet-truth | apps/api/src/prisma/rls-access-inventory.spec.ts | | Die maschinelle Bestandsaufnahme (rls-access-inventory.spec.ts) ist fuer Relationszugriffe strukturell blind. Sie erkennt nur direkte Zugriffe der Form this.prisma. bzw. .. Ein Zugriff, der ueber include:/_count:/select: in eine ZWEITE Tabelle hineinreicht, ist fuer sie unsichtbar — obwohl Prisma daraus eine Unterabfrage auf diese zweite Tabelle macht, die unter DEREN Regel laeuft. Nachgewiesen in 260911-e2s: drei Zugriffe in tenant.controller.ts zaehlten ueber include: { _count: { select: { users } } } in die geschuetzte Tabelle User hinein (Prisma 6.19 rendert das als LEFT JOIN (SELECT tenantId, COUNT(*) FROM User ...)); nach dem Scharfschalten haette die Mandantenliste des Plattform-Administrators fuer jeden Mandanten 0 Benutzer gezeigt und der Loeschriegel T-02-09 waere vakuum geworden. Diese drei Stellen sind behoben (Fan-out je Mandant ueber gebundenen Client). Zur Planungszeit wurden alle 19 include:-Stellen und alle _count-Stellen in apps/api/src einzeln beurteilt, vom Orchestrator und vom Verifizierer unabhaengig gegengeprueft: nur diese drei waren gefaehrlich (tenders zaehlt auf dem plattformglobalen Katalog ohne Zeilenschutz, groups zaehlt ueber einen bereits gebundenen Client in eine Tabelle desselben Mandanten). OFFEN bleibt der MECHANISMUS: jede kuenftige include:/_count:-Stelle in eine fremd geschuetzte Tabelle bleibt fuer die Pruefung unsichtbar. Zu schliessen, indem der Detektor include:/select:/_count:-Bloecke auf Modellnamen durchsucht und die Zieltabelle als eigene Fundstelle fuehrt — oder durch eine Pruefung, die jede include:-Stelle einer expliziten Freigabeliste unterwirft. Gehoert vor das Scharfschalten (Etappe 4), weil die Vorabpruefung sich sonst auf eine Bestandsaufnahme stuetzt, die diese Form nicht sieht. | open | | 2026-09-11T09:08:00.435Z | | +| 27 | 2 | unmet-truth | apps/api/src/prisma/rls-access-inventory.spec.ts | | Die maschinelle Bestandsaufnahme (rls-access-inventory.spec.ts) ist fuer Relationszugriffe strukturell blind. Sie erkennt nur direkte Zugriffe der Form this.prisma. bzw. .. Ein Zugriff, der ueber include:/_count:/select: in eine ZWEITE Tabelle hineinreicht, ist fuer sie unsichtbar — obwohl Prisma daraus eine Unterabfrage auf diese zweite Tabelle macht, die unter DEREN Regel laeuft. Nachgewiesen in 260911-e2s: drei Zugriffe in tenant.controller.ts zaehlten ueber include: { _count: { select: { users } } } in die geschuetzte Tabelle User hinein (Prisma 6.19 rendert das als LEFT JOIN (SELECT tenantId, COUNT(*) FROM User ...)); nach dem Scharfschalten haette die Mandantenliste des Plattform-Administrators fuer jeden Mandanten 0 Benutzer gezeigt und der Loeschriegel T-02-09 waere vakuum geworden. Diese drei Stellen sind behoben (Fan-out je Mandant ueber gebundenen Client). Zur Planungszeit wurden alle 19 include:-Stellen und alle _count-Stellen in apps/api/src einzeln beurteilt, vom Orchestrator und vom Verifizierer unabhaengig gegengeprueft: nur diese drei waren gefaehrlich (tenders zaehlt auf dem plattformglobalen Katalog ohne Zeilenschutz, groups zaehlt ueber einen bereits gebundenen Client in eine Tabelle desselben Mandanten). OFFEN bleibt der MECHANISMUS: jede kuenftige include:/_count:-Stelle in eine fremd geschuetzte Tabelle bleibt fuer die Pruefung unsichtbar. Zu schliessen, indem der Detektor include:/select:/_count:-Bloecke auf Modellnamen durchsucht und die Zieltabelle als eigene Fundstelle fuehrt — oder durch eine Pruefung, die jede include:-Stelle einer expliziten Freigabeliste unterwirft. Gehoert vor das Scharfschalten (Etappe 4), weil die Vorabpruefung sich sonst auf eine Bestandsaufnahme stuetzt, die diese Form nicht sieht. | fixed | | 2026-09-11T09:08:00.435Z | 2026-09-11T14:48:15.447Z | | 28 | quick-260911-fh9 | deviation | apps/web/src/components/layout/header.tsx | | Bereich auth: getMe liefert nach dem Scharfschalten null, der Controller antwortet 200 mit leerem Rumpf, fetchCurrentUser (auth-actions.ts) macht daraus null, header.tsx und account-settings-form.tsx tun bei null nichts — die Portalhuelle rendert ohne angemeldeten Benutzer; changePassword liest sich als networkError (nicht als falsches Kennwort); adminResetPassword als 'User not found' ohne UI-Aufrufer. 'nicht angemeldet' und 'Zeile unsichtbar' sind fuer das Frontend derselbe Wert — an dieselbe Bedingung gebunden wie WINDOWS #18; Familie #23/#25/#26; Etappe-4-Vorabpruefung: bekannten Benutzer ueber die Wartungsrolle lesen und den gebundenen findUnique unter seinem Claim-Mandanten daneben halten. Das Frontend wird von 260911-fh9 NICHT geaendert. | open | | 2026-09-11T10:00:29.558Z | | | 29 | quick-260911-fh9 | unmet-truth | apps/api/src/user/user.controller.ts | | Bereich auth: adminResetPassword schliesst die Rechteausweitung ADMIN -> SUPER_ADMIN im eigenen Handler (T-FH9-04), der Schwesterweg PATCH /users/:id tut das nicht. UserController.update (T-02-08) prueft nur, ob die Rolle SUPER_ADMIN NEU ZUGEWIESEN wird (dto.role === Role.SUPER_ADMIN), nicht ob das ZIEL diese Rolle bereits HAT — password/isActive gehen fuer ein bestehendes SUPER_ADMIN-Ziel ungeprueft durch; remove prueft ueberhaupt keine Rolle des Ziels, nur Selbstloeschung und Mandantengrenze. Kein Mandantenproblem, sondern Rechteausweitung INNERHALB des Mandanten. Reparatur in einem Satz: die Rolle des ZIELS pruefen (ist user.role === Role.SUPER_ADMIN und der Aufrufer nicht SUPER_ADMIN, ForbiddenException) — die Vorlage steht seit 260911-fh9 in AuthService.adminResetPassword. Ausserhalb der Erlaubnisliste dieses Plans, deshalb Ledger statt Reparatur; vor dem ersten Mandanten mit einem zweiten Administrator zu schliessen. | open | | 2026-09-11T10:00:38.418Z | | | 30 | quick-260911-gwh | deviation | apps/api/src/mail/mail.module.ts | | Startpfad des Mailmoduls (SettingsService.loadAnySmtpConfigForStartupTransport(), vormals getStartupSmtpConfig()) als SECHSTER Fall der Hintergrunddienst-Falle bleibt bewusst UNGEBUNDEN. HEUTE bereits falsch: findFirst() ohne Bedingung zieht bei mehreren Mandanten den SMTP-Server und Absender EINES beliebigen Mandanten fuer ALLE Kennwort-Zuruecksetzungs- und Willkommensmails ALLER Mandanten (T-GWH-03, Nutzung fremder Zugangsdaten). NACH DEM SCHARFSCHALTEN (#18) liefert dieselbe Abfrage null, mail.module.ts faellt auf MAIL_*/TESSERA_SMTP_*/localhost:1025 zurueck, MailService faengt den Transportfehler (T-02-12) -- KEINE Protokollzeile, das Verstummen ist doppelt verdeckt (Unsymmetrie zu ldap.getAllActiveConfigs [heute korrekt] UND zu dkv WINDOWS #21 [verstummt mit Protokollzeile]). Drei erwogene Formen: an einen aufgeloesten Mandanten binden (unmoeglich, kein Kontext beim Start); Mehrmandanten-Versand (abgelehnt als Funktion -- Vorlage steht in DkvMailService/TenderMailService, Transport je Versand aus getDecryptedSmtpConfig(tenantId)); als benannte Altlast weiterfuehren mit Markierung (GEWAEHLT). Eigener Eintrag statt Anschluss an #21: andere Datei, andere Reparatur, andere Verdeckungsform. Signal fuer rls-preflight.mjs gehoert in Etappe 4. Siehe docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich settings' (s4)(a). | open | | 2026-09-11T11:57:36.656Z | | | 31 | quick-260911-gwh | deviation | apps/web/src/components/dashboard/widgets/favorites-widget.tsx | | Bereich favorites: ein nach dem Scharfschalten (#18) zu klein gebliebenes Leseergebnis auf list() sieht aus wie 'Noch keine Favoriten.' (favorites-widget.tsx Zeile um 212, de.json favorites.empty) -- fetchFavorites (favorites-api.ts) reicht die leere Liste durch. 'Nie einen gespeichert' und 'Zeile unsichtbar' sind fuer das Frontend derselbe Wert []. Etappe-4-Vorabpruefung (f4)(d): fuer einen bekannten Nutzer/Widget die Favoritenzahl ueber die Wartungsrolle und ueber den gebundenen findMany daneben halten. Familie #23/#25/#26/#28. Das Frontend wird von 260911-gwh NICHT geaendert. Siehe docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich favorites' (f3)/(f4). | open | | 2026-09-11T11:57:50.276Z | | | 32 | quick-260911-gwh | deviation | apps/web/src/components/settings/smtp-settings-form.tsx | | Bereich settings: getSmtpConfig liefert nach dem Scharfschalten (#18) null, der Controller antwortet 200 mit leerem Rumpf, fetchSmtp (settings-api.ts) laeuft mit res.json() auf den leeren Rumpf und wirft, smtp-settings-form.tsx verschluckt das in .catch(() => {}) -- leeres Formular 'nicht eingerichtet', waehrend die Zugangsdaten physisch da sind. Ein erneutes Speichern unter der ungebundenen Form scheitert am Eindeutigkeitsindex SmtpConfig_tenantId_key (PrismaClientUnknownRequestError, gemessen in Aufgabe 1 Pruefung 8) -- nach diesem Lauf ist saveSmtpConfig gebunden und trifft die eigene Zeile, dieser Rest bestand nur unter der ungebundenen Form vor dieser Aenderung. Dieselbe 200-leerer-Rumpf-Kette wie #28. Etappe-4-Vorabpruefung (s4)(e). Das Frontend wird von 260911-gwh NICHT geaendert. Siehe docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich settings' (s3)/(s4). | open | | 2026-09-11T11:57:50.484Z | | +| 33 | quick-260911-mkj | unmet-truth | apps/api/src/tenders/tenders.seed.ts | | Modellaufrufe auf Empfaengern, die weder this.prisma noch eine const X = forTenant(-Zuweisung noch ein Transaktionsparameter sind, sind fuer ALLE vier Erkennungsformen der Bestandsaufnahme unsichtbar. Gemessen 260911-mkj: tenders/tenders.seed.ts (Funktionsparameter prisma: PrismaService, tenderRssFeedSource.findFirst/create, kein Eintrag in der Bestandsaufnahme) und tenders/backfill-tender-source.ts (eigenstaendiges Skript mit new PrismaClient(), tender.findMany/update, durch RELATION_SPEC_EXCEPTIONS laut gehalten). Beide beruehren nur den plattformglobalen Katalog bzw. die plattformweite RSS-Verwaltung (WINDOWS #24), heute ungefaehrlich; OFFEN ist der Mechanismus (ein kuenftiger Dienst mit Parameter-Empfaenger auf einer geschuetzten Tabelle bliebe unsichtbar). Zu schliessen vor Etappe 4 durch eine Zaehlung ALLER ..(-Anker gegen die bekannte Empfaengermenge, Ueberschuss laut. | open | | 2026-09-11T14:48:09.723Z | | ````json [ @@ -369,10 +370,10 @@ last_updated: 2026-09-11T11:57:50.484Z "file": "apps/api/src/prisma/rls-access-inventory.spec.ts", "line": null, "description": "Die maschinelle Bestandsaufnahme (rls-access-inventory.spec.ts) ist fuer Relationszugriffe strukturell blind. Sie erkennt nur direkte Zugriffe der Form this.prisma. bzw. .. Ein Zugriff, der ueber include:/_count:/select: in eine ZWEITE Tabelle hineinreicht, ist fuer sie unsichtbar — obwohl Prisma daraus eine Unterabfrage auf diese zweite Tabelle macht, die unter DEREN Regel laeuft. Nachgewiesen in 260911-e2s: drei Zugriffe in tenant.controller.ts zaehlten ueber include: { _count: { select: { users } } } in die geschuetzte Tabelle User hinein (Prisma 6.19 rendert das als LEFT JOIN (SELECT tenantId, COUNT(*) FROM User ...)); nach dem Scharfschalten haette die Mandantenliste des Plattform-Administrators fuer jeden Mandanten 0 Benutzer gezeigt und der Loeschriegel T-02-09 waere vakuum geworden. Diese drei Stellen sind behoben (Fan-out je Mandant ueber gebundenen Client). Zur Planungszeit wurden alle 19 include:-Stellen und alle _count-Stellen in apps/api/src einzeln beurteilt, vom Orchestrator und vom Verifizierer unabhaengig gegengeprueft: nur diese drei waren gefaehrlich (tenders zaehlt auf dem plattformglobalen Katalog ohne Zeilenschutz, groups zaehlt ueber einen bereits gebundenen Client in eine Tabelle desselben Mandanten). OFFEN bleibt der MECHANISMUS: jede kuenftige include:/_count:-Stelle in eine fremd geschuetzte Tabelle bleibt fuer die Pruefung unsichtbar. Zu schliessen, indem der Detektor include:/select:/_count:-Bloecke auf Modellnamen durchsucht und die Zieltabelle als eigene Fundstelle fuehrt — oder durch eine Pruefung, die jede include:-Stelle einer expliziten Freigabeliste unterwirft. Gehoert vor das Scharfschalten (Etappe 4), weil die Vorabpruefung sich sonst auf eine Bestandsaufnahme stuetzt, die diese Form nicht sieht.", - "status": "open", + "status": "fixed", "reason": "", "recorded_at": "2026-09-11T09:08:00.435Z", - "resolved_at": null + "resolved_at": "2026-09-11T14:48:15.447Z" }, { "id": 28, @@ -433,6 +434,18 @@ last_updated: 2026-09-11T11:57:50.484Z "reason": "", "recorded_at": "2026-09-11T11:57:50.484Z", "resolved_at": null + }, + { + "id": 33, + "kind": "unmet-truth", + "phase": "quick-260911-mkj", + "file": "apps/api/src/tenders/tenders.seed.ts", + "line": null, + "description": "Modellaufrufe auf Empfaengern, die weder this.prisma noch eine const X = forTenant(-Zuweisung noch ein Transaktionsparameter sind, sind fuer ALLE vier Erkennungsformen der Bestandsaufnahme unsichtbar. Gemessen 260911-mkj: tenders/tenders.seed.ts (Funktionsparameter prisma: PrismaService, tenderRssFeedSource.findFirst/create, kein Eintrag in der Bestandsaufnahme) und tenders/backfill-tender-source.ts (eigenstaendiges Skript mit new PrismaClient(), tender.findMany/update, durch RELATION_SPEC_EXCEPTIONS laut gehalten). Beide beruehren nur den plattformglobalen Katalog bzw. die plattformweite RSS-Verwaltung (WINDOWS #24), heute ungefaehrlich; OFFEN ist der Mechanismus (ein kuenftiger Dienst mit Parameter-Empfaenger auf einer geschuetzten Tabelle bliebe unsichtbar). Zu schliessen vor Etappe 4 durch eine Zaehlung ALLER ..(-Anker gegen die bekannte Empfaengermenge, Ueberschuss laut.", + "status": "open", + "reason": "", + "recorded_at": "2026-09-11T14:48:09.723Z", + "resolved_at": null } ] ```` diff --git a/apps/api/src/prisma/rls-access-inventory.spec.ts b/apps/api/src/prisma/rls-access-inventory.spec.ts index 725bbbb..916eecd 100644 --- a/apps/api/src/prisma/rls-access-inventory.spec.ts +++ b/apps/api/src/prisma/rls-access-inventory.spec.ts @@ -105,8 +105,7 @@ const INTERACTIVE_TRANSACTION_EXCEPTIONS = new Set([]); * Transaktionsparameter). Zusammen mit `tenders.seed.ts` * (Funktionsparameter `prisma: PrismaService`, keine Relationsangabe, * deshalb hier nicht gelistet) als eigener Ledger-Eintrag gefuehrt: - * WINDOWS #TBD-MKJ (Aufgabe 2 ersetzt den Platzhalter durch die vergebene - * Nummer). + * WINDOWS #33. */ const RELATION_SPEC_EXCEPTIONS = new Set(['apps/api/src/tenders/backfill-tender-source.ts']); diff --git a/docs/mandantentrennung-etappe2-fehlerrichtung.md b/docs/mandantentrennung-etappe2-fehlerrichtung.md index ac6703c..92eb684 100644 --- a/docs/mandantentrennung-etappe2-fehlerrichtung.md +++ b/docs/mandantentrennung-etappe2-fehlerrichtung.md @@ -2471,6 +2471,28 @@ Mandantenzahl belanglos, dieselbe Form wie Wartungsrolle gegen die gebundene Fan-out-Zaehlung ist dieselbe Pruefung wie im Bereich `user` — kein eigener Eintrag noetig. +**Nachtrag (260911-mkj):** WINDOWS #27 — die in (b) beschriebene +Erkennungsluecke der Bestandsaufnahme — ist geschlossen. Eine vierte +Erkennungsform in `rls-access-inventory.spec.ts` (`analyzeSource`, +`SCHEMA_RELATIONS`) loest Relationsfelder ueber `schema.prisma` auf ihr +Zielmodell auf und traegt das Zielmodell als eigene Fundstelle ein — der +Nachweis steht gegen KONTROLLIERTE Proben im Spec, nicht gegen die drei +realen Stellen aus (b): die einzige gefaehrliche Auspraegung +(`tenant.controller.ts`, ungebundener aeusserer Aufruf auf ungeschuetzter +Tabelle mit Einbindung in eine geschuetzte Tabelle) traegt die Form seit +(n3)/Aufgabe 3 (dem Fan-out-Ersatz) nicht mehr — deshalb Probestrings, die +dieselbe Form nachbilden (ungebunden und gebunden). Gemessen: SIEBEN neue +(Datei, Modell)-Paare, DREI fortgeschriebene Staende, davon +`ldap-config.service.ts`/`ldapFieldMapping` als einziger Klassenwechsel +(`muss-mandantengebunden` -> `beides`, siehe (b) oben: `getAllActiveConfigs` +reicht auch in `LdapFieldMapping` hinein, nicht nur in `Tenant`). Verbleibende +Grenze, LAUT gehalten statt still: ein Empfaenger ausserhalb aller vier +Erkennungsformen bleibt fuer JEDE Form unsichtbar — gemessen +`tenders/tenders.seed.ts` (Funktionsparameter `prisma: PrismaService`) und +`tenders/backfill-tender-source.ts` (eigenstaendiges Skript, eigener `new +PrismaClient()`), beide als eigener Ledger-Eintrag gefuehrt (nicht +stillschweigend mit #27 mitgeschlossen). + ### (n5) Was dieser Durchlauf bewusst nicht anfasst - Der direkte Prisma-Zugriff im Controller (Muster wie `user.controller.ts`) @@ -3072,7 +3094,8 @@ Bedingung gebunden wie #18), #21 (dkv-Planer-Startpfad), #22 (module-registry, unterscheidbares Signal fehlt), #24 (plattformweite RSS-Verwaltung unter der Anwendungsrolle), #25 (dashboard, beweisvernichtende Fehlerrichtung), #26 (calendar, verschluckte Leere), #27 -(Relationszugriffe für die Bestandsaufnahme unsichtbar), #28 (auth, +(Relationszugriffe für die Bestandsaufnahme unsichtbar, seit 260911-mkj +geschlossen), #28 (auth, verschluckte Leere), plus die drei neuen Einträge dieses Laufs (Startpfad des Mailmoduls, verschluckte Leere `favorites`, verschluckte Leere `settings` — Nummern siehe Aufgabe 3 dieses Plans). diff --git a/docs/mandantentrennung-zugriffsklassifikation.md b/docs/mandantentrennung-zugriffsklassifikation.md index f922ee3..f8df955 100644 --- a/docs/mandantentrennung-zugriffsklassifikation.md +++ b/docs/mandantentrennung-zugriffsklassifikation.md @@ -131,6 +131,25 @@ Bodensatz, nicht die vollständige Zahl gebundener Zugriffe in `groups`; die Bestandsaufnahme unten (Spalte "Stand", je (Datei, Modell)-Paar) ist die autoritative Quelle. +**Zweite methodische Lücke, seit 260911-mkj geschlossen — aber nur in der +Bestandsaufnahme:** die beiden Rohtreffer-Greps dieser Tabelle +(`this\.prisma\.[a-zA-Z]*` bzw. `tenantPrisma\.[a-zA-Z]*\.`) zählen +Relationsziele (`include:`, `select:`, `_count:`, Relationsfilter in +`where:`/`orderBy:`) strukturell NICHT — ein `include: { module: true }` +auf `tenantPrisma.tenantModuleActivation` erzeugt keinen Rohtreffer auf +`module`, weil `module` in der Datei nie als `tenantPrisma.module` steht. +Die Spalten und die Summenzeile behalten deshalb ihre bisherige Bedeutung +und ihre bisherigen Werte (weiterhin 68 ungebunden / 178 gebunden, NACH- +GERECHNET, nicht abgeschrieben) — sie zählen weiterhin nur direkte +Modellaufrufe. Autoritativ für Relationszugriffe ist allein die +Bestandsaufnahme unten, deren vierte Erkennungsform +(`rls-access-inventory.spec.ts`, `analyzeSource`) die Ziele als eigene +Paare führt: sieben der 72 Paare dort tauchen in KEINER Rohtrefferzahl +dieser Übersicht auf, zum Beispiel +`module-registry/module-access.service.ts`/`group` (nur über den +Relationsfilter `group: { memberships: { some: { userId } } }` sichtbar, +niemals als `tenantPrisma.group` im Quelltext). + | Bereich | Ungebunden | Gebunden | Hinweis | |---|---|---|---| | tenders | 35 | 27 | **war 62/0**, dann 36/26 nach 260909-laa — 260910-jab (Aufgabe 2) hat `tender-rss-feed.service.ts`/`listForUser` zusätzlich auf `forTenant()` umgestellt (WINDOWS #19 geschlossen, Befund F: ungebunden hätte die Reparatur den Pfad sonst still auf nur die plattformweiten Zeilen reduziert): ein Rohtreffer wandert von ungebunden nach gebunden (36→35, 26→27). Die 35 verbleibenden ungebundenen Treffer sind die zwölf bewusst nicht angefassten Paare (D-03-Katalog, zwei Fan-out-Adapter) plus die zwei bewusst ungebundenen RSS-Pfade (`createPlatform`/`remove`, WINDOWS #24) plus die übergreifenden Hälften der beiden Hintergrunddienste (Etappe-3-Übergabe) | @@ -147,7 +166,7 @@ autoritative Quelle. | settings | 1 | 3 | **war 4/0** — 260911-gwh (Aufgabe 2) hat `getSmtpConfig`, `saveSmtpConfig`, `getDecryptedSmtpConfig` auf `forTenant()` umgestellt (3 gebundene `smtpConfig`-Rohtreffer). Der eine verbleibende ungebundene Rohtreffer ist der umbenannte Planer-Startpfad `loadAnySmtpConfigForStartupTransport()` (Befund D, WINDOWS #30) — bewusst, mit dreifacher Markierung; Befund K (`tenders`/`dkv` hängen an `getDecryptedSmtpConfig`) ist damit erfüllt | | **Summe** | **68** | **178** | Ungebunden: war 118 nach 260910-das, dann 108 nach 260910-exd (module-registry 17→7), dann 107 nach 260910-jab (`tenders` 36→35, `listForUser` gebunden), dann 95 nach 260910-krx (`dashboard` 13→1), dann 83 nach 260911-cwh (`calendar` 12→0), unverändert nach 260911-e2s (`tenant` bleibt bei 8 ungebundenen Rohtreffern), dann 78 nach 260911-fh9 (`auth` 8→3), jetzt 68 nach 260911-gwh (`favorites` 7→0, `settings` 4→1). Gebunden: war 124, dann 134 nach 260910-exd (zusätzlich 10 in `module-registry`), dann 135 nach 260910-jab (zusätzlich 1 in `tenders`), dann 147 nach 260910-krx (zusätzlich 12 in `dashboard`), dann 159 nach 260911-cwh (zusätzlich 12 in `calendar`), dann 162 nach 260911-e2s (zusätzlich 3 in `tenant`), dann 167 nach 260911-fh9 (zusätzlich 5 in `auth`), jetzt 178 nach 260911-gwh (zusätzlich 8 in `favorites`, 3 in `settings`). Dies ist der ENDSTAND der Etappe 2: jeder verbleibende ungebundene Rohtreffer ist einer der in diesem Dokument benannten, bewusst ungebundenen Fälle. Diese Übersicht ist eine Buchführungshilfe; **autoritativ ist die Fundstellentabelle unten**, die `rls-access-inventory.spec.ts` bei jedem Lauf gegen den Quelltext prüft | -## Klassen-Verteilung (nach (Datei, Modell)-Fundstellen, 65 Paare) +## Klassen-Verteilung (nach (Datei, Modell)-Fundstellen, 72 Paare) Stand 260909-jts (Aufgabe 3): 61 Paare aus dem vorherigen Durchlauf (260909-ipc) plus ein bisher vollstaendig unsichtbares Paar @@ -236,13 +255,36 @@ bestaetigt). Zwei Paare aendern nur ihre `Stand`-Spalte, keine ihrer Klasse: der ENDSTAND der Etappe 2 — die Zahl ist der Ausgabe von `rls-access-inventory.spec.ts` entnommen, nicht geschaetzt. +**Stand 260911-mkj (Aufgabe 1/2):** 72 Paare — 65 aus dem Endstand der +Etappe 2 plus SIEBEN neue Paare der vierten Erkennungsform (WINDOWS #27): +`groups/module-grants.service.ts`/`module` (`keine-mandantengebundene-tabelle`, +`gebunden`), `ldap/ldap-config.service.ts`/`tenant` +(`keine-mandantengebundene-tabelle`, `ungebunden`), +`module-registry/module-access.service.ts`/`group` UND `/groupMembership` +(beide `muss-mandantengebunden`, `gebunden`), +`tenders/tender-digest.scheduler.ts`/`tender` +(`keine-mandantengebundene-tabelle`, `gebunden`) UND `/tenderSavedSearch` +(`muss-mandantengebunden`, `gebunden`), +`tenders/tenders.controller.ts`/`tenderSource` +(`keine-mandantengebundene-tabelle`, `ungebunden`). Drei Paare aendern ihren +Stand, eines davon zusaetzlich die Klasse: +`ldap-config.service.ts`/`ldapFieldMapping` wechselt von +`muss-mandantengebunden`/`gebunden` auf `beides`/`gemischt` — der +uebergreifende Planer-Lesepfad `getAllActiveConfigs()` reicht ueber +`include: { fieldMappings: true }` in `LdapFieldMapping` hinein, dieselbe +Unterabfrage-Form, die WINDOWS #27 aufgedeckt hat. +`module-registry.service.ts`/`module` und `tender-matching.service.ts`/ +`tender` wechseln je von `ungebunden` auf `gemischt`, ihre Klasse bleibt. +Die Zahl 72 ist der Ausgabe von `rls-access-inventory.spec.ts` entnommen, +nicht geschaetzt. + | Klasse | Anzahl Paare | |---|---| -| muss-mandantengebunden | 33 | -| keine-mandantengebundene-tabelle | 17 | -| beides | 13 | +| muss-mandantengebunden | 35 | +| keine-mandantengebundene-tabelle | 21 | +| beides | 14 | | bewusst-uebergreifend | 2 | -| **Summe** | **65** | +| **Summe** | **72** | ## Der Hintergrunddienst als Falle — sechs Fälle @@ -278,6 +320,19 @@ beim Start — mit einer zusätzlichen Verdeckungsschicht, siehe unten: Zustand zum Zeitpunkt der ldap-Umstellung korrekt beschreibt und für spätere Etappen als Beleg dient, siehe auch `docs/mandantentrennung-etappe2-fehlerrichtung.md`, Abschnitt (e), Befund D. + + **Nachtrag (260911-mkj):** WINDOWS #27 geschlossen — derselbe Bereich + `ldap` traegt + einen zweiten, bislang unsichtbaren Planer-Lesezugriff: + `LdapConfigService.getAllActiveConfigs()` (`ldap-config.service.ts`) + reicht ueber `include: { tenant: true, fieldMappings: true }` sowohl in + `Tenant` (schutzlos, harmlos) als auch in `LdapFieldMapping` (geschuetzt + ueber Join auf `LdapConfig`) hinein. Nach dem Scharfschalten verstummt + die Feldzuordnung mit der Elternzeile, nicht getrennt von ihr — der + Etappe-3-Systemkontext muss BEIDE Tabellen sehen. Die Bestandsaufnahme + fuehrt `ldapFieldMapping` deshalb seither als `beides`/`gemischt` statt + `muss-mandantengebunden`/`gebunden` (siehe Bestandsaufnahme unten, + `ldap-config.service.ts`/`ldapFieldMapping`). - **`tender-digest.scheduler.ts`** (Ausschreibungs-Digest) — **Stand 260909-laa, Aufgabe 3: Je-Treffer-Hälfte geschlossen, übergreifende Hälfte an Etappe 3 übergeben.** Liest `tenderMatch` (Kandidatenabfrage,