Files
archivmail/features/PROJ-22-ldap-webgui.md
T
sysops ac91dceac2 feat(PROJ-22): LDAP Web-GUI + feat(PROJ-21): Multi-Tenancy Phase 1
PROJ-22 – LDAP Web-GUI Konfiguration & Test:
- internal/ldapconfig/store.go: AES-256-GCM Passwortspeicherung, CRUD Upsert (id=1)
- internal/ldapauth/client.go: TestConnection (RootDSE, UserCount) + Authenticate (2-step bind)
- internal/auth/auth.go: LDAP-Fallback in Login(), Gruppen-Rollenzuordnung, issueToken helper
- internal/api/ldap_tenants.go: GET/PUT/DELETE/POST-test /api/admin/ldap mit Audit-Log
- go.mod: github.com/go-ldap/ldap/v3 v3.4.8 hinzugefügt
- Frontend: LDAPConfig/LDAPTestResult Typen, LDAP-Tab mit Gruppen-Mappings + Testergebnis

PROJ-21 Phase 1+6+7 – Multi-Tenancy Grundstruktur:
- internal/tenantstore/store.go: tenants, tenant_domains, tenant_ldap Schema; Migration users/audit_log
- API: 8 Tenant-Routen (CRUD + Domain-Management) via SetTenants()
- cmd/archivmail/main.go: ldapSt + tenantSt initialisiert
- Frontend: Mandanten-Tab mit Tabelle, Domain-Dialog, Deaktivieren/Löschen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 20:27:56 +01:00

289 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: PROJ-22
title: LDAP / Active Directory Web-GUI Konfiguration & Test
status: Deployed
created: 2026-03-17
depends_on: PROJ-16
---
## Implementiert (2026-03-17)
- `internal/ldapconfig/store.go` — CRUD für `ldap_config` Tabelle, AES-256-GCM Passwortverschlüsselung (identisch zu imap/store.go)
- `internal/ldapauth/client.go``TestConnection` + `Authenticate` via go-ldap/ldap/v3; STARTTLS + LDAPS + RootDSE-Abfrage + User-Zählung
- `internal/auth/auth.go``Login` mit LDAP-Fallback; `issueToken` als privater Helfer; Gruppen-Rollenzuordnung via `group_mappings`
- `internal/api/ldap_tenants.go` — Handler: GET/PUT/DELETE/POST-test `/api/admin/ldap`; Audit-Log für alle Aktionen
- `internal/api/server.go``ldapStore`/`tenantStore` Felder + Imports ergänzt
- `cmd/archivmail/main.go``ldapcfg.New()` initialisiert; `auth.New()` mit ldapStore; `srv.SetLDAP()`
- `src/lib/api.ts``LDAPConfig`, `LDAPTestResult`, `getLDAPConfig/saveLDAPConfig/deleteLDAPConfig/testLDAPConfig`
- `src/app/admin/page.tsx` — LDAP-Tab mit Formular, Passwort-Schutz, Gruppen-Mappings, Testergebnis-Card
- `go.mod``github.com/go-ldap/ldap/v3 v3.4.8` hinzugefügt
## 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