mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
f458e57523
commit
31d5dd0a12
@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/prometheus/common/model"
|
"github.com/prometheus/common/model"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
alertingModels "github.com/grafana/alerting/models"
|
||||||
"github.com/grafana/grafana/pkg/expr"
|
"github.com/grafana/grafana/pkg/expr"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
"github.com/grafana/grafana/pkg/services/folder"
|
"github.com/grafana/grafana/pkg/services/folder"
|
||||||
@ -1054,6 +1055,29 @@ func (n SilenceMutators) WithMatcher(name, value string, matchType labels.MatchT
|
|||||||
s.Silence.Matchers = append(s.Silence.Matchers, &m)
|
s.Silence.Matchers = append(s.Silence.Matchers, &m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
func (n SilenceMutators) WithRuleUID(value string) Mutator[Silence] {
|
||||||
|
return func(s *Silence) {
|
||||||
|
name := alertingModels.RuleUIDLabel
|
||||||
|
m := amv2.Matcher{
|
||||||
|
Name: &name,
|
||||||
|
Value: &value,
|
||||||
|
IsRegex: util.Pointer(false),
|
||||||
|
IsEqual: util.Pointer(true),
|
||||||
|
}
|
||||||
|
for _, matcher := range s.Silence.Matchers {
|
||||||
|
if isRuleUIDMatcher(*matcher) {
|
||||||
|
*matcher = m
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.Silence.Matchers = append(s.Silence.Matchers, &m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (n SilenceMutators) Expired() Mutator[Silence] {
|
||||||
|
return func(s *Silence) {
|
||||||
|
s.EndsAt = util.Pointer(strfmt.DateTime(time.Now().Add(-time.Minute)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (n SilenceMutators) WithEmptyId() Mutator[Silence] {
|
func (n SilenceMutators) WithEmptyId() Mutator[Silence] {
|
||||||
return func(s *Silence) {
|
return func(s *Silence) {
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"golang.org/x/exp/maps"
|
"golang.org/x/exp/maps"
|
||||||
|
|
||||||
|
alertingModels "github.com/grafana/alerting/models"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/services/auth/identity"
|
"github.com/grafana/grafana/pkg/services/auth/identity"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
|
||||||
@ -115,6 +116,15 @@ func (s *SilenceService) UpdateSilence(ctx context.Context, user identity.Reques
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
existing, err := s.store.GetSilence(ctx, user.GetOrgID(), *ps.ID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateSilenceUpdate(existing, ps); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
silenceId, err := s.store.UpdateSilence(ctx, user.GetOrgID(), ps)
|
silenceId, err := s.store.UpdateSilence(ctx, user.GetOrgID(), ps)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@ -226,3 +236,21 @@ func (s *SilenceService) WithRuleMetadata(ctx context.Context, user identity.Req
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validateSilenceUpdate validates the diff between an existing silence and a new silence. Currently, this is use to
|
||||||
|
// prevent changing the rule UID matcher.
|
||||||
|
// Alternatively, we could check WRITE permission on the old silence followed by CREATE permission on the new silence
|
||||||
|
// if the rule folder is different.
|
||||||
|
func validateSilenceUpdate(existing *models.Silence, new models.Silence) error {
|
||||||
|
existingRuleUID := existing.GetRuleUID()
|
||||||
|
newRuleUID := new.GetRuleUID()
|
||||||
|
if existingRuleUID == nil || newRuleUID == nil {
|
||||||
|
if existingRuleUID != newRuleUID {
|
||||||
|
return WithPublicError(ErrSilencesBadRequest.Errorf("Silence rule matcher '%s' cannot be updated, please create a new silence", alertingModels.RuleUIDLabel))
|
||||||
|
}
|
||||||
|
} else if *existingRuleUID != *newRuleUID {
|
||||||
|
return WithPublicError(ErrSilencesBadRequest.Errorf("Silence rule matcher '%s' cannot be updated, please create a new silence", alertingModels.RuleUIDLabel))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -161,3 +161,74 @@ func TestWithRuleMetadata(t *testing.T) {
|
|||||||
assert.Equal(t, "folder1", ruleAuthz.Calls[0].Arguments[2].(accesscontrol.Namespaced).GetNamespaceUID())
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
62
pkg/services/ngalert/tests/fakes/silences.go
Normal file
62
pkg/services/ngalert/tests/fakes/silences.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package fakes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"golang.org/x/exp/maps"
|
||||||
|
|
||||||
|
alertingNotify "github.com/grafana/alerting/notify"
|
||||||
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
|
"github.com/grafana/grafana/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Call struct {
|
||||||
|
MethodName string
|
||||||
|
Arguments []interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FakeSilenceStore struct {
|
||||||
|
Silences map[string]*models.Silence
|
||||||
|
RuleUIDFolders map[string]string
|
||||||
|
|
||||||
|
RecordedOps []GenericRecordedQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FakeSilenceStore) ListSilences(ctx context.Context, orgID int64, filter []string) ([]*models.Silence, error) {
|
||||||
|
s.RecordedOps = append(s.RecordedOps, GenericRecordedQuery{"ListSilences", []interface{}{ctx, orgID, filter}})
|
||||||
|
return maps.Values(s.Silences), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FakeSilenceStore) GetSilence(ctx context.Context, orgID int64, id string) (*models.Silence, error) {
|
||||||
|
s.RecordedOps = append(s.RecordedOps, GenericRecordedQuery{"GetSilence", []interface{}{ctx, orgID, id}})
|
||||||
|
if silence, ok := s.Silences[id]; ok {
|
||||||
|
return silence, nil
|
||||||
|
}
|
||||||
|
return nil, alertingNotify.ErrSilenceNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FakeSilenceStore) CreateSilence(ctx context.Context, orgID int64, ps models.Silence) (string, error) {
|
||||||
|
s.RecordedOps = append(s.RecordedOps, GenericRecordedQuery{"CreateSilence", []interface{}{ctx, orgID, ps}})
|
||||||
|
uid := util.GenerateShortUID()
|
||||||
|
ps.ID = &uid
|
||||||
|
s.Silences[uid] = &ps
|
||||||
|
return uid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FakeSilenceStore) UpdateSilence(ctx context.Context, orgID int64, ps models.Silence) (string, error) {
|
||||||
|
s.RecordedOps = append(s.RecordedOps, GenericRecordedQuery{"UpdateSilence", []interface{}{ctx, orgID, ps}})
|
||||||
|
if _, ok := s.Silences[*ps.ID]; !ok {
|
||||||
|
return "", alertingNotify.ErrSilenceNotFound
|
||||||
|
}
|
||||||
|
s.Silences[*ps.ID] = &ps
|
||||||
|
return *ps.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FakeSilenceStore) DeleteSilence(ctx context.Context, orgID int64, id string) error {
|
||||||
|
s.RecordedOps = append(s.RecordedOps, GenericRecordedQuery{"DeleteSilence", []interface{}{ctx, orgID, id}})
|
||||||
|
if _, ok := s.Silences[id]; !ok {
|
||||||
|
return alertingNotify.ErrSilenceNotFound
|
||||||
|
}
|
||||||
|
delete(s.Silences, id)
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user