mirror of
https://github.com/navidrome/navidrome.git
synced 2025-06-19 16:24:13 +03:00
* refactor(config): reorganize configuration handling Signed-off-by: Deluan <deluan@navidrome.org> * refactor(aboutUtils): improve array formatting and handling in TOML conversion Signed-off-by: Deluan <deluan@navidrome.org> * refactor(aboutUtils): add escapeTomlKey function to handle special characters in TOML keys Signed-off-by: Deluan <deluan@navidrome.org> * fix(test): remove unused getNestedValue function * fix(ui): apply prettier formatting --------- Signed-off-by: Deluan <deluan@navidrome.org>
139 lines
3.9 KiB
Go
139 lines
3.9 KiB
Go
package nativeapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/log"
|
|
"github.com/navidrome/navidrome/model/request"
|
|
)
|
|
|
|
// sensitiveFieldsPartialMask contains configuration field names that should be redacted
|
|
// using partial masking (first and last character visible, middle replaced with *).
|
|
// For values with 7+ characters: "secretvalue123" becomes "s***********3"
|
|
// For values with <7 characters: "short" becomes "****"
|
|
// Add field paths using dot notation (e.g., "LastFM.ApiKey", "Spotify.Secret")
|
|
var sensitiveFieldsPartialMask = []string{
|
|
"LastFM.ApiKey",
|
|
"LastFM.Secret",
|
|
"Prometheus.MetricsPath",
|
|
"Spotify.ID",
|
|
"Spotify.Secret",
|
|
"DevAutoLoginUsername",
|
|
}
|
|
|
|
// sensitiveFieldsFullMask contains configuration field names that should always be
|
|
// completely masked with "****" regardless of their length.
|
|
// Add field paths using dot notation for any fields that should never show any content.
|
|
var sensitiveFieldsFullMask = []string{
|
|
"DevAutoCreateAdminPassword",
|
|
"PasswordEncryptionKey",
|
|
"Prometheus.Password",
|
|
}
|
|
|
|
type configResponse struct {
|
|
ID string `json:"id"`
|
|
ConfigFile string `json:"configFile"`
|
|
Config map[string]interface{} `json:"config"`
|
|
}
|
|
|
|
func redactValue(key string, value string) string {
|
|
// Return empty values as-is
|
|
if len(value) == 0 {
|
|
return value
|
|
}
|
|
|
|
// Check if this field should be fully masked
|
|
for _, field := range sensitiveFieldsFullMask {
|
|
if field == key {
|
|
return "****"
|
|
}
|
|
}
|
|
|
|
// Check if this field should be partially masked
|
|
for _, field := range sensitiveFieldsPartialMask {
|
|
if field == key {
|
|
if len(value) < 7 {
|
|
return "****"
|
|
}
|
|
// Show first and last character with * in between
|
|
return string(value[0]) + strings.Repeat("*", len(value)-2) + string(value[len(value)-1])
|
|
}
|
|
}
|
|
|
|
// Return original value if not sensitive
|
|
return value
|
|
}
|
|
|
|
// applySensitiveFieldMasking recursively applies masking to sensitive fields in the configuration map
|
|
func applySensitiveFieldMasking(ctx context.Context, config map[string]interface{}, prefix string) {
|
|
for key, value := range config {
|
|
fullKey := key
|
|
if prefix != "" {
|
|
fullKey = prefix + "." + key
|
|
}
|
|
|
|
switch v := value.(type) {
|
|
case map[string]interface{}:
|
|
// Recursively process nested maps
|
|
applySensitiveFieldMasking(ctx, v, fullKey)
|
|
case string:
|
|
// Apply masking to string values
|
|
config[key] = redactValue(fullKey, v)
|
|
default:
|
|
// For other types (numbers, booleans, etc.), convert to string and check for masking
|
|
if str := fmt.Sprint(v); str != "" {
|
|
masked := redactValue(fullKey, str)
|
|
if masked != str {
|
|
// Only replace if masking was applied
|
|
config[key] = masked
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func getConfig(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
user, _ := request.UserFrom(ctx)
|
|
if !user.IsAdmin {
|
|
http.Error(w, "Config endpoint is only available to admin users", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Marshal the actual configuration struct to preserve original field names
|
|
configBytes, err := json.Marshal(*conf.Server)
|
|
if err != nil {
|
|
log.Error(ctx, "Error marshaling config", err)
|
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Unmarshal back to map to get the structure with proper field names
|
|
var configMap map[string]interface{}
|
|
err = json.Unmarshal(configBytes, &configMap)
|
|
if err != nil {
|
|
log.Error(ctx, "Error unmarshaling config to map", err)
|
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Apply sensitive field masking
|
|
applySensitiveFieldMasking(ctx, configMap, "")
|
|
|
|
resp := configResponse{
|
|
ID: "config",
|
|
ConfigFile: conf.Server.ConfigFile,
|
|
Config: configMap,
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
|
log.Error(ctx, "Error encoding config response", err)
|
|
}
|
|
}
|