mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
dc913f2311
commit
9f97f05fcc
@ -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>}
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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),
|
||||||
|
65
public/app/features/folders/state/actions.test.ts
Normal file
65
public/app/features/folders/state/actions.test.ts
Normal 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),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user