The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/ngalert/notifier/silence_svc.go

256 lines
8.8 KiB

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
}