package accesscontrol import ( "context" "math" "math/rand" "testing" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util" ) func createAllCombinationsOfPermissions(permissions map[string][]string) []map[string][]string { type actionscope struct { action string scope string } var flattenPermissions []actionscope for action, scopes := range permissions { for _, scope := range scopes { flattenPermissions = append(flattenPermissions, actionscope{ action, scope, }) } } l := len(flattenPermissions) // this is all possible combinations of the permissions var permissionCombinations []map[string][]string for bit := uint(0); bit < uint(math.Pow(2, float64(l))); bit++ { var tuple []actionscope for idx := 0; idx < l; idx++ { if (bit>>idx)&1 == 1 { tuple = append(tuple, flattenPermissions[idx]) } } combination := make(map[string][]string) for _, perm := range tuple { combination[perm.action] = append(combination[perm.action], perm.scope) } permissionCombinations = append(permissionCombinations, combination) } return permissionCombinations } func getDatasourceScopesForRules(rules models.RulesGroup) []string { scopesMap := map[string]struct{}{} var result []string for _, rule := range rules { for _, query := range rule.Data { scope := datasources.ScopeProvider.GetResourceScopeUID(query.DatasourceUID) if _, ok := scopesMap[scope]; ok { continue } result = append(result, scope) scopesMap[scope] = struct{}{} } } return result } func getReceiverScopesForRules(rules models.RulesGroup) []string { scopesMap := map[string]struct{}{} var result []string for _, rule := range rules { for _, ns := range rule.NotificationSettings { scope := ScopeReceiversProvider.GetResourceScopeUID(legacy_storage.NameToUid(ns.Receiver)) if _, ok := scopesMap[scope]; ok { continue } result = append(result, scope) scopesMap[scope] = struct{}{} } } return result } func mapUpdates(updates []store.RuleDelta, mapFunc func(store.RuleDelta) *models.AlertRule) models.RulesGroup { result := make(models.RulesGroup, 0, len(updates)) for _, update := range updates { result = append(result, mapFunc(update)) } return result } func createUserWithPermissions(permissions map[string][]string) identity.Requester { return &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{ 1: permissions, }} } func TestAuthorizeRuleChanges(t *testing.T) { groupKey := models.GenerateGroupKey(rand.Int63()) namespaceIdScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(groupKey.NamespaceUID) gen := models.RuleGen genWithGroupKey := gen.With(gen.WithGroupKey(groupKey)) testCases := []struct { name string changes func() *store.GroupDelta permissions func(c *store.GroupDelta) map[string][]string }{ { name: "if there are rules to add it should check create action and query for datasource", changes: func() *store.GroupDelta { return &store.GroupDelta{ GroupKey: groupKey, New: genWithGroupKey.GenerateManyRef(1, 5), Update: nil, Delete: nil, } }, permissions: func(c *store.GroupDelta) map[string][]string { return map[string][]string{ ruleCreate: { namespaceIdScope, }, ruleRead: { namespaceIdScope, }, dashboards.ActionFoldersRead: { namespaceIdScope, }, datasources.ActionQuery: getDatasourceScopesForRules(c.New), accesscontrol.ActionAlertingReceiversRead: getReceiverScopesForRules(c.New), } }, }, { name: "if there are rules to delete it should check delete action and query for datasource", changes: func() *store.GroupDelta { rules := genWithGroupKey.GenerateManyRef(1, 5) rules2 := genWithGroupKey.GenerateManyRef(1, 5) return &store.GroupDelta{ GroupKey: groupKey, AffectedGroups: map[models.AlertRuleGroupKey]models.RulesGroup{ groupKey: append(rules, rules2...), }, New: nil, Update: nil, Delete: rules2, } }, permissions: func(c *store.GroupDelta) map[string][]string { return map[string][]string{ ruleRead: { namespaceIdScope, }, dashboards.ActionFoldersRead: { namespaceIdScope, }, ruleDelete: { namespaceIdScope, }, datasources.ActionQuery: getDatasourceScopesForRules(c.Delete), } }, }, { name: "if there are rules to update within the same namespace it should check update action and access to datasource", changes: func() *store.GroupDelta { rules1 := genWithGroupKey.GenerateManyRef(1, 5) rules := genWithGroupKey.GenerateManyRef(1, 5) updates := make([]store.RuleDelta, 0, len(rules)) for _, rule := range rules { cp := models.CopyRule(rule) cp.Data = []models.AlertQuery{models.GenerateAlertQuery()} updates = append(updates, store.RuleDelta{ Existing: rule, New: cp, Diff: nil, }) } return &store.GroupDelta{ GroupKey: groupKey, AffectedGroups: map[models.AlertRuleGroupKey]models.RulesGroup{ groupKey: append(rules, rules1...), }, New: nil, Update: updates, Delete: nil, } }, permissions: func(c *store.GroupDelta) map[string][]string { scopes := getDatasourceScopesForRules(mapUpdates(c.Update, func(update store.RuleDelta) *models.AlertRule { return update.New })) return map[string][]string{ ruleRead: { namespaceIdScope, }, dashboards.ActionFoldersRead: { namespaceIdScope, }, ruleUpdate: { namespaceIdScope, }, datasources.ActionQuery: scopes, } }, }, { name: "if there are rules that are moved between namespaces it should check delete+add action and access to group where rules come from", changes: func() *store.GroupDelta { rules1 := genWithGroupKey.GenerateManyRef(1, 5) rules := genWithGroupKey.GenerateManyRef(1, 5) targetGroupKey := models.GenerateGroupKey(groupKey.OrgID) updates := make([]store.RuleDelta, 0, len(rules)) for _, rule := range rules { cp := models.CopyRule(rule, gen.WithGroupKey(targetGroupKey), gen.WithQuery(gen.GenerateQuery())) updates = append(updates, store.RuleDelta{ Existing: rule, New: cp, }) } return &store.GroupDelta{ GroupKey: targetGroupKey, AffectedGroups: map[models.AlertRuleGroupKey]models.RulesGroup{ groupKey: append(rules, rules1...), }, New: nil, Update: updates, Delete: nil, } }, permissions: func(c *store.GroupDelta) map[string][]string { dsScopes := getDatasourceScopesForRules( mapUpdates(c.Update, func(update store.RuleDelta) *models.AlertRule { return update.New }), ) var deleteScopes []string for key := range c.AffectedGroups { deleteScopes = append(deleteScopes, dashboards.ScopeFoldersProvider.GetResourceScopeUID(key.NamespaceUID)) } return map[string][]string{ ruleDelete: deleteScopes, ruleCreate: { dashboards.ScopeFoldersProvider.GetResourceScopeUID(c.GroupKey.NamespaceUID), }, datasources.ActionQuery: dsScopes, } }, }, { name: "if there are rules that are moved between groups in the same namespace it should check update action and access to all groups (source+target)", changes: func() *store.GroupDelta { targetGroupKey := models.AlertRuleGroupKey{ OrgID: groupKey.OrgID, NamespaceUID: groupKey.NamespaceUID, RuleGroup: util.GenerateShortUID(), } sourceGroup := genWithGroupKey.GenerateManyRef(1, 5) targetGroup := gen.With(gen.WithGroupKey(targetGroupKey)).GenerateManyRef(1, 5) updates := make([]store.RuleDelta, 0, len(sourceGroup)) toCopy := len(sourceGroup) if toCopy > 1 { toCopy = rand.Intn(toCopy-1) + 1 } for i := 0; i < toCopy; i++ { rule := sourceGroup[0] cp := models.CopyRule(rule, gen.WithGroupKey(targetGroupKey), gen.WithQuery(models.GenerateAlertQuery())) updates = append(updates, store.RuleDelta{ Existing: rule, New: cp, }) } return &store.GroupDelta{ GroupKey: targetGroupKey, AffectedGroups: map[models.AlertRuleGroupKey]models.RulesGroup{ groupKey: sourceGroup, targetGroupKey: targetGroup, }, New: nil, Update: updates, Delete: nil, } }, permissions: func(c *store.GroupDelta) map[string][]string { dsScopes := getDatasourceScopesForRules( mapUpdates(c.Update, func(update store.RuleDelta) *models.AlertRule { return update.New }), ) return map[string][]string{ ruleRead: { dashboards.ScopeFoldersProvider.GetResourceScopeUID(c.GroupKey.NamespaceUID), }, dashboards.ActionFoldersRead: { dashboards.ScopeFoldersProvider.GetResourceScopeUID(c.GroupKey.NamespaceUID), }, ruleUpdate: { dashboards.ScopeFoldersProvider.GetResourceScopeUID(c.GroupKey.NamespaceUID), }, datasources.ActionQuery: dsScopes, } }, }, { name: "if there are new rules that have notification settings it should check access to all receivers", changes: func() *store.GroupDelta { receiverName := "test-receiver" genWithNotificationSettings := genWithGroupKey.With(gen.WithNotificationSettingsGen(models.NotificationSettingsGen(models.NSMuts.WithReceiver(receiverName)))) return &store.GroupDelta{ GroupKey: groupKey, New: genWithNotificationSettings.GenerateManyRef(1, 5), Update: nil, Delete: nil, } }, permissions: func(c *store.GroupDelta) map[string][]string { return map[string][]string{ ruleCreate: { namespaceIdScope, }, ruleRead: { namespaceIdScope, }, dashboards.ActionFoldersRead: { namespaceIdScope, }, datasources.ActionQuery: getDatasourceScopesForRules(c.New), accesscontrol.ActionAlertingReceiversRead: getReceiverScopesForRules(c.New), } }, }, { name: "if there are rules that modify notification settings it should check access to all receivers", changes: func() *store.GroupDelta { receiverName := "test-receiver" genWithNotificationSettings := genWithGroupKey.With(gen.WithNotificationSettingsGen(models.NotificationSettingsGen(models.NSMuts.WithReceiver(receiverName)))) rules1 := genWithNotificationSettings.GenerateManyRef(1, 5) rules := genWithNotificationSettings.GenerateManyRef(1, 5) updates := make([]store.RuleDelta, 0, len(rules)) for _, rule := range rules { cp := models.CopyRule(rule) for i := range cp.NotificationSettings { cp.NotificationSettings[i].Receiver = "new-receiver" } updates = append(updates, store.RuleDelta{ Existing: rule, New: cp, Diff: nil, }) } return &store.GroupDelta{ GroupKey: groupKey, AffectedGroups: map[models.AlertRuleGroupKey]models.RulesGroup{ groupKey: append(rules, rules1...), }, New: nil, Update: updates, Delete: nil, } }, permissions: func(c *store.GroupDelta) map[string][]string { return map[string][]string{ ruleRead: { namespaceIdScope, }, dashboards.ActionFoldersRead: { namespaceIdScope, }, ruleUpdate: { namespaceIdScope, }, datasources.ActionQuery: getDatasourceScopesForRules(mapUpdates(c.Update, func(update store.RuleDelta) *models.AlertRule { return update.New })), accesscontrol.ActionAlertingReceiversRead: getReceiverScopesForRules(mapUpdates(c.Update, func(update store.RuleDelta) *models.AlertRule { return update.New })), } }, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { groupChanges := testCase.changes() permissions := testCase.permissions(groupChanges) t.Run("should fail with insufficient permissions", func(t *testing.T) { permissionCombinations := createAllCombinationsOfPermissions(permissions) permissionCombinations = permissionCombinations[0 : len(permissionCombinations)-1] // exclude all permissions for _, missing := range permissionCombinations { ac := &recordingAccessControlFake{} srv := NewRuleService(ac) err := srv.AuthorizeRuleChanges(context.Background(), createUserWithPermissions(missing), groupChanges) assert.Errorf(t, err, "expected error because less permissions than expected were provided. Provided: %v; Expected: %v; Diff: %v", missing, permissions, cmp.Diff(permissions, missing)) require.NotEmptyf(t, ac.EvaluateRecordings, "Access control was supposed to be called but it was not") } }) ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures()) srv := NewRuleService(ac) err := srv.AuthorizeRuleChanges(context.Background(), createUserWithPermissions(permissions), groupChanges) require.NoError(t, err) }) } } func TestCheckDatasourcePermissionsForRule(t *testing.T) { rule := models.RuleGen.GenerateRef() expressionByType := models.GenerateAlertQuery() expressionByType.QueryType = expr.DatasourceType expressionByUID := models.GenerateAlertQuery() expressionByUID.DatasourceUID = expr.DatasourceUID var data []models.AlertQuery var scopes []string for i := 0; i < rand.Intn(3)+2; i++ { q := models.GenerateAlertQuery() scopes = append(scopes, datasources.ScopeProvider.GetResourceScopeUID(q.DatasourceUID)) data = append(data, q) } data = append(data, expressionByType, expressionByUID) rand.Shuffle(len(data), func(i, j int) { data[j], data[i] = data[i], data[j] }) rule.Data = data t.Run("should check only expressions", func(t *testing.T) { permissions := map[string][]string{ ruleRead: { dashboards.ScopeFoldersProvider.GetResourceScopeUID(rule.NamespaceUID), }, dashboards.ActionFoldersRead: { dashboards.ScopeFoldersProvider.GetResourceScopeUID(rule.NamespaceUID), }, datasources.ActionQuery: scopes, } ac := &recordingAccessControlFake{} svc := NewRuleService(ac) eval := svc.AuthorizeDatasourceAccessForRule(context.Background(), createUserWithPermissions(permissions), rule) require.NoError(t, eval) require.Len(t, ac.EvaluateRecordings, 1) }) t.Run("should return on first negative evaluation", func(t *testing.T) { ac := &recordingAccessControlFake{ Callback: func(user identity.Requester, evaluator accesscontrol.Evaluator) (bool, error) { return false, nil }, } svc := NewRuleService(ac) result := svc.AuthorizeDatasourceAccessForRule(context.Background(), createUserWithPermissions(nil), rule) require.Error(t, result) require.Len(t, ac.EvaluateRecordings, 1) }) } func Test_authorizeAccessToRuleGroup(t *testing.T) { t.Run("should succeed if user has access to all namespaces", func(t *testing.T) { rules := models.RuleGen.GenerateManyRef(1, 5) namespaceScopes := make([]string, 0) for _, rule := range rules { namespaceScopes = append(namespaceScopes, dashboards.ScopeFoldersProvider.GetResourceScopeUID(rule.NamespaceUID)) } permissions := map[string][]string{ ruleRead: namespaceScopes, dashboards.ActionFoldersRead: namespaceScopes, } ac := &recordingAccessControlFake{} svc := NewRuleService(ac) result := svc.AuthorizeAccessToRuleGroup(context.Background(), createUserWithPermissions(permissions), rules) require.NoError(t, result) require.NotEmpty(t, ac.EvaluateRecordings) }) t.Run("should fail if user does not have access to namespace", func(t *testing.T) { f := &folder.Folder{UID: "test-folder"} gen := models.RuleGen genWithFolder := gen.With(gen.WithNamespace(f)) rules := genWithFolder.GenerateManyRef(1, 5) ac := &recordingAccessControlFake{} svc := NewRuleService(ac) result := svc.AuthorizeAccessToRuleGroup(context.Background(), createUserWithPermissions(map[string][]string{}), rules) require.Error(t, result) require.ErrorIs(t, result, ErrAuthorizationBase) }) } func TestCanReadAllRules(t *testing.T) { ac := &recordingAccessControlFake{} svc := NewRuleService(ac) testCases := []struct { permissions map[string][]string expected bool }{ { permissions: map[string][]string{ ruleRead: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}, dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}, }, expected: true, }, { permissions: make(map[string][]string), }, { permissions: map[string][]string{ ruleRead: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("test")}, dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}, }, }, { permissions: map[string][]string{ ruleRead: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}, dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("test")}, }, }, { permissions: map[string][]string{ ruleRead: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}, }, }, { permissions: map[string][]string{ dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}, }, }, } for _, tc := range testCases { result, err := svc.CanReadAllRules(context.Background(), createUserWithPermissions(tc.permissions)) assert.NoError(t, err) assert.Equalf(t, tc.expected, result, "permissions: %v", tc.permissions) } }