Git-Repository für bestehenden archivdms-Code initialisiert, Branch-/Commit-Konvention (feature/<ticket>-<slug>-Branches, Ticket-Prefix in Commit-Nachricht) etabliert.
169 lines
5.0 KiB
Go
169 lines
5.0 KiB
Go
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)
|
|
}
|