Files
schalli bd84a26824
Tessera CI/CD / Lint & Type Check (push) Successful in 44s
Tessera CI/CD / Tests (push) Successful in 48s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m40s
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>
2026-07-29 09:43:06 +02:00

9.2 KiB

phase, verified, status, score, behavior_unverified, overrides_applied
phase verified status score behavior_unverified overrides_applied
quick-260729-d3k 2026-07-29T09:45:00Z passed 6/6 must-haves verified 0 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.
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)