247 lines
23 KiB
Markdown
247 lines
23 KiB
Markdown
---
|
||
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"
|
||
---
|
||
|
||
<objective>
|
||
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.
|
||
</objective>
|
||
|
||
<execution_context>
|
||
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
||
@$HOME/.claude/gsd-core/templates/summary.md
|
||
</execution_context>
|
||
|
||
<context>
|
||
@.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
|
||
</context>
|
||
|
||
<tasks>
|
||
|
||
<task type="auto" tdd="true">
|
||
<name>Task 1: GroupsModule — CRUD für Gruppen, Mitgliedschaften und Löschauswirkung</name>
|
||
<files>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</files>
|
||
<read_first>
|
||
- 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
|
||
</read_first>
|
||
<behavior>
|
||
- `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.
|
||
</behavior>
|
||
<action>
|
||
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 `<behavior>` 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 `<behavior>` genannten Fall mit gemocktem PrismaService abdeckt.
|
||
</action>
|
||
<verify>
|
||
<automated>pnpm --filter @tessera/api test -- groups.service</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- `pnpm --filter @tessera/api test -- groups.service` ist grün und enthält für jeden unter `<behavior>` 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.
|
||
</acceptance_criteria>
|
||
<done>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.</done>
|
||
</task>
|
||
|
||
<task type="auto" tdd="true">
|
||
<name>Task 2: Automatische Standardgruppen-Mitgliedschaft an genau einem Ort</name>
|
||
<files>apps/api/src/user/user.service.ts, apps/api/src/user/user.service.spec.ts, apps/api/src/user/user.module.ts</files>
|
||
<read_first>
|
||
- 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
|
||
</read_first>
|
||
<behavior>
|
||
- `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.
|
||
</behavior>
|
||
<action>
|
||
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 `<behavior>` 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.
|
||
</action>
|
||
<verify>
|
||
<automated>pnpm --filter @tessera/api test -- user.service && pnpm --filter @tessera/api test -- ldap.service</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- `pnpm --filter @tessera/api test -- user.service` ist grün und enthält für jeden unter `<behavior>` 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.
|
||
</acceptance_criteria>
|
||
<done>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.</done>
|
||
</task>
|
||
|
||
</tasks>
|
||
|
||
<threat_model>
|
||
## 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 |
|
||
</threat_model>
|
||
|
||
<verification>
|
||
- `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.
|
||
</verification>
|
||
|
||
<success_criteria>
|
||
- 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).
|
||
</success_criteria>
|
||
|
||
## 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`.
|
||
|
||
<output>
|
||
Erstelle `.planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-02-SUMMARY.md`, wenn der Plan abgeschlossen ist.
|
||
</output>
|