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
|
// CheckAuth checks both basic auth and API token
|
||||||
func CheckAuth(cfg *config.Config, w http.ResponseWriter, r *http.Request) bool {
|
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
|
// Check proxy auth first if configured
|
||||||
if cfg.ProxyAuthSecret != "" {
|
if cfg.ProxyAuthSecret != "" {
|
||||||
if valid, username, _ := CheckProxyAuth(cfg, r); valid {
|
if valid, username, _ := CheckProxyAuth(cfg, r); valid {
|
||||||
|
|
|
||||||
|
|
@ -1201,6 +1201,9 @@ func (r *Router) SetConfig(cfg *config.Config) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.Mu.Lock()
|
||||||
|
defer config.Mu.Unlock()
|
||||||
|
|
||||||
if r.config == nil {
|
if r.config == nil {
|
||||||
r.config = cfg
|
r.config = cfg
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -2839,6 +2842,9 @@ func (r *Router) handleConfig(w http.ResponseWriter, req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.Mu.RLock()
|
||||||
|
defer config.Mu.RUnlock()
|
||||||
|
|
||||||
// Return public configuration
|
// Return public configuration
|
||||||
config := map[string]interface{}{
|
config := map[string]interface{}{
|
||||||
"csrfProtection": false, // Not implemented yet
|
"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!
|
// Update runtime config immediately with hashed token - no restart needed!
|
||||||
|
config.Mu.Lock()
|
||||||
r.config.AuthUser = setupRequest.Username
|
r.config.AuthUser = setupRequest.Username
|
||||||
r.config.AuthPass = hashedPassword
|
r.config.AuthPass = hashedPassword
|
||||||
r.config.APITokens = []config.APITokenRecord{*tokenRecord}
|
r.config.APITokens = []config.APITokenRecord{*tokenRecord}
|
||||||
r.config.SortAPITokens()
|
r.config.SortAPITokens()
|
||||||
r.config.APITokenEnabled = true
|
r.config.APITokenEnabled = true
|
||||||
|
config.Mu.Unlock()
|
||||||
|
|
||||||
if r.persistence != nil {
|
if r.persistence != nil {
|
||||||
if err := r.persistence.SaveAPITokens(r.config.APITokens); err != 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.Mu.Lock()
|
||||||
r.config.APITokens = []config.APITokenRecord{*tokenRecord}
|
r.config.APITokens = []config.APITokenRecord{*tokenRecord}
|
||||||
r.config.SortAPITokens()
|
r.config.SortAPITokens()
|
||||||
r.config.APITokenEnabled = true
|
r.config.APITokenEnabled = true
|
||||||
|
config.Mu.Unlock()
|
||||||
log.Info().Msg("Runtime config updated with new API token - active immediately")
|
log.Info().Msg("Runtime config updated with new API token - active immediately")
|
||||||
|
|
||||||
if r.persistence != nil {
|
if r.persistence != nil {
|
||||||
|
|
@ -672,7 +676,9 @@ func (r *Router) HandleValidateAPIToken(w http.ResponseWriter, rq *http.Request)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the token (compare hash)
|
// Validate the token (compare hash)
|
||||||
|
config.Mu.RLock()
|
||||||
_, isValid := r.config.ValidateAPIToken(validateRequest.Token)
|
_, isValid := r.config.ValidateAPIToken(validateRequest.Token)
|
||||||
|
config.Mu.RUnlock()
|
||||||
|
|
||||||
// Log validation attempt without logging the token itself
|
// Log validation attempt without logging the token itself
|
||||||
if isValid {
|
if isValid {
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,9 @@ func (r *Router) handleListAPITokens(w http.ResponseWriter, req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.Mu.RLock()
|
||||||
|
defer config.Mu.RUnlock()
|
||||||
|
|
||||||
tokens := make([]apiTokenDTO, 0, len(r.config.APITokens))
|
tokens := make([]apiTokenDTO, 0, len(r.config.APITokens))
|
||||||
for _, record := range r.config.APITokens {
|
for _, record := range r.config.APITokens {
|
||||||
tokens = append(tokens, toAPITokenDTO(record))
|
tokens = append(tokens, toAPITokenDTO(record))
|
||||||
|
|
@ -147,6 +150,9 @@ func (r *Router) handleCreateAPIToken(w http.ResponseWriter, req *http.Request)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.Mu.Lock()
|
||||||
|
defer config.Mu.Unlock()
|
||||||
|
|
||||||
r.config.APITokens = append(r.config.APITokens, *record)
|
r.config.APITokens = append(r.config.APITokens, *record)
|
||||||
r.config.SortAPITokens()
|
r.config.SortAPITokens()
|
||||||
r.config.APITokenEnabled = true
|
r.config.APITokenEnabled = true
|
||||||
|
|
@ -181,6 +187,9 @@ func (r *Router) handleDeleteAPIToken(w http.ResponseWriter, req *http.Request)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.Mu.Lock()
|
||||||
|
defer config.Mu.Unlock()
|
||||||
|
|
||||||
removed := r.config.RemoveAPIToken(id)
|
removed := r.config.RemoveAPIToken(id)
|
||||||
if !removed {
|
if !removed {
|
||||||
http.Error(w, "Token not found", http.StatusNotFound)
|
http.Error(w, "Token not found", http.StatusNotFound)
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/auth"
|
"github.com/rcourtman/pulse-go-rewrite/internal/auth"
|
||||||
|
|
@ -29,6 +31,10 @@ import (
|
||||||
"github.com/rs/zerolog/log"
|
"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 (
|
const (
|
||||||
DefaultGuestMetadataMinRefresh = 2 * time.Minute
|
DefaultGuestMetadataMinRefresh = 2 * time.Minute
|
||||||
DefaultGuestMetadataRefreshJitter = 45 * time.Second
|
DefaultGuestMetadataRefreshJitter = 45 * time.Second
|
||||||
|
|
|
||||||
|
|
@ -310,6 +310,9 @@ func (cw *ConfigWatcher) reloadConfig() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply auth user
|
// Apply auth user
|
||||||
|
Mu.Lock()
|
||||||
|
defer Mu.Unlock()
|
||||||
|
|
||||||
newUser := strings.Trim(envMap["PULSE_AUTH_USER"], "'\"")
|
newUser := strings.Trim(envMap["PULSE_AUTH_USER"], "'\"")
|
||||||
if newUser != oldAuthUser {
|
if newUser != oldAuthUser {
|
||||||
cw.config.AuthUser = newUser
|
cw.config.AuthUser = newUser
|
||||||
|
|
@ -485,6 +488,8 @@ func (cw *ConfigWatcher) reloadAPITokens() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only update if we successfully loaded tokens
|
// Only update if we successfully loaded tokens
|
||||||
|
Mu.Lock()
|
||||||
|
defer Mu.Unlock()
|
||||||
cw.config.APITokens = tokens
|
cw.config.APITokens = tokens
|
||||||
cw.config.SortAPITokens()
|
cw.config.SortAPITokens()
|
||||||
cw.config.APITokenEnabled = len(tokens) > 0
|
cw.config.APITokenEnabled = len(tokens) > 0
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue