mirror of
https://github.com/grafana/grafana.git
synced 2024-11-30 04:34:23 -06:00
28de94f6a2
* Folders: Modify Get() to optionally return fullpath * Set FullPath to folder title if feature flag is off * Apply suggestion from code review
1862 lines
71 KiB
Go
1862 lines
71 KiB
Go
package folderimpl
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math/rand"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"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/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/alerting"
|
|
alertmodels "github.com/grafana/grafana/pkg/services/alerting/models"
|
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
|
"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/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) {
|
|
cfg := setting.NewCfg()
|
|
ac := acmock.New()
|
|
db := sqlstore.InitTestDB(t)
|
|
ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, nil, nil, db, &featuremgmt.FeatureManager{}, 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 := sqlstore.InitTestDB(t)
|
|
nestedFolderStore := ProvideStore(db, db.Cfg)
|
|
|
|
folderStore := foldertest.NewFakeFolderStore(t)
|
|
|
|
cfg := setting.NewCfg()
|
|
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{
|
|
cfg: cfg,
|
|
log: log.New("test-folder-service"),
|
|
dashboardStore: dashStore,
|
|
dashboardFolderStore: folderStore,
|
|
store: nestedFolderStore,
|
|
features: features,
|
|
bus: bus.ProvideBus(tracing.InitializeTracerForTest()),
|
|
db: db,
|
|
accessControl: acimpl.ProvideAccessControl(cfg),
|
|
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.Cleanup(func() {
|
|
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 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 := sqlstore.InitTestDB(t)
|
|
quotaService := quotatest.New(false, nil)
|
|
folderStore := ProvideDashboardFolderStore(db)
|
|
|
|
cfg := setting.NewCfg()
|
|
|
|
featuresFlagOn := featuremgmt.WithFeatures("nestedFolders")
|
|
dashStore, err := database.ProvideDashboardStore(db, db.Cfg, featuresFlagOn, tagimpl.ProvideService(db), quotaService)
|
|
require.NoError(t, err)
|
|
nestedFolderStore := ProvideStore(db, db.Cfg)
|
|
|
|
b := bus.ProvideBus(tracing.InitializeTracerForTest())
|
|
ac := acimpl.ProvideAccessControl(cfg)
|
|
|
|
serviceWithFlagOn := &Service{
|
|
cfg: cfg,
|
|
log: log.New("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, nil, 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
|
|
_, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd)
|
|
require.NoError(t, err)
|
|
// nolint:staticcheck
|
|
libraryElementCmd.FolderID = subfolder.ID
|
|
_, 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, db.Cfg, featuresFlagOff, tagimpl.ProvideService(db), quotaService)
|
|
require.NoError(t, err)
|
|
nestedFolderStore := ProvideStore(db, db.Cfg)
|
|
|
|
serviceWithFlagOff := &Service{
|
|
cfg: cfg,
|
|
log: log.New("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, nil, 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
|
|
_, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd)
|
|
require.NoError(t, err)
|
|
// nolint:staticcheck
|
|
libraryElementCmd.FolderID = subfolder.ID
|
|
_, 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{
|
|
cfg: cfg,
|
|
log: log.New("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, db.Cfg, tc.featuresFlag, tagimpl.ProvideService(db), quotaService)
|
|
require.NoError(t, err)
|
|
nestedFolderStore := ProvideStore(db, db.Cfg)
|
|
tc.service.dashboardStore = dashStore
|
|
tc.service.store = nestedFolderStore
|
|
|
|
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, nil, 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
|
|
subPanel, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd)
|
|
require.NoError(t, err)
|
|
}
|
|
// nolint:staticcheck
|
|
libraryElementCmd.FolderID = parent.ID
|
|
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)
|
|
|
|
cfg := setting.NewCfg()
|
|
folderService := &Service{
|
|
cfg: cfg,
|
|
store: nestedFolderStore,
|
|
db: sqlstore.InitTestDB(t),
|
|
dashboardStore: &dashStore,
|
|
dashboardFolderStore: dashboardFolderStore,
|
|
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
|
|
log: log.New("test-folder-service"),
|
|
accessControl: acimpl.ProvideAccessControl(cfg),
|
|
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, cfg)
|
|
|
|
dashStore, err := database.ProvideDashboardStore(db, cfg, features, tagimpl.ProvideService(db), "atest.FakeQuotaService{})
|
|
require.NoError(t, err)
|
|
|
|
dashboardFolderStore := ProvideDashboardFolderStore(db)
|
|
|
|
folderService := &Service{
|
|
cfg: setting.NewCfg(),
|
|
store: nestedFolderStore,
|
|
db: sqlstore.InitTestDB(t),
|
|
dashboardStore: dashStore,
|
|
dashboardFolderStore: dashboardFolderStore,
|
|
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
|
|
log: log.New("test-folder-service"),
|
|
accessControl: acimpl.ProvideAccessControl(cfg),
|
|
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()
|
|
|
|
folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures(), acimpl.ProvideAccessControl(setting.NewCfg()), sqlstore.InitTestDB(t))
|
|
_, 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()
|
|
|
|
folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), acimpl.ProvideAccessControl(setting.NewCfg()), sqlstore.InitTestDB(t))
|
|
_, 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)
|
|
|
|
folderSvc := setup(t, dashStore, nil, nil, featuremgmt.WithFeatures("nestedFolders"), acimpl.ProvideAccessControl(setting.NewCfg()), 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()
|
|
folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), acimpl.ProvideAccessControl(setting.NewCfg()), sqlstore.InitTestDB(t))
|
|
_, 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()
|
|
|
|
folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), actest.FakeAccessControl{
|
|
ExpectedEvaluate: true,
|
|
}, sqlstore.InitTestDB(t))
|
|
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,
|
|
}
|
|
|
|
folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), actest.FakeAccessControl{
|
|
ExpectedEvaluate: true,
|
|
}, sqlstore.InitTestDB(t))
|
|
_, 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
|
|
folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), actest.FakeAccessControl{
|
|
ExpectedEvaluate: true,
|
|
}, sqlstore.InitTestDB(t))
|
|
_, 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)
|
|
|
|
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")}}
|
|
|
|
folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), acimpl.ProvideAccessControl(setting.NewCfg()), 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")}}
|
|
|
|
folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), acimpl.ProvideAccessControl(setting.NewCfg()), 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("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("")}}
|
|
|
|
folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), acimpl.ProvideAccessControl(setting.NewCfg()), 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: {}}
|
|
|
|
folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), acimpl.ProvideAccessControl(setting.NewCfg()), 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
|
|
|
|
folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), actest.FakeAccessControl{
|
|
ExpectedEvaluate: true,
|
|
}, sqlstore.InitTestDB(t))
|
|
_, 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 := sqlstore.InitTestDB(t)
|
|
quotaService := quotatest.New(false, nil)
|
|
folderStore := ProvideDashboardFolderStore(db)
|
|
|
|
cfg := setting.NewCfg()
|
|
|
|
featuresFlagOn := featuremgmt.WithFeatures("nestedFolders")
|
|
dashStore, err := database.ProvideDashboardStore(db, db.Cfg, featuresFlagOn, tagimpl.ProvideService(db), quotaService)
|
|
require.NoError(t, err)
|
|
nestedFolderStore := ProvideStore(db, db.Cfg)
|
|
|
|
b := bus.ProvideBus(tracing.InitializeTracerForTest())
|
|
ac := acimpl.ProvideAccessControl(cfg)
|
|
|
|
serviceWithFlagOn := &Service{
|
|
cfg: cfg,
|
|
log: log.New("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, &dummyDashAlertExtractor{},
|
|
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 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, db.Cfg, featuresFlagOff, tagimpl.ProvideService(db), quotaService)
|
|
require.NoError(t, err)
|
|
nestedFolderStore := ProvideStore(db, db.Cfg)
|
|
|
|
b := bus.ProvideBus(tracing.InitializeTracerForTest())
|
|
ac := acimpl.ProvideAccessControl(cfg)
|
|
|
|
return Service{
|
|
cfg: cfg,
|
|
log: log.New("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 := sqlstore.InitTestDB(t)
|
|
quotaService := quotatest.New(false, nil)
|
|
folderStore := ProvideDashboardFolderStore(db)
|
|
|
|
cfg := setting.NewCfg()
|
|
|
|
featuresFlagOff := featuremgmt.WithFeatures()
|
|
dashStore, err := database.ProvideDashboardStore(db, db.Cfg, featuresFlagOff, tagimpl.ProvideService(db), quotaService)
|
|
require.NoError(t, err)
|
|
nestedFolderStore := ProvideStore(db, db.Cfg)
|
|
|
|
b := bus.ProvideBus(tracing.InitializeTracerForTest())
|
|
ac := acimpl.ProvideAccessControl(cfg)
|
|
|
|
serviceWithFlagOff := &Service{
|
|
cfg: cfg,
|
|
log: log.New("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)
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
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
|
|
cfg := setting.NewCfg()
|
|
return &Service{
|
|
cfg: cfg,
|
|
log: log.New("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
|
|
}
|
|
|
|
type dummyDashAlertExtractor struct {
|
|
}
|
|
|
|
func (d *dummyDashAlertExtractor) GetAlerts(ctx context.Context, dashAlertInfo alerting.DashAlertInfo) ([]*alertmodels.Alert, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (d *dummyDashAlertExtractor) ValidateAlerts(ctx context.Context, dashAlertInfo alerting.DashAlertInfo) error {
|
|
return nil
|
|
}
|