Files
sysops 1d27dc2d8b refactor: große API-Handler-Dateien in fokussierte Module aufteilen
Reine Code-Verschiebung, keine Logikänderung. Betroffen:
- ldap_tenants.go (994 Zeilen) -> ldap_tenants.go (Routing) + ldap_handlers.go
  + tenant_handlers.go + tenant_domain_handlers.go + tenant_logo_handlers.go
  + tenant_helpers.go
- import_handlers.go (621 Zeilen) -> imap_handlers.go + pop3_handlers.go
  + import_helpers.go
- admin_handlers.go (702 Zeilen) -> admin_users_handlers.go +
  admin_status_handlers.go + admin_services_handlers.go +
  admin_security_handlers.go

Lokal kein Go-Build möglich — Verifikation manuell per Funktions- und
Import-Abgleich Alt/Neu (alle Symbole exakt einmal vorhanden). Build
muss vor Deploy auf 192.168.1.131/132 bestätigt werden.
2026-06-21 23:38:57 +02:00

440 lines
12 KiB
Go

package api
import (
"encoding/json"
"net/http"
"strconv"
"archivmail/internal/audit"
"archivmail/internal/ldapauth"
ldapcfg "archivmail/internal/ldapconfig"
)
// ── LDAP handlers ────────────────────────────────────────────────────────────
func (s *Server) handleGetLDAP(w http.ResponseWriter, r *http.Request) {
if s.ldapStore == nil {
writeError(w, http.StatusServiceUnavailable, "ldap store not available")
return
}
cfg, err := s.ldapStore.Get(r.Context())
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to load ldap config")
return
}
if cfg == nil {
writeError(w, http.StatusNotFound, "no ldap config")
return
}
writeJSON(w, http.StatusOK, cfg)
}
func (s *Server) handleSaveLDAP(w http.ResponseWriter, r *http.Request) {
if s.ldapStore == nil {
writeError(w, http.StatusServiceUnavailable, "ldap store not available")
return
}
var cfg ldapcfg.LDAPConfig
if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
sess := sessionFromCtx(r.Context())
if err := s.ldapStore.Save(r.Context(), cfg, sess.Username); err != nil {
writeError(w, http.StatusInternalServerError, "failed to save ldap config")
return
}
s.audlog.Log(audit.Entry{
EventType: "ldap_config_saved",
Username: sess.Username,
TenantID: sess.TenantID,
IPAddress: s.remoteIP(r),
Success: true,
Detail: "LDAP-Konfiguration gespeichert",
})
// Return the saved config (with masked password)
saved, err := s.ldapStore.Get(r.Context())
if err != nil || saved == nil {
writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
return
}
writeJSON(w, http.StatusOK, saved)
}
func (s *Server) handleDeleteLDAP(w http.ResponseWriter, r *http.Request) {
if s.ldapStore == nil {
writeError(w, http.StatusServiceUnavailable, "ldap store not available")
return
}
if err := s.ldapStore.Delete(r.Context()); err != nil {
writeError(w, http.StatusInternalServerError, "failed to delete ldap config")
return
}
sess := sessionFromCtx(r.Context())
s.audlog.Log(audit.Entry{
EventType: "ldap_config_deleted",
Username: sess.Username,
TenantID: sess.TenantID,
IPAddress: s.remoteIP(r),
Success: true,
Detail: "LDAP-Konfiguration gelöscht",
})
w.WriteHeader(http.StatusNoContent)
}
func (s *Server) handleTestLDAP(w http.ResponseWriter, r *http.Request) {
if s.ldapStore == nil {
writeError(w, http.StatusServiceUnavailable, "ldap store not available")
return
}
var body struct {
UseSaved bool `json:"use_saved"`
ldapcfg.LDAPConfig
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
var testCfg ldapauth.Config
if body.UseSaved {
saved, err := s.ldapStore.GetWithPassword(r.Context())
if err != nil || saved == nil {
writeError(w, http.StatusNotFound, "no ldap config saved")
return
}
testCfg = ldapauth.Config{
URL: saved.URL,
BindDN: saved.BindDN,
BindPassword: saved.BindPassword,
BaseDN: saved.BaseDN,
UserFilter: saved.UserFilter,
TLS: saved.TLS,
TLSSkipVerify: saved.TLSSkipVerify,
}
} else {
testCfg = ldapauth.Config{
URL: body.URL,
BindDN: body.BindDN,
BindPassword: body.BindPassword,
BaseDN: body.BaseDN,
UserFilter: body.UserFilter,
TLS: body.TLS,
TLSSkipVerify: body.TLSSkipVerify,
}
}
result := ldapauth.TestConnection(testCfg)
sess := sessionFromCtx(r.Context())
s.audlog.Log(audit.Entry{
EventType: "ldap_connection_test",
Username: sess.Username,
TenantID: sess.TenantID,
IPAddress: s.remoteIP(r),
Success: result.OK,
Detail: result.Message,
})
writeJSON(w, http.StatusOK, result)
}
// ── PROJ-23: Per-Tenant LDAP — domain_admin handlers (own tenant) ────────────
func (s *Server) handleGetTenantLDAP(w http.ResponseWriter, r *http.Request) {
if s.tenantLdapStore == nil {
writeError(w, http.StatusServiceUnavailable, "tenant ldap store not available")
return
}
sess := sessionFromCtx(r.Context())
if sess.TenantID == nil {
writeError(w, http.StatusBadRequest, "no tenant context")
return
}
cfg, err := s.tenantLdapStore.Get(r.Context(), *sess.TenantID)
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to load tenant ldap config")
return
}
if cfg == nil {
writeError(w, http.StatusNotFound, "no tenant ldap config")
return
}
writeJSON(w, http.StatusOK, cfg)
}
func (s *Server) handleSaveTenantLDAP(w http.ResponseWriter, r *http.Request) {
if s.tenantLdapStore == nil {
writeError(w, http.StatusServiceUnavailable, "tenant ldap store not available")
return
}
sess := sessionFromCtx(r.Context())
if sess.TenantID == nil {
writeError(w, http.StatusBadRequest, "no tenant context")
return
}
var cfg ldapcfg.TenantLDAPConfig
if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
cfg.TenantID = *sess.TenantID
// BUG-1 fix: domain_admin may only assign user/auditor roles — prevent privilege escalation
// via LDAP default_role or group_mappings even when bypassing the frontend.
allowedForTenantAdmin := map[string]bool{"user": true, "auditor": true}
if cfg.DefaultRole != "" && !allowedForTenantAdmin[cfg.DefaultRole] {
writeError(w, http.StatusForbidden, "role not allowed for tenant LDAP config")
return
}
for _, gm := range cfg.GroupMappings {
if !allowedForTenantAdmin[gm.Role] {
writeError(w, http.StatusForbidden, "group mapping role not allowed")
return
}
}
if err := s.tenantLdapStore.Save(r.Context(), cfg, sess.Username); err != nil {
writeError(w, http.StatusInternalServerError, "failed to save tenant ldap config")
return
}
s.audlog.Log(audit.Entry{
EventType: "tenant_ldap_config_saved",
Username: sess.Username,
TenantID: sess.TenantID,
IPAddress: s.remoteIP(r),
Success: true,
Detail: "Mandant-LDAP-Konfiguration gespeichert",
})
saved, err := s.tenantLdapStore.Get(r.Context(), *sess.TenantID)
if err != nil || saved == nil {
writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
return
}
writeJSON(w, http.StatusOK, saved)
}
func (s *Server) handleDeleteTenantLDAP(w http.ResponseWriter, r *http.Request) {
if s.tenantLdapStore == nil {
writeError(w, http.StatusServiceUnavailable, "tenant ldap store not available")
return
}
sess := sessionFromCtx(r.Context())
if sess.TenantID == nil {
writeError(w, http.StatusBadRequest, "no tenant context")
return
}
if err := s.tenantLdapStore.Delete(r.Context(), *sess.TenantID); err != nil {
writeError(w, http.StatusInternalServerError, "failed to delete tenant ldap config")
return
}
s.audlog.Log(audit.Entry{
EventType: "tenant_ldap_config_deleted",
Username: sess.Username,
TenantID: sess.TenantID,
IPAddress: s.remoteIP(r),
Success: true,
Detail: "Mandant-LDAP-Konfiguration gelöscht",
})
w.WriteHeader(http.StatusNoContent)
}
func (s *Server) handleTestTenantLDAP(w http.ResponseWriter, r *http.Request) {
if s.tenantLdapStore == nil {
writeError(w, http.StatusServiceUnavailable, "tenant ldap store not available")
return
}
sess := sessionFromCtx(r.Context())
if sess.TenantID == nil {
writeError(w, http.StatusBadRequest, "no tenant context")
return
}
var body struct {
UseSaved bool `json:"use_saved"`
ldapcfg.TenantLDAPConfig
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
testCfg := s.buildTenantTestConfig(r, body.UseSaved, *sess.TenantID, body.TenantLDAPConfig)
if testCfg == nil {
writeError(w, http.StatusNotFound, "no tenant ldap config saved")
return
}
result := ldapauth.TestConnection(*testCfg)
s.audlog.Log(audit.Entry{
EventType: "tenant_ldap_connection_test",
Username: sess.Username,
TenantID: sess.TenantID,
IPAddress: s.remoteIP(r),
Success: result.OK,
Detail: result.Message,
})
writeJSON(w, http.StatusOK, result)
}
// ── PROJ-23: Per-Tenant LDAP — superadmin handlers (arbitrary tenant) ────────
func (s *Server) handleAdminGetTenantLDAP(w http.ResponseWriter, r *http.Request) {
if s.tenantLdapStore == nil {
writeError(w, http.StatusServiceUnavailable, "tenant ldap store not available")
return
}
id, err := parseTenantID(r)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid tenant id")
return
}
cfg, err := s.tenantLdapStore.Get(r.Context(), id)
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to load tenant ldap config")
return
}
if cfg == nil {
writeError(w, http.StatusNotFound, "no tenant ldap config")
return
}
writeJSON(w, http.StatusOK, cfg)
}
func (s *Server) handleAdminSaveTenantLDAP(w http.ResponseWriter, r *http.Request) {
if s.tenantLdapStore == nil {
writeError(w, http.StatusServiceUnavailable, "tenant ldap store not available")
return
}
id, err := parseTenantID(r)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid tenant id")
return
}
var cfg ldapcfg.TenantLDAPConfig
if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
cfg.TenantID = id
// superadmin may assign up to domain_admin in group mappings — not superadmin itself.
allowedForSuperAdmin := map[string]bool{"user": true, "auditor": true, "domain_admin": true}
if cfg.DefaultRole != "" && !allowedForSuperAdmin[cfg.DefaultRole] {
writeError(w, http.StatusForbidden, "role not allowed for tenant LDAP config")
return
}
for _, gm := range cfg.GroupMappings {
if !allowedForSuperAdmin[gm.Role] {
writeError(w, http.StatusForbidden, "group mapping role not allowed")
return
}
}
sess := sessionFromCtx(r.Context())
if err := s.tenantLdapStore.Save(r.Context(), cfg, sess.Username); err != nil {
writeError(w, http.StatusInternalServerError, "failed to save tenant ldap config")
return
}
s.audlog.Log(audit.Entry{
EventType: "tenant_ldap_config_saved",
Username: sess.Username,
TenantID: sess.TenantID,
IPAddress: s.remoteIP(r),
Success: true,
Detail: "Mandant-LDAP-Konfiguration gespeichert (tenant " + strconv.FormatInt(id, 10) + ")",
})
saved, err := s.tenantLdapStore.Get(r.Context(), id)
if err != nil || saved == nil {
writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
return
}
writeJSON(w, http.StatusOK, saved)
}
func (s *Server) handleAdminDeleteTenantLDAP(w http.ResponseWriter, r *http.Request) {
if s.tenantLdapStore == nil {
writeError(w, http.StatusServiceUnavailable, "tenant ldap store not available")
return
}
id, err := parseTenantID(r)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid tenant id")
return
}
if err := s.tenantLdapStore.Delete(r.Context(), id); err != nil {
writeError(w, http.StatusInternalServerError, "failed to delete tenant ldap config")
return
}
sess := sessionFromCtx(r.Context())
s.audlog.Log(audit.Entry{
EventType: "tenant_ldap_config_deleted",
Username: sess.Username,
TenantID: sess.TenantID,
IPAddress: s.remoteIP(r),
Success: true,
Detail: "Mandant-LDAP-Konfiguration gelöscht (tenant " + strconv.FormatInt(id, 10) + ")",
})
w.WriteHeader(http.StatusNoContent)
}
func (s *Server) handleAdminTestTenantLDAP(w http.ResponseWriter, r *http.Request) {
if s.tenantLdapStore == nil {
writeError(w, http.StatusServiceUnavailable, "tenant ldap store not available")
return
}
id, err := parseTenantID(r)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid tenant id")
return
}
var body struct {
UseSaved bool `json:"use_saved"`
ldapcfg.TenantLDAPConfig
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
testCfg := s.buildTenantTestConfig(r, body.UseSaved, id, body.TenantLDAPConfig)
if testCfg == nil {
writeError(w, http.StatusNotFound, "no tenant ldap config saved")
return
}
result := ldapauth.TestConnection(*testCfg)
sess := sessionFromCtx(r.Context())
s.audlog.Log(audit.Entry{
EventType: "tenant_ldap_connection_test",
Username: sess.Username,
TenantID: sess.TenantID,
IPAddress: s.remoteIP(r),
Success: result.OK,
Detail: result.Message + " (tenant " + strconv.FormatInt(id, 10) + ")",
})
writeJSON(w, http.StatusOK, result)
}