Add mutex protection for config watcher reloads (re #748)
Introduced sync.RWMutex to protect concurrent access to configuration fields (AuthUser, AuthPass, APITokens) that are modified by the ConfigWatcher at runtime. - Added global config.Mu RWMutex in internal/config/config.go - Protected config updates in ConfigWatcher.reloadConfig() and reloadAPITokens() - Protected config reads in CheckAuth and all API token handlers - Protected Router.SetConfig() during full config reloads This prevents race conditions when .env file changes trigger config reloads while authentication handlers are reading the same fields.
This commit is contained in:
parent
0c7af34ad2
commit
60721cbc22
6 changed files with 35 additions and 0 deletions
|
|
@ -196,6 +196,9 @@ func min(a, b int) int {
|
|||
|
||||
// CheckAuth checks both basic auth and API token
|
||||
func CheckAuth(cfg *config.Config, w http.ResponseWriter, r *http.Request) bool {
|
||||
config.Mu.RLock()
|
||||
defer config.Mu.RUnlock()
|
||||
|
||||
// Check proxy auth first if configured
|
||||
if cfg.ProxyAuthSecret != "" {
|
||||
if valid, username, _ := CheckProxyAuth(cfg, r); valid {
|
||||
|
|
|
|||
|
|
@ -1201,6 +1201,9 @@ func (r *Router) SetConfig(cfg *config.Config) {
|
|||
return
|
||||
}
|
||||
|
||||
config.Mu.Lock()
|
||||
defer config.Mu.Unlock()
|
||||
|
||||
if r.config == nil {
|
||||
r.config = cfg
|
||||
} else {
|
||||
|
|
@ -2839,6 +2842,9 @@ func (r *Router) handleConfig(w http.ResponseWriter, req *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
config.Mu.RLock()
|
||||
defer config.Mu.RUnlock()
|
||||
|
||||
// Return public configuration
|
||||
config := map[string]interface{}{
|
||||
"csrfProtection": false, // Not implemented yet
|
||||
|
|
|
|||
|
|
@ -275,11 +275,13 @@ func handleQuickSecuritySetupFixed(r *Router) http.HandlerFunc {
|
|||
}
|
||||
|
||||
// Update runtime config immediately with hashed token - no restart needed!
|
||||
config.Mu.Lock()
|
||||
r.config.AuthUser = setupRequest.Username
|
||||
r.config.AuthPass = hashedPassword
|
||||
r.config.APITokens = []config.APITokenRecord{*tokenRecord}
|
||||
r.config.SortAPITokens()
|
||||
r.config.APITokenEnabled = true
|
||||
config.Mu.Unlock()
|
||||
|
||||
if r.persistence != nil {
|
||||
if err := r.persistence.SaveAPITokens(r.config.APITokens); err != nil {
|
||||
|
|
@ -539,9 +541,11 @@ func (r *Router) HandleRegenerateAPIToken(w http.ResponseWriter, rq *http.Reques
|
|||
return
|
||||
}
|
||||
|
||||
config.Mu.Lock()
|
||||
r.config.APITokens = []config.APITokenRecord{*tokenRecord}
|
||||
r.config.SortAPITokens()
|
||||
r.config.APITokenEnabled = true
|
||||
config.Mu.Unlock()
|
||||
log.Info().Msg("Runtime config updated with new API token - active immediately")
|
||||
|
||||
if r.persistence != nil {
|
||||
|
|
@ -672,7 +676,9 @@ func (r *Router) HandleValidateAPIToken(w http.ResponseWriter, rq *http.Request)
|
|||
}
|
||||
|
||||
// Validate the token (compare hash)
|
||||
config.Mu.RLock()
|
||||
_, isValid := r.config.ValidateAPIToken(validateRequest.Token)
|
||||
config.Mu.RUnlock()
|
||||
|
||||
// Log validation attempt without logging the token itself
|
||||
if isValid {
|
||||
|
|
|
|||
|
|
@ -91,6 +91,9 @@ func (r *Router) handleListAPITokens(w http.ResponseWriter, req *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
config.Mu.RLock()
|
||||
defer config.Mu.RUnlock()
|
||||
|
||||
tokens := make([]apiTokenDTO, 0, len(r.config.APITokens))
|
||||
for _, record := range r.config.APITokens {
|
||||
tokens = append(tokens, toAPITokenDTO(record))
|
||||
|
|
@ -147,6 +150,9 @@ func (r *Router) handleCreateAPIToken(w http.ResponseWriter, req *http.Request)
|
|||
return
|
||||
}
|
||||
|
||||
config.Mu.Lock()
|
||||
defer config.Mu.Unlock()
|
||||
|
||||
r.config.APITokens = append(r.config.APITokens, *record)
|
||||
r.config.SortAPITokens()
|
||||
r.config.APITokenEnabled = true
|
||||
|
|
@ -181,6 +187,9 @@ func (r *Router) handleDeleteAPIToken(w http.ResponseWriter, req *http.Request)
|
|||
return
|
||||
}
|
||||
|
||||
config.Mu.Lock()
|
||||
defer config.Mu.Unlock()
|
||||
|
||||
removed := r.config.RemoveAPIToken(id)
|
||||
if !removed {
|
||||
http.Error(w, "Token not found", http.StatusNotFound)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"sync"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/auth"
|
||||
|
|
@ -29,6 +31,10 @@ import (
|
|||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// Mu protects concurrent access to the configuration,
|
||||
// particularly for fields updated by the config watcher (AuthUser, AuthPass, APITokens).
|
||||
var Mu sync.RWMutex
|
||||
|
||||
const (
|
||||
DefaultGuestMetadataMinRefresh = 2 * time.Minute
|
||||
DefaultGuestMetadataRefreshJitter = 45 * time.Second
|
||||
|
|
|
|||
|
|
@ -310,6 +310,9 @@ func (cw *ConfigWatcher) reloadConfig() {
|
|||
}
|
||||
|
||||
// Apply auth user
|
||||
Mu.Lock()
|
||||
defer Mu.Unlock()
|
||||
|
||||
newUser := strings.Trim(envMap["PULSE_AUTH_USER"], "'\"")
|
||||
if newUser != oldAuthUser {
|
||||
cw.config.AuthUser = newUser
|
||||
|
|
@ -485,6 +488,8 @@ func (cw *ConfigWatcher) reloadAPITokens() {
|
|||
}
|
||||
|
||||
// Only update if we successfully loaded tokens
|
||||
Mu.Lock()
|
||||
defer Mu.Unlock()
|
||||
cw.config.APITokens = tokens
|
||||
cw.config.SortAPITokens()
|
||||
cw.config.APITokenEnabled = len(tokens) > 0
|
||||
|
|
|
|||
Loading…
Reference in a new issue