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

12 KiB
Raw Blame History

id, title, status, created, depends_on
id title status created depends_on
PROJ-22 LDAP / Active Directory Web-GUI Konfiguration & Test Deployed 2026-03-17 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.goTestConnection + Authenticate via go-ldap/ldap/v3; STARTTLS + LDAPS + RootDSE-Abfrage + User-Zählung
  • internal/auth/auth.goLogin 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.goldapStore/tenantStore Felder + Imports ergänzt
  • cmd/archivmail/main.goldapcfg.New() initialisiert; auth.New() mit ldapStore; srv.SetLDAP()
  • src/lib/api.tsLDAPConfig, LDAPTestResult, getLDAPConfig/saveLDAPConfig/deleteLDAPConfig/testLDAPConfig
  • src/app/admin/page.tsx — LDAP-Tab mit Formular, Passwort-Schutz, Gruppen-Mappings, Testergebnis-Card
  • go.modgithub.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)

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:

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=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

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