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>
10 KiB
id, title, status, created, depends_on
| id | title | status | created | depends_on |
|---|---|---|---|---|
| PROJ-22 | LDAP / Active Directory – Web-GUI Konfiguration & Test | Planned | 2026-03-17 | 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)
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_configwird durchtenant_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:
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:
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:
{
"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:
{
"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:
{
"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=adminaction=ldap_config_deleted, changed_by=adminaction=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/ldapaufgerufen. 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/testmituse_saved=false. Testergebnis erscheint direkt unter dem Formular. - Speichern:
PUT /api/admin/ldap, danach Reload. Erfolgsmeldung via Toast. - Aktivieren/Deaktivieren: Toggle oben rechts — sendet
PUTmitenabled: 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
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: truelöst Admin-Warnung im UI aus- LDAP-Konfig nur für
admin/domain_adminsichtbar — nie füruseroderauditor
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