Files
tessera-ctl/.planning/phases/16-ad-gruppen-synchronisation/16-02-SUMMARY.md
T

190 lines
14 KiB
Markdown

---
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.