mirror of
https://github.com/grafana/grafana.git
synced 2024-11-25 18:30:41 -06:00
509691b416
* extract genericService from RuleService just to reuse it later * implement silence service --------- Co-authored-by: William Wernert <william.wernert@grafana.com> Co-authored-by: Matthew Jacobson <matthew.jacobson@grafana.com>
257 lines
9.6 KiB
Go
257 lines
9.6 KiB
Go
package accesscontrol
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"golang.org/x/exp/maps"
|
|
|
|
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
|
"github.com/grafana/grafana/pkg/services/auth/identity"
|
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
|
)
|
|
|
|
const (
|
|
instancesRead = ac.ActionAlertingInstanceRead
|
|
instancesCreate = ac.ActionAlertingInstanceCreate
|
|
instancesWrite = ac.ActionAlertingInstanceUpdate
|
|
silenceRead = ac.ActionAlertingSilencesRead
|
|
silenceCreate = ac.ActionAlertingSilencesCreate
|
|
silenceWrite = ac.ActionAlertingSilencesWrite
|
|
)
|
|
|
|
var (
|
|
// asserts full read-only access to silences
|
|
readAllSilencesEvaluator = ac.EvalPermission(instancesRead)
|
|
// shortcut assertion that to check if user can read silences
|
|
readSomeSilenceEvaluator = ac.EvalAny(ac.EvalPermission(instancesRead), ac.EvalPermission(silenceRead))
|
|
// asserts whether user has read access to rules in a specific folder
|
|
readRuleSilenceEvaluator = func(folderUID string) ac.Evaluator {
|
|
return ac.EvalAny(
|
|
ac.EvalPermission(instancesRead),
|
|
ac.EvalPermission(silenceRead, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folderUID)),
|
|
)
|
|
}
|
|
|
|
// shortcut assertion to check if user can create any silence
|
|
createAnySilenceEvaluator = ac.EvalAll(ac.EvalPermission(instancesCreate), readAllSilencesEvaluator)
|
|
// asserts that user has access to create general silences, the ones that can match alerts created by one or many rules
|
|
createGeneralSilenceEvaluator = ac.EvalAll(ac.EvalPermission(instancesCreate), readSomeSilenceEvaluator)
|
|
// shortcut assertion to check if user can create silences at all
|
|
createSomeRuleSilenceEvaluator = ac.EvalAll(
|
|
readSomeSilenceEvaluator,
|
|
ac.EvalAny(
|
|
ac.EvalPermission(instancesCreate),
|
|
ac.EvalPermission(silenceCreate)),
|
|
)
|
|
// asserts that user has access to create silences in a specific folder
|
|
createRuleSilenceEvaluator = func(uid string) ac.Evaluator {
|
|
return ac.EvalAll(
|
|
ac.EvalAny(
|
|
ac.EvalPermission(instancesCreate),
|
|
ac.EvalPermission(silenceCreate, dashboards.ScopeFoldersProvider.GetResourceScopeUID(uid)),
|
|
),
|
|
readRuleSilenceEvaluator(uid),
|
|
)
|
|
}
|
|
|
|
// shortcut assertion to check if user can update any silence
|
|
updateAnySilenceEvaluator = ac.EvalAll(ac.EvalPermission(instancesWrite), readAllSilencesEvaluator)
|
|
// asserts that user has access to update general silences
|
|
updateGeneralSilenceEvaluator = ac.EvalAll(ac.EvalPermission(instancesWrite), readSomeSilenceEvaluator)
|
|
// asserts that user has access to update silences at all
|
|
updateSomeRuleSilenceEvaluator = ac.EvalAll(
|
|
readSomeSilenceEvaluator,
|
|
ac.EvalAny(
|
|
ac.EvalPermission(instancesWrite),
|
|
ac.EvalPermission(silenceWrite)),
|
|
)
|
|
// asserts that user has access to create silences in a specific folder
|
|
updateRuleSilenceEvaluator = func(uid string) ac.Evaluator {
|
|
return ac.EvalAll(
|
|
ac.EvalAny(
|
|
ac.EvalPermission(instancesWrite),
|
|
ac.EvalPermission(silenceWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(uid)),
|
|
),
|
|
readRuleSilenceEvaluator(uid),
|
|
)
|
|
}
|
|
)
|
|
|
|
type Silence interface {
|
|
GetRuleUID() *string
|
|
}
|
|
|
|
type RuleUIDToNamespaceStore interface {
|
|
GetNamespacesByRuleUID(ctx context.Context, orgID int64, uids ...string) (map[string]string, error)
|
|
}
|
|
|
|
type SilenceService struct {
|
|
genericService
|
|
store RuleUIDToNamespaceStore
|
|
}
|
|
|
|
func NewSilenceService(ac ac.AccessControl, store RuleUIDToNamespaceStore) *SilenceService {
|
|
return &SilenceService{
|
|
genericService: genericService{
|
|
ac: ac,
|
|
},
|
|
store: store,
|
|
}
|
|
}
|
|
|
|
// FilterByAccess filters the given list of silences based on the access control permissions of the user.
|
|
// Global silence (one that is not attached to a particular rule) is considered available to all users.
|
|
// For silences that are not attached to a rule, are checked against authorization.
|
|
// This method is more preferred when many silences need to be checked.
|
|
func (s SilenceService) FilterByAccess(ctx context.Context, user identity.Requester, silences ...Silence) ([]Silence, error) {
|
|
canAll, err := s.HasAccess(ctx, user, readAllSilencesEvaluator)
|
|
if err != nil || canAll { // return early if user can either read all silences or there is an error
|
|
return silences, err
|
|
}
|
|
canSome, err := s.HasAccess(ctx, user, readSomeSilenceEvaluator)
|
|
if err != nil || !canSome {
|
|
return nil, err
|
|
}
|
|
result := make([]Silence, 0, len(silences))
|
|
silencesByRuleUID := make(map[string][]Silence, len(silences))
|
|
for _, silence := range silences {
|
|
ruleUID := silence.GetRuleUID()
|
|
if ruleUID == nil { // if this is a general silence
|
|
result = append(result, silence)
|
|
continue
|
|
}
|
|
key := *ruleUID
|
|
silencesByRuleUID[key] = append(silencesByRuleUID[key], silence)
|
|
}
|
|
if len(silencesByRuleUID) == 0 { // if only general silences are provided no need in other checks
|
|
return result, nil
|
|
}
|
|
namespacesByRuleUID, err := s.store.GetNamespacesByRuleUID(ctx, user.GetOrgID(), maps.Keys(silencesByRuleUID)...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
namespacesByAccess := make(map[string]bool) // caches results of permissions check for each namespace to avoid repeated checks for the same folder
|
|
for ruleUID, silence := range silencesByRuleUID {
|
|
ns, ok := namespacesByRuleUID[ruleUID]
|
|
if !ok { // this means that there is no rule with such UID.
|
|
continue
|
|
}
|
|
hasAccess, ok := namespacesByAccess[ns]
|
|
if !ok {
|
|
hasAccess, err = s.HasAccess(ctx, user, readRuleSilenceEvaluator(ns))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
namespacesByAccess[ns] = hasAccess
|
|
}
|
|
if hasAccess {
|
|
result = append(result, silence...)
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// AuthorizeReadSilence checks if user has access to read a silence
|
|
func (s SilenceService) AuthorizeReadSilence(ctx context.Context, user identity.Requester, silence Silence) error {
|
|
canAll, err := s.HasAccess(ctx, user, readAllSilencesEvaluator)
|
|
if canAll || err != nil { // return early if user can either read all silences or there is error
|
|
return err
|
|
}
|
|
|
|
can, err := s.HasAccess(ctx, user, readSomeSilenceEvaluator)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !can { // User does not have silence permissions at all.
|
|
return NewAuthorizationErrorWithPermissions("read any silences", readSomeSilenceEvaluator)
|
|
}
|
|
ruleUID := silence.GetRuleUID()
|
|
if ruleUID == nil {
|
|
return nil // no rule UID means that this is a general silence and at this point the user can read them
|
|
}
|
|
|
|
// otherwise resolve rule key to the action's scope
|
|
folderUID, err := s.ruleUIDToFolderUID(ctx, user.GetOrgID(), *ruleUID)
|
|
if err != nil {
|
|
return fmt.Errorf("resolve rule UID to folder UID: %w", err)
|
|
}
|
|
if folderUID == "" { // if we did not find folder by rule UID then it does not exist.
|
|
return NewAuthorizationErrorGeneric(fmt.Sprintf("read silence for rule %s", *ruleUID))
|
|
}
|
|
return s.HasAccessOrError(ctx, user, readRuleSilenceEvaluator(folderUID), func() string {
|
|
return "read silence"
|
|
})
|
|
}
|
|
|
|
// AuthorizeCreateSilence checks if user has access to create a silence. Returns ErrAuthorizationBase if user is not authorized
|
|
func (s SilenceService) AuthorizeCreateSilence(ctx context.Context, user identity.Requester, silence Silence) error {
|
|
canAny, err := s.HasAccess(ctx, user, createAnySilenceEvaluator)
|
|
if err != nil || canAny {
|
|
// return early if user can either create any silence or there is an error
|
|
return err
|
|
}
|
|
ruleUID := silence.GetRuleUID()
|
|
if ruleUID == nil {
|
|
return s.HasAccessOrError(ctx, user, createGeneralSilenceEvaluator, func() string {
|
|
return "create a general silence"
|
|
})
|
|
}
|
|
// pre-check whether a user has at least some basic permissions before hit the store
|
|
if err := s.HasAccessOrError(ctx, user, createSomeRuleSilenceEvaluator, func() string { return "create any silences" }); err != nil {
|
|
return err
|
|
}
|
|
folderUID, err := s.ruleUIDToFolderUID(ctx, user.GetOrgID(), *ruleUID)
|
|
if err != nil {
|
|
return fmt.Errorf("resolve rule UID to folder UID: %w", err)
|
|
}
|
|
if folderUID == "" { // if we did not find folder by rule UID then it does not exist.
|
|
return NewAuthorizationErrorGeneric(fmt.Sprintf("create silence for rule %s", *ruleUID))
|
|
}
|
|
return s.HasAccessOrError(ctx, user, createRuleSilenceEvaluator(folderUID), func() string {
|
|
return fmt.Sprintf("create silence for rule %s", *ruleUID)
|
|
})
|
|
}
|
|
|
|
// AuthorizeUpdateSilence checks if user has access to update\expire a silence. Returns ErrAuthorizationBase if user is not authorized
|
|
func (s SilenceService) AuthorizeUpdateSilence(ctx context.Context, user identity.Requester, silence Silence) error {
|
|
canAny, err := s.HasAccess(ctx, user, updateAnySilenceEvaluator)
|
|
if err != nil || canAny {
|
|
// return early if user can either update any silence or there is an error
|
|
return err
|
|
}
|
|
ruleUID := silence.GetRuleUID()
|
|
if ruleUID == nil {
|
|
return s.HasAccessOrError(ctx, user, updateGeneralSilenceEvaluator, func() string {
|
|
return "update a general silence"
|
|
})
|
|
}
|
|
// pre-check whether a user has at least some basic permissions before hit the store
|
|
if err := s.HasAccessOrError(ctx, user, updateSomeRuleSilenceEvaluator, func() string { return "update any silences" }); err != nil {
|
|
return err
|
|
}
|
|
folderUID, err := s.ruleUIDToFolderUID(ctx, user.GetOrgID(), *ruleUID)
|
|
if err != nil {
|
|
return fmt.Errorf("resolve rule UID to folder UID: %w", err)
|
|
}
|
|
if folderUID == "" { // if we did not find folder by rule UID then it does not exist.
|
|
return NewAuthorizationErrorGeneric(fmt.Sprintf("update silence for rule %s", *ruleUID))
|
|
}
|
|
return s.HasAccessOrError(ctx, user, updateRuleSilenceEvaluator(folderUID), func() string {
|
|
return fmt.Sprintf("update silence for rule %s", *ruleUID)
|
|
})
|
|
}
|
|
|
|
func (s SilenceService) ruleUIDToFolderUID(ctx context.Context, orgID int64, ruleUID string) (string, error) {
|
|
namespaces, err := s.store.GetNamespacesByRuleUID(ctx, orgID, ruleUID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
uid, ok := namespaces[ruleUID]
|
|
if !ok {
|
|
return "", nil
|
|
}
|
|
return uid, nil
|
|
}
|