Files

14 KiB

phase, plan, subsystem, tags, requires, provides, affects, actuals, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, coverage, duration, completed, status
phase plan subsystem tags requires provides affects actuals tech-stack key-files key-decisions patterns-established requirements-completed coverage duration completed status
16-ad-gruppen-synchronisation 02 auth
nestjs
prisma
groups
module-grants
vitest
phase provides
16-ad-gruppen-synchronisation (Plan 16-01) Group.internalName + Group.ldapObjectGuid Spalten (Migration angewendet), LdapService.importGroupsByDn()
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)
16-03-rekonziliation
16-04-dialog-umbau
16-05-anzeige-fallback-frontend
tokens tasks commits
7155 3 3
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
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
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
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
PERM-02
id description requirement verification human_judgment
D1 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) PERM-02
kind ref status
unit apps/api/src/groups/groups.service.spec.ts#GroupsService.reassignDefaultBeforeDelete (D-06) (6 Faelle) pass
false
id description requirement verification human_judgment
D2 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) PERM-02
kind ref status
unit apps/api/src/groups/groups.service.spec.ts#GroupsService.update — Namenssperre und interner Name (D-03/D-04) (9 Faelle) pass
false
id description requirement verification human_judgment
D3 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) PERM-02
kind ref status
unit apps/api/src/groups/groups.service.spec.ts#GroupsService.update — Namenssperre und interner Name (D-03/D-04) pass
false
id description requirement verification human_judgment
D4 UpdateGroupDto verliert ldapDn (D-07) und erhaelt internalName?: string | null — kein Codepfad kann eine lokale Gruppe mehr nachtraeglich an AD binden PERM-02
kind ref status
unit cd apps/api && npx tsc --noEmit (typkorrekt, Controller kompiliert weiterhin) + acceptance-criteria grep auf update-group.dto.ts (0 ldapDn, internalName vorhanden) pass
false
id description requirement verification human_judgment
D5 ModuleGrantsService.getUserAccess() liefert an beiden Projektionsstellen (viaGroups, groups[].name) internalName ?? name; kein Frontend-Diff (UI-SPEC Surface Contract 6) PERM-02
kind ref status
unit apps/api/src/groups/module-grants.service.spec.ts#ModuleGrantsService.getUserAccess — Anzeigename mit Fallback (D-04) (3 Faelle) pass
kind ref status
other git diff --name-only HEAD -- apps/web | wc -l == 0 pass
false
5min (Commit-Spanne 2ef9b86 bis f71e614; Kontext-Lesen davor nicht mitgerechnet) 2026-08-06 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.