--- phase: quick-260728-lih plan: 01 subsystem: ldap tags: [ldap, sync, security, prisma, i18n] status: complete dependency-graph: requires: [] provides: - "LdapConfig.syncIntervalMin defaults to 0 (auto-sync off for new configs)" - "syncUsersForTenant() no-op guard on empty groupFilterDns" affects: - apps/api/src/ldap/ldap.service.ts - apps/api/prisma/schema.prisma - apps/web/src/app/(portal)/admin/ldap/page.tsx tech-stack: added: [] patterns: - "Early-return guard placed before any side effect (search/deactivation) to make a no-op path structurally safe" key-files: created: - apps/api/prisma/migrations/20260728134010_ldap_sync_interval_default_off/migration.sql modified: - apps/api/prisma/schema.prisma - 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 decisions: - "Chosen lastSyncAt policy on empty selection: leave it untouched — the early return happens before any DB write, so a no-op leaves no trace" - "DELIBERATE back-compat break: empty groupFilterDns previously meant 'import everyone under baseDn'; it now means 'sync nothing' (both code semantics and admin UI copy in de+en updated to reflect this)" metrics: duration: 25 min completed: 2026-07-28 --- # Quick Task 260728-lih: LDAP Sync Selektiv + Auto-Sync Default Summary LDAP directory sync made strictly selective (empty group/OU filter now syncs nothing instead of importing the whole directory) and new LDAP configs default to auto-sync OFF (`syncIntervalMin` 60 -> 0). ## What Was Built Three coordinated, atomically-committed changes: **Task 1 — Schema default + migration (commit `c54e424`)** - `LdapConfig.syncIntervalMin` default changed from `60` to `0` in `schema.prisma`. - `isActive @default(true)` left completely untouched — it gates LDAP login (`auth.service.ts:55`), not auto-sync. - Migration `20260728134010_ldap_sync_interval_default_off` generated and applied against the local `tessera` Postgres DB (container `tessera-ctl-db-1`, started fresh for this task — it was `Exited`). The emitted SQL is exactly: ```sql ALTER TABLE "LdapConfig" ALTER COLUMN "syncIntervalMin" SET DEFAULT 0; ``` No data rewrite of existing rows — verified live: `information_schema.columns` shows `column_default = 0` for `LdapConfig.syncIntervalMin` after the migration. **Task 2 — Selective-only sync semantics + tests (commit `57bc7f9`)** - `collectSearchEntries()`: the empty/undefined `groupFilterDns` branch no longer runs `client.search(config.baseDn, ...)` — it returns `[]` immediately. Doc comment updated to state this is a deliberate no-op, not "identical to pre-filter behavior." - `syncUsersForTenant()`: added an early-return guard as the very first statement after constructing the empty `result` object — **before** `new Client(...)`, before any bind/search, and before the deactivation loop (~line 631 in the pre-change file). When `config.groupFilterDns` is missing or empty, the method returns the empty successful result (`created=0/updated=0/deactivated=0/errors=[]`) immediately. This is the critical safety fix: an empty selection can no longer mass-deactivate every existing LDAP user, because the deactivation loop is never reached. - `ldap.service.spec.ts`: - The three existing exclude-list tests (`describe('...per-user exclude list')`) now use a non-empty `baseConfig.groupFilterDns` (`['ou=people,dc=example,dc=com']`) so they still exercise the search/import/exclude/deactivation logic — all three still pass unchanged in behavior. - New `describe('LdapService.syncUsersForTenant — empty selection no-op')` block proves the safety property: with an existing LDAP user in `prisma.user.findMany`'s mock return and `groupFilterDns: []`, the sync result is exactly `{created:0,updated:0,deactivated:0,errors:[]}`, and `mockSearch`, `mockBind`, `userService.create`, `prisma.user.update`, and `prisma.ldapConfig.update` were all NEVER called. **Task 3 — Frontend default + i18n rewording (commit `63a07ab`)** - `page.tsx`: the create-form `formData` initializer's `syncIntervalMin` changed from `60` to `0`, matching the new backend default for brand-new configs. The `data.syncIntervalMin ?? 60` fallback in `fetchConfig` (only relevant when editing an already-saved, non-null DB value) was deliberately left as-is — out of scope, never fires for real data. - `de.json` + `en.json` under `admin.ldap.groupFilter`: `description` and `emptyMeansAll` reworded in both locales from "no selection imports everyone under the base DN" to "no selection syncs nothing," matching the new backend semantics exactly as specified in the plan (verbatim strings). ## Verification Results - **API test suite:** `cd apps/api && pnpm vitest run` — **394/394 tests passed** across 31 test files, including the updated `ldap.service.spec.ts` (16/16 passed: 3 updated exclude-list tests + 1 new no-op test + 12 pre-existing unrelated LDAP tests). - **API typecheck:** `cd apps/api && pnpm exec tsc --noEmit` — clean, no errors. - **Schema/migration verification:** both automated checks from the plan passed — `grep -Eq 'syncIntervalMin[[:space:]]+Int[[:space:]]+@default\(0\)' apps/api/prisma/schema.prisma` and the live `column_default = 0` psql check. - **Frontend verification:** all four checks from the plan's Task 3 `` passed — `syncIntervalMin: 0,` present in `page.tsx`; both `de.json`/`en.json` parse as valid JSON; `"nichts synchronisiert"` appears exactly 2x in `de.json`; `"nothing is synced"` appears exactly 2x in `en.json`. - **Untouched files confirmed:** `git diff --stat` across all 3 commits shows zero changes to `ldap-sync.scheduler.ts` or `auth.service.ts`, matching the plan's explicit "do not touch" constraints. - **No frontend test suite exists** for `admin/ldap/page.tsx` (confirmed via search) — not a regression, no coverage was removed. ## Deviations from Plan None — plan executed exactly as written. All file paths, migration steps, exact string values, and test assertions matched the plan's action blocks. ## Threat Model Verification All three registered threats from the plan's `` are mitigated/accepted as specified: - **T-lih-01 (DoS via mass-deactivation):** Mitigated — the early-return guard runs before the deactivation loop; proven by the new no-op unit test (`prisma.user.update` never called). - **T-lih-02 (Tampering via migration data rewrite):** Mitigated — migration SQL contains only `SET DEFAULT 0`, verified live; no drift was proposed by Prisma, so no halt-and-report was needed. - **T-lih-03 (Elevation via isActive change):** Accepted, correctly — `isActive @default(true)` was not touched; verified via `git diff`. ## Self-Check: PASSED **Files:** - FOUND: apps/api/prisma/schema.prisma - FOUND: apps/api/prisma/migrations/20260728134010_ldap_sync_interval_default_off/migration.sql - FOUND: apps/api/src/ldap/ldap.service.ts - FOUND: apps/api/src/ldap/ldap.service.spec.ts - FOUND: apps/web/src/app/(portal)/admin/ldap/page.tsx - FOUND: apps/web/src/messages/de.json - FOUND: apps/web/src/messages/en.json **Commits:** - FOUND: c54e424 - FOUND: 57bc7f9 - FOUND: 63a07ab