787db6638f
- BUG-1 (P0): domain_admin kann keine Rollen > auditor in default_role/ group_mappings setzen — serverseitige Allowlist-Prüfung in handleSaveTenantLDAP (user/auditor) und handleAdminSaveTenantLDAP (user/auditor/domain_admin) - WARN-1: Login-Fallback-Reihenfolge korrigiert — tenant_ldap wird jetzt VOR globalem ldap_config geprüft (Spec: tenant > global > local) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
729 lines
23 KiB
Go
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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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)
|
|
}
|
|
|