diff --git a/pkg/services/ngalert/remote/alertmanager.go b/pkg/services/ngalert/remote/alertmanager.go index 60f63cf8a32..5788506eb4c 100644 --- a/pkg/services/ngalert/remote/alertmanager.go +++ b/pkg/services/ngalert/remote/alertmanager.go @@ -285,7 +285,7 @@ func (am *Alertmanager) isDefaultConfiguration(configHash [16]byte) bool { return fmt.Sprintf("%x", configHash) == am.defaultConfigHash } -// decryptConfiguration decrypts the configuration in-place and returns the decrypted configuration alongside its hash. +// decryptConfiguration creates a copy of the configuration, decrypts it, and returns the decrypted configuration alongside its hash. // Should not be used outside of this package and the specific use case of decrypting the configuration before sending // it to the remote Alertmanager. func (am *Alertmanager) decryptConfiguration(ctx context.Context, cfg *apimodels.PostableUserConfig) ([]byte, [16]byte, error) { @@ -293,10 +293,18 @@ func (am *Alertmanager) decryptConfiguration(ctx context.Context, cfg *apimodels return am.decrypt(ctx, payload) } - // Iterate through receivers and decrypt secure settings. - // It's not necessary to be careful about not modifying the original, as it's used only in a specific context where - // the config is read from json and then immediately sent to the remote Alertmanager. - for _, rcv := range cfg.AlertmanagerConfig.Receivers { + // Create a copy of the configuration to avoid modifying the original + cfgCopy := &apimodels.PostableUserConfig{} + rawCfg, err := json.Marshal(cfg) + if err != nil { + return nil, [16]byte{}, fmt.Errorf("unable to marshal original configuration: %w", err) + } + if err := json.Unmarshal(rawCfg, cfgCopy); err != nil { + return nil, [16]byte{}, fmt.Errorf("unable to unmarshal original configuration: %w", err) + } + + // Iterate through receivers and decrypt secure settings on the copy + for _, rcv := range cfgCopy.AlertmanagerConfig.Receivers { for _, gmr := range rcv.GrafanaManagedReceivers { decrypted, err := gmr.DecryptSecureSettings(fn) if err != nil { @@ -306,7 +314,7 @@ func (am *Alertmanager) decryptConfiguration(ctx context.Context, cfg *apimodels } } - rawDecrypted, err := json.Marshal(cfg) + rawDecrypted, err := json.Marshal(cfgCopy) if err != nil { return nil, [16]byte{}, fmt.Errorf("unable to marshal decrypted configuration: %w", err) } diff --git a/pkg/services/ngalert/remote/alertmanager_test.go b/pkg/services/ngalert/remote/alertmanager_test.go index 0fcd1e8f93e..85f88c1c613 100644 --- a/pkg/services/ngalert/remote/alertmanager_test.go +++ b/pkg/services/ngalert/remote/alertmanager_test.go @@ -43,7 +43,18 @@ import ( "github.com/grafana/grafana/pkg/util" ) +var ( + testPasswordBase64 = base64.StdEncoding.EncodeToString([]byte(testPassword)) + defaultGrafanaConfig = setting.GetAlertmanagerDefaultConfiguration() + errTest = errors.New("test") + + // Valid Grafana Alertmanager configuration with secret in base64. + testGrafanaConfigWithEncryptedSecret = fmt.Sprintf(`{"template_files":{},"alertmanager_config":{"time_intervals":[{"name":"weekends","time_intervals":[{"weekdays":["saturday","sunday"],"location":"Africa/Accra"}]}],"route":{"receiver":"grafana-default-email","group_by":["grafana_folder","alertname"]},"receivers":[{"name":"grafana-default-email","grafana_managed_receiver_configs":[{"uid":"dde6ntuob69dtf","name":"WH","type":"webhook","disableResolveMessage":false,"settings":{"url":"http://localhost:8080","username":"test"},"secureSettings":{"password":"%s"}}]}]}}`, testPasswordBase64) +) + const ( + testPassword = "test" + // Valid Grafana Alertmanager configurations. testGrafanaConfig = `{"template_files":{},"alertmanager_config":{"time_intervals":[{"name":"weekends","time_intervals":[{"weekdays":["saturday","sunday"],"location":"Africa/Accra"}]}],"route":{"receiver":"grafana-default-email","group_by":["grafana_folder","alertname"]},"receivers":[{"name":"grafana-default-email","grafana_managed_receiver_configs":[{"uid":"","name":"some other name","type":"email","disableResolveMessage":false,"settings":{"addresses":"\u003cexample@email.com\u003e"}}]}]}}` testGrafanaConfigWithSecret = `{"template_files":{},"alertmanager_config":{"time_intervals":[{"name":"weekends","time_intervals":[{"weekdays":["saturday","sunday"],"location":"Africa/Accra"}]}],"route":{"receiver":"grafana-default-email","group_by":["grafana_folder","alertname"]},"receivers":[{"name":"grafana-default-email","grafana_managed_receiver_configs":[{"uid":"dde6ntuob69dtf","name":"WH","type":"webhook","disableResolveMessage":false,"settings":{"url":"http://localhost:8080","username":"test"},"secureSettings":{"password":"test"}}]}]}}` @@ -56,11 +67,6 @@ const ( testNflog2 = "OgoqCgZncm91cDISEgoJcmVjZWl2ZXIyEgV0ZXN0MyoMCLSDkbAGEMvaofYCEgwIxJ+RsAYQy9qh9gI6CioKBmdyb3VwMRISCglyZWNlaXZlcjESBXRlc3QzKgwItIORsAYQy9qh9gISDAjEn5GwBhDL2qH2Ag==" ) -var ( - defaultGrafanaConfig = setting.GetAlertmanagerDefaultConfiguration() - errTest = errors.New("test") -) - func TestMain(m *testing.M) { testsuite.Run(m) } @@ -505,6 +511,44 @@ func Test_isDefaultConfiguration(t *testing.T) { } } +func TestDecryptConfiguration(t *testing.T) { + t.Run("should not modify the original config", func(t *testing.T) { + var inputCfg apimodels.PostableUserConfig + require.NoError(t, json.Unmarshal([]byte(testGrafanaConfigWithEncryptedSecret), &inputCfg)) + + decryptFn := func(_ context.Context, payload []byte) ([]byte, error) { + if string(payload) == testPassword { + return []byte(testPassword), nil + } + return nil, fmt.Errorf("incorrect payload") + } + + am := &Alertmanager{ + decrypt: decryptFn, + } + + rawDecrypted, _, err := am.decryptConfiguration(context.Background(), &inputCfg) + require.NoError(t, err) + + currentJSON, err := json.Marshal(inputCfg) + require.NoError(t, err) + require.JSONEq(t, testGrafanaConfigWithEncryptedSecret, string(currentJSON), "Original configuration should not be modified") + + var decryptedCfg apimodels.PostableUserConfig + require.NoError(t, json.Unmarshal(rawDecrypted, &decryptedCfg)) + + found := false + for _, rcv := range decryptedCfg.AlertmanagerConfig.Receivers { + for _, gmr := range rcv.GrafanaManagedReceivers { + if gmr.Type == "webhook" && gmr.SecureSettings["password"] == testPassword { + found = true + } + } + } + require.True(t, found, "Decrypted configuration should contain decrypted password") + }) +} + func TestIntegrationRemoteAlertmanagerConfiguration(t *testing.T) { if testing.Short() { t.Skip("skipping integration test")