diff --git a/pkg/services/ngalert/provisioning/contactpoints.go b/pkg/services/ngalert/provisioning/contactpoints.go index 78959045adf..539b81ce0cb 100644 --- a/pkg/services/ngalert/provisioning/contactpoints.go +++ b/pkg/services/ngalert/provisioning/contactpoints.go @@ -12,6 +12,7 @@ import ( "github.com/prometheus/alertmanager/config" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -516,13 +517,39 @@ func RemoveSecretsForContactPoint(e *apimodels.EmbeddedContactPoint) (map[string return nil, err } for _, secretKey := range secretKeys { - secretValue := e.Settings.Get(secretKey).MustString() - e.Settings.Del(secretKey) + foundSecretKey, secretValue, err := getCaseInsensitive(e.Settings, secretKey) + if err != nil { + return nil, err + } + e.Settings.Del(foundSecretKey) s[secretKey] = secretValue } return s, nil } +// getCaseInsensitive returns the value of the specified key, preferring an exact match but accepting a case-insensitive match. +// If no key matches, the second return value is an empty string. +func getCaseInsensitive(jsonObj *simplejson.Json, key string) (string, string, error) { + // Check for an exact key match first. + if value, ok := jsonObj.CheckGet(key); ok { + return key, value.MustString(), nil + } + + // If no exact match is found, look for a case-insensitive match. + settingsMap, err := jsonObj.Map() + if err != nil { + return "", "", err + } + + for k, v := range settingsMap { + if strings.EqualFold(k, key) { + return k, v.(string), nil + } + } + + return key, "", nil +} + // convertRecSvcErr converts errors from notifier.ReceiverService to errors expected from ContactPointService. func convertRecSvcErr(err error) error { if errors.Is(err, store.ErrNoAlertmanagerConfiguration) { diff --git a/pkg/services/ngalert/provisioning/contactpoints_test.go b/pkg/services/ngalert/provisioning/contactpoints_test.go index 9ce9af52f93..a502f9c8798 100644 --- a/pkg/services/ngalert/provisioning/contactpoints_test.go +++ b/pkg/services/ngalert/provisioning/contactpoints_test.go @@ -40,6 +40,11 @@ func TestContactPointService(t *testing.T) { accesscontrol.ActionAlertingProvisioningRead: nil, }, }} + decryptedUser := &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{ + 1: { + accesscontrol.ActionAlertingProvisioningReadSecrets: nil, + }, + }} t.Run("service gets contact points from AM config", func(t *testing.T) { sut := createContactPointServiceSut(t, secretsService) @@ -265,6 +270,52 @@ func TestContactPointService(t *testing.T) { intercepted := fakeConfigStore.LastSaveCommand require.Equal(t, expectedConcurrencyToken, intercepted.FetchedConfigurationHash) }) + + t.Run("secrets are parsed in a case-insensitive way", func(t *testing.T) { + // JSON unmarshalling is case-insensitive. This means we can have + // a setting named "TOKEN" instead of "token". This test ensures that + // we handle such cases correctly and the token value is properly parsed, + // even if the setting key does not match the JSON key exactly. + tests := []struct { + settingsJSON string + expectedValue string + name string + }{ + { + settingsJSON: `{"recipient":"value_recipient","TOKEN":"some-other-token"}`, + expectedValue: "some-other-token", + name: "token key is uppercased", + }, + + // This test checks that if multiple token keys are present in the settings, + // the key with the exact matching name is used. + { + settingsJSON: `{"recipient":"value_recipient","TOKEN":"some-other-token", "token": "second-token"}`, + expectedValue: "second-token", + name: "multiple token keys", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + sut := createContactPointServiceSut(t, secretsService) + + newCp := createTestContactPoint() + settings, _ := simplejson.NewJson([]byte(tc.settingsJSON)) + newCp.Settings = settings + + _, err := sut.CreateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI) + require.NoError(t, err) + + q := cpsQueryWithName(1, newCp.Name) + q.Decrypt = true + cps, err := sut.GetContactPoints(context.Background(), q, decryptedUser) + require.NoError(t, err) + require.Len(t, cps, 1) + require.Equal(t, tc.expectedValue, cps[0].Settings.Get("token").MustString()) + }) + } + }) } func TestContactPointServiceDecryptRedact(t *testing.T) {