Alerting: check provenance of alert rules in current API (#48694) (#48827)

(cherry picked from commit 30d9cc81ec)

Co-authored-by: Jean-Philippe Quéméner <JohnnyQQQQ@users.noreply.github.com>
This commit is contained in:
Grot (@grafanabot)
2022-05-06 21:27:51 +02:00
committed by GitHub
co-authored by Jean-Philippe Quéméner
parent c9b8317352
commit 8917c920ae
2 changed files with 151 additions and 26 deletions
+99 -26
View File
@@ -67,7 +67,12 @@ func (srv RulerSrv) RouteDeleteAlertRules(c *models.ReqContext) response.Respons
return accesscontrol.HasAccess(srv.ac, c)(accesscontrol.ReqOrgAdminOrEditor, evaluator) return accesscontrol.HasAccess(srv.ac, c)(accesscontrol.ReqOrgAdminOrEditor, evaluator)
} }
var canDelete, cannotDelete []string 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")
}
var deletableRules []string
err = srv.xactManager.InTransaction(c.Req.Context(), func(ctx context.Context) error { err = srv.xactManager.InTransaction(c.Req.Context(), func(ctx context.Context) error {
q := ngmodels.ListAlertRulesQuery{ q := ngmodels.ListAlertRulesQuery{
OrgID: c.SignedInUser.OrgId, OrgID: c.SignedInUser.OrgId,
@@ -83,24 +88,52 @@ func (srv RulerSrv) RouteDeleteAlertRules(c *models.ReqContext) response.Respons
return nil return nil
} }
canDelete = make([]string, 0, len(q.Result)) var canDelete []*ngmodels.AlertRule
for _, rule := range q.Result { var cannotDelete []string
if authorizeDatasourceAccessForRule(rule, hasAccess) {
canDelete = append(canDelete, rule.UID) // partition will partation the given rules in two, one partition
continue // being the rules that fulfill the predicate the other partation being
// the ruleIDs not fulfilling it.
partition := func(alerts []*ngmodels.AlertRule, predicate func(rule *ngmodels.AlertRule) bool) ([]*ngmodels.AlertRule, []string) {
positive, negative := make([]*ngmodels.AlertRule, 0, len(alerts)), make([]string, 0, len(alerts))
for _, rule := range alerts {
if predicate(rule) {
positive = append(positive, rule)
continue
}
negative = append(negative, rule.UID)
} }
cannotDelete = append(cannotDelete, rule.UID) return positive, negative
} }
canDelete, cannotDelete = partition(q.Result, func(rule *ngmodels.AlertRule) bool {
return authorizeDatasourceAccessForRule(rule, hasAccess)
})
if len(canDelete) == 0 { if len(canDelete) == 0 {
return fmt.Errorf("%w to delete rules because user is not authorized to access data sources used by the rules", ErrAuthorization) return fmt.Errorf("%w to delete rules because user is not authorized to access data sources used by the rules", ErrAuthorization)
} }
if len(cannotDelete) > 0 { if len(cannotDelete) > 0 {
logger.Info("user cannot delete one or many alert rules because it does not have access to data sources. Those rules will be skipped", "expected", len(q.Result), "authorized", len(canDelete), "unauthorized", cannotDelete) logger.Info("user cannot delete one or many alert rules because it does not have access to data sources. Those rules will be skipped", "expected", len(q.Result), "authorized", len(canDelete), "unauthorized", cannotDelete)
} }
return srv.store.DeleteAlertRulesByUID(ctx, c.SignedInUser.OrgId, canDelete...) canDelete, cannotDelete = partition(canDelete, func(rule *ngmodels.AlertRule) bool {
provenance, exists := provenances[rule.UID]
return (exists && provenance == ngmodels.ProvenanceNone) || !exists
})
if len(canDelete) == 0 {
return fmt.Errorf("all rules have been provisioned and cannot be deleted through this api")
}
if len(cannotDelete) > 0 {
logger.Info("user cannot delete one or many alert rules because it does have a provenance set. Those rules will be skipped", "expected", len(q.Result), "provenance_none", len(canDelete), "provenance_set", cannotDelete)
}
for _, rule := range canDelete {
deletableRules = append(deletableRules, rule.UID)
}
return srv.store.DeleteAlertRulesByUID(ctx, c.SignedInUser.OrgId, deletableRules...)
}) })
if err != nil { if err != nil {
@@ -112,7 +145,7 @@ func (srv RulerSrv) RouteDeleteAlertRules(c *models.ReqContext) response.Respons
logger.Debug("rules have been deleted from the store. updating scheduler") logger.Debug("rules have been deleted from the store. updating scheduler")
for _, uid := range canDelete { for _, uid := range deletableRules {
srv.scheduleService.DeleteAlertRule(ngmodels.AlertRuleKey{ srv.scheduleService.DeleteAlertRule(ngmodels.AlertRuleKey{
OrgID: c.SignedInUser.OrgId, OrgID: c.SignedInUser.OrgId,
UID: uid, UID: uid,
@@ -335,8 +368,9 @@ func (srv RulerSrv) RoutePostNameRulesConfig(c *models.ReqContext, ruleGroupConf
// updateAlertRulesInGroup calculates changes (rules to add,update,delete), verifies that the user is authorized to do the calculated changes and updates database. // updateAlertRulesInGroup calculates changes (rules to add,update,delete), verifies that the user is authorized to do the calculated changes and updates database.
// All operations are performed in a single transaction // All operations are performed in a single transaction
//nolint: gocyclo
func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, namespace *models.Folder, groupName string, rules []*ngmodels.AlertRule) response.Response { func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, namespace *models.Folder, groupName string, rules []*ngmodels.AlertRule) response.Response {
var authorizedChanges *changes var finalChanges *changes
hasAccess := accesscontrol.HasAccess(srv.ac, c) hasAccess := accesscontrol.HasAccess(srv.ac, c)
err := srv.xactManager.InTransaction(c.Req.Context(), func(tranCtx context.Context) error { err := srv.xactManager.InTransaction(c.Req.Context(), func(tranCtx context.Context) error {
logger := srv.log.New("namespace_uid", namespace.Uid, "group", groupName, "org_id", c.OrgId, "user_id", c.UserId) logger := srv.log.New("namespace_uid", namespace.Uid, "group", groupName, "org_id", c.OrgId, "user_id", c.UserId)
@@ -347,12 +381,12 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, namespace *mod
} }
if groupChanges.isEmpty() { if groupChanges.isEmpty() {
authorizedChanges = groupChanges finalChanges = groupChanges
logger.Info("no changes detected in the request. Do nothing") logger.Info("no changes detected in the request. Do nothing")
return nil return nil
} }
authorizedChanges, err = authorizeRuleChanges(namespace, groupChanges, func(evaluator accesscontrol.Evaluator) bool { authorizedChanges, err := authorizeRuleChanges(namespace, groupChanges, func(evaluator accesscontrol.Evaluator) bool {
return hasAccess(accesscontrol.ReqOrgAdminOrEditor, evaluator) return hasAccess(accesscontrol.ReqOrgAdminOrEditor, evaluator)
}) })
if err != nil { if err != nil {
@@ -368,19 +402,58 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, namespace *mod
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)) 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))
} }
logger.Debug("updating database with the authorized changes", "add", len(authorizedChanges.New), "update", len(authorizedChanges.New), "delete", len(authorizedChanges.Delete)) provenances, err := srv.provenanceStore.GetProvenances(c.Req.Context(), c.OrgId, (&ngmodels.AlertRule{}).ResourceType())
if err != nil {
return err
}
if len(authorizedChanges.Update) > 0 || len(authorizedChanges.New) > 0 { // New rules don't need to be checked for provenance, just copy the whole slice.
updates := make([]store.UpdateRule, 0, len(authorizedChanges.Update)) finalChanges = &changes{}
inserts := make([]ngmodels.AlertRule, 0, len(authorizedChanges.New)) finalChanges.New = authorizedChanges.New
for _, update := range authorizedChanges.Update { for _, rule := range authorizedChanges.Update {
if provenance, exists := provenances[rule.Existing.UID]; (exists && provenance == ngmodels.ProvenanceNone) || !exists {
finalChanges.Update = append(finalChanges.Update, rule)
}
}
for _, rule := range authorizedChanges.Delete {
if provenance, exists := provenances[rule.UID]; (exists && provenance == ngmodels.ProvenanceNone) || !exists {
finalChanges.Delete = append(finalChanges.Delete, rule)
}
}
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))
return nil
}
if len(authorizedChanges.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))
}
if len(authorizedChanges.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))
}
logger.Debug("updating database with the authorized changes", "add", len(finalChanges.New), "update", len(finalChanges.New), "delete", len(finalChanges.Delete))
if len(finalChanges.Update) > 0 || len(finalChanges.New) > 0 {
updates := make([]store.UpdateRule, 0, len(finalChanges.Update))
inserts := make([]ngmodels.AlertRule, 0, len(finalChanges.New))
for _, update := range finalChanges.Update {
logger.Debug("updating rule", "rule_uid", update.New.UID, "diff", update.Diff.String()) logger.Debug("updating rule", "rule_uid", update.New.UID, "diff", update.Diff.String())
updates = append(updates, store.UpdateRule{ updates = append(updates, store.UpdateRule{
Existing: update.Existing, Existing: update.Existing,
New: *update.New, New: *update.New,
}) })
} }
for _, rule := range authorizedChanges.New { for _, rule := range finalChanges.New {
inserts = append(inserts, *rule) inserts = append(inserts, *rule)
} }
err = srv.store.InsertAlertRules(tranCtx, inserts) err = srv.store.InsertAlertRules(tranCtx, inserts)
@@ -393,9 +466,9 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, namespace *mod
} }
} }
if len(authorizedChanges.Delete) > 0 { if len(finalChanges.Delete) > 0 {
UIDs := make([]string, 0, len(authorizedChanges.Delete)) UIDs := make([]string, 0, len(finalChanges.Delete))
for _, rule := range authorizedChanges.Delete { for _, rule := range finalChanges.Delete {
UIDs = append(UIDs, rule.UID) UIDs = append(UIDs, rule.UID)
} }
@@ -404,7 +477,7 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, namespace *mod
} }
} }
if len(authorizedChanges.New) > 0 { if len(finalChanges.New) > 0 {
limitReached, err := srv.QuotaService.CheckQuotaReached(tranCtx, "alert_rule", &quota.ScopeParameters{ limitReached, err := srv.QuotaService.CheckQuotaReached(tranCtx, "alert_rule", &quota.ScopeParameters{
OrgId: c.OrgId, OrgId: c.OrgId,
UserId: c.UserId, UserId: c.UserId,
@@ -432,21 +505,21 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, namespace *mod
return ErrResp(http.StatusInternalServerError, err, "failed to update rule group") return ErrResp(http.StatusInternalServerError, err, "failed to update rule group")
} }
for _, rule := range authorizedChanges.Update { for _, rule := range finalChanges.Update {
srv.scheduleService.UpdateAlertRule(ngmodels.AlertRuleKey{ srv.scheduleService.UpdateAlertRule(ngmodels.AlertRuleKey{
OrgID: c.SignedInUser.OrgId, OrgID: c.SignedInUser.OrgId,
UID: rule.Existing.UID, UID: rule.Existing.UID,
}) })
} }
for _, rule := range authorizedChanges.Delete { for _, rule := range finalChanges.Delete {
srv.scheduleService.DeleteAlertRule(ngmodels.AlertRuleKey{ srv.scheduleService.DeleteAlertRule(ngmodels.AlertRuleKey{
OrgID: c.SignedInUser.OrgId, OrgID: c.SignedInUser.OrgId,
UID: rule.UID, UID: rule.UID,
}) })
} }
if authorizedChanges.isEmpty() { if finalChanges.isEmpty() {
return response.JSON(http.StatusAccepted, util.DynMap{"message": "no changes detected in the rule group"}) return response.JSON(http.StatusAccepted, util.DynMap{"message": "no changes detected in the rule group"})
} }
@@ -376,6 +376,32 @@ func TestRouteDeleteAlertRules(t *testing.T) {
require.Equalf(t, 202, response.Status(), "Expected 202 but got %d: %v", response.Status(), string(response.Body())) require.Equalf(t, 202, response.Status(), "Expected 202 but got %d: %v", response.Status(), string(response.Body()))
assertRulesDeleted(t, rulesInFolderInGroup, ruleStore, scheduler) assertRulesDeleted(t, rulesInFolderInGroup, ruleStore, scheduler)
}) })
t.Run("editor shouldn't be able to delete provisioned rules", func(t *testing.T) {
ruleStore := store.NewFakeRuleStore(t)
orgID := rand.Int63()
folder := randFolder()
ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder)
rulesInFolder := models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withNamespace(folder)))
ruleStore.PutRule(context.Background(), rulesInFolder...)
ruleStore.PutRule(context.Background(), models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID)))...)
scheduler := &schedule.FakeScheduleService{}
scheduler.On("DeleteAlertRule", mock.Anything)
ac := acMock.New().WithDisabled()
svc := createService(ac, ruleStore, scheduler)
err := svc.provenanceStore.SetProvenance(context.Background(), rulesInFolder[0], orgID, models.ProvenanceAPI)
require.NoError(t, err)
request := createRequestContext(orgID, models2.ROLE_EDITOR, map[string]string{
":Namespace": folder.Title,
})
response := svc.RouteDeleteAlertRules(request)
require.Equalf(t, 202, response.Status(), "Expected 202 but got %d: %v", response.Status(), string(response.Body()))
assertRulesDeleted(t, rulesInFolder[1:], ruleStore, scheduler)
})
}) })
t.Run("when fine-grained access is enabled", func(t *testing.T) { t.Run("when fine-grained access is enabled", func(t *testing.T) {
t.Run("and user does not have access to any of data sources used by alert rules", func(t *testing.T) { t.Run("and user does not have access to any of data sources used by alert rules", func(t *testing.T) {
@@ -421,6 +447,32 @@ func TestRouteDeleteAlertRules(t *testing.T) {
require.Equalf(t, 202, response.Status(), "Expected 202 but got %d: %v", response.Status(), string(response.Body())) require.Equalf(t, 202, response.Status(), "Expected 202 but got %d: %v", response.Status(), string(response.Body()))
assertRulesDeleted(t, rulesInFolder, ruleStore, scheduler) assertRulesDeleted(t, rulesInFolder, ruleStore, scheduler)
}) })
t.Run("shouldn't be able to delete provisioned rules", func(t *testing.T) {
ruleStore := store.NewFakeRuleStore(t)
orgID := rand.Int63()
folder := randFolder()
ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder)
rulesInFolder := models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withNamespace(folder)))
ruleStore.PutRule(context.Background(), rulesInFolder...)
ruleStore.PutRule(context.Background(), models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID)))...)
scheduler := &schedule.FakeScheduleService{}
scheduler.On("DeleteAlertRule", mock.Anything)
ac := acMock.New().WithPermissions(createPermissionsForRules(rulesInFolder))
svc := createService(ac, ruleStore, scheduler)
err := svc.provenanceStore.SetProvenance(context.Background(), rulesInFolder[0], orgID, models.ProvenanceAPI)
require.NoError(t, err)
request := createRequestContext(orgID, "None", map[string]string{
":Namespace": folder.Title,
})
response := svc.RouteDeleteAlertRules(request)
require.Equalf(t, 202, response.Status(), "Expected 202 but got %d: %v", response.Status(), string(response.Body()))
assertRulesDeleted(t, rulesInFolder[1:], ruleStore, scheduler)
})
}) })
t.Run("and user has access to data sources of some of alert rules", func(t *testing.T) { t.Run("and user has access to data sources of some of alert rules", func(t *testing.T) {
t.Run("should delete only those that are accessible in folder", func(t *testing.T) { t.Run("should delete only those that are accessible in folder", func(t *testing.T) {