162 lines
18 KiB
Markdown
162 lines
18 KiB
Markdown
---
|
|
phase: quick-260909-jts
|
|
plan: 01
|
|
subsystem: database
|
|
tags: [prisma, postgresql, row-level-security, groups, module-grants, multi-tenancy, nestjs]
|
|
|
|
requires:
|
|
- phase: quick-260909-ipc
|
|
provides: "ldap area fully bound via forTenant(), distinguishable-client test pattern, rls-access-inventory.spec.ts with bound/unbound Stand column, rls-scratch-check.mjs scratch-database tool"
|
|
provides:
|
|
- "groups.service.ts (12 methods) and module-grants.service.ts (5 methods) fully converted to forTenant()/withTenantTransaction() — 34 previously visible plus 9 previously invisible model accesses"
|
|
- "withTenantTransaction(prisma, tenantId, fn) in prisma-tenant.extension.ts — the interactive-transaction-on-unbound-client helper, empirically the only one of three measured forms that survives real concurrency (the array-form-on-bound-client and interactive-form-on-bound-client both proved unreliable when measured against the live database)"
|
|
- "rls-access-inventory.spec.ts detects model access routed through an interactive-transaction callback parameter (Befund B) — surfaces the (groups.service.ts, tenantModuleActivation) pair that no check in this project had ever seen"
|
|
- "closed the T-JTS-02 gap in addUserToDefaultGroup (Befund E): a foreign-tenant target user is now rejected by the application, since the GroupMembership RLS policy only checks the group side"
|
|
- "docs/mandantentrennung-etappe2-fehlerrichtung.md — groups section with the measured transaction-shape decision, a per-path signal table, and the four code paths that read emptiness as absence"
|
|
- "the Etappe-4 ordering condition from the ldap area (Befund D — default-group handoff before deletion) is closed"
|
|
affects: [mandantentrennung-etappe-2-tenders, mandantentrennung-etappe-3, mandantentrennung-etappe-4]
|
|
|
|
actuals:
|
|
tokens: 26773
|
|
tasks: 3
|
|
commits: 3
|
|
plan_head_before: b532eaa
|
|
|
|
tech-stack:
|
|
added: []
|
|
patterns:
|
|
- "withTenantTransaction(prisma, tenantId, fn): interactive $transaction on the UNbound client, set_config as the first raw statement directly on tx, same tx handed to fn — the empirically-safe pattern for any multi-step, tenant-bound change; forTenant() remains correct for single operations"
|
|
- "Transaction-shape decisions must be measured against the live database before conversion, not assumed from reading the extension code — the array-form-on-bound-client silently splits into multiple sub-transactions (broken atomicity, not a data leak), and the interactive-form-on-bound-client passes a narrow single-shot check but throws P2028 under real concurrent load"
|
|
- "rls-access-inventory.spec.ts's third detection (interactive-transaction callback parameters) generalizes to two receiver forms: <bound-name>.$transaction(async (tx) => ...) and withTenantTransaction(<any>, tenantId, async (tx) => ...) — the latter is always bound by construction"
|
|
|
|
key-files:
|
|
created: []
|
|
modified:
|
|
- apps/api/scripts/rls-scratch-check.mjs
|
|
- apps/api/src/groups/groups.service.ts
|
|
- apps/api/src/groups/groups.service.spec.ts
|
|
- apps/api/src/groups/module-grants.service.ts
|
|
- apps/api/src/groups/module-grants.service.spec.ts
|
|
- apps/api/src/prisma/prisma-tenant.extension.ts
|
|
- apps/api/src/prisma/prisma-tenant.extension.spec.ts
|
|
- apps/api/src/prisma/rls-access-inventory.spec.ts
|
|
- docs/mandantentrennung-etappe2-fehlerrichtung.md
|
|
- docs/mandantentrennung-zugriffsklassifikation.md
|
|
|
|
key-decisions:
|
|
- "withTenantTransaction() built and used for ALL THREE transaction sites (update()'s isDefault:true branch, reassignDefaultBeforeDelete(), ensureDefaultGroup()), not just the one the plan's narrow single-shot measurement required a fallback for. Task 1's single-shot measurement showed both the interactive-form-on-bound-client AND the interactive-form-on-unbound-client passing the plan's three stated conditions (same connection, correct context, correct row count). An additional due-diligence stress test (40 parallel calls, alternating tenants, not required by the plan but directly motivated by threat T-JTS-08) showed the bound-client form throwing P2028 (Transaction API error: Unable to start a transaction in the given time) under real concurrency, while the unbound-client form showed 0 violations. Given ensureDefaultGroup() runs from a startup-repair loop over multiple tenants (exactly the T-JTS-08 scenario), the more fragile form was rejected in favor of the one that survived both tests."
|
|
- "addUserToDefaultGroup() gained a tenant check on the target user (Befund E, T-JTS-02), following the addMembers() precedent two methods above — a foreign user is silently skipped, the method does not throw. This changed two pre-existing tests' fixtures (both needed a __seedUser call they'd been missing) but not their assertions."
|
|
- "The stale comment above the membership query in getUserAccess() ('Kein forTenant hier — dieselbe Begruendung wie bei den drei Abfragen oben') was replaced, not left standing — after binding, the old comment would have actively misled the next reader about the actual state."
|
|
- "listForTenant()'s intermediate `groups` variable needed an explicit `: any[]` annotation (not just `: any`) — TypeScript's noImplicitAny flags callback parameters on a bare `any`-typed value when passed through Array.prototype methods across a function boundary, but not on an explicitly-array-typed one. Confirmed empirically with a minimal repro before applying the fix broadly."
|
|
|
|
patterns-established:
|
|
- "Distinguishable-client test pattern (from 260909-ipc) extended to the interactive-transaction case: __makeBoundClient(tenantId) wraps each of the five relevant models with a call-logging proxy over the SAME backing Maps, and __withTenantTransaction(tenantId, fn) hands that same bound client to fn as the transaction parameter — a forgotten forTenant()/withTenantTransaction() call now fails a specific, per-operation assertion instead of merely 'forTenant was called at some point'."
|
|
|
|
requirements-completed: [WINDOWS-20, ETAPPE-2-GROUPS]
|
|
|
|
duration: ~70min
|
|
completed: 2026-09-09
|
|
status: complete
|
|
---
|
|
|
|
# Quick Task 260909-jts: Mandantentrennung Etappe 2, Bereich groups — Summary
|
|
|
|
**34 sichtbare und 9 zuvor fuer jede Pruefung unsichtbare Datenbankzugriffe in `groups.service.ts` (12 Methoden) und `module-grants.service.ts` (5 Methoden) auf `forTenant()`/`withTenantTransaction()` umgestellt — die Umstellung, auf der die gesamte Etappe ruht, wurde per Messung entschieden, nicht angenommen, und die Reihenfolgebedingung fuer Etappe 4 aus dem ldap-Durchlauf ist geschlossen.**
|
|
|
|
## Performance
|
|
|
|
- **Duration:** ~70 min
|
|
- **Tasks:** 3/3 completed
|
|
- **Files modified:** 10 (0 created, 10 modified)
|
|
- **Commits:** 3
|
|
|
|
## Accomplishments
|
|
|
|
- Der Bereich `groups` (37 Rohtreffer, davon 34 tatsaechliche Modellzugriffe, plus 9 bislang unsichtbare Zugriffe ueber den Rueckgabeparameter einer interaktiven Transaktion) laeuft jetzt vollstaendig ueber den Mandantenkontext.
|
|
- Die einzige offene Architekturfrage der gesamten Umstellung — welche Transaktionsform den Mandantenkontext auf derselben Verbindung traegt — ist an der echten Datenbank gemessen: die Array-Form auf dem gebundenen Client versagt nachweisbar (zwei verschiedene `pg_backend_pid()` fuer zwei Teilschritte einer angeblich gemeinsamen Transaktion), und von den beiden bestehenden Formen ueberlebt nur eine (`withTenantTransaction`, interaktiv auf dem ungebundenen Client) eine zusaetzliche Lastprobe mit 40 parallelen Aufrufen.
|
|
- Zwei latente Mandantenluecken sind gemessen und dokumentiert: die `GroupMembership`-Regel prueft nur die Gruppenseite (Befund E, T-JTS-02 — jetzt in `addUserToDefaultGroup` durch eine Anwendungspruefung geschlossen), und die `ModuleGrant`-Regel prueft nur die Mandantenkennung der Zeile, nicht die referenzierte Gruppe (Befund F, T-JTS-03 — die bestehende `assertTargetBelongsToTenant`-Pruefung bleibt deshalb der primaere Schutz).
|
|
- Die maschinelle Absicherung (`rls-access-inventory.spec.ts`) sieht jetzt Modellzugriffe ueber den Rueckgabeparameter einer interaktiven Transaktion — macht das Paar (`groups.service.ts`, `tenantModuleActivation`) erstmals sichtbar, das genau auf der Schreibstelle mit der groessten Wirkung sass (Modulfreigaben fuer neu angelegte Standardgruppen).
|
|
- Die Reihenfolgebedingung fuer Etappe 4 aus dem ldap-Durchlauf (Befund D: die Standardgruppen-Uebergabe vor einer Gruppenloeschung) ist geschlossen und in beiden Dokumenten als erledigt vermerkt.
|
|
- `docs/mandantentrennung-etappe2-fehlerrichtung.md` bekommt einen eigenen `groups`-Abschnitt mit den tatsaechlich beobachteten Messwerten (nicht erwarteten), einer Signaltabelle je Pfad und der Stelle, an der zu wenig Lesen zu viel Schreiben ausloest.
|
|
|
|
## Task Commits
|
|
|
|
1. **Aufgabe 1: Fehlerrichtung fuer groups schreiben und die Transaktionsfrage messen** - `fd0b9f7` (feat)
|
|
2. **Aufgabe 2: groups.service.ts binden, die Transaktionen tragfaehig machen, die Absicherung sehend machen** - `7f08b27` (feat)
|
|
3. **Aufgabe 3: module-grants.service.ts binden und beide Dokumente schliessen** - `abb6c8b` (feat)
|
|
|
|
**Plan metadata:** committed separately by the orchestrator after this SUMMARY.
|
|
|
|
## Files Created/Modified
|
|
|
|
- `apps/api/scripts/rls-scratch-check.mjs` - neuer Abschnitt `runGroupsAreaChecks` (9 Messungen gegen die aus zwei ausgelieferten Migrationen extrahierten Group/GroupMembership/ModuleGrant/TenantModuleActivation-Policies) plus `runTransactionShapeMeasurement` (die eine Transaktionspruefung, benennt namentlich welche der drei Formen tragen)
|
|
- `apps/api/src/prisma/prisma-tenant.extension.ts` - neues `withTenantTransaction(prisma, tenantId, fn)`, Kopfkommentar um die gemessene Entscheidung fuer diesen Bereich erweitert
|
|
- `apps/api/src/prisma/prisma-tenant.extension.spec.ts` - bestehender "keine interaktive Form"-Test auf `forTenant()` selbst verengt (nicht mehr die ganze Datei), 4 neue Tests fuer `withTenantTransaction`
|
|
- `apps/api/src/prisma/rls-access-inventory.spec.ts` - dritte Erkennung fuer Modellzugriffe ueber Transaktionsparameter (zwei Empfaengerformen), neue Vollstaendigkeits-Pruefung, Ausnahmeliste startet leer
|
|
- `apps/api/src/groups/groups.service.ts` - alle 12 Methoden gebunden, drei Transaktionsstellen auf `withTenantTransaction()` umgestellt, `addUserToDefaultGroup` prueft neu den Zielbenutzer-Mandanten (Befund E)
|
|
- `apps/api/src/groups/groups.service.spec.ts` - zwei unterscheidbare Clients ueber demselben Speicher-Fake, 13 neue Bindungsnachweise, alle 42 Bestandstests unveraendert gruen (zwei Fixture-Ergaenzungen fuer den neuen Mandantencheck)
|
|
- `apps/api/src/groups/module-grants.service.ts` - alle 5 Methoden gebunden, T-JTS-03-Begruendung an `grant()` ergaenzt, veralteter Kommentar in `getUserAccess()` ersetzt
|
|
- `apps/api/src/groups/module-grants.service.spec.ts` - derselbe Bindungsnachweis-Mock, 6 neue Tests, alle 28 Bestandstests unveraendert gruen
|
|
- `docs/mandantentrennung-etappe2-fehlerrichtung.md` - neuer Abschnitt "Bereich groups" (Messbeleg, Transaktionsform-Entscheidung inkl. Lastprobe, Signaltabelle, vier Pflichtpunkte, was nicht geloest wird), ldap-Abschnitt (e) um Nachtrag zu Befund D erweitert
|
|
- `docs/mandantentrennung-zugriffsklassifikation.md` - neun Zeilen fuer `groups.service.ts`/`module-grants.service.ts` auf `gebunden`, neue Zeile fuer das bislang unsichtbare Paar (`groups.service.ts`, `tenantModuleActivation`), Bereichsuebersicht neu gemessen (0/31, mit dokumentiertem methodischen Bodensatz), Klassen-Verteilung auf 62 Paare, "Hintergrunddienst als Falle" fuer `ldap.service.ts` auf geschlossen aktualisiert
|
|
|
|
## Decisions Made
|
|
|
|
- **`withTenantTransaction()` fuer alle drei Transaktionsstellen gewaehlt, nicht nur wo der Plan es zwingend verlangte.** Die Einzelmessung aus Aufgabe 1 liess technisch zwei Formen bestehen (interaktiv auf gebundenem Client; interaktiv auf ungebundenem Client). Eine zusaetzliche, ueber den Plan hinausgehende Lastprobe (40 parallele Aufrufe) zeigte, dass die erste Form unter echter Nebenlaeufigkeit mit `P2028` (Transaction API error) abbricht — ein Denial-of-Service-Risiko genau an der von T-JTS-08 benannten Stelle (Startreparatur ueber mehrere Mandanten). Die zweite Form zeigte 0 Verletzungen unter derselben Last und wurde deshalb durchgehend gewaehlt.
|
|
- **`addUserToDefaultGroup()` prueft jetzt den Mandanten des Zielbenutzers** (Befund E, T-JTS-02), nach dem Vorbild von `addMembers()`. Zwei Bestandstests brauchten dafuer einen zusaetzlichen `__seedUser`-Aufruf (die Methode wurde vorher mit einer nie existierenden `userId` getestet — eine Luecke im urspruenglichen Testaufbau, die die Umstellung sichtbar machte).
|
|
- **Der veraltete Kommentar in `ModuleGrantsService.getUserAccess()` wurde ersetzt, nicht stehen gelassen** — nach der Bindung waere die alte Begruendung ("kein forTenant hier") aktiv irrefuehrend gewesen.
|
|
- **`listForTenant()` bekam eine explizite `any[]`-Annotation** statt der impliziten `any`, nachdem eine minimale Reproduktion zeigte, dass TypeScript sonst `noImplicitAny` in JEDEM Aufrufer auslöst, der `.find()`/`.map()` auf dem Rueckgabewert aufruft — ein reiner `any`-Typ propagiert diese Pruefung nicht ab, ein `any[]`-Typ schon.
|
|
|
|
## Deviations from Plan
|
|
|
|
None (Rule 1-3) — plan executed as written, with one auto-fixed mechanical issue:
|
|
|
|
**1. [Rule 1 - Bug] TypeScript implicit-any errors in twelve test-file call sites after binding**
|
|
- **Found during:** Task 2, type-check
|
|
- **Issue:** `listForTenant()`'s inferred return type collapsed from a concrete Prisma-generated shape to a bare `any` once its underlying query ran through the `any`-cast `forTenant()` client; every caller in `groups.service.spec.ts` chaining `.find()`/`.map()` on that result tripped `noImplicitAny` (TS7006), confirmed via a minimal standalone repro before fixing broadly.
|
|
- **Fix:** Annotated the intermediate `groups` variable inside `listForTenant()` as `any[]` instead of leaving it unannotated.
|
|
- **Files modified:** apps/api/src/groups/groups.service.ts
|
|
- **Verification:** `npm --prefix apps/api run type-check` returns 0.
|
|
- **Committed in:** 7f08b27 (part of task commit)
|
|
|
|
---
|
|
|
|
**Total deviations:** 1 auto-fixed (Rule 1, mechanical/type-inference correctness, no scope creep).
|
|
**Impact on plan:** None — the fix was necessary to make the plan's own conversion type-clean; it touched no production behavior.
|
|
|
|
## Issues Encountered
|
|
|
|
None beyond the deviation above. The RED-first TDD cycle for Aufgabe 2 and Aufgabe 3 both ran cleanly: the new binding-proof tests failed against the pre-conversion code for the expected reason (missing bound-call-log entries), then passed after conversion, without needing a second red/green iteration.
|
|
|
|
## User Setup Required
|
|
|
|
None - no external service configuration required. The scratch-database check requires `TESSERA_SCRATCH_ADMIN_URL` (existing convention, not new to this plan).
|
|
|
|
## Measured Numbers (for the record)
|
|
|
|
- `npm --prefix apps/api run test` → **743 tests green** (53 test files), baseline was 719 (+24: 13 new groups.service.ts binding tests, 6 new module-grants.service.ts binding tests, 4 new withTenantTransaction tests, 1 new inventory completeness test).
|
|
- `npm --prefix apps/api run type-check` → **0**.
|
|
- `node apps/api/scripts/rls-scratch-check.mjs` → **22/22 Pruefungen bestanden** (13 aus den Vorlaeufern + 9 neue Verhaltenspruefungen des Bereichs groups). Belegzeile: `group-ungebunden-null-zeilen: bestanden — ungebundener SELECT auf "Group" liefert 0 Zeile(n)`.
|
|
- Transaktionsmessung, namentlich: `bestanden: [Form (ii) — interaktive Callback-Form auf gebundenem Client ; Form (iii) — interaktive Callback-Form auf ungebundenem Client (set_config auf tx)] — nicht bestanden: [Form (i) — Array-Form auf gebundenem Client]`. Zusaetzliche, ueber den Plan hinausgehende Lastprobe (40 parallele Aufrufe, nicht im Werkzeug persistiert, manuell im Kritikschrift-Abschnitt dokumentiert): Form (ii) brach mit `P2028` ab, Form (iii) zeigte 0 Verletzungen.
|
|
- Klassifikationsdokument: 62 (Datei, Modell)-Paare (war 61), Klasse `muss-mandantengebunden` jetzt 33 (war 32). Bereichsuebersicht `groups`: 0 ungebunden / 31 gebunden (Rohtrefferzaehlung, dokumentierter Bodensatz — 9 weitere ueber `tx` gebundene Zugriffe zaehlt die einfache Grep-Konvention strukturell nicht, die rigorose (Datei,Modell)-Bestandsaufnahme sieht sie).
|
|
- `git status --porcelain -- apps/api/prisma/schema.prisma apps/api/prisma/migrations docker-compose*.yml` → leer, bestaetigt ueber den gesamten Plan.
|
|
- `DATABASE_URL` / Rolle `tessera` (BYPASSRLS) unveraendert — der Schalter bleibt AUS.
|
|
|
|
## Deferred to Later Stages
|
|
|
|
1. **Die offene Architekturfrage `req.tenantPrisma`** — auch `groups` entscheidet sie nicht; bindet dienst-intern wie `ldap` und `auth.service.ts`.
|
|
2. **Die zwei gemessenen Regel-Luecken (Befund E: `GroupMembership` prueft nur die Gruppenseite; Befund F: `ModuleGrant` prueft nur die eigene Mandantenkennung, nicht die referenzierte Gruppe)** bleiben Anwendungspruefungen — sie werden durch dieses Vorgehen NICHT durch eine Datenbankregel ersetzt. Eine etwaige Schemaerweiterung (z. B. eine zusammengesetzte Fremdschluessel-Regel) ist ausdruecklich nicht Teil dieses Durchlaufs (Schema-Tor greift nicht, aber eine Erweiterung waere ein Abbruchgrund gewesen — trat nicht ein).
|
|
3. **Der naechste Bereich der Etappe 2 ist `tenders`** (62 Rohtreffer, groesster verbleibender Bereich) — die in diesem Durchlauf gebaute `withTenantTransaction()`-Infrastruktur und die dritte Erkennung in `rls-access-inventory.spec.ts` sind wiederverwendbar, falls `tenders` ebenfalls mehrschrittige Transaktionen enthaelt (noch nicht gemessen).
|
|
|
|
## Next Phase Readiness
|
|
|
|
Der Bereich `groups` der Etappe 2 ist per Erfolgskriterien dieses Plans vollstaendig geschlossen. Die Reihenfolgebedingung fuer Etappe 4 aus dem ldap-Durchlauf (Befund D) ist erfuellt. Naechster Bereich laut Klassen-Verteilung: `tenders` (62 Rohtreffer, groesster verbleibender Bereich der Etappe 2) — noch nicht dahingehend gemessen, ob dort eigene Transaktionen vorkommen; falls ja, ist `withTenantTransaction()` bereits vorhanden und muss nicht neu gebaut werden.
|
|
|
|
---
|
|
*Phase: quick-260909-jts*
|
|
*Completed: 2026-09-09*
|
|
|
|
## Self-Check: PASSED
|
|
|
|
All 11 claimed files verified present on disk; all 3 claimed commit hashes (fd0b9f7, 7f08b27, abb6c8b) verified present in git history.
|