mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
918f11d81e
commit
237ce5ea82
@ -56,82 +56,74 @@ func (srv RulerSrv) RouteDeleteAlertRules(c *contextmodel.ReqContext, namespaceT
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return toNamespaceErrorResponse(err)
|
return toNamespaceErrorResponse(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var loggerCtx = []any{
|
var loggerCtx = []any{
|
||||||
"namespace",
|
"userId",
|
||||||
namespace.Title,
|
c.SignedInUser.UserID,
|
||||||
|
"namespaceUid",
|
||||||
|
namespace.UID,
|
||||||
}
|
}
|
||||||
var ruleGroup string
|
|
||||||
if group != "" {
|
if group != "" {
|
||||||
ruleGroup = group
|
|
||||||
loggerCtx = append(loggerCtx, "group", group)
|
loggerCtx = append(loggerCtx, "group", group)
|
||||||
}
|
}
|
||||||
logger := srv.log.New(loggerCtx...)
|
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())
|
provenances, err := srv.provenanceStore.GetProvenances(c.Req.Context(), c.SignedInUser.OrgID, (&ngmodels.AlertRule{}).ResourceType())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrResp(http.StatusInternalServerError, err, "failed to fetch provenances of alert rules")
|
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 {
|
err = srv.xactManager.InTransaction(c.Req.Context(), func(ctx context.Context) error {
|
||||||
unauthz, provisioned := false, false
|
deletionCandidates := map[ngmodels.AlertRuleGroupKey]ngmodels.RulesGroup{}
|
||||||
q := ngmodels.ListAlertRulesQuery{
|
if group != "" {
|
||||||
OrgID: c.SignedInUser.OrgID,
|
key := ngmodels.AlertRuleGroupKey{
|
||||||
NamespaceUIDs: []string{namespace.UID},
|
OrgID: c.OrgID,
|
||||||
RuleGroup: ruleGroup,
|
NamespaceUID: namespace.UID,
|
||||||
}
|
RuleGroup: group,
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
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) {
|
if containsProvisionedAlerts(provenances, rules) {
|
||||||
|
logger.Debug("Alert group cannot be deleted because it is provisioned", "group", groupKey.RuleGroup)
|
||||||
provisioned = true
|
provisioned = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
uid := make([]string, 0, len(rules))
|
uid := make([]string, 0, len(rules))
|
||||||
keys := make([]ngmodels.AlertRuleKey, 0, len(rules))
|
|
||||||
for _, rule := range rules {
|
for _, rule := range rules {
|
||||||
uid = append(uid, rule.UID)
|
uid = append(uid, rule.UID)
|
||||||
keys = append(keys, rule.GetKey())
|
|
||||||
}
|
}
|
||||||
rulesToDelete = append(rulesToDelete, uid...)
|
rulesToDelete = append(rulesToDelete, uid...)
|
||||||
deletedGroups[groupKey] = keys
|
|
||||||
}
|
}
|
||||||
if len(rulesToDelete) > 0 {
|
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.
|
// 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.
|
// 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 {
|
if provisioned {
|
||||||
return errProvisionedResource
|
return errProvisionedResource
|
||||||
}
|
}
|
||||||
if unauthz {
|
logger.Info("No alert rules were deleted")
|
||||||
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)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -154,36 +146,19 @@ func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *contextmodel.ReqContext, nam
|
|||||||
return toNamespaceErrorResponse(err)
|
return toNamespaceErrorResponse(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
q := ngmodels.ListAlertRulesQuery{
|
ruleGroups, _, err := srv.searchAuthorizedAlertRules(c.Req.Context(), c, []string{namespace.UID}, "", 0)
|
||||||
OrgID: c.SignedInUser.OrgID,
|
|
||||||
NamespaceUIDs: []string{namespace.UID},
|
|
||||||
}
|
|
||||||
ruleList, err := srv.store.ListAlertRules(c.Req.Context(), &q)
|
|
||||||
if err != nil {
|
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())
|
provenanceRecords, err := srv.provenanceStore.GetProvenances(c.Req.Context(), c.SignedInUser.OrgID, (&ngmodels.AlertRule{}).ResourceType())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrResp(http.StatusInternalServerError, err, "failed to get provenance for rule group")
|
return ErrResp(http.StatusInternalServerError, err, "failed to get provenance for rule group")
|
||||||
}
|
}
|
||||||
|
|
||||||
ruleGroups := make(map[string]ngmodels.RulesGroup)
|
result := apimodels.NamespaceConfigResponse{}
|
||||||
for _, r := range ruleList {
|
|
||||||
ruleGroups[r.RuleGroup] = append(ruleGroups[r.RuleGroup], r)
|
|
||||||
}
|
|
||||||
|
|
||||||
for groupName, rules := range ruleGroups {
|
for groupKey, rules := range ruleGroups {
|
||||||
if !authorizeAccessToRuleGroup(rules, hasAccess) {
|
result[namespaceTitle] = append(result[namespaceTitle], toGettableRuleGroupConfig(groupKey.RuleGroup, rules, namespace.ID, provenanceRecords))
|
||||||
continue
|
|
||||||
}
|
|
||||||
result[namespaceTitle] = append(result[namespaceTitle], toGettableRuleGroupConfig(groupName, rules, namespace.ID, provenanceRecords))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.JSON(http.StatusAccepted, result)
|
return response.JSON(http.StatusAccepted, result)
|
||||||
@ -197,18 +172,13 @@ func (srv RulerSrv) RouteGetRulesGroupConfig(c *contextmodel.ReqContext, namespa
|
|||||||
return toNamespaceErrorResponse(err)
|
return toNamespaceErrorResponse(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
q := ngmodels.ListAlertRulesQuery{
|
rules, err := srv.getAuthorizedRuleGroup(c.Req.Context(), c, ngmodels.AlertRuleGroupKey{
|
||||||
OrgID: c.SignedInUser.OrgID,
|
OrgID: c.OrgID,
|
||||||
NamespaceUIDs: []string{namespace.UID},
|
RuleGroup: ruleGroup,
|
||||||
RuleGroup: ruleGroup,
|
NamespaceUID: namespace.UID,
|
||||||
}
|
})
|
||||||
ruleList, err := srv.store.ListAlertRules(c.Req.Context(), &q)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrResp(http.StatusInternalServerError, err, "failed to get group 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())
|
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")
|
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{
|
result := apimodels.RuleGroupConfigResponse{
|
||||||
GettableRuleGroupConfig: toGettableRuleGroupConfig(ruleGroup, ruleList, namespace.ID, provenanceRecords),
|
GettableRuleGroupConfig: toGettableRuleGroupConfig(ruleGroup, rules, namespace.ID, provenanceRecords),
|
||||||
}
|
}
|
||||||
return response.JSON(http.StatusAccepted, result)
|
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"), "")
|
return ErrResp(http.StatusBadRequest, errors.New("panel_id must be set with dashboard_uid"), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
q := ngmodels.ListAlertRulesQuery{
|
configs, _, err := srv.searchAuthorizedAlertRules(c.Req.Context(), c, namespaceUIDs, dashboardUID, panelID)
|
||||||
OrgID: c.SignedInUser.OrgID,
|
|
||||||
NamespaceUIDs: namespaceUIDs,
|
|
||||||
DashboardUID: dashboardUID,
|
|
||||||
PanelID: panelID,
|
|
||||||
}
|
|
||||||
|
|
||||||
ruleList, err := srv.store.ListAlertRules(c.Req.Context(), &q)
|
|
||||||
if err != nil {
|
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())
|
provenanceRecords, err := srv.provenanceStore.GetProvenances(c.Req.Context(), c.SignedInUser.OrgID, (&ngmodels.AlertRule{}).ResourceType())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrResp(http.StatusInternalServerError, err, "failed to get alert rules")
|
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 {
|
for groupKey, rules := range configs {
|
||||||
folder, ok := namespaceMap[groupKey.NamespaceUID]
|
folder, ok := namespaceMap[groupKey.NamespaceUID]
|
||||||
if !ok {
|
if !ok {
|
||||||
srv.log.Error("Namespace not visible to the user", "user", c.SignedInUser.UserID, "namespace", groupKey.NamespaceUID)
|
srv.log.Error("Namespace not visible to the user", "user", c.SignedInUser.UserID, "namespace", groupKey.NamespaceUID)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !authorizeAccessToRuleGroup(rules, hasAccess) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
namespace := folder.Title
|
namespace := folder.Title
|
||||||
result[namespace] = append(result[namespace], toGettableRuleGroupConfig(groupKey.RuleGroup, rules, folder.ID, provenanceRecords))
|
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
|
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
|
||||||
|
}
|
||||||
|
@ -571,3 +571,15 @@ func RuleKeyFromContext(ctx context.Context) (AlertRuleKey, bool) {
|
|||||||
key, ok := ctx.Value(ruleKeyContextKey{}).(AlertRuleKey)
|
key, ok := ctx.Value(ruleKeyContextKey{}).(AlertRuleKey)
|
||||||
return key, ok
|
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
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user