Alerting: rules delete API to check data source authorization (#46906)

* merge RuleSrv rule delete methods
* remove unused store methods
* implement delete by uid for fake store
* add scheduler mock
* implement tests for RouteDeleteAlertRules
This commit is contained in:
Yuriy Tseretyan 2022-03-25 12:39:24 -04:00 committed by GitHub
parent a06329d988
commit e20d157a9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 508 additions and 111 deletions

View File

@ -42,52 +42,82 @@ var (
errQuotaReached = errors.New("quota has been exceeded")
)
func (srv RulerSrv) RouteDeleteNamespaceRulesConfig(c *models.ReqContext) response.Response {
// RouteDeleteAlertRules deletes all alert rules user is authorized to access in the namespace (request parameter :Namespace)
// or, if specified, a group of rules (request parameter :Groupname) in the namespace
func (srv RulerSrv) RouteDeleteAlertRules(c *models.ReqContext) response.Response {
namespaceTitle := web.Params(c.Req)[":Namespace"]
namespace, err := srv.store.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.OrgId, c.SignedInUser, true)
if err != nil {
return toNamespaceErrorResponse(err)
}
var loggerCtx = []interface{}{
"namespace",
namespace.Title,
}
var ruleGroup *string
if group, ok := web.Params(c.Req)[":Groupname"]; ok {
ruleGroup = &group
loggerCtx = append(loggerCtx, "group", group)
}
logger := srv.log.New(loggerCtx...)
uids, err := srv.store.DeleteNamespaceAlertRules(c.Req.Context(), c.SignedInUser.OrgId, namespace.Uid)
if err != nil {
return ErrResp(http.StatusInternalServerError, err, "failed to delete namespace alert rules")
hasAccess := func(evaluator accesscontrol.Evaluator) bool {
return accesscontrol.HasAccess(srv.ac, c)(accesscontrol.ReqOrgAdminOrEditor, evaluator)
}
for _, uid := range uids {
srv.scheduleService.DeleteAlertRule(ngmodels.AlertRuleKey{
OrgID: c.SignedInUser.OrgId,
UID: uid,
})
}
var canDelete, cannotDelete []string
err = srv.xactManager.InTransaction(c.Req.Context(), func(ctx context.Context) error {
q := ngmodels.GetAlertRulesQuery{
OrgID: c.SignedInUser.OrgId,
NamespaceUID: namespace.Uid,
RuleGroup: ruleGroup,
}
if err = srv.store.GetAlertRules(ctx, &q); err != nil {
return err
}
return response.JSON(http.StatusAccepted, util.DynMap{"message": "namespace rules deleted"})
}
if len(q.Result) == 0 {
logger.Debug("no alert rules to delete from namespace/group")
return nil
}
func (srv RulerSrv) RouteDeleteRuleGroupConfig(c *models.ReqContext) response.Response {
namespaceTitle := web.Params(c.Req)[":Namespace"]
namespace, err := srv.store.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.OrgId, c.SignedInUser, true)
if err != nil {
return toNamespaceErrorResponse(err)
}
ruleGroup := web.Params(c.Req)[":Groupname"]
uids, err := srv.store.DeleteRuleGroupAlertRules(c.Req.Context(), c.SignedInUser.OrgId, namespace.Uid, ruleGroup)
canDelete = make([]string, 0, len(q.Result))
for _, rule := range q.Result {
if authorizeDatasourceAccessForRule(rule, hasAccess) {
canDelete = append(canDelete, rule.UID)
continue
}
cannotDelete = append(cannotDelete, rule.UID)
}
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...)
})
if err != nil {
if errors.Is(err, ngmodels.ErrRuleGroupNamespaceNotFound) {
return ErrResp(http.StatusNotFound, err, "failed to delete rule group")
if errors.Is(err, ErrAuthorization) {
return ErrResp(http.StatusUnauthorized, err, "")
}
return ErrResp(http.StatusInternalServerError, err, "failed to delete rule group")
}
for _, uid := range uids {
logger.Debug("rules have been deleted from the store. updating scheduler")
for _, uid := range canDelete {
srv.scheduleService.DeleteAlertRule(ngmodels.AlertRuleKey{
OrgID: c.SignedInUser.OrgId,
UID: uid,
})
}
return response.JSON(http.StatusAccepted, util.DynMap{"message": "rule group deleted"})
return response.JSON(http.StatusAccepted, util.DynMap{"message": "rules deleted"})
}
func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *models.ReqContext) response.Response {

View File

@ -4,15 +4,24 @@ import (
"context"
"errors"
"math/rand"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/log"
models2 "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
acMock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/schedule"
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
func TestCalculateChanges(t *testing.T) {
@ -255,6 +264,266 @@ func TestCalculateChanges(t *testing.T) {
})
}
func TestRouteDeleteAlertRules(t *testing.T) {
createService := func(ac *acMock.Mock, store *store.FakeRuleStore, scheduler schedule.ScheduleService) *RulerSrv {
return &RulerSrv{
xactManager: store,
store: store,
DatasourceCache: nil,
QuotaService: nil,
scheduleService: scheduler,
log: log.New("test"),
cfg: nil,
ac: ac,
}
}
getRecordedCommand := func(ruleStore *store.FakeRuleStore) []store.GenericRecordedQuery {
results := ruleStore.GetRecordedCommands(func(cmd interface{}) (interface{}, bool) {
c, ok := cmd.(store.GenericRecordedQuery)
if !ok || c.Name != "DeleteAlertRulesByUID" {
return nil, false
}
return c, ok
})
var result []store.GenericRecordedQuery
for _, cmd := range results {
result = append(result, cmd.(store.GenericRecordedQuery))
}
return result
}
assertRulesDeleted := func(t *testing.T, expectedRules []*models.AlertRule, ruleStore *store.FakeRuleStore, scheduler *schedule.FakeScheduleService) {
deleteCommands := getRecordedCommand(ruleStore)
require.Len(t, deleteCommands, 1)
cmd := deleteCommands[0]
actualUIDs := cmd.Params[1].([]string)
require.Len(t, actualUIDs, len(expectedRules))
for _, rule := range expectedRules {
require.Containsf(t, actualUIDs, rule.UID, "Rule %s was expected to be deleted but it wasn't", rule.UID)
}
require.Len(t, scheduler.Calls, len(expectedRules))
for _, call := range scheduler.Calls {
require.Equal(t, "DeleteAlertRule", call.Method)
key, ok := call.Arguments.Get(0).(models.AlertRuleKey)
require.Truef(t, ok, "Expected AlertRuleKey but got something else")
found := false
for _, rule := range expectedRules {
if rule.GetKey() == key {
found = true
break
}
}
require.Truef(t, found, "Key %v was not expected to be submitted to scheduler", key)
}
}
t.Run("when fine-grained access is disabled", func(t *testing.T) {
t.Run("viewer should not be authorized", func(t *testing.T) {
ruleStore := store.NewFakeRuleStore(t)
orgID := rand.Int63()
folder := randFolder()
ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder)
ruleStore.PutRule(context.Background(), models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withNamespace(folder)))...)
ruleStore.PutRule(context.Background(), models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID)))...)
scheduler := &schedule.FakeScheduleService{}
scheduler.On("DeleteAlertRule", mock.Anything).Panic("should not be called")
ac := acMock.New().WithDisabled()
request := createRequestContext(orgID, models2.ROLE_VIEWER, map[string]string{
":Namespace": folder.Title,
})
response := createService(ac, ruleStore, scheduler).RouteDeleteAlertRules(request)
require.Equalf(t, 401, response.Status(), "Expected 403 but got %d: %v", response.Status(), string(response.Body()))
scheduler.AssertNotCalled(t, "DeleteAlertRule")
require.Empty(t, getRecordedCommand(ruleStore))
})
t.Run("editor should be able to delete all rules in folder", 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()
request := createRequestContext(orgID, models2.ROLE_EDITOR, map[string]string{
":Namespace": folder.Title,
})
response := createService(ac, ruleStore, scheduler).RouteDeleteAlertRules(request)
require.Equalf(t, 202, response.Status(), "Expected 202 but got %d: %v", response.Status(), string(response.Body()))
assertRulesDeleted(t, rulesInFolder, ruleStore, scheduler)
})
t.Run("editor should be able to delete rules in a group in a folder", func(t *testing.T) {
ruleStore := store.NewFakeRuleStore(t)
orgID := rand.Int63()
groupName := util.GenerateShortUID()
folder := randFolder()
ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder)
rulesInFolderInGroup := models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withNamespace(folder), withGroup(groupName)))
ruleStore.PutRule(context.Background(), rulesInFolderInGroup...)
// rules in different groups but in the same namespace
ruleStore.PutRule(context.Background(), models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withNamespace(folder)))...)
// rules in the same group but different folder
ruleStore.PutRule(context.Background(), models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withGroup(groupName)))...)
scheduler := &schedule.FakeScheduleService{}
scheduler.On("DeleteAlertRule", mock.Anything)
ac := acMock.New().WithDisabled()
request := createRequestContext(orgID, models2.ROLE_EDITOR, map[string]string{
":Namespace": folder.Title,
":Groupname": groupName,
})
response := createService(ac, ruleStore, scheduler).RouteDeleteAlertRules(request)
require.Equalf(t, 202, response.Status(), "Expected 202 but got %d: %v", response.Status(), string(response.Body()))
assertRulesDeleted(t, rulesInFolderInGroup, 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) {
ruleStore := store.NewFakeRuleStore(t)
orgID := rand.Int63()
folder := randFolder()
ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder)
ruleStore.PutRule(context.Background(), models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withNamespace(folder)))...)
ruleStore.PutRule(context.Background(), models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID)))...)
scheduler := &schedule.FakeScheduleService{}
scheduler.On("DeleteAlertRule", mock.Anything).Panic("should not be called")
ac := acMock.New()
request := createRequestContext(orgID, "None", map[string]string{
":Namespace": folder.Title,
})
response := createService(ac, ruleStore, scheduler).RouteDeleteAlertRules(request)
require.Equalf(t, 401, response.Status(), "Expected 403 but got %d: %v", response.Status(), string(response.Body()))
scheduler.AssertNotCalled(t, "DeleteAlertRule")
require.Empty(t, getRecordedCommand(ruleStore))
})
t.Run("and user has access to all alert rules", func(t *testing.T) {
t.Run("should delete all 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)
var permissions []*accesscontrol.Permission
for _, rule := range rulesInFolder {
for _, query := range rule.Data {
permissions = append(permissions, &accesscontrol.Permission{
Action: datasources.ActionQuery, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(query.DatasourceUID),
})
}
}
ac := acMock.New().WithPermissions(permissions)
request := createRequestContext(orgID, "None", map[string]string{
":Namespace": folder.Title,
})
response := createService(ac, ruleStore, scheduler).RouteDeleteAlertRules(request)
require.Equalf(t, 202, response.Status(), "Expected 202 but got %d: %v", response.Status(), string(response.Body()))
assertRulesDeleted(t, rulesInFolder, 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) {
ruleStore := store.NewFakeRuleStore(t)
orgID := rand.Int63()
folder := randFolder()
ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder)
authorizedRulesInFolder := models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withNamespace(folder)))
ruleStore.PutRule(context.Background(), authorizedRulesInFolder...)
// more rules in the same namespace but user does not have access to them
ruleStore.PutRule(context.Background(), models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withNamespace(folder)))...)
ruleStore.PutRule(context.Background(), models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID)))...)
scheduler := &schedule.FakeScheduleService{}
scheduler.On("DeleteAlertRule", mock.Anything)
var permissions []*accesscontrol.Permission
for _, rule := range authorizedRulesInFolder {
for _, query := range rule.Data {
permissions = append(permissions, &accesscontrol.Permission{
Action: datasources.ActionQuery, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(query.DatasourceUID),
})
}
}
ac := acMock.New().WithPermissions(permissions)
request := createRequestContext(orgID, "None", map[string]string{
":Namespace": folder.Title,
})
response := createService(ac, ruleStore, scheduler).RouteDeleteAlertRules(request)
require.Equalf(t, 202, response.Status(), "Expected 202 but got %d: %v", response.Status(), string(response.Body()))
assertRulesDeleted(t, authorizedRulesInFolder, ruleStore, scheduler)
})
t.Run("should delete only rules in a group that are authorized", func(t *testing.T) {
ruleStore := store.NewFakeRuleStore(t)
orgID := rand.Int63()
groupName := util.GenerateShortUID()
folder := randFolder()
ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder)
authorizedRulesInGroup := models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withNamespace(folder), withGroup(groupName)))
ruleStore.PutRule(context.Background(), authorizedRulesInGroup...)
// more rules in the same group but user is not authorized to access them
ruleStore.PutRule(context.Background(), models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withNamespace(folder), withGroup(groupName)))...)
// rules in different groups but in the same namespace
ruleStore.PutRule(context.Background(), models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withNamespace(folder)))...)
// rules in the same group but different folder
ruleStore.PutRule(context.Background(), models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withGroup(groupName)))...)
scheduler := &schedule.FakeScheduleService{}
scheduler.On("DeleteAlertRule", mock.Anything)
var permissions []*accesscontrol.Permission
for _, rule := range authorizedRulesInGroup {
for _, query := range rule.Data {
permissions = append(permissions, &accesscontrol.Permission{
Action: datasources.ActionQuery, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(query.DatasourceUID),
})
}
}
ac := acMock.New().WithPermissions(permissions)
request := createRequestContext(orgID, "None", map[string]string{
":Namespace": folder.Title,
":Groupname": groupName,
})
response := createService(ac, ruleStore, scheduler).RouteDeleteAlertRules(request)
require.Equalf(t, 202, response.Status(), "Expected 202 but got %d: %v", response.Status(), string(response.Body()))
assertRulesDeleted(t, authorizedRulesInGroup, ruleStore, scheduler)
})
})
})
}
func createRequestContext(orgID int64, role models2.RoleType, params map[string]string) *models2.ReqContext {
ctx := web.Context{Req: &http.Request{}}
ctx.Req = web.SetURLParams(ctx.Req, params)
return &models2.ReqContext{
SignedInUser: &models2.SignedInUser{
OrgRole: role,
OrgId: orgID,
},
Context: &ctx,
}
}
func withOrgID(orgId int64) func(rule *models.AlertRule) {
return func(rule *models.AlertRule) {
rule.OrgID = orgId

View File

@ -110,11 +110,11 @@ func (f *ForkedRulerApi) forkRoutePostNameRulesConfig(ctx *models.ReqContext, co
}
func (f *ForkedRulerApi) forkRouteDeleteNamespaceGrafanaRulesConfig(ctx *models.ReqContext) response.Response {
return f.GrafanaRuler.RouteDeleteNamespaceRulesConfig(ctx)
return f.GrafanaRuler.RouteDeleteAlertRules(ctx)
}
func (f *ForkedRulerApi) forkRouteDeleteGrafanaRuleGroupConfig(ctx *models.ReqContext) response.Response {
return f.GrafanaRuler.RouteDeleteRuleGroupConfig(ctx)
return f.GrafanaRuler.RouteDeleteAlertRules(ctx)
}
func (f *ForkedRulerApi) forkRouteGetNamespaceGrafanaRulesConfig(ctx *models.ReqContext) response.Response {

View File

@ -26,6 +26,7 @@ import (
// ScheduleService is an interface for a service that schedules the evaluation
// of alert rules.
//go:generate mockery --name ScheduleService --structname FakeScheduleService --inpackage --filename schedule_mock.go
type ScheduleService interface {
// Run the scheduler until the context is canceled or the scheduler returns
// an error. The scheduler is terminated when this function returns.

View File

@ -0,0 +1,118 @@
// Code generated by mockery v2.10.0. DO NOT EDIT.
package schedule
import (
context "context"
models "github.com/grafana/grafana/pkg/services/ngalert/models"
mock "github.com/stretchr/testify/mock"
time "time"
url "net/url"
)
// FakeScheduleService is an autogenerated mock type for the ScheduleService type
type FakeScheduleService struct {
mock.Mock
}
// AlertmanagersFor provides a mock function with given fields: orgID
func (_m *FakeScheduleService) AlertmanagersFor(orgID int64) []*url.URL {
ret := _m.Called(orgID)
var r0 []*url.URL
if rf, ok := ret.Get(0).(func(int64) []*url.URL); ok {
r0 = rf(orgID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*url.URL)
}
}
return r0
}
// DeleteAlertRule provides a mock function with given fields: key
func (_m *FakeScheduleService) DeleteAlertRule(key models.AlertRuleKey) {
_m.Called(key)
}
// DroppedAlertmanagersFor provides a mock function with given fields: orgID
func (_m *FakeScheduleService) DroppedAlertmanagersFor(orgID int64) []*url.URL {
ret := _m.Called(orgID)
var r0 []*url.URL
if rf, ok := ret.Get(0).(func(int64) []*url.URL); ok {
r0 = rf(orgID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*url.URL)
}
}
return r0
}
// Pause provides a mock function with given fields:
func (_m *FakeScheduleService) Pause() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// Run provides a mock function with given fields: _a0
func (_m *FakeScheduleService) Run(_a0 context.Context) error {
ret := _m.Called(_a0)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
r0 = rf(_a0)
} else {
r0 = ret.Error(0)
}
return r0
}
// Unpause provides a mock function with given fields:
func (_m *FakeScheduleService) Unpause() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateAlertRule provides a mock function with given fields: key
func (_m *FakeScheduleService) UpdateAlertRule(key models.AlertRuleKey) {
_m.Called(key)
}
// evalApplied provides a mock function with given fields: _a0, _a1
func (_m *FakeScheduleService) evalApplied(_a0 models.AlertRuleKey, _a1 time.Time) {
_m.Called(_a0, _a1)
}
// overrideCfg provides a mock function with given fields: cfg
func (_m *FakeScheduleService) overrideCfg(cfg SchedulerCfg) {
_m.Called(cfg)
}
// stopApplied provides a mock function with given fields: _a0
func (_m *FakeScheduleService) stopApplied(_a0 models.AlertRuleKey) {
_m.Called(_a0)
}

View File

@ -36,8 +36,6 @@ type UpsertRule struct {
// Store is the interface for persisting alert rules and instances
type RuleStore interface {
DeleteAlertRulesByUID(ctx context.Context, orgID int64, ruleUID ...string) error
DeleteNamespaceAlertRules(ctx context.Context, orgID int64, namespaceUID string) ([]string, error)
DeleteRuleGroupAlertRules(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string) ([]string, error)
DeleteAlertInstancesByRuleUID(ctx context.Context, orgID int64, ruleUID string) error
GetAlertRuleByUID(ctx context.Context, query *ngmodels.GetAlertRuleByUIDQuery) error
GetAlertRulesForScheduling(ctx context.Context, query *ngmodels.ListAlertRulesQuery) error
@ -88,76 +86,6 @@ func (st DBstore) DeleteAlertRulesByUID(ctx context.Context, orgID int64, ruleUI
})
}
// DeleteNamespaceAlertRules is a handler for deleting namespace alert rules. A list of deleted rule UIDs are returned.
func (st DBstore) DeleteNamespaceAlertRules(ctx context.Context, orgID int64, namespaceUID string) ([]string, error) {
ruleUIDs := []string{}
err := st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
if err := sess.SQL("SELECT uid FROM alert_rule WHERE org_id = ? and namespace_uid = ?", orgID, namespaceUID).Find(&ruleUIDs); err != nil {
return err
}
if _, err := sess.Exec("DELETE FROM alert_rule WHERE org_id = ? and namespace_uid = ?", orgID, namespaceUID); err != nil {
return err
}
if _, err := sess.Exec("DELETE FROM alert_rule WHERE org_id = ? and namespace_uid = ?", orgID, namespaceUID); err != nil {
return err
}
if _, err := sess.Exec("DELETE FROM alert_rule_version WHERE rule_org_id = ? and rule_namespace_uid = ?", orgID, namespaceUID); err != nil {
return err
}
if _, err := sess.Exec(`DELETE FROM alert_instance WHERE rule_org_id = ? AND rule_uid NOT IN (
SELECT uid FROM alert_rule where org_id = ?
)`, orgID, orgID); err != nil {
return err
}
return nil
})
return ruleUIDs, err
}
// DeleteRuleGroupAlertRules is a handler for deleting rule group alert rules. A list of deleted rule UIDs are returned.
func (st DBstore) DeleteRuleGroupAlertRules(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string) ([]string, error) {
ruleUIDs := []string{}
err := st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
if err := sess.SQL("SELECT uid FROM alert_rule WHERE org_id = ? and namespace_uid = ? and rule_group = ?",
orgID, namespaceUID, ruleGroup).Find(&ruleUIDs); err != nil {
return err
}
exist, err := sess.Exist(&ngmodels.AlertRule{OrgID: orgID, NamespaceUID: namespaceUID, RuleGroup: ruleGroup})
if err != nil {
return err
}
if !exist {
return ngmodels.ErrRuleGroupNamespaceNotFound
}
if _, err := sess.Exec("DELETE FROM alert_rule WHERE org_id = ? and namespace_uid = ? and rule_group = ?", orgID, namespaceUID, ruleGroup); err != nil {
return err
}
if _, err := sess.Exec("DELETE FROM alert_rule_version WHERE rule_org_id = ? and rule_namespace_uid = ? and rule_group = ?", orgID, namespaceUID, ruleGroup); err != nil {
return err
}
if _, err := sess.Exec(`DELETE FROM alert_instance WHERE rule_org_id = ? AND rule_uid NOT IN (
SELECT uid FROM alert_rule where org_id = ?
)`, orgID, orgID); err != nil {
return err
}
return nil
})
return ruleUIDs, err
}
// DeleteAlertInstanceByRuleUID is a handler for deleting alert instances by alert rule UID when a rule has been updated
func (st DBstore) DeleteAlertInstancesByRuleUID(ctx context.Context, orgID int64, ruleUID string) error {
return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {

View File

@ -3,13 +3,16 @@ package store
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"math/rand"
"net/http"
"net/http/httptest"
"sync"
"testing"
"github.com/grafana/grafana/pkg/services/annotations"
"github.com/grafana/grafana/pkg/util"
models2 "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/ngalert/models"
@ -27,6 +30,7 @@ func NewFakeRuleStore(t *testing.T) *FakeRuleStore {
Hook: func(interface{}) error {
return nil
},
Folders: map[int64][]*models2.Folder{},
}
}
@ -38,6 +42,12 @@ type FakeRuleStore struct {
Rules map[int64][]*models.AlertRule
Hook func(cmd interface{}) error // use Hook if you need to intercept some query and return an error
RecordedOps []interface{}
Folders map[int64][]*models2.Folder
}
type GenericRecordedQuery struct {
Name string
Params []interface{}
}
// PutRule puts the rule in the Rules map. If there are existing rule in the same namespace, they will be overwritten
@ -55,6 +65,23 @@ mainloop:
}
rgs = append(rgs, r)
f.Rules[r.OrgID] = rgs
var existing *models2.Folder
folders := f.Folders[r.OrgID]
for _, folder := range folders {
if folder.Uid == r.NamespaceUID {
existing = folder
break
}
}
if existing == nil {
folders = append(folders, &models2.Folder{
Id: rand.Int63(),
Uid: r.NamespaceUID,
Title: "TEST-FOLDER-" + util.GenerateShortUID(),
})
f.Folders[r.OrgID] = folders
}
}
}
@ -74,15 +101,33 @@ func (f *FakeRuleStore) GetRecordedCommands(predicate func(cmd interface{}) (int
return result
}
func (f *FakeRuleStore) DeleteAlertRulesByUID(_ context.Context, _ int64, _ ...string) error {
func (f *FakeRuleStore) DeleteAlertRulesByUID(_ context.Context, orgID int64, UIDs ...string) error {
f.RecordedOps = append(f.RecordedOps, GenericRecordedQuery{
Name: "DeleteAlertRulesByUID",
Params: []interface{}{orgID, UIDs},
})
rules := f.Rules[orgID]
var result = make([]*models.AlertRule, 0, len(rules))
for _, rule := range rules {
add := true
for _, UID := range UIDs {
if rule.UID == UID {
add = false
break
}
}
if add {
result = append(result, rule)
}
}
f.Rules[orgID] = result
return nil
}
func (f *FakeRuleStore) DeleteNamespaceAlertRules(_ context.Context, _ int64, _ string) ([]string, error) {
return []string{}, nil
}
func (f *FakeRuleStore) DeleteRuleGroupAlertRules(_ context.Context, _ int64, _ string, _ string) ([]string, error) {
return []string{}, nil
}
func (f *FakeRuleStore) DeleteAlertInstancesByRuleUID(_ context.Context, _ int64, _ string) error {
return nil
}
@ -179,8 +224,14 @@ func (f *FakeRuleStore) GetNamespaces(_ context.Context, orgID int64, _ *models2
}
return namespacesMap, nil
}
func (f *FakeRuleStore) GetNamespaceByTitle(_ context.Context, _ string, _ int64, _ *models2.SignedInUser, _ bool) (*models2.Folder, error) {
return nil, nil
func (f *FakeRuleStore) GetNamespaceByTitle(_ context.Context, title string, orgID int64, _ *models2.SignedInUser, _ bool) (*models2.Folder, error) {
folders := f.Folders[orgID]
for _, folder := range folders {
if folder.Title == title {
return folder, nil
}
}
return nil, fmt.Errorf("not found")
}
func (f *FakeRuleStore) GetOrgRuleGroups(_ context.Context, q *models.ListOrgRuleGroupsQuery) error {
f.mtx.Lock()

View File

@ -1890,7 +1890,7 @@ func TestAlertRuleCRUD(t *testing.T) {
client := &http.Client{}
// Finally, make sure we can delete it.
{
t.Run("fail if he rule group name does not exists", func(t *testing.T) {
t.Run("succeed if the rule group name does not exists", func(t *testing.T) {
u := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default/groupnotexist", grafanaListedAddr)
req, err := http.NewRequest(http.MethodDelete, u, nil)
require.NoError(t, err)
@ -1903,8 +1903,8 @@ func TestAlertRuleCRUD(t *testing.T) {
b, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, http.StatusNotFound, resp.StatusCode)
require.JSONEq(t, `{"message": "failed to delete rule group: rule group not found under this namespace"}`, string(b))
require.Equal(t, http.StatusAccepted, resp.StatusCode)
require.JSONEq(t, `{"message":"rules deleted"}`, string(b))
})
t.Run("succeed if the rule group name does exist", func(t *testing.T) {
@ -1921,7 +1921,7 @@ func TestAlertRuleCRUD(t *testing.T) {
require.NoError(t, err)
require.Equal(t, http.StatusAccepted, resp.StatusCode)
require.JSONEq(t, `{"message":"rule group deleted"}`, string(b))
require.JSONEq(t, `{"message":"rules deleted"}`, string(b))
})
}
}