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/expr" "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/datasources" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/ngalert/models" "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 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 { var scopes []string for _, rule := range c.New { for _, query := range rule.Data { scopes = append(scopes, datasources.ScopeProvider.GetResourceScopeUID(query.DatasourceUID)) } } return map[string][]string{ ruleCreate: { namespaceIdScope, }, ruleRead: { namespaceIdScope, }, dashboards.ActionFoldersRead: { namespaceIdScope, }, datasources.ActionQuery: scopes, } }, }, { 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.AffectedGroups[c.GroupKey]), } }, }, { 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(append(c.AffectedGroups[c.GroupKey], 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( append(append(append(c.AffectedGroups[c.GroupKey], mapUpdates(c.Update, func(update store.RuleDelta) *models.AlertRule { return update.New })..., ), mapUpdates(c.Update, func(update store.RuleDelta) *models.AlertRule { return update.Existing })...), c.AffectedGroups[groupKey]...), ) 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 { scopes := make(map[string]struct{}) for _, update := range c.Update { for _, query := range update.New.Data { scopes[datasources.ScopeProvider.GetResourceScopeUID(query.DatasourceUID)] = struct{}{} } for _, query := range update.Existing.Data { scopes[datasources.ScopeProvider.GetResourceScopeUID(query.DatasourceUID)] = struct{}{} } } for _, rules := range c.AffectedGroups { for _, rule := range rules { for _, query := range rule.Data { scopes[datasources.ScopeProvider.GetResourceScopeUID(query.DatasourceUID)] = struct{}{} } } } dsScopes := make([]string, 0, len(scopes)) for key := range scopes { dsScopes = append(dsScopes, key) } 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, } }, }, } 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 := RuleService{ genericService{ac: 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 := &recordingAccessControlFake{ Callback: func(user identity.Requester, evaluator accesscontrol.Evaluator) (bool, error) { response := evaluator.Evaluate(user.GetPermissions()) require.Truef(t, response, "provided permissions [%v] is not enough for requested permissions [%s]", permissions, evaluator.GoString()) return response, nil }, } srv := RuleService{ genericService{ac: ac}, } err := srv.AuthorizeRuleChanges(context.Background(), createUserWithPermissions(permissions), groupChanges) require.NoError(t, err) require.NotEmptyf(t, ac.EvaluateRecordings, "evaluation function is expected to be called but it was not.") }) } } 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 := RuleService{ genericService{ac: 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 := RuleService{ genericService{ac: 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 return true if user has access to all datasources of all rules in group", func(t *testing.T) { rules := models.RuleGen.GenerateManyRef(1, 5) var scopes []string for _, rule := range rules { for _, query := range rule.Data { scopes = append(scopes, datasources.ScopeProvider.GetResourceScopeUID(query.DatasourceUID)) } } 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, datasources.ActionQuery: scopes, } ac := &recordingAccessControlFake{} svc := RuleService{ genericService{ac: ac}, } result := svc.AuthorizeAccessToRuleGroup(context.Background(), createUserWithPermissions(permissions), rules) require.NoError(t, result) require.NotEmpty(t, ac.EvaluateRecordings) }) t.Run("should return false if user does not have access to at least one rule in group", func(t *testing.T) { f := &folder.Folder{UID: "test-folder"} gen := models.RuleGen genWithFolder := gen.With(gen.WithNamespace(f)) rules := genWithFolder.GenerateManyRef(1, 5) var scopes []string for _, rule := range rules { for _, query := range rule.Data { scopes = append(scopes, datasources.ScopeProvider.GetResourceScopeUID(query.DatasourceUID)) } } permissions := map[string][]string{ ruleRead: { dashboards.ScopeFoldersProvider.GetResourceScopeUID(f.UID), }, dashboards.ActionFoldersRead: { dashboards.ScopeFoldersProvider.GetResourceScopeUID(f.UID), }, datasources.ActionQuery: scopes, } rule := genWithFolder.GenerateRef() rules = append(rules, rule) ac := &recordingAccessControlFake{} svc := RuleService{ genericService{ac: ac}, } result := svc.AuthorizeAccessToRuleGroup(context.Background(), createUserWithPermissions(permissions), rules) require.Error(t, result) }) }