Skip to content

Commit 595c6ce

Browse files
authored
chore: Add granular permissions for actions in Dashboard (#27029)
1 parent 13915bb commit 595c6ce

File tree

10 files changed

+272
-49
lines changed

10 files changed

+272
-49
lines changed

superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,14 @@ const ChartContextMenu = (
9090
const canExplore = useSelector((state: RootState) =>
9191
findPermission('can_explore', 'Superset', state.user?.roles),
9292
);
93-
const canViewDrill = useSelector((state: RootState) =>
94-
findPermission('can_view_and_drill', 'Dashboard', state.user?.roles),
93+
const canWriteExploreFormData = useSelector((state: RootState) =>
94+
findPermission('can_write', 'ExploreFormDataRestApi', state.user?.roles),
9595
);
9696
const canDatasourceSamples = useSelector((state: RootState) =>
9797
findPermission('can_samples', 'Datasource', state.user?.roles),
9898
);
99+
const canDrillBy = canExplore && canWriteExploreFormData;
100+
const canDrillToDetail = canExplore && canDatasourceSamples;
99101
const crossFiltersEnabled = useSelector<RootState, boolean>(
100102
({ dashboardInfo }) => dashboardInfo.crossFiltersEnabled,
101103
);
@@ -111,17 +113,15 @@ const ChartContextMenu = (
111113
}>({ clientX: 0, clientY: 0 });
112114

113115
const menuItems = [];
114-
const canExploreOrView = canExplore || canViewDrill;
115116

116117
const showDrillToDetail =
117118
isFeatureEnabled(FeatureFlag.DrillToDetail) &&
118-
canExploreOrView &&
119-
canDatasourceSamples &&
119+
canDrillToDetail &&
120120
isDisplayed(ContextMenuItem.DrillToDetail);
121121

122122
const showDrillBy =
123123
isFeatureEnabled(FeatureFlag.DrillBy) &&
124-
canExploreOrView &&
124+
canDrillBy &&
125125
isDisplayed(ContextMenuItem.DrillBy);
126126

127127
const showCrossFilters =

superset-frontend/src/components/Chart/ChartContextMenu/useContextMenu.test.tsx

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ const setup = ({
6464
Admin: [
6565
['can_explore', 'Superset'],
6666
['can_samples', 'Datasource'],
67+
['can_write', 'ExploreFormDataRestApi'],
6768
],
6869
},
6970
},
@@ -92,27 +93,48 @@ test('Context menu contains all displayed items only', () => {
9293
expect(screen.getByText('Drill by')).toBeInTheDocument();
9394
});
9495

95-
test('Context menu shows all items tied to can_view_and_drill permission', () => {
96+
test('Context menu shows "Drill by"', () => {
97+
const result = setup({
98+
roles: {
99+
Admin: [
100+
['can_write', 'ExploreFormDataRestApi'],
101+
['can_explore', 'Superset'],
102+
],
103+
},
104+
});
105+
result.current.onContextMenu(0, 0, {});
106+
expect(screen.getByText('Drill by')).toBeInTheDocument();
107+
});
108+
109+
test('Context menu does not show "Drill by"', () => {
110+
const result = setup({
111+
roles: {
112+
Admin: [['invalid_permission', 'Dashboard']],
113+
},
114+
});
115+
result.current.onContextMenu(0, 0, {});
116+
expect(screen.queryByText('Drill by')).not.toBeInTheDocument();
117+
});
118+
119+
test('Context menu shows "Drill to detail"', () => {
96120
const result = setup({
97121
roles: {
98122
Admin: [
99-
['can_view_and_drill', 'Dashboard'],
100123
['can_samples', 'Datasource'],
124+
['can_explore', 'Superset'],
101125
],
102126
},
103127
});
104128
result.current.onContextMenu(0, 0, {});
105129
expect(screen.getByText('Drill to detail')).toBeInTheDocument();
106-
expect(screen.getByText('Drill by')).toBeInTheDocument();
107-
expect(screen.getByText('Add cross-filter')).toBeInTheDocument();
108130
});
109131

110-
test('Context menu does not show "Drill to detail" without proper permissions', () => {
132+
test('Context menu does not show "Drill to detail"', () => {
111133
const result = setup({
112-
roles: { Admin: [['can_view_and_drill', 'Dashboard']] },
134+
roles: {
135+
Admin: [['can_explore', 'Superset']],
136+
},
113137
});
114138
result.current.onContextMenu(0, 0, {});
115139
expect(screen.queryByText('Drill to detail')).not.toBeInTheDocument();
116-
expect(screen.getByText('Drill by')).toBeInTheDocument();
117-
expect(screen.getByText('Add cross-filter')).toBeInTheDocument();
118140
});

superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ test('should render "Edit chart" as disabled without can_explore permission', as
246246
{
247247
user: {
248248
...drillByModalState.user,
249-
roles: { Admin: [['test_invalid_role', 'Superset']] },
249+
roles: { Admin: [['invalid_permission', 'Superset']] },
250250
},
251251
},
252252
);

superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ test('should render "Edit chart" as disabled without can_explore permission', as
108108
await renderModal({
109109
user: {
110110
...drillToDetailModalState.user,
111-
roles: { Admin: [['test_invalid_role', 'Superset']] },
111+
roles: { Admin: [['invalid_permission', 'Superset']] },
112112
},
113113
});
114114
expect(screen.getByRole('button', { name: 'Edit chart' })).toBeDisabled();

superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@
1919

2020
import userEvent from '@testing-library/user-event';
2121
import React from 'react';
22-
import { getMockStore } from 'spec/fixtures/mockStore';
2322
import { render, screen } from 'spec/helpers/testing-library';
2423
import { FeatureFlag } from '@superset-ui/core';
24+
import mockState from 'spec/fixtures/mockState';
2525
import SliceHeaderControls, { SliceHeaderControlsProps } from '.';
2626

2727
jest.mock('src/components/Dropdown', () => {
@@ -104,16 +104,16 @@ const renderWrapper = (
104104
roles?: Record<string, string[][]>,
105105
) => {
106106
const props = overrideProps || createProps();
107-
const store = getMockStore();
108-
const mockState = store.getState();
109107
return render(<SliceHeaderControls {...props} />, {
110108
useRedux: true,
111109
useRouter: true,
112110
initialState: {
113111
...mockState,
114112
user: {
115113
...mockState.user,
116-
roles: roles ?? mockState.user.roles,
114+
roles: roles ?? {
115+
Admin: [['can_samples', 'Datasource']],
116+
},
117117
},
118118
},
119119
});
@@ -310,18 +310,18 @@ test('Should show "Drill to detail"', () => {
310310
global.featureFlags = {
311311
[FeatureFlag.DrillToDetail]: true,
312312
};
313-
const props = createProps();
313+
const props = {
314+
...createProps(),
315+
supersetCanExplore: true,
316+
};
314317
props.slice.slice_id = 18;
315318
renderWrapper(props, {
316-
Admin: [
317-
['can_view_and_drill', 'Dashboard'],
318-
['can_samples', 'Datasource'],
319-
],
319+
Admin: [['can_samples', 'Datasource']],
320320
});
321321
expect(screen.getByText('Drill to detail')).toBeInTheDocument();
322322
});
323323

324-
test('Should show menu items tied to can_view_and_drill permission', () => {
324+
test('Should not show "Drill to detail"', () => {
325325
// @ts-ignore
326326
global.featureFlags = {
327327
[FeatureFlag.DrillToDetail]: true,
@@ -332,21 +332,71 @@ test('Should show menu items tied to can_view_and_drill permission', () => {
332332
};
333333
props.slice.slice_id = 18;
334334
renderWrapper(props, {
335-
Admin: [['can_view_and_drill', 'Dashboard']],
335+
Admin: [['invalid_permission', 'Dashboard']],
336+
});
337+
expect(screen.queryByText('Drill to detail')).not.toBeInTheDocument();
338+
});
339+
340+
test('Should show "View query"', () => {
341+
const props = {
342+
...createProps(),
343+
supersetCanExplore: false,
344+
};
345+
props.slice.slice_id = 18;
346+
renderWrapper(props, {
347+
Admin: [['can_view_query', 'Dashboard']],
336348
});
337349
expect(screen.getByText('View query')).toBeInTheDocument();
350+
});
351+
352+
test('Should not show "View query"', () => {
353+
const props = {
354+
...createProps(),
355+
supersetCanExplore: false,
356+
};
357+
props.slice.slice_id = 18;
358+
renderWrapper(props, {
359+
Admin: [['invalid_permission', 'Dashboard']],
360+
});
361+
expect(screen.queryByText('View query')).not.toBeInTheDocument();
362+
});
363+
364+
test('Should show "View as table"', () => {
365+
const props = {
366+
...createProps(),
367+
supersetCanExplore: false,
368+
};
369+
props.slice.slice_id = 18;
370+
renderWrapper(props, {
371+
Admin: [['can_view_chart_as_table', 'Dashboard']],
372+
});
338373
expect(screen.getByText('View as table')).toBeInTheDocument();
339-
expect(screen.queryByText('Drill to detail')).not.toBeInTheDocument();
340374
});
341375

342-
test('Should not show the "Edit chart" without proper permissions', () => {
376+
test('Should not show "View as table"', () => {
343377
const props = {
344378
...createProps(),
345379
supersetCanExplore: false,
346380
};
347381
props.slice.slice_id = 18;
348382
renderWrapper(props, {
349-
Admin: [['can_view_and_drill', 'Dashboard']],
383+
Admin: [['invalid_permission', 'Dashboard']],
384+
});
385+
expect(screen.queryByText('View as table')).not.toBeInTheDocument();
386+
});
387+
388+
test('Should not show the "Edit chart" button', () => {
389+
const props = {
390+
...createProps(),
391+
supersetCanExplore: false,
392+
};
393+
props.slice.slice_id = 18;
394+
renderWrapper(props, {
395+
Admin: [
396+
['can_samples', 'Datasource'],
397+
['can_view_query', 'Dashboard'],
398+
['can_view_chart_as_table', 'Dashboard'],
399+
],
350400
});
351401
expect(screen.queryByText('Edit chart')).not.toBeInTheDocument();
352402
});

superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -273,13 +273,17 @@ const SliceHeaderControls = (props: SliceHeaderControlsPropsWithRouter) => {
273273
getChartMetadataRegistry()
274274
.get(props.slice.viz_type)
275275
?.behaviors?.includes(Behavior.InteractiveChart);
276-
const canViewDrill = useSelector((state: RootState) =>
277-
findPermission('can_view_and_drill', 'Dashboard', state.user?.roles),
278-
);
279-
const canExploreOrView = props.supersetCanExplore || canViewDrill;
276+
const canExplore = props.supersetCanExplore;
280277
const canDatasourceSamples = useSelector((state: RootState) =>
281278
findPermission('can_samples', 'Datasource', state.user?.roles),
282279
);
280+
const canDrillToDetail = canExplore && canDatasourceSamples;
281+
const canViewQuery = useSelector((state: RootState) =>
282+
findPermission('can_view_query', 'Dashboard', state.user?.roles),
283+
);
284+
const canViewTable = useSelector((state: RootState) =>
285+
findPermission('can_view_chart_as_table', 'Dashboard', state.user?.roles),
286+
);
283287
const refreshChart = () => {
284288
if (props.updatedDttm) {
285289
props.forceRefresh(props.slice.slice_id, props.dashboardId);
@@ -429,7 +433,7 @@ const SliceHeaderControls = (props: SliceHeaderControlsPropsWithRouter) => {
429433
</Menu.Item>
430434
)}
431435

432-
{props.supersetCanExplore && (
436+
{canExplore && (
433437
<Menu.Item key={MENU_KEYS.EXPLORE_CHART}>
434438
<Link to={props.exploreUrl}>
435439
<Tooltip title={getSliceHeaderTooltip(props.slice.slice_name)}>
@@ -448,7 +452,7 @@ const SliceHeaderControls = (props: SliceHeaderControlsPropsWithRouter) => {
448452
</>
449453
)}
450454

451-
{canExploreOrView && (
455+
{(canExplore || canViewQuery) && (
452456
<Menu.Item key={MENU_KEYS.VIEW_QUERY}>
453457
<ModalTrigger
454458
triggerNode={
@@ -463,7 +467,7 @@ const SliceHeaderControls = (props: SliceHeaderControlsPropsWithRouter) => {
463467
</Menu.Item>
464468
)}
465469

466-
{canExploreOrView && (
470+
{(canExplore || canViewTable) && (
467471
<Menu.Item key={MENU_KEYS.VIEW_RESULTS}>
468472
<ViewResultsModalTrigger
469473
canExplore={props.supersetCanExplore}
@@ -485,16 +489,14 @@ const SliceHeaderControls = (props: SliceHeaderControlsPropsWithRouter) => {
485489
</Menu.Item>
486490
)}
487491

488-
{isFeatureEnabled(FeatureFlag.DrillToDetail) &&
489-
canExploreOrView &&
490-
canDatasourceSamples && (
491-
<DrillDetailMenuItems
492-
chartId={slice.slice_id}
493-
formData={props.formData}
494-
/>
495-
)}
492+
{isFeatureEnabled(FeatureFlag.DrillToDetail) && canDrillToDetail && (
493+
<DrillDetailMenuItems
494+
chartId={slice.slice_id}
495+
formData={props.formData}
496+
/>
497+
)}
496498

497-
{(slice.description || canExploreOrView) && <Menu.Divider />}
499+
{(slice.description || canExplore) && <Menu.Divider />}
498500

499501
{supersetCanShare && (
500502
<Menu.SubMenu title={t('Share')}>

0 commit comments

Comments
 (0)