diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index f034402f797..78e66c0eab9 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -676,11 +676,14 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d // Validate folder if dash.FolderID != 0 || dash.FolderUID != "" { // nolint:staticcheck - folder, err := dr.folderService.Get(ctx, &folder.GetFolderQuery{ + // use a background requester, because the user may have been granted edit access to that specific dashboard, but + // not have access to the parent folder. we should still allow editing in that case. + ctxIdentity, backgroundRequester := identity.WithServiceIdentity(ctx, dash.OrgID) + folder, err := dr.folderService.Get(ctxIdentity, &folder.GetFolderQuery{ OrgID: dash.OrgID, UID: &dash.FolderUID, ID: &dash.FolderID, // nolint:staticcheck - SignedInUser: dto.User, + SignedInUser: backgroundRequester, }) if err != nil { return nil, err diff --git a/pkg/tests/api/dashboards/api_dashboards_test.go b/pkg/tests/api/dashboards/api_dashboards_test.go index 7c080b84505..50ab7d939ea 100644 --- a/pkg/tests/api/dashboards/api_dashboards_test.go +++ b/pkg/tests/api/dashboards/api_dashboards_test.go @@ -1211,5 +1211,137 @@ func TestIntegrationDashboardServicePermissions(t *testing.T) { 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") }) + + t.Run("user with edit permissions on dashboard but not on parent folder should be able to edit the dashboard", func(t *testing.T) { + testFolder := createFolder(t, grafanaListedAddr, "restricted folder") + testDash := createDashboard(t, grafanaListedAddr, "dashboard with specific permissions", testFolder.ID, testFolder.UID) // nolint:staticcheck + restrictedUserID := tests.CreateUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleNone), + Login: "restricteduser", + Password: "restricteduser", + IsAdmin: false, + }) + setDashboardPermissions := func(t *testing.T, grafanaListedAddr string, dashboardUID 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/dashboards/uid/%s/permissions", grafanaListedAddr, dashboardUID) + req, err := http.NewRequest(http.MethodPost, u, bytes.NewBuffer(payloadBytes)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + err = resp.Body.Close() + require.NoError(t, err) + } + editPermissions := []map[string]interface{}{ + { + "permission": 2, + "userId": restrictedUserID, + }, + } + setDashboardPermissions(t, grafanaListedAddr, testDash.UID, editPermissions) + + // user cannot access the folder + u := fmt.Sprintf("http://restricteduser:restricteduser@%s/api/folders/%s", grafanaListedAddr, testFolder.UID) + resp, err := http.Get(u) // nolint:gosec + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, resp.StatusCode, "User should not have access to the folder") + err = resp.Body.Close() + require.NoError(t, err) + + // but can edit the dashboard + dashboardPayload := map[string]interface{}{ + "dashboard": map[string]interface{}{ + "uid": testDash.UID, + "title": "Updated title by restricted user", + }, + "folderUid": testFolder.UID, + "overwrite": true, + } + resp, err = postDashboard(t, grafanaListedAddr, "restricteduser", "restricteduser", dashboardPayload) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode, "User should be able to edit dashboard even without folder access") + err = resp.Body.Close() + require.NoError(t, err) + }) + + t.Run("user with no permissions on dashboard or parent folder should not be able to edit the dashboard", func(t *testing.T) { + testFolder := createFolder(t, grafanaListedAddr, "restricted folder") + testDash := createDashboard(t, grafanaListedAddr, "dashboard with specific permissions", testFolder.ID, testFolder.UID) // nolint:staticcheck + + // user cannot access the folder + u := fmt.Sprintf("http://restricteduser:restricteduser@%s/api/folders/%s", grafanaListedAddr, testFolder.UID) + resp, err := http.Get(u) // nolint:gosec + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, resp.StatusCode, "User should not have access to the folder") + err = resp.Body.Close() + require.NoError(t, err) + + // and cannot edit the dashboard + dashboardPayload := map[string]interface{}{ + "dashboard": map[string]interface{}{ + "uid": testDash.UID, + "title": "Updated title by restricted user", + }, + "folderUid": testFolder.UID, + "overwrite": true, + } + resp, err = postDashboard(t, grafanaListedAddr, "restricteduser", "restricteduser", dashboardPayload) + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, resp.StatusCode, "User should not be able to edit dashboard without any permissions") + err = resp.Body.Close() + require.NoError(t, err) + }) + + t.Run("user with edit permissions on parent folder should be able to edit dashboard through permission inheritance", func(t *testing.T) { + inheritedPermissionsUserID := tests.CreateUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleNone), + Login: "inheriteduser", + Password: "inheriteduser", + IsAdmin: false, + }) + testFolder := createFolder(t, grafanaListedAddr, "folder with inherited permissions") + editFolderPermissions := []map[string]interface{}{ + { + "permission": 2, + "userId": inheritedPermissionsUserID, + }, + } + setFolderPermissions(t, grafanaListedAddr, testFolder.UID, editFolderPermissions) + + // create dashboard in the folder without specific dashboard permissions + testDash := createDashboard(t, grafanaListedAddr, "dashboard inheriting permissions", testFolder.ID, testFolder.UID) // nolint:staticcheck + u := fmt.Sprintf("http://inheriteduser:inheriteduser@%s/api/folders/%s", grafanaListedAddr, testFolder.UID) + resp, err := http.Get(u) // nolint:gosec + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode, "User should have access to the folder") + err = resp.Body.Close() + require.NoError(t, err) + + // and can edit the dashboard by inheriting permissions from the folder + dashboardPayload := map[string]interface{}{ + "dashboard": map[string]interface{}{ + "uid": testDash.UID, + "title": "Updated title via inherited permissions", + }, + "folderUid": testFolder.UID, + "overwrite": true, + } + resp, err = postDashboard(t, grafanaListedAddr, "inheriteduser", "inheriteduser", dashboardPayload) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode, "User should be able to edit dashboard through inherited folder permissions") + err = resp.Body.Close() + require.NoError(t, err) + }) }) }