- handlers/waf.go: Reloader-Func; nach Upsert wird HAProxy async neu gerendert (SPOE-Filter erscheint/verschwindet je nach enabled-Stand) - main.go: haproxyReloader an NewWafHandler übergeben - haproxy.cfg.tpl: cfg-file → config (HAProxy 3.0 kennt cfg-file nicht) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
150 lines
4.3 KiB
Go
150 lines
4.3 KiB
Go
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: "",
|
|
}
|
|
}
|