--- quick_id: 260811-f9i slug: fix-objectguid-existence-sweep-to-use-eq date: 2026-08-11 status: planned relates_to: 16-ad-gruppen-synchronisation severity: critical --- # Quick Task: objectGUID-Existenzprüfung auf EqualityFilter mit Rohbytes umstellen ## Problem `syncBoundGroupsForTenant()` (`apps/api/src/ldap/ldap.service.ts:1318`) baut den Filter der Existenzprüfung als **String**: ```ts const filter = `(objectGUID=${LdapService.escapeLdapFilterBuffer(guidBuffer)})`; ``` `escapeLdapFilterBuffer()` erzeugt `\1e\4b\4d…`. ldapts wandelt diese Sequenzen beim Parsen des Filter-Strings nicht in Rohbytes zurück, also steht auf dem Draht ein anderer Vergleichswert als gemeint. Die Suche trifft nie. Am 2026-08-11 read-only gegen das echte AD (`balios.ctl.local`) gemessen, Sonde `CN=Domain Admins,CN=Users,DC=ctl,DC=local`, GUID `1e4b4df0b3caeb4c9b49fddf05227e10`: | Variante | Treffer | |---|---| | `(objectGUID=\1e\4b…)` — heutiger Code | 0 | | `(objectGUID=\1E\4B…)` — Großbuchstaben | 0 | | `new EqualityFilter({ attribute: 'objectGUID', value: })` | 1, korrekte DN | | `EqualityFilter` mit escaptem String als Wert | 0 | | Kontrolle `(cn=Domain Admins)` | 1 | Der Domain Controller ist also in Ordnung; die Annahme A2 aus `16-RESEARCH.md` ist für die gewählte Umsetzung falsch. ## Auswirkung Der Ablauf in `syncBoundGroupsForTenant()` ist: schmale Suche über die konfigurierten Base-DNs → kein Treffer → weite Suche über `domainRoots` (WR-03-Absicherung) → kein Treffer → Gruppe gilt als im AD gelöscht (`ldap.service.ts:1432`). **Beide** Suchen verwenden denselben kaputten Filter. Folge: der erste echte Sync-Lauf hätte jede AD-gebundene Gruppe gelöscht, samt `GroupMembership` und `ModuleGrant` per Kaskade, und vorher die Standardgruppen-Markierung weitergereicht (D-06). Der Bericht hätte das als reguläre Löschung ausgewiesen. Auf alpha ist nichts passiert — dort lief nie ein Sync. ## Tasks 1. `EqualityFilter` aus `ldapts` importieren; in `syncBoundGroupsForTenant()` den String-Filter durch ein `EqualityFilter`-Objekt mit dem rohen 16-Byte-Buffer ersetzen. Beide Schleifen (Base-DNs und `domainRoots`) teilen sich weiterhin denselben Filterwert. 2. `escapeLdapFilterBuffer()` entfernen — nach Task 1 ohne Aufrufer, und die Methode ist genau die Falle, in die der Code gelaufen ist. `escapeLdapFilterValue()` (String-Werte, RFC 4515) bleibt unberührt, die ist korrekt und wird weiter genutzt. 3. Die Mocks in `ldap.service.spec.ts`, die auf dem escapten String matchen (Zeilen 579/586, 1060, 1587), auf den Buffer-Wert des Filterobjekts umstellen. 4. Regressionstests ergänzen: die Existenzprüfung übergibt ein Filterobjekt mit `attribute: 'objectGUID'` und einem `Buffer`-Wert, der byteweise dem gespeicherten `ldapObjectGuid` entspricht — und **kein** String. Ein Test hält ausdrücklich fest, dass ein String-Filter der Form `(objectGUID=…)` nicht mehr vorkommt. ## Erfolgskriterium `pnpm --filter @tessera/api test` läuft grün, und ein Test schlägt fehl, sobald jemand wieder einen escapten String als GUID-Filter übergibt. ## Offen nach diesem Fix Der Fix belegt die Filtermechanik, nicht das Verhalten des Sync-Laufs. UAT 1, 3 und 4 (Umbenennen und Löschen im AD) bleiben offen und sollen gegen einen Wegwerf-Domänencontroller nachgezogen werden.