From 237ce5ea8262871def09dc3ddefffd68ec1fe850 Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Tue, 26 Sep 2023 12:45:22 -0400 Subject: [PATCH] Alerting: Extract methods for fetching rule groups with authorization (#75375) * extract methods for fetching rule groups with authorization and refactor the request handlers. * add logging to delete handler --- pkg/services/ngalert/api/api_ruler.go | 206 ++++++++++------------ pkg/services/ngalert/models/alert_rule.go | 12 ++ 2 files changed, 110 insertions(+), 108 deletions(-) diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index a87976458f1..76b978c5e73 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -56,82 +56,74 @@ func (srv RulerSrv) RouteDeleteAlertRules(c *contextmodel.ReqContext, namespaceT if err != nil { return toNamespaceErrorResponse(err) } + var loggerCtx = []any{ - "namespace", - namespace.Title, + "userId", + c.SignedInUser.UserID, + "namespaceUid", + namespace.UID, } - var ruleGroup string if group != "" { - ruleGroup = group loggerCtx = append(loggerCtx, "group", group) } logger := srv.log.New(loggerCtx...) - hasAccess := func(evaluator accesscontrol.Evaluator) bool { - return accesscontrol.HasAccess(srv.ac, c)(evaluator) - } - provenances, err := srv.provenanceStore.GetProvenances(c.Req.Context(), c.SignedInUser.OrgID, (&ngmodels.AlertRule{}).ResourceType()) if err != nil { return ErrResp(http.StatusInternalServerError, err, "failed to fetch provenances of alert rules") } - deletedGroups := make(map[ngmodels.AlertRuleGroupKey][]ngmodels.AlertRuleKey) err = srv.xactManager.InTransaction(c.Req.Context(), func(ctx context.Context) error { - unauthz, provisioned := false, false - q := ngmodels.ListAlertRulesQuery{ - OrgID: c.SignedInUser.OrgID, - NamespaceUIDs: []string{namespace.UID}, - RuleGroup: ruleGroup, - } - ruleList, err := srv.store.ListAlertRules(ctx, &q) - if err != nil { - return err - } - - if len(ruleList) == 0 { - logger.Debug("No alert rules to delete from namespace/group") - return nil - } - - var deletionCandidates = make(map[ngmodels.AlertRuleGroupKey][]*ngmodels.AlertRule) - for _, rule := range ruleList { - key := rule.GetGroupKey() - deletionCandidates[key] = append(deletionCandidates[key], rule) - } - rulesToDelete := make([]string, 0, len(ruleList)) - for groupKey, rules := range deletionCandidates { - if !authorizeAccessToRuleGroup(rules, hasAccess) { - unauthz = true - continue + deletionCandidates := map[ngmodels.AlertRuleGroupKey]ngmodels.RulesGroup{} + if group != "" { + key := ngmodels.AlertRuleGroupKey{ + OrgID: c.OrgID, + NamespaceUID: namespace.UID, + RuleGroup: group, } + rules, err := srv.getAuthorizedRuleGroup(ctx, c, key) + if err != nil { + return err + } + deletionCandidates[key] = rules + } else { + var totalGroups int + deletionCandidates, totalGroups, err = srv.searchAuthorizedAlertRules(ctx, c, []string{namespace.UID}, "", 0) + if err != nil { + return err + } + if totalGroups > 0 && len(deletionCandidates) == 0 { + return fmt.Errorf("%w to delete any existing rules in the namespace", ErrAuthorization) + } + } + rulesToDelete := make([]string, 0) + provisioned := false + for groupKey, rules := range deletionCandidates { if containsProvisionedAlerts(provenances, rules) { + logger.Debug("Alert group cannot be deleted because it is provisioned", "group", groupKey.RuleGroup) provisioned = true continue } uid := make([]string, 0, len(rules)) - keys := make([]ngmodels.AlertRuleKey, 0, len(rules)) for _, rule := range rules { uid = append(uid, rule.UID) - keys = append(keys, rule.GetKey()) } rulesToDelete = append(rulesToDelete, uid...) - deletedGroups[groupKey] = keys } if len(rulesToDelete) > 0 { - return srv.store.DeleteAlertRulesByUID(ctx, c.SignedInUser.OrgID, rulesToDelete...) + err := srv.store.DeleteAlertRulesByUID(ctx, c.SignedInUser.OrgID, rulesToDelete...) + if err != nil { + return err + } + logger.Info("Alert rules were deleted", "ruleUid", strings.Join(rulesToDelete, ",")) + return nil } // if none rules were deleted return an error. // Check whether provisioned check failed first because if it is true, then all rules that the user can access (actually read via GET API) are provisioned. if provisioned { return errProvisionedResource } - if unauthz { - if group == "" { - return fmt.Errorf("%w to delete any existing rules in the namespace", ErrAuthorization) - } - return fmt.Errorf("%w to delete group of the rules", ErrAuthorization) - } + logger.Info("No alert rules were deleted") return nil }) @@ -154,36 +146,19 @@ func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *contextmodel.ReqContext, nam return toNamespaceErrorResponse(err) } - q := ngmodels.ListAlertRulesQuery{ - OrgID: c.SignedInUser.OrgID, - NamespaceUIDs: []string{namespace.UID}, - } - ruleList, err := srv.store.ListAlertRules(c.Req.Context(), &q) + ruleGroups, _, err := srv.searchAuthorizedAlertRules(c.Req.Context(), c, []string{namespace.UID}, "", 0) if err != nil { - return ErrResp(http.StatusInternalServerError, err, "failed to update rule group") + return errorToResponse(err) } - - result := apimodels.NamespaceConfigResponse{} - - hasAccess := func(evaluator accesscontrol.Evaluator) bool { - return accesscontrol.HasAccess(srv.ac, c)(evaluator) - } - provenanceRecords, err := srv.provenanceStore.GetProvenances(c.Req.Context(), c.SignedInUser.OrgID, (&ngmodels.AlertRule{}).ResourceType()) if err != nil { return ErrResp(http.StatusInternalServerError, err, "failed to get provenance for rule group") } - ruleGroups := make(map[string]ngmodels.RulesGroup) - for _, r := range ruleList { - ruleGroups[r.RuleGroup] = append(ruleGroups[r.RuleGroup], r) - } + result := apimodels.NamespaceConfigResponse{} - for groupName, rules := range ruleGroups { - if !authorizeAccessToRuleGroup(rules, hasAccess) { - continue - } - result[namespaceTitle] = append(result[namespaceTitle], toGettableRuleGroupConfig(groupName, rules, namespace.ID, provenanceRecords)) + for groupKey, rules := range ruleGroups { + result[namespaceTitle] = append(result[namespaceTitle], toGettableRuleGroupConfig(groupKey.RuleGroup, rules, namespace.ID, provenanceRecords)) } return response.JSON(http.StatusAccepted, result) @@ -197,18 +172,13 @@ func (srv RulerSrv) RouteGetRulesGroupConfig(c *contextmodel.ReqContext, namespa return toNamespaceErrorResponse(err) } - q := ngmodels.ListAlertRulesQuery{ - OrgID: c.SignedInUser.OrgID, - NamespaceUIDs: []string{namespace.UID}, - RuleGroup: ruleGroup, - } - ruleList, err := srv.store.ListAlertRules(c.Req.Context(), &q) + rules, err := srv.getAuthorizedRuleGroup(c.Req.Context(), c, ngmodels.AlertRuleGroupKey{ + OrgID: c.OrgID, + RuleGroup: ruleGroup, + NamespaceUID: namespace.UID, + }) if err != nil { - return ErrResp(http.StatusInternalServerError, err, "failed to get group alert rules") - } - - hasAccess := func(evaluator accesscontrol.Evaluator) bool { - return accesscontrol.HasAccess(srv.ac, c)(evaluator) + return errorToResponse(err) } provenanceRecords, err := srv.provenanceStore.GetProvenances(c.Req.Context(), c.SignedInUser.OrgID, (&ngmodels.AlertRule{}).ResourceType()) @@ -216,12 +186,8 @@ func (srv RulerSrv) RouteGetRulesGroupConfig(c *contextmodel.ReqContext, namespa return ErrResp(http.StatusInternalServerError, err, "failed to get group alert rules") } - if !authorizeAccessToRuleGroup(ruleList, hasAccess) { - 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), "") - } - result := apimodels.RuleGroupConfigResponse{ - GettableRuleGroupConfig: toGettableRuleGroupConfig(ruleGroup, ruleList, namespace.ID, provenanceRecords), + GettableRuleGroupConfig: toGettableRuleGroupConfig(ruleGroup, rules, namespace.ID, provenanceRecords), } return response.JSON(http.StatusAccepted, result) } @@ -253,44 +219,21 @@ func (srv RulerSrv) RouteGetRulesConfig(c *contextmodel.ReqContext) response.Res return ErrResp(http.StatusBadRequest, errors.New("panel_id must be set with dashboard_uid"), "") } - q := ngmodels.ListAlertRulesQuery{ - OrgID: c.SignedInUser.OrgID, - NamespaceUIDs: namespaceUIDs, - DashboardUID: dashboardUID, - PanelID: panelID, - } - - ruleList, err := srv.store.ListAlertRules(c.Req.Context(), &q) + configs, _, err := srv.searchAuthorizedAlertRules(c.Req.Context(), c, namespaceUIDs, dashboardUID, panelID) if err != nil { - return ErrResp(http.StatusInternalServerError, err, "failed to get alert rules") + return errorToResponse(err) } - - hasAccess := func(evaluator accesscontrol.Evaluator) bool { - return accesscontrol.HasAccess(srv.ac, c)(evaluator) - } - provenanceRecords, err := srv.provenanceStore.GetProvenances(c.Req.Context(), c.SignedInUser.OrgID, (&ngmodels.AlertRule{}).ResourceType()) if err != nil { return ErrResp(http.StatusInternalServerError, err, "failed to get alert rules") } - configs := make(map[ngmodels.AlertRuleGroupKey]ngmodels.RulesGroup) - for _, r := range ruleList { - groupKey := r.GetGroupKey() - group := configs[groupKey] - group = append(group, r) - configs[groupKey] = group - } - for groupKey, rules := range configs { folder, ok := namespaceMap[groupKey.NamespaceUID] if !ok { 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)) } @@ -531,3 +474,50 @@ func validateQueries(ctx context.Context, groupChanges *store.GroupDelta, valida } return nil } + +// getAuthorizedRuleGroup fetches rules that belong to the specified models.AlertRuleGroupKey and validate user's authorization. +// A user is authorized to access a group of rules only when it has permission to query all data sources used by all rules in this group. +// Returns models.RuleGroup if authorization passed or ErrAuthorization if user is not authorized to access the rule. +func (srv RulerSrv) getAuthorizedRuleGroup(ctx context.Context, c *contextmodel.ReqContext, ruleGroupKey ngmodels.AlertRuleGroupKey) (ngmodels.RulesGroup, error) { + hasAccess := accesscontrol.HasAccess(srv.ac, c) + + q := ngmodels.ListAlertRulesQuery{ + OrgID: ruleGroupKey.OrgID, + NamespaceUIDs: []string{ruleGroupKey.NamespaceUID}, + RuleGroup: ruleGroupKey.RuleGroup, + } + rules, err := srv.store.ListAlertRules(ctx, &q) + if err != nil { + return nil, err + } + if !authorizeAccessToRuleGroup(rules, hasAccess) { + return nil, fmt.Errorf("%w to access rules in this group", ErrAuthorization) + } + return rules, nil +} + +// searchAuthorizedAlertRules fetches rules according to the filters, groups them by models.AlertRuleGroupKey and filters out groups that the current user is not authorized to access. +// A user is authorized to access a group of rules only when it has permission to query all data sources used by all rules in this group. +// Returns groups that user is authorized to access, and total count of groups returned by query +func (srv RulerSrv) searchAuthorizedAlertRules(ctx context.Context, c *contextmodel.ReqContext, folderUIDs []string, dashboardUID string, panelID int64) (map[ngmodels.AlertRuleGroupKey]ngmodels.RulesGroup, int, error) { + hasAccess := accesscontrol.HasAccess(srv.ac, c) + query := ngmodels.ListAlertRulesQuery{ + OrgID: c.OrgID, + NamespaceUIDs: folderUIDs, + DashboardUID: dashboardUID, + PanelID: panelID, + } + rules, err := srv.store.ListAlertRules(ctx, &query) + if err != nil { + return nil, 0, err + } + + byGroupKey := ngmodels.GroupByAlertRuleGroupKey(rules) + totalGroups := len(byGroupKey) + for groupKey, rulesGroup := range byGroupKey { + if !authorizeAccessToRuleGroup(rulesGroup, hasAccess) { + delete(byGroupKey, groupKey) + } + } + return byGroupKey, totalGroups, nil +} diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go index 028599f1c5e..30bd57d0cea 100644 --- a/pkg/services/ngalert/models/alert_rule.go +++ b/pkg/services/ngalert/models/alert_rule.go @@ -571,3 +571,15 @@ func RuleKeyFromContext(ctx context.Context) (AlertRuleKey, bool) { key, ok := ctx.Value(ruleKeyContextKey{}).(AlertRuleKey) return key, ok } + +// GroupByAlertRuleGroupKey groups all rules by AlertRuleGroupKey. Returns map of RulesGroup sorted by AlertRule.RuleGroupIndex +func GroupByAlertRuleGroupKey(rules []*AlertRule) map[AlertRuleGroupKey]RulesGroup { + result := make(map[AlertRuleGroupKey]RulesGroup) + for _, rule := range rules { + result[rule.GetGroupKey()] = append(result[rule.GetGroupKey()], rule) + } + for _, group := range result { + group.SortByGroupIndex() + } + return result +}