quintodrome/server/nativeapi/config.go

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

134 lines
3.7 KiB
Go
Raw Normal View History

package nativeapi
import (
"context"
"encoding/json"
"fmt"
"net/http"
"reflect"
"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 configEntry struct {
Key string `json:"key"`
EnvVar string `json:"envVar"`
Value interface{} `json:"value"`
}
type configResponse struct {
ID string `json:"id"`
ConfigFile string `json:"configFile"`
Config []configEntry `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
}
func flatten(ctx context.Context, entries *[]configEntry, prefix string, v reflect.Value) {
if v.Kind() == reflect.Struct && v.Type().PkgPath() != "time" {
t := v.Type()
for i := 0; i < v.NumField(); i++ {
if !t.Field(i).IsExported() {
continue
}
flatten(ctx, entries, prefix+"."+t.Field(i).Name, v.Field(i))
}
return
}
key := strings.TrimPrefix(prefix, ".")
envVar := "ND_" + strings.ToUpper(strings.ReplaceAll(key, ".", "_"))
var val interface{}
switch v.Kind() {
case reflect.Map, reflect.Slice, reflect.Array:
b, err := json.Marshal(v.Interface())
if err != nil {
log.Error(ctx, "Error marshalling config value", "key", key, err)
val = "error marshalling value"
} else {
val = string(b)
}
default:
originalValue := fmt.Sprint(v.Interface())
val = redactValue(key, originalValue)
}
*entries = append(*entries, configEntry{Key: key, EnvVar: envVar, Value: val})
}
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
}
entries := make([]configEntry, 0)
v := reflect.ValueOf(*conf.Server)
t := reflect.TypeOf(*conf.Server)
for i := 0; i < v.NumField(); i++ {
fieldVal := v.Field(i)
fieldType := t.Field(i)
flatten(ctx, &entries, fieldType.Name, fieldVal)
}
resp := configResponse{ID: "config", ConfigFile: conf.Server.ConfigFile, Config: entries}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Error(ctx, "Error encoding config response", err)
}
}