Files

15 KiB

phase, plan, subsystem, tags, requires, provides, affects, actuals, tech-stack, key-files, key-decisions, requirements-completed, coverage, duration, completed, status
phase plan subsystem tags requires provides affects actuals tech-stack key-files key-decisions requirements-completed coverage duration completed status
15-modul-berechtigungen-gruppen-user-grants 03 auth
nestjs
prisma
module-access
groups
grants
vitest
phase plan provides
15-modul-berechtigungen-gruppen-user-grants 01 Group/GroupMembership/ModuleGrant-Schema, ModuleAccessService.getAccessibleModuleIds (D-01) als Single Source of Truth
phase plan provides
15-modul-berechtigungen-gruppen-user-grants 02 GroupsModule/GroupsService (CRUD, Ownership-Check-Muster), an das dieser Plan Controller und Service anhängt
ModuleGrantsService.grant/revoke — Schreibseite der Modul-Freigaben (PERM-03), mit assertTargetBelongsToTenant als Cross-Tenant-Grant-Injection-Schutz vor jedem Insert
ModuleGrantsService.getMatrix — Datenlieferung für die Freigabe-Matrix-Seite (D-15), konsumiert von Plan 15-06/15-07
ModuleGrantsService.getUserAccess — Datenlieferung für das Benutzer-Detail (D-16), zeigt geerbte Gruppen + Direkt-Grant
ModuleGrantsController — vier rollengeschützte /module-grants-Routen
ModuleAccessService.getCatalogFlags + GET /modules/catalog — beide Statusflags (isActiveForTenant, hasAccess) in einer Antwort für den Marketplace (D-08), konsumiert von Plan 15-08
15-06
15-07
15-08
tokens tasks commits
9438 3 3
added patterns
assertTargetBelongsToTenant als eigenständige Cross-Tenant-Prüfung eines REFERENZIERTEN Fremdobjekts (nicht nur direktes Eigentum wie DashboardService.removeWidget) — kein Vorbild im Bestandscode, neu etabliert für Grant-Ziele
P2002 als Erfolgspfad: create() wirft, catch fängt den Fehlercode ab und liefert per findFirst den bestehenden Datensatz zurück statt eines HTTP 500 — Fortführung des in GroupsService.create etablierten Verfahrens
getCatalogFlags liefert eine Map nur für aktive Module; ein fehlender Eintrag wird im Controller auf beide Flags false gemappt, statt die Map für jedes registrierte Modul vorzubefüllen
created modified
apps/api/src/groups/module-grants.service.ts
apps/api/src/groups/module-grants.service.spec.ts
apps/api/src/groups/module-grants.controller.ts
apps/api/src/groups/dto/create-module-grant.dto.ts
apps/api/src/groups/groups.module.ts
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-registry.controller.ts
Kein Import von ModuleRegistryModule in GroupsModule — ModuleGrantsService injiziert ausschließlich PrismaService, die im Plan-Action-Text als 'falls benötigt' formulierte Ergänzung war nicht nötig
grant prüft die aktive TenantModuleActivation ausschließlich über tenantModuleActivation.findUnique (kein zusätzlicher module.findUnique-Existenzcheck) — eine nicht existierende moduleId fällt bereits über die fehlende Aktivierung in dieselbe BadRequestException, ein zweiter Query wäre redundant
PERM-03
PERM-04
id description requirement verification human_judgment
D1 ModuleGrantsService.grant/revoke mit assertTargetBelongsToTenant vor jedem Insert (T-15-01), Entweder-oder-Regel (D-04), aktive TenantModuleActivation als Voraussetzung (D-02), P2002 als Erfolg PERM-03
kind ref status
unit apps/api/src/groups/module-grants.service.spec.ts (20 Tests, deckt jeden <behavior>-Fall inkl. adjacency/empty/ordering/idempotency/concurrency) pass
kind ref status
e2e curl gegen laufende lokale API: POST /module-grants mit groupId eines fremden Test-Mandanten -> 404, Datensatz unangetastet; Gruppen-Grant gesetzt -> GET /modules/tender-radar als Gruppenmitglied 200; Grant entzogen -> derselbe Aufruf sofort 403 (D-09, ohne Zwischenschritt) pass
false
id description requirement verification human_judgment
D2 getMatrix (D-15) und getUserAccess (D-16) liefern Freigabe-Matrix- und Benutzer-Detail-Daten in je einer Antwort, sortiert und mandantengescoped PERM-03
kind ref status
unit apps/api/src/groups/module-grants.service.spec.ts — getMatrix/getUserAccess-Testgruppen pass
kind ref status
e2e curl gegen laufende lokale API: GET /module-grants/matrix als ADMIN -> 200 mit modules/groups/grants, als USER -> 403; GET /module-grants/users/:userId zeigt viaGroups=['Alle Benutzer'] + direct:false für den Testbenutzer pass
false
id description requirement verification human_judgment
D3 ModuleAccessService.getCatalogFlags + GET /modules/catalog liefert isActiveForTenant und hasAccess in einer Antwort (D-08), ADMIN/SUPER_ADMIN sehen hasAccess immer wahr für aktive Module (D-03) PERM-04
kind ref status
unit apps/api/src/module-registry/module-access.service.spec.ts (4 neue getCatalogFlags-Tests) pass
kind ref status
e2e curl gegen laufende lokale API: GET /modules/catalog als USER ohne Gruppen-Mitgliedschaft zeigt tender-radar mit isActiveForTenant:true, hasAccess:false; derselbe Aufruf als ADMIN zeigt beide Flags true pass
false
32min 2026-08-04 complete

