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:
Yuri Tseretyan 2024-04-08 18:02:28 -04:00 committed by GitHub
parent 9d7e758e04
commit 509691b416
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 792 additions and 25 deletions

View File

@ -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"

View 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
}

View File

@ -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(

View File

@ -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)

View 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
}

View 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)
}