Files
2026-09-27 16:39:38 +08:00

111 lines
3.1 KiB
Go

package store
import (
"encoding/json"
"reflect"
"sort"
"strings"
)
type ConfigChange struct {
Path string `json:"path"`
Before any `json:"before"`
After any `json:"after"`
SecretChanged bool `json:"secretChanged"`
}
func secretField(field string) bool {
switch strings.ToLower(field) {
case "password", "targetpassword", "gatewaypassword", "privatekey", "passphrase", "hostkey", "kubeconfig", "kubeconfigcontent", "singboxoutbound", "content", "certificate", "targetca", "authorizedkeys", "webhooksecret":
return true
}
return false
}
func configLeaves(value any, path string, secret bool, result map[string]any, secrets map[string]bool) {
if secret {
result[path] = value
secrets[path] = true
return
}
switch v := value.(type) {
case map[string]any:
for key, child := range v {
configLeaves(child, path+"/"+strings.ReplaceAll(strings.ReplaceAll(key, "~", "~0"), "/", "~1"), secretField(key), result, secrets)
}
case []any:
// Account IDs keep comparisons stable when the editor reorders accounts.
isAccounts := strings.HasSuffix(path, "/accounts") || strings.HasSuffix(path, "/gatewayAccounts")
if isAccounts {
for _, child := range v {
if account, ok := child.(map[string]any); ok {
if id, ok := account["id"].(string); ok {
configLeaves(child, path+"/"+strings.ReplaceAll(strings.ReplaceAll(id, "~", "~0"), "/", "~1"), false, result, secrets)
}
}
}
} else {
result[path] = value
}
default:
result[path] = value
}
}
// ConfigurationDiff compares the current persisted configuration with a revision.
// Secret values never leave this method, including secrets in added accounts.
func (s *Store) ConfigurationDiff(kind, id string, revision int64) ([]ConfigChange, error) {
purpose, err := configPurpose(kind, id)
if err != nil {
return nil, err
}
historical, err := s.ConfigRevision(kind, id, revision)
if err != nil {
return nil, err
}
var blob []byte
if err = s.db.QueryRow("SELECT config FROM "+kind+" WHERE id=?", id).Scan(&blob); err != nil {
return nil, err
}
current, err := s.vault.Open(blob, purpose)
if err != nil {
return nil, err
}
var before, after any
if err = json.Unmarshal(current, &before); err != nil {
return nil, err
}
if err = json.Unmarshal(historical, &after); err != nil {
return nil, err
}
return compareConfig(before, after), nil
}
func compareConfig(before, after any) []ConfigChange {
a, b := map[string]any{}, map[string]any{}
secrets := map[string]bool{}
configLeaves(before, "", false, a, secrets)
configLeaves(after, "", false, b, secrets)
keys := map[string]bool{}
for key := range a {
keys[key] = true
}
for key := range b {
keys[key] = true
}
changes := []ConfigChange{}
for key := range keys {
if reflect.DeepEqual(a[key], b[key]) {
continue
}
change := ConfigChange{Path: key, Before: a[key], After: b[key], SecretChanged: secrets[key]}
if change.SecretChanged {
change.Before = nil
change.After = nil
}
changes = append(changes, change)
}
sort.Slice(changes, func(i, j int) bool { return changes[i].Path < changes[j].Path })
return changes
}