Phase 15 Plan 03: Modul-Freigaben — Schreibseite, Matrix, Benutzer-Detail, Katalog Summary

ModuleGrantsService/ModuleGrantsController für Gruppen- und Direkt-Grants mit einer eigens etablierten Cross-Tenant-Gegenprüfung vor jedem Insert (assertTargetBelongsToTenant, T-15-01), die Datenlieferung für Freigabe-Matrix (D-15) und Benutzer-Detail (D-16), und GET /modules/catalog, das dem Marketplace beide Statusflags (isActiveForTenant, hasAccess) in einer Antwort liefert (D-08) — alles end-to-end gegen die laufende lokale API bewiesen, inklusive des sofortigen Entzugs ohne Zwischenschritt (D-09).

Performance

  • Duration: 32 min
  • Started: 2026-08-04T16:28:00Z
  • Completed: 2026-08-04T18:41:00Z (inkl. manueller E2E-Verifikation)
  • Tasks: 3
  • Files modified: 8

Accomplishments

  • ModuleGrantsService mit grant, revoke, getMatrix, getUserAccess und der privaten assertTargetBelongsToTenant — der zentrale Schutz gegen mandantenübergreifende Freigaben (T-15-01). Für dieses Muster gab es kein Vorbild im Bestandscode: bisherige Ownership-Checks (DashboardService.removeWidget, GroupsService.findOwned) prüfen nur direktes Eigentum, nicht eine zweite Mandantengrenze über eine referenzierte Relation
  • grant prüft in fester Reihenfolge: Entweder-oder von groupId/userId (D-04, BadRequestException mit Klartext statt rohem Postgres-Constraint-Namen), dann die Mandanten-Gegenprüfung, dann die aktive TenantModuleActivation (D-02), dann create. Ein P2002 aus dem partiellen Unique-Index (zwei parallele Klicks auf dieselbe Matrix-Zelle) wird abgefangen und liefert per findFirst den bestehenden Datensatz zurück — kein HTTP 500 bei Doppelklick oder echter Nebenläufigkeit
  • revoke nutzt deleteMany mit tenantId im where als IDOR-Schutz (T-15-02) — ein Ziel eines fremden Mandanten trifft null Zeilen, kein vorheriger Lookup nötig
  • getMatrix(tenantId) liefert { modules, groups, grants } in einer Antwort: aktive Module nach category+name, Gruppen nach name, Gruppen-Grants als Paare aus moduleId/groupId — deterministisch über wiederholte Aufrufe
  • getUserAccess(tenantId, userId) liefert je aktivem Modul die Namen der Gruppen, über die der Benutzer erbt, und ein Direkt-Grant-Kennzeichen — Gruppen-Grant und Direkt-Grant auf dasselbe Modul erscheinen gleichzeitig, keiner verdrängt den anderen (Union statt Vorrang)
  • ModuleGrantsController bildet vier rollengeschützte Routen unter /module-grants ab (GET matrix, GET users/:userId, POST, DELETE); matrix ist vor users/:userId deklariert (Beschattungsfehler-Vermeidung, Projekt hatte diesen Fehler schon einmal)
  • ModuleAccessService.getCatalogFlags(tenantId, userId, role) und ModuleRegistryController.findCatalog (GET /modules/catalog) liefern je registriertem Modul beide Statusflags in einer Antwort (D-08) — für ADMIN/SUPER_ADMIN ist hasAccess bei jedem aktiven Modul wahr (D-03), weil getAccessibleModuleIds den Rollen-Kurzschluss anwendet
  • Jede erfolgreiche grant-/revoke-Operation schreibt eine Logzeile über this.logger (D-23) — keine Audit-Tabelle, keine Ansicht im Admin-UI
  • D-04 vollständig eingehalten: der Datensatz trägt kein Rechtestufen-Feld, und der Service bietet keine Methode, die eines setzen könnte
  • End-to-End-Nachweis gegen die laufende lokale API (dist/main gegen die DB-Container-IP, DATABASE_URL/JWT_SECRET wie in vorherigen Plänen dieser Phase): GET /module-grants/matrix als ADMIN → 200 mit modules/groups/grants, als USER → 403; POST /module-grants mit groupId eines eigens angelegten Test-Fremdmandanten → 404, kein Datensatz angelegt; GET /modules/catalog als USER (ohne Gruppen-Mitgliedschaft) zeigt tender-radar mit isActiveForTenant:true, hasAccess:false, derselbe Aufruf als ADMIN zeigt beide Flags true; Testbenutzer der Gruppe „Alle Benutzer“ hinzugefügt → GET /modules/tender-radar 200; Grant entzogen → derselbe Aufruf sofort 403 ohne Zwischenschritt (D-09); GET /module-grants/users/:userId zeigt viaGroups:['Alle Benutzer'], direct:false

