package handlers import ( "context" "errors" "log/slog" "net/http" "strconv" "github.com/gin-gonic/gin" "git.netcell-it.de/projekte/edgeguard-native/internal/handlers/response" "git.netcell-it.de/projekte/edgeguard-native/internal/models" "git.netcell-it.de/projekte/edgeguard-native/internal/services/audit" wafsvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/waf" ) // WafHandler exposes the per-domain WAF configuration REST API: // // GET /waf/configs — list all configs (one per domain) // GET /waf/configs/:domain_id — get config for a domain // PUT /waf/configs/:domain_id — upsert config for a domain type WafHandler struct { Repo *wafsvc.Repo Audit *audit.Repo NodeID string Reloader func(ctx context.Context) error } func NewWafHandler(repo *wafsvc.Repo, a *audit.Repo, nodeID string, reloader func(context.Context) error) *WafHandler { return &WafHandler{Repo: repo, Audit: a, NodeID: nodeID, Reloader: reloader} } func (h *WafHandler) Register(rg *gin.RouterGroup) { g := rg.Group("/waf") g.GET("/configs", h.List) g.GET("/configs/:domain_id", h.Get) g.PUT("/configs/:domain_id", h.Upsert) } // List returns all WAF configs. func (h *WafHandler) List(c *gin.Context) { configs, err := h.Repo.List(c.Request.Context()) if err != nil { response.Internal(c, err) return } response.OK(c, gin.H{"configs": configs}) } // Get returns the WAF config for a single domain. // Returns a default (disabled) config when none exists yet. func (h *WafHandler) Get(c *gin.Context) { domainID, err := strconv.ParseInt(c.Param("domain_id"), 10, 64) if err != nil { response.BadRequest(c, errors.New("invalid domain_id")) return } cfg, err := h.Repo.GetByDomain(c.Request.Context(), domainID) if err != nil { if errors.Is(err, wafsvc.ErrNotFound) { // Return a default config so the UI always gets a usable object. response.OK(c, gin.H{"config": defaultConfig(domainID)}) return } response.Internal(c, err) return } response.OK(c, gin.H{"config": cfg}) } // upsertBody is the accepted JSON for PUT /waf/configs/:domain_id. type upsertBody struct { Enabled bool `json:"enabled"` Mode string `json:"mode"` ParanoiaLevel int `json:"paranoia_level"` RuleExclusions []string `json:"rule_exclusions"` TrustedProxies []string `json:"trusted_proxies"` CustomRules string `json:"custom_rules"` } // Upsert creates or updates the WAF config for a domain. func (h *WafHandler) Upsert(c *gin.Context) { domainID, err := strconv.ParseInt(c.Param("domain_id"), 10, 64) if err != nil { response.BadRequest(c, errors.New("invalid domain_id")) return } var body upsertBody if err := c.ShouldBindJSON(&body); err != nil { response.BadRequest(c, err) return } if body.Mode == "" { body.Mode = "detection" } if body.ParanoiaLevel < 1 || body.ParanoiaLevel > 4 { body.ParanoiaLevel = 1 } if body.RuleExclusions == nil { body.RuleExclusions = []string{} } if body.TrustedProxies == nil { body.TrustedProxies = []string{} } cfg := models.WafConfig{ DomainID: domainID, Enabled: body.Enabled, Mode: body.Mode, ParanoiaLevel: body.ParanoiaLevel, RuleExclusions: body.RuleExclusions, TrustedProxies: body.TrustedProxies, CustomRules: body.CustomRules, } result, err := h.Repo.Upsert(c.Request.Context(), cfg) if err != nil { response.Internal(c, err) return } _ = h.Audit.Log(c.Request.Context(), actorOf(c), "waf.config.upsert", strconv.FormatInt(domainID, 10), gin.H{"enabled": body.Enabled, "mode": body.Mode, "paranoia_level": body.ParanoiaLevel}, h.NodeID) // Reload HAProxy so the SPOE filter is added/removed based on // whether any domain now has WAF enabled. if h.Reloader != nil { go func() { if err := h.Reloader(context.Background()); err != nil { slog.Warn("waf: haproxy reload after config change failed", "error", err) } }() } c.JSON(http.StatusOK, gin.H{"config": result}) } // defaultConfig returns a sensible disabled default for a domain // that has no WAF config row yet. func defaultConfig(domainID int64) models.WafConfig { return models.WafConfig{ DomainID: domainID, Enabled: false, Mode: "detection", ParanoiaLevel: 1, RuleExclusions: []string{}, TrustedProxies: []string{}, CustomRules: "", } }