DashboardSettings: Prevent Dashboard permissions from linking to folder permissions when user does not have sufficient permissions (#44212)

* user essentials mob! 🔱

* user essentials mob! 🔱

* user essentials mob! 🔱

* user essentials mob! 🔱

* user essentials mob! 🔱

* user essentials mob! 🔱

* user essentials mob! 🔱

* add tests

* fix up

Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
Co-authored-by: Alexandra Vargas <alexa1866@gmail.com>
Co-authored-by: Hugo Häggmark <hugo.haggmark@gmail.com>
This commit is contained in:
Josh Hunt 2022-01-19 15:27:33 +00:00 committed by GitHub
parent dc913f2311
commit 9f97f05fcc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 136 additions and 8 deletions

View File

@ -65,9 +65,13 @@ export default class PermissionsListItem extends PureComponent<Props> {
{item.inherited && folderInfo && ( {item.inherited && folderInfo && (
<em className="muted no-wrap"> <em className="muted no-wrap">
Inherited from folder{' '} Inherited from folder{' '}
{folderInfo.canViewFolderPermissions ? (
<a className="text-link" href={`${folderInfo.url}/permissions`}> <a className="text-link" href={`${folderInfo.url}/permissions`}>
{folderInfo.title} {folderInfo.title}
</a>{' '} </a>
) : (
folderInfo.title
)}
</em> </em>
)} )}
{inheritedFromRoot && <em className="muted no-wrap">Default Permission</em>} {inheritedFromRoot && <em className="muted no-wrap">Default Permission</em>}

View File

@ -10,6 +10,7 @@ import {
removeDashboardPermission, removeDashboardPermission,
updateDashboardPermission, updateDashboardPermission,
} from '../../state/actions'; } from '../../state/actions';
import { checkFolderPermissions } from '../../../folders/state/actions';
import { DashboardModel } from '../../state/DashboardModel'; import { DashboardModel } from '../../state/DashboardModel';
import PermissionList from 'app/core/components/PermissionList/PermissionList'; import PermissionList from 'app/core/components/PermissionList/PermissionList';
import AddPermission from 'app/core/components/PermissionList/AddPermission'; import AddPermission from 'app/core/components/PermissionList/AddPermission';
@ -17,6 +18,7 @@ import PermissionsInfo from 'app/core/components/PermissionList/PermissionsInfo'
const mapStateToProps = (state: StoreState) => ({ const mapStateToProps = (state: StoreState) => ({
permissions: state.dashboard.permissions, permissions: state.dashboard.permissions,
canViewFolderPermissions: state.folder.canViewFolderPermissions,
}); });
const mapDispatchToProps = { const mapDispatchToProps = {
@ -24,6 +26,7 @@ const mapDispatchToProps = {
addDashboardPermission, addDashboardPermission,
removeDashboardPermission, removeDashboardPermission,
updateDashboardPermission, updateDashboardPermission,
checkFolderPermissions,
}; };
const connector = connect(mapStateToProps, mapDispatchToProps); const connector = connect(mapStateToProps, mapDispatchToProps);
@ -49,6 +52,9 @@ export class DashboardPermissionsUnconnected extends PureComponent<Props, State>
componentDidMount() { componentDidMount() {
this.props.getDashboardPermissions(this.props.dashboard.id); this.props.getDashboardPermissions(this.props.dashboard.id);
if (this.props.dashboard.meta.folderUid) {
this.props.checkFolderPermissions(this.props.dashboard.meta.folderUid);
}
} }
onOpenAddPermissions = () => { onOpenAddPermissions = () => {
@ -72,12 +78,13 @@ export class DashboardPermissionsUnconnected extends PureComponent<Props, State>
}; };
getFolder() { getFolder() {
const { dashboard } = this.props; const { dashboard, canViewFolderPermissions } = this.props;
return { return {
id: dashboard.meta.folderId, id: dashboard.meta.folderId,
title: dashboard.meta.folderTitle, title: dashboard.meta.folderTitle,
url: dashboard.meta.folderUrl, url: dashboard.meta.folderUrl,
canViewFolderPermissions,
}; };
} }

View File

@ -20,6 +20,7 @@ const setup = (propOverrides?: object) => {
hasChanged: false, hasChanged: false,
version: 1, version: 1,
permissions: [], permissions: [],
canViewFolderPermissions: true,
}, },
getFolderByUid: jest.fn(), getFolderByUid: jest.fn(),
setFolderTitle: mockToolkitActionCreator(setFolderTitle), setFolderTitle: mockToolkitActionCreator(setFolderTitle),

View File

@ -0,0 +1,65 @@
import { Observable, of, throwError } from 'rxjs';
import { thunkTester } from 'test/core/thunk/thunkTester';
import { checkFolderPermissions } from './actions';
import { setCanViewFolderPermissions } from './reducers';
import { backendSrv } from 'app/core/services/backend_srv';
import { notifyApp } from 'app/core/actions';
import { createWarningNotification } from 'app/core/copy/appNotification';
import { FetchResponse } from '@grafana/runtime';
describe('folder actions', () => {
let fetchSpy: jest.SpyInstance<Observable<FetchResponse<unknown>>>;
beforeAll(() => {
fetchSpy = jest.spyOn(backendSrv, 'fetch');
});
afterAll(() => {
fetchSpy.mockRestore();
});
function mockFetch(resp: Observable<any>) {
fetchSpy.mockReturnValueOnce(resp);
}
const folderUid = 'abc123';
describe('checkFolderPermissions', () => {
it('should dispatch true when the api call is successful', async () => {
mockFetch(of({}));
const dispatchedActions = await thunkTester({})
.givenThunk(checkFolderPermissions)
.whenThunkIsDispatched(folderUid);
expect(dispatchedActions).toEqual([setCanViewFolderPermissions(true)]);
});
it('should dispatch just "false" when the api call fails with 403', async () => {
mockFetch(throwError(() => ({ status: 403, data: { message: 'Access denied' } })));
const dispatchedActions = await thunkTester({})
.givenThunk(checkFolderPermissions)
.whenThunkIsDispatched(folderUid);
expect(dispatchedActions).toEqual([setCanViewFolderPermissions(false)]);
});
it('should also dispatch a notification when the api call fails with an error other than 403', async () => {
mockFetch(throwError(() => ({ status: 500, data: { message: 'Server error' } })));
const dispatchedActions = await thunkTester({})
.givenThunk(checkFolderPermissions)
.whenThunkIsDispatched(folderUid);
const notificationAction = notifyApp(
createWarningNotification('Error checking folder permissions', 'Server error')
);
notificationAction.payload.id = expect.any(String);
expect(dispatchedActions).toEqual([
expect.objectContaining(notificationAction),
setCanViewFolderPermissions(false),
]);
});
});
});

View File

@ -3,10 +3,12 @@ import { getBackendSrv, locationService } from '@grafana/runtime';
import { backendSrv } from 'app/core/services/backend_srv'; import { backendSrv } from 'app/core/services/backend_srv';
import { FolderState, ThunkResult } from 'app/types'; import { FolderState, ThunkResult } from 'app/types';
import { DashboardAcl, DashboardAclUpdateDTO, NewDashboardAclItem, PermissionLevel } from 'app/types/acl'; import { DashboardAcl, DashboardAclUpdateDTO, NewDashboardAclItem, PermissionLevel } from 'app/types/acl';
import { updateNavIndex } from 'app/core/actions'; import { notifyApp, updateNavIndex } from 'app/core/actions';
import { buildNavModel } from './navModel'; import { buildNavModel } from './navModel';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import { loadFolder, loadFolderPermissions } from './reducers'; import { loadFolder, loadFolderPermissions, setCanViewFolderPermissions } from './reducers';
import { lastValueFrom } from 'rxjs';
import { createWarningNotification } from 'app/core/copy/appNotification';
export function getFolderByUid(uid: string): ThunkResult<void> { export function getFolderByUid(uid: string): ThunkResult<void> {
return async (dispatch) => { return async (dispatch) => {
@ -43,6 +45,28 @@ export function getFolderPermissions(uid: string): ThunkResult<void> {
}; };
} }
export function checkFolderPermissions(uid: string): ThunkResult<void> {
return async (dispatch) => {
try {
await lastValueFrom(
backendSrv.fetch({
method: 'GET',
showErrorAlert: false,
showSuccessAlert: false,
url: `/api/folders/${uid}/permissions`,
})
);
dispatch(setCanViewFolderPermissions(true));
} catch (err) {
if (err.status !== 403) {
dispatch(notifyApp(createWarningNotification('Error checking folder permissions', err.data?.message)));
}
dispatch(setCanViewFolderPermissions(false));
}
};
}
function toUpdateItem(item: DashboardAcl): DashboardAclUpdateDTO { function toUpdateItem(item: DashboardAcl): DashboardAclUpdateDTO {
return { return {
userId: item.userId, userId: item.userId,

View File

@ -1,5 +1,12 @@
import { FolderDTO, FolderState, OrgRole, PermissionLevel } from 'app/types'; import { FolderDTO, FolderState, OrgRole, PermissionLevel } from 'app/types';
import { folderReducer, initialState, loadFolder, loadFolderPermissions, setFolderTitle } from './reducers'; import {
folderReducer,
initialState,
loadFolder,
loadFolderPermissions,
setCanViewFolderPermissions,
setFolderTitle,
} from './reducers';
import { reducerTester } from '../../../../test/core/redux/reducerTester'; import { reducerTester } from '../../../../test/core/redux/reducerTester';
function getTestFolder(): FolderDTO { function getTestFolder(): FolderDTO {
@ -142,4 +149,16 @@ describe('folder reducer', () => {
}); });
}); });
}); });
describe('setCanViewFolderPermissions', () => {
it('should set the canViewFolderPermissions value', () => {
reducerTester<FolderState>()
.givenReducer(folderReducer, { ...initialState })
.whenActionIsDispatched(setCanViewFolderPermissions(true))
.thenStateShouldEqual({
...initialState,
canViewFolderPermissions: true,
});
});
});
}); });

