Files
patrick 9a24ea29e1 FDN-01: repository & projektgerüst
Git-Repository für bestehenden archivdms-Code initialisiert, Branch-/Commit-Konvention (feature/<ticket>-<slug>-Branches, Ticket-Prefix in Commit-Nachricht) etabliert.
2026-08-11 21:27:53 +02:00

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