* introduce new fields created_by in rule tables * update domain model and compat layer to support UpdatedBy * add alert rule generator mutators for UpdatedBy * ignore UpdatedBy in diff and hash calculation * Add user context to alert rule insert/update operations Updated InsertAlertRules and UpdateAlertRules methods to accept a user context parameter. This change ensures auditability and better tracking of user actions when creating or updating alert rules. Adjusted all relevant calls and interfaces to pass the user context accordingly. * set UpdatedBy in PreSave because this is where Updated is set * Use nil userID for system-initiated updates This ensures differentiation between system and user-initiated changes for better traceability and clarity in update origins. --------- Signed-off-by: Yuri Tseretyan <yuriy.tseretyan@grafana.com>
2642 lines
102 KiB
Go
2642 lines
102 KiB
Go
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/org"
|
|
"github.com/grafana/grafana/pkg/services/publicdashboards"
|
|
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
|
"github.com/grafana/grafana/pkg/services/store/entity"
|
|
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
|
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
|
"github.com/grafana/grafana/pkg/services/user"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
"github.com/grafana/grafana/pkg/util"
|
|
)
|
|
|
|
var orgID = int64(1)
|
|
var usr = &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{orgID: {dashboards.ActionFoldersCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)}}}}
|
|
var noPermUsr = &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{}}
|
|
|
|
func TestIntegrationProvideFolderService(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test")
|
|
}
|
|
t.Run("should register scope resolvers", func(t *testing.T) {
|
|
ac := acmock.New()
|
|
db, cfg := db.InitTestDBWithCfg(t)
|
|
store := ProvideStore(db)
|
|
ProvideService(store, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), nil, nil, db,
|
|
featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest())
|
|
|
|
require.Len(t, ac.Calls.RegisterAttributeScopeResolver, 2)
|
|
})
|
|
}
|
|
|
|
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)
|
|
publicDashboardService := publicdashboards.NewFakePublicDashboardServiceWrapper(t)
|
|
features := featuremgmt.WithFeatures()
|
|
|
|
alertingStore := ngstore.DBstore{
|
|
SQLStore: db,
|
|
Cfg: cfg.UnifiedAlerting,
|
|
Logger: log.New("test-alerting-store"),
|
|
AccessControl: actest.FakeAccessControl{ExpectedEvaluate: true},
|
|
}
|
|
|
|
service := &Service{
|
|
log: slog.New(logtest.NewTestHandler(t)).With("logger", "test-folder-service"),
|
|
dashboardStore: dashStore,
|
|
dashboardFolderStore: folderStore,
|
|
store: nestedFolderStore,
|
|
publicDashboardService: publicDashboardService,
|
|
features: features,
|
|
bus: bus.ProvideBus(tracing.InitializeTracerForTest()),
|
|
db: db,
|
|
accessControl: acimpl.ProvideAccessControl(features),
|
|
metrics: newFoldersMetrics(nil),
|
|
registry: make(map[string]folder.RegistryService),
|
|
tracer: tracing.InitializeTracerForTest(),
|
|
}
|
|
|
|
require.NoError(t, service.RegisterService(alertingStore))
|
|
|
|
t.Run("Given user has no permissions", func(t *testing.T) {
|
|
origNewGuardian := guardian.New
|
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{})
|
|
|
|
folderUID := util.GenerateShortUID()
|
|
|
|
f := folder.NewFolder("Folder", "")
|
|
f.UID = folderUID
|
|
|
|
folderStore.On("GetFolderByUID", mock.Anything, orgID, folderUID).Return(f, nil)
|
|
|
|
t.Run("When get folder by id should return access denied error", func(t *testing.T) {
|
|
_, err := service.Get(context.Background(), &folder.GetFolderQuery{
|
|
UID: &folderUID,
|
|
OrgID: orgID,
|
|
SignedInUser: noPermUsr,
|
|
})
|
|
require.Equal(t, err, dashboards.ErrFolderAccessDenied)
|
|
})
|
|
|
|
t.Run("When get folder by uid should return access denied error", func(t *testing.T) {
|
|
_, err := service.Get(context.Background(), &folder.GetFolderQuery{
|
|
UID: &folderUID,
|
|
OrgID: orgID,
|
|
SignedInUser: noPermUsr,
|
|
})
|
|
require.Equal(t, err, dashboards.ErrFolderAccessDenied)
|
|
})
|
|
|
|
t.Run("When creating folder should return access denied error", func(t *testing.T) {
|
|
dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*dashboards.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil).Times(2)
|
|
_, err := service.Create(context.Background(), &folder.CreateFolderCommand{
|
|
OrgID: orgID,
|
|
Title: f.Title,
|
|
UID: folderUID,
|
|
SignedInUser: noPermUsr,
|
|
})
|
|
require.Error(t, err)
|
|
})
|
|
|
|
title := "Folder-TEST"
|
|
t.Run("When updating folder should return access denied error", func(t *testing.T) {
|
|
folderResult := dashboards.NewDashboard("dashboard-test")
|
|
folderResult.IsFolder = true
|
|
dashStore.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(folderResult, nil)
|
|
_, err := service.Update(context.Background(), &folder.UpdateFolderCommand{
|
|
UID: folderUID,
|
|
OrgID: orgID,
|
|
NewTitle: &title,
|
|
SignedInUser: noPermUsr,
|
|
})
|
|
require.Equal(t, err, dashboards.ErrFolderAccessDenied)
|
|
})
|
|
|
|
t.Run("When deleting folder by uid should return access denied error", func(t *testing.T) {
|
|
newFolder := folder.NewFolder("Folder", "")
|
|
newFolder.UID = folderUID
|
|
|
|
folderStore.On("GetFolderByUID", mock.Anything, orgID, folderUID).Return(newFolder, nil)
|
|
|
|
err := service.Delete(context.Background(), &folder.DeleteFolderCommand{
|
|
UID: folderUID,
|
|
OrgID: orgID,
|
|
ForceDeleteRules: false,
|
|
SignedInUser: noPermUsr,
|
|
})
|
|
require.Error(t, err)
|
|
require.Equal(t, err, dashboards.ErrFolderAccessDenied)
|
|
})
|
|
|
|
t.Cleanup(func() {
|
|
guardian.New = origNewGuardian
|
|
})
|
|
})
|
|
|
|
t.Run("Given user has permission to save", func(t *testing.T) {
|
|
origNewGuardian := guardian.New
|
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true})
|
|
service.features = featuremgmt.WithFeatures()
|
|
|
|
t.Run("When creating folder should not return access denied error", func(t *testing.T) {
|
|
dash := dashboards.NewDashboardFolder("Test-Folder")
|
|
dash.ID = rand.Int63()
|
|
dash.UID = util.GenerateShortUID()
|
|
f := dashboards.FromDashboard(dash)
|
|
|
|
dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*dashboards.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil)
|
|
dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(dash, nil).Once()
|
|
|
|
actualFolder, err := service.Create(context.Background(), &folder.CreateFolderCommand{
|
|
OrgID: orgID,
|
|
Title: dash.Title,
|
|
UID: dash.UID,
|
|
SignedInUser: usr,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, f, actualFolder)
|
|
})
|
|
|
|
t.Run("When creating folder should return error if uid is general", func(t *testing.T) {
|
|
dash := dashboards.NewDashboardFolder("Test-Folder")
|
|
dash.ID = rand.Int63()
|
|
|
|
_, err := service.Create(context.Background(), &folder.CreateFolderCommand{
|
|
OrgID: orgID,
|
|
Title: dash.Title,
|
|
UID: "general",
|
|
SignedInUser: usr,
|
|
})
|
|
require.ErrorIs(t, err, dashboards.ErrFolderInvalidUID)
|
|
})
|
|
|
|
t.Run("When updating folder should not return access denied error", func(t *testing.T) {
|
|
dashboardFolder := dashboards.NewDashboardFolder("Folder")
|
|
dashboardFolder.ID = rand.Int63()
|
|
dashboardFolder.UID = util.GenerateShortUID()
|
|
dashboardFolder.OrgID = orgID
|
|
|
|
f, err := service.store.Create(context.Background(), folder.CreateFolderCommand{
|
|
OrgID: orgID,
|
|
Title: dashboardFolder.Title,
|
|
UID: dashboardFolder.UID,
|
|
SignedInUser: usr,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Folder", f.Title)
|
|
|
|
dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*dashboards.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil)
|
|
title := "TEST-Folder"
|
|
updatedDashboardFolder := *dashboardFolder
|
|
updatedDashboardFolder.Title = title
|
|
dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(&updatedDashboardFolder, nil)
|
|
dashStore.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(&updatedDashboardFolder, nil)
|
|
|
|
folderStore.On("GetFolderByID", mock.Anything, orgID, dashboardFolder.ID).Return(&folder.Folder{
|
|
OrgID: orgID,
|
|
ID: dashboardFolder.ID,
|
|
UID: dashboardFolder.UID,
|
|
Title: title,
|
|
}, nil)
|
|
|
|
req := &folder.UpdateFolderCommand{
|
|
UID: dashboardFolder.UID,
|
|
OrgID: orgID,
|
|
NewTitle: &title,
|
|
SignedInUser: usr,
|
|
}
|
|
|
|
reqResult, err := service.Update(context.Background(), req)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, title, reqResult.Title)
|
|
})
|
|
|
|
t.Run("When deleting folder by uid should not return access denied error", func(t *testing.T) {
|
|
f := folder.NewFolder(util.GenerateShortUID(), "")
|
|
f.UID = util.GenerateShortUID()
|
|
folderStore.On("GetFolderByUID", mock.Anything, orgID, f.UID).Return(f, nil)
|
|
publicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
|
|
|
|
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()
|
|
dashStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{}, 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.ErrDashboardWithSameUIDExists, ExpectedError: dashboards.ErrFolderWithSameUIDExists},
|
|
{ActualError: dashboards.ErrDashboardVersionMismatch, ExpectedError: dashboards.ErrFolderVersionMismatch},
|
|
{ActualError: dashboards.ErrDashboardNotFound, ExpectedError: dashboards.ErrFolderNotFound},
|
|
{ActualError: dashboards.ErrDashboardInvalidUid, ExpectedError: dashboards.ErrDashboardInvalidUid},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
actualError := toFolderError(tc.ActualError)
|
|
assert.EqualErrorf(t, actualError, tc.ExpectedError.Error(),
|
|
"For error '%s' expected error '%s', actual '%s'", tc.ActualError, tc.ExpectedError, actualError)
|
|
}
|
|
})
|
|
|
|
t.Run("Returns root folder", func(t *testing.T) {
|
|
t.Run("When the folder UID is blank should return the root folder", func(t *testing.T) {
|
|
emptyString := ""
|
|
actual, err := service.Get(context.Background(), &folder.GetFolderQuery{
|
|
UID: &emptyString,
|
|
OrgID: 1,
|
|
SignedInUser: usr,
|
|
})
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, folder.GeneralFolder.UID, actual.UID)
|
|
assert.Equal(t, folder.GeneralFolder.Title, actual.Title)
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestIntegrationNestedFolderService(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test")
|
|
}
|
|
db, cfg := sqlstore.InitTestDB(t)
|
|
cfg.UnifiedAlerting.BaseInterval = time.Second
|
|
quotaService := quotatest.New(false, nil)
|
|
folderStore := ProvideDashboardFolderStore(db)
|
|
|
|
featuresFlagOn := featuremgmt.WithFeatures("nestedFolders")
|
|
dashStore, err := database.ProvideDashboardStore(db, cfg, featuresFlagOn, tagimpl.ProvideService(db))
|
|
require.NoError(t, err)
|
|
nestedFolderStore := ProvideStore(db)
|
|
publicDashboardFakeService := publicdashboards.NewFakePublicDashboardServiceWrapper(t)
|
|
|
|
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),
|
|
tracer: tracing.InitializeTracerForTest(),
|
|
publicDashboardService: publicDashboardFakeService,
|
|
}
|
|
|
|
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,
|
|
})
|
|
publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
|
|
|
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOn, folderPermissions, ac, serviceWithFlagOn, nestedFolderStore, nil, nil, nil, nil, quotaService, nil, publicDashboardFakeService)
|
|
require.NoError(t, err)
|
|
dashSrv.RegisterDashboardPermissions(dashboardPermissions)
|
|
|
|
alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOn, db, serviceWithFlagOn, dashSrv, ac, b)
|
|
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, true)
|
|
|
|
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))
|
|
require.NoError(t, err)
|
|
nestedFolderStore := ProvideStore(db)
|
|
|
|
serviceWithFlagOff := &Service{
|
|
log: slog.New(logtest.NewTestHandler(t)).With("logger", "test-folder-service"),
|
|
dashboardStore: dashStore,
|
|
dashboardFolderStore: folderStore,
|
|
store: nestedFolderStore,
|
|
features: featuresFlagOff,
|
|
bus: b,
|
|
db: db,
|
|
registry: make(map[string]folder.RegistryService),
|
|
metrics: newFoldersMetrics(nil),
|
|
tracer: tracing.InitializeTracerForTest(),
|
|
publicDashboardService: publicDashboardFakeService,
|
|
}
|
|
|
|
origNewGuardian := guardian.New
|
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
|
|
CanSaveValue: true,
|
|
CanViewValue: true,
|
|
// CanEditValue is required to create library elements
|
|
CanEditValue: true,
|
|
})
|
|
|
|
publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
|
|
|
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOff,
|
|
folderPermissions, ac, serviceWithFlagOff, nestedFolderStore, nil, nil, nil, nil, quotaService, nil, publicDashboardFakeService)
|
|
require.NoError(t, err)
|
|
dashSrv.RegisterDashboardPermissions(dashboardPermissions)
|
|
alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOff, db, serviceWithFlagOff, dashSrv, ac, b)
|
|
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, true)
|
|
|
|
parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestors[0].UID)
|
|
require.NoError(t, err)
|
|
subfolder, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestors[1].UID)
|
|
require.NoError(t, err)
|
|
// nolint:staticcheck
|
|
_ = insertTestDashboard(t, serviceWithFlagOn.dashboardStore, "dashboard in parent", orgID, parent.ID, parent.UID, "prod")
|
|
// nolint:staticcheck
|
|
_ = insertTestDashboard(t, serviceWithFlagOn.dashboardStore, "dashboard in subfolder", orgID, subfolder.ID, subfolder.UID, "prod")
|
|
_ = createRule(t, alertStore, parent.UID, "parent alert")
|
|
_ = createRule(t, alertStore, subfolder.UID, "sub alert")
|
|
|
|
// nolint:staticcheck
|
|
libraryElementCmd.FolderID = parent.ID
|
|
libraryElementCmd.FolderUID = &parent.UID
|
|
_, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd)
|
|
require.NoError(t, err)
|
|
// nolint:staticcheck
|
|
libraryElementCmd.FolderID = subfolder.ID
|
|
libraryElementCmd.FolderUID = &subfolder.UID
|
|
_, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd)
|
|
require.NoError(t, err)
|
|
|
|
countCmd := folder.GetDescendantCountsQuery{
|
|
UID: &ancestors[0].UID,
|
|
OrgID: orgID,
|
|
SignedInUser: &signedInUser,
|
|
}
|
|
m, err := serviceWithFlagOff.GetDescendantCounts(context.Background(), &countCmd)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(0), m[entity.StandardKindFolder])
|
|
require.Equal(t, int64(1), m[entity.StandardKindDashboard])
|
|
require.Equal(t, int64(1), m[entity.StandardKindAlertRule])
|
|
require.Equal(t, int64(1), m[entity.StandardKindLibraryPanel])
|
|
|
|
t.Cleanup(func() {
|
|
guardian.New = origNewGuardian
|
|
for _, ancestor := range ancestors {
|
|
err := serviceWithFlagOn.store.Delete(context.Background(), []string{ancestor.UID}, orgID)
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
})
|
|
})
|
|
|
|
t.Run("Should delete folders", func(t *testing.T) {
|
|
featuresFlagOff := featuremgmt.WithFeatures()
|
|
serviceWithFlagOff := &Service{
|
|
log: slog.New(logtest.NewTestHandler(t)).With("logger", "test-folder-service"),
|
|
dashboardFolderStore: folderStore,
|
|
features: featuresFlagOff,
|
|
bus: b,
|
|
db: db,
|
|
registry: make(map[string]folder.RegistryService),
|
|
metrics: newFoldersMetrics(nil),
|
|
tracer: tracing.InitializeTracerForTest(),
|
|
publicDashboardService: publicDashboardFakeService,
|
|
}
|
|
|
|
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))
|
|
require.NoError(t, err)
|
|
nestedFolderStore := ProvideStore(db)
|
|
tc.service.dashboardStore = dashStore
|
|
tc.service.store = nestedFolderStore
|
|
publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
|
|
|
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, tc.featuresFlag, folderPermissions, ac, tc.service, tc.service.store, nil, nil, nil, nil, quotaService, nil, publicDashboardFakeService)
|
|
require.NoError(t, err)
|
|
dashSrv.RegisterDashboardPermissions(dashboardPermissions)
|
|
|
|
alertStore, err := ngstore.ProvideDBStore(cfg, tc.featuresFlag, db, tc.service, dashSrv, ac, b)
|
|
require.NoError(t, err)
|
|
|
|
ancestors := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, tc.depth, tc.prefix, createCmd, true)
|
|
|
|
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 = &subfolder.UID
|
|
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 := folder.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),
|
|
tracer: tracing.InitializeTracerForTest(),
|
|
}
|
|
t.Run("create folder", func(t *testing.T) {
|
|
nestedFolderStore.ExpectedFolder = &folder.Folder{ParentUID: util.GenerateShortUID()}
|
|
res, err := folderService.Create(context.Background(), &folder.CreateFolderCommand{SignedInUser: usr, Title: "my folder"})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res.UID)
|
|
require.NotEmpty(t, res.ParentUID)
|
|
})
|
|
}
|
|
|
|
func TestFolderServiceDualWrite(t *testing.T) {
|
|
g := guardian.New
|
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true})
|
|
t.Cleanup(func() {
|
|
guardian.New = g
|
|
})
|
|
|
|
db, _ := sqlstore.InitTestDB(t)
|
|
cfg := setting.NewCfg()
|
|
features := featuremgmt.WithFeatures()
|
|
nestedFolderStore := ProvideStore(db)
|
|
|
|
dashStore, err := database.ProvideDashboardStore(db, cfg, features, tagimpl.ProvideService(db))
|
|
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),
|
|
tracer: tracing.InitializeTracerForTest(),
|
|
bus: bus.ProvideBus(tracing.InitializeTracerForTest()),
|
|
}
|
|
|
|
t.Run("When creating a folder it should trim leading and trailing spaces in both dashboard and folder tables", func(t *testing.T) {
|
|
f, err := folderService.Create(context.Background(), &folder.CreateFolderCommand{SignedInUser: usr, OrgID: orgID, Title: " my folder "})
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "my folder", f.Title)
|
|
|
|
dashFolder, err := dashboardFolderStore.GetFolderByUID(context.Background(), orgID, f.UID)
|
|
require.NoError(t, err)
|
|
|
|
nestedFolder, err := nestedFolderStore.Get(context.Background(), folder.GetFolderQuery{UID: &f.UID, OrgID: orgID})
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, dashFolder.Title, nestedFolder.Title)
|
|
})
|
|
|
|
t.Run("When updating a folder it should trim leading and trailing spaces in both dashboard and folder tables", func(t *testing.T) {
|
|
f, err := folderService.Create(context.Background(), &folder.CreateFolderCommand{SignedInUser: usr, OrgID: orgID, Title: "my folder 2"})
|
|
require.NoError(t, err)
|
|
|
|
f, err = folderService.Update(context.Background(), &folder.UpdateFolderCommand{SignedInUser: usr, OrgID: orgID, UID: f.UID, NewTitle: util.Pointer(" my updated folder 2 "), Version: f.Version})
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "my updated folder 2", f.Title)
|
|
|
|
dashFolder, err := dashboardFolderStore.GetFolderByUID(context.Background(), orgID, f.UID)
|
|
require.NoError(t, err)
|
|
|
|
nestedFolder, err := nestedFolderStore.Get(context.Background(), folder.GetFolderQuery{UID: &f.UID, OrgID: orgID})
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, dashFolder.Title, nestedFolder.Title)
|
|
})
|
|
}
|
|
|
|
func TestNestedFolderService(t *testing.T) {
|
|
t.Run("with feature flag unset", func(t *testing.T) {
|
|
t.Run("Should create a folder in both dashboard and folders tables", func(t *testing.T) {
|
|
g := guardian.New
|
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true})
|
|
t.Cleanup(func() {
|
|
guardian.New = g
|
|
})
|
|
|
|
// dash is needed here because folderSvc.Create expects SaveDashboard to return it
|
|
dash := dashboards.NewDashboardFolder("myFolder")
|
|
dash.ID = rand.Int63()
|
|
dash.UID = "some_uid"
|
|
|
|
// dashboard store & service commands that should be called.
|
|
dashStore := &dashboards.FakeDashboardStore{}
|
|
dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*dashboards.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil)
|
|
dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(dash, nil)
|
|
|
|
dashboardFolderStore := foldertest.NewFakeFolderStore(t)
|
|
|
|
nestedFolderStore := folder.NewFakeStore()
|
|
features := featuremgmt.WithFeatures()
|
|
|
|
db, _ := sqlstore.InitTestDB(t)
|
|
folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, features, acimpl.ProvideAccessControl(features), db)
|
|
|
|
tempUser := &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{}}
|
|
tempUser.Permissions[orgID] = map[string][]string{dashboards.ActionFoldersCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)}}
|
|
|
|
_, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
|
|
OrgID: orgID,
|
|
Title: dash.Title,
|
|
UID: dash.UID,
|
|
SignedInUser: tempUser,
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, nestedFolderStore.CreateCalled)
|
|
})
|
|
})
|
|
|
|
t.Run("with nested folder feature flag on", func(t *testing.T) {
|
|
t.Run("Should be able to create a nested folder under the root with the right permissions", func(t *testing.T) {
|
|
g := guardian.New
|
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true})
|
|
t.Cleanup(func() {
|
|
guardian.New = g
|
|
})
|
|
|
|
dash := dashboards.NewDashboardFolder("myFolder")
|
|
dash.ID = rand.Int63()
|
|
dash.UID = "some_uid"
|
|
|
|
// dashboard store commands that should be called.
|
|
dashStore := &dashboards.FakeDashboardStore{}
|
|
dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*dashboards.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil)
|
|
dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(dash, nil)
|
|
|
|
dashboardFolderStore := foldertest.NewFakeFolderStore(t)
|
|
nestedFolderStore := folder.NewFakeStore()
|
|
features := featuremgmt.WithFeatures("nestedFolders")
|
|
|
|
tempUser := &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{}}
|
|
tempUser.Permissions[orgID] = map[string][]string{dashboards.ActionFoldersCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)}}
|
|
|
|
db, _ := sqlstore.InitTestDB(t)
|
|
folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, features, acimpl.ProvideAccessControl(features), db)
|
|
_, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
|
|
OrgID: orgID,
|
|
Title: dash.Title,
|
|
UID: dash.UID,
|
|
SignedInUser: tempUser,
|
|
})
|
|
require.NoError(t, err)
|
|
// CreateFolder should also call the folder store's create method.
|
|
require.True(t, nestedFolderStore.CreateCalled)
|
|
})
|
|
|
|
t.Run("Should not be able to create a folder under the root with subfolder creation permissions", func(t *testing.T) {
|
|
g := guardian.New
|
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true})
|
|
t.Cleanup(func() {
|
|
guardian.New = g
|
|
})
|
|
|
|
// dashboard store commands that should be called.
|
|
dashStore := &dashboards.FakeDashboardStore{}
|
|
|
|
dashboardFolderStore := foldertest.NewFakeFolderStore(t)
|
|
nestedFolderStore := folder.NewFakeStore()
|
|
features := featuremgmt.WithFeatures("nestedFolders")
|
|
|
|
tempUser := &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{}}
|
|
tempUser.Permissions[orgID] = map[string][]string{dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("subfolder_uid")}}
|
|
|
|
db, _ := sqlstore.InitTestDB(t)
|
|
folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, features, acimpl.ProvideAccessControl(features), db)
|
|
_, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
|
|
OrgID: orgID,
|
|
Title: "some_folder",
|
|
UID: "some_uid",
|
|
SignedInUser: tempUser,
|
|
})
|
|
require.ErrorIs(t, err, dashboards.ErrFolderCreationAccessDenied)
|
|
})
|
|
|
|
t.Run("Should not be able to create new folder under another folder without the right permissions", func(t *testing.T) {
|
|
g := guardian.New
|
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true})
|
|
t.Cleanup(func() {
|
|
guardian.New = g
|
|
})
|
|
|
|
dash := dashboards.NewDashboardFolder("Test-Folder")
|
|
dash.ID = rand.Int63()
|
|
dash.UID = "some_uid"
|
|
|
|
tempUser := &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{}}
|
|
tempUser.Permissions[orgID] = map[string][]string{dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("wrong_uid")}}
|
|
|
|
// dashboard store commands that should be called.
|
|
dashStore := &dashboards.FakeDashboardStore{}
|
|
dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*dashboards.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil)
|
|
dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(&dashboards.Dashboard{}, nil)
|
|
|
|
features := featuremgmt.WithFeatures("nestedFolders")
|
|
folderSvc := setup(t, dashStore, nil, nil, features, acimpl.ProvideAccessControl(features), dbtest.NewFakeDB())
|
|
_, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
|
|
OrgID: orgID,
|
|
Title: dash.Title,
|
|
UID: dash.UID,
|
|
SignedInUser: tempUser,
|
|
ParentUID: "some_parent",
|
|
})
|
|
require.ErrorIs(t, err, dashboards.ErrFolderCreationAccessDenied)
|
|
})
|
|
|
|
t.Run("Should be able to create new folder under another folder with the right permissions", func(t *testing.T) {
|
|
g := guardian.New
|
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true})
|
|
t.Cleanup(func() {
|
|
guardian.New = g
|
|
})
|
|
|
|
dash := dashboards.NewDashboardFolder("Test-Folder")
|
|
dash.ID = rand.Int63()
|
|
dash.UID = "some_uid"
|
|
|
|
// dashboard store commands that should be called.
|
|
dashStore := &dashboards.FakeDashboardStore{}
|
|
dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*dashboards.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil)
|
|
dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(dash, nil)
|
|
|
|
dashboardFolderStore := foldertest.NewFakeFolderStore(t)
|
|
dashboardFolderStore.On("GetFolderByUID", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")).Return(&folder.Folder{}, nil)
|
|
|
|
nestedFolderUser := &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{}}
|
|
nestedFolderUser.Permissions[orgID] = map[string][]string{dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("some_parent")}}
|
|
|
|
nestedFolderStore := folder.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)
|
|
|
|
// Parent write access check will eventually be replaced with scoped folder creation check
|
|
nestedFolderUser.Permissions[orgID] = map[string][]string{dashboards.ActionFoldersCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("some_parent")}}
|
|
|
|
_, err = folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
|
|
OrgID: orgID,
|
|
Title: dash.Title + "2",
|
|
UID: dash.UID + "2",
|
|
SignedInUser: nestedFolderUser,
|
|
ParentUID: "some_parent",
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, nestedFolderStore.CreateCalled)
|
|
})
|
|
|
|
t.Run("create without UID, no error", func(t *testing.T) {
|
|
g := guardian.New
|
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true})
|
|
t.Cleanup(func() {
|
|
guardian.New = g
|
|
})
|
|
|
|
// dashboard store commands that should be called.
|
|
dashStore := &dashboards.FakeDashboardStore{}
|
|
dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*dashboards.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil)
|
|
dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(&dashboards.Dashboard{UID: "newUID"}, nil)
|
|
|
|
dashboardFolderStore := foldertest.NewFakeFolderStore(t)
|
|
nestedFolderStore := folder.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 := folder.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 := folder.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 := folder.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.ErrMoveAccessDenied)
|
|
})
|
|
|
|
t.Run("move with the right permissions succeeds", func(t *testing.T) {
|
|
dashStore := &dashboards.FakeDashboardStore{}
|
|
dashboardFolderStore := foldertest.NewFakeFolderStore(t)
|
|
|
|
nestedFolderStore := folder.NewFakeStore()
|
|
nestedFolderStore.ExpectedFolder = &folder.Folder{UID: "myFolder", ParentUID: "newFolder"}
|
|
nestedFolderStore.ExpectedParentFolders = []*folder.Folder{
|
|
{UID: "newFolder", ParentUID: "newFolder"},
|
|
{UID: "newFolder2", ParentUID: "newFolder2"},
|
|
{UID: "newFolder3", ParentUID: "newFolder3"},
|
|
}
|
|
|
|
nestedFolderUser := &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{}}
|
|
nestedFolderUser.Permissions[orgID] = map[string][]string{
|
|
dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("myFolder"), dashboards.ScopeFoldersProvider.GetResourceScopeUID("newFolder")},
|
|
}
|
|
|
|
features := featuremgmt.WithFeatures("nestedFolders")
|
|
folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, features, acimpl.ProvideAccessControl(features), dbtest.NewFakeDB())
|
|
_, err := folderSvc.Move(context.Background(), &folder.MoveFolderCommand{UID: "myFolder", NewParentUID: "newFolder", OrgID: orgID, SignedInUser: nestedFolderUser})
|
|
require.NoError(t, err)
|
|
|
|
// Parent write access check will eventually be replaced with scoped folder creation check
|
|
nestedFolderUser.Permissions[orgID] = map[string][]string{
|
|
dashboards.ActionFoldersCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("myFolder"), dashboards.ScopeFoldersProvider.GetResourceScopeUID("newFolder2")},
|
|
}
|
|
_, err = folderSvc.Move(context.Background(), &folder.MoveFolderCommand{UID: "myFolder", NewParentUID: "newFolder2", OrgID: orgID, SignedInUser: nestedFolderUser})
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("cannot move the k6 folder even when has permissions to move folders", func(t *testing.T) {
|
|
nestedFolderUser := &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{}}
|
|
nestedFolderUser.Permissions[orgID] = map[string][]string{dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}}
|
|
|
|
features := featuremgmt.WithFeatures("nestedFolders")
|
|
folderSvc := setup(t, &dashboards.FakeDashboardStore{}, foldertest.NewFakeFolderStore(t), folder.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 := folder.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 := folder.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 root folder creation permissions succeeds", func(t *testing.T) {
|
|
dashStore := &dashboards.FakeDashboardStore{}
|
|
dashboardFolderStore := foldertest.NewFakeFolderStore(t)
|
|
|
|
nestedFolderStore := folder.NewFakeStore()
|
|
nestedFolderStore.ExpectedFolder = &folder.Folder{UID: "myFolder", ParentUID: "newFolder"}
|
|
nestedFolderStore.ExpectedParentFolders = []*folder.Folder{
|
|
{UID: "newFolder", ParentUID: "newFolder"},
|
|
{UID: "newFolder2", ParentUID: "newFolder2"},
|
|
{UID: "newFolder3", ParentUID: "newFolder3"},
|
|
}
|
|
|
|
nestedFolderUser := &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{}}
|
|
nestedFolderUser.Permissions[orgID] = map[string][]string{
|
|
dashboards.ActionFoldersCreate: {
|
|
dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID),
|
|
dashboards.ScopeFoldersProvider.GetResourceScopeUID("myFolder"),
|
|
},
|
|
}
|
|
|
|
features := featuremgmt.WithFeatures("nestedFolders")
|
|
folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, features, acimpl.ProvideAccessControl(features), dbtest.NewFakeDB())
|
|
_, err := folderSvc.Move(context.Background(), &folder.MoveFolderCommand{UID: "myFolder", NewParentUID: "", OrgID: orgID, SignedInUser: nestedFolderUser})
|
|
require.NoError(t, err)
|
|
// the folder is set inside InTransaction() but the fake one is called
|
|
// require.NotNil(t, f)
|
|
})
|
|
|
|
t.Run("move to the root folder with only subfolder creation permissions fails", func(t *testing.T) {
|
|
dashStore := &dashboards.FakeDashboardStore{}
|
|
dashboardFolderStore := foldertest.NewFakeFolderStore(t)
|
|
|
|
nestedFolderStore := folder.NewFakeStore()
|
|
|
|
nestedFolderUser := &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{}}
|
|
nestedFolderUser.Permissions[orgID] = map[string][]string{dashboards.ActionFoldersCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("some_subfolder")}}
|
|
|
|
features := featuremgmt.WithFeatures("nestedFolders")
|
|
folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, features, acimpl.ProvideAccessControl(features), dbtest.NewFakeDB())
|
|
_, err := folderSvc.Move(context.Background(), &folder.MoveFolderCommand{UID: "myFolder", NewParentUID: "", OrgID: orgID, SignedInUser: nestedFolderUser})
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("move when parentUID in the current subtree returns error from nested folder service", func(t *testing.T) {
|
|
g := guardian.New
|
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true})
|
|
t.Cleanup(func() {
|
|
guardian.New = g
|
|
})
|
|
|
|
dashStore := &dashboards.FakeDashboardStore{}
|
|
dashboardFolderStore := foldertest.NewFakeFolderStore(t)
|
|
|
|
nestedFolderStore := folder.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 := folder.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 := folder.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 := folder.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 := folder.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))
|
|
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),
|
|
tracer: tracing.InitializeTracerForTest(),
|
|
}
|
|
|
|
dashboardPermissions := acmock.NewMockedPermissionsService()
|
|
dashboardService, err := dashboardservice.ProvideDashboardServiceImpl(
|
|
cfg, dashStore, folderStore,
|
|
featuresFlagOn,
|
|
acmock.NewMockedPermissionsService(),
|
|
actest.FakeAccessControl{},
|
|
serviceWithFlagOn,
|
|
nestedFolderStore,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
quotaService,
|
|
nil,
|
|
nil,
|
|
)
|
|
require.NoError(t, err)
|
|
dashboardService.RegisterDashboardPermissions(dashboardPermissions)
|
|
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, true)
|
|
ancestorFoldersWithoutPermissions := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "withoutPermissions", createCmd, true)
|
|
|
|
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, true)
|
|
tree2 := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "tree2-", createCmd, true)
|
|
|
|
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 {
|
|
folderStore := ProvideDashboardFolderStore(db)
|
|
|
|
cfg := setting.NewCfg()
|
|
|
|
featuresFlagOff := featuremgmt.WithFeatures()
|
|
dashStore, err := database.ProvideDashboardStore(db, cfg, featuresFlagOff, tagimpl.ProvideService(db))
|
|
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, false)
|
|
f := folders[1]
|
|
|
|
testCases := []struct {
|
|
name string
|
|
svc *Service
|
|
WithFullpath bool
|
|
WithFullpathUIDs bool
|
|
expectedFullpath string
|
|
expectedFullpathUIDs 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",
|
|
},
|
|
{
|
|
name: "when flag is on and WithFullpathUIDs is false",
|
|
svc: &folderSvcOn,
|
|
WithFullpathUIDs: false,
|
|
expectedFullpathUIDs: "",
|
|
},
|
|
{
|
|
name: "when flag is on and WithFullpathUIDs is true",
|
|
svc: &folderSvcOn,
|
|
WithFullpathUIDs: true,
|
|
expectedFullpathUIDs: "uidfor-0/uidfor-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)
|
|
folderStore := ProvideDashboardFolderStore(db)
|
|
|
|
featuresFlagOff := featuremgmt.WithFeatures()
|
|
dashStore, err := database.ProvideDashboardStore(db, cfg, featuresFlagOff, tagimpl.ProvideService(db))
|
|
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),
|
|
tracer: tracing.InitializeTracerForTest(),
|
|
}
|
|
|
|
signedInAdminUser := user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{
|
|
orgID: {
|
|
dashboards.ActionFoldersCreate: {},
|
|
dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll},
|
|
dashboards.ActionFoldersRead: {dashboards.ScopeFoldersAll},
|
|
},
|
|
}}
|
|
|
|
createCmd := folder.CreateFolderCommand{
|
|
OrgID: orgID,
|
|
ParentUID: "",
|
|
SignedInUser: &signedInAdminUser,
|
|
}
|
|
|
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
|
|
CanSaveValue: true,
|
|
CanViewValue: true,
|
|
})
|
|
|
|
prefix := "getfolders/ff/off"
|
|
folders := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOff, 5, prefix, createCmd, true)
|
|
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.ScopeFoldersAll},
|
|
dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll},
|
|
dashboards.ActionFoldersRead: {dashboards.ScopeFoldersAll},
|
|
},
|
|
}}
|
|
|
|
folderStore := ProvideDashboardFolderStore(db)
|
|
|
|
featuresFlagOff := featuremgmt.WithFeatures()
|
|
dashStore, err := database.ProvideDashboardStore(db, cfg, featuresFlagOff, tagimpl.ProvideService(db))
|
|
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),
|
|
tracer: tracing.InitializeTracerForTest(),
|
|
}
|
|
|
|
origGuardian := guardian.New
|
|
fakeGuardian := &guardian.FakeDashboardGuardian{
|
|
CanSaveValue: true,
|
|
CanEditUIDs: []string{},
|
|
CanViewUIDs: []string{},
|
|
}
|
|
guardian.MockDashboardGuardian(fakeGuardian)
|
|
t.Cleanup(func() {
|
|
guardian.New = origGuardian
|
|
})
|
|
|
|
viewer := user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{
|
|
orgID: {
|
|
dashboards.ActionFoldersRead: {},
|
|
dashboards.ActionFoldersWrite: {},
|
|
},
|
|
}}
|
|
|
|
// no view permission
|
|
// |_ subfolder under no view permission with view permission
|
|
// |_ subfolder under no view permission with view permissionn and with edit permission
|
|
// with edit permission
|
|
// |_ subfolder under with edit permission
|
|
// no edit permission
|
|
// |_ subfolder under no edit permission
|
|
// |_ subfolder under no edit permission with edit permission
|
|
noViewPermission, err := folderSvcOn.Create(context.Background(), &folder.CreateFolderCommand{
|
|
OrgID: orgID,
|
|
ParentUID: "",
|
|
Title: "no view permission",
|
|
SignedInUser: &signedInAdminUser,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
f, err := folderSvcOn.Create(context.Background(), &folder.CreateFolderCommand{
|
|
OrgID: orgID,
|
|
ParentUID: noViewPermission.UID,
|
|
Title: "subfolder under no view permission with view permission",
|
|
SignedInUser: &signedInAdminUser,
|
|
})
|
|
viewer.Permissions[orgID][dashboards.ActionFoldersRead] = append(viewer.Permissions[orgID][dashboards.ActionFoldersRead], dashboards.ScopeFoldersProvider.GetResourceScopeUID(f.UID))
|
|
fakeGuardian.CanViewUIDs = append(fakeGuardian.CanViewUIDs, f.UID)
|
|
|
|
require.NoError(t, err)
|
|
f, err = folderSvcOn.Create(context.Background(), &folder.CreateFolderCommand{
|
|
OrgID: orgID,
|
|
ParentUID: noViewPermission.UID,
|
|
Title: "subfolder under no view permission with view permission and with edit permission",
|
|
SignedInUser: &signedInAdminUser,
|
|
})
|
|
require.NoError(t, err)
|
|
viewer.Permissions[orgID][dashboards.ActionFoldersRead] = append(viewer.Permissions[orgID][dashboards.ActionFoldersRead], dashboards.ScopeFoldersProvider.GetResourceScopeUID(f.UID))
|
|
fakeGuardian.CanViewUIDs = append(fakeGuardian.CanViewUIDs, f.UID)
|
|
viewer.Permissions[orgID][dashboards.ActionFoldersWrite] = append(viewer.Permissions[orgID][dashboards.ActionFoldersWrite], dashboards.ScopeFoldersProvider.GetResourceScopeUID(f.UID))
|
|
fakeGuardian.CanEditUIDs = append(fakeGuardian.CanEditUIDs, f.UID)
|
|
|
|
withEditPermission, err := folderSvcOn.Create(context.Background(), &folder.CreateFolderCommand{
|
|
OrgID: orgID,
|
|
ParentUID: "",
|
|
Title: "with edit permission",
|
|
SignedInUser: &signedInAdminUser,
|
|
})
|
|
require.NoError(t, err)
|
|
viewer.Permissions[orgID][dashboards.ActionFoldersRead] = append(viewer.Permissions[orgID][dashboards.ActionFoldersRead], dashboards.ScopeFoldersProvider.GetResourceScopeUID(withEditPermission.UID))
|
|
fakeGuardian.CanViewUIDs = append(fakeGuardian.CanViewUIDs, withEditPermission.UID)
|
|
viewer.Permissions[orgID][dashboards.ActionFoldersWrite] = append(viewer.Permissions[orgID][dashboards.ActionFoldersWrite], dashboards.ScopeFoldersProvider.GetResourceScopeUID(withEditPermission.UID))
|
|
fakeGuardian.CanEditUIDs = append(fakeGuardian.CanEditUIDs, withEditPermission.UID)
|
|
|
|
_, err = folderSvcOn.Create(context.Background(), &folder.CreateFolderCommand{
|
|
OrgID: orgID,
|
|
ParentUID: withEditPermission.UID,
|
|
Title: "subfolder under with edit permission",
|
|
SignedInUser: &signedInAdminUser,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
noEditPermission, err := folderSvcOn.Create(context.Background(), &folder.CreateFolderCommand{
|
|
OrgID: orgID,
|
|
ParentUID: "",
|
|
Title: "no edit permission",
|
|
SignedInUser: &signedInAdminUser,
|
|
})
|
|
require.NoError(t, err)
|
|
viewer.Permissions[orgID][dashboards.ActionFoldersRead] = append(viewer.Permissions[orgID][dashboards.ActionFoldersRead], dashboards.ScopeFoldersProvider.GetResourceScopeUID(noEditPermission.UID))
|
|
fakeGuardian.CanViewUIDs = append(fakeGuardian.CanViewUIDs, noEditPermission.UID)
|
|
|
|
_, err = folderSvcOn.Create(context.Background(), &folder.CreateFolderCommand{
|
|
OrgID: orgID,
|
|
ParentUID: noEditPermission.UID,
|
|
Title: "subfolder under no edit permission",
|
|
SignedInUser: &signedInAdminUser,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
f, err = folderSvcOn.Create(context.Background(), &folder.CreateFolderCommand{
|
|
OrgID: orgID,
|
|
ParentUID: noEditPermission.UID,
|
|
Title: "subfolder under no edit permission with edit permission",
|
|
SignedInUser: &signedInAdminUser,
|
|
})
|
|
require.NoError(t, err)
|
|
viewer.Permissions[orgID][dashboards.ActionFoldersWrite] = append(viewer.Permissions[orgID][dashboards.ActionFoldersWrite], dashboards.ScopeFoldersProvider.GetResourceScopeUID(f.UID))
|
|
fakeGuardian.CanEditUIDs = append(fakeGuardian.CanEditUIDs, f.UID)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
q folder.GetChildrenQuery
|
|
expectedErr error
|
|
expectedFolders []string
|
|
}{
|
|
{
|
|
name: "should return root folders with view permission",
|
|
q: folder.GetChildrenQuery{
|
|
OrgID: orgID,
|
|
SignedInUser: &viewer,
|
|
},
|
|
expectedFolders: []string{
|
|
"Shared with me",
|
|
"no edit permission",
|
|
"with edit permission"},
|
|
},
|
|
{
|
|
name: "should return subfolders with view permission",
|
|
q: folder.GetChildrenQuery{
|
|
OrgID: orgID,
|
|
SignedInUser: &viewer,
|
|
UID: noEditPermission.UID,
|
|
},
|
|
expectedFolders: []string{
|
|
"subfolder under no edit permission",
|
|
"subfolder under no edit permission with edit permission"},
|
|
},
|
|
{
|
|
name: "should return shared with me folders with view permission",
|
|
q: folder.GetChildrenQuery{
|
|
OrgID: orgID,
|
|
SignedInUser: &viewer,
|
|
UID: folder.SharedWithMeFolderUID,
|
|
},
|
|
expectedFolders: []string{
|
|
"subfolder under no view permission with view permission",
|
|
"subfolder under no view permission with view permission and with edit permission"},
|
|
},
|
|
{
|
|
name: "should return root folders with edit permission",
|
|
q: folder.GetChildrenQuery{
|
|
OrgID: orgID,
|
|
SignedInUser: &viewer,
|
|
Permission: dashboardaccess.PERMISSION_EDIT,
|
|
},
|
|
expectedFolders: []string{
|
|
"Shared with me",
|
|
"with edit permission"},
|
|
},
|
|
{
|
|
name: "should fail returning subfolders with edit permission when parent folder has no edit permission",
|
|
q: folder.GetChildrenQuery{
|
|
OrgID: orgID,
|
|
SignedInUser: &viewer,
|
|
Permission: dashboardaccess.PERMISSION_EDIT,
|
|
UID: noEditPermission.UID,
|
|
},
|
|
expectedErr: dashboards.ErrFolderAccessDenied,
|
|
},
|
|
{
|
|
name: "should return shared with me folders with edit permission",
|
|
q: folder.GetChildrenQuery{
|
|
OrgID: orgID,
|
|
SignedInUser: &viewer,
|
|
Permission: dashboardaccess.PERMISSION_EDIT,
|
|
UID: folder.SharedWithMeFolderUID,
|
|
},
|
|
expectedFolders: []string{
|
|
"subfolder under no edit permission with edit permission",
|
|
"subfolder under no view permission with view permission and with edit permission",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
folders, err := folderSvcOn.GetChildren(context.Background(), &tc.q)
|
|
if tc.expectedErr != nil {
|
|
require.Error(t, err)
|
|
require.Equal(t, tc.expectedErr, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
actual := make([]string, 0, len(folders))
|
|
for _, f := range folders {
|
|
actual = append(actual, f.Title)
|
|
}
|
|
if cmp.Diff(tc.expectedFolders, actual) != "" {
|
|
t.Fatalf("unexpected folders: %s", cmp.Diff(tc.expectedFolders, actual))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIntegration_canMove(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test")
|
|
}
|
|
|
|
dashStore := &dashboards.FakeDashboardStore{}
|
|
dashboardFolderStore := foldertest.NewFakeFolderStore(t)
|
|
|
|
db, cfg := sqlstore.InitTestDB(t)
|
|
folderStore := ProvideStore(db)
|
|
orgID := CreateOrg(t, db, cfg)
|
|
|
|
adminUsr := &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleAdmin}
|
|
|
|
// Set up source folder and a source folder parent
|
|
sourceParent, err := folderStore.Create(context.Background(), folder.CreateFolderCommand{
|
|
UID: "source-parent",
|
|
OrgID: orgID,
|
|
Title: "Source parent",
|
|
SignedInUser: adminUsr,
|
|
})
|
|
require.NoError(t, err)
|
|
sourceFolder, err := folderStore.Create(context.Background(), folder.CreateFolderCommand{
|
|
UID: "source",
|
|
OrgID: orgID,
|
|
Title: "Source",
|
|
ParentUID: sourceParent.UID,
|
|
SignedInUser: adminUsr,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Set up destination folder and destination folder parent
|
|
destParent, err := folderStore.Create(context.Background(), folder.CreateFolderCommand{
|
|
UID: "destination-parent",
|
|
OrgID: orgID,
|
|
Title: "Destination parent",
|
|
SignedInUser: adminUsr,
|
|
})
|
|
require.NoError(t, err)
|
|
destFolder, err := folderStore.Create(context.Background(), folder.CreateFolderCommand{
|
|
UID: "destination",
|
|
OrgID: orgID,
|
|
Title: "Destination",
|
|
ParentUID: destParent.UID,
|
|
SignedInUser: adminUsr,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
features := featuremgmt.WithFeatures("nestedFolders")
|
|
folderSvc := setup(t, dashStore, dashboardFolderStore, folderStore, features, acimpl.ProvideAccessControl(features), dbtest.NewFakeDB())
|
|
|
|
testCases := []struct {
|
|
description string
|
|
destinationFolder string
|
|
permissions map[string][]string
|
|
expectedErr error
|
|
}{
|
|
{
|
|
description: "can move a folder if has edit access to both folders",
|
|
destinationFolder: destFolder.UID,
|
|
permissions: map[string][]string{
|
|
dashboards.ActionFoldersWrite: {
|
|
dashboards.ScopeFoldersProvider.GetResourceScopeUID(sourceFolder.UID),
|
|
dashboards.ScopeFoldersProvider.GetResourceScopeUID(destFolder.UID),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
description: "can't move a folder if missing write access to the destination folder",
|
|
destinationFolder: destFolder.UID,
|
|
permissions: map[string][]string{
|
|
dashboards.ActionFoldersWrite: {
|
|
dashboards.ScopeFoldersProvider.GetResourceScopeUID(sourceFolder.UID),
|
|
},
|
|
dashboards.ActionFoldersRead: {
|
|
dashboards.ScopeFoldersProvider.GetResourceScopeUID(destFolder.UID),
|
|
},
|
|
},
|
|
expectedErr: dashboards.ErrMoveAccessDenied,
|
|
},
|
|
{
|
|
description: "can't move a folder to the root if missing folder create permissions",
|
|
destinationFolder: "",
|
|
permissions: map[string][]string{
|
|
dashboards.ActionFoldersWrite: {
|
|
dashboards.ScopeFoldersProvider.GetResourceScopeUID(sourceFolder.UID),
|
|
},
|
|
},
|
|
expectedErr: dashboards.ErrMoveAccessDenied,
|
|
},
|
|
{
|
|
description: "can move a folder to the root with folder create permissions",
|
|
destinationFolder: "",
|
|
permissions: map[string][]string{
|
|
dashboards.ActionFoldersCreate: {
|
|
dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID),
|
|
dashboards.ScopeFoldersProvider.GetResourceScopeUID(sourceFolder.UID),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
description: "can't move a folder to another folder where user has higher plugin permissions",
|
|
destinationFolder: destFolder.UID,
|
|
permissions: map[string][]string{
|
|
dashboards.ActionFoldersWrite: {
|
|
dashboards.ScopeFoldersProvider.GetResourceScopeUID(sourceFolder.UID),
|
|
dashboards.ScopeFoldersProvider.GetResourceScopeUID(destFolder.UID),
|
|
},
|
|
"some_plugin:action": {
|
|
dashboards.ScopeFoldersProvider.GetResourceScopeUID(destFolder.UID),
|
|
},
|
|
},
|
|
expectedErr: dashboards.ErrFolderAccessEscalation,
|
|
},
|
|
{
|
|
description: "can move a folder to another folder where user has lower permissions",
|
|
destinationFolder: destFolder.UID,
|
|
permissions: map[string][]string{
|
|
dashboards.ActionFoldersWrite: {
|
|
dashboards.ScopeFoldersProvider.GetResourceScopeUID(sourceFolder.UID),
|
|
dashboards.ScopeFoldersProvider.GetResourceScopeUID(destFolder.UID),
|
|
},
|
|
"some_plugin:action": {
|
|
dashboards.ScopeFoldersProvider.GetResourceScopeUID(sourceFolder.UID),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
description: "can't move a folder to another folder where user has higher plugin permissions through inheritance",
|
|
destinationFolder: destFolder.UID,
|
|
permissions: map[string][]string{
|
|
dashboards.ActionFoldersWrite: {
|
|
dashboards.ScopeFoldersProvider.GetResourceScopeUID(sourceFolder.UID),
|
|
dashboards.ScopeFoldersProvider.GetResourceScopeUID(destFolder.UID),
|
|
},
|
|
"some_plugin:action": {
|
|
dashboards.ScopeFoldersProvider.GetResourceScopeUID(destParent.UID),
|
|
},
|
|
},
|
|
expectedErr: dashboards.ErrFolderAccessEscalation,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
usr := &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{}}
|
|
usr.Permissions[orgID] = tc.permissions
|
|
|
|
t.Run(tc.description, func(t *testing.T) {
|
|
_, err := folderSvc.Move(context.Background(), &folder.MoveFolderCommand{UID: sourceFolder.UID, NewParentUID: tc.destinationFolder, OrgID: orgID, SignedInUser: usr})
|
|
if tc.expectedErr == nil {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.ErrorIs(t, err, tc.expectedErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSupportBundle(t *testing.T) {
|
|
f := func(uid, parent string) *folder.Folder { return &folder.Folder{UID: uid, ParentUID: parent} }
|
|
for _, tc := range []struct {
|
|
Folders []*folder.Folder
|
|
ExpectedTotal int
|
|
ExpectedDepths map[int]int
|
|
ExpectedChildren map[int]int
|
|
}{
|
|
// Empty folder list
|
|
{
|
|
Folders: []*folder.Folder{},
|
|
ExpectedTotal: 0,
|
|
ExpectedDepths: map[int]int{},
|
|
ExpectedChildren: map[int]int{},
|
|
},
|
|
// Single folder
|
|
{
|
|
Folders: []*folder.Folder{f("a", "")},
|
|
ExpectedTotal: 1,
|
|
ExpectedDepths: map[int]int{1: 1},
|
|
ExpectedChildren: map[int]int{0: 1},
|
|
},
|
|
// Flat folders
|
|
{
|
|
Folders: []*folder.Folder{f("a", ""), f("b", ""), f("c", "")},
|
|
ExpectedTotal: 3,
|
|
ExpectedDepths: map[int]int{1: 3},
|
|
ExpectedChildren: map[int]int{0: 3},
|
|
},
|
|
// Nested folders
|
|
{
|
|
Folders: []*folder.Folder{f("a", ""), f("ab", "a"), f("ac", "a"), f("x", ""), f("xy", "x"), f("xyz", "xy")},
|
|
ExpectedTotal: 6,
|
|
ExpectedDepths: map[int]int{1: 2, 2: 3, 3: 1},
|
|
ExpectedChildren: map[int]int{0: 3, 1: 2, 2: 1},
|
|
},
|
|
} {
|
|
svc := &Service{}
|
|
supportItem, err := svc.supportItemFromFolders(tc.Folders)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
stats := struct {
|
|
Total int `json:"total"`
|
|
Depths map[int]int `json:"depths"`
|
|
Children map[int]int `json:"children"`
|
|
}{}
|
|
if err := json.Unmarshal(supportItem.FileBytes, &stats); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if stats.Total != tc.ExpectedTotal {
|
|
t.Error("Total mismatch", stats, tc)
|
|
}
|
|
if fmt.Sprint(stats.Depths) != fmt.Sprint(tc.ExpectedDepths) {
|
|
t.Error("Depths mismatch", stats, tc.ExpectedDepths)
|
|
}
|
|
if fmt.Sprint(stats.Children) != fmt.Sprint(tc.ExpectedChildren) {
|
|
t.Error("Depths mismatch", stats, tc.ExpectedChildren)
|
|
}
|
|
}
|
|
}
|
|
|
|
func CreateSubtreeInStore(t *testing.T, store folder.Store, service *Service, depth int, prefix string, cmd folder.CreateFolderCommand, randomUID bool) []*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()
|
|
if !randomUID {
|
|
cmd.UID = fmt.Sprintf("uidfor-%d", i)
|
|
}
|
|
cmd.OrgID = orgID
|
|
cmd.SignedInUser = &user.SignedInUser{OrgID: orgID, Permissions: map[int64]map[string][]string{orgID: {dashboards.ActionFoldersCreate: {dashboards.ScopeFoldersAll}}}}
|
|
|
|
f, err := service.Create(context.Background(), &cmd)
|
|
require.NoError(t, err)
|
|
require.Equal(t, title, f.Title)
|
|
require.NotEmpty(t, f.UID)
|
|
|
|
folders = append(folders, f)
|
|
|
|
cmd.ParentUID = f.UID
|
|
}
|
|
|
|
return folders
|
|
}
|
|
|
|
func setup(t *testing.T, dashStore dashboards.Store, dashboardFolderStore folder.FolderStore, nestedFolderStore folder.Store, features featuremgmt.FeatureToggles, ac accesscontrol.AccessControl, db db.DB) folder.Service {
|
|
t.Helper()
|
|
|
|
// nothing enabled yet
|
|
return &Service{
|
|
log: slog.New(logtest.NewTestHandler(t)).With("logger", "test-folder-service"),
|
|
dashboardStore: dashStore,
|
|
dashboardFolderStore: dashboardFolderStore,
|
|
store: nestedFolderStore,
|
|
features: features,
|
|
accessControl: ac,
|
|
db: db,
|
|
metrics: newFoldersMetrics(nil),
|
|
tracer: tracing.InitializeTracerForTest(),
|
|
}
|
|
}
|
|
|
|
func createRule(t *testing.T, store *ngstore.DBstore, folderUID, title string) *models.AlertRule {
|
|
t.Helper()
|
|
gen := models.RuleGen
|
|
rule := gen.With(
|
|
gen.WithOrgID(orgID),
|
|
gen.WithTitle(title),
|
|
gen.WithNamespaceUID(folderUID),
|
|
gen.WithIntervalSeconds(10),
|
|
).Generate()
|
|
ids, err := store.InsertAlertRules(context.Background(), nil, []models.AlertRule{rule})
|
|
require.NoError(t, err)
|
|
|
|
result, err := store.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{OrgID: orgID, UID: ids[0].UID})
|
|
require.NoError(t, err)
|
|
return result
|
|
}
|
|
|
|
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)
|
|
})
|
|
}
|
|
}
|