Files

10 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, coverage, duration, completed, status
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions patterns-established requirements-completed coverage duration completed status
quick-260729-d3k 01 auth
ldap
ldapts
active-directory
multi-tenancy
admin-ui
next-intl
phase provides
quick-260728-lih syncUsersForTenant early-return no-op guard + groupFilterDns-based selective sync (the model this plan replaces)
parseBaseDns() helper turning the newline-separated baseDn String into a trimmed DN list
Multi-base directory search across collectSearchEntries/listGroups/searchUsers, merged/deduped by entry dn
syncUsersForTenant no-op guard re-keyed on an empty parsed base-DN list (was empty groupFilterDns)
Multi-line Base-DN admin textarea + reworked de/en scope wording
ldap
admin-ldap-page
i18n
added patterns
Map<string, Entry> keyed by entry.dn used to merge/dedupe results across multiple LDAP search-base calls
created modified
apps/api/src/ldap/ldap.service.ts
apps/api/src/ldap/ldap.service.spec.ts
apps/web/src/app/(portal)/admin/ldap/page.tsx
apps/web/src/messages/de.json
apps/web/src/messages/en.json
The syncUsersForTenant no-op guard is keyed SOLELY on parseBaseDns(config.baseDn).length === 0 — an empty groupFilterDns is no longer a no-op condition, it now performs a normal multi-base search with no memberOf restriction
groupFilterDns ou= entries stay ADDITIONAL search bases (plain filter); non-ou= entries become an optional memberOf constraint ANDed onto every base-DN search
Base-DN stays a single String column (newline-separated); no schema change or migration — parseBaseDns() is the sole place the list is derived
parseBaseDns()-then-loop-then-Map-dedupe pattern for turning a single delimited config field into a multi-target directory search, reusable for any future multi-value LDAP config field
260729-d3k
id description requirement verification human_judgment
D1 parseBaseDns() splits the Base-DN field into a trimmed, non-empty list; empty/whitespace-only input yields [] 260729-d3k
kind ref status
unit apps/api/src/ldap/ldap.service.spec.ts#LdapService.syncUsersForTenant — empty base DN no-op > creates nobody and deactivates nobody when the base DN is empty (whitespace-only) pass
false
id description requirement verification human_judgment
D2 syncUsersForTenant is a total no-op (no bind, no search, no create/update, no deactivation, no ldapConfig.update) ONLY when the parsed base-DN list is empty 260729-d3k
kind ref status
unit apps/api/src/ldap/ldap.service.spec.ts#LdapService.syncUsersForTenant — empty base DN no-op > creates nobody and deactivates nobody when the base DN is empty (whitespace-only) pass
kind ref status
unit apps/api/src/ldap/ldap.service.spec.ts#LdapService.syncUsersForTenant — empty base DN no-op > is NOT a no-op when baseDn is set but groupFilterDns is empty (normal multi-base search) pass
false
id description requirement verification human_judgment
D3 collectSearchEntries/listGroups/searchUsers search every configured base DN and merge/dedupe results by entry dn 260729-d3k
kind ref status
unit apps/api/src/ldap/ldap.service.spec.ts#LdapService.syncUsersForTenant — multi base DN scope > searches every configured base DN and merges/dedupes results by dn pass
kind ref status
unit pnpm --filter @tessera/api exec vitest run src/ldap/ldap.service.spec.ts (full 18-spec file, incl. searchUsers/listGroups paths exercised by existing specs) pass
false
id description requirement verification human_judgment rationale
D4 Admin Base-DN field is a multi-line textarea persisting one newline-separated string; de/en i18n reworded to describe the group filter as optional and the base DN(s) as the sync scope 260729-d3k
kind ref status
unit node -e JSON.parse(...) de.json/en.json + grep Basis-DN(s)/base DN(s)/baseDnHint/textarea pass
kind ref status
other pnpm --filter @tessera/web run type-check pass
true Visual rendering of the new textarea and hint text in the actual admin page was not verified via a running browser session in this quick task — automated checks confirm JSON validity, wording, and TypeScript correctness only.
3min 2026-07-29 complete

Quick Task 260729-d3k: LDAP Multi-Base-DN & Base-DN-as-Scope Summary

Replaced the "empty group filter = sync nothing" model from Quick 260728-lih with a "Base-DN(s) = sync scope" model: the Base-DN admin field now accepts multiple newline-separated DNs, all three LDAP directory-search paths loop and merge every configured base, and the mass-deactivation safety guard is re-keyed to the parsed base-DN list instead of groupFilterDns.

Performance

  • Duration: ~3 min
  • Started: 2026-07-29T07:34:57Z
  • Completed: 2026-07-29T07:38:16Z
  • Tasks: 2/2 completed
  • Files modified: 5

