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

diegopucci 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 6c029ce2e8 chore: Add permission to view and drill on Dashboard 
context (#26798)
6c029ce2e8 is described below

commit 6c029ce2e81bfd46adb36f15dc7ab039fe625ac7
Author: Geido <[email protected]>
AuthorDate: Mon Jan 29 18:28:10 2024 +0100

    chore: Add permission to view and drill on Dashboard context (#26798)
---
 .../Chart/ChartContextMenu/ChartContextMenu.tsx    | 12 ++++-
 .../Chart/ChartContextMenu/useContextMenu.test.tsx | 36 ++++++++++++-
 .../components/Chart/DrillBy/DrillByModal.test.tsx | 36 ++++++++++++-
 .../src/components/Chart/DrillBy/DrillByModal.tsx  | 17 +++++-
 .../Chart/DrillDetail/DrillDetailModal.test.tsx    | 26 +++++++--
 .../Chart/DrillDetail/DrillDetailModal.tsx         | 63 ++++++++++++++++------
 .../SliceHeaderControls.test.tsx                   | 53 ++++++++++++++++--
 .../components/SliceHeaderControls/index.tsx       | 32 +++++++++--
 superset/security/manager.py                       |  1 +
 tests/integration_tests/security_tests.py          |  2 +
 10 files changed, 242 insertions(+), 36 deletions(-)

diff --git 
a/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx 
b/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx
index 6ab80aad9e..ce8bb9d7d7 100644
--- 
a/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx
+++ 
b/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx
@@ -90,6 +90,12 @@ const ChartContextMenu = (
   const canExplore = useSelector((state: RootState) =>
     findPermission('can_explore', 'Superset', state.user?.roles),
   );
+  const canViewDrill = useSelector((state: RootState) =>
+    findPermission('can_view_and_drill', 'Dashboard', state.user?.roles),
+  );
+  const canDatasourceSamples = useSelector((state: RootState) =>
+    findPermission('can_samples', 'Datasource', state.user?.roles),
+  );
   const crossFiltersEnabled = useSelector<RootState, boolean>(
     ({ dashboardInfo }) => dashboardInfo.crossFiltersEnabled,
   );
@@ -105,15 +111,17 @@ const ChartContextMenu = (
   }>({ clientX: 0, clientY: 0 });
 
   const menuItems = [];
+  const canExploreOrView = canExplore || canViewDrill;
 
   const showDrillToDetail =
     isFeatureEnabled(FeatureFlag.DRILL_TO_DETAIL) &&
-    canExplore &&
+    canExploreOrView &&
+    canDatasourceSamples &&
     isDisplayed(ContextMenuItem.DrillToDetail);
 
   const showDrillBy =
     isFeatureEnabled(FeatureFlag.DRILL_BY) &&
-    canExplore &&
+    canExploreOrView &&
     isDisplayed(ContextMenuItem.DrillBy);
 
   const showCrossFilters =
diff --git 
a/superset-frontend/src/components/Chart/ChartContextMenu/useContextMenu.test.tsx
 
b/superset-frontend/src/components/Chart/ChartContextMenu/useContextMenu.test.tsx
index ebab36b14f..0d65bb6038 100644
--- 
a/superset-frontend/src/components/Chart/ChartContextMenu/useContextMenu.test.tsx
+++ 
b/superset-frontend/src/components/Chart/ChartContextMenu/useContextMenu.test.tsx
@@ -38,10 +38,12 @@ const setup = ({
   onSelection = noOp,
   displayedItems = ContextMenuItem.All,
   additionalConfig = {},
+  roles = undefined,
 }: {
   onSelection?: () => void;
   displayedItems?: ContextMenuItem | ContextMenuItem[];
   additionalConfig?: Record<string, any>;
+  roles?: Record<string, string[][]>;
 } = {}) => {
   const { result } = renderHook(() =>
     useContextMenu(
@@ -58,7 +60,12 @@ const setup = ({
       ...mockState,
       user: {
         ...mockState.user,
-        roles: { Admin: [['can_explore', 'Superset']] },
+        roles: roles ?? {
+          Admin: [
+            ['can_explore', 'Superset'],
+            ['can_samples', 'Datasource'],
+          ],
+        },
       },
     },
   });
@@ -75,7 +82,7 @@ test('Context menu renders', () => {
   expect(screen.getByText('Drill by')).toBeInTheDocument();
 });
 
-test('Context menu contains all items only', () => {
+test('Context menu contains all displayed items only', () => {
   const result = setup({
     displayedItems: [ContextMenuItem.DrillToDetail, ContextMenuItem.DrillBy],
   });
@@ -84,3 +91,28 @@ test('Context menu contains all items only', () => {
   expect(screen.getByText('Drill to detail')).toBeInTheDocument();
   expect(screen.getByText('Drill by')).toBeInTheDocument();
 });
+
+test('Context menu shows all items tied to can_view_and_drill permission', () 
=> {
+  const result = setup({
+    roles: {
+      Admin: [
+        ['can_view_and_drill', 'Dashboard'],
+        ['can_samples', 'Datasource'],
+      ],
+    },
+  });
+  result.current.onContextMenu(0, 0, {});
+  expect(screen.getByText('Drill to detail')).toBeInTheDocument();
+  expect(screen.getByText('Drill by')).toBeInTheDocument();
+  expect(screen.getByText('Add cross-filter')).toBeInTheDocument();
+});
+
+test('Context menu does not show "Drill to detail" without proper 
permissions', () => {
+  const result = setup({
+    roles: { Admin: [['can_view_and_drill', 'Dashboard']] },
+  });
+  result.current.onContextMenu(0, 0, {});
+  expect(screen.queryByText('Drill to detail')).not.toBeInTheDocument();
+  expect(screen.getByText('Drill by')).toBeInTheDocument();
+  expect(screen.getByText('Add cross-filter')).toBeInTheDocument();
+});
diff --git 
a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx 
b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx
index 95e2f6027f..fb43fbfcb3 100644
--- a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx
+++ b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx
@@ -68,7 +68,10 @@ const dataset = {
   ],
 };
 
-const renderModal = async (modalProps: Partial<DrillByModalProps> = {}) => {
+const renderModal = async (
+  modalProps: Partial<DrillByModalProps> = {},
+  overrideState: Record<string, any> = {},
+) => {
   const DrillByModalWrapper = () => {
     const [showModal, setShowModal] = useState(false);
 
@@ -93,7 +96,10 @@ const renderModal = async (modalProps: 
Partial<DrillByModalProps> = {}) => {
     useDnd: true,
     useRedux: true,
     useRouter: true,
-    initialState: drillByModalState,
+    initialState: {
+      ...drillByModalState,
+      ...overrideState,
+    },
   });
 
   userEvent.click(screen.getByRole('button', { name: 'Show modal' }));
@@ -233,3 +239,29 @@ test('render breadcrumbs', async () => {
   expect(newBreadcrumbItems).toHaveLength(1);
   expect(within(breadcrumbItems[0]).getByText('gender')).toBeInTheDocument();
 });
+
+test('should render "Edit chart" as disabled without can_explore permission', 
async () => {
+  await renderModal(
+    {},
+    {
+      user: {
+        ...drillByModalState.user,
+        roles: { Admin: [['test_invalid_role', 'Superset']] },
+      },
+    },
+  );
+  expect(screen.getByRole('button', { name: 'Edit chart' })).toBeDisabled();
+});
+
+test('should render "Edit chart" enabled with can_explore permission', async 
() => {
+  await renderModal(
+    {},
+    {
+      user: {
+        ...drillByModalState.user,
+        roles: { Admin: [['can_explore', 'Superset']] },
+      },
+    },
+  );
+  expect(screen.getByRole('button', { name: 'Edit chart' })).toBeEnabled();
+});
diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx 
b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx
index d1e8b39e0c..8662b7582f 100644
--- a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx
+++ b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx
@@ -54,6 +54,7 @@ import {
   LOG_ACTIONS_DRILL_BY_MODAL_OPENED,
   LOG_ACTIONS_FURTHER_DRILL_BY,
 } from 'src/logger/LogUtils';
+import { findPermission } from 'src/utils/findPermission';
 import { Dataset, DrillByType } from '../types';
 import DrillByChart from './DrillByChart';
 import { ContextMenuItem } from '../ChartContextMenu/ChartContextMenu';
@@ -75,6 +76,7 @@ interface ModalFooterProps {
 const ModalFooter = ({ formData, closeModal }: ModalFooterProps) => {
   const dispatch = useDispatch();
   const { addDangerToast } = useToasts();
+  const theme = useTheme();
   const [url, setUrl] = useState('');
   const dashboardPageId = useContext(DashboardPageIdContext);
   const onEditChartClick = useCallback(() => {
@@ -84,6 +86,9 @@ const ModalFooter = ({ formData, closeModal }: 
ModalFooterProps) => {
       }),
     );
   }, [dispatch, formData.slice_id]);
+  const canExplore = useSelector((state: RootState) =>
+    findPermission('can_explore', 'Superset', state.user?.roles),
+  );
 
   const [datasource_id, datasource_type] = formData.datasource.split('__');
   useEffect(() => {
@@ -103,13 +108,20 @@ const ModalFooter = ({ formData, closeModal }: 
ModalFooterProps) => {
     datasource_type,
     formData,
   ]);
+  const isEditDisabled = !url || !canExplore;
+
   return (
     <>
       <Button
         buttonStyle="secondary"
         buttonSize="small"
         onClick={onEditChartClick}
-        disabled={!url}
+        disabled={isEditDisabled}
+        tooltip={
+          isEditDisabled
+            ? t('You do not have sufficient permissions to edit the chart')
+            : undefined
+        }
       >
         <Link
           css={css`
@@ -128,6 +140,9 @@ const ModalFooter = ({ formData, closeModal }: 
ModalFooterProps) => {
         buttonSize="small"
         onClick={closeModal}
         data-test="close-drill-by-modal"
+        css={css`
+          margin-left: ${theme.gridUnit * 2}px;
+        `}
       >
         {t('Close')}
       </Button>
diff --git 
a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.test.tsx 
b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.test.tsx
index 038541d390..5b6cc71241 100644
--- 
a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.test.tsx
+++ 
b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.test.tsx
@@ -35,9 +35,16 @@ jest.mock('react-router-dom', () => ({
 
 const { id: chartId, form_data: formData } = chartQueries[sliceId];
 const { slice_name: chartName } = formData;
+const store = getMockStoreWithNativeFilters();
+const drillToDetailModalState = {
+  ...store.getState(),
+  user: {
+    ...store.getState().user,
+    roles: { Admin: [['can_explore', 'Superset']] },
+  },
+};
 
-const renderModal = async () => {
-  const store = getMockStoreWithNativeFilters();
+const renderModal = async (overrideState: Record<string, any> = {}) => {
   const DrillDetailModalWrapper = () => {
     const [showModal, setShowModal] = useState(false);
     return (
@@ -59,7 +66,10 @@ const renderModal = async () => {
   render(<DrillDetailModalWrapper />, {
     useRouter: true,
     useRedux: true,
-    store,
+    initialState: {
+      ...drillToDetailModalState,
+      ...overrideState,
+    },
   });
 
   userEvent.click(screen.getByRole('button', { name: 'Show modal' }));
@@ -93,3 +103,13 @@ test('should forward to Explore', async () => {
     `/explore/?dashboard_page_id=&slice_id=${sliceId}`,
   );
 });
+
+test('should render "Edit chart" as disabled without can_explore permission', 
async () => {
+  await renderModal({
+    user: {
+      ...drillToDetailModalState.user,
+      roles: { Admin: [['test_invalid_role', 'Superset']] },
+    },
+  });
+  expect(screen.getByRole('button', { name: 'Edit chart' })).toBeDisabled();
+});
diff --git 
a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.tsx 
b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.tsx
index e1207f8aac..26c31b5846 100644
--- a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.tsx
+++ b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.tsx
@@ -31,28 +31,52 @@ import Button from 'src/components/Button';
 import { useSelector } from 'react-redux';
 import { DashboardPageIdContext } from 
'src/dashboard/containers/DashboardPage';
 import { Slice } from 'src/types/Chart';
+import { RootState } from 'src/dashboard/types';
+import { findPermission } from 'src/utils/findPermission';
 import DrillDetailPane from './DrillDetailPane';
 
 interface ModalFooterProps {
-  exploreChart: () => void;
+  canExplore: boolean;
   closeModal?: () => void;
+  exploreChart: () => void;
 }
 
-const ModalFooter = ({ exploreChart, closeModal }: ModalFooterProps) => (
-  <>
-    <Button buttonStyle="secondary" buttonSize="small" onClick={exploreChart}>
-      {t('Edit chart')}
-    </Button>
-    <Button
-      buttonStyle="primary"
-      buttonSize="small"
-      onClick={closeModal}
-      data-test="close-drilltodetail-modal"
-    >
-      {t('Close')}
-    </Button>
-  </>
-);
+const ModalFooter = ({
+  canExplore,
+  closeModal,
+  exploreChart,
+}: ModalFooterProps) => {
+  const theme = useTheme();
+
+  return (
+    <>
+      <Button
+        buttonStyle="secondary"
+        buttonSize="small"
+        onClick={exploreChart}
+        disabled={!canExplore}
+        tooltip={
+          !canExplore
+            ? t('You do not have sufficient permissions to edit the chart')
+            : undefined
+        }
+      >
+        {t('Edit chart')}
+      </Button>
+      <Button
+        buttonStyle="primary"
+        buttonSize="small"
+        onClick={closeModal}
+        data-test="close-drilltodetail-modal"
+        css={css`
+          margin-left: ${theme.gridUnit * 2}px;
+        `}
+      >
+        {t('Close')}
+      </Button>
+    </>
+  );
+};
 
 interface DrillDetailModalProps {
   chartId: number;
@@ -76,6 +100,9 @@ export default function DrillDetailModal({
     (state: { sliceEntities: { slices: Record<number, Slice> } }) =>
       state.sliceEntities.slices[chartId],
   );
+  const canExplore = useSelector((state: RootState) =>
+    findPermission('can_explore', 'Superset', state.user?.roles),
+  );
 
   const exploreUrl = useMemo(
     () => `/explore/?dashboard_page_id=${dashboardPageId}&slice_id=${chartId}`,
@@ -97,7 +124,9 @@ export default function DrillDetailModal({
         }
       `}
       title={t('Drill to detail: %s', chartName)}
-      footer={<ModalFooter exploreChart={exploreChart} />}
+      footer={
+        <ModalFooter exploreChart={exploreChart} canExplore={canExplore} />
+      }
       responsive
       resizable
       resizableConfig={{
diff --git 
a/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx
 
b/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx
index 83099e5490..dc94e77cac 100644
--- 
a/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx
+++ 
b/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx
@@ -99,13 +99,23 @@ const createProps = (viz_type = 'sunburst_v2') =>
     exploreUrl: '/explore',
   } as SliceHeaderControlsProps);
 
-const renderWrapper = (overrideProps?: SliceHeaderControlsProps) => {
+const renderWrapper = (
+  overrideProps?: SliceHeaderControlsProps,
+  roles?: Record<string, string[][]>,
+) => {
   const props = overrideProps || createProps();
   const store = getMockStore();
+  const mockState = store.getState();
   return render(<SliceHeaderControls {...props} />, {
     useRedux: true,
     useRouter: true,
-    store,
+    initialState: {
+      ...mockState,
+      user: {
+        ...mockState.user,
+        roles: roles ?? mockState.user.roles,
+      },
+    },
   });
 };
 
@@ -295,13 +305,48 @@ test('Drill to detail modal is under featureflag', () => {
   expect(screen.queryByText('Drill to detail')).not.toBeInTheDocument();
 });
 
-test('Should show the "Drill to detail"', () => {
+test('Should show "Drill to detail"', () => {
   // @ts-ignore
   global.featureFlags = {
     [FeatureFlag.DRILL_TO_DETAIL]: true,
   };
   const props = createProps();
   props.slice.slice_id = 18;
-  renderWrapper(props);
+  renderWrapper(props, {
+    Admin: [
+      ['can_view_and_drill', 'Dashboard'],
+      ['can_samples', 'Datasource'],
+    ],
+  });
   expect(screen.getByText('Drill to detail')).toBeInTheDocument();
 });
+
+test('Should show menu items tied to can_view_and_drill permission', () => {
+  // @ts-ignore
+  global.featureFlags = {
+    [FeatureFlag.DRILL_TO_DETAIL]: true,
+  };
+  const props = {
+    ...createProps(),
+    supersetCanExplore: false,
+  };
+  props.slice.slice_id = 18;
+  renderWrapper(props, {
+    Admin: [['can_view_and_drill', 'Dashboard']],
+  });
+  expect(screen.getByText('View query')).toBeInTheDocument();
+  expect(screen.getByText('View as table')).toBeInTheDocument();
+  expect(screen.queryByText('Drill to detail')).not.toBeInTheDocument();
+});
+
+test('Should not show the "Edit chart" without proper permissions', () => {
+  const props = {
+    ...createProps(),
+    supersetCanExplore: false,
+  };
+  props.slice.slice_id = 18;
+  renderWrapper(props, {
+    Admin: [['can_view_and_drill', 'Dashboard']],
+  });
+  expect(screen.queryByText('Edit chart')).not.toBeInTheDocument();
+});
diff --git 
a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx 
b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx
index 305bc38434..b1196e7c11 100644
--- a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx
+++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx
@@ -57,6 +57,7 @@ import Modal from 'src/components/Modal';
 import { DrillDetailMenuItems } from 'src/components/Chart/DrillDetail';
 import { LOG_ACTIONS_CHART_DOWNLOAD_AS_IMAGE } from 'src/logger/LogUtils';
 import { RootState } from 'src/dashboard/types';
+import { findPermission } from 'src/utils/findPermission';
 import { useCrossFiltersScopingModal } from 
'../nativeFilters/FilterBar/CrossFilters/ScopingModal/useCrossFiltersScopingModal';
 
 const MENU_KEYS = {
@@ -170,11 +171,13 @@ const dropdownIconsStyles = css`
 `;
 
 const ViewResultsModalTrigger = ({
+  canExplore,
   exploreUrl,
   triggerNode,
   modalTitle,
   modalBody,
 }: {
+  canExplore?: boolean;
   exploreUrl: string;
   triggerNode: ReactChild;
   modalTitle: ReactChild;
@@ -214,6 +217,14 @@ const ViewResultsModalTrigger = ({
                 buttonStyle="secondary"
                 buttonSize="small"
                 onClick={exploreChart}
+                disabled={!canExplore}
+                tooltip={
+                  !canExplore
+                    ? t(
+                        'You do not have sufficient permissions to edit the 
chart',
+                      )
+                    : undefined
+                }
               >
                 {t('Edit chart')}
               </Button>
@@ -221,6 +232,9 @@ const ViewResultsModalTrigger = ({
                 buttonStyle="primary"
                 buttonSize="small"
                 onClick={closeModal}
+                css={css`
+                  margin-left: ${theme.gridUnit * 2}px;
+                `}
               >
                 {t('Close')}
               </Button>
@@ -259,7 +273,13 @@ const SliceHeaderControls = (props: 
SliceHeaderControlsPropsWithRouter) => {
     getChartMetadataRegistry()
       .get(props.slice.viz_type)
       ?.behaviors?.includes(Behavior.INTERACTIVE_CHART);
-
+  const canViewDrill = useSelector((state: RootState) =>
+    findPermission('can_view_and_drill', 'Dashboard', state.user?.roles),
+  );
+  const canExploreOrView = props.supersetCanExplore || canViewDrill;
+  const canDatasourceSamples = useSelector((state: RootState) =>
+    findPermission('can_samples', 'Datasource', state.user?.roles),
+  );
   const refreshChart = () => {
     if (props.updatedDttm) {
       props.forceRefresh(props.slice.slice_id, props.dashboardId);
@@ -428,7 +448,7 @@ const SliceHeaderControls = (props: 
SliceHeaderControlsPropsWithRouter) => {
         </>
       )}
 
-      {props.supersetCanExplore && (
+      {canExploreOrView && (
         <Menu.Item key={MENU_KEYS.VIEW_QUERY}>
           <ModalTrigger
             triggerNode={
@@ -443,9 +463,10 @@ const SliceHeaderControls = (props: 
SliceHeaderControlsPropsWithRouter) => {
         </Menu.Item>
       )}
 
-      {props.supersetCanExplore && (
+      {canExploreOrView && (
         <Menu.Item key={MENU_KEYS.VIEW_RESULTS}>
           <ViewResultsModalTrigger
+            canExplore={props.supersetCanExplore}
             exploreUrl={props.exploreUrl}
             triggerNode={
               <span data-test="view-query-menu-item">{t('View as 
table')}</span>
@@ -465,14 +486,15 @@ const SliceHeaderControls = (props: 
SliceHeaderControlsPropsWithRouter) => {
       )}
 
       {isFeatureEnabled(FeatureFlag.DRILL_TO_DETAIL) &&
-        props.supersetCanExplore && (
+        canExploreOrView &&
+        canDatasourceSamples && (
           <DrillDetailMenuItems
             chartId={slice.slice_id}
             formData={props.formData}
           />
         )}
 
-      {(slice.description || props.supersetCanExplore) && <Menu.Divider />}
+      {(slice.description || canExploreOrView) && <Menu.Divider />}
 
       {supersetCanShare && (
         <Menu.SubMenu title={t('Share')}>
diff --git a/superset/security/manager.py b/superset/security/manager.py
index 7e1b697840..b29529edb2 100644
--- a/superset/security/manager.py
+++ b/superset/security/manager.py
@@ -726,6 +726,7 @@ class SupersetSecurityManager(  # pylint: 
disable=too-many-public-methods
         self.add_permission_view_menu("can_csv", "Superset")
         self.add_permission_view_menu("can_share_dashboard", "Superset")
         self.add_permission_view_menu("can_share_chart", "Superset")
+        self.add_permission_view_menu("can_view_and_drill", "Dashboard")
 
     def create_missing_perms(self) -> None:
         """
diff --git a/tests/integration_tests/security_tests.py 
b/tests/integration_tests/security_tests.py
index 395aebf29c..d9c98aa253 100644
--- a/tests/integration_tests/security_tests.py
+++ b/tests/integration_tests/security_tests.py
@@ -1337,6 +1337,7 @@ class TestRolePermission(SupersetTestCase):
         self.assertIn(("can_explore_json", "Superset"), perm_set)
         self.assertIn(("can_explore_json", "Superset"), perm_set)
         self.assertIn(("can_userinfo", "UserDBModelView"), perm_set)
+        self.assertIn(("can_view_and_drill", "Dashboard"), perm_set)
         self.assert_can_menu("Databases", perm_set)
         self.assert_can_menu("Datasets", perm_set)
         self.assert_can_menu("Data", perm_set)
@@ -1504,6 +1505,7 @@ class TestRolePermission(SupersetTestCase):
         self.assertIn(("can_share_dashboard", "Superset"), gamma_perm_set)
         self.assertIn(("can_explore_json", "Superset"), gamma_perm_set)
         self.assertIn(("can_userinfo", "UserDBModelView"), gamma_perm_set)
+        self.assertIn(("can_view_and_drill", "Dashboard"), gamma_perm_set)
 
     def test_views_are_secured(self):
         """Preventing the addition of unsecured views without has_access 
decorator"""

Reply via email to