You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2024/06/20 15:26:57 UTC

(superset) branch master updated: fix(ui): Disable ability to export data when user does not have the correct permission (#28429)

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 70f6f5f3ef fix(ui): Disable ability to export data when user does not have the correct permission (#28429)
70f6f5f3ef is described below

commit 70f6f5f3efc20d3ac7f573dad69eeacc2a125317
Author: Ed Jannoo <16...@users.noreply.github.com>
AuthorDate: Thu Jun 20 16:26:51 2024 +0100

    fix(ui): Disable ability to export data when user does not have the correct permission (#28429)
---
 .../SqlLab/components/ResultSet/ResultSet.test.tsx | 78 ++++++++++++++++++++++
 .../src/SqlLab/components/ResultSet/index.tsx      | 41 ++++++++----
 .../Chart/ChartContextMenu/ChartContextMenu.tsx    |  4 ++
 .../Chart/DrillBy/DrillByMenuItems.test.tsx        |  1 +
 .../components/Chart/DrillBy/DrillByMenuItems.tsx  |  3 +
 .../components/Chart/DrillBy/DrillByModal.test.tsx |  1 +
 .../src/components/Chart/DrillBy/DrillByModal.tsx  |  3 +
 .../Chart/DrillBy/useResultsTableView.test.ts      |  4 +-
 .../Chart/DrillBy/useResultsTableView.tsx          |  3 +
 .../components/SliceHeaderControls/index.tsx       |  1 +
 .../components/DataTablesPane/DataTablesPane.tsx   |  3 +
 .../components/DataTableControls.tsx               |  5 +-
 .../components/ResultsPaneOnDashboard.tsx          |  2 +
 .../DataTablesPane/components/SamplesPane.tsx      |  3 +
 .../components/SingleQueryResultPane.tsx           |  2 +
 .../DataTablesPane/components/useResultsPane.tsx   |  3 +
 .../DataTablesPane/test/DataTablesPane.test.tsx    | 28 ++++++++
 .../components/DataTablesPane/test/fixture.tsx     |  3 +
 .../src/explore/components/DataTablesPane/types.ts |  5 ++
 .../ExploreChartHeader/ExploreChartHeader.test.tsx | 58 +++++++++++++++-
 .../explore/components/ExploreChartPanel/index.jsx |  2 +
 .../useExploreAdditionalActionsMenu/index.jsx      | 30 +++++----
 22 files changed, 253 insertions(+), 30 deletions(-)

diff --git a/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx b/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx
index d5f30a575a..9c04fee8e7 100644
--- a/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx
+++ b/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx
@@ -446,4 +446,82 @@ describe('ResultSet', () => {
       }),
     ).toBe(null);
   });
+
+  test('should allow download as CSV when user has permission to export data', async () => {
+    const { queryByTestId } = setup(
+      mockedProps,
+      mockStore({
+        ...initialState,
+        user: {
+          ...user,
+          roles: {
+            sql_lab: [['can_export_csv', 'SQLLab']],
+          },
+        },
+        sqlLab: {
+          ...initialState.sqlLab,
+          queries: {
+            [queries[0].id]: queries[0],
+          },
+        },
+      }),
+    );
+    expect(queryByTestId('export-csv-button')).toBeInTheDocument();
+  });
+
+  test('should not allow download as CSV when user does not have permission to export data', async () => {
+    const { queryByTestId } = setup(
+      mockedProps,
+      mockStore({
+        ...initialState,
+        user,
+        sqlLab: {
+          ...initialState.sqlLab,
+          queries: {
+            [queries[0].id]: queries[0],
+          },
+        },
+      }),
+    );
+    expect(queryByTestId('export-csv-button')).not.toBeInTheDocument();
+  });
+
+  test('should allow copy to clipboard when user has permission to export data', async () => {
+    const { queryByTestId } = setup(
+      mockedProps,
+      mockStore({
+        ...initialState,
+        user: {
+          ...user,
+          roles: {
+            sql_lab: [['can_export_csv', 'SQLLab']],
+          },
+        },
+        sqlLab: {
+          ...initialState.sqlLab,
+          queries: {
+            [queries[0].id]: queries[0],
+          },
+        },
+      }),
+    );
+    expect(queryByTestId('copy-to-clipboard-button')).toBeInTheDocument();
+  });
+
+  test('should not allow copy to clipboard when user does not have permission to export data', async () => {
+    const { queryByTestId } = setup(
+      mockedProps,
+      mockStore({
+        ...initialState,
+        user,
+        sqlLab: {
+          ...initialState.sqlLab,
+          queries: {
+            [queries[0].id]: queries[0],
+          },
+        },
+      }),
+    );
+    expect(queryByTestId('copy-to-clipboard-button')).not.toBeInTheDocument();
+  });
 });
