111 lines
3.1 KiB
Go
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
|
|
}
|