feat(quick-260910-krx): Fehlerrichtung fuer Bereich dashboard gemessen
Aufgabe 1 — misst die umgekehrte Fehlerrichtung des Bereichs dashboard an den Regeln nach Migration 20260910120000, VOR der Umstellung: - rls-scratch-check.mjs bekommt einen neunten Abschnitt (runDashboardAreaChecks) mit 13 neuen, namentlich benannten Pruefungen gegen die aus der ausgelieferten Migration geschnittenen Regeln fuer DashboardLayout, WidgetInstance und SearchProvider. Alle 87 Pruefungen bestehen (74 bisherige + 13 neue). - Die Konfliktmessung (Befund K) ist gemessen, nicht angenommen: ein gebundenes INSERT ... ON CONFLICT auf eine unter dem Mandanten unsichtbare Zeile scheitert laut mit SQLSTATE 42501. Zusaetzlich am echten generierten Prisma Client gemessen: prisma.dashboardLayout.upsert() wirft PrismaClientUnknownRequestError (nicht P2002) — das tenders-Muster laesst sich deshalb nicht woertlich uebernehmen. - Die widerlegte Praemisse zu SearchProvider (WINDOWS #19) ist in diesem Durchlauf eigenstaendig nachgeprueft, mit benannter Suchreichweite. - docs/mandantentrennung-etappe2-fehlerrichtung.md bekommt den Abschnitt "## Bereich dashboard" (w1)-(w5) samt der beweisvernichtenden Schleife (leeres Dashboard -> Neuaufbau -> automatisches Zurueckschreiben -> ueberschriebene Anordnung) und einen Nachtrag im Abschnitt "## Bereich module-registry" zur geerbten Bindungsentlastung. Baseline gehalten: 839 Tests / 56 Dateien gruen, Typpruefung sauber. Schalter bleibt aus.
This commit is contained in:
@@ -1611,6 +1611,21 @@ nichts gefunden" unterscheidet, mit der Vorabprüfung für Etappe 4 und der
|
||||
begründeten Verwerfung einer Laufzeitwarnung — siehe (m3) oben und
|
||||
`.planning/WINDOWS.md`.
|
||||
|
||||
**Nachtrag (260910-krx):** der oben in Befund E festgehaltene Befund zur
|
||||
Reihenfolge — der Bereich `dashboard` erbt die Bindung der
|
||||
Modul-Zugriffsauflösung, ohne dass eine Datei unter
|
||||
`apps/api/src/module-registry` dafür angefasst werden muss — ist mit
|
||||
Quick-Task 260910-krx EINGELÖST und NACHGEPRÜFT: `dashboard.service.ts`
|
||||
ruft `ModuleAccessService.getAccessibleModuleIds` für den Widget-Modulfilter
|
||||
in `getWidgets` unverändert auf, diese Auflösung bindet seit 260910-exd
|
||||
bereits über `forTenant()`, und der Filter ist damit gebunden. Nachgeprüft
|
||||
mit `grep -n "const tenantPrisma = forTenant" apps/api/src/module-registry/
|
||||
module-access.service.ts` (ein Treffer) und mit einem Wachhund-Testfall in
|
||||
`dashboard.service.spec.ts`, der den Modulkatalog aus dem
|
||||
Bindungsprotokoll heraushält. Der Widget-Modulfilter wurde von 260910-krx
|
||||
NICHT ein zweites Mal gebunden — keine Datei unter
|
||||
`apps/api/src/module-registry` ist Teil dieses Plans.
|
||||
|
||||
## Regelschluss T-JTS-02, T-JTS-03 und WINDOWS #19
|
||||
|
||||
Dieser Abschnitt weicht bewusst von der geplanten Reihenfolge ab (260910-jab,
|
||||
@@ -1727,6 +1742,264 @@ Tabellen, inklusive der NEUEN Stelle aus Befund F:
|
||||
Regeln sind heute wirkungslos; das Wegwerf-Werkzeug und die Regelliste der
|
||||
lebenden Datenbank sind die einzigen Zeugen dafür, dass sie greifen.
|
||||
|
||||
## Bereich dashboard
|
||||
|
||||
Dieser Abschnitt erweitert die Kritikschrift um den Bereich `dashboard`
|
||||
(Quick-Task 260910-krx), den achten Bereich der Etappe und den einzigen
|
||||
Dienst, der ausschließlich hält, was ein Nutzer sich selbst eingerichtet
|
||||
hat: die Anordnung seiner Widgets und seine eigenen Suchmaschinen. Die
|
||||
umgekehrte Fehlerrichtung sieht hier nicht wie ein Fehler aus, sondern wie
|
||||
ein Zurücksetzen — und sie ist in diesem Bereich beweisvernichtend, siehe
|
||||
(w3).
|
||||
|
||||
### (w1) Die Messung
|
||||
|
||||
Aufgabe 1 hat `apps/api/scripts/rls-scratch-check.mjs` um einen neunten
|
||||
Abschnitt (`runDashboardAreaChecks`) erweitert, unmittelbar nach
|
||||
`runModuleRegistryAreaChecks` und vor `runTransactionShapeMeasurement`
|
||||
aufgerufen. Er legt die beiden Wegwerf-Tabellen `DashboardLayout` und
|
||||
`WidgetInstance` selbst neu an und benutzt die von
|
||||
`runSearchProviderAreaChecks` bereits angelegte Tabelle `SearchProvider`
|
||||
WEITER (eigene Kennungen, keine zweite Anlage) — alle drei Policies mit
|
||||
`extractPolicySql()` WORTGLEICH aus der ausgelieferten Migration
|
||||
`20260909140000_rls_remaining_tenant_tables` geschnitten, **gemessen am
|
||||
Regelstand NACH der Migration `20260910120000_rls_widen_membership_grant_and_platform_read`**
|
||||
(die drei Regeln dieses Bereichs sind von jener Migration unverändert
|
||||
gelassen worden, siehe deren Abschnitt (4) — die Messung gilt trotzdem dem
|
||||
aktuellen, lebenden Regelstand, nicht einem veralteten). Tatsächlich
|
||||
beobachtete Ausgabe dieses Laufs (2026-09-11, gegen `tessera-ctl-db-1`,
|
||||
Adresse `172.19.0.2`, nur die dreizehn neuen Zeilen dieses Abschnitts sowie
|
||||
die abschließende Summenzeile):
|
||||
|
||||
```
|
||||
dashboardlayout-gebunden-nur-eigener-mandant: bestanden — forTenant(TENANT-A) liefert 2 Zeile(n): ["layout-a1","layout-a2"]
|
||||
dashboardlayout-fremder-nutzer-desselben-mandanten-gebunden-sichtbar: bestanden — forTenant(TENANT-A) liefert die Anordnung von 'user-a2' (anderer Benutzer, gleicher Mandant) mit: true — die Regel auf "DashboardLayout" kennt keine Benutzerdimension, die anwendungsseitige Pruefung ueber die Benutzerkennung bleibt deshalb der einzige Schutz gegen Quer-Lesen zwischen Nutzern DESSELBEN Mandanten
|
||||
dashboardlayout-ungebunden-null-zeilen: bestanden — ungebundener SELECT auf "DashboardLayout" liefert 0 Zeile(n), tatsaechlich vorhanden sind 4
|
||||
widgetinstance-ungebunden-null-zeilen: bestanden — ungebundener SELECT auf "WidgetInstance" liefert 0 Zeile(n), tatsaechlich vorhanden sind 3
|
||||
widgetinstance-gebunden-nur-eigener-mandant: bestanden — forTenant(TENANT-A) liefert 2 Zeile(n): ["widget-a1","widget-a2"]
|
||||
widgetinstance-fremder-nutzer-desselben-mandanten-gebunden-sichtbar: bestanden — forTenant(TENANT-A) liefert das Widget von 'user-a2' (anderer Benutzer, gleicher Mandant) mit: true — dieselbe fehlende Benutzerdimension wie bei "DashboardLayout" und "SearchProvider" (Befund G), der dritte der drei Faelle dieses Bereichs
|
||||
dashboardlayout-gebundener-konfliktschreibvorgang-auf-unsichtbare-zeile-scheitert-laut: bestanden — gebundenes INSERT ... ON CONFLICT ("userId") DO UPDATE unter TENANT-A auf die unter TENANT-B physisch vorhandene, unsichtbare Zeile (user-conflict) scheitert LAUT mit SQLSTATE 42501: ERROR: new row violates row-level security policy (USING expression) for table "DashboardLayout"
|
||||
widgetinstance-gebundenes-einfuegen-fremder-mandant-abgelehnt: bestanden — gebundenes INSERT unter TENANT-A mit tenantId=TENANT-B abgewiesen mit SQLSTATE 42501 (ERROR: new row violates row-level security policy for table "WidgetInstance")
|
||||
widgetinstance-gebundenes-loeschen-fremder-zeile-trifft-keine-zeile: bestanden — gebundenes DELETE unter TENANT-A ueber die Kennung 'widget-b1' (gehoert TENANT-B) trifft 0 Zeile(n) — die vorgeschaltete Besitzpruefung im Anwendungscode bleibt deshalb der einzige Schutz vor dem Scharfschalten
|
||||
searchprovider-ungebunden-null-zeilen: bestanden — ungebundener SELECT auf "SearchProvider" liefert 0 Zeile(n), tatsaechlich vorhanden sind 4
|
||||
searchprovider-gebunden-nur-eigener-mandant: bestanden — forTenant(TENANT-A) liefert 2 Zeile(n) aus den neu hinzugefuegten: ["search-a1","search-a2"]
|
||||
searchprovider-fremder-nutzer-desselben-mandanten-gebunden-sichtbar: bestanden — forTenant(TENANT-A) liefert die Suchmaschine von 'user-a2' (anderer Benutzer, gleicher Mandant) mit: true — dieselbe fehlende Benutzerdimension wie bei "DashboardLayout" und "WidgetInstance" (Befund G)
|
||||
searchprovider-gebundenes-einfuegen-ohne-mandant-abgelehnt: bestanden — gebundenes INSERT unter TENANT-A mit tenantId=NULL abgewiesen mit SQLSTATE 42501 (ERROR: new row violates row-level security policy for table "SearchProvider")
|
||||
Alle 87 Pruefungen bestanden.
|
||||
```
|
||||
|
||||
Dreizehn neue Prüfungen, nicht zwölf wie in der Aufzählung des Plans
|
||||
namentlich vorgezeichnet — die dreizehnte
|
||||
(`widgetinstance-fremder-nutzer-desselben-mandanten-gebunden-sichtbar`)
|
||||
wurde ergänzt, weil Befund G des Plans ausdrücklich alle DREI Tabellen
|
||||
dieses Bereichs als ohne Benutzerdimension benennt, der Plan aber nur für
|
||||
`DashboardLayout` und `SearchProvider` einen entsprechenden Testfall
|
||||
vorzeichnete. Die Gesamtzahl der Werkzeugprüfungen steigt damit von 74 auf
|
||||
87 (74 + 13).
|
||||
|
||||
**Die tragende Belegzeile ist `dashboardlayout-ungebunden-null-zeilen`:**
|
||||
der IDENTISCHE `SELECT "tenantId" FROM "DashboardLayout"` ohne vorheriges
|
||||
`set_config` liefert **0 Zeilen**, nicht die 4 tatsächlich vorhandenen —
|
||||
an der echten, ausgelieferten Policy gemessen. `widgetinstance-ungebunden-
|
||||
null-zeilen` misst dieselbe Unsichtbarkeit für die Widget-Tabelle.
|
||||
|
||||
**Die Konfliktmessung (Befund K, Prüfung 5) hat ein Ergebnis, nicht eine
|
||||
Vermutung.** Ein gebundenes `INSERT ... ON CONFLICT ("userId") DO UPDATE`
|
||||
unter TENANT-A, das auf die unter TENANT-B physisch vorhandene, unter
|
||||
TENANT-A unsichtbare Zeile trifft, scheitert LAUT mit SQLSTATE `42501`
|
||||
("new row violates row-level security policy (USING expression) for table
|
||||
\"DashboardLayout\""), nicht mit einem Eindeutigkeitsfehler (`23505`) und
|
||||
nicht mit einem stillen Erfolg. Zusätzlich am ECHTEN, generierten Prisma
|
||||
Client gemessen (nicht nur an rohem SQL), weil `saveLayout` in Wahrheit
|
||||
`prisma.dashboardLayout.upsert()` aufruft, nicht `$executeRaw`: gegen eine
|
||||
eigens dafür angelegte Wegwerf-Datenbank mit vollständigem Spaltensatz
|
||||
(`id`, `userId`, `tenantId`, `layouts`, `createdAt`, `updatedAt`) und einer
|
||||
eigenen Wegwerf-Rolle ohne `BYPASSRLS` liefert derselbe Konfliktfall über
|
||||
`tenantPrisma.dashboardLayout.upsert({ where: { userId }, update, create })`
|
||||
einen **`PrismaClientUnknownRequestError`** — nicht den bekannten
|
||||
`PrismaClientKnownRequestError` mit `.code === 'P2002'`, den der Bereich
|
||||
`tenders` für seinen Eindeutigkeitsfall abfängt. `.code` und `.meta` sind
|
||||
bei diesem Fehlertyp `undefined`; die einzige verlässliche Information
|
||||
steht im rohen `.message`-Text, der den PostgreSQL-Fehler eingebettet
|
||||
enthält (`code: "42501"`, `message: "new row violates row-level security
|
||||
policy (USING expression) for table \"DashboardLayout\""`). **Das ist die
|
||||
zentrale Abweichung von der Annahme, das `tenders`-P2002-Muster ließe sich
|
||||
wörtlich übernehmen** — es lässt sich nicht, weil dieser Fehler eine andere
|
||||
Prisma-Fehlerklasse ist. Aufgabe 2 fängt deshalb
|
||||
`Prisma.PrismaClientUnknownRequestError` ab (Prüfung auf die Fehlerklasse,
|
||||
nicht auf `.code`) und übersetzt ihn in eine verständliche deutsche
|
||||
Meldung — siehe (w4) für die Grenze dieser Behandlung.
|
||||
|
||||
**Die eigenständige Nachprüfung der widerlegten Prämisse (Befund F,
|
||||
WINDOWS #19).** Nachgeprüft mit derselben Anweisung wie zur Planungszeit,
|
||||
diesmal gegen den aktuellen Quelltext (2026-09-11):
|
||||
`grep -rn "searchProvider\|SearchProvider" apps packages prisma
|
||||
--include=*.ts --include=*.mjs --include=*.js --include=*.sql
|
||||
--include=*.json` (ohne `node_modules`, `dist/`, `.next/`). Ergebnis
|
||||
unverändert: der einzige Schreibweg ist
|
||||
`dashboard.service.ts:241` (`addSearchProvider` → `create`) mit
|
||||
`tenantId: string` als PFLICHTPARAMETER der aufrufenden Methode; keine
|
||||
Seed-Datei (`apps/api/prisma/` enthält ausschließlich `migrations` und
|
||||
`schema.prisma`), kein Skript, kein weiterer Schreibweg. Reichweite der
|
||||
Suche, wie zur Planungszeit benannt: sie findet keinen Schreibweg über
|
||||
einen dynamisch gebildeten Modellnamen und deckt keine manuelle
|
||||
Datenbankänderung ab — die Aussage lautet deshalb "kein Anwendungspfad
|
||||
erzeugt eine mandantenlose Zeile", nicht "es kann keine geben". Zusätzlich
|
||||
datenbankseitig verteidigt: `searchprovider-gebundenes-einfuegen-ohne-
|
||||
mandant-abgelehnt` weist ein gebundenes Einfügen mit `tenantId = NULL`
|
||||
laut mit SQLSTATE `42501` ab, weil die Regel ohne eigene `WITH CHECK`-
|
||||
Klausel ihre `USING`-Klausel dafür wiederverwendet.
|
||||
|
||||
### (w2) Signaltabelle je umgestelltem Pfad
|
||||
|
||||
| Pfad | Verhalten bei zu wenig Ergebnis | Konkretes Signal, Ort |
|
||||
|---|---|---|
|
||||
| `DashboardService.getLayout` | Der gebundene Lesezugriff liefert `null` statt der vorhandenen Zeile — identisch zum heutigen "noch keine Anordnung gespeichert" | Der Rückgabepunkt liefert die Vorgabeanordnung `{ lg: [], md: [], sm: [], xs: [], xxs: [] }`, Status 200, kein Fehler. Sichtbar im Browser als leeres Dashboard — siehe (w3) für die Folgekette |
|
||||
| `DashboardService.saveLayout` | Der gebundene Schreibzugriff trifft im `update`-Zweig die vorhandene, aber unter dem laufenden Mandanten unsichtbare Zeile über den plattformweit eindeutigen Schlüssel `userId` — Konfliktmessung, siehe (w1) | `PUT /dashboard/layout` liefert nach Aufgabe 2 eine verständliche deutsche Konfliktmeldung statt eines rohen 500ers — siehe (w4) für die Grenze |
|
||||
| `DashboardService.getWidgets`, Widget-Lesezugriff | Der gebundene Lesezugriff liefert eine leere Liste statt der platzierten Widgets | Der Nutzer sieht ein Dashboard ohne jedes Widget, Status 200, kein Fehler |
|
||||
| `DashboardService.addWidget` | Der gebundene Schreibzugriff schlägt fehl bzw. legt die Zeile unter einer Mandantenkennung an, die der Sitzungsnachweis liefert — kein Leere-Fall in diese Richtung | `POST /dashboard/widgets` liefert einen Fehler statt eines neuen Widgets, falls der Mandant fehlt (`ForbiddenException` bereits im Controller) |
|
||||
| `DashboardService.updateWidgetConfig`/`removeWidget`, Besitzprüfung | Die gebundene `findUnique`-Abfrage liefert `null` statt der eigenen Zeile — ununterscheidbar vom echten "gehört jemand anderem" | `NotFoundException` — dieselbe Meldung wie beim echten Besitzverstoß, siehe (w4) |
|
||||
| `DashboardService.getSearchProviders` | Der gebundene Lesezugriff auf `custom` liefert eine leere Liste statt der eigenen Suchmaschinen — die drei Vorgaben aus der Konstante bleiben unberührt und werden IMMER vorangestellt | Die eigenen Suchmaschinen verschwinden aus der Auswahlliste des Such-Widgets, die Leiste funktioniert weiter — siehe (w3), Befund J |
|
||||
| `DashboardService.addSearchProvider` | Kein Leere-Fall — der Schreibzugriff verlangt die Mandantenkennung als Pflichtparameter | — |
|
||||
| `DashboardService.removeSearchProvider`, Besitzprüfung | Wie bei Widgets: `null` statt der eigenen Zeile | `NotFoundException` — dieselbe Meldung wie beim echten Besitzverstoß |
|
||||
|
||||
### (w3) Welcher Code Leere als Abwesenheit deutet
|
||||
|
||||
**Backend, eine Stelle:** `DashboardService.getLayout` —
|
||||
`if (!record) return { lg: [], md: [], sm: [], xs: [], xxs: [] };`. Kein
|
||||
Datensatz bedeutet hier nicht "Fehler", sondern "Vorgabeanordnung" — dieselbe
|
||||
Deutung wie bei `getWidgets` (leere Liste) und `getSearchProviders` (nur die
|
||||
drei Konstanten).
|
||||
|
||||
**Frontend, drei Stellen, zur Ausführungszeit an den beiden Dateien erneut
|
||||
nachgeprüft (Befund I/J), nicht aus dem Plan abgeschrieben — dieser Plan
|
||||
ändert an KEINER der beiden Dateien etwas:**
|
||||
|
||||
1. `apps/web/src/lib/stores/dashboard-store.ts`, `loadDashboard`: setzt
|
||||
`layouts` und `widgets` genau auf das, was `api.fetchLayout()` und
|
||||
`api.fetchWidgets()` liefern. Der `catch`-Zweig
|
||||
(`error: 'Failed to load dashboard'`) feuert nur bei einem Netzwerk-
|
||||
oder Statusfehler — eine erfolgreiche, leere Antwort setzt keinen
|
||||
Fehlerzustand.
|
||||
2. `apps/web/src/lib/stores/dashboard-store.ts`, `setEditMode`:
|
||||
`if (prev && !mode && get().isDirty) { get().saveLayout(); }` — der
|
||||
Neuaufbau wird beim bloßen VERLASSEN des Bearbeitungsmodus automatisch
|
||||
zurückgeschrieben, ohne dass jemand auf "Speichern" klickt.
|
||||
3. `apps/web/src/components/dashboard/widgets/search-widget.tsx`: die
|
||||
Rückfallprüfung `if (!cancelled && data.length > 0)` greift NIE, weil
|
||||
`getSearchProviders` die drei Vorgaben immer voranstellt — `data` ist
|
||||
nie leer, selbst wenn `custom` (die eigenen Suchmaschinen) nach dem
|
||||
Scharfschalten leer geblieben wäre. Der `.catch()`-Rückfallzweig auf
|
||||
`DEFAULT_PROVIDERS` feuert deshalb ebenfalls nie in diesem Fall.
|
||||
|
||||
**Die beweisvernichtende Schleife, als Schleife beschrieben (Befund I):**
|
||||
leeres Dashboard (Stelle 1) → der Nutzer hält das für einen Fehler des
|
||||
Widget-Systems oder für verlorene Einstellungen, baut seine Anordnung neu
|
||||
auf, `addWidget` legt echte neue `WidgetInstance`-Zeilen an (keine
|
||||
Eindeutigkeitsbedingung über `(userId, widgetType)`, Dubletten häufen sich
|
||||
also bei wiederholtem Neuaufbau an) → beim Verlassen des Bearbeitungsmodus
|
||||
schreibt Stelle 2 den Neuaufbau AUTOMATISCH zurück, ohne dass der Nutzer
|
||||
"Speichern" geklickt hat → die `layouts`-Spalte der ursprünglichen Zeile
|
||||
ist überschrieben, die einzige Aufzeichnung der ursprünglichen Anordnung
|
||||
ist verloren, bevor irgendjemand die Ursache untersuchen konnte. Der
|
||||
Nutzer hat dabei eine fertige, FALSCHE Erklärung zur Hand ("das
|
||||
Widget-System spinnt", "meine Einstellungen sind weg") und meldet deshalb
|
||||
keinen Fehler — derselbe Mechanismus, der auch in `module-registry` (m3)
|
||||
und `ldap` beschrieben ist, hier aber mit einer zusätzlichen, aktiven
|
||||
Zerstörungshandlung (das automatische Zurückschreiben), die es in keinem
|
||||
der beiden anderen Bereiche gibt.
|
||||
|
||||
**Das Verhalten der Suchleiste (Befund J), präzise statt "still"
|
||||
formuliert:** verschwinden die eigenen Suchmaschinen des Nutzers aus
|
||||
`custom`, bleibt die Auswahlliste (`<select>`) dennoch gefüllt — mit den
|
||||
drei Vorgaben. Sichtbar wird das nur, wenn der Nutzer VORHER eine eigene
|
||||
Suchmaschine ausgewählt hatte: React setzt `value={selectedProviderId}`
|
||||
auf dem `<select>`, aber `selectedProviderId` referenziert nach dem
|
||||
Verschwinden keinen vorhandenen `<option>`-Wert mehr — der Browser zeigt in
|
||||
diesem Fall (kein `<option>` mit passendem `value`) den ERSTEN Eintrag der
|
||||
Liste an, also "Google", OHNE dass der interne React-State
|
||||
`selectedProviderId` sich ändert oder irgendeine Meldung erscheint. Klickt
|
||||
der Nutzer danach Suchen, greift `handleSearch`:
|
||||
`providers.find((p) => p.id === selectedProviderId) ?? providers[0]` — der
|
||||
`find` schlägt fehl (die eigene Suchmaschine ist nicht mehr in `providers`),
|
||||
der Rückfall auf `providers[0]` (Google) greift, und eine Anfrage, die für
|
||||
ein internes Werkzeug gedacht war, geht an eine externe Suchmaschine. Für
|
||||
einen Nutzer, der die Anzeige "Google" im Dropdown nicht bewusst als
|
||||
Abweichung von seiner eigenen Auswahl liest, bleibt das unbemerkt.
|
||||
|
||||
### (w4) Was dieser Durchlauf bewusst nicht löst
|
||||
|
||||
- **Die plattformweite Eindeutigkeit von `DashboardLayout.userId`
|
||||
(Befund K).** `userId` trägt `@unique` ohne Mandantenanteil — strukturell
|
||||
dieselbe Kette wie WINDOWS #22 im Bereich `user`: unsichtbare Zeile,
|
||||
falsches "frei", harter Eindeutigkeitsfehler. Anders als bei #22 ist der
|
||||
Fehler hier aber GEMESSEN statt angenommen (siehe (w1), Prüfung 5) und
|
||||
wird in Aufgabe 2 BEDINGT auf das gemessene Ergebnis behandelt: eine
|
||||
verständliche deutsche Meldung statt eines rohen 500ers, indem
|
||||
`Prisma.PrismaClientUnknownRequestError` abgefangen wird — NICHT
|
||||
`.code === 'P2002'` wie im Bereich `tenders`, weil der gemessene Fehler
|
||||
eine andere Prisma-Fehlerklasse ist. Die ehrliche Reparatur wäre eine
|
||||
Schemaänderung (Eindeutigkeit mit Mandantendimension) und ist als
|
||||
Produktentscheidung für Etappe 3 vorgemerkt, wie #22 es für denselben
|
||||
Fall bereits tut.
|
||||
- **Die fehlende Unterscheidbarkeit von "noch keine Anordnung gespeichert"
|
||||
und "Anordnung nicht sichtbar".** Beide liefern identisch die
|
||||
Vorgabeanordnung, Status 200, keinen Protokolleintrag. Die konkrete
|
||||
Vorabprüfung für Etappe 4 (`rls-preflight.mjs`): eine physisch
|
||||
vorhandene `DashboardLayout`-Zeile für einen bekannten Benutzer, aber der
|
||||
gebundene Lesezugriff für dessen Mandanten liefert `null` — das
|
||||
unterscheidet den echten Erstbenutzer-Fall (keine Zeile vorhanden) vom
|
||||
Trennungsfehler (Zeile vorhanden, aber unsichtbar).
|
||||
- **Eine Laufzeitwarnung an `getLayout`/`getWidgets`/`getSearchProviders`
|
||||
wurde erwogen und VERWORFEN**, mit derselben Begründung wie bei
|
||||
`getAllActiveConfigs` im Bereich `ldap`: eine leere Anordnung, eine leere
|
||||
Widget-Liste oder keine eigene Suchmaschine sind auf einer frischen
|
||||
Installation oder für einen neuen Benutzer der NORMALZUSTAND — eine
|
||||
Warnung an dieser Stelle wäre Dauerlärm und verlöre ihr Signal, bevor sie
|
||||
gebraucht wird. Das gilt hier NOCH ausgeprägter als in `ldap`, weil
|
||||
praktisch jeder neue Benutzer diesen Zustand beim ersten Login durchläuft.
|
||||
- Der offene Ledger-Eintrag zur beweisvernichtenden Schleife (angelegt in
|
||||
Aufgabe 3, siehe `.planning/WINDOWS.md`) ist an dieselbe Bedingung
|
||||
gebunden wie #18 — er wird erst
|
||||
nach dem Scharfschalten beobachtbar.
|
||||
|
||||
### (w5) Was dieser Durchlauf bewusst nicht anfasst
|
||||
|
||||
- **Das Frontend** — geprüft (Befund I/J, (w3) oben) und bewusst gelassen,
|
||||
keine Datei dieses Plans. `dashboard-store.ts` und `search-widget.tsx`
|
||||
werden NICHT geändert.
|
||||
- **Der Bereich `favorites`.** `FavoriteLink` hängt per Fremdschlüssel an
|
||||
`WidgetInstance` (`apps/api/prisma/schema.prisma`,
|
||||
`favoriteLinks FavoriteLink[]` auf `WidgetInstance`), ist aber ein
|
||||
eigener Bereich mit eigener Umstellung (`apps/api/src/favorites/
|
||||
favorites.service.ts`, ungebunden, sieben Rohtreffer laut
|
||||
Klassifikationsdokument) — nicht Teil dieses Plans.
|
||||
- **Der Bereich `module-registry`** samt der geerbten Entlastung aus
|
||||
Befund E: `dashboard.service.ts` ruft
|
||||
`ModuleAccessService.getAccessibleModuleIds` für den Widget-Modulfilter
|
||||
auf, und diese Auflösung bindet bereits seit 260910-exd
|
||||
(`const tenantPrisma = forTenant(this.prisma, tenantId)` in
|
||||
`module-access.service.ts`). Der Filter ist damit gebunden, OHNE dass
|
||||
dieser Plan eine Datei unter `apps/api/src/module-registry` anfasst —
|
||||
nachgeprüft mit `grep -n "const tenantPrisma = forTenant" apps/api/src/
|
||||
module-registry/module-access.service.ts`, ein Treffer. Der eine
|
||||
Katalogzugriff in `dashboard.service.ts` (`getWidgets`, `this.prisma.
|
||||
module`) bleibt bewusst ungebunden — siehe die Begründung in der
|
||||
Klassifikationstabelle, die Messung und Bedingung trennt.
|
||||
- **Die Ungenauigkeit in `apps/api/src/groups/groups.service.ts` (Befund
|
||||
L), zur Ausführungszeit gegengelesen und WÖRTLICH bestätigt, NICHT
|
||||
behoben.** Der Kopfkommentar um Zeile 24 begründet die dortige
|
||||
Join-Filterung mit "nach dem Ownership-Check-Muster aus
|
||||
DashboardService.removeWidget". Das trifft in zwei Punkten nicht zu:
|
||||
`removeWidget` filtert NICHT zusätzlich in der Datenbankabfrage, sondern
|
||||
vergleicht NACH dem Laden im JavaScript (`widget.userId !== userId`),
|
||||
und dieser Vergleich läuft über die BENUTZER-, nicht die
|
||||
Mandantenkennung. Die Datei `apps/api/src/groups/groups.service.ts`
|
||||
wird von diesem Plan NICHT angefasst — diese Feststellung steht hier als
|
||||
Richtigstellung, nicht als Änderung an der fremden Datei.
|
||||
- Schema und Migrationen — geprüft und bewusst gelassen, keine
|
||||
Schemaänderung in dieser Etappe.
|
||||
|
||||
## Verweis
|
||||
|
||||
Die Bestandsaufnahme, welche Fundstelle den hier beschriebenen Übergang
|
||||
|
||||
Reference in New Issue
Block a user