mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Plugins: Check if the user has permissions to access the plugin custom page (#74664)
* Check if the user has permissions to access the plugin custom page * If plugin does not have includes we should show the plugin * chngaes after review, added the test for AppRootPage * fix the type error * add more test cases * test: wrap location pushes to act() calls * Add no existing role to test * fix name of the test * fix not existing role test --------- Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com>
This commit is contained in:
parent
1c61427f57
commit
44bf663942
@ -4,12 +4,13 @@ import { Provider } from 'react-redux';
|
|||||||
import { Route, Router } from 'react-router-dom';
|
import { Route, Router } from 'react-router-dom';
|
||||||
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
|
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
|
||||||
|
|
||||||
import { AppPlugin, PluginType, AppRootProps, NavModelItem } from '@grafana/data';
|
import { AppPlugin, PluginType, AppRootProps, NavModelItem, PluginIncludeType, OrgRole } from '@grafana/data';
|
||||||
import { getMockPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
|
import { getMockPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
|
||||||
import { locationService, setEchoSrv } from '@grafana/runtime';
|
import { locationService, setEchoSrv } from '@grafana/runtime';
|
||||||
import { GrafanaContext } from 'app/core/context/GrafanaContext';
|
import { GrafanaContext } from 'app/core/context/GrafanaContext';
|
||||||
import { GrafanaRoute } from 'app/core/navigation/GrafanaRoute';
|
import { GrafanaRoute } from 'app/core/navigation/GrafanaRoute';
|
||||||
import { RouteDescriptor } from 'app/core/navigation/types';
|
import { RouteDescriptor } from 'app/core/navigation/types';
|
||||||
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import { Echo } from 'app/core/services/echo/Echo';
|
import { Echo } from 'app/core/services/echo/Echo';
|
||||||
import { configureStore } from 'app/store/configureStore';
|
import { configureStore } from 'app/store/configureStore';
|
||||||
|
|
||||||
@ -43,7 +44,7 @@ class RootComponent extends Component<AppRootProps> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderUnderRouter() {
|
async function renderUnderRouter(page = '') {
|
||||||
const appPluginNavItem: NavModelItem = {
|
const appPluginNavItem: NavModelItem = {
|
||||||
text: 'App',
|
text: 'App',
|
||||||
id: 'plugin-page-app',
|
id: 'plugin-page-app',
|
||||||
@ -68,17 +69,21 @@ function renderUnderRouter() {
|
|||||||
|
|
||||||
appPluginNavItem.parentItem = appsSection;
|
appPluginNavItem.parentItem = appsSection;
|
||||||
|
|
||||||
|
const pagePath = page ? `/${page}` : '';
|
||||||
const store = configureStore();
|
const store = configureStore();
|
||||||
const route = {
|
const route = {
|
||||||
component: () => <AppRootPage pluginId="my-awesome-plugin" pluginNavSection={appsSection} />,
|
component: () => <AppRootPage pluginId="my-awesome-plugin" pluginNavSection={appsSection} />,
|
||||||
} as unknown as RouteDescriptor;
|
} as unknown as RouteDescriptor;
|
||||||
locationService.push('/a/my-awesome-plugin');
|
|
||||||
|
await act(async () => {
|
||||||
|
locationService.push(`/a/my-awesome-plugin${pagePath}`);
|
||||||
|
});
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<Router history={locationService.getHistory()}>
|
<Router history={locationService.getHistory()}>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<GrafanaContext.Provider value={getGrafanaContextMock()}>
|
<GrafanaContext.Provider value={getGrafanaContextMock()}>
|
||||||
<Route path="/a/:pluginId" exact render={(props) => <GrafanaRoute {...props} route={route} />} />
|
<Route path={`/a/:pluginId${pagePath}`} exact render={(props) => <GrafanaRoute {...props} route={route} />} />
|
||||||
</GrafanaContext.Provider>
|
</GrafanaContext.Provider>
|
||||||
</Provider>
|
</Provider>
|
||||||
</Router>
|
</Router>
|
||||||
@ -97,7 +102,7 @@ describe('AppRootPage', () => {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not render component if not at plugin path', async () => {
|
it('should not render the component if we are not under a plugin path', async () => {
|
||||||
getPluginSettingsMock.mockResolvedValue(pluginMeta);
|
getPluginSettingsMock.mockResolvedValue(pluginMeta);
|
||||||
|
|
||||||
const plugin = new AppPlugin();
|
const plugin = new AppPlugin();
|
||||||
@ -106,23 +111,150 @@ describe('AppRootPage', () => {
|
|||||||
|
|
||||||
importAppPluginMock.mockResolvedValue(plugin);
|
importAppPluginMock.mockResolvedValue(plugin);
|
||||||
|
|
||||||
renderUnderRouter();
|
// Renders once for the first time
|
||||||
|
await renderUnderRouter();
|
||||||
expect(await screen.findByText('my great component')).toBeVisible();
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
|
|
||||||
// renders the first time
|
|
||||||
expect(RootComponent.timesRendered).toEqual(1);
|
expect(RootComponent.timesRendered).toEqual(1);
|
||||||
|
|
||||||
|
// Does not render again when navigating to a non-plugin path
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
locationService.push('/foo');
|
locationService.push('/foo');
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(RootComponent.timesRendered).toEqual(1);
|
expect(RootComponent.timesRendered).toEqual(1);
|
||||||
|
|
||||||
|
// Renders it again when navigating back to a plugin path
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
locationService.push('/a/my-awesome-plugin');
|
locationService.push('/a/my-awesome-plugin');
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(RootComponent.timesRendered).toEqual(2);
|
expect(RootComponent.timesRendered).toEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('When accessing using different roles', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const pluginMetaWithIncludes = getMockPlugin({
|
||||||
|
id: 'my-awesome-plugin',
|
||||||
|
type: PluginType.app,
|
||||||
|
enabled: true,
|
||||||
|
includes: [
|
||||||
|
{
|
||||||
|
type: PluginIncludeType.page,
|
||||||
|
name: 'Awesome page 1',
|
||||||
|
path: '/a/my-awesome-plugin/viewer-page',
|
||||||
|
role: 'Viewer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: PluginIncludeType.page,
|
||||||
|
name: 'Awesome page 2',
|
||||||
|
path: '/a/my-awesome-plugin/editor-page',
|
||||||
|
role: 'Editor',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: PluginIncludeType.page,
|
||||||
|
name: 'Awesome page 2',
|
||||||
|
path: '/a/my-awesome-plugin/admin-page',
|
||||||
|
role: 'Admin',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: PluginIncludeType.page,
|
||||||
|
name: 'Awesome page with mistake',
|
||||||
|
path: '/a/my-awesome-plugin/mistake-page',
|
||||||
|
role: 'NotExistingRole',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: PluginIncludeType.page,
|
||||||
|
name: 'Awesome page 2',
|
||||||
|
path: '/a/my-awesome-plugin/page-without-role',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
getPluginSettingsMock.mockResolvedValue(pluginMetaWithIncludes);
|
||||||
|
|
||||||
|
const plugin = new AppPlugin();
|
||||||
|
plugin.meta = pluginMetaWithIncludes;
|
||||||
|
plugin.root = RootComponent;
|
||||||
|
|
||||||
|
importAppPluginMock.mockResolvedValue(plugin);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('an User should not be able to see page with not existing role', async () => {
|
||||||
|
contextSrv.user.orgRole = OrgRole.Editor;
|
||||||
|
|
||||||
|
await renderUnderRouter('mistake-page');
|
||||||
|
expect(await screen.findByText('Access denied')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('a Viewer should only have access to pages with "Viewer" roles', async () => {
|
||||||
|
contextSrv.user.orgRole = OrgRole.Viewer;
|
||||||
|
|
||||||
|
// Viewer has access to a plugin entry page by default
|
||||||
|
await renderUnderRouter('');
|
||||||
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
|
|
||||||
|
// Viewer has access to a page without roles
|
||||||
|
await renderUnderRouter('page-without-role');
|
||||||
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
|
|
||||||
|
// Viewer has access to Viewer page
|
||||||
|
await renderUnderRouter('viewer-page');
|
||||||
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
|
|
||||||
|
// Viewer does not have access to Editor page
|
||||||
|
await renderUnderRouter('editor-page');
|
||||||
|
expect(await screen.findByText('Access denied')).toBeVisible();
|
||||||
|
|
||||||
|
// Viewer does not have access to a Admin page
|
||||||
|
await renderUnderRouter('admin-page');
|
||||||
|
expect(await screen.findByText('Access denied')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('an Editor should have access to pages with both "Viewer" and "Editor" roles', async () => {
|
||||||
|
contextSrv.user.orgRole = OrgRole.Editor;
|
||||||
|
contextSrv.isEditor = true;
|
||||||
|
|
||||||
|
// Viewer has access to a plugin entry page by default
|
||||||
|
await renderUnderRouter('');
|
||||||
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
|
|
||||||
|
// Editor has access to a page without roles
|
||||||
|
await renderUnderRouter('page-without-role');
|
||||||
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
|
|
||||||
|
// Editor has access to Viewer page
|
||||||
|
await renderUnderRouter('viewer-page');
|
||||||
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
|
|
||||||
|
// Editor has access to Editor page
|
||||||
|
await renderUnderRouter('editor-page');
|
||||||
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
|
|
||||||
|
// Editor does not have access to a Admin page
|
||||||
|
await renderUnderRouter('admin-page');
|
||||||
|
expect(await screen.findByText('Access denied')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('a Grafana Admin should be able to see any page', async () => {
|
||||||
|
contextSrv.isGrafanaAdmin = true;
|
||||||
|
|
||||||
|
// Viewer has access to a plugin entry page
|
||||||
|
await renderUnderRouter('');
|
||||||
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
|
|
||||||
|
// Admin has access to a page without roles
|
||||||
|
await renderUnderRouter('page-without-role');
|
||||||
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
|
|
||||||
|
// Admin has access to Viewer page
|
||||||
|
await renderUnderRouter('viewer-page');
|
||||||
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
|
|
||||||
|
// Admin has access to Editor page
|
||||||
|
await renderUnderRouter('editor-page');
|
||||||
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
|
|
||||||
|
// Admin has access to a Admin page
|
||||||
|
await renderUnderRouter('admin-page');
|
||||||
|
expect(await screen.findByText('my great component')).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -3,12 +3,13 @@ import { AnyAction, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
|||||||
import React, { useCallback, useEffect, useMemo, useReducer } from 'react';
|
import React, { useCallback, useEffect, useMemo, useReducer } from 'react';
|
||||||
import { useLocation, useRouteMatch } from 'react-router-dom';
|
import { useLocation, useRouteMatch } from 'react-router-dom';
|
||||||
|
|
||||||
import { AppEvents, AppPlugin, AppPluginMeta, NavModel, NavModelItem, PluginType } from '@grafana/data';
|
import { AppEvents, AppPlugin, AppPluginMeta, NavModel, NavModelItem, OrgRole, PluginType } from '@grafana/data';
|
||||||
import { config, locationSearchToObject } from '@grafana/runtime';
|
import { config, locationSearchToObject } from '@grafana/runtime';
|
||||||
|
import { Alert } from '@grafana/ui';
|
||||||
import { Page } from 'app/core/components/Page/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
import PageLoader from 'app/core/components/PageLoader/PageLoader';
|
import PageLoader from 'app/core/components/PageLoader/PageLoader';
|
||||||
import { useGrafana } from 'app/core/context/GrafanaContext';
|
import { useGrafana } from 'app/core/context/GrafanaContext';
|
||||||
import { appEvents } from 'app/core/core';
|
import { appEvents, contextSrv } from 'app/core/core';
|
||||||
import { getNotFoundNav, getWarningNav, getExceptionNav } from 'app/core/navigation/errorModels';
|
import { getNotFoundNav, getWarningNav, getExceptionNav } from 'app/core/navigation/errorModels';
|
||||||
|
|
||||||
import { getPluginSettings } from '../pluginSettings';
|
import { getPluginSettings } from '../pluginSettings';
|
||||||
@ -81,6 +82,39 @@ export function AppRootPage({ pluginId, pluginNavSection }: Props) {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Because of the fallback at plugin routes, we need to check
|
||||||
|
// if the user has permissions to see the plugin page.
|
||||||
|
const userHasPermissionsToPluginPage = () => {
|
||||||
|
// Check if plugin does not have any configurations or the user is Grafana Admin
|
||||||
|
if (!plugin.meta?.includes || contextSrv.isGrafanaAdmin || contextSrv.user.orgRole === OrgRole.Admin) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginInclude = plugin.meta?.includes.find((include) => include.path === pluginRoot.props.path);
|
||||||
|
// Check if include configuration contains current path
|
||||||
|
if (!pluginInclude) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const pathRole: string = pluginInclude?.role || '';
|
||||||
|
// Check if role exists and give access to Editor to be able to see Viewer pages
|
||||||
|
if (!pathRole || (contextSrv.isEditor && pathRole === OrgRole.Viewer)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return contextSrv.hasRole(pathRole);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AccessDenied = () => {
|
||||||
|
return (
|
||||||
|
<Alert severity="warning" title="Access denied">
|
||||||
|
You do not have permission to see this page.
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!userHasPermissionsToPluginPage()) {
|
||||||
|
return <AccessDenied />;
|
||||||
|
}
|
||||||
|
|
||||||
if (!pluginNav) {
|
if (!pluginNav) {
|
||||||
return <PluginPageContext.Provider value={context}>{pluginRoot}</PluginPageContext.Provider>;
|
return <PluginPageContext.Provider value={context}>{pluginRoot}</PluginPageContext.Provider>;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user