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.
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user