mirror of
https://github.com/grafana/grafana.git
synced 2024-11-29 04:04:00 -06:00
31d5dd0a12
Prevents updating the `__alert_rule_uid__` equality matcher (used for rule-specific silences) on existing silences
257 lines
8.8 KiB
Go
257 lines
8.8 KiB
Go
package notifier
|
|
|
|
import (
|
|
"context"
|
|
|
|
"golang.org/x/exp/maps"
|
|
|
|
alertingModels "github.com/grafana/alerting/models"
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
"github.com/grafana/grafana/pkg/services/auth/identity"
|
|
"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
|
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
|
)
|
|
|
|
// SilenceService is the authenticated service for managing alertmanager silences.
|
|
type SilenceService struct {
|
|
authz SilenceAccessControlService
|
|
xact transactionManager
|
|
log log.Logger
|
|
store SilenceStore
|
|
ruleStore RuleStore
|
|
ruleAuthz RuleAccessControlService
|
|
}
|
|
|
|
type RuleAccessControlService interface {
|
|
HasAccessInFolder(ctx context.Context, user identity.Requester, rule accesscontrol.Namespaced) (bool, error)
|
|
}
|
|
|
|
// SilenceAccessControlService provides access control for silences.
|
|
type SilenceAccessControlService interface {
|
|
FilterByAccess(ctx context.Context, user identity.Requester, silences ...*models.Silence) ([]*models.Silence, error)
|
|
AuthorizeReadSilence(ctx context.Context, user identity.Requester, silence *models.Silence) error
|
|
AuthorizeCreateSilence(ctx context.Context, user identity.Requester, silence *models.Silence) error
|
|
AuthorizeUpdateSilence(ctx context.Context, user identity.Requester, silence *models.Silence) error
|
|
SilenceAccess(ctx context.Context, user identity.Requester, silences []*models.Silence) (map[*models.Silence]models.SilencePermissionSet, error)
|
|
}
|
|
|
|
// SilenceStore is the interface for storing and retrieving silences. Currently, this is implemented by
|
|
// MultiOrgAlertmanager but should eventually be replaced with an actual store.
|
|
type SilenceStore interface {
|
|
ListSilences(ctx context.Context, orgID int64, filter []string) ([]*models.Silence, error)
|
|
GetSilence(ctx context.Context, orgID int64, id string) (*models.Silence, error)
|
|
CreateSilence(ctx context.Context, orgID int64, ps models.Silence) (string, error)
|
|
UpdateSilence(ctx context.Context, orgID int64, ps models.Silence) (string, error)
|
|
DeleteSilence(ctx context.Context, orgID int64, id string) error
|
|
}
|
|
|
|
type RuleStore interface {
|
|
ListAlertRules(ctx context.Context, query *models.ListAlertRulesQuery) (models.RulesGroup, error)
|
|
}
|
|
|
|
func NewSilenceService(
|
|
authz SilenceAccessControlService,
|
|
xact transactionManager,
|
|
log log.Logger,
|
|
store SilenceStore,
|
|
ruleStore RuleStore,
|
|
ruleAuthz RuleAccessControlService,
|
|
) *SilenceService {
|
|
return &SilenceService{
|
|
authz: authz,
|
|
xact: xact,
|
|
log: log,
|
|
store: store,
|
|
ruleStore: ruleStore,
|
|
ruleAuthz: ruleAuthz,
|
|
}
|
|
}
|
|
|
|
// GetSilence retrieves a silence by its ID.
|
|
func (s *SilenceService) GetSilence(ctx context.Context, user identity.Requester, silenceID string) (*models.Silence, error) {
|
|
silence, err := s.store.GetSilence(ctx, user.GetOrgID(), silenceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := s.authz.AuthorizeReadSilence(ctx, user, silence); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return silence, nil
|
|
}
|
|
|
|
// ListSilences retrieves all silences that match the given filter. This will include all rule-specific silences that
|
|
// the user has access to as well as all general silences.
|
|
func (s *SilenceService) ListSilences(ctx context.Context, user identity.Requester, filter []string) ([]*models.Silence, error) {
|
|
silences, err := s.store.ListSilences(ctx, user.GetOrgID(), filter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return s.authz.FilterByAccess(ctx, user, silences...)
|
|
}
|
|
|
|
// CreateSilence creates a new silence.
|
|
// For rule-specific silences, the user needs permission to create silences in the folder that the associated rule is in.
|
|
// For general silences, the user needs broader permissions.
|
|
func (s *SilenceService) CreateSilence(ctx context.Context, user identity.Requester, ps models.Silence) (string, error) {
|
|
if err := s.authz.AuthorizeCreateSilence(ctx, user, &ps); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
silenceId, err := s.store.CreateSilence(ctx, user.GetOrgID(), ps)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return silenceId, nil
|
|
}
|
|
|
|
// UpdateSilence updates an existing silence.
|
|
// For rule-specific silences, the user needs permission to update silences in the folder that the associated rule is in.
|
|
// For general silences, the user needs broader permissions.
|
|
func (s *SilenceService) UpdateSilence(ctx context.Context, user identity.Requester, ps models.Silence) (string, error) {
|
|
if err := s.authz.AuthorizeUpdateSilence(ctx, user, &ps); err != nil {
|
|
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)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return silenceId, nil
|
|
}
|
|
|
|
// DeleteSilence deletes a silence by its ID.
|
|
// For rule-specific silences, the user needs permission to update silences in the folder that the associated rule is in.
|
|
// For general silences, the user needs broader permissions.
|
|
func (s *SilenceService) DeleteSilence(ctx context.Context, user identity.Requester, silenceID string) error {
|
|
silence, err := s.GetSilence(ctx, user, silenceID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := s.authz.AuthorizeUpdateSilence(ctx, user, silence); err != nil {
|
|
return err
|
|
}
|
|
|
|
err = s.store.DeleteSilence(ctx, user.GetOrgID(), silenceID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// WithAccessControlMetadata adds access control metadata to the given SilenceWithMetadata.
|
|
func (s *SilenceService) WithAccessControlMetadata(ctx context.Context, user identity.Requester, silencesWithMetadata ...*models.SilenceWithMetadata) error {
|
|
silences := make([]*models.Silence, 0, len(silencesWithMetadata))
|
|
for _, silence := range silencesWithMetadata {
|
|
silences = append(silences, silence.Silence)
|
|
}
|
|
permissions, err := s.authz.SilenceAccess(ctx, user, silences)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(permissions) != len(silences) {
|
|
s.log.Warn("failed to get metadata for all silences")
|
|
}
|
|
|
|
for _, silence := range silencesWithMetadata {
|
|
if perms, ok := permissions[silence.Silence]; ok {
|
|
silence.Metadata.Permissions = &perms
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// WithRuleMetadata adds rule metadata to the given SilenceWithMetadata.
|
|
func (s *SilenceService) WithRuleMetadata(ctx context.Context, user identity.Requester, silences ...*models.SilenceWithMetadata) error {
|
|
byRuleUID := make(map[string][]*models.SilenceWithMetadata, len(silences))
|
|
for _, silence := range silences {
|
|
ruleUID := silence.GetRuleUID()
|
|
if ruleUID != nil {
|
|
byRuleUID[*ruleUID] = append(byRuleUID[*ruleUID], silence)
|
|
silence.Metadata.RuleMetadata = &models.SilenceRuleMetadata{ // Attach metadata with rule UID regardless of access.
|
|
RuleUID: *ruleUID,
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(byRuleUID) == 0 {
|
|
return nil
|
|
}
|
|
|
|
q := models.ListAlertRulesQuery{
|
|
RuleUIDs: maps.Keys(byRuleUID),
|
|
OrgID: user.GetOrgID(),
|
|
}
|
|
|
|
rules, err := s.ruleStore.ListAlertRules(ctx, &q)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
accessCacheByFolder := make(map[string]bool)
|
|
for _, rule := range rules {
|
|
// TODO: Preferably silence service should not need to know about the internal details of rule access control.
|
|
// This can be improved by adding a method to ruleAuthz that does the filtering itself or a method that exposes
|
|
// an access fingerprint for a rule that callers can use to do their own caching.
|
|
fp := rule.NamespaceUID
|
|
canAccess, ok := accessCacheByFolder[fp]
|
|
if !ok {
|
|
var err error
|
|
if canAccess, err = s.ruleAuthz.HasAccessInFolder(ctx, user, rule); err != nil {
|
|
continue // Assume no access if there is an error but don't cache.
|
|
}
|
|
accessCacheByFolder[fp] = canAccess // Only cache if there is no error.
|
|
}
|
|
if !canAccess {
|
|
continue
|
|
}
|
|
|
|
if ruleSilences, ok := byRuleUID[rule.UID]; ok {
|
|
for _, sil := range ruleSilences {
|
|
if sil.Metadata.RuleMetadata == nil {
|
|
sil.Metadata.RuleMetadata = &models.SilenceRuleMetadata{}
|
|
}
|
|
sil.Metadata.RuleMetadata.RuleTitle = rule.Title
|
|
sil.Metadata.RuleMetadata.FolderUID = rule.NamespaceUID
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|