grafana/pkg/services/ngalert/notifier/receiver_svc_test.go

1645 lines
66 KiB
Go

package notifier
import (
"context"
"encoding/json"
"fmt"
"strings"
"testing"
"github.com/grafana/alerting/receivers/line"
"github.com/prometheus/alertmanager/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/featuremgmt"
ac "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
"github.com/grafana/grafana/pkg/services/ngalert/provisioning/validation"
"github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/services/secrets/database"
fake_secrets "github.com/grafana/grafana/pkg/services/secrets/fakes"
"github.com/grafana/grafana/pkg/services/secrets/manager"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/util/testutil"
)
func TestIntegrationReceiverService_GetReceiver(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
sqlStore := db.InitTestDB(t)
secretsService := manager.SetupTestService(t, database.ProvideSecretsStore(sqlStore))
redactedUser := &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{
1: {
accesscontrol.ActionAlertingNotificationsRead: nil,
},
}}
t.Run("service gets receiver from AM config", func(t *testing.T) {
sut := createReceiverServiceSut(t, secretsService)
Receiver, err := sut.GetReceiver(context.Background(), singleQ(1, "slack receiver"), redactedUser)
require.NoError(t, err)
require.Equal(t, "slack receiver", Receiver.Name)
require.Len(t, Receiver.Integrations, 1)
require.Equal(t, "UID2", Receiver.Integrations[0].UID)
})
t.Run("service returns error when receiver does not exist", func(t *testing.T) {
sut := createReceiverServiceSut(t, secretsService)
_, err := sut.GetReceiver(context.Background(), singleQ(1, "nonexistent"), redactedUser)
require.ErrorIs(t, err, legacy_storage.ErrReceiverNotFound)
})
}
func TestIntegrationReceiverService_GetReceivers(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
sqlStore := db.InitTestDB(t)
secretsService := manager.SetupTestService(t, database.ProvideSecretsStore(sqlStore))
redactedUser := &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{
1: {
accesscontrol.ActionAlertingNotificationsRead: nil,
},
}}
t.Run("service gets receivers from AM config", func(t *testing.T) {
sut := createReceiverServiceSut(t, secretsService)
Receivers, err := sut.GetReceivers(context.Background(), multiQ(1), redactedUser)
require.NoError(t, err)
require.Len(t, Receivers, 2)
require.Equal(t, "grafana-default-email", Receivers[0].Name)
require.Equal(t, "slack receiver", Receivers[1].Name)
})
t.Run("service filters receivers by name", func(t *testing.T) {
sut := createReceiverServiceSut(t, secretsService)
Receivers, err := sut.GetReceivers(context.Background(), multiQ(1, "slack receiver"), redactedUser)
require.NoError(t, err)
require.Len(t, Receivers, 1)
require.Equal(t, "slack receiver", Receivers[0].Name)
})
}
func TestIntegrationReceiverService_DecryptRedact(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
sqlStore := db.InitTestDB(t)
secretsService := manager.SetupTestService(t, database.ProvideSecretsStore(sqlStore))
getMethods := []string{"single", "multi"}
readUser := &user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {accesscontrol.ActionAlertingNotificationsRead: nil},
},
}
secretUser := &user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {
accesscontrol.ActionAlertingNotificationsRead: nil,
accesscontrol.ActionAlertingReceiversReadSecrets: {ac.ScopeReceiversAll},
},
},
}
for _, tc := range []struct {
name string
decrypt bool
user identity.Requester
err string
}{
{
name: "service redacts receivers by default",
decrypt: false,
user: readUser,
err: "",
},
{
name: "service returns error when trying to decrypt without permission",
decrypt: true,
user: readUser,
err: "[alerting.unauthorized] user is not authorized to read any decrypted receiver",
},
{
name: "service returns error if user is nil and decrypt is true",
decrypt: true,
user: nil,
err: "[alerting.unauthorized] user is not authorized to read any decrypted receiver",
},
{
name: "service decrypts receivers with permission",
decrypt: true,
user: secretUser,
err: "",
},
} {
for _, method := range getMethods {
t.Run(fmt.Sprintf("%s %s", tc.name, method), func(t *testing.T) {
sut := createReceiverServiceSut(t, secretsService)
var res *models.Receiver
var err error
if method == "single" {
q := singleQ(1, "slack receiver")
q.Decrypt = tc.decrypt
res, err = sut.GetReceiver(context.Background(), q, tc.user)
} else {
q := multiQ(1, "slack receiver")
q.Decrypt = tc.decrypt
var multiRes []*models.Receiver
multiRes, err = sut.GetReceivers(context.Background(), q, tc.user)
if tc.err == "" {
require.Len(t, multiRes, 1)
res = multiRes[0]
}
}
if tc.err == "" {
require.NoError(t, err)
} else {
require.ErrorContains(t, err, tc.err)
}
if tc.err == "" {
require.Equal(t, "slack receiver", res.Name)
require.Len(t, res.Integrations, 1)
require.Equal(t, "UID2", res.Integrations[0].UID)
require.NoError(t, err)
if tc.decrypt {
require.Equal(t, "secure url", res.Integrations[0].Settings["url"])
require.NotContains(t, res.Integrations[0].SecureSettings, "url")
} else {
require.NotContains(t, res.Integrations[0].Settings, "url")
// Ensure the encrypted value exists and is not redacted or decrypted.
require.NotEmpty(t, res.Integrations[0].SecureSettings["url"])
require.NotEqual(t, definitions.RedactedValue, res.Integrations[0].SecureSettings["url"])
require.NotEqual(t, "secure url", res.Integrations[0].SecureSettings["url"])
}
}
})
}
}
}
func TestReceiverService_Delete(t *testing.T) {
secretsService := fake_secrets.NewFakeSecretsService()
writer := &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{
1: {
accesscontrol.ActionAlertingNotificationsWrite: nil,
accesscontrol.ActionAlertingNotificationsRead: nil,
},
}}
slackIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("slack"))()
emailIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("email"))()
baseReceiver := models.ReceiverGen(models.ReceiverMuts.WithName("test receiver"), models.ReceiverMuts.WithIntegrations(slackIntegration))()
for _, tc := range []struct {
name string
user identity.Requester
deleteUID string
callerProvenance models.Provenance
version string
storeSettings map[models.AlertRuleKey][]models.NotificationSettings
existing *models.Receiver
expectedErr error
}{
{
name: "service deletes receiver",
user: writer,
deleteUID: baseReceiver.UID,
existing: util.Pointer(baseReceiver.Clone()),
},
{
name: "service deletes receiver with multiple integrations",
user: writer,
deleteUID: baseReceiver.UID,
existing: util.Pointer(models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithIntegrations(slackIntegration, emailIntegration))),
},
{
name: "service deletes receiver with provenance",
user: writer,
deleteUID: baseReceiver.UID,
callerProvenance: models.ProvenanceAPI,
existing: util.Pointer(models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithProvenance(models.ProvenanceAPI), models.ReceiverMuts.WithIntegrations(slackIntegration, emailIntegration))),
},
{
name: "non-existing receiver doesn't fail",
user: writer,
deleteUID: "non-existent",
},
{
name: "delete receiver used by route fails",
user: writer,
deleteUID: legacy_storage.NameToUid("grafana-default-email"),
version: "cd95627c75892a39", // Correct version for grafana-default-email.
expectedErr: makeReceiverInUseErr(true, nil),
},
{
name: "delete receiver used by rule fails",
user: writer,
deleteUID: baseReceiver.UID,
existing: util.Pointer(baseReceiver.Clone()),
storeSettings: map[models.AlertRuleKey][]models.NotificationSettings{
{OrgID: 1, UID: "rule1"}: {
models.NotificationSettingsGen(models.NSMuts.WithReceiver(baseReceiver.Name))(),
},
},
expectedErr: makeReceiverInUseErr(false, []models.AlertRuleKey{{OrgID: 1, UID: "rule1"}}),
},
{
name: "delete provisioning provenance fails when caller is ProvenanceNone",
user: writer,
deleteUID: baseReceiver.UID,
callerProvenance: models.ProvenanceNone,
existing: util.Pointer(models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithProvenance(models.ProvenanceFile))),
expectedErr: validation.MakeErrProvenanceChangeNotAllowed(models.ProvenanceFile, models.ProvenanceNone),
},
{
name: "delete provisioning provenance fails when caller is a different type", // TODO: This should fail once we move from lenient to strict validation.
user: writer,
deleteUID: baseReceiver.UID,
callerProvenance: models.ProvenanceFile,
existing: util.Pointer(models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithProvenance(models.ProvenanceAPI))),
// expectedErr: validation.MakeErrProvenanceChangeNotAllowed(models.ProvenanceAPI, models.ProvenanceFile),
},
{
name: "delete receiver with optimistic version mismatch fails",
user: writer,
deleteUID: baseReceiver.UID,
existing: util.Pointer(baseReceiver.Clone()),
version: "wrong version",
expectedErr: ErrReceiverVersionConflict,
},
} {
t.Run(tc.name, func(t *testing.T) {
store := &fakeAlertRuleNotificationStore{}
store.ListNotificationSettingsFn = func(ctx context.Context, q models.ListNotificationSettingsQuery) (map[models.AlertRuleKey][]models.NotificationSettings, error) {
return tc.storeSettings, nil
}
sut := createReceiverServiceSut(t, &secretsService)
sut.ruleNotificationsStore = store
if tc.existing != nil {
created, err := sut.CreateReceiver(context.Background(), tc.existing, tc.user.GetOrgID(), tc.user)
require.NoError(t, err)
if tc.version == "" {
tc.version = created.Version
}
}
err := sut.DeleteReceiver(context.Background(), tc.deleteUID, tc.callerProvenance, tc.version, tc.user.GetOrgID(), tc.user)
if tc.expectedErr == nil {
require.NoError(t, err)
} else {
assert.ErrorIs(t, err, tc.expectedErr)
return
}
// Ensure receiver saved to store is correct.
name, err := legacy_storage.UidToName(tc.deleteUID)
require.NoError(t, err)
q := models.GetReceiverQuery{OrgID: tc.user.GetOrgID(), Name: name}
_, err = sut.GetReceiver(context.Background(), q, writer)
assert.ErrorIs(t, err, legacy_storage.ErrReceiverNotFound)
provenances, err := sut.provisioningStore.GetProvenances(context.Background(), tc.user.GetOrgID(), (&definitions.EmbeddedContactPoint{}).ResourceType())
require.NoError(t, err)
assert.Len(t, provenances, 0)
})
}
}
func TestReceiverService_Create(t *testing.T) {
secretsService := fake_secrets.NewFakeSecretsService()
writer := &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{
1: {
accesscontrol.ActionAlertingNotificationsWrite: nil,
accesscontrol.ActionAlertingNotificationsRead: nil,
},
}}
decryptUser := &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{
1: {
accesscontrol.ActionAlertingReceiversReadSecrets: {ac.ScopeReceiversAll},
},
}}
// Used to mark generated fields to replace during test runtime.
generated := func(n int) string { return fmt.Sprintf("[GENERATED]%d", n) }
isGenerated := func(s string) bool { return strings.HasPrefix(s, "[GENERATED]") }
slackIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("slack"))()
emailIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("email"))()
lineIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig(line.Type))()
baseReceiver := models.ReceiverGen(models.ReceiverMuts.WithName("test receiver"), models.ReceiverMuts.WithIntegrations(slackIntegration))()
for _, tc := range []struct {
name string
user identity.Requester
receiver models.Receiver
expectedCreate models.Receiver
expectedStored *definitions.PostableApiReceiver
expectedErr error
expectedProvenances map[string]models.Provenance
}{
{
name: "service creates receiver",
user: writer,
receiver: baseReceiver.Clone(),
expectedCreate: models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.Encrypted(models.Base64Enrypt)),
expectedProvenances: map[string]models.Provenance{slackIntegration.UID: models.ProvenanceNone},
},
{
name: "service creates receiver with multiple integrations",
user: writer,
receiver: models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithIntegrations(slackIntegration, emailIntegration)),
expectedCreate: models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithIntegrations(slackIntegration, emailIntegration), models.ReceiverMuts.Encrypted(models.Base64Enrypt)),
expectedProvenances: map[string]models.Provenance{slackIntegration.UID: models.ProvenanceNone, emailIntegration.UID: models.ProvenanceNone},
},
{
name: "service creates receiver with provenance",
user: writer,
receiver: models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithProvenance(models.ProvenanceAPI), models.ReceiverMuts.WithIntegrations(slackIntegration, emailIntegration)),
expectedCreate: models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithProvenance(models.ProvenanceAPI), models.ReceiverMuts.WithIntegrations(slackIntegration, emailIntegration), models.ReceiverMuts.Encrypted(models.Base64Enrypt)),
expectedProvenances: map[string]models.Provenance{slackIntegration.UID: models.ProvenanceAPI, emailIntegration.UID: models.ProvenanceAPI},
},
{
name: "existing receiver fails",
user: writer,
receiver: models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithName("grafana-default-email")),
expectedErr: legacy_storage.ErrReceiverExists,
},
{
name: "create integration with empty UID generates a new UID",
user: writer,
receiver: models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithIntegrations(
models.CopyIntegrationWith(slackIntegration, models.IntegrationMuts.WithUID("")),
models.CopyIntegrationWith(emailIntegration, models.IntegrationMuts.WithUID("")),
)),
expectedCreate: models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithIntegrations(
models.CopyIntegrationWith(slackIntegration, models.IntegrationMuts.WithUID(generated(0))), // Mark UIDs as generated so that test will insert generated UID.
models.CopyIntegrationWith(emailIntegration, models.IntegrationMuts.WithUID(generated(1))),
), models.ReceiverMuts.Encrypted(models.Base64Enrypt)),
expectedProvenances: map[string]models.Provenance{generated(0): models.ProvenanceNone, generated(1): models.ProvenanceNone}, // Mark UIDs as generated so that test will insert generated UID.
},
{
name: "create receiver with non-Grafana origin fails",
user: writer,
receiver: models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithOrigin(models.ResourceOriginImported)),
expectedErr: ErrReceiverOrigin,
},
{
name: "create integration with invalid UID fails",
user: writer,
receiver: models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithIntegrations(
models.CopyIntegrationWith(slackIntegration, models.IntegrationMuts.WithUID("///@#$%^&*(")),
)),
expectedErr: legacy_storage.ErrReceiverInvalid,
},
{
name: "create integration with existing UID fails",
user: writer,
receiver: models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithIntegrations(
models.CopyIntegrationWith(slackIntegration, models.IntegrationMuts.WithUID("UID1")), // UID of grafana-default-email.
)),
expectedErr: legacy_storage.ErrReceiverInvalid,
},
{
name: "create with invalid integration fails",
user: writer,
receiver: models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithInvalidIntegration("slack")),
expectedErr: legacy_storage.ErrReceiverInvalid,
},
{
name: "create integration with no normal settings should not store nil settings",
user: writer,
receiver: models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithIntegrations(
models.CopyIntegrationWith(lineIntegration,
models.IntegrationMuts.WithSettings(
map[string]any{ // Line is valid with only the single secure field "token", so Settings will be empty when saving.
"token": "secret",
},
)),
)),
expectedCreate: models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithIntegrations(
models.CopyIntegrationWith(lineIntegration,
models.IntegrationMuts.WithSettings(
map[string]any{}, // Empty settings, not nil.
),
models.IntegrationMuts.WithSecureSettings(
map[string]string{
"token": "c2VjcmV0", // base64 encoded "secret".
},
),
),
)),
expectedStored: &definitions.PostableApiReceiver{
Receiver: config.Receiver{
Name: lineIntegration.Name,
},
PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
{
UID: lineIntegration.UID,
Name: lineIntegration.Name,
Type: string(lineIntegration.Config.Type()),
DisableResolveMessage: lineIntegration.DisableResolveMessage,
Settings: definitions.RawMessage(`{}`), // Empty settings, not nil.
SecureSettings: map[string]string{
"token": "c2VjcmV0", // base64 encoded "secret".
},
},
},
},
},
},
} {
t.Run(tc.name, func(t *testing.T) {
sut := createReceiverServiceSut(t, &secretsService)
created, err := sut.CreateReceiver(context.Background(), &tc.receiver, tc.user.GetOrgID(), tc.user)
if tc.expectedErr == nil {
require.NoError(t, err)
} else {
assert.ErrorIs(t, err, tc.expectedErr)
return
}
// First verify generated UIDs. We can't compare set them directly in expected because they are generated,
// so we ensure that all empty UIDs in expectedUpdate are not empty in updated.
generatedUIDs := make(map[string]string)
for i, integration := range tc.expectedCreate.Integrations {
if isGenerated(integration.UID) {
// Check that the UID was, in fact, generated.
if created.Integrations[i].UID != "" {
generatedUIDs[integration.UID] = created.Integrations[i].UID
// This ensures the following assert.Equal will pass for this generated field.
integration.UID = created.Integrations[i].UID
}
}
}
if len(generatedUIDs) > 0 {
// Version was calculated without generated UIDs.
tc.expectedCreate.Version = tc.expectedCreate.Fingerprint()
// Set UIDs in expected provenance.
for k, v := range tc.expectedProvenances {
if gen, ok := generatedUIDs[k]; ok {
tc.expectedProvenances[gen] = v
delete(tc.expectedProvenances, k)
}
}
}
assert.Equal(t, tc.expectedCreate, *created)
// Ensure receiver saved to store is correct.
q := models.GetReceiverQuery{OrgID: tc.user.GetOrgID(), Name: tc.receiver.Name, Decrypt: true}
stored, err := sut.GetReceiver(context.Background(), q, decryptUser)
require.NoError(t, err)
decrypted := models.CopyReceiverWith(tc.expectedCreate, models.ReceiverMuts.Decrypted(models.Base64Decrypt))
decrypted.Version = tc.expectedCreate.Version // Version is calculated before decryption.
assert.Equal(t, decrypted, *stored)
if tc.expectedProvenances != nil {
provenances, err := sut.provisioningStore.GetProvenances(context.Background(), tc.user.GetOrgID(), (&definitions.EmbeddedContactPoint{}).ResourceType())
require.NoError(t, err)
assert.Equal(t, tc.expectedProvenances, provenances)
}
if tc.expectedStored != nil {
revision, err := sut.cfgStore.Get(context.Background(), writer.GetOrgID())
require.NoError(t, err)
for _, apiReceiver := range revision.Config.AlertmanagerConfig.Receivers {
if apiReceiver.Name == tc.expectedStored.Name {
assert.Equal(t, tc.expectedStored, apiReceiver)
return
}
}
t.Fatalf("expected to find receiver %q in revision", tc.expectedStored.Name)
}
})
}
}
func TestReceiverService_Update(t *testing.T) {
secretsService := fake_secrets.NewFakeSecretsService()
writer := &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{
1: {
accesscontrol.ActionAlertingNotificationsWrite: nil,
accesscontrol.ActionAlertingNotificationsRead: nil,
},
}}
decryptUser := &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{
1: {
accesscontrol.ActionAlertingReceiversReadSecrets: {ac.ScopeReceiversAll},
},
}}
// Used to mark generated fields to replace during test runtime.
generated := func(n int) string { return fmt.Sprintf("[GENERATED]%d", n) }
isGenerated := func(s string) bool { return strings.HasPrefix(s, "[GENERATED]") }
rm := models.ReceiverMuts
im := models.IntegrationMuts
slackIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("slack"))()
emailIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("email"))()
baseReceiver := models.ReceiverGen(models.ReceiverMuts.WithName("test receiver"), models.ReceiverMuts.WithIntegrations(slackIntegration))()
for _, tc := range []struct {
name string
user identity.Requester
receiver models.Receiver
version string
secureFields map[string][]string
existing *models.Receiver
expectedUpdate models.Receiver
expectedProvenances map[string]models.Provenance
expectedErr error
}{
{
name: "copies existing secure fields",
user: writer,
receiver: models.CopyReceiverWith(baseReceiver, rm.WithIntegrations(
models.CopyIntegrationWith(slackIntegration, im.AddSetting("newField", "newValue"))),
),
secureFields: map[string][]string{slackIntegration.UID: {"token"}},
existing: util.Pointer(models.CopyReceiverWith(baseReceiver, rm.WithIntegrations(
models.CopyIntegrationWith(slackIntegration,
im.AddSecureSetting("token", "ZXhpc3RpbmdUb2tlbg=="), // This will get copied.
im.AddSecureSetting("url", "ZXhpc3RpbmdVcmw="), // This won't get copied.
),
))),
expectedUpdate: models.CopyReceiverWith(baseReceiver, rm.WithIntegrations(
models.CopyIntegrationWith(slackIntegration,
im.AddSetting("newField", "newValue"),
im.AddSecureSetting("token", "ZXhpc3RpbmdUb2tlbg==")),
), rm.Encrypted(models.Base64Enrypt)),
expectedProvenances: map[string]models.Provenance{slackIntegration.UID: models.ProvenanceNone},
},
{
name: "encrypts previously unencrypted secure fields",
user: writer,
receiver: models.CopyReceiverWith(baseReceiver, rm.WithIntegrations(
models.CopyIntegrationWith(slackIntegration, im.AddSetting("token", "unencryptedValue"))),
),
existing: util.Pointer(models.CopyReceiverWith(baseReceiver, rm.WithIntegrations(
models.CopyIntegrationWith(slackIntegration,
im.AddSetting("token", "unencryptedValue"), // This will get encrypted.
),
))),
expectedUpdate: models.CopyReceiverWith(baseReceiver, rm.WithIntegrations(
models.CopyIntegrationWith(slackIntegration,
im.AddSecureSetting("token", "dW5lbmNyeXB0ZWRWYWx1ZQ==")),
), rm.Encrypted(models.Base64Enrypt)),
expectedProvenances: map[string]models.Provenance{slackIntegration.UID: models.ProvenanceNone},
},
{
// This test is important for covering the rare case when an existing field is marked as secure.
// The UI will receive the field as secure and, if unchanged, will pass it back on update as a secureField instead of a Setting.
name: "encrypts previously unencrypted secure fields when passed in as secureFields",
user: writer,
receiver: models.CopyReceiverWith(baseReceiver, rm.WithIntegrations(
models.CopyIntegrationWith(slackIntegration, im.AddSetting("newField", "newValue"))),
),
secureFields: map[string][]string{slackIntegration.UID: {"token"}},
existing: util.Pointer(models.CopyReceiverWith(baseReceiver, rm.WithIntegrations(
models.CopyIntegrationWith(slackIntegration,
im.AddSetting("token", "unencryptedValue"), // This will get encrypted.
),
))),
expectedUpdate: models.CopyReceiverWith(baseReceiver, rm.WithIntegrations(
models.CopyIntegrationWith(slackIntegration,
im.AddSetting("newField", "newValue"),
im.AddSecureSetting("token", "dW5lbmNyeXB0ZWRWYWx1ZQ==")),
), rm.Encrypted(models.Base64Enrypt)),
expectedProvenances: map[string]models.Provenance{slackIntegration.UID: models.ProvenanceNone},
},
{
name: "doesn't copy existing unsecure fields",
user: writer,
receiver: models.CopyReceiverWith(baseReceiver, rm.WithIntegrations(
models.CopyIntegrationWith(slackIntegration, im.AddSetting("newField", "newValue"))),
),
secureFields: map[string][]string{slackIntegration.UID: {"somefield"}},
existing: util.Pointer(models.CopyReceiverWith(baseReceiver, rm.WithIntegrations(
models.CopyIntegrationWith(slackIntegration,
im.AddSetting("somefield", "somevalue"), // This won't get copied.
),
))),
expectedUpdate: models.CopyReceiverWith(baseReceiver, rm.WithIntegrations(
models.CopyIntegrationWith(slackIntegration,
im.AddSetting("newField", "newValue")),
), rm.Encrypted(models.Base64Enrypt)),
expectedProvenances: map[string]models.Provenance{slackIntegration.UID: models.ProvenanceNone},
},
{
name: "creates new provenance when integration is added",
user: writer,
receiver: models.CopyReceiverWith(baseReceiver, rm.WithIntegrations(slackIntegration, emailIntegration), models.ReceiverMuts.WithProvenance(models.ProvenanceFile)),
existing: util.Pointer(models.CopyReceiverWith(baseReceiver, rm.WithIntegrations(slackIntegration), models.ReceiverMuts.WithProvenance(models.ProvenanceFile))),
expectedUpdate: models.CopyReceiverWith(baseReceiver,
rm.WithIntegrations(slackIntegration, emailIntegration),
models.ReceiverMuts.WithProvenance(models.ProvenanceFile),
rm.Encrypted(models.Base64Enrypt)),
expectedProvenances: map[string]models.Provenance{slackIntegration.UID: models.ProvenanceFile, emailIntegration.UID: models.ProvenanceFile},
},
{
name: "deletes old provenance when integration is removed",
user: writer,
receiver: models.CopyReceiverWith(baseReceiver, rm.WithIntegrations(slackIntegration), models.ReceiverMuts.WithProvenance(models.ProvenanceFile)),
existing: util.Pointer(models.CopyReceiverWith(baseReceiver, rm.WithIntegrations(slackIntegration, emailIntegration), models.ReceiverMuts.WithProvenance(models.ProvenanceFile))),
expectedUpdate: models.CopyReceiverWith(baseReceiver,
rm.WithIntegrations(slackIntegration),
models.ReceiverMuts.WithProvenance(models.ProvenanceFile),
rm.Encrypted(models.Base64Enrypt)),
expectedProvenances: map[string]models.Provenance{slackIntegration.UID: models.ProvenanceFile},
},
{
name: "changing provenance from something to None fails",
user: writer,
receiver: models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithProvenance(models.ProvenanceNone)),
existing: util.Pointer(models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithProvenance(models.ProvenanceFile))),
expectedErr: validation.MakeErrProvenanceChangeNotAllowed(models.ProvenanceFile, models.ProvenanceNone),
},
{
name: "changing provenance from one type to another fails", // TODO: This should fail once we move from lenient to strict validation.
user: writer,
receiver: models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithProvenance(models.ProvenanceAPI)),
existing: util.Pointer(models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithProvenance(models.ProvenanceFile))),
// expectedErr: validation.MakeErrProvenanceChangeNotAllowed(models.ProvenanceFile, models.ProvenanceAPI),
expectedUpdate: models.CopyReceiverWith(baseReceiver,
models.ReceiverMuts.WithProvenance(models.ProvenanceAPI),
rm.Encrypted(models.Base64Enrypt)),
expectedProvenances: map[string]models.Provenance{slackIntegration.UID: models.ProvenanceAPI},
},
{
name: "update receiver with optimistic version mismatch fails",
user: writer,
receiver: baseReceiver.Clone(),
version: "wrong version",
existing: util.Pointer(baseReceiver.Clone()),
expectedErr: ErrReceiverVersionConflict,
},
{
name: "update receiver that doesn't exist fails",
user: writer,
receiver: baseReceiver.Clone(),
expectedErr: legacy_storage.ErrReceiverNotFound,
},
{
name: "update that adds new integration generates a new UID",
user: writer,
receiver: models.CopyReceiverWith(baseReceiver, rm.WithIntegrations(slackIntegration, models.CopyIntegrationWith(emailIntegration, im.WithUID("")))),
existing: util.Pointer(baseReceiver.Clone()),
expectedUpdate: models.CopyReceiverWith(baseReceiver,
rm.WithIntegrations(slackIntegration, models.CopyIntegrationWith(emailIntegration, im.WithUID(generated(0)))), // Mark UID as generated so that test will insert generated UID.
rm.Encrypted(models.Base64Enrypt)),
expectedProvenances: map[string]models.Provenance{slackIntegration.UID: models.ProvenanceNone, generated(0): models.ProvenanceNone}, // Mark UID as generated so that test will insert generated UID.
},
{
name: "update with integration that has a UID that already exists fails",
user: writer,
receiver: models.CopyReceiverWith(baseReceiver, rm.WithIntegrations(slackIntegration, models.CopyIntegrationWith(emailIntegration, im.WithUID(slackIntegration.UID)))),
existing: util.Pointer(baseReceiver.Clone()),
expectedErr: legacy_storage.ErrReceiverInvalid,
},
{
name: "update with invalid integration fails",
user: writer,
receiver: models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithInvalidIntegration("slack")),
existing: util.Pointer(baseReceiver.Clone()),
expectedErr: legacy_storage.ErrReceiverInvalid,
},
} {
t.Run(tc.name, func(t *testing.T) {
sut := createReceiverServiceSut(t, &secretsService)
if tc.existing != nil {
// Create route after receivers as they will be referenced.
revision, err := sut.cfgStore.Get(context.Background(), tc.user.GetOrgID())
require.NoError(t, err)
created, err := revision.CreateReceiver(tc.existing)
require.NoError(t, err)
err = sut.cfgStore.Save(context.Background(), revision, tc.user.GetOrgID())
require.NoError(t, err)
for _, integration := range created.Integrations {
target := definitions.EmbeddedContactPoint{UID: integration.UID}
err = sut.provisioningStore.SetProvenance(context.Background(), &target, tc.user.GetOrgID(), created.Provenance)
require.NoError(t, err)
}
if tc.version == "" {
tc.version = created.Version
}
}
tc.receiver.Version = tc.version
updated, err := sut.UpdateReceiver(context.Background(), &tc.receiver, tc.secureFields, tc.user.GetOrgID(), tc.user)
if tc.expectedErr == nil {
require.NoError(t, err)
} else {
assert.ErrorIs(t, err, tc.expectedErr)
return
}
// First verify generated UIDs. We can't compare set them directly in expected because they are generated,
// so we ensure that all empty UIDs in expectedUpdate are not empty in updated.
generatedUIDs := make(map[string]string)
for i, integration := range tc.expectedUpdate.Integrations {
if isGenerated(integration.UID) {
// Check that the UID was, in fact, generated.
if updated.Integrations[i].UID != "" {
generatedUIDs[integration.UID] = updated.Integrations[i].UID
// This ensures the following assert.Equal will pass for this generated field.
integration.UID = updated.Integrations[i].UID
}
}
}
if len(generatedUIDs) > 0 {
// Version was calculated without generated UIDs.
tc.expectedUpdate.Version = tc.expectedUpdate.Fingerprint()
// Set UIDs in expected provenance.
for k, v := range tc.expectedProvenances {
if gen, ok := generatedUIDs[k]; ok {
tc.expectedProvenances[gen] = v
delete(tc.expectedProvenances, k)
}
}
}
assert.Equal(t, tc.expectedUpdate, *updated)
// Ensure receiver saved to store is correct.
q := models.GetReceiverQuery{OrgID: tc.user.GetOrgID(), Name: tc.receiver.Name, Decrypt: true}
stored, err := sut.GetReceiver(context.Background(), q, decryptUser)
require.NoError(t, err)
decrypted := models.CopyReceiverWith(tc.expectedUpdate, models.ReceiverMuts.Decrypted(models.Base64Decrypt))
decrypted.Version = tc.expectedUpdate.Version // Version is calculated before decryption.
assert.Equal(t, decrypted, *stored)
provenances, err := sut.provisioningStore.GetProvenances(context.Background(), tc.user.GetOrgID(), (&definitions.EmbeddedContactPoint{}).ResourceType())
require.NoError(t, err)
assert.Equal(t, tc.expectedProvenances, provenances)
})
}
}
func TestReceiverService_UpdateReceiverName(t *testing.T) {
// This test is to ensure that the receiver name is updated in routes and notification settings when the name is changed.
writer := &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{
1: {
accesscontrol.ActionAlertingNotificationsWrite: nil,
accesscontrol.ActionAlertingNotificationsRead: nil,
},
}}
secretsService := fake_secrets.NewFakeSecretsService()
receiverName := "grafana-default-email"
newReceiverName := "new-name"
slackIntegration := models.IntegrationGen(models.IntegrationMuts.WithName(receiverName), models.IntegrationMuts.WithValidConfig("slack"))()
baseReceiver := models.ReceiverGen(models.ReceiverMuts.WithName(receiverName), models.ReceiverMuts.WithIntegrations(slackIntegration))()
baseReceiver.Version = "cd95627c75892a39" // Correct version for grafana-default-email.
baseReceiver.Name = newReceiverName // Done here instead of in a mutator so we keep the same uid.
t.Run("renames receiver and all its dependencies", func(t *testing.T) {
ruleStore := &fakeAlertRuleNotificationStore{}
sut := createReceiverServiceSut(t, &secretsService)
sut.ruleNotificationsStore = ruleStore
_, err := sut.UpdateReceiver(context.Background(), &baseReceiver, nil, writer.GetOrgID(), writer)
require.NoError(t, err)
assert.Equal(t, "RenameReceiverInNotificationSettings", ruleStore.Calls[0].Method)
assert.Equal(t, writer.OrgID, ruleStore.Calls[0].Args[1])
assert.Equal(t, receiverName, ruleStore.Calls[0].Args[2])
assert.Equal(t, newReceiverName, ruleStore.Calls[0].Args[3])
assert.NotNil(t, ruleStore.Calls[0].Args[4])
assert.Falsef(t, ruleStore.Calls[0].Args[5].(bool), "dryrun expected to be false")
// Ensure receiver name is updated in routes.
revision, err := sut.cfgStore.Get(context.Background(), writer.GetOrgID())
require.NoError(t, err)
assert.Falsef(t, revision.ReceiverNameUsedByRoutes(receiverName), "old receiver name '%s' should not be used by routes", receiverName)
assert.Truef(t, revision.ReceiverNameUsedByRoutes(newReceiverName), "new receiver name '%s' should be used by routes", newReceiverName)
})
t.Run("returns ErrReceiverDependentResourcesProvenance if route has different provenance status", func(t *testing.T) {
sut := createReceiverServiceSut(t, &secretsService)
provenanceStore := sut.provisioningStore.(*fakes.FakeProvisioningStore)
provenanceStore.Records[1] = map[string]models.Provenance{
(&definitions.Route{}).ResourceType(): models.ProvenanceFile,
}
ruleStore := &fakeAlertRuleNotificationStore{
RenameReceiverInNotificationSettingsFn: func(ctx context.Context, orgID int64, old, new string, validate func(models.Provenance) bool, dryRun bool) ([]models.AlertRuleKey, []models.AlertRuleKey, error) {
assertInTransaction(t, ctx)
return nil, nil, nil
},
}
sut.ruleNotificationsStore = ruleStore
_, err := sut.UpdateReceiver(context.Background(), &baseReceiver, nil, writer.GetOrgID(), writer)
require.ErrorIs(t, err, ErrReceiverDependentResourcesProvenance)
require.Len(t, ruleStore.Calls, 1)
assert.Equal(t, "RenameReceiverInNotificationSettings", ruleStore.Calls[0].Method)
assert.Equal(t, writer.OrgID, ruleStore.Calls[0].Args[1])
assert.Equal(t, receiverName, ruleStore.Calls[0].Args[2])
assert.Equal(t, newReceiverName, ruleStore.Calls[0].Args[3])
assert.NotNil(t, ruleStore.Calls[0].Args[4])
assert.True(t, ruleStore.Calls[0].Args[5].(bool)) // still check if there are rules that have incompatible provenance
})
t.Run("returns ErrReceiverDependentResourcesProvenance if rules have different provenance status", func(t *testing.T) {
sut := createReceiverServiceSut(t, &secretsService)
ruleStore := &fakeAlertRuleNotificationStore{
RenameReceiverInNotificationSettingsFn: func(ctx context.Context, orgID int64, old, new string, validate func(models.Provenance) bool, dryRun bool) ([]models.AlertRuleKey, []models.AlertRuleKey, error) {
assertInTransaction(t, ctx)
return nil, []models.AlertRuleKey{models.GenerateRuleKey(orgID)}, nil
},
}
sut.ruleNotificationsStore = ruleStore
_, err := sut.UpdateReceiver(context.Background(), &baseReceiver, nil, writer.GetOrgID(), writer)
require.ErrorIs(t, err, ErrReceiverDependentResourcesProvenance)
require.Len(t, ruleStore.Calls, 1)
assert.Equal(t, "RenameReceiverInNotificationSettings", ruleStore.Calls[0].Method)
assert.Equal(t, writer.OrgID, ruleStore.Calls[0].Args[1])
assert.Equal(t, receiverName, ruleStore.Calls[0].Args[2])
assert.Equal(t, newReceiverName, ruleStore.Calls[0].Args[3])
assert.NotNil(t, ruleStore.Calls[0].Args[4])
assert.Falsef(t, ruleStore.Calls[0].Args[5].(bool), "dryrun expected to be false")
})
}
func TestReceiverServiceAC_Read(t *testing.T) {
var orgId int64 = 1
secretsService := fake_secrets.NewFakeSecretsService()
admin := &user.SignedInUser{OrgID: orgId, OrgRole: org.RoleAdmin, Permissions: map[int64]map[string][]string{
orgId: {
accesscontrol.ActionAlertingNotificationsWrite: nil,
accesscontrol.ActionAlertingNotificationsRead: nil,
},
}}
slackIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("slack"))
emailIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("email"))
recv1 := models.ReceiverGen(models.ReceiverMuts.WithName("receiver1"), models.ReceiverMuts.WithIntegrations(slackIntegration(), emailIntegration()))()
recv2 := models.ReceiverGen(models.ReceiverMuts.WithName("receiver2"), models.ReceiverMuts.WithIntegrations(slackIntegration(), emailIntegration()))()
recv3 := models.ReceiverGen(models.ReceiverMuts.WithName("receiver with a really long name that surpasses 40 characters"), models.ReceiverMuts.WithIntegrations(slackIntegration(), emailIntegration()))()
allReceivers := func() []models.Receiver {
return []models.Receiver{recv1, recv2, recv3}
}
testCases := []struct {
name string
permissions map[string][]string
existing []models.Receiver
visible []models.Receiver
visibleWithProvisioning []models.Receiver
}{
{
name: "not authorized without permissions",
existing: allReceivers(),
visible: nil,
},
{
name: "not authorized without receivers scope",
permissions: map[string][]string{accesscontrol.ActionAlertingReceiversRead: nil},
existing: allReceivers(),
visible: nil,
},
{
name: "global legacy permissions - read all",
permissions: map[string][]string{accesscontrol.ActionAlertingNotificationsRead: nil},
existing: allReceivers(),
visible: allReceivers(),
},
{
name: "global receivers permissions - read all",
permissions: map[string][]string{accesscontrol.ActionAlertingReceiversRead: {ac.ScopeReceiversAll}},
existing: allReceivers(),
visible: allReceivers(),
},
{
name: "single receivers permissions - read some",
permissions: map[string][]string{accesscontrol.ActionAlertingReceiversRead: {
ac.ScopeReceiversProvider.GetResourceScopeUID(recv1.UID),
ac.ScopeReceiversProvider.GetResourceScopeUID(recv3.UID),
}},
existing: allReceivers(),
visible: []models.Receiver{recv1, recv3},
},
{
name: "global receivers secret permissions - read all",
permissions: map[string][]string{accesscontrol.ActionAlertingReceiversReadSecrets: {ac.ScopeReceiversAll}},
existing: allReceivers(),
visible: allReceivers(),
},
{
name: "single receivers secret permissions - read some",
permissions: map[string][]string{accesscontrol.ActionAlertingReceiversReadSecrets: {
ac.ScopeReceiversProvider.GetResourceScopeUID(recv1.UID),
ac.ScopeReceiversProvider.GetResourceScopeUID(recv3.UID),
}},
existing: allReceivers(),
visible: []models.Receiver{recv1, recv3},
},
{
name: "provisioning read applies to only provisioning",
permissions: map[string][]string{accesscontrol.ActionAlertingProvisioningRead: nil},
existing: allReceivers(),
visible: nil,
visibleWithProvisioning: allReceivers(),
},
{
name: "provisioning read secrets applies to only provisioning",
permissions: map[string][]string{accesscontrol.ActionAlertingProvisioningReadSecrets: nil},
existing: allReceivers(),
visible: nil,
visibleWithProvisioning: allReceivers(),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
sut := createReceiverServiceSut(t, &secretsService)
for _, recv := range tc.existing {
_, err := sut.CreateReceiver(context.Background(), &recv, orgId, admin)
require.NoError(t, err)
}
usr := &user.SignedInUser{OrgID: orgId, Permissions: map[int64]map[string][]string{
orgId: tc.permissions,
}}
isVisible := func(uid string) bool {
for _, recv := range tc.visible {
if recv.UID == uid {
return true
}
}
return false
}
for _, recv := range allReceivers() {
response, err := sut.GetReceiver(context.Background(), singleQ(orgId, recv.Name), usr)
if isVisible(recv.UID) {
require.NoErrorf(t, err, "receiver '%s' should be visible, but isn't", recv.Name)
assert.NotNil(t, response)
} else {
assert.ErrorIsf(t, err, ac.ErrAuthorizationBase, "receiver '%s' should not be visible, but is", recv.Name)
}
}
isVisibleInProvisioning := func(uid string) bool {
if tc.visibleWithProvisioning == nil {
return isVisible(uid)
}
for _, recv := range tc.visibleWithProvisioning {
if recv.UID == uid {
return true
}
}
return false
}
sut.authz = ac.NewReceiverAccess[*models.Receiver](acimpl.ProvideAccessControl(featuremgmt.WithFeatures()), true)
for _, recv := range allReceivers() {
response, err := sut.GetReceiver(context.Background(), singleQ(orgId, recv.Name), usr)
if isVisibleInProvisioning(recv.UID) {
require.NoErrorf(t, err, "receiver '%s' should be visible, but isn't", recv.Name)
assert.NotNil(t, response)
} else {
assert.ErrorIsf(t, err, ac.ErrAuthorizationBase, "receiver '%s' should not be visible, but is", recv.Name)
}
}
})
}
}
func TestReceiverServiceAC_Create(t *testing.T) {
var orgId int64 = 1
secretsService := fake_secrets.NewFakeSecretsService()
slackIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("slack"))
emailIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("email"))
recv1 := models.ReceiverGen(models.ReceiverMuts.WithName("receiver1"), models.ReceiverMuts.WithIntegrations(slackIntegration(), emailIntegration()))()
recv2 := models.ReceiverGen(models.ReceiverMuts.WithName("receiver2"), models.ReceiverMuts.WithIntegrations(slackIntegration(), emailIntegration()))()
recv3 := models.ReceiverGen(models.ReceiverMuts.WithName("receiver with a really long name that surpasses 40 characters"), models.ReceiverMuts.WithIntegrations(slackIntegration(), emailIntegration()))()
allReceivers := func() []models.Receiver {
return []models.Receiver{recv1, recv2, recv3}
}
testCases := []struct {
name string
permissions map[string][]string
hasAccess []models.Receiver
}{
{
name: "not authorized without permissions",
hasAccess: nil,
},
{
name: "global legacy permissions - authorized without read",
permissions: map[string][]string{accesscontrol.ActionAlertingNotificationsWrite: nil},
hasAccess: allReceivers(),
},
{
name: "receivers permissions - authorized without read",
permissions: map[string][]string{accesscontrol.ActionAlertingReceiversCreate: nil},
hasAccess: allReceivers(),
},
{
name: "global legacy permissions - create all",
permissions: map[string][]string{accesscontrol.ActionAlertingNotificationsWrite: nil, accesscontrol.ActionAlertingNotificationsRead: nil},
hasAccess: allReceivers(),
},
{
name: "receivers permissions - create all",
permissions: map[string][]string{accesscontrol.ActionAlertingReceiversCreate: nil, accesscontrol.ActionAlertingReceiversRead: nil},
hasAccess: allReceivers(),
},
{
name: "receivers mixed global read permissions - create all",
permissions: map[string][]string{
accesscontrol.ActionAlertingReceiversCreate: nil,
accesscontrol.ActionAlertingNotificationsRead: nil,
},
hasAccess: allReceivers(),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
sut := createReceiverServiceSut(t, &secretsService)
usr := &user.SignedInUser{OrgID: orgId, Permissions: map[int64]map[string][]string{
orgId: tc.permissions,
}}
hasAccess := func(uid string) bool {
for _, recv := range tc.hasAccess {
if recv.UID == uid {
return true
}
}
return false
}
for _, recv := range allReceivers() {
response, err := sut.CreateReceiver(context.Background(), &recv, orgId, usr)
if hasAccess(recv.UID) {
require.NoErrorf(t, err, "should have access to receiver '%s', but doesn't", recv.Name)
assert.NotNil(t, response)
} else {
assert.ErrorIsf(t, err, ac.ErrAuthorizationBase, "should not have access to receiver '%s', but does", recv.Name)
}
}
})
}
}
func TestReceiverServiceAC_Update(t *testing.T) {
var orgId int64 = 1
secretsService := fake_secrets.NewFakeSecretsService()
admin := &user.SignedInUser{OrgID: orgId, OrgRole: org.RoleAdmin, Permissions: map[int64]map[string][]string{
orgId: {
accesscontrol.ActionAlertingNotificationsWrite: nil,
accesscontrol.ActionAlertingNotificationsRead: nil,
},
}}
slackIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("slack"))
emailIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("email"))
recv1 := models.ReceiverGen(models.ReceiverMuts.WithName("receiver1"), models.ReceiverMuts.WithIntegrations(slackIntegration(), emailIntegration()))()
recv2 := models.ReceiverGen(models.ReceiverMuts.WithName("receiver2"), models.ReceiverMuts.WithIntegrations(slackIntegration(), emailIntegration()))()
recv3 := models.ReceiverGen(models.ReceiverMuts.WithName("receiver with a really long name that surpasses 40 characters"), models.ReceiverMuts.WithIntegrations(slackIntegration(), emailIntegration()))()
allReceivers := func() []models.Receiver {
return []models.Receiver{recv1, recv2, recv3}
}
testCases := []struct {
name string
permissions map[string][]string
existing []models.Receiver
hasAccess []models.Receiver
}{
{
name: "not authorized without permissions",
existing: allReceivers(),
hasAccess: nil,
},
{
name: "not authorized without receivers scope",
permissions: map[string][]string{accesscontrol.ActionAlertingReceiversUpdate: nil},
existing: allReceivers(),
hasAccess: nil,
},
{
name: "global legacy permissions - not authorized without read",
permissions: map[string][]string{accesscontrol.ActionAlertingNotificationsWrite: nil},
existing: allReceivers(),
hasAccess: nil,
},
{
name: "global receivers permissions - not authorized without read",
permissions: map[string][]string{accesscontrol.ActionAlertingReceiversUpdate: {ac.ScopeReceiversAll}},
existing: allReceivers(),
hasAccess: nil,
},
{
name: "single receivers permissions - not authorized without read",
permissions: map[string][]string{accesscontrol.ActionAlertingReceiversUpdate: {
ac.ScopeReceiversProvider.GetResourceScopeUID(recv1.UID),
ac.ScopeReceiversProvider.GetResourceScopeUID(recv3.UID),
}},
existing: allReceivers(),
hasAccess: nil,
},
{
name: "global legacy permissions - update all",
permissions: map[string][]string{accesscontrol.ActionAlertingNotificationsWrite: nil, accesscontrol.ActionAlertingNotificationsRead: nil},
existing: allReceivers(),
hasAccess: allReceivers(),
},
{
name: "global receivers permissions - update all",
permissions: map[string][]string{accesscontrol.ActionAlertingReceiversUpdate: {ac.ScopeReceiversAll}, accesscontrol.ActionAlertingReceiversRead: {ac.ScopeReceiversAll}},
existing: allReceivers(),
hasAccess: allReceivers(),
},
{
name: "single receivers permissions - update some",
permissions: map[string][]string{
accesscontrol.ActionAlertingReceiversUpdate: {
ac.ScopeReceiversProvider.GetResourceScopeUID(recv1.UID),
ac.ScopeReceiversProvider.GetResourceScopeUID(recv3.UID),
},
accesscontrol.ActionAlertingReceiversRead: {
ac.ScopeReceiversProvider.GetResourceScopeUID(recv1.UID),
ac.ScopeReceiversProvider.GetResourceScopeUID(recv3.UID),
},
},
existing: allReceivers(),
hasAccess: []models.Receiver{recv1, recv3},
},
{
name: "single receivers mixed read permissions - update some",
permissions: map[string][]string{
accesscontrol.ActionAlertingReceiversUpdate: {
ac.ScopeReceiversProvider.GetResourceScopeUID(recv1.UID),
ac.ScopeReceiversProvider.GetResourceScopeUID(recv3.UID),
},
accesscontrol.ActionAlertingReceiversRead: {
ac.ScopeReceiversProvider.GetResourceScopeUID(recv2.UID),
ac.ScopeReceiversProvider.GetResourceScopeUID(recv3.UID),
},
},
existing: allReceivers(),
hasAccess: []models.Receiver{recv3},
},
{
name: "single receivers mixed global read permissions - update some",
permissions: map[string][]string{
accesscontrol.ActionAlertingReceiversUpdate: {
ac.ScopeReceiversProvider.GetResourceScopeUID(recv1.UID),
ac.ScopeReceiversProvider.GetResourceScopeUID(recv3.UID),
},
accesscontrol.ActionAlertingNotificationsRead: nil,
},
existing: allReceivers(),
hasAccess: []models.Receiver{recv1, recv3},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
sut := createReceiverServiceSut(t, &secretsService)
versions := map[string]string{}
for _, recv := range tc.existing {
created, err := sut.CreateReceiver(context.Background(), &recv, orgId, admin)
require.NoError(t, err)
versions[recv.UID] = created.Version
}
usr := &user.SignedInUser{OrgID: orgId, Permissions: map[int64]map[string][]string{
orgId: tc.permissions,
}}
hasAccess := func(uid string) bool {
for _, recv := range tc.hasAccess {
if recv.UID == uid {
return true
}
}
return false
}
for _, recv := range allReceivers() {
clone := recv.Clone()
clone.Version = versions[recv.UID]
response, err := sut.UpdateReceiver(context.Background(), &clone, nil, orgId, usr)
if hasAccess(clone.UID) {
require.NoErrorf(t, err, "should have access to receiver '%s', but doesn't", clone.Name)
assert.NotNil(t, response)
} else {
assert.ErrorIsf(t, err, ac.ErrAuthorizationBase, "should not have access to receiver '%s', but does", clone.Name)
}
}
})
}
}
func TestReceiverServiceAC_Delete(t *testing.T) {
var orgId int64 = 1
secretsService := fake_secrets.NewFakeSecretsService()
admin := &user.SignedInUser{OrgID: orgId, OrgRole: org.RoleAdmin, Permissions: map[int64]map[string][]string{
orgId: {
accesscontrol.ActionAlertingNotificationsWrite: nil,
accesscontrol.ActionAlertingNotificationsRead: nil,
},
}}
slackIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("slack"))
emailIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("email"))
recv1 := models.ReceiverGen(models.ReceiverMuts.WithName("receiver1"), models.ReceiverMuts.WithIntegrations(slackIntegration(), emailIntegration()))()
recv2 := models.ReceiverGen(models.ReceiverMuts.WithName("receiver2"), models.ReceiverMuts.WithIntegrations(slackIntegration(), emailIntegration()))()
recv3 := models.ReceiverGen(models.ReceiverMuts.WithName("receiver with a really long name that surpasses 40 characters"), models.ReceiverMuts.WithIntegrations(slackIntegration(), emailIntegration()))()
allReceivers := func() []models.Receiver {
return []models.Receiver{recv1, recv2, recv3}
}
testCases := []struct {
name string
permissions map[string][]string
existing []models.Receiver
hasAccess []models.Receiver
}{
{
name: "not authorized without permissions",
existing: allReceivers(),
hasAccess: nil,
},
{
name: "not authorized without receivers scope",
permissions: map[string][]string{accesscontrol.ActionAlertingReceiversDelete: nil},
existing: allReceivers(),
hasAccess: nil,
},
{
name: "global legacy permissions - not authorized without read",
permissions: map[string][]string{accesscontrol.ActionAlertingNotificationsWrite: nil},
existing: allReceivers(),
hasAccess: nil,
},
{
name: "global receivers permissions - not authorized without read",
permissions: map[string][]string{accesscontrol.ActionAlertingReceiversDelete: {ac.ScopeReceiversAll}},
existing: allReceivers(),
hasAccess: nil,
},
{
name: "single receivers permissions - not authorized without read",
permissions: map[string][]string{accesscontrol.ActionAlertingReceiversDelete: {
ac.ScopeReceiversProvider.GetResourceScopeUID(recv1.UID),
ac.ScopeReceiversProvider.GetResourceScopeUID(recv3.UID),
}},
existing: allReceivers(),
hasAccess: nil,
},
{
name: "global legacy permissions - delete all",
permissions: map[string][]string{accesscontrol.ActionAlertingNotificationsWrite: nil, accesscontrol.ActionAlertingNotificationsRead: nil},
existing: allReceivers(),
hasAccess: allReceivers(),
},
{
name: "global receivers permissions - delete all",
permissions: map[string][]string{accesscontrol.ActionAlertingReceiversDelete: {ac.ScopeReceiversAll}, accesscontrol.ActionAlertingReceiversRead: {ac.ScopeReceiversAll}},
existing: allReceivers(),
hasAccess: allReceivers(),
},
{
name: "single receivers permissions - delete some",
permissions: map[string][]string{
accesscontrol.ActionAlertingReceiversDelete: {
ac.ScopeReceiversProvider.GetResourceScopeUID(recv1.UID),
ac.ScopeReceiversProvider.GetResourceScopeUID(recv3.UID),
},
accesscontrol.ActionAlertingReceiversRead: {
ac.ScopeReceiversProvider.GetResourceScopeUID(recv1.UID),
ac.ScopeReceiversProvider.GetResourceScopeUID(recv3.UID),
},
},
existing: allReceivers(),
hasAccess: []models.Receiver{recv1, recv3},
},
{
name: "single receivers mixed read permissions - delete some",
permissions: map[string][]string{
accesscontrol.ActionAlertingReceiversDelete: {
ac.ScopeReceiversProvider.GetResourceScopeUID(recv1.UID),
ac.ScopeReceiversProvider.GetResourceScopeUID(recv3.UID),
},
accesscontrol.ActionAlertingReceiversRead: {
ac.ScopeReceiversProvider.GetResourceScopeUID(recv2.UID),
ac.ScopeReceiversProvider.GetResourceScopeUID(recv3.UID),
},
},
existing: allReceivers(),
hasAccess: []models.Receiver{recv3},
},
{
name: "single receivers mixed global read permissions - delete some",
permissions: map[string][]string{
accesscontrol.ActionAlertingReceiversDelete: {
ac.ScopeReceiversProvider.GetResourceScopeUID(recv1.UID),
ac.ScopeReceiversProvider.GetResourceScopeUID(recv3.UID),
},
accesscontrol.ActionAlertingNotificationsRead: nil,
},
existing: allReceivers(),
hasAccess: []models.Receiver{recv1, recv3},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
sut := createReceiverServiceSut(t, &secretsService)
versions := map[string]string{}
for _, recv := range tc.existing {
created, err := sut.CreateReceiver(context.Background(), &recv, orgId, admin)
require.NoError(t, err)
versions[recv.UID] = created.Version
}
usr := &user.SignedInUser{OrgID: orgId, Permissions: map[int64]map[string][]string{
orgId: tc.permissions,
}}
hasAccess := func(uid string) bool {
for _, recv := range tc.hasAccess {
if recv.UID == uid {
return true
}
}
return false
}
for _, recv := range allReceivers() {
err := sut.DeleteReceiver(context.Background(), recv.UID, models.ProvenanceNone, versions[recv.UID], orgId, usr)
if hasAccess(recv.UID) {
require.NoErrorf(t, err, "should have access to receiver '%s', but doesn't", recv.Name)
} else {
assert.ErrorIsf(t, err, ac.ErrAuthorizationBase, "should not have access to receiver '%s', but does", recv.Name)
}
}
})
}
}
func TestReceiverService_InUseMetadata(t *testing.T) {
secretsService := fake_secrets.NewFakeSecretsService()
admin := &user.SignedInUser{OrgID: 1, OrgRole: org.RoleAdmin, Permissions: map[int64]map[string][]string{
1: {
accesscontrol.ActionAlertingNotificationsWrite: nil,
accesscontrol.ActionAlertingNotificationsRead: nil,
},
}}
for _, tc := range []struct {
name string
user identity.Requester
storeRoute definitions.Route
storeSettings map[models.AlertRuleKey][]models.NotificationSettings
existing []*models.Receiver
expectedMetadata map[string]models.ReceiverMetadata
}{
{
name: "mixed metadata",
user: admin,
existing: []*models.Receiver{
util.Pointer(models.ReceiverGen(models.ReceiverMuts.WithName("receiver1"))()),
util.Pointer(models.ReceiverGen(models.ReceiverMuts.WithName("receiver2"))()),
util.Pointer(models.ReceiverGen(models.ReceiverMuts.WithName("receiver3"))()),
util.Pointer(models.ReceiverGen(models.ReceiverMuts.WithName("receiver4"))()),
},
storeSettings: map[models.AlertRuleKey][]models.NotificationSettings{
{OrgID: 1, UID: "rule1uid"}: {
models.NotificationSettingsGen(models.NSMuts.WithReceiver("receiver1"))(),
models.NotificationSettingsGen(models.NSMuts.WithReceiver("receiver2"))(),
},
{OrgID: 1, UID: "rule2uid"}: {
models.NotificationSettingsGen(models.NSMuts.WithReceiver("receiver2"))(),
models.NotificationSettingsGen(models.NSMuts.WithReceiver("receiver3"))(),
},
},
storeRoute: definitions.Route{
Receiver: "receiver1",
Routes: []*definitions.Route{
{Receiver: "receiver2"},
{Receiver: "receiver3"},
{
Receiver: "receiver4",
Routes: []*definitions.Route{
{Receiver: "receiver1"},
{Receiver: "receiver3"},
},
},
},
},
expectedMetadata: map[string]models.ReceiverMetadata{
legacy_storage.NameToUid("receiver1"): {
InUseByRules: []models.AlertRuleKey{{OrgID: 1, UID: "rule1uid"}},
InUseByRoutes: 2,
CanUse: true,
},
legacy_storage.NameToUid("receiver2"): {
InUseByRules: []models.AlertRuleKey{{OrgID: 1, UID: "rule1uid"}, {OrgID: 1, UID: "rule2uid"}},
InUseByRoutes: 1,
CanUse: true,
},
legacy_storage.NameToUid("receiver3"): {
InUseByRules: []models.AlertRuleKey{{OrgID: 1, UID: "rule2uid"}},
InUseByRoutes: 2,
CanUse: true,
},
legacy_storage.NameToUid("receiver4"): {
InUseByRules: []models.AlertRuleKey{},
InUseByRoutes: 1,
CanUse: true,
},
},
},
} {
t.Run(tc.name, func(t *testing.T) {
store := &fakeAlertRuleNotificationStore{}
store.ListNotificationSettingsFn = func(ctx context.Context, q models.ListNotificationSettingsQuery) (map[models.AlertRuleKey][]models.NotificationSettings, error) {
return tc.storeSettings, nil
}
sut := createReceiverServiceSut(t, &secretsService)
sut.ruleNotificationsStore = store
for _, recv := range tc.existing {
_, err := sut.CreateReceiver(context.Background(), recv, tc.user.GetOrgID(), tc.user)
require.NoError(t, err)
}
// Create route after receivers as they will be referenced.
revision, err := sut.cfgStore.Get(context.Background(), tc.user.GetOrgID())
require.NoError(t, err)
revision.Config.AlertmanagerConfig.Route = &tc.storeRoute
err = sut.cfgStore.Save(context.Background(), revision, tc.user.GetOrgID())
require.NoError(t, err)
metadata, err := sut.InUseMetadata(context.Background(), tc.user.GetOrgID(), tc.existing...)
require.NoError(t, err)
assert.Lenf(t, metadata, len(tc.expectedMetadata), "unexpected metadata length")
for _, recv := range tc.existing {
expected, ok := tc.expectedMetadata[recv.UID]
assert.Truef(t, ok, "missing metadata for receiver uid: %q, name: %q", recv.UID, recv.Name)
assert.ElementsMatch(t, expected.InUseByRules, metadata[recv.UID].InUseByRules, "unexpected rules metadata for receiver uid: %q, name: %q", recv.UID, recv.Name)
assert.Equalf(t, expected.InUseByRoutes, metadata[recv.UID].InUseByRoutes, "unexpected routes metadata for receiver uid: %q, name: %q", recv.UID, recv.Name)
}
})
}
}
func createReceiverServiceSut(t *testing.T, encryptSvc secretService) *ReceiverService {
cfg := createEncryptedConfig(t, encryptSvc)
store := fakes.NewFakeAlertmanagerConfigStore(cfg)
xact := newNopTransactionManager()
provisioningStore := fakes.NewFakeProvisioningStore()
return NewReceiverService(
ac.NewReceiverAccess[*models.Receiver](acimpl.ProvideAccessControl(featuremgmt.WithFeatures()), false),
legacy_storage.NewAlertmanagerConfigStore(store, NewExtraConfigsCrypto(encryptSvc)),
provisioningStore,
&fakeAlertRuleNotificationStore{},
encryptSvc,
xact,
log.NewNopLogger(),
fakes.NewFakeReceiverPermissionsService(),
tracing.InitializeTracerForTest(),
)
}
func createEncryptedConfig(t *testing.T, secretService secretService) string {
c := &definitions.PostableUserConfig{}
err := json.Unmarshal([]byte(defaultAlertmanagerConfigJSON), c)
require.NoError(t, err)
err = EncryptReceiverConfigs(c.AlertmanagerConfig.Receivers, func(ctx context.Context, payload []byte) ([]byte, error) {
return secretService.Encrypt(ctx, payload, secrets.WithoutScope())
})
require.NoError(t, err)
bytes, err := json.Marshal(c)
require.NoError(t, err)
return string(bytes)
}
func singleQ(orgID int64, name string) models.GetReceiverQuery {
return models.GetReceiverQuery{
OrgID: orgID,
Name: name,
}
}
func multiQ(orgID int64, names ...string) models.GetReceiversQuery {
return models.GetReceiversQuery{
OrgID: orgID,
Names: names,
}
}
const defaultAlertmanagerConfigJSON = `
{
"template_files": null,
"alertmanager_config": {
"route": {
"receiver": "grafana-default-email",
"group_by": [
"..."
],
"routes": [{
"receiver": "grafana-default-email",
"object_matchers": [["a", "=", "b"]]
}]
},
"templates": null,
"receivers": [{
"name": "grafana-default-email",
"grafana_managed_receiver_configs": [{
"uid": "UID1",
"name": "grafana-default-email",
"type": "email",
"disableResolveMessage": false,
"settings": {
"addresses": "\u003cexample@email.com\u003e"
},
"secureFields": {}
}]
}, {
"name": "slack receiver",
"grafana_managed_receiver_configs": [{
"uid": "UID2",
"name": "slack receiver",
"type": "slack",
"disableResolveMessage": false,
"settings": {},
"secureSettings": {"url":"secure url"}
}]
}]
}
}
`
type NopTransactionManager struct{}
func newNopTransactionManager() *NopTransactionManager {
return &NopTransactionManager{}
}
func (n *NopTransactionManager) InTransaction(ctx context.Context, work func(ctx context.Context) error) error {
return work(context.WithValue(ctx, NopTransactionManager{}, struct{}{}))
}
func assertInTransaction(t *testing.T, ctx context.Context) {
assert.Truef(t, ctx.Value(NopTransactionManager{}) != nil, "Expected to be executed in transaction but there is none")
}