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) }