--- phase: 16-ad-gruppen-synchronisation plan: 02 subsystem: auth tags: [nestjs, prisma, groups, module-grants, vitest] # Dependency graph requires: - phase: 16-ad-gruppen-synchronisation (Plan 16-01) provides: "Group.internalName + Group.ldapObjectGuid Spalten (Migration angewendet), LdapService.importGroupsByDn()" provides: - "GroupsService.reassignDefaultBeforeDelete(tenantId, groupId): deterministischer, nicht-werfender Standardgruppen-Handoff-Baustein (D-06) fuer Plan 16-03" - "GroupsService.update(): serverseitige Namenssperre fuer importierte Gruppen (BadRequestException bei gesetztem ldapObjectGuid), unabhaengiges internalName-Feld mit Leerstring-Normalisierung auf null (D-03/D-04)" - "GroupsService.listForTenant(): liefert internalName zusaetzlich zu name" - "UpdateGroupDto ohne ldapDn (D-07 — keine nachtraegliche AD-Bindung ueber diesen Weg mehr moeglich)" - "ModuleGrantsService.getUserAccess(): Anzeigename mit Fallback (internalName ?? name) an beiden Projektionsstellen, ohne Frontend-Diff (UI-SPEC Surface Contract 6)" affects: [16-03-rekonziliation, 16-04-dialog-umbau, 16-05-anzeige-fallback-frontend] # Actuals (#2632) actuals: tokens: 7155 tasks: 3 commits: 3 # Tech tracking tech-stack: added: [] patterns: - "Batch-taugliche Variante eines Ownership-Checks: reassignDefaultBeforeDelete() nutzt bewusst nicht findOwned() (dessen NotFoundException ist HTTP-gebaut), sondern ein eigenes findFirst-Muster, das bei fehlendem/fremdem Datensatz still false liefert statt zu werfen — fuer Batch-Sync-Laeufe (Plan 16-03)" - "Serverseitige Feldsperre als Backend-Invariante statt UI-Disable: die Namenssperre auf importierten Gruppen sitzt in GroupsService.update(), nicht nur im Formular" key-files: created: [] modified: - apps/api/src/groups/groups.service.ts - apps/api/src/groups/groups.service.spec.ts - apps/api/src/groups/dto/update-group.dto.ts - apps/api/src/groups/module-grants.service.ts - apps/api/src/groups/module-grants.service.spec.ts key-decisions: - "Alle drei Tasks in einer Datei (groups.service.ts) editiert, aber fuer atomare Task-Commits per Write/Edit-Sequenz in Task-Reihenfolge auseinandergezogen (kein git add -p auf einer Live-Bearbeitung) — Task 1 zuerst vollstaendig geschrieben, getestet und committet, danach erst Task-2-Aenderungen an derselben Datei vorgenommen" - "Bestehender Test 'update() mit ldapDn bindet an eine AD-Gruppe' ersatzlos entfernt statt angepasst — die Funktionalitaet ist nach D-07 absichtlich abgeschafft, kein Aequivalent noetig" - "PERM-02 bleibt in REQUIREMENTS.md unangetastet auf [ ] (Pending) — der Plan deckt nur einen Teil der Anforderung ab, das Requirement schliesst laut Plan-Vorgabe erst mit Plan 16-05" patterns-established: - "Namenssperre vor Leerstring-Pruefung: die Reihenfolge in update() (erst ldapObjectGuid-Check, dann trim/empty-Check) verhindert, dass eine gesperrte Gruppe mit der falschen Fehlermeldung antwortet" requirements-completed: [PERM-02] # Plan-Frontmatter-Vertrag; die tatsaechliche REQUIREMENTS.md-Checkbox bleibt bewusst [ ] (siehe Decisions) — schliesst erst mit Plan 16-05 coverage: - id: D1 description: "reassignDefaultBeforeDelete(tenantId, groupId) verschiebt die Standardmarkierung deterministisch (DEFAULT_GROUP_NAME zuerst, sonst aelteste andere Gruppe per createdAt asc), loescht nichts, wirft in keinem der sechs Faelle (D-06)" requirement: "PERM-02" verification: - kind: unit ref: "apps/api/src/groups/groups.service.spec.ts#GroupsService.reassignDefaultBeforeDelete (D-06) (6 Faelle)" status: pass human_judgment: false - id: D2 description: "Ein PATCH mit name auf eine Gruppe mit gesetztem ldapObjectGuid wird serverseitig mit BadRequestException/HTTP 400 abgelehnt — echte Backend-Invariante, nicht nur UI-Konvention (D-03)" requirement: "PERM-02" verification: - kind: unit ref: "apps/api/src/groups/groups.service.spec.ts#GroupsService.update — Namenssperre und interner Name (D-03/D-04) (9 Faelle)" status: pass human_judgment: false - id: D3 description: "internalName ist fuer jede Gruppe setz-/loeschbar; leere/nur-Leerzeichen-Werte normalisieren auf null statt eines leeren Anzeigenamens; listForTenant() liefert internalName zusaetzlich zu name (D-04)" requirement: "PERM-02" verification: - kind: unit ref: "apps/api/src/groups/groups.service.spec.ts#GroupsService.update — Namenssperre und interner Name (D-03/D-04)" status: pass human_judgment: false - id: D4 description: "UpdateGroupDto verliert ldapDn (D-07) und erhaelt internalName?: string | null — kein Codepfad kann eine lokale Gruppe mehr nachtraeglich an AD binden" requirement: "PERM-02" verification: - kind: unit ref: "cd apps/api && npx tsc --noEmit (typkorrekt, Controller kompiliert weiterhin) + acceptance-criteria grep auf update-group.dto.ts (0 ldapDn, internalName vorhanden)" status: pass human_judgment: false - id: D5 description: "ModuleGrantsService.getUserAccess() liefert an beiden Projektionsstellen (viaGroups, groups[].name) internalName ?? name; kein Frontend-Diff (UI-SPEC Surface Contract 6)" requirement: "PERM-02" verification: - kind: unit ref: "apps/api/src/groups/module-grants.service.spec.ts#ModuleGrantsService.getUserAccess — Anzeigename mit Fallback (D-04) (3 Faelle)" status: pass - kind: other ref: "git diff --name-only HEAD -- apps/web | wc -l == 0" status: pass human_judgment: false duration: 5min (Commit-Spanne 2ef9b86 bis f71e614; Kontext-Lesen davor nicht mitgerechnet) completed: 2026-08-06 status: complete --- # Phase 16 Plan 2: Backend-Bausteine fuer internen Namen, Namenssperre und Default-Handoff Summary **GroupsService.update() sperrt den Namen importierter Gruppen serverseitig (BadRequestException statt UI-Konvention), traegt ein unabhaengiges internalName-Feld mit Leerstring-Normalisierung, und ein neuer reassignDefaultBeforeDelete()-Baustein bereitet den Standardgruppen-Handoff fuer den Sync in Plan 16-03 vor — module-grants.service.ts zeigt den internen Namen ohne einen einzigen Frontend-Diff an.** ## Performance - **Duration:** 5 min (Commit-Spanne 2ef9b86 15:55:53 Uhr bis f71e614 16:00:18 Uhr; Lesen von PLAN.md, 16-01-SUMMARY.md, PATTERNS.md, RESEARCH.md und der Zieldateien davor nicht mitgerechnet) - **Started:** 2026-08-06T15:55:53+02:00 (erster Task-Commit) - **Completed:** 2026-08-06T16:00:18+02:00 (dritter Task-Commit) - **Tasks:** 3 - **Files modified:** 5 ## Accomplishments - `DEFAULT_GROUP_NAME` als geteilte Konstante extrahiert (aus `ensureDefaultGroup()`), neue Methode `reassignDefaultBeforeDelete(tenantId, groupId)` verschiebt die Standardmarkierung deterministisch, loescht nichts, wirft in keinem der sechs getesteten Faelle (kein Treffer, nicht markiert, kein anderes Ziel, fremder Mandant, P2002-Rennen) — Baustein fuer Plan 16-03 - `GroupsService.update()` lehnt `name` fuer eine Gruppe mit gesetztem `ldapObjectGuid` mit `BadRequestException` ab — echte Backend-Invariante vor der bestehenden Leerstring-Pruefung, nicht nur ein deaktiviertes Formularfeld - `internalName` ist unabhaengig von der Namenssperre setz- und loeschbar; leere/nur-Leerzeichen-Werte normalisieren auf `null`, nie auf einen leeren Anzeigenamen; `listForTenant()` liefert das Feld zusaetzlich zu `name` - `UpdateGroupDto` verliert `ldapDn` (D-07) und erhaelt `internalName?: string | null` — kein Codepfad kann ueber diese Route mehr eine lokale Gruppe nachtraeglich an AD binden - `ModuleGrantsService.getUserAccess()` liefert an beiden Projektionsstellen (`viaGroups`-Namen, Mitgliedschafts-Chips) `internalName ?? name`; die Sortierung der Chips laeuft dadurch automatisch ueber den angezeigten statt den rohen Namen — bewusster Unterschied zu `listForTenant()`. Kein einziger `apps/web/`-Diff. - Vollstaendige API-Testsuite gruen: 40 Testdateien, 543 Tests (davon 18 neue Faelle aus diesem Plan) ## Task Commits Each task was committed atomically: 1. **Task 1: Standardgruppen-Handoff als eigener Baustein (D-06)** - `2ef9b86` (feat, tdd) 2. **Task 2: Namenssperre fuer importierte Gruppen und internalName im GroupsService (D-03, D-04, D-07)** - `253da91` (feat, tdd) 3. **Task 3: Anzeigename mit Fallback im Benutzer-Detail (D-04, UI-SPEC Surface Contract 6)** - `f71e614` (feat, tdd) **Plan metadata:** wird mit diesem Summary committet ## Files Created/Modified - `apps/api/src/groups/groups.service.ts` - `DEFAULT_GROUP_NAME`, `reassignDefaultBeforeDelete()`, Namenssperre + `internalName`-Normalisierung in `update()`, `internalName` in `listForTenant()` - `apps/api/src/groups/groups.service.spec.ts` - zwei neue `describe`-Bloecke: `GroupsService.reassignDefaultBeforeDelete (D-06)` (6 Faelle), `GroupsService.update — Namenssperre und interner Name (D-03/D-04)` (9 Faelle); veralteter `ldapDn`-Update-Test entfernt - `apps/api/src/groups/dto/update-group.dto.ts` - `ldapDn` entfernt, `internalName?: string | null` ergaenzt - `apps/api/src/groups/module-grants.service.ts` - `internalName: true` in der Mitgliedschafts-Query-Projektion, `internalName ?? name` an beiden Anzeigestellen in `getUserAccess()` - `apps/api/src/groups/module-grants.service.spec.ts` - neuer `describe`-Block `ModuleGrantsService.getUserAccess — Anzeigename mit Fallback (D-04)` (3 Faelle), `__seedGroup`/`groupMembership.findMany`-Fake um `internalName` erweitert ## Decisions Made - Alle drei Tasks aendern `groups.service.ts`; um trotzdem pro Task atomar zu committen, wurde die Datei zunaechst vollstaendig auf den Task-1-Endzustand geschrieben (Write-Tool), getestet und committet — erst danach wurden die Task-2-Aenderungen an derselben Datei vorgenommen. Kein `git add -p` auf einer gemischten Arbeitskopie noetig. - Der bestehende Test "update() mit ldapDn bindet an eine AD-Gruppe; ldapDn:null loest die Bindung" wurde ersatzlos entfernt statt angepasst — nach D-07 ist die Funktionalitaet absichtlich abgeschafft, ein Aequivalent widerspraeche dem Plan. - PERM-02 bleibt in `REQUIREMENTS.md` unveraendert auf `[ ]` (Pending) — dieser Plan liefert nur den Backend-Teil der Anforderung; das Requirement schliesst laut Plan-Vorgabe erst mit dem letzten Plan der Phase (16-05). ## Deviations from Plan None - plan executed exactly as written. Zwei kleinere, in-scope Korrekturen an bestehenden Kommentaren waren fuer die Erfuellung der eigenen Acceptance-Kriterien noetig (kein neuer Code, kein Scope Creep): **1. [Rule 1 - Bug] Bestehender Docstring-Kommentar enthielt ein zweites Vorkommen des Literals `'Alle Benutzer'`** - **Found during:** Task 1 (Acceptance-Kriterium `grep -c "'Alle Benutzer'"` sollte 1 ergeben, lieferte 2) - **Issue:** Der Docblock von `ensureDefaultGroup()` (aus Plan 15-02, unveraendert seither) beschrieb das Verhalten mit dem woertlichen String `'Alle Benutzer'` statt mit dem neuen Konstantennamen — ein reiner Kommentartext, kein Code, aber das Acceptance-Kriterium zaehlt jedes Vorkommen. - **Fix:** Kommentarzeile auf `DEFAULT_GROUP_NAME` umgestellt, Bedeutung unveraendert. - **Files modified:** apps/api/src/groups/groups.service.ts - **Verification:** `grep -c "'Alle Benutzer'" apps/api/src/groups/groups.service.ts` = 1 - **Committed in:** 2ef9b86 **2. [Rule 1 - Bug] DTO-Docblock enthielt das Wort "ldapDn" trotz entfernten Feldes** - **Found during:** Task 2 (Acceptance-Kriterium `grep -c 'ldapDn'` sollte 0 ergeben, lieferte 1) - **Issue:** Der neue Docblock-Kommentar erklaerte die Entfernung des Feldes mit dem woertlichen Feldnamen "ldapDn-Feld" — dadurch schlug das eigene Grep-Kriterium fehl. - **Fix:** Formulierung auf "AD-Bindungsfeld" umgestellt, Bedeutung unveraendert. - **Files modified:** apps/api/src/groups/dto/update-group.dto.ts - **Verification:** `grep -c 'ldapDn' apps/api/src/groups/dto/update-group.dto.ts` = 0 - **Committed in:** 253da91 --- **Total deviations:** 2 auto-fixed (beide Rule 1, reine Kommentartext-Korrekturen fuer die eigenen Acceptance-Kriterien) **Impact on plan:** Kein Einfluss auf Funktionalitaet oder Scope — beide Korrekturen betrafen ausschliesslich Kommentartext. ## Issues Encountered None. ## User Setup Required None - keine externe Service-Konfiguration erforderlich. Der laufende Docker-Stack (lokal) wurde von diesem Plan nicht angefasst; die Aenderungen liegen im Quellcode und werden erst mit einem `--build`/`--force-recreate` durch den Nutzer wirksam. ## Next Phase Readiness - `reassignDefaultBeforeDelete()` steht bereit fuer Plan 16-03, das es vor jedem sync-getriebenen `group.delete()` aufrufen muss — fehlt der Aufruf, bleibt ein Mandant ohne Standardgruppe - Die serverseitige Namenssperre (D-03) ist die Grundlage, auf die sich das gesperrte Namensfeld im Frontend-Dialog von Plan 16-04 verlaesst - `internalName ?? name` ist jetzt an drei von vier geplanten Stellen implementiert (`listForTenant()`, beide `getUserAccess()`-Projektionen); die verbleibenden Frontend-Anzeigestellen (`admin/groups/page.tsx`, `admin/modules/grants/page.tsx`) sind Gegenstand von Plan 16-05 - Die drei aus Plan 16-01 zurueckgestellten kosmetischen Beobachtungen (this.prisma statt tenantPrisma in LdapService, ungenutztes `escapeLdapFilterBuffer()`, `alreadyImported:false` bei Legacy-`ldapDn`-Gruppen ohne GUID) bleiben unveraendert offen — keine davon lag im Feilenbereich dieses Plans --- *Phase: 16-ad-gruppen-synchronisation* *Completed: 2026-08-06* ## Self-Check: PASSED Alle 5 in diesem Plan genannten Quelldateien sowie dieses Summary existieren auf der Festplatte; alle 3 Task-Commit-Hashes (2ef9b86, 253da91, f71e614) sind im lokalen Git-Log auffindbar.