access-list: Hash basic-auth passwords and redact API reads #18
+64
-4
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
@@ -107,7 +108,7 @@ func mountAPI(r chi.Router, st store.Store, eng *proxy.Engine, cm *certificate.M
|
||||
}
|
||||
_ = claims
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(st.GetAccessLists())
|
||||
_ = json.NewEncoder(w).Encode(redactAccessLists(st.GetAccessLists()))
|
||||
})
|
||||
r.Post("/access-lists", func(w http.ResponseWriter, r *http.Request) {
|
||||
claims, ok := auth.RequireAuth(w, r)
|
||||
@@ -122,13 +123,17 @@ func mountAPI(r chi.Router, st store.Store, eng *proxy.Engine, cm *certificate.M
|
||||
if payload.OwnerUserID == 0 {
|
||||
payload.OwnerUserID = claims.UserID
|
||||
}
|
||||
if err := hashAccessListPasswords(&payload); err != nil {
|
||||
http.Error(w, "password hash failed", 500)
|
||||
return
|
||||
}
|
||||
created, err := st.CreateAccessList(payload)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(created)
|
||||
_ = json.NewEncoder(w).Encode(redactAccessList(created))
|
||||
eng.ReloadFromStore()
|
||||
_, _ = st.AddAuditLog(store.AuditLog{
|
||||
UserID: claims.UserID,
|
||||
@@ -153,7 +158,7 @@ func mountAPI(r chi.Router, st store.Store, eng *proxy.Engine, cm *certificate.M
|
||||
return
|
||||
}
|
||||
if al, ok := st.GetAccessList(id); ok {
|
||||
_ = json.NewEncoder(w).Encode(al)
|
||||
_ = json.NewEncoder(w).Encode(redactAccessList(al))
|
||||
return
|
||||
}
|
||||
http.Error(w, "not found", 404)
|
||||
@@ -187,13 +192,29 @@ func mountAPI(r chi.Router, st store.Store, eng *proxy.Engine, cm *certificate.M
|
||||
payload.OwnerUserID = existing.OwnerUserID
|
||||
// preserve CreatedOn on update (server-set on create; do not lose on edit)
|
||||
payload.CreatedOn = existing.CreatedOn
|
||||
// Empty password on an item means "keep existing" when username matches.
|
||||
for i := range payload.Items {
|
||||
if payload.Items[i].Password != "" {
|
||||
continue
|
||||
}
|
||||
for _, prev := range existing.Items {
|
||||
if prev.Username == payload.Items[i].Username && prev.Password != "" {
|
||||
payload.Items[i].Password = prev.Password
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := hashAccessListPasswords(&payload); err != nil {
|
||||
http.Error(w, "password hash failed", 500)
|
||||
return
|
||||
}
|
||||
updated, err := st.UpdateAccessList(payload)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
eng.ReloadFromStore()
|
||||
_ = json.NewEncoder(w).Encode(updated)
|
||||
_ = json.NewEncoder(w).Encode(redactAccessList(updated))
|
||||
_, _ = st.AddAuditLog(store.AuditLog{
|
||||
UserID: claims.UserID,
|
||||
ObjectType: "access-list",
|
||||
@@ -1281,3 +1302,42 @@ func mountAPI(r chi.Router, st store.Store, eng *proxy.Engine, cm *certificate.M
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
func hashAccessListPasswords(al *store.AccessList) error {
|
||||
for i := range al.Items {
|
||||
pw := al.Items[i].Password
|
||||
if pw == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(pw, "$2a$") || strings.HasPrefix(pw, "$2b$") || strings.HasPrefix(pw, "$2y$") {
|
||||
continue
|
||||
}
|
||||
h, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
al.Items[i].Password = string(h)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func redactAccessList(al store.AccessList) store.AccessList {
|
||||
out := al
|
||||
out.Items = make([]store.AccessItem, len(al.Items))
|
||||
copy(out.Items, al.Items)
|
||||
for i := range out.Items {
|
||||
if out.Items[i].Password != "" {
|
||||
out.Items[i].Password = ""
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func redactAccessLists(list []store.AccessList) []store.AccessList {
|
||||
out := make([]store.AccessList, len(list))
|
||||
for i, al := range list {
|
||||
out[i] = redactAccessList(al)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
@@ -349,7 +350,7 @@ func (e *Engine) Handler() http.Handler {
|
||||
user, pass := creds[0], creds[1]
|
||||
authed := false
|
||||
for _, item := range hcfg.AccessItems {
|
||||
if item.Username == user && item.Password == pass { // demo: plain compare; real would hash
|
||||
if item.Username == user && accessListPasswordOK(item.Password, pass) {
|
||||
authed = true
|
||||
break
|
||||
}
|
||||
@@ -1250,3 +1251,15 @@ func parseAdvancedConfig(adv string) (reqSets map[string]string, respAdds map[st
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
func accessListPasswordOK(stored, provided string) bool {
|
||||
if stored == "" {
|
||||
return false
|
||||
}
|
||||
if strings.HasPrefix(stored, "$2a$") || strings.HasPrefix(stored, "$2b$") || strings.HasPrefix(stored, "$2y$") {
|
||||
return bcrypt.CompareHashAndPassword([]byte(stored), []byte(provided)) == nil
|
||||
}
|
||||
// Legacy plaintext rows until re-saved via admin API.
|
||||
return stored == provided
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user