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);