docs(quick-260911-cwh): Plan fuer Etappe 2, Bereich calendar
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
+821
@@ -0,0 +1,821 @@
|
|||||||
|
---
|
||||||
|
phase: quick-260911-cwh
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
autonomous: true
|
||||||
|
requirements: [WINDOWS-18, ETAPPE-2-CALENDAR]
|
||||||
|
|
||||||
|
files_modified:
|
||||||
|
- apps/api/scripts/rls-scratch-check.mjs
|
||||||
|
- docs/mandantentrennung-etappe2-fehlerrichtung.md
|
||||||
|
- apps/api/src/calendar/calendar.service.spec.ts
|
||||||
|
- apps/api/src/calendar/calendar.service.ts
|
||||||
|
- apps/api/src/calendar/calendar.controller.ts
|
||||||
|
- docs/mandantentrennung-zugriffsklassifikation.md
|
||||||
|
- .planning/WINDOWS.md
|
||||||
|
|
||||||
|
estimate:
|
||||||
|
tokens: 190000
|
||||||
|
raw_tokens: 190000
|
||||||
|
tasks: 3
|
||||||
|
confidence: low
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Alle zwoelf Datenbankzugriffe dieses Bereichs laufen gebunden unter dem woertlichen Namen `tenantPrisma`, genau ein gebundener Klient je Methode mit Datenbankzugriff. Es gibt in diesem Bereich KEINEN begruendet ungebundenen Zugriff — jede Zeile von `CalendarSource` traegt eine Pflicht-Mandantenkennung, und kein Pfad liest ueber Mandanten hinweg."
|
||||||
|
- "Lesen und Schreiben derselben Zeile sind nirgends auf gebunden und ungebunden aufgeteilt: die drei Besitzpruefungen (`updateSource`, `deleteSource`, `testConnection`) fuehren Nachschlagen UND Schreiben ueber DENSELBEN gebundenen Klienten, und die beiden Synchronstatus-Rueckschreibungen innerhalb der Ereignisaggregation laufen ueber denselben Klienten wie das Laden der Quellen. Das ist je Pfad als Testfall festgenagelt, nicht behauptet."
|
||||||
|
- "Die umgekehrte Fehlerrichtung dieses Bereichs ist an der ECHTEN, ausgelieferten Regel fuer `CalendarSource` gemessen — an dem Stand, den die Datenbank NACH der Migration 20260910120000 hat (die diese Tabelle nachweislich nicht angefasst hat, mit Anweisung belegt). Mindestens ein Schreib- und ein Lesepfad sind ueber den GENERIERTEN Prisma-Client gemessen, an einer Wegwerf-Tabelle, die saemtliche Spalten des Modells traegt (die Lehre aus Pruefung 5b im Bereich `dashboard`)."
|
||||||
|
- "Die umgekehrte Fehlerrichtung ist in ihrer Auspraegung fuer diesen Bereich benannt: nach dem Scharfschalten liefert ein zu kleines Leseergebnis KEIN Fehlerbild, sondern einen leeren Kalender, der als 'mein Kalender ist leer' oder 'die Synchronisation ist kaputt' gelesen wird, und eine leere Quellenliste, die als 'keine Quelle eingerichtet' gelesen wird. Zusaetzlich benannt: das Frontend verwandelt sogar LAUTE Fehler der Lesepfade in denselben leeren Zustand. Festgehalten in der Kritikschrift UND als offener Eintrag im Broken-Windows-Register mit konkreter Vorabpruefung fuer Etappe 4."
|
||||||
|
- "Die Frage nach der Zugangsdaten-Erhaltung ist mit Messung beantwortet, nicht mit Annahme: ob `calendar.service.ts` die zerstoerende Form aus `dkv` (lesen, entschluesseln, neu verschluesseln — nach dem Scharfschalten leer) traegt oder nicht. Der Befund steht in der Kritikschrift, und das tatsaechliche Erhaltungsverhalten (Passwort weggelassen, Passwort leer, Passwort gesetzt) ist als drei Testfaelle festgenagelt, damit eine spaetere Aenderung der Form sichtbar wird."
|
||||||
|
- "Der Ereignis-Cache-Schluessel ohne Mandantenanteil hat ein schriftliches, belegtes Urteil: entweder bleibt die Benutzerkennung, die er traegt, auch nach der Etappe-3-Entscheidung 'Anmeldenamen pro Mandant eindeutig' plattformweit eindeutig — dann ist der Schluessel sicher und bleibt — oder sie bleibt es nicht, dann bekommt der Schluessel einen Mandantenanteil. Die Kette (Schema, Sitzungsnachweis, Controller) ist Glied fuer Glied nachgesehen, mit Anweisung, und das Urteil steht in Code UND Kritikschrift."
|
||||||
|
- "Die drei Besitzpruefungen sind GELESEN, nicht angenommen: dieser Durchlauf hat je Pfad nachgesehen, ob zwischen Nachschlagen ueber die Kennung und Schreiben tatsaechlich ein Vergleich gegen die Benutzerkennung aus dem Sitzungsnachweis steht (das Vorhaben fand bei `ldap` und `dkv` je einmal keinen). Die Pruefungen bleiben bestehen, werden durch die Bindung ERGAENZT und sind als Testfaelle festgenagelt — sie sind der einzige Schutz zwischen Kollegen DESSELBEN Mandanten, weil die Regel keine Benutzerdimension kennt (gemessen)."
|
||||||
|
- "Die Testlage steht VOR der Umstellung: `calendar.service.spec.ts` existiert heute nicht und wird mit dem Zwei-Klienten-Nachweis neu angelegt, mit Attrappen fuer Verschluesselung und die drei Provider — die Provider selbst werden NICHT ausgeuebt. Ein vergessener Bindungsaufruf wird dadurch rot, statt aus einem anderen Grund zu scheitern."
|
||||||
|
- "Der Controller reicht die bereits in `extractContext` aufgeloeste Mandantenkennung an alle fuenf Handler durch, die sie heute verwerfen; es entsteht KEINE neue Vertrauensquelle, nichts wird aus Rumpf oder Pfad uebernommen."
|
||||||
|
- "Alle fuenf handgepflegten Dokumentstellen der Klassifikation sind nachgezogen und maschinell gegatet, die Gates LEITEN ihre Werte aus den im Dokument genannten Messanweisungen ab, und der Umfang ist als ERLAUBNISLISTE gegen `50b3a36` gegatet."
|
||||||
|
- "Baseline gehalten am Ende JEDER Aufgabe: mindestens 860 Tests gruen (mindestens 56 Dateien), Typpruefung sauber, das Wegwerf-Werkzeug meldet alle Pruefungen bestanden. Der Schalter bleibt AUS, Schema und Migrationen unveraendert, keine Compose- oder Umgebungsdatei angefasst, nichts in Active Directory."
|
||||||
|
artifacts:
|
||||||
|
- "apps/api/scripts/rls-scratch-check.mjs — ein elfter Abschnitt `runCalendarAreaChecks` mit mindestens zwoelf namentlich benannten Pruefungen gegen die aus der ausgelieferten Migration geschnittene Regel fuer `CalendarSource`, davon mindestens vier ueber den generierten Client"
|
||||||
|
- "docs/mandantentrennung-etappe2-fehlerrichtung.md — Abschnitt `## Bereich calendar` mit (k1) Messung, (k2) Signaltabelle, (k3) Leere-als-Abwesenheit im Backend UND im Frontend samt der Fehlerverschluckung, (k4) bewusst nicht geloest (darunter das Urteil zum Cache-Schluessel und der Befund zur Zugangsdaten-Erhaltung), (k5) bewusst nicht angefasst"
|
||||||
|
- "apps/api/src/calendar/calendar.service.spec.ts — NEU, Zwei-Klienten-Nachweis ueber `__makeBoundClient`, Attrappen fuer `CryptoService` und die drei Provider, Faelle fuer jeden Pfad mit Datenbankzugriff"
|
||||||
|
- "apps/api/src/calendar/calendar.service.ts — alle zwoelf Zugriffe gebunden, ein Klient je Methode, das Cache-Schluessel-Urteil als Kommentar an der Stelle"
|
||||||
|
- "apps/api/src/calendar/calendar.controller.ts — die fuenf Handler, die den aufgeloesten Mandanten heute verwerfen, reichen ihn durch"
|
||||||
|
- "docs/mandantentrennung-zugriffsklassifikation.md — eine Bestandsaufnahme-Zeile, Uebersichtszeile, Summenzeile, Klassen-Verteilung (unveraendert, ausdruecklich vermerkt), Hintergrunddienst-Abschnitt (kein sechster Fall, gemessen, mit dem Sonderfall der abgekoppelten Cache-Auffrischung), Abschnitt `Was diese Etappe NICHT entscheidet`"
|
||||||
|
- ".planning/WINDOWS.md — ein neuer OFFENER Eintrag zur lautlosen Leere dieses Bereichs, ueber `gsd-tools windows append` angelegt, damit Tabelle, JSON-Block und Kopfzaehler zusammenpassen"
|
||||||
|
key_links:
|
||||||
|
- "`extractContext` im Controller loest den Mandanten bereits auf und bricht ohne ihn mit `ForbiddenException` ab; fuenf von sechs kontextnutzenden Handlern verwerfen ihn heute. Die Umstellung ist ein Durchreichen, keine neue Vertrauensquelle."
|
||||||
|
- "`fetchAndCacheEvents` laedt die Quellen und schreibt je Quelle den Synchronstatus zurueck — innerhalb eines `try/catch`, dessen `catch` selbst wieder schreibt. Waere das Laden gebunden und das Rueckschreiben nicht, schluege das erste Rueckschreiben nach dem Scharfschalten mit 'Zeile nicht gefunden' fehl, der `catch` versuchte das zweite, das ebenso scheitert, und `Promise.allSettled` liesse die Ereignisse dieser Quelle STILL fallen. Ein Klient je Methode ist hier keine Stilfrage."
|
||||||
|
- "Der Cache-Schluessel traegt `req.user.id`; das ist laut `JwtStrategy.validate` `payload.sub`, das laut `auth.service.ts` `user.id` ist, das laut Schema `@default(uuid())` traegt. Diese Kette ist das Urteil — jedes Glied ist zur Ausfuehrungszeit nachzusehen."
|
||||||
|
- "Die Regel auf `CalendarSource` lautet `\"tenantId\" = current_tenant_id()` ohne Benutzerdimension: die verschluesselten Exchange-/CalDAV-Zugangsdaten eines Kollegen DESSELBEN Mandanten sind auf Datenbankebene sichtbar. Die anwendungsseitigen `userId`-Filter und Besitzpruefungen sind der einzige Schutz und bleiben unveraendert."
|
||||||
|
- "Das Frontend (`calendar-widget.tsx`, `calendar-settings-panel.tsx`) faengt Fehler der Lesepfade und zeigt denselben leeren Zustand wie bei einer leeren Antwort. Damit ist selbst ein LAUTER Backend-Fehler auf `GET /calendar/sources` oder `GET /calendar/events` fuer den Nutzer unsichtbar — das Signal existiert nur im Netzwerkprotokoll des Browsers und im API-Log."
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Etappe 2 der Mandantentrennung, neunter Bereich: `calendar`. Die zwoelf
|
||||||
|
Datenbankzugriffe des einzigen Dienstes dieses Bereichs werden auf
|
||||||
|
`forTenant()` umgestellt — alle zwoelf, denn `CalendarSource` traegt eine
|
||||||
|
Pflicht-Mandantenkennung, und kein Pfad dieses Bereichs liest ueber Mandanten
|
||||||
|
hinweg.
|
||||||
|
|
||||||
|
Zweck: dieser Bereich haelt nicht bloss Daten, sondern ZUGANGSDATEN zu fremden
|
||||||
|
Servern — die verschluesselten 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. Und die
|
||||||
|
umgekehrte Fehlerrichtung sieht hier nicht wie ein Fehler aus, sondern wie ein
|
||||||
|
leerer Kalender: nach dem Scharfschalten liefert eine ungebunden gebliebene
|
||||||
|
Abfrage keine Meldung, sondern null Quellen und null Ereignisse. Der Nutzer
|
||||||
|
liest das als "die Synchronisation ist kaputt" oder "ich habe keine Quelle
|
||||||
|
eingerichtet", legt seine Quelle neu an — und tippt dabei sein
|
||||||
|
Exchange-Passwort ein zweites Mal in ein System, das gerade aussieht, als
|
||||||
|
waere es defekt. Das Frontend verstaerkt das: es faengt sogar laute Fehler
|
||||||
|
der Lesepfade und zeigt denselben leeren Zustand. Deshalb braucht dieser
|
||||||
|
Bereich seine Kritikschrift VOR der Umstellung, gemessen.
|
||||||
|
|
||||||
|
Ergebnis: zwoelf gebundene Zugriffe, eine gemessene Kritikschrift, eine aus
|
||||||
|
dem Nichts angelegte Testlage, die einen vergessenen Bindungsaufruf rot
|
||||||
|
macht, ein schriftliches Urteil zum Cache-Schluessel, ein belegter Befund zur
|
||||||
|
Zugangsdaten-Erhaltung, und zwei Dokumente, die am Ende nachweislich mit dem
|
||||||
|
Quelltext uebereinstimmen.
|
||||||
|
|
||||||
|
**Der Schalter bleibt AUS.** `DATABASE_URL` zeigt weiterhin auf die Rolle
|
||||||
|
`tessera` mit `BYPASSRLS`. Das Scharfschalten ist Etappe 4 und findet hier
|
||||||
|
NICHT statt. Schema und Migrationen werden NICHT angefasst. Nichts wird in
|
||||||
|
Active Directory geaendert.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@~/.claude/gsd-core/workflows/execute-plan.md
|
||||||
|
@~/.claude/gsd-core/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/STATE.md
|
||||||
|
@docs/mandantentrennung-zugriffsklassifikation.md
|
||||||
|
@docs/mandantentrennung-etappe2-fehlerrichtung.md
|
||||||
|
@apps/api/prisma/migrations/20260909140000_rls_remaining_tenant_tables/migration.sql
|
||||||
|
@apps/api/prisma/migrations/20260910120000_rls_widen_membership_grant_and_platform_read/migration.sql
|
||||||
|
@apps/api/src/prisma/prisma-tenant.extension.ts
|
||||||
|
@apps/api/src/prisma/rls-access-inventory.spec.ts
|
||||||
|
@apps/api/scripts/rls-scratch-check.mjs
|
||||||
|
@apps/api/src/calendar/calendar.service.ts
|
||||||
|
@apps/api/src/calendar/calendar.controller.ts
|
||||||
|
@apps/api/src/calendar/calendar.module.ts
|
||||||
|
@apps/api/src/calendar/dto/update-calendar-source.dto.ts
|
||||||
|
@apps/api/src/dkv/dkv.service.ts
|
||||||
|
@apps/api/src/dkv/dkv.service.spec.ts
|
||||||
|
@apps/api/src/dashboard/dashboard.service.spec.ts
|
||||||
|
@apps/api/src/auth/strategies/jwt.strategy.ts
|
||||||
|
@.planning/quick/260910-krx-mandantentrennung-etappe-2-bereich-dashb/260910-krx-PLAN.md
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<planning_time_findings>
|
||||||
|
|
||||||
|
Alle Zahlen unten sind zur Planungszeit am 2026-09-11 gegen HEAD `50b3a36`
|
||||||
|
GEMESSEN, mit der jeweils angegebenen Anweisung. Sie leiten die Untersuchung,
|
||||||
|
sie sind KEINE Bearbeitungsvollmacht — jede Datei wird vor jeder Aenderung
|
||||||
|
erneut gelesen, jede Zahl zur Ausfuehrungszeit erneut gemessen, und weicht
|
||||||
|
eine Messung ab, gilt die Messung und nicht dieser Plan.
|
||||||
|
|
||||||
|
**Befund A — die Zahl haelt, ein Modell, eine Datei.** Gemessen mit
|
||||||
|
`grep -rnoE "this\.prisma\.[a-zA-Z]+" apps/api/src/calendar --include=*.ts | grep -v spec`:
|
||||||
|
zwoelf Treffer, alle in `calendar.service.ts`, alle auf `calendarSource`
|
||||||
|
(Zeilen 124, 163, 176, 210, 227, 239, 248, 267, 278, 361, 387, 398). Verteilt
|
||||||
|
auf sechs Methoden mit Datenbankzugriff: `getSources` (1), `addSource` (1),
|
||||||
|
`updateSource` (2), `deleteSource` (2), `testConnection` (3),
|
||||||
|
`fetchAndCacheEvents` (3). `aggregateEvents` und `refreshCacheInBackground`
|
||||||
|
greifen nicht selbst zu, sie delegieren an `fetchAndCacheEvents`.
|
||||||
|
`calendar.controller.ts`, `calendar.module.ts`, die vier DTOs und die drei
|
||||||
|
Provider unter `providers/` halten null Zugriffe (gemessen mit
|
||||||
|
`grep -rln "prisma" apps/api/src/calendar --include=*.ts` — einzige Datei ist
|
||||||
|
der Dienst). Fuenfter Bereich in Folge, in dem beim Hineinschauen nichts
|
||||||
|
schrumpft. Die eine Bestandsaufnahme-Zeile des Bereichs
|
||||||
|
(`calendar.service.ts`/`calendarSource`, `muss-mandantengebunden`,
|
||||||
|
`ungebunden`) stimmt mit dem Quelltext ueberein.
|
||||||
|
|
||||||
|
**Befund B — keine Transaktion, kein Roh-SQL, kein Hintergrunddienst — aber
|
||||||
|
ein Sonderfall.** `grep -rn '\$transaction(\|\$queryRaw\|\$executeRaw' apps/api/src/calendar --include=*.ts`:
|
||||||
|
null Treffer; `withTenantTransaction()` wird hier nicht gebraucht.
|
||||||
|
`grep -rn "@Cron\|onModuleInit\|onApplicationBootstrap\|setInterval\|setTimeout" apps/api/src/calendar --include=*.ts`:
|
||||||
|
ein einziger Treffer, `providers/ics.provider.ts:100` — ein `setTimeout` fuer
|
||||||
|
den Abbruch eines HTTP-Abrufs nach acht Sekunden, kein Planer. ABER:
|
||||||
|
`refreshCacheInBackground` (Zeile 430) ist eine ABGEKOPPELTE Fortsetzung einer
|
||||||
|
Anfrage: `aggregateEvents` stoesst sie an, wartet nicht auf sie, und sie ruft
|
||||||
|
`fetchAndCacheEvents` mit den Parametern der Anfrage auf. Nach der Umstellung
|
||||||
|
muss sie die Mandantenkennung der Anfrage MITNEHMEN — sie hat keinen anderen
|
||||||
|
Mandanten, aus dem sie schoepfen koennte. Das ist NICHT die Bauform des
|
||||||
|
Hintergrunddienst-Abschnitts (uebergreifend lesen, dann je Mandant binden),
|
||||||
|
sondern ein Anfragekontext, der die Anfrage ueberlebt. Aufgabe 3 haelt das
|
||||||
|
im Hintergrunddienst-Abschnitt als PLAIN-Absatz fest.
|
||||||
|
|
||||||
|
**Befund C — es gibt KEINE Testdatei.** `ls apps/api/src/calendar/*.spec.ts`
|
||||||
|
liefert nichts. Das ist die `dkv`-Form (260909-mir): eine
|
||||||
|
`calendar.service.spec.ts` mit dem Zwei-Klienten-Nachweis ist die
|
||||||
|
VORAUSSETZUNG dafuer, dass irgendeine Aussage dieses Plans nachpruefbar ist —
|
||||||
|
nicht eine Zugabe. Der Dienst hat fuenf Konstruktorabhaengigkeiten
|
||||||
|
(`PrismaService`, `CryptoService`, `ICSProvider`, `CalDAVProvider`,
|
||||||
|
`ExchangeProvider`); die Provider reden mit echten Servern und bekommen
|
||||||
|
Attrappen (`fetchEvents`/`testConnection` als `vi.fn`), sie werden NICHT
|
||||||
|
ausgeuebt. Vorlage fuer Aufbau und Attrappen: `dkv.service.spec.ts`
|
||||||
|
(`makeFakeCrypto`, `__makeBoundClient`, `expectBoundCall`); Vorlage fuer den
|
||||||
|
Wachhund "genau ein Klient je Aufruf": `dashboard.service.spec.ts` ab Zeile
|
||||||
|
545 (`vi.mocked(forTenant).mock.calls.length`).
|
||||||
|
|
||||||
|
**Befund D — die Besitzpruefungen sind ECHT, in allen drei Pfaden, und sie
|
||||||
|
antworten anders als im Bereich `dashboard`.** `updateSource` (176-186),
|
||||||
|
`deleteSource` (227-237) und `testConnection` (248-250) laden ueber die
|
||||||
|
Kennung, pruefen `existing.userId !== userId` gegen die Benutzerkennung aus
|
||||||
|
dem Sitzungsnachweis und werfen `ForbiddenException('Not your calendar source')`
|
||||||
|
— nicht `NotFoundException` wie `dashboard`. Ein fremder Nutzer bekommt damit
|
||||||
|
403 statt 404: die Existenz einer Kennung wird preisgegeben. Kennungen sind
|
||||||
|
UUIDs, also nicht erratbar; nach der Bindung bekommt ein Nutzer eines FREMDEN
|
||||||
|
Mandanten ohnehin 404 (Zeile unsichtbar), nur der Kollege DESSELBEN Mandanten
|
||||||
|
weiterhin 403. Dieser Plan aendert die Antwortsemantik NICHT (waere eine
|
||||||
|
API-Aenderung ausserhalb des Auftrags), haelt sie aber in (k4) fest. **Die
|
||||||
|
Echtheit aller drei Pruefungen ist zur Ausfuehrungszeit erneut zu lesen,
|
||||||
|
bevor sie im SUMMARY behauptet wird** — dieses Vorhaben fand bei `ldap` und
|
||||||
|
`dkv` je einmal keine.
|
||||||
|
|
||||||
|
Was daraus folgt: die beiden (bei `testConnection`: drei) Abfragen jedes
|
||||||
|
Pfads muessen ueber DENSELBEN gebundenen Klienten laufen. Waere das Nachschlagen
|
||||||
|
gebunden und das Schreiben nicht, ginge die Pruefung auf einer Zeile auf, die
|
||||||
|
der Schreibvorgang nicht mehr sieht — und umgekehrt.
|
||||||
|
|
||||||
|
**Befund E — die zerstoerende `dkv`-Form der Zugangsdaten-Erhaltung existiert
|
||||||
|
hier NICHT, und das ist ein Befund, kein Freispruch.** `dkv.service.ts`
|
||||||
|
`saveConfig` liest die bestehende Zeile, entschluesselt das gespeicherte
|
||||||
|
Passwort und verschluesselt es neu, wenn das Formularfeld leer blieb — laeuft
|
||||||
|
dieser Lesezugriff nach dem Scharfschalten leer, wird ein leerer Wert
|
||||||
|
verschluesselt abgelegt (d3, Stelle 5). `calendar.service.ts` `updateSource`
|
||||||
|
(204-208) macht etwas anderes: `if (dto.password !== undefined)` — nur wenn
|
||||||
|
das Feld im Rumpf STEHT, wird geschrieben (gesetzt: verschluesseln; leer:
|
||||||
|
`null`, also ausdrueckliches Loeschen); FEHLT das Feld, wird `encryptedPassword`
|
||||||
|
im Prisma-`update` gar nicht angefasst und bleibt in der Datenbank stehen. Es
|
||||||
|
gibt keinen Lesezugriff, der leer laufen koennte. Auf der Web-Seite
|
||||||
|
(`apps/web/src/components/settings/calendar-source-form.tsx`, um Zeile 149:
|
||||||
|
`if (password) payload.password = password;`) wird ein leer gelassenes
|
||||||
|
Passwortfeld WEGGELASSEN, nicht als leere Zeichenkette gesendet — die
|
||||||
|
Erhaltung laeuft also per Weglassen, und `update-calendar-source.dto.ts`
|
||||||
|
fuehrt `password` als `@IsOptional()`. Die einzige Stelle, die gespeicherte
|
||||||
|
Zugangsdaten LIEST, um sie zu benutzen, ist `testConnection` (256-258,
|
||||||
|
Verbindungstest mit gespeichertem Passwort) und `fetchAndCacheEvents`
|
||||||
|
(374-376) — beide entschluesseln nur, sie schreiben nichts Entschluesseltes
|
||||||
|
zurueck. **Zur Ausfuehrungszeit an allen vier Stellen erneut nachzulesen;**
|
||||||
|
faellt es anders aus, ist die `dkv`-Reparatur (gemeinsame Bindung, kein
|
||||||
|
stilles Weiterlaufen mit leerem Wert) hier anzuwenden und der Befund
|
||||||
|
umzudrehen, nicht zu uebergehen.
|
||||||
|
|
||||||
|
**Befund F — der Cache-Schluessel traegt eine plattformweit eindeutige
|
||||||
|
Kennung, und die Kette ist vierteilig.** `eventCache` (Zeile 109) wird mit
|
||||||
|
`${userId}:${from}:${to}` (Zeile 337) beschluesselt, ohne Mandantenanteil.
|
||||||
|
Die Benutzerkennung stammt aus `calendar.controller.ts` `extractContext`
|
||||||
|
(`req.user?.id`), das ist laut `apps/api/src/auth/strategies/jwt.strategy.ts`
|
||||||
|
`validate` das Feld `id: payload.sub`, das laut `apps/api/src/auth/auth.service.ts`
|
||||||
|
(Zeile 143, `sub: user.id`) die Datenbankkennung ist, und die traegt laut
|
||||||
|
`apps/api/prisma/schema.prisma` `model User` `@id @default(uuid())`. Die
|
||||||
|
Etappe-3-Entscheidung des Users vom 2026-09-10 (STATE.md, Sitzungsabschnitt,
|
||||||
|
Commit 89fb027) betrifft `User.username`/`User.email` — NICHT `User.id`.
|
||||||
|
Damit lautet das voraussichtliche Urteil: der Schluessel ist sicher, weil er
|
||||||
|
eine UUID traegt, die kein Mandant mit einem anderen teilen kann; er bleibt
|
||||||
|
unveraendert. **Das Urteil wird in Aufgabe 1 Glied fuer Glied nachgesehen und
|
||||||
|
aufgeschrieben, nicht aus diesem Absatz uebernommen.** Faellt ein Glied anders
|
||||||
|
aus (etwa: `req.user.id` waere ein Anmeldename), bekommt der Schluessel in
|
||||||
|
Aufgabe 2 einen Mandantenanteil.
|
||||||
|
|
||||||
|
**Befund G — die Regel kennt keine Benutzerdimension, und das wiegt hier
|
||||||
|
schwerer als anderswo.** `20260909140000_rls_remaining_tenant_tables/migration.sql`
|
||||||
|
Zeile 61: `USING ("tenantId" = current_tenant_id())`, ein Ausdruck, ohne
|
||||||
|
`WITH CHECK`, ohne Benutzerdimension. Gemessen mit
|
||||||
|
`grep -n CalendarSource apps/api/prisma/migrations/20260910120000_rls_widen_membership_grant_and_platform_read/migration.sql`:
|
||||||
|
null Treffer — die Regelaenderung von 260910-jab hat diese Tabelle NICHT
|
||||||
|
angefasst, die Regel aus 20260909140000 ist der Stand, gegen den gemessen
|
||||||
|
wird (Aufgabe 1 prueft das zur Laufzeit im Werkzeug, damit die Messfalle aus
|
||||||
|
260910-jab — still die abgeloeste Regel messen — hier nicht zuschlagen kann).
|
||||||
|
Zwei Nutzer DESSELBEN Mandanten sind fuereinander auf Datenbankebene
|
||||||
|
vollstaendig sichtbar — einschliesslich `encryptedPassword`. Die
|
||||||
|
Etappe-3-Entscheidung (2) des Users (Kollegen strikt getrennt, Benutzerdimension
|
||||||
|
in den Regeln, `CalendarSource` ausdruecklich in der Liste) schliesst das
|
||||||
|
spaeter; bis dahin sind der `userId`-Filter in `getSources`/`fetchAndCacheEvents`
|
||||||
|
und die drei Besitzpruefungen der EINZIGE Schutz und bleiben unveraendert.
|
||||||
|
|
||||||
|
**Befund H — keine Eindeutigkeitskette.** `model CalendarSource` traegt ausser
|
||||||
|
dem Primaerschluessel (UUID, clientseitig erzeugt) KEINE Eindeutigkeitsbedingung.
|
||||||
|
Die Kette unsichtbare Zeile -> falsches 'frei' -> harter Eindeutigkeitsfehler
|
||||||
|
(WINDOWS #22, `tenders`/`user`/`dashboard`) kann hier strukturell nicht
|
||||||
|
auftreten — der zweite Bereich nach `module-registry`, in dem sie abwesend
|
||||||
|
statt umgangen ist. Es wird deshalb KEINE Konfliktuebersetzung eingebaut,
|
||||||
|
die nichts uebersetzt. Aufgabe 1 misst stattdessen, was ein gebundenes
|
||||||
|
`update` ueber den generierten Client auf eine unsichtbare Zeile tut — das
|
||||||
|
ist der Fall, den `testConnection`/`fetchAndCacheEvents` bei einem Wettlauf
|
||||||
|
zwischen Nachschlagen und Rueckschreiben traefen.
|
||||||
|
|
||||||
|
**Befund I — welcher Code Leere als Abwesenheit deutet, Backend.** Zwei
|
||||||
|
Stellen: `getSources` (124-136) liefert bei null Treffern eine leere Liste,
|
||||||
|
Status 200; `fetchAndCacheEvents` (365) `if (sources.length === 0) return [];`
|
||||||
|
— kehrt VOR dem Cache-Eintrag zurueck, also wird ein leeres Quellenergebnis
|
||||||
|
nicht einmal fuer fuenf Minuten festgehalten, es wird bei jedem Aufruf neu
|
||||||
|
leer geliefert. Die Besitzpruefungspfade (`updateSource`, `deleteSource`,
|
||||||
|
`testConnection`) sind LAUT: ein zu kleines Nachschlagen wirft
|
||||||
|
`NotFoundException('Calendar source not found')`. `addSource` ist laut in die
|
||||||
|
andere Richtung: ungebunden unter der Anwendungsrolle wuerde das Einfuegen
|
||||||
|
mit SQLSTATE 42501 abgewiesen (gemessen in Aufgabe 1).
|
||||||
|
|
||||||
|
**Befund J — welcher Code Leere als Abwesenheit deutet, Frontend, und die
|
||||||
|
Fehlerverschluckung.** Gemessen an drei Web-Dateien, die dieser Plan NICHT
|
||||||
|
aendert:
|
||||||
|
|
||||||
|
1. `apps/web/src/components/dashboard/widgets/calendar-widget.tsx`, um Zeile
|
||||||
|
37: `if (sources.length === 0)` — leere Quellenliste heisst
|
||||||
|
`emptyNoSources` ("keine Quelle eingerichtet"); um Zeile 50: `catch { // Silent fail — show empty state }`
|
||||||
|
— ein FEHLER von `GET /calendar/sources` oder `GET /calendar/events`
|
||||||
|
(403, 500, Netzwerk) fuehrt zu `setEvents([])`, also zu demselben leeren
|
||||||
|
Zustand wie eine erfolgreiche leere Antwort.
|
||||||
|
2. `apps/web/src/components/settings/calendar-settings-panel.tsx`, um Zeile
|
||||||
|
49: `.catch(() => { // Silent fail — show empty state })`; um Zeile 127:
|
||||||
|
`sources.length === 0` zeigt `sourceEmpty`. Dieselbe Verschluckung auf der
|
||||||
|
Einstellungsseite.
|
||||||
|
3. `calendar-source-form.tsx` (Befund E) — nicht Leere, sondern Weglassen;
|
||||||
|
gehoert hierhin, weil es die Erhaltungsfrage beantwortet.
|
||||||
|
|
||||||
|
Folge nach dem Scharfschalten bei einem zu kleinen Lesepfad: Widget zeigt
|
||||||
|
"keine Quelle eingerichtet", Einstellungsseite zeigt "keine Quelle
|
||||||
|
eingerichtet", der Nutzer legt seine Quelle NEU an (`addSource`, gebunden,
|
||||||
|
gelingt), tippt sein Passwort erneut ein, und die urspruengliche Zeile bleibt
|
||||||
|
unsichtbar liegen — nicht ueberschrieben (anders als `dashboard`), aber
|
||||||
|
verdoppelt, sobald die Ursache behoben ist: doppelte Quellen, doppelte
|
||||||
|
Termine. Und: weil das Frontend Fehler verschluckt, ist selbst ein LAUTER
|
||||||
|
Fehler (etwa ein 500 durch eine halb gebundene Schleife) fuer den Nutzer vom
|
||||||
|
leeren Kalender nicht zu unterscheiden — das Signal existiert nur im
|
||||||
|
Netzwerkprotokoll des Browsers und im API-Log. **Zur Ausfuehrungszeit an den
|
||||||
|
Dateien erneut zu pruefen, bevor es in der Kritikschrift behauptet wird.**
|
||||||
|
|
||||||
|
**Befund K — die halb gebundene Schleife als eigener Gefahrenfall.**
|
||||||
|
`fetchAndCacheEvents` laedt die Quellen (361) und schreibt je Quelle den
|
||||||
|
Synchronstatus zurueck: bei Erfolg (387), bei Fehler im `catch` (398).
|
||||||
|
Waere das Laden gebunden und das Rueckschreiben nicht (oder umgekehrt), traefe
|
||||||
|
das Rueckschreiben nach dem Scharfschalten keine Zeile, Prisma wuerfe 'Record
|
||||||
|
to update not found', der `catch` versuchte das Fehler-Rueckschreiben, das
|
||||||
|
ebenso scheitert, und `Promise.allSettled` liesse das Ergebnis dieser Quelle
|
||||||
|
als `rejected` STILL fallen — die Ereignisse fehlen, `lastSyncError` wird nie
|
||||||
|
gesetzt, der Nutzer sieht "keine Termine". Deshalb: ein gebundener Klient je
|
||||||
|
Methode, und die beiden Rueckschreibungen als Testfaelle festgenagelt.
|
||||||
|
|
||||||
|
**Befund L — der Controller verwirft den Mandanten in fuenf von sechs
|
||||||
|
Handlern.** `extractContext` (Zeile 38-51) loest `userId` und `tenantId` aus
|
||||||
|
dem Sitzungsnachweis auf und bricht ohne einen von beiden mit
|
||||||
|
`ForbiddenException` ab. `addSource` reicht beide durch; `getSources`,
|
||||||
|
`updateSource`, `deleteSource`, `testSource`, `getEvents` nehmen nur die
|
||||||
|
Benutzerkennung. `testSourceConfig` ruft `extractContext` gar nicht auf und
|
||||||
|
hat keinen Datenbankzugriff — er bleibt unveraendert. Die Umstellung ist ein
|
||||||
|
Durchreichen; die Parameterreihenfolge des Dienstes folgt `addSource`:
|
||||||
|
Mandantenkennung unmittelbar hinter der Benutzerkennung.
|
||||||
|
|
||||||
|
</planning_time_findings>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="tracer">
|
||||||
|
<name>Aufgabe 1: Die Fehlerrichtung fuer diesen Bereich MESSEN und aufschreiben — an der Regel, wie sie nach der Migration 20260910120000 steht, durch den generierten Client</name>
|
||||||
|
<precondition>Der lokale Datenbank-Container `tessera-ctl-db-1` laeuft; `docker inspect tessera-ctl-db-1` liefert eine Adresse. Ohne ihn kann das Wegwerf-Werkzeug nichts messen und die Aufgabe ist zu stoppen, nicht zu schaetzen.</precondition>
|
||||||
|
<files>apps/api/scripts/rls-scratch-check.mjs, docs/mandantentrennung-etappe2-fehlerrichtung.md</files>
|
||||||
|
<read_first>apps/api/scripts/rls-scratch-check.mjs (Kopf, `extractPolicySql`, `readRlsWidenMigrationSql`, `runDkvAreaChecks`, `runDashboardAreaChecks` VOLLSTAENDIG einschliesslich Pruefung 5b, `buildInlineExtendedClient`, `main`), apps/api/prisma/migrations/20260909140000_rls_remaining_tenant_tables/migration.sql, apps/api/prisma/migrations/20260910120000_rls_widen_membership_grant_and_platform_read/migration.sql, apps/api/prisma/migrations/20260629130000_add_missing_tables/migration.sql (CREATE TABLE "CalendarSource"), apps/api/prisma/migrations/20260723111946_add_rss_feed_source_and_poll_granularity/migration.sql (ALTER "CalendarSource" ADD "domain"), apps/api/prisma/schema.prisma (model CalendarSource, model User), apps/api/src/calendar/calendar.service.ts, apps/api/src/calendar/calendar.controller.ts, apps/api/src/calendar/dto/update-calendar-source.dto.ts, apps/api/src/auth/strategies/jwt.strategy.ts, apps/api/src/auth/auth.service.ts (Aufbau des Sitzungsnachweises), apps/web/src/components/dashboard/widgets/calendar-widget.tsx, apps/web/src/components/settings/calendar-settings-panel.tsx, apps/web/src/components/settings/calendar-source-form.tsx, docs/mandantentrennung-etappe2-fehlerrichtung.md (Abschnitte `## Bereich dkv` (d3) und `## Bereich dashboard` vollstaendig)</read_first>
|
||||||
|
<action>
|
||||||
|
TEIL 1 — die Messung. Erweitere `apps/api/scripts/rls-scratch-check.mjs` um
|
||||||
|
einen elften Abschnitt `runCalendarAreaChecks(adminUrl, scratchRoleUrl, results)`
|
||||||
|
und rufe ihn in `main()` NACH `runDashboardAreaChecks` und VOR
|
||||||
|
`runTransactionShapeMeasurement` auf. Der Abschnitt ist ein Blatt in der
|
||||||
|
Aufrufkette: er legt die Wegwerf-Tabelle `CalendarSource` selbst an, setzt
|
||||||
|
auf keiner Tabelle eines anderen Abschnitts auf, und keine spaetere Pruefung
|
||||||
|
setzt auf seiner auf. Halte diese Reihenfolgebedingung im Kopfkommentar fest,
|
||||||
|
so wie `runDkvAreaChecks` es vormacht.
|
||||||
|
|
||||||
|
Die Regel wird mit `extractPolicySql()` WORTGLEICH aus
|
||||||
|
`20260909140000_rls_remaining_tenant_tables` geschnitten, nicht nachgetippt.
|
||||||
|
Zusaetzlich — die Messfalle aus 260910-jab — prueft der Abschnitt zur
|
||||||
|
Laufzeit, dass `readRlsWidenMigrationSql()` KEINE Regel fuer `"CalendarSource"`
|
||||||
|
enthaelt (Befund G); faende er eine, meldet er eine FEHLGESCHLAGENE Pruefung
|
||||||
|
`calendarsource-regelstand-eindeutig` und bricht ab, statt die abgeloeste
|
||||||
|
Regel weiterzumessen. Findet `extractPolicySql()` die Regel nicht, ebenso
|
||||||
|
Abbruch mit FEHLGESCHLAGEN.
|
||||||
|
|
||||||
|
Die Wegwerf-Tabelle traegt SAEMTLICHE Spalten des Modells `CalendarSource`
|
||||||
|
mit den Typen und Vorgaben der ausgelieferten Migrationen (CREATE TABLE aus
|
||||||
|
20260629130000 plus die Spalte `domain` aus 20260723111946) — nicht nur die,
|
||||||
|
die Roh-SQL braucht. Das ist die Lehre aus Pruefung 5b im Bereich `dashboard`:
|
||||||
|
der generierte Client waehlt standardmaessig JEDE Spalte des Modells aus und
|
||||||
|
scheitert mit P2022 an jeder fehlenden, Roh-SQL merkt das nie. Lies die
|
||||||
|
Spaltenliste zur Laufzeit aus `apps/api/prisma/schema.prisma` (Block
|
||||||
|
`model CalendarSource`, Feldname = erstes Wort jeder Zeile, die nicht leer
|
||||||
|
ist, nicht mit `@@` und nicht mit `//` beginnt) und vergleiche sie mit
|
||||||
|
`information_schema.columns` der angelegten Tabelle — das ist Pruefung 8
|
||||||
|
unten, keine Annahme.
|
||||||
|
|
||||||
|
Testzeilen ueber die Wartungsrolle: zwei Quellen unter TENANT-A mit
|
||||||
|
VERSCHIEDENEN Benutzerkennungen (`user-a1`, `user-a2`), beide mit einem
|
||||||
|
gesetzten `encryptedPassword`-Platzhalter, damit Pruefung 3 zeigen kann, dass
|
||||||
|
der Kollege die verschluesselten Zugangsdaten sieht; eine Quelle unter
|
||||||
|
TENANT-B; alle mit `isVisible = true` und den Pflichtspalten.
|
||||||
|
|
||||||
|
Mindestens zwoelf namentlich benannte Pruefungen, jede mit einer
|
||||||
|
Belegausgabe, die die beobachteten Werte nennt:
|
||||||
|
|
||||||
|
1. `calendarsource-gebunden-nur-eigener-mandant` — gebundener Lesezugriff fuer
|
||||||
|
TENANT-A liefert ausschliesslich A-Zeilen.
|
||||||
|
2. `calendarsource-ungebunden-null-zeilen` — die tragende Belegzeile: der
|
||||||
|
IDENTISCHE Lesezugriff ohne Mandantenkontext liefert null Zeilen, nicht
|
||||||
|
alle vorhandenen.
|
||||||
|
3. `calendarsource-fremder-nutzer-desselben-mandanten-gebunden-sichtbar` —
|
||||||
|
das GELINGEN ist das bestandene Ergebnis: die Regel kennt keine
|
||||||
|
Benutzerdimension, die Zeile von `user-a2` ist unter TENANT-A sichtbar,
|
||||||
|
EINSCHLIESSLICH `encryptedPassword`. Die Belegausgabe sagt ausdruecklich,
|
||||||
|
dass die verschluesselten Zugangsdaten eines Kollegen auf Datenbankebene
|
||||||
|
lesbar sind und die anwendungsseitige Filterung ueber die Benutzerkennung
|
||||||
|
deshalb der einzige Schutz bleibt — bis die Etappe-3-Entscheidung (2)
|
||||||
|
die Benutzerdimension nachzieht.
|
||||||
|
4. `calendarsource-ungebundene-einzelabfrage-ueber-kennung-liefert-keine-zeile`
|
||||||
|
— die Datenbankseite der drei Besitzpruefungen (Befund I): ein
|
||||||
|
ungebundenes Nachschlagen ueber eine vorhandene Kennung liefert null
|
||||||
|
Zeilen; das ist der Weg in `NotFoundException`.
|
||||||
|
5. `calendarsource-gebundenes-einfuegen-fremder-mandant-abgelehnt` — ein
|
||||||
|
gebundenes Einfuegen unter TENANT-A mit `tenantId = TENANT-B` wird mit
|
||||||
|
SQLSTATE 42501 abgewiesen.
|
||||||
|
6. `calendarsource-gebundenes-loeschen-fremder-zeile-trifft-keine-zeile` —
|
||||||
|
gebundenes Loeschen ueber die Kennung der B-Zeile entfernt nichts, wirft
|
||||||
|
nichts; die Zeile ist danach ueber die Wartungsrolle noch da.
|
||||||
|
7. `calendarsource-gebundenes-update-ueber-kennung-allein-trifft-null-zeilen`
|
||||||
|
— gebundenes `UPDATE ... WHERE id = <B-Zeile>` unter TENANT-A trifft null
|
||||||
|
Zeilen (Roh-SQL, `lastSyncError` bleibt unveraendert).
|
||||||
|
8. `calendarsource-wegwerftabelle-deckt-alle-spalten-des-generierten-clients`
|
||||||
|
— Spaltenmenge der Wegwerf-Tabelle ist identisch mit der Feldmenge des
|
||||||
|
Modells im Schema (siehe oben). Faellt sie durch, sind alle folgenden
|
||||||
|
Client-Messungen wertlos — deshalb steht sie VOR ihnen.
|
||||||
|
9. `calendarsource-generierter-client-gebundene-quellenliste-nur-eigener-mandant`
|
||||||
|
— ueber `buildInlineExtendedClient(prisma, 'TENANT-A').calendarSource.findMany({ where: { userId: 'user-a1', isVisible: true } })`,
|
||||||
|
also der Abfrage, die `fetchAndCacheEvents` stellt: genau die A1-Zeile.
|
||||||
|
10. `calendarsource-generierter-client-ungebundene-quellenliste-null-zeilen`
|
||||||
|
— dieselbe Abfrage auf dem UNGEBUNDENEN generierten Client liefert eine
|
||||||
|
leere Liste ohne Fehler. Die Belegausgabe nennt es beim Namen: das ist
|
||||||
|
exakt der Wert, den `getSources` als "keine Quelle" und
|
||||||
|
`fetchAndCacheEvents` als "keine Termine" weiterreicht.
|
||||||
|
11. `calendarsource-generierter-client-gebundenes-update-auf-unsichtbare-zeile-scheitert-laut`
|
||||||
|
— `bound.calendarSource.update({ where: { id: <B-Zeile> }, data: { lastSyncError: 'x' } })`
|
||||||
|
unter TENANT-A. Bestanden genau dann, wenn ein Fehler geworfen wird; die
|
||||||
|
Belegausgabe nennt KONSTRUKTORNAME und `code` woertlich, damit (k2) und
|
||||||
|
die Kritikschrift die tatsaechlich gemessene Klasse nennen. Rate das
|
||||||
|
Ergebnis NICHT vorweg — es ist der Wettlauf-Fall aus Befund H/K.
|
||||||
|
12. `calendarsource-generierter-client-gebundenes-anlegen-eigener-mandant-gelingt`
|
||||||
|
— `bound.calendarSource.create(...)` unter TENANT-A mit `tenantId: 'TENANT-A'`
|
||||||
|
und den Pflichtfeldern (der `addSource`-Weg) gelingt, die Zeile ist danach
|
||||||
|
gebunden lesbar. Das prueft nebenbei, dass die Wegwerf-Tabelle die
|
||||||
|
clientseitig erzeugten Werte (`id`, `createdAt`, `updatedAt`) annimmt.
|
||||||
|
|
||||||
|
Ergaenzt wird nur, gestrichen wird nicht; nenne im SUMMARY die tatsaechlich
|
||||||
|
gezaehlte Zahl, nicht diese.
|
||||||
|
|
||||||
|
TEIL 2 — die Codeaussagen, jede mit ihrer Reichweite. Fuehre die
|
||||||
|
Nachpruefungen aus den Befunden D, E, F, J und L tatsaechlich aus und notiere
|
||||||
|
jeweils die Anweisung oder die Datei-und-Zeile, mit der du nachgesehen hast,
|
||||||
|
damit jede Aussage widerlegbar bleibt:
|
||||||
|
|
||||||
|
- Befund D: die drei Besitzpruefungen — steht in `updateSource`,
|
||||||
|
`deleteSource` UND `testConnection` zwischen Nachschlagen und Schreiben ein
|
||||||
|
Vergleich gegen die Benutzerkennung aus dem Sitzungsnachweis? Welche
|
||||||
|
Ausnahme wird geworfen?
|
||||||
|
- Befund E: die Zugangsdaten-Erhaltung — gibt es in `calendar.service.ts`
|
||||||
|
einen Lesezugriff, der ein gespeichertes Passwort laedt, um es neu zu
|
||||||
|
verschluesseln? Wie behandelt `updateSource` die drei Faelle Feld fehlt,
|
||||||
|
Feld leer, Feld gesetzt? Sendet `calendar-source-form.tsx` ein leeres Feld
|
||||||
|
oder laesst es es weg?
|
||||||
|
- Befund F: das Urteil zum Cache-Schluessel — alle vier Glieder (Schema
|
||||||
|
`User.id`, `JwtStrategy.validate`, Aufbau des Sitzungsnachweises in
|
||||||
|
`auth.service.ts`, `extractContext` im Controller), jedes einzeln
|
||||||
|
nachgesehen. Formuliere das Urteil in EINEM Satz, der die Etappe-3-
|
||||||
|
Entscheidung (1) beim Namen nennt und sagt, warum sie den Schluessel
|
||||||
|
beruehrt oder nicht.
|
||||||
|
- Befund J: die drei Web-Dateien — an welcher Zeile wird Leere als
|
||||||
|
"keine Quelle" gedeutet, an welcher Zeile wird ein Fehler verschluckt?
|
||||||
|
- Befund L: welche Handler verwerfen den Mandanten, welche nicht.
|
||||||
|
- Befund B: die Anweisung fuer den Hintergrunddienst-Abschnitt und der
|
||||||
|
Sonderfall der abgekoppelten Cache-Auffrischung.
|
||||||
|
|
||||||
|
Faellt eine Nachpruefung ANDERS aus als in den Planungsbefunden, gilt die
|
||||||
|
Messung; schreibe sie auf und benenne die Abweichung ausdruecklich.
|
||||||
|
|
||||||
|
TEIL 3 — die Kritikschrift. Erweitere
|
||||||
|
`docs/mandantentrennung-etappe2-fehlerrichtung.md` um einen Abschnitt
|
||||||
|
`## Bereich calendar` unmittelbar VOR `## Verweis`, in der Form der
|
||||||
|
vorhandenen Bereichsabschnitte, mit fuenf Unterabschnitten unter dem
|
||||||
|
Buchstaben `k`:
|
||||||
|
|
||||||
|
- `### (k1) Die Messung` — die tatsaechlich beobachtete Werkzeugausgabe
|
||||||
|
woertlich eingerueckt, die tragende Belegzeile benannt, ausdruecklich der
|
||||||
|
Regelstand NACH 20260910120000 samt der Anweisung, mit der belegt ist, dass
|
||||||
|
jene Migration `CalendarSource` nicht anfasst. Nenne, welche Pruefungen
|
||||||
|
ueber den generierten Client laufen und warum (dashboard-Lehre).
|
||||||
|
- `### (k2) Signaltabelle je umgestelltem Pfad` — je Dienstmethode mit
|
||||||
|
Datenbankzugriff eine Zeile (sechs Zeilen), plus eine fuer
|
||||||
|
`refreshCacheInBackground`: Verhalten bei zu wenig Ergebnis und das
|
||||||
|
konkrete Signal, an dem man es saehe — UND, in einer eigenen Spalte oder
|
||||||
|
im Text, ob das Frontend dieses Signal durchlaesst oder verschluckt. Die
|
||||||
|
Zeile zu `fetchAndCacheEvents` nennt die gemessene Fehlerklasse aus
|
||||||
|
Pruefung 11 fuer den Wettlauf-Fall und die halb gebundene Schleife aus
|
||||||
|
Befund K.
|
||||||
|
- `### (k3) Welcher Code Leere als Abwesenheit deutet` — namentlich, mit
|
||||||
|
Dateiname und Stelle, Backend (zwei Stellen, Befund I) getrennt vom
|
||||||
|
Frontend (Befund J). Beschreibe die Folgekette: leerer Kalender,
|
||||||
|
"Synchronisation kaputt" oder "keine Quelle", Neuanlage, Passwort ein
|
||||||
|
zweites Mal eingetippt, urspruengliche Zeile bleibt unsichtbar liegen,
|
||||||
|
Dublette nach Behebung. Grenze ausdruecklich gegen `dashboard` ab: hier
|
||||||
|
wird nichts ueberschrieben, aber der Nutzer gibt Zugangsdaten in ein
|
||||||
|
scheinbar defektes System ein. Und benenne die Fehlerverschluckung als
|
||||||
|
eigenen Punkt: selbst ein LAUTER Fehler der Lesepfade sieht fuer den
|
||||||
|
Nutzer wie ein leerer Kalender aus.
|
||||||
|
- `### (k4) Was dieser Durchlauf bewusst nicht löst` — (a) das Urteil zum
|
||||||
|
Cache-Schluessel mit der vierteiligen Kette und der Anweisung je Glied;
|
||||||
|
(b) der Befund zur Zugangsdaten-Erhaltung (Befund E) mit dem Vergleich zur
|
||||||
|
`dkv`-Form und der Aussage, warum hier kein Lesezugriff leer laufen kann;
|
||||||
|
(c) die fehlende Benutzerdimension der Regel (Befund G) mit Verweis auf
|
||||||
|
die Etappe-3-Entscheidung (2) und dem Hinweis, dass die verschluesselten
|
||||||
|
Zugangsdaten eines Kollegen bis dahin datenbankseitig sichtbar sind;
|
||||||
|
(d) 403 statt 404 als Existenzpreisgabe zwischen Kollegen (Befund D);
|
||||||
|
(e) die fehlende Unterscheidbarkeit von "keine Quelle" und "Quelle nicht
|
||||||
|
sichtbar" und die konkrete Vorabpruefung fuer Etappe 4
|
||||||
|
(`rls-preflight.mjs`: physisch vorhandene `CalendarSource`-Zeilen je
|
||||||
|
Mandant ueber die Wartungsrolle zaehlen und mit der gebundenen Zaehlung je
|
||||||
|
Mandant vergleichen — jede Abweichung ist ein Trennungsfehler, kein
|
||||||
|
Erstbenutzer). Eine Laufzeitwarnung ist zu erwaegen und, wenn verworfen,
|
||||||
|
mit eigener Begruendung zu verwerfen (Praezedenz: `getAllActiveConfigs`
|
||||||
|
im Bereich `ldap`, Dauerlaerm auf frischer Installation) — begruende,
|
||||||
|
uebernimm nicht.
|
||||||
|
- `### (k5) Was dieser Durchlauf bewusst nicht anfasst` — das Frontend
|
||||||
|
(nur beschrieben), die drei Provider (reden mit echten Servern, werden
|
||||||
|
nicht getestet), `testConnectionFromConfig` (kein Datenbankzugriff, kein
|
||||||
|
Mandant, unveraendert), der Bereich `favorites` (eigener Bereich), die
|
||||||
|
Antwortsemantik 403/404, und der Fremdkommentar in `ldap-config.service.ts`
|
||||||
|
(Zeile 24, nennt `CalendarSource` als Vorbild der Verschluesselung) —
|
||||||
|
zutreffend, nicht zu aendern.
|
||||||
|
|
||||||
|
Aendere in dieser Aufgabe KEINE Datei unter `apps/api/src`, KEINE unter
|
||||||
|
`apps/api/prisma` und KEINE unter `apps/web`.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && OUT=$(TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs) && echo "$OUT" && for K in calendarsource-gebunden-nur-eigener-mandant calendarsource-ungebunden-null-zeilen calendarsource-fremder-nutzer-desselben-mandanten-gebunden-sichtbar calendarsource-ungebundene-einzelabfrage-ueber-kennung-liefert-keine-zeile calendarsource-gebundenes-einfuegen-fremder-mandant-abgelehnt calendarsource-gebundenes-loeschen-fremder-zeile-trifft-keine-zeile calendarsource-gebundenes-update-ueber-kennung-allein-trifft-null-zeilen calendarsource-wegwerftabelle-deckt-alle-spalten-des-generierten-clients calendarsource-generierter-client-gebundene-quellenliste-nur-eigener-mandant calendarsource-generierter-client-ungebundene-quellenliste-null-zeilen calendarsource-generierter-client-gebundenes-update-auf-unsichtbare-zeile-scheitert-laut calendarsource-generierter-client-gebundenes-anlegen-eigener-mandant-gelingt; do echo "$OUT" | grep -q "^$K: bestanden" || { echo "FEHLENDE ODER FEHLGESCHLAGENE PRUEFUNG: $K"; exit 1; }; done && echo "$OUT" | grep -qE '^Alle [0-9]+ Pruefungen bestanden\.$' && N=$(echo "$OUT" | sed -nE 's/^Alle ([0-9]+) Pruefungen bestanden\.$/\1/p') && { test "$N" -ge 100 || { echo "PRUEFUNGSZAHL: $N, erwartet mindestens 100 (88 bisherige plus mindestens 12 neue)"; exit 1; }; } && grep -q 'runCalendarAreaChecks' apps/api/scripts/rls-scratch-check.mjs && awk '/await runDashboardAreaChecks\(/{d=NR} /await runCalendarAreaChecks\(/{c=NR} /await runTransactionShapeMeasurement\(/{t=NR} END{ if(!(d&&c&&t&&d<c&&c<t)){print "REIHENFOLGE in main(): runCalendarAreaChecks muss nach runDashboardAreaChecks und vor runTransactionShapeMeasurement stehen"; exit 1} }' apps/api/scripts/rls-scratch-check.mjs && grep -q '^## Bereich calendar$' docs/mandantentrennung-etappe2-fehlerrichtung.md && for S in k1 k2 k3 k4 k5; do grep -qE "^### \($S\) " docs/mandantentrennung-etappe2-fehlerrichtung.md || { echo "FEHLENDER UNTERABSCHNITT: ($S)"; exit 1; }; done && awk '/^## Bereich calendar$/{f=1; next} /^## /{f=0} f && /calendar-widget|calendar-settings-panel|calendar-source-form/{m++} f && /JwtStrategy|jwt\.strategy/{j=1} f && /uuid/{u=1} f && /20260910120000/{r=1} END{ if(m+0 < 3){print "ABSCHNITT (k3): die drei gemessenen Frontend-Dateien sind nicht namentlich genannt"; exit 1} if(!j||!u){print "ABSCHNITT (k4): das Urteil zum Cache-Schluessel nennt nicht beide Glieder der Kette (JwtStrategy, uuid)"; exit 1} if(!r){print "ABSCHNITT (k1): der Regelstand nach 20260910120000 ist nicht benannt"; exit 1} }' docs/mandantentrennung-etappe2-fehlerrichtung.md && npm --prefix apps/api run test && npm --prefix apps/api run type-check && git rev-parse --verify 50b3a36 >/dev/null && PRISMA_CHANGED=$(git diff --name-only 50b3a36 -- apps/api/prisma) && { test -z "$PRISMA_CHANGED" || { printf 'SCHEMA/MIGRATION GEAENDERT — in diesem Plan verboten:\n%s\n' "$PRISMA_CHANGED"; exit 1; }; } && CHANGED=$(git diff --name-only 50b3a36) && UNEXPECTED=$(printf '%s\n' "$CHANGED" | grep -v '^$' | grep -vE '^(apps/api/scripts/rls-scratch-check\.mjs|docs/mandantentrennung-etappe2-fehlerrichtung\.md|\.planning/.*)$' || true) && { test -z "$UNEXPECTED" || { printf 'UNERWARTETE AENDERUNGEN AUSSERHALB DES PLANUMFANGS:\n%s\n' "$UNEXPECTED"; exit 1; }; }</automated>
|
||||||
|
</verify>
|
||||||
|
<done>`apps/api/scripts/rls-scratch-check.mjs` hat einen elften Abschnitt `runCalendarAreaChecks` in der richtigen Reihenfolge mit mindestens zwoelf neuen, namentlich benannten Pruefungen gegen die aus der ausgelieferten Migration geschnittene Regel, davon vier ueber den generierten Client an einer Wegwerf-Tabelle, deren Spaltenmenge zur Laufzeit gegen das Schema geprueft wird; alle Pruefungen des Werkzeugs bestehen. `docs/mandantentrennung-etappe2-fehlerrichtung.md` hat einen Abschnitt `## Bereich calendar` mit (k1) bis (k5), die tatsaechlich beobachtete Werkzeugausgabe woertlich, die drei Web-Dateien namentlich, das Urteil zum Cache-Schluessel mit vierteiliger Kette, den Befund zur Zugangsdaten-Erhaltung und die Vorabpruefung fuer Etappe 4. Baseline gehalten: 860 Tests gruen, Typpruefung sauber. Unter `apps/api/src`, `apps/api/prisma`, `apps/web` und den Compose-/Umgebungsdateien ist nichts geaendert.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Aufgabe 2: Die Testlage aus dem Nichts anlegen, dann alle zwoelf Zugriffe binden und den Mandanten durchreichen — Nachschlagen und Schreiben nie getrennt</name>
|
||||||
|
<files>apps/api/src/calendar/calendar.service.spec.ts, apps/api/src/calendar/calendar.service.ts, apps/api/src/calendar/calendar.controller.ts, docs/mandantentrennung-zugriffsklassifikation.md</files>
|
||||||
|
<read_first>apps/api/src/calendar/calendar.service.ts, apps/api/src/calendar/calendar.controller.ts, apps/api/src/dkv/dkv.service.spec.ts (Kopf, `makeFakePrisma` mit `__makeBoundClient`, `expectBoundCall`, `makeFakeCrypto`, `makeDkvService`), apps/api/src/dashboard/dashboard.service.spec.ts (Zeilen 545-570, Wachhund fuer einen Klienten je Aufruf), apps/api/src/prisma/prisma-tenant.extension.ts, apps/api/src/prisma/rls-access-inventory.spec.ts (`analyzeFile`, `computeStandByKey`), docs/mandantentrennung-zugriffsklassifikation.md (Bestandsaufnahme-Zeile `calendar.service.ts`), docs/mandantentrennung-etappe2-fehlerrichtung.md (Abschnitt `## Bereich calendar` aus Aufgabe 1)</read_first>
|
||||||
|
<behavior>
|
||||||
|
ZUERST die Testlage, DANN die Umstellung. Lege
|
||||||
|
`apps/api/src/calendar/calendar.service.spec.ts` NEU an, in der Form von
|
||||||
|
`dkv.service.spec.ts`: `vi.mock` auf das Bindungshilfsmittel, umgeleitet auf
|
||||||
|
`prisma.__makeBoundClient(tenantId)`; ein handgerollter Prisma-Nachbau mit
|
||||||
|
In-Memory-Zeilen fuer `calendarSource` (`findMany`, `findUnique`, `create`,
|
||||||
|
`update`, `delete`), dessen ungebundene Form NICHT protokolliert und dessen
|
||||||
|
gebundene Form je Aufruf Mandantenkennung, Modell und Methode in ein
|
||||||
|
Bindungsprotokoll schreibt; Attrappen fuer `CryptoService`
|
||||||
|
(`encrypt`/`decrypt` als umkehrbare Textfunktionen) und die drei Provider
|
||||||
|
(`fetchEvents`/`testConnection` als `vi.fn`). Die Provider werden NICHT
|
||||||
|
ausgeuebt.
|
||||||
|
|
||||||
|
Jeder Fall eigenstaendig, jeder mit sprechendem Namen:
|
||||||
|
|
||||||
|
- `getSources`: laeuft gebunden mit der uebergebenen Mandantenkennung im
|
||||||
|
Protokoll; die Antwort traegt `hasCredentials` und NIE `encryptedPassword`.
|
||||||
|
- `getSources` von Nutzer A liefert nicht die Quellen von Nutzer B desselben
|
||||||
|
Mandanten — der `userId`-Filter bleibt, die Bindung ergaenzt ihn.
|
||||||
|
- `addSource`: gebunden, die Mandantenkennung wird als Pflichtwert
|
||||||
|
geschrieben, das Passwort verschluesselt, die Antwort ohne
|
||||||
|
`encryptedPassword`.
|
||||||
|
- `updateSource`: BEIDE Abfragen (Nachschlagen und Aendern) ueber DENSELBEN
|
||||||
|
gebundenen Klienten und dieselbe Mandantenkennung.
|
||||||
|
- `updateSource`, Zugangsdaten-Erhaltung in drei Faellen (Befund E): Feld
|
||||||
|
FEHLT — `encryptedPassword` bleibt unveraendert und es findet KEIN
|
||||||
|
Lesezugriff statt, der ein Passwort laedt; Feld LEER — wird `null`; Feld
|
||||||
|
GESETZT — wird verschluesselt. Diese drei Faelle nageln fest, dass hier
|
||||||
|
keine `dkv`-Form existiert; sie werden rot, sobald jemand eine einbaut.
|
||||||
|
- `updateSource`/`deleteSource`/`testConnection`: die Besitzpruefung bleibt
|
||||||
|
wirksam — eine Quelle eines anderen Benutzers fuehrt weiterhin zu
|
||||||
|
`ForbiddenException`, eine unbekannte Kennung zu `NotFoundException`.
|
||||||
|
Drei Faelle je Ausnahmeart. Diese Faelle sind der Nachweis, dass die
|
||||||
|
Bindung die Pruefung ERGAENZT und nicht ersetzt.
|
||||||
|
- `deleteSource`: beide Abfragen ueber denselben gebundenen Klienten.
|
||||||
|
- `testConnection`: alle drei Abfragen (Nachschlagen, Rueckschreiben bei
|
||||||
|
Erfolg ODER Rueckschreiben im `catch`) ueber denselben gebundenen Klienten;
|
||||||
|
der Provider erhaelt das ENTSCHLUESSELTE Passwort; bei Providerfehler ist
|
||||||
|
die Antwort generisch (kein Passwort, keine Serverdetails).
|
||||||
|
- `aggregateEvents`: das Laden der Quellen laeuft gebunden; die beiden
|
||||||
|
Synchronstatus-Rueckschreibungen (Erfolgspfad UND Fehlerpfad, Befund K)
|
||||||
|
stehen gebunden unter derselben Mandantenkennung im Protokoll. Zwei Faelle.
|
||||||
|
- `aggregateEvents` ohne Quellen: Rueckgabe ist eine leere Liste, kein
|
||||||
|
Fehler, und es wird KEIN Cache-Eintrag angelegt — die Deutung von Leere als
|
||||||
|
Abwesenheit als heutiges Verhalten festgehalten, damit eine spaetere
|
||||||
|
Aenderung sichtbar wird.
|
||||||
|
- Cache: ein zweiter Aufruf innerhalb der Lebensdauer erzeugt keinen
|
||||||
|
weiteren Datenbankzugriff; zwei VERSCHIEDENE Benutzerkennungen teilen sich
|
||||||
|
keinen Eintrag (das Urteil aus Aufgabe 1, festgenagelt).
|
||||||
|
- `testConnectionFromConfig`: kein Datenbankzugriff, weder gebunden noch
|
||||||
|
ungebunden — der Nachbau bleibt unberuehrt.
|
||||||
|
- Wachhund: keine Methode dieses Bereichs erzeugt mehr als EINEN gebundenen
|
||||||
|
Klienten je Aufruf (Muster `dashboard.service.spec.ts` Zeile 545).
|
||||||
|
|
||||||
|
Die Zahl der Faelle wird am Ende ABGEZAEHLT und im SUMMARY mit der gezaehlten
|
||||||
|
Zahl genannt, nicht mit der hier aufgelisteten — `tenders` und
|
||||||
|
`module-registry` haben genau an dieser Stelle je eine falsche Zahl
|
||||||
|
behauptet.
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
Stelle in `calendar.service.ts` alle zwoelf Zugriffe auf `forTenant()` um.
|
||||||
|
Ein gebundener Klient JE METHODE mit Datenbankzugriff, unter dem woertlichen
|
||||||
|
Namen `tenantPrisma` in der Zuweisungsform, die `rls-access-inventory.spec.ts`
|
||||||
|
erkennt, wie in jedem bereits umgestellten Bereich — sechs Aufrufstellen
|
||||||
|
(`getSources`, `addSource`, `updateSource`, `deleteSource`, `testConnection`,
|
||||||
|
`fetchAndCacheEvents`); `aggregateEvents` und `refreshCacheInBackground`
|
||||||
|
erzeugen keinen eigenen Klienten, sie reichen die Mandantenkennung an
|
||||||
|
`fetchAndCacheEvents` durch. Gebundene Klienten werden nicht zwischen
|
||||||
|
Methoden weitergereicht. Die Mandantenkennung steht in jeder Signatur
|
||||||
|
unmittelbar hinter der Benutzerkennung, wie `addSource` es vormacht.
|
||||||
|
|
||||||
|
Die bestehenden `where`-Filter ueber die Benutzerkennung und die drei
|
||||||
|
Besitzpruefungen bleiben ausnahmslos stehen. Begruende im Quelltext an EINER
|
||||||
|
Stelle (Klassenkommentar), warum sie kein Beiwerk sind: die Regel dieses
|
||||||
|
Bereichs kennt keine Benutzerdimension (gemessen in Aufgabe 1), die
|
||||||
|
verschluesselten Zugangsdaten eines Kollegen sind datenbankseitig sichtbar,
|
||||||
|
und bis zum Scharfschalten ist die Anwendungspruefung ohnehin der einzige
|
||||||
|
wirksame Schutz. Der veraltete Satz "Source config is per-user (D-09), not
|
||||||
|
per-tenant" im Klassenkommentar ist zu praezisieren: je Nutzer UND je
|
||||||
|
Mandant gebunden.
|
||||||
|
|
||||||
|
Schreibe das Urteil zum Cache-Schluessel aus Aufgabe 1 als Kommentar
|
||||||
|
unmittelbar ueber die Zuweisung von `eventCache`: nenne `User.id` als das,
|
||||||
|
was der Schluessel traegt, die Kette bis zum Sitzungsnachweis, und die
|
||||||
|
Etappe-3-Entscheidung (1) mit dem Grund, warum sie den Schluessel nicht
|
||||||
|
beruehrt. Lautet das Urteil aus Aufgabe 1 anders, bekommt der Schluessel
|
||||||
|
einen Mandantenanteil — dann steht das hier, mit dem Grund. Die Entscheidung
|
||||||
|
folgt der Messung, nicht diesem Absatz.
|
||||||
|
|
||||||
|
`fetchAndCacheEvents` und `refreshCacheInBackground` bekommen die
|
||||||
|
Mandantenkennung als Parameter; die abgekoppelte Auffrischung nimmt sie aus
|
||||||
|
der Anfrage mit, die sie angestossen hat. Halte im Kommentar von
|
||||||
|
`refreshCacheInBackground` fest, dass sie den Mandanten der urspruenglichen
|
||||||
|
Anfrage traegt und keinen anderen haben kann.
|
||||||
|
|
||||||
|
Reiche in `calendar.controller.ts` bei den fuenf Handlern, die den bereits
|
||||||
|
aufgeloesten Mandanten heute verwerfen (`getSources`, `updateSource`,
|
||||||
|
`deleteSource`, `testSource`, `getEvents`), diesen an den Dienst durch: jeder
|
||||||
|
der sechs kontextnutzenden Handler nimmt Benutzer- UND Mandantenkennung aus
|
||||||
|
`extractContext` in einer Destrukturierung (die Form, die `addSource` heute
|
||||||
|
schon hat), keiner nimmt nur die Benutzerkennung. Die Mandantenkennung stammt
|
||||||
|
unveraendert aus `extractContext` und damit aus dem Sitzungsnachweis — es
|
||||||
|
entsteht KEINE neue Vertrauensquelle, aus Rumpf oder Pfad wird nichts
|
||||||
|
uebernommen. `testSourceConfig` bleibt unveraendert. Schreibe den
|
||||||
|
Kopfkommentar des Controllers nach, damit er das Durchreichen nennt.
|
||||||
|
|
||||||
|
Kommentare in `calendar.service.ts` duerfen die Zeichenfolge, mit der die
|
||||||
|
Uebersichtstabelle der Klassifikation ungebundene Zugriffe zaehlt (siehe
|
||||||
|
deren Messanweisung), NICHT woertlich enthalten — sonst zaehlt die
|
||||||
|
Buchfuehrung einen Zugriff, den es nicht mehr gibt; das Gate vergleicht die
|
||||||
|
Zaehlung mit und ohne Kommentare.
|
||||||
|
|
||||||
|
Ziehe in DIESER Aufgabe die eine Bestandsaufnahme-Zeile in
|
||||||
|
`docs/mandantentrennung-zugriffsklassifikation.md` nach
|
||||||
|
(`calendar.service.ts`/`calendarSource`, Spalte `Stand` auf den gemessenen
|
||||||
|
Wert, Begruendung fortgeschrieben mit Verweis auf 260911-cwh, die gemeinsame
|
||||||
|
Bindung je Pfad und den Befund zur Zugangsdaten-Erhaltung) — sonst ist die
|
||||||
|
maschinelle Bestandspruefung am Ende dieser Aufgabe rot und die Baseline
|
||||||
|
gebrochen (die Lehre aus 260910-exd). Die uebrigen vier handgepflegten
|
||||||
|
Stellen sind Aufgabe 3.
|
||||||
|
|
||||||
|
Aendere keine Datei ausserhalb der vier genannten. Fuehre am Ende dieser
|
||||||
|
Aufgabe einen Falsifizierungsnachweis durch: nimm probeweise die Bindung
|
||||||
|
EINER Synchronstatus-Rueckschreibung in `fetchAndCacheEvents` zurueck
|
||||||
|
(ungebundener Klient nur fuer diese eine Anweisung), ueberzeuge dich, dass
|
||||||
|
die Testlage rot wird, notiere Testname und Fehlermeldung woertlich, und
|
||||||
|
stelle den Zustand wieder her.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs && npm --prefix apps/api run test && npm --prefix apps/api run type-check && npm --prefix apps/api run test -- src/calendar/calendar.service.spec.ts && npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts && test -f apps/api/src/calendar/calendar.service.spec.ts && grep -q '__makeBoundClient' apps/api/src/calendar/calendar.service.spec.ts && grep -q "vi.mock('../prisma/prisma-tenant.extension'" apps/api/src/calendar/calendar.service.spec.ts && grep -qi 'cache' apps/api/src/calendar/calendar.service.spec.ts && grep -q 'testConnectionFromConfig' apps/api/src/calendar/calendar.service.spec.ts && grep -q "from '../prisma/prisma-tenant.extension'" apps/api/src/calendar/calendar.service.ts && SRC=$(grep -vE '^\s*(//|\*|/\*)' apps/api/src/calendar/calendar.service.ts) && B=$(printf '%s\n' "$SRC" | grep -o "tenantPrisma\.calendarSource\." | wc -l | tr -d ' ') && { test "$B" -ge 12 || { echo "BINDUNG: nur $B gebundene Modellzugriffe in calendar.service.ts, erwartet mindestens 12"; exit 1; }; } && U=$(printf '%s\n' "$SRC" | grep -o "this\.prisma\.[a-zA-Z]*" | wc -l | tr -d ' ') && { test "$U" -eq 0 || { echo "REST: $U ungebundene Modellzugriffe in calendar.service.ts, erwartet 0 — dieser Bereich hat keinen begruendet ungebundenen Zugriff"; exit 1; }; } && URAW=$(grep -o "this\.prisma\.[a-zA-Z]*" apps/api/src/calendar/calendar.service.ts | wc -l | tr -d ' ') && { test "$URAW" -eq "$U" || { echo "KOMMENTARE in calendar.service.ts nennen die ungebundene Zugriffsform woertlich ($URAW mit, $U ohne Kommentare) — die Uebersichtstabelle wuerde sie mitzaehlen"; exit 1; }; } && C=$(printf '%s\n' "$SRC" | grep -o 'forTenant(this\.prisma' | wc -l | tr -d ' ') && { test "$C" -eq 6 || { echo "KLIENTEN: $C forTenant-Aufrufstellen in calendar.service.ts, erwartet genau 6 (eine je Methode mit Datenbankzugriff, keine zweite in derselben Methode, keine in aggregateEvents/refreshCacheInBackground)"; exit 1; }; } && grep -B10 'eventCache = new Map' apps/api/src/calendar/calendar.service.ts | grep -q 'User.id' && test 0 -eq "$(grep -rn '\$transaction(' apps/api/src/calendar --include=*.ts | grep -v spec | wc -l | tr -d ' ')" && H=$(grep -c 'const { userId, tenantId } = this.extractContext(req)' apps/api/src/calendar/calendar.controller.ts) && { test "$H" -eq 6 || { echo "CONTROLLER: $H Handler nehmen Benutzer- und Mandantenkennung, erwartet 6"; exit 1; }; } && test 0 -eq "$(grep -vE '^\s*(//|\*|/\*)' apps/api/src/calendar/calendar.controller.ts | grep -c 'const { userId } = this.extractContext')" && grep -qE '^\| apps/api/src/calendar/calendar\.service\.ts \| calendarSource \| muss-mandantengebunden \| gebunden \|' docs/mandantentrennung-zugriffsklassifikation.md && git rev-parse --verify 50b3a36 >/dev/null && PRISMA_CHANGED=$(git diff --name-only 50b3a36 -- apps/api/prisma) && { test -z "$PRISMA_CHANGED" || { printf 'SCHEMA/MIGRATION GEAENDERT — in diesem Plan verboten:\n%s\n' "$PRISMA_CHANGED"; exit 1; }; } && CHANGED=$(git diff --name-only 50b3a36) && UNEXPECTED=$(printf '%s\n' "$CHANGED" | grep -v '^$' | grep -vE '^(apps/api/scripts/rls-scratch-check\.mjs|docs/mandantentrennung-etappe2-fehlerrichtung\.md|apps/api/src/calendar/calendar\.service\.ts|apps/api/src/calendar/calendar\.service\.spec\.ts|apps/api/src/calendar/calendar\.controller\.ts|docs/mandantentrennung-zugriffsklassifikation\.md|\.planning/.*)$' || true) && { test -z "$UNEXPECTED" || { printf 'UNERWARTETE AENDERUNGEN AUSSERHALB DES PLANUMFANGS:\n%s\n' "$UNEXPECTED"; exit 1; }; }</automated>
|
||||||
|
</verify>
|
||||||
|
<done>`calendar.service.spec.ts` existiert neu mit dem Zwei-Klienten-Nachweis, Attrappen fuer Verschluesselung und Provider, und deckt jeden in `<behavior>` genannten Fall ab — einschliesslich der drei Erhaltungsfaelle, der Besitzpruefungen je Ausnahmeart, der beiden Rueckschreibungen der Aggregationsschleife und des Wachhunds. In `calendar.service.ts` laufen alle zwoelf Zugriffe ueber `forTenant()` unter dem Namen `tenantPrisma`, genau ein Klient je Methode mit Datenbankzugriff, das Cache-Schluessel-Urteil steht als Kommentar an der Stelle. `calendar.controller.ts` reicht den bereits aufgeloesten Mandanten in allen sechs kontextnutzenden Handlern durch, ohne neue Vertrauensquelle. Die Bestandsaufnahme-Zeile steht auf `gebunden`, die maschinelle Bestandspruefung ist gruen. Der Falsifizierungsnachweis ist durchgefuehrt, zurueckgenommen und mit Testname und Fehlermeldung notiert. Baseline gehalten.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Aufgabe 3: Die uebrigen vier handgepflegten Dokumentstellen nachziehen, den Ledger-Eintrag anlegen und die Gates falsifizieren</name>
|
||||||
|
<files>docs/mandantentrennung-zugriffsklassifikation.md, .planning/WINDOWS.md</files>
|
||||||
|
<read_first>docs/mandantentrennung-zugriffsklassifikation.md (vollstaendig: Uebersichtstabelle samt Messanweisung, Klassen-Verteilung, Hintergrunddienst-Abschnitt, Abschnitt `Was diese Etappe NICHT entscheidet`), .planning/WINDOWS.md (Kopfzeilen, Eintrag 23 und 25 in Tabelle und JSON), docs/mandantentrennung-etappe2-fehlerrichtung.md (Abschnitt `## Bereich calendar`), apps/api/src/calendar/calendar.service.ts (Endstand aus Aufgabe 2)</read_first>
|
||||||
|
<action>
|
||||||
|
Ziehe `docs/mandantentrennung-zugriffsklassifikation.md` an den vier noch
|
||||||
|
offenen handgepflegten Stellen nach — jede einzeln nachgesehen, keine
|
||||||
|
ueberflogen (Fehler 4 dieses Vorhabens):
|
||||||
|
|
||||||
|
1. Die Uebersichtszeile `calendar` mit den NEU GEMESSENEN Zahlen aus der im
|
||||||
|
Dokument genannten Messanweisung (beide Spalten), im etablierten Stil mit
|
||||||
|
dem Vermerk des vorherigen Standes (`**war 12/0**`) und der Aufzaehlung der
|
||||||
|
sechs umgestellten Methoden. Anders als bei den sieben Bereichen davor
|
||||||
|
gibt es hier keinen verbleibenden ungebundenen Treffer zu begruenden —
|
||||||
|
schreibe das ausdruecklich hin, mit dem Grund (Pflicht-Mandantenkennung,
|
||||||
|
kein uebergreifender Pfad).
|
||||||
|
2. Die Summenzeile derselben Tabelle, mit fortgeschriebener Herkunftsspur im
|
||||||
|
Hinweisfeld.
|
||||||
|
3. Die Klassen-Verteilung samt der Zahl in ihrer Ueberschrift. Aendert sich
|
||||||
|
nichts, schreibe in einem `**Stand 260911-cwh**`-Absatz ausdruecklich hin,
|
||||||
|
dass sich nichts aendert und warum (das eine Paar war bereits richtig
|
||||||
|
klassifiziert, nur der `Stand` wechselte in Aufgabe 2) — eine
|
||||||
|
unveraenderte Tabelle ohne Vermerk ist von einer vergessenen nicht zu
|
||||||
|
unterscheiden.
|
||||||
|
4. Den Abschnitt zur Hintergrunddienst-Falle: ergaenze einen PLAIN-Absatz
|
||||||
|
(KEINEN Aufzaehlungspunkt in der Form der bestehenden Faelle und KEINE
|
||||||
|
Zeile der Form "Der ... Fall, anderer Bauart", sonst waere die Zahl in der
|
||||||
|
Ueberschrift falsch), der festhaelt, dass dieser Bereich keinen sechsten
|
||||||
|
Fall hinzufuegt, mit der Anweisung aus Aufgabe 1 — UND der den Sonderfall
|
||||||
|
`refreshCacheInBackground` benennt: eine abgekoppelte Fortsetzung einer
|
||||||
|
Anfrage, die den Mandanten der Anfrage mitnimmt, nicht die Bauform
|
||||||
|
"uebergreifend lesen, dann je Mandant binden". Die Abwesenheit steht da,
|
||||||
|
damit sie nicht wie ein Uebersehen aussieht.
|
||||||
|
|
||||||
|
Ergaenze den Abschnitt `Was diese Etappe NICHT entscheidet` um die
|
||||||
|
Entscheidung dieses Bereichs zur offenen Architekturfrage — er bindet
|
||||||
|
dienst-intern, ein Klient je Methode, wie alle acht Bereiche vor ihm.
|
||||||
|
|
||||||
|
Lege in `.planning/WINDOWS.md` einen neuen OFFENEN Eintrag an, und zwar ueber
|
||||||
|
`gsd-tools windows append --kind deviation --phase quick-260911-cwh --file apps/web/src/components/dashboard/widgets/calendar-widget.tsx --description "..."`,
|
||||||
|
damit Tabelle, JSON-Block und die Zaehler im Dateikopf zusammenpassen —
|
||||||
|
nicht von Hand. Inhalt: die lautlose Auspraegung der umgekehrten
|
||||||
|
Fehlerrichtung im Bereich calendar aus (k3): zu kleines Leseergebnis auf
|
||||||
|
`getSources`/`fetchAndCacheEvents` sieht aus wie "keine Quelle eingerichtet"
|
||||||
|
beziehungsweise "keine Termine"; das Frontend (`calendar-widget.tsx`,
|
||||||
|
`calendar-settings-panel.tsx`, mit Stellen) verschluckt zusaetzlich LAUTE
|
||||||
|
Fehler derselben Pfade in denselben leeren Zustand; der Nutzer legt seine
|
||||||
|
Quelle neu an und tippt seine Exchange-/CalDAV-Zugangsdaten ein zweites Mal
|
||||||
|
in ein scheinbar defektes System ein, die urspruengliche Zeile bleibt
|
||||||
|
unsichtbar liegen und wird nach Behebung zur Dublette; die konkrete
|
||||||
|
Vorabpruefung fuer Etappe 4 aus (k4)(e); an dieselbe Bedingung gebunden wie
|
||||||
|
#18; das Frontend wird von 260911-cwh NICHT geaendert. Verweise auf #23 und
|
||||||
|
#25 als Familie. Pruefe nach dem Anlegen, dass der Eintrag in Tabelle UND
|
||||||
|
JSON-Block steht und die Kopfzaehler stimmen.
|
||||||
|
|
||||||
|
Fuehre am Ende zwei Falsifizierungsnachweise durch, jeder zurueckgenommen und
|
||||||
|
mit Meldung woertlich notiert: (a) setze die Bestandsaufnahme-Zeile dieses
|
||||||
|
Bereichs probeweise auf einen falschen `Stand` — die maschinelle
|
||||||
|
Bestandspruefung muss rot werden; (b) setze die Uebersichtszeile probeweise
|
||||||
|
auf eine falsche Zahl — das herleitende Gate dieser Aufgabe muss fehlschlagen.
|
||||||
|
|
||||||
|
Aendere in dieser Aufgabe KEINE Datei unter `apps/`.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs && npm --prefix apps/api run test && npm --prefix apps/api run type-check && npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts && DU=$(grep -ro "this\.prisma\.[a-zA-Z]*" apps/api/src/calendar | grep -v spec | wc -l | tr -d ' ') && DB=$(grep -ro "tenantPrisma\.[a-zA-Z]*\." apps/api/src/calendar | grep -v spec | wc -l | tr -d ' ') && { test "$DU" -eq 0 || { echo "UEBERSICHTSZEILE: ungebundene Rohtreffer in apps/api/src/calendar sind $DU, erwartet 0"; exit 1; }; } && { test "$DB" -ge 12 || { echo "UEBERSICHTSZEILE: gebundene Rohtreffer in apps/api/src/calendar sind $DB, erwartet mindestens 12"; exit 1; }; } && { grep -qE "^\| calendar \| ${DU} \| ${DB} \| \*\*war 12/0\*\*" docs/mandantentrennung-zugriffsklassifikation.md || { echo "UEBERSICHTSZEILE calendar nennt nicht die neu gemessenen Zahlen ${DU}/${DB} im etablierten Stil"; exit 1; }; } && awk -F'|' '$2 ~ /^ *[a-z][a-z-]* *$/ && $3 ~ /^ *[0-9]+ *$/ && $4 ~ /^ *[0-9]+ *$/ { su+=$3; sb+=$4; n++ } $2 ~ /^ *\*\*Summe\*\* *$/ && $4 !~ /^ *$/ { g3=$3; g4=$4; gsub(/[^0-9]/,"",g3); gsub(/[^0-9]/,"",g4); ru=g3+0; rb=g4+0; seen=1 } END { if (!seen || n+0 != 12) { print "UEBERSICHTSTABELLE nicht erkannt, Bereichszeilen: " n; exit 1 } if (su+0 != ru || sb+0 != rb) { print "SUMMENZEILE stimmt nicht: Bereichszeilen ergeben " su "/" sb ", Summenzeile nennt " ru "/" rb; exit 1 } }' docs/mandantentrennung-zugriffsklassifikation.md && awk -F'|' '$2 ~ /^ *apps\/api\/src\// { k=$4; gsub(/^ +| +$/,"",k); cls[k]++; pairs++ } $2 ~ /^ *(muss-mandantengebunden|keine-mandantengebundene-tabelle|beides|bewusst-uebergreifend) *$/ && $3 ~ /^ *[0-9]+ *$/ && $4 ~ /^ *$/ { k=$2; gsub(/^ +| +$/,"",k); v=$3; gsub(/[^0-9]/,"",v); tab[k]=v+0; tn++ } $2 ~ /^ *\*\*Summe\*\* *$/ && $4 ~ /^ *$/ { v=$3; gsub(/[^0-9]/,"",v); tsum=v+0; tseen=1 } /^## Klassen-Verteilung/ { h=$0; gsub(/[^0-9]/,"",h); hp=h+0; hseen=1 } END { if (tn+0 != 4 || !tseen || !hseen) { print "KLASSEN-VERTEILUNG nicht erkannt: Klassenzeilen " tn ", Summenzeile " tseen ", Ueberschrift " hseen; exit 1 } if (tsum != pairs+0) { print "KLASSEN-SUMME stimmt nicht: Bestandsaufnahme hat " pairs " Paare, Tabellensumme nennt " tsum; exit 1 } if (hp != pairs+0) { print "UEBERSCHRIFT der Klassen-Verteilung nennt " hp " Paare, Bestandsaufnahme hat " pairs; exit 1 } s=0; for (k in tab) { if (tab[k] != cls[k]+0) { print "KLASSE " k ": Tabelle nennt " tab[k] ", Bestandsaufnahme zaehlt " cls[k]+0; exit 1 } s+=tab[k] } if (s != pairs+0) { print "KLASSENZEILEN ergeben " s ", Bestandsaufnahme hat " pairs; exit 1 } }' docs/mandantentrennung-zugriffsklassifikation.md && awk '/^## Klassen-Verteilung/{f=1; next} /^## /{f=0} f && /Stand 260911-cwh/{m=1} END{ if(!m){print "KLASSEN-VERTEILUNG: kein Stand-Vermerk fuer 260911-cwh — eine unveraenderte Tabelle ohne Vermerk ist von einer vergessenen nicht zu unterscheiden"; exit 1} }' docs/mandantentrennung-zugriffsklassifikation.md && awk 'BEGIN{split("ein zwei drei vier x sechs sieben acht neun",w," "); w[5]="fünf"} /^## Der Hintergrunddienst als Falle/{seen=1; head=$0; f=1; next} /^## /{f=0} f && /^- \*\*`/{n++} f && /^\*\*Der .* Fall, anderer Bauart/{n++} f && /calendar/{m=1} f && /refreshCacheInBackground/{r=1} END{ if(!seen){print "ABSCHNITT Hintergrunddienst nicht gefunden"; exit 1} want="## Der Hintergrunddienst als Falle — " w[n] " Fälle"; if(head != want){printf "HINTERGRUNDDIENST-UEBERSCHRIFT nennt \"%s\", gezaehlt wurden %d Faelle, erwartet \"%s\"\n", head, n, want; exit 1} if(!m){print "ABSCHNITT Hintergrunddienst nennt diesen Bereich nicht — die Abwesenheit eines sechsten Falls ist nicht belegt"; exit 1} if(!r){print "ABSCHNITT Hintergrunddienst nennt den Sonderfall refreshCacheInBackground nicht"; exit 1} }' docs/mandantentrennung-zugriffsklassifikation.md && awk '/^## Was diese Etappe NICHT entscheidet/{f=1; next} f && /calendar/{m=1} END{ if(!m){print "ABSCHNITT \"Was diese Etappe NICHT entscheidet\" nennt den Bereich calendar nicht"; exit 1} }' docs/mandantentrennung-zugriffsklassifikation.md && python3 -c "
|
||||||
|
import re,sys
|
||||||
|
s=open('.planning/WINDOWS.md',encoding='utf-8').read()
|
||||||
|
rows=[l for l in s.splitlines() if re.match(r'^\| \d+ \|', l)]
|
||||||
|
ids=re.findall(r'\"id\": (\d+),', s)
|
||||||
|
if len(rows)!=len(ids): print('WINDOWS: %d Tabellenzeilen, %d JSON-Eintraege' % (len(rows), len(ids))); sys.exit(1)
|
||||||
|
mine=[l for l in rows if 'quick-260911-cwh' in l]
|
||||||
|
if len(mine)!=1: print('WINDOWS: erwartet genau einen Tabelleneintrag fuer quick-260911-cwh, gefunden %d' % len(mine)); sys.exit(1)
|
||||||
|
mid=re.match(r'^\| (\d+) \|', mine[0]).group(1)
|
||||||
|
if mid not in ids: print('WINDOWS: Eintrag %s fehlt im JSON-Block' % mid); sys.exit(1)
|
||||||
|
if '| open |' not in mine[0]: print('WINDOWS: Eintrag %s ist nicht offen' % mid); sys.exit(1)
|
||||||
|
if 'calendar' not in mine[0] or 'calendar-widget' not in mine[0]: print('WINDOWS: Eintrag %s nennt Bereich oder Widget-Datei nicht' % mid); sys.exit(1)
|
||||||
|
tc=int(re.search(r'^total_count: (\d+)', s, re.M).group(1)); oc=int(re.search(r'^open_count: (\d+)', s, re.M).group(1))
|
||||||
|
if tc!=len(rows): print('WINDOWS: total_count %d, Tabellenzeilen %d' % (tc,len(rows))); sys.exit(1)
|
||||||
|
op=len([l for l in rows if '| open |' in l])
|
||||||
|
if oc!=op: print('WINDOWS: open_count %d, offene Zeilen %d' % (oc,op)); sys.exit(1)
|
||||||
|
" && test 0 -eq "$(grep -rn '\$transaction(' apps/api/src/calendar --include=*.ts | grep -v spec | wc -l | tr -d ' ')" && git rev-parse --verify 50b3a36 >/dev/null && PRISMA_CHANGED=$(git diff --name-only 50b3a36 -- apps/api/prisma) && { test -z "$PRISMA_CHANGED" || { printf 'SCHEMA/MIGRATION GEAENDERT — in diesem Plan verboten:\n%s\n' "$PRISMA_CHANGED"; exit 1; }; } && CHANGED=$(git diff --name-only 50b3a36) && UNEXPECTED=$(printf '%s\n' "$CHANGED" | grep -v '^$' | grep -vE '^(apps/api/scripts/rls-scratch-check\.mjs|docs/mandantentrennung-etappe2-fehlerrichtung\.md|docs/mandantentrennung-zugriffsklassifikation\.md|apps/api/src/calendar/calendar\.service\.ts|apps/api/src/calendar/calendar\.service\.spec\.ts|apps/api/src/calendar/calendar\.controller\.ts|\.planning/.*)$' || true) && { test -z "$UNEXPECTED" || { printf 'UNERWARTETE AENDERUNGEN AUSSERHALB DES PLANUMFANGS:\n%s\n' "$UNEXPECTED"; exit 1; }; }</automated>
|
||||||
|
</verify>
|
||||||
|
<done>Alle fuenf handgepflegten Stellen von `docs/mandantentrennung-zugriffsklassifikation.md` sind nachgezogen und maschinell gegatet: Bestandsaufnahme-Zeile (Aufgabe 2), Uebersichtszeile mit neu gemessenen Zahlen, Summenzeile, Klassen-Verteilung mit ausdruecklichem Unveraendert-Vermerk, Hintergrunddienst-Abschnitt mit Messbeleg fuer die Abwesenheit eines sechsten Falls und dem Sonderfall der abgekoppelten Auffrischung; dazu der Abschnitt `Was diese Etappe NICHT entscheidet`. `.planning/WINDOWS.md` traegt den neuen offenen Eintrag ueber das Werkzeug in Tabelle, JSON-Block und Kopfzaehlern. Beide Falsifizierungsnachweise sind durchgefuehrt, zurueckgenommen und woertlich notiert. Baseline gehalten, Schalter unveraendert aus.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
|
||||||
|
Konfiguriert: ASVS-Stufe 1, blockierend ab `high`.
|
||||||
|
|
||||||
|
## Trust Boundaries
|
||||||
|
|
||||||
|
| Boundary | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| Browser/Benutzer → Kalender-API | Benutzer- und Mandantenkennung stammen ausschliesslich aus dem validierten Sitzungsnachweis (`extractContext` liest `req.user.id` und `req.tenantId`/`req.user.tenantId`, bricht ohne beide ab). Die Quellenkennung im Pfad ist frei waehlbare Nutzereingabe. |
|
||||||
|
| Nutzer A → Kalenderquellen (samt Zugangsdaten) von Nutzer B DESSELBEN Mandanten | Die Grenze, die die Datenbank NACHWEISLICH nicht zieht — die Regel kennt nur die Mandantendimension. Gezogen allein vom `userId`-Filter in den Listenpfaden und den drei Besitzpruefungen. |
|
||||||
|
| Mandant A → Zeilen des Mandanten B | Die Grenze dieses Plans. Heute nur von der Anwendung gezogen, nach diesem Plan zusaetzlich von der Datenbank — wirksam erst nach Etappe 4. |
|
||||||
|
| API → PostgreSQL | Die Zeilenschutz-Grenze. Heute wirkungslos (Rolle mit `BYPASSRLS`, WINDOWS #18) — dieser Plan bereitet sie vor, schaltet sie NICHT scharf. |
|
||||||
|
| API → externe Kalenderserver (Exchange/CalDAV/ICS) | Die Grenze, ueber die entschluesselte Zugangsdaten gehen. Dieser Plan aendert daran nichts; er sorgt dafuer, dass nur die Zugangsdaten der eigenen Quelle dorthin gehen. |
|
||||||
|
| Anfrage → abgekoppelte Cache-Auffrischung | Ein Anfragekontext, der die Anfrage ueberlebt: die Auffrischung traegt Benutzer- und Mandantenkennung der Anfrage, die sie angestossen hat, und kann keinen anderen Mandanten haben. |
|
||||||
|
|
||||||
|
## STRIDE Threat Register
|
||||||
|
|
||||||
|
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||||
|
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||||
|
| T-CWH-01 | Information Disclosure | `calendar.service.ts`, `getSources`/`fetchAndCacheEvents`/`testConnection`/`updateSource` (Lesepfade, die `encryptedPassword` laden) | high | mitigate | Quer-Lesen der Kalenderquellen eines fremden Mandanten EINSCHLIESSLICH der verschluesselten Exchange-/CalDAV-Zugangsdaten — die Klasse, in der der Bereich `dkv` Postfach-Zugangsdaten behandelt hat. Alle Lesepfade werden gebunden; `SOURCE_SAFE_SELECT` bleibt, `encryptedPassword` verlaesst die API weiterhin nie. Gemessen in Aufgabe 1: `calendarsource-gebunden-nur-eigener-mandant`, `calendarsource-generierter-client-gebundene-quellenliste-nur-eigener-mandant`. |
|
||||||
|
| T-CWH-02 | Information Disclosure | Regel auf `CalendarSource`, keine Benutzerdimension | high | accept | Quer-Lesen der Zugangsdaten eines Kollegen DESSELBEN Mandanten auf Datenbankebene. Gemessen in Aufgabe 1 (`calendarsource-fremder-nutzer-desselben-mandanten-gebunden-sichtbar`, das Gelingen IST das Ergebnis, Belegausgabe nennt `encryptedPassword`). Bewusst akzeptiert und aufgezeichnet: der Schutz bleibt vollstaendig beim `userId`-Filter und den drei Besitzpruefungen, die dieser Plan NICHT entfernt und als Testfaelle festnagelt; die Benutzerdimension in der Regel ist die Etappe-3-Entscheidung (2) des Users vom 2026-09-10, `CalendarSource` steht dort ausdruecklich in der Liste. Bis dahin ist derselbe Zustand wie heute — dieser Plan verschlechtert ihn nicht. |
|
||||||
|
| T-CWH-03 | Tampering | `calendar.service.ts`, `updateSource`/`deleteSource`/`testConnection` | high | mitigate | Quer-Aendern, Quer-Loeschen oder fremder Verbindungstest ueber die Kennung im Pfad — die Bauform, die bei `ldap` und `dkv` je eine Luecke riss. Hier existiert die Besitzpruefung in allen drei Pfaden (Befund D, zur Ausfuehrungszeit erneut zu lesen), sie wird durch die Bindung ERGAENZT, und alle Abfragen jedes Pfads laufen ueber DENSELBEN gebundenen Klienten. Als Testfaelle je Ausnahmeart festgenagelt; datenbankseitig gemessen mit `calendarsource-gebundenes-loeschen-fremder-zeile-trifft-keine-zeile` und `calendarsource-gebundenes-update-ueber-kennung-allein-trifft-null-zeilen`. |
|
||||||
|
| T-CWH-04 | Denial of Service | `calendar.service.ts` `getSources`/`fetchAndCacheEvents`, plus `calendar-widget.tsx`/`calendar-settings-panel.tsx` | high | mitigate | Die umgekehrte Fehlerrichtung: ein zu kleines Leseergebnis liefert einen leeren Kalender und eine leere Quellenliste, kein Fehlerbild — gelesen als "Synchronisation kaputt" oder "keine Quelle eingerichtet". Das Frontend verschluckt zusaetzlich LAUTE Fehler derselben Pfade in denselben leeren Zustand. Der Nutzer legt neu an und tippt Zugangsdaten in ein scheinbar defektes System. Vollstaendige Bindung aller Lesepfade; Signaltabelle mit Spalte "laesst das Frontend das Signal durch"; namentliche Liste in (k3); offener Ledger-Eintrag mit konkreter Vorabpruefung fuer Etappe 4. Das Frontend wird NICHT geaendert — beschrieben, nicht unterbrochen. |
|
||||||
|
| T-CWH-05 | Tampering | `calendar.service.ts` `fetchAndCacheEvents`, Synchronstatus-Rueckschreibungen | high | mitigate | Die halb gebundene Schleife (Befund K): Laden gebunden, Rueckschreiben nicht (oder umgekehrt) — nach dem Scharfschalten scheitert das Rueckschreiben, der `catch` scheitert erneut, `Promise.allSettled` laesst die Ereignisse dieser Quelle STILL fallen, `lastSyncError` wird nie gesetzt. Ein Klient je Methode; beide Rueckschreibungen als Testfaelle festgenagelt; der Falsifizierungsnachweis von Aufgabe 2 nimmt genau eine dieser Bindungen zurueck. Fehlerklasse des Wettlauf-Falls gemessen (`...-gebundenes-update-auf-unsichtbare-zeile-scheitert-laut`). |
|
||||||
|
| T-CWH-06 | Tampering | `calendar.service.ts` `updateSource`, Zugangsdaten-Erhaltung | medium | mitigate | Zugangsdatenverlust ueber einen Erhaltungspfad, der nach dem Scharfschalten leer laeuft (die `dkv`-Form: lesen, entschluesseln, neu verschluesseln). Befund E: die Form existiert hier NICHT — Erhaltung per Weglassen des Felds, das Web-Formular laesst ein leeres Feld weg, kein Lesezugriff kann leer laufen. Zur Ausfuehrungszeit an allen vier Stellen nachgelesen (Aufgabe 1), als drei Testfaelle festgenagelt (Aufgabe 2), damit eine spaeter eingebaute Erhaltungsform sofort rot wird. Faellt der Befund anders aus, wird die `dkv`-Reparatur angewandt. |
|
||||||
|
| T-CWH-07 | Information Disclosure | `calendar.service.ts` `eventCache`, Schluessel ohne Mandantenanteil | medium | mitigate | Quer-Lesen von Ereignissen ueber einen Cache-Treffer, falls Benutzerkennungen zwischen Mandanten kollidieren koennten. Befund F: der Schluessel traegt `User.id` (`@default(uuid())`), nicht den Anmeldenamen; die Etappe-3-Entscheidung (1) betrifft `username`/`email`, nicht `id`. Vierteilige Kette in Aufgabe 1 Glied fuer Glied nachgesehen; Urteil als Kommentar an der Stelle und in (k4); Testfall, dass zwei Benutzerkennungen keinen Eintrag teilen. Lautet das Urteil anders, bekommt der Schluessel einen Mandantenanteil. |
|
||||||
|
| T-CWH-08 | Elevation of Privilege | `calendar.controller.ts`, Durchreichen des Mandanten | high | mitigate | Die Mandantenkennung koennte beim Umbau versehentlich aus Rumpf oder Pfad statt aus dem Sitzungsnachweis genommen werden. Alle sechs kontextnutzenden Handler nehmen sie unveraendert aus `extractContext`, das ohne Mandant mit `ForbiddenException` abbricht — keine neue Vertrauensquelle. Gate: sechs Handler mit Benutzer- und Mandantenkennung, keiner mit Benutzerkennung allein; Erlaubnisliste des Umfangs. |
|
||||||
|
| T-CWH-09 | Information Disclosure | Besitzpruefung, 403 statt 404 | low | accept | Ein Kollege desselben Mandanten erfaehrt ueber 403 die Existenz einer fremden Quellenkennung. Kennungen sind UUIDs; ein fremder Mandant bekommt nach der Bindung 404. Antwortsemantik wird NICHT geaendert (API-Aenderung ausserhalb des Auftrags); festgehalten in (k4)(d). |
|
||||||
|
| T-CWH-10 | Information Disclosure | `validateUrlNotPrivate`, SSRF-Ausnahme fuer Exchange | low | accept | Bestehender Zustand aus Phase 05/T-05-11 (Exchange-Server liegen im Intranet, Pruefung fuer diesen Typ uebersprungen). Dieser Plan aendert daran nichts und schwaecht es nicht. |
|
||||||
|
| T-CWH-11 | Spoofing | Sitzungsnachweis | low | accept | Mandanten- oder Benutzerkennung aus Rumpf oder Pfad. Bereits in Phase 05 behandelt (T-05-12); dieser Plan aendert daran nichts. |
|
||||||
|
| T-CWH-SC | Tampering | Paketinstallation | low | accept | Dieser Plan installiert kein Paket (npm/pip/cargo) und fuegt keine Abhaengigkeit hinzu. Das Legitimitaets-Gate faellt nicht an; ausdruecklich festgehalten statt schweigend ausgelassen. |
|
||||||
|
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
|
||||||
|
Nach Abschluss aller drei Aufgaben:
|
||||||
|
|
||||||
|
1. `node apps/api/scripts/rls-scratch-check.mjs` meldet alle Pruefungen
|
||||||
|
bestanden (88 bisherige plus die neuen), Rueckgabewert 0.
|
||||||
|
2. `npm --prefix apps/api run test` meldet mindestens 860 Tests gruen in
|
||||||
|
mindestens 57 Dateien (56 bisherige plus die neue Testdatei dieses
|
||||||
|
Bereichs).
|
||||||
|
3. `npm --prefix apps/api run type-check` ist sauber.
|
||||||
|
4. `npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts`
|
||||||
|
ist gruen — die Bestandsaufnahme stimmt mit dem Quelltext ueberein.
|
||||||
|
5. Alle fuenf handgepflegten Stellen der Klassifikation sind maschinell
|
||||||
|
gegatet und gruen, und die Zaehlgates LEITEN ihre Werte aus den im Dokument
|
||||||
|
selbst genannten Messanweisungen ab.
|
||||||
|
6. Der Umfang ist als ERLAUBNISLISTE gegatet: jede Datei, die sich gegenueber
|
||||||
|
`50b3a36` geaendert hat, ist eine der sieben in `files_modified` genannten
|
||||||
|
(oder liegt unter `.planning/`). Unter `apps/api/prisma` und `apps/web` hat
|
||||||
|
sich nichts geaendert. Beide Pruefungen laufen gegen den Ausgangsstand,
|
||||||
|
nicht gegen `HEAD`.
|
||||||
|
7. `DATABASE_URL` zeigt unveraendert auf die Rolle `tessera`; keine Compose-
|
||||||
|
oder Umgebungsdatei ist angefasst; nichts in Active Directory.
|
||||||
|
8. Der Ledger-Eintrag steht in Tabelle, JSON-Block und Kopfzaehlern von
|
||||||
|
`.planning/WINDOWS.md`.
|
||||||
|
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
|
||||||
|
- Die zwoelf Zugriffe des Bereichs sind vollstaendig gebunden, genau ein
|
||||||
|
Klient je Methode mit Datenbankzugriff, keiner unentschieden, keiner
|
||||||
|
begruendet ungebunden — und die Abwesenheit eines ungebundenen Rests ist
|
||||||
|
in der Uebersichtstabelle ausdruecklich begruendet.
|
||||||
|
- Keine Zeile dieses Bereichs wird halb gebunden: Nachschlagen und Schreiben
|
||||||
|
jeder Besitzpruefung und Laden und Rueckschreiben der Aggregationsschleife
|
||||||
|
laufen ueber denselben Klienten — festgenagelt, falsifiziert.
|
||||||
|
- Die umgekehrte Fehlerrichtung ist an der echten Regel in ihrem AKTUELLEN
|
||||||
|
Stand gemessen, mindestens vier Pruefungen laufen ueber den generierten
|
||||||
|
Client an einer schemagleichen Wegwerf-Tabelle, und die Fehlerverschluckung
|
||||||
|
des Frontends ist als eigener Punkt benannt.
|
||||||
|
- Das Urteil zum Cache-Schluessel ist vierteilig belegt und steht in Code und
|
||||||
|
Kritikschrift; der Befund zur Zugangsdaten-Erhaltung ist an vier Stellen
|
||||||
|
nachgelesen und als drei Testfaelle festgenagelt.
|
||||||
|
- Die drei Besitzpruefungen sind gelesen, bestaetigt, ergaenzt und als
|
||||||
|
Testfaelle festgenagelt.
|
||||||
|
- Alle drei Falsifizierungsnachweise sind durchgefuehrt, zurueckgenommen und
|
||||||
|
im SUMMARY mit Testname beziehungsweise Meldung festgehalten.
|
||||||
|
- Jede im SUMMARY genannte Zahl (neue Werkzeugpruefungen, neue Testfaelle,
|
||||||
|
umgestellte Zugriffe) ist ABGEZAEHLT, nicht aus diesem Plan abgeschrieben.
|
||||||
|
- Baseline gehalten am Ende jeder Aufgabe. Der Schalter ist weiterhin AUS.
|
||||||
|
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
Create `.planning/quick/260911-cwh-mandantentrennung-etappe-2-bereich-calen/260911-cwh-SUMMARY.md` when done
|
||||||
|
</output>
|
||||||
Reference in New Issue
Block a user