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 -43
View File
@@ -238,7 +238,7 @@ func (srv RulerSrv) RouteGetRulesGroupConfig(c *models.ReqContext) response.Resp
groupRules := make([]*ngmodels.AlertRule, 0, len(q.Result))
for _, r := range q.Result {
if !authorizeDatasourceAccessForRule(r, hasAccess) {
continue
return ErrResp(http.StatusUnauthorized, fmt.Errorf("%w to access the group because it does not have access to one or many data sources one or many rules in the group use", ErrAuthorization), "")
}
groupRules = append(groupRules, r)
}
@@ -297,9 +297,6 @@ func (srv RulerSrv) RouteGetRulesConfig(c *models.ReqContext) response.Response
configs := make(map[ngmodels.AlertRuleGroupKey][]*ngmodels.AlertRule)
for _, r := range q.Result {
if !authorizeDatasourceAccessForRule(r, hasAccess) {
continue
}
groupKey := r.GetGroupKey()
group := configs[groupKey]
group = append(group, r)
@@ -312,6 +309,9 @@ func (srv RulerSrv) RouteGetRulesConfig(c *models.ReqContext) response.Response
srv.log.Error("namespace not visible to the user", "user", c.SignedInUser.UserId, "namespace", groupKey.NamespaceUID)
continue
}
if !authorizeAccessToRuleGroup(rules, hasAccess) {
continue
}
namespace := folder.Title
result[namespace] = append(result[namespace], toGettableRuleGroupConfig(groupKey.RuleGroup, rules, folder.Id, provenanceRecords))
}
@@ -358,21 +358,14 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, groupKey ngmod
return nil
}
authorizedChanges := groupChanges // if RBAC is disabled the permission are limited to folder access that is done upstream
// if RBAC is disabled the permission are limited to folder access that is done upstream
if !srv.ac.IsDisabled() {
authorizedChanges, err = authorizeRuleChanges(groupChanges, func(evaluator accesscontrol.Evaluator) bool {
err = authorizeRuleChanges(groupChanges, func(evaluator accesscontrol.Evaluator) bool {
return hasAccess(accesscontrol.ReqOrgAdminOrEditor, evaluator)
})
if err != nil {
return err
}
if authorizedChanges.isEmpty() {
logger.Info("no authorized changes detected in the request. Do nothing", "not_authorized_add", len(groupChanges.New), "not_authorized_update", len(groupChanges.Update), "not_authorized_delete", len(groupChanges.Delete))
return nil
}
if len(groupChanges.Delete) > len(authorizedChanges.Delete) {
logger.Info("user is not authorized to delete one or many rules in the group. those rules will be skipped", "expected", len(groupChanges.Delete), "authorized", len(authorizedChanges.Delete))
}
}
provenances, err := srv.provenanceStore.GetProvenances(c.Req.Context(), c.OrgId, (&ngmodels.AlertRule{}).ResourceType())
@@ -382,13 +375,13 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, groupKey ngmod
// New rules don't need to be checked for provenance, just copy the whole slice.
finalChanges = &changes{}
finalChanges.New = authorizedChanges.New
for _, rule := range authorizedChanges.Update {
finalChanges.New = groupChanges.New
for _, rule := range groupChanges.Update {
if provenance, exists := provenances[rule.Existing.UID]; (exists && provenance == ngmodels.ProvenanceNone) || !exists {
finalChanges.Update = append(finalChanges.Update, rule)
}
}
for _, rule := range authorizedChanges.Delete {
for _, rule := range groupChanges.Delete {
if provenance, exists := provenances[rule.UID]; (exists && provenance == ngmodels.ProvenanceNone) || !exists {
finalChanges.Delete = append(finalChanges.Delete, rule)
}
@@ -396,22 +389,22 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, groupKey ngmod
if finalChanges.isEmpty() {
logger.Info("no changes detected that have 'none' provenance in the request. Do nothing",
"provenance_invalid_add", len(authorizedChanges.New),
"provenance_invalid_update", len(authorizedChanges.Update),
"provenance_invalid_delete", len(authorizedChanges.Delete))
"provenance_invalid_add", len(groupChanges.New),
"provenance_invalid_update", len(groupChanges.Update),
"provenance_invalid_delete", len(groupChanges.Delete))
return nil
}
if len(authorizedChanges.Delete) > len(finalChanges.Delete) {
if len(groupChanges.Delete) > len(finalChanges.Delete) {
logger.Info("provenance is not 'none' for one or many rules in the group that should be deleted. those rules will be skipped",
"expected", len(authorizedChanges.Delete),
"allowed", len(authorizedChanges.Delete))
"expected", len(groupChanges.Delete),
"allowed", len(groupChanges.Delete))
}
if len(authorizedChanges.Update) > len(finalChanges.Update) {
if len(groupChanges.Update) > len(finalChanges.Update) {
logger.Info("provenance is not 'none' for one or many rules in the group that should be updated. those rules will be skipped",
"expected", len(authorizedChanges.Update),
"allowed", len(authorizedChanges.Update))
"expected", len(groupChanges.Update),
"allowed", len(groupChanges.Update))
}
logger.Debug("updating database with the authorized changes", "add", len(finalChanges.New), "update", len(finalChanges.New), "delete", len(finalChanges.Delete))
@@ -564,10 +557,11 @@ type ruleUpdate struct {
}
type changes struct {
GroupKey ngmodels.AlertRuleGroupKey
New []*ngmodels.AlertRule
Update []ruleUpdate
Delete []*ngmodels.AlertRule
GroupKey ngmodels.AlertRuleGroupKey
AffectedGroups map[ngmodels.AlertRuleGroupKey][]*ngmodels.AlertRule
New []*ngmodels.AlertRule
Update []ruleUpdate
Delete []*ngmodels.AlertRule
}
func (c *changes) isEmpty() bool {
@@ -577,6 +571,7 @@ func (c *changes) isEmpty() bool {
// calculateChanges calculates the difference between rules in the group in the database and the submitted rules. If a submitted rule has UID it tries to find it in the database (in other groups).
// returns a list of rules that need to be added, updated and deleted. Deleted considered rules in the database that belong to the group but do not exist in the list of submitted rules.
func calculateChanges(ctx context.Context, ruleStore store.RuleStore, groupKey ngmodels.AlertRuleGroupKey, submittedRules []*ngmodels.AlertRule) (*changes, error) {
affectedGroups := make(map[ngmodels.AlertRuleGroupKey][]*ngmodels.AlertRule)
q := &ngmodels.ListAlertRulesQuery{
OrgID: groupKey.OrgID,
NamespaceUIDs: []string{groupKey.NamespaceUID},
@@ -586,6 +581,9 @@ func calculateChanges(ctx context.Context, ruleStore store.RuleStore, groupKey n
return nil, fmt.Errorf("failed to query database for rules in the group %s: %w", groupKey, err)
}
existingGroupRules := q.Result
if len(existingGroupRules) > 0 {
affectedGroups[groupKey] = existingGroupRules
}
existingGroupRulesUIDs := make(map[string]*ngmodels.AlertRule, len(existingGroupRules))
for _, r := range existingGroupRules {
@@ -594,25 +592,30 @@ func calculateChanges(ctx context.Context, ruleStore store.RuleStore, groupKey n
var toAdd, toDelete []*ngmodels.AlertRule
var toUpdate []ruleUpdate
loadedRulesByUID := map[string]*ngmodels.AlertRule{} // auxiliary cache to avoid unnecessary queries if there are multiple moves from the same group
for _, r := range submittedRules {
var existing *ngmodels.AlertRule = nil
if r.UID != "" {
if existingGroupRule, ok := existingGroupRulesUIDs[r.UID]; ok {
existing = existingGroupRule
// remove the rule from existingGroupRulesUIDs
delete(existingGroupRulesUIDs, r.UID)
} else {
} else if existing, ok = loadedRulesByUID[r.UID]; !ok { // check the "cache" and if there is no hit, query the database
// Rule can be from other group or namespace
q := &ngmodels.GetAlertRuleByUIDQuery{OrgID: groupKey.OrgID, UID: r.UID}
if err := ruleStore.GetAlertRuleByUID(ctx, q); err != nil || q.Result == nil {
// if rule has UID then it is considered an update. Therefore, fail if there is no rule to update
if errors.Is(err, ngmodels.ErrAlertRuleNotFound) || q.Result == nil && err == nil {
return nil, fmt.Errorf("failed to update rule with UID %s because %w", r.UID, ngmodels.ErrAlertRuleNotFound)
}
return nil, fmt.Errorf("failed to query database for an alert rule with UID %s: %w", r.UID, err)
q := &ngmodels.GetAlertRulesGroupByRuleUIDQuery{OrgID: groupKey.OrgID, UID: r.UID}
if err := ruleStore.GetAlertRulesGroupByRuleUID(ctx, q); err != nil {
return nil, fmt.Errorf("failed to query database for a group of alert rules: %w", err)
}
existing = q.Result
for _, rule := range q.Result {
if rule.UID == r.UID {
existing = rule
}
loadedRulesByUID[rule.UID] = rule
}
if existing == nil {
return nil, fmt.Errorf("failed to update rule with UID %s because %w", r.UID, ngmodels.ErrAlertRuleNotFound)
}
affectedGroups[existing.GetGroupKey()] = q.Result
}
}
@@ -641,10 +644,11 @@ func calculateChanges(ctx context.Context, ruleStore store.RuleStore, groupKey n
}
return &changes{
GroupKey: groupKey,
New: toAdd,
Delete: toDelete,
Update: toUpdate,
GroupKey: groupKey,
AffectedGroups: affectedGroups,
New: toAdd,
Delete: toDelete,
Update: toUpdate,
}, nil
}