mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Folder store (#46431)
* create FolderStore * update usages to provide context * implement methods to get folder by ID and UID * update folder service to use store methods
This commit is contained in:
@@ -35,7 +35,7 @@ func NewNameScopeResolver(db Store) (string, ac.AttributeScopeResolveFunc) {
|
||||
if len(nsName) == 0 {
|
||||
return "", ac.ErrInvalidScope
|
||||
}
|
||||
folder, err := db.GetFolderByTitle(orgID, nsName)
|
||||
folder, err := db.GetFolderByTitle(ctx, orgID, nsName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -28,8 +28,9 @@ func TestNewNameScopeResolver(t *testing.T) {
|
||||
orgId := rand.Int63()
|
||||
title := "Very complex :title with: and /" + util.GenerateShortUID()
|
||||
|
||||
db := &models.Dashboard{Id: rand.Int63()}
|
||||
dashboardStore.On("GetFolderByTitle", mock.Anything, mock.Anything).Return(db, nil).Once()
|
||||
db := models.NewFolder(title)
|
||||
db.Id = rand.Int63()
|
||||
dashboardStore.On("GetFolderByTitle", mock.Anything, mock.Anything, mock.Anything).Return(db, nil).Once()
|
||||
|
||||
scope := "folders:name:" + title
|
||||
|
||||
@@ -38,7 +39,7 @@ func TestNewNameScopeResolver(t *testing.T) {
|
||||
|
||||
require.Equal(t, fmt.Sprintf("folders:id:%v", db.Id), resolvedScope)
|
||||
|
||||
dashboardStore.AssertCalled(t, "GetFolderByTitle", orgId, title)
|
||||
dashboardStore.AssertCalled(t, "GetFolderByTitle", mock.Anything, orgId, title)
|
||||
})
|
||||
t.Run("resolver should fail if input scope is not expected", func(t *testing.T) {
|
||||
dashboardStore := &FakeDashboardStore{}
|
||||
@@ -60,7 +61,7 @@ func TestNewNameScopeResolver(t *testing.T) {
|
||||
_, resolver := NewNameScopeResolver(dashboardStore)
|
||||
|
||||
orgId := rand.Int63()
|
||||
dashboardStore.On("GetFolderByTitle", mock.Anything, mock.Anything).Return(nil, models.ErrDashboardNotFound).Once()
|
||||
dashboardStore.On("GetFolderByTitle", mock.Anything, mock.Anything, mock.Anything).Return(nil, models.ErrDashboardNotFound).Once()
|
||||
|
||||
scope := "folders:name:" + util.GenerateShortUID()
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
//go:generate mockery --name Store --structname FakeDashboardStore --inpackage --filename database_mock.go
|
||||
|
||||
// DashboardService is a service for operating on dashboards.
|
||||
type DashboardService interface {
|
||||
SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*models.Dashboard, error)
|
||||
@@ -36,12 +34,11 @@ type DashboardProvisioningService interface {
|
||||
DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *models.DeleteOrphanedProvisionedDashboardsCommand) error
|
||||
}
|
||||
|
||||
//go:generate mockery --name Store --structname FakeDashboardStore --inpackage --filename database_mock.go
|
||||
// Store is a dashboard store.
|
||||
type Store interface {
|
||||
// ValidateDashboardBeforeSave validates a dashboard before save.
|
||||
ValidateDashboardBeforeSave(dashboard *models.Dashboard, overwrite bool) (bool, error)
|
||||
// GetFolderByTitle retrieves a dashboard by its title and is used by unified alerting
|
||||
GetFolderByTitle(orgID int64, title string) (*models.Dashboard, error)
|
||||
GetProvisionedDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error)
|
||||
GetProvisionedDataByDashboardUID(orgID int64, dashboardUID string) (*models.DashboardProvisioning, error)
|
||||
GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error)
|
||||
@@ -54,4 +51,16 @@ type Store interface {
|
||||
UnprovisionDashboard(ctx context.Context, id int64) error
|
||||
// GetDashboardsByPluginID retrieves dashboards identified by plugin.
|
||||
GetDashboardsByPluginID(ctx context.Context, query *models.GetDashboardsByPluginIdQuery) error
|
||||
FolderStore
|
||||
}
|
||||
|
||||
//go:generate mockery --name FolderStore --structname FakeFolderStore --inpackage --filename folder_store_mock.go
|
||||
// FolderStore is a folder store.
|
||||
type FolderStore interface {
|
||||
// GetFolderByTitle retrieves a folder by its title
|
||||
GetFolderByTitle(ctx context.Context, orgID int64, title string) (*models.Folder, error)
|
||||
// GetFolderByUID retrieves a folder by its UID
|
||||
GetFolderByUID(ctx context.Context, orgID int64, uid string) (*models.Folder, error)
|
||||
// GetFolderByID retrieves a folder by its ID
|
||||
GetFolderByID(ctx context.Context, orgID int64, id int64) (*models.Folder, error)
|
||||
}
|
||||
|
||||
@@ -47,27 +47,71 @@ func (d *DashboardStore) ValidateDashboardBeforeSave(dashboard *models.Dashboard
|
||||
return isParentFolderChanged, nil
|
||||
}
|
||||
|
||||
func (d *DashboardStore) GetFolderByTitle(orgID int64, title string) (*models.Dashboard, error) {
|
||||
func (d *DashboardStore) GetFolderByTitle(ctx context.Context, orgID int64, title string) (*models.Folder, error) {
|
||||
if title == "" {
|
||||
return nil, models.ErrDashboardIdentifierNotSet
|
||||
return nil, models.ErrFolderTitleEmpty
|
||||
}
|
||||
|
||||
// there is a unique constraint on org_id, folder_id, title
|
||||
// there are no nested folders so the parent folder id is always 0
|
||||
dashboard := models.Dashboard{OrgId: orgID, FolderId: 0, Title: title}
|
||||
err := d.sqlStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
||||
err := d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
has, err := sess.Table(&models.Dashboard{}).Where("is_folder = " + d.sqlStore.Dialect.BooleanStr(true)).Where("folder_id=0").Get(&dashboard)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !has {
|
||||
return models.ErrDashboardNotFound
|
||||
return models.ErrFolderNotFound
|
||||
}
|
||||
dashboard.SetId(dashboard.Id)
|
||||
dashboard.SetUid(dashboard.Uid)
|
||||
return nil
|
||||
})
|
||||
return &dashboard, err
|
||||
return models.DashboardToFolder(&dashboard), err
|
||||
}
|
||||
|
||||
func (d *DashboardStore) GetFolderByID(ctx context.Context, orgID int64, id int64) (*models.Folder, error) {
|
||||
dashboard := models.Dashboard{OrgId: orgID, FolderId: 0, Id: id}
|
||||
err := d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
has, err := sess.Table(&models.Dashboard{}).Where("is_folder = " + d.sqlStore.Dialect.BooleanStr(true)).Where("folder_id=0").Get(&dashboard)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !has {
|
||||
return models.ErrFolderNotFound
|
||||
}
|
||||
dashboard.SetId(dashboard.Id)
|
||||
dashboard.SetUid(dashboard.Uid)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return models.DashboardToFolder(&dashboard), nil
|
||||
}
|
||||
|
||||
func (d *DashboardStore) GetFolderByUID(ctx context.Context, orgID int64, uid string) (*models.Folder, error) {
|
||||
if uid == "" {
|
||||
return nil, models.ErrDashboardIdentifierNotSet
|
||||
}
|
||||
|
||||
dashboard := models.Dashboard{OrgId: orgID, FolderId: 0, Uid: uid}
|
||||
err := d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
has, err := sess.Table(&models.Dashboard{}).Where("is_folder = " + d.sqlStore.Dialect.BooleanStr(true)).Where("folder_id=0").Get(&dashboard)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !has {
|
||||
return models.ErrFolderNotFound
|
||||
}
|
||||
dashboard.SetId(dashboard.Id)
|
||||
dashboard.SetUid(dashboard.Uid)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return models.DashboardToFolder(&dashboard), nil
|
||||
}
|
||||
|
||||
func (d *DashboardStore) GetProvisionedDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error) {
|
||||
|
||||
@@ -7,11 +7,12 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDashboardFolderDataAccess(t *testing.T) {
|
||||
@@ -525,11 +526,59 @@ func TestDashboardFolderDataAccess(t *testing.T) {
|
||||
folder1 = insertTestDashboard(t, dashboardStore, title, orgId, 0, true, "prod")
|
||||
|
||||
t.Run("GetFolderByTitle should find the folder", func(t *testing.T) {
|
||||
result, err := dashboardStore.GetFolderByTitle(orgId, title)
|
||||
result, err := dashboardStore.GetFolderByTitle(context.Background(), orgId, title)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, folder1.Id, result.Id)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("GetFolderByUID", func(t *testing.T) {
|
||||
var orgId int64 = 1
|
||||
sqlStore := sqlstore.InitTestDB(t)
|
||||
dashboardStore := ProvideDashboardStore(sqlStore)
|
||||
folder := insertTestDashboard(t, dashboardStore, "TEST", orgId, 0, true, "prod")
|
||||
dash := insertTestDashboard(t, dashboardStore, "Very Unique Name", orgId, folder.Id, false, "prod")
|
||||
|
||||
t.Run("should return folder by UID", func(t *testing.T) {
|
||||
d, err := dashboardStore.GetFolderByUID(context.Background(), orgId, folder.Uid)
|
||||
require.Equal(t, folder.Id, d.Id)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
t.Run("should not find dashboard", func(t *testing.T) {
|
||||
d, err := dashboardStore.GetFolderByUID(context.Background(), orgId, dash.Uid)
|
||||
require.Nil(t, d)
|
||||
require.ErrorIs(t, err, models.ErrFolderNotFound)
|
||||
})
|
||||
t.Run("should search in organization", func(t *testing.T) {
|
||||
d, err := dashboardStore.GetFolderByUID(context.Background(), orgId+1, folder.Uid)
|
||||
require.Nil(t, d)
|
||||
require.ErrorIs(t, err, models.ErrFolderNotFound)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("GetFolderByID", func(t *testing.T) {
|
||||
var orgId int64 = 1
|
||||
sqlStore := sqlstore.InitTestDB(t)
|
||||
dashboardStore := ProvideDashboardStore(sqlStore)
|
||||
folder := insertTestDashboard(t, dashboardStore, "TEST", orgId, 0, true, "prod")
|
||||
dash := insertTestDashboard(t, dashboardStore, "Very Unique Name", orgId, folder.Id, false, "prod")
|
||||
|
||||
t.Run("should return folder by ID", func(t *testing.T) {
|
||||
d, err := dashboardStore.GetFolderByID(context.Background(), orgId, folder.Id)
|
||||
require.Equal(t, folder.Id, d.Id)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
t.Run("should not find dashboard", func(t *testing.T) {
|
||||
d, err := dashboardStore.GetFolderByID(context.Background(), orgId, dash.Id)
|
||||
require.Nil(t, d)
|
||||
require.ErrorIs(t, err, models.ErrFolderNotFound)
|
||||
})
|
||||
t.Run("should search in organization", func(t *testing.T) {
|
||||
d, err := dashboardStore.GetFolderByID(context.Background(), orgId+1, folder.Id)
|
||||
require.Nil(t, d)
|
||||
require.ErrorIs(t, err, models.ErrFolderNotFound)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -42,22 +42,68 @@ func (_m *FakeDashboardStore) GetDashboardsByPluginID(ctx context.Context, query
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetFolderByTitle provides a mock function with given fields: orgID, title
|
||||
func (_m *FakeDashboardStore) GetFolderByTitle(orgID int64, title string) (*models.Dashboard, error) {
|
||||
ret := _m.Called(orgID, title)
|
||||
// GetFolderByID provides a mock function with given fields: ctx, orgID, id
|
||||
func (_m *FakeDashboardStore) GetFolderByID(ctx context.Context, orgID int64, id int64) (*models.Folder, error) {
|
||||
ret := _m.Called(ctx, orgID, id)
|
||||
|
||||
var r0 *models.Dashboard
|
||||
if rf, ok := ret.Get(0).(func(int64, string) *models.Dashboard); ok {
|
||||
r0 = rf(orgID, title)
|
||||
var r0 *models.Folder
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, int64) *models.Folder); ok {
|
||||
r0 = rf(ctx, orgID, id)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*models.Dashboard)
|
||||
r0 = ret.Get(0).(*models.Folder)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(int64, string) error); ok {
|
||||
r1 = rf(orgID, title)
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int64, int64) error); ok {
|
||||
r1 = rf(ctx, orgID, id)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetFolderByTitle provides a mock function with given fields: ctx, orgID, title
|
||||
func (_m *FakeDashboardStore) GetFolderByTitle(ctx context.Context, orgID int64, title string) (*models.Folder, error) {
|
||||
ret := _m.Called(ctx, orgID, title)
|
||||
|
||||
var r0 *models.Folder
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, string) *models.Folder); ok {
|
||||
r0 = rf(ctx, orgID, title)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*models.Folder)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int64, string) error); ok {
|
||||
r1 = rf(ctx, orgID, title)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetFolderByUID provides a mock function with given fields: ctx, orgID, uid
|
||||
func (_m *FakeDashboardStore) GetFolderByUID(ctx context.Context, orgID int64, uid string) (*models.Folder, error) {
|
||||
ret := _m.Called(ctx, orgID, uid)
|
||||
|
||||
var r0 *models.Folder
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, string) *models.Folder); ok {
|
||||
r0 = rf(ctx, orgID, uid)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*models.Folder)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int64, string) error); ok {
|
||||
r1 = rf(ctx, orgID, uid)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
84
pkg/services/dashboards/folder_store_mock.go
Normal file
84
pkg/services/dashboards/folder_store_mock.go
Normal file
@@ -0,0 +1,84 @@
|
||||
// Code generated by mockery v2.10.0. DO NOT EDIT.
|
||||
|
||||
package dashboards
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
models "github.com/grafana/grafana/pkg/models"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// FakeFolderStore is an autogenerated mock type for the FolderStore type
|
||||
type FakeFolderStore struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// GetFolderByID provides a mock function with given fields: ctx, orgID, id
|
||||
func (_m *FakeFolderStore) GetFolderByID(ctx context.Context, orgID int64, id int64) (*models.Folder, error) {
|
||||
ret := _m.Called(ctx, orgID, id)
|
||||
|
||||
var r0 *models.Folder
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, int64) *models.Folder); ok {
|
||||
r0 = rf(ctx, orgID, id)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*models.Folder)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int64, int64) error); ok {
|
||||
r1 = rf(ctx, orgID, id)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetFolderByTitle provides a mock function with given fields: ctx, orgID, title
|
||||
func (_m *FakeFolderStore) GetFolderByTitle(ctx context.Context, orgID int64, title string) (*models.Folder, error) {
|
||||
ret := _m.Called(ctx, orgID, title)
|
||||
|
||||
var r0 *models.Folder
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, string) *models.Folder); ok {
|
||||
r0 = rf(ctx, orgID, title)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*models.Folder)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int64, string) error); ok {
|
||||
r1 = rf(ctx, orgID, title)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetFolderByUID provides a mock function with given fields: ctx, orgID, uid
|
||||
func (_m *FakeFolderStore) GetFolderByUID(ctx context.Context, orgID int64, uid string) (*models.Folder, error) {
|
||||
ret := _m.Called(ctx, orgID, uid)
|
||||
|
||||
var r0 *models.Folder
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, string) *models.Folder); ok {
|
||||
r0 = rf(ctx, orgID, uid)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*models.Folder)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int64, string) error); ok {
|
||||
r1 = rf(ctx, orgID, uid)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
@@ -79,10 +79,9 @@ func (f *FolderServiceImpl) GetFolderByID(ctx context.Context, user *models.Sign
|
||||
if id == 0 {
|
||||
return &models.Folder{Id: id, Title: "General"}, nil
|
||||
}
|
||||
query := models.GetDashboardQuery{OrgId: orgID, Id: id}
|
||||
dashFolder, err := getFolder(ctx, query)
|
||||
dashFolder, err := f.dashboardStore.GetFolderByID(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return nil, toFolderError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
g := guardian.New(ctx, dashFolder.Id, orgID, user)
|
||||
@@ -93,15 +92,13 @@ func (f *FolderServiceImpl) GetFolderByID(ctx context.Context, user *models.Sign
|
||||
return nil, models.ErrFolderAccessDenied
|
||||
}
|
||||
|
||||
return dashToFolder(dashFolder), nil
|
||||
return dashFolder, nil
|
||||
}
|
||||
|
||||
func (f *FolderServiceImpl) GetFolderByUID(ctx context.Context, user *models.SignedInUser, orgID int64, uid string) (*models.Folder, error) {
|
||||
query := models.GetDashboardQuery{OrgId: orgID, Uid: uid}
|
||||
dashFolder, err := getFolder(ctx, query)
|
||||
|
||||
dashFolder, err := f.dashboardStore.GetFolderByUID(ctx, orgID, uid)
|
||||
if err != nil {
|
||||
return nil, toFolderError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
g := guardian.New(ctx, dashFolder.Id, orgID, user)
|
||||
@@ -112,13 +109,13 @@ func (f *FolderServiceImpl) GetFolderByUID(ctx context.Context, user *models.Sig
|
||||
return nil, models.ErrFolderAccessDenied
|
||||
}
|
||||
|
||||
return dashToFolder(dashFolder), nil
|
||||
return dashFolder, nil
|
||||
}
|
||||
|
||||
func (f *FolderServiceImpl) GetFolderByTitle(ctx context.Context, user *models.SignedInUser, orgID int64, title string) (*models.Folder, error) {
|
||||
dashFolder, err := f.dashboardStore.GetFolderByTitle(orgID, title)
|
||||
dashFolder, err := f.dashboardStore.GetFolderByTitle(ctx, orgID, title)
|
||||
if err != nil {
|
||||
return nil, toFolderError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
g := guardian.New(ctx, dashFolder.Id, orgID, user)
|
||||
@@ -129,7 +126,7 @@ func (f *FolderServiceImpl) GetFolderByTitle(ctx context.Context, user *models.S
|
||||
return nil, models.ErrFolderAccessDenied
|
||||
}
|
||||
|
||||
return dashToFolder(dashFolder), nil
|
||||
return dashFolder, nil
|
||||
}
|
||||
|
||||
func (f *FolderServiceImpl) CreateFolder(ctx context.Context, user *models.SignedInUser, orgID int64, title, uid string) (*models.Folder, error) {
|
||||
@@ -160,29 +157,29 @@ func (f *FolderServiceImpl) CreateFolder(ctx context.Context, user *models.Signe
|
||||
return nil, toFolderError(err)
|
||||
}
|
||||
|
||||
query := models.GetDashboardQuery{OrgId: orgID, Id: dash.Id}
|
||||
dashFolder, err = getFolder(ctx, query)
|
||||
var folder *models.Folder
|
||||
folder, err = f.dashboardStore.GetFolderByID(ctx, orgID, dash.Id)
|
||||
if err != nil {
|
||||
return nil, toFolderError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var permissionErr error
|
||||
if f.features.IsEnabled(featuremgmt.FlagAccesscontrol) {
|
||||
resourceID := strconv.FormatInt(dashFolder.Id, 10)
|
||||
resourceID := strconv.FormatInt(folder.Id, 10)
|
||||
_, permissionErr = f.permissions.SetPermissions(ctx, orgID, resourceID, []accesscontrol.SetResourcePermissionCommand{
|
||||
{UserID: userID, Permission: models.PERMISSION_ADMIN.String()},
|
||||
{BuiltinRole: string(models.ROLE_EDITOR), Permission: models.PERMISSION_EDIT.String()},
|
||||
{BuiltinRole: string(models.ROLE_VIEWER), Permission: models.PERMISSION_VIEW.String()},
|
||||
}...)
|
||||
} else if f.cfg.EditorsCanAdmin {
|
||||
permissionErr = f.MakeUserAdmin(ctx, orgID, userID, dashFolder.Id, true)
|
||||
permissionErr = f.MakeUserAdmin(ctx, orgID, userID, folder.Id, true)
|
||||
}
|
||||
|
||||
if permissionErr != nil {
|
||||
f.log.Error("Could not make user admin", "folder", dashFolder.Title, "user", userID, "error", permissionErr)
|
||||
f.log.Error("Could not make user admin", "folder", folder.Title, "user", userID, "error", permissionErr)
|
||||
}
|
||||
|
||||
return dashToFolder(dashFolder), nil
|
||||
return folder, nil
|
||||
}
|
||||
|
||||
func (f *FolderServiceImpl) UpdateFolder(ctx context.Context, user *models.SignedInUser, orgID int64, existingUid string, cmd *models.UpdateFolderCommand) error {
|
||||
@@ -211,22 +208,19 @@ func (f *FolderServiceImpl) UpdateFolder(ctx context.Context, user *models.Signe
|
||||
return toFolderError(err)
|
||||
}
|
||||
|
||||
query = models.GetDashboardQuery{OrgId: orgID, Id: dash.Id}
|
||||
dashFolder, err = getFolder(ctx, query)
|
||||
var folder *models.Folder
|
||||
folder, err = f.dashboardStore.GetFolderByID(ctx, orgID, dash.Id)
|
||||
if err != nil {
|
||||
return toFolderError(err)
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.Result = dashToFolder(dashFolder)
|
||||
|
||||
cmd.Result = folder
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FolderServiceImpl) DeleteFolder(ctx context.Context, user *models.SignedInUser, orgID int64, uid string, forceDeleteRules bool) (*models.Folder, error) {
|
||||
query := models.GetDashboardQuery{OrgId: orgID, Uid: uid}
|
||||
dashFolder, err := getFolder(ctx, query)
|
||||
dashFolder, err := f.dashboardStore.GetFolderByUID(ctx, orgID, uid)
|
||||
if err != nil {
|
||||
return nil, toFolderError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
guard := guardian.New(ctx, dashFolder.Id, orgID, user)
|
||||
@@ -242,7 +236,7 @@ func (f *FolderServiceImpl) DeleteFolder(ctx context.Context, user *models.Signe
|
||||
return nil, toFolderError(err)
|
||||
}
|
||||
|
||||
return dashToFolder(dashFolder), nil
|
||||
return dashFolder, nil
|
||||
}
|
||||
|
||||
func (f *FolderServiceImpl) MakeUserAdmin(ctx context.Context, orgID int64, userID, folderID int64, setViewAndEditPermissions bool) error {
|
||||
@@ -261,21 +255,6 @@ func getFolder(ctx context.Context, query models.GetDashboardQuery) (*models.Das
|
||||
return query.Result, nil
|
||||
}
|
||||
|
||||
func dashToFolder(dash *models.Dashboard) *models.Folder {
|
||||
return &models.Folder{
|
||||
Id: dash.Id,
|
||||
Uid: dash.Uid,
|
||||
Title: dash.Title,
|
||||
HasAcl: dash.HasAcl,
|
||||
Url: dash.GetUrl(),
|
||||
Version: dash.Version,
|
||||
Created: dash.Created,
|
||||
CreatedBy: dash.CreatedBy,
|
||||
Updated: dash.Updated,
|
||||
UpdatedBy: dash.UpdatedBy,
|
||||
}
|
||||
}
|
||||
|
||||
func toFolderError(err error) error {
|
||||
if errors.Is(err, models.ErrDashboardTitleEmpty) {
|
||||
return models.ErrFolderTitleEmpty
|
||||
|
||||
@@ -5,6 +5,7 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -12,44 +13,71 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
var orgID = int64(1)
|
||||
var user = &models.SignedInUser{UserId: 1}
|
||||
|
||||
func TestFolderService(t *testing.T) {
|
||||
t.Run("Folder service tests", func(t *testing.T) {
|
||||
func TestProvideFolderService(t *testing.T) {
|
||||
t.Run("should register scope resolvers", func(t *testing.T) {
|
||||
store := &dashboards.FakeDashboardStore{}
|
||||
defer store.AssertExpectations(t)
|
||||
cfg := setting.NewCfg()
|
||||
features := featuremgmt.WithFeatures()
|
||||
permissionsServices := acmock.NewPermissionsServicesMock()
|
||||
dashboardService := ProvideDashboardService(cfg, store, nil, features, permissionsServices)
|
||||
ac := acmock.New()
|
||||
service := ProvideFolderService(
|
||||
|
||||
ProvideFolderService(
|
||||
cfg, &dashboards.FakeDashboardService{DashboardService: dashboardService},
|
||||
store, nil, features, permissionsServices, ac,
|
||||
)
|
||||
|
||||
require.Len(t, ac.Calls.RegisterAttributeScopeResolver, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFolderService(t *testing.T) {
|
||||
t.Run("Folder service tests", func(t *testing.T) {
|
||||
store := &dashboards.FakeDashboardStore{}
|
||||
cfg := setting.NewCfg()
|
||||
features := featuremgmt.WithFeatures()
|
||||
permissionsServices := acmock.NewPermissionsServicesMock()
|
||||
dashboardService := ProvideDashboardService(cfg, store, nil, features, permissionsServices)
|
||||
|
||||
service := FolderServiceImpl{
|
||||
cfg: cfg,
|
||||
log: log.New("test-folder-service"),
|
||||
dashboardService: dashboardService,
|
||||
dashboardStore: store,
|
||||
searchService: nil,
|
||||
features: features,
|
||||
permissions: permissionsServices.GetFolderService(),
|
||||
}
|
||||
|
||||
t.Run("Given user has no permissions", func(t *testing.T) {
|
||||
origNewGuardian := guardian.New
|
||||
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{})
|
||||
|
||||
bus.AddHandler("test", func(ctx context.Context, query *models.GetDashboardQuery) error {
|
||||
query.Result = models.NewDashboardFolder("Folder")
|
||||
return nil
|
||||
})
|
||||
folderId := rand.Int63()
|
||||
folderUID := util.GenerateShortUID()
|
||||
|
||||
folder := models.NewFolder("Folder")
|
||||
folder.Id = folderId
|
||||
folder.Uid = folderUID
|
||||
|
||||
store.On("GetFolderByID", mock.Anything, orgID, folderId).Return(folder, nil)
|
||||
store.On("GetFolderByUID", mock.Anything, orgID, folderUID).Return(folder, nil)
|
||||
|
||||
t.Run("When get folder by id should return access denied error", func(t *testing.T) {
|
||||
_, err := service.GetFolderByID(context.Background(), user, 1, orgID)
|
||||
_, err := service.GetFolderByID(context.Background(), user, folderId, orgID)
|
||||
require.Equal(t, err, models.ErrFolderAccessDenied)
|
||||
})
|
||||
|
||||
@@ -60,26 +88,32 @@ func TestFolderService(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("When get folder by uid should return access denied error", func(t *testing.T) {
|
||||
_, err := service.GetFolderByUID(context.Background(), user, orgID, "uid")
|
||||
_, err := service.GetFolderByUID(context.Background(), user, orgID, folderUID)
|
||||
require.Equal(t, err, models.ErrFolderAccessDenied)
|
||||
})
|
||||
|
||||
t.Run("When creating folder should return access denied error", func(t *testing.T) {
|
||||
store.On("ValidateDashboardBeforeSave", mock.Anything, mock.Anything).Return(true, nil).Times(2)
|
||||
_, err := service.CreateFolder(context.Background(), user, orgID, "Folder", "")
|
||||
_, err := service.CreateFolder(context.Background(), user, orgID, folder.Title, folderUID)
|
||||
require.Equal(t, err, models.ErrFolderAccessDenied)
|
||||
})
|
||||
|
||||
t.Run("When updating folder should return access denied error", func(t *testing.T) {
|
||||
err := service.UpdateFolder(context.Background(), user, orgID, "uid", &models.UpdateFolderCommand{
|
||||
Uid: "uid",
|
||||
Title: "Folder",
|
||||
bus.AddHandler("test", func(ctx context.Context, query *models.GetDashboardQuery) error {
|
||||
query.Result = models.NewDashboardFolder("Folder")
|
||||
return nil
|
||||
})
|
||||
defer bus.ClearBusHandlers()
|
||||
|
||||
err := service.UpdateFolder(context.Background(), user, orgID, folderUID, &models.UpdateFolderCommand{
|
||||
Uid: folderUID,
|
||||
Title: "Folder-TEST",
|
||||
})
|
||||
require.Equal(t, err, models.ErrFolderAccessDenied)
|
||||
})
|
||||
|
||||
t.Run("When deleting folder by uid should return access denied error", func(t *testing.T) {
|
||||
_, err := service.DeleteFolder(context.Background(), user, orgID, "uid", false)
|
||||
_, err := service.DeleteFolder(context.Background(), user, orgID, folderUID, false)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, err, models.ErrFolderAccessDenied)
|
||||
})
|
||||
@@ -93,42 +127,77 @@ func TestFolderService(t *testing.T) {
|
||||
origNewGuardian := guardian.New
|
||||
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true})
|
||||
|
||||
dash := models.NewDashboardFolder("Folder")
|
||||
dash.Id = 1
|
||||
|
||||
bus.AddHandler("test", func(ctx context.Context, query *models.GetDashboardQuery) error {
|
||||
query.Result = dash
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(ctx context.Context, cmd *models.SaveDashboardCommand) error {
|
||||
cmd.Result = dash
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(ctx context.Context, cmd *models.DeleteDashboardCommand) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
t.Run("When creating folder should not return access denied error", func(t *testing.T) {
|
||||
store.On("ValidateDashboardBeforeSave", mock.Anything, mock.Anything).Return(true, nil).Times(2)
|
||||
store.On("SaveDashboard", mock.Anything).Return(&models.Dashboard{Id: 1}, nil).Once()
|
||||
_, err := service.CreateFolder(context.Background(), user, orgID, "Folder", "")
|
||||
dash := models.NewDashboardFolder("Test-Folder")
|
||||
dash.Id = rand.Int63()
|
||||
f := models.DashboardToFolder(dash)
|
||||
|
||||
bus.AddHandler("test", func(ctx context.Context, cmd *models.SaveDashboardCommand) error {
|
||||
cmd.Result = dash
|
||||
return nil
|
||||
})
|
||||
defer bus.ClearBusHandlers()
|
||||
|
||||
store.On("ValidateDashboardBeforeSave", mock.Anything, mock.Anything).Return(true, nil)
|
||||
store.On("SaveDashboard", mock.Anything).Return(dash, nil).Once()
|
||||
store.On("GetFolderByID", mock.Anything, orgID, dash.Id).Return(f, nil)
|
||||
|
||||
actualFolder, err := service.CreateFolder(context.Background(), user, orgID, dash.Title, "")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, f, actualFolder)
|
||||
})
|
||||
|
||||
t.Run("When updating folder should not return access denied error", func(t *testing.T) {
|
||||
store.On("SaveDashboard", mock.Anything).Return(&models.Dashboard{Id: 1}, nil).Once()
|
||||
err := service.UpdateFolder(context.Background(), user, orgID, "uid", &models.UpdateFolderCommand{
|
||||
Uid: "uid",
|
||||
Title: "Folder",
|
||||
dashboardFolder := models.NewDashboardFolder("Folder")
|
||||
dashboardFolder.Id = rand.Int63()
|
||||
dashboardFolder.Uid = util.GenerateShortUID()
|
||||
f := models.DashboardToFolder(dashboardFolder)
|
||||
|
||||
bus.AddHandler("test", func(ctx context.Context, query *models.GetDashboardQuery) error {
|
||||
query.Result = dashboardFolder
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(ctx context.Context, cmd *models.SaveDashboardCommand) error {
|
||||
cmd.Result = dashboardFolder
|
||||
return nil
|
||||
})
|
||||
|
||||
defer bus.ClearBusHandlers()
|
||||
|
||||
store.On("ValidateDashboardBeforeSave", mock.Anything, mock.Anything).Return(true, nil)
|
||||
store.On("SaveDashboard", mock.Anything).Return(dashboardFolder, nil)
|
||||
store.On("GetFolderByID", mock.Anything, orgID, dashboardFolder.Id).Return(f, nil)
|
||||
|
||||
req := &models.UpdateFolderCommand{
|
||||
Uid: dashboardFolder.Uid,
|
||||
Title: "TEST-Folder",
|
||||
}
|
||||
|
||||
err := service.UpdateFolder(context.Background(), user, orgID, dashboardFolder.Uid, req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, f, req.Result)
|
||||
})
|
||||
|
||||
t.Run("When deleting folder by uid should not return access denied error", func(t *testing.T) {
|
||||
_, err := service.DeleteFolder(context.Background(), user, orgID, "uid", false)
|
||||
f := models.NewFolder(util.GenerateShortUID())
|
||||
f.Id = rand.Int63()
|
||||
f.Uid = util.GenerateShortUID()
|
||||
store.On("GetFolderByUID", mock.Anything, orgID, f.Uid).Return(f, nil)
|
||||
var actualCmd *models.DeleteDashboardCommand
|
||||
bus.AddHandler("test", func(ctx context.Context, cmd *models.DeleteDashboardCommand) error {
|
||||
actualCmd = cmd
|
||||
return nil
|
||||
})
|
||||
defer bus.ClearBusHandlers()
|
||||
|
||||
expectedForceDeleteRules := rand.Int63()%2 == 0
|
||||
_, err := service.DeleteFolder(context.Background(), user, orgID, f.Uid, expectedForceDeleteRules)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, actualCmd)
|
||||
require.Equal(t, f.Id, actualCmd.Id)
|
||||
require.Equal(t, orgID, actualCmd.OrgId)
|
||||
require.Equal(t, expectedForceDeleteRules, actualCmd.ForceDeleteFolderRules)
|
||||
})
|
||||
|
||||
t.Cleanup(func() {
|
||||
@@ -140,27 +209,36 @@ func TestFolderService(t *testing.T) {
|
||||
origNewGuardian := guardian.New
|
||||
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true})
|
||||
|
||||
dashFolder := models.NewDashboardFolder("Folder")
|
||||
dashFolder.Id = 1
|
||||
dashFolder.Uid = "uid-abc"
|
||||
|
||||
bus.AddHandler("test", func(ctx context.Context, query *models.GetDashboardQuery) error {
|
||||
query.Result = dashFolder
|
||||
return nil
|
||||
})
|
||||
|
||||
t.Run("When get folder by id should return folder", func(t *testing.T) {
|
||||
f, _ := service.GetFolderByID(context.Background(), user, orgID, 1)
|
||||
require.Equal(t, f.Id, dashFolder.Id)
|
||||
require.Equal(t, f.Uid, dashFolder.Uid)
|
||||
require.Equal(t, f.Title, dashFolder.Title)
|
||||
expected := models.NewFolder(util.GenerateShortUID())
|
||||
expected.Id = rand.Int63()
|
||||
|
||||
store.On("GetFolderByID", mock.Anything, orgID, expected.Id).Return(expected, nil)
|
||||
|
||||
actual, err := service.GetFolderByID(context.Background(), user, expected.Id, orgID)
|
||||
require.Equal(t, expected, actual)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("When get folder by uid should return folder", func(t *testing.T) {
|
||||
f, _ := service.GetFolderByUID(context.Background(), user, orgID, "uid")
|
||||
require.Equal(t, f.Id, dashFolder.Id)
|
||||
require.Equal(t, f.Uid, dashFolder.Uid)
|
||||
require.Equal(t, f.Title, dashFolder.Title)
|
||||
expected := models.NewFolder(util.GenerateShortUID())
|
||||
expected.Uid = util.GenerateShortUID()
|
||||
|
||||
store.On("GetFolderByUID", mock.Anything, orgID, expected.Uid).Return(expected, nil)
|
||||
|
||||
actual, err := service.GetFolderByUID(context.Background(), user, orgID, expected.Uid)
|
||||
require.Equal(t, expected, actual)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("When get folder by title should return folder", func(t *testing.T) {
|
||||
expected := models.NewFolder("TEST-" + util.GenerateShortUID())
|
||||
|
||||
store.On("GetFolderByTitle", mock.Anything, orgID, expected.Title).Return(expected, nil)
|
||||
|
||||
actual, err := service.GetFolderByTitle(context.Background(), user, orgID, expected.Title)
|
||||
require.Equal(t, expected, actual)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Cleanup(func() {
|
||||
|
||||
Reference in New Issue
Block a user