Plugins: Allow plugin page access granting via permissions (#82508)

* AccessControl: Check permissions on AppRootPage

* add frontend tests for app root permission checks

* add accesscontrol oncall ft to tests
This commit is contained in:
Jo 2024-02-16 09:36:52 +01:00 committed by GitHub
parent cdd3e1c776
commit c5d1b295ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 177 additions and 28 deletions

View File

@ -1028,6 +1028,12 @@
"count": 5
}
],
"/packages/grafana-ui/src/components/Splitter/Splitter.tsx": [
{
"message": "Do not use any type assertions.",
"count": 1
}
],
"/packages/grafana-ui/src/components/StatsPicker/StatsPicker.story.tsx": [
{
"message": "Unexpected any. Specify a different type.",
@ -2962,12 +2968,12 @@
],
"/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx": [
{
"message": "Do not use any type assertions.",
"message": "Unexpected any. Specify a different type.",
"count": 2
},
{
"message": "Unexpected any. Specify a different type.",
"count": 2
"message": "Do not use any type assertions.",
"count": 1
}
],
"/public/app/features/dashboard-scene/scene/setDashboardPanelContext.test.ts": [
@ -3018,6 +3024,12 @@
"count": 1
}
],
"/public/app/features/dashboard-scene/utils/PanelModelCompatibilityWrapper.ts": [
{
"message": "Do not use any type assertions.",
"count": 1
}
],
"/public/app/features/dashboard-scene/utils/test-utils.ts": [
{
"message": "Do not use any type assertions.",
@ -4060,12 +4072,6 @@
"count": 1
}
],
"/public/app/features/explore/TraceView/components/common/Divider.tsx": [
{
"message": "Styles should be written using objects.",
"count": 3
}
],
"/public/app/features/explore/TraceView/components/common/LabeledList.tsx": [
{
"message": "Styles should be written using objects.",
@ -6597,11 +6603,11 @@
"/public/app/plugins/datasource/tempo/datasource.ts": [
{
"message": "Do not use any type assertions.",
"count": 7
"count": 2
},
{
"message": "Unexpected any. Specify a different type.",
"count": 5
"count": 4
}
],
"/public/app/plugins/datasource/tempo/language_provider.ts": [
@ -7213,10 +7219,6 @@
}
],
"/public/app/plugins/panel/timeseries/plugins/annotations/AnnotationEditor.tsx": [
{
"message": "Styles should be written using objects.",
"count": 5
},
{
"message": "Do not use any type assertions.",
"count": 1
@ -7228,12 +7230,6 @@
"count": 7
}
],
"/public/app/plugins/panel/timeseries/plugins/annotations/AnnotationMarker.tsx": [
{
"message": "Styles should be written using objects.",
"count": 3
}
],
"/public/app/plugins/panel/timeseries/plugins/annotations/AnnotationTooltip.tsx": [
{
"message": "Styles should be written using objects.",

View File

@ -121,6 +121,10 @@ export interface PluginInclude {
// "Admin", "Editor" or "Viewer". If set then the include will only show up in the navigation if the user has the required roles.
role?: string;
// if action is set then the include will only show up in the navigation if the user has the required permission.
// The action will take precedence over the role.
action?: string;
// Adds the "page" or "dashboard" type includes to the navigation if set to `true`.
addToNav?: boolean;

View File

@ -26,6 +26,25 @@ jest.mock('../plugin_loader', () => ({
importAppPlugin: jest.fn(),
}));
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
config: {
featureToggles: {
accessControlOnCall: true,
},
theme2: {
breakpoints: {
values: {
sm: 576,
md: 768,
lg: 992,
xl: 1200,
},
},
},
},
}));
const importAppPluginMock = importAppPlugin as jest.Mock<
ReturnType<typeof importAppPlugin>,
Parameters<typeof importAppPlugin>
@ -175,6 +194,19 @@ describe('AppRootPage', () => {
name: 'Awesome page 2',
path: '/a/my-awesome-plugin/page-without-role',
},
{
type: PluginIncludeType.page,
name: 'Awesome page 3',
path: '/a/my-awesome-plugin/page-with-action-no-role',
action: 'grafana-awesomeapp.user-settings:read',
},
{
type: PluginIncludeType.page,
name: 'Awesome page 4',
path: '/a/my-awesome-plugin/page-with-action-and-role',
role: 'Viewer',
action: 'grafana-awesomeapp.user-settings:read',
},
],
});
@ -194,13 +226,58 @@ describe('AppRootPage', () => {
expect(await screen.findByText('Access denied')).toBeVisible();
});
it('a None role user should only have access to pages with actions defined or undefined', async () => {
contextSrv.user.orgRole = OrgRole.None;
// has access to a plugin entry page by default
await renderUnderRouter('');
expect(await screen.findByText('my great component')).toBeVisible();
// does not have access to a page with an action but no role
await renderUnderRouter('page-with-action-no-role');
expect(await screen.findByText('Access denied')).toBeVisible();
// does not have access to a page with an action and role
await renderUnderRouter('page-with-action-and-role');
expect(await screen.findByText('Access denied')).toBeVisible();
// has access to a page without roles
await renderUnderRouter('page-without-role');
expect(await screen.findByText('my great component')).toBeVisible();
// has access to Viewer page
await renderUnderRouter('viewer-page');
expect(await screen.findByText('Access denied')).toBeVisible();
contextSrv.user.permissions = {
'grafana-awesomeapp.user-settings:read': true,
};
// has access to a page with an action but no role
await renderUnderRouter('page-with-action-no-role');
expect(await screen.findByText('my great component')).toBeVisible();
// has access to a page with an action and role
await renderUnderRouter('page-with-action-and-role');
expect(await screen.findByText('my great component')).toBeVisible();
});
it('a Viewer should only have access to pages with "Viewer" roles', async () => {
contextSrv.user.orgRole = OrgRole.Viewer;
contextSrv.user.permissions = {};
// Viewer has access to a plugin entry page by default
await renderUnderRouter('');
expect(await screen.findByText('my great component')).toBeVisible();
// Viewer does not have access to a page with an action but no role
await renderUnderRouter('page-with-action-no-role');
expect(await screen.findByText('Access denied')).toBeVisible();
// Viewer does not have access to a page with an action and role
await renderUnderRouter('page-with-action-and-role');
expect(await screen.findByText('Access denied')).toBeVisible();
// Viewer has access to a page without roles
await renderUnderRouter('page-without-role');
expect(await screen.findByText('my great component')).toBeVisible();
@ -221,35 +298,97 @@ describe('AppRootPage', () => {
it('an Editor should have access to pages with both "Viewer" and "Editor" roles', async () => {
contextSrv.user.orgRole = OrgRole.Editor;
contextSrv.isEditor = true;
contextSrv.user.permissions = {};
// Viewer has access to a plugin entry page by default
// does not have access to a page with an action but no role
await renderUnderRouter('page-with-action-no-role');
expect(await screen.findByText('Access denied')).toBeVisible();
// does not have access to a page with an action and role
await renderUnderRouter('page-with-action-and-role');
expect(await screen.findByText('Access denied')).toBeVisible();
// 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
// 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
// has access to Viewer page
await renderUnderRouter('viewer-page');
expect(await screen.findByText('my great component')).toBeVisible();
// Editor has access to Editor page
// 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
// does not have access to a Admin page
await renderUnderRouter('admin-page');
expect(await screen.findByText('Access denied')).toBeVisible();
contextSrv.user.permissions = {
'grafana-awesomeapp.user-settings:read': true,
};
// has access to a page with an action but no role
await renderUnderRouter('page-with-action-no-role');
expect(await screen.findByText('my great component')).toBeVisible();
// has access to a page with an action and role
await renderUnderRouter('page-with-action-and-role');
expect(await screen.findByText('my great component')).toBeVisible();
});
it('a Grafana Admin should be able to see any page', async () => {
it('an Admin should have access to pages with both "Viewer" and "Editor" roles', async () => {
contextSrv.user.orgRole = OrgRole.Admin;
contextSrv.user.permissions = {};
// does not have access to a page with an action but no role
await renderUnderRouter('page-with-action-no-role');
expect(await screen.findByText('Access denied')).toBeVisible();
// does not have access to a page with an action and role
await renderUnderRouter('page-with-action-and-role');
expect(await screen.findByText('Access denied')).toBeVisible();
// has access to a plugin entry page by default
await renderUnderRouter('');
expect(await screen.findByText('my great component')).toBeVisible();
// has access to a page without roles
await renderUnderRouter('page-without-role');
expect(await screen.findByText('my great component')).toBeVisible();
// has access to Viewer page
await renderUnderRouter('viewer-page');
expect(await screen.findByText('my great component')).toBeVisible();
// has access to Editor page
await renderUnderRouter('editor-page');
expect(await screen.findByText('my great component')).toBeVisible();
// has access to a Admin page
await renderUnderRouter('admin-page');
expect(await screen.findByText('my great component')).toBeVisible();
});
it('a Grafana Admin should be able to see any page without action specifier', async () => {
contextSrv.isGrafanaAdmin = true;
// Viewer has access to a plugin entry page
await renderUnderRouter('');
expect(await screen.findByText('my great component')).toBeVisible();
// Viewer does not have access to a page with an action but no role
await renderUnderRouter('page-with-action-no-role');
expect(await screen.findByText('Access denied')).toBeVisible();
// Viewer does not have access to a page with an action and role
await renderUnderRouter('page-with-action-and-role');
expect(await screen.findByText('Access denied')).toBeVisible();
// Admin has access to a page without roles
await renderUnderRouter('page-without-role');
expect(await screen.findByText('my great component')).toBeVisible();

View File

@ -101,7 +101,7 @@ export function AppRootPage({ pluginId, pluginNavSection }: Props) {
// 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) {
if (!plugin.meta?.includes) {
return true;
}
@ -110,6 +110,16 @@ export function AppRootPage({ pluginId, pluginNavSection }: Props) {
if (!pluginInclude) {
return true;
}
// Check if action exists and give access if user has the required permission.
if (pluginInclude?.action && config.featureToggles.accessControlOnCall) {
return contextSrv.hasPermission(pluginInclude.action);
}
if (contextSrv.isGrafanaAdmin || contextSrv.user.orgRole === OrgRole.Admin) {
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)) {