docs(16): record the objectGUID sweep defect found by UAT test 2
Quick task 260811-f9i: plan and summary of the fix, STATE.md row, and the UAT test 2 result. Test 2 was the read-only A2 check against the real directory -- it turned assumption A2 from "unverified" into "false as implemented" and surfaced a defect that would have deleted every AD-bound group on the first real sync. Also records why the defect survived review: the spec mocks built their expected filter with the same escape helper the production code used, so the test asserted self-consistency rather than directory behaviour. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
---
|
||||
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: <Buffer> })` | 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.
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
quick_id: 260811-f9i
|
||||
slug: fix-objectguid-existence-sweep-to-use-eq
|
||||
date: 2026-08-11
|
||||
status: complete
|
||||
relates_to: 16-ad-gruppen-synchronisation
|
||||
severity: critical
|
||||
commits:
|
||||
- d2019dc fix(ldap): search objectGUID by raw bytes, not an escaped filter string
|
||||
---
|
||||
|
||||
# Summary: objectGUID-Existenzprüfung auf Rohbytes umgestellt
|
||||
|
||||
## Was gemacht wurde
|
||||
|
||||
`syncBoundGroupsForTenant()` baut den Filter der Existenzprüfung jetzt als
|
||||
`EqualityFilter` über den rohen 16-Byte-Buffer statt als interpolierten
|
||||
`(objectGUID=\xx…)`-String. Beide Suchen — die schmale über die konfigurierten
|
||||
Base-DNs und die weite über die `domainRoots` (WR-03-Absicherung) — teilen sich
|
||||
denselben Filterwert, damit die Absicherung nicht erneut einen kaputten Filter
|
||||
erben kann.
|
||||
|
||||
`escapeLdapFilterBuffer()` ist entfernt. An seiner Stelle steht ein Kommentar,
|
||||
der erklärt, warum die Methode nicht zurückkommen darf.
|
||||
`escapeLdapFilterValue()` (String-Werte nach RFC 4515) bleibt unverändert und
|
||||
weiter in Gebrauch.
|
||||
|
||||
## Wie der Fehler gefunden wurde
|
||||
|
||||
Über UAT-Test 2 aus Phase 16 — read-only gegen das echte AD (`balios.ctl.local`),
|
||||
Sonde `CN=Domain Admins,CN=Users,DC=ctl,DC=local`, GUID
|
||||
`1e4b4df0b3caeb4c9b49fddf05227e10`:
|
||||
|
||||
| Variante | Treffer |
|
||||
|---|---|
|
||||
| `(objectGUID=\1e\4b…)` — bisheriger Code | 0 |
|
||||
| `(objectGUID=\1E\4B…)` — Großbuchstaben | 0 |
|
||||
| `EqualityFilter` mit rohem Buffer | 1, korrekte DN |
|
||||
| `EqualityFilter` mit escaptem String als Wert | 0 |
|
||||
| Kontrolle `(cn=Domain Admins)` | 1 |
|
||||
|
||||
Der Domain Controller war also nie das Problem. Annahme A2 aus `16-RESEARCH.md`
|
||||
galt für die gewählte Umsetzung nicht.
|
||||
|
||||
## Warum es durch Review und Tests gekommen ist
|
||||
|
||||
Die Mocks in `ldap.service.spec.ts` haben den erwarteten Filter mit **derselben**
|
||||
`escapeLdapFilterBuffer()`-Funktion gebaut, die der Produktionscode benutzt hat.
|
||||
Damit war der Test tautologisch: er hat bestätigt, dass die Funktion sich selbst
|
||||
gegenüber konsistent ist, nicht dass ein Verzeichnis den Filter versteht. Der
|
||||
Code-Kommentar hat die Annahme sogar ausdrücklich als `[ASSUMED — nicht gegen ein
|
||||
echtes AD verifiziert]` markiert.
|
||||
|
||||
Die neuen Tests prüfen jetzt die Form des Aufrufs statt seines Inhalts: der
|
||||
Filter muss ein Objekt mit `attribute: 'objectGUID'` und einem `Buffer`-Wert
|
||||
sein, und kein Suchaufruf des Laufs darf einen String-Filter mit `objectGUID=`
|
||||
tragen.
|
||||
|
||||
## Verifikation
|
||||
|
||||
- `pnpm --filter @tessera/api test` — 568 Tests grün (40 Dateien)
|
||||
- `npx tsc --noEmit` — sauber
|
||||
- Gegenprobe: der alte String-Filter kurzzeitig wiederhergestellt → 8 Tests rot,
|
||||
darunter beide neuen. Die Regressionstests haben also Zähne.
|
||||
|
||||
## Was offen bleibt
|
||||
|
||||
Der Fix belegt die Filtermechanik gegen ein echtes AD, nicht das Verhalten eines
|
||||
vollständigen Sync-Laufs. Offen bleiben aus `16-UAT.md`:
|
||||
|
||||
- Test 1 (A1: GUID überlebt eine Umbenennung) — braucht Schreibrechte im AD
|
||||
- Test 3 (Umbenennung end-to-end) und Test 4 (Löschung end-to-end)
|
||||
- Der Teil von Test 7, der die Zahlenzeilen eines erfolgreichen Sync-Laufs prüft
|
||||
|
||||
Nächster Schritt laut Absprache: ein Wegwerf-Domänencontroller im Container, gegen
|
||||
den Umbenennen und Löschen vollständig durchgespielt werden können.
|
||||
Reference in New Issue
Block a user