Files

13 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 02 auth
nestjs
prisma
groups
rbac
vitest
phase plan provides
15-modul-berechtigungen-gruppen-user-grants 01 Group/GroupMembership/ModuleGrant-Schema mit DB-erzwungener Entweder-oder- und Ein-Default-Invariante, RLS auf allen drei Tabellen
GroupsService/GroupsController — vollständiges CRUD für Gruppen und Mitgliedschaften eines Mandanten (PERM-01)
GroupsService.getImpact — Zahlenmaterial ({ memberCount, grantCount }) für den Löschdialog (D-17), konsumiert von Plan 15-06
GroupsService.addUserToDefaultGroup — einziger Codepfad für die automatische Standardgruppen-Mitgliedschaft, exportiert für UserModule
UserService.create erweitert um die Standardgruppen-Zuordnung (D-11/D-12) — erbt an LdapService.upsertMappedUser/importUsersByDn ohne eigene Kopie der Regel
15-03
15-04
15-06
tokens tasks commits
10100 2 2
added patterns
Ownership-Check-Muster aus DashboardService.removeWidget auf Gruppen-IDs übertragen: private findOwned(tenantId, id) vor jeder Mutation/jedem Lookup, NotFoundException statt eines Fremdmandanten-Treffers
P2002/P2025-Fehlercode-Übersetzung im Service (ConflictException/NotFoundException) statt unbehandelter Prisma-Fehler bis zum Controller durchzureichen
Hand-rolled In-Memory-Prisma-Fake im Stil von tender-saved-search.service.spec.ts statt eines Mocking-Frameworks — auch für $transaction (Promise.all über bereits erstellte Promises)
created modified
apps/api/src/groups/groups.module.ts
apps/api/src/groups/groups.service.ts
apps/api/src/groups/groups.controller.ts
apps/api/src/groups/groups.service.spec.ts
apps/api/src/groups/dto/create-group.dto.ts
apps/api/src/groups/dto/update-group.dto.ts
apps/api/src/groups/dto/add-group-members.dto.ts
apps/api/src/user/user.service.spec.ts
apps/api/src/app.module.ts
apps/api/src/user/user.service.ts
apps/api/src/user/user.module.ts
isDefault:true läuft in this.prisma.$transaction([updateMany, update]) statt zwei sequenziellen Calls — der partielle Unique-Index aus 15-01 bleibt das Sicherheitsnetz, die Transaktion der normale Pfad (D-13, wie im Plan vorgegeben)
remove() fängt zusätzlich P2025 (Record-not-found) ab und übersetzt es in NotFoundException — nicht explizit im Plan-Action-Text benannt, aber notwendig für die im must_haves-Block geforderte Concurrency-Eigenschaft (zwei DELETE auf dieselbe ID erzeugen kein HTTP 500)
GroupsService.listMembers als zusätzliche Methode ergänzt (nicht im <behavior>-Block des Plans einzeln aufgeführt, aber in den Artefakten/Routen gefordert: GET /groups/:id/members) — Rule 2, notwendig für die vollständige CRUD-Oberfläche
PERM-01
PERM-06
id description requirement verification human_judgment
D1 GroupsService/GroupsController: vollständiges CRUD für Gruppen (create/update/remove/list), Mitgliederverwaltung (addMembers/removeMember/listMembers) und Löschauswirkung (getImpact), jede Lookup-Query tenantId-gescoped PERM-01
kind ref status
unit apps/api/src/groups/groups.service.spec.ts (19 Tests, deckt jeden <behavior>-Fall inkl. adjacency/empty/encoding/idempotency) pass
kind ref status
e2e curl gegen laufende lokale API: POST /groups mit vergebenem Namen -> 409; GET /groups/:id/impact -> {memberCount,grantCount}; DELETE /groups/:id einer echten Fremdmandanten-Gruppe -> 404, Gruppe unangetastet in der DB pass
false
id description requirement verification human_judgment
D2 addUserToDefaultGroup + UserService.create-Erweiterung: jeder neu angelegte Benutzer wird automatisch Mitglied der markierten Standardgruppe seines Mandanten, an genau einer Codestelle PERM-06
kind ref status
unit apps/api/src/groups/groups.service.spec.ts (addUserToDefaultGroup-Fälle) + apps/api/src/user/user.service.spec.ts (4 Tests) pass
kind ref status
e2e curl gegen laufende lokale API: Gruppe als Standard markiert, POST /users angelegt, GET /groups/:defaultGroupId/members zeigt den neuen Benutzer direkt danach pass
false
id description requirement verification human_judgment
D3 Cascade-Löschung: DELETE /groups/:id entfernt GroupMembership-Zeilen ohne anwendungsseitiges Aufräumen (onDelete:Cascade aus 15-01) PERM-01
kind ref status
e2e curl + psql gegen laufende lokale API: Gruppe mit 2 Mitgliedern gelöscht, anschließend SELECT count(*) FROM GroupMembership WHERE groupId=... liefert 0 pass
false
11min 2026-08-04 complete

