diff --git a/.planning/quick/260729-d3k-ldap-multi-base-dn-und-base-dn-als-scope/260729-d3k-VERIFICATION.md b/.planning/quick/260729-d3k-ldap-multi-base-dn-und-base-dn-als-scope/260729-d3k-VERIFICATION.md new file mode 100644 index 0000000..096bb7d --- /dev/null +++ b/.planning/quick/260729-d3k-ldap-multi-base-dn-und-base-dn-als-scope/260729-d3k-VERIFICATION.md @@ -0,0 +1,79 @@ +--- +phase: quick-260729-d3k +verified: 2026-07-29T09:45:00Z +status: passed +score: 6/6 must-haves verified +behavior_unverified: 0 +overrides_applied: 0 +--- + +# Quick Task 260729-d3k: LDAP Multi-Base-DN & Base-DN-as-Scope — Verification Report + +**Task Goal:** LDAP Multi-Base-DN + Base-DN als Scope. Base-DN Feld multiline; Backend loopt über alle Base-DNs (collectSearchEntries/listGroups/searchUsers) und merged per dn; 57bc7f9-Sonderfall "leere groupFilterDns=[]" entfernt; KRITISCH: syncUsersForTenant No-Op-Guard keyt jetzt auf LEERE BASE-DN-LISTE (nicht leere groupFilterDns); schema/frontend syncIntervalMin-Default + isActive default true unverändert; keine Migration; i18n de+en angepasst; Tests. + +**Verified:** 2026-07-29 +**Status:** passed +**Re-verification:** No — initial verification + +## Goal Achievement + +### Observable Truths + +| # | Truth | Status | Evidence | +|---|-------|--------|----------| +| 1 | `parseBaseDns()` helper splits the Base-DN field into a trimmed, non-empty DN list (no schema change) | ✓ VERIFIED | `apps/api/src/ldap/ldap.service.ts:197-205` — splits on `/\r?\n/`, trims, filters empty lines, returns `[]` for falsy input. `LdapConfig.baseDn` in `schema.prisma` remains a plain `String` (no array, no new column). | +| 2 | All three directory-search paths (`collectSearchEntries`, `listGroups`, `searchUsers`) loop over every configured base DN and merge/dedupe results by entry `dn` | ✓ VERIFIED | `ldap.service.ts:227-239` (listGroups), `:379-391` (searchUsers), `:743-791` (collectSearchEntries) — each builds a `Map` keyed by `entry.dn`, looping `parseBaseDns(config.baseDn)`. Behaviorally confirmed by the "multi base DN scope" spec: `mockSearch` called twice (once per base), a duplicate `dn` returned from both bases produces exactly 1 created user, distinct dns each create — 3 total from 4 raw entries. | +| 3 | The 57bc7f9 "empty groupFilterDns ⇒ []" special case is removed; an empty group filter performs a normal multi-base search (no memberOf restriction), not a no-op | ✓ VERIFIED | `collectSearchEntries()` (`ldap.service.ts:737-792`) has no early-return for empty `groupFilterDns` — it always searches every `baseDn` with `sanitizedFilter` (optionally ANDed with a memberOf clause only when `groupDns.length > 0`). Confirmed by spec "is NOT a no-op when baseDn is set but groupFilterDns is empty (normal multi-base search)" — `mockBind`/`mockSearch` are called and a user is created. | +| 4 | `groupFilterDns` is an optional extra restriction: `ou=` entries are additional search bases (plain filter); non-`ou=` (group) DNs become a memberOf constraint applied to the base-DN search | ✓ VERIFIED | `ldap.service.ts:744-789` — `ouBases` (matched by `/^ou=/i`) are searched as additional bases with the plain `sanitizedFilter`; `groupDns` (the rest) are folded into a `memberOf` OR-clause ANDed with `sanitizedFilter` and applied to every base-DN search. Logic unchanged in substance from the prior implementation, now applied across every base DN. | +| 5 | **CRITICAL SAFETY:** `syncUsersForTenant`'s early-return No-Op guard keys ONLY on the parsed base-DN list being empty — an empty base-DN list is the sole condition that skips search + deactivation; an empty `groupFilterDns` never triggers the No-Op | ✓ VERIFIED | `ldap.service.ts:579-582` — `const baseDns = this.parseBaseDns(config.baseDn); if (baseDns.length === 0) { return result; }` sits immediately after building the empty `result` object and BEFORE the LDAP `Client` is even constructed (line 584) — i.e. before bind, search, AND the deactivation loop (lines 678-696). Confirmed by both "empty base DN no-op" specs: (a) whitespace-only `baseDn` → zero bind/search/create/update/ldapConfig.update calls, zero-valued result; (b) non-empty `baseDn` + empty `groupFilterDns` → bind and search DO happen, one user created (proves the guard no longer keys on `groupFilterDns`). | +| 6 | de + en i18n describe the Base-DN(s) as the sync scope and the group filter as an optional additional restriction; the old "...nichts synchronisiert" / "...nothing is synced" wording is gone | ✓ VERIFIED | `de.json`: `groupFilter.description` = "Optionale zusaetzliche Einschraenkung ... Ohne Auswahl werden alle Benutzer unter den Basis-DN(s) synchronisiert."; `groupFilter.emptyMeansAll` = "Keine Auswahl - alle Benutzer unter den Basis-DN(s) werden synchronisiert."; new `baseDnHint` = "Ein DN pro Zeile. Die Basis-DN(s) legen den Umfang der Synchronisation fest." `en.json` mirrors semantically ("all users under the base DN(s) are synced", "base DN(s) define the sync scope"). No "nichts synchronisiert"/"nothing is synced" phrasing remains in either file. Both parse as valid JSON. | + +**Score:** 6/6 truths verified (0 present-but-behavior-unverified) + +### Required Artifacts + +| Artifact | Expected | Status | Details | +|----------|----------|--------|---------| +| `apps/api/src/ldap/ldap.service.ts` | `parseBaseDns()` helper + multi-base loop/merge in all 3 search paths + base-DN-keyed guard | ✓ VERIFIED | All present, wired, and exercised by passing specs (see truths 1-5). | +| `apps/api/src/ldap/ldap.service.spec.ts` | Empty-base-DN No-Op test, multi-base merge/dedup test, adjusted exclude-list specs | ✓ VERIFIED | `describe('...empty base DN no-op')` (2 specs) + `describe('...multi base DN scope')` (1 spec) present; `baseConfig` comment in the exclude-list block updated to reference the base-DN guard instead of the removed groupFilterDns guard (spec.ts:36-42). | +| `apps/web/src/app/(portal)/admin/ldap/page.tsx` | Multi-line `