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.
440 lines
12 KiB
Go
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)
|
|
}
|