The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/folder/folderimpl/folder_test.go

2350 lines
90 KiB

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/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/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}
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)
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()
ac := acmock.New().WithPermissions([]accesscontrol.Permission{
{Action: accesscontrol.ActionAlertingRuleDelete, Scope: dashboards.ScopeFoldersAll},
})
alertingStore := ngstore.DBstore{
SQLStore: db,
Cfg: cfg.UnifiedAlerting,
Logger: log.New("test-alerting-store"),
AccessControl: ac,
}
service := &Service{
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),
metrics: newFoldersMetrics(nil),
registry: make(map[string]folder.RegistryService),
}
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: usr,
})
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: usr,
})
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: usr,
})
require.Equal(t, err, dashboards.ErrFolderAccessDenied)
})
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: usr,
})
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: usr,
})
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)
}
})
})
}
func TestIntegrationNestedFolderService(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
db, cfg := sqlstore.InitTestDB(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())
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),
}
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),
}
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),
}
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()),
metrics: newFoldersMetrics(nil),
}
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.InitTestDB(t)
cfg := setting.NewCfg()
features := featuremgmt.WithFeatures()
nestedFolderStore := ProvideStore(db)
dashStore, err := database.ProvideDashboardStore(db, cfg, features, tagimpl.ProvideService(db), &quotatest.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()),
metrics: newFoldersMetrics(nil),
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), db)
_, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
OrgID: orgID,
Title: dash.Title,
UID: dash.UID,
SignedInUser: usr,
})
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", 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")
db, _ := sqlstore.InitTestDB(t)
folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, features, acimpl.ProvideAccessControl(features), db)
_, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
OrgID: orgID,
Title: dash.Title,
UID: dash.UID,
SignedInUser: usr,
})
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 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), 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.ErrFolderAccessDenied)
})
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), 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)
})
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), dbtest.NewFakeDB())
_, err := folderSvc.Move(context.Background(), &folder.MoveFolderCommand{UID: "myFolder", NewParentUID: "newFolder", OrgID: orgID, SignedInUser: nestedFolderUser})
require.ErrorIs(t, err, dashboards.ErrFolderAccessDenied)
})
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("newFolder")}}
features := featuremgmt.WithFeatures("nestedFolders")
folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, features, acimpl.ProvideAccessControl(features), dbtest.NewFakeDB())
_, err := folderSvc.Move(context.Background(), &folder.MoveFolderCommand{UID: "myFolder", NewParentUID: "newFolder", 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("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), 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), 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), 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 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: {}}
features := featuremgmt.WithFeatures("nestedFolders")
folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, features, acimpl.ProvideAccessControl(features), 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 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.InitTestDB(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)
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),
}
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.InitTestDB(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)
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.InitTestDB(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)
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),
}
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.InitTestDB(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},
},
}}
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)
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),
}
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 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()
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),
}
}
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)
})
}
}