mirror of
https://github.com/grafana/grafana.git
synced 2024-11-25 10:20:29 -06:00
Alerting: Introduce authorization logic for operations on silences (#85418)
* 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>
This commit is contained in:
parent
9d7e758e04
commit
509691b416
@ -440,6 +440,10 @@ const (
|
||||
ActionAlertingInstanceUpdate = "alert.instances:write"
|
||||
ActionAlertingInstanceRead = "alert.instances:read"
|
||||
|
||||
ActionAlertingSilencesRead = "alert.silences:read"
|
||||
ActionAlertingSilencesCreate = "alert.silences:create"
|
||||
ActionAlertingSilencesWrite = "alert.silences:write"
|
||||
|
||||
// Alerting Notification policies actions
|
||||
ActionAlertingNotificationsRead = "alert.notifications:read"
|
||||
ActionAlertingNotificationsWrite = "alert.notifications:write"
|
||||
|
28
pkg/services/ngalert/accesscontrol/accesscontrol.go
Normal file
28
pkg/services/ngalert/accesscontrol/accesscontrol.go
Normal file
@ -0,0 +1,28 @@
|
||||
package accesscontrol
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/auth/identity"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type genericService struct {
|
||||
ac accesscontrol.AccessControl
|
||||
}
|
||||
|
||||
// HasAccess returns true if the identity.Requester has all permissions specified by the evaluator. Returns error if access control backend could not evaluate permissions
|
||||
func (r genericService) HasAccess(ctx context.Context, user identity.Requester, evaluator accesscontrol.Evaluator) (bool, error) {
|
||||
return r.ac.Evaluate(ctx, user, evaluator)
|
||||
}
|
||||
|
||||
// HasAccessOrError returns nil if the identity.Requester has enough permissions to pass the accesscontrol.Evaluator. Otherwise, returns authorization error that contains action that was performed
|
||||
func (r genericService) HasAccessOrError(ctx context.Context, user identity.Requester, evaluator accesscontrol.Evaluator, action func() string) error {
|
||||
has, err := r.HasAccess(ctx, user, evaluator)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !has {
|
||||
return NewAuthorizationErrorWithPermissions(action(), evaluator)
|
||||
}
|
||||
return nil
|
||||
}
|
@ -22,32 +22,15 @@ const (
|
||||
)
|
||||
|
||||
type RuleService struct {
|
||||
ac accesscontrol.AccessControl
|
||||
genericService
|
||||
}
|
||||
|
||||
func NewRuleService(ac accesscontrol.AccessControl) *RuleService {
|
||||
return &RuleService{
|
||||
ac: ac,
|
||||
genericService{ac: ac},
|
||||
}
|
||||
}
|
||||
|
||||
// HasAccess returns true if the identity.Requester has all permissions specified by the evaluator. Returns error if access control backend could not evaluate permissions
|
||||
func (r *RuleService) HasAccess(ctx context.Context, user identity.Requester, evaluator accesscontrol.Evaluator) (bool, error) {
|
||||
return r.ac.Evaluate(ctx, user, evaluator)
|
||||
}
|
||||
|
||||
// HasAccessOrError returns nil if the identity.Requester has enough permissions to pass the accesscontrol.Evaluator. Otherwise, returns authorization error that contains action that was performed
|
||||
func (r *RuleService) HasAccessOrError(ctx context.Context, user identity.Requester, evaluator accesscontrol.Evaluator, action func() string) error {
|
||||
has, err := r.HasAccess(ctx, user, evaluator)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !has {
|
||||
return NewAuthorizationErrorWithPermissions(action(), evaluator)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getReadFolderAccessEvaluator constructs accesscontrol.Evaluator that checks all permissions required to read rules in specific folder
|
||||
func getReadFolderAccessEvaluator(folderUID string) accesscontrol.Evaluator {
|
||||
return accesscontrol.EvalAll(
|
||||
|
@ -352,7 +352,7 @@ func TestAuthorizeRuleChanges(t *testing.T) {
|
||||
for _, missing := range permissionCombinations {
|
||||
ac := &recordingAccessControlFake{}
|
||||
srv := RuleService{
|
||||
ac: ac,
|
||||
genericService{ac: ac},
|
||||
}
|
||||
err := srv.AuthorizeRuleChanges(context.Background(), createUserWithPermissions(missing), groupChanges)
|
||||
|
||||
@ -369,7 +369,7 @@ func TestAuthorizeRuleChanges(t *testing.T) {
|
||||
},
|
||||
}
|
||||
srv := RuleService{
|
||||
ac: ac,
|
||||
genericService{ac: ac},
|
||||
}
|
||||
err := srv.AuthorizeRuleChanges(context.Background(), createUserWithPermissions(permissions), groupChanges)
|
||||
require.NoError(t, err)
|
||||
@ -414,7 +414,7 @@ func TestCheckDatasourcePermissionsForRule(t *testing.T) {
|
||||
|
||||
ac := &recordingAccessControlFake{}
|
||||
svc := RuleService{
|
||||
ac: ac,
|
||||
genericService{ac: ac},
|
||||
}
|
||||
|
||||
eval := svc.AuthorizeDatasourceAccessForRule(context.Background(), createUserWithPermissions(permissions), rule)
|
||||
@ -430,7 +430,7 @@ func TestCheckDatasourcePermissionsForRule(t *testing.T) {
|
||||
},
|
||||
}
|
||||
svc := RuleService{
|
||||
ac: ac,
|
||||
genericService{ac: ac},
|
||||
}
|
||||
|
||||
result := svc.AuthorizeDatasourceAccessForRule(context.Background(), createUserWithPermissions(nil), rule)
|
||||
@ -460,7 +460,7 @@ func Test_authorizeAccessToRuleGroup(t *testing.T) {
|
||||
}
|
||||
ac := &recordingAccessControlFake{}
|
||||
svc := RuleService{
|
||||
ac: ac,
|
||||
genericService{ac: ac},
|
||||
}
|
||||
|
||||
result := svc.AuthorizeAccessToRuleGroup(context.Background(), createUserWithPermissions(permissions), rules)
|
||||
@ -493,7 +493,7 @@ func Test_authorizeAccessToRuleGroup(t *testing.T) {
|
||||
ac := &recordingAccessControlFake{}
|
||||
|
||||
svc := RuleService{
|
||||
ac: ac,
|
||||
genericService{ac: ac},
|
||||
}
|
||||
|
||||
result := svc.AuthorizeAccessToRuleGroup(context.Background(), createUserWithPermissions(permissions), rules)
|
||||
|
256
pkg/services/ngalert/accesscontrol/silences.go
Normal file
256
pkg/services/ngalert/accesscontrol/silences.go
Normal file
@ -0,0 +1,256 @@
|
||||
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
|
||||
}
|
496
pkg/services/ngalert/accesscontrol/silences_test.go
Normal file
496
pkg/services/ngalert/accesscontrol/silences_test.go
Normal file
@ -0,0 +1,496 @@
|
||||
package accesscontrol
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/auth/identity"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils"
|
||||
)
|
||||
|
||||
var orgID = rand.Int63()
|
||||
|
||||
func TestFilterByAccess(t *testing.T) {
|
||||
global := testSilence{ID: "global", RuleUID: nil}
|
||||
ruleSilence1 := testSilence{ID: "rule-1", RuleUID: utils.Pointer("rule-1-uid")}
|
||||
folder1 := "rule-1-folder-uid"
|
||||
folder1Scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder1)
|
||||
ruleSilence2 := testSilence{ID: "rule-2", RuleUID: utils.Pointer("rule-2-uid")}
|
||||
folder2 := "rule-2-folder-uid"
|
||||
notFoundRule := testSilence{ID: "unknown-rule", RuleUID: utils.Pointer("unknown-rule-uid")}
|
||||
|
||||
silences := []Silence{
|
||||
global,
|
||||
ruleSilence1,
|
||||
ruleSilence2,
|
||||
notFoundRule,
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
user identity.Requester
|
||||
expected []Silence
|
||||
expectedDbAccess bool
|
||||
}{
|
||||
{
|
||||
name: "no silence access, empty list",
|
||||
user: newUser(),
|
||||
expected: []Silence{},
|
||||
expectedDbAccess: false,
|
||||
},
|
||||
{
|
||||
name: "instance reader should get all",
|
||||
user: newUser(ac.Permission{Action: instancesRead}),
|
||||
expected: []Silence{
|
||||
global,
|
||||
ruleSilence1,
|
||||
ruleSilence2,
|
||||
notFoundRule,
|
||||
},
|
||||
expectedDbAccess: false,
|
||||
},
|
||||
{
|
||||
name: "silence reader should get global + folder",
|
||||
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}),
|
||||
expected: []Silence{
|
||||
global,
|
||||
ruleSilence1,
|
||||
},
|
||||
expectedDbAccess: true,
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
ac := &recordingAccessControlFake{}
|
||||
store := &fakeRuleUIDToNamespaceStore{
|
||||
Response: map[string]string{
|
||||
*ruleSilence1.RuleUID: folder1,
|
||||
*ruleSilence2.RuleUID: folder2,
|
||||
},
|
||||
}
|
||||
svc := NewSilenceService(ac, store)
|
||||
|
||||
actual, err := svc.FilterByAccess(context.Background(), testCase.user, silences...)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, testCase.expected, actual)
|
||||
|
||||
if testCase.expectedDbAccess {
|
||||
require.Equal(t, store.Calls, 1)
|
||||
} else {
|
||||
require.Equal(t, store.Calls, 0)
|
||||
}
|
||||
require.NotEmpty(t, ac.EvaluateRecordings)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorizeReadSilence(t *testing.T) {
|
||||
global := testSilence{ID: "global", RuleUID: nil}
|
||||
ruleSilence1 := testSilence{ID: "rule-1", RuleUID: utils.Pointer("rule-1-uid")}
|
||||
folder1 := "rule-1-folder-uid"
|
||||
folder1Scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder1)
|
||||
ruleSilence2 := testSilence{ID: "rule-2", RuleUID: utils.Pointer("rule-2-uid")}
|
||||
folder2 := "rule-2-folder-uid"
|
||||
notFoundRule := testSilence{ID: "unknown-rule", RuleUID: utils.Pointer("unknown-rule-uid")}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
user identity.Requester
|
||||
silence []testSilence
|
||||
expectedErr error
|
||||
expectedDbAccess bool
|
||||
}{
|
||||
{
|
||||
name: "not authorized without permissions",
|
||||
user: newUser(),
|
||||
silence: []testSilence{global, ruleSilence1, notFoundRule},
|
||||
expectedErr: ErrAuthorizationBase,
|
||||
expectedDbAccess: false,
|
||||
},
|
||||
{
|
||||
name: "instance reader can read any silence",
|
||||
user: newUser(ac.Permission{Action: instancesRead}),
|
||||
silence: []testSilence{global, ruleSilence1, notFoundRule},
|
||||
expectedErr: nil,
|
||||
expectedDbAccess: false,
|
||||
},
|
||||
{
|
||||
name: "silence reader can read global",
|
||||
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}),
|
||||
silence: []testSilence{global},
|
||||
expectedErr: nil,
|
||||
expectedDbAccess: false,
|
||||
},
|
||||
{
|
||||
name: "silence reader can read from allowed folder",
|
||||
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}),
|
||||
silence: []testSilence{ruleSilence1},
|
||||
expectedErr: nil,
|
||||
expectedDbAccess: true,
|
||||
},
|
||||
{
|
||||
name: "silence reader cannot read from other folders",
|
||||
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}),
|
||||
silence: []testSilence{ruleSilence2},
|
||||
expectedErr: ErrAuthorizationBase,
|
||||
expectedDbAccess: true,
|
||||
},
|
||||
{
|
||||
name: "silence reader cannot read unknown rule",
|
||||
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}),
|
||||
silence: []testSilence{notFoundRule},
|
||||
expectedErr: ErrAuthorizationBase,
|
||||
expectedDbAccess: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
for _, silence := range testCase.silence {
|
||||
t.Run(silence.ID, func(t *testing.T) {
|
||||
ac := &recordingAccessControlFake{}
|
||||
store := &fakeRuleUIDToNamespaceStore{
|
||||
Response: map[string]string{
|
||||
*ruleSilence1.RuleUID: folder1,
|
||||
*ruleSilence2.RuleUID: folder2,
|
||||
},
|
||||
}
|
||||
svc := NewSilenceService(ac, store)
|
||||
|
||||
err := svc.AuthorizeReadSilence(context.Background(), testCase.user, silence)
|
||||
if testCase.expectedErr != nil {
|
||||
assert.ErrorIs(t, err, testCase.expectedErr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
if testCase.expectedDbAccess {
|
||||
require.Equal(t, store.Calls, 1)
|
||||
} else {
|
||||
require.Equal(t, store.Calls, 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorizeCreateSilence(t *testing.T) {
|
||||
global := testSilence{ID: "global", RuleUID: nil}
|
||||
ruleSilence1 := testSilence{ID: "rule-1", RuleUID: utils.Pointer("rule-1-uid")}
|
||||
folder1 := "rule-1-folder-uid"
|
||||
folder1Scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder1)
|
||||
ruleSilence2 := testSilence{ID: "rule-2", RuleUID: utils.Pointer("rule-2-uid")}
|
||||
folder2 := "rule-2-folder-uid"
|
||||
folder2Scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder2)
|
||||
notFoundRule := testSilence{ID: "unknown-rule", RuleUID: utils.Pointer("unknown-rule-uid")}
|
||||
|
||||
silences := []testSilence{
|
||||
global,
|
||||
ruleSilence1,
|
||||
ruleSilence2,
|
||||
notFoundRule,
|
||||
}
|
||||
|
||||
type override struct {
|
||||
expectedErr error
|
||||
expectedDbAccess bool
|
||||
}
|
||||
testCases := []struct {
|
||||
name string
|
||||
user identity.Requester
|
||||
expectedErr error
|
||||
expectedDbAccess bool
|
||||
overrides map[testSilence]override
|
||||
}{
|
||||
{
|
||||
name: "not authorized without permissions",
|
||||
user: newUser(),
|
||||
expectedErr: ErrAuthorizationBase,
|
||||
},
|
||||
{
|
||||
name: "no create access, instance reader",
|
||||
user: newUser(ac.Permission{Action: instancesRead}),
|
||||
expectedErr: ErrAuthorizationBase,
|
||||
},
|
||||
{
|
||||
name: "no create access, silence reader",
|
||||
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}, ac.Permission{Action: silenceRead, Scope: folder2Scope}),
|
||||
expectedErr: ErrAuthorizationBase,
|
||||
},
|
||||
{
|
||||
name: "only create access, instance create",
|
||||
user: newUser(ac.Permission{Action: instancesCreate}),
|
||||
expectedErr: ErrAuthorizationBase,
|
||||
},
|
||||
{
|
||||
name: "only create access, silence create",
|
||||
user: newUser(ac.Permission{Action: silenceCreate, Scope: folder1Scope}, ac.Permission{Action: silenceCreate, Scope: folder2Scope}),
|
||||
expectedErr: ErrAuthorizationBase,
|
||||
},
|
||||
{
|
||||
name: "instance read + create can do everything",
|
||||
user: newUser(ac.Permission{Action: instancesCreate}, ac.Permission{Action: instancesRead}),
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "instance read + silence create",
|
||||
user: newUser(ac.Permission{Action: silenceCreate, Scope: folder1Scope}, ac.Permission{Action: instancesRead}),
|
||||
overrides: map[testSilence]override{
|
||||
global: {
|
||||
expectedErr: ErrAuthorizationBase,
|
||||
expectedDbAccess: false,
|
||||
},
|
||||
ruleSilence1: {
|
||||
expectedErr: nil,
|
||||
expectedDbAccess: true,
|
||||
},
|
||||
},
|
||||
expectedErr: ErrAuthorizationBase,
|
||||
expectedDbAccess: true,
|
||||
},
|
||||
{
|
||||
name: "silence read + instance create",
|
||||
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}, ac.Permission{Action: instancesCreate}),
|
||||
overrides: map[testSilence]override{
|
||||
global: {
|
||||
expectedErr: nil,
|
||||
expectedDbAccess: false,
|
||||
},
|
||||
ruleSilence1: {
|
||||
expectedErr: nil,
|
||||
expectedDbAccess: true,
|
||||
},
|
||||
},
|
||||
expectedErr: ErrAuthorizationBase,
|
||||
expectedDbAccess: true,
|
||||
},
|
||||
{
|
||||
name: "silence read + create",
|
||||
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}, ac.Permission{Action: silenceCreate, Scope: folder1Scope}),
|
||||
overrides: map[testSilence]override{
|
||||
global: {
|
||||
expectedErr: ErrAuthorizationBase,
|
||||
expectedDbAccess: false,
|
||||
},
|
||||
ruleSilence1: {
|
||||
expectedErr: nil,
|
||||
expectedDbAccess: true,
|
||||
},
|
||||
},
|
||||
expectedErr: ErrAuthorizationBase,
|
||||
expectedDbAccess: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
for _, silence := range silences {
|
||||
expectedErr := testCase.expectedErr
|
||||
expectedDbAccess := testCase.expectedDbAccess
|
||||
if s, ok := testCase.overrides[silence]; ok {
|
||||
expectedErr = s.expectedErr
|
||||
expectedDbAccess = s.expectedDbAccess
|
||||
}
|
||||
t.Run(silence.ID, func(t *testing.T) {
|
||||
ac := &recordingAccessControlFake{}
|
||||
store := &fakeRuleUIDToNamespaceStore{
|
||||
Response: map[string]string{
|
||||
*ruleSilence1.RuleUID: folder1,
|
||||
*ruleSilence2.RuleUID: folder2,
|
||||
},
|
||||
}
|
||||
svc := NewSilenceService(ac, store)
|
||||
|
||||
err := svc.AuthorizeCreateSilence(context.Background(), testCase.user, silence)
|
||||
if expectedErr != nil {
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, expectedErr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
if expectedDbAccess {
|
||||
require.Equal(t, 1, store.Calls)
|
||||
} else {
|
||||
require.Equal(t, 0, store.Calls)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorizeUpdateSilence(t *testing.T) {
|
||||
global := testSilence{ID: "global", RuleUID: nil}
|
||||
ruleSilence1 := testSilence{ID: "rule-1", RuleUID: utils.Pointer("rule-1-uid")}
|
||||
folder1 := "rule-1-folder-uid"
|
||||
folder1Scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder1)
|
||||
ruleSilence2 := testSilence{ID: "rule-2", RuleUID: utils.Pointer("rule-2-uid")}
|
||||
folder2 := "rule-2-folder-uid"
|
||||
folder2Scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder2)
|
||||
notFoundRule := testSilence{ID: "unknown-rule", RuleUID: utils.Pointer("unknown-rule-uid")}
|
||||
|
||||
silences := []testSilence{
|
||||
global,
|
||||
ruleSilence1,
|
||||
ruleSilence2,
|
||||
notFoundRule,
|
||||
}
|
||||
|
||||
type override struct {
|
||||
expectedErr error
|
||||
expectedDbAccess bool
|
||||
}
|
||||
testCases := []struct {
|
||||
name string
|
||||
user identity.Requester
|
||||
expectedErr error
|
||||
expectedDbAccess bool
|
||||
overrides map[testSilence]override
|
||||
}{
|
||||
{
|
||||
name: "not authorized without permissions",
|
||||
user: newUser(),
|
||||
expectedErr: ErrAuthorizationBase,
|
||||
},
|
||||
{
|
||||
name: "no write access, instance reader",
|
||||
user: newUser(ac.Permission{Action: instancesRead}),
|
||||
expectedErr: ErrAuthorizationBase,
|
||||
},
|
||||
{
|
||||
name: "no write access, silence reader",
|
||||
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}, ac.Permission{Action: silenceRead, Scope: folder2Scope}),
|
||||
expectedErr: ErrAuthorizationBase,
|
||||
},
|
||||
{
|
||||
name: "only write access, instance write",
|
||||
user: newUser(ac.Permission{Action: instancesWrite}),
|
||||
expectedErr: ErrAuthorizationBase,
|
||||
},
|
||||
{
|
||||
name: "only write access, silence write",
|
||||
user: newUser(ac.Permission{Action: silenceWrite, Scope: folder1Scope}, ac.Permission{Action: silenceWrite, Scope: folder2Scope}),
|
||||
expectedErr: ErrAuthorizationBase,
|
||||
},
|
||||
{
|
||||
name: "instance read + write can do everything",
|
||||
user: newUser(ac.Permission{Action: instancesWrite}, ac.Permission{Action: instancesRead}),
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "instance read + silence write",
|
||||
user: newUser(ac.Permission{Action: silenceWrite, Scope: folder1Scope}, ac.Permission{Action: instancesRead}),
|
||||
overrides: map[testSilence]override{
|
||||
global: {
|
||||
expectedErr: ErrAuthorizationBase,
|
||||
expectedDbAccess: false,
|
||||
},
|
||||
ruleSilence1: {
|
||||
expectedErr: nil,
|
||||
expectedDbAccess: true,
|
||||
},
|
||||
},
|
||||
expectedErr: ErrAuthorizationBase,
|
||||
expectedDbAccess: true,
|
||||
},
|
||||
{
|
||||
name: "silence read + instance write",
|
||||
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}, ac.Permission{Action: instancesWrite}),
|
||||
overrides: map[testSilence]override{
|
||||
global: {
|
||||
expectedErr: nil,
|
||||
expectedDbAccess: false,
|
||||
},
|
||||
ruleSilence1: {
|
||||
expectedErr: nil,
|
||||
expectedDbAccess: true,
|
||||
},
|
||||
},
|
||||
expectedErr: ErrAuthorizationBase,
|
||||
expectedDbAccess: true,
|
||||
},
|
||||
{
|
||||
name: "silence read + write",
|
||||
user: newUser(ac.Permission{Action: silenceRead, Scope: folder1Scope}, ac.Permission{Action: silenceWrite, Scope: folder1Scope}),
|
||||
overrides: map[testSilence]override{
|
||||
global: {
|
||||
expectedErr: ErrAuthorizationBase,
|
||||
expectedDbAccess: false,
|
||||
},
|
||||
ruleSilence1: {
|
||||
expectedErr: nil,
|
||||
expectedDbAccess: true,
|
||||
},
|
||||
},
|
||||
expectedErr: ErrAuthorizationBase,
|
||||
expectedDbAccess: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
for _, silence := range silences {
|
||||
expectedErr := testCase.expectedErr
|
||||
expectedDbAccess := testCase.expectedDbAccess
|
||||
if s, ok := testCase.overrides[silence]; ok {
|
||||
expectedErr = s.expectedErr
|
||||
expectedDbAccess = s.expectedDbAccess
|
||||
}
|
||||
t.Run(silence.ID, func(t *testing.T) {
|
||||
ac := &recordingAccessControlFake{}
|
||||
store := &fakeRuleUIDToNamespaceStore{
|
||||
Response: map[string]string{
|
||||
*ruleSilence1.RuleUID: folder1,
|
||||
*ruleSilence2.RuleUID: folder2,
|
||||
},
|
||||
}
|
||||
svc := NewSilenceService(ac, store)
|
||||
|
||||
err := svc.AuthorizeUpdateSilence(context.Background(), testCase.user, silence)
|
||||
if expectedErr != nil {
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, expectedErr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
if expectedDbAccess {
|
||||
require.Equal(t, 1, store.Calls)
|
||||
} else {
|
||||
require.Equal(t, 0, store.Calls)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type testSilence struct {
|
||||
ID string
|
||||
RuleUID *string
|
||||
}
|
||||
|
||||
func (t testSilence) GetRuleUID() *string {
|
||||
return t.RuleUID
|
||||
}
|
||||
|
||||
type fakeRuleUIDToNamespaceStore struct {
|
||||
Response map[string]string
|
||||
Calls int
|
||||
}
|
||||
|
||||
func (f *fakeRuleUIDToNamespaceStore) GetNamespacesByRuleUID(ctx context.Context, orgID int64, uids ...string) (map[string]string, error) {
|
||||
f.Calls++
|
||||
return f.Response, nil
|
||||
}
|
||||
|
||||
func newUser(permissions ...ac.Permission) identity.Requester {
|
||||
return ac.BackgroundUser("test", orgID, org.RoleNone, permissions)
|
||||
}
|
Loading…
Reference in New Issue
Block a user