diff --git a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
index ac41d19b82..6e62c17da5 100644
--- a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
+++ b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
@@ -78,6 +78,7 @@ import {
   LOG_ACTIONS_SQLLAB_DOWNLOAD_CSV,
 } from 'src/logger/LogUtils';
 import Icons from 'src/components/Icons';
+import { findPermission } from 'src/utils/findPermission';
 import ExploreCtasResultsButton from '../ExploreCtasResultsButton';
 import ExploreResultsButton from '../ExploreResultsButton';
 import HighlightedSql from '../HighlightedSql';
@@ -309,6 +310,12 @@ const ResultSet = ({
         schema: query?.schema,
       };
 
+      const canExportData = findPermission(
+        'can_export_csv',
+        'SQLLab',
+        user?.roles,
+      );
+
       return (
         <ResultSetControls>
           <SaveDatasetModal
@@ -328,29 +335,35 @@ const ResultSet = ({
                 onClick={createExploreResultsOnClick}
               />
             )}
-            {csv && (
+            {csv && canExportData && (
               <Button
                 buttonSize="small"
                 href={getExportCsvUrl(query.id)}
+                data-test="export-csv-button"
                 onClick={() => logAction(LOG_ACTIONS_SQLLAB_DOWNLOAD_CSV, {})}
               >
                 <i className="fa fa-file-text-o" /> {t('Download to CSV')}
               </Button>
             )}
 
-            <CopyToClipboard
-              text={prepareCopyToClipboardTabularData(data, columns)}
-              wrapped={false}
-              copyNode={
-                <Button buttonSize="small">
-                  <i className="fa fa-clipboard" /> {t('Copy to Clipboard')}
-                </Button>
-              }
-              hideTooltip
-              onCopyEnd={() =>
-                logAction(LOG_ACTIONS_SQLLAB_COPY_RESULT_TO_CLIPBOARD, {})
-              }
-            />
+            {canExportData && (
+              <CopyToClipboard
+                text={prepareCopyToClipboardTabularData(data, columns)}
+                wrapped={false}
+                copyNode={
+                  <Button
+                    buttonSize="small"
+                    data-test="copy-to-clipboard-button"
+                  >
+                    <i className="fa fa-clipboard" /> {t('Copy to Clipboard')}
+                  </Button>
+                }
+                hideTooltip
+                onCopyEnd={() =>
+                  logAction(LOG_ACTIONS_SQLLAB_COPY_RESULT_TO_CLIPBOARD, {})
+                }
+              />
+            )}
           </ResultSetButtons>
           {search && (
             <input
diff --git a/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx b/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx
index bf1ea84ff1..9df0e73bc9 100644
--- a/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx
+++ b/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx
@@ -97,6 +97,9 @@ const ChartContextMenu = (
   const canDatasourceSamples = useSelector((state: RootState) =>
     findPermission('can_samples', 'Datasource', state.user?.roles),
   );
+  const canDownload = useSelector((state: RootState) =>
+    findPermission('can_csv', 'Superset', state.user?.roles),
+  );
   const canDrill = useSelector((state: RootState) =>
     findPermission('can_drill', 'Dashboard', state.user?.roles),
   );
@@ -256,6 +259,7 @@ const ChartContextMenu = (
         formData={formData}
         contextMenuY={clientY}
         submenuIndex={submenuIndex}
+        canDownload={canDownload}
         open={openKeys.includes('drill-by-submenu')}
         key="drill-by-submenu"
         {...(additionalConfig?.drillBy || {})}
diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.test.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.test.tsx
index 8b65d21409..2e60c08586 100644
--- a/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.test.tsx
+++ b/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.test.tsx
@@ -74,6 +74,7 @@ const renderMenu = ({
       <DrillByMenuItems
         formData={formData ?? defaultFormData}
         drillByConfig={drillByConfig}
+        canDownload
         open
         {...rest}
       />
diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx
index 6df180cb78..f694db3d27 100644
--- a/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx
+++ b/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx
@@ -74,6 +74,7 @@ export interface DrillByMenuItemsProps {
   onClick?: (event: MouseEvent) => void;
   openNewModal?: boolean;
   excludedColumns?: Column[];
+  canDownload: boolean;
   open: boolean;
 }
 
@@ -105,6 +106,7 @@ export const DrillByMenuItems = ({
   onClick = () => {},
   excludedColumns,
   openNewModal = true,
+  canDownload,
   open,
   ...rest
 }: DrillByMenuItemsProps) => {
@@ -344,6 +346,7 @@ export const DrillByMenuItems = ({
           formData={formData}
           onHideModal={closeModal}
           dataset={{ ...dataset!, verbose_map: verboseMap }}
+          canDownload={canDownload}
         />
       )}
     </>
diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx
index d546c071cb..2299f091b6 100644
--- a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx
+++ b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx
@@ -86,6 +86,7 @@ const renderModal = async (
             onHideModal={() => setShowModal(false)}
             dataset={dataset}
             drillByConfig={{ groupbyFieldName: 'groupby', filters: [] }}
+            canDownload
             {...modalProps}
           />
         )}
diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx
index 0cf6b1f016..2a9262e19e 100644
--- a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx
+++ b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx
@@ -151,6 +151,7 @@ export interface DrillByModalProps {
   drillByConfig: Required<ContextMenuFilters>['drillBy'];
   formData: BaseFormData & { [key: string]: any };
   onHideModal: () => void;
+  canDownload: boolean;
 }
 
 type DrillByConfigs = (ContextMenuFilters['drillBy'] & { column?: Column })[];
@@ -161,6 +162,7 @@ export default function DrillByModal({
   drillByConfig,
   formData,
   onHideModal,
+  canDownload,
 }: DrillByModalProps) {
   const dispatch = useDispatch();
   const theme = useTheme();
@@ -200,6 +202,7 @@ export default function DrillByModal({
   const resultsTable = useResultsTableView(
     chartDataResult,
     formData.datasource,
+    canDownload,
   );
 
   const [currentFormData, setCurrentFormData] = useState(formData);
diff --git a/superset-frontend/src/components/Chart/DrillBy/useResultsTableView.test.ts b/superset-frontend/src/components/Chart/DrillBy/useResultsTableView.test.ts
index 36cc51a684..7e64d3a42f 100644
--- a/superset-frontend/src/components/Chart/DrillBy/useResultsTableView.test.ts
+++ b/superset-frontend/src/components/Chart/DrillBy/useResultsTableView.test.ts
@@ -65,7 +65,7 @@ const MOCK_CHART_DATA_RESULT = [
 
 test('Displays results table for 1 query', () => {
   const { result } = renderHook(() =>
-    useResultsTableView(MOCK_CHART_DATA_RESULT.slice(0, 1), '1__table'),
+    useResultsTableView(MOCK_CHART_DATA_RESULT.slice(0, 1), '1__table', true),
   );
   render(result.current, { useRedux: true });
   expect(screen.queryByRole('tablist')).not.toBeInTheDocument();
@@ -76,7 +76,7 @@ test('Displays results table for 1 query', () => {
 
 test('Displays results for 2 queries', async () => {
   const { result } = renderHook(() =>
-    useResultsTableView(MOCK_CHART_DATA_RESULT, '1__table'),
+    useResultsTableView(MOCK_CHART_DATA_RESULT, '1__table', true),
   );
   render(result.current, { useRedux: true });
   const getActiveTabElement = () =>
diff --git a/superset-frontend/src/components/Chart/DrillBy/useResultsTableView.tsx b/superset-frontend/src/components/Chart/DrillBy/useResultsTableView.tsx
index 8b7fd684b0..4d85f949b2 100644
--- a/superset-frontend/src/components/Chart/DrillBy/useResultsTableView.tsx
+++ b/superset-frontend/src/components/Chart/DrillBy/useResultsTableView.tsx
@@ -33,6 +33,7 @@ const PaginationContainer = styled.div`
 export const useResultsTableView = (
   chartDataResult: QueryData[] | undefined,
   datasourceId: string,
+  canDownload: boolean,
 ) => {
   if (!isDefined(chartDataResult)) {
     return <div />;
@@ -48,6 +49,7 @@ export const useResultsTableView = (
           dataSize={DATA_SIZE}
           datasourceId={datasourceId}
           isVisible
+          canDownload={canDownload}
         />
       </PaginationContainer>
     );
@@ -65,6 +67,7 @@ export const useResultsTableView = (
               dataSize={DATA_SIZE}
               datasourceId={datasourceId}
               isVisible
+              canDownload={canDownload}
             />
           </PaginationContainer>
         </Tabs.TabPane>
diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx
index 756f2ed934..632428f32f 100644
--- a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx
+++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx
@@ -809,6 +809,7 @@ const SliceHeaderControls = (props: SliceHeaderControlsPropsWithRouter) => {
                 dataSize={20}
                 isRequest
                 isVisible
+                canDownload={!!props.supersetCanCSV}
               />
             }
           />
diff --git a/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx
index 8601c41a66..640834ba94 100644
--- a/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx
+++ b/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx
@@ -88,6 +88,7 @@ export const DataTablesPane = ({
   ownState,
   errorMessage,
   actions,
+  canDownload,
 }: DataTablesPaneProps) => {
   const theme = useTheme();
   const [activeTabKey, setActiveTabKey] = useState<string>(ResultTypes.Results);
@@ -198,6 +199,7 @@ export const DataTablesPane = ({
     isRequest: isRequest.results,
     actions,
     isVisible: ResultTypes.Results === activeTabKey,
+    canDownload,
   }).map((pane, idx) => {
     if (idx === 0) {
       return (
@@ -235,6 +237,7 @@ export const DataTablesPane = ({
             isRequest={isRequest.samples}
             actions={actions}
             isVisible={ResultTypes.Samples === activeTabKey}
+            canDownload={canDownload}
           />
         </Tabs.TabPane>
       </Tabs>
diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/DataTableControls.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/DataTableControls.tsx
index ff134da1f7..30dfb87c74 100644
--- a/superset-frontend/src/explore/components/DataTablesPane/components/DataTableControls.tsx
+++ b/superset-frontend/src/explore/components/DataTablesPane/components/DataTableControls.tsx
@@ -49,6 +49,7 @@ export const TableControls = ({
   columnTypes,
   rowcount,
   isLoading,
+  canDownload,
 }: TableControlsProps) => {
   const originalTimeColumns = getTimeColumns(datasourceId);
   const formattedTimeColumns = zip<string, GenericDataType>(
@@ -76,7 +77,9 @@ export const TableControls = ({
         `}
       >
         <RowCountLabel rowcount={rowcount} loading={isLoading} />
-        <CopyToClipboardButton data={formattedData} columns={columnNames} />
+        {canDownload && (
+          <CopyToClipboardButton data={formattedData} columns={columnNames} />
+        )}
       </div>
     </TableControlsWrapper>
   );
diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/ResultsPaneOnDashboard.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/ResultsPaneOnDashboard.tsx
index 410d55cbfd..b6691f87af 100644
--- a/superset-frontend/src/explore/components/DataTablesPane/components/ResultsPaneOnDashboard.tsx
+++ b/superset-frontend/src/explore/components/DataTablesPane/components/ResultsPaneOnDashboard.tsx
@@ -53,6 +53,7 @@ export const ResultsPaneOnDashboard = ({
   actions,
   isVisible,
   dataSize = 50,
+  canDownload,
 }: ResultsPaneProps) => {
   const resultsPanes = useResultsPane({
     errorMessage,
@@ -63,6 +64,7 @@ export const ResultsPaneOnDashboard = ({
     actions,
     dataSize,
     isVisible,
+    canDownload,
   });
 
   if (resultsPanes.length === 1) {
diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx
index dd9f430a33..ca0a1d6af4 100644
--- a/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx
+++ b/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx
@@ -42,6 +42,7 @@ export const SamplesPane = ({
   actions,
   dataSize = 50,
   isVisible,
+  canDownload,
 }: SamplesPaneProps) => {
   const [filterText, setFilterText] = useState('');
   const [data, setData] = useState<Record<string, any>[][]>([]);
@@ -114,6 +115,7 @@ export const SamplesPane = ({
           datasourceId={datasourceId}
           onInputChange={input => setFilterText(input)}
           isLoading={isLoading}
+          canDownload={canDownload}
         />
         <Error>{responseError}</Error>
       </>
@@ -135,6 +137,7 @@ export const SamplesPane = ({
         datasourceId={datasourceId}
         onInputChange={input => setFilterText(input)}
         isLoading={isLoading}
+        canDownload={canDownload}
       />
       <TableView
         columns={columns}
diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/SingleQueryResultPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/SingleQueryResultPane.tsx
index a990703586..9a3dd4eded 100644
--- a/superset-frontend/src/explore/components/DataTablesPane/components/SingleQueryResultPane.tsx
+++ b/superset-frontend/src/explore/components/DataTablesPane/components/SingleQueryResultPane.tsx
@@ -34,6 +34,7 @@ export const SingleQueryResultPane = ({
   datasourceId,
   dataSize = 50,
   isVisible,
+  canDownload,
 }: SingleQueryResultPaneProp) => {
   const [filterText, setFilterText] = useState('');
 
@@ -60,6 +61,7 @@ export const SingleQueryResultPane = ({
         datasourceId={datasourceId}
         onInputChange={input => setFilterText(input)}
         isLoading={false}
+        canDownload={canDownload}
       />
       <TableView
         columns={columns}
diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx
index 4b39062208..fe262fe720 100644
--- a/superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx
+++ b/superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx
@@ -47,6 +47,7 @@ export const useResultsPane = ({
   actions,
   isVisible,
   dataSize = 50,
+  canDownload,
 }: ResultsPaneProps): ReactElement[] => {
   const metadata = getChartMetadataRegistry().get(
     queryFormData?.viz_type || queryFormData?.vizType,
@@ -124,6 +125,7 @@ export const useResultsPane = ({
           datasourceId={queryFormData.datasource}
           onInputChange={() => {}}
           isLoading={false}
+          canDownload={canDownload}
         />
         <Error>{responseError}</Error>
       </>
@@ -149,6 +151,7 @@ export const useResultsPane = ({
         datasourceId={queryFormData.datasource}
         key={idx}
         isVisible={isVisible}
+        canDownload={canDownload}
       />
     ));
 };
diff --git a/superset-frontend/src/explore/components/DataTablesPane/test/DataTablesPane.test.tsx b/superset-frontend/src/explore/components/DataTablesPane/test/DataTablesPane.test.tsx
index fb5a20a0d6..4dcb5c7000 100644
--- a/superset-frontend/src/explore/components/DataTablesPane/test/DataTablesPane.test.tsx
+++ b/superset-frontend/src/explore/components/DataTablesPane/test/DataTablesPane.test.tsx
@@ -114,6 +114,34 @@ describe('DataTablesPane', () => {
     fetchMock.restore();
   });
 
+  test('Should not allow copy data table content when canDownload=false', async () => {
+    fetchMock.post(
+      'glob:*/api/v1/chart/data?form_data=%7B%22slice_id%22%3A456%7D',
+      {
+        result: [
+          {
+            data: [{ __timestamp: 1230768000000, genre: 'Action' }],
+            colnames: ['__timestamp', 'genre'],
+            coltypes: [2, 1],
+            rowcount: 1,
+            sql_rowcount: 1,
+          },
+        ],
+      },
+    );
+    const props = {
+      ...createDataTablesPaneProps(456),
+      canDownload: false,
+    };
+    render(<DataTablesPane {...props} />, {
+      useRedux: true,
+    });
+    userEvent.click(screen.getByText('Results'));
+    expect(await screen.findByText('1 row')).toBeVisible();
+    expect(screen.queryByLabelText('Copy')).not.toBeInTheDocument();
+    fetchMock.restore();
+  });
+
   test('Search table', async () => {
     fetchMock.post(
       'glob:*/api/v1/chart/data?form_data=%7B%22slice_id%22%3A789%7D',
diff --git a/superset-frontend/src/explore/components/DataTablesPane/test/fixture.tsx b/superset-frontend/src/explore/components/DataTablesPane/test/fixture.tsx
index ee766acd6a..c09419e7d0 100644
--- a/superset-frontend/src/explore/components/DataTablesPane/test/fixture.tsx
+++ b/superset-frontend/src/explore/components/DataTablesPane/test/fixture.tsx
@@ -73,6 +73,7 @@ export const createDataTablesPaneProps = (sliceId: number) =>
     chartStatus: 'rendered' as ChartStatus,
     onCollapseChange: jest.fn(),
     actions: exploreActions,
+    canDownload: true,
   }) as DataTablesPaneProps;
 
 export const createSamplesPaneProps = ({
@@ -90,6 +91,7 @@ export const createSamplesPaneProps = ({
     queryForce,
     isVisible: true,
     actions: exploreActions,
+    canDownload: true,
   }) as SamplesPaneProps;
 
 export const createResultsPaneOnDashboardProps = ({
@@ -116,4 +118,5 @@ export const createResultsPaneOnDashboardProps = ({
     isVisible: true,
     actions: exploreActions,
     errorMessage,
+    canDownload: true,
   }) as ResultsPaneProps;
diff --git a/superset-frontend/src/explore/components/DataTablesPane/types.ts b/superset-frontend/src/explore/components/DataTablesPane/types.ts
index 080388e256..5e62e21fa3 100644
--- a/superset-frontend/src/explore/components/DataTablesPane/types.ts
+++ b/superset-frontend/src/explore/components/DataTablesPane/types.ts
@@ -40,6 +40,7 @@ export interface DataTablesPaneProps {
   onCollapseChange: (isOpen: boolean) => void;
   errorMessage?: JSX.Element;
   actions: ExploreActions;
+  canDownload: boolean;
 }
 
 export interface ResultsPaneProps {
@@ -52,6 +53,7 @@ export interface ResultsPaneProps {
   dataSize?: number;
   // reload OriginalFormattedTimeColumns from localStorage when isVisible is true
   isVisible: boolean;
+  canDownload: boolean;
 }
 
 export interface SamplesPaneProps {
@@ -62,6 +64,7 @@ export interface SamplesPaneProps {
   dataSize?: number;
   // reload OriginalFormattedTimeColumns from localStorage when isVisible is true
   isVisible: boolean;
+  canDownload: boolean;
 }
 
 export interface TableControlsProps {
@@ -73,6 +76,7 @@ export interface TableControlsProps {
   columnTypes: GenericDataType[];
   isLoading: boolean;
   rowcount: number;
+  canDownload: boolean;
 }
 
 export interface QueryResultInterface {
@@ -88,4 +92,5 @@ export interface SingleQueryResultPaneProp extends QueryResultInterface {
   dataSize?: number;
   // reload OriginalFormattedTimeColumns from localStorage when isVisible is true
   isVisible: boolean;
+  canDownload: boolean;
 }
diff --git a/superset-frontend/src/explore/components/ExploreChartHeader/ExploreChartHeader.test.tsx b/superset-frontend/src/explore/components/ExploreChartHeader/ExploreChartHeader.test.tsx
index fc28b4e8f6..f064643a22 100644
--- a/superset-frontend/src/explore/components/ExploreChartHeader/ExploreChartHeader.test.tsx
+++ b/superset-frontend/src/explore/components/ExploreChartHeader/ExploreChartHeader.test.tsx
@@ -394,11 +394,25 @@ describe('Additional actions tests', () => {
       spyExportChart.restore();
     });
 
-    test('Should export to JSON', async () => {
+    test('Should not export to JSON if canDownload=false', async () => {
       const props = createProps();
       render(<ExploreHeader {...props} />, {
         useRedux: true,
       });
+      userEvent.click(screen.getByLabelText('Menu actions trigger'));
+      userEvent.hover(screen.getByText('Download'));
+      const exportJsonElement = await screen.findByText('Export to .JSON');
+      userEvent.click(exportJsonElement);
+      expect(spyExportChart.callCount).toBe(0);
+      spyExportChart.restore();
+    });
+
+    test('Should export to JSON if canDownload=true', async () => {
+      const props = createProps();
+      props.canDownload = true;
+      render(<ExploreHeader {...props} />, {
+        useRedux: true,
+      });
 
       userEvent.click(screen.getByLabelText('Menu actions trigger'));
       userEvent.hover(screen.getByText('Download'));
@@ -407,6 +421,22 @@ describe('Additional actions tests', () => {
       expect(spyExportChart.callCount).toBe(1);
     });
 
+    test('Should not export to pivoted CSV if canDownloadCSV=false and viz_type=pivot_table_v2', async () => {
+      const props = createProps();
+      props.chart.latestQueryFormData.viz_type = 'pivot_table_v2';
+      render(<ExploreHeader {...props} />, {
+        useRedux: true,
+      });
+
+      userEvent.click(screen.getByLabelText('Menu actions trigger'));
+      userEvent.hover(screen.getByText('Download'));
+      const exportCSVElement = await screen.findByText(
+        'Export to pivoted .CSV',
+      );
+      userEvent.click(exportCSVElement);
+      expect(spyExportChart.callCount).toBe(0);
+    });
+
     test('Should export to pivoted CSV if canDownloadCSV=true and viz_type=pivot_table_v2', async () => {
       const props = createProps();
       props.canDownload = true;
@@ -423,5 +453,31 @@ describe('Additional actions tests', () => {
       userEvent.click(exportCSVElement);
       expect(spyExportChart.callCount).toBe(1);
     });
+
+    test('Should not export to Excel if canDownload=false', async () => {
+      const props = createProps();
+      render(<ExploreHeader {...props} />, {
+        useRedux: true,
+      });
+      userEvent.click(screen.getByLabelText('Menu actions trigger'));
+      userEvent.hover(screen.getByText('Download'));
+      const exportExcelElement = await screen.findByText('Export to Excel');
+      userEvent.click(exportExcelElement);
+      expect(spyExportChart.callCount).toBe(0);
+      spyExportChart.restore();
+    });
+
+    test('Should export to Excel if canDownload=true', async () => {
+      const props = createProps();
+      props.canDownload = true;
+      render(<ExploreHeader {...props} />, {
+        useRedux: true,
+      });
+      userEvent.click(screen.getByLabelText('Menu actions trigger'));
+      userEvent.hover(screen.getByText('Download'));
+      const exportExcelElement = await screen.findByText('Export to Excel');
+      userEvent.click(exportExcelElement);
+      expect(spyExportChart.callCount).toBe(1);
+    });
   });
 });
diff --git a/superset-frontend/src/explore/components/ExploreChartPanel/index.jsx b/superset-frontend/src/explore/components/ExploreChartPanel/index.jsx
index bf95495c61..189177019c 100644
--- a/superset-frontend/src/explore/components/ExploreChartPanel/index.jsx
+++ b/superset-frontend/src/explore/components/ExploreChartPanel/index.jsx
@@ -137,6 +137,7 @@ const ExploreChartPanel = ({
   standalone,
   chartIsStale,
   chartAlert,
+  can_download: canDownload,
 }) => {
   const theme = useTheme();
   const gutterMargin = theme.gridUnit * GUTTER_SIZE_FACTOR;
@@ -449,6 +450,7 @@ const ExploreChartPanel = ({
           chartStatus={chart.chartStatus}
           errorMessage={errorMessage}
           actions={actions}
+          canDownload={canDownload}
         />
       </Split>
       {showDatasetModal && (
diff --git a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx
index a40d0f5e30..448836254c 100644
--- a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx
+++ b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx
@@ -173,22 +173,26 @@ export const useExploreAdditionalActionsMenu = (
 
   const exportJson = useCallback(
     () =>
-      exportChart({
-        formData: latestQueryFormData,
-        resultType: 'results',
-        resultFormat: 'json',
-      }),
-    [latestQueryFormData],
+      canDownloadCSV
+        ? exportChart({
+            formData: latestQueryFormData,
+            resultType: 'results',
+            resultFormat: 'json',
+          })
+        : null,
+    [canDownloadCSV, latestQueryFormData],
   );
 
   const exportExcel = useCallback(
     () =>
-      exportChart({
-        formData: latestQueryFormData,
-        resultType: 'results',
-        resultFormat: 'xlsx',
-      }),
-    [latestQueryFormData],
+      canDownloadCSV
+        ? exportChart({
+            formData: latestQueryFormData,
+            resultType: 'results',
+            resultFormat: 'xlsx',
+          })
+        : null,
+    [canDownloadCSV, latestQueryFormData],
   );
 
   const copyLink = useCallback(async () => {
@@ -350,6 +354,7 @@ export const useExploreAdditionalActionsMenu = (
           <Menu.Item
             key={MENU_KEYS.EXPORT_TO_JSON}
             icon={<Icons.FileOutlined css={iconReset} />}
+            disabled={!canDownloadCSV}
           >
             {t('Export to .JSON')}
           </Menu.Item>
@@ -362,6 +367,7 @@ export const useExploreAdditionalActionsMenu = (
           <Menu.Item
             key={MENU_KEYS.EXPORT_TO_XLSX}
             icon={<Icons.FileOutlined css={iconReset} />}
+            disabled={!canDownloadCSV}
           >
             {t('Export to Excel')}
           </Menu.Item>