Files
archivmail/internal/api/ldap_tenants.go
T
sysops 0fbb1924bb fix(security): W-02 Secure-Cookie-Flag + W-03 TrustedProxies für X-Forwarded-For
W-02: Cookie Secure-Flag ist nun über config.yml steuerbar.
      api.secure_cookies: true/false — default false (kein Breaking Change).
      Alle 3 SetCookie-Aufrufe (Login, Logout, TOTP) nutzen s.cfg.SecureCookies.

W-03: remoteIP() ist jetzt eine Methode und prüft api.trusted_proxies.
      X-Forwarded-For wird nur ausgewertet wenn der direkte Peer in der
      trusted_proxies-Liste steht (IP oder CIDR). Sonst wird r.RemoteAddr
      verwendet — kein Spoofing mehr möglich.
      Neue Hilfsfunktion: isTrustedProxy(ip, proxies).

config.go: APIConfig um SecureCookies bool + TrustedProxies []string erweitert.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 01:10:24 +01:00

729 lines
23 KiB
Go

package api
import (
"encoding/json"
"net/http"
"strconv"
"github.com/archivmail/internal/audit"
"github.com/archivmail/internal/ldapauth"
ldapcfg "github.com/archivmail/internal/ldapconfig"
"github.com/archivmail/internal/tenantstore"
"github.com/archivmail/internal/userstore"
)
// ── Server extension fields and wiring ──────────────────────────────────────
// ldapStore and tenantStore are added to the Server struct via SetLDAP / SetTenants.
// They are declared separately from server.go to keep that file unmodified.
// Access is via the embedded pointer fields on *Server.
// SetLDAP wires the LDAP config store into the API server.
func (s *Server) SetLDAP(store *ldapcfg.Store) {
s.ldapStore = store
// Register LDAP routes only after the store is available.
s.mux.HandleFunc("GET /api/admin/ldap", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleGetLDAP)))
s.mux.HandleFunc("PUT /api/admin/ldap", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleSaveLDAP)))
s.mux.HandleFunc("DELETE /api/admin/ldap", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleDeleteLDAP)))
s.mux.HandleFunc("POST /api/admin/ldap/test", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleTestLDAP)))
}
// SetTenants wires the tenant store into the API server.
func (s *Server) SetTenants(store *tenantstore.Store) {
s.tenantStore = store
// Register tenant routes only after the store is available.
s.mux.HandleFunc("GET /api/tenants", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleListTenants)))
s.mux.HandleFunc("POST /api/tenants", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleCreateTenant)))
s.mux.HandleFunc("GET /api/tenants/{id}", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleGetTenant)))
s.mux.HandleFunc("PATCH /api/tenants/{id}", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleUpdateTenant)))
s.mux.HandleFunc("DELETE /api/tenants/{id}", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleDeleteTenant)))
s.mux.HandleFunc("GET /api/tenants/{id}/domains", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleListTenantDomains)))
s.mux.HandleFunc("POST /api/tenants/{id}/domains", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleAddTenantDomain)))
s.mux.HandleFunc("DELETE /api/tenants/{id}/domains/{did}", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleRemoveTenantDomain)))
}
// ── 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,
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,
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,
IPAddress: s.remoteIP(r),
Success: result.OK,
Detail: result.Message,
})
writeJSON(w, http.StatusOK, result)
}
// ── Tenant handlers ──────────────────────────────────────────────────────────
func (s *Server) handleListTenants(w http.ResponseWriter, r *http.Request) {
if s.tenantStore == nil {
writeError(w, http.StatusServiceUnavailable, "tenant store not available")
return
}
tenants, err := s.tenantStore.List(r.Context())
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to list tenants")
return
}
writeJSON(w, http.StatusOK, tenants)
}
func (s *Server) handleCreateTenant(w http.ResponseWriter, r *http.Request) {
if s.tenantStore == nil {
writeError(w, http.StatusServiceUnavailable, "tenant store not available")
return
}
var req struct {
Name string `json:"name"`
Slug string `json:"slug"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if req.Name == "" || req.Slug == "" {
writeError(w, http.StatusBadRequest, "name and slug are required")
return
}
tenant, err := s.tenantStore.Create(r.Context(), req.Name, req.Slug)
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to create tenant")
return
}
sess := sessionFromCtx(r.Context())
s.audlog.Log(audit.Entry{
EventType: "tenant_created",
Username: sess.Username,
IPAddress: s.remoteIP(r),
Success: true,
Detail: "Mandant erstellt: " + req.Name,
})
writeJSON(w, http.StatusCreated, tenant)
}
func (s *Server) handleGetTenant(w http.ResponseWriter, r *http.Request) {
if s.tenantStore == nil {
writeError(w, http.StatusServiceUnavailable, "tenant store not available")
return
}
id, err := parseTenantID(r)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid tenant id")
return
}
tenant, err := s.tenantStore.Get(r.Context(), id)
if err != nil {
writeError(w, http.StatusNotFound, "tenant not found")
return
}
writeJSON(w, http.StatusOK, tenant)
}
func (s *Server) handleUpdateTenant(w http.ResponseWriter, r *http.Request) {
if s.tenantStore == nil {
writeError(w, http.StatusServiceUnavailable, "tenant store not available")
return
}
id, err := parseTenantID(r)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid tenant id")
return
}
var req struct {
Name string `json:"name"`
Active *bool `json:"active"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
// Load existing to keep unset fields
existing, err := s.tenantStore.Get(r.Context(), id)
if err != nil {
writeError(w, http.StatusNotFound, "tenant not found")
return
}
name := existing.Name
active := existing.Active
if req.Name != "" {
name = req.Name
}
if req.Active != nil {
active = *req.Active
}
tenant, err := s.tenantStore.Update(r.Context(), id, name, active)
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to update tenant")
return
}
writeJSON(w, http.StatusOK, tenant)
}
func (s *Server) handleDeleteTenant(w http.ResponseWriter, r *http.Request) {
if s.tenantStore == nil {
writeError(w, http.StatusServiceUnavailable, "tenant store not available")
return
}
id, err := parseTenantID(r)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid tenant id")
return
}
sess := sessionFromCtx(r.Context())
if err := s.tenantStore.Delete(r.Context(), id); err != nil {
writeError(w, http.StatusInternalServerError, "failed to delete tenant")
return
}
s.audlog.Log(audit.Entry{
EventType: "tenant_deleted",
Username: sess.Username,
IPAddress: s.remoteIP(r),
Success: true,
Detail: "Mandant gelöscht: " + strconv.FormatInt(id, 10),
})
w.WriteHeader(http.StatusNoContent)
}
func (s *Server) handleListTenantDomains(w http.ResponseWriter, r *http.Request) {
if s.tenantStore == nil {
writeError(w, http.StatusServiceUnavailable, "tenant store not available")
return
}
id, err := parseTenantID(r)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid tenant id")
return
}
domains, err := s.tenantStore.ListDomains(r.Context(), id)
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to list domains")
return
}
writeJSON(w, http.StatusOK, domains)
}
func (s *Server) handleAddTenantDomain(w http.ResponseWriter, r *http.Request) {
if s.tenantStore == nil {
writeError(w, http.StatusServiceUnavailable, "tenant store not available")
return
}
id, err := parseTenantID(r)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid tenant id")
return
}
var req struct {
Domain string `json:"domain"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Domain == "" {
writeError(w, http.StatusBadRequest, "domain is required")
return
}
domain, err := s.tenantStore.AddDomain(r.Context(), id, req.Domain)
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to add domain")
return
}
writeJSON(w, http.StatusCreated, domain)
}
func (s *Server) handleRemoveTenantDomain(w http.ResponseWriter, r *http.Request) {
if s.tenantStore == nil {
writeError(w, http.StatusServiceUnavailable, "tenant store not available")
return
}
tenantID, err := parseTenantID(r)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid tenant id")
return
}
didStr := r.PathValue("did")
domainID, err := strconv.ParseInt(didStr, 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid domain id")
return
}
if err := s.tenantStore.RemoveDomain(r.Context(), tenantID, domainID); err != nil {
writeError(w, http.StatusInternalServerError, "failed to remove domain")
return
}
w.WriteHeader(http.StatusNoContent)
}
// ── PROJ-23: Per-Tenant LDAP handlers (Phase B) ─────────────────────────────
// SetTenantLDAP wires the per-tenant LDAP config store into the API server and
// registers the tenant LDAP routes.
func (s *Server) SetTenantLDAP(store *ldapcfg.TenantStore) {
s.tenantLdapStore = store
// domain_admin routes — tenant_id comes from JWT session, NOT from URL
s.mux.HandleFunc("GET /api/tenant/ldap", s.authAdmin(s.handleGetTenantLDAP))
s.mux.HandleFunc("PUT /api/tenant/ldap", s.authAdmin(s.handleSaveTenantLDAP))
s.mux.HandleFunc("DELETE /api/tenant/ldap", s.authAdmin(s.handleDeleteTenantLDAP))
s.mux.HandleFunc("POST /api/tenant/ldap/test", s.authAdmin(s.handleTestTenantLDAP))
// superadmin routes — tenant_id from URL parameter
s.mux.HandleFunc("GET /api/admin/tenants/{id}/ldap", s.authMiddleware(s.requireRole(userstore.RoleSuperAdmin, s.handleAdminGetTenantLDAP)))
s.mux.HandleFunc("PUT /api/admin/tenants/{id}/ldap", s.authMiddleware(s.requireRole(userstore.RoleSuperAdmin, s.handleAdminSaveTenantLDAP)))
s.mux.HandleFunc("DELETE /api/admin/tenants/{id}/ldap", s.authMiddleware(s.requireRole(userstore.RoleSuperAdmin, s.handleAdminDeleteTenantLDAP)))
s.mux.HandleFunc("POST /api/admin/tenants/{id}/ldap/test", s.authMiddleware(s.requireRole(userstore.RoleSuperAdmin, s.handleAdminTestTenantLDAP)))
}
// ── 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,
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,
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,
IPAddress: s.remoteIP(r),
Success: result.OK,
Detail: result.Message,
})
writeJSON(w, http.StatusOK, result)
}
// ── 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,
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,
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,
IPAddress: s.remoteIP(r),
Success: result.OK,
Detail: result.Message + " (tenant " + strconv.FormatInt(id, 10) + ")",
})
writeJSON(w, http.StatusOK, result)
}
// buildTenantTestConfig constructs an ldapauth.Config for testing, either from
// the saved tenant config or from the provided request body.
func (s *Server) buildTenantTestConfig(r *http.Request, useSaved bool, tenantID int64, provided ldapcfg.TenantLDAPConfig) *ldapauth.Config {
if useSaved {
saved, err := s.tenantLdapStore.GetWithPassword(r.Context(), tenantID)
if err != nil || saved == nil {
return nil
}
return &ldapauth.Config{
URL: saved.URL,
BindDN: saved.BindDN,
BindPassword: saved.BindPassword,
BaseDN: saved.BaseDN,
UserFilter: saved.UserFilter,
TLS: saved.TLS,
TLSSkipVerify: saved.TLSSkipVerify,
}
}
return &ldapauth.Config{
URL: provided.URL,
BindDN: provided.BindDN,
BindPassword: provided.BindPassword,
BaseDN: provided.BaseDN,
UserFilter: provided.UserFilter,
TLS: provided.TLS,
TLSSkipVerify: provided.TLSSkipVerify,
}
}
// ── helpers ──────────────────────────────────────────────────────────────────
func parseTenantID(r *http.Request) (int64, error) {
return strconv.ParseInt(r.PathValue("id"), 10, 64)
}