Phase 15 Plan 02: Gruppenverwaltung & Standardgruppen-Mitgliedschaft Summary

GroupsModule mit vollständigem Gruppen-CRUD, Mitgliederverwaltung und Löschauswirkungs-Zahlenmaterial (D-17), plus die Erweiterung von UserService.create um die automatische Standardgruppen-Mitgliedschaft (D-11/D-12) an genau einer Codestelle — beide Teile end-to-end gegen die laufende lokale API bewiesen.

Performance

  • Duration: 11 min
  • Started: 2026-08-04T13:15:00Z
  • Completed: 2026-08-04T13:25:32Z
  • Tasks: 2
  • Files modified: 11

Accomplishments

  • GroupsService mit listForTenant, create, update, remove, getImpact, listMembers, addMembers, removeMember, addUserToDefaultGroup — jede Methode mit Gruppen-ID filtert zusätzlich auf tenantId (Ownership-Check-Muster aus DashboardService.removeWidget), RLS aus 15-01 ist das zweite Netz, nicht der primäre Schutz
  • update mit isDefault:true läuft in this.prisma.$transaction([updateMany, update]): erst alle Gruppen des Mandanten auf isDefault:false, dann die Zielgruppe auf true — der partielle Unique-Index Group_one_default_per_tenant aus 15-01 bleibt das Sicherheitsnetz gegen parallele Aufrufe
  • getImpact(tenantId, id) liefert { memberCount, grantCount } über zwei parallele count-Queries — das Zahlenmaterial, das der Löschdialog aus D-17 in Plan 15-06 direkt anzeigt
  • remove löscht ausschließlich die Gruppenzeile; Mitgliedschaften und Grants verschwinden über die in 15-01 definierten onDelete:Cascade-Regeln, per psql-Nachweis bestätigt (0 verwaiste GroupMembership-Zeilen nach dem Löschen)
  • addMembers/removeMember setzen D-19 um: addMembers verifiziert jede userId gegen die tenantId des Aufrufers und überspringt fremde IDs (T-15-12), removeMember löscht ausschließlich source: MANUAL-Mitgliedschaften und lässt LDAP-Mitgliedschaften unberührt
  • GroupsController bildet acht rollengeschützte Routen unter /groups ab, tenantId ausschließlich aus req.tenantId ?? req.user?.tenantId (nie aus Body/Params, T-03-04)
  • UserService.create ruft nach prisma.user.create(...) genau einmal GroupsService.addUserToDefaultGroup(created.tenantId, created.id) in try/catch mit Logger auf — dies ist die EINZIGE Stelle im Backend, an der D-11/D-12 steht; LdapService.upsertMappedUser und importUsersByDn erben die Regel unverändert über den bestehenden userService.create-Aufruf (git diff --stat bestätigt: ldap.service.ts in diesem Task nicht verändert)
  • End-to-End-Nachweis gegen die laufende lokale API: Gruppe angelegt, Duplikatname → 409, Mitglied hinzugefügt, impact → {memberCount:1, grantCount:0}, echte Fremdmandanten-Gruppen-ID (separater Test-Tenant, per psql angelegt) auf DELETE /groups/:id → 404 und in der DB unangetastet, Gruppe als Standard markiert, POST /users angelegt → Benutzer erscheint direkt danach in GET /groups/:defaultGroupId/members, Gruppe gelöscht → 0 verwaiste GroupMembership-Zeilen

Task Commits

Jeder Task wurde atomar committet:

  1. Task 1: GroupsModule — CRUD für Gruppen, Mitgliedschaften und Löschauswirkung - 69494d7 (feat)
  2. Task 2: Automatische Standardgruppen-Mitgliedschaft an genau einem Ort - 33838dd (feat)

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

