package folders import ( "bytes" "encoding/json" "fmt" "net/http" "testing" "github.com/grafana/grafana-openapi-client-go/client/folders" "github.com/grafana/grafana-openapi-client-go/models" "github.com/grafana/grafana/pkg/services/org" "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/util/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func setFolderPermissions(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) } func TestIntegrationFolderServiceGetFolder(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableLegacyAlerting: true, EnableUnifiedAlerting: true, DisableAnonymous: true, AppModeProduction: true, }) grafanaListedAddr, _ := testinfra.StartGrafanaEnv(t, dir, p) adminClient := tests.GetClient(grafanaListedAddr, "admin", "admin") parentResp, err := adminClient.Folders.CreateFolder(&models.CreateFolderCommand{ Title: "Parent Folder", UID: "parent-folder", }) require.NoError(t, err) require.Equal(t, http.StatusOK, parentResp.Code()) childResp, err := adminClient.Folders.CreateFolder(&models.CreateFolderCommand{ Title: "Child Folder", UID: "child-folder", ParentUID: parentResp.Payload.UID, }) require.NoError(t, err) require.Equal(t, http.StatusOK, childResp.Code()) testCases := []struct { name string WithFullpath bool WithFullpathUIDs bool expectedFullpath string expectedFullpathUIDs string }{ { name: "when flag is on and WithFullpath is false", WithFullpath: false, expectedFullpath: "", }, { name: "when flag is on and WithFullpath is true", WithFullpath: true, expectedFullpath: "Parent Folder/Child Folder", }, { name: "when flag is on and WithFullpathUIDs is false", WithFullpathUIDs: false, expectedFullpathUIDs: "", }, { name: "when flag is on and WithFullpathUIDs is true", WithFullpathUIDs: true, expectedFullpathUIDs: "parent-folder/child-folder", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { resp, err := adminClient.Folders.GetFolderByUID(childResp.Payload.UID) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.Code()) require.Equal(t, childResp.Payload.UID, resp.Payload.UID) require.Equal(t, childResp.Payload.CreatedBy, resp.Payload.CreatedBy) require.Equal(t, childResp.Payload.UpdatedBy, resp.Payload.UpdatedBy) }) } } func TestIntegrationUpdateFolder(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableAnonymous: true, EnableQuota: true, }) grafanaListedAddr, _ := testinfra.StartGrafanaEnv(t, dir, path) adminClient := tests.GetClient(grafanaListedAddr, "admin", "admin") resp, err := adminClient.Folders.CreateFolder(&models.CreateFolderCommand{ Title: "folder", }) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.Code()) t.Run("update folder should succeed", func(t *testing.T) { resp, err := adminClient.Folders.UpdateFolder(resp.Payload.UID, &models.UpdateFolderCommand{ Title: "new title", Version: resp.Payload.Version, }) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.Code()) require.Equal(t, "new title", resp.Payload.Title) }) t.Run("When updating a folder it should trim leading and trailing spaces", func(t *testing.T) { resp, err := adminClient.Folders.CreateFolder(&models.CreateFolderCommand{ Title: "my folder 2", }) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.Code()) updateResp, err := adminClient.Folders.UpdateFolder(resp.Payload.UID, &models.UpdateFolderCommand{ Title: " my updated folder 2 ", Version: resp.Payload.Version, }) require.NoError(t, err) require.Equal(t, http.StatusOK, updateResp.Code()) require.Equal(t, "my updated folder 2", updateResp.Payload.Title) }) } func TestIntegrationCreateFolder(t *testing.T) { dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableAnonymous: true, EnableQuota: true, }) grafanaListedAddr, _ := testinfra.StartGrafanaEnv(t, dir, path) adminClient := tests.GetClient(grafanaListedAddr, "admin", "admin") t.Run("create folder under root should succeed", func(t *testing.T) { resp, err := adminClient.Folders.CreateFolder(&models.CreateFolderCommand{ Title: "folder", }) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.Code()) t.Run("create folder with same name under root should succeed", func(t *testing.T) { _, err := adminClient.Folders.CreateFolder(&models.CreateFolderCommand{ Title: "folder", }) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.Code()) }) }) t.Run("When creating a folder it should trim leading and trailing spaces", func(t *testing.T) { resp, err := adminClient.Folders.CreateFolder(&models.CreateFolderCommand{ Title: " my folder ", }) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.Code()) require.Equal(t, "my folder", resp.Payload.Title) }) t.Run("create without UID, no error", func(t *testing.T) { resp, err := adminClient.Folders.CreateFolder(&models.CreateFolderCommand{ Title: "myFolder", }) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.Code()) require.NotEmpty(t, resp.Payload.UID) }) } func TestIntegrationNestedFolders(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableAnonymous: true, EnableQuota: true, }) grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) adminClient := tests.GetClient(grafanaListedAddr, "admin", "admin") t.Run("create folder under root should succeed", func(t *testing.T) { resp, err := adminClient.Folders.CreateFolder(&models.CreateFolderCommand{ Title: "folder", }) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.Code()) t.Run("create folder with same name under root should succeed", func(t *testing.T) { _, err := adminClient.Folders.CreateFolder(&models.CreateFolderCommand{ Title: "folder", }) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.Code()) }) }) t.Run("create subfolder should succeed", func(t *testing.T) { resp, err := adminClient.Folders.CreateFolder(&models.CreateFolderCommand{ Title: "parent", }) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.Code()) parentUID := resp.Payload.UID resp, err = adminClient.Folders.CreateFolder(&models.CreateFolderCommand{ Title: "subfolder", ParentUID: parentUID, }) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.Code()) t.Run("create subfolder with same name should succeed", func(t *testing.T) { resp, err = adminClient.Folders.CreateFolder(&models.CreateFolderCommand{ Title: "subfolder", ParentUID: parentUID, }) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.Code()) }) t.Run("create subfolder with same name under other folder should succeed", func(t *testing.T) { resp, err := adminClient.Folders.CreateFolder(&models.CreateFolderCommand{ Title: "other", }) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.Code()) other := resp.Payload.UID resp, err = adminClient.Folders.CreateFolder(&models.CreateFolderCommand{ Title: "subfolder", ParentUID: other, }) require.NoError(t, err) assert.Equal(t, http.StatusOK, resp.Code()) assert.Equal(t, other, resp.Payload.ParentUID) subfolderUnderOther := resp.Payload.UID t.Run("move subfolder to other folder containing folder with the same name should be ok", func(t *testing.T) { resp, err := adminClient.Folders.MoveFolder(subfolderUnderOther, &models.MoveFolderCommand{ ParentUID: parentUID, }) require.NoError(t, err) assert.Equal(t, http.StatusOK, resp.Code()) assert.Equal(t, parentUID, resp.Payload.ParentUID) }) t.Run("move subfolder to root should succeed", func(t *testing.T) { resp, err := adminClient.Folders.MoveFolder(subfolderUnderOther, &models.MoveFolderCommand{}) require.NoError(t, err) assert.Equal(t, http.StatusOK, resp.Code()) assert.Equal(t, "", resp.Payload.ParentUID) }) t.Run("should prevent moving folders to escalate permissions", func(t *testing.T) { store, cfg := env.SQLStore, env.Cfg orgID := int64(1) editorUser := tests.CreateUser(t, store, cfg, user.CreateUserCommand{ DefaultOrgRole: string(org.RoleViewer), OrgID: orgID, Password: "editor", Login: "editor", }) editorClient := tests.GetClient(grafanaListedAddr, "editor", "editor") sourceResp, err := adminClient.Folders.CreateFolder(&models.CreateFolderCommand{ Title: "Source Folder", UID: "source-folder-limited", }) require.NoError(t, err) require.Equal(t, http.StatusOK, sourceResp.Code()) destResp, err := adminClient.Folders.CreateFolder(&models.CreateFolderCommand{ Title: "Destination Folder", UID: "dest-folder-higher", }) require.NoError(t, err) require.Equal(t, http.StatusOK, destResp.Code()) // downgrade to viewer on destination folder setFolderPermissions(t, grafanaListedAddr, destResp.Payload.UID, []map[string]interface{}{ { "userId": editorUser, "permission": 1, }, }) _, err = editorClient.Folders.MoveFolder(sourceResp.Payload.UID, &models.MoveFolderCommand{ ParentUID: destResp.Payload.UID, }) require.Error(t, err) }) }) }) t.Run("should delete children of a folder when deleted", func(t *testing.T) { parentResp, err := adminClient.Folders.CreateFolder(&models.CreateFolderCommand{ Title: "Parent Folder for Delete", UID: "parent-folder-delete", }) require.NoError(t, err) require.Equal(t, http.StatusOK, parentResp.Code()) childResp, err := adminClient.Folders.CreateFolder(&models.CreateFolderCommand{ Title: "Child Folder for Delete", UID: "child-folder-delete", ParentUID: parentResp.Payload.UID, }) require.NoError(t, err) require.Equal(t, http.StatusOK, childResp.Code()) _, err = adminClient.Folders.DeleteFolder(folders.NewDeleteFolderParams().WithFolderUID(parentResp.Payload.UID)) require.NoError(t, err) _, err = adminClient.Folders.GetFolderByUID(childResp.Payload.UID) require.Error(t, err) }) } func TestIntegrationSharedWithMe(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableLegacyAlerting: true, EnableUnifiedAlerting: true, DisableAnonymous: true, AppModeProduction: true, }) grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p) store, cfg := env.SQLStore, env.Cfg orgID := int64(1) noneUser := tests.CreateUser(t, store, cfg, user.CreateUserCommand{ DefaultOrgRole: string(org.RoleNone), OrgID: orgID, Password: "none", Login: "none", }) adminClient := tests.GetClient(grafanaListedAddr, "admin", "admin") noneClient := tests.GetClient(grafanaListedAddr, "none", "none") t.Run("Should get folders shared with given user", func(t *testing.T) { folder1Resp, err := adminClient.Folders.CreateFolder(&models.CreateFolderCommand{ Title: "Folder 1", UID: "folder-1", }) require.NoError(t, err) require.Equal(t, http.StatusOK, folder1Resp.Code()) folder2Resp, err := adminClient.Folders.CreateFolder(&models.CreateFolderCommand{ Title: "Folder 2", UID: "folder-2", ParentUID: folder1Resp.Payload.UID, }) require.NoError(t, err) require.Equal(t, http.StatusOK, folder2Resp.Code()) setFolderPermissions(t, grafanaListedAddr, folder2Resp.Payload.UID, []map[string]interface{}{ { "userId": noneUser, "permission": 1, }, }) resp, err := noneClient.Folders.GetFolders(nil) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.Code()) require.Len(t, resp.Payload, 1) }) } func TestIntegrationBasicRoles(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableLegacyAlerting: true, EnableUnifiedAlerting: true, DisableAnonymous: true, AppModeProduction: true, }) grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p) store, cfg := env.SQLStore, env.Cfg orgID := int64(1) tests.CreateUser(t, store, cfg, user.CreateUserCommand{ DefaultOrgRole: string(org.RoleViewer), OrgID: orgID, Password: "viewer", Login: "viewer", }) tests.CreateUser(t, store, cfg, user.CreateUserCommand{ OrgID: orgID, DefaultOrgRole: string(org.RoleEditor), Password: "editor", Login: "editor", }) adminClient := tests.GetClient(grafanaListedAddr, "admin", "admin") viewerClient := tests.GetClient(grafanaListedAddr, "viewer", "viewer") t.Run("Folder service tests", func(t *testing.T) { t.Run("Given user has no permissions", func(t *testing.T) { folderUID := "test-folder-no-perm" t.Run("get folder by uid should return access denied error", func(t *testing.T) { _, err := viewerClient.Folders.GetFolderByUID(folderUID) require.Error(t, err) }) t.Run("create folder should return access denied error", func(t *testing.T) { _, err := viewerClient.Folders.CreateFolder(&models.CreateFolderCommand{ Title: "Test Folder", UID: "test-create-no-perm", }) require.Error(t, err) }) t.Run("update folder should return access denied error", func(t *testing.T) { _, err := viewerClient.Folders.UpdateFolder(folderUID, &models.UpdateFolderCommand{ Title: "Updated Title", }) require.Error(t, err) }) t.Run("delete folder by uid should return access denied error", func(t *testing.T) { _, err := viewerClient.Folders.DeleteFolder(folders.NewDeleteFolderParams().WithFolderUID(folderUID)) require.Error(t, err) }) }) t.Run("Given user has permission to save", func(t *testing.T) { t.Run("create folder should be ok", func(t *testing.T) { resp, err := adminClient.Folders.CreateFolder(&models.CreateFolderCommand{ Title: "Test-Folder", UID: "test-folder-create", }) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.Code()) require.Equal(t, "Test-Folder", resp.Payload.Title) }) t.Run("can never create a folder with the uid of general", func(t *testing.T) { _, err := adminClient.Folders.CreateFolder(&models.CreateFolderCommand{ Title: "Test-Folder", UID: "general", }) require.Error(t, err) }) t.Run("update should be okay", func(t *testing.T) { resp, err := adminClient.Folders.CreateFolder(&models.CreateFolderCommand{ Title: "Folder to Update", UID: "test-folder-update", }) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.Code()) updateResp, err := adminClient.Folders.UpdateFolder(resp.Payload.UID, &models.UpdateFolderCommand{ Title: "Updated Folder Title", Version: resp.Payload.Version, }) require.NoError(t, err) require.Equal(t, http.StatusOK, updateResp.Code()) require.Equal(t, "Updated Folder Title", updateResp.Payload.Title) }) t.Run("delete should be okay", func(t *testing.T) { resp, err := adminClient.Folders.CreateFolder(&models.CreateFolderCommand{ Title: "Folder to Delete", UID: "test-folder-delete", }) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.Code()) _, err = adminClient.Folders.DeleteFolder(folders.NewDeleteFolderParams().WithFolderUID(resp.Payload.UID)) require.NoError(t, err) }) }) t.Run("Given user has permission to view", func(t *testing.T) { t.Run("get folder by uid should return folder", func(t *testing.T) { resp, err := adminClient.Folders.CreateFolder(&models.CreateFolderCommand{ Title: "Viewable Folder", UID: "test-folder-view", }) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.Code()) getResp, err := viewerClient.Folders.GetFolderByUID(resp.Payload.UID) require.NoError(t, err) require.Equal(t, http.StatusOK, getResp.Code()) require.Equal(t, "Viewable Folder", getResp.Payload.Title) }) t.Run("get folder by uid and uid is general should return the root folder object", func(t *testing.T) { resp, err := adminClient.Folders.GetFolderByUID("general") require.NoError(t, err) require.Equal(t, http.StatusOK, resp.Code()) require.Equal(t, "Dashboards", resp.Payload.Title) }) }) }) } func TestIntegrationFineGrainedPermissions(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableLegacyAlerting: true, EnableUnifiedAlerting: true, DisableAnonymous: true, AppModeProduction: true, }) grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p) store, cfg := env.SQLStore, env.Cfg orgID := int64(1) noneUser := tests.CreateUser(t, store, cfg, user.CreateUserCommand{ DefaultOrgRole: string(org.RoleNone), OrgID: orgID, Password: "none", Login: "none", }) adminClient := tests.GetClient(grafanaListedAddr, "admin", "admin") noneClient := tests.GetClient(grafanaListedAddr, "none", "none") // create parent -> child -> grandchild folder structure parentResp, err := adminClient.Folders.CreateFolder(&models.CreateFolderCommand{ Title: "Parent Folder", UID: "parent-folder-fine-grained", }) require.NoError(t, err) require.Equal(t, http.StatusOK, parentResp.Code()) childResp, err := adminClient.Folders.CreateFolder(&models.CreateFolderCommand{ Title: "Child Folder", UID: "child-folder-fine-grained", ParentUID: parentResp.Payload.UID, }) require.NoError(t, err) require.Equal(t, http.StatusOK, childResp.Code()) grandchildResp, err := adminClient.Folders.CreateFolder(&models.CreateFolderCommand{ Title: "Grandchild Folder", UID: "grandchild-folder-fine-grained", ParentUID: childResp.Payload.UID, }) require.NoError(t, err) require.Equal(t, http.StatusOK, grandchildResp.Code()) // only grant access to the child folder, should then get child & grandchildren folders setFolderPermissions(t, grafanaListedAddr, childResp.Payload.UID, []map[string]interface{}{ { "userId": noneUser, "permission": 1, }, }) childFolderResp, err := noneClient.Folders.GetFolderByUID(childResp.Payload.UID) require.NoError(t, err) require.Equal(t, http.StatusOK, childFolderResp.Code()) assert.Equal(t, "Child Folder", childFolderResp.Payload.Title) grandchildFolderResp, err := noneClient.Folders.GetFolderByUID(grandchildResp.Payload.UID) require.NoError(t, err) require.Equal(t, http.StatusOK, grandchildFolderResp.Code()) assert.Equal(t, "Grandchild Folder", grandchildFolderResp.Payload.Title) _, err = noneClient.Folders.GetFolderByUID(parentResp.Payload.UID) require.Error(t, err, "None user should not be able to access parent folder directly") }