ac91dceac2
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>
289 lines
12 KiB
Markdown
289 lines
12 KiB
Markdown
---
|
||
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
|