mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Fix deleting rules in a folder with matching UID in another organization (#78258)
* Remove usage of obsolete function for deleting alert rules under folder * Apply suggestion from code review * Update tests
This commit is contained in:
parent
4e2201ffeb
commit
6d4625ad52
@ -18,8 +18,7 @@ func ToFolderErrorResponse(err error) response.Response {
|
|||||||
if errors.Is(err, dashboards.ErrFolderTitleEmpty) ||
|
if errors.Is(err, dashboards.ErrFolderTitleEmpty) ||
|
||||||
errors.Is(err, dashboards.ErrDashboardTypeMismatch) ||
|
errors.Is(err, dashboards.ErrDashboardTypeMismatch) ||
|
||||||
errors.Is(err, dashboards.ErrDashboardInvalidUid) ||
|
errors.Is(err, dashboards.ErrDashboardInvalidUid) ||
|
||||||
errors.Is(err, dashboards.ErrDashboardUidTooLong) ||
|
errors.Is(err, dashboards.ErrDashboardUidTooLong) {
|
||||||
errors.Is(err, dashboards.ErrFolderContainsAlertRules) {
|
|
||||||
return response.Error(400, err.Error(), nil)
|
return response.Error(400, err.Error(), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -680,10 +680,6 @@ func (d *dashboardStore) deleteDashboard(cmd *dashboards.DeleteDashboardCommand,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := deleteFolderAlertRules(sess, dashboard, cmd.ForceDeleteFolderRules); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if err := d.deleteResourcePermissions(sess, dashboard.OrgID, ac.GetResourceScopeUID("dashboards", dashboard.UID)); err != nil {
|
if err := d.deleteResourcePermissions(sess, dashboard.OrgID, ac.GetResourceScopeUID("dashboards", dashboard.UID)); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -778,33 +774,6 @@ func (d *dashboardStore) deleteChildrenDashboardAssociations(sess *db.Session, d
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteFolderAlertRules(sess *db.Session, dashboard dashboards.Dashboard, forceDeleteFolderAlertRules bool) error {
|
|
||||||
var existingRuleID int64
|
|
||||||
exists, err := sess.Table("alert_rule").Where("namespace_uid = (SELECT uid FROM dashboard WHERE id = ?)", dashboard.ID).Cols("id").Get(&existingRuleID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if exists {
|
|
||||||
if !forceDeleteFolderAlertRules {
|
|
||||||
return fmt.Errorf("folder cannot be deleted: %w", dashboards.ErrFolderContainsAlertRules)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete all rules under this folder.
|
|
||||||
deleteNGAlertsByFolder := []string{
|
|
||||||
"DELETE FROM alert_rule WHERE namespace_uid = (SELECT uid FROM dashboard WHERE id = ?)",
|
|
||||||
"DELETE FROM alert_rule_version WHERE rule_namespace_uid = (SELECT uid FROM dashboard WHERE id = ?)",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, sql := range deleteNGAlertsByFolder {
|
|
||||||
_, err := sess.Exec(sql, dashboard.ID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func createEntityEvent(dashboard *dashboards.Dashboard, eventType store.EntityEventType) *store.EntityEvent {
|
func createEntityEvent(dashboard *dashboards.Dashboard, eventType store.EntityEventType) *store.EntityEvent {
|
||||||
var entityEvent *store.EntityEvent
|
var entityEvent *store.EntityEvent
|
||||||
if dashboard.IsFolder {
|
if dashboard.IsFolder {
|
||||||
|
@ -3,7 +3,6 @@ package database
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -265,45 +264,6 @@ func TestIntegrationDashboardDataAccess(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Should be not able to delete a dashboard if force delete rules is disabled", func(t *testing.T) {
|
|
||||||
setup()
|
|
||||||
deleteCmd := &dashboards.DeleteDashboardCommand{ID: savedFolder.ID, ForceDeleteFolderRules: false}
|
|
||||||
err := dashboardStore.DeleteDashboard(context.Background(), deleteCmd)
|
|
||||||
require.True(t, errors.Is(err, dashboards.ErrFolderContainsAlertRules))
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Should be able to delete a dashboard folder and its children if force delete rules is enabled", func(t *testing.T) {
|
|
||||||
setup()
|
|
||||||
deleteCmd := &dashboards.DeleteDashboardCommand{ID: savedFolder.ID, ForceDeleteFolderRules: true}
|
|
||||||
err := dashboardStore.DeleteDashboard(context.Background(), deleteCmd)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
query := dashboards.FindPersistedDashboardsQuery{
|
|
||||||
OrgId: 1,
|
|
||||||
FolderIds: []int64{savedFolder.ID}, // nolint:staticcheck
|
|
||||||
SignedInUser: &user.SignedInUser{},
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := dashboardStore.FindDashboards(context.Background(), &query)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, len(res), 0)
|
|
||||||
|
|
||||||
err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error {
|
|
||||||
var existingRuleID int64
|
|
||||||
exists, err := sess.Table("alert_rule").Where("namespace_uid = (SELECT uid FROM dashboard WHERE id = ?)", savedFolder.ID).Cols("id").Get(&existingRuleID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.False(t, exists)
|
|
||||||
|
|
||||||
var existingRuleVersionID int64
|
|
||||||
exists, err = sess.Table("alert_rule_version").Where("rule_namespace_uid = (SELECT uid FROM dashboard WHERE id = ?)", savedFolder.ID).Cols("id").Get(&existingRuleVersionID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.False(t, exists)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Should return error if no dashboard is found for update when dashboard id is greater than zero", func(t *testing.T) {
|
t.Run("Should return error if no dashboard is found for update when dashboard id is greater than zero", func(t *testing.T) {
|
||||||
cmd := dashboards.SaveDashboardCommand{
|
cmd := dashboards.SaveDashboardCommand{
|
||||||
OrgID: 1,
|
OrgID: 1,
|
||||||
|
@ -117,14 +117,13 @@ var (
|
|||||||
Status: "not-found",
|
Status: "not-found",
|
||||||
}
|
}
|
||||||
|
|
||||||
ErrFolderNotFound = errors.New("folder not found")
|
ErrFolderNotFound = errors.New("folder not found")
|
||||||
ErrFolderVersionMismatch = errors.New("the folder has been changed by someone else")
|
ErrFolderVersionMismatch = errors.New("the folder has been changed by someone else")
|
||||||
ErrFolderTitleEmpty = errors.New("folder title cannot be empty")
|
ErrFolderTitleEmpty = errors.New("folder title cannot be empty")
|
||||||
ErrFolderWithSameUIDExists = errors.New("a folder/dashboard with the same uid already exists")
|
ErrFolderWithSameUIDExists = errors.New("a folder/dashboard with the same uid already exists")
|
||||||
ErrFolderInvalidUID = errors.New("invalid uid for folder provided")
|
ErrFolderInvalidUID = errors.New("invalid uid for folder provided")
|
||||||
ErrFolderSameNameExists = errors.New("a folder or dashboard in the general folder with the same name already exists")
|
ErrFolderSameNameExists = errors.New("a folder or dashboard in the general folder with the same name already exists")
|
||||||
ErrFolderAccessDenied = errors.New("access denied to folder")
|
ErrFolderAccessDenied = errors.New("access denied to folder")
|
||||||
ErrFolderContainsAlertRules = errors.New("folder contains alert rules")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// DashboardErr represents a dashboard error.
|
// DashboardErr represents a dashboard error.
|
||||||
|
@ -217,7 +217,7 @@ func TestDashboardService(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("Given non provisioned dashboard", func(t *testing.T) {
|
t.Run("Given non provisioned dashboard", func(t *testing.T) {
|
||||||
t.Run("DeleteProvisionedDashboard should delete the dashboard", func(t *testing.T) {
|
t.Run("DeleteProvisionedDashboard should delete the dashboard", func(t *testing.T) {
|
||||||
args := &dashboards.DeleteDashboardCommand{OrgID: 1, ID: 1, ForceDeleteFolderRules: false}
|
args := &dashboards.DeleteDashboardCommand{OrgID: 1, ID: 1}
|
||||||
fakeStore.On("DeleteDashboard", mock.Anything, args).Return(nil).Once()
|
fakeStore.On("DeleteDashboard", mock.Anything, args).Return(nil).Once()
|
||||||
err := service.DeleteProvisionedDashboard(context.Background(), 1, 1)
|
err := service.DeleteProvisionedDashboard(context.Background(), 1, 1)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -642,6 +642,19 @@ func (s *Service) Delete(ctx context.Context, cmd *folder.DeleteFolderCommand) e
|
|||||||
if err := s.deleteChildrenInFolder(ctx, dashFolder.OrgID, dashFolder.UID, cmd.SignedInUser); err != nil {
|
if err := s.deleteChildrenInFolder(ctx, dashFolder.OrgID, dashFolder.UID, cmd.SignedInUser); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
alertRuleSrv, ok := s.registry[entity.StandardKindAlertRule]
|
||||||
|
if !ok {
|
||||||
|
return folder.ErrInternal.Errorf("no alert rule service found in registry")
|
||||||
|
}
|
||||||
|
alertRulesInFolder, err := alertRuleSrv.CountInFolder(ctx, dashFolder.OrgID, dashFolder.UID, cmd.SignedInUser)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error("failed to count alert rules in folder", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if alertRulesInFolder > 0 {
|
||||||
|
return folder.ErrFolderNotEmpty.Errorf("folder contains %d alert rules", alertRulesInFolder)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = s.legacyDelete(ctx, cmd, dashFolder); err != nil {
|
if err = s.legacyDelete(ctx, cmd, dashFolder); err != nil {
|
||||||
|
@ -74,6 +74,16 @@ func TestIntegrationFolderService(t *testing.T) {
|
|||||||
cfg := setting.NewCfg()
|
cfg := setting.NewCfg()
|
||||||
features := featuremgmt.WithFeatures()
|
features := featuremgmt.WithFeatures()
|
||||||
|
|
||||||
|
ac := acmock.New().WithPermissions([]accesscontrol.Permission{
|
||||||
|
{Action: accesscontrol.ActionAlertingRuleDelete, Scope: dashboards.ScopeFoldersAll},
|
||||||
|
})
|
||||||
|
alertingStore := ngstore.DBstore{
|
||||||
|
SQLStore: db,
|
||||||
|
Cfg: cfg.UnifiedAlerting,
|
||||||
|
Logger: log.New("test-alerting-store"),
|
||||||
|
AccessControl: ac,
|
||||||
|
}
|
||||||
|
|
||||||
service := &Service{
|
service := &Service{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
log: log.New("test-folder-service"),
|
log: log.New("test-folder-service"),
|
||||||
@ -84,8 +94,11 @@ func TestIntegrationFolderService(t *testing.T) {
|
|||||||
bus: bus.ProvideBus(tracing.InitializeTracerForTest()),
|
bus: bus.ProvideBus(tracing.InitializeTracerForTest()),
|
||||||
db: db,
|
db: db,
|
||||||
accessControl: acimpl.ProvideAccessControl(cfg),
|
accessControl: acimpl.ProvideAccessControl(cfg),
|
||||||
|
registry: make(map[string]folder.RegistryService),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
require.NoError(t, service.RegisterService(alertingStore))
|
||||||
|
|
||||||
t.Run("Given user has no permissions", func(t *testing.T) {
|
t.Run("Given user has no permissions", func(t *testing.T) {
|
||||||
origNewGuardian := guardian.New
|
origNewGuardian := guardian.New
|
||||||
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{})
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{})
|
||||||
@ -382,8 +395,10 @@ func TestIntegrationNestedFolderService(t *testing.T) {
|
|||||||
|
|
||||||
signedInUser := user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{
|
signedInUser := user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{
|
||||||
orgID: {
|
orgID: {
|
||||||
dashboards.ActionFoldersCreate: {},
|
dashboards.ActionFoldersCreate: {},
|
||||||
dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll}},
|
dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll},
|
||||||
|
accesscontrol.ActionAlertingRuleDelete: {dashboards.ScopeFoldersAll},
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
createCmd := folder.CreateFolderCommand{
|
createCmd := folder.CreateFolderCommand{
|
||||||
OrgID: orgID,
|
OrgID: orgID,
|
||||||
@ -422,7 +437,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
|
|||||||
dashSrv, err := service.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, nil, featuresFlagOn, folderPermissions, dashboardPermissions, ac, serviceWithFlagOn)
|
dashSrv, err := service.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, nil, featuresFlagOn, folderPermissions, dashboardPermissions, ac, serviceWithFlagOn)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOn, db, serviceWithFlagOn, dashSrv)
|
alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOn, db, serviceWithFlagOn, dashSrv, ac)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
elementService := libraryelements.ProvideService(cfg, db, routeRegister, serviceWithFlagOn, featuresFlagOn, ac)
|
elementService := libraryelements.ProvideService(cfg, db, routeRegister, serviceWithFlagOn, featuresFlagOn, ac)
|
||||||
@ -501,7 +516,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
|
|||||||
folderPermissions, dashboardPermissions, ac, serviceWithFlagOff)
|
folderPermissions, dashboardPermissions, ac, serviceWithFlagOff)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOff, db, serviceWithFlagOff, dashSrv)
|
alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOff, db, serviceWithFlagOff, dashSrv, ac)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
elementService := libraryelements.ProvideService(cfg, db, routeRegister, serviceWithFlagOff, featuresFlagOff, ac)
|
elementService := libraryelements.ProvideService(cfg, db, routeRegister, serviceWithFlagOff, featuresFlagOff, ac)
|
||||||
@ -595,7 +610,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
|
|||||||
prefix: "flagon-noforce",
|
prefix: "flagon-noforce",
|
||||||
depth: 3,
|
depth: 3,
|
||||||
forceDelete: false,
|
forceDelete: false,
|
||||||
deletionErr: dashboards.ErrFolderContainsAlertRules,
|
deletionErr: folder.ErrFolderNotEmpty,
|
||||||
desc: "With nested folder feature flag on and no force deletion of rules",
|
desc: "With nested folder feature flag on and no force deletion of rules",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -615,7 +630,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
|
|||||||
prefix: "flagoff-noforce",
|
prefix: "flagoff-noforce",
|
||||||
depth: 1,
|
depth: 1,
|
||||||
forceDelete: false,
|
forceDelete: false,
|
||||||
deletionErr: dashboards.ErrFolderContainsAlertRules,
|
deletionErr: folder.ErrFolderNotEmpty,
|
||||||
desc: "With nested folder feature flag off and no force deletion of rules",
|
desc: "With nested folder feature flag off and no force deletion of rules",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -642,7 +657,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
|
|||||||
|
|
||||||
dashSrv, err := service.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, nil, tc.featuresFlag, folderPermissions, dashboardPermissions, ac, tc.service)
|
dashSrv, err := service.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, nil, tc.featuresFlag, folderPermissions, dashboardPermissions, ac, tc.service)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
alertStore, err := ngstore.ProvideDBStore(cfg, tc.featuresFlag, db, tc.service, dashSrv)
|
alertStore, err := ngstore.ProvideDBStore(cfg, tc.featuresFlag, db, tc.service, dashSrv, ac)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, tc.depth, tc.prefix, createCmd)
|
ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, tc.depth, tc.prefix, createCmd)
|
||||||
|
@ -16,6 +16,7 @@ var ErrDatabaseError = errutil.Internal("folder.database-error")
|
|||||||
var ErrInternal = errutil.Internal("folder.internal")
|
var ErrInternal = errutil.Internal("folder.internal")
|
||||||
var ErrCircularReference = errutil.BadRequest("folder.circular-reference", errutil.WithPublicMessage("Circular reference detected"))
|
var ErrCircularReference = errutil.BadRequest("folder.circular-reference", errutil.WithPublicMessage("Circular reference detected"))
|
||||||
var ErrTargetRegistrySrvConflict = errutil.Internal("folder.target-registry-srv-conflict")
|
var ErrTargetRegistrySrvConflict = errutil.Internal("folder.target-registry-srv-conflict")
|
||||||
|
var ErrFolderNotEmpty = errutil.BadRequest("folder.not-empty", errutil.WithPublicMessage("Folder cannot be deleted: folder is not empty"))
|
||||||
|
|
||||||
const (
|
const (
|
||||||
GeneralFolderUID = "general"
|
GeneralFolderUID = "general"
|
||||||
|
@ -69,6 +69,9 @@ func NewTestMigrationStore(t testing.TB, sqlStore *sqlstore.SQLStore, cfg *setti
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
folderService := folderimpl.ProvideService(ac, bus, cfg, dashboardStore, folderStore, sqlStore, features)
|
folderService := folderimpl.ProvideService(ac, bus, cfg, dashboardStore, folderStore, sqlStore, features)
|
||||||
|
|
||||||
|
err = folderService.RegisterService(alertingStore)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
folderPermissions, err := ossaccesscontrol.ProvideFolderPermissions(
|
folderPermissions, err := ossaccesscontrol.ProvideFolderPermissions(
|
||||||
features, routeRegister, sqlStore, ac, license, dashboardStore, folderService, acSvc, teamSvc, userSvc)
|
features, routeRegister, sqlStore, ac, license, dashboardStore, folderService, acSvc, teamSvc, userSvc)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
"github.com/grafana/grafana/pkg/infra/db"
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/auth/identity"
|
"github.com/grafana/grafana/pkg/services/auth/identity"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
|
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
|
||||||
@ -584,6 +585,17 @@ func (st DBstore) GetAlertRulesForScheduling(ctx context.Context, query *ngmodel
|
|||||||
|
|
||||||
// DeleteInFolder deletes the rules contained in a given folder along with their associated data.
|
// DeleteInFolder deletes the rules contained in a given folder along with their associated data.
|
||||||
func (st DBstore) DeleteInFolder(ctx context.Context, orgID int64, folderUID string, user identity.Requester) error {
|
func (st DBstore) DeleteInFolder(ctx context.Context, orgID int64, folderUID string, user identity.Requester) error {
|
||||||
|
evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAlertingRuleDelete, dashboards.ScopeFoldersProvider.GetResourceScopeName(folderUID))
|
||||||
|
canSave, err := st.AccessControl.Evaluate(ctx, user, evaluator)
|
||||||
|
if err != nil {
|
||||||
|
st.Logger.Error("Failed to evaluate access control", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !canSave {
|
||||||
|
st.Logger.Error("user is not allowed to delete alert rules in folder", "folder", folderUID, "user")
|
||||||
|
return dashboards.ErrFolderAccessDenied
|
||||||
|
}
|
||||||
|
|
||||||
rules, err := st.ListAlertRules(ctx, &ngmodels.ListAlertRulesQuery{
|
rules, err := st.ListAlertRules(ctx, &ngmodels.ListAlertRulesQuery{
|
||||||
OrgID: orgID,
|
OrgID: orgID,
|
||||||
NamespaceUIDs: []string{folderUID},
|
NamespaceUIDs: []string{folderUID},
|
||||||
|
@ -14,6 +14,8 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/infra/log/logtest"
|
"github.com/grafana/grafana/pkg/infra/log/logtest"
|
||||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/folder"
|
"github.com/grafana/grafana/pkg/services/folder"
|
||||||
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
|
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
|
||||||
@ -26,6 +28,7 @@ import (
|
|||||||
"golang.org/x/exp/rand"
|
"golang.org/x/exp/rand"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
"github.com/grafana/grafana/pkg/infra/db"
|
||||||
|
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
@ -476,12 +479,23 @@ func TestIntegration_DeleteInFolder(t *testing.T) {
|
|||||||
}
|
}
|
||||||
rule := createRule(t, store, nil)
|
rule := createRule(t, store, nil)
|
||||||
|
|
||||||
err := store.DeleteInFolder(context.Background(), rule.OrgID, rule.NamespaceUID, nil)
|
t.Run("should not be able to delete folder without permissions to delete rules", func(t *testing.T) {
|
||||||
require.NoError(t, err)
|
store.AccessControl = acmock.New()
|
||||||
|
err := store.DeleteInFolder(context.Background(), rule.OrgID, rule.NamespaceUID, &user.SignedInUser{})
|
||||||
|
require.ErrorIs(t, err, dashboards.ErrFolderAccessDenied)
|
||||||
|
})
|
||||||
|
|
||||||
c, err := store.CountInFolder(context.Background(), rule.OrgID, rule.NamespaceUID, nil)
|
t.Run("should be able to delete folder with permissions to delete rules", func(t *testing.T) {
|
||||||
require.NoError(t, err)
|
store.AccessControl = acmock.New().WithPermissions([]accesscontrol.Permission{
|
||||||
require.Equal(t, int64(0), c)
|
{Action: accesscontrol.ActionAlertingRuleDelete, Scope: dashboards.ScopeFoldersAll},
|
||||||
|
})
|
||||||
|
err := store.DeleteInFolder(context.Background(), rule.OrgID, rule.NamespaceUID, &user.SignedInUser{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
c, err := store.CountInFolder(context.Background(), rule.OrgID, rule.NamespaceUID, &user.SignedInUser{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, int64(0), c)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIntegration_GetNamespaceByUID(t *testing.T) {
|
func TestIntegration_GetNamespaceByUID(t *testing.T) {
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
"github.com/grafana/grafana/pkg/infra/db"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/folder"
|
"github.com/grafana/grafana/pkg/services/folder"
|
||||||
@ -39,10 +40,11 @@ type DBstore struct {
|
|||||||
Logger log.Logger
|
Logger log.Logger
|
||||||
FolderService folder.Service
|
FolderService folder.Service
|
||||||
DashboardService dashboards.DashboardService
|
DashboardService dashboards.DashboardService
|
||||||
|
AccessControl accesscontrol.AccessControl
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProvideDBStore(
|
func ProvideDBStore(
|
||||||
cfg *setting.Cfg, featureToggles featuremgmt.FeatureToggles, sqlstore db.DB, folderService folder.Service, dashboards dashboards.DashboardService) (*DBstore, error) {
|
cfg *setting.Cfg, featureToggles featuremgmt.FeatureToggles, sqlstore db.DB, folderService folder.Service, dashboards dashboards.DashboardService, ac accesscontrol.AccessControl) (*DBstore, error) {
|
||||||
store := DBstore{
|
store := DBstore{
|
||||||
Cfg: cfg.UnifiedAlerting,
|
Cfg: cfg.UnifiedAlerting,
|
||||||
FeatureToggles: featureToggles,
|
FeatureToggles: featureToggles,
|
||||||
@ -50,6 +52,7 @@ func ProvideDBStore(
|
|||||||
Logger: log.New("ngalert.dbstore"),
|
Logger: log.New("ngalert.dbstore"),
|
||||||
FolderService: folderService,
|
FolderService: folderService,
|
||||||
DashboardService: dashboards,
|
DashboardService: dashboards,
|
||||||
|
AccessControl: ac,
|
||||||
}
|
}
|
||||||
if err := folderService.RegisterService(store); err != nil {
|
if err := folderService.RegisterService(store); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -62,7 +62,7 @@ func SetupTestEnv(tb testing.TB, baseInterval time.Duration) (*ngalert.AlertNG,
|
|||||||
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
|
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
|
||||||
dashboardService, dashboardStore := testutil.SetupDashboardService(tb, sqlStore, folderStore, cfg)
|
dashboardService, dashboardStore := testutil.SetupDashboardService(tb, sqlStore, folderStore, cfg)
|
||||||
folderService := testutil.SetupFolderService(tb, cfg, sqlStore, dashboardStore, folderStore, bus)
|
folderService := testutil.SetupFolderService(tb, cfg, sqlStore, dashboardStore, folderStore, bus)
|
||||||
ruleStore, err := store.ProvideDBStore(cfg, featuremgmt.WithFeatures(), sqlStore, folderService, &dashboards.FakeDashboardService{})
|
ruleStore, err := store.ProvideDBStore(cfg, featuremgmt.WithFeatures(), sqlStore, folderService, &dashboards.FakeDashboardService{}, ac)
|
||||||
require.NoError(tb, err)
|
require.NoError(tb, err)
|
||||||
ng, err := ngalert.ProvideService(
|
ng, err := ngalert.ProvideService(
|
||||||
cfg, featuremgmt.WithFeatures(), nil, nil, routing.NewRouteRegister(), sqlStore, nil, nil, nil, quotatest.New(false, nil),
|
cfg, featuremgmt.WithFeatures(), nil, nil, routing.NewRouteRegister(), sqlStore, nil, nil, nil, quotatest.New(false, nil),
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||||
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||||
"github.com/grafana/grafana/pkg/services/annotations/annotationstest"
|
"github.com/grafana/grafana/pkg/services/annotations/annotationstest"
|
||||||
"github.com/grafana/grafana/pkg/services/apikey"
|
"github.com/grafana/grafana/pkg/services/apikey"
|
||||||
@ -480,7 +481,9 @@ func setupEnv(t *testing.T, sqlStore *sqlstore.SQLStore, b bus.Bus, quotaService
|
|||||||
_, err = dsservice.ProvideService(sqlStore, secretsService, secretsStore, sqlStore.Cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{})
|
_, err = dsservice.ProvideService(sqlStore, secretsService, secretsStore, sqlStore.Cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
m := metrics.NewNGAlert(prometheus.NewRegistry())
|
m := metrics.NewNGAlert(prometheus.NewRegistry())
|
||||||
ruleStore, err := ngstore.ProvideDBStore(sqlStore.Cfg, featuremgmt.WithFeatures(), sqlStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{})
|
|
||||||
|
ac := acimpl.ProvideAccessControl(sqlStore.Cfg)
|
||||||
|
ruleStore, err := ngstore.ProvideDBStore(sqlStore.Cfg, featuremgmt.WithFeatures(), sqlStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_, err = ngalert.ProvideService(
|
_, err = ngalert.ProvideService(
|
||||||
sqlStore.Cfg, featuremgmt.WithFeatures(), nil, nil, routing.NewRouteRegister(), sqlStore, nil, nil, nil, quotaService,
|
sqlStore.Cfg, featuremgmt.WithFeatures(), nil, nil, routing.NewRouteRegister(), sqlStore, nil, nil, nil, quotaService,
|
||||||
|
@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/expr"
|
"github.com/grafana/grafana/pkg/expr"
|
||||||
|
"github.com/grafana/grafana/pkg/util/errutil"
|
||||||
|
|
||||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
@ -799,7 +800,10 @@ func TestIntegrationDeleteFolderWithRules(t *testing.T) {
|
|||||||
b, err := io.ReadAll(resp.Body)
|
b, err := io.ReadAll(resp.Body)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||||
require.JSONEq(t, `{"message":"folder cannot be deleted: folder contains alert rules"}`, string(b))
|
var errutilErr errutil.PublicError
|
||||||
|
err = json.Unmarshal(b, &errutilErr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "Folder cannot be deleted: folder is not empty", errutilErr.Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next, the editor can delete the folder if forceDeleteRules is true.
|
// Next, the editor can delete the folder if forceDeleteRules is true.
|
||||||
|
Loading…
Reference in New Issue
Block a user