This is an automated email from the ASF dual-hosted git repository.

rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new c0117f78a9c feat(dashboard): respect GranularExportControls for 
download permission (#41799)
c0117f78a9c is described below

commit c0117f78a9ce014ed16c11d02928c99db42f8b4a
Author: Pawan <[email protected]>
AuthorDate: Tue Jul 28 04:00:03 2026 +0100

    feat(dashboard): respect GranularExportControls for download permission 
(#41799)
---
 superset-frontend/src/dashboard/actions/hydrate.ts |  4 +-
 superset-frontend/src/utils/findPermission.test.ts | 52 +++++++++++++++++++++-
 superset-frontend/src/utils/findPermission.ts      | 13 ++++++
 3 files changed, 66 insertions(+), 3 deletions(-)

diff --git a/superset-frontend/src/dashboard/actions/hydrate.ts 
b/superset-frontend/src/dashboard/actions/hydrate.ts
index 95b04ae452a..527b754e83c 100644
--- a/superset-frontend/src/dashboard/actions/hydrate.ts
+++ b/superset-frontend/src/dashboard/actions/hydrate.ts
@@ -27,7 +27,7 @@ import { initSliceEntities } from 
'src/dashboard/reducers/sliceEntities';
 import { getInitialState as getInitialNativeFilterState } from 
'src/dashboard/reducers/nativeFilters';
 import { applyDefaultFormData } from 'src/explore/store';
 import { buildActiveFilters } from 'src/dashboard/util/activeDashboardFilters';
-import { findPermission } from 'src/utils/findPermission';
+import { canDownloadData, findPermission } from 'src/utils/findPermission';
 import {
   canUserEditDashboard,
   canUserSaveAsDashboard,
@@ -369,7 +369,7 @@ export const hydrateDashboard =
             'Superset',
             roles,
           ),
-          superset_can_download: findPermission('can_csv', 'Superset', roles),
+          superset_can_download: canDownloadData(roles),
           common: {
             // legacy, please use state.common instead
             conf: common?.conf,
diff --git a/superset-frontend/src/utils/findPermission.test.ts 
b/superset-frontend/src/utils/findPermission.test.ts
index 4dadb3a4ed2..4e9ec5527ce 100644
--- a/superset-frontend/src/utils/findPermission.test.ts
+++ b/superset-frontend/src/utils/findPermission.test.ts
@@ -16,7 +16,16 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { findPermission } from './findPermission';
+import { isFeatureEnabled } from '@superset-ui/core';
+import { UserRoles } from 'src/types/bootstrapTypes';
+import { canDownloadData, findPermission } from './findPermission';
+
+jest.mock('@superset-ui/core', () => ({
+  ...jest.requireActual('@superset-ui/core'),
+  isFeatureEnabled: jest.fn(),
+}));
+
+const mockIsFeatureEnabled = isFeatureEnabled as jest.Mock;
 
 test('findPermission for single role', () => {
   expect(findPermission('abc', 'def', { role: [['abc', 'def']] })).toEqual(
@@ -61,3 +70,44 @@ test('findPermission for multiple roles', () => {
 test('handles nonexistent roles', () => {
   expect(findPermission('abc', 'def', null)).toEqual(false);
 });
+
+describe('canDownloadData', () => {
+  const csvOnly: UserRoles = { role: [['can_csv', 'Superset']] };
+  const exportOnly: UserRoles = { role: [['can_export_data', 'Superset']] };
+  const both: UserRoles = {
+    role: [
+      ['can_csv', 'Superset'],
+      ['can_export_data', 'Superset'],
+    ],
+  };
+  const neither: UserRoles = { role: [['can_write', 'Chart']] };
+
+  afterEach(() => {
+    mockIsFeatureEnabled.mockReset();
+  });
+
+  test('checks can_csv when GranularExportControls is off', () => {
+    mockIsFeatureEnabled.mockReturnValue(false);
+    expect(canDownloadData(csvOnly)).toEqual(true);
+    expect(canDownloadData(exportOnly)).toEqual(false);
+    expect(canDownloadData(both)).toEqual(true);
+    expect(canDownloadData(neither)).toEqual(false);
+  });
+
+  test('checks can_export_data only when GranularExportControls is on, 
matching the backend', () => {
+    mockIsFeatureEnabled.mockReturnValue(true);
+    expect(canDownloadData(exportOnly)).toEqual(true);
+    // no can_csv fallback: a can_csv-only user would 403 at the backend, so
+    // the button must not show
+    expect(canDownloadData(csvOnly)).toEqual(false);
+    expect(canDownloadData(both)).toEqual(true);
+    expect(canDownloadData(neither)).toEqual(false);
+  });
+
+  test('handles nonexistent roles', () => {
+    mockIsFeatureEnabled.mockReturnValue(true);
+    expect(canDownloadData(null)).toEqual(false);
+    mockIsFeatureEnabled.mockReturnValue(false);
+    expect(canDownloadData(undefined)).toEqual(false);
+  });
+});
diff --git a/superset-frontend/src/utils/findPermission.ts 
b/superset-frontend/src/utils/findPermission.ts
index 69c45be5a07..4f4ef81f6d1 100644
--- a/superset-frontend/src/utils/findPermission.ts
+++ b/superset-frontend/src/utils/findPermission.ts
@@ -17,6 +17,7 @@
  * under the License.
  */
 import memoizeOne from 'memoize-one';
+import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core';
 import { UserRoles } from 'src/types/bootstrapTypes';
 
 export const findPermission = memoizeOne(
@@ -26,3 +27,15 @@ export const findPermission = memoizeOne(
       permissions.some(([perm_, view_]) => perm_ === perm && view_ === view),
     ),
 );
+
+/**
+ * Whether the user may download chart data (CSV, Excel). Mirrors what the
+ * backend enforces: with GranularExportControls enabled it checks the granular
+ * can_export_data permission, otherwise can_csv. The same shape as
+ * hydrateExplore and usePermissions, so the download button never shows for a
+ * user the backend would 403.
+ */
+export const canDownloadData = (roles?: UserRoles | null): boolean =>
+  isFeatureEnabled(FeatureFlag.GranularExportControls)
+    ? findPermission('can_export_data', 'Superset', roles)
+    : findPermission('can_csv', 'Superset', roles);

Reply via email to