Alerting: Copy alertmanager configuration before decrypting (#105271)

pull/104794/head
Alexander Akhmetov 1 week ago committed by GitHub
parent 91d9cac157
commit 29128f7ae4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 20
      pkg/services/ngalert/remote/alertmanager.go
  2. 54
      pkg/services/ngalert/remote/alertmanager_test.go

@ -285,7 +285,7 @@ func (am *Alertmanager) isDefaultConfiguration(configHash [16]byte) bool {
return fmt.Sprintf("%x", configHash) == am.defaultConfigHash 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 // Should not be used outside of this package and the specific use case of decrypting the configuration before sending
// it to the remote Alertmanager. // it to the remote Alertmanager.
func (am *Alertmanager) decryptConfiguration(ctx context.Context, cfg *apimodels.PostableUserConfig) ([]byte, [16]byte, error) { 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) return am.decrypt(ctx, payload)
} }
// Iterate through receivers and decrypt secure settings. // Create a copy of the configuration to avoid modifying the original
// It's not necessary to be careful about not modifying the original, as it's used only in a specific context where cfgCopy := &apimodels.PostableUserConfig{}
// the config is read from json and then immediately sent to the remote Alertmanager. rawCfg, err := json.Marshal(cfg)
for _, rcv := range cfg.AlertmanagerConfig.Receivers { 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 { for _, gmr := range rcv.GrafanaManagedReceivers {
decrypted, err := gmr.DecryptSecureSettings(fn) decrypted, err := gmr.DecryptSecureSettings(fn)
if err != nil { 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 { if err != nil {
return nil, [16]byte{}, fmt.Errorf("unable to marshal decrypted configuration: %w", err) return nil, [16]byte{}, fmt.Errorf("unable to marshal decrypted configuration: %w", err)
} }

@ -43,7 +43,18 @@ import (
"github.com/grafana/grafana/pkg/util" "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 ( const (
testPassword = "test"
// Valid Grafana Alertmanager configurations. // 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"}}]}]}}` 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"}}]}]}}` 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==" testNflog2 = "OgoqCgZncm91cDISEgoJcmVjZWl2ZXIyEgV0ZXN0MyoMCLSDkbAGEMvaofYCEgwIxJ+RsAYQy9qh9gI6CioKBmdyb3VwMRISCglyZWNlaXZlcjESBXRlc3QzKgwItIORsAYQy9qh9gISDAjEn5GwBhDL2qH2Ag=="
) )
var (
defaultGrafanaConfig = setting.GetAlertmanagerDefaultConfiguration()
errTest = errors.New("test")
)
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
testsuite.Run(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) { func TestIntegrationRemoteAlertmanagerConfiguration(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("skipping integration test") t.Skip("skipping integration test")

Loading…
Cancel
Save