docs(PROJ-22): Plan LDAP/AD Web-GUI Konfiguration & Test
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 <noreply@anthropic.com>
This commit is contained in:
+2
-1
@@ -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 |
|
||||
|
||||
<!-- Add features above this line -->
|
||||
|
||||
## Next Available ID: PROJ-22
|
||||
## Next Available ID: PROJ-23
|
||||
|
||||
@@ -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 `<Tabs>`-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<LDAPConfig>
|
||||
export async function saveLDAPConfig(cfg: LDAPConfig): Promise<void>
|
||||
export async function deleteLDAPConfig(): Promise<void>
|
||||
export async function testLDAPConfig(cfg: LDAPConfig, useSaved: boolean): Promise<LDAPTestResult>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
Reference in New Issue
Block a user