Files
tessera-ctl/.planning/quick/260729-d3k-ldap-multi-base-dn-und-base-dn-als-scope/260729-d3k-SUMMARY.md
T

160 lines
10 KiB
Markdown

---
phase: quick-260729-d3k
plan: 01
subsystem: auth
tags: [ldap, ldapts, active-directory, multi-tenancy, admin-ui, next-intl]
requires:
- phase: quick-260728-lih
provides: "syncUsersForTenant early-return no-op guard + groupFilterDns-based selective sync (the model this plan replaces)"
provides:
- "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"
affects: [ldap, admin-ldap-page, i18n]
tech-stack:
added: []
patterns:
- "Map<string, Entry> keyed by entry.dn used to merge/dedupe results across multiple LDAP search-base calls"
key-files:
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
key-decisions:
- "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"
patterns-established:
- "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"
requirements-completed: [260729-d3k]
coverage:
- id: D1
description: "parseBaseDns() splits the Base-DN field into a trimmed, non-empty list; empty/whitespace-only input yields []"
requirement: "260729-d3k"
verification:
- kind: unit
ref: "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)"
status: pass
human_judgment: false
- id: D2
description: "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"
requirement: "260729-d3k"
verification:
- kind: unit
ref: "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)"
status: pass
- kind: unit
ref: "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)"
status: pass
human_judgment: false
- id: D3
description: "collectSearchEntries/listGroups/searchUsers search every configured base DN and merge/dedupe results by entry dn"
requirement: "260729-d3k"
verification:
- kind: unit
ref: "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"
status: pass
- kind: unit
ref: "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)"
status: pass
human_judgment: false
- id: D4
description: "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"
requirement: "260729-d3k"
verification:
- kind: unit
ref: "node -e JSON.parse(...) de.json/en.json + grep Basis-DN(s)/base DN(s)/baseDnHint/textarea"
status: pass
- kind: other
ref: "pnpm --filter @tessera/web run type-check"
status: pass
human_judgment: true
rationale: "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."
duration: 3min
completed: 2026-07-29
status: 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.