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.
134 lines
4.1 KiB
Go
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)
|
|
}
|