docs(260728-lih): complete LDAP sync selektiv und auto-sync default plan

This commit is contained in:
2026-07-28 15:43:37 +02:00
parent 63a07abb47
commit fd9c1bee34
2 changed files with 108 additions and 4 deletions
+6 -4
View File
@@ -5,8 +5,8 @@ milestone_name: Ausschreibungs-Radar
current_phase: 14 current_phase: 14
current_phase_name: rss-email-alert-ingestion-module-rollout current_phase_name: rss-email-alert-ingestion-module-rollout
status: executing status: executing
stopped_at: Completed 14-05-PLAN.md (last plan of Phase 14) stopped_at: Completed 260728-lih quick task (LDAP sync selective + auto-sync default off)
last_updated: "2026-07-23T12:23:13.422Z" last_updated: "2026-07-28T13:43:32.444Z"
last_activity: 2026-07-23 last_activity: 2026-07-23
last_activity_desc: Completed 14-02-PLAN.md (RSS ingestion slice) last_activity_desc: Completed 14-02-PLAN.md (RSS ingestion slice)
progress: progress:
@@ -223,6 +223,7 @@ Recent decisions affecting current work:
- [Phase ?]: PORTAL_URLS typed as Record<(typeof DENYLISTED_PORTALS)[number], string> so the compiler enforces a URL for every denylisted portal (no re-declared set, no silent gap) - [Phase ?]: PORTAL_URLS typed as Record<(typeof DENYLISTED_PORTALS)[number], string> so the compiler enforces a URL for every denylisted portal (no re-declared set, no silent gap)
- [Phase ?]: CoverageBanner's denylist block is independent of the onlyDoe coverage-note condition — component renders when either block has content, not gated behind the DOE-only check - [Phase ?]: CoverageBanner's denylist block is independent of the onlyDoe coverage-note condition — component renders when either block has content, not gated behind the DOE-only check
- [Phase ?]: 14-05: tenderRadar i18n namespace added; Bundesland/CPV filter option values stay canonical German for backend compatibility, only labels translated; portal display slugs left untranslated as proper nouns - [Phase ?]: 14-05: tenderRadar i18n namespace added; Bundesland/CPV filter option values stay canonical German for backend compatibility, only labels translated; portal display slugs left untranslated as proper nouns
- [Phase ?]: [260728-lih]: DELIBERATE back-compat break — empty groupFilterDns now means 'sync nothing' (was 'import everyone under baseDn'); early-return guard in syncUsersForTenant runs before search/deactivation so an empty selection never mass-deactivates existing LDAP users
### Pending Todos ### Pending Todos
@@ -251,6 +252,7 @@ None yet.
| 260709-sbx | LDAP: Suchbox fuer die entdeckten Gruppen/OUs-Liste | 2026-07-09 | aaa2922 | (direct) | | 260709-sbx | LDAP: Suchbox fuer die entdeckten Gruppen/OUs-Liste | 2026-07-09 | aaa2922 | (direct) |
| 260714-lex | LDAP: Per-User Exclude/Denylist-Filter (Service-Accounts vom Sync ausschliessen) — live verifiziert: deaktiviert 4 Accounts, 2 echte User aktiv | 2026-07-14 | 9d1323f | (direct) | | 260714-lex | LDAP: Per-User Exclude/Denylist-Filter (Service-Accounts vom Sync ausschliessen) — live verifiziert: deaktiviert 4 Accounts, 2 echte User aktiv | 2026-07-14 | 9d1323f | (direct) |
| 13 | Normalizer-Gap Phase 13 schliessen: NetServer/Cosinex-Bag-Dispatch (TenderNormalizerService) | 2026-07-23 | 9881005 | — | | 13 | Normalizer-Gap Phase 13 schliessen: NetServer/Cosinex-Bag-Dispatch (TenderNormalizerService) | 2026-07-23 | 9881005 | — |
| 260728-lih | LDAP: Sync strikt selektiv (leere Auswahl = No-Op statt Voll-Import) + Auto-Sync-Default aus (syncIntervalMin 60→0) | 2026-07-28 | c54e424,57bc7f9,63a07ab | [260728-lih-ldap-sync-selektiv-und-auto-sync-default](.planning/quick/260728-lih-ldap-sync-selektiv-und-auto-sync-default/) |
## Deferred Items ## Deferred Items
@@ -262,7 +264,7 @@ Items acknowledged and carried forward from previous milestone close:
## Session Continuity ## Session Continuity
Last session: 2026-07-23T12:23:13.408Z Last session: 2026-07-28T13:43:32.416Z
Stopped at: Completed 14-05-PLAN.md (last plan of Phase 14) Stopped at: Completed 260728-lih quick task (LDAP sync selective + auto-sync default off)
Resume file: None Resume file: None
Last activity: 2026-07-14 - Built LDAP per-user exclude/denylist filter (9d1323f), migration applied on live DB, verified via Playwright: sync deactivated 4 excluded service accounts (administrator/krbtgt/guest/dns-ldap), 2 real LDAP users stay active, 0 wrongly created Last activity: 2026-07-14 - Built LDAP per-user exclude/denylist filter (9d1323f), migration applied on live DB, verified via Playwright: sync deactivated 4 excluded service accounts (administrator/krbtgt/guest/dns-ldap), 2 real LDAP users stay active, 0 wrongly created
@@ -0,0 +1,102 @@
---
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 `<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:**
- FOUND: c54e424
- FOUND: 57bc7f9
- FOUND: 63a07ab