Files
crowdsec-dashy/internal/handlers/allowlist.go
T
2026-05-19 04:30:14 +00:00

223 lines
5.7 KiB
Go

package handlers
import (
"context"
"fmt"
"net"
"net/http"
"strings"
"time"
"crowdsec-dashy/internal/crowdsec"
)
// AllowlistHandler manages the allowlist page.
type AllowlistHandler struct {
deps Deps
}
func NewAllowlistHandler(deps Deps) *AllowlistHandler {
return &AllowlistHandler{deps: deps}
}
type AllowlistData struct {
PageData
Lists []crowdsec.Allowlist
FetchErr string
}
func (h *AllowlistHandler) List(w http.ResponseWriter, r *http.Request) {
pd := NewPageData(r, "Allowlist", h.deps.CLIAvailable, h.deps.PollInterval)
if f := readFlash(r); f.Message != "" {
pd.Flash = f
}
if !h.deps.CLIAvailable {
h.deps.Renderer.Render(w, "allowlist", AllowlistData{
PageData: pd,
FetchErr: "cscli is not available — allowlist management requires the cscli binary.",
})
return
}
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
defer cancel()
lists, err := h.deps.CLI.ListAllowlists(ctx)
fetchErr := ""
if err != nil {
fetchErr = err.Error()
}
h.deps.Renderer.Render(w, "allowlist", AllowlistData{
PageData: pd,
Lists: lists,
FetchErr: fetchErr,
})
}
// CreateList creates a new named allowlist.
func (h *AllowlistHandler) CreateList(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 1024)
if err := r.ParseForm(); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
if !checkCSRF(r) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
name := strings.TrimSpace(r.FormValue("name"))
if name == "" {
flashRedirect(w, r, "/allowlist", "error", "list name is required")
return
}
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
defer cancel()
if err := h.deps.CLI.CreateAllowlist(ctx, name); err != nil {
flashRedirect(w, r, "/allowlist", "error", "create failed: "+err.Error())
return
}
flashRedirect(w, r, "/allowlist", "success", "Allowlist "+name+" created")
}
// AddEntry adds one or more IPs/CIDRs to an allowlist. Auto-creates the list if missing.
func (h *AllowlistHandler) AddEntry(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 16384)
if err := r.ParseForm(); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
if !checkCSRF(r) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
listName := strings.TrimSpace(r.FormValue("list"))
raw := r.FormValue("value")
if listName == "" {
flashRedirect(w, r, "/allowlist", "error", "list name is required")
return
}
comment := strings.TrimSpace(r.FormValue("comment"))
values, err := parseAllowlistValues(raw)
if err != nil {
flashRedirect(w, r, "/allowlist", "error", err.Error())
return
}
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
// Auto-create the list — CreateAllowlist is a no-op if it already exists.
if err := h.deps.CLI.CreateAllowlist(ctx, listName); err != nil {
flashRedirect(w, r, "/allowlist", "error", "create list failed: "+err.Error())
return
}
if err := h.deps.CLI.AddAllowlistEntries(ctx, listName, comment, values); err != nil {
flashRedirect(w, r, "/allowlist", "error", "add failed: "+err.Error())
return
}
msg := fmt.Sprintf("%d entr%s added to %s", len(values), pluralY(len(values)), listName)
flashRedirect(w, r, "/allowlist", "success", msg)
}
// DeleteList removes an entire allowlist.
func (h *AllowlistHandler) DeleteList(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 1024)
if err := r.ParseForm(); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
if !checkCSRF(r) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
name := strings.TrimSpace(r.FormValue("name"))
if name == "" {
flashRedirect(w, r, "/allowlist", "error", "list name is required")
return
}
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
defer cancel()
if err := h.deps.CLI.DeleteAllowlist(ctx, name); err != nil {
flashRedirect(w, r, "/allowlist", "error", "delete failed: "+err.Error())
return
}
flashRedirect(w, r, "/allowlist", "success", "Allowlist "+name+" deleted")
}
func (h *AllowlistHandler) RemoveEntry(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 4096)
if err := r.ParseForm(); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
if !checkCSRF(r) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
listName := strings.TrimSpace(r.FormValue("list"))
value := strings.TrimSpace(r.FormValue("value"))
if listName == "" || value == "" {
flashRedirect(w, r, "/allowlist", "error", "list name and value are required")
return
}
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
defer cancel()
if err := h.deps.CLI.RemoveAllowlistEntry(ctx, listName, value); err != nil {
flashRedirect(w, r, "/allowlist", "error", "remove failed: "+err.Error())
return
}
flashRedirect(w, r, "/allowlist", "success", value+" removed from "+listName)
}
// parseAllowlistValues splits a raw multi-value string (newline/comma/space delimited)
// and validates each entry as an IP address or CIDR range.
func parseAllowlistValues(raw string) ([]string, error) {
fields := strings.FieldsFunc(raw, func(r rune) bool {
return r == ',' || r == '\n' || r == '\r' || r == ' ' || r == '\t'
})
var out []string
for _, f := range fields {
f = strings.TrimSpace(f)
if f == "" {
continue
}
if net.ParseIP(f) == nil {
if _, _, err := net.ParseCIDR(f); err != nil {
return nil, fmt.Errorf("invalid IP or CIDR: %q", f)
}
}
out = append(out, f)
}
if len(out) == 0 {
return nil, fmt.Errorf("no valid IP addresses or CIDR ranges provided")
}
return out, nil
}
func pluralY(n int) string {
if n == 1 {
return "y"
}
return "ies"
}