Alerting: Update RBAC for alert rules to consider access to rule as access to group it belongs (#49033)

* update authz to exclude entire group if user does not have access to rule
* change rule update authz to not return changes because if user does not have access to any rule in group, they do not have access to the rule
* a new query that returns alerts in group by UID of alert that belongs to that group
* collect all affected groups during calculate changes
* update authorize to check access to groups
* update tests for calculateChanges to assert new fields
* add authorization tests
This commit is contained in:
Yuriy Tseretyan
2022-06-01 10:23:54 -04:00
committed by GitHub
parent 333195ce21
commit ad25e2a20c
8 changed files with 443 additions and 278 deletions
+47 -29
View File
@@ -219,34 +219,43 @@ func authorizeDatasourceAccessForRule(rule *ngmodels.AlertRule, evaluator func(e
return true
}
// authorizeAccessToRuleGroup checks all rules against authorizeDatasourceAccessForRule and exits on the first negative result
func authorizeAccessToRuleGroup(rules []*ngmodels.AlertRule, evaluator func(evaluator ac.Evaluator) bool) bool {
for _, rule := range rules {
if !authorizeDatasourceAccessForRule(rule, evaluator) {
return false
}
}
return true
}
// 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.
// 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 authorizeRuleChanges(change *changes, evaluator func(evaluator ac.Evaluator) bool) (*changes, error) {
var result = &changes{
GroupKey: change.GroupKey,
New: change.New,
Update: change.Update,
Delete: change.Delete,
func authorizeRuleChanges(change *changes, evaluator func(evaluator ac.Evaluator) bool) error {
namespaceScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(change.GroupKey.NamespaceUID)
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 !authorizeAccessToRuleGroup(rules, evaluator) { // 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)
}
} 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
return fmt.Errorf("failed to authorize changes in rule group %s. Detected %d deletes but group was not provided", change.GroupKey.RuleGroup, len(change.Delete))
}
namespaceScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(change.GroupKey.NamespaceUID)
if len(change.Delete) > 0 {
var allowedToDelete []*ngmodels.AlertRule
allowed := evaluator(ac.EvalPermission(ac.ActionAlertingRuleDelete, namespaceScope))
if !allowed {
return fmt.Errorf("%w to delete alert rules that belong to folder %s", ErrAuthorization, change.GroupKey.NamespaceUID)
}
for _, rule := range change.Delete {
dsAllowed := authorizeDatasourceAccessForRule(rule, evaluator)
if dsAllowed {
allowedToDelete = append(allowedToDelete, rule)
if !authorizeDatasourceAccessForRule(rule, evaluator) {
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)
}
}
if len(allowedToDelete) > 0 {
allowed := evaluator(ac.EvalPermission(ac.ActionAlertingRuleDelete, namespaceScope))
if !allowed {
return nil, fmt.Errorf("%w to delete alert rules that belong to folder %s", ErrAuthorization, change.GroupKey.NamespaceUID)
}
}
result.Delete = allowedToDelete
}
var addAuthorized, updateAuthorized bool
@@ -254,12 +263,12 @@ func authorizeRuleChanges(change *changes, evaluator func(evaluator ac.Evaluator
if len(change.New) > 0 {
addAuthorized = evaluator(ac.EvalPermission(ac.ActionAlertingRuleCreate, namespaceScope))
if !addAuthorized {
return nil, fmt.Errorf("%w to create alert rules in the folder %s", ErrAuthorization, change.GroupKey.NamespaceUID)
return fmt.Errorf("%w to create alert rules in the folder %s", ErrAuthorization, change.GroupKey.NamespaceUID)
}
for _, rule := range change.New {
dsAllowed := authorizeDatasourceAccessForRule(rule, evaluator)
if !dsAllowed {
return nil, 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.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)
}
}
}
@@ -267,31 +276,40 @@ func authorizeRuleChanges(change *changes, evaluator func(evaluator ac.Evaluator
for _, rule := range change.Update {
dsAllowed := authorizeDatasourceAccessForRule(rule.New, evaluator)
if !dsAllowed {
return nil, 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.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)
}
// 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 {
allowed := evaluator(ac.EvalAll(ac.EvalPermission(ac.ActionAlertingRuleDelete, dashboards.ScopeFoldersProvider.GetResourceScopeUID(rule.Existing.NamespaceUID))))
if !allowed {
return nil, fmt.Errorf("%w to delete alert rules from folder UID %s", ErrAuthorization, rule.Existing.NamespaceUID)
return fmt.Errorf("%w to delete alert rules from folder UID %s", ErrAuthorization, rule.Existing.NamespaceUID)
}
if !addAuthorized {
addAuthorized = evaluator(ac.EvalPermission(ac.ActionAlertingRuleCreate, namespaceScope))
if !addAuthorized {
return nil, fmt.Errorf("%w to create alert rules in the folder '%s'", ErrAuthorization, change.GroupKey.NamespaceUID)
return fmt.Errorf("%w to create alert rules in the folder '%s'", ErrAuthorization, change.GroupKey.NamespaceUID)
}
}
continue
} 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 = evaluator(ac.EvalPermission(ac.ActionAlertingRuleUpdate, namespaceScope))
if !updateAuthorized {
return fmt.Errorf("%w to update alert rules that belong to folder %s", ErrAuthorization, change.GroupKey.NamespaceUID)
}
}
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 = evaluator(ac.EvalAll(ac.EvalPermission(ac.ActionAlertingRuleUpdate, namespaceScope)))
if !updateAuthorized {
return nil, fmt.Errorf("%w to update alert rules that belong to folder %s", ErrAuthorization, change.GroupKey.NamespaceUID)
if rule.Existing.NamespaceUID != rule.New.NamespaceUID || rule.Existing.RuleGroup != rule.New.RuleGroup {
key := rule.Existing.GetGroupKey()
rules, ok = change.AffectedGroups[key]
if !ok {
// 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)
}
if !authorizeAccessToRuleGroup(rules, evaluator) {
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 result, nil
return nil
}