package api import ( "encoding/json" "errors" "net/http" "strconv" "archivdms/internal/audit" "archivdms/internal/ldapstore" "archivdms/internal/userstore" ) // resolveLDAPTenant determines which tenant's LDAP config the request targets. // domain_admin is pinned to its own session tenant. superadmin (which has no // session tenant) must pass ?tenant_id=; may also override to inspect any // tenant. Returns the tenant ID and false when the request is not authorised // or the tenant cannot be determined (the caller has already written nothing). func (s *Server) resolveLDAPTenant(w http.ResponseWriter, r *http.Request) (int64, bool) { sess := sessionFromCtx(r.Context()) // superadmin may target any tenant via ?tenant_id=. if sess.Role == userstore.RoleSuperAdmin { q := r.URL.Query().Get("tenant_id") if q == "" { writeError(w, http.StatusBadRequest, "tenant_id query parameter required for superadmin") return 0, false } tid, err := strconv.ParseInt(q, 10, 64) if err != nil || tid <= 0 { writeError(w, http.StatusBadRequest, "invalid tenant_id") return 0, false } return tid, true } // domain_admin: always scoped to its own tenant (IDOR-safe: query params // are ignored, the tenant comes from the signed session). if sess.TenantID == nil { writeError(w, http.StatusForbidden, "no tenant context") return 0, false } return *sess.TenantID, true } // handleGetLDAPConfig returns the tenant's LDAP config WITHOUT the bind // password (only bind_password_set). Returns 404 when none is configured. func (s *Server) handleGetLDAPConfig(w http.ResponseWriter, r *http.Request) { if s.ldapStore == nil { writeError(w, http.StatusServiceUnavailable, "ldap not configured on this server") return } tenantID, ok := s.resolveLDAPTenant(w, r) if !ok { return } cfg, err := s.ldapStore.Get(r.Context(), tenantID) if errors.Is(err, ldapstore.ErrNotFound) { writeError(w, http.StatusNotFound, "no ldap config for this tenant") return } if err != nil { writeError(w, http.StatusInternalServerError, "load ldap config failed") return } writeJSON(w, http.StatusOK, cfg) } type upsertLDAPConfigRequest struct { Enabled bool `json:"enabled"` Host string `json:"host"` Port int `json:"port"` UseTLS string `json:"use_tls"` BindDN string `json:"bind_dn"` BindPassword *string `json:"bind_password"` // nil = keep existing BaseDN string `json:"base_dn"` UserFilter string `json:"user_filter"` AttrUsername string `json:"attr_username"` AttrEmail string `json:"attr_email"` AttrName string `json:"attr_name"` GroupBaseDN string `json:"group_base_dn"` GroupFilter string `json:"group_filter"` AdminGroupDN string `json:"admin_group_dn"` } // handleUpsertLDAPConfig creates or updates the tenant's LDAP config. The bind // password is optional (omit to keep the stored one); on creation it is // mandatory. Every attempt — success or failure — is audit-logged. func (s *Server) handleUpsertLDAPConfig(w http.ResponseWriter, r *http.Request) { sess := sessionFromCtx(r.Context()) if s.ldapStore == nil { writeError(w, http.StatusServiceUnavailable, "ldap not configured on this server") return } tenantID, ok := s.resolveLDAPTenant(w, r) if !ok { return } logFail := func(detail string) { tid := tenantID s.audlog.Log(audit.Entry{ EventType: audit.EventLdapConfigChanged, Username: sess.Username, TenantID: &tid, Success: false, Detail: detail, }) } var req upsertLDAPConfigRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { logFail("invalid_body") writeError(w, http.StatusBadRequest, "invalid request body") return } if req.Host == "" || req.BindDN == "" || req.BaseDN == "" { logFail("missing_fields") writeError(w, http.StatusBadRequest, "host, bind_dn and base_dn are required") return } // Defaults mirroring the schema so a minimal request still yields a // working config. if req.UserFilter == "" { req.UserFilter = "(uid=%s)" } if req.AttrUsername == "" { req.AttrUsername = "uid" } if req.AttrEmail == "" { req.AttrEmail = "mail" } if req.AttrName == "" { req.AttrName = "cn" } cfg := ldapstore.Config{ TenantID: tenantID, Enabled: req.Enabled, Host: req.Host, Port: req.Port, UseTLS: req.UseTLS, BindDN: req.BindDN, BaseDN: req.BaseDN, UserFilter: req.UserFilter, AttrUsername: req.AttrUsername, AttrEmail: req.AttrEmail, AttrName: req.AttrName, GroupBaseDN: req.GroupBaseDN, GroupFilter: req.GroupFilter, AdminGroupDN: req.AdminGroupDN, } saved, err := s.ldapStore.Upsert(r.Context(), cfg, req.BindPassword) if err != nil { logFail("upsert_failed") writeError(w, http.StatusBadRequest, "save ldap config failed: "+err.Error()) return } tid := tenantID s.audlog.Log(audit.Entry{ EventType: audit.EventLdapConfigChanged, Username: sess.Username, TenantID: &tid, Success: true, Detail: "ldap_config_saved host:" + saved.Host, }) writeJSON(w, http.StatusOK, saved) }