docs(260729-d3k): add plan + verification (passed 6/6) for LDAP multi-base-DN
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+79
@@ -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<string, Entry>` 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 `<textarea>` for baseDn + one-DN-per-line hint | ✓ VERIFIED | `page.tsx:466-474` — `<textarea rows={3} className="... min-h-20 ...">` bound to `formData.baseDn`/`setFormData`, unchanged `fetchConfig`/`handleSave` wiring, followed by `<p>{t('baseDnHint')}</p>`. |
|
||||||
|
| `apps/web/src/messages/de.json` + `en.json` | Reworded `groupFilter.description`/`emptyMeansAll` + new `baseDnHint` key | ✓ VERIFIED | Confirmed via direct JSON parse — see truth 6. |
|
||||||
|
|
||||||
|
### Key Link Verification
|
||||||
|
|
||||||
|
| From | To | Via | Status | Details |
|
||||||
|
|------|----|----|--------|---------|
|
||||||
|
| `syncUsersForTenant` early-return guard | `parseBaseDns(config.baseDn).length === 0` | Guard condition placed before `Client` construction (bind) and before the deactivation loop | ✓ WIRED | `ldap.service.ts:579-582` vs. bind at `:593` and deactivation loop at `:688-696` — guard is textually and structurally first. |
|
||||||
|
| `collectSearchEntries` base-search filter | presence of `groupDns` | `memberOf` OR-clause ANDed with `sanitizedFilter` only when `groupDns.length > 0`, else plain `sanitizedFilter` | ✓ WIRED | `ldap.service.ts:755-763`. |
|
||||||
|
| `ldap.controller.ts` | `listGroups`/`searchUsers` | passes `config.baseDn` (unchanged String) — service splits internally | ✓ WIRED (unchanged) | Controller not in the diff (`git diff --stat` confirms only the 5 planned files changed); `listGroups`/`searchUsers` signatures still accept `baseDn: string` and call `this.parseBaseDns(config.baseDn)` internally. |
|
||||||
|
|
||||||
|
### Behavioral Spot-Checks
|
||||||
|
|
||||||
|
| Behavior | Command | Result | Status |
|
||||||
|
|----------|---------|--------|--------|
|
||||||
|
| Full ldap.service.spec.ts suite (18 specs, incl. the 3 new/changed no-op + multi-base specs) | `pnpm --filter @tessera/api exec vitest run src/ldap/ldap.service.spec.ts` | `18 tests passed (18)` | ✓ PASS |
|
||||||
|
| API type-check | `pnpm --filter @tessera/api run type-check` | Clean, no errors | ✓ PASS |
|
||||||
|
| Web type-check | `pnpm --filter @tessera/web run type-check` | Clean, no errors | ✓ PASS |
|
||||||
|
| de.json / en.json valid JSON + wording present | Direct `json.load` + key inspection | Both valid; `groupFilter.description`/`emptyMeansAll`/`baseDnHint` present with expected wording, old "nichts synchronisiert" wording absent | ✓ PASS |
|
||||||
|
|
||||||
|
### Anti-Patterns Found
|
||||||
|
|
||||||
|
None. No `TBD`/`FIXME`/`XXX`/`TODO`/`HACK`/`PLACEHOLDER` markers in the three modified source/UI files. No empty-return stubs, no hardcoded-empty props.
|
||||||
|
|
||||||
|
### Schema / Migration Scope Check
|
||||||
|
|
||||||
|
- `apps/api/prisma/schema.prisma` `LdapConfig` model: `baseDn String` (unchanged, no array), `syncIntervalMin Int @default(0)` (unchanged from 260728-lih commit c54e424), `isActive Boolean @default(true)` (unchanged). No new migration directory added — `apps/api/prisma/migrations` latest entry is still `20260728134010_ldap_sync_interval_default_off`.
|
||||||
|
- `git diff --stat c54e424 HEAD` (the three commits this task builds on) touches exactly the 5 files declared in the plan's `files_modified` frontmatter: `ldap.service.ts`, `ldap.service.spec.ts`, `page.tsx`, `de.json`, `en.json`. No controller, scheduler, or Dockerfile changes.
|
||||||
|
|
||||||
|
### Human Verification Required
|
||||||
|
|
||||||
|
None. All must-haves are backend-logic and i18n-content truths fully verifiable via source inspection and a passing automated test suite; the frontend textarea change is a low-risk mechanical swap (bound state and save/fetch logic untouched) confirmed via source read, consistent with how the SUMMARY flagged its own D4 rationale (no live browser render was needed to confirm the wiring is correct).
|
||||||
|
|
||||||
|
### Gaps Summary
|
||||||
|
|
||||||
|
None. All 6 must-have truths verified against live code, the modified spec file's 18 tests pass, both workspace type-checks are clean, and the diff scope matches the plan exactly (5 files, no schema/migration/controller change).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Verified: 2026-07-29T09:45:00Z_
|
||||||
|
_Verifier: Claude (gsd-verifier)_
|
||||||
Reference in New Issue
Block a user