package folderimpl import ( "context" "encoding/json" "errors" "fmt" "log/slog" "math/rand" "strings" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "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" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log/logtest" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/authz/zanzana" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" "github.com/grafana/grafana/pkg/services/dashboards/database" dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service" "github.com/grafana/grafana/pkg/services/featuremgmt" "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/org" "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/supportbundles/supportbundlestest" "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) var orgID = int64(1) var usr = &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{orgID: {dashboards.ActionFoldersCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)}}}} var noPermUsr = &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{}} func TestIntegrationProvideFolderService(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } t.Run("should register scope resolvers", func(t *testing.T) { ac := acmock.New() db := db.InitTestDB(t) ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), nil, nil, db, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) require.Len(t, ac.Calls.RegisterAttributeScopeResolver, 3) }) } func TestIntegrationFolderService(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } t.Run("Folder service tests", func(t *testing.T) { dashStore := &dashboards.FakeDashboardStore{} db, cfg := sqlstore.InitTestDB(t) nestedFolderStore := ProvideStore(db) folderStore := foldertest.NewFakeFolderStore(t) features := featuremgmt.WithFeatures() alertingStore := ngstore.DBstore{ SQLStore: db, Cfg: cfg.UnifiedAlerting, Logger: log.New("test-alerting-store"), AccessControl: actest.FakeAccessControl{ExpectedEvaluate: true}, } service := &Service{ log: slog.New(logtest.NewTestHandler(t)).With("logger", "test-folder-service"), dashboardStore: dashStore, dashboardFolderStore: folderStore, store: nestedFolderStore, features: features, bus: bus.ProvideBus(tracing.InitializeTracerForTest()), db: db, accessControl: acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()), metrics: newFoldersMetrics(nil), registry: make(map[string]folder.RegistryService), tracer: tracing.InitializeTracerForTest(), } require.NoError(t, service.RegisterService(alertingStore)) t.Run("Given user has no permissions", func(t *testing.T) { origNewGuardian := guardian.New guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{}) folderUID := util.GenerateShortUID() f := folder.NewFolder("Folder", "") f.UID = folderUID folderStore.On("GetFolderByUID", mock.Anything, orgID, folderUID).Return(f, nil) t.Run("When get folder by id should return access denied error", func(t *testing.T) { _, err := service.Get(context.Background(), &folder.GetFolderQuery{ UID: &folderUID, OrgID: orgID, SignedInUser: noPermUsr, }) require.Equal(t, err, dashboards.ErrFolderAccessDenied) }) t.Run("When get folder by uid should return access denied error", func(t *testing.T) { _, err := service.Get(context.Background(), &folder.GetFolderQuery{ UID: &folderUID, OrgID: orgID, SignedInUser: noPermUsr, }) require.Equal(t, err, dashboards.ErrFolderAccessDenied) }) t.Run("When creating folder should return access denied error", func(t *testing.T) { dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*dashboards.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil).Times(2) _, err := service.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, Title: f.Title, UID: folderUID, SignedInUser: noPermUsr, }) require.Error(t, err) }) title := "Folder-TEST" t.Run("When updating folder should return access denied error", func(t *testing.T) { folderResult := dashboards.NewDashboard("dashboard-test") folderResult.IsFolder = true dashStore.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(folderResult, nil) _, err := service.Update(context.Background(), &folder.UpdateFolderCommand{ UID: folderUID, OrgID: orgID, NewTitle: &title, SignedInUser: noPermUsr, }) require.Equal(t, err, dashboards.ErrFolderAccessDenied) }) t.Run("When deleting folder by uid should return access denied error", func(t *testing.T) { newFolder := folder.NewFolder("Folder", "") newFolder.UID = folderUID folderStore.On("GetFolderByUID", mock.Anything, orgID, folderUID).Return(newFolder, nil) err := service.Delete(context.Background(), &folder.DeleteFolderCommand{ UID: folderUID, OrgID: orgID, ForceDeleteRules: false, SignedInUser: noPermUsr, }) require.Error(t, err) require.Equal(t, err, dashboards.ErrFolderAccessDenied) }) t.Cleanup(func() { guardian.New = origNewGuardian }) }) t.Run("Given user has permission to save", func(t *testing.T) { origNewGuardian := guardian.New guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true}) service.features = featuremgmt.WithFeatures() t.Run("When creating folder should not return access denied error", func(t *testing.T) { dash := dashboards.NewDashboardFolder("Test-Folder") dash.ID = rand.Int63() dash.UID = util.GenerateShortUID() f := dashboards.FromDashboard(dash) dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*dashboards.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil) dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(dash, nil).Once() actualFolder, err := service.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, Title: dash.Title, UID: dash.UID, SignedInUser: usr, }) require.NoError(t, err) require.Equal(t, f, actualFolder) }) t.Run("When creating folder should return error if uid is general", func(t *testing.T) { dash := dashboards.NewDashboardFolder("Test-Folder") dash.ID = rand.Int63() _, err := service.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, Title: dash.Title, UID: "general", SignedInUser: usr, }) require.ErrorIs(t, err, dashboards.ErrFolderInvalidUID) }) t.Run("When updating folder should not return access denied error", func(t *testing.T) { dashboardFolder := dashboards.NewDashboardFolder("Folder") dashboardFolder.ID = rand.Int63() dashboardFolder.UID = util.GenerateShortUID() dashboardFolder.OrgID = orgID f, err := service.store.Create(context.Background(), folder.CreateFolderCommand{ OrgID: orgID, Title: dashboardFolder.Title, UID: dashboardFolder.UID, SignedInUser: usr, }) require.NoError(t, err) assert.Equal(t, "Folder", f.Title) dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*dashboards.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil) title := "TEST-Folder" updatedDashboardFolder := *dashboardFolder updatedDashboardFolder.Title = title dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(&updatedDashboardFolder, nil) dashStore.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(&updatedDashboardFolder, nil) folderStore.On("GetFolderByID", mock.Anything, orgID, dashboardFolder.ID).Return(&folder.Folder{ OrgID: orgID, ID: dashboardFolder.ID, UID: dashboardFolder.UID, Title: title, }, nil) req := &folder.UpdateFolderCommand{ UID: dashboardFolder.UID, OrgID: orgID, NewTitle: &title, SignedInUser: usr, } reqResult, err := service.Update(context.Background(), req) require.NoError(t, err) assert.Equal(t, title, reqResult.Title) }) t.Run("When deleting folder by uid should not return access denied error", func(t *testing.T) { f := folder.NewFolder(util.GenerateShortUID(), "") f.UID = util.GenerateShortUID() folderStore.On("GetFolderByUID", mock.Anything, orgID, f.UID).Return(f, nil) var actualCmd *dashboards.DeleteDashboardCommand dashStore.On("DeleteDashboard", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { actualCmd = args.Get(1).(*dashboards.DeleteDashboardCommand) }).Return(nil).Once() expectedForceDeleteRules := rand.Int63()%2 == 0 err := service.Delete(context.Background(), &folder.DeleteFolderCommand{ UID: f.UID, OrgID: orgID, ForceDeleteRules: expectedForceDeleteRules, SignedInUser: usr, }) require.NoError(t, err) require.NotNil(t, actualCmd) require.Equal(t, orgID, actualCmd.OrgID) require.Equal(t, expectedForceDeleteRules, actualCmd.ForceDeleteFolderRules) }) t.Run("When deleting folder by uid, expectedForceDeleteRules as false, and dashboard Restore turned on should not return access denied error", func(t *testing.T) { f := folder.NewFolder(util.GenerateShortUID(), "") f.UID = util.GenerateShortUID() folderStore.On("GetFolderByUID", mock.Anything, orgID, f.UID).Return(f, nil) var actualCmd *dashboards.DeleteDashboardCommand dashStore.On("DeleteDashboard", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { actualCmd = args.Get(1).(*dashboards.DeleteDashboardCommand) }).Return(nil).Once() service.features = featuremgmt.WithFeatures(featuremgmt.FlagDashboardRestore) expectedForceDeleteRules := false err := service.Delete(context.Background(), &folder.DeleteFolderCommand{ UID: f.UID, OrgID: orgID, ForceDeleteRules: expectedForceDeleteRules, SignedInUser: usr, }) require.NoError(t, err) require.NotNil(t, actualCmd) require.Equal(t, orgID, actualCmd.OrgID) require.Equal(t, expectedForceDeleteRules, actualCmd.ForceDeleteFolderRules) }) t.Run("When deleting folder by uid, expectedForceDeleteRules as true, and dashboard Restore turned on should not return access denied error", func(t *testing.T) { f := folder.NewFolder(util.GenerateShortUID(), "") f.UID = util.GenerateShortUID() folderStore.On("GetFolderByUID", mock.Anything, orgID, f.UID).Return(f, nil) var actualCmd *dashboards.DeleteDashboardCommand dashStore.On("DeleteDashboard", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { actualCmd = args.Get(1).(*dashboards.DeleteDashboardCommand) }).Return(nil).Once() service.features = featuremgmt.WithFeatures(featuremgmt.FlagDashboardRestore) expectedForceDeleteRules := true err := service.Delete(context.Background(), &folder.DeleteFolderCommand{ UID: f.UID, OrgID: orgID, ForceDeleteRules: expectedForceDeleteRules, SignedInUser: usr, }) require.NoError(t, err) require.NotNil(t, actualCmd) require.Equal(t, orgID, actualCmd.OrgID) require.Equal(t, expectedForceDeleteRules, actualCmd.ForceDeleteFolderRules) }) t.Cleanup(func() { service.features = featuremgmt.WithFeatures() guardian.New = origNewGuardian }) }) t.Run("Given user has permission to view", func(t *testing.T) { origNewGuardian := guardian.New guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true}) t.Run("When get folder by uid should return folder", func(t *testing.T) { expected := folder.NewFolder(util.GenerateShortUID(), "") expected.UID = util.GenerateShortUID() folderStore.On("GetFolderByUID", mock.Anything, orgID, expected.UID).Return(expected, nil) actual, err := service.getFolderByUID(context.Background(), orgID, expected.UID) require.Equal(t, expected, actual) require.NoError(t, err) }) t.Run("When get folder by uid and uid is general should return the root folder object", func(t *testing.T) { uid := accesscontrol.GeneralFolderUID query := &folder.GetFolderQuery{ UID: &uid, SignedInUser: usr, } actual, err := service.Get(context.Background(), query) require.Equal(t, folder.RootFolder, actual) require.NoError(t, err) }) t.Run("When get folder by title should return folder", func(t *testing.T) { expected := folder.NewFolder("TEST-"+util.GenerateShortUID(), "") folderStore.On("GetFolderByTitle", mock.Anything, orgID, expected.Title, mock.Anything).Return(expected, nil) actual, err := service.getFolderByTitle(context.Background(), orgID, expected.Title, nil) require.Equal(t, expected, actual) require.NoError(t, err) }) t.Cleanup(func() { guardian.New = origNewGuardian }) }) t.Run("Should map errors correct", func(t *testing.T) { testCases := []struct { ActualError error ExpectedError error }{ {ActualError: dashboards.ErrDashboardTitleEmpty, ExpectedError: dashboards.ErrFolderTitleEmpty}, {ActualError: dashboards.ErrDashboardUpdateAccessDenied, ExpectedError: dashboards.ErrFolderAccessDenied}, {ActualError: dashboards.ErrDashboardWithSameNameInFolderExists, ExpectedError: dashboards.ErrFolderSameNameExists}, {ActualError: dashboards.ErrDashboardWithSameUIDExists, ExpectedError: dashboards.ErrFolderWithSameUIDExists}, {ActualError: dashboards.ErrDashboardVersionMismatch, ExpectedError: dashboards.ErrFolderVersionMismatch}, {ActualError: dashboards.ErrDashboardNotFound, ExpectedError: dashboards.ErrFolderNotFound}, {ActualError: dashboards.ErrDashboardInvalidUid, ExpectedError: dashboards.ErrDashboardInvalidUid}, } for _, tc := range testCases { actualError := toFolderError(tc.ActualError) assert.EqualErrorf(t, actualError, tc.ExpectedError.Error(), "For error '%s' expected error '%s', actual '%s'", tc.ActualError, tc.ExpectedError, actualError) } }) t.Run("Returns root folder", func(t *testing.T) { t.Run("When the folder UID is blank should return the root folder", func(t *testing.T) { emptyString := "" actual, err := service.Get(context.Background(), &folder.GetFolderQuery{ UID: &emptyString, OrgID: 1, SignedInUser: usr, }) assert.NoError(t, err) assert.Equal(t, folder.GeneralFolder.UID, actual.UID) assert.Equal(t, folder.GeneralFolder.Title, actual.Title) }) }) }) } func TestIntegrationNestedFolderService(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } db, cfg := sqlstore.InitTestReplDB(t) quotaService := quotatest.New(false, nil) folderStore := ProvideDashboardFolderStore(db) featuresFlagOn := featuremgmt.WithFeatures("nestedFolders") dashStore, err := database.ProvideDashboardStore(db, cfg, featuresFlagOn, tagimpl.ProvideService(db), quotaService) require.NoError(t, err) nestedFolderStore := ProvideStore(db) b := bus.ProvideBus(tracing.InitializeTracerForTest()) ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()) serviceWithFlagOn := &Service{ log: slog.New(logtest.NewTestHandler(t)).With("logger", "test-folder-service"), dashboardStore: dashStore, dashboardFolderStore: folderStore, store: nestedFolderStore, features: featuresFlagOn, bus: b, db: db, accessControl: ac, registry: make(map[string]folder.RegistryService), metrics: newFoldersMetrics(nil), tracer: tracing.InitializeTracerForTest(), } signedInUser := user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{ orgID: { dashboards.ActionFoldersCreate: {}, dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll}, accesscontrol.ActionAlertingRuleDelete: {dashboards.ScopeFoldersAll}, }, }} createCmd := folder.CreateFolderCommand{ OrgID: orgID, ParentUID: "", 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() t.Run("Should get descendant counts", func(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, // CanEditValue is required to create library elements CanEditValue: true, }) dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOn, folderPermissions, dashboardPermissions, ac, serviceWithFlagOn, nil) require.NoError(t, err) alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOn, db, serviceWithFlagOn, dashSrv, ac) require.NoError(t, err) elementService := libraryelements.ProvideService(cfg, db, routeRegister, serviceWithFlagOn, featuresFlagOn, ac) lps, err := librarypanels.ProvideService(cfg, db, routeRegister, elementService, serviceWithFlagOn) require.NoError(t, err) ancestors := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "getDescendantCountsOn", createCmd) parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestors[0].UID) require.NoError(t, err) subfolder, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestors[1].UID) require.NoError(t, err) // nolint:staticcheck _ = insertTestDashboard(t, serviceWithFlagOn.dashboardStore, "dashboard in parent", orgID, parent.ID, parent.UID, "prod") // nolint:staticcheck _ = insertTestDashboard(t, serviceWithFlagOn.dashboardStore, "dashboard in subfolder", orgID, subfolder.ID, subfolder.UID, "prod") _ = createRule(t, alertStore, parent.UID, "parent alert") _ = createRule(t, alertStore, subfolder.UID, "sub alert") // nolint:staticcheck libraryElementCmd.FolderID = parent.ID libraryElementCmd.FolderUID = &parent.UID _, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd) require.NoError(t, err) // nolint:staticcheck libraryElementCmd.FolderID = subfolder.ID libraryElementCmd.FolderUID = &subfolder.UID _, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd) require.NoError(t, err) countCmd := folder.GetDescendantCountsQuery{ UID: &ancestors[0].UID, OrgID: orgID, SignedInUser: &signedInUser, } m, err := serviceWithFlagOn.GetDescendantCounts(context.Background(), &countCmd) require.NoError(t, err) 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 for _, ancestor := range ancestors { err := serviceWithFlagOn.store.Delete(context.Background(), []string{ancestor.UID}, orgID) assert.NoError(t, err) } }) }) t.Run("With nested folder feature flag off", func(t *testing.T) { featuresFlagOff := featuremgmt.WithFeatures() dashStore, err := database.ProvideDashboardStore(db, cfg, featuresFlagOff, tagimpl.ProvideService(db), quotaService) require.NoError(t, err) nestedFolderStore := ProvideStore(db) serviceWithFlagOff := &Service{ log: slog.New(logtest.NewTestHandler(t)).With("logger", "test-folder-service"), dashboardStore: dashStore, dashboardFolderStore: folderStore, store: nestedFolderStore, features: featuresFlagOff, bus: b, db: db, registry: make(map[string]folder.RegistryService), metrics: newFoldersMetrics(nil), tracer: tracing.InitializeTracerForTest(), } origNewGuardian := guardian.New guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ CanSaveValue: true, CanViewValue: true, // CanEditValue is required to create library elements CanEditValue: true, }) dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOff, folderPermissions, dashboardPermissions, ac, serviceWithFlagOff, nil) require.NoError(t, err) alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOff, db, serviceWithFlagOff, dashSrv, ac) require.NoError(t, err) elementService := libraryelements.ProvideService(cfg, db, routeRegister, serviceWithFlagOff, featuresFlagOff, ac) lps, err := librarypanels.ProvideService(cfg, db, routeRegister, elementService, serviceWithFlagOff) require.NoError(t, err) ancestors := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "getDescendantCountsOff", createCmd) parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestors[0].UID) require.NoError(t, err) subfolder, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestors[1].UID) require.NoError(t, err) // nolint:staticcheck _ = insertTestDashboard(t, serviceWithFlagOn.dashboardStore, "dashboard in parent", orgID, parent.ID, parent.UID, "prod") // nolint:staticcheck _ = insertTestDashboard(t, serviceWithFlagOn.dashboardStore, "dashboard in subfolder", orgID, subfolder.ID, subfolder.UID, "prod") _ = createRule(t, alertStore, parent.UID, "parent alert") _ = createRule(t, alertStore, subfolder.UID, "sub alert") // nolint:staticcheck libraryElementCmd.FolderID = parent.ID libraryElementCmd.FolderUID = &parent.UID _, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd) require.NoError(t, err) // nolint:staticcheck libraryElementCmd.FolderID = subfolder.ID libraryElementCmd.FolderUID = &subfolder.UID _, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd) require.NoError(t, err) countCmd := folder.GetDescendantCountsQuery{ UID: &ancestors[0].UID, OrgID: orgID, SignedInUser: &signedInUser, } m, err := serviceWithFlagOff.GetDescendantCounts(context.Background(), &countCmd) require.NoError(t, err) 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 for _, ancestor := range ancestors { err := serviceWithFlagOn.store.Delete(context.Background(), []string{ancestor.UID}, orgID) assert.NoError(t, err) } }) }) }) t.Run("Should delete folders", func(t *testing.T) { featuresFlagOff := featuremgmt.WithFeatures() serviceWithFlagOff := &Service{ log: slog.New(logtest.NewTestHandler(t)).With("logger", "test-folder-service"), dashboardFolderStore: folderStore, features: featuresFlagOff, bus: b, db: db, registry: make(map[string]folder.RegistryService), metrics: newFoldersMetrics(nil), tracer: tracing.InitializeTracerForTest(), } testCases := []struct { service *Service featuresFlag featuremgmt.FeatureToggles 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: folder.ErrFolderNotEmpty, 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, folderErr: folder.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: folder.ErrFolderNotEmpty, 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, // CanEditValue is required to create library elements CanEditValue: true, }) elementService := libraryelements.ProvideService(cfg, db, routeRegister, tc.service, tc.featuresFlag, ac) lps, err := librarypanels.ProvideService(cfg, db, routeRegister, elementService, tc.service) require.NoError(t, err) dashStore, err := database.ProvideDashboardStore(db, cfg, tc.featuresFlag, tagimpl.ProvideService(db), quotaService) require.NoError(t, err) nestedFolderStore := ProvideStore(db) tc.service.dashboardStore = dashStore tc.service.store = nestedFolderStore dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, tc.featuresFlag, folderPermissions, dashboardPermissions, ac, tc.service, nil) require.NoError(t, err) alertStore, err := ngstore.ProvideDBStore(cfg, tc.featuresFlag, db, tc.service, dashSrv, ac) require.NoError(t, err) ancestors := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, tc.depth, tc.prefix, createCmd) parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestors[0].UID) require.NoError(t, err) _ = createRule(t, alertStore, parent.UID, "parent alert") var ( subfolder *folder.Folder subPanel model.LibraryElementDTO ) if tc.depth > 1 { subfolder, err = serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestors[1].UID) require.NoError(t, err) _ = createRule(t, alertStore, subfolder.UID, "sub alert") // nolint:staticcheck libraryElementCmd.FolderID = subfolder.ID libraryElementCmd.FolderUID = &subPanel.FolderUID subPanel, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd) require.NoError(t, err) } // nolint:staticcheck libraryElementCmd.FolderID = parent.ID libraryElementCmd.FolderUID = &parent.UID parentPanel, err := lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd) require.NoError(t, err) deleteCmd := folder.DeleteFolderCommand{ UID: ancestors[0].UID, OrgID: orgID, SignedInUser: &signedInUser, ForceDeleteRules: tc.forceDelete, } err = tc.service.Delete(context.Background(), &deleteCmd) require.ErrorIs(t, err, tc.deletionErr) for i, ancestor := range ancestors { // dashboard table _, err := tc.service.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestor.UID) require.ErrorIs(t, err, tc.dashboardErr) // folder table _, err = tc.service.store.Get(context.Background(), folder.GetFolderQuery{UID: &ancestors[i].UID, OrgID: orgID}) require.ErrorIs(t, err, tc.folderErr) } _, err = lps.LibraryElementService.GetElement(context.Background(), &signedInUser, model.GetLibraryElementCommand{ FolderName: parent.Title, FolderID: parent.ID, // nolint:staticcheck 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, // nolint:staticcheck UID: subPanel.UID, }) require.ErrorIs(t, err, tc.libPanelSubErr) } t.Cleanup(func() { guardian.New = origNewGuardian }) }) } }) } func TestNestedFolderServiceFeatureToggle(t *testing.T) { g := guardian.New guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) t.Cleanup(func() { guardian.New = g }) nestedFolderStore := NewFakeStore() dashStore := dashboards.FakeDashboardStore{} dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*dashboards.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil) dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(&dashboards.Dashboard{}, nil) dashboardFolderStore := foldertest.NewFakeFolderStore(t) db, _ := sqlstore.InitTestDB(t) folderService := &Service{ store: nestedFolderStore, log: slog.New(logtest.NewTestHandler(t)).With("logger", "test-folder-service"), db: db, dashboardStore: &dashStore, dashboardFolderStore: dashboardFolderStore, features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders), accessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()), metrics: newFoldersMetrics(nil), tracer: tracing.InitializeTracerForTest(), } t.Run("create folder", func(t *testing.T) { nestedFolderStore.ExpectedFolder = &folder.Folder{ParentUID: util.GenerateShortUID()} res, err := folderService.Create(context.Background(), &folder.CreateFolderCommand{SignedInUser: usr, Title: "my folder"}) require.NoError(t, err) require.NotNil(t, res.UID) require.NotEmpty(t, res.ParentUID) }) } func TestFolderServiceDualWrite(t *testing.T) { g := guardian.New guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) t.Cleanup(func() { guardian.New = g }) db, _ := sqlstore.InitTestReplDB(t) cfg := setting.NewCfg() features := featuremgmt.WithFeatures() nestedFolderStore := ProvideStore(db) dashStore, err := database.ProvideDashboardStore(db, cfg, features, tagimpl.ProvideService(db), "atest.FakeQuotaService{}) require.NoError(t, err) dashboardFolderStore := ProvideDashboardFolderStore(db) folderService := &Service{ store: nestedFolderStore, log: slog.New(logtest.NewTestHandler(t)).With("logger", "test-folder-service"), db: db, dashboardStore: dashStore, dashboardFolderStore: dashboardFolderStore, features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders), accessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()), metrics: newFoldersMetrics(nil), tracer: tracing.InitializeTracerForTest(), bus: bus.ProvideBus(tracing.InitializeTracerForTest()), } t.Run("When creating a folder it should trim leading and trailing spaces in both dashboard and folder tables", func(t *testing.T) { f, err := folderService.Create(context.Background(), &folder.CreateFolderCommand{SignedInUser: usr, OrgID: orgID, Title: " my folder "}) require.NoError(t, err) assert.Equal(t, "my folder", f.Title) dashFolder, err := dashboardFolderStore.GetFolderByUID(context.Background(), orgID, f.UID) require.NoError(t, err) nestedFolder, err := nestedFolderStore.Get(context.Background(), folder.GetFolderQuery{UID: &f.UID, OrgID: orgID}) require.NoError(t, err) assert.Equal(t, dashFolder.Title, nestedFolder.Title) }) t.Run("When updating a folder it should trim leading and trailing spaces in both dashboard and folder tables", func(t *testing.T) { f, err := folderService.Create(context.Background(), &folder.CreateFolderCommand{SignedInUser: usr, OrgID: orgID, Title: "my folder 2"}) require.NoError(t, err) f, err = folderService.Update(context.Background(), &folder.UpdateFolderCommand{SignedInUser: usr, OrgID: orgID, UID: f.UID, NewTitle: util.Pointer(" my updated folder 2 "), Version: f.Version}) require.NoError(t, err) assert.Equal(t, "my updated folder 2", f.Title) dashFolder, err := dashboardFolderStore.GetFolderByUID(context.Background(), orgID, f.UID) require.NoError(t, err) nestedFolder, err := nestedFolderStore.Get(context.Background(), folder.GetFolderQuery{UID: &f.UID, OrgID: orgID}) require.NoError(t, err) assert.Equal(t, dashFolder.Title, nestedFolder.Title) }) } func TestNestedFolderService(t *testing.T) { t.Run("with feature flag unset", func(t *testing.T) { t.Run("Should create a folder in both dashboard and folders tables", func(t *testing.T) { g := guardian.New guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) t.Cleanup(func() { guardian.New = g }) // dash is needed here because folderSvc.Create expects SaveDashboard to return it dash := dashboards.NewDashboardFolder("myFolder") dash.ID = rand.Int63() dash.UID = "some_uid" // dashboard store & service commands that should be called. dashStore := &dashboards.FakeDashboardStore{} dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*dashboards.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil) dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(dash, nil) dashboardFolderStore := foldertest.NewFakeFolderStore(t) nestedFolderStore := NewFakeStore() features := featuremgmt.WithFeatures() db, _ := sqlstore.InitTestDB(t) folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, features, acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()), db) tempUser := &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{}} tempUser.Permissions[orgID] = map[string][]string{dashboards.ActionFoldersCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)}} _, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, Title: dash.Title, UID: dash.UID, SignedInUser: tempUser, }) require.NoError(t, err) require.True(t, nestedFolderStore.CreateCalled) }) }) t.Run("with nested folder feature flag on", func(t *testing.T) { t.Run("Should be able to create a nested folder under the root with the right permissions", func(t *testing.T) { g := guardian.New guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) t.Cleanup(func() { guardian.New = g }) dash := dashboards.NewDashboardFolder("myFolder") dash.ID = rand.Int63() dash.UID = "some_uid" // dashboard store commands that should be called. dashStore := &dashboards.FakeDashboardStore{} dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*dashboards.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil) dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(dash, nil) dashboardFolderStore := foldertest.NewFakeFolderStore(t) nestedFolderStore := NewFakeStore() features := featuremgmt.WithFeatures("nestedFolders") tempUser := &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{}} tempUser.Permissions[orgID] = map[string][]string{dashboards.ActionFoldersCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)}} db, _ := sqlstore.InitTestDB(t) folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, features, acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()), db) _, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, Title: dash.Title, UID: dash.UID, SignedInUser: tempUser, }) require.NoError(t, err) // CreateFolder should also call the folder store's create method. require.True(t, nestedFolderStore.CreateCalled) }) t.Run("Should not be able to create a folder under the root with subfolder creation permissions", func(t *testing.T) { g := guardian.New guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) t.Cleanup(func() { guardian.New = g }) // dashboard store commands that should be called. dashStore := &dashboards.FakeDashboardStore{} dashboardFolderStore := foldertest.NewFakeFolderStore(t) nestedFolderStore := NewFakeStore() features := featuremgmt.WithFeatures("nestedFolders") tempUser := &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{}} tempUser.Permissions[orgID] = map[string][]string{dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("subfolder_uid")}} db, _ := sqlstore.InitTestDB(t) folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, features, acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()), db) _, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, Title: "some_folder", UID: "some_uid", SignedInUser: tempUser, }) require.ErrorIs(t, err, dashboards.ErrFolderCreationAccessDenied) }) t.Run("Should not be able to create new folder under another folder without the right permissions", func(t *testing.T) { g := guardian.New guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) t.Cleanup(func() { guardian.New = g }) dash := dashboards.NewDashboardFolder("Test-Folder") dash.ID = rand.Int63() dash.UID = "some_uid" tempUser := &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{}} tempUser.Permissions[orgID] = map[string][]string{dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("wrong_uid")}} // dashboard store commands that should be called. dashStore := &dashboards.FakeDashboardStore{} dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*dashboards.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil) dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(&dashboards.Dashboard{}, nil) features := featuremgmt.WithFeatures("nestedFolders") folderSvc := setup(t, dashStore, nil, nil, features, acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()), dbtest.NewFakeDB()) _, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, Title: dash.Title, UID: dash.UID, SignedInUser: tempUser, ParentUID: "some_parent", }) require.ErrorIs(t, err, dashboards.ErrFolderCreationAccessDenied) }) t.Run("Should be able to create new folder under another folder with the right permissions", func(t *testing.T) { g := guardian.New guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) t.Cleanup(func() { guardian.New = g }) dash := dashboards.NewDashboardFolder("Test-Folder") dash.ID = rand.Int63() dash.UID = "some_uid" // dashboard store commands that should be called. dashStore := &dashboards.FakeDashboardStore{} dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*dashboards.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil) dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(dash, nil) dashboardFolderStore := foldertest.NewFakeFolderStore(t) dashboardFolderStore.On("GetFolderByUID", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")).Return(&folder.Folder{}, nil) nestedFolderUser := &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{}} nestedFolderUser.Permissions[orgID] = map[string][]string{dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("some_parent")}} nestedFolderStore := NewFakeStore() db, _ := sqlstore.InitTestDB(t) features := featuremgmt.WithFeatures("nestedFolders") folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, features, acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()), db) _, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, Title: dash.Title, UID: dash.UID, SignedInUser: nestedFolderUser, ParentUID: "some_parent", }) require.NoError(t, err) require.True(t, nestedFolderStore.CreateCalled) // Parent write access check will eventually be replaced with scoped folder creation check nestedFolderUser.Permissions[orgID] = map[string][]string{dashboards.ActionFoldersCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("some_parent")}} _, err = folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, Title: dash.Title + "2", UID: dash.UID + "2", SignedInUser: nestedFolderUser, ParentUID: "some_parent", }) require.NoError(t, err) require.True(t, nestedFolderStore.CreateCalled) }) t.Run("create without UID, no error", func(t *testing.T) { g := guardian.New guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) t.Cleanup(func() { guardian.New = g }) // dashboard store commands that should be called. dashStore := &dashboards.FakeDashboardStore{} dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*dashboards.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil) dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(&dashboards.Dashboard{UID: "newUID"}, nil) dashboardFolderStore := foldertest.NewFakeFolderStore(t) nestedFolderStore := NewFakeStore() db, _ := sqlstore.InitTestDB(t) folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), actest.FakeAccessControl{ ExpectedEvaluate: true, }, db) f, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, Title: "myFolder", SignedInUser: usr, }) require.NoError(t, err) // CreateFolder should also call the folder store's create method. require.True(t, nestedFolderStore.CreateCalled) require.Equal(t, "newUID", f.UID) }) t.Run("create failed because of circular reference", func(t *testing.T) { g := guardian.New guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) t.Cleanup(func() { guardian.New = g }) dashboardFolder := dashboards.NewDashboardFolder("myFolder") dashboardFolder.ID = rand.Int63() dashboardFolder.UID = "myFolder" f := dashboards.FromDashboard(dashboardFolder) // dashboard store commands that should be called. dashStore := &dashboards.FakeDashboardStore{} dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*dashboards.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil) dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(dashboardFolder, nil) dashboardFolderStore := foldertest.NewFakeFolderStore(t) dashboardFolderStore.On("GetFolderByUID", mock.Anything, orgID, dashboardFolder.UID).Return(f, nil) nestedFolderStore := NewFakeStore() nestedFolderStore.ExpectedParentFolders = []*folder.Folder{ {UID: "newFolder", ParentUID: "newFolder"}, {UID: "newFolder2", ParentUID: "newFolder2"}, {UID: "newFolder3", ParentUID: "newFolder3"}, {UID: "myFolder", ParentUID: "newFolder"}, } cmd := folder.CreateFolderCommand{ ParentUID: dashboardFolder.UID, OrgID: orgID, Title: "myFolder1", UID: "myFolder1", SignedInUser: usr, } db, _ := sqlstore.InitTestDB(t) folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), actest.FakeAccessControl{ ExpectedEvaluate: true, }, db) _, err := folderSvc.Create(context.Background(), &cmd) require.Error(t, err, folder.ErrCircularReference) // CreateFolder should not call the folder store's create method. require.False(t, nestedFolderStore.CreateCalled) }) t.Run("create returns error from nested folder service", func(t *testing.T) { // This test creates and deletes the dashboard, so needs some extra setup. g := guardian.New guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) t.Cleanup(func() { guardian.New = g }) // dashboard store commands that should be called. dashStore := &dashboards.FakeDashboardStore{} dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*dashboards.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil) dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(&dashboards.Dashboard{}, nil) dashboardFolderStore := foldertest.NewFakeFolderStore(t) // return an error from the folder store nestedFolderStore := NewFakeStore() nestedFolderStore.ExpectedError = errors.New("FAILED") // the service return success as long as the legacy create succeeds db, _ := sqlstore.InitTestDB(t) folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), actest.FakeAccessControl{ ExpectedEvaluate: true, }, db) _, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, Title: "myFolder", UID: "myFolder", SignedInUser: usr, }) require.Error(t, err, "FAILED") // CreateFolder should also call the folder store's create method. require.True(t, nestedFolderStore.CreateCalled) }) t.Run("move without the right permissions should fail", func(t *testing.T) { dashStore := &dashboards.FakeDashboardStore{} dashboardFolderStore := foldertest.NewFakeFolderStore(t) //dashboardFolderStore.On("GetFolderByUID", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")).Return(&folder.Folder{}, nil) nestedFolderStore := NewFakeStore() nestedFolderStore.ExpectedFolder = &folder.Folder{UID: "myFolder", ParentUID: "newFolder"} nestedFolderUser := &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{}} nestedFolderUser.Permissions[orgID] = map[string][]string{dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("wrong_uid")}} features := featuremgmt.WithFeatures("nestedFolders") folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, features, acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()), dbtest.NewFakeDB()) _, err := folderSvc.Move(context.Background(), &folder.MoveFolderCommand{UID: "myFolder", NewParentUID: "newFolder", OrgID: orgID, SignedInUser: nestedFolderUser}) require.ErrorIs(t, err, dashboards.ErrMoveAccessDenied) }) t.Run("move with the right permissions succeeds", func(t *testing.T) { dashStore := &dashboards.FakeDashboardStore{} dashboardFolderStore := foldertest.NewFakeFolderStore(t) nestedFolderStore := NewFakeStore() nestedFolderStore.ExpectedFolder = &folder.Folder{UID: "myFolder", ParentUID: "newFolder"} nestedFolderStore.ExpectedParentFolders = []*folder.Folder{ {UID: "newFolder", ParentUID: "newFolder"}, {UID: "newFolder2", ParentUID: "newFolder2"}, {UID: "newFolder3", ParentUID: "newFolder3"}, } nestedFolderUser := &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{}} nestedFolderUser.Permissions[orgID] = map[string][]string{ dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("myFolder"), dashboards.ScopeFoldersProvider.GetResourceScopeUID("newFolder")}, } features := featuremgmt.WithFeatures("nestedFolders") folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, features, acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()), dbtest.NewFakeDB()) _, err := folderSvc.Move(context.Background(), &folder.MoveFolderCommand{UID: "myFolder", NewParentUID: "newFolder", OrgID: orgID, SignedInUser: nestedFolderUser}) require.NoError(t, err) // Parent write access check will eventually be replaced with scoped folder creation check nestedFolderUser.Permissions[orgID] = map[string][]string{ dashboards.ActionFoldersCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("myFolder"), dashboards.ScopeFoldersProvider.GetResourceScopeUID("newFolder2")}, } _, err = folderSvc.Move(context.Background(), &folder.MoveFolderCommand{UID: "myFolder", NewParentUID: "newFolder2", OrgID: orgID, SignedInUser: nestedFolderUser}) require.NoError(t, err) }) t.Run("cannot move the k6 folder even when has permissions to move folders", func(t *testing.T) { nestedFolderUser := &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{}} nestedFolderUser.Permissions[orgID] = map[string][]string{dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}} features := featuremgmt.WithFeatures("nestedFolders") folderSvc := setup(t, &dashboards.FakeDashboardStore{}, foldertest.NewFakeFolderStore(t), NewFakeStore(), features, acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()), dbtest.NewFakeDB()) _, err := folderSvc.Move(context.Background(), &folder.MoveFolderCommand{UID: accesscontrol.K6FolderUID, NewParentUID: "newFolder", OrgID: orgID, SignedInUser: nestedFolderUser}) require.Error(t, err, folder.ErrBadRequest) }) t.Run("cannot move a k6 subfolder even when has permissions to move folders", func(t *testing.T) { nestedFolderUser := &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{}} nestedFolderUser.Permissions[orgID] = map[string][]string{dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}} childUID := "k6-app-child" nestedFolderStore := NewFakeStore() nestedFolderStore.ExpectedFolder = &folder.Folder{ OrgID: orgID, UID: childUID, ParentUID: accesscontrol.K6FolderUID, } features := featuremgmt.WithFeatures("nestedFolders") folderSvc := setup(t, &dashboards.FakeDashboardStore{}, foldertest.NewFakeFolderStore(t), nestedFolderStore, features, acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()), dbtest.NewFakeDB()) _, err := folderSvc.Move(context.Background(), &folder.MoveFolderCommand{UID: childUID, NewParentUID: "newFolder", OrgID: orgID, SignedInUser: nestedFolderUser}) require.Error(t, err, folder.ErrBadRequest) }) t.Run("move to the root folder without folder creation permissions fails", func(t *testing.T) { dashStore := &dashboards.FakeDashboardStore{} dashboardFolderStore := foldertest.NewFakeFolderStore(t) nestedFolderStore := NewFakeStore() nestedFolderStore.ExpectedFolder = &folder.Folder{UID: "myFolder", ParentUID: "newFolder"} nestedFolderUser := &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{}} nestedFolderUser.Permissions[orgID] = map[string][]string{dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("")}} features := featuremgmt.WithFeatures("nestedFolders") folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, features, acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()), dbtest.NewFakeDB()) _, err := folderSvc.Move(context.Background(), &folder.MoveFolderCommand{UID: "myFolder", NewParentUID: "", OrgID: orgID, SignedInUser: nestedFolderUser}) require.Error(t, err, dashboards.ErrFolderAccessDenied) }) t.Run("move to the root folder with root folder creation permissions succeeds", func(t *testing.T) { dashStore := &dashboards.FakeDashboardStore{} dashboardFolderStore := foldertest.NewFakeFolderStore(t) nestedFolderStore := NewFakeStore() nestedFolderStore.ExpectedFolder = &folder.Folder{UID: "myFolder", ParentUID: "newFolder"} nestedFolderStore.ExpectedParentFolders = []*folder.Folder{ {UID: "newFolder", ParentUID: "newFolder"}, {UID: "newFolder2", ParentUID: "newFolder2"}, {UID: "newFolder3", ParentUID: "newFolder3"}, } nestedFolderUser := &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{}} nestedFolderUser.Permissions[orgID] = map[string][]string{ dashboards.ActionFoldersCreate: { dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID), dashboards.ScopeFoldersProvider.GetResourceScopeUID("myFolder"), }, } features := featuremgmt.WithFeatures("nestedFolders") folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, features, acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()), dbtest.NewFakeDB()) _, err := folderSvc.Move(context.Background(), &folder.MoveFolderCommand{UID: "myFolder", NewParentUID: "", OrgID: orgID, SignedInUser: nestedFolderUser}) require.NoError(t, err) // the folder is set inside InTransaction() but the fake one is called // require.NotNil(t, f) }) t.Run("move to the root folder with only subfolder creation permissions fails", func(t *testing.T) { dashStore := &dashboards.FakeDashboardStore{} dashboardFolderStore := foldertest.NewFakeFolderStore(t) nestedFolderStore := NewFakeStore() nestedFolderUser := &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{}} nestedFolderUser.Permissions[orgID] = map[string][]string{dashboards.ActionFoldersCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("some_subfolder")}} features := featuremgmt.WithFeatures("nestedFolders") folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, features, acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()), dbtest.NewFakeDB()) _, err := folderSvc.Move(context.Background(), &folder.MoveFolderCommand{UID: "myFolder", NewParentUID: "", OrgID: orgID, SignedInUser: nestedFolderUser}) require.Error(t, err) }) t.Run("move when parentUID in the current subtree returns error from nested folder service", func(t *testing.T) { g := guardian.New guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true}) t.Cleanup(func() { guardian.New = g }) dashStore := &dashboards.FakeDashboardStore{} dashboardFolderStore := foldertest.NewFakeFolderStore(t) nestedFolderStore := NewFakeStore() nestedFolderStore.ExpectedFolder = &folder.Folder{UID: "myFolder", ParentUID: "newFolder"} nestedFolderStore.ExpectedError = folder.ErrCircularReference folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), actest.FakeAccessControl{ ExpectedEvaluate: true, }, dbtest.NewFakeDB()) f, err := folderSvc.Move(context.Background(), &folder.MoveFolderCommand{UID: "myFolder", NewParentUID: "newFolder", OrgID: orgID, SignedInUser: usr}) require.Error(t, err, folder.ErrCircularReference) require.Nil(t, f) }) t.Run("move when new parentUID depth + subTree height bypassed maximum depth returns error", func(t *testing.T) { g := guardian.New guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true}) t.Cleanup(func() { guardian.New = g }) dashStore := &dashboards.FakeDashboardStore{} dashboardFolderStore := foldertest.NewFakeFolderStore(t) nestedFolderStore := NewFakeStore() nestedFolderStore.ExpectedFolder = &folder.Folder{UID: "myFolder", ParentUID: "newFolder"} nestedFolderStore.ExpectedParentFolders = []*folder.Folder{ {UID: "newFolder", ParentUID: "newFolder"}, {UID: "newFolder2", ParentUID: "newFolder2"}, } nestedFolderStore.ExpectedFolderHeight = 5 folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), actest.FakeAccessControl{ ExpectedEvaluate: true, }, dbtest.NewFakeDB()) f, err := folderSvc.Move(context.Background(), &folder.MoveFolderCommand{UID: "myFolder", NewParentUID: "newFolder2", OrgID: orgID, SignedInUser: usr}) require.Error(t, err, folder.ErrMaximumDepthReached) require.Nil(t, f) }) t.Run("move when parentUID in the current subtree returns error from nested folder service", func(t *testing.T) { g := guardian.New guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true}) t.Cleanup(func() { guardian.New = g }) dashStore := &dashboards.FakeDashboardStore{} dashboardFolderStore := foldertest.NewFakeFolderStore(t) nestedFolderStore := NewFakeStore() nestedFolderStore.ExpectedFolder = &folder.Folder{UID: "myFolder", ParentUID: "newFolder"} nestedFolderStore.ExpectedParentFolders = []*folder.Folder{{UID: "myFolder", ParentUID: "12345"}, {UID: "12345", ParentUID: ""}} folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), actest.FakeAccessControl{ ExpectedEvaluate: true, }, dbtest.NewFakeDB()) f, err := folderSvc.Move(context.Background(), &folder.MoveFolderCommand{UID: "myFolder", NewParentUID: "newFolder2", OrgID: orgID, SignedInUser: usr}) require.Error(t, err, folder.ErrCircularReference) require.Nil(t, f) }) t.Run("create returns error if maximum depth reached", func(t *testing.T) { // This test creates and deletes the dashboard, so needs some extra setup. g := guardian.New guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) t.Cleanup(func() { guardian.New = g }) // dashboard store commands that should be called. dashStore := &dashboards.FakeDashboardStore{} dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*dashboards.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil).Times(2) dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(&dashboards.Dashboard{}, nil) dashboardFolderStore := foldertest.NewFakeFolderStore(t) dashboardFolderStore.On("GetFolderByUID", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")).Return(&folder.Folder{}, nil) parents := make([]*folder.Folder, 0, folder.MaxNestedFolderDepth) for i := 0; i < folder.MaxNestedFolderDepth; i++ { parents = append(parents, &folder.Folder{UID: fmt.Sprintf("folder%d", i)}) } nestedFolderStore := NewFakeStore() //nestedFolderStore.ExpectedFolder = &folder.Folder{UID: "myFolder", ParentUID: "newFolder"} nestedFolderStore.ExpectedParentFolders = parents db, _ := sqlstore.InitTestDB(t) folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), actest.FakeAccessControl{ ExpectedEvaluate: true, }, db) _, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ Title: "folder", OrgID: orgID, ParentUID: parents[len(parents)-1].UID, UID: util.GenerateShortUID(), SignedInUser: usr, }) assert.ErrorIs(t, err, folder.ErrMaximumDepthReached) }) t.Run("get default folder, no error", func(t *testing.T) { g := guardian.New guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) t.Cleanup(func() { guardian.New = g }) // dashboard store commands that should be called. dashStore := &dashboards.FakeDashboardStore{} dashboardFolderStore := foldertest.NewFakeFolderStore(t) nestedFolderStore := NewFakeStore() nestedFolderStore.ExpectedError = folder.ErrFolderNotFound folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), actest.FakeAccessControl{ ExpectedEvaluate: true, }, dbtest.NewFakeDB()) _, err := folderSvc.Get(context.Background(), &folder.GetFolderQuery{ OrgID: orgID, ID: &folder.GeneralFolder.ID, // nolint:staticcheck SignedInUser: usr, }) require.NoError(t, err) }) }) } func TestIntegrationNestedFolderSharedWithMe(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } db, cfg := sqlstore.InitTestReplDB(t) quotaService := quotatest.New(false, nil) folderStore := ProvideDashboardFolderStore(db) featuresFlagOn := featuremgmt.WithFeatures("nestedFolders") dashStore, err := database.ProvideDashboardStore(db, cfg, featuresFlagOn, tagimpl.ProvideService(db), quotaService) require.NoError(t, err) nestedFolderStore := ProvideStore(db) b := bus.ProvideBus(tracing.InitializeTracerForTest()) ac := acimpl.ProvideAccessControl(featuresFlagOn, zanzana.NewNoopClient()) serviceWithFlagOn := &Service{ log: slog.New(logtest.NewTestHandler(t)).With("logger", "test-folder-service"), dashboardStore: dashStore, dashboardFolderStore: folderStore, store: nestedFolderStore, features: featuresFlagOn, bus: b, db: db, accessControl: ac, registry: make(map[string]folder.RegistryService), metrics: newFoldersMetrics(nil), tracer: tracing.InitializeTracerForTest(), } dashboardPermissions := acmock.NewMockedPermissionsService() dashboardService, err := dashboardservice.ProvideDashboardServiceImpl( cfg, dashStore, folderStore, featuresFlagOn, acmock.NewMockedPermissionsService(), dashboardPermissions, actest.FakeAccessControl{}, serviceWithFlagOn, nil, ) require.NoError(t, err) signedInUser := user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{ orgID: { dashboards.ActionFoldersRead: {}, }, }} signedInAdminUser := user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{ orgID: { dashboards.ActionFoldersCreate: {}, dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll}, dashboards.ActionFoldersRead: {dashboards.ScopeFoldersAll}, }, }} createCmd := folder.CreateFolderCommand{ OrgID: orgID, ParentUID: "", SignedInUser: &signedInAdminUser, } guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ CanSaveValue: true, CanViewValue: true, }) t.Run("Should get folders shared with given user", func(t *testing.T) { depth := 3 ancestorFoldersWithPermissions := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "withPermissions", createCmd) ancestorFoldersWithoutPermissions := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "withoutPermissions", createCmd) parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorFoldersWithoutPermissions[0].UID) require.NoError(t, err) subfolder, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorFoldersWithoutPermissions[1].UID) require.NoError(t, err) // nolint:staticcheck dash1 := insertTestDashboard(t, serviceWithFlagOn.dashboardStore, "dashboard in parent", orgID, parent.ID, parent.UID, "prod") // nolint:staticcheck dash2 := insertTestDashboard(t, serviceWithFlagOn.dashboardStore, "dashboard in subfolder", orgID, subfolder.ID, subfolder.UID, "prod") signedInUser.Permissions[orgID][dashboards.ActionFoldersRead] = []string{ dashboards.ScopeFoldersProvider.GetResourceScopeUID(ancestorFoldersWithPermissions[0].UID), // Add permission to the subfolder of folder with permission (to check deduplication) dashboards.ScopeFoldersProvider.GetResourceScopeUID(ancestorFoldersWithPermissions[1].UID), // Add permission to the subfolder of folder without permission dashboards.ScopeFoldersProvider.GetResourceScopeUID(ancestorFoldersWithoutPermissions[1].UID), } signedInUser.Permissions[orgID][dashboards.ActionDashboardsRead] = []string{ dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dash1.UID), dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dash2.UID), } getSharedCmd := folder.GetChildrenQuery{ UID: folder.SharedWithMeFolderUID, OrgID: orgID, SignedInUser: &signedInUser, } sharedFolders, err := serviceWithFlagOn.GetChildren(context.Background(), &getSharedCmd) sharedFoldersUIDs := make([]string, 0) for _, f := range sharedFolders { sharedFoldersUIDs = append(sharedFoldersUIDs, f.UID) } require.NoError(t, err) require.Len(t, sharedFolders, 1) require.Contains(t, sharedFoldersUIDs, ancestorFoldersWithoutPermissions[1].UID) require.NotContains(t, sharedFoldersUIDs, ancestorFoldersWithPermissions[1].UID) sharedDashboards, err := dashboardService.GetDashboardsSharedWithUser(context.Background(), &signedInUser) sharedDashboardsUIDs := make([]string, 0) for _, d := range sharedDashboards { sharedDashboardsUIDs = append(sharedDashboardsUIDs, d.UID) } require.NoError(t, err) require.Len(t, sharedDashboards, 1) require.Contains(t, sharedDashboardsUIDs, dash1.UID) require.NotContains(t, sharedDashboardsUIDs, dash2.UID) t.Cleanup(func() { //guardian.New = origNewGuardian toDelete := make([]string, 0, len(ancestorFoldersWithPermissions)+len(ancestorFoldersWithoutPermissions)) for _, ancestor := range append(ancestorFoldersWithPermissions, ancestorFoldersWithoutPermissions...) { toDelete = append(toDelete, ancestor.UID) } err := serviceWithFlagOn.store.Delete(context.Background(), toDelete, orgID) assert.NoError(t, err) }) }) t.Run("Should not list k6 folders or subfolders", func(t *testing.T) { _, err = nestedFolderStore.Create(context.Background(), folder.CreateFolderCommand{ UID: accesscontrol.K6FolderUID, OrgID: orgID, SignedInUser: &signedInAdminUser, }) require.NoError(t, err) k6ChildFolder, err := nestedFolderStore.Create(context.Background(), folder.CreateFolderCommand{ UID: "k6-app-child", ParentUID: accesscontrol.K6FolderUID, OrgID: orgID, SignedInUser: &signedInAdminUser, }) require.NoError(t, err) unrelatedFolder, err := nestedFolderStore.Create(context.Background(), folder.CreateFolderCommand{ UID: "another-folder", OrgID: orgID, SignedInUser: &signedInAdminUser, }) require.NoError(t, err) folders, err := serviceWithFlagOn.GetFolders(context.Background(), folder.GetFoldersQuery{ OrgID: orgID, SignedInUser: &signedInAdminUser, }) require.NoError(t, err) assert.Equal(t, 1, len(folders), "should not return k6 folders or subfolders") assert.Equal(t, unrelatedFolder.UID, folders[0].UID) // Service accounts should be able to list k6 folders svcAccountUser := user.SignedInUser{UserID: 2, IsServiceAccount: true, OrgID: orgID, Permissions: map[int64]map[string][]string{ orgID: { dashboards.ActionFoldersRead: {dashboards.ScopeFoldersAll}, }, }} folders, err = serviceWithFlagOn.GetFolders(context.Background(), folder.GetFoldersQuery{ OrgID: orgID, SignedInUser: &svcAccountUser, }) require.NoError(t, err) assert.Equal(t, 3, len(folders), "service accounts should be able to list k6 folders") t.Cleanup(func() { //guardian.New = origNewGuardian toDelete := []string{k6ChildFolder.UID, accesscontrol.K6FolderUID, unrelatedFolder.UID} err := serviceWithFlagOn.store.Delete(context.Background(), toDelete, orgID) assert.NoError(t, err) }) }) t.Run("Should get org folders visible", func(t *testing.T) { depth := 3 // create folder sctructure like this: // tree1-folder-0 // └──tree1-folder-1 // └──tree1-folder-2 // tree2-folder-0 // └──tree2-folder-1 // └──tree2-folder-2 tree1 := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "tree1-", createCmd) tree2 := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "tree2-", createCmd) signedInUser.Permissions[orgID][dashboards.ActionFoldersRead] = []string{ // Add permission to tree1-folder-0 dashboards.ScopeFoldersProvider.GetResourceScopeUID(tree1[0].UID), // Add permission to the subfolder of folder with permission (tree1-folder-1) to check deduplication dashboards.ScopeFoldersProvider.GetResourceScopeUID(tree1[1].UID), // Add permission to the subfolder of folder without permission (tree2-folder-1) dashboards.ScopeFoldersProvider.GetResourceScopeUID(tree2[1].UID), } t.Cleanup(func() { toDelete := make([]string, 0, len(tree1)+len(tree2)) for _, f := range append(tree1, tree2...) { toDelete = append(toDelete, f.UID) } err := serviceWithFlagOn.store.Delete(context.Background(), toDelete, orgID) assert.NoError(t, err) }) testCases := []struct { name string cmd folder.GetFoldersQuery expected []*folder.Folder }{ { name: "Should get all org folders visible to the user", cmd: folder.GetFoldersQuery{ OrgID: orgID, SignedInUser: &signedInUser, }, expected: []*folder.Folder{ { UID: tree1[0].UID, }, { UID: tree1[1].UID, }, { UID: tree1[2].UID, }, { UID: tree2[1].UID, }, { UID: tree2[2].UID, }, }, }, { name: "Should get all org folders visible to the user with fullpath", cmd: folder.GetFoldersQuery{ OrgID: orgID, WithFullpath: true, SignedInUser: &signedInUser, }, expected: []*folder.Folder{ { UID: tree1[0].UID, Fullpath: "tree1-folder-0", }, { UID: tree1[1].UID, Fullpath: "tree1-folder-0/tree1-folder-1", }, { UID: tree1[2].UID, Fullpath: "tree1-folder-0/tree1-folder-1/tree1-folder-2", }, { UID: tree2[1].UID, Fullpath: "tree2-folder-0/tree2-folder-1", }, { UID: tree2[2].UID, Fullpath: "tree2-folder-0/tree2-folder-1/tree2-folder-2", }, }, }, { name: "Should get all org folders visible to the user with fullpath UIDs", cmd: folder.GetFoldersQuery{ OrgID: orgID, WithFullpathUIDs: true, SignedInUser: &signedInUser, }, expected: []*folder.Folder{ { UID: tree1[0].UID, FullpathUIDs: strings.Join([]string{tree1[0].UID}, "/"), }, { UID: tree1[1].UID, FullpathUIDs: strings.Join([]string{tree1[0].UID, tree1[1].UID}, "/"), }, { UID: tree1[2].UID, FullpathUIDs: strings.Join([]string{tree1[0].UID, tree1[1].UID, tree1[2].UID}, "/"), }, { UID: tree2[1].UID, FullpathUIDs: strings.Join([]string{tree2[0].UID, tree2[1].UID}, "/"), }, { UID: tree2[2].UID, FullpathUIDs: strings.Join([]string{tree2[0].UID, tree2[1].UID, tree2[2].UID}, "/"), }, }, }, { name: "Should get specific org folders visible to the user", cmd: folder.GetFoldersQuery{ OrgID: orgID, UIDs: []string{tree1[0].UID, tree2[0].UID, tree2[1].UID}, SignedInUser: &signedInUser, }, expected: []*folder.Folder{ { UID: tree1[0].UID, }, { UID: tree2[1].UID, }, }, }, { name: "Should get all org folders visible to the user with admin permissions", cmd: folder.GetFoldersQuery{ OrgID: orgID, SignedInUser: &signedInAdminUser, }, expected: []*folder.Folder{ { UID: tree1[0].UID, Fullpath: "tree1-folder-0", FullpathUIDs: strings.Join([]string{tree1[0].UID}, "/"), }, { UID: tree1[1].UID, Fullpath: "tree1-folder-0/tree1-folder-1", FullpathUIDs: strings.Join([]string{tree1[0].UID, tree1[1].UID}, "/"), }, { UID: tree1[2].UID, Fullpath: "tree1-folder-0/tree1-folder-1/tree1-folder-2", }, { UID: tree2[0].UID, Fullpath: "tree2-folder-0", FullpathUIDs: strings.Join([]string{tree2[0].UID}, "/"), }, { UID: tree2[1].UID, Fullpath: "tree2-folder-0/tree2-folder-1", FullpathUIDs: strings.Join([]string{tree2[0].UID, tree2[1].UID}, "/"), }, { UID: tree2[2].UID, Fullpath: "tree2-folder-0/tree2-folder-1/tree2-folder-2", FullpathUIDs: strings.Join([]string{tree2[0].UID, tree2[1].UID, tree2[2].UID}, "/"), }, }, }, { name: "Should not get any folders if user has no permissions", cmd: folder.GetFoldersQuery{ OrgID: orgID, SignedInUser: &user.SignedInUser{UserID: 999, OrgID: orgID, Permissions: map[int64]map[string][]string{ orgID: {}, }}, }, expected: nil, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { actualFolders, err := serviceWithFlagOn.GetFolders(context.Background(), tc.cmd) require.NoError(t, err) require.NoError(t, err) require.Len(t, actualFolders, len(tc.expected)) for _, expected := range tc.expected { var actualFolder *folder.Folder for _, f := range actualFolders { if f.UID == expected.UID { actualFolder = f break } } if actualFolder == nil { t.Fatalf("expected folder with UID %s not found", expected.UID) } if tc.cmd.WithFullpath { require.Equal(t, expected.Fullpath, actualFolder.Fullpath) } else { require.Empty(t, actualFolder.Fullpath) } if tc.cmd.WithFullpathUIDs { require.Equal(t, expected.FullpathUIDs, actualFolder.FullpathUIDs) } else { require.Empty(t, actualFolder.FullpathUIDs) } } }) } }) } func TestFolderServiceGetFolder(t *testing.T) { db, _ := sqlstore.InitTestReplDB(t) signedInAdminUser := user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{ orgID: { dashboards.ActionFoldersCreate: {}, dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll}, dashboards.ActionFoldersRead: {dashboards.ScopeFoldersAll}, }, }} guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ CanSaveValue: true, CanViewValue: true, }) getSvc := func(features featuremgmt.FeatureToggles) Service { quotaService := quotatest.New(false, nil) folderStore := ProvideDashboardFolderStore(db) cfg := setting.NewCfg() featuresFlagOff := featuremgmt.WithFeatures() dashStore, err := database.ProvideDashboardStore(db, cfg, featuresFlagOff, tagimpl.ProvideService(db), quotaService) require.NoError(t, err) nestedFolderStore := ProvideStore(db) b := bus.ProvideBus(tracing.InitializeTracerForTest()) ac := acimpl.ProvideAccessControl(featuresFlagOff, zanzana.NewNoopClient()) return Service{ log: slog.New(logtest.NewTestHandler(t)).With("logger", "test-folder-service"), dashboardStore: dashStore, dashboardFolderStore: folderStore, store: nestedFolderStore, features: features, bus: b, db: db, accessControl: ac, registry: make(map[string]folder.RegistryService), metrics: newFoldersMetrics(nil), } } folderSvcOn := getSvc(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)) folderSvcOff := getSvc(featuremgmt.WithFeatures()) createCmd := folder.CreateFolderCommand{ OrgID: orgID, ParentUID: "", SignedInUser: &signedInAdminUser, } depth := 3 folders := CreateSubtreeInStore(t, folderSvcOn.store, &folderSvcOn, depth, "get/folder-", createCmd) f := folders[1] testCases := []struct { name string svc *Service WithFullpath bool expectedFullpath string }{ { name: "when flag is off", svc: &folderSvcOff, expectedFullpath: f.Title, }, { name: "when flag is on and WithFullpath is false", svc: &folderSvcOn, WithFullpath: false, expectedFullpath: "", }, { name: "when flag is on and WithFullpath is true", svc: &folderSvcOn, WithFullpath: true, expectedFullpath: "get\\/folder-folder-0/get\\/folder-folder-1", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { q := folder.GetFolderQuery{ OrgID: orgID, UID: &f.UID, WithFullpath: tc.WithFullpath, SignedInUser: &signedInAdminUser, } fldr, err := tc.svc.Get(context.Background(), &q) require.NoError(t, err) require.Equal(t, f.UID, fldr.UID) require.Equal(t, tc.expectedFullpath, fldr.Fullpath) }) } } func TestFolderServiceGetFolders(t *testing.T) { db, cfg := sqlstore.InitTestReplDB(t) quotaService := quotatest.New(false, nil) folderStore := ProvideDashboardFolderStore(db) featuresFlagOff := featuremgmt.WithFeatures() dashStore, err := database.ProvideDashboardStore(db, cfg, featuresFlagOff, tagimpl.ProvideService(db), quotaService) require.NoError(t, err) nestedFolderStore := ProvideStore(db) b := bus.ProvideBus(tracing.InitializeTracerForTest()) ac := acimpl.ProvideAccessControl(featuresFlagOff, zanzana.NewNoopClient()) serviceWithFlagOff := &Service{ log: slog.New(logtest.NewTestHandler(t)).With("logger", "test-folder-service"), dashboardStore: dashStore, dashboardFolderStore: folderStore, store: nestedFolderStore, features: featuresFlagOff, bus: b, db: db, accessControl: ac, registry: make(map[string]folder.RegistryService), metrics: newFoldersMetrics(nil), tracer: tracing.InitializeTracerForTest(), } signedInAdminUser := user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{ orgID: { dashboards.ActionFoldersCreate: {}, dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll}, dashboards.ActionFoldersRead: {dashboards.ScopeFoldersAll}, }, }} createCmd := folder.CreateFolderCommand{ OrgID: orgID, ParentUID: "", SignedInUser: &signedInAdminUser, } guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ CanSaveValue: true, CanViewValue: true, }) prefix := "getfolders/ff/off" folders := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOff, 5, prefix, createCmd) f := folders[rand.Intn(len(folders))] t.Run("when flag is off", func(t *testing.T) { t.Run("full path should be a title", func(t *testing.T) { q := folder.GetFoldersQuery{ OrgID: orgID, WithFullpath: true, WithFullpathUIDs: true, SignedInUser: &signedInAdminUser, UIDs: []string{f.UID}, } fldrs, err := serviceWithFlagOff.GetFolders(context.Background(), q) require.NoError(t, err) require.Len(t, fldrs, 1) require.Equal(t, f.UID, fldrs[0].UID) require.Equal(t, f.Title, fldrs[0].Title) require.Equal(t, f.Title, fldrs[0].Fullpath) t.Run("path should not be escaped", func(t *testing.T) { require.Contains(t, fldrs[0].Fullpath, prefix) require.Contains(t, fldrs[0].Title, prefix) }) }) }) } // TODO replace it with an API test under /pkg/tests/api/folders // whenever the golang client with get updated to allow filtering child folders by permission func TestGetChildrenFilterByPermission(t *testing.T) { db, cfg := sqlstore.InitTestReplDB(t) signedInAdminUser := user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{ orgID: { dashboards.ActionFoldersCreate: {dashboards.ScopeFoldersAll}, dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll}, dashboards.ActionFoldersRead: {dashboards.ScopeFoldersAll}, }, }} quotaService := quotatest.New(false, nil) folderStore := ProvideDashboardFolderStore(db) featuresFlagOff := featuremgmt.WithFeatures() dashStore, err := database.ProvideDashboardStore(db, cfg, featuresFlagOff, tagimpl.ProvideService(db), quotaService) require.NoError(t, err) nestedFolderStore := ProvideStore(db) b := bus.ProvideBus(tracing.InitializeTracerForTest()) ac := acimpl.ProvideAccessControl(featuresFlagOff, zanzana.NewNoopClient()) features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders) folderSvcOn := &Service{ log: slog.New(logtest.NewTestHandler(t)).With("logger", "test-folder-service"), dashboardStore: dashStore, dashboardFolderStore: folderStore, store: nestedFolderStore, features: features, bus: b, db: db, accessControl: ac, registry: make(map[string]folder.RegistryService), metrics: newFoldersMetrics(nil), tracer: tracing.InitializeTracerForTest(), } origGuardian := guardian.New fakeGuardian := &guardian.FakeDashboardGuardian{ CanSaveValue: true, CanEditUIDs: []string{}, CanViewUIDs: []string{}, } guardian.MockDashboardGuardian(fakeGuardian) t.Cleanup(func() { guardian.New = origGuardian }) viewer := user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{ orgID: { dashboards.ActionFoldersRead: {}, dashboards.ActionFoldersWrite: {}, }, }} // no view permission // |_ subfolder under no view permission with view permission // |_ subfolder under no view permission with view permissionn and with edit permission // with edit permission // |_ subfolder under with edit permission // no edit permission // |_ subfolder under no edit permission // |_ subfolder under no edit permission with edit permission noViewPermission, err := folderSvcOn.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, ParentUID: "", Title: "no view permission", SignedInUser: &signedInAdminUser, }) require.NoError(t, err) f, err := folderSvcOn.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, ParentUID: noViewPermission.UID, Title: "subfolder under no view permission with view permission", SignedInUser: &signedInAdminUser, }) viewer.Permissions[orgID][dashboards.ActionFoldersRead] = append(viewer.Permissions[orgID][dashboards.ActionFoldersRead], dashboards.ScopeFoldersProvider.GetResourceScopeUID(f.UID)) fakeGuardian.CanViewUIDs = append(fakeGuardian.CanViewUIDs, f.UID) require.NoError(t, err) f, err = folderSvcOn.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, ParentUID: noViewPermission.UID, Title: "subfolder under no view permission with view permission and with edit permission", SignedInUser: &signedInAdminUser, }) require.NoError(t, err) viewer.Permissions[orgID][dashboards.ActionFoldersRead] = append(viewer.Permissions[orgID][dashboards.ActionFoldersRead], dashboards.ScopeFoldersProvider.GetResourceScopeUID(f.UID)) fakeGuardian.CanViewUIDs = append(fakeGuardian.CanViewUIDs, f.UID) viewer.Permissions[orgID][dashboards.ActionFoldersWrite] = append(viewer.Permissions[orgID][dashboards.ActionFoldersWrite], dashboards.ScopeFoldersProvider.GetResourceScopeUID(f.UID)) fakeGuardian.CanEditUIDs = append(fakeGuardian.CanEditUIDs, f.UID) withEditPermission, err := folderSvcOn.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, ParentUID: "", Title: "with edit permission", SignedInUser: &signedInAdminUser, }) require.NoError(t, err) viewer.Permissions[orgID][dashboards.ActionFoldersRead] = append(viewer.Permissions[orgID][dashboards.ActionFoldersRead], dashboards.ScopeFoldersProvider.GetResourceScopeUID(withEditPermission.UID)) fakeGuardian.CanViewUIDs = append(fakeGuardian.CanViewUIDs, withEditPermission.UID) viewer.Permissions[orgID][dashboards.ActionFoldersWrite] = append(viewer.Permissions[orgID][dashboards.ActionFoldersWrite], dashboards.ScopeFoldersProvider.GetResourceScopeUID(withEditPermission.UID)) fakeGuardian.CanEditUIDs = append(fakeGuardian.CanEditUIDs, withEditPermission.UID) _, err = folderSvcOn.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, ParentUID: withEditPermission.UID, Title: "subfolder under with edit permission", SignedInUser: &signedInAdminUser, }) require.NoError(t, err) noEditPermission, err := folderSvcOn.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, ParentUID: "", Title: "no edit permission", SignedInUser: &signedInAdminUser, }) require.NoError(t, err) viewer.Permissions[orgID][dashboards.ActionFoldersRead] = append(viewer.Permissions[orgID][dashboards.ActionFoldersRead], dashboards.ScopeFoldersProvider.GetResourceScopeUID(noEditPermission.UID)) fakeGuardian.CanViewUIDs = append(fakeGuardian.CanViewUIDs, noEditPermission.UID) _, err = folderSvcOn.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, ParentUID: noEditPermission.UID, Title: "subfolder under no edit permission", SignedInUser: &signedInAdminUser, }) require.NoError(t, err) f, err = folderSvcOn.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, ParentUID: noEditPermission.UID, Title: "subfolder under no edit permission with edit permission", SignedInUser: &signedInAdminUser, }) require.NoError(t, err) viewer.Permissions[orgID][dashboards.ActionFoldersWrite] = append(viewer.Permissions[orgID][dashboards.ActionFoldersWrite], dashboards.ScopeFoldersProvider.GetResourceScopeUID(f.UID)) fakeGuardian.CanEditUIDs = append(fakeGuardian.CanEditUIDs, f.UID) testCases := []struct { name string q folder.GetChildrenQuery expectedErr error expectedFolders []string }{ { name: "should return root folders with view permission", q: folder.GetChildrenQuery{ OrgID: orgID, SignedInUser: &viewer, }, expectedFolders: []string{ "Shared with me", "no edit permission", "with edit permission"}, }, { name: "should return subfolders with view permission", q: folder.GetChildrenQuery{ OrgID: orgID, SignedInUser: &viewer, UID: noEditPermission.UID, }, expectedFolders: []string{ "subfolder under no edit permission", "subfolder under no edit permission with edit permission"}, }, { name: "should return shared with me folders with view permission", q: folder.GetChildrenQuery{ OrgID: orgID, SignedInUser: &viewer, UID: folder.SharedWithMeFolderUID, }, expectedFolders: []string{ "subfolder under no view permission with view permission", "subfolder under no view permission with view permission and with edit permission"}, }, { name: "should return root folders with edit permission", q: folder.GetChildrenQuery{ OrgID: orgID, SignedInUser: &viewer, Permission: dashboardaccess.PERMISSION_EDIT, }, expectedFolders: []string{ "Shared with me", "with edit permission"}, }, { name: "should fail returning subfolders with edit permission when parent folder has no edit permission", q: folder.GetChildrenQuery{ OrgID: orgID, SignedInUser: &viewer, Permission: dashboardaccess.PERMISSION_EDIT, UID: noEditPermission.UID, }, expectedErr: dashboards.ErrFolderAccessDenied, }, { name: "should return shared with me folders with edit permission", q: folder.GetChildrenQuery{ OrgID: orgID, SignedInUser: &viewer, Permission: dashboardaccess.PERMISSION_EDIT, UID: folder.SharedWithMeFolderUID, }, expectedFolders: []string{ "subfolder under no edit permission with edit permission", "subfolder under no view permission with view permission and with edit permission", }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { folders, err := folderSvcOn.GetChildren(context.Background(), &tc.q) if tc.expectedErr != nil { require.Error(t, err) require.Equal(t, tc.expectedErr, err) } else { require.NoError(t, err) actual := make([]string, 0, len(folders)) for _, f := range folders { actual = append(actual, f.Title) } if cmp.Diff(tc.expectedFolders, actual) != "" { t.Fatalf("unexpected folders: %s", cmp.Diff(tc.expectedFolders, actual)) } } }) } } func TestIntegration_canMove(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } dashStore := &dashboards.FakeDashboardStore{} dashboardFolderStore := foldertest.NewFakeFolderStore(t) db, cfg := sqlstore.InitTestDB(t) folderStore := ProvideStore(db) orgID := CreateOrg(t, db, cfg) adminUsr := &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleAdmin} // Set up source folder and a source folder parent sourceParent, err := folderStore.Create(context.Background(), folder.CreateFolderCommand{ UID: "source-parent", OrgID: orgID, Title: "Source parent", SignedInUser: adminUsr, }) require.NoError(t, err) sourceFolder, err := folderStore.Create(context.Background(), folder.CreateFolderCommand{ UID: "source", OrgID: orgID, Title: "Source", ParentUID: sourceParent.UID, SignedInUser: adminUsr, }) require.NoError(t, err) // Set up destination folder and destination folder parent destParent, err := folderStore.Create(context.Background(), folder.CreateFolderCommand{ UID: "destination-parent", OrgID: orgID, Title: "Destination parent", SignedInUser: adminUsr, }) require.NoError(t, err) destFolder, err := folderStore.Create(context.Background(), folder.CreateFolderCommand{ UID: "destination", OrgID: orgID, Title: "Destination", ParentUID: destParent.UID, SignedInUser: adminUsr, }) require.NoError(t, err) features := featuremgmt.WithFeatures("nestedFolders") folderSvc := setup(t, dashStore, dashboardFolderStore, folderStore, features, acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()), dbtest.NewFakeDB()) testCases := []struct { description string destinationFolder string permissions map[string][]string expectedErr error }{ { description: "can move a folder if has edit access to both folders", destinationFolder: destFolder.UID, permissions: map[string][]string{ dashboards.ActionFoldersWrite: { dashboards.ScopeFoldersProvider.GetResourceScopeUID(sourceFolder.UID), dashboards.ScopeFoldersProvider.GetResourceScopeUID(destFolder.UID), }, }, }, { description: "can't move a folder if missing write access to the destination folder", destinationFolder: destFolder.UID, permissions: map[string][]string{ dashboards.ActionFoldersWrite: { dashboards.ScopeFoldersProvider.GetResourceScopeUID(sourceFolder.UID), }, dashboards.ActionFoldersRead: { dashboards.ScopeFoldersProvider.GetResourceScopeUID(destFolder.UID), }, }, expectedErr: dashboards.ErrMoveAccessDenied, }, { description: "can't move a folder to the root if missing folder create permissions", destinationFolder: "", permissions: map[string][]string{ dashboards.ActionFoldersWrite: { dashboards.ScopeFoldersProvider.GetResourceScopeUID(sourceFolder.UID), }, }, expectedErr: dashboards.ErrMoveAccessDenied, }, { description: "can move a folder to the root with folder create permissions", destinationFolder: "", permissions: map[string][]string{ dashboards.ActionFoldersCreate: { dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID), dashboards.ScopeFoldersProvider.GetResourceScopeUID(sourceFolder.UID), }, }, }, { description: "can't move a folder to another folder where user has higher plugin permissions", destinationFolder: destFolder.UID, permissions: map[string][]string{ dashboards.ActionFoldersWrite: { dashboards.ScopeFoldersProvider.GetResourceScopeUID(sourceFolder.UID), dashboards.ScopeFoldersProvider.GetResourceScopeUID(destFolder.UID), }, "some_plugin:action": { dashboards.ScopeFoldersProvider.GetResourceScopeUID(destFolder.UID), }, }, expectedErr: dashboards.ErrFolderAccessEscalation, }, { description: "can move a folder to another folder where user has lower permissions", destinationFolder: destFolder.UID, permissions: map[string][]string{ dashboards.ActionFoldersWrite: { dashboards.ScopeFoldersProvider.GetResourceScopeUID(sourceFolder.UID), dashboards.ScopeFoldersProvider.GetResourceScopeUID(destFolder.UID), }, "some_plugin:action": { dashboards.ScopeFoldersProvider.GetResourceScopeUID(sourceFolder.UID), }, }, }, { description: "can't move a folder to another folder where user has higher plugin permissions through inheritance", destinationFolder: destFolder.UID, permissions: map[string][]string{ dashboards.ActionFoldersWrite: { dashboards.ScopeFoldersProvider.GetResourceScopeUID(sourceFolder.UID), dashboards.ScopeFoldersProvider.GetResourceScopeUID(destFolder.UID), }, "some_plugin:action": { dashboards.ScopeFoldersProvider.GetResourceScopeUID(destParent.UID), }, }, expectedErr: dashboards.ErrFolderAccessEscalation, }, } for _, tc := range testCases { usr := &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{}} usr.Permissions[orgID] = tc.permissions t.Run(tc.description, func(t *testing.T) { _, err := folderSvc.Move(context.Background(), &folder.MoveFolderCommand{UID: sourceFolder.UID, NewParentUID: tc.destinationFolder, OrgID: orgID, SignedInUser: usr}) if tc.expectedErr == nil { require.NoError(t, err) } else { require.ErrorIs(t, err, tc.expectedErr) } }) } } func TestSupportBundle(t *testing.T) { f := func(uid, parent string) *folder.Folder { return &folder.Folder{UID: uid, ParentUID: parent} } for _, tc := range []struct { Folders []*folder.Folder ExpectedTotal int ExpectedDepths map[int]int ExpectedChildren map[int]int }{ // Empty folder list { Folders: []*folder.Folder{}, ExpectedTotal: 0, ExpectedDepths: map[int]int{}, ExpectedChildren: map[int]int{}, }, // Single folder { Folders: []*folder.Folder{f("a", "")}, ExpectedTotal: 1, ExpectedDepths: map[int]int{1: 1}, ExpectedChildren: map[int]int{0: 1}, }, // Flat folders { Folders: []*folder.Folder{f("a", ""), f("b", ""), f("c", "")}, ExpectedTotal: 3, ExpectedDepths: map[int]int{1: 3}, ExpectedChildren: map[int]int{0: 3}, }, // Nested folders { Folders: []*folder.Folder{f("a", ""), f("ab", "a"), f("ac", "a"), f("x", ""), f("xy", "x"), f("xyz", "xy")}, ExpectedTotal: 6, ExpectedDepths: map[int]int{1: 2, 2: 3, 3: 1}, ExpectedChildren: map[int]int{0: 3, 1: 2, 2: 1}, }, } { svc := &Service{} supportItem, err := svc.supportItemFromFolders(tc.Folders) if err != nil { t.Fatal(err) } stats := struct { Total int `json:"total"` Depths map[int]int `json:"depths"` Children map[int]int `json:"children"` }{} if err := json.Unmarshal(supportItem.FileBytes, &stats); err != nil { t.Fatal(err) } if stats.Total != tc.ExpectedTotal { t.Error("Total mismatch", stats, tc) } if fmt.Sprint(stats.Depths) != fmt.Sprint(tc.ExpectedDepths) { t.Error("Depths mismatch", stats, tc.ExpectedDepths) } if fmt.Sprint(stats.Children) != fmt.Sprint(tc.ExpectedChildren) { t.Error("Depths mismatch", stats, tc.ExpectedChildren) } } } func CreateSubtreeInStore(t *testing.T, store store, service *Service, depth int, prefix string, cmd folder.CreateFolderCommand) []*folder.Folder { t.Helper() folders := make([]*folder.Folder, 0, depth) for i := 0; i < depth; i++ { title := fmt.Sprintf("%sfolder-%d", prefix, i) cmd.Title = title cmd.UID = util.GenerateShortUID() cmd.OrgID = orgID cmd.SignedInUser = &user.SignedInUser{OrgID: orgID, Permissions: map[int64]map[string][]string{orgID: {dashboards.ActionFoldersCreate: {dashboards.ScopeFoldersAll}}}} f, err := service.Create(context.Background(), &cmd) require.NoError(t, err) require.Equal(t, title, f.Title) require.NotEmpty(t, f.UID) folders = append(folders, f) cmd.ParentUID = f.UID } return folders } func setup(t *testing.T, dashStore dashboards.Store, dashboardFolderStore folder.FolderStore, nestedFolderStore store, features featuremgmt.FeatureToggles, ac accesscontrol.AccessControl, db db.DB) folder.Service { t.Helper() // nothing enabled yet return &Service{ log: slog.New(logtest.NewTestHandler(t)).With("logger", "test-folder-service"), dashboardStore: dashStore, dashboardFolderStore: dashboardFolderStore, store: nestedFolderStore, features: features, accessControl: ac, db: db, metrics: newFoldersMetrics(nil), tracer: tracing.InitializeTracerForTest(), } } func createRule(t *testing.T, store *ngstore.DBstore, folderUID, title string) *models.AlertRule { t.Helper() rule := models.AlertRule{ OrgID: orgID, NamespaceUID: folderUID, Title: title, Updated: time.Now(), UID: util.GenerateShortUID(), } err := store.SQLStore.WithDbSession(context.Background(), func(sess *db.Session) error { _, err := sess.Table(models.AlertRule{}).InsertOne(rule) if err != nil { return err } return nil }) require.NoError(t, err) return &rule } func TestSplitFullpath(t *testing.T) { tests := []struct { name string input string expected []string }{ { name: "empty string", input: "", expected: []string{}, }, { name: "root folder", input: "/", expected: []string{}, }, { name: "single folder", input: "folder", expected: []string{"folder"}, }, { name: "single folder with leading slash", input: "/folder", expected: []string{"folder"}, }, { name: "nested folder", input: "folder/subfolder/subsubfolder", expected: []string{"folder", "subfolder", "subsubfolder"}, }, { name: "escaped slashes", input: "folder\\/with\\/slashes", expected: []string{"folder/with/slashes"}, }, { name: "nested folder with escaped slashes", input: "folder\\/with\\/slashes/subfolder\\/with\\/slashes", expected: []string{"folder/with/slashes", "subfolder/with/slashes"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { actual := SplitFullpath(tt.input) assert.Equal(t, tt.expected, actual) }) } }