diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index ef920ef..ceccb10 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -562,7 +562,7 @@ Plans: **Offen für die Planung**: ob ein Admin eine automatisch angelegte Gruppe in Tessera umbenennen darf (würde beim nächsten Sync überschrieben), und wie sich die Auswahl-Oberfläche zur bestehenden AD-Gruppenauswahl in `/admin/groups` verhält. -**Plans**: 1/5 plans executed +**Plans**: 2/5 plans executed Plans: **Wave 1** @@ -571,7 +571,7 @@ Plans: **Wave 2** *(blocked on Wave 1 completion)* -- [ ] 16-02-PLAN.md — GroupsService: Standardgruppen-Handoff, Namenssperre, interner Name, Anzeige-Fallback (D-03, D-04, D-06, D-07) +- [x] 16-02-PLAN.md — GroupsService: Standardgruppen-Handoff, Namenssperre, interner Name, Anzeige-Fallback (D-03, D-04, D-06, D-07) **Wave 3** *(blocked on Wave 2 completion)* diff --git a/.planning/STATE.md b/.planning/STATE.md index 317e3ee..4bf14d6 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -5,15 +5,15 @@ milestone_name: Ausschreibungs-Radar current_phase: 16 current_phase_name: AD-Gruppen-Synchronisation status: executing -stopped_at: Completed 16-01-PLAN.md -last_updated: "2026-08-06T13:48:07.306Z" +stopped_at: Completed 16-02-PLAN.md +last_updated: "2026-08-06T14:03:42.541Z" last_activity: 2026-08-06 last_activity_desc: Phase 16 execution started progress: total_phases: 16 completed_phases: 13 total_plans: 80 - completed_plans: 74 + completed_plans: 75 --- # Project State @@ -28,11 +28,11 @@ See: .planning/PROJECT.md (updated 2026-07-17) ## Current Position Phase: 16 (AD-Gruppen-Synchronisation) — EXECUTING -Plan: 2 of 5 +Plan: 3 of 5 Status: Ready to execute Last activity: 2026-08-06 — Phase 16 execution started -Progress: [█████████░] 93% +Progress: [█████████░] 94% ## Performance Metrics @@ -108,6 +108,7 @@ Progress: [█████████░] 93% | Phase 15 P07 | 30min | 3 tasks | 7 files | | Phase 15 P08 | 30min | 2 tasks | 12 files | | Phase 16 P01 | 34min | 3 tasks | 10 files | +| Phase 16-ad-gruppen-synchronisation P02 | 5min | 3 tasks | 5 files | ## Accumulated Context @@ -258,6 +259,9 @@ Recent decisions affecting current work: - [Phase ?]: [quick-260805-fok]: ensureDefaultGroup-Waechter prueft ausschliesslich group.count === 0, nie die fehlende isDefault-Markierung (D-13); Reparatur laeuft als zweiter sequenzieller await-Schritt in AdminSeedService.onApplicationBootstrap statt als eigener Hook in GroupsModule (Ordering-Falle wie in tender-scheduler.service.ts) - [Phase ?]: Checkpoint 1 (16-01 Task 1): approve-both — Group.internalName + ldapObjectGuid + Unique-Index in einer Migration, freigegeben 2026-08-06 - [Phase ?]: Migrationsverfahren angepasst: prisma migrate dev verweigert nicht-interaktive Shell — Ersatz via migrate diff + Handdatei + migrate deploy, inkl. Baseline der 24 Altmigrationen per migrate resolve --applied +- [Phase ?]: [16-02]: reassignDefaultBeforeDelete() nutzt bewusst nicht findOwned() — eigenes still-false-Muster fuer Batch-Sync-Laeufe (D-06), kein NotFoundException-Abbruch +- [Phase ?]: [16-02]: Namenssperre fuer importierte Gruppen (D-03) ist eine Backend-Invariante in GroupsService.update() (BadRequestException), nicht nur ein UI-Disable +- [Phase ?]: [16-02]: PERM-02 bleibt in REQUIREMENTS.md bewusst auf [ ] — dieser Plan liefert nur den Backend-Teil, das Requirement schliesst erst mit Plan 16-05 ### Pending Todos @@ -302,7 +306,7 @@ Items acknowledged and carried forward from previous milestone close: ## Session Continuity -Last session: 2026-08-06T13:48:07.273Z -Stopped at: Completed 16-01-PLAN.md +Last session: 2026-08-06T14:03:42.506Z +Stopped at: Completed 16-02-PLAN.md Resume file: None Last activity: 2026-07-14 - Built LDAP per-user exclude/denylist filter (9d1323f), migration applied on live DB, verified via Playwright: sync deactivated 4 excluded service accounts (administrator/krbtgt/guest/dns-ldap), 2 real LDAP users stay active, 0 wrongly created diff --git a/.planning/phases/16-ad-gruppen-synchronisation/16-02-SUMMARY.md b/.planning/phases/16-ad-gruppen-synchronisation/16-02-SUMMARY.md new file mode 100644 index 0000000..0367d3a --- /dev/null +++ b/.planning/phases/16-ad-gruppen-synchronisation/16-02-SUMMARY.md @@ -0,0 +1,189 @@ +--- +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.