Files
vibeproxy/internal/web/accesslists.go
T
owen db91c24d7f Default new access lists to allow-by-default
Basic-Auth-only access lists (no CIDR rules) were unreachable: with
Default allow off, checkAccess denied every IP with 403 before the
auth prompt ever ran. New lists now default to allow-by-default, and
the label clarifies the interaction with Basic Auth.
2026-08-17 11:35:16 -05:00

134 lines
4.1 KiB
Go

package web
import (
"fmt"
"net/http"
"strconv"
"github.com/owen/vibeproxy/internal/auth"
"github.com/owen/vibeproxy/internal/database"
"github.com/owen/vibeproxy/internal/logging"
"github.com/owen/vibeproxy/internal/models"
)
func (h *Web) handleAccessListsList(w http.ResponseWriter, r *http.Request) {
lists, err := database.ListAccessLists(h.db)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
render(w, "access_lists", PageData{
Active: "access_lists", Title: "Access Lists", CSRFToken: h.csrf.Token(w),
Flash: r.URL.Query().Get("flash"), FlashKind: r.URL.Query().Get("kind"),
Data: lists,
})
}
func (h *Web) handleAccessListForm(w http.ResponseWriter, r *http.Request) {
l := models.AccessList{DefaultAllow: true}
isNew := true
if idStr := r.PathValue("id"); idStr != "" {
isNew = false
id, _ := strconv.ParseInt(idStr, 10, 64)
existing, err := database.GetAccessList(h.db, id)
if err != nil {
http.Error(w, "access list not found", http.StatusNotFound)
return
}
l = *existing
}
render(w, "access_list_form", PageData{
Active: "access_lists", Title: pageTitle(isNew, "Access List"), CSRFToken: h.csrf.Token(w),
Data: map[string]any{"List": l, "IsNew": isNew},
})
}
func (h *Web) handleAccessListCreate(w http.ResponseWriter, r *http.Request) {
h.saveAccessListForm(w, r, 0)
}
func (h *Web) handleAccessListUpdate(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.ParseInt(r.PathValue("id"), 10, 64)
h.saveAccessListForm(w, r, id)
}
func (h *Web) saveAccessListForm(w http.ResponseWriter, r *http.Request, id int64) {
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
l := models.AccessList{
ID: id,
Name: r.FormValue("name"),
DefaultAllow: r.FormValue("default_allow") == "on",
Comments: r.FormValue("comments"),
}
actions := r.Form["rule_action"]
cidrs := r.Form["rule_cidr"]
for i := range cidrs {
if cidrs[i] == "" {
continue
}
l.Rules = append(l.Rules, models.AccessRule{
Action: models.AccessRuleAction(get(actions, i)),
CIDR: cidrs[i],
Priority: i,
})
}
existingHashes := map[string]string{}
if id != 0 {
if existing, err := database.GetAccessList(h.db, id); err == nil {
for _, u := range existing.BasicAuthUsers {
existingHashes[u.Username] = u.PasswordHash
}
}
}
usernames := r.Form["auth_username"]
passwords := r.Form["auth_password"]
for i := range usernames {
if usernames[i] == "" {
continue
}
hash := ""
if passwords[i] != "" {
h2, err := auth.HashPassword(passwords[i])
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
hash = h2
} else if existing, ok := existingHashes[usernames[i]]; ok {
hash = existing // password left blank on edit: keep the existing hash
} else {
continue // new user with no password set — skip
}
l.BasicAuthUsers = append(l.BasicAuthUsers, models.AccessListUser{Username: usernames[i], PasswordHash: hash})
}
if l.Name == "" {
render(w, "access_list_form", PageData{
Active: "access_lists", Title: pageTitle(id == 0, "Access List"), CSRFToken: h.csrf.Token(w),
Flash: "Name is required", FlashKind: "danger",
Data: map[string]any{"List": l, "IsNew": id == 0},
})
return
}
if _, err := database.SaveAccessList(h.db, &l); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
h.reload(r)
logging.Audit(h.db, auth.ActorFromContext(r.Context()), "access_list_changed", l.Name, "")
http.Redirect(w, r, "/access-lists?flash="+flashMsg("Access list saved")+"&kind=success", http.StatusFound)
}
func (h *Web) handleAccessListDelete(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err := database.DeleteAccessList(h.db, id); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
h.reload(r)
logging.Audit(h.db, auth.ActorFromContext(r.Context()), "access_list_changed", fmt.Sprintf("id=%d", id), "deleted")
http.Redirect(w, r, "/access-lists?flash="+flashMsg("Access list deleted")+"&kind=success", http.StatusFound)
}