Files
tessera-ctl/.planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-01-SUMMARY.md
T

12 KiB

phase, plan, subsystem, tags, requires, provides, affects, actuals, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, coverage, duration, completed, status
phase plan subsystem tags requires provides affects actuals tech-stack key-files key-decisions patterns-established requirements-completed coverage duration completed status
15-modul-berechtigungen-gruppen-user-grants 01 auth
prisma
postgres
nestjs
rls
module-access
guards
vitest
phase provides
03-module-marketplace Module/TenantModuleActivation-Modelle, ModuleGuard/ModuleRegistryService/@UseModule als bestehendes Zugriffs-Grundgerüst
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)
15-02
15-03
15-04
15-05
15-06
15-07
15-08
tokens tasks commits
10800 3 3
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
created modified
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
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
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
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
PERM-04
PERM-05
PERM-06
id description requirement verification human_judgment
D1 Group/GroupMembership/ModuleGrant-Schema mit DB-erzwungener Entweder-oder-Beziehung (CHECK num_nonnulls) und Ein-Default-pro-Mandant (partieller Unique-Index) PERM-06
kind ref status
unit apps/api/src/groups/migration-sql.spec.ts — add_groups_and_module_grants migration.sql (6 Tests) pass
kind ref status
manual_procedural 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" pass
false
id description requirement verification human_judgment
D2 D-06-Migrations-Backfill: pro Mandant eine Standardgruppe mit allen Bestandsbenutzern und Grants für alle aktiven Module, idempotent bei Wiederholungslauf PERM-06
kind ref status
unit apps/api/src/groups/migration-sql.spec.ts — gen_random_uuid()/NOT EXISTS/Reihenfolge-Tests pass
kind ref status
manual_procedural Migration gegen lokale DB angewendet: 1 Tenant -> 1 Default-Gruppe, 1 User -> 1 Membership, 1 aktive Aktivierung -> 1 Grant pass
false
id description requirement verification human_judgment
D3 ModuleAccessService.getAccessibleModuleIds als Single Source of Truth (D-01) — ADMIN/SUPER_ADMIN-Kurzschluss, Direkt- und Gruppen-Grants, Schnittmenge mit aktiven Aktivierungen (D-02) PERM-04
kind ref status
unit apps/api/src/module-registry/module-access.service.spec.ts (13 Tests, deckt alle <behavior>-Fälle inkl. adjacency/empty/idempotency/concurrency) pass
false
id description requirement verification human_judgment
D4 ModuleGuard erzwingt getAccessibleModuleIds statt der alten tenant-only isModuleActive-Prüfung; GET /modules/active nutzt dieselbe Auflösung PERM-05
kind ref status
unit apps/api/src/module-registry/module.guard.spec.ts (7 Tests) pass
kind ref status
e2e 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) pass
false
id description verification human_judgment
D5 RLS-Policies für Group/GroupMembership/ModuleGrant (T-15-11), defense-in-depth analog Auth-Kerntabellen
kind ref status
unit apps/api/src/groups/migration-sql.spec.ts — groups_rls_policies migration.sql (4 Tests) pass
kind ref status
manual_procedural Task-2-E2E-Nachweis nach Anwenden der RLS-Migration unverändert reproduziert (kein Verhaltensbruch) pass
false
24min 2026-08-04 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

Self-Check: PASSED

Alle in dieser SUMMARY genannten Dateien existieren auf der Platte, alle genannten Commit-Hashes (c5c704b, 9a4ba8a, 92e8eaf) sind im Git-Log auffindbar.