Task Commits

Jeder Task wurde atomar committet:

  1. Task 1: ModuleGrantsService — Freigaben setzen und entziehen mit Mandanten-Gegenprüfung - 5e256db (feat)
  2. Task 2: ModuleGrantsController und Einbindung in GroupsModule - 072fb7f (feat)
  3. Task 3: GET /modules/catalog — beide Statusflags in einer Antwort für den Marketplace - 1c32543 (feat)

Plan metadata: siehe Commit dieser SUMMARY.md (docs: complete plan)

Files Created/Modified

  • apps/api/src/groups/module-grants.service.ts - ModuleGrantsService (neu): grant/revoke/getMatrix/getUserAccess/assertTargetBelongsToTenant
  • apps/api/src/groups/module-grants.service.spec.ts - 20 Tests, hand-rolled In-Memory-Prisma-Fake (Projekt-Konvention)
  • apps/api/src/groups/dto/create-module-grant.dto.ts - CreateModuleGrantDto (moduleId required, groupId/userId optional)
  • apps/api/src/groups/module-grants.controller.ts - ModuleGrantsController (neu), vier rollengeschützte /module-grants-Routen
  • apps/api/src/groups/groups.module.ts - ModuleGrantsController/ModuleGrantsService in controllers/providers/exports ergänzt
  • apps/api/src/module-registry/module-access.service.ts - getCatalogFlags ergänzt
  • apps/api/src/module-registry/module-access.service.spec.ts - 4 neue getCatalogFlags-Tests
  • apps/api/src/module-registry/module-registry.controller.ts - findCatalog-Handler (GET /modules/catalog) ergänzt

Decisions Made

  • Kein Import von ModuleRegistryModule in GroupsModule — der Plan-Action-Text formulierte dies als „falls der Service dessen Registry-Methoden benötigt“; ModuleGrantsService injiziert ausschließlich PrismaService, der Import wäre unbenutzte Kopplung gewesen
  • grant prüft die Modul-Aktivierung ausschließlich über tenantModuleActivation.findUnique, ohne separaten module.findUnique-Existenzcheck — eine nicht existierende moduleId landet bereits über die fehlende Aktivierung in derselben BadRequestException, ein zweiter Query wäre redundant gewesen und ist im <behavior>-Block auch nicht gefordert

Deviations from Plan

