Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
77 KiB
phase, plan, type, wave, depends_on, autonomous, requirements, files_modified, estimate, must_haves
| phase | plan | type | wave | depends_on | autonomous | requirements | files_modified | estimate | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| quick-260911-cwh | 01 | execute | 1 | true |
|
|
|
|
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.
<execution_context>
@/.claude/gsd-core/workflows/execute-plan.md
@/.claude/gsd-core/templates/summary.md
</execution_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:
apps/web/src/components/dashboard/widgets/calendar-widget.tsx, um Zeile 37:if (sources.length === 0)— leere Quellenliste heisstemptyNoSources("keine Quelle eingerichtet"); um Zeile 50:catch { // Silent fail — show empty state }— ein FEHLER vonGET /calendar/sourcesoderGET /calendar/events(403, 500, Netzwerk) fuehrt zusetEvents([]), also zu demselben leeren Zustand wie eine erfolgreiche leere Antwort.apps/web/src/components/settings/calendar-settings-panel.tsx, um Zeile 49:.catch(() => { // Silent fail — show empty state }); um Zeile 127:sources.length === 0zeigtsourceEmpty. Dieselbe Verschluckung auf der Einstellungsseite.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>
Aufgabe 1: Die Fehlerrichtung fuer diesen Bereich MESSEN und aufschreiben — an der Regel, wie sie nach der Migration 20260910120000 steht, durch den generierten Client 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. apps/api/scripts/rls-scratch-check.mjs, docs/mandantentrennung-etappe2-fehlerrichtung.md 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) 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:
calendarsource-gebunden-nur-eigener-mandant— gebundener Lesezugriff fuer TENANT-A liefert ausschliesslich A-Zeilen.calendarsource-ungebunden-null-zeilen— die tragende Belegzeile: der IDENTISCHE Lesezugriff ohne Mandantenkontext liefert null Zeilen, nicht alle vorhandenen.calendarsource-fremder-nutzer-desselben-mandanten-gebunden-sichtbar— das GELINGEN ist das bestandene Ergebnis: die Regel kennt keine Benutzerdimension, die Zeile vonuser-a2ist unter TENANT-A sichtbar, EINSCHLIESSLICHencryptedPassword. 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.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 inNotFoundException.calendarsource-gebundenes-einfuegen-fremder-mandant-abgelehnt— ein gebundenes Einfuegen unter TENANT-A mittenantId = TENANT-Bwird mit SQLSTATE 42501 abgewiesen.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.calendarsource-gebundenes-update-ueber-kennung-allein-trifft-null-zeilen— gebundenesUPDATE ... WHERE id = <B-Zeile>unter TENANT-A trifft null Zeilen (Roh-SQL,lastSyncErrorbleibt unveraendert).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.calendarsource-generierter-client-gebundene-quellenliste-nur-eigener-mandant— ueberbuildInlineExtendedClient(prisma, 'TENANT-A').calendarSource.findMany({ where: { userId: 'user-a1', isVisible: true } }), also der Abfrage, diefetchAndCacheEventsstellt: genau die A1-Zeile.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, dengetSourcesals "keine Quelle" undfetchAndCacheEventsals "keine Termine" weiterreicht.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 undcodewoertlich, damit (k2) und die Kritikschrift die tatsaechlich gemessene Klasse nennen. Rate das Ergebnis NICHT vorweg — es ist der Wettlauf-Fall aus Befund H/K.calendarsource-generierter-client-gebundenes-anlegen-eigener-mandant-gelingt—bound.calendarSource.create(...)unter TENANT-A mittenantId: 'TENANT-A'und den Pflichtfeldern (deraddSource-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,deleteSourceUNDtestConnectionzwischen 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.tseinen Lesezugriff, der ein gespeichertes Passwort laedt, um es neu zu verschluesseln? Wie behandeltupdateSourcedie drei Faelle Feld fehlt, Feld leer, Feld gesetzt? Sendetcalendar-source-form.tsxein 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 inauth.service.ts,extractContextim 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 MigrationCalendarSourcenicht 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 fuerrefreshCacheInBackground: 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 zufetchAndCacheEventsnennt 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 gegendashboardab: 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 zurdkv-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 vorhandeneCalendarSource-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:getAllActiveConfigsim Bereichldap, 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 Bereichfavorites(eigener Bereich), die Antwortsemantik 403/404, und der Fremdkommentar inldap-config.service.ts(Zeile 24, nenntCalendarSourceals 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.
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; }; }
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.
Jeder Fall eigenstaendig, jeder mit sprechendem Namen:
getSources: laeuft gebunden mit der uebergebenen Mandantenkennung im Protokoll; die Antwort traegthasCredentialsund NIEencryptedPassword.getSourcesvon Nutzer A liefert nicht die Quellen von Nutzer B desselben Mandanten — deruserId-Filter bleibt, die Bindung ergaenzt ihn.addSource: gebunden, die Mandantenkennung wird als Pflichtwert geschrieben, das Passwort verschluesselt, die Antwort ohneencryptedPassword.updateSource: BEIDE Abfragen (Nachschlagen und Aendern) ueber DENSELBEN gebundenen Klienten und dieselbe Mandantenkennung.updateSource, Zugangsdaten-Erhaltung in drei Faellen (Befund E): Feld FEHLT —encryptedPasswordbleibt unveraendert und es findet KEIN Lesezugriff statt, der ein Passwort laedt; Feld LEER — wirdnull; Feld GESETZT — wird verschluesselt. Diese drei Faelle nageln fest, dass hier keinedkv-Form existiert; sie werden rot, sobald jemand eine einbaut.updateSource/deleteSource/testConnection: die Besitzpruefung bleibt wirksam — eine Quelle eines anderen Benutzers fuehrt weiterhin zuForbiddenException, eine unbekannte Kennung zuNotFoundException. 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 imcatch) 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.aggregateEventsohne 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.tsZeile 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.
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.
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; }; }
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.
- Die Uebersichtszeile
calendarmit 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). - Die Summenzeile derselben Tabelle, mit fortgeschriebener Herkunftsspur im Hinweisfeld.
- 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 derStandwechselte in Aufgabe 2) — eine unveraenderte Tabelle ohne Vermerk ist von einer vergessenen nicht zu unterscheiden. - 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
refreshCacheInBackgroundbenennt: 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/.
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.mdsind 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 AbschnittWas 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.
<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>
Nach Abschluss aller drei Aufgaben:
node apps/api/scripts/rls-scratch-check.mjsmeldet alle Pruefungen bestanden (88 bisherige plus die neuen), Rueckgabewert 0.npm --prefix apps/api run testmeldet mindestens 860 Tests gruen in mindestens 57 Dateien (56 bisherige plus die neue Testdatei dieses Bereichs).npm --prefix apps/api run type-checkist sauber.npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.tsist gruen — die Bestandsaufnahme stimmt mit dem Quelltext ueberein.- 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.
- Der Umfang ist als ERLAUBNISLISTE gegatet: jede Datei, die sich gegenueber
50b3a36geaendert hat, ist eine der sieben infiles_modifiedgenannten (oder liegt unter.planning/). Unterapps/api/prismaundapps/webhat sich nichts geaendert. Beide Pruefungen laufen gegen den Ausgangsstand, nicht gegenHEAD. DATABASE_URLzeigt unveraendert auf die Rolletessera; keine Compose- oder Umgebungsdatei ist angefasst; nichts in Active Directory.- Der Ledger-Eintrag steht in Tabelle, JSON-Block und Kopfzaehlern von
.planning/WINDOWS.md.
<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>
Create `.planning/quick/260911-cwh-mandantentrennung-etappe-2-bereich-calen/260911-cwh-SUMMARY.md` when done