docs(15-01): add SUMMARY for Group/ModuleGrant foundation + access resolution tracer
This commit is contained in:
@@ -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 <behavior>-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 `<behavior>`-Fall aus dem Plan ab
|
||||
- `apps/api/src/module-registry/module.guard.spec.ts` - 7 Tests, deckt jeden Guard-`<behavior>`-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*
|
||||
Reference in New Issue
Block a user