Files Created/Modified

  • apps/api/src/groups/groups.module.ts - GroupsModule, exportiert GroupsService für UserModule
  • apps/api/src/groups/groups.service.ts - GroupsService (neu), CRUD/Mitgliederverwaltung/Standardgruppen-Zuordnung
  • apps/api/src/groups/groups.controller.ts - GroupsController (neu), acht rollengeschützte /groups-Routen
  • apps/api/src/groups/dto/create-group.dto.ts, update-group.dto.ts, add-group-members.dto.ts - class-validator-DTOs
  • apps/api/src/groups/groups.service.spec.ts - 19 Tests, hand-rolled In-Memory-Prisma-Fake
  • apps/api/src/app.module.ts - GroupsModule in der Import-Liste nach ModuleRegistryModule
  • apps/api/src/user/user.service.ts - create() erweitert um addUserToDefaultGroup-Aufruf in try/catch mit Logger
  • apps/api/src/user/user.service.spec.ts - 4 Tests, gemockter PrismaService + GroupsService
  • apps/api/src/user/user.module.ts - Import von GroupsModule ergänzt

Decisions Made

  • isDefault:true läuft in this.prisma.$transaction([updateMany, update]) — wie im Plan vorgegeben, kein Abweichen
  • remove() fängt zusätzlich Prisma-Fehlercode P2025 (Record-not-found) ab und übersetzt ihn in NotFoundException — im Plan-Action-Text nicht wörtlich benannt, aber notwendig, damit ein zweiter remove()-Aufruf auf dieselbe (bereits gelöschte) ID nie einen unbehandelten 500er erzeugt, wie es der must_haves-Block unter „concurrency/PERM-01" explizit fordert
  • listMembers als zusätzliche Service-Methode ergänzt — nicht einzeln im <behavior>-Block aufgeführt, aber Voraussetzung für die im Plan gelistete Route GET /groups/:id/members und damit Teil der geforderten vollständigen CRUD-Oberfläche (Rule 2: fehlende, aber für Korrektheit/Vollständigkeit nötige Funktionalität)

Deviations from Plan

1. [Rule 2 - fehlende Funktionalität] remove() fängt P2025 zusätzlich ab

  • Found during: Task 1, beim Schreiben des Concurrency-Tests aus dem must_haves-Block
  • Issue: Der reine findOwned-Vorab-Check schützt nicht gegen den Fall, dass die Gruppe zwischen dem Lookup und dem delete-Call bereits von einem parallelen Request gelöscht wurde — dann würde prisma.group.delete einen unbehandelten P2025-Fehler werfen statt der geforderten NotFoundException
  • Fix: try/catch um prisma.group.delete, P2025 → NotFoundException
  • Files modified: apps/api/src/groups/groups.service.ts
  • Commit: 69494d7

2. [Rule 2 - fehlende Funktionalität] listMembers-Methode ergänzt

  • Found during: Task 1, beim Umsetzen der im Plan gelisteten Route GET /groups/:id/members
  • Issue: Der <behavior>-Block listet die Methode nicht einzeln auf, aber die Route ist Teil der geforderten CRUD-Oberfläche und ohne sie nicht implementierbar
  • Fix: listMembers(tenantId, id) ergänzt, mandantengescoped wie jede andere Methode
  • Files modified: apps/api/src/groups/groups.service.ts
  • Commit: 69494d7

Issues Encountered

  • Kein API-Container im Docker-Compose-Stack lief zu Beginn (nur tessera-ctl-db-1); der End-to-End-Nachweis lief über pnpm start:dev mit DATABASE_URL gegen die Container-IP der DB (172.19.0.2, tessera:tessera_dev, wie in project_local_db_migrations dokumentiert), Port fest auf 3001 in main.ts (der PORT-Env-Var-Versuch wurde ignoriert). Kein Deploy auf den Testserver, ausschließlich lokal, wie in den Constraints gefordert.
  • Für den Fremdmandanten-Nachweis von DELETE /groups/:id wurde per psql ein zweiter, isolierter Test-Tenant samt Gruppe angelegt und nach dem Test wieder gelöscht — kein Nachweis über eine erratene/zufällige ID, sondern über eine real existierende Gruppe eines anderen Mandanten.

User Setup Required

None - keine externe Service-Konfiguration nötig.

Next Phase Readiness

  • GroupsService.getImpact ist bereit für den Löschdialog in Plan 15-06 (D-17: konkrete Zahlen statt allgemeiner Warnung)
  • GroupsModule exportiert GroupsService, bereit für Plan 15-03 (Grant-CRUD gegen Gruppen) und Plan 15-04 (AD-Bindung/Sync-Erweiterung von Group.ldapDn)
  • Kein Bestandsverhalten gebrochen: volle API-Testsuite (449/449) und ldap.service.ts unverändert (git diff --stat bestätigt 0 Zeilen Diff für diesen Task)
  • 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, beide Commit-Hashes (69494d7, 33838dd) sind im Git-Log auffindbar.