1. [Sonstiges — Akzeptanzkriterium-Diskrepanz, keine Rule 1-4] DTO-Grep-Kriterium zählt 2 statt 3

  • Found during: Task 1, beim Verifizieren der Akzeptanzkriterien nach dem Schreiben von create-module-grant.dto.ts
  • Issue: Das Akzeptanzkriterium grep -cE '(moduleId|groupId|userId)[?]?: string' ... gibt 3 aus erwartet implizit ein Pflichtfeld ohne Definite-Assignment-Assertion (moduleId: string). Das Projekt erzwingt aber strict: true (inkl. strictPropertyInitialization) in tsconfig.base.json, und jede bestehende DTO im Projekt (CreateGroupDto, AddGroupMembersDto, CreateWidgetDto u. a.) schreibt Pflichtfelder deshalb als feld!: string. Mit moduleId!: string; liefert der Grep nur 2 Treffer (groupId?:, userId?:), weil das ! zwischen Feldname und : steht und die Regex [?]? nur ein optionales ? erlaubt
  • Warum keine Codeänderung: moduleId: string; ohne ! bricht tsc --noEmit mit TS2564: Property 'moduleId' has no initializer — das zweite, härtere Akzeptanzkriterium desselben Tasks. Die beiden Kriterien stehen im Widerspruch; die Projekt-Konvention (! für Pflichtfelder) und ein sauberer type-check wiegen schwerer als der wörtliche Grep-Zähler eines einzelnen Akzeptanzkriteriums
  • Verifiziert stattdessen: pnpm --filter @tessera/api run type-check läuft fehlerfrei, alle 20 Service-Tests inkl. der Entweder-oder-/XOR-Fälle sind grün, und grep -cE '^\s+[a-zA-Z]+[?]?:' ... zeigt weiterhin genau die drei erwarteten Feldnamen (kein viertes Feld)
  • Files modified: apps/api/src/groups/dto/create-module-grant.dto.ts
  • Commit: 5e256db

Issues Encountered

  • Für den End-to-End-Nachweis lief zunächst ein veralteter dist/main-Prozess aus einer vorherigen Sitzung (Build-Stand von 15-05, 15:36 Uhr) auf Port 3001 — beendet, pnpm --filter @tessera/api build neu ausgeführt, mit denselben DATABASE_URL/JWT_SECRET-Werten wie die vorherige Sitzung (Container-IP 172.19.0.2, tessera:tessera_dev) neu gestartet
  • Für den Fremdmandanten-Nachweis (T-15-01) wurde per psql ein zweiter, isolierter Test-Tenant samt Gruppe und Benutzer angelegt (analog zum in 15-02 etablierten Verfahren) — nach dem Nachweis vollständig wieder aus der DB entfernt
  • Für den D-09-Nachweis (sofortiger Entzug) wurde vorübergehend der einzige produktionsnahe Gruppen-Grant (aus dem D-06-Backfill: „Alle Benutzer“ → tender-radar) entzogen und danach exakt wiederhergestellt — die lokale DB enthält nach diesem Plan denselben ModuleGrant-Datensatz wie zuvor (neue id, identische tenantId/moduleId/groupId)
  • Zwei Testbenutzer (test-admin-1503, test-user-1503) und ein Test-Fremdmandant (test-foreign-1503 samt Tenant/Gruppe) wurden ausschließlich für die Dauer der Verifikation angelegt und danach vollständig entfernt — SELECT * FROM "User"/"Tenant" zeigt nach dem Aufräumen wieder exakt den Ausgangsstand (1 Benutzer, 1 Mandant)
  • isDefault der Gruppe „Alle Benutzer“ war bereits vor diesem Plan false (siehe local_db_note im Auftrag) — dieser Plan hat daran nichts geändert und war davon nicht betroffen, da grant/revoke/getMatrix/getUserAccess nicht auf isDefault zugreifen

User Setup Required

None - keine externe Service-Konfiguration nötig.

Next Phase Readiness

  • ModuleGrantsService.getImpact-Äquivalent für Freigaben existiert nicht in diesem Plan (nicht gefordert) — Plan 15-06/15-07 bauen die Admin-Oberflächen (Matrix-Seite, Benutzer-Detail) direkt auf getMatrix/getUserAccess/grant/revoke auf
  • GET /modules/catalog ist bereit für MarketplaceCard (Plan 15-08) — beide Flags kommen in einer Antwort, keine Karte kann kurzzeitig ohne Sperrhinweis klickbar erscheinen
  • Kein Bestandsverhalten gebrochen: volle API-Testsuite (493/493) grün, type-check fehlerfrei
  • 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 (5e256db, 072fb7f, 1c32543) sind im Git-Log auffindbar.