Accomplishments

  • parseBaseDns() splits the single baseDn String column (still no schema/migration change) into a trimmed, non-empty DN list, dropping blank/whitespace-only lines.
  • syncUsersForTenant()'s early-return no-op guard is now keyed exclusively on parseBaseDns(config.baseDn).length === 0 — the sole condition that skips bind/search and the deactivation loop. An empty groupFilterDns no longer triggers a no-op; it now performs a normal multi-base search with no memberOf restriction.
  • collectSearchEntries(), listGroups(), and searchUsers() all loop over every configured base DN and merge/dedupe results into a Map<string, Entry> keyed by entry.dn. groupFilterDns remains an optional extra restriction: ou= entries become additional search bases (plain filter), non-ou= entries become an optional memberOf OR-clause ANDed onto every base-DN search.
  • Admin LDAP page's Base-DN field is now a multi-line <textarea rows={3}> (was a single-line <input>), with a baseDnHint paragraph underneath. The bound value stays one plain string with newline separators — fetchConfig/handleSave were untouched.
  • de/en i18n reworded: groupFilter.description and groupFilter.emptyMeansAll now describe the group filter as an optional additional restriction and state that an empty selection syncs all users under the Base-DN(s) — the old "nothing is synced" framing is gone. New baseDnHint key added to both locales.

Task Commits

Each task was committed atomically:

  1. Task 1: Backend multi-base scope + base-DN-keyed deactivation guard + spec - 5cbd530 (feat)
  2. Task 2: Multi-line Base-DN textarea + reworked de/en i18n - 96be7e1 (feat)

Plan metadata: pending (docs commit follows, handled by orchestrator)

Note: This plan had tdd="true" on Task 1's frontmatter, but the plan's own <action> text specified writing/updating tests alongside the implementation change rather than a strict RED→GREEN cycle (this is a refactor of existing tested behavior, not new-feature TDD). Both the implementation and spec changes were committed together in a single feat commit, consistent with how the task's <verify>/<done> criteria were framed (spec pass + type-check, not a RED-then-GREEN gate sequence).

Files Created/Modified

  • apps/api/src/ldap/ldap.service.ts - Added parseBaseDns(); re-keyed syncUsersForTenant()'s no-op guard; rewrote collectSearchEntries(), listGroups(), searchUsers() to loop and merge every configured base DN
  • apps/api/src/ldap/ldap.service.spec.ts - Replaced the "empty selection no-op" describe block with an "empty base DN no-op" block (plus a second spec proving empty-groupFilterDns is NOT a no-op); added a "multi base DN scope" describe block with a two-base merge/dedup spec
  • apps/web/src/app/(portal)/admin/ldap/page.tsx - Base-DN <input> replaced with a multi-line <textarea rows={3}> + hint paragraph
  • apps/web/src/messages/de.json - Added baseDnHint; reworded groupFilter.description/emptyMeansAll
  • apps/web/src/messages/en.json - Added baseDnHint; reworded groupFilter.description/emptyMeansAll

Decisions Made

  • The no-op guard is keyed SOLELY on the parsed base-DN list being empty — this is the one and only condition under which syncUsersForTenant skips search and the deactivation loop, per the plan's CRITICAL SAFETY requirement.
  • ou= group-filter entries remain additional search bases; non-ou= entries remain an optional memberOf constraint — unchanged in substance from the prior Quick 260728-lih logic, just now applied across every configured base DN instead of the single config.baseDn.
  • No controller change: ldap.controller.ts still passes config.baseDn as a plain String into listGroups/searchUsers — the service splits it internally.

Deviations from Plan

None - plan executed exactly as written. All five files listed in the plan's files_modified frontmatter were touched, and no others (schema.prisma, ldap-sync.scheduler.ts, auth.service.ts, ldap.controller.ts were explicitly left untouched per the plan's critical notes).

Issues Encountered

None.

User Setup Required

None - no external service configuration required.

Next Phase Readiness

  • All 18 ldap.service.spec.ts specs pass (up from the prior file's spec count, +5 net new/changed specs: empty-base-DN no-op, non-no-op-with-empty-groupFilterDns, multi-base merge/dedup).
  • Full API suite: 396/396 tests pass (pnpm --filter @tessera/api exec vitest run).
  • pnpm --filter @tessera/api run type-check and pnpm --filter @tessera/web run type-check both clean.
  • de.json/en.json parse as valid JSON and carry the reworded scope wording plus the new baseDnHint key.
  • No Prisma schema change, no migration, no new endpoint, no Dockerfile/infra change — git diff for this quick task touches exactly the five files listed in the plan.
  • Live UI rendering of the new textarea was not visually verified in a browser during this quick task (see coverage.D4.rationale); it is a low-risk mechanical swap (input type="text" → textarea) with the same bound state and unchanged save/fetch logic.

Phase: quick-260729-d3k Completed: 2026-07-29

Self-Check: PASSED

All 6 claimed files found on disk; both task commit hashes (5cbd530, 96be7e1) found in git log.