From b6fa668002fd15d1936eccf416b99d940e45c4a1 Mon Sep 17 00:00:00 2001 From: sysops Date: Tue, 17 Mar 2026 14:41:19 +0100 Subject: [PATCH] docs(PROJ-22): Plan LDAP/AD Web-GUI Konfiguration & Test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vollständiger Plan: ldap_config DB-Tabelle, internal/ldapconfig + ldapauth Pakete, GET/PUT/DELETE/test API-Endpunkte, LDAP-Tab im Admin-UI mit Formular, Testergebnis-Panel, Gruppen-Zuordnungen, Passwort-Masking und Audit-Log. Co-Authored-By: Claude Sonnet 4.6 --- features/INDEX.md | 3 +- features/PROJ-22-ldap-webgui.md | 276 ++++++++++++++++++++++++++++++++ 2 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 features/PROJ-22-ldap-webgui.md diff --git a/features/INDEX.md b/features/INDEX.md index 8447af8..46d29f3 100644 --- a/features/INDEX.md +++ b/features/INDEX.md @@ -34,7 +34,8 @@ | PROJ-19 | Mailpiler → archivmail Migrationstool | Deployed | [PROJ-19](PROJ-19-import-piler.md) | 2026-03-17 | | PROJ-20 | Nutzer-Löschung & E-Mail-Verbleib (GoBD-konform) | Deployed | [PROJ-20](PROJ-20-nutzer-loeschung.md) | 2026-03-17 | | PROJ-21 | Multi-Mandanten-Fähigkeit (Multi-Tenancy) | Planned | [PROJ-21](PROJ-21-multi-tenancy.md) | 2026-03-17 | +| PROJ-22 | LDAP / AD – Web-GUI Konfiguration & Test | Planned | [PROJ-22](PROJ-22-ldap-webgui.md) | 2026-03-17 | -## Next Available ID: PROJ-22 +## Next Available ID: PROJ-23 diff --git a/features/PROJ-22-ldap-webgui.md b/features/PROJ-22-ldap-webgui.md new file mode 100644 index 0000000..2ff2c7b --- /dev/null +++ b/features/PROJ-22-ldap-webgui.md @@ -0,0 +1,276 @@ +--- +id: PROJ-22 +title: LDAP / Active Directory – Web-GUI Konfiguration & Test +status: Planned +created: 2026-03-17 +depends_on: PROJ-16 +--- + +## Ziel + +LDAP/AD-Verbindungen sollen vollständig über die Web-GUI eingetragen, bearbeitet und +getestet werden können — ohne direkten Zugriff auf `config.yml` oder den Server. + +--- + +## Abhängigkeiten + +| Projekt | Abhängigkeit | +|---|---| +| PROJ-16 | Backend-LDAP-Authentifizierung muss implementiert sein (Phase A) | +| PROJ-21 | Für Multi-Tenant-Betrieb (Phase B) — PROJ-22 Phase A ist unabhängig | + +--- + +## Kernproblem: Config-Speicherung + +LDAP ist aktuell nur in `config.yml` konfigurierbar — eine GUI kann diese Datei nicht +sicher lesen/schreiben. Lösung: **LDAP-Konfiguration wird in der Datenbank gespeichert.** +`config.yml` bleibt als Fallback für initiale Einrichtung erhalten. + +**Priorität:** DB-Konfiguration > `config.yml` + +--- + +## Datenbankschema + +### Neue Tabelle: `ldap_config` (Single-Tenant / Phase A) + +```sql +CREATE TABLE ldap_config ( + id BIGSERIAL PRIMARY KEY, + enabled BOOLEAN NOT NULL DEFAULT false, + url TEXT NOT NULL, -- ldap://... oder ldaps://... + bind_dn TEXT NOT NULL, + bind_password BYTEA NOT NULL, -- AES-256-GCM verschlüsselt + base_dn TEXT NOT NULL, + user_filter TEXT NOT NULL DEFAULT '(sAMAccountName=%s)', + tls BOOLEAN NOT NULL DEFAULT false, -- STARTTLS + tls_skip_verify BOOLEAN NOT NULL DEFAULT false, + default_role VARCHAR(20) NOT NULL DEFAULT 'user', + group_mappings JSONB, -- [{group_dn, role}, ...] + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_by TEXT -- Username des letzten Bearbeiters +); +-- Maximal ein Datensatz (Single-Tenant). Nach PROJ-21 wird tenant_ldap verwendet. +``` + +> **Multi-Tenant (PROJ-21 Phase B):** `ldap_config` wird durch `tenant_ldap` (aus PROJ-21) +> ersetzt. Die API-Endpunkte bleiben identisch — nur der Routing-Kontext ändert sich. + +--- + +## Backend + +### Neues Paket: `internal/ldapconfig` + +``` +internal/ldapconfig/ + store.go — CRUD für ldap_config Tabelle, Passwort-Ver/Entschlüsselung +``` + +**Methoden:** +```go +func (s *Store) Get(ctx) (*LDAPConfig, error) // Passwort maskiert +func (s *Store) GetWithPassword(ctx) (*LDAPConfig, error) // inkl. entschlüsseltem PW +func (s *Store) Save(ctx, cfg LDAPConfig) error // Upsert +func (s *Store) Delete(ctx) error // LDAP deaktivieren +``` + +### Neues Paket: `internal/ldapauth` + +``` +internal/ldapauth/ + client.go — Verbindung, Bind, User-Suche, Gruppen-Abfrage + test.go — TestConnection(cfg) → TestResult +``` + +**TestResult:** +```go +type TestResult struct { + OK bool + Message string + LatencyMS int64 + ServerInfo string // z.B. "OpenLDAP 2.6" oder "Active Directory" + UsersFound int // Anzahl User im base_dn + ErrorDetail string +} +``` + +### API-Endpunkte (admin only) + +``` +GET /api/admin/ldap Aktuelle Konfiguration abrufen (Passwort maskiert: "••••••") +PUT /api/admin/ldap Konfiguration speichern (leeres Passwort = bestehendes behalten) +DELETE /api/admin/ldap LDAP deaktivieren und Konfiguration löschen +POST /api/admin/ldap/test Verbindung testen +``` + +**`PUT /api/admin/ldap` Request:** +```json +{ + "enabled": true, + "url": "ldap://192.168.1.10:389", + "bind_dn": "CN=archivmail-svc,OU=ServiceAccounts,DC=corp,DC=local", + "bind_password": "", // leer = bestehendes Passwort beibehalten + "base_dn": "OU=Users,DC=corp,DC=local", + "user_filter": "(sAMAccountName=%s)", + "tls": false, + "tls_skip_verify": false, + "default_role": "user", + "group_mappings": [ + { "group_dn": "CN=archivmail-admins,OU=Groups,DC=corp,DC=local", "role": "domain_admin" }, + { "group_dn": "CN=archivmail-auditors,OU=Groups,DC=corp,DC=local", "role": "auditor" } + ] +} +``` + +**`POST /api/admin/ldap/test` Request:** +```json +{ + "use_saved": true // true = gespeicherte Konfig testen; false = Body-Felder verwenden +} +// Bei use_saved=false: gleicher Body wie PUT (zum Testen vor dem Speichern) +``` + +**`POST /api/admin/ldap/test` Response:** +```json +{ + "ok": true, + "message": "Verbindung erfolgreich", + "latency_ms": 12, + "server_info": "Active Directory (Windows Server 2022)", + "users_found": 247, + "error_detail": "" +} +``` + +### Audit-Log + +Jede Änderung der LDAP-Konfiguration wird geloggt: +- `action=ldap_config_updated, changed_by=admin` +- `action=ldap_config_deleted, changed_by=admin` +- `action=ldap_test, result=ok|fail, latency_ms=12` + +--- + +## Frontend + +### Neuer Tab im Admin-Bereich: „LDAP" + +Eingebaut zwischen „Import" und „Module" im bestehenden ``-Block in `src/app/admin/page.tsx`. + +#### Layout + +``` +┌─────────────────────────────────────────────────────┐ +│ LDAP / Active Directory [●] Aktiv │ +├─────────────────────────────────────────────────────┤ +│ │ +│ Server-URL [ldap://192.168.1.10:389 ] │ +│ Bind-DN [CN=svc,DC=corp,DC=local ] │ +│ Bind-Passwort [•••••••• ] [↺] │ +│ Base-DN [OU=Users,DC=corp,DC=local ] │ +│ Benutzer-Filter [(sAMAccountName=%s) ] │ +│ │ +│ [ ] STARTTLS [ ] TLS-Zertifikat ignorieren │ +│ │ +│ Standard-Rolle [Benutzer ▼] │ +│ │ +│ Gruppen-Zuordnungen [+ Hinzuf.] │ +│ ┌────────────────────────────────────────────────┐ │ +│ │ CN=admins,... → Domain-Admin [🗑] │ │ +│ │ CN=auditors,... → Auditor [🗑] │ │ +│ └────────────────────────────────────────────────┘ │ +│ │ +│ [Verbindung testen] [Speichern] [Konfiguration │ +│ löschen] │ +│ │ +│ ┌─ Testergebnis ─────────────────────────────────┐ │ +│ │ ✓ Verbindung erfolgreich (12 ms) │ │ +│ │ Server: Active Directory (Windows Server 22) │ │ +│ │ Benutzer im Verzeichnis: 247 │ │ +│ └────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────┘ +``` + +#### Verhalten + +- **Laden:** Beim Öffnen des Tabs wird `GET /api/admin/ldap` aufgerufen. Passwort-Feld zeigt `••••••`, echter Wert nie übertragen. +- **Passwort-Feld:** Leer lassen = bestehendes Passwort unverändert. Passwort-Symbol `↺` setzt Feld zurück (zwingt Neueingabe). +- **Verbindung testen:** Sendet aktuellen Formular-Inhalt (ungespeichert) an `POST /api/admin/ldap/test` mit `use_saved=false`. Testergebnis erscheint direkt unter dem Formular. +- **Speichern:** `PUT /api/admin/ldap`, danach Reload. Erfolgsmeldung via Toast. +- **Aktivieren/Deaktivieren:** Toggle oben rechts — sendet `PUT` mit `enabled: true/false`, kein Reload nötig. +- **Gruppen-Zuordnungen:** Zeilen dynamisch hinzufügen/entfernen. Kein separater Speichern-Button — beim Haupt-„Speichern" mitübertragen. +- **Konfiguration löschen:** Bestätigungs-Dialog — `DELETE /api/admin/ldap`. + +#### Neue API-Funktionen in `src/lib/api.ts` + +```typescript +export interface LDAPConfig { + enabled: boolean; + url: string; + bind_dn: string; + bind_password: string; // immer "••••••" in GET-Response + base_dn: string; + user_filter: string; + tls: boolean; + tls_skip_verify: boolean; + default_role: string; + group_mappings: { group_dn: string; role: string }[]; + updated_at?: string; + updated_by?: string; +} + +export interface LDAPTestResult { + ok: boolean; + message: string; + latency_ms: number; + server_info: string; + users_found: number; + error_detail: string; +} + +export async function getLDAPConfig(): Promise +export async function saveLDAPConfig(cfg: LDAPConfig): Promise +export async function deleteLDAPConfig(): Promise +export async function testLDAPConfig(cfg: LDAPConfig, useSaved: boolean): Promise +``` + +--- + +## Umsetzungsreihenfolge + +| Schritt | Inhalt | +|---|---| +| 1 | DB-Tabelle `ldap_config` anlegen (`initSchema`) | +| 2 | `internal/ldapconfig`: Store mit AES-Passwort-Verschlüsselung | +| 3 | `internal/ldapauth`: TestConnection-Funktion | +| 4 | API-Handler: GET / PUT / DELETE / POST test | +| 5 | `auth.Manager.Login` nutzt DB-Config (Fallback: config.yml) | +| 6 | `src/lib/api.ts`: neue Funktionen | +| 7 | `src/app/admin/page.tsx`: neuer LDAP-Tab | +| 8 | Audit-Log für alle LDAP-Aktionen | + +--- + +## Sicherheit + +- Passwort wird **niemals** im Klartext über die API zurückgegeben (GET maskiert mit `••••••`) +- Passwort-Speicherung: AES-256-GCM, gleiche Methode wie IMAP-Passwörter (`internal/imap/store.go`) +- Leeres Passwort im PUT = bestehendes beibehalten (kein versehentliches Löschen) +- `tls_skip_verify: true` löst Admin-Warnung im UI aus +- LDAP-Konfig nur für `admin` / `domain_admin` sichtbar — nie für `user` oder `auditor` + +--- + +## Akzeptanzkriterien + +- [ ] Admin kann LDAP-Verbindung vollständig über die Web-GUI konfigurieren +- [ ] „Verbindung testen" gibt Latenz, Server-Typ und User-Anzahl zurück — auch mit ungespeicherten Werten +- [ ] Passwort wird niemals im Klartext an das Frontend gesendet +- [ ] Bestehendes Passwort bleibt erhalten wenn Feld leer gelassen wird +- [ ] Gruppen-Zuordnungen sind im UI verwaltbar (hinzufügen, entfernen) +- [ ] Alle Änderungen landen im Audit-Log +- [ ] config.yml-Konfiguration funktioniert weiterhin als Fallback wenn keine DB-Konfig vorhanden +- [ ] STARTTLS und LDAPS werden korrekt unterstützt und getestet