Files

7.1 KiB

phase, plan, subsystem, tags, status, dependency-graph, tech-stack, key-files, decisions, metrics
phase plan subsystem tags status dependency-graph tech-stack key-files decisions metrics
quick-260728-lih 01 ldap
ldap
sync
security
prisma
i18n
complete
requires provides affects
LdapConfig.syncIntervalMin defaults to 0 (auto-sync off for new configs)
syncUsersForTenant() no-op guard on empty groupFilterDns
apps/api/src/ldap/ldap.service.ts
apps/api/prisma/schema.prisma
apps/web/src/app/(portal)/admin/ldap/page.tsx
added patterns
Early-return guard placed before any side effect (search/deactivation) to make a no-op path structurally safe
created modified
apps/api/prisma/migrations/20260728134010_ldap_sync_interval_default_off/migration.sql
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
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)
duration completed
25 min 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:
    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 <verify> 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 <threat_model> 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: