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

This commit is contained in:
Jean-Philippe Quéméner
2022-05-06 20:55:27 +02:00
committed by GitHub
parent e16dc72c94
commit 30d9cc81ec
2 changed files with 151 additions and 26 deletions

View File

@@ -67,7 +67,12 @@ func (srv RulerSrv) RouteDeleteAlertRules(c *models.ReqContext) response.Respons
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 {
q := ngmodels.ListAlertRulesQuery{
OrgID: c.SignedInUser.OrgId,
@@ -83,24 +88,52 @@ func (srv RulerSrv) RouteDeleteAlertRules(c *models.ReqContext) response.Respons
return nil
}
canDelete = make([]string, 0, len(q.Result))
for _, rule := range q.Result {
if authorizeDatasourceAccessForRule(rule, hasAccess) {
canDelete = append(canDelete, rule.UID)
continue
var canDelete []*ngmodels.AlertRule
var cannotDelete []string
// partition will partation the given rules in two, one partition
// 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 {
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 {
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 {
@@ -112,7 +145,7 @@ func (srv RulerSrv) RouteDeleteAlertRules(c *models.ReqContext) response.Respons
logger.Debug("rules have been deleted from the store. updating scheduler")
for _, uid := range canDelete {
for _, uid := range deletableRules {
srv.scheduleService.DeleteAlertRule(ngmodels.AlertRuleKey{
OrgID: c.SignedInUser.OrgId,
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.
// 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 {
var authorizedChanges *changes
var finalChanges *changes
hasAccess := accesscontrol.HasAccess(srv.ac, c)
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)
@@ -347,12 +381,12 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, namespace *mod
}
if groupChanges.isEmpty() {
authorizedChanges = groupChanges
finalChanges = groupChanges
logger.Info("no changes detected in the request. Do nothing")
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)
})
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.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 {
updates := make([]store.UpdateRule, 0, len(authorizedChanges.Update))
inserts := make([]ngmodels.AlertRule, 0, len(authorizedChanges.New))
for _, update := range authorizedChanges.Update {
// 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 {
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())
updates = append(updates, store.UpdateRule{
Existing: update.Existing,
New: *update.New,
})
}
for _, rule := range authorizedChanges.New {
for _, rule := range finalChanges.New {
inserts = append(inserts, *rule)
}
err = srv.store.InsertAlertRules(tranCtx, inserts)
@@ -393,9 +466,9 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, namespace *mod
}
}
if len(authorizedChanges.Delete) > 0 {
UIDs := make([]string, 0, len(authorizedChanges.Delete))
for _, rule := range authorizedChanges.Delete {
if len(finalChanges.Delete) > 0 {
UIDs := make([]string, 0, len(finalChanges.Delete))
for _, rule := range finalChanges.Delete {
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{
OrgId: c.OrgId,
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")
}
for _, rule := range authorizedChanges.Update {
for _, rule := range finalChanges.Update {
srv.scheduleService.UpdateAlertRule(ngmodels.AlertRuleKey{
OrgID: c.SignedInUser.OrgId,
UID: rule.Existing.UID,
})
}
for _, rule := range authorizedChanges.Delete {
for _, rule := range finalChanges.Delete {
srv.scheduleService.DeleteAlertRule(ngmodels.AlertRuleKey{
OrgID: c.SignedInUser.OrgId,
UID: rule.UID,
})
}
if authorizedChanges.isEmpty() {
if finalChanges.isEmpty() {
return response.JSON(http.StatusAccepted, util.DynMap{"message": "no changes detected in the rule group"})
}

View File

@@ -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()))
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("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()))
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("should delete only those that are accessible in folder", func(t *testing.T) {