From ac866b01147296c286c59ee798bb4843e4313c86 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Tue, 23 Dec 2025 14:51:45 +0100 Subject: [PATCH] Alerting: Prevent convert API from deleting non-imported rule groups (#115667) --- .../ngalert/api/api_convert_prometheus.go | 7 +++- .../api/api_convert_prometheus_test.go | 33 ++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/pkg/services/ngalert/api/api_convert_prometheus.go b/pkg/services/ngalert/api/api_convert_prometheus.go index 588f6dee213..79198ababf1 100644 --- a/pkg/services/ngalert/api/api_convert_prometheus.go +++ b/pkg/services/ngalert/api/api_convert_prometheus.go @@ -251,7 +251,12 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteRuleGroup(c *contex logger.Info("Deleting Prometheus-imported rule group", "folder_uid", folder.UID, "folder_title", namespaceTitle, "group", group) provenance := getProvenance(c) - err = srv.alertRuleService.DeleteRuleGroup(c.Req.Context(), c.SignedInUser, folder.UID, group, provenance) + filterOpts := &provisioning.FilterOptions{ + NamespaceUIDs: []string{folder.UID}, + RuleGroups: []string{group}, + HasPrometheusRuleDefinition: util.Pointer(true), + } + err = srv.alertRuleService.DeleteRuleGroups(c.Req.Context(), c.SignedInUser, provenance, filterOpts) if errors.Is(err, models.ErrAlertRuleGroupNotFound) { return response.Empty(http.StatusNotFound) } diff --git a/pkg/services/ngalert/api/api_convert_prometheus_test.go b/pkg/services/ngalert/api/api_convert_prometheus_test.go index 08ce147a5c3..b4377431249 100644 --- a/pkg/services/ngalert/api/api_convert_prometheus_test.go +++ b/pkg/services/ngalert/api/api_convert_prometheus_test.go @@ -1390,7 +1390,7 @@ func TestRouteConvertPrometheusDeleteRuleGroup(t *testing.T) { t.Run("with disable provenance header should still be able to delete rules", func(t *testing.T) { provenanceStore := fakes.NewFakeProvisioningStore() - srv, ruleStore, fldr, rule := initGroup("", groupName, withProvenanceStore(provenanceStore)) + srv, ruleStore, fldr, rule := initGroup("prometheus definition", groupName, withProvenanceStore(provenanceStore)) // Mark the rule as provisioned with API provenance err := provenanceStore.SetProvenance(context.Background(), rule, 1, models.ProvenanceConvertedPrometheus) @@ -1411,6 +1411,37 @@ func TestRouteConvertPrometheusDeleteRuleGroup(t *testing.T) { require.Nil(t, remaining) }) }) + + t.Run("should not delete non-imported rule groups", func(t *testing.T) { + folderService := foldertest.NewFakeService() + srv, _, ruleStore := createConvertPrometheusSrv(t, withFolderService(folderService)) + rc := createRequestCtx() + + fldr := randFolder() + fldr.ParentUID = "" + folderService.ExpectedFolder = fldr + folderService.ExpectedFolders = []*folder.Folder{fldr} + ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr) + + rule := models.RuleGen. + With(models.RuleGen.WithNamespaceUID(fldr.UID)). + With(models.RuleGen.WithOrgID(1)). + With(models.RuleGen.WithGroupName(groupName)). + GenerateRef() + ruleStore.PutRule(context.Background(), rule) + + // Attempt to delete via convert endpoint should return 404 + response := srv.RouteConvertPrometheusDeleteRuleGroup(rc, fldr.Title, groupName) + require.Equal(t, http.StatusNotFound, response.Status()) + + // Verify the rule is still present + remaining, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{ + UID: rule.UID, + OrgID: rule.OrgID, + }) + require.NoError(t, err) + require.NotNil(t, remaining) + }) } func TestRouteConvertPrometheusPostRuleGroups(t *testing.T) {