diff --git a/.planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-01-SUMMARY.md b/.planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-01-SUMMARY.md new file mode 100644 index 0000000..c84d2fb --- /dev/null +++ b/.planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-01-SUMMARY.md @@ -0,0 +1,185 @@ +--- +phase: 15-modul-berechtigungen-gruppen-user-grants +plan: 01 +subsystem: auth +tags: [prisma, postgres, nestjs, rls, module-access, guards, vitest] + +# Dependency graph +requires: + - phase: 03-module-marketplace + provides: Module/TenantModuleActivation-Modelle, ModuleGuard/ModuleRegistryService/@UseModule als bestehendes Zugriffs-Grundgerüst +provides: + - Group/GroupMembership/ModuleGrant-Datenmodelle mit DB-erzwungener Entweder-oder- und Ein-Default-Invariante + - D-06-Backfill (Standardgruppe je Mandant, Bestandsbenutzer, Bestandsgrants) als Teil von `prisma migrate deploy` + - ModuleAccessService als Single Source of Truth für Modulzugriff (D-01) + - ModuleGuard und GET /modules/active auf Benutzer-Ebene (statt nur Mandanten-Aktivierung) umgestellt + - RLS-Policies für die drei neuen Tabellen (T-15-11, defense-in-depth) +affects: [15-02, 15-03, 15-04, 15-05, 15-06, 15-07, 15-08] + +actuals: + tokens: 10800 + tasks: 3 + commits: 3 + +tech-stack: + added: [] + patterns: + - "Hand-editierte migration.sql für DB-Invarianten, die Prisma nicht ausdrücken kann (CHECK num_nonnulls, partielle Unique-Indizes) — Fortführung des in 20260618112133_rls_policies/20260721150000_tender_cpv_divisions_backfill etablierten Verfahrens" + - "Migrations-Backfill mit WHERE-NOT-EXISTS-Wächtern statt separatem TS-Skript, weil er beim automatischen Container-Start (prisma migrate deploy) unbeaufsichtigt laufen muss" + - "Single-Source-of-Truth Access Resolution: eine Methode (getAccessibleModuleIds) speist Guard UND Listing-Endpoint, keine zweite Implementierung" + +key-files: + created: + - apps/api/prisma/migrations/20260804130130_add_groups_and_module_grants/migration.sql + - apps/api/prisma/migrations/20260804130918_groups_rls_policies/migration.sql + - apps/api/src/module-registry/module-access.service.ts + - apps/api/src/module-registry/module-access.service.spec.ts + - apps/api/src/module-registry/module.guard.spec.ts + - apps/api/src/groups/migration-sql.spec.ts + modified: + - apps/api/prisma/schema.prisma + - apps/api/src/module-registry/module.guard.ts + - apps/api/src/module-registry/module-registry.controller.ts + - apps/api/src/module-registry/module-registry.module.ts + +key-decisions: + - "D-01/D-05/D-06/D-02 wie in 15-CONTEXT.md gesperrt umgesetzt (Nutzer-Checkpoint mit 'proceed' bestätigt)" + - "RLS für Group/GroupMembership/ModuleGrant aktiviert (T-15-11) statt sie wie Tender* RLS-frei zu lassen — sie steuern Zugriff wie die Auth-Kerntabellen" + - "grantedIds.length===0 kurzschließt die zweite Query in getAccessibleModuleIds (kein leerer IN-Filter gegen tenantModuleActivation) — Optimierung, keine Verhaltensänderung" + +patterns-established: + - "Pattern 1 aus 15-RESEARCH.md (Single-Source-of-Truth Access Resolution) 1:1 umgesetzt" + - "Pattern 2 (Per-Request-Memoisierung über request.moduleAccessIds statt Scope.REQUEST) im Guard verankert" + +requirements-completed: [PERM-04, PERM-05, PERM-06] + +coverage: + - id: D1 + description: "Group/GroupMembership/ModuleGrant-Schema mit DB-erzwungener Entweder-oder-Beziehung (CHECK num_nonnulls) und Ein-Default-pro-Mandant (partieller Unique-Index)" + requirement: "PERM-06" + verification: + - kind: unit + ref: "apps/api/src/groups/migration-sql.spec.ts — add_groups_and_module_grants migration.sql (6 Tests)" + status: pass + - kind: manual_procedural + ref: "psql gegen lokale DB: INSERT ohne groupId/userId schlägt mit ModuleGrant_group_xor_user fehl; SELECT count(*) FROM \"Group\" WHERE isDefault=true entspricht count(*) FROM \"Tenant\"" + status: pass + human_judgment: false + - id: D2 + description: "D-06-Migrations-Backfill: pro Mandant eine Standardgruppe mit allen Bestandsbenutzern und Grants für alle aktiven Module, idempotent bei Wiederholungslauf" + requirement: "PERM-06" + verification: + - kind: unit + ref: "apps/api/src/groups/migration-sql.spec.ts — gen_random_uuid()/NOT EXISTS/Reihenfolge-Tests" + status: pass + - kind: manual_procedural + ref: "Migration gegen lokale DB angewendet: 1 Tenant -> 1 Default-Gruppe, 1 User -> 1 Membership, 1 aktive Aktivierung -> 1 Grant" + status: pass + human_judgment: false + - id: D3 + description: "ModuleAccessService.getAccessibleModuleIds als Single Source of Truth (D-01) — ADMIN/SUPER_ADMIN-Kurzschluss, Direkt- und Gruppen-Grants, Schnittmenge mit aktiven Aktivierungen (D-02)" + requirement: "PERM-04" + verification: + - kind: unit + ref: "apps/api/src/module-registry/module-access.service.spec.ts (13 Tests, deckt alle -Fälle inkl. adjacency/empty/idempotency/concurrency)" + status: pass + human_judgment: false + - id: D4 + description: "ModuleGuard erzwingt getAccessibleModuleIds statt der alten tenant-only isModuleActive-Prüfung; GET /modules/active nutzt dieselbe Auflösung" + requirement: "PERM-05" + verification: + - kind: unit + ref: "apps/api/src/module-registry/module.guard.spec.ts (7 Tests)" + status: pass + - kind: e2e + ref: "curl gegen laufende lokale API: USER ohne Grant -> 403 auf GET /modules/tender-radar + [] in GET /modules/active; USER mit Direkt-Grant -> 200 + Slug in der Liste; ADMIN ohne Grant -> 200 + Slug in der Liste (vor UND nach der RLS-Migration in Task 3 identisch reproduziert)" + status: pass + human_judgment: false + - id: D5 + description: "RLS-Policies für Group/GroupMembership/ModuleGrant (T-15-11), defense-in-depth analog Auth-Kerntabellen" + verification: + - kind: unit + ref: "apps/api/src/groups/migration-sql.spec.ts — groups_rls_policies migration.sql (4 Tests)" + status: pass + - kind: manual_procedural + ref: "Task-2-E2E-Nachweis nach Anwenden der RLS-Migration unverändert reproduziert (kein Verhaltensbruch)" + status: pass + human_judgment: false + +duration: 24min +completed: 2026-08-04 +status: complete +--- + +# Phase 15 Plan 01: Fundament der Modul-Berechtigungen Summary + +**Group/GroupMembership/ModuleGrant-Schema mit DB-erzwungener Entweder-oder- und Ein-Default-Invariante, produktionswirksamer D-06-Migrationsbackfill, und `ModuleAccessService.getAccessibleModuleIds` als einzige Auflösungsfunktion, verdrahtet in `ModuleGuard` und `GET /modules/active` — beide End-to-End gegen die laufende lokale API bewiesen, vor und nach Aktivierung der RLS-Policies.** + +## Performance + +- **Duration:** 24 min +- **Started:** 2026-08-04T12:48:45Z +- **Completed:** 2026-08-04T13:12:24Z +- **Tasks:** 3 (plus 1 Checkpoint:decision) +- **Files modified:** 10 + +## Accomplishments + +- Drei neue Prisma-Modelle (`Group`, `GroupMembership`, `ModuleGrant`) plus `MembershipSource`-Enum, mit hand-editierter Migrations-SQL für Invarianten, die Prisma 6.19 nicht ausdrücken kann: CHECK-Constraint `num_nonnulls("groupId","userId") = 1` (D-04, Gruppe XOR Benutzer), partieller Unique-Index für genau eine Standardgruppe pro Mandant (D-13), zwei partielle Unique-Indizes gegen Duplikat-Grants +- D-06-Backfill in derselben Migration: pro Mandant eine Gruppe "Alle Benutzer", alle Bestandsbenutzer als Mitglieder, Grants für alle zum Migrationszeitpunkt aktiven Module — läuft automatisch bei jedem `prisma migrate deploy` (Container-Start), jedes der drei INSERTs ist über `WHERE NOT EXISTS` idempotent gegen Wiederholungsläufe abgesichert +- `ModuleAccessService.getAccessibleModuleIds(tenantId, userId, role)` als einzige Auflösungsfunktion (D-01): ADMIN/SUPER_ADMIN-Kurzschluss vor jeder Grant-Query (D-03), sonst eine einzige verschachtelte Prisma-Query für Direkt- und Gruppen-Grants (kein N+1), geschnitten mit den mandantenweit aktiven Modulen (D-02) +- `ModuleGuard` liest `userId`/`role` jetzt zusätzlich zu `tenantId` aus `request.user` (JWT-Herkunft, nie Body/Params) und ruft `getAccessibleModuleIds` auf; `GET /modules/active` delegiert an `ModuleAccessService.findAccessibleModules` statt an die alte mandantenweite `findActiveForTenant` (die für Plan 15-03 unverändert erhalten bleibt) +- RLS aktiviert für alle drei neuen Tabellen (T-15-11), nach dem Muster der Auth-Kerntabellen (`User`, `LdapConfig`) statt der RLS-freien `Tender*`-Tabellen — als separate, eigenständig review-bare Migration +- End-to-End-Nachweis zweimal gegen die laufende lokale API geführt (vor und nach der RLS-Migration, identisches Ergebnis): USER ohne Grant → 403 auf einem `@UseModule`-geschützten Endpoint plus leere `GET /modules/active`-Liste; derselbe USER mit einem Direkt-Grant → 200 plus Slug in der Liste; ADMIN ohne jeden Grant → 200 plus Slug in der Liste + +## Task Commits + +Jeder Task wurde atomar committet: + +1. **Task 1: Schema-Sync — Group/GroupMembership/ModuleGrant plus D-06-Bestandsübernahme** - `c5c704b` (feat) +2. **Task 2: Zugriffsauflösung end-to-end — ModuleAccessService, ModuleGuard, GET /modules/active** - `9a4ba8a` (feat) +3. **Task 3: RLS-Policies für die drei neuen Tabellen und erneuter Tracer-Nachweis** - `92e8eaf` (feat) + +**Plan metadata:** siehe Commit dieser SUMMARY.md (docs: complete plan) + +## Files Created/Modified + +- `apps/api/prisma/schema.prisma` - `MembershipSource`-Enum plus `Group`/`GroupMembership`/`ModuleGrant`-Modelle, Gegenrelationen an `Tenant`/`User`/`Module` +- `apps/api/prisma/migrations/20260804130130_add_groups_and_module_grants/migration.sql` - generierte `CREATE TABLE`-Statements plus hand-editierte Invarianten und D-06-Backfill +- `apps/api/prisma/migrations/20260804130918_groups_rls_policies/migration.sql` - RLS ENABLE/FORCE plus `tenant_isolation_policy` für alle drei Tabellen +- `apps/api/src/module-registry/module-access.service.ts` - `ModuleAccessService` (neu), Single Source of Truth für Modulzugriff +- `apps/api/src/module-registry/module.guard.ts` - erweitert um Benutzer-Dimension (userId/role aus JWT, `getAccessibleModuleIds`-Aufruf, Per-Request-Memoisierung) +- `apps/api/src/module-registry/module-registry.controller.ts` - `findActive` nutzt jetzt `ModuleAccessService.findAccessibleModules` +- `apps/api/src/module-registry/module-registry.module.ts` - `ModuleAccessService` in providers/exports +- `apps/api/src/module-registry/module-access.service.spec.ts` - 13 Tests, deckt jeden ``-Fall aus dem Plan ab +- `apps/api/src/module-registry/module.guard.spec.ts` - 7 Tests, deckt jeden Guard-``-Fall ab +- `apps/api/src/groups/migration-sql.spec.ts` - 10 Tests, prüft beide Migrationsdateien per Textabgleich ohne DB + +## Decisions Made + +- Checkpoint bestätigt: D-01, D-05, D-06 (one-way) sowie D-02 (costly) wie in `15-CONTEXT.md` gesperrt umgesetzt — keine inhaltliche Abweichung +- RLS für die drei neuen Tabellen aktiviert (T-15-11-Empfehlung aus `15-RESEARCH.md` Pitfall 4 übernommen), weil sie wie `LdapConfig` unmittelbar Zugriff steuern, nicht wie `Tender*` reine App-Layer-Daten sind +- `getAccessibleModuleIds` kürzt die zweite Query ab, wenn `grantedIds` leer ist (kein `moduleId: { in: [] }`-Query gegen `tenantModuleActivation`) — reine Performance-Optimierung ohne Verhaltensänderung, durch Tests abgedeckt (`empty`-Fall) + +## Deviations from Plan + +None - plan executed exactly as written. + +## Issues Encountered + +- Der `db`-Container war zu Beginn gestoppt (`Exited`); `docker compose up -d db` startete ihn neu mit demselben `pgdata`-Volume, keine Datenverluste. `psql` war lokal nicht installiert — Verifikationsabfragen liefen stattdessen über `docker exec tessera-ctl-db-1 psql ...` +- Task 3 verlangte, den End-to-End-Nachweis erneut zu führen und bei Abweichung als Blocker zu melden statt die Policies stillschweigend zu entfernen: die Datenbankrolle `tessera` ist ein Postgres-Superuser mit `rolbypassrls=true` und umgeht RLS strukturell — der Nachweis lief nach der RLS-Migration identisch zu Task 2 durch, kein Blocker + +## User Setup Required + +None - keine externe Service-Konfiguration nötig. + +## Next Phase Readiness + +- `ModuleAccessService` ist exportiert und bereit für Plan 15-02 (Gruppen-CRUD, Standardgruppen-Mitgliedschaft in `UserService.create`), Plan 15-03 (Grant-CRUD, Marketplace-Katalog über die unveränderte `findActiveForTenant`) und Plan 15-05 (Dashboard-Widget-Filterung) +- Kein Bestandsbenutzer verliert Zugriff: die Migration wurde gegen die lokale DB angewendet und der Bestand (1 Tenant, 1 User, 1 aktive Aktivierung) korrekt in Standardgruppe/Mitgliedschaft/Grant übernommen +- Kein offener Blocker aus diesem Plan + +--- +*Phase: 15-modul-berechtigungen-gruppen-user-grants* +*Completed: 2026-08-04*