--- phase: 15-modul-berechtigungen-gruppen-user-grants plan: 02 type: execute wave: 2 depends_on: ["15-01"] files_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.ts - apps/api/src/user/user.service.spec.ts - apps/api/src/app.module.ts autonomous: true requirements: [PERM-01, PERM-06] user_setup: [] estimate: tokens: 58000 raw_tokens: 58000 tasks: 2 confidence: low must_haves: truths: - "Ein Admin kann über die API Gruppen seines Mandanten anlegen, umbenennen, löschen sowie Mitglieder manuell zuweisen und entfernen (PERM-01, D-14)." - "Vor dem Löschen einer Gruppe liefert die API die konkrete Zahl der Mitglieder und der Modul-Freigaben, damit der Dialog beide Zahlen nennen kann statt allgemein zu warnen (D-17)." - "Beim Löschen einer Gruppe verschwinden Mitgliedschaften und Grants mit — erzwungen über onDelete: Cascade, nicht über anwendungsseitiges Aufräumen (D-17)." - "Welche Gruppe die Standardgruppe ist, entscheidet die Markierung isDefault, nicht der Name; die Markierung lässt sich auf eine andere Gruppe umhängen und ganz abschalten (D-13)." - "Die Standardgruppe bleibt eine gewöhnliche Gruppe: umbenennbar, löschbar, mit AD bindbar — sie ist kein Sonderobjekt mit eigenen Regeln (D-13)." - "Jeder neu angelegte Benutzer wird automatisch Mitglied der markierten Standardgruppe seines Mandanten — manuell angelegt wie per LDAP importiert, über genau einen Codepfad (D-11, D-12, PERM-06)." - "Existiert keine markierte Standardgruppe, wird ein neuer Benutzer keiner Gruppe zugeordnet und die Anlage schlägt trotzdem nicht fehl (D-13)." - "Jede Gruppen- und Mitgliedschafts-Query filtert zusätzlich auf die tenantId aus dem JWT — eine Gruppen-ID eines fremden Mandanten führt zu 404, nicht zu einem Treffer." # --- UI-SPEC 'UI Considerations' — kein Eintrag dieses Plans (Backend); Backstop-Zeile 'error/Gruppe löschen schlägt fehl' lebt in 15-06 --- # --- Edge-Probe PERM-01 (6 Kategorien) --- - "adjacency/PERM-01: Ein zweites POST /groups mit exakt gleichem Namen im selben Mandanten wird mit HTTP 409 abgelehnt; derselbe Name in einem anderen Mandanten ist erlaubt." - "empty/PERM-01: POST /groups mit leerem oder nur aus Leerzeichen bestehendem Namen wird mit HTTP 400 abgelehnt; GET /groups eines Mandanten ohne Gruppen liefert eine leere Liste mit HTTP 200, nicht 404." - "encoding/PERM-01: Gruppennamen werden getrimmt, aber weder normalisiert noch kleingeschrieben gespeichert und verglichen — 'Vertrieb' und 'vertrieb' sind zwei verschiedene Gruppen (bewusst anders als bei Benutzernamen, die kleingeschrieben werden)." - "ordering/PERM-01: GET /groups sortiert nach name aufsteigend; da (tenantId, name) eindeutig ist, gibt es keine Gleichstände und die Reihenfolge ist über Aufrufe hinweg stabil." - "idempotency/PERM-01: Ein bereits vorhandenes Mitglied erneut hinzuzufügen ist ein folgenloser Upsert mit HTTP 200 statt eines Fehlers; ein nicht vorhandenes Mitglied zu entfernen antwortet HTTP 204 ohne Seiteneffekt." - "concurrency/PERM-01: Zwei gleichzeitige DELETE /groups/:id auf dieselbe Gruppe erzeugen keinen HTTP 500 — der unterlegene Aufruf endet in HTTP 404, Mitgliedschaften und Grants werden per Cascade genau einmal entfernt." artifacts: - "apps/api/src/groups/groups.module.ts — GroupsModule" - "apps/api/src/groups/groups.service.ts — GroupsService" - "apps/api/src/groups/groups.controller.ts — GroupsController" - "apps/api/src/groups/dto/create-group.dto.ts, update-group.dto.ts, add-group-members.dto.ts" - "apps/api/src/groups/groups.service.spec.ts" - "apps/api/src/user/user.service.spec.ts" key_links: - "UserService.create → GroupsService.addUserToDefaultGroup — der einzige Codepfad, über den sowohl der Admin-Controller als auch LdapService.upsertMappedUser und LdapService.importUsersByDn Benutzer erzeugen (D-11/D-12)" - "GroupsController → RolesGuard + @Roles(ADMIN, SUPER_ADMIN) — dieselbe Absicherung wie die Modul-Aktivierungsrouten" - "GET /groups/:id/impact → das Zahlenmaterial für den Löschdialog aus D-17 (Plan 15-06)" - "AppModule importiert GroupsModule — ohne diese Zeile existiert keine der neuen Routen" --- Die Gruppenverwaltung auf der API-Seite: vollständiges CRUD für Gruppen und Mitgliedschaften eines Mandanten, das Zahlenmaterial für den Löschdialog, und die automatische Standardgruppen-Mitgliedschaft für jeden neu entstehenden Benutzer. Purpose: Ohne Gruppen gibt es nichts, worauf Freigaben zeigen könnten. Die automatische Standardgruppen-Mitgliedschaft (D-11/D-12) ist der Teil von PERM-06, der über den einmaligen Migrations-Backfill hinausgeht: sie sorgt dafür, dass auch nach dem Deploy neu angelegte Benutzer nicht in einem zugriffslosen Zustand landen. Output: `GroupsModule` mit Service und Controller, drei DTOs, die Erweiterung von `UserService.create` und zwei Testsuites. @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md @.planning/PROJECT.md @.planning/STATE.md @.planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-CONTEXT.md @.planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-PATTERNS.md @.planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-01-SUMMARY.md Task 1: GroupsModule — CRUD für Gruppen, Mitgliedschaften und Löschauswirkung 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/app.module.ts - apps/api/src/module-registry/module-registry.service.ts — Prisma-Query-Stil, NotFoundException-Behandlung, upsert-Muster als Vorlage - apps/api/src/module-registry/module-registry.controller.ts — die Zeilen 47–54 und 62–74: tenantId aus `req.tenantId ?? req.user?.tenantId`, `@UseGuards(RolesGuard)` plus `@Roles(Role.ADMIN, Role.SUPER_ADMIN)` - apps/api/src/module-registry/module-registry.module.ts — Aufbau eines NestJS-Moduls in diesem Projekt (imports/controllers/providers/exports) - apps/api/src/dashboard/dashboard.service.ts — Zeilen 119–164: das Ownership-Prüfmuster vor jeder Lookup-Mutation (IDOR-Schutz) - apps/api/src/app.module.ts — die Reihenfolge der Modul-Importe - apps/api/prisma/schema.prisma — die aus 15-01 stammenden Modelle Group, GroupMembership, ModuleGrant und ihre Constraints - apps/api/src/tenders/tender-saved-search.service.spec.ts — Vorbild für eine Service-Testsuite mit gemocktem PrismaService - apps/api/src/dkv/dto/ — ein bestehendes DTO als Vorlage für den class-validator-Stil dieses Projekts - `listForTenant(tenantId)` liefert die Gruppen des Mandanten nach `name` aufsteigend, jeweils mit der Zahl der Mitglieder als `memberCount` (über `_count`). - `listForTenant` eines Mandanten ohne Gruppen liefert ein leeres Array. - `create(tenantId, { name })` legt eine Gruppe an; führender und abschliessender Leerraum wird entfernt. - `create` mit leerem oder nur aus Leerzeichen bestehendem Namen wirft BadRequestException. - `create` mit einem im Mandanten bereits vergebenen Namen wirft ConflictException (Prisma-Fehlercode P2002 wird abgefangen). - `create` mit demselben Namen in einem anderen Mandanten ist erfolgreich. - `update(tenantId, id, { name })` benennt um; eine ID eines fremden Mandanten wirft NotFoundException. - `update(tenantId, id, { isDefault: true })` setzt die Markierung und entfernt sie in derselben Transaktion von jeder anderen Gruppe desselben Mandanten. - `update(tenantId, id, { isDefault: false })` schaltet die Markierung ab, ohne sie irgendwo anders zu setzen. - `update(tenantId, id, { ldapDn })` bindet an eine AD-Gruppe; `ldapDn: null` löst die Bindung. - `getImpact(tenantId, id)` liefert `{ memberCount, grantCount }`; eine ID eines fremden Mandanten wirft NotFoundException. - `remove(tenantId, id)` löscht die Gruppe; ein zweiter Aufruf auf dieselbe ID wirft NotFoundException statt eines unbehandelten Prisma-Fehlers. - `addMembers(tenantId, id, userIds)` legt fehlende Mitgliedschaften mit `source: MANUAL` an und ist für bereits vorhandene folgenlos. - `addMembers` mit einer userId eines fremden Mandanten überspringt diese ID und nimmt sie nicht auf. - `removeMember(tenantId, id, userId)` entfernt ausschliesslich Mitgliedschaften mit `source: MANUAL` und lässt LDAP-Mitgliedschaften unberührt. - `removeMember` für ein nicht vorhandenes Mitglied ist folgenlos und wirft nicht. - `addUserToDefaultGroup(tenantId, userId)` legt eine Mitgliedschaft in der Gruppe mit `isDefault: true` an. - `addUserToDefaultGroup` bei einem Mandanten ohne markierte Standardgruppe tut nichts und wirft nicht. Lege das Verzeichnis `apps/api/src/groups/` an mit `groups.module.ts`, `groups.service.ts`, `groups.controller.ts` und `dto/`. `GroupsService` injiziert `PrismaService` (global bereitgestellt) und implementiert die unter `` beschriebenen Methoden. Zentrale Regel für jede Methode mit einer Gruppen-ID: die Lookup-Query filtert immer zusätzlich auf `tenantId`, nach dem Vorbild des Ownership-Checks in `DashboardService.removeWidget` — eine ID aus einem anderen Mandanten darf nie einen Treffer liefern, sondern führt zu `NotFoundException`. Verlasse dich dabei nicht auf RLS, sondern schreibe den Filter explizit. `create` fängt Prisma-Fehlercode `P2002` ab und wandelt ihn in `ConflictException` mit einer Meldung, die den Namenskonflikt benennt — der Unique-Index aus 15-01 ist der eigentliche Durchsetzungspunkt, die Ausnahmebehandlung liefert nur die brauchbare Fehlermeldung. `update` mit `isDefault: true` läuft in `this.prisma.$transaction`: zuerst `updateMany` auf alle Gruppen des Mandanten mit `isDefault: false`, dann `update` der Zielgruppe auf `true`. Der partielle Unique-Index `Group_one_default_per_tenant` aus 15-01 ist das Sicherheitsnetz gegen parallele Aufrufe; die Transaktion ist der normale Pfad. `getImpact` zählt Mitgliedschaften und Grants der Gruppe über zwei `count`-Queries und gibt beide Zahlen zurück. `remove` löscht ausschliesslich die Gruppenzeile — Mitgliedschaften und Grants verschwinden über die in 15-01 definierten `onDelete: Cascade`-Regeln; räume nicht zusätzlich anwendungsseitig auf, sonst gibt es zwei Wahrheiten über das Aufräumverhalten. `addMembers` prüft für jede übergebene userId zuerst, dass der Benutzer zum selben Mandanten gehört, und nutzt dann `createMany` mit `skipDuplicates: true` gegen `@@unique([groupId, userId])`. `removeMember` nutzt `deleteMany` mit `where: { groupId, userId, source: 'MANUAL' }` — die Einschränkung auf MANUAL ist die Umsetzung von D-19: eine über AD gesteuerte Mitgliedschaft entfernt ausschliesslich der Sync. `addUserToDefaultGroup(tenantId, userId)` sucht die Gruppe mit `tenantId` und `isDefault: true`; ist keine vorhanden, kehrt die Methode ohne Wirkung zurück (D-13: die Markierung darf abgeschaltet sein). Andernfalls legt sie die Mitgliedschaft mit `source: MANUAL` per `createMany` mit `skipDuplicates: true` an. Diese Methode wird in Task 2 von `UserService` aufgerufen und muss deshalb aus `GroupsModule` exportiert werden. `GroupsController` unter dem Pfad `groups` bildet ab: `GET /groups` (Liste), `POST /groups` (anlegen), `PATCH /groups/:id` (umbenennen, isDefault setzen, ldapDn setzen oder lösen), `DELETE /groups/:id`, `GET /groups/:id/impact`, `GET /groups/:id/members`, `POST /groups/:id/members`, `DELETE /groups/:id/members/:userId`. Jede Route liest `tenantId` mit `(req as any).tenantId ?? (req as any).user?.tenantId` und wirft bei fehlendem Kontext `ForbiddenException('No tenant context')`. Jede Route trägt `@UseGuards(RolesGuard)` und `@Roles(Role.ADMIN, Role.SUPER_ADMIN)`. Statische Segmente stehen vor Parameter-Routen, damit `:id` keine Route beschattet — dieses Projekt hatte den Fehler schon einmal (Memory `project_nest_route_order`). Die drei DTOs nutzen class-validator im Stil der bestehenden DTOs: `CreateGroupDto` mit `@IsString()` und `@IsNotEmpty()` auf `name`; `UpdateGroupDto` mit optionalem `name`, optionalem `isDefault` als Boolean und optionalem `ldapDn` als String oder null; `AddGroupMembersDto` mit `@IsArray()` und `@IsString({ each: true })` auf `userIds`. Registriere `GroupsModule` in `apps/api/src/app.module.ts` in der Import-Liste nach `ModuleRegistryModule`. Lege `groups.service.spec.ts` an, das jeden unter `` genannten Fall mit gemocktem PrismaService abdeckt. pnpm --filter @tessera/api test -- groups.service - `pnpm --filter @tessera/api test -- groups.service` ist grün und enthält für jeden unter `` gelisteten Fall ein eigenes `it(...)`. - `grep -c 'tenantId' apps/api/src/groups/groups.service.ts` gibt mindestens `10` aus — jede Lookup- und Mutations-Query trägt den Mandantenfilter. - `grep -c "source: 'MANUAL'\|source: MembershipSource.MANUAL" apps/api/src/groups/groups.service.ts` gibt mindestens `2` aus (removeMember und addMembers). - `grep -c '@Roles(Role.ADMIN, Role.SUPER_ADMIN)' apps/api/src/groups/groups.controller.ts` gibt mindestens `8` aus — jede Route ist rollengeschützt. - `grep -c 'GroupsModule' apps/api/src/app.module.ts` gibt mindestens `2` aus (Import und Eintrag in der Modulliste). - Gegen die laufende lokale API: `POST /groups` mit einem bereits vergebenen Namen liefert HTTP 409; `GET /groups/:id/impact` einer Gruppe mit Mitgliedern liefert ein JSON mit den Schlüsseln `memberCount` und `grantCount`; `DELETE /groups/:id` mit einer ID eines fremden Mandanten liefert HTTP 404. - `pnpm --filter @tessera/api run type-check` läuft fehlerfrei durch. Gruppen eines Mandanten lassen sich über die API vollständig verwalten, die Löschauswirkung ist als Zahlenpaar abrufbar, und keine Route lässt sich mit einer ID eines fremden Mandanten bedienen. Task 2: Automatische Standardgruppen-Mitgliedschaft an genau einem Ort apps/api/src/user/user.service.ts, apps/api/src/user/user.service.spec.ts, apps/api/src/user/user.module.ts - apps/api/src/user/user.service.ts — die bestehende `create()` (Zeilen 32–50), der einzige Erzeugungspunkt für Benutzer - apps/api/src/ldap/ldap.service.ts — Zeilen 303–342 (`upsertMappedUser`, ruft `userService.create` im Neuanlage-Zweig) und die zweite Aufrufstelle in `importUsersByDn` - apps/api/src/user/user.module.ts — providers/exports, die um den Import von GroupsModule ergänzt werden - apps/api/src/groups/groups.service.ts — die in Task 1 entstandene Methode `addUserToDefaultGroup` - apps/api/src/auth/auth.service.spec.ts — Vorbild für eine Testsuite mit mehreren gemockten Abhängigkeiten - `UserService.create` mit einem Mandanten, der eine markierte Standardgruppe besitzt, legt den Benutzer an und ruft danach genau einmal `GroupsService.addUserToDefaultGroup` mit derselben tenantId und der frisch erzeugten userId auf. - `UserService.create` mit einem Mandanten ohne markierte Standardgruppe legt den Benutzer an; der Aufruf von `addUserToDefaultGroup` bleibt folgenlos und die Anlage schlägt nicht fehl. - Schlägt die Zuordnung zur Standardgruppe fehl, wird der Benutzer trotzdem zurückgegeben und der Fehler protokolliert — eine gescheiterte Gruppenzuordnung darf keine Benutzeranlage und keinen LDAP-Sync-Lauf abbrechen. - `UserService.create` gibt weiterhin den erzeugten Benutzerdatensatz zurück; die Signatur bleibt unverändert. Erweitere `UserService.create`: nach dem bestehenden `prisma.user.create(...)` wird `GroupsService.addUserToDefaultGroup(data.tenantId, created.id)` aufgerufen und der Rückgabewert von `create` bleibt der erzeugte Benutzer. Der Aufruf liegt in einem try/catch, das den Fehler über einen `Logger` protokolliert und schluckt — ein LDAP-Sync-Durchlauf über hunderte Benutzer darf nicht daran scheitern, dass eine einzelne Gruppenzuordnung klemmt. Dies ist bewusst die einzige Stelle im gesamten Backend, an der die Regel aus D-11 und D-12 steht. Weder `LdapService.upsertMappedUser` noch `LdapService.importUsersByDn` noch der Admin-Benutzer-Controller bekommen eine eigene Kopie: alle drei erzeugen Benutzer ausschliesslich über `UserService.create`, weshalb "eine Regel für beide Herkünfte" hier strukturell erfüllt ist statt durch Konvention. Ergänze in `apps/api/src/user/user.module.ts` den Import von `GroupsModule`. Prüfe dabei auf eine zirkuläre Abhängigkeit: `GroupsModule` darf `UserModule` nicht importieren — `GroupsService` greift für Benutzerprüfungen direkt auf `PrismaService` zu. Sollte doch eine Zirkularität entstehen, löse sie über `forwardRef` und halte den Grund im Kommentar fest. Lege `apps/api/src/user/user.service.spec.ts` an, das die vier unter `` beschriebenen Fälle mit gemocktem PrismaService und gemocktem GroupsService abdeckt. Der Test für den Fehlerfall belegt insbesondere, dass `create` trotz werfendem `addUserToDefaultGroup` den Benutzer zurückgibt. pnpm --filter @tessera/api test -- user.service && pnpm --filter @tessera/api test -- ldap.service - `pnpm --filter @tessera/api test -- user.service` ist grün und enthält für jeden unter `` gelisteten Fall ein eigenes `it(...)`. - `grep -c 'addUserToDefaultGroup' apps/api/src/user/user.service.ts` gibt `1` aus — die Regel steht an genau einer Stelle. - `grep -c 'userService.create' apps/api/src/ldap/ldap.service.ts` ist unverändert gegenüber dem Stand vor diesem Task — der LDAP-Pfad erbt die Regel über den bestehenden Aufruf, statt eine eigene Kopie zu bekommen; `git diff --stat apps/api/src/ldap/ldap.service.ts` zeigt für diesen Task keine Änderung an der Datei. - `pnpm --filter @tessera/api test -- ldap.service` bleibt grün — die bestehenden Sync-Tests laufen unverändert durch. - Gegen die laufende lokale API: ein über `POST /users` angelegter Benutzer erscheint direkt danach in `GET /groups/:defaultGroupId/members`. - `pnpm --filter @tessera/api run type-check` läuft fehlerfrei durch. Jeder neu entstehende Benutzer landet automatisch in der markierten Standardgruppe seines Mandanten, unabhängig davon, ob er manuell angelegt oder aus dem AD importiert wurde — und die Anlage übersteht eine fehlgeschlagene Zuordnung. ## Trust Boundaries | Boundary | Description | |----------|-------------| | Browser → GroupsController | Gruppen- und Benutzer-IDs kommen aus Pfad und Body eines Admin-Clients und sind damit nicht vertrauenswürdig; tenantId kommt ausschliesslich aus dem JWT | | GroupsService → PostgreSQL | Jede Query trägt den Mandantenfilter explizit; RLS aus 15-01 ist das zweite Netz | ## STRIDE Threat Register | Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | |-----------|----------|-----------|----------|-------------|-----------------| | T-15-02 | Information Disclosure / Tampering | `GET/PATCH/DELETE /groups/:id`, `GET /groups/:id/impact`, `POST /groups/:id/members` | high | mitigate | Jede Lookup-Query in `GroupsService` filtert zusätzlich auf die tenantId aus dem JWT nach dem Vorbild von `DashboardService.removeWidget`; eine ID eines fremden Mandanten liefert NotFoundException statt eines Treffers. `groups.service.spec.ts` deckt den Fremdmandanten-Fall pro Methode ab | | T-15-12 | Elevation of Privilege | `POST /groups/:id/members` mit einer userId eines fremden Mandanten | high | mitigate | `addMembers` verifiziert vor jeder Mitgliedschaft, dass `user.tenantId` mit der tenantId aus dem JWT übereinstimmt, und überspringt abweichende IDs — sonst könnte ein Admin einen fremden Benutzer in eine eigene Gruppe legen und ihm darüber Modulzugriff verschaffen | | T-15-13 | Elevation of Privilege | `PATCH /groups/:id` durch einen Benutzer mit Rolle USER | high | mitigate | Jede Route trägt `@UseGuards(RolesGuard)` mit `@Roles(Role.ADMIN, Role.SUPER_ADMIN)`, identisch zu den bestehenden Modul-Aktivierungsrouten | | T-15-14 | Denial of Service | Fehlgeschlagene Standardgruppen-Zuordnung während eines LDAP-Sync-Laufs über viele Benutzer | medium | mitigate | Der Aufruf in `UserService.create` liegt in try/catch mit Logger — ein Einzelfehler bricht weder die Benutzeranlage noch den gesamten Sync-Durchlauf ab | - `pnpm --filter @tessera/api test` vollständig grün. - `pnpm --filter @tessera/api run type-check` fehlerfrei. - Manuell gegen die lokale API: Gruppe anlegen, umbenennen, Standardmarkierung umhängen, Mitglied hinzufügen und entfernen, Löschauswirkung abrufen, Gruppe löschen — anschliessend sind in der Datenbank weder verwaiste `GroupMembership`- noch `ModuleGrant`-Zeilen zu dieser Gruppe vorhanden. - Gruppen und Mitgliedschaften sind über die API vollständig verwaltbar (PERM-01). - Der Löschdialog kann konkrete Zahlen nennen, weil die API sie liefert (D-17). - Neue Benutzer beider Herkünfte treten der markierten Standardgruppe bei, ohne dass die Regel doppelt im Code steht (D-11, D-12, PERM-06). ## Artifacts this phase produces Von diesem Plan erzeugt beziehungsweise verändert: **NestJS-Module, Services, Controller** - `GroupsModule` (`apps/api/src/groups/groups.module.ts`) - `GroupsService` mit `listForTenant`, `create`, `update`, `remove`, `getImpact`, `listMembers`, `addMembers`, `removeMember`, `addUserToDefaultGroup` - `GroupsController` mit `GET /groups`, `POST /groups`, `PATCH /groups/:id`, `DELETE /groups/:id`, `GET /groups/:id/impact`, `GET /groups/:id/members`, `POST /groups/:id/members`, `DELETE /groups/:id/members/:userId` - `UserService.create` (erweitert um die Standardgruppen-Mitgliedschaft) - `AppModule` (erweitert um `GroupsModule`) **DTOs** - `CreateGroupDto`, `UpdateGroupDto`, `AddGroupMembersDto` in `apps/api/src/groups/dto/` **Testdateien** - `apps/api/src/groups/groups.service.spec.ts` - `apps/api/src/user/user.service.spec.ts` Die phasenweite Gesamtliste steht in `15-01-PLAN.md`. Erstelle `.planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-02-SUMMARY.md`, wenn der Plan abgeschlossen ist.