diff --git a/.planning/STATE.md b/.planning/STATE.md index c50a4c3..163ad2c 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -373,6 +373,7 @@ None yet. | 260909-cx0 | Hochgeladene Dateien (Avatare, DKV-Exporte) ueberlebten kein `--force-recreate` des api-Containers (WINDOWS #17) — lagen nur in der fluechtigen Container-Schicht, keine Compose-Datei mountete `/app/user-files`. Jetzt benanntes Docker-Volume `user-files` in `docker-compose.yml` und `docker-compose.prod.yml` (Eigentuemerschaft uid 1001 aus dem Image, kein Bind-Mount), Betriebshandbuch Kapitel 6/7 entsprechend nachgezogen. Zweiter, unabhaengiger Punkt: CLAUDE.md nannte fuer die Technik-Tabelle noch die 2026-06/07-Empfehlung (Next.js 16.2.x, Prisma 7.8.x, Keycloak, Redis, TanStack Query, shadcn/ui, Playwright, Husky, lint-staged) statt des installierten Stands — jetzt korrigiert auf Next.js 15.5.19, Prisma 6.19.3 etc., nie uebernommene Empfehlungen in eigenem Abschnitt "Recommended But Not Adopted", `.planning/research/STACK.md` nur mit Hinweiszeile ergaenzt. Keine Abhaengigkeit aktualisiert. **WINDOWS #17 bleibt offen** — die Aenderung erreicht die laufende Installation auf alpha nicht, `/opt/tessera/docker-compose.yml` weicht vom Repository ab und muss vom Nutzer selbst ergaenzt werden | 2026-09-09 | dab72eb,c807049 | [260909-cx0-dateisicherung-nachruesten-und-versionsa](./quick/260909-cx0-dateisicherung-nachruesten-und-versionsa/) | | 260909-cx0 | Dateisicherung nachgeruestet und Versionsangaben geradegezogen. **user-files** liegt jetzt in einem benannten Volume (docker-compose.yml und .prod.yml) — vorher lag der Ordner nur in der fluechtigen Container-Schicht, hochgeladene Profilbilder und DKV-Exporte waeren bei jedem --force-recreate weg gewesen. Benanntes Volume statt Bind-Mount, weil das Image /app/user-files an uid 1001 uebereignet; ein frisch angelegtes Host-Verzeichnis gehoert root und haette aus dem Datenverlust einen kaputten Upload gemacht. docker-compose.dev.yml blieb bewusst unveraendert (Compose fuehrt Mount-Listen ueber das Ziel zusammen). **CLAUDE.md** nennt jetzt die installierten Fassungen statt der urspruenglich empfohlenen (Next.js 15.5.19 statt 16, Prisma 6.19.3 statt 7); neu ist ein Abschnitt 'Recommended But Not Adopted', der sechs nie eingebaute Empfehlungen benennt — darunter Keycloak, Redis und shadcn/ui. Der Block ist generiert, deshalb traegt er einen Herkunftsvermerk und die Recherchedatei eine datierte Hinweiszeile; ihre Zahlen blieben unangetastet. Keine Abhaengigkeit angefasst (per git diff gegengeprueft). **WINDOWS #17 am 2026-09-09 geschlossen.** Beim Nachtragen auf dem Server kam heraus, dass dort gar nicht `docker-compose.yml` gilt: die `.env` setzt `COMPOSE_FILE=docker-compose.prod.yml`. Die drei Zeilen wurden in dieser Datei ergaenzt (Sicherung `docker-compose.prod.yml.bak.20260909-0818`). Nach dem Neuerstellen durch den User belegt: Mount `tessera_user-files -> /app/user-files` vorhanden, Ordner gehoert uid 1001 (das benannte Volume hat die Eigentuemerschaft uebernommen), Schreiben als Dienstnutzer funktioniert, und eine Probedatei lag tatsaechlich unter /var/lib/docker/volumes/tessera_user-files/_data auf dem Host — also ausserhalb des Containers | 2026-09-09 | c807049 | [260909-cx0-dateisicherung-nachruesten-und-versionsa](./quick/260909-cx0-dateisicherung-nachruesten-und-versionsa/) | | 260909-dgj | Mandantentrennung auf Datenbankebene vorbereitet (WINDOWS #18). Ausloeser war ein gemessener Befund: die Anwendung verbindet als Rolle mit Superuser- und BYPASSRLS-Recht, daher greifen die sieben vorhandenen Policies gar nicht — ohne gesetzten Mandantenkontext lieferte 'SELECT count(*) FROM Group' zwei statt null Zeilen. Gebaut wurden: Rolle `tessera_app` ohne Umgehungsrecht (wiederholbare Migration, kein Kennwort im SQL), Trennung von Migrations- und Laufzeitverbindung ueber TESSERA_MIGRATE_DATABASE_URL, ein Pruefwerkzeug mit fuenf transaktionssicheren Nachweisen, Policies fuer die 16 fehlenden Tabellen und eine Betriebsanleitung. **Der Schalter bleibt bewusst aus, #18 bleibt offen:** im Code stehen 182 Datenbankzugriffe ohne Mandantenkontext gegen 19 mit — darunter zwingend der Anmeldeweg, der die Benutzerzeile liest, bevor der Mandant bekannt ist (er kommt erst aus dieser Zeile). Ein Umschalten wuerde die Anmeldung fuer alle sperren. Dabei fiel #19 an: SearchProvider und TenderRssFeedSource haben ein nullable tenantId; die einfache Policy wuerde die plattformweiten Zeilen nach dem Scharfschalten fuer jeden Mandanten unsichtbar machen. 673/673 Tests gruen | 2026-09-09 | efaabc9 | [260909-dgj-mandantentrennung-auf-alle-tabellen-mit-](./quick/260909-dgj-mandantentrennung-auf-alle-tabellen-mit-/) | +| 260909-ipc | Mandantentrennung Etappe 2, Bereich ldap: alle 21 klassifizierten Zugriffe in `ldap-config.service.ts` (9) und `ldap.service.ts` (12) an `forTenant()` gebunden. **Zuerst gemessen, dann gebaut:** `rls-scratch-check.mjs` um `runLdapAreaChecks` erweitert (13/13 bestanden), Beleg ist die Zeile `ldapconfig-ungebunden-null-zeilen` gegen die echte, ausgelieferte Policy — die Umkehr der Fehlerrichtung ist damit gemessen, nicht behauptet. Die geforderte Kritikschrift liegt in `docs/mandantentrennung-etappe2-fehlerrichtung.md` mit Signaltabelle je Pfad und vier namentlich benannten Stellen, die Leere als Abwesenheit deuten. **Sicherheitsluecke nebenbei geschlossen (T-IPC-01):** `DELETE /ldap/config/mappings/:id` nahm nur die Kennung — ein Administrator von Mandant A konnte die Feldzuordnung von B loeschen; der Mandant kommt jetzt aus der Sitzung. **Drei Stellen bleiben bewusst ungebunden, jede mit Begruendung im Code:** `getAllActiveConfigs` und die Start-Nachverschluesselung lesen zwingend uebergreifend; `resolveEmailForWrite` darf nicht gebunden werden, weil `email`/`username` plattformweit eindeutig sind — gebunden saehe die Kollisionspruefung keinen fremden Halter, meldete 'frei', und aus einer sauber berichteten Kollision wuerde ein P2002-Abbruch (Produktfrage fuer Etappe 3). **Befund, der die Testlage aendert:** `forTenant` war in `ldap.service.spec.ts` als Identitaet gemockt — die Tests haetten den Umbau in keiner Richtung bemerkt; ersetzt durch zwei unterscheidbare Clients. `rls-access-inventory.spec.ts` um eine `Stand`-Spalte und Erkennung gebundener Fundstellen erweitert, dabei zwei bisher unbekannte Paare gefunden (`auth.service.ts`/`passwordResetToken`, `ldap.service.ts`/`groupMembership`), Klassifikationsdokument auf 61 Paare nachgezogen. Schema, Migrationen, Compose und Umgebungsdateien unberuehrt, Schalter weiter aus. **Verifiziert 7/7** (unabhaengig nachgemessen: 719/719 Tests, Typpruefung sauber, 13/13 Live-Pruefungen gegen den echten Container) | 2026-09-09 | a0c9ef0,9a57fa7,e1586a4 | [260909-ipc-mandantentrennung-etappe-2-bereich-ldap-](./quick/260909-ipc-mandantentrennung-etappe-2-bereich-ldap-/) | | 260909-eor | Etappe 1 der Mandantentrennung: Anmeldeweg mandantenfaehig gemacht und alle Zugriffe klassifiziert. **Kernfund (#20):** `forTenant()` setzte den Mandantenkontext per set_config auf der Transaktionsverbindung, dispatchte die Abfrage aber ueber den aeusseren Client — empirisch reproduziert (set_config auf Backend-PID 254999, Abfrage auf 255000, Kontext dort NULL). Die Trennung hat damit nie funktioniert, auch nicht an den Stellen, die sie scheinbar nutzten; nach dem Scharfschalten haetten diese Abfragen NULL Zeilen geliefert, was der LDAP-Loeschzweig als 'Gruppe im Verzeichnis verschwunden' gedeutet und geloescht haette. Behoben und live nachgewiesen. Der Anmeldeweg bekam drei SECURITY-DEFINER-Funktionen als schmale Ausnahme (feste Spaltenliste, Gleichheitsbedingung, LIMIT 1) — eine Policy haette nicht gereicht, weil sie zwangslaeufig die ganze Tabelle freigibt. Browser-Gegenprobe lokal bestanden: Anmeldung laedt das Portal, falsches Kennwort verraet weiterhin nicht welches Feld, Kennwort-vergessen laeuft durch (der einzige Protokollfehler war ein lokal fehlender Mailserver, also NACH dem Datenbankzugriff). Klassifikation aller 227 Zugriffe in 59 Einheiten, maschinell gegen Abdriften abgesichert: 31 muessen mandantengebunden werden, 9 teilweise, 16 betreffen keine mandantengebundene Tabelle, 3 bleiben bewusst uebergreifend. 701 Tests gruen | 2026-09-09 | da0ac04 | [260909-eor-anmeldeweg-mandantenfaehig-machen-und-al](./quick/260909-eor-anmeldeweg-mandantenfaehig-machen-und-al/) | ## Deferred Items @@ -417,4 +418,4 @@ Last session: 2026-09-09T12:13:05.681Z Resumed: 2026-09-09 — Sitzung ueber /gsd-resume-work fortgesetzt; Einstiegspunkt Etappe 2 (Bereich ldap), Auswahl des Nutzers steht aus. Stopped at: Etappe 2 Bereich ldap abgeschlossen (Quick 260909-ipc). Naechster Bereich laut .continue-here.md: groups (37 Zugriffe). Resume file: None -Last activity: 2026-09-09 - Etappe 1 der Mandantentrennung abgeschlossen +Last activity: 2026-09-09 - Completed quick task 260909-ipc: Mandantentrennung Etappe 2, Bereich ldap (verifiziert 7/7) diff --git a/.planning/quick/260909-ipc-mandantentrennung-etappe-2-bereich-ldap-/260909-ipc-VERIFICATION.md b/.planning/quick/260909-ipc-mandantentrennung-etappe-2-bereich-ldap-/260909-ipc-VERIFICATION.md new file mode 100644 index 0000000..063d22f --- /dev/null +++ b/.planning/quick/260909-ipc-mandantentrennung-etappe-2-bereich-ldap-/260909-ipc-VERIFICATION.md @@ -0,0 +1,137 @@ +--- +phase: quick-260909-ipc +verified: 2026-09-09T14:20:00Z +status: passed +score: 7/7 must-haves verified +covered_files: + - .planning/quick/260909-ipc-mandantentrennung-etappe-2-bereich-ldap-/260909-ipc-PLAN.md + - .planning/quick/260909-ipc-mandantentrennung-etappe-2-bereich-ldap-/260909-ipc-SUMMARY.md + - apps/api/scripts/rls-scratch-check.mjs + - apps/api/src/ldap/ldap-config.service.spec.ts + - apps/api/src/ldap/ldap-config.service.ts + - apps/api/src/ldap/ldap.controller.ts + - apps/api/src/ldap/ldap.service.spec.ts + - apps/api/src/ldap/ldap.service.ts + - apps/api/src/prisma/rls-access-inventory.spec.ts + - docs/mandantentrennung-etappe2-fehlerrichtung.md + - docs/mandantentrennung-zugriffsklassifikation.md +covered_digest: "v1:sha256:2d8c27e76a2953a31a1eaca490d972fac12725f11f4d2e2f3ff67c2b3229e3dd" +behavior_unverified: 0 +overrides_applied: 0 +--- + +# Quick Task 260909-ipc: Mandantentrennung Etappe 2, Bereich ldap — Verification Report + +**Task Goal:** Convert the 21 classified database access sites in the `ldap` area +(`ldap-config.service.ts`, `ldap.service.ts`) to `forTenant()`, keep the classification +document and its machine guard in sync with the code. + +**Verified:** 2026-09-09 +**Status:** passed +**Re-verification:** No — initial verification + +## Goal Achievement + +### Observable Truths + +| # | Truth | Status | Evidence | +|---|-------|--------|----------| +| 1 | Every tenant-bound DB access in `ldap` runs through `forTenant()` with the caller's known tenant | ✓ VERIFIED | Post-change grep of `this.prisma.` in `ldap-config.service.ts` yields exactly 3 hits (lines 66, 78 in `onApplicationBootstrap`, line 309 in `getAllActiveConfigs`) and in `ldap.service.ts` exactly 1 hit (line 439, `resolveEmailForWrite`) — the three documented exceptions, nothing more | +| 2 | The two deliberate exceptions stay unbound and carry a written reason in the code | ✓ VERIFIED | `resolveEmailForWrite` (ldap.service.ts:417-432) and `getAllActiveConfigs`/`onApplicationBootstrap` (ldap-config.service.ts) each carry a multi-paragraph German comment explaining the platform-wide `@unique` constraint / cross-tenant scheduler read, read in full above | +| 3 | A written critique names, per path, the concrete signal a too-few result would produce, and names the code that reads emptiness as absence | ✓ VERIFIED | `docs/mandantentrennung-etappe2-fehlerrichtung.md` (164 lines) has a per-path signal table (section c, 8 rows) and a dedicated "Welcher Code deutet Leere als Abwesenheit" section (d) naming 4 specific methods with line/behavior detail — not generic prose | +| 4 | `LdapFieldMapping` visibility via the `LdapConfig` join is MEASURED under a role without BYPASSRLS, not asserted | ✓ VERIFIED | Independently re-ran `TESSERA_SCRATCH_ADMIN_URL=... node apps/api/scripts/rls-scratch-check.mjs` myself — all 13 checks passed, including `fieldmapping-folgt-join-auf-ldapconfig: bestanden` and `fieldmapping-schreiben-fremde-konfiguration-abgelehnt: bestanden — ... ERROR: new row violates row-level security policy`. Policy SQL is extracted verbatim from `20260618112133_rls_policies/migration.sql` (confirmed by reading both files), not retyped | +| 5 | Deleting a foreign tenant's field mapping by id no longer succeeds (T-IPC-01) | ✓ VERIFIED | `ldap.controller.ts` `removeFieldMapping` now derives `tenantId` from `req.tenantId` (session) and passes it to the service; `ldap-config.service.ts` `removeFieldMapping(tenantId, mappingId)` does a `tenantPrisma.ldapFieldMapping.findUnique` first and returns `null` (→ 404) when invisible under that tenant. Test `removeFieldMapping() liefert null, wenn die Zuordnung unter diesem Mandanten nicht sichtbar ist (T-IPC-01)` exists and is part of the 719 green tests | +| 6 | Classification doc and machine guard reflect the new state; a green run with a stale doc is impossible | ✓ VERIFIED | `rls-access-inventory.spec.ts` strips comments before scanning, detects `const X = forTenant(` + `X.` bound sites in addition to `this.prisma.` unbound sites, computes a `Stand` per (file, model) pair and asserts it against the doc's new 4th column; ran as part of the full suite (9 tests, all green) | +| 7 | 701+ tests and type-check are green; DATABASE_URL, compose, .env, schema.prisma unchanged | ✓ VERIFIED | Independently ran `npm --prefix apps/api run test` → 719/719 passed (53 files); `npm --prefix apps/api run type-check` → exit 0; `git diff --stat b34500b..HEAD` (11 files changed) contains no schema/migration/compose/.env entries | + +**Score:** 7/7 truths verified (0 present, behavior-unverified) + +### Required Artifacts + +| Artifact | Expected | Status | Details | +|----------|----------|--------|---------| +| `docs/mandantentrennung-etappe2-fehlerrichtung.md` | New critique doc, substantive | ✓ VERIFIED | 164 lines, per-path signal table, named "reads emptiness as absence" section, measured (not assumed) values quoted verbatim | +| `apps/api/scripts/rls-scratch-check.mjs` | 5 new LDAP checks, policies extracted from migration | ✓ VERIFIED | `runLdapAreaChecks` present; independently executed, 13/13 checks pass; policy SQL sliced out of the shipped migration with a hard-fail guard (`ldap-policies-aus-migration-gefunden`) if extraction fails | +| `apps/api/src/ldap/ldap-config.service.ts` | 5 methods bound, 2 stay cross-tenant with reason | ✓ VERIFIED | `getConfig`/`createConfig`/`updateConfig`/`addFieldMapping`/`removeFieldMapping` all create `forTenant(this.prisma, tenantId)`; `getAllActiveConfigs`/`onApplicationBootstrap` unchanged and commented | +| `apps/api/src/ldap/ldap.service.ts` | 11 queries in 6 methods bound, 1 stays cross-tenant | ✓ VERIFIED | Only remaining `this.prisma.` hit is `resolveEmailForWrite` (line 439); all others route through a per-method `tenantPrisma` | +| `apps/api/src/ldap/ldap.controller.ts` | tenant sourced from session for delete | ✓ VERIFIED | `removeFieldMapping(@Req() req, @Param('id') id)` reads `req.tenantId`, 400s if absent, passes to service | +| `apps/api/src/prisma/rls-access-inventory.spec.ts` | detects bound + unbound sites, Stand column check | ✓ VERIFIED | Full implementation read; 9 tests, all green in the full suite run | +| `docs/mandantentrennung-zugriffsklassifikation.md` | new Stand column, corrected class, 2 new pairs | ✓ VERIFIED | All `ldap` rows carry a `Stand` value consistent with source; `(ldap-config.service.ts, ldapConfig)` corrected to `beides`; `auth.service.ts/passwordResetToken` and `ldap.service.ts/groupMembership` present as newly-surfaced pairs with explanatory text | + +### Key Link Verification + +| From | To | Via | Status | Details | +|------|-----|-----|--------|---------| +| `forTenant()` | `tenant_isolation_policy` on `LdapConfig`/`LdapFieldMapping` | scratch-database measurement against real migration SQL | ✓ WIRED | Independently re-run, all 13 checks green including the two evidentiary lines quoted above | +| `LdapFieldMapping` | `LdapConfig` | join-based RLS policy (read AND write measured) | ✓ WIRED | `fieldmapping-folgt-join-auf-ldapconfig` (read) and `fieldmapping-schreiben-fremde-konfiguration-abgelehnt` (write, rejected) both measured and passed | +| `ldap.controller.ts req.tenantId` | `LdapConfigService.removeFieldMapping(tenantId, ...)` | session-derived tenant parameter | ✓ WIRED | Confirmed by reading the controller source; closes the T-IPC-01 gap | +| `ldap.service.ts` delete branch | `groups.service.ts` (reassignDefaultBeforeDelete/ensureDefaultGroup) | documented handoff, NOT part of this conversion | ✓ CONFIRMED OUT OF SCOPE | `grep` of `groups.service.ts` shows it is entirely `this.prisma.*`-based, unconverted, exactly as the plan/critique doc describes as a deferred Etappe-4 ordering condition | +| `rls-access-inventory.spec.ts` | Bestandsaufnahme table incl. Stand column | mechanical cross-check | ✓ WIRED | Comment-stripped regex scan of both `this.prisma.` and `.`; asserts doc rows match measured Stand; ran green | + +### Behavioral Spot-Checks + +| Behavior | Command | Result | Status | +|----------|---------|--------|--------| +| Full test suite | `npm --prefix apps/api run test` | 719/719 passed, 53 files | ✓ PASS | +| Type-check | `npm --prefix apps/api run type-check` | exit 0 | ✓ PASS | +| Scratch RLS probe (all 13, incl. 5 new ldap checks) | `TESSERA_SCRATCH_ADMIN_URL=... node apps/api/scripts/rls-scratch-check.mjs` | "Alle 13 Pruefungen bestanden." | ✓ PASS | +| Debt-marker scan of all 9 touched code/doc files | `grep -nE "TBD\|FIXME\|XXX\|TODO\|HACK\|PLACEHOLDER"` | 0 hits across all 9 files | ✓ PASS | + +### Requirements Coverage + +| Requirement | Source Plan | Description | Status | Evidence | +|-------------|------------|-------------|--------|----------| +| WINDOWS-20 | 260909-ipc-PLAN.md | Mandantentrennung Etappe 2, ldap area | ✓ SATISFIED | 21 sites converted/justified, T-IPC-01 closed, doc + guard in sync | +| ETAPPE-2-LDAP | 260909-ipc-PLAN.md | ldap area of Etappe 2 | ✓ SATISFIED | Same evidence as above | + +### Anti-Patterns Found + +None. No TBD/FIXME/XXX/TODO/HACK/PLACEHOLDER markers in any of the 9 touched implementation/doc files. No stub returns, no hardcoded empty arrays feeding rendered/consumed output. + +### Test Honesty Check (Item 4 of the verification brief) + +`ldap.service.spec.ts` still carries a top-level identity mock for `forTenant` +(`forTenant: vi.fn((p) => p)`), used by the pre-existing describe blocks — this +mock alone genuinely cannot detect a binding regression, matching Befund F's +own diagnosis. A dedicated new describe block ("Bindungsnachweis mit +unterscheidbaren Clients", ~140 lines) overrides `forTenant`'s mock +implementation to return a second, structurally distinct object +(`boundPrisma`, whose `user`/`group`/`groupMembership` sub-objects expose +different methods than `unboundPrisma`). Reasoning through failure modes: if +`resolveEmailForWrite` were changed to query the bound client, or if any of +the six converted methods were changed back to query `this.prisma` directly, +the assertions (`expect(unboundPrisma.user.findUnique).toHaveBeenCalledWith(...)` +/ `expect(boundPrisma.user.findFirst).toHaveBeenCalled()`) would fail — +either because the wrong spy recorded the call, or because the mismatched +mock object lacks the method being called and throws. This is a real, +falsifiable regression test, not a rebranded identity mock. + +`ldap-config.service.spec.ts` keeps the identity mock throughout (per the +plan's own, weaker, behavior spec — it only asserts `forTenant` was called +with the right tenant id, not which object received the query). This leaves +a narrower gap than `ldap.service.ts`, but it is closed by the independent, +textual `rls-access-inventory.spec.ts` guard, which inspects the literal +source for `tenantPrisma.` vs `this.prisma.` regardless of what +any mock returns. + +### Human Verification Required + +None. All must-haves are verifiable from the codebase and confirmed by +independently re-running the test suite, the type-check, and the scratch RLS +probe (not merely trusting SUMMARY.md's reported numbers). + +### Gaps Summary + +No gaps. All 7 must-have truths hold, all artifacts are substantive and +wired, the two deliberate cross-tenant exceptions are justified in code and +tested, the T-IPC-01 deletion gap is closed and tested, the classification +document and its machine guard are in sync (9/9 inventory tests green, +Stand column present and consistent), and the mandated critique document is +substantive with named per-path signals rather than generalities. No schema, +migration, compose, or `.env` changes were made; the cutover switch remains +untouched by this task's diff. + +--- + +_Verified: 2026-09-09_ +_Verifier: Claude (gsd-verifier)_