diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index ee9ef7347ea..2de65e70e0b 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -1359,7 +1359,7 @@ func (l *mockLibraryElementService) CreateElement(c context.Context, signedInUse } // GetElement gets an element from a UID. -func (l *mockLibraryElementService) GetElement(c context.Context, signedInUser *user.SignedInUser, UID string) (model.LibraryElementDTO, error) { +func (l *mockLibraryElementService) GetElement(c context.Context, signedInUser *user.SignedInUser, cmd model.GetLibraryElementCommand) (model.LibraryElementDTO, error) { return model.LibraryElementDTO{}, nil } diff --git a/pkg/api/folder.go b/pkg/api/folder.go index 7c347dec3a5..211b1cb7e18 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -281,6 +281,12 @@ func (hs *HTTPServer) DeleteFolder(c *contextmodel.ReqContext) response.Response } return apierrors.ToFolderErrorResponse(err) } + /* TODO: after a decision regarding folder deletion permissions has been made + (https://github.com/grafana/grafana-enterprise/issues/5144), + remove the previous call to hs.LibraryElementService.DeleteLibraryElementsInFolder + and remove "user" from the signature of DeleteInFolder in the folder RegistryService. + Context: https://github.com/grafana/grafana/pull/69149#discussion_r1235057903 + */ uid := web.Params(c.Req)[":uid"] err = hs.folderService.Delete(c.Req.Context(), &folder.DeleteFolderCommand{UID: uid, OrgID: c.OrgID, ForceDeleteRules: c.QueryBool("forceDeleteRules"), SignedInUser: c.SignedInUser}) diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 139201b6c5c..8c1c29d7142 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -635,7 +635,7 @@ func (dr DashboardServiceImpl) CountInFolder(ctx context.Context, orgID int64, f return dr.dashboardStore.CountDashboardsInFolder(ctx, &dashboards.CountDashboardsInFolderRequest{FolderID: folder.ID, OrgID: orgID}) } -func (dr *DashboardServiceImpl) DeleteInFolder(ctx context.Context, orgID int64, folderUID string) error { +func (dr *DashboardServiceImpl) DeleteInFolder(ctx context.Context, orgID int64, folderUID string, u *user.SignedInUser) error { return dr.dashboardStore.DeleteDashboardsInFolder(ctx, &dashboards.DeleteDashboardsInFolderRequest{FolderUID: folderUID, OrgID: orgID}) } diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go index 6c435914484..5a907f3716e 100644 --- a/pkg/services/dashboards/service/dashboard_service_test.go +++ b/pkg/services/dashboards/service/dashboard_service_test.go @@ -244,7 +244,7 @@ func TestDashboardService(t *testing.T) { t.Run("Delete dashboards in folder", func(t *testing.T) { args := &dashboards.DeleteDashboardsInFolderRequest{OrgID: 1, FolderUID: "uid"} fakeStore.On("DeleteDashboardsInFolder", mock.Anything, args).Return(nil).Once() - err := service.DeleteInFolder(context.Background(), 1, "uid") + err := service.DeleteInFolder(context.Background(), 1, "uid", nil) require.NoError(t, err) }) }) diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index 1b7a1c55a96..42f4e89b766 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -509,7 +509,7 @@ func (s *Service) Delete(ctx context.Context, cmd *folder.DeleteFolderCommand) e } if cmd.ForceDeleteRules { - if err := s.deleteChildrenInFolder(ctx, dashFolder.OrgID, dashFolder.UID); err != nil { + if err := s.deleteChildrenInFolder(ctx, dashFolder.OrgID, dashFolder.UID, cmd.SignedInUser); err != nil { return err } } @@ -525,9 +525,9 @@ func (s *Service) Delete(ctx context.Context, cmd *folder.DeleteFolderCommand) e return err } -func (s *Service) deleteChildrenInFolder(ctx context.Context, orgID int64, folderUID string) error { +func (s *Service) deleteChildrenInFolder(ctx context.Context, orgID int64, folderUID string, user *user.SignedInUser) error { for _, v := range s.registry { - if err := v.DeleteInFolder(ctx, orgID, folderUID); err != nil { + if err := v.DeleteInFolder(ctx, orgID, folderUID, user); err != nil { return err } } diff --git a/pkg/services/folder/folderimpl/folder_test.go b/pkg/services/folder/folderimpl/folder_test.go index ecbd4f0fa5f..36358ee1e59 100644 --- a/pkg/services/folder/folderimpl/folder_test.go +++ b/pkg/services/folder/folderimpl/folder_test.go @@ -12,6 +12,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db/dbtest" @@ -28,10 +29,14 @@ import ( "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/services/guardian" + "github.com/grafana/grafana/pkg/services/libraryelements" + "github.com/grafana/grafana/pkg/services/libraryelements/model" + "github.com/grafana/grafana/pkg/services/librarypanels" "github.com/grafana/grafana/pkg/services/ngalert/models" ngstore "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -356,7 +361,9 @@ func TestIntegrationNestedFolderService(t *testing.T) { } signedInUser := user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{ - orgID: {dashboards.ActionFoldersCreate: {}, dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll}}, + orgID: { + dashboards.ActionFoldersCreate: {}, + dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll}}, }} createCmd := folder.CreateFolderCommand{ OrgID: orgID, @@ -364,6 +371,20 @@ func TestIntegrationNestedFolderService(t *testing.T) { SignedInUser: &signedInUser, } + libraryElementCmd := model.CreateLibraryElementCommand{ + Model: []byte(` + { + "datasource": "${DS_GDEV-TESTDATA}", + "id": 1, + "title": "Text - Library Panel", + "type": "text", + "description": "A description" + } + `), + Kind: int64(model.PanelElement), + } + routeRegister := routing.NewRouteRegister() + folderPermissions := acmock.NewMockedPermissionsService() dashboardPermissions := acmock.NewMockedPermissionsService() @@ -371,7 +392,12 @@ func TestIntegrationNestedFolderService(t *testing.T) { depth := 5 t.Run("With nested folder feature flag on", func(t *testing.T) { origNewGuardian := guardian.New - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true}) + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ + CanSaveValue: true, + CanViewValue: true, + // CanEditValue is required to create library elements + CanEditValue: true, + }) dashSrv, err := service.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, nil, featuresFlagOn, folderPermissions, dashboardPermissions, ac, serviceWithFlagOn) require.NoError(t, err) @@ -379,6 +405,10 @@ func TestIntegrationNestedFolderService(t *testing.T) { alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOn, db, serviceWithFlagOn, ac, dashSrv) require.NoError(t, err) + elementService := libraryelements.ProvideService(cfg, db, routeRegister, serviceWithFlagOn, featuresFlagOn) + lps, err := librarypanels.ProvideService(cfg, db, routeRegister, elementService, serviceWithFlagOn) + require.NoError(t, err) + ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "getDescendantCountsOn", createCmd) parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[0]) @@ -390,6 +420,13 @@ func TestIntegrationNestedFolderService(t *testing.T) { _ = createRule(t, alertStore, parent.UID, "parent alert") _ = createRule(t, alertStore, subfolder.UID, "sub alert") + libraryElementCmd.FolderID = parent.ID + _, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd) + require.NoError(t, err) + libraryElementCmd.FolderID = subfolder.ID + _, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd) + require.NoError(t, err) + countCmd := folder.GetDescendantCountsQuery{ UID: &ancestorUIDs[0], OrgID: orgID, @@ -397,9 +434,10 @@ func TestIntegrationNestedFolderService(t *testing.T) { } m, err := serviceWithFlagOn.GetDescendantCounts(context.Background(), &countCmd) require.NoError(t, err) - require.Equal(t, int64(depth-1), m["folder"]) - require.Equal(t, int64(2), m["dashboard"]) - require.Equal(t, int64(2), m["alertrule"]) + require.Equal(t, int64(depth-1), m[entity.StandardKindFolder]) + require.Equal(t, int64(2), m[entity.StandardKindDashboard]) + require.Equal(t, int64(2), m[entity.StandardKindAlertRule]) + require.Equal(t, int64(2), m[entity.StandardKindLibraryPanel]) t.Cleanup(func() { guardian.New = origNewGuardian @@ -428,7 +466,12 @@ func TestIntegrationNestedFolderService(t *testing.T) { } origNewGuardian := guardian.New - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true}) + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ + CanSaveValue: true, + CanViewValue: true, + // CanEditValue is required to create library elements + CanEditValue: true, + }) dashSrv, err := service.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, nil, featuresFlagOff, folderPermissions, dashboardPermissions, ac, serviceWithFlagOff) @@ -437,6 +480,10 @@ func TestIntegrationNestedFolderService(t *testing.T) { alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOff, db, serviceWithFlagOff, ac, dashSrv) require.NoError(t, err) + elementService := libraryelements.ProvideService(cfg, db, routeRegister, serviceWithFlagOff, featuresFlagOff) + lps, err := librarypanels.ProvideService(cfg, db, routeRegister, elementService, serviceWithFlagOff) + require.NoError(t, err) + ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "getDescendantCountsOff", createCmd) parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[0]) @@ -448,6 +495,13 @@ func TestIntegrationNestedFolderService(t *testing.T) { _ = createRule(t, alertStore, parent.UID, "parent alert") _ = createRule(t, alertStore, subfolder.UID, "sub alert") + libraryElementCmd.FolderID = parent.ID + _, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd) + require.NoError(t, err) + libraryElementCmd.FolderID = subfolder.ID + _, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd) + require.NoError(t, err) + countCmd := folder.GetDescendantCountsQuery{ UID: &ancestorUIDs[0], OrgID: orgID, @@ -455,9 +509,10 @@ func TestIntegrationNestedFolderService(t *testing.T) { } m, err := serviceWithFlagOff.GetDescendantCounts(context.Background(), &countCmd) require.NoError(t, err) - require.Equal(t, int64(0), m["folder"]) - require.Equal(t, int64(1), m["dashboard"]) - require.Equal(t, int64(1), m["alertrule"]) + require.Equal(t, int64(0), m[entity.StandardKindFolder]) + require.Equal(t, int64(1), m[entity.StandardKindDashboard]) + require.Equal(t, int64(1), m[entity.StandardKindAlertRule]) + require.Equal(t, int64(1), m[entity.StandardKindLibraryPanel]) t.Cleanup(func() { guardian.New = origNewGuardian @@ -470,169 +525,158 @@ func TestIntegrationNestedFolderService(t *testing.T) { }) t.Run("Should delete folders", func(t *testing.T) { - t.Run("With nested folder feature flag on", func(t *testing.T) { - dashSrv, err := service.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, nil, featuresFlagOn, folderPermissions, dashboardPermissions, ac, serviceWithFlagOn) - require.NoError(t, err) + featuresFlagOff := featuremgmt.WithFeatures() + serviceWithFlagOff := &Service{ + cfg: cfg, + log: log.New("test-folder-service"), + dashboardFolderStore: folderStore, + features: featuresFlagOff, + bus: b, + db: db, + registry: make(map[string]folder.RegistryService), + } - alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOn, db, serviceWithFlagOn, ac, dashSrv) - require.NoError(t, err) - t.Run("With force deletion of rules", func(t *testing.T) { + testCases := []struct { + service *Service + featuresFlag *featuremgmt.FeatureManager + prefix string + depth int + forceDelete bool + deletionErr error + dashboardErr error + folderErr error + libPanelParentErr error + libPanelSubErr error + desc string + }{ + { + service: serviceWithFlagOn, + featuresFlag: featuresFlagOn, + prefix: "flagon-force", + depth: 3, + forceDelete: true, + dashboardErr: dashboards.ErrFolderNotFound, + folderErr: folder.ErrFolderNotFound, + libPanelParentErr: model.ErrLibraryElementNotFound, + libPanelSubErr: model.ErrLibraryElementNotFound, + desc: "With nested folder feature flag on and force deletion of rules", + }, + { + service: serviceWithFlagOn, + featuresFlag: featuresFlagOn, + prefix: "flagon-noforce", + depth: 3, + forceDelete: false, + deletionErr: dashboards.ErrFolderContainsAlertRules, + desc: "With nested folder feature flag on and no force deletion of rules", + }, + { + service: serviceWithFlagOff, + featuresFlag: featuresFlagOff, + prefix: "flagoff-force", + depth: 1, + forceDelete: true, + dashboardErr: dashboards.ErrFolderNotFound, + libPanelParentErr: model.ErrLibraryElementNotFound, + desc: "With nested folder feature flag off and force deletion of rules", + }, + { + service: serviceWithFlagOff, + featuresFlag: featuresFlagOff, + prefix: "flagoff-noforce", + depth: 1, + forceDelete: false, + deletionErr: dashboards.ErrFolderContainsAlertRules, + desc: "With nested folder feature flag off and no force deletion of rules", + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { origNewGuardian := guardian.New - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true}) + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ + CanSaveValue: true, + CanViewValue: true, + // CanEditValue is required to create library elements + CanEditValue: true, + }) - ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, 3, "with-force", createCmd) + elementService := libraryelements.ProvideService(cfg, db, routeRegister, tc.service, tc.featuresFlag) + lps, err := librarypanels.ProvideService(cfg, db, routeRegister, elementService, tc.service) + require.NoError(t, err) + + dashStore, err := database.ProvideDashboardStore(db, db.Cfg, tc.featuresFlag, tagimpl.ProvideService(db, db.Cfg), quotaService) + require.NoError(t, err) + nestedFolderStore := ProvideStore(db, db.Cfg, tc.featuresFlag) + tc.service.dashboardStore = dashStore + tc.service.store = nestedFolderStore + + dashSrv, err := service.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, nil, tc.featuresFlag, folderPermissions, dashboardPermissions, ac, tc.service) + require.NoError(t, err) + alertStore, err := ngstore.ProvideDBStore(cfg, tc.featuresFlag, db, tc.service, ac, dashSrv) + require.NoError(t, err) + + ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, tc.depth, tc.prefix, createCmd) parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[0]) require.NoError(t, err) - subfolder, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[1]) - require.NoError(t, err) _ = createRule(t, alertStore, parent.UID, "parent alert") - _ = createRule(t, alertStore, subfolder.UID, "sub alert") + + var ( + subfolder *folder.Folder + subPanel model.LibraryElementDTO + ) + if tc.depth > 1 { + subfolder, err = serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[1]) + require.NoError(t, err) + _ = createRule(t, alertStore, subfolder.UID, "sub alert") + libraryElementCmd.FolderID = subfolder.ID + subPanel, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd) + require.NoError(t, err) + } + + libraryElementCmd.FolderID = parent.ID + parentPanel, err := lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd) + require.NoError(t, err) deleteCmd := folder.DeleteFolderCommand{ UID: ancestorUIDs[0], OrgID: orgID, SignedInUser: &signedInUser, - ForceDeleteRules: true, + ForceDeleteRules: tc.forceDelete, } - err = serviceWithFlagOn.Delete(context.Background(), &deleteCmd) - require.NoError(t, err) + + err = tc.service.Delete(context.Background(), &deleteCmd) + require.ErrorIs(t, err, tc.deletionErr) for i, uid := range ancestorUIDs { // dashboard table - _, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, uid) - require.ErrorIs(t, err, dashboards.ErrFolderNotFound) + _, err := tc.service.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, uid) + require.ErrorIs(t, err, tc.dashboardErr) // folder table - _, err = serviceWithFlagOn.store.Get(context.Background(), folder.GetFolderQuery{UID: &ancestorUIDs[i], OrgID: orgID}) - require.ErrorIs(t, err, folder.ErrFolderNotFound) + _, err = tc.service.store.Get(context.Background(), folder.GetFolderQuery{UID: &ancestorUIDs[i], OrgID: orgID}) + require.ErrorIs(t, err, tc.folderErr) + } + + _, err = lps.LibraryElementService.GetElement(context.Background(), &signedInUser, model.GetLibraryElementCommand{ + FolderName: parent.Title, + FolderID: parent.ID, + UID: parentPanel.UID, + }) + require.ErrorIs(t, err, tc.libPanelParentErr) + if tc.depth > 1 { + _, err = lps.LibraryElementService.GetElement(context.Background(), &signedInUser, model.GetLibraryElementCommand{ + FolderName: subfolder.Title, + FolderID: subfolder.ID, + UID: subPanel.UID, + }) + require.ErrorIs(t, err, tc.libPanelSubErr) } t.Cleanup(func() { guardian.New = origNewGuardian }) }) - t.Run("Without force deletion of rules", func(t *testing.T) { - origNewGuardian := guardian.New - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true}) - - ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, 3, "without-force", createCmd) - - parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[0]) - require.NoError(t, err) - subfolder, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[1]) - require.NoError(t, err) - _ = createRule(t, alertStore, parent.UID, "parent alert") - _ = createRule(t, alertStore, subfolder.UID, "sub alert") - - deleteCmd := folder.DeleteFolderCommand{ - UID: ancestorUIDs[0], - OrgID: orgID, - SignedInUser: &signedInUser, - ForceDeleteRules: false, - } - err = serviceWithFlagOn.Delete(context.Background(), &deleteCmd) - require.Error(t, dashboards.ErrFolderContainsAlertRules, err) - - for i, uid := range ancestorUIDs { - // dashboard table - _, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, uid) - require.NoError(t, err) - // folder table - _, err = serviceWithFlagOn.store.Get(context.Background(), folder.GetFolderQuery{UID: &ancestorUIDs[i], OrgID: orgID}) - require.NoError(t, err) - } - t.Cleanup(func() { - guardian.New = origNewGuardian - }) - }) - }) - t.Run("With nested folder feature flag off", func(t *testing.T) { - featuresFlagOff := featuremgmt.WithFeatures() - dashStore, err := database.ProvideDashboardStore(db, db.Cfg, featuresFlagOff, tagimpl.ProvideService(db, db.Cfg), quotaService) - require.NoError(t, err) - nestedFolderStore := ProvideStore(db, db.Cfg, featuresFlagOff) - - dashSrv, err := service.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, nil, featuresFlagOff, folderPermissions, dashboardPermissions, ac, serviceWithFlagOn) - require.NoError(t, err) - alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOff, db, serviceWithFlagOn, ac, dashSrv) - require.NoError(t, err) - - serviceWithFlagOff := &Service{ - cfg: cfg, - log: log.New("test-folder-service"), - dashboardStore: dashStore, - dashboardFolderStore: folderStore, - store: nestedFolderStore, - features: featuresFlagOff, - bus: b, - db: db, - } - t.Run("With force deletion of rules", func(t *testing.T) { - origNewGuardian := guardian.New - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true}) - - ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, 1, "off-force", createCmd) - - parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[0]) - require.NoError(t, err) - _ = createRule(t, alertStore, parent.UID, "parent alert") - - deleteCmd := folder.DeleteFolderCommand{ - UID: ancestorUIDs[0], - OrgID: orgID, - SignedInUser: &signedInUser, - ForceDeleteRules: true, - } - err = serviceWithFlagOff.Delete(context.Background(), &deleteCmd) - require.NoError(t, err) - - // dashboard table - _, err = serviceWithFlagOff.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[0]) - require.ErrorIs(t, err, dashboards.ErrFolderNotFound) - // folder table - _, err = serviceWithFlagOff.store.Get(context.Background(), folder.GetFolderQuery{UID: &ancestorUIDs[0], OrgID: orgID}) - require.NoError(t, err) - t.Cleanup(func() { - guardian.New = origNewGuardian - for _, uid := range ancestorUIDs { - err := serviceWithFlagOff.store.Delete(context.Background(), uid, orgID) - require.NoError(t, err) - } - }) - }) - t.Run("Without force deletion of rules", func(t *testing.T) { - origNewGuardian := guardian.New - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true}) - - ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, 1, "off-no-force", createCmd) - - parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[0]) - require.NoError(t, err) - _ = createRule(t, alertStore, parent.UID, "parent alert") - - deleteCmd := folder.DeleteFolderCommand{ - UID: ancestorUIDs[0], - OrgID: orgID, - SignedInUser: &signedInUser, - ForceDeleteRules: false, - } - err = serviceWithFlagOff.Delete(context.Background(), &deleteCmd) - require.Error(t, dashboards.ErrFolderContainsAlertRules, err) - - // dashboard table - _, err = serviceWithFlagOff.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[0]) - require.NoError(t, err) - // folder table - _, err = serviceWithFlagOff.store.Get(context.Background(), folder.GetFolderQuery{UID: &ancestorUIDs[0], OrgID: orgID}) - require.NoError(t, err) - t.Cleanup(func() { - guardian.New = origNewGuardian - for _, uid := range ancestorUIDs { - err := serviceWithFlagOff.store.Delete(context.Background(), uid, orgID) - require.NoError(t, err) - } - }) - }) - }) + } }) } diff --git a/pkg/services/folder/registry.go b/pkg/services/folder/registry.go index 468f097bf8c..efbb89008dd 100644 --- a/pkg/services/folder/registry.go +++ b/pkg/services/folder/registry.go @@ -7,7 +7,7 @@ import ( ) type RegistryService interface { - DeleteInFolder(ctx context.Context, orgID int64, folderUID string) error + DeleteInFolder(ctx context.Context, orgID int64, folderUID string, user *user.SignedInUser) error CountInFolder(ctx context.Context, orgID int64, folderUID string, user *user.SignedInUser) (int64, error) Kind() string } diff --git a/pkg/services/libraryelements/api.go b/pkg/services/libraryelements/api.go index d739bd0b20d..95bbcb3cad4 100644 --- a/pkg/services/libraryelements/api.go +++ b/pkg/services/libraryelements/api.go @@ -112,7 +112,12 @@ func (l *LibraryElementService) deleteHandler(c *contextmodel.ReqContext) respon // 404: notFoundError // 500: internalServerError func (l *LibraryElementService) getHandler(c *contextmodel.ReqContext) response.Response { - element, err := l.getLibraryElementByUid(c.Req.Context(), c.SignedInUser, web.Params(c.Req)[":uid"]) + element, err := l.getLibraryElementByUid(c.Req.Context(), c.SignedInUser, + model.GetLibraryElementCommand{ + UID: web.Params(c.Req)[":uid"], + FolderName: dashboards.RootFolderName, + }, + ) if err != nil { return toLibraryElementError(err, "Failed to get library element") } diff --git a/pkg/services/libraryelements/database.go b/pkg/services/libraryelements/database.go index bf476134c67..86ed8ae6e75 100644 --- a/pkg/services/libraryelements/database.go +++ b/pkg/services/libraryelements/database.go @@ -228,7 +228,7 @@ func (l *LibraryElementService) deleteLibraryElement(c context.Context, signedIn } // getLibraryElements gets a Library Element where param == value -func getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signedInUser *user.SignedInUser, params []Pair, features featuremgmt.FeatureToggles) ([]model.LibraryElementDTO, error) { +func getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signedInUser *user.SignedInUser, params []Pair, features featuremgmt.FeatureToggles, cmd model.GetLibraryElementCommand) ([]model.LibraryElementDTO, error) { libraryElements := make([]model.LibraryElementWithMeta, 0) recursiveQueriesAreSupported, err := store.RecursiveQueriesAreSupported() @@ -239,10 +239,10 @@ func getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signed err = store.WithDbSession(c, func(session *db.Session) error { builder := db.NewSqlBuilder(cfg, features, store.GetDialect(), recursiveQueriesAreSupported) builder.Write(selectLibraryElementDTOWithMeta) - builder.Write(", 'General' as folder_name ") + builder.Write(", ? as folder_name ", cmd.FolderName) builder.Write(", '' as folder_uid ") builder.Write(getFromLibraryElementDTOWithMeta(store.GetDialect())) - writeParamSelectorSQL(&builder, append(params, Pair{"folder_id", 0})...) + writeParamSelectorSQL(&builder, append(params, Pair{"folder_id", cmd.FolderID})...) builder.Write(" UNION ") builder.Write(selectLibraryElementDTOWithMeta) builder.Write(", dashboard.title as folder_name ") @@ -303,8 +303,8 @@ func getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signed } // getLibraryElementByUid gets a Library Element by uid. -func (l *LibraryElementService) getLibraryElementByUid(c context.Context, signedInUser *user.SignedInUser, UID string) (model.LibraryElementDTO, error) { - libraryElements, err := getLibraryElements(c, l.SQLStore, l.Cfg, signedInUser, []Pair{{key: "org_id", value: signedInUser.OrgID}, {key: "uid", value: UID}}, l.features) +func (l *LibraryElementService) getLibraryElementByUid(c context.Context, signedInUser *user.SignedInUser, cmd model.GetLibraryElementCommand) (model.LibraryElementDTO, error) { + libraryElements, err := getLibraryElements(c, l.SQLStore, l.Cfg, signedInUser, []Pair{{key: "org_id", value: signedInUser.OrgID}, {key: "uid", value: cmd.UID}}, l.features, cmd) if err != nil { return model.LibraryElementDTO{}, err } @@ -317,7 +317,10 @@ func (l *LibraryElementService) getLibraryElementByUid(c context.Context, signed // getLibraryElementByName gets a Library Element by name. func (l *LibraryElementService) getLibraryElementsByName(c context.Context, signedInUser *user.SignedInUser, name string) ([]model.LibraryElementDTO, error) { - return getLibraryElements(c, l.SQLStore, l.Cfg, signedInUser, []Pair{{"org_id", signedInUser.OrgID}, {"name", name}}, l.features) + return getLibraryElements(c, l.SQLStore, l.Cfg, signedInUser, []Pair{{"org_id", signedInUser.OrgID}, {"name", name}}, l.features, + model.GetLibraryElementCommand{ + FolderName: dashboards.RootFolderName, + }) } // getAllLibraryElements gets all Library Elements. diff --git a/pkg/services/libraryelements/libraryelements.go b/pkg/services/libraryelements/libraryelements.go index 562bc4b2160..3a0c4aa1e2b 100644 --- a/pkg/services/libraryelements/libraryelements.go +++ b/pkg/services/libraryelements/libraryelements.go @@ -29,7 +29,7 @@ func ProvideService(cfg *setting.Cfg, sqlStore db.DB, routeRegister routing.Rout // Service is a service for operating on library elements. type Service interface { CreateElement(c context.Context, signedInUser *user.SignedInUser, cmd model.CreateLibraryElementCommand) (model.LibraryElementDTO, error) - GetElement(c context.Context, signedInUser *user.SignedInUser, UID string) (model.LibraryElementDTO, error) + GetElement(c context.Context, signedInUser *user.SignedInUser, cmd model.GetLibraryElementCommand) (model.LibraryElementDTO, error) GetElementsForDashboard(c context.Context, dashboardID int64) (map[string]model.LibraryElementDTO, error) ConnectElementsToDashboard(c context.Context, signedInUser *user.SignedInUser, elementUIDs []string, dashboardID int64) error DisconnectElementsFromDashboard(c context.Context, dashboardID int64) error @@ -52,8 +52,8 @@ func (l *LibraryElementService) CreateElement(c context.Context, signedInUser *u } // GetElement gets an element from a UID. -func (l *LibraryElementService) GetElement(c context.Context, signedInUser *user.SignedInUser, UID string) (model.LibraryElementDTO, error) { - return l.getLibraryElementByUid(c, signedInUser, UID) +func (l *LibraryElementService) GetElement(c context.Context, signedInUser *user.SignedInUser, cmd model.GetLibraryElementCommand) (model.LibraryElementDTO, error) { + return l.getLibraryElementByUid(c, signedInUser, cmd) } // GetElementsForDashboard gets all connected elements for a specific dashboard. diff --git a/pkg/services/libraryelements/model/model.go b/pkg/services/libraryelements/model/model.go index 8646b27a7d7..194e6620b18 100644 --- a/pkg/services/libraryelements/model/model.go +++ b/pkg/services/libraryelements/model/model.go @@ -230,6 +230,13 @@ type PatchLibraryElementCommand struct { UID string `json:"uid"` } +// GetLibraryElementCommand is the command for getting a library element. +type GetLibraryElementCommand struct { + FolderName string + FolderID int64 + UID string +} + // SearchLibraryElementsQuery is the query used for searching for Elements type SearchLibraryElementsQuery struct { PerPage int diff --git a/pkg/services/librarypanels/librarypanels.go b/pkg/services/librarypanels/librarypanels.go index ef885741087..26501f92657 100644 --- a/pkg/services/librarypanels/librarypanels.go +++ b/pkg/services/librarypanels/librarypanels.go @@ -10,21 +10,30 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/libraryelements" "github.com/grafana/grafana/pkg/services/libraryelements/model" + "github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" ) func ProvideService(cfg *setting.Cfg, sqlStore db.DB, routeRegister routing.RouteRegister, - libraryElementService libraryelements.Service) *LibraryPanelService { - return &LibraryPanelService{ + libraryElementService libraryelements.Service, folderService folder.Service) (*LibraryPanelService, error) { + lps := LibraryPanelService{ Cfg: cfg, SQLStore: sqlStore, RouteRegister: routeRegister, LibraryElementService: libraryElementService, + FolderService: folderService, log: log.New("library-panels"), } + + if err := folderService.RegisterService(lps); err != nil { + return nil, err + } + + return &lps, nil } // Service is a service for operating on library panels. @@ -44,6 +53,7 @@ type LibraryPanelService struct { SQLStore db.DB RouteRegister routing.RouteRegister LibraryElementService libraryelements.Service + FolderService folder.Service log log.Logger } @@ -130,7 +140,7 @@ func importLibraryPanelsRecursively(c context.Context, service libraryelements.S return errLibraryPanelHeaderUIDMissing } - _, err := service.GetElement(c, signedInUser, UID) + _, err := service.GetElement(c, signedInUser, model.GetLibraryElementCommand{UID: UID, FolderName: dashboards.RootFolderName}) if err == nil { continue } @@ -171,3 +181,28 @@ func importLibraryPanelsRecursively(c context.Context, service libraryelements.S return nil } + +// CountInFolder is a handler for retrieving the number of library panels contained +// within a given folder and for a specific organisation. +func (lps LibraryPanelService) CountInFolder(ctx context.Context, orgID int64, folderUID string, u *user.SignedInUser) (int64, error) { + var count int64 + return count, lps.SQLStore.WithDbSession(ctx, func(sess *db.Session) error { + folder, err := lps.FolderService.Get(ctx, &folder.GetFolderQuery{UID: &folderUID, OrgID: orgID, SignedInUser: u}) + if err != nil { + return err + } + + q := sess.Table("library_element").Where("org_id = ?", u.OrgID). + Where("folder_id = ?", folder.ID).Where("kind = ?", int64(model.PanelElement)) + count, err = q.Count() + return err + }) +} + +// DeleteInFolder deletes the library panels contained in a given folder. +func (lps LibraryPanelService) DeleteInFolder(ctx context.Context, orgID int64, folderUID string, user *user.SignedInUser) error { + return lps.LibraryElementService.DeleteLibraryElementsInFolder(ctx, user, folderUID) +} + +// Kind returns the name of the library panel type of entity. +func (lps LibraryPanelService) Kind() string { return entity.StandardKindLibraryPanel } diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index 36765e66966..254e0396c48 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -319,6 +319,23 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { require.Len(t, elements, 1) require.Equal(t, sc.initialResult.Result.UID, elements[sc.initialResult.Result.UID].UID) }) + + scenarioWithLibraryPanel(t, "It should return the correct count of library panels in a folder", + func(t *testing.T, sc scenarioContext) { + count, err := sc.lps.CountInFolder(context.Background(), sc.user.OrgID, sc.folder.UID, sc.user) + require.NoError(t, err) + require.Equal(t, int64(1), count) + }) + + scenarioWithLibraryPanel(t, "It should delete library panels in a folder", + func(t *testing.T, sc scenarioContext) { + err := sc.lps.DeleteInFolder(context.Background(), sc.user.OrgID, sc.folder.UID, sc.user) + require.NoError(t, err) + + _, err = sc.elementService.GetElement(sc.ctx, sc.user, + model.GetLibraryElementCommand{UID: sc.initialResult.Result.UID, FolderName: sc.folder.Title}) + require.EqualError(t, err, model.ErrLibraryElementNotFound.Error()) + }) } func TestImportLibraryPanelsForDashboard(t *testing.T) { @@ -367,14 +384,16 @@ func TestImportLibraryPanelsForDashboard(t *testing.T) { }, } - _, err := sc.elementService.GetElement(sc.ctx, sc.user, missingUID) + _, err := sc.elementService.GetElement(sc.ctx, sc.user, + model.GetLibraryElementCommand{UID: missingUID, FolderName: dashboards.RootFolderName}) require.EqualError(t, err, model.ErrLibraryElementNotFound.Error()) err = sc.service.ImportLibraryPanelsForDashboard(sc.ctx, sc.user, simplejson.NewFromAny(libraryElements), panels, 0) require.NoError(t, err) - element, err := sc.elementService.GetElement(sc.ctx, sc.user, missingUID) + element, err := sc.elementService.GetElement(sc.ctx, sc.user, + model.GetLibraryElementCommand{UID: missingUID, FolderName: dashboards.RootFolderName}) require.NoError(t, err) var expected = getExpected(t, element, missingUID, missingName, missingModel) var result = toLibraryElement(t, element) @@ -406,13 +425,15 @@ func TestImportLibraryPanelsForDashboard(t *testing.T) { }, } - _, err := sc.elementService.GetElement(sc.ctx, sc.user, existingUID) + _, err := sc.elementService.GetElement(sc.ctx, sc.user, + model.GetLibraryElementCommand{UID: existingUID, FolderName: dashboards.RootFolderName}) require.NoError(t, err) err = sc.service.ImportLibraryPanelsForDashboard(sc.ctx, sc.user, simplejson.New(), panels, sc.folder.ID) require.NoError(t, err) - element, err := sc.elementService.GetElement(sc.ctx, sc.user, existingUID) + element, err := sc.elementService.GetElement(sc.ctx, sc.user, + model.GetLibraryElementCommand{UID: existingUID, FolderName: dashboards.RootFolderName}) require.NoError(t, err) var expected = getExpected(t, element, existingUID, existingName, sc.initialResult.Result.Model) expected.FolderID = sc.initialResult.Result.FolderID @@ -519,16 +540,15 @@ func TestImportLibraryPanelsForDashboard(t *testing.T) { }, }, } - - _, err := sc.elementService.GetElement(sc.ctx, sc.user, outsideUID) + _, err := sc.elementService.GetElement(sc.ctx, sc.user, model.GetLibraryElementCommand{UID: outsideUID, FolderName: dashboards.RootFolderName}) require.EqualError(t, err, model.ErrLibraryElementNotFound.Error()) - _, err = sc.elementService.GetElement(sc.ctx, sc.user, insideUID) + _, err = sc.elementService.GetElement(sc.ctx, sc.user, model.GetLibraryElementCommand{UID: insideUID, FolderName: dashboards.RootFolderName}) require.EqualError(t, err, model.ErrLibraryElementNotFound.Error()) err = sc.service.ImportLibraryPanelsForDashboard(sc.ctx, sc.user, simplejson.NewFromAny(libraryElements), panels, 0) require.NoError(t, err) - element, err := sc.elementService.GetElement(sc.ctx, sc.user, outsideUID) + element, err := sc.elementService.GetElement(sc.ctx, sc.user, model.GetLibraryElementCommand{UID: outsideUID, FolderName: dashboards.RootFolderName}) require.NoError(t, err) expected := getExpected(t, element, outsideUID, outsideName, outsideModel) result := toLibraryElement(t, element) @@ -536,7 +556,7 @@ func TestImportLibraryPanelsForDashboard(t *testing.T) { t.Fatalf("Result mismatch (-want +got):\n%s", diff) } - element, err = sc.elementService.GetElement(sc.ctx, sc.user, insideUID) + element, err = sc.elementService.GetElement(sc.ctx, sc.user, model.GetLibraryElementCommand{UID: insideUID, FolderName: dashboards.RootFolderName}) require.NoError(t, err) expected = getExpected(t, element, insideUID, insideName, insideModel) result = toLibraryElement(t, element) @@ -607,6 +627,7 @@ type scenarioContext struct { folder *folder.Folder initialResult libraryPanelResult sqlStore db.DB + lps LibraryPanelService } func toLibraryElement(t *testing.T, res model.LibraryElementDTO) libraryElement { @@ -814,6 +835,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo Cfg: cfg, SQLStore: sqlStore, LibraryElementService: elementService, + FolderService: folderService, } usr := &user.SignedInUser{ @@ -853,6 +875,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo service: &service, elementService: elementService, sqlStore: sqlStore, + lps: service, } foldr := createFolder(t, sc, "ScenarioFolder") diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index 4e8fe18481c..20ad938085a 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -586,7 +586,7 @@ func (st DBstore) GetAlertRulesForScheduling(ctx context.Context, query *ngmodel } // 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) error { +func (st DBstore) DeleteInFolder(ctx context.Context, orgID int64, folderUID string, user *user.SignedInUser) error { rules, err := st.ListAlertRules(ctx, &ngmodels.ListAlertRulesQuery{ OrgID: orgID, NamespaceUIDs: []string{folderUID}, diff --git a/pkg/services/ngalert/store/alert_rule_test.go b/pkg/services/ngalert/store/alert_rule_test.go index 7323097b4e9..b2148f8ae52 100644 --- a/pkg/services/ngalert/store/alert_rule_test.go +++ b/pkg/services/ngalert/store/alert_rule_test.go @@ -474,7 +474,7 @@ func TestIntegration_DeleteInFolder(t *testing.T) { } rule := createRule(t, store, nil) - err := store.DeleteInFolder(context.Background(), rule.OrgID, rule.NamespaceUID) + err := store.DeleteInFolder(context.Background(), rule.OrgID, rule.NamespaceUID, nil) require.NoError(t, err) c, err := store.CountInFolder(context.Background(), rule.OrgID, rule.NamespaceUID, nil) diff --git a/pkg/services/store/entity/models.go b/pkg/services/store/entity/models.go index b4b951a9d68..a52dca70518 100644 --- a/pkg/services/store/entity/models.go +++ b/pkg/services/store/entity/models.go @@ -42,7 +42,7 @@ const ( // the kind may need to change to better encapsulate { targets:[], transforms:[] } StandardKindQuery = "query" - // KindAlertRule is not a real kind. It's used to refer to alert rules, for instance + // StandardKindAlertRule is not a real kind. It's used to refer to alert rules, for instance // in the folder registry service. StandardKindAlertRule = "alertrule"