feat(quick-260911-cwh): Fehlerrichtung Bereich calendar messen (Aufgabe 1)
- rls-scratch-check.mjs: elfter Abschnitt runCalendarAreaChecks mit 13 namentlich benannten Pruefungen gegen die aus 20260909140000_rls_remaining_tenant_tables geschnittene Regel, davon 4 ueber den generierten Client an einer schemagleichen Wegwerf-Tabelle (17 Spalten, gegen schema.prisma laufzeitgeprueft); Laufzeitpruefung, dass 20260910120000 keine eigene CalendarSource-Regel traegt (Messfalle 260910-jab) - Alle 101 Pruefungen bestehen (88 bisherige + 13 neue) - docs/mandantentrennung-etappe2-fehlerrichtung.md: Abschnitt "## Bereich calendar" mit (k1)-(k5) — Messung, Signaltabelle je Pfad, Leere-als-Abwesenheit in Backend UND Frontend samt Fehlerverschluckung, bewusst nicht geloest (Cache-Schluessel-Urteil, Zugangsdaten-Erhaltung, fehlende Benutzerdimension, 403/404), bewusst nicht angefasst - Wettlauf-Fehlerklasse gemessen: PrismaClientKnownRequestError (P2025), NICHT PrismaClientUnknownRequestError wie im Bereich dashboard — Aufgabe 2 braucht deshalb keine neue Fehleruebersetzung fuer die Besitzpruefungen Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
@@ -2000,6 +2000,265 @@ Abweichung von seiner eigenen Auswahl liest, bleibt das unbemerkt.
|
||||
- Schema und Migrationen — geprüft und bewusst gelassen, keine
|
||||
Schemaänderung in dieser Etappe.
|
||||
|
||||
## Bereich calendar
|
||||
|
||||
Dieser Abschnitt erweitert die Kritikschrift um den Bereich `calendar`
|
||||
(Quick-Task 260911-cwh), den neunten Bereich der Etappe und den einzigen
|
||||
Dienst, der nicht bloß Daten hält, sondern ZUGANGSDATEN zu fremden Servern —
|
||||
die verschlüsselten Exchange-, CalDAV- und ICS-Anmeldungen eines Nutzers.
|
||||
Ein Quer-Lesen ist hier nicht Offenlegung eines Termins, sondern Offenlegung
|
||||
der Anmeldung einer anderen Firma bei ihrem Mailserver. Die umgekehrte
|
||||
Fehlerrichtung sieht hier nicht wie ein Fehler aus, sondern wie ein leerer
|
||||
Kalender — und das Frontend verstärkt das, siehe (k3).
|
||||
|
||||
### (k1) Die Messung
|
||||
|
||||
Aufgabe 1 hat `apps/api/scripts/rls-scratch-check.mjs` um einen elften
|
||||
Abschnitt (`runCalendarAreaChecks`) erweitert, unmittelbar nach
|
||||
`runDashboardAreaChecks` und vor `runTransactionShapeMeasurement` aufgerufen.
|
||||
Er legt die Wegwerf-Tabelle `CalendarSource` selbst neu an, mit SÄMTLICHEN
|
||||
17 Spalten des Modells (nicht nur den für Roh-SQL nötigen — die Lehre aus
|
||||
Prüfung 5b im Bereich `dashboard`: der generierte Client wählt standardmäßig
|
||||
JEDE Spalte des Modells aus und scheitert mit P2022 an jeder fehlenden), mit
|
||||
der Regel `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`**. Diese
|
||||
zweite Migration wird zur LAUFZEIT geprüft (Prüfung
|
||||
`calendarsource-regelstand-eindeutig`), nicht nur zur Planungszeit behauptet:
|
||||
`extractPolicySql(readRlsWidenMigrationSql(), 'CalendarSource')` liefert
|
||||
`null` — jene Migration trägt KEINE eigene Regel für `CalendarSource`, der
|
||||
Stand aus `20260909140000` ist weiterhin der ausgelieferte, aktuelle
|
||||
Regelstand. Fände sich dort doch eine Regel, bräche der Abschnitt mit einer
|
||||
FEHLGESCHLAGENEN Prüfung ab, statt die abgelöste Regel weiterzumessen — die
|
||||
Messfalle aus 260910-jab.
|
||||
|
||||
Vier der dreizehn neuen Prüfungen laufen über den GENERIERTEN Prisma-Client
|
||||
(nicht nur an rohem SQL), weil `calendar.service.ts` in Wahrheit
|
||||
`this.prisma.calendarSource.findMany/update/create()` aufruft, nicht
|
||||
`$executeRaw` — dieselbe dashboard-Lehre: Prüfung 8
|
||||
(`calendarsource-wegwerftabelle-deckt-alle-spalten-des-generierten-clients`)
|
||||
vergleicht die zur Laufzeit aus `schema.prisma` gelesenen 17 Feldnamen des
|
||||
Modells `CalendarSource` mit den tatsächlichen Spalten der Wegwerf-Tabelle
|
||||
über `information_schema.columns` — sie steht VOR den Client-Prüfungen 9-12
|
||||
und bricht den Abschnitt ab, wenn sie durchfällt, weil alle folgenden
|
||||
Client-Messungen sonst wertlos wären.
|
||||
|
||||
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):
|
||||
|
||||
```
|
||||
calendarsource-regelstand-eindeutig: bestanden — die *_rls_widen_membership_grant_and_platform_read-Migration (260910-jab) enthaelt KEINE eigene Regel fuer "CalendarSource" (Befund G) — der Stand aus 20260909140000_rls_remaining_tenant_tables ist weiterhin der ausgelieferte, aktuelle Regelstand
|
||||
calendarsource-wegwerftabelle-deckt-alle-spalten-des-generierten-clients: bestanden — Schema-Felder aus schema.prisma (model CalendarSource, 17): ["color","createdAt","domain","encryptedPassword","exchangeMode","id","isVisible","lastSyncAt","lastSyncError","name","syncIntervalMin","tenantId","type","updatedAt","url","userId","username"]; Spalten der Wegwerf-Tabelle (17): ["color","createdAt","domain","encryptedPassword","exchangeMode","id","isVisible","lastSyncAt","lastSyncError","name","syncIntervalMin","tenantId","type","updatedAt","url","userId","username"]
|
||||
calendarsource-gebunden-nur-eigener-mandant: bestanden — forTenant(TENANT-A) liefert 2 Zeile(n): ["source-a1","source-a2"]
|
||||
calendarsource-fremder-nutzer-desselben-mandanten-gebunden-sichtbar: bestanden — forTenant(TENANT-A) liefert die Quelle von 'user-a2' (anderer Benutzer, gleicher Mandant) mit encryptedPassword="enc(a2-passwort-platzhalter)" — die Regel auf "CalendarSource" kennt keine Benutzerdimension, die verschluesselten Zugangsdaten eines Kollegen DESSELBEN Mandanten sind auf Datenbankebene lesbar; die anwendungsseitige Filterung ueber die Benutzerkennung bleibt deshalb der einzige Schutz, bis die Etappe-3-Entscheidung (2) die Benutzerdimension nachzieht
|
||||
calendarsource-ungebunden-null-zeilen: bestanden — ungebundener SELECT auf "CalendarSource" liefert 0 Zeile(n), tatsaechlich vorhanden sind 3
|
||||
calendarsource-ungebundene-einzelabfrage-ueber-kennung-liefert-keine-zeile: bestanden — ungebundenes SELECT ueber die Kennung 'source-b1' (vorhanden) liefert 0 Zeile(n) — die Datenbankseite der drei Besitzpruefungen: ein ungebundenes Nachschlagen ueber eine vorhandene Kennung liefert null Zeilen, das ist der Weg in NotFoundException
|
||||
calendarsource-gebundenes-einfuegen-fremder-mandant-abgelehnt: bestanden — gebundenes INSERT unter TENANT-A mit tenantId=TENANT-B abgewiesen mit SQLSTATE 42501 (Invalid `prisma.$executeRaw()` invocation: Raw query failed. Code: `42501`. Message: `ERROR: new row violates row-level security policy for table "CalendarSource"`)
|
||||
calendarsource-gebundenes-loeschen-fremder-zeile-trifft-keine-zeile: bestanden — gebundenes DELETE unter TENANT-A ueber die Kennung 'source-b1' (gehoert TENANT-B) trifft 0 Zeile(n); ueber die Wartungsrolle ist die Zeile danach noch vorhanden: true
|
||||
calendarsource-gebundenes-update-ueber-kennung-allein-trifft-null-zeilen: bestanden — gebundenes UPDATE ... WHERE id = 'source-b1' (gehoert TENANT-B) unter TENANT-A trifft 0 Zeile(n), lastSyncError bleibt unveraendert
|
||||
calendarsource-generierter-client-gebundene-quellenliste-nur-eigener-mandant: bestanden — bound.calendarSource.findMany({ where: { userId: 'user-a1', isVisible: true } }) unter TENANT-A liefert 1 Zeile(n): ["source-a1"] — die Abfrage, die fetchAndCacheEvents stellt
|
||||
calendarsource-generierter-client-ungebundene-quellenliste-null-zeilen: bestanden — dieselbe Abfrage auf dem UNGEBUNDENEN generierten Client liefert 0 Zeile(n) ohne Fehler — exakt der Wert, den getSources als "keine Quelle" und fetchAndCacheEvents als "keine Termine" weiterreicht
|
||||
calendarsource-generierter-client-gebundenes-update-auf-unsichtbare-zeile-scheitert-laut: bestanden — bound.calendarSource.update unter TENANT-A auf die unter TENANT-B unsichtbare Zeile (source-b1) wirft PrismaClientKnownRequestError (code P2025): Invalid `prisma.calendarSource.update()` invocation: An operation failed because it depends on one or more records that were required but not found. No record was found for an update.
|
||||
calendarsource-generierter-client-gebundenes-anlegen-eigener-mandant-gelingt: bestanden — bound.calendarSource.create unter TENANT-A gelingt (id=62b383e2-4768-48f2-997a-cc5251174f8b, createdAt="2026-09-11T07:44:53.869Z"), gebunden lesbar: true — bestaetigt nebenbei, dass die Wegwerf-Tabelle die clientseitig erzeugten Werte (id, createdAt, updatedAt) annimmt
|
||||
Alle 101 Pruefungen bestanden.
|
||||
```
|
||||
|
||||
Dreizehn neue Prüfungen (101 = 88 + 13), nicht zwölf wie in der Aufzählung
|
||||
des Plans namentlich vorgezeichnet — die dreizehnte
|
||||
(`calendarsource-regelstand-eindeutig`) wurde ergänzt, weil sie die
|
||||
Messfalle aus 260910-jab zur LAUFZEIT prüft statt sie nur als Planungsprosa
|
||||
festzuhalten, derselbe Grund, aus dem der Bereich `dashboard` eine
|
||||
dreizehnte Prüfung ergänzt hatte.
|
||||
|
||||
**Die tragende Belegzeile ist `calendarsource-ungebunden-null-zeilen`:** der
|
||||
IDENTISCHE `SELECT "tenantId" FROM "CalendarSource"` ohne vorheriges
|
||||
`set_config` liefert **0 Zeilen**, nicht die 3 tatsächlich vorhandenen — an
|
||||
der echten, ausgelieferten Policy gemessen.
|
||||
|
||||
**Das Wettlauf-Ergebnis aus Befund H/K ist gemessen, nicht vorweggenommen —
|
||||
und es ist eine ANDERE Fehlerklasse als im Bereich `dashboard`.** Ein
|
||||
gebundenes `bound.calendarSource.update({ where: { id: 'source-b1' }, ... })`
|
||||
unter TENANT-A auf die unter TENANT-B unsichtbare Zeile wirft eine
|
||||
**`PrismaClientKnownRequestError` mit `.code === 'P2025'`** ("Record to
|
||||
update not found") — NICHT die `PrismaClientUnknownRequestError`, die
|
||||
`dashboardLayout.upsert()` im Bereich `dashboard` bei genau diesem
|
||||
Wettlauf-Fall geworfen hat. Der Unterschied erklärt sich aus der Form des
|
||||
Zugriffs: `dashboardLayout.upsert()` löst ein `INSERT ... ON CONFLICT`
|
||||
aus, das die RLS-`USING`-Klausel beim `INSERT`-Zweig verletzt (SQLSTATE
|
||||
`42501`, von Prisma als unbekannter Fehler durchgereicht); ein einfaches
|
||||
`calendarSource.update({ where: { id } })` ohne Konfliktbehandlung sieht
|
||||
unter der gebundenen Regel schlicht KEINE passende Zeile — dasselbe
|
||||
Verhalten wie ein `UPDATE` über eine nicht existierende Kennung, das Prisma
|
||||
grundsätzlich als P2025 meldet. **Das bedeutet für Aufgabe 2: keine der drei
|
||||
Besitzprüfungsschreibpfade (`updateSource`/`deleteSource`/`testConnection`)
|
||||
braucht eine neue Fehlerübersetzung** — P2025 wäre ohnehin nicht der Pfad,
|
||||
über den ein Nutzer diese Zeile erreicht (die vorgeschaltete
|
||||
`findUnique`-Besitzprüfung fängt den Fall vorher über `NotFoundException`
|
||||
ab), sondern ausschließlich der Wettlauf-Fall der Aggregationsschleife
|
||||
(Befund K, siehe (k2)).
|
||||
|
||||
### (k2) Signaltabelle je umgestelltem Pfad
|
||||
|
||||
| Pfad | Verhalten bei zu wenig Ergebnis | Konkretes Signal, Ort | Frontend lässt Signal durch? |
|
||||
|---|---|---|---|
|
||||
| `CalendarService.getSources` | Der gebundene Lesezugriff liefert eine leere Liste statt der vorhandenen Quellen | `GET /calendar/sources` liefert `[]`, Status 200, kein Fehler | Nein — `calendar-widget.tsx` zeigt `emptyNoSources` ("keine Quelle eingerichtet"), `calendar-settings-panel.tsx` zeigt `sourceEmpty`; beide ununterscheidbar vom echten Erstbenutzer-Zustand |
|
||||
| `CalendarService.addSource` | Kein Leere-Fall in diese Richtung — die Mandantenkennung ist Pflichtparameter | Schlägt ohne Mandant im Controller bereits mit `ForbiddenException` fehl | — |
|
||||
| `CalendarService.updateSource`, Besitzprüfung | Die gebundene `findUnique`-Abfrage liefert `null` statt der eigenen Zeile — ununterscheidbar vom echten "gehört jemand anderem" | `NotFoundException('Calendar source not found')` — dieselbe Meldung wie beim echten Besitzverstoß, siehe (k4)(d) für 403/404 | `calendar-settings-panel.tsx` fängt Schreibfehler bei `handleVisibilityToggle` (`catch { revert }`) bzw. beim Formular (`saveError`/`editSaveError`) — sichtbar als Fehlermeldung, NICHT als leerer Zustand |
|
||||
| `CalendarService.deleteSource`, Besitzprüfung | Wie bei `updateSource`: `null` statt der eigenen Zeile | `NotFoundException` — dieselbe Meldung wie beim echten Besitzverstoß | Wie oben — Löschfehler werden sichtbar gemeldet, nicht verschluckt |
|
||||
| `CalendarService.testConnection`, Besitzprüfung | Wie oben; zusätzlich der Wettlauf-Fall der beiden Rückschreibungen bei Erfolg/Fehler (Befund K) | `NotFoundException` bei fehlender/fremder Zeile; ein Wettlauf zwischen Nachschlagen und Rückschreiben würfe gemessen `PrismaClientKnownRequestError` (P2025, siehe (k1)) — heute strukturell ausgeschlossen, weil derselbe `tenantPrisma`-Klient beide Schritte trägt | `testResults`-State zeigt `'error'` — sichtbar, nicht verschluckt |
|
||||
| `CalendarService.fetchAndCacheEvents` | Der gebundene Lesezugriff auf die Quellenliste liefert eine leere Liste statt der sichtbaren Quellen — `if (sources.length === 0) return [];` kehrt VOR dem Cache-Eintrag zurück, ein leeres Ergebnis wird also NIE zwischengespeichert, jeder Aufruf misst neu leer. Der Wettlauf-Fall der beiden Synchronstatus-Rückschreibungen (Befund K) ist gemessen: `PrismaClientKnownRequestError` (P2025) — strukturell ausgeschlossen durch EINEN `tenantPrisma`-Klient je Aufruf | `GET /calendar/events` liefert `[]`, Status 200, kein Fehler, keine Protokollzeile | Nein — `calendar-widget.tsx` zeigt bei leerem `events` (aber `hasSources === true`) `emptyNoEvents` ("keine Termine"); ein LAUTER Fehler von `fetchEvents()` ODER `fetchSources()` landet im selben `catch` und setzt ebenfalls `events = []` — die Unterscheidung zwischen "leer" und "Fehler" existiert im State nicht |
|
||||
| `CalendarService.refreshCacheInBackground` | Ruft `fetchAndCacheEvents` mit der Mandantenkennung der urspünglichen Anfrage auf (kein eigener Kontext, siehe Befund B) — dasselbe Leere-Verhalten wie oben, zusätzlich verschluckt durch `.catch((error) => this.logger.warn(...))`: selbst ein LAUTER Fehler der Hintergrundauffrischung erzeugt nur eine Protokollzeile, kein Signal an den Aufrufer, der die Antwort bereits erhalten hat | Kein HTTP-Signal — die auslösende Anfrage ist bereits beantwortet, bevor die Hintergrundauffrischung beginnt | Kann das Frontend strukturell nicht erreichen — die Anfrage, die es ausgelöst hat, ist längst beantwortet |
|
||||
|
||||
### (k3) Welcher Code Leere als Abwesenheit deutet
|
||||
|
||||
**Backend, zwei Stellen (Befund I):** `CalendarService.getSources` liefert
|
||||
bei null Treffern eine leere Liste, Status 200 — dieselbe Deutung wie überall
|
||||
in dieser Etappe. `CalendarService.fetchAndCacheEvents`,
|
||||
`if (sources.length === 0) return [];`: kehrt VOR dem Cache-Eintrag zurück,
|
||||
ein leeres Quellenergebnis wird also nicht einmal für die TTL festgehalten,
|
||||
sondern bei JEDEM Aufruf neu leer gemessen — anders als ein echter
|
||||
Cache-Treffer, der fünf Minuten stehen bleibt. Die drei
|
||||
Besitzprüfungspfade (`updateSource`, `deleteSource`, `testConnection`) sind
|
||||
dagegen LAUT: ein zu kleines Nachschlagen wirft `NotFoundException`.
|
||||
|
||||
**Frontend, drei Dateien, zur Ausführungszeit erneut nachgeprüft (Befund
|
||||
J), nicht aus dem Plan abgeschrieben — dieser Plan ändert an KEINER der drei
|
||||
Dateien etwas:**
|
||||
|
||||
1. `apps/web/src/components/dashboard/widgets/calendar-widget.tsx`, Zeile
|
||||
37: `if (sources.length === 0)` setzt `hasSources = false`, gerendert als
|
||||
`emptyNoSources` ("keine Quelle eingerichtet"). Zeile 50:
|
||||
`catch { // Silent fail — show empty state }` fängt jeden Fehler von
|
||||
`fetchSources()` ODER `fetchEvents()` — bei einem Fehler bleibt
|
||||
`hasSources` jedoch beim vorherigen Wert (nicht `false`) und `events`
|
||||
wird auf `[]` gesetzt, wodurch die Render-Logik in den Zweig
|
||||
`events.length === 0` fällt und `emptyNoEvents` ("keine Termine")
|
||||
zeigt — NICHT `emptyNoSources`. Ein LAUTER Fehler (403, 500,
|
||||
Netzwerkfehler) auf `GET /calendar/sources` oder `GET /calendar/events`
|
||||
ist damit für den Nutzer vom echten "Quellen vorhanden, aber gerade
|
||||
keine Termine" nicht zu unterscheiden — beide zeigen `emptyNoEvents`.
|
||||
2. `apps/web/src/components/settings/calendar-settings-panel.tsx`, Zeile
|
||||
49: `.catch(() => { // Silent fail — show empty state })` — hier bleibt
|
||||
`sources` beim initialen `[]`, unabhängig davon, ob `fetchSources()` eine
|
||||
echte leere Antwort ODER einen LAUTEN Fehler liefert; Zeile 127:
|
||||
`sources.length === 0` zeigt `sourceEmpty`. Auf dieser Seite sind "keine
|
||||
Quelle" und "Fehler beim Laden" damit VOLLSTÄNDIG ununterscheidbar — eine
|
||||
schärfere Form derselben Verschluckung als im Widget.
|
||||
3. `apps/web/src/components/settings/calendar-source-form.tsx`, Zeile 149:
|
||||
`if (password) payload.password = password;` — nicht Leere, sondern
|
||||
Weglassen; gehört hierhin, weil es die Erhaltungsfrage beantwortet (siehe
|
||||
(k4)(b)): ein leer gelassenes Passwortfeld wird aus dem Sendeobjekt
|
||||
WEGGELASSEN, nicht als leere Zeichenkette übertragen.
|
||||
|
||||
**Die Folgekette, abgegrenzt gegen `dashboard` (Befund J):** leerer Kalender
|
||||
oder leere Quellenliste → der Nutzer liest das als "die Synchronisation ist
|
||||
kaputt" oder "ich habe keine Quelle eingerichtet" → er legt seine Quelle
|
||||
NEU an (`addSource`, gebunden, gelingt) → er tippt sein Exchange- oder
|
||||
CalDAV-Passwort ein ZWEITES MAL in ein System, das gerade aussieht, als wäre
|
||||
es defekt → die ursprüngliche Zeile bleibt unsichtbar liegen. Anders als bei
|
||||
`dashboard` wird dabei NICHTS überschrieben (`CalendarSource` hat keinen
|
||||
automatischen Rückschreibpfad wie `setEditMode` im Dashboard) — die
|
||||
Zerstörung ist hier keine verlorene Aufzeichnung, sondern eine preisgegebene
|
||||
Anmeldung: der Nutzer gibt Zugangsdaten zu einem fremden Mailserver in ein
|
||||
System ein, das er für kaputt hält, und sobald die Ursache behoben ist
|
||||
(Etappe 4), liegen ZWEI Quellen mit ZWEI Sätzen von Zugangsdaten für
|
||||
denselben Server vor — Dubletten bei Quellen UND bei Terminen.
|
||||
|
||||
**Die Fehlerverschluckung als eigener Punkt:** selbst ein LAUTER
|
||||
Backend-Fehler auf `GET /calendar/sources` oder `GET /calendar/events` (403,
|
||||
500, Netzwerkfehler) ist für den Nutzer vom leeren Kalender nicht zu
|
||||
unterscheiden — das Signal existiert ausschließlich im Netzwerkprotokoll des
|
||||
Browsers und im API-Log, an keiner Stelle in der Benutzeroberfläche.
|
||||
|
||||
### (k4) Was dieser Durchlauf bewusst nicht löst
|
||||
|
||||
- **(a) Das Urteil zum Cache-Schlüssel**, vierteilig belegt, jedes Glied
|
||||
einzeln nachgesehen: `eventCache` (Zeile 109 alt) wird mit
|
||||
`${userId}:${from}:${to}` beschlüsselt. `userId` kommt aus
|
||||
`calendar.controller.ts` `extractContext` (`req.user?.id`); das ist laut
|
||||
`apps/api/src/auth/strategies/jwt.strategy.ts` `validate` (Zeile 27-34)
|
||||
wörtlich `id: payload.sub`; `payload.sub` ist laut
|
||||
`apps/api/src/auth/auth.service.ts` (Zeilen 143 und 332, beide
|
||||
`sub: user.id`) die Datenbankkennung `User.id`; die trägt laut
|
||||
`apps/api/prisma/schema.prisma` `model User` `@id @default(uuid())`.
|
||||
**Urteil:** der Schlüssel trägt eine plattformweit eindeutige UUID, kein
|
||||
Anmeldename — die Etappe-3-Entscheidung (1) des Users (Anmeldenamen
|
||||
eindeutig PRO MANDANT statt plattformweit) betrifft `User.username` und
|
||||
`User.email`, nicht `User.id`, und berührt den Schlüssel deshalb NICHT.
|
||||
Der Schlüssel bleibt unverändert; das Urteil steht zusätzlich als
|
||||
Kommentar unmittelbar über der `eventCache`-Zuweisung in
|
||||
`calendar.service.ts` (Aufgabe 2).
|
||||
- **(b) Der Befund zur Zugangsdaten-Erhaltung (Befund E)**, an vier Stellen
|
||||
zur Ausführungszeit nachgelesen: `calendar.service.ts` `updateSource`
|
||||
besitzt KEINEN Lesezugriff, der ein gespeichertes Passwort lädt, um es neu
|
||||
zu verschlüsseln — die `dkv`-Form (lesen, entschlüsseln, neu
|
||||
verschlüsseln) existiert hier nicht. Stattdessen: `if (dto.password !== undefined)`
|
||||
entscheidet, ob überhaupt geschrieben wird — Feld FEHLT im Rumpf →
|
||||
`encryptedPassword` bleibt im `update`-Aufruf gänzlich unerwähnt und damit
|
||||
in der Datenbank unverändert; Feld LEER (`''`) → wird explizit auf `null`
|
||||
gesetzt (Löschen); Feld GESETZT → wird verschlüsselt. Auf der Web-Seite
|
||||
(`calendar-source-form.tsx`, Zeile 149) wird ein leer gelassenes
|
||||
Passwortfeld WEGGELASSEN, nicht als leere Zeichenkette gesendet — die
|
||||
Erhaltung läuft also über das Weglassen des Felds im Rumpf, nicht über
|
||||
einen Lesezugriff, der nach dem Scharfschalten leerlaufen könnte. Die
|
||||
beiden lesenden Stellen (`testConnection`, `fetchAndCacheEvents`)
|
||||
entschlüsseln nur, um den Provider aufzurufen, und schreiben nichts
|
||||
Entschlüsseltes zurück. Als drei Testfälle in Aufgabe 2 festgenagelt.
|
||||
- **(c) Die fehlende Benutzerdimension der Regel (Befund G)**, gemessen in
|
||||
(k1) (`calendarsource-fremder-nutzer-desselben-mandanten-gebunden-sichtbar`):
|
||||
die verschlüsselten Exchange-/CalDAV-Zugangsdaten eines Kollegen
|
||||
DESSELBEN Mandanten sind auf Datenbankebene lesbar, bis die
|
||||
Etappe-3-Entscheidung (2) des Users die Benutzerdimension in die Regel
|
||||
aufnimmt (`CalendarSource` steht dort ausdrücklich in der Liste). Bis
|
||||
dahin bleiben der `userId`-Filter in `getSources`/`fetchAndCacheEvents`
|
||||
und die drei Besitzprüfungen der EINZIGE Schutz.
|
||||
- **(d) 403 statt 404 als Existenzpreisgabe zwischen Kollegen (Befund D):**
|
||||
`updateSource`, `deleteSource` und `testConnection` werfen bei fremdem
|
||||
Besitz `ForbiddenException('Not your calendar source')` (403), bei
|
||||
unbekannter Kennung `NotFoundException('Calendar source not found')`
|
||||
(404) — ein Kollege DESSELBEN Mandanten erfährt über 403 die Existenz
|
||||
einer fremden Quellenkennung, ein Nutzer eines FREMDEN Mandanten bekommt
|
||||
nach der Bindung durchgängig 404 (die Zeile ist für ihn unsichtbar).
|
||||
Kennungen sind UUIDs, nicht erratbar. Die Antwortsemantik wird von diesem
|
||||
Plan NICHT geändert (wäre eine API-Änderung außerhalb des Auftrags).
|
||||
- **(e) Die fehlende Unterscheidbarkeit von "keine Quelle" und "Quelle
|
||||
nicht sichtbar".** Beide liefern identisch eine leere Liste, Status 200,
|
||||
keinen Protokolleintrag — siehe (k3). Die konkrete Vorabprüfung für
|
||||
Etappe 4 (`rls-preflight.mjs`): physisch vorhandene
|
||||
`CalendarSource`-Zeilen je Mandant über die Wartungsrolle zählen und mit
|
||||
der gebundenen Zählung je Mandant vergleichen — jede Abweichung ist ein
|
||||
Trennungsfehler, kein Erstbenutzer. Eine Laufzeitwarnung an
|
||||
`getSources`/`fetchAndCacheEvents` wurde erwogen und VERWORFEN, mit
|
||||
derselben Begründung wie bei `getAllActiveConfigs` im Bereich `ldap` und
|
||||
bei `getLayout`/`getWidgets`/`getSearchProviders` im Bereich `dashboard`:
|
||||
keine Quelle ist auf einer frischen Installation oder für einen neuen
|
||||
Nutzer der NORMALZUSTAND — eine Warnung an dieser Stelle wäre Dauerlärm
|
||||
und verlöre ihr Signal, bevor sie gebraucht wird.
|
||||
|
||||
### (k5) Was dieser Durchlauf bewusst nicht anfasst
|
||||
|
||||
- **Das Frontend** — geprüft (Befund I/J, (k3) oben) und bewusst gelassen,
|
||||
keine Datei dieses Plans. `calendar-widget.tsx`,
|
||||
`calendar-settings-panel.tsx` und `calendar-source-form.tsx` werden NICHT
|
||||
geändert.
|
||||
- **Die drei Provider** (`ics.provider.ts`, `caldav.provider.ts`,
|
||||
`exchange.provider.ts`) — reden mit echten Servern, werden in Aufgabe 2
|
||||
NICHT ausgeübt, nur als Attrappen (`fetchEvents`/`testConnection` als
|
||||
`vi.fn`) verwendet.
|
||||
- **`testConnectionFromConfig`** — kein Datenbankzugriff, kein Mandant
|
||||
(prüft eine Konfiguration, bevor sie gespeichert wird), unverändert.
|
||||
- **Der Bereich `favorites`** — eigener Bereich mit eigener Umstellung,
|
||||
nicht Teil dieses Plans.
|
||||
- **Die Antwortsemantik 403/404** — siehe (k4)(d), bewusst nicht geändert.
|
||||
- **Der Fremdkommentar in `apps/api/src/ldap/ldap-config.service.ts`**
|
||||
(Zeile 24, nennt `CalendarSource` als Vorbild der Verschlüsselung) —
|
||||
zutreffend (`CalendarCryptoService`, siehe Dezision `07-01` in
|
||||
STATE.md), nicht zu ändern.
|
||||
- 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