diff --git a/pkg/services/dashboards/database/database_folder_test.go b/pkg/services/dashboards/database/database_folder_test.go index f41ffe9a11f..dfe02a39416 100644 --- a/pkg/services/dashboards/database/database_folder_test.go +++ b/pkg/services/dashboards/database/database_folder_test.go @@ -2,36 +2,20 @@ package database import ( "context" - "errors" - "fmt" "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" - "github.com/grafana/grafana/pkg/services/accesscontrol/mock" - "github.com/grafana/grafana/pkg/services/apiserver" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/folder" - "github.com/grafana/grafana/pkg/services/folder/folderimpl" "github.com/grafana/grafana/pkg/services/org" - "github.com/grafana/grafana/pkg/services/org/orgimpl" - "github.com/grafana/grafana/pkg/services/quota/quotatest" - "github.com/grafana/grafana/pkg/services/search/sort" - "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/services/user/userimpl" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" ) var testFeatureToggles = featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch) @@ -230,197 +214,6 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { }) } -func TestIntegrationDashboardInheritedFolderRBAC(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - // the maximux nested folder hierarchy starting from parent down to subfolders - nestedFolders := make([]*folder.Folder, 0, folder.MaxNestedFolderDepth+1) - - var sqlStore db.DB - var cfg *setting.Cfg - const ( - dashInRootTitle = "dashboard in root" - dashInParentTitle = "dashboard in parent" - dashInSubfolderTitle = "dashboard in subfolder" - ) - var viewer *user.SignedInUser - - setup := func() { - sqlStore, cfg = db.InitTestDBWithCfg(t) - cfg.AutoAssignOrg = true - cfg.AutoAssignOrgId = 1 - cfg.AutoAssignOrgRole = string(org.RoleViewer) - - tracer := tracing.InitializeTracerForTest() - quotaService := quotatest.New(false, nil) - - // enable nested folders so that the folder table is populated for all the tests - features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders) - - var err error - dashboardWriteStore, err := ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore)) - require.NoError(t, err) - - orgService, err := orgimpl.ProvideService(sqlStore, cfg, quotaService) - require.NoError(t, err) - usrSvc, err := userimpl.ProvideService( - sqlStore, orgService, cfg, nil, nil, tracer, - quotaService, supportbundlestest.NewFakeBundleService(), - ) - require.NoError(t, err) - - usr := createUser(t, usrSvc, orgService, "viewer", false) - viewer = &user.SignedInUser{ - UserID: usr.ID, - OrgID: usr.OrgID, - OrgRole: org.RoleViewer, - } - - // create admin user in the same org - currentUserCmd := user.CreateUserCommand{Login: "admin", Email: "admin@test.com", Name: "an admin", IsAdmin: false, OrgID: viewer.OrgID} - u, err := usrSvc.Create(context.Background(), ¤tUserCmd) - require.NoError(t, err) - admin := user.SignedInUser{ - UserID: u.ID, - OrgID: u.OrgID, - OrgRole: org.RoleAdmin, - Permissions: map[int64]map[string][]string{u.OrgID: accesscontrol.GroupScopesByActionContext(context.Background(), []accesscontrol.Permission{ - { - Action: dashboards.ActionFoldersCreate, - Scope: dashboards.ScopeFoldersAll, - }}), - }, - } - require.NotEqual(t, viewer.UserID, admin.UserID) - - folderStore := folderimpl.ProvideStore(sqlStore) - folderSvc := folderimpl.ProvideService( - folderStore, mock.New(), bus.ProvideBus(tracer), dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(sqlStore), - nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService(), apiserver.WithoutRestConfig) - - parentUID := "" - for i := 0; ; i++ { - uid := fmt.Sprintf("f%d", i) - f, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ - UID: uid, - OrgID: admin.OrgID, - Title: uid, - SignedInUser: &admin, - ParentUID: parentUID, - }) - if err != nil { - if errors.Is(err, folder.ErrMaximumDepthReached) { - break - } - - t.Log("unexpected error", "error", err) - t.Fail() - } - - nestedFolders = append(nestedFolders, f) - - parentUID = f.UID - } - require.LessOrEqual(t, 2, len(nestedFolders)) - - saveDashboardCmd := dashboards.SaveDashboardCommand{ - UserID: admin.UserID, - OrgID: admin.OrgID, - IsFolder: false, - Dashboard: simplejson.NewFromAny(map[string]any{ - "title": dashInRootTitle, - }), - } - _, err = dashboardWriteStore.SaveDashboard(context.Background(), saveDashboardCmd) - require.NoError(t, err) - - saveDashboardCmd = dashboards.SaveDashboardCommand{ - UserID: admin.UserID, - OrgID: admin.OrgID, - IsFolder: false, - Dashboard: simplejson.NewFromAny(map[string]any{ - "title": dashInParentTitle, - }), - FolderUID: nestedFolders[0].UID, - } - _, err = dashboardWriteStore.SaveDashboard(context.Background(), saveDashboardCmd) - require.NoError(t, err) - - saveDashboardCmd = dashboards.SaveDashboardCommand{ - UserID: admin.UserID, - OrgID: admin.OrgID, - IsFolder: false, - Dashboard: simplejson.NewFromAny(map[string]any{ - "title": dashInSubfolderTitle, - }), - FolderUID: nestedFolders[1].UID, - } - _, err = dashboardWriteStore.SaveDashboard(context.Background(), saveDashboardCmd) - require.NoError(t, err) - } - - setup() - - nestedFolderTitles := make([]string, 0, len(nestedFolders)) - for _, f := range nestedFolders { - nestedFolderTitles = append(nestedFolderTitles, f.Title) - } - - testCases := []struct { - desc string - features featuremgmt.FeatureToggles - permissions map[string][]string - expectedTitles []string - }{ - { - desc: "it should not return folder if ACL is not set for parent folder", - features: featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch), - permissions: nil, - expectedTitles: nil, - }, - { - desc: "it should not return subfolder if nested folders are disabled and the user has permission to read folders under parent folder", - features: featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch), - permissions: map[string][]string{ - dashboards.ActionFoldersRead: {fmt.Sprintf("folders:uid:%s", nestedFolders[0].UID)}, - }, - expectedTitles: []string{nestedFolders[0].Title}, - }, - { - desc: "it should return subfolder if nested folders are enabled and the user has permission to read folders under parent folder", - features: featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch, featuremgmt.FlagNestedFolders), - permissions: map[string][]string{ - dashboards.ActionFoldersRead: {fmt.Sprintf("folders:uid:%s", nestedFolders[0].UID)}, - }, - expectedTitles: nestedFolderTitles, - }, - } - - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - dashboardReadStore, err := ProvideDashboardStore(sqlStore, cfg, tc.features, tagimpl.ProvideService(sqlStore)) - require.NoError(t, err) - - viewer.Permissions = map[int64]map[string][]string{viewer.OrgID: tc.permissions} - actest.AddUserPermissionToDB(t, sqlStore, viewer) - - query := &dashboards.FindPersistedDashboardsQuery{ - SignedInUser: viewer, - OrgId: viewer.OrgID, - } - - res, err := testSearchDashboards(dashboardReadStore, query) - require.NoError(t, err) - - require.Equal(t, len(tc.expectedTitles), len(res)) - for i, tlt := range tc.expectedTitles { - assert.Equal(t, tlt, res[i].Title) - } - }) - } -} - func moveDashboard(t *testing.T, dashboardStore dashboards.Store, orgId int64, dashboard *simplejson.Json, newFolderId int64, newFolderUID string) *dashboards.Dashboard { t.Helper() @@ -437,15 +230,3 @@ func moveDashboard(t *testing.T, dashboardStore dashboards.Store, orgId int64, d return dash } - -func createUser(t *testing.T, userSrv user.Service, orgSrv org.Service, name string, isAdmin bool) user.User { - t.Helper() - - o, err := orgSrv.CreateWithMember(context.Background(), &org.CreateOrgCommand{Name: fmt.Sprintf("test org %d", time.Now().UnixNano())}) - require.NoError(t, err) - - currentUserCmd := user.CreateUserCommand{Login: name, Email: name + "@test.com", Name: "a " + name, IsAdmin: isAdmin, OrgID: o.ID} - currentUser, err := userSrv.Create(context.Background(), ¤tUserCmd) - require.NoError(t, err) - return *currentUser -} diff --git a/pkg/services/dashboards/service/dashboard_service_integration_test.go b/pkg/services/dashboards/service/dashboard_service_integration_test.go deleted file mode 100644 index 38c11736a1b..00000000000 --- a/pkg/services/dashboards/service/dashboard_service_integration_test.go +++ /dev/null @@ -1,1092 +0,0 @@ -package service - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/kvstore" - "github.com/grafana/grafana/pkg/infra/serverlock" - "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" - accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" - "github.com/grafana/grafana/pkg/services/apiserver" - "github.com/grafana/grafana/pkg/services/apiserver/client" - "github.com/grafana/grafana/pkg/services/dashboards" - "github.com/grafana/grafana/pkg/services/dashboards/database" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/folder/folderimpl" - "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/search/sort" - "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/storage/legacysql/dualwrite" - "github.com/grafana/grafana/pkg/tests/testsuite" -) - -const testOrgID int64 = 1 - -func TestMain(m *testing.M) { - testsuite.Run(m) -} - -func TestIntegrationIntegratedDashboardService(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - t.Run("Given saved folders and dashboards in organization A", func(t *testing.T) { - // Basic validation tests - - permissionScenario(t, "When saving a dashboard with non-existing id", func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": float64(123412321), - "title": "Expect error", - }), - } - - _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) - assert.Equal(t, dashboards.ErrDashboardNotFound, err) - }) - - // Given other organization - - t.Run("Given organization B", func(t *testing.T) { - const otherOrgId int64 = 2 - - permissionScenario(t, "When creating a dashboard with same id as dashboard in organization A", func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: otherOrgId, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": sc.savedDashInFolder.ID, - "title": "Expect error", - }), - Overwrite: false, - } - - _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) - assert.Equal(t, dashboards.ErrDashboardNotFound, err) - }) - - permissionScenario(t, "When creating a dashboard with same uid as dashboard in organization A, it should create a new dashboard in org B", func(t *testing.T, sc *permissionScenarioContext) { - const otherOrgId int64 = 2 - cmd := dashboards.SaveDashboardCommand{ - OrgID: otherOrgId, - Dashboard: simplejson.NewFromAny(map[string]any{ - "uid": sc.savedDashInFolder.UID, - "title": "Dash with existing uid in other org", - }), - Overwrite: false, - } - - res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) - require.NotNil(t, res) - - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - OrgID: otherOrgId, - UID: sc.savedDashInFolder.UID, - }) - require.NoError(t, err) - }) - }) - - t.Run("Given user has permission to save", func(t *testing.T) { - t.Run("and overwrite flag is set to false", func(t *testing.T) { - const shouldOverwrite = false - - permissionScenario(t, "When creating a dashboard in General folder with same name as dashboard in other folder", func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": nil, - "title": sc.savedDashInFolder.Title, - }), - FolderUID: "", - Overwrite: shouldOverwrite, - } - - res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) - require.NotNil(t, res) - - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: res.ID, - OrgID: cmd.OrgID, - }) - - require.NoError(t, err) - }) - - permissionScenario(t, "When creating a dashboard in other folder with same name as dashboard in General folder", func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": nil, - "title": sc.savedDashInGeneralFolder.Title, - }), - FolderUID: sc.savedFolder.UID, - Overwrite: shouldOverwrite, - } - - res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) - require.NotNil(t, res) - - assert.NotEqual(t, sc.savedDashInGeneralFolder.ID, res.ID) - - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: res.ID, - OrgID: cmd.OrgID, - }) - require.NoError(t, err) - }) - - permissionScenario(t, "When creating a folder with same name as dashboard in other folder", func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": nil, - "title": sc.savedDashInFolder.Title, - }), - IsFolder: true, - Overwrite: shouldOverwrite, - } - - res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) - require.NotNil(t, res) - - assert.NotEqual(t, sc.savedDashInGeneralFolder.ID, res.ID) - assert.True(t, res.IsFolder) - - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: res.ID, - OrgID: cmd.OrgID, - }) - require.NoError(t, err) - }) - - permissionScenario(t, "When saving a dashboard without id and uid and unique title in folder", func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "title": "Dash without id and uid", - }), - Overwrite: shouldOverwrite, - } - - res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) - require.NotNil(t, res) - - assert.Greater(t, res.ID, int64(0)) - assert.NotEmpty(t, res.UID) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: res.ID, - OrgID: cmd.OrgID, - }) - require.NoError(t, err) - }) - - permissionScenario(t, "When saving a dashboard when dashboard id is zero ", func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": 0, - "title": "Dash with zero id", - }), - Overwrite: shouldOverwrite, - } - - res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) - require.NotNil(t, res) - - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: res.ID, - OrgID: cmd.OrgID, - }) - require.NoError(t, err) - }) - - permissionScenario(t, "When saving a dashboard in non-existing folder", func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "title": "Expect error", - }), - FolderUID: "123412321", - Overwrite: shouldOverwrite, - } - - _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) - assert.Equal(t, dashboards.ErrFolderNotFound, err) - }) - - permissionScenario(t, "When updating an existing dashboard by id without current version", func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": sc.savedDashInGeneralFolder.ID, - "title": "test dash 23", - }), - FolderUID: sc.savedFolder.UID, - Overwrite: shouldOverwrite, - } - - _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) - assert.Equal(t, dashboards.ErrDashboardVersionMismatch, err) - }) - - permissionScenario(t, "When updating an existing dashboard by id with current version", func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": sc.savedDashInGeneralFolder.ID, - "title": "Updated title", - "version": sc.savedDashInGeneralFolder.Version, - }), - FolderUID: sc.savedFolder.UID, - Overwrite: shouldOverwrite, - } - - res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) - require.NotNil(t, res) - - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: sc.savedDashInGeneralFolder.ID, - OrgID: cmd.OrgID, - }) - - require.NoError(t, err) - }) - - permissionScenario(t, "When updating an existing dashboard by uid without current version", func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "uid": sc.savedDashInFolder.UID, - "title": "test dash 23", - }), - FolderUID: "", - Overwrite: shouldOverwrite, - } - - _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) - assert.Equal(t, dashboards.ErrDashboardVersionMismatch, err) - }) - - permissionScenario(t, "When updating an existing dashboard by uid with current version", func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "uid": sc.savedDashInFolder.UID, - "title": "Updated title", - "version": sc.savedDashInFolder.Version, - }), - FolderUID: "", - Overwrite: shouldOverwrite, - } - - res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) - require.NotNil(t, res) - - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: sc.savedDashInFolder.ID, - OrgID: cmd.OrgID, - }) - require.NoError(t, err) - }) - - permissionScenario(t, "When creating a dashboard with same name as dashboard in other folder", func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": nil, - "title": sc.savedDashInFolder.Title, - }), - FolderUID: sc.savedDashInFolder.FolderUID, - Overwrite: shouldOverwrite, - } - - _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) - require.NoError(t, err) - }) - - permissionScenario(t, "When creating a dashboard with same name as dashboard in General folder", func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": nil, - "title": sc.savedDashInGeneralFolder.Title, - }), - FolderUID: sc.savedDashInGeneralFolder.FolderUID, - Overwrite: shouldOverwrite, - } - - _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) - require.NoError(t, err) - }) - - permissionScenario(t, "When creating a folder with same name as existing folder", func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": nil, - "title": sc.savedFolder.Title, - }), - IsFolder: true, - Overwrite: shouldOverwrite, - } - - _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) - require.NoError(t, err) - }) - }) - - t.Run("and overwrite flag is set to true", func(t *testing.T) { - const shouldOverwrite = true - - permissionScenario(t, "When updating an existing dashboard by id without current version", func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": sc.savedDashInGeneralFolder.ID, - "title": "Updated title", - }), - FolderUID: sc.savedFolder.UID, - Overwrite: shouldOverwrite, - } - - res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) - require.NotNil(t, res) - - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: sc.savedDashInGeneralFolder.ID, - OrgID: cmd.OrgID, - }) - require.NoError(t, err) - }) - - permissionScenario(t, "When updating an existing dashboard by uid without current version", func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "uid": sc.savedDashInFolder.UID, - "title": "Updated title", - }), - FolderUID: "", - Overwrite: shouldOverwrite, - } - - res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) - require.NotNil(t, res) - - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: sc.savedDashInFolder.ID, - OrgID: cmd.OrgID, - }) - require.NoError(t, err) - }) - - permissionScenario(t, "When updating uid for existing dashboard using id", func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": sc.savedDashInFolder.ID, - "uid": "new-uid", - "title": sc.savedDashInFolder.Title, - }), - Overwrite: shouldOverwrite, - } - - res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) - require.NotNil(t, res) - assert.Equal(t, sc.savedDashInFolder.ID, res.ID) - assert.Equal(t, "new-uid", res.UID) - - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: sc.savedDashInFolder.ID, - OrgID: cmd.OrgID, - }) - require.NoError(t, err) - }) - - permissionScenario(t, "When updating uid to an existing uid for existing dashboard using id", func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": sc.savedDashInFolder.ID, - "uid": sc.savedDashInGeneralFolder.UID, - "title": sc.savedDashInFolder.Title, - }), - Overwrite: shouldOverwrite, - } - - _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) - assert.Equal(t, dashboards.ErrDashboardWithSameUIDExists, err) - }) - - permissionScenario(t, "When creating a dashboard with same name as dashboard in other folder", func(t *testing.T, sc *permissionScenarioContext) { - t.Skip() - - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": nil, - "title": sc.savedDashInFolder.Title, - }), - FolderUID: sc.savedDashInFolder.FolderUID, - Overwrite: shouldOverwrite, - } - - res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) - require.NotNil(t, res) - assert.Equal(t, sc.savedDashInFolder.ID, res.ID) - assert.Equal(t, sc.savedDashInFolder.UID, res.UID) - - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: res.ID, - OrgID: cmd.OrgID, - }) - require.NoError(t, err) - }) - - permissionScenario(t, "When creating a dashboard with same name as dashboard in General folder", func(t *testing.T, sc *permissionScenarioContext) { - t.Skip() - - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": nil, - "title": sc.savedDashInGeneralFolder.Title, - }), - FolderUID: sc.savedDashInGeneralFolder.FolderUID, - Overwrite: shouldOverwrite, - } - - res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) - require.NotNil(t, res) - assert.Equal(t, sc.savedDashInGeneralFolder.ID, res.ID) - assert.Equal(t, sc.savedDashInGeneralFolder.UID, res.UID) - - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: res.ID, - OrgID: cmd.OrgID, - }) - require.NoError(t, err) - }) - - permissionScenario(t, "When updating existing folder to a dashboard using id", func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": sc.savedFolder.ID, - "title": "new title", - }), - IsFolder: false, - Overwrite: shouldOverwrite, - } - - _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) - assert.Equal(t, dashboards.ErrDashboardTypeMismatch, err) - }) - - permissionScenario(t, "When updating existing dashboard to a folder using id", func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": sc.savedDashInFolder.ID, - "title": "new folder title", - }), - IsFolder: true, - Overwrite: shouldOverwrite, - } - - _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) - assert.Equal(t, dashboards.ErrDashboardTypeMismatch, err) - }) - - permissionScenario(t, "When updating existing folder to a dashboard using uid", func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "uid": sc.savedFolder.UID, - "title": "new title", - }), - IsFolder: false, - Overwrite: shouldOverwrite, - } - - _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) - assert.Equal(t, dashboards.ErrDashboardTypeMismatch, err) - }) - - permissionScenario(t, "When updating existing dashboard to a folder using uid", func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "uid": sc.savedDashInFolder.UID, - "title": "new folder title", - }), - IsFolder: true, - Overwrite: shouldOverwrite, - } - - _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) - assert.Equal(t, dashboards.ErrDashboardTypeMismatch, err) - }) - - permissionScenario(t, "When updating existing folder to a dashboard using title", func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "title": sc.savedFolder.Title, - }), - IsFolder: false, - Overwrite: shouldOverwrite, - } - - _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) - require.NoError(t, err) - }) - - permissionScenario(t, "When updating existing dashboard to a folder using title", func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "title": sc.savedDashInGeneralFolder.Title, - }), - IsFolder: true, - Overwrite: shouldOverwrite, - } - - _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) - require.NoError(t, err) - }) - }) - }) - }) -} - -func TestIntegrationDashboardServicePermissions(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - t.Run("Given saved folders and dashboards in organization A", func(t *testing.T) { - permissionScenario(t, "When creating a new dashboard in the General folder, requires create permissions scoped to the general folder", - func(t *testing.T, sc *permissionScenarioContext) { - sqlStore := db.InitTestDB(t) - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "title": "Dash", - }), - UserID: 10000, - Overwrite: true, - } - - permissions := map[int64]map[string][]string{ - testOrgID: { - dashboards.ActionDashboardsWrite: {dashboards.ScopeDashboardsAll}, - }, - } - _, err := callSaveWithResult(t, cmd, sqlStore, permissions) - assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - - permissions = map[int64]map[string][]string{ - testOrgID: { - dashboards.ActionDashboardsCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID(accesscontrol.GeneralFolderUID)}, - }, - } - _, err = callSaveWithResult(t, cmd, sqlStore, permissions) - assert.Nil(t, err) - }) - - permissionScenario(t, "When creating a new dashboard in other folder, requires create permissions scoped to the other folder", func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "title": "Dash", - }), - FolderUID: sc.otherSavedFolder.UID, - UserID: 10000, - Overwrite: true, - } - - permissions := map[int64]map[string][]string{ - testOrgID: { - dashboards.ActionDashboardsCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("different_folder_uid")}, - }, - } - _, err := callSaveWithResult(t, cmd, sc.sqlStore, permissions) - assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - - permissions = map[int64]map[string][]string{ - testOrgID: { - dashboards.ActionDashboardsCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID(sc.otherSavedFolder.UID)}, - }, - } - _, err = callSaveWithResult(t, cmd, sc.sqlStore, permissions) - assert.Nil(t, err) - }) - - permissionScenario(t, "When creating a new dashboard by existing UID in folder, requires write permissions on the existing dashboard", func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "uid": sc.savedDashInFolder.UID, - "title": "New dash", - }), - FolderUID: sc.savedFolder.UID, - UserID: 10000, - Overwrite: true, - } - - permissions := map[int64]map[string][]string{ - testOrgID: { - dashboards.ActionDashboardsWrite: {dashboards.ScopeDashboardsProvider.GetResourceScopeUID("different_dash_uid")}, - }, - } - _, err := callSaveWithResult(t, cmd, sc.sqlStore, permissions) - assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - - permissions = map[int64]map[string][]string{ - testOrgID: { - dashboards.ActionDashboardsWrite: {dashboards.ScopeDashboardsProvider.GetResourceScopeUID(sc.savedDashInFolder.UID)}, - }, - } - _, err = callSaveWithResult(t, cmd, sc.sqlStore, permissions) - assert.Nil(t, err) - }) - - permissionScenario(t, "When moving a dashboard by existing uid to other folder from General folder, requires dashboard creation permissions on the destination folder and write access to the dashboard", func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "uid": sc.savedDashInGeneralFolder.UID, - "title": "Dash", - }), - FolderUID: sc.otherSavedFolder.UID, - UserID: 10000, - Overwrite: true, - } - - // Perms to write dashboard but not create dashboards in the destination folder - permissions := map[int64]map[string][]string{ - testOrgID: { - dashboards.ActionDashboardsWrite: {dashboards.ScopeDashboardsProvider.GetResourceScopeUID(sc.savedDashInGeneralFolder.UID)}, - }, - } - _, err := callSaveWithResult(t, cmd, sc.sqlStore, permissions) - assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - - // Perms to create dashboards in the destination folder but not write the dashboard - permissions = map[int64]map[string][]string{ - testOrgID: { - dashboards.ActionDashboardsCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID(sc.otherSavedFolder.UID)}, - }, - } - _, err = callSaveWithResult(t, cmd, sc.sqlStore, permissions) - assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - - // Perms to write dashboard and create dashboards in the destination folder - permissions = map[int64]map[string][]string{ - testOrgID: { - dashboards.ActionDashboardsWrite: {dashboards.ScopeDashboardsProvider.GetResourceScopeUID(sc.savedDashInGeneralFolder.UID)}, - dashboards.ActionDashboardsCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID(sc.otherSavedFolder.UID)}, - }, - } - _, err = callSaveWithResult(t, cmd, sc.sqlStore, permissions) - assert.Nil(t, err) - }) - - permissionScenario(t, "When moving a dashboard by existing uid to the General folder from other folder, requires dashboard creation permissions on the general folder and write access to the dashboard", func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "uid": sc.savedDashInFolder.UID, - "title": "Dash", - }), - FolderUID: "", - UserID: 10000, - Overwrite: true, - } - - // Perms to write dashboard but not create dashboards in the destination folder - permissions := map[int64]map[string][]string{ - testOrgID: { - dashboards.ActionDashboardsWrite: {dashboards.ScopeDashboardsProvider.GetResourceScopeUID(sc.savedDashInFolder.UID)}, - }, - } - _, err := callSaveWithResult(t, cmd, sc.sqlStore, permissions) - assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - - // Perms to create dashboards in the destination folder but not write the dashboard - permissions = map[int64]map[string][]string{ - testOrgID: { - dashboards.ActionDashboardsCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID(accesscontrol.GeneralFolderUID)}, - }, - } - _, err = callSaveWithResult(t, cmd, sc.sqlStore, permissions) - assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - - // Perms to write dashboard and create dashboards in the destination folder - permissions = map[int64]map[string][]string{ - testOrgID: { - dashboards.ActionDashboardsWrite: {dashboards.ScopeDashboardsProvider.GetResourceScopeUID(sc.savedDashInFolder.UID)}, - dashboards.ActionDashboardsCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID(accesscontrol.GeneralFolderUID)}, - }, - } - _, err = callSaveWithResult(t, cmd, sc.sqlStore, permissions) - assert.NoError(t, err) - }) - }) -} - -type permissionScenarioContext struct { - sqlStore db.DB - dashboardStore dashboards.Store - savedFolder *dashboards.Dashboard - savedDashInFolder *dashboards.Dashboard - otherSavedFolder *dashboards.Dashboard - savedDashInGeneralFolder *dashboards.Dashboard -} - -type permissionScenarioFunc func(t *testing.T, sc *permissionScenarioContext) - -func permissionScenario(t *testing.T, desc string, fn permissionScenarioFunc) { - t.Helper() - - t.Run(desc, func(t *testing.T) { - features := featuremgmt.WithFeatures() - cfg := setting.NewCfg() - sqlStore := db.InitTestDB(t) - quotaService := quotatest.New(false, nil) - ac := actest.FakeAccessControl{ExpectedEvaluate: true} - dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore)) - require.NoError(t, err) - folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - folderPermissions := accesscontrolmock.NewMockedPermissionsService() - folderPermissions.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) - tracer := tracing.InitializeTracerForTest() - publicDashboardFakeService := publicdashboards.NewFakePublicDashboardServiceWrapper(t) - folderStore2 := folderimpl.ProvideStore(sqlStore) - folderService := folderimpl.ProvideService( - folderStore2, - actest.FakeAccessControl{ExpectedEvaluate: true}, - bus.ProvideBus(tracer), - dashboardStore, - folderStore, - nil, - sqlStore, - features, - supportbundlestest.NewFakeBundleService(), - publicDashboardFakeService, - cfg, - nil, - tracer, - nil, - dualwrite.ProvideTestService(), - sort.ProvideService(), - apiserver.WithoutRestConfig, - ) - dashboardPermissions := accesscontrolmock.NewMockedPermissionsService() - dashboardService, err := ProvideDashboardServiceImpl( - cfg, dashboardStore, folderStore, - featuremgmt.WithFeatures(), - folderPermissions, - ac, - actest.FakeService{}, - folderService, - nil, - client.MockTestRestConfig{}, - nil, - quotaService, - nil, - nil, - nil, - dualwrite.ProvideTestService(), - sort.ProvideService(), - serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()), - kvstore.NewFakeKVStore(), - ) - dashboardService.RegisterDashboardPermissions(dashboardPermissions) - require.NoError(t, err) - - savedFolder := saveTestFolder(t, "Saved folder", testOrgID, sqlStore) - savedDashInFolder := saveTestDashboard(t, "Saved dash in folder", testOrgID, savedFolder.UID, sqlStore) - saveTestDashboard(t, "Other saved dash in folder", testOrgID, savedFolder.UID, sqlStore) - savedDashInGeneralFolder := saveTestDashboard(t, "Saved dashboard in general folder", testOrgID, "", sqlStore) - otherSavedFolder := saveTestFolder(t, "Other saved folder", testOrgID, sqlStore) - - require.Equal(t, "Saved folder", savedFolder.Title) - require.Equal(t, "saved-folder", savedFolder.Slug) - require.NotEqual(t, int64(0), savedFolder.ID) - require.True(t, savedFolder.IsFolder) - require.NotEmpty(t, savedFolder.UID) - - require.Equal(t, "Saved dash in folder", savedDashInFolder.Title) - require.Equal(t, "saved-dash-in-folder", savedDashInFolder.Slug) - require.NotEqual(t, int64(0), savedDashInFolder.ID) - require.False(t, savedDashInFolder.IsFolder) - require.NotEmpty(t, savedDashInFolder.UID) - - sc := &permissionScenarioContext{ - sqlStore: sqlStore, - savedDashInFolder: savedDashInFolder, - otherSavedFolder: otherSavedFolder, - savedDashInGeneralFolder: savedDashInGeneralFolder, - savedFolder: savedFolder, - dashboardStore: dashboardStore, - } - - fn(t, sc) - }) -} - -func callSaveWithResult(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlStore db.DB, permissions map[int64]map[string][]string) (*dashboards.Dashboard, error) { - t.Helper() - - features := featuremgmt.WithFeatures() - dto := toSaveDashboardDto(cmd) - var ac accesscontrol.AccessControl - ac = actest.FakeAccessControl{ExpectedEvaluate: true} - if permissions != nil { - dto.User = &user.SignedInUser{UserID: cmd.UserID, OrgID: testOrgID, Permissions: permissions} - ac = acimpl.ProvideAccessControl(features) - } - cfg := setting.NewCfg() - quotaService := quotatest.New(false, nil) - dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore)) - require.NoError(t, err) - folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - folderPermissions := accesscontrolmock.NewMockedPermissionsService() - folderPermissions.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) - tracer := tracing.InitializeTracerForTest() - publicDashboardFakeService := publicdashboards.NewFakePublicDashboardServiceWrapper(t) - folderStore2 := folderimpl.ProvideStore(sqlStore) - folderService := folderimpl.ProvideService( - folderStore2, - actest.FakeAccessControl{ExpectedEvaluate: true}, - bus.ProvideBus(tracer), - dashboardStore, - folderStore, - nil, - sqlStore, - features, - supportbundlestest.NewFakeBundleService(), - publicDashboardFakeService, - cfg, - nil, - tracer, - nil, - dualwrite.ProvideTestService(), - sort.ProvideService(), - apiserver.WithoutRestConfig, - ) - dashboardPermissions := accesscontrolmock.NewMockedPermissionsService() - dashboardPermissions.On("SetPermissions", - mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) - service, err := ProvideDashboardServiceImpl( - cfg, dashboardStore, folderStore, - featuremgmt.WithFeatures(), - folderPermissions, - ac, - actest.FakeService{}, - folderService, - nil, - client.MockTestRestConfig{}, - nil, - quotaService, - nil, - nil, - nil, - dualwrite.ProvideTestService(), - sort.ProvideService(), - serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()), - kvstore.NewFakeKVStore(), - ) - require.NoError(t, err) - service.RegisterDashboardPermissions(dashboardPermissions) - return service.SaveDashboard(context.Background(), &dto, false) -} - -func saveTestDashboard(t *testing.T, title string, orgID int64, folderUID string, sqlStore db.DB) *dashboards.Dashboard { - t.Helper() - - cmd := dashboards.SaveDashboardCommand{ - OrgID: orgID, - FolderUID: folderUID, - IsFolder: false, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": nil, - "title": title, - }), - } - - dto := dashboards.SaveDashboardDTO{ - OrgID: orgID, - Dashboard: cmd.GetDashboardModel(), - User: &user.SignedInUser{ - UserID: 1, - OrgRole: org.RoleAdmin, - }, - } - features := featuremgmt.WithFeatures() - cfg := setting.NewCfg() - quotaService := quotatest.New(false, nil) - dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore)) - require.NoError(t, err) - folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - dashboardPermissions := accesscontrolmock.NewMockedPermissionsService() - dashboardPermissions.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) - tracer := tracing.InitializeTracerForTest() - publicDashboardFakeService := publicdashboards.NewFakePublicDashboardServiceWrapper(t) - folderStore2 := folderimpl.ProvideStore(sqlStore) - folderService := folderimpl.ProvideService(folderStore2, - actest.FakeAccessControl{ExpectedEvaluate: true}, - bus.ProvideBus(tracer), - dashboardStore, - folderStore, - nil, - sqlStore, - features, - supportbundlestest.NewFakeBundleService(), - publicDashboardFakeService, - cfg, - nil, - tracer, - nil, - dualwrite.ProvideTestService(), - sort.ProvideService(), - apiserver.WithoutRestConfig, - ) - service, err := ProvideDashboardServiceImpl( - cfg, dashboardStore, folderStore, - features, - accesscontrolmock.NewMockedPermissionsService(), - actest.FakeAccessControl{ExpectedEvaluate: true}, - actest.FakeService{}, - folderService, - nil, - client.MockTestRestConfig{}, - nil, - quotaService, - nil, - nil, - nil, - dualwrite.ProvideTestService(), - sort.ProvideService(), - serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()), - kvstore.NewFakeKVStore(), - ) - require.NoError(t, err) - service.RegisterDashboardPermissions(dashboardPermissions) - res, err := service.SaveDashboard(context.Background(), &dto, false) - - require.NoError(t, err) - - return res -} - -func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore db.DB) *dashboards.Dashboard { - t.Helper() - cmd := dashboards.SaveDashboardCommand{ - OrgID: orgID, - FolderUID: "", - IsFolder: true, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": nil, - "title": title, - }), - } - - dto := dashboards.SaveDashboardDTO{ - OrgID: orgID, - Dashboard: cmd.GetDashboardModel(), - User: &user.SignedInUser{ - OrgID: orgID, - UserID: 1, - OrgRole: org.RoleAdmin, - Permissions: map[int64]map[string][]string{ - orgID: {dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll}, dashboards.ActionDashboardsWrite: {dashboards.ScopeDashboardsAll}}, - }, - }, - } - - features := featuremgmt.WithFeatures() - cfg := setting.NewCfg() - quotaService := quotatest.New(false, nil) - dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore)) - require.NoError(t, err) - folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - folderPermissions := accesscontrolmock.NewMockedPermissionsService() - tracer := tracing.InitializeTracerForTest() - publicDashboardFakeService := publicdashboards.NewFakePublicDashboardServiceWrapper(t) - folderStore2 := folderimpl.ProvideStore(sqlStore) - folderService := folderimpl.ProvideService(folderStore2, - actest.FakeAccessControl{ExpectedEvaluate: true}, - bus.ProvideBus(tracer), - dashboardStore, - folderStore, - nil, - sqlStore, - features, - supportbundlestest.NewFakeBundleService(), - publicDashboardFakeService, - cfg, - nil, - tracer, - nil, - dualwrite.ProvideTestService(), - sort.ProvideService(), - apiserver.WithoutRestConfig, - ) - folderPermissions.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) - service, err := ProvideDashboardServiceImpl( - cfg, dashboardStore, folderStore, - featuremgmt.WithFeatures(), - folderPermissions, - actest.FakeAccessControl{ExpectedEvaluate: true}, - actest.FakeService{}, - folderService, - nil, - client.MockTestRestConfig{}, - nil, - quotaService, - nil, - nil, - nil, - dualwrite.ProvideTestService(), - sort.ProvideService(), - serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()), - kvstore.NewFakeKVStore(), - ) - require.NoError(t, err) - service.RegisterDashboardPermissions(accesscontrolmock.NewMockedPermissionsService()) - res, err := service.SaveDashboard(context.Background(), &dto, false) - require.NoError(t, err) - - return res -} - -func toSaveDashboardDto(cmd dashboards.SaveDashboardCommand) dashboards.SaveDashboardDTO { - dash := (&cmd).GetDashboardModel() - - return dashboards.SaveDashboardDTO{ - Dashboard: dash, - Message: cmd.Message, - OrgID: cmd.OrgID, - User: &user.SignedInUser{UserID: cmd.UserID}, - Overwrite: cmd.Overwrite, - } -} diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go index 68da3b789cc..34a9279bec4 100644 --- a/pkg/services/dashboards/service/dashboard_service_test.go +++ b/pkg/services/dashboards/service/dashboard_service_test.go @@ -47,8 +47,13 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/storage/unified/search" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestDashboardService(t *testing.T) { t.Run("Dashboard service tests", func(t *testing.T) { fakeStore := dashboards.FakeDashboardStore{} diff --git a/pkg/tests/api/dashboards/api_dashboards_test.go b/pkg/tests/api/dashboards/api_dashboards_test.go index 37a1f195044..0d87e0c6f00 100644 --- a/pkg/tests/api/dashboards/api_dashboards_test.go +++ b/pkg/tests/api/dashboards/api_dashboards_test.go @@ -21,8 +21,11 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" + "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/plugindashboards" "github.com/grafana/grafana/pkg/services/search/model" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/tests" "github.com/grafana/grafana/pkg/tests/testinfra" "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util" @@ -32,21 +35,243 @@ func TestMain(m *testing.M) { testsuite.Run(m) } +func TestIntegrationDashboardServiceValidation(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ + DisableAnonymous: true, + EnableFeatureToggles: []string{featuremgmt.FlagKubernetesClientDashboardsFolders}, + }) + grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) + + orgPayload := map[string]interface{}{ + "name": "Org B", + } + orgPayloadBytes, err := json.Marshal(orgPayload) + require.NoError(t, err) + + orgURL := fmt.Sprintf("http://admin:admin@%s/api/orgs", grafanaListedAddr) + orgResp, err := http.Post(orgURL, "application/json", bytes.NewBuffer(orgPayloadBytes)) // nolint:gosec + require.NoError(t, err) + assert.Equal(t, http.StatusOK, orgResp.StatusCode) + err = orgResp.Body.Close() + require.NoError(t, err) + + tests.CreateUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleAdmin), + Login: "admin-org2", + Password: "admin", + IsAdmin: true, + OrgID: 2, + }) + + savedFolder := createFolder(t, grafanaListedAddr, "Saved folder") + savedDashInFolder := createDashboard(t, grafanaListedAddr, "Saved dash in folder", savedFolder.ID, savedFolder.UID) // nolint:staticcheck + savedDashInGeneralFolder := createDashboard(t, grafanaListedAddr, "Saved dashboard in general folder", 0, "") + + t.Run("When saving a dashboard with non-existing id in org A", func(t *testing.T) { + resp, err := postDashboard(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{ + "dashboard": map[string]interface{}{ + "id": 123412321, + "title": "Expect error", + }, + }) + require.NoError(t, err) + assert.Equal(t, http.StatusNotFound, resp.StatusCode) + err = resp.Body.Close() + require.NoError(t, err) + }) + + t.Run("When saving a dashboard with existing ID from org A in org B", func(t *testing.T) { + resp, err := postDashboard(t, grafanaListedAddr, "admin-org2", "admin", map[string]interface{}{ + "dashboard": map[string]interface{}{ + "id": savedDashInFolder.ID, // nolint:staticcheck + "title": "Expect error", + }, + }) + require.NoError(t, err) + assert.Equal(t, http.StatusNotFound, resp.StatusCode) + err = resp.Body.Close() + require.NoError(t, err) + }) + + t.Run("When saving a dashboard with same UID in org A and org B, should be okay", func(t *testing.T) { + resp, err := postDashboard(t, grafanaListedAddr, "admin-org2", "admin", map[string]interface{}{ + "dashboard": map[string]interface{}{ + "uid": savedDashInFolder.UID, + "title": "Saved dash in folder", + }, + }) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + err = resp.Body.Close() + require.NoError(t, err) + }) + + t.Run("When creating a dashboard in General folder with same name as dashboard in other folder", func(t *testing.T) { + resp, err := postDashboard(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{ + "dashboard": map[string]interface{}{ + "title": "Saved dash in folder", + }, + }) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + err = resp.Body.Close() + require.NoError(t, err) + }) + t.Run("When creating a dashboard in other folder with same name as dashboard in General folder", func(t *testing.T) { + resp, err := postDashboard(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{ + "dashboard": map[string]interface{}{ + "uid": savedDashInFolder, + "title": "Dash with existing uid in other org", + }, + }) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + err = resp.Body.Close() + require.NoError(t, err) + }) + + t.Run("When creating a folder with same name as dashboard in other folder", func(t *testing.T) { + f := createFolder(t, grafanaListedAddr, "Saved dashboard in general folder") + require.Equal(t, f.Title, "Saved dashboard in general folder") + }) + + t.Run("When saving a dashboard without id and uid and unique title in folder", func(t *testing.T) { + resp, err := postDashboard(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{ + "dashboard": map[string]interface{}{ + "title": "Unique", + }, + }) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + err = resp.Body.Close() + require.NoError(t, err) + }) + + t.Run("When saving a dashboard with id 0", func(t *testing.T) { + resp, err := postDashboard(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{ + "dashboard": map[string]interface{}{ + "id": 0, + "title": "Dash with zero id", + }, + }) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + err = resp.Body.Close() + require.NoError(t, err) + }) + + t.Run("When saving a dashboard in non-existing folder", func(t *testing.T) { + resp, err := postDashboard(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{ + "dashboard": map[string]interface{}{ + "title": "no folder", + }, + "folderUid": "non-existing-folder", + }) + require.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + err = resp.Body.Close() + require.NoError(t, err) + }) + + t.Run("When saving a dashboard with incorrect version but no overwrite", func(t *testing.T) { + resp, err := postDashboard(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{ + "dashboard": map[string]interface{}{ + "uid": savedDashInFolder.UID, + "version": 1, + }, + "folderUid": savedDashInFolder.FolderUID, + }) + require.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + err = resp.Body.Close() + require.NoError(t, err) + }) + + t.Run("When saving a dashboard with current version and overwrite is true", func(t *testing.T) { + resp, err := postDashboard(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{ + "dashboard": map[string]interface{}{ + "uid": savedDashInFolder.UID, + "version": savedDashInFolder.Version, + "title": "Saved dash in folder", + }, + "folderUid": savedDashInFolder.FolderUID, + }) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + err = resp.Body.Close() + require.NoError(t, err) + }) + + t.Run("When saving a dashboard with no version set and title set to a folder title", func(t *testing.T) { + resp, err := postDashboard(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{ + "dashboard": map[string]interface{}{ + "uid": savedDashInFolder.UID, + "title": "Saved folder", + }, + "folderUid": savedDashInFolder.FolderUID, + "overwrite": true, + }) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + err = resp.Body.Close() + require.NoError(t, err) + }) + + t.Run("When updating uid with id", func(t *testing.T) { + resp, err := postDashboard(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{ + "dashboard": map[string]interface{}{ + "id": savedDashInFolder.ID, // nolint:staticcheck + "uid": "new-uid", + "title": "Updated title", + }, + "folderUid": savedDashInFolder.FolderUID, + "overwrite": true, + }) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + err = resp.Body.Close() + require.NoError(t, err) + }) + t.Run("When updating uid with a dashboard already using that uid", func(t *testing.T) { + resp, err := postDashboard(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{ + "dashboard": map[string]interface{}{ + "id": savedDashInFolder.ID, // nolint:staticcheck + "uid": savedDashInGeneralFolder.UID, + "title": "Updated title", + }, + "folderUid": savedDashInFolder.FolderUID, + "overwrite": true, + }) + require.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + err = resp.Body.Close() + require.NoError(t, err) + }) + + t.Run("When trying to update to a folder", func(t *testing.T) { + resp, err := postDashboard(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{ + "dashboard": map[string]interface{}{ + "id": savedDashInFolder.ID, // nolint:staticcheck + "uid": savedDashInFolder.UID, + "title": "Updated title", + }, + "isFolder": true, + "folderUid": savedDashInFolder.FolderUID, + "overwrite": true, + }) + require.NoError(t, err) + require.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + err = resp.Body.Close() + require.NoError(t, err) + }) +} + func TestIntegrationDashboardQuota(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - testDashboardQuota(t, []string{}) -} - -func TestIntegrationDashboardQuotaK8s(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - testDashboardQuota(t, []string{featuremgmt.FlagKubernetesClientDashboardsFolders}) -} - -func testDashboardQuota(t *testing.T, featureToggles []string) { // enable quota and set low dashboard quota // Setup Grafana and its Database dashboardQuota := int64(1) @@ -54,7 +279,7 @@ func testDashboardQuota(t *testing.T, featureToggles []string) { DisableAnonymous: true, EnableQuota: true, DashboardOrgQuota: &dashboardQuota, - EnableFeatureToggles: featureToggles, + EnableFeatureToggles: []string{featuremgmt.FlagKubernetesClientDashboardsFolders}, }) grafanaListedAddr, _ := testinfra.StartGrafanaEnv(t, dir, path) @@ -110,27 +335,10 @@ func testDashboardQuota(t *testing.T, featureToggles []string) { } func TestIntegrationUpdatingProvisionionedDashboards(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - - testUpdatingProvisionionedDashboards(t, []string{}) -} - -func TestIntegrationUpdatingProvisionionedDashboardsK8s(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - - // will be the default in g12 - testUpdatingProvisionionedDashboards(t, []string{featuremgmt.FlagKubernetesClientDashboardsFolders}) -} - -func testUpdatingProvisionionedDashboards(t *testing.T, featureToggles []string) { // Setup Grafana and its Database dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableAnonymous: true, - EnableFeatureToggles: featureToggles, + EnableFeatureToggles: []string{featuremgmt.FlagKubernetesClientDashboardsFolders}, }) provDashboardsDir := filepath.Join(dir, "conf", "provisioning", "dashboards") @@ -187,7 +395,7 @@ providers: var dashboardID int64 for _, d := range *dashboardList { dashboardUID = d.UID - dashboardID = d.ID + dashboardID = d.ID // nolint:staticcheck } assert.Equal(t, int64(1), dashboardID) @@ -281,34 +489,10 @@ providers: } func TestIntegrationCreate(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - - testCreate(t, []string{}) -} - -func TestIntegrationCreateK8s(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - - testCreate(t, []string{featuremgmt.FlagKubernetesClientDashboardsFolders}) -} - -func TestIntegrationPreserveSchemaVersion(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - - testPreserveSchemaVersion(t, []string{featuremgmt.FlagKubernetesClientDashboardsFolders}) -} - -func testCreate(t *testing.T, featureToggles []string) { // Setup Grafana and its Database dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableAnonymous: true, - EnableFeatureToggles: featureToggles, + EnableFeatureToggles: []string{featuremgmt.FlagKubernetesClientDashboardsFolders}, }) grafanaListedAddr, _ := testinfra.StartGrafanaEnv(t, dir, path) @@ -461,10 +645,10 @@ func intPtr(n int) *int { return &n } -func testPreserveSchemaVersion(t *testing.T, featureToggles []string) { +func TestIntegrationPreserveSchemaVersion(t *testing.T) { dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableAnonymous: true, - EnableFeatureToggles: featureToggles, + EnableFeatureToggles: []string{featuremgmt.FlagKubernetesClientDashboardsFolders}, }) grafanaListedAddr, _ := testinfra.StartGrafanaEnv(t, dir, path) @@ -553,25 +737,9 @@ func testPreserveSchemaVersion(t *testing.T, featureToggles []string) { } func TestIntegrationImportDashboardWithLibraryPanels(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - - testImportDashboardWithLibraryPanels(t, []string{}) -} - -func TestIntegrationImportDashboardWithLibraryPanelsK8s(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - - testImportDashboardWithLibraryPanels(t, []string{featuremgmt.FlagKubernetesClientDashboardsFolders}) -} - -func testImportDashboardWithLibraryPanels(t *testing.T, featureToggles []string) { dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableAnonymous: true, - EnableFeatureToggles: featureToggles, + EnableFeatureToggles: []string{featuremgmt.FlagKubernetesClientDashboardsFolders}, }) grafanaListedAddr, _ := testinfra.StartGrafanaEnv(t, dir, path) @@ -762,3 +930,297 @@ func testImportDashboardWithLibraryPanels(t *testing.T, featureToggles []string) }) }) } + +func createDashboard(t *testing.T, grafanaListedAddr string, title string, folderID int64, folderUID string) *dashboards.Dashboard { + t.Helper() + + buf := &bytes.Buffer{} + err := json.NewEncoder(buf).Encode(map[string]interface{}{ + "dashboard": map[string]interface{}{ + "title": title, + }, + "folderId": folderID, + "folderUid": folderUID, + "overwrite": true, + }) + require.NoError(t, err) + + u := fmt.Sprintf("http://admin:admin@%s/api/dashboards/db", grafanaListedAddr) + // nolint:gosec + resp, err := http.Post(u, "application/json", buf) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + t.Cleanup(func() { + err := resp.Body.Close() + require.NoError(t, err) + }) + + b, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + var saveResp struct { + Status string `json:"status"` + Slug string `json:"slug"` + Version int64 `json:"version"` + ID int64 `json:"id"` + UID string `json:"uid"` + URL string `json:"url"` + FolderUID string `json:"folderUid"` + } + err = json.Unmarshal(b, &saveResp) + require.NoError(t, err) + require.NotEmpty(t, saveResp.UID) + + return &dashboards.Dashboard{ + ID: saveResp.ID, // nolint:staticcheck + UID: saveResp.UID, + Slug: saveResp.Slug, + Version: int(saveResp.Version), + FolderUID: saveResp.FolderUID, + } +} + +func postDashboard(t *testing.T, grafanaListedAddr, user, password string, payload map[string]interface{}) (*http.Response, error) { + t.Helper() + + payloadBytes, err := json.Marshal(payload) + require.NoError(t, err) + + u := fmt.Sprintf("http://%s:%s@%s/api/dashboards/db", user, password, grafanaListedAddr) + return http.Post(u, "application/json", bytes.NewBuffer(payloadBytes)) // nolint:gosec +} + +func TestIntegrationDashboardServicePermissions(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ + DisableAnonymous: true, + EnableFeatureToggles: []string{featuremgmt.FlagKubernetesClientDashboardsFolders}, + }) + grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) + tests.CreateUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleEditor), + Login: "editor", + Password: "editor", + IsAdmin: false, + }) + tests.CreateUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleViewer), + Login: "viewer", + Password: "viewer", + IsAdmin: false, + }) + savedFolder := createFolder(t, grafanaListedAddr, "Saved folder") + otherSavedFolder := createFolder(t, grafanaListedAddr, "Other saved folder") + savedDashInFolder := createDashboard(t, grafanaListedAddr, "Saved dash in folder", savedFolder.ID, savedFolder.UID) // nolint:staticcheck + savedDashInGeneralFolder := createDashboard(t, grafanaListedAddr, "Saved dashboard in general folder", 0, "") + + t.Run("When creating a new dashboard in the General folder, requires create permissions scoped to the general folder", func(t *testing.T) { + dashboardPayload := map[string]interface{}{ + "dashboard": map[string]interface{}{ + "title": "Dash", + }, + "overwrite": true, + } + + payloadBytes, err := json.Marshal(dashboardPayload) + require.NoError(t, err) + + u := fmt.Sprintf("http://viewer:viewer@%s/api/dashboards/db", grafanaListedAddr) + resp, err := http.Post(u, "application/json", bytes.NewBuffer(payloadBytes)) // nolint:gosec + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, resp.StatusCode) + err = resp.Body.Close() + require.NoError(t, err) + + u = fmt.Sprintf("http://editor:editor@%s/api/dashboards/db", grafanaListedAddr) + resp, err = http.Post(u, "application/json", bytes.NewBuffer(payloadBytes)) // nolint:gosec + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + err = resp.Body.Close() + require.NoError(t, err) + }) + + t.Run("When creating a new dashboard in other folder, requires create permissions scoped to the other folder", func(t *testing.T) { + dashboardPayload := map[string]interface{}{ + "dashboard": map[string]interface{}{ + "title": "Dash", + }, + "folderUid": otherSavedFolder.UID, + "overwrite": true, + } + + resp, err := postDashboard(t, grafanaListedAddr, "viewer", "viewer", dashboardPayload) + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, resp.StatusCode) + err = resp.Body.Close() + require.NoError(t, err) + + resp, err = postDashboard(t, grafanaListedAddr, "editor", "editor", dashboardPayload) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + err = resp.Body.Close() + require.NoError(t, err) + }) + + t.Run("When creating a new dashboard by existing UID in folder, requires write permissions on the existing dashboard", func(t *testing.T) { + dashboardPayload := map[string]interface{}{ + "dashboard": map[string]interface{}{ + "uid": savedDashInFolder.UID, + "title": "New dash", + }, + "folderUid": savedFolder.UID, + "overwrite": true, + } + + resp, err := postDashboard(t, grafanaListedAddr, "viewer", "viewer", dashboardPayload) + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, resp.StatusCode) + err = resp.Body.Close() + require.NoError(t, err) + + resp, err = postDashboard(t, grafanaListedAddr, "editor", "editor", dashboardPayload) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + err = resp.Body.Close() + require.NoError(t, err) + }) + + t.Run("When moving a dashboard by existing uid to other folder from General folder, requires dashboard creation permissions on the destination folder and write access to the dashboard", func(t *testing.T) { + dashboardPayload := map[string]interface{}{ + "dashboard": map[string]interface{}{ + "uid": savedDashInGeneralFolder.UID, + "title": "Dash", + }, + "folderUid": otherSavedFolder.UID, + "overwrite": true, + } + + resp, err := postDashboard(t, grafanaListedAddr, "viewer", "viewer", dashboardPayload) + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, resp.StatusCode) + err = resp.Body.Close() + require.NoError(t, err) + + resp, err = postDashboard(t, grafanaListedAddr, "editor", "editor", dashboardPayload) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + err = resp.Body.Close() + require.NoError(t, err) + }) + + t.Run("When moving a dashboard by existing uid to the General folder from other folder, requires dashboard creation permissions on the general folder and write access to the dashboard", func(t *testing.T) { + dashboardPayload := map[string]interface{}{ + "dashboard": map[string]interface{}{ + "uid": savedDashInFolder.UID, + "title": "Dash", + }, + "folderUid": "", + "overwrite": true, + } + + resp, err := postDashboard(t, grafanaListedAddr, "viewer", "viewer", dashboardPayload) + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, resp.StatusCode) + err = resp.Body.Close() + require.NoError(t, err) + + resp, err = postDashboard(t, grafanaListedAddr, "editor", "editor", dashboardPayload) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + err = resp.Body.Close() + require.NoError(t, err) + }) + + t.Run("RBAC tests", func(t *testing.T) { + setFolderPermissions := func(t *testing.T, grafanaListedAddr string, folderUID string, permissions []map[string]interface{}) { + t.Helper() + + permissionPayload := map[string]interface{}{ + "items": permissions, + } + + payloadBytes, err := json.Marshal(permissionPayload) + require.NoError(t, err) + + u := fmt.Sprintf("http://admin:admin@%s/api/folders/%s/permissions", grafanaListedAddr, folderUID) + resp, err := http.Post(u, "application/json", bytes.NewBuffer(payloadBytes)) // nolint:gosec + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + err = resp.Body.Close() + require.NoError(t, err) + } + + searchDashboards := func(t *testing.T, grafanaListedAddr string, userLogin, userPassword string) []map[string]interface{} { + t.Helper() + + u := fmt.Sprintf("http://%s:%s@%s/api/search?type=dash-db", userLogin, userPassword, grafanaListedAddr) + resp, err := http.Get(u) // nolint:gosec + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + defer resp.Body.Close() // nolint:errcheck + + var results []map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&results) + require.NoError(t, err) + + return results + } + + noneUserID := tests.CreateUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleNone), + Login: "noneuser", + Password: "noneuser", + IsAdmin: false, + }) + parentFolder := createFolder(t, grafanaListedAddr, "parent") + childFolder := createFolder(t, grafanaListedAddr, "child") + createDashboard(t, grafanaListedAddr, "dashboard in root", 0, "") + createDashboard(t, grafanaListedAddr, "dashboard in parent", parentFolder.ID, parentFolder.UID) // nolint:staticcheck + createDashboard(t, grafanaListedAddr, "dashboard in child", childFolder.ID, childFolder.UID) // nolint:staticcheck + + viewPermissions := []map[string]interface{}{ + { + "permission": 1, + "userId": noneUserID, + }, + } + t.Run("it should not return folder if ACL is not set for parent folder", func(t *testing.T) { + results := searchDashboards(t, grafanaListedAddr, "noneuser", "noneuser") + assert.Empty(t, results, "Should not return any dashboards when no permissions are set") + }) + + t.Run("it should return child folder when user has permission to read child folder", func(t *testing.T) { + setFolderPermissions(t, grafanaListedAddr, childFolder.UID, viewPermissions) + results := searchDashboards(t, grafanaListedAddr, "noneuser", "noneuser") + + foundTitles := make([]string, 0) + for _, result := range results { + if title, ok := result["title"].(string); ok { + foundTitles = append(foundTitles, title) + } + } + + assert.Contains(t, foundTitles, "dashboard in child", "Should return dashboard in child folder") + }) + + t.Run("it should return parent folder when user has permission to read parent folder but no permission to read child folder", func(t *testing.T) { + setFolderPermissions(t, grafanaListedAddr, parentFolder.UID, viewPermissions) + setFolderPermissions(t, grafanaListedAddr, childFolder.UID, []map[string]interface{}{}) + + results := searchDashboards(t, grafanaListedAddr, "noneuser", "noneuser") + + foundTitles := make([]string, 0) + for _, result := range results { + if title, ok := result["title"].(string); ok { + foundTitles = append(foundTitles, title) + } + } + + assert.Contains(t, foundTitles, "dashboard in parent", "Should return dashboard in parent folder") + assert.NotContains(t, foundTitles, "dashboard in child", "Should not return dashboard in child folder") + }) + }) +}