mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Update rule access control to return errutil errors (#78284)
* update rule access control to return errutil errors * use alerting in msgID
This commit is contained in:
parent
6644e5e676
commit
64feeddc23
@ -1,9 +1,28 @@
|
|||||||
package accesscontrol
|
package accesscontrol
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/util/errutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrAuthorization = errors.New("user is not authorized")
|
errAuthorizationGeneric = errutil.Unauthorized("alerting.unauthorized")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func NewAuthorizationErrorWithPermissions(action string, eval accesscontrol.Evaluator) error {
|
||||||
|
msg := fmt.Sprintf("user is not authorized to %s", action)
|
||||||
|
err := errAuthorizationGeneric.Errorf(msg)
|
||||||
|
err.PublicMessage = msg
|
||||||
|
if eval != nil {
|
||||||
|
err.PublicPayload = map[string]any{
|
||||||
|
"permissions": eval.GoString(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthorizationErrorGeneric(action string) error {
|
||||||
|
return NewAuthorizationErrorWithPermissions(action, nil)
|
||||||
|
}
|
||||||
|
@ -6,7 +6,6 @@ import (
|
|||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/expr"
|
"github.com/grafana/grafana/pkg/expr"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/auth/identity"
|
"github.com/grafana/grafana/pkg/services/auth/identity"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
@ -22,8 +21,6 @@ const (
|
|||||||
ruleDelete = accesscontrol.ActionAlertingRuleDelete
|
ruleDelete = accesscontrol.ActionAlertingRuleDelete
|
||||||
)
|
)
|
||||||
|
|
||||||
var logger = log.New("ngalert.accesscontrol")
|
|
||||||
|
|
||||||
type RuleService struct {
|
type RuleService struct {
|
||||||
ac accesscontrol.AccessControl
|
ac accesscontrol.AccessControl
|
||||||
}
|
}
|
||||||
@ -34,51 +31,92 @@ func NewRuleService(ac accesscontrol.AccessControl) *RuleService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasAccess returns true if the user has all permissions specified by the evaluator
|
// 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 {
|
func (r *RuleService) HasAccess(ctx context.Context, user identity.Requester, evaluator accesscontrol.Evaluator) (bool, error) {
|
||||||
result, err := r.ac.Evaluate(ctx, user, evaluator)
|
return r.ac.Evaluate(ctx, user, evaluator)
|
||||||
if err != nil { // this is how accesscontrol.HasAccess works. //TODO change when AuthorizeDatasourceAccessForRule can return errors
|
}
|
||||||
logger.FromContext(ctx).Error("Failed to evaluate access control", "error", err)
|
|
||||||
return false
|
// 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
|
||||||
}
|
}
|
||||||
return result
|
if !has {
|
||||||
|
return NewAuthorizationErrorWithPermissions(action(), evaluator)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getRulesReadEvaluator constructs accesscontrol.Evaluator that checks all permission required to read all provided rules
|
||||||
|
func (r *RuleService) getRulesReadEvaluator(rules ...*models.AlertRule) accesscontrol.Evaluator {
|
||||||
|
return r.getRulesQueryEvaluator(rules...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getRulesQueryEvaluator constructs accesscontrol.Evaluator that checks all permissions to query data sources used by the provided rules
|
||||||
|
func (r *RuleService) getRulesQueryEvaluator(rules ...*models.AlertRule) accesscontrol.Evaluator {
|
||||||
|
added := make(map[string]struct{}, 2)
|
||||||
|
evals := make([]accesscontrol.Evaluator, 0, 2)
|
||||||
|
for _, rule := range rules {
|
||||||
|
for _, query := range rule.Data {
|
||||||
|
if query.QueryType == expr.DatasourceType || query.DatasourceUID == expr.DatasourceUID || query.
|
||||||
|
DatasourceUID == expr.OldDatasourceUID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := added[query.DatasourceUID]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
evals = append(evals, accesscontrol.EvalPermission(datasources.ActionQuery, datasources.ScopeProvider.GetResourceScopeUID(query.DatasourceUID)))
|
||||||
|
added[query.DatasourceUID] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(evals) == 1 {
|
||||||
|
return evals[0]
|
||||||
|
}
|
||||||
|
return accesscontrol.EvalAll(evals...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthorizeDatasourceAccessForRule checks that user has access to all data sources declared by the rule
|
// AuthorizeDatasourceAccessForRule checks that user has access to all data sources declared by the rule
|
||||||
func (r *RuleService) AuthorizeDatasourceAccessForRule(ctx context.Context, user identity.Requester, rule *models.AlertRule) bool {
|
func (r *RuleService) AuthorizeDatasourceAccessForRule(ctx context.Context, user identity.Requester, rule *models.AlertRule) error {
|
||||||
for _, query := range rule.Data {
|
ds := r.getRulesQueryEvaluator(rule)
|
||||||
if query.QueryType == expr.DatasourceType || query.DatasourceUID == expr.DatasourceUID || query.
|
return r.HasAccessOrError(ctx, user, ds, func() string {
|
||||||
DatasourceUID == expr.OldDatasourceUID {
|
suffix := ""
|
||||||
continue
|
if rule.UID != "" {
|
||||||
|
suffix = fmt.Sprintf(" of the rule UID '%s'", rule.UID)
|
||||||
}
|
}
|
||||||
if !r.HasAccess(ctx, user, accesscontrol.EvalPermission(datasources.ActionQuery, datasources.ScopeProvider.GetResourceScopeUID(query.DatasourceUID))) {
|
return fmt.Sprintf("access one or many data sources%s", suffix)
|
||||||
return false
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return true
|
// HasAccessToRuleGroup returns false if
|
||||||
|
func (r *RuleService) HasAccessToRuleGroup(ctx context.Context, user identity.Requester, rules models.RulesGroup) (bool, error) {
|
||||||
|
eval := r.getRulesReadEvaluator(rules...)
|
||||||
|
return r.HasAccess(ctx, user, eval)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthorizeAccessToRuleGroup checks all rules against AuthorizeDatasourceAccessForRule and exits on the first negative result
|
// AuthorizeAccessToRuleGroup checks all rules against AuthorizeDatasourceAccessForRule and exits on the first negative result
|
||||||
func (r *RuleService) AuthorizeAccessToRuleGroup(ctx context.Context, user identity.Requester, rules models.RulesGroup) bool {
|
func (r *RuleService) AuthorizeAccessToRuleGroup(ctx context.Context, user identity.Requester, rules models.RulesGroup) error {
|
||||||
for _, rule := range rules {
|
eval := r.getRulesReadEvaluator(rules...)
|
||||||
if !r.AuthorizeDatasourceAccessForRule(ctx, user, rule) {
|
return r.HasAccessOrError(ctx, user, eval, func() string {
|
||||||
return false
|
var groupName, folderUID string
|
||||||
|
if len(rules) > 0 {
|
||||||
|
groupName = rules[0].RuleGroup
|
||||||
|
folderUID = rules[0].NamespaceUID
|
||||||
}
|
}
|
||||||
}
|
return fmt.Sprintf("access rule group '%s' in folder '%s'", groupName, folderUID)
|
||||||
return true
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthorizeRuleChanges analyzes changes in the rule group, and checks whether the changes are authorized.
|
// AuthorizeRuleChanges analyzes changes in the rule group, and checks whether the changes are authorized.
|
||||||
// NOTE: if there are rules for deletion, and the user does not have access to data sources that a rule uses, the rule is removed from the list.
|
// NOTE: if there are rules for deletion, and the user does not have access to data sources that a rule uses, the rule is removed from the list.
|
||||||
// If the user is not authorized to perform the changes the function returns ErrAuthorization with a description of what action is not authorized.
|
// If the user is not authorized to perform the changes the function returns ErrAuthorization with a description of what action is not authorized.
|
||||||
// Return changes that the user is authorized to perform or ErrAuthorization
|
|
||||||
func (r *RuleService) AuthorizeRuleChanges(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
|
func (r *RuleService) AuthorizeRuleChanges(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
|
||||||
namespaceScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(change.GroupKey.NamespaceUID)
|
namespaceScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(change.GroupKey.NamespaceUID)
|
||||||
|
|
||||||
rules, ok := change.AffectedGroups[change.GroupKey]
|
rules, ok := change.AffectedGroups[change.GroupKey]
|
||||||
if ok { // not ok can be when user creates a new rule group or moves existing alerts to a new group
|
if ok { // not ok can be when user creates a new rule group or moves existing alerts to a new group
|
||||||
if !r.AuthorizeAccessToRuleGroup(ctx, user, rules) { // if user is not authorized to do operation in the group that is being changed
|
if err := r.AuthorizeAccessToRuleGroup(ctx, user, rules); err != nil { // if user is not authorized to do operation in the group that is being changed
|
||||||
return fmt.Errorf("%w to change group %s because it does not have access to one or many rules in this group", ErrAuthorization, change.GroupKey.RuleGroup)
|
return err
|
||||||
}
|
}
|
||||||
} else if len(change.Delete) > 0 {
|
} else if len(change.Delete) > 0 {
|
||||||
// add a safeguard in the case of inconsistency. If user hit this then there is a bug in the calculating of changes struct
|
// add a safeguard in the case of inconsistency. If user hit this then there is a bug in the calculating of changes struct
|
||||||
@ -86,54 +124,68 @@ func (r *RuleService) AuthorizeRuleChanges(ctx context.Context, user identity.Re
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(change.Delete) > 0 {
|
if len(change.Delete) > 0 {
|
||||||
allowed := r.HasAccess(ctx, user, accesscontrol.EvalPermission(ruleDelete, namespaceScope))
|
if err := r.HasAccessOrError(ctx, user, accesscontrol.EvalPermission(ruleDelete, namespaceScope), func() string {
|
||||||
if !allowed {
|
return fmt.Sprintf("delete alert rules that belong to folder %s", change.GroupKey.NamespaceUID)
|
||||||
return fmt.Errorf("%w to delete alert rules that belong to folder %s", ErrAuthorization, change.GroupKey.NamespaceUID)
|
}); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
for _, rule := range change.Delete {
|
for _, rule := range change.Delete {
|
||||||
if !r.AuthorizeDatasourceAccessForRule(ctx, user, rule) {
|
if err := r.HasAccessOrError(ctx, user, r.getRulesQueryEvaluator(rule), func() string {
|
||||||
return fmt.Errorf("%w to delete an alert rule '%s' because the user does not have read permissions for one or many datasources the rule uses", ErrAuthorization, rule.UID)
|
return fmt.Sprintf("delete an alert rule '%s'", rule.UID)
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var addAuthorized, updateAuthorized bool
|
var addAuthorized, updateAuthorized bool // these are needed to check authorization for the rule create\update only once
|
||||||
|
|
||||||
if len(change.New) > 0 {
|
if len(change.New) > 0 {
|
||||||
addAuthorized = r.HasAccess(ctx, user, accesscontrol.EvalPermission(ruleCreate, namespaceScope))
|
if err := r.HasAccessOrError(ctx, user, accesscontrol.EvalPermission(ruleCreate, namespaceScope), func() string {
|
||||||
if !addAuthorized {
|
return fmt.Sprintf("create alert rules in the folder %s", change.GroupKey.NamespaceUID)
|
||||||
return fmt.Errorf("%w to create alert rules in the folder %s", ErrAuthorization, change.GroupKey.NamespaceUID)
|
}); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
addAuthorized = true
|
||||||
for _, rule := range change.New {
|
for _, rule := range change.New {
|
||||||
if !r.AuthorizeDatasourceAccessForRule(ctx, user, rule) {
|
if err := r.HasAccessOrError(ctx, user, r.getRulesQueryEvaluator(rule), func() string {
|
||||||
return fmt.Errorf("%w to create a new alert rule '%s' because the user does not have read permissions for one or many datasources the rule uses", ErrAuthorization, rule.Title)
|
return fmt.Sprintf("create a new alert rule '%s'", rule.Title)
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, rule := range change.Update {
|
for _, rule := range change.Update {
|
||||||
if !r.AuthorizeDatasourceAccessForRule(ctx, user, rule.New) {
|
if err := r.HasAccessOrError(ctx, user, r.getRulesQueryEvaluator(rule.New), func() string {
|
||||||
return fmt.Errorf("%w to update alert rule '%s' (UID: %s) because the user does not have read permissions for one or many datasources the rule uses", ErrAuthorization, rule.Existing.Title, rule.Existing.UID)
|
return fmt.Sprintf("update alert rule '%s' (UID: %s)", rule.Existing.Title, rule.Existing.UID)
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the rule is moved from one folder to the current. If yes, then the user must have the authorization to delete rules from the source folder and add rules to the target folder.
|
// Check if the rule is moved from one folder to the current. If yes, then the user must have the authorization to delete rules from the source folder and add rules to the target folder.
|
||||||
if rule.Existing.NamespaceUID != rule.New.NamespaceUID {
|
if rule.Existing.NamespaceUID != rule.New.NamespaceUID {
|
||||||
allowed := r.HasAccess(ctx, user, accesscontrol.EvalPermission(ruleDelete, dashboards.ScopeFoldersProvider.GetResourceScopeUID(rule.Existing.NamespaceUID)))
|
ev := accesscontrol.EvalPermission(ruleDelete, dashboards.ScopeFoldersProvider.GetResourceScopeUID(rule.Existing.NamespaceUID))
|
||||||
if !allowed {
|
if err := r.HasAccessOrError(ctx, user, ev, func() string {
|
||||||
return fmt.Errorf("%w to delete alert rules from folder UID %s", ErrAuthorization, rule.Existing.NamespaceUID)
|
return fmt.Sprintf("move alert rules from folder %s", rule.Existing.NamespaceUID)
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !addAuthorized {
|
if !addAuthorized {
|
||||||
addAuthorized = r.HasAccess(ctx, user, accesscontrol.EvalPermission(ruleCreate, namespaceScope))
|
if err := r.HasAccessOrError(ctx, user, accesscontrol.EvalPermission(ruleCreate, namespaceScope), func() string {
|
||||||
if !addAuthorized {
|
return fmt.Sprintf("move alert rules to folder '%s'", change.GroupKey.NamespaceUID)
|
||||||
return fmt.Errorf("%w to create alert rules in the folder '%s'", ErrAuthorization, change.GroupKey.NamespaceUID)
|
}); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
addAuthorized = true
|
||||||
}
|
}
|
||||||
} else if !updateAuthorized { // if it is false then the authorization was not checked. If it is true then the user is authorized to update rules
|
} else if !updateAuthorized { // if it is false then the authorization was not checked. If it is true then the user is authorized to update rules
|
||||||
updateAuthorized = r.HasAccess(ctx, user, accesscontrol.EvalPermission(ruleUpdate, namespaceScope))
|
if err := r.HasAccessOrError(ctx, user, accesscontrol.EvalPermission(ruleUpdate, namespaceScope), func() string {
|
||||||
if !updateAuthorized {
|
return fmt.Sprintf("update alert rules that belongs to folder '%s'", change.GroupKey.NamespaceUID)
|
||||||
return fmt.Errorf("%w to update alert rules that belong to folder %s", ErrAuthorization, change.GroupKey.NamespaceUID)
|
}); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
updateAuthorized = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if rule.Existing.NamespaceUID != rule.New.NamespaceUID || rule.Existing.RuleGroup != rule.New.RuleGroup {
|
if rule.Existing.NamespaceUID != rule.New.NamespaceUID || rule.Existing.RuleGroup != rule.New.RuleGroup {
|
||||||
@ -143,8 +195,10 @@ func (r *RuleService) AuthorizeRuleChanges(ctx context.Context, user identity.Re
|
|||||||
// add a safeguard in the case of inconsistency. If user hit this then there is a bug in the calculating of changes struct
|
// add a safeguard in the case of inconsistency. If user hit this then there is a bug in the calculating of changes struct
|
||||||
return fmt.Errorf("failed to authorize moving an alert rule %s between groups because unable to check access to group %s from which the rule is moved", rule.Existing.UID, rule.Existing.RuleGroup)
|
return fmt.Errorf("failed to authorize moving an alert rule %s between groups because unable to check access to group %s from which the rule is moved", rule.Existing.UID, rule.Existing.RuleGroup)
|
||||||
}
|
}
|
||||||
if !r.AuthorizeAccessToRuleGroup(ctx, user, rules) {
|
if err := r.HasAccessOrError(ctx, user, r.getRulesQueryEvaluator(rules...), func() string {
|
||||||
return fmt.Errorf("%w to move rule %s between two different groups because user does not have access to the source group %s", ErrAuthorization, rule.Existing.UID, rule.Existing.RuleGroup)
|
return fmt.Sprintf("move rule %s between two different groups because user does not have access to the source group %s", rule.Existing.UID, rule.Existing.RuleGroup)
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,8 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/expr"
|
"github.com/grafana/grafana/pkg/expr"
|
||||||
@ -328,8 +330,8 @@ func TestAuthorizeRuleChanges(t *testing.T) {
|
|||||||
ac: ac,
|
ac: ac,
|
||||||
}
|
}
|
||||||
err := srv.AuthorizeRuleChanges(context.Background(), createUserWithPermissions(missing), groupChanges)
|
err := srv.AuthorizeRuleChanges(context.Background(), createUserWithPermissions(missing), groupChanges)
|
||||||
require.Errorf(t, err, "expected error because less permissions than expected were provided. Provided: %v; Expected: %v", missing, permissions)
|
|
||||||
require.ErrorIs(t, err, ErrAuthorization)
|
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")
|
require.NotEmptyf(t, ac.EvaluateRecordings, "Access control was supposed to be called but it was not")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -361,8 +363,7 @@ func TestCheckDatasourcePermissionsForRule(t *testing.T) {
|
|||||||
|
|
||||||
var data []models.AlertQuery
|
var data []models.AlertQuery
|
||||||
var scopes []string
|
var scopes []string
|
||||||
expectedExecutions := rand.Intn(3) + 2
|
for i := 0; i < rand.Intn(3)+2; i++ {
|
||||||
for i := 0; i < expectedExecutions; i++ {
|
|
||||||
q := models.GenerateAlertQuery()
|
q := models.GenerateAlertQuery()
|
||||||
scopes = append(scopes, datasources.ScopeProvider.GetResourceScopeUID(q.DatasourceUID))
|
scopes = append(scopes, datasources.ScopeProvider.GetResourceScopeUID(q.DatasourceUID))
|
||||||
data = append(data, q)
|
data = append(data, q)
|
||||||
@ -387,8 +388,8 @@ func TestCheckDatasourcePermissionsForRule(t *testing.T) {
|
|||||||
|
|
||||||
eval := svc.AuthorizeDatasourceAccessForRule(context.Background(), createUserWithPermissions(permissions), rule)
|
eval := svc.AuthorizeDatasourceAccessForRule(context.Background(), createUserWithPermissions(permissions), rule)
|
||||||
|
|
||||||
require.True(t, eval)
|
require.NoError(t, eval)
|
||||||
require.Len(t, ac.EvaluateRecordings, expectedExecutions)
|
require.Len(t, ac.EvaluateRecordings, 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("should return on first negative evaluation", func(t *testing.T) {
|
t.Run("should return on first negative evaluation", func(t *testing.T) {
|
||||||
@ -401,9 +402,9 @@ func TestCheckDatasourcePermissionsForRule(t *testing.T) {
|
|||||||
ac: ac,
|
ac: ac,
|
||||||
}
|
}
|
||||||
|
|
||||||
eval := svc.AuthorizeDatasourceAccessForRule(context.Background(), createUserWithPermissions(nil), rule)
|
result := svc.AuthorizeDatasourceAccessForRule(context.Background(), createUserWithPermissions(nil), rule)
|
||||||
|
|
||||||
require.False(t, eval)
|
require.Error(t, result)
|
||||||
require.Len(t, ac.EvaluateRecordings, 1)
|
require.Len(t, ac.EvaluateRecordings, 1)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -427,7 +428,7 @@ func Test_authorizeAccessToRuleGroup(t *testing.T) {
|
|||||||
|
|
||||||
result := svc.AuthorizeAccessToRuleGroup(context.Background(), createUserWithPermissions(permissions), rules)
|
result := svc.AuthorizeAccessToRuleGroup(context.Background(), createUserWithPermissions(permissions), rules)
|
||||||
|
|
||||||
require.True(t, result)
|
require.NoError(t, result)
|
||||||
require.NotEmpty(t, ac.EvaluateRecordings)
|
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) {
|
t.Run("should return false if user does not have access to at least one rule in group", func(t *testing.T) {
|
||||||
@ -453,6 +454,6 @@ func Test_authorizeAccessToRuleGroup(t *testing.T) {
|
|||||||
|
|
||||||
result := svc.AuthorizeAccessToRuleGroup(context.Background(), createUserWithPermissions(permissions), rules)
|
result := svc.AuthorizeAccessToRuleGroup(context.Background(), createUserWithPermissions(permissions), rules)
|
||||||
|
|
||||||
require.False(t, result)
|
require.Error(t, result)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -40,8 +40,10 @@ type AlertingStore interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RuleAccessControlService interface {
|
type RuleAccessControlService interface {
|
||||||
AuthorizeAccessToRuleGroup(ctx context.Context, user identity.Requester, rules models.RulesGroup) bool
|
HasAccessToRuleGroup(ctx context.Context, user identity.Requester, rules models.RulesGroup) (bool, error)
|
||||||
|
AuthorizeAccessToRuleGroup(ctx context.Context, user identity.Requester, rules models.RulesGroup) error
|
||||||
AuthorizeRuleChanges(ctx context.Context, user identity.Requester, change *store.GroupDelta) error
|
AuthorizeRuleChanges(ctx context.Context, user identity.Requester, change *store.GroupDelta) error
|
||||||
|
AuthorizeDatasourceAccessForRule(ctx context.Context, user identity.Requester, rule *models.AlertRule) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// API handlers.
|
// API handlers.
|
||||||
|
@ -235,7 +235,11 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon
|
|||||||
srv.log.Warn("Query returned rules that belong to folder the user does not have access to. All rules that belong to that namespace will not be added to the response", "folder_uid", groupKey.NamespaceUID)
|
srv.log.Warn("Query returned rules that belong to folder the user does not have access to. All rules that belong to that namespace will not be added to the response", "folder_uid", groupKey.NamespaceUID)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !srv.authz.AuthorizeAccessToRuleGroup(c.Req.Context(), c.SignedInUser, rules) {
|
ok, err := srv.authz.HasAccessToRuleGroup(c.Req.Context(), c.SignedInUser, rules)
|
||||||
|
if err != nil {
|
||||||
|
return response.ErrOrFallback(http.StatusInternalServerError, "cannot authorize access to rule group", err)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ruleGroup, totals := srv.toRuleGroup(groupKey, folder, rules, limitAlertsPerRule, withStatesFast, matchers, labelOptions)
|
ruleGroup, totals := srv.toRuleGroup(groupKey, folder, rules, limitAlertsPerRule, withStatesFast, matchers, labelOptions)
|
||||||
|
@ -25,6 +25,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/quota"
|
"github.com/grafana/grafana/pkg/services/quota"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
|
"github.com/grafana/grafana/pkg/util/errutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConditionValidator interface {
|
type ConditionValidator interface {
|
||||||
@ -96,7 +97,7 @@ func (srv RulerSrv) RouteDeleteAlertRules(c *contextmodel.ReqContext, namespaceT
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if totalGroups > 0 && len(deletionCandidates) == 0 {
|
if totalGroups > 0 && len(deletionCandidates) == 0 {
|
||||||
return fmt.Errorf("%w to delete any existing rules in the namespace", accesscontrol.ErrAuthorization)
|
return accesscontrol.NewAuthorizationErrorGeneric("delete any existing rules in the namespace")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rulesToDelete := make([]string, 0)
|
rulesToDelete := make([]string, 0)
|
||||||
@ -131,8 +132,8 @@ func (srv RulerSrv) RouteDeleteAlertRules(c *contextmodel.ReqContext, namespaceT
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, accesscontrol.ErrAuthorization) {
|
if errors.As(err, &errutil.Error{}) {
|
||||||
return ErrResp(http.StatusUnauthorized, err, "failed to delete rule group")
|
return response.Err(err)
|
||||||
}
|
}
|
||||||
if errors.Is(err, errProvisionedResource) {
|
if errors.Is(err, errProvisionedResource) {
|
||||||
return ErrResp(http.StatusBadRequest, err, "failed to delete rule group")
|
return ErrResp(http.StatusBadRequest, err, "failed to delete rule group")
|
||||||
@ -365,14 +366,14 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *contextmodel.ReqContext, groupKey
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, ngmodels.ErrAlertRuleNotFound) {
|
if errors.As(err, &errutil.Error{}) {
|
||||||
|
return response.Err(err)
|
||||||
|
} else if errors.Is(err, ngmodels.ErrAlertRuleNotFound) {
|
||||||
return ErrResp(http.StatusNotFound, err, "failed to update rule group")
|
return ErrResp(http.StatusNotFound, err, "failed to update rule group")
|
||||||
} else if errors.Is(err, ngmodels.ErrAlertRuleFailedValidation) || errors.Is(err, errProvisionedResource) {
|
} else if errors.Is(err, ngmodels.ErrAlertRuleFailedValidation) || errors.Is(err, errProvisionedResource) {
|
||||||
return ErrResp(http.StatusBadRequest, err, "failed to update rule group")
|
return ErrResp(http.StatusBadRequest, err, "failed to update rule group")
|
||||||
} else if errors.Is(err, ngmodels.ErrQuotaReached) {
|
} else if errors.Is(err, ngmodels.ErrQuotaReached) {
|
||||||
return ErrResp(http.StatusForbidden, err, "")
|
return ErrResp(http.StatusForbidden, err, "")
|
||||||
} else if errors.Is(err, accesscontrol.ErrAuthorization) {
|
|
||||||
return ErrResp(http.StatusUnauthorized, err, "")
|
|
||||||
} else if errors.Is(err, store.ErrOptimisticLock) {
|
} else if errors.Is(err, store.ErrOptimisticLock) {
|
||||||
return ErrResp(http.StatusConflict, err, "")
|
return ErrResp(http.StatusConflict, err, "")
|
||||||
}
|
}
|
||||||
@ -521,8 +522,8 @@ func (srv RulerSrv) getAuthorizedRuleByUid(ctx context.Context, c *contextmodel.
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return ngmodels.AlertRule{}, err
|
return ngmodels.AlertRule{}, err
|
||||||
}
|
}
|
||||||
if !srv.authz.AuthorizeAccessToRuleGroup(ctx, c.SignedInUser, rules) {
|
if err := srv.authz.AuthorizeAccessToRuleGroup(ctx, c.SignedInUser, rules); err != nil {
|
||||||
return ngmodels.AlertRule{}, fmt.Errorf("%w to access rules in this group", accesscontrol.ErrAuthorization)
|
return ngmodels.AlertRule{}, err
|
||||||
}
|
}
|
||||||
for _, rule := range rules {
|
for _, rule := range rules {
|
||||||
if rule.UID == ruleUID {
|
if rule.UID == ruleUID {
|
||||||
@ -545,8 +546,8 @@ func (srv RulerSrv) getAuthorizedRuleGroup(ctx context.Context, c *contextmodel.
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !srv.authz.AuthorizeAccessToRuleGroup(ctx, c.SignedInUser, rules) {
|
if err := srv.authz.AuthorizeAccessToRuleGroup(ctx, c.SignedInUser, rules); err != nil {
|
||||||
return nil, fmt.Errorf("%w to access rules in this group", accesscontrol.ErrAuthorization)
|
return nil, err
|
||||||
}
|
}
|
||||||
return rules, nil
|
return rules, nil
|
||||||
}
|
}
|
||||||
@ -569,7 +570,10 @@ func (srv RulerSrv) searchAuthorizedAlertRules(ctx context.Context, c *contextmo
|
|||||||
byGroupKey := ngmodels.GroupByAlertRuleGroupKey(rules)
|
byGroupKey := ngmodels.GroupByAlertRuleGroupKey(rules)
|
||||||
totalGroups := len(byGroupKey)
|
totalGroups := len(byGroupKey)
|
||||||
for groupKey, rulesGroup := range byGroupKey {
|
for groupKey, rulesGroup := range byGroupKey {
|
||||||
if !srv.authz.AuthorizeAccessToRuleGroup(ctx, c.SignedInUser, rulesGroup) {
|
if ok, err := srv.authz.HasAccessToRuleGroup(ctx, c.SignedInUser, rulesGroup); !ok || err != nil {
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
delete(byGroupKey, groupKey)
|
delete(byGroupKey, groupKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -148,7 +148,7 @@ func (srv RulerSrv) getRulesWithFolderTitleInFolders(c *contextmodel.ReqContext,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(query.NamespaceUIDs) == 0 {
|
if len(query.NamespaceUIDs) == 0 {
|
||||||
return nil, fmt.Errorf("%w access rules in the specified folders", accesscontrol.ErrAuthorization)
|
return nil, accesscontrol.NewAuthorizationErrorGeneric("access rules in the specified folders")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for _, folder := range folders {
|
for _, folder := range folders {
|
||||||
|
@ -2,7 +2,6 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -21,7 +20,6 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/folder"
|
"github.com/grafana/grafana/pkg/services/folder"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
|
|
||||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/backtesting"
|
"github.com/grafana/grafana/pkg/services/ngalert/backtesting"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/eval"
|
"github.com/grafana/grafana/pkg/services/ngalert/eval"
|
||||||
@ -65,8 +63,8 @@ func (srv TestingApiSrv) RouteTestGrafanaRuleConfig(c *contextmodel.ReqContext,
|
|||||||
return ErrResp(http.StatusBadRequest, err, "")
|
return ErrResp(http.StatusBadRequest, err, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !srv.authz.AuthorizeAccessToRuleGroup(c.Req.Context(), c.SignedInUser, ngmodels.RulesGroup{rule}) {
|
if err := srv.authz.AuthorizeAccessToRuleGroup(c.Req.Context(), c.SignedInUser, ngmodels.RulesGroup{rule}); err != nil {
|
||||||
return errorToResponse(fmt.Errorf("%w to query one or many data sources used by the rule", accesscontrol.ErrAuthorization))
|
return response.ErrOrFallback(http.StatusInternalServerError, "failed to authorize access to rule group", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := store.OptimizeAlertQueries(rule.Data); err != nil {
|
if _, err := store.OptimizeAlertQueries(rule.Data); err != nil {
|
||||||
@ -153,8 +151,8 @@ func (srv TestingApiSrv) RouteTestRuleConfig(c *contextmodel.ReqContext, body ap
|
|||||||
|
|
||||||
func (srv TestingApiSrv) RouteEvalQueries(c *contextmodel.ReqContext, cmd apimodels.EvalQueriesPayload) response.Response {
|
func (srv TestingApiSrv) RouteEvalQueries(c *contextmodel.ReqContext, cmd apimodels.EvalQueriesPayload) response.Response {
|
||||||
queries := AlertQueriesFromApiAlertQueries(cmd.Data)
|
queries := AlertQueriesFromApiAlertQueries(cmd.Data)
|
||||||
if !srv.authz.AuthorizeAccessToRuleGroup(c.Req.Context(), c.SignedInUser, ngmodels.RulesGroup{&ngmodels.AlertRule{Data: queries}}) {
|
if err := srv.authz.AuthorizeDatasourceAccessForRule(c.Req.Context(), c.SignedInUser, &ngmodels.AlertRule{Data: queries}); err != nil {
|
||||||
return ErrResp(http.StatusUnauthorized, fmt.Errorf("%w to query one or many data sources used by the rule", accesscontrol.ErrAuthorization), "")
|
return response.ErrOrFallback(http.StatusInternalServerError, "failed to authorize access to data sources", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cond := ngmodels.Condition{
|
cond := ngmodels.Condition{
|
||||||
@ -215,8 +213,8 @@ func (srv TestingApiSrv) BacktestAlertRule(c *contextmodel.ReqContext, cmd apimo
|
|||||||
}
|
}
|
||||||
|
|
||||||
queries := AlertQueriesFromApiAlertQueries(cmd.Data)
|
queries := AlertQueriesFromApiAlertQueries(cmd.Data)
|
||||||
if !srv.authz.AuthorizeAccessToRuleGroup(c.Req.Context(), c.SignedInUser, ngmodels.RulesGroup{&ngmodels.AlertRule{Data: queries}}) {
|
if err := srv.authz.AuthorizeAccessToRuleGroup(c.Req.Context(), c.SignedInUser, ngmodels.RulesGroup{&ngmodels.AlertRule{Data: queries}}); err != nil {
|
||||||
return errorToResponse(fmt.Errorf("%w to query one or many data sources used by the rule", accesscontrol.ErrAuthorization))
|
return errorToResponse(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
rule := &ngmodels.AlertRule{
|
rule := &ngmodels.AlertRule{
|
||||||
|
@ -6,8 +6,8 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/response"
|
"github.com/grafana/grafana/pkg/api/response"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
|
|
||||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||||
|
"github.com/grafana/grafana/pkg/util/errutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -29,15 +29,15 @@ func backendTypeDoesNotMatchPayloadTypeError(backendType apimodels.Backend, payl
|
|||||||
}
|
}
|
||||||
|
|
||||||
func errorToResponse(err error) response.Response {
|
func errorToResponse(err error) response.Response {
|
||||||
|
if errors.As(err, &errutil.Error{}) {
|
||||||
|
return response.Err(err)
|
||||||
|
}
|
||||||
if errors.Is(err, datasources.ErrDataSourceNotFound) {
|
if errors.Is(err, datasources.ErrDataSourceNotFound) {
|
||||||
return ErrResp(404, err, "")
|
return ErrResp(404, err, "")
|
||||||
}
|
}
|
||||||
if errors.Is(err, errUnexpectedDatasourceType) {
|
if errors.Is(err, errUnexpectedDatasourceType) {
|
||||||
return ErrResp(400, err, "")
|
return ErrResp(400, err, "")
|
||||||
}
|
}
|
||||||
if errors.Is(err, accesscontrol.ErrAuthorization) {
|
|
||||||
return ErrResp(401, err, "")
|
|
||||||
}
|
|
||||||
if errors.Is(err, errFolderAccess) {
|
if errors.Is(err, errFolderAccess) {
|
||||||
return toNamespaceErrorResponse(err)
|
return toNamespaceErrorResponse(err)
|
||||||
}
|
}
|
||||||
|
@ -145,10 +145,18 @@ var _ accesscontrol.AccessControl = &recordingAccessControlFake{}
|
|||||||
type fakeRuleAccessControlService struct {
|
type fakeRuleAccessControlService struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f fakeRuleAccessControlService) AuthorizeAccessToRuleGroup(ctx context.Context, user identity.Requester, rules models.RulesGroup) bool {
|
func (f fakeRuleAccessControlService) HasAccessToRuleGroup(ctx context.Context, user identity.Requester, rules models.RulesGroup) (bool, error) {
|
||||||
return true
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeRuleAccessControlService) AuthorizeAccessToRuleGroup(ctx context.Context, user identity.Requester, rules models.RulesGroup) error {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f fakeRuleAccessControlService) AuthorizeRuleChanges(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
|
func (f fakeRuleAccessControlService) AuthorizeRuleChanges(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f fakeRuleAccessControlService) AuthorizeDatasourceAccessForRule(ctx context.Context, user identity.Requester, rule *models.AlertRule) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -1051,7 +1051,7 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) {
|
|||||||
}(),
|
}(),
|
||||||
expectedMessage: func() string {
|
expectedMessage: func() string {
|
||||||
if setting.IsEnterprise {
|
if setting.IsEnterprise {
|
||||||
return "user is not authorized to create a new alert rule 'AlwaysFiring' because the user does not have read permissions for one or many datasources the rule uses"
|
return "user is not authorized to create a new alert rule 'AlwaysFiring'"
|
||||||
}
|
}
|
||||||
return "failed to update rule group: invalid alert rule 'AlwaysFiring': failed to build query 'A': data source not found"
|
return "failed to update rule group: invalid alert rule 'AlwaysFiring': failed to build query 'A': data source not found"
|
||||||
}(),
|
}(),
|
||||||
@ -2284,7 +2284,7 @@ func TestIntegrationEval(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedMessage: func() string {
|
expectedMessage: func() string {
|
||||||
if setting.IsEnterprise {
|
if setting.IsEnterprise {
|
||||||
return "user is not authorized to query one or many data sources used by the rule"
|
return "user is not authorized to access one or many data sources"
|
||||||
}
|
}
|
||||||
return "Failed to build evaluator for queries and expressions: failed to build query 'A': data source not found"
|
return "Failed to build evaluator for queries and expressions: failed to build query 'A': data source not found"
|
||||||
},
|
},
|
||||||
|
@ -18,7 +18,6 @@ import (
|
|||||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
|
||||||
"github.com/grafana/grafana/pkg/tests/testinfra"
|
"github.com/grafana/grafana/pkg/tests/testinfra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -92,10 +91,6 @@ func TestBacktesting(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("if user does not have permissions", func(t *testing.T) {
|
t.Run("if user does not have permissions", func(t *testing.T) {
|
||||||
if !setting.IsEnterprise {
|
|
||||||
t.Skip("Enterprise-only test")
|
|
||||||
}
|
|
||||||
|
|
||||||
testUserId := createUser(t, env.SQLStore, user.CreateUserCommand{
|
testUserId := createUser(t, env.SQLStore, user.CreateUserCommand{
|
||||||
DefaultOrgRole: "",
|
DefaultOrgRole: "",
|
||||||
Password: "test",
|
Password: "test",
|
||||||
@ -128,7 +123,7 @@ func TestBacktesting(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("fail if can't query data sources", func(t *testing.T) {
|
t.Run("fail if can't query data sources", func(t *testing.T) {
|
||||||
status, body := testUserApiCli.SubmitRuleForBacktesting(t, queryRequest)
|
status, body := testUserApiCli.SubmitRuleForBacktesting(t, queryRequest)
|
||||||
require.Contains(t, body, "user is not authorized to query one or many data sources used by the rule")
|
require.Contains(t, body, "user is not authorized to access rule group")
|
||||||
require.Equalf(t, http.StatusUnauthorized, status, "Response: %s", body)
|
require.Equalf(t, http.StatusUnauthorized, status, "Response: %s", body)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user