From 936894a5feeb18480a9af69c872b23394a64bc93 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Tue, 18 Nov 2025 19:33:26 +0000 Subject: [PATCH] Sanitize duplicate allowed_nodes blocks --- cmd/pulse-sensor-proxy/config.go | 48 +++++++++++++++++++++++++ cmd/pulse-sensor-proxy/config_test.go | 52 +++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 cmd/pulse-sensor-proxy/config_test.go diff --git a/cmd/pulse-sensor-proxy/config.go b/cmd/pulse-sensor-proxy/config.go index 62e7092..34c9976 100644 --- a/cmd/pulse-sensor-proxy/config.go +++ b/cmd/pulse-sensor-proxy/config.go @@ -6,6 +6,7 @@ import ( "fmt" "net" "os" + "regexp" "strconv" "strings" "time" @@ -85,6 +86,10 @@ func loadConfig(configPath string) (*Config, error) { return nil, fmt.Errorf("failed to read config file: %w", err) } + if sanitized, newData := sanitizeDuplicateAllowedNodesBlocks(configPath, data); sanitized { + data = newData + } + if err := yaml.Unmarshal(data, cfg); err != nil { return nil, fmt.Errorf("failed to parse config file: %w", err) } @@ -518,6 +523,49 @@ func detectHostCIDRs() []string { return cidrs } +var allowedNodesBlockPattern = regexp.MustCompile(`(?m)(?:^[ \t]*#.*\n)*^[ \t]*allowed_nodes:\n(?:^[ \t]+-.*\n?)+`) + +func sanitizeDuplicateAllowedNodesBlocks(path string, data []byte) (bool, []byte) { + matches := allowedNodesBlockPattern.FindAllIndex(data, -1) + if len(matches) <= 1 { + return false, data + } + + var buf bytes.Buffer + last := 0 + for idx, match := range matches { + start, end := match[0], match[1] + if idx == 0 { + buf.Write(data[last:end]) + } else { + buf.Write(data[last:start]) + } + last = end + } + buf.Write(data[last:]) + + cleaned := buf.Bytes() + if path != "" { + if err := os.WriteFile(path, cleaned, 0600); err != nil { + log.Warn(). + Err(err). + Str("config_file", path). + Msg("Failed to rewrite sanitized configuration; using in-memory copy only") + } else { + log.Warn(). + Str("config_file", path). + Int("removed_duplicate_blocks", len(matches)-1). + Msg("Detected duplicate allowed_nodes blocks and sanitized configuration automatically") + } + } else { + log.Warn(). + Int("removed_duplicate_blocks", len(matches)-1). + Msg("Detected duplicate allowed_nodes blocks and sanitized configuration (in-memory only)") + } + + return true, cleaned +} + // parseAllowedSubnets validates and normalizes subnet specifications func parseAllowedSubnets(cfg []string) ([]string, error) { seen := make(map[string]struct{}) diff --git a/cmd/pulse-sensor-proxy/config_test.go b/cmd/pulse-sensor-proxy/config_test.go new file mode 100644 index 0000000..a48d26c --- /dev/null +++ b/cmd/pulse-sensor-proxy/config_test.go @@ -0,0 +1,52 @@ +package main + +import ( + "strings" + "testing" +) + +func TestSanitizeDuplicateAllowedNodesBlocks_RemovesExtraBlocks(t *testing.T) { + raw := ` +allowed_nodes: + - delly + - minipc + +# Cluster nodes (auto-discovered during installation) +# These nodes are allowed to request temperature data when cluster IPC validation is unavailable +allowed_nodes: + - delly + - minipc + - extra +` + + sanitized, out := sanitizeDuplicateAllowedNodesBlocks("", []byte(raw)) + if !sanitized { + t.Fatalf("expected sanitization to occur") + } + + result := string(out) + if strings.Count(result, "allowed_nodes:") != 1 { + t.Fatalf("expected only one allowed_nodes block, got %q", result) + } + if strings.Contains(result, "extra") { + t.Fatalf("duplicate entries should be removed, got %q", result) + } + if strings.Contains(result, "Cluster nodes (auto-discovered during installation)") { + t.Fatalf("duplicate comment block should be removed") + } +} + +func TestSanitizeDuplicateAllowedNodesBlocks_NoChangeWhenUnique(t *testing.T) { + raw := ` +metrics_address: 127.0.0.1:9127 +allowed_nodes: + - delly +` + sanitized, out := sanitizeDuplicateAllowedNodesBlocks("", []byte(raw)) + if sanitized { + t.Fatalf("unexpected sanitization for unique config") + } + if string(out) != raw { + t.Fatalf("expected config to remain unchanged") + } +}