View File

@ -12,6 +12,7 @@ export const initialState: FolderState = {
hasChanged: false, hasChanged: false,
version: 1, version: 1,
permissions: [], permissions: [],
canViewFolderPermissions: false,
}; };
const folderSlice = createSlice({ const folderSlice = createSlice({
@ -38,10 +39,14 @@ const folderSlice = createSlice({
permissions: processAclItems(action.payload), permissions: processAclItems(action.payload),
}; };
}, },
setCanViewFolderPermissions: (state, action: PayloadAction<boolean>): FolderState => {
state.canViewFolderPermissions = action.payload;
return state;
},
}, },
}); });
export const { loadFolderPermissions, loadFolder, setFolderTitle } = folderSlice.actions; export const { loadFolderPermissions, loadFolder, setFolderTitle, setCanViewFolderPermissions } = folderSlice.actions;
export const folderReducer = folderSlice.reducer; export const folderReducer = folderSlice.reducer;

View File

@ -17,6 +17,7 @@ export interface DashboardMeta {
canAdmin?: boolean; canAdmin?: boolean;
url?: string; url?: string;
folderId?: number; folderId?: number;
folderUid?: string;
fromExplore?: boolean; fromExplore?: boolean;
canMakeEditable?: boolean; canMakeEditable?: boolean;
submenuEnabled?: boolean; submenuEnabled?: boolean;

View File

@ -20,10 +20,12 @@ export interface FolderState {
hasChanged: boolean; hasChanged: boolean;
version: number; version: number;
permissions: DashboardAcl[]; permissions: DashboardAcl[];
canViewFolderPermissions: boolean;
} }
export interface FolderInfo { export interface FolderInfo {
id?: number; id?: number;
title?: string; title?: string;
url?: string; url?: string;
canViewFolderPermissions?: boolean;
} }