--- phase: 16-ad-gruppen-synchronisation plan: 03 subsystem: auth tags: [nestjs, prisma, ldap, ldapts, groups, vitest, rls] # Dependency graph requires: - phase: 16-ad-gruppen-synchronisation (Plan 16-01) provides: "Group.ldapObjectGuid (Migration angewendet), LdapService.escapeLdapFilterBuffer() (bislang ungenutzt)" - phase: 16-ad-gruppen-synchronisation (Plan 16-02) provides: "GroupsService.reassignDefaultBeforeDelete(tenantId, groupId), GroupsService.ensureDefaultGroup(tenantId), DEFAULT_GROUP_NAME" provides: - "LdapService.syncBoundGroupsForTenant(client, config, tenantId, result): Rename-Erkennung (SC-3), Verschwinden-Loeschung mit Default-Handoff (SC-4/D-05/D-06), Alt-Bindungs-GUID-Nachtrag (D-07), 32-Hex-Validierung vor jeder Filter-Interpolation (T-16-01)" - "LdapSyncResult erweitert um groupsAdopted, groupsRenamed, groupsDeleted, defaultMarkerMoved" - "syncUsersForTenant() Schritt 5a: syncBoundGroupsForTenant() laeuft nachweislich vor Schritt 5b (syncGroupMembershipsForTenant, D-21) — Kernkorrektheitsbedingung der Phase (RESEARCH.md Pitfall 1)" - "LdapService(prisma, userService, groupsService) — dritter Konstruktor-Parameter; LdapModule importiert GroupsModule" affects: [16-04-dialog-umbau, 16-05-anzeige-fallback-frontend] # Actuals (#2632) actuals: tokens: 11091 tasks: 2 commits: 2 # Tech tracking tech-stack: added: [] patterns: - "Erster tatsaechlicher Aufrufer von escapeLdapFilterBuffer() (aus Plan 16-01) — der binaere Existenz-Sweep-Filter (objectGUID=...)" - "Reihenfolge-Kopplung zweier privater Sync-Schritte durch einen beobachtenden Test (Call-Order-Spy via vi.spyOn mit mockImplementation, das die Originalmethode aufruft) statt nur behaupteter Kommentar-Reihenfolge" - "32-Zeichen-Hex-Validierung als eigener Guard VOR jeder Buffer-Rueckwandlung — ein Wert, der die Regex nicht besteht, erzeugt eine Fehlerzeile statt einer Filter-Interpolation (T-16-01)" key-files: created: [] modified: - apps/api/src/ldap/ldap.service.ts - apps/api/src/ldap/ldap.module.ts - apps/api/src/ldap/ldap.service.spec.ts key-decisions: - "syncBoundGroupsForTenant() wird in Task 1 als eigenstaendige, noch nicht eingehaengte Methode gebaut und per direktem Cast ((service as any).syncBoundGroupsForTenant(...)) getestet — die Verdrahtung in syncUsersForTenant() ist bewusst Task 2 vorbehalten (Plan-Vorgabe), damit die Reihenfolge-Korrektheit als eigener, isoliert testbarer Schritt sichtbar bleibt." - "D-21-Testfixtures (Plan 15/vor Plan 16) hatten nie ein ldapObjectGuid-Feld gesetzt. Nach der Verdrahtung in Task 2 durchlaeuft jede dieser Gruppen jetzt zwingend Schritt 5a (Alt-Bindungs-Nachtrag). Statt jede der ueber zehn Test-Fixtures im D-21-Block einzeln um ein Feld zu erweitern, wurde der gemeinsame mockSearch so erweitert, dass er 5a's zwei Filterformen (Backfill-Probe, Existenz-Sweep) deterministisch als No-Op aufloest (DN-abgeleitete GUID, Identitaets-Treffer) — kleinerer, zentralerer Diff als 11 Einzelaenderungen." - "PERM-02 bleibt in REQUIREMENTS.md bewusst auf [ ] (Pending) stehen — dieser Plan liefert Rekonziliation und Verdrahtung, das Requirement schliesst laut Plan-Vorgabe erst mit Plan 16-05." - "A1/A2-Live-Pruefung gegen ViCoTest nicht durchfuehrbar: kein erreichbares Active Directory in dieser Sandbox (Umgebungsvorgabe fuer diesen Lauf). Als offener Punkt im Broken-Windows-Ledger (WINDOWS.md #4) und unten dokumentiert, statt stillschweigend uebersprungen." patterns-established: - "Reihenfolge-Kopplung zwischen zwei privaten Sync-Schritten wird durch einen beobachtenden Call-Order-Test abgesichert, nicht nur durch Kommentartext an der Aufrufstelle" requirements-completed: [PERM-02] # Plan-Frontmatter-Vertrag; REQUIREMENTS.md-Checkbox bleibt bewusst [ ] (siehe Decisions) — schliesst erst mit Plan 16-05 coverage: - id: D1 description: "syncBoundGroupsForTenant() erkennt Rename (cn/dn-Aenderung -> Group.name/ldapDn-Update, internalName bleibt unberuehrt) und Verschwinden (kein Treffer -> Loeschung) korrekt, ohne lokale Gruppen (ldapObjectGuid+ldapDn beide null) jemals abzufragen (SC-3/SC-4/SC-5, D-04)" requirement: "PERM-02" verification: - kind: unit ref: "apps/api/src/ldap/ldap.service.spec.ts#LdapService.syncBoundGroupsForTenant — Rekonziliation gegen das Verzeichnis (SC-3/SC-4/SC-5, D-05/D-06) (14 it-Faelle)" status: pass - kind: other ref: "grep -c internalName im Methodenkoerper (awk-Range) = 0" status: pass human_judgment: false - id: D2 description: "Loesch-Zweig ruft reassignDefaultBeforeDelete() nachweislich VOR group.delete(); nach jeder Loeschung laeuft ensureDefaultGroup() einmal — kein Mandant bleibt ohne markierte Standardgruppe (D-06, RESEARCH.md Pitfall 5)" requirement: "PERM-02" verification: - kind: unit ref: "apps/api/src/ldap/ldap.service.spec.ts — Faelle 'no hit, IS the default group...' und 'no hit, is the ONLY group of the tenant...'" status: pass - kind: other ref: "awk-Range-Grep: reassignDefaultBeforeDelete-Zeile < group.delete-Zeile" status: pass human_judgment: false - id: D3 description: "Alt-Bindungen (ldapDn gesetzt, ldapObjectGuid null) werden per Base-Scoped-Lookup einmalig nachgetragen (groupsAdopted) und im selben Lauf regulaer weiterverarbeitet; loest der DN nicht mehr auf, wird NICHT geloescht, nur eine Fehlerzeile erzeugt (D-07, T-16-11)" requirement: "PERM-02" verification: - kind: unit ref: "apps/api/src/ldap/ldap.service.spec.ts — Faelle 'a legacy binding ... whose DN still resolves ...' und '... whose DN no longer resolves ...'" status: pass human_judgment: false - id: D4 description: "Der gespeicherte Hex-Wert wird vor jeder Filter-Interpolation auf exakt 32 [0-9a-f]-Zeichen validiert; ein ungueltiger Wert erzeugt eine Fehlerzeile statt eines Filters (T-16-01)" requirement: "PERM-02" verification: - kind: unit ref: "apps/api/src/ldap/ldap.service.spec.ts#'an invalid stored ldapObjectGuid (not 32 [0-9a-f] chars) never reaches a filter'" status: pass human_judgment: false - id: D5 description: "syncBoundGroupsForTenant() laeuft in syncUsersForTenant() nachweislich VOR syncGroupMembershipsForTenant() (Schritt 5a vor 5b); ein im selben Lauf erkannter Rename fuehrt beobachtbar zu keinem memberOf-Filter mit der alten DN mehr (RESEARCH.md Pitfall 1)" requirement: "PERM-02" verification: - kind: unit ref: "apps/api/src/ldap/ldap.service.spec.ts#LdapService.syncUsersForTenant — Schrittreihenfolge Gruppen vor Mitgliedschaften (3 it-Faelle: Call-Order-Spy, No-Op-Waechter, Regressions-Filter-Check)" status: pass - kind: other ref: "grep -n Aufrufzeilen: syncBoundGroupsForTenant-Zeile < syncGroupMembershipsForTenant-Zeile" status: pass human_judgment: false - id: D6 description: "RESEARCH.md-Annahmen A1 (objectGUID uebersteht AD-Umbenennung) und A2 (byteweise Hex-Filter-Syntax vom Ziel-AD akzeptiert) read-only gegen ViCoTest verifiziert, bevor die Loeschsemantik (D-05) als produktionsreif gilt" requirement: "PERM-02" verification: - kind: other ref: "Kein erreichbares Active Directory in dieser Sandbox (Umgebungsvorgabe) — Live-Pruefung nicht durchgefuehrt" status: fail human_judgment: true rationale: "A1/A2 bleiben unverifizierte Annahmen aus RESEARCH.md. Als WINDOWS.md-Eintrag #4 (unrun-verify) festgehalten. Ein negatives Ergebnis bei A1 ODER A2 ist ein Stopp-Grund fuer die Loeschsemantik aus D-05 — vor produktivem Einsatz gegen ViCoTest (balios.ctl.local) nachzuholen." duration: 12min completed: 2026-08-06 status: complete --- # Phase 16 Plan 3: Gruppen-Rekonziliation gegen das Verzeichnis Summary **syncBoundGroupsForTenant() unterscheidet AD-Umbenennung von AD-Verschwinden ueber den rename-stabilen ldapObjectGuid, laeuft nachweislich vor dem Mitgliedschafts-Abgleich, uebergibt die Standardmarkierung vor jeder Loeschung und traegt Alt-Bindungen aus Plan 15-06 nach — die A1/A2-Live-Pruefung gegen ein echtes Active Directory bleibt offen (kein AD in dieser Sandbox erreichbar).** ## Performance - **Duration:** 12 min (Commit-Spanne 16:15:09 Uhr bis 16:21:10 Uhr; Lesen von PLAN.md, 16-01-/16-02-SUMMARY.md, PATTERNS.md, RESEARCH.md und der Zieldateien davor nicht mitgerechnet) - **Started:** 2026-08-06T14:15:09Z (erster Task-Commit) - **Completed:** 2026-08-06T14:21:10Z (zweiter Task-Commit) - **Tasks:** 2 - **Files modified:** 3 ## Accomplishments - Neue private Methode `LdapService.syncBoundGroupsForTenant()`: fragt alle Gruppen mit gesetztem `ldapObjectGuid` ODER `ldapDn` ab (der D-07-Anschluss fuer Alt-Bindungen), tut fuer eine rein lokale Gruppe (beide Felder `null`) nichts und loest dafuer keine einzige LDAP-Suche aus - Rename-Erkennung (SC-3): ein AD-Treffer mit geaendertem `cn`/`dn` zieht `Group.name`/`Group.ldapDn` nach, `internalName` bleibt in jedem Codepfad der Methode unberuehrt (D-04); ein `P2002` aus einer Namenskollision wird abgefangen und als Fehlerzeile gemeldet, der Lauf geht weiter - Verschwinden-Loeschung (SC-4/D-05): kein Treffer im binaeren Existenz-Sweep loescht die Gruppe — aber immer erst NACH dem Standardmarkierungs-Handoff (`reassignDefaultBeforeDelete`, Plan 16-02), nachweislich in dieser Zeilenreihenfolge; nach jeder Loeschung laeuft `ensureDefaultGroup()` einmal, damit kein Mandant ohne Standardgruppe dasteht (D-06) - Alt-Bindungs-Nachtrag (D-07): eine Gruppe mit `ldapDn`, aber ohne `ldapObjectGuid`, bekommt den Identitaetsschluessel per Base-Scoped-Lookup einmalig nachgetragen (`groupsAdopted`) und wird im selben Lauf reguelaer weiterverarbeitet; loest der DN nicht mehr auf, wird **nicht** geloescht, nur eine Fehlerzeile erzeugt — ohne stabilen Schluessel waere eine Loeschung eine Vermutung - Der gespeicherte Hex-Wert wird vor jeder Filter-Interpolation auf exakt 32 `[0-9a-f]`-Zeichen validiert (T-16-01); ein ungueltiger Wert erzeugt eine Fehlerzeile statt einer Filter-Interpolation - Schritt 5a (`syncBoundGroupsForTenant`) laeuft in `syncUsersForTenant()` nachweislich VOR Schritt 5b (`syncGroupMembershipsForTenant`, D-21) — abgesichert durch einen beobachtenden Call-Order-Test (nicht nur durch Kommentartext), plus einen Regressionstest, der zeigt, dass ein im selben Lauf erkannter Rename zu keinem `memberOf`-Filter mit der alten DN mehr fuehrt - `LdapService`-Konstruktor nimmt neu `GroupsService` entgegen; `LdapModule` importiert `GroupsModule` (kein Zyklus, da `GroupsModule` weder `LdapModule` noch `UserModule` importiert) - 17 neue Testfaelle (14 fuer die neue Methode isoliert, 3 fuer die Verdrahtungsreihenfolge); vollstaendige API-Testsuite gruen: 40 Testdateien, 560 Tests ## Task Commits Each task was committed atomically: 1. **Task 1: Gruppen-Rekonziliation gegen das Verzeichnis (SC-3, SC-4, SC-5, D-05, D-06)** — `5222934` (feat, tdd) 2. **Task 2: Einhaengen als Schritt 5a vor dem Mitgliedschafts-Abgleich (RESEARCH.md Pitfall 1)** — `68aca81` (feat, tdd) **Plan metadata:** wird mit diesem Summary committet ## Files Created/Modified - `apps/api/src/ldap/ldap.service.ts` — `LdapSyncResult` um `groupsAdopted`/`groupsRenamed`/`groupsDeleted`/`defaultMarkerMoved` erweitert; Konstruktor nimmt `GroupsService`; neue private Methode `syncBoundGroupsForTenant()`; Aufruf als Schritt 5a in `syncUsersForTenant()` vor Schritt 5b - `apps/api/src/ldap/ldap.module.ts` — `GroupsModule`-Import ergaenzt - `apps/api/src/ldap/ldap.service.spec.ts` — zwei neue `describe`-Bloecke (`syncBoundGroupsForTenant` mit 14 Faellen, `Schrittreihenfolge Gruppen vor Mitgliedschaften` mit 3 Faellen); alle acht `new LdapService(...)`-Konstruktionen um einen dritten Mock-Parameter ergaenzt; D-21-Block-Fixtures um eine 5a-No-Op-Aufloesung im gemeinsamen `mockSearch` erweitert, ohne jede der ueber zehn Einzel-Fixtures anzufassen ## Decisions Made - `syncBoundGroupsForTenant()` wurde in Task 1 bewusst noch NICHT in `syncUsersForTenant()` eingehaengt (Plan-Vorgabe) — Task 1 testet die Methode per direktem Cast auf die private Methode, Task 2 liefert ausschliesslich die Verdrahtung samt Reihenfolge-Test. Diese Trennung macht die zentrale Korrektheitsbedingung der Phase (Reihenfolge) als eigenen, isoliert nachvollziehbaren Schritt sichtbar. - Statt jede der ueber zehn D-21-Test-Fixtures (aus Plan 15, vor Plan 16 gebaut, nie mit `ldapObjectGuid` versehen) einzeln um ein Feld zu erweitern, wurde der gemeinsame `mockSearch` im D-21-Block um eine deterministische No-Op-Aufloesung fuer beide 5a-Filterformen erweitert (DN-abgeleitete GUID, Identitaets-Treffer bei unveraendertem `cn`/`dn`). Kleinerer, zentralerer Diff; die D-21-Tests bleiben inhaltlich unveraendert auf die Mitgliedschafts-Reconciliation fokussiert. - PERM-02 bleibt in `REQUIREMENTS.md` bewusst auf `[ ]` (Pending) — dieser Plan liefert Rekonziliation und Verdrahtung, das Requirement schliesst laut Plan-Vorgabe erst mit Plan 16-05. ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 1 - Bug] Bestehender D-21-Test brach nach der additiven `LdapSyncResult`-Erweiterung (exaktes `toEqual`)** - **Found during:** Task 1 - **Issue:** Der Test „creates nobody and deactivates nobody when the base DN is empty" verglich `result` per `toEqual` gegen ein Literal ohne die vier neuen Felder — nach der additiven Interface-Erweiterung schlug der Vergleich fehl, obwohl das Verhalten unveraendert korrekt war. - **Fix:** Die vier neuen Felder (`groupsAdopted: 0, groupsRenamed: 0, groupsDeleted: 0, defaultMarkerMoved: 0`) im erwarteten Literal ergaenzt. - **Files modified:** apps/api/src/ldap/ldap.service.spec.ts - **Verification:** `npx vitest run src/ldap/ldap.service.spec.ts` gruen - **Committed in:** 5222934 **2. [Rule 1 - Bug] Wiederverwendete GUID-Test-Fixture aus Plan 16-01 war tatsaechlich nur 31 Hex-Zeichen lang** - **Found during:** Task 1 - **Issue:** Der bestehende `guidBuffer`-Fixture-String im "AD group import"-Block (`'0123456789abcdef0123456789abcde'`) ist bei genauer Zaehlung 31, nicht 32 Zeichen lang — `Buffer.from(..., 'hex')` schneidet das letzte ungerade Zeichen stumm ab und liefert 15 statt 16 Bytes zurueck. Das fiel dort nie auf, weil `importGroupsByDn()` die Laenge nie validiert; die neue 32-Hex-Validierung in `syncBoundGroupsForTenant()` (T-16-01) schlug mit demselben Wert sofort fehl. - **Fix:** Eigene, lokal im neuen `describe`-Block definierte, garantiert 32 Zeichen lange Fixture (`'0123456789abcdef'.repeat(2)`) statt den bestehenden (fehlerhaften) Fixture-String zu teilen. Der bestehende Fixture-String im 16-01-Block wurde NICHT angefasst (aussarhalb des Scopes dieses Plans, dortige Tests bleiben unveraendert gruen). - **Files modified:** apps/api/src/ldap/ldap.service.spec.ts - **Verification:** `npx vitest run src/ldap/ldap.service.spec.ts` gruen, neuer Kommentar dokumentiert den Unterschied explizit - **Committed in:** 5222934 **3. [Rule 1 - Bug] Verdrahtung in Task 2 brach vier bestehende D-21-Tests (Schritt 5a lief nun ungewollt gegen deren Fixtures)** - **Found during:** Task 2 - **Issue:** Nach dem Einhaengen von Schritt 5a liefen die vorher isoliert getesteten D-21-Membership-Tests real durch `syncUsersForTenant()` — deren Gruppen-Fixtures (nur `ldapDn`, kein `ldapObjectGuid`, vor Plan 16 gebaut) loesten in jedem Lauf den Alt-Bindungs-Nachtragspfad aus und produzierten unerwuenschte Fehlerzeilen bzw. veraenderten `result.errors`. - **Fix:** Der gemeinsame `mockSearch` im D-21-`beforeEach` (und die eine Test-lokale Ueberschreibung im „records a search failure"-Test) um eine deterministische Aufloesung beider 5a-Filterformen erweitert (`resolve5aNoOp`-Helfer auf Describe-Ebene), die pro Fixture-Gruppe einen DN-abgeleiteten, immer gueltigen GUID annimmt und einen Identitaets-Treffer liefert — 5a wird dadurch fuer jede D-21-Fixture ein reiner No-Op, ohne die ueber zehn einzelnen `groups = [...]`-Literale anzufassen. - **Files modified:** apps/api/src/ldap/ldap.service.spec.ts - **Verification:** `npx vitest run src/ldap/ldap.service.spec.ts` — alle 55 Faelle gruen; `npx vitest run` — vollstaendige API-Suite (560 Tests) gruen - **Committed in:** 68aca81 --- **Total deviations:** 3 auto-fixed (alle Rule 1, direkte Folgen der additiven Interface-Erweiterung bzw. der Verdrahtung — kein Scope Creep, kein Architekturwechsel) **Impact on plan:** Keine Auswirkung auf Funktionalitaet oder Scope der neuen Methode selbst — alle drei Korrekturen betrafen ausschliesslich bestehende Testinfrastruktur, die durch additive/verdrahtende Aenderungen aus diesem Plan beruehrt wurde. ## Outstanding Live Verification (A1/A2) **RESEARCH.md fuehrt zwei Annahmen, die dieser Plan gegen ein echtes Active Directory pruefen sollte (Task 2 ``), aber in dieser Sandbox nicht pruefen konnte — es ist kein Active-Directory-Server erreichbar (Umgebungsvorgabe fuer diesen Ausfuehrungslauf, siehe `` im Auftrag):** 1. **A1 — `objectGUID` uebersteht eine reine CN-Umbenennung unveraendert.** Unverifiziert. Die gesamte Rename-Erkennung (SC-3) haengt an dieser Annahme — ist sie falsch, wuerde jede echte AD-Umbenennung weiterhin wie ein Verschwinden+Neuanlage behandelt. 2. **A2 — die byteweise `\XX`-Hex-Escaping-Syntax fuer den binaeren `(objectGUID=...)`-Filter wird vom Ziel-AD korrekt interpretiert.** Unverifiziert. Ist sie falsch, liefert der Existenz-Sweep fuer JEDE gebundene Gruppe null Treffer — SC-4 (Loeschung) wuerde dann faelschlich auf jede weiterhin existierende Gruppe angewendet. **Beide Antworten stehen noch aus.** Sie sind als offener Eintrag im projektweiten Broken-Windows-Ledger festgehalten: `.planning/WINDOWS.md` Eintrag #4 (kind: `unrun-verify`, phase: 16). Ein negatives Ergebnis bei A1 ODER A2 ist ein **Stopp-Grund fuer die Loeschsemantik aus D-05** — die Implementierung darf in diesem Fall NICHT einfach "umgebogen" werden (so RESEARCH.md/PLAN.md ausdruecklich), sondern erfordert eine erneute Planungsentscheidung. Vor produktivem Einsatz der Loeschsemantik muss die read-only Pruefung gegen ViCoTest (`balios.ctl.local`) aus `16-VALIDATION.md` nachgeholt werden: (1) eine AD-Gruppe suchen, `objectGUID` notieren, im AD umbenennen, erneut suchen, GUID vergleichen; (2) eine Suche mit dem byteweise escaped `objectGUID`-Filter absetzen und pruefen, ob sie exakt diese Gruppe zurueckliefert. Gemaess `workflow.human_verify_mode: end-of-phase` wurde dieser Plan trotz der offenen Pruefung bis zum Ende ausgefuehrt, statt mittendrin anzuhalten — die offene Verifikation ist hier klar dokumentiert und im Ledger sichtbar, nicht stillschweigend uebersprungen. ## Issues Encountered Siehe Deviations oben — alle drei Punkte betrafen ausschliesslich bestehende Testinfrastruktur, die durch die additiven bzw. verdrahtenden Aenderungen dieses Plans beruehrt wurde, nicht die fachliche Logik der neuen Methode selbst. ## 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. Kein Deploy auf den Testserver durch diesen Lauf (Projektregel). ## Next Phase Readiness - `syncBoundGroupsForTenant()` ist vollstaendig implementiert, verdrahtet und getestet — die Kernkorrektheitsbedingung der Phase (Reihenfolge vor dem Mitgliedschafts-Abgleich) ist durch einen beobachtenden Test abgesichert, nicht nur behauptet - Plan 16-04 (Dialog-Umbau) und 16-05 (Anzeige-Fallback Frontend, Requirement-Abschluss PERM-02) koennen auf dieser Grundlage aufsetzen - **Offener Punkt bleibt bestehen (WINDOWS.md #4):** A1/A2-Live-Pruefung gegen ein echtes Active Directory (ViCoTest) muss nachgeholt werden, bevor die Loeschsemantik aus D-05 als produktionsreif gilt — siehe Abschnitt „Outstanding Live Verification" oben ## Post-Review Fix (16-REVIEW.md, 2026-08-06) Der Code-Review dieser Phase (`16-REVIEW.md`) fand drei Warnungen an genau der in diesem Plan gebauten `syncBoundGroupsForTenant()`-Methode. Alle drei sind seither behoben — die obige Beschreibung dieses Plans spiegelt noch den Vor-Fix-Stand wider, daher dieser Nachtrag: - **WR-02 (Commit `1971795`):** Der Rename-Zweig meldete jeden P2002 beim Zurückschreiben von `name`/`ldapDn` pauschal als Namenskollision, ohne `updateError.meta.target` zu prüfen — obwohl der Aufruf zwei verschiedene Unique-Indizes treffen kann. Behoben durch dieselbe Ziel-Prüfung, die `importGroupsByDn()` bereits nutzte. - **WR-03 (Commit `00de6a6`, der wichtigste der drei Punkte):** Der Existenz-Sweep vor einer Löschung suchte ausschließlich unter den konfigurierten Base-DNs. Eine AD-Gruppe, die in eine andere OU verschoben (nicht gelöscht) wurde, lieferte dort `searchEntries.length === 0` und wurde fälschlich als „verschwunden" gelesen und samt Mitgliedschaften/Modulfreigaben gelöscht — Zugriffsverlust durch eine reine Verzeichnis-Umstrukturierung, kein tatsächliches AD-Löschen. Die Methode führt jetzt vor jeder Löschung einen zweiten, weiter gefassten Sweep gegen die Domänen-Wurzel jeder konfigurierten Base-DN aus; ein Treffer dort verhindert die Löschung und wird stattdessen als Fehlerzeile gemeldet. Erst wenn auch dieser weite Sweep leer bleibt, gilt SC-4/D-05 als tatsächlich erfüllt. - **WR-04 (Commit `2779d42`):** Die Umbenennungs-Erkennung verglich `cn`/`dn` byteweise. Eine reine Groß-/Kleinschreibungs-Abweichung zwischen zwei Sync-Läufen (z. B. nach einem Domain-Controller-Wechsel) hätte jedes Mal fälschlich eine Umbenennung ausgelöst und die Idempotenz-Garantie verletzt. Der Vergleich zur Entscheidung „ist das eine Umbenennung" ist jetzt case-insensitive; der tatsächlich gespeicherte Wert bleibt weiterhin exakt der vom Verzeichnis gemeldete (D-03 unangetastet). (WR-01, in `groups.service.ts`, betrifft nicht diese Methode — siehe `16-02-SUMMARY.md`. Commit `dd59bf5`.) Alle vier Fixes sind mit neuen Tests in `ldap.service.spec.ts` bzw. `groups.service.spec.ts` abgesichert; die volle API-Suite lief zuletzt gruen. Details siehe `16-REVIEW.md`. --- *Phase: 16-ad-gruppen-synchronisation* *Completed: 2026-08-06* ## Self-Check: PASSED Alle 3 in diesem Plan genannten Quelldateien sowie dieses Summary existieren auf der Festplatte; beide Task-Commit-Hashes (5222934, 68aca81) sind im lokalen Git-Log auffindbar.