grafana/pkg/services/ngalert/notifier/silence_svc_test.go
Matthew Jacobson 31d5dd0a12
Alerting: Prevent updating rule uid matcher for silences (#88519)
Prevents updating the `__alert_rule_uid__` equality matcher (used for rule-specific silences) on existing silences
2024-06-03 17:39:06 -04:00

235 lines
9.0 KiB
Go

package notifier
import (
"context"
"math/rand"
"testing"
"github.com/prometheus/alertmanager/pkg/labels"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
alertingmodels "github.com/grafana/alerting/models"
ngfakes "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol/fakes"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/util"
)
func TestWithAccessControlMetadata(t *testing.T) {
user := ac.BackgroundUser("test", 1, org.RoleNone, nil)
silencesWithMetadata := []*models.SilenceWithMetadata{
{Silence: util.Pointer(models.SilenceGen()())},
{Silence: util.Pointer(models.SilenceGen()())},
{Silence: util.Pointer(models.SilenceGen()())},
}
randPerm := func() models.SilencePermissionSet {
return models.SilencePermissionSet{
models.SilencePermissionRead: rand.Intn(2) == 1,
models.SilencePermissionWrite: rand.Intn(2) == 1,
models.SilencePermissionCreate: rand.Intn(2) == 1,
}
}
t.Run("Attach permissions to silences", func(t *testing.T) {
authz := fakes.FakeSilenceService{}
response := map[*models.Silence]models.SilencePermissionSet{
silencesWithMetadata[0].Silence: randPerm(),
silencesWithMetadata[1].Silence: randPerm(),
silencesWithMetadata[2].Silence: randPerm(),
}
authz.SilenceAccessFunc = func(ctx context.Context, user identity.Requester, silences []*models.Silence) (map[*models.Silence]models.SilencePermissionSet, error) {
return response, nil
}
svc := SilenceService{
authz: &authz,
}
require.NoError(t, svc.WithAccessControlMetadata(context.Background(), user, silencesWithMetadata...))
for _, silence := range silencesWithMetadata {
assert.Equal(t, response[silence.Silence], *silence.Metadata.Permissions)
}
})
}
func TestWithRuleMetadata(t *testing.T) {
user := ac.BackgroundUser("test", 1, org.RoleNone, nil)
t.Run("Attach rule metadata to silences", func(t *testing.T) {
ruleAuthz := fakes.FakeRuleService{}
ruleAuthz.HasAccessInFolderFunc = func(ctx context.Context, user identity.Requester, silence accesscontrol.Namespaced) (bool, error) {
return true, nil
}
rules := []*models.AlertRule{
{UID: "rule1", NamespaceUID: "folder1"},
{UID: "rule2", NamespaceUID: "folder2"},
{UID: "rule3", NamespaceUID: "folder3"},
}
ruleStore := ngfakes.NewRuleStore(t)
ruleStore.Rules[1] = rules
svc := SilenceService{
ruleAuthz: &ruleAuthz,
ruleStore: ruleStore,
}
silencesWithMetadata := []*models.SilenceWithMetadata{
{Silence: util.Pointer(models.SilenceGen(models.SilenceMuts.WithMatcher(alertingmodels.RuleUIDLabel, "rule1", labels.MatchEqual))())},
{Silence: util.Pointer(models.SilenceGen(models.SilenceMuts.WithMatcher(alertingmodels.RuleUIDLabel, "rule2", labels.MatchEqual))())},
{Silence: util.Pointer(models.SilenceGen(models.SilenceMuts.WithMatcher(alertingmodels.RuleUIDLabel, "rule3", labels.MatchEqual))())},
}
require.NoError(t, svc.WithRuleMetadata(context.Background(), user, silencesWithMetadata...))
for i, silence := range silencesWithMetadata {
metadata := &models.SilenceRuleMetadata{
RuleUID: rules[i].UID,
RuleTitle: rules[i].Title,
FolderUID: rules[i].NamespaceUID,
}
assert.Equal(t, silence.Metadata, models.SilenceMetadata{RuleMetadata: metadata})
}
})
t.Run("Don't attach full rule metadata if no access or global", func(t *testing.T) {
ruleAuthz := fakes.FakeRuleService{}
ruleAuthz.HasAccessInFolderFunc = func(ctx context.Context, user identity.Requester, silence accesscontrol.Namespaced) (bool, error) {
return silence.GetNamespaceUID() == "folder1", nil
}
rules := []*models.AlertRule{
{UID: "rule1", NamespaceUID: "folder1"},
{UID: "rule2", NamespaceUID: "folder2"},
{UID: "rule3", NamespaceUID: "folder3"},
}
ruleStore := ngfakes.NewRuleStore(t)
ruleStore.Rules[1] = rules
svc := SilenceService{
ruleAuthz: &ruleAuthz,
ruleStore: ruleStore,
}
silencesWithMetadata := []*models.SilenceWithMetadata{
{Silence: util.Pointer(models.SilenceGen(models.SilenceMuts.WithMatcher(alertingmodels.RuleUIDLabel, "rule1", labels.MatchEqual))())},
{Silence: util.Pointer(models.SilenceGen(models.SilenceMuts.WithMatcher(alertingmodels.RuleUIDLabel, "rule2", labels.MatchEqual))())},
{Silence: util.Pointer(models.SilenceGen(models.SilenceMuts.WithMatcher(alertingmodels.RuleUIDLabel, "rule3", labels.MatchEqual))())},
{Silence: util.Pointer(models.SilenceGen()())},
}
require.NoError(t, svc.WithRuleMetadata(context.Background(), user, silencesWithMetadata...))
assert.Equal(t, silencesWithMetadata[0].Metadata, models.SilenceMetadata{RuleMetadata: &models.SilenceRuleMetadata{ // Attach all metadata.
RuleUID: rules[0].UID,
RuleTitle: rules[0].Title,
FolderUID: rules[0].NamespaceUID,
}})
assert.Equal(t, silencesWithMetadata[1].Metadata, models.SilenceMetadata{RuleMetadata: &models.SilenceRuleMetadata{ // Attach metadata with rule UID regardless of access.
RuleUID: rules[1].UID,
}})
assert.Equal(t, silencesWithMetadata[2].Metadata, models.SilenceMetadata{RuleMetadata: &models.SilenceRuleMetadata{ // Attach metadata with rule UID regardless of access.
RuleUID: rules[2].UID,
}})
assert.Equal(t, silencesWithMetadata[3].Metadata, models.SilenceMetadata{}) // Global silence, no rule metadata.
})
t.Run("Don't check same namespace access more than once", func(t *testing.T) {
ruleAuthz := fakes.FakeRuleService{}
ruleAuthz.HasAccessInFolderFunc = func(ctx context.Context, user identity.Requester, silence accesscontrol.Namespaced) (bool, error) {
return true, nil
}
rules := []*models.AlertRule{
{UID: "rule1", NamespaceUID: "folder1"},
{UID: "rule2", NamespaceUID: "folder1"},
{UID: "rule3", NamespaceUID: "folder1"},
}
ruleStore := ngfakes.NewRuleStore(t)
ruleStore.Rules[1] = rules
svc := SilenceService{
ruleAuthz: &ruleAuthz,
ruleStore: ruleStore,
}
silencesWithMetadata := []*models.SilenceWithMetadata{
{Silence: util.Pointer(models.SilenceGen(models.SilenceMuts.WithMatcher(alertingmodels.RuleUIDLabel, "rule1", labels.MatchEqual))())},
{Silence: util.Pointer(models.SilenceGen(models.SilenceMuts.WithMatcher(alertingmodels.RuleUIDLabel, "rule2", labels.MatchEqual))())},
{Silence: util.Pointer(models.SilenceGen(models.SilenceMuts.WithMatcher(alertingmodels.RuleUIDLabel, "rule3", labels.MatchEqual))())},
}
require.NoError(t, svc.WithRuleMetadata(context.Background(), user, silencesWithMetadata...))
assert.Lenf(t, ruleAuthz.Calls, 1, "HasAccessInFolder should be called only once per namespace")
assert.Equal(t, "HasAccessInFolder", ruleAuthz.Calls[0].MethodName)
assert.Equal(t, "folder1", ruleAuthz.Calls[0].Arguments[2].(accesscontrol.Namespaced).GetNamespaceUID())
})
}
func TestUpdateSilence(t *testing.T) {
user := ac.BackgroundUser("test", 1, org.RoleNone, nil)
testCases := []struct {
name string
existing func() models.Silence
mutators []models.Mutator[models.Silence]
errContains string
}{
{
name: "Updates to general silences allowed",
existing: models.SilenceGen(),
mutators: []models.Mutator[models.Silence]{
models.SilenceMuts.Expired(),
},
errContains: "", // No Error.
},
{
name: "Updates to general silences that add rule_uid matcher error",
existing: models.SilenceGen(),
mutators: []models.Mutator[models.Silence]{
models.SilenceMuts.WithRuleUID("rule1"),
},
errContains: alertingmodels.RuleUIDLabel, // Mention matcher in error message.
},
{
name: "Updates that change rule_uid matcher error",
existing: models.SilenceGen(models.SilenceMuts.WithRuleUID("rule1")),
mutators: []models.Mutator[models.Silence]{
models.SilenceMuts.WithRuleUID("rule2"),
},
errContains: alertingmodels.RuleUIDLabel, // Mention matcher in error message.
},
{
name: "Updates that don't change rule_uid matcher are allowed",
existing: models.SilenceGen(models.SilenceMuts.WithRuleUID("rule1")),
mutators: []models.Mutator[models.Silence]{
models.SilenceMuts.Expired(),
},
errContains: "", // No Error.
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
authz := fakes.FakeSilenceService{}
authz.AuthorizeUpdateSilenceFunc = func(ctx context.Context, user identity.Requester, silence *models.Silence) error {
return nil
}
silence := tc.existing()
silenceStore := ngfakes.FakeSilenceStore{
Silences: map[string]*models.Silence{
*silence.ID: &silence,
},
}
svc := SilenceService{
authz: &authz,
store: &silenceStore,
}
modified := models.CopySilenceWith(silence, tc.mutators...)
_, err := svc.UpdateSilence(context.Background(), user, modified)
if tc.errContains != "" {
assert.Error(t, err)
assert.ErrorContains(t, err, tc.errContains)
} else {
require.NoError(t, err)
}
})
}
}