mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Access Control: Add fine-grained access control to explore (#35883)
* add fixed role for datasource read operations * Add action for datasource explore * add authorize middleware to explore index route * add fgac support for explore navlink * update hasAccessToExplore to check if accesscontrol is enable and evalute action if it is * add getExploreRoles to evalute roles based onaccesscontrol, viewersCanEdit and default * create function to evaluate permissions or using fallback if accesscontrol is disabled * change hasAccess to prop and derive the value in mapStateToProps * add test case to ensure buttons is not rendered when user does not have access * Only hide return with changes button * remove internal links if user does not have access to explorer Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>
This commit is contained in:
@@ -25,6 +25,7 @@ Fixed roles | Permissions | Descriptions
|
|||||||
`fixed:server:admin:read` | `server.stats:read` | Read server stats
|
`fixed:server:admin:read` | `server.stats:read` | Read server stats
|
||||||
`fixed:settings:admin:read` | `settings:read` | Read settings
|
`fixed:settings:admin:read` | `settings:read` | Read settings
|
||||||
`fixed:settings:admin:edit` | All permissions from `fixed:settings:admin:read` and<br>`settings:write` | Update settings
|
`fixed:settings:admin:edit` | All permissions from `fixed:settings:admin:read` and<br>`settings:write` | Update settings
|
||||||
|
`fixed:datasource:editor:read` | `datasources:explore` | Explore datasources
|
||||||
|
|
||||||
## Default built-in role assignments
|
## Default built-in role assignments
|
||||||
|
|
||||||
@@ -32,3 +33,4 @@ Built-in roles | Associated roles | Descriptions
|
|||||||
--- | --- | ---
|
--- | --- | ---
|
||||||
Grafana Admin | `fixed:permissions:admin:edit`<br>`fixed:permissions:admin:read`<br>`fixed:reporting:admin:edit`<br>`fixed:reporting:admin:read`<br>`fixed:users:admin:edit`<br>`fixed:users:admin:read`<br>`fixed:users:org:edit`<br>`fixed:users:org:read`<br>`fixed:ldap:admin:edit`<br>`fixed:ldap:admin:read`<br>`fixed:server:admin:read`<br>`fixed:settings:admin:read`<br>`fixed:settings:admin:edit` | Allows access to resources which [Grafana Server Admin]({{< relref "../../permissions/_index.md#grafana-server-admin-role" >}}) has permissions by default.
|
Grafana Admin | `fixed:permissions:admin:edit`<br>`fixed:permissions:admin:read`<br>`fixed:reporting:admin:edit`<br>`fixed:reporting:admin:read`<br>`fixed:users:admin:edit`<br>`fixed:users:admin:read`<br>`fixed:users:org:edit`<br>`fixed:users:org:read`<br>`fixed:ldap:admin:edit`<br>`fixed:ldap:admin:read`<br>`fixed:server:admin:read`<br>`fixed:settings:admin:read`<br>`fixed:settings:admin:edit` | Allows access to resources which [Grafana Server Admin]({{< relref "../../permissions/_index.md#grafana-server-admin-role" >}}) has permissions by default.
|
||||||
Admin | `fixed:users:org:edit`<br>`fixed:users:org:read`<br>`fixed:reporting:admin:edit`<br>`fixed:reporting:admin:read` | Allows access to resource which [Admin]({{< relref "../../permissions/organization_roles.md" >}}) has permissions by default.
|
Admin | `fixed:users:org:edit`<br>`fixed:users:org:read`<br>`fixed:reporting:admin:edit`<br>`fixed:reporting:admin:read` | Allows access to resource which [Admin]({{< relref "../../permissions/organization_roles.md" >}}) has permissions by default.
|
||||||
|
Editor | `fixed:datasource:editor:read`
|
||||||
|
@@ -66,6 +66,7 @@ Actions | Applicable scopes | Descriptions
|
|||||||
`settings:read` | `settings:**`<br>`settings:auth.saml:*`<br>`settings:auth.saml:enabled` (property level) | Read settings
|
`settings:read` | `settings:**`<br>`settings:auth.saml:*`<br>`settings:auth.saml:enabled` (property level) | Read settings
|
||||||
`settings:write` | `settings:**`<br>`settings:auth.saml:*`<br>`settings:auth.saml:enabled` (property level) | Update settings
|
`settings:write` | `settings:**`<br>`settings:auth.saml:*`<br>`settings:auth.saml:enabled` (property level) | Update settings
|
||||||
`server.stats:read` | n/a | Read server stats
|
`server.stats:read` | n/a | Read server stats
|
||||||
|
`datasources:explore` | n/a | Enable explore
|
||||||
|
|
||||||
## Scope definitions
|
## Scope definitions
|
||||||
|
|
||||||
|
@@ -94,7 +94,12 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
r.Get("/dashboards/*", reqSignedIn, hs.Index)
|
r.Get("/dashboards/*", reqSignedIn, hs.Index)
|
||||||
r.Get("/goto/:uid", reqSignedIn, hs.redirectFromShortURL, hs.Index)
|
r.Get("/goto/:uid", reqSignedIn, hs.redirectFromShortURL, hs.Index)
|
||||||
|
|
||||||
r.Get("/explore", reqSignedIn, middleware.EnsureEditorOrViewerCanEdit, hs.Index)
|
r.Get("/explore", authorize(func(c *models.ReqContext) {
|
||||||
|
if f, ok := reqSignedIn.(func(c *models.ReqContext)); ok {
|
||||||
|
f(c)
|
||||||
|
}
|
||||||
|
middleware.EnsureEditorOrViewerCanEdit(c)
|
||||||
|
}, accesscontrol.ActionDatasourcesExplore), hs.Index)
|
||||||
|
|
||||||
r.Get("/playlists/", reqSignedIn, hs.Index)
|
r.Get("/playlists/", reqSignedIn, hs.Index)
|
||||||
r.Get("/playlists/*", reqSignedIn, hs.Index)
|
r.Get("/playlists/*", reqSignedIn, hs.Index)
|
||||||
|
@@ -186,7 +186,11 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
|||||||
Children: dashboardChildNavs,
|
Children: dashboardChildNavs,
|
||||||
})
|
})
|
||||||
|
|
||||||
if setting.ExploreEnabled && (c.OrgRole == models.ROLE_ADMIN || c.OrgRole == models.ROLE_EDITOR || setting.ViewersCanEdit) {
|
canExplore := func(context *models.ReqContext) bool {
|
||||||
|
return c.OrgRole == models.ROLE_ADMIN || c.OrgRole == models.ROLE_EDITOR || setting.ViewersCanEdit
|
||||||
|
}
|
||||||
|
|
||||||
|
if setting.ExploreEnabled && hasAccess(canExplore, ac.ActionDatasourcesExplore) {
|
||||||
navTree = append(navTree, &dtos.NavLink{
|
navTree = append(navTree, &dtos.NavLink{
|
||||||
Text: "Explore",
|
Text: "Explore",
|
||||||
Id: "explore",
|
Id: "explore",
|
||||||
|
@@ -42,7 +42,6 @@ func (p RoleDTO) Role() Role {
|
|||||||
const (
|
const (
|
||||||
// Permission actions
|
// Permission actions
|
||||||
|
|
||||||
// Actions
|
|
||||||
// Provisioning actions
|
// Provisioning actions
|
||||||
ActionProvisioningReload = "provisioning:reload"
|
ActionProvisioningReload = "provisioning:reload"
|
||||||
|
|
||||||
@@ -86,6 +85,9 @@ const (
|
|||||||
// Settings actions
|
// Settings actions
|
||||||
ActionSettingsRead = "settings:read"
|
ActionSettingsRead = "settings:read"
|
||||||
|
|
||||||
|
// Datasources actions
|
||||||
|
ActionDatasourcesExplore = "datasources:explore"
|
||||||
|
|
||||||
// Global Scopes
|
// Global Scopes
|
||||||
ScopeGlobalUsersAll = "global:users:*"
|
ScopeGlobalUsersAll = "global:users:*"
|
||||||
|
|
||||||
|
@@ -2,6 +2,16 @@ package accesscontrol
|
|||||||
|
|
||||||
import "github.com/grafana/grafana/pkg/models"
|
import "github.com/grafana/grafana/pkg/models"
|
||||||
|
|
||||||
|
var datasourcesEditorReadRole = RoleDTO{
|
||||||
|
Version: 1,
|
||||||
|
Name: datasourcesEditorRead,
|
||||||
|
Permissions: []Permission{
|
||||||
|
{
|
||||||
|
Action: ActionDatasourcesExplore,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
var ldapAdminReadRole = RoleDTO{
|
var ldapAdminReadRole = RoleDTO{
|
||||||
Name: ldapAdminRead,
|
Name: ldapAdminRead,
|
||||||
Version: 1,
|
Version: 1,
|
||||||
@@ -166,6 +176,7 @@ var provisioningAdminRole = RoleDTO{
|
|||||||
// resource. FixedRoleGrants lists which built-in roles are
|
// resource. FixedRoleGrants lists which built-in roles are
|
||||||
// assigned which fixed roles in this list.
|
// assigned which fixed roles in this list.
|
||||||
var FixedRoles = map[string]RoleDTO{
|
var FixedRoles = map[string]RoleDTO{
|
||||||
|
datasourcesEditorRead: datasourcesEditorReadRole,
|
||||||
serverAdminRead: serverAdminReadRole,
|
serverAdminRead: serverAdminReadRole,
|
||||||
|
|
||||||
settingsAdminRead: settingsAdminReadRole,
|
settingsAdminRead: settingsAdminReadRole,
|
||||||
@@ -183,6 +194,8 @@ var FixedRoles = map[string]RoleDTO{
|
|||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
datasourcesEditorRead = "fixed:datasources:editor:read"
|
||||||
|
|
||||||
serverAdminRead = "fixed:server:admin:read"
|
serverAdminRead = "fixed:server:admin:read"
|
||||||
|
|
||||||
settingsAdminRead = "fixed:settings:admin:read"
|
settingsAdminRead = "fixed:settings:admin:read"
|
||||||
@@ -217,6 +230,9 @@ var FixedRoleGrants = map[string][]string{
|
|||||||
usersOrgEdit,
|
usersOrgEdit,
|
||||||
usersOrgRead,
|
usersOrgRead,
|
||||||
},
|
},
|
||||||
|
string(models.ROLE_EDITOR): {
|
||||||
|
datasourcesEditorRead,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConcatPermissions(permissions ...[]Permission) []Permission {
|
func ConcatPermissions(permissions ...[]Permission) []Permission {
|
||||||
|
@@ -106,6 +106,9 @@ export class ContextSrv {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hasAccessToExplore() {
|
hasAccessToExplore() {
|
||||||
|
if (config.featureToggles['accesscontrol']) {
|
||||||
|
return this.hasPermission(AccessControlAction.DataSourcesExplore);
|
||||||
|
}
|
||||||
return (this.isEditor || config.viewersCanEdit) && config.exploreEnabled;
|
return (this.isEditor || config.viewersCanEdit) && config.exploreEnabled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -7,6 +7,7 @@ const createProps = (propsOverride?: Partial<ComponentProps<typeof ReturnToDashb
|
|||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
originPanelId: 1,
|
originPanelId: 1,
|
||||||
splitted: false,
|
splitted: false,
|
||||||
|
canEdit: true,
|
||||||
exploreId: ExploreId.left,
|
exploreId: ExploreId.left,
|
||||||
queries: [],
|
queries: [],
|
||||||
setDashboardQueriesToUpdateOnLoad: jest.fn(),
|
setDashboardQueriesToUpdateOnLoad: jest.fn(),
|
||||||
@@ -31,6 +32,11 @@ describe('ReturnToDashboardButton', () => {
|
|||||||
expect(screen.queryByTestId(/returnButton/i)).toBeNull();
|
expect(screen.queryByTestId(/returnButton/i)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not render return to panel with changes button if user cannot edit panel', () => {
|
||||||
|
render(<ReturnToDashboardButton {...createProps({ canEdit: false })} />);
|
||||||
|
expect(screen.getAllByTestId(/returnButton/i)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
it('should show option to return to dashboard with changes', () => {
|
it('should show option to return to dashboard with changes', () => {
|
||||||
render(<ReturnToDashboardButton {...createProps()} />);
|
render(<ReturnToDashboardButton {...createProps()} />);
|
||||||
const returnWithChangesButton = screen.getByTestId('returnButtonWithChanges');
|
const returnWithChangesButton = screen.getByTestId('returnButtonWithChanges');
|
||||||
|
@@ -4,24 +4,32 @@ import { ButtonGroup, ButtonSelect, Icon, ToolbarButton, Tooltip } from '@grafan
|
|||||||
import { DataQuery, urlUtil } from '@grafana/data';
|
import { DataQuery, urlUtil } from '@grafana/data';
|
||||||
|
|
||||||
import kbn from '../../core/utils/kbn';
|
import kbn from '../../core/utils/kbn';
|
||||||
|
import config from 'app/core/config';
|
||||||
import { getDashboardSrv } from '../dashboard/services/DashboardSrv';
|
import { getDashboardSrv } from '../dashboard/services/DashboardSrv';
|
||||||
import { StoreState } from 'app/types';
|
import { StoreState } from 'app/types';
|
||||||
import { ExploreId } from 'app/types/explore';
|
import { ExploreId } from 'app/types/explore';
|
||||||
import { setDashboardQueriesToUpdateOnLoad } from '../dashboard/state/reducers';
|
import { setDashboardQueriesToUpdateOnLoad } from '../dashboard/state/reducers';
|
||||||
import { isSplit } from './state/selectors';
|
import { isSplit } from './state/selectors';
|
||||||
import { locationService } from '@grafana/runtime';
|
import { locationService } from '@grafana/runtime';
|
||||||
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
|
|
||||||
function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreId }) {
|
function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreId }) {
|
||||||
const explore = state.explore;
|
const explore = state.explore;
|
||||||
const splitted = isSplit(state);
|
const splitted = isSplit(state);
|
||||||
const { datasourceInstance, queries, originPanelId } = explore[exploreId]!;
|
const { datasourceInstance, queries, originPanelId } = explore[exploreId]!;
|
||||||
|
|
||||||
|
const roles = ['Editor', 'Admin'];
|
||||||
|
if (config.viewersCanEdit) {
|
||||||
|
roles.push('Viewer');
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
exploreId,
|
exploreId,
|
||||||
datasourceInstance,
|
datasourceInstance,
|
||||||
queries,
|
queries,
|
||||||
originPanelId,
|
originPanelId,
|
||||||
splitted,
|
splitted,
|
||||||
|
canEdit: roles.some((r) => contextSrv.hasRole(r)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +45,7 @@ export const UnconnectedReturnToDashboardButton: FC<Props> = ({
|
|||||||
setDashboardQueriesToUpdateOnLoad,
|
setDashboardQueriesToUpdateOnLoad,
|
||||||
queries,
|
queries,
|
||||||
splitted,
|
splitted,
|
||||||
|
canEdit,
|
||||||
}) => {
|
}) => {
|
||||||
const withOriginId = originPanelId && Number.isInteger(originPanelId);
|
const withOriginId = originPanelId && Number.isInteger(originPanelId);
|
||||||
|
|
||||||
@@ -87,11 +96,13 @@ export const UnconnectedReturnToDashboardButton: FC<Props> = ({
|
|||||||
<Icon name="arrow-left" />
|
<Icon name="arrow-left" />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
{canEdit && (
|
||||||
<ButtonSelect
|
<ButtonSelect
|
||||||
data-testid="returnButtonWithChanges"
|
data-testid="returnButtonWithChanges"
|
||||||
options={[{ label: 'Return to panel with changes', value: '' }]}
|
options={[{ label: 'Return to panel with changes', value: '' }]}
|
||||||
onChange={() => returnToPanel({ withChanges: true })}
|
onChange={() => returnToPanel({ withChanges: true })}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -10,6 +10,7 @@ import {
|
|||||||
TimeRange,
|
TimeRange,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { setLinkSrv } from '../../panel/panellinks/link_srv';
|
import { setLinkSrv } from '../../panel/panellinks/link_srv';
|
||||||
|
import { setContextSrv } from '../../../core/services/context_srv';
|
||||||
|
|
||||||
describe('getFieldLinksForExplore', () => {
|
describe('getFieldLinksForExplore', () => {
|
||||||
it('returns correct link model for external link', () => {
|
it('returns correct link model for external link', () => {
|
||||||
@@ -62,9 +63,40 @@ describe('getFieldLinksForExplore', () => {
|
|||||||
range,
|
range,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns correct link model for external link when user does not have access to explore', () => {
|
||||||
|
const { field, range } = setup(
|
||||||
|
{
|
||||||
|
title: 'external',
|
||||||
|
url: 'http://regionalhost',
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
const links = getFieldLinksForExplore({ field, rowIndex: 0, range });
|
||||||
|
|
||||||
|
expect(links[0].href).toBe('http://regionalhost');
|
||||||
|
expect(links[0].title).toBe('external');
|
||||||
});
|
});
|
||||||
|
|
||||||
function setup(link: DataLink) {
|
it('returns no internal links if when user does not have access to explore', () => {
|
||||||
|
const { field, range } = setup(
|
||||||
|
{
|
||||||
|
title: '',
|
||||||
|
url: '',
|
||||||
|
internal: {
|
||||||
|
query: { query: 'query_1' },
|
||||||
|
datasourceUid: 'uid_1',
|
||||||
|
datasourceName: 'test_ds',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
const links = getFieldLinksForExplore({ field, rowIndex: 0, range });
|
||||||
|
expect(links).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function setup(link: DataLink, hasAccess = true) {
|
||||||
setLinkSrv({
|
setLinkSrv({
|
||||||
getDataLinkUIModel(link: DataLink, replaceVariables: InterpolateFunction | undefined, origin: any): LinkModel<any> {
|
getDataLinkUIModel(link: DataLink, replaceVariables: InterpolateFunction | undefined, origin: any): LinkModel<any> {
|
||||||
return {
|
return {
|
||||||
@@ -82,6 +114,10 @@ function setup(link: DataLink) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setContextSrv({
|
||||||
|
hasAccessToExplore: () => hasAccess,
|
||||||
|
} as any);
|
||||||
|
|
||||||
const field: Field<string> = {
|
const field: Field<string> = {
|
||||||
name: 'flux-dimensions',
|
name: 'flux-dimensions',
|
||||||
type: FieldType.string,
|
type: FieldType.string,
|
||||||
|
@@ -12,6 +12,7 @@ import {
|
|||||||
import { getTemplateSrv } from '@grafana/runtime';
|
import { getTemplateSrv } from '@grafana/runtime';
|
||||||
import { SplitOpen } from 'app/types/explore';
|
import { SplitOpen } from 'app/types/explore';
|
||||||
import { getLinkSrv } from '../../panel/panellinks/link_srv';
|
import { getLinkSrv } from '../../panel/panellinks/link_srv';
|
||||||
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get links from the field of a dataframe and in addition check if there is associated
|
* Get links from the field of a dataframe and in addition check if there is associated
|
||||||
@@ -52,8 +53,16 @@ export const getFieldLinksForExplore = (options: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return field.config.links
|
if (field.config.links) {
|
||||||
? field.config.links.map((link) => {
|
const links = [];
|
||||||
|
|
||||||
|
if (!contextSrv.hasAccessToExplore()) {
|
||||||
|
links.push(...field.config.links.filter((l) => !l.internal));
|
||||||
|
} else {
|
||||||
|
links.push(...field.config.links);
|
||||||
|
}
|
||||||
|
|
||||||
|
return links.map((link) => {
|
||||||
if (!link.internal) {
|
if (!link.internal) {
|
||||||
const replace: InterpolateFunction = (value, vars) =>
|
const replace: InterpolateFunction = (value, vars) =>
|
||||||
getTemplateSrv().replace(value, { ...vars, ...scopedVars });
|
getTemplateSrv().replace(value, { ...vars, ...scopedVars });
|
||||||
@@ -74,8 +83,10 @@ export const getFieldLinksForExplore = (options: {
|
|||||||
replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()),
|
replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
: [];
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
function getTitleFromHref(href: string): string {
|
function getTitleFromHref(href: string): string {
|
||||||
|
@@ -3,11 +3,12 @@ import LdapPage from 'app/features/admin/ldap/LdapPage';
|
|||||||
import UserAdminPage from 'app/features/admin/UserAdminPage';
|
import UserAdminPage from 'app/features/admin/UserAdminPage';
|
||||||
import { LoginPage } from 'app/core/components/Login/LoginPage';
|
import { LoginPage } from 'app/core/components/Login/LoginPage';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import { DashboardRoutes } from 'app/types';
|
import { AccessControlAction, DashboardRoutes } from 'app/types';
|
||||||
import { SafeDynamicImport } from '../core/components/DynamicImports/SafeDynamicImport';
|
import { SafeDynamicImport } from '../core/components/DynamicImports/SafeDynamicImport';
|
||||||
import { RouteDescriptor } from '../core/navigation/types';
|
import { RouteDescriptor } from '../core/navigation/types';
|
||||||
import { Redirect } from 'react-router-dom';
|
import { Redirect } from 'react-router-dom';
|
||||||
import ErrorPage from 'app/core/components/ErrorPage/ErrorPage';
|
import ErrorPage from 'app/core/components/ErrorPage/ErrorPage';
|
||||||
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
|
|
||||||
export const extraRoutes: RouteDescriptor[] = [];
|
export const extraRoutes: RouteDescriptor[] = [];
|
||||||
|
|
||||||
@@ -135,7 +136,11 @@ export function getAppRoutes(): RouteDescriptor[] {
|
|||||||
{
|
{
|
||||||
path: '/explore',
|
path: '/explore',
|
||||||
pageClass: 'page-explore',
|
pageClass: 'page-explore',
|
||||||
roles: () => (config.viewersCanEdit ? [] : ['Editor', 'Admin']),
|
roles: () =>
|
||||||
|
evaluatePermission(
|
||||||
|
() => (config.viewersCanEdit ? [] : ['Editor', 'Admin']),
|
||||||
|
AccessControlAction.DataSourcesExplore
|
||||||
|
),
|
||||||
component: SafeDynamicImport(() => import(/* webpackChunkName: "explore" */ 'app/features/explore/Wrapper')),
|
component: SafeDynamicImport(() => import(/* webpackChunkName: "explore" */ 'app/features/explore/Wrapper')),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -515,3 +520,16 @@ export function getAppRoutes(): RouteDescriptor[] {
|
|||||||
// ...playlistRoutes,
|
// ...playlistRoutes,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// evaluates access control permission, using fallback if access control is disabled
|
||||||
|
const evaluatePermission = (fallback: () => string[], action: AccessControlAction): string[] => {
|
||||||
|
if (!config.featureToggles['accesscontrol']) {
|
||||||
|
return fallback();
|
||||||
|
}
|
||||||
|
if (contextSrv.hasPermission(action)) {
|
||||||
|
return [];
|
||||||
|
} else {
|
||||||
|
// Hack to reject when user does not have permission
|
||||||
|
return ['Reject'];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@@ -33,4 +33,5 @@ export enum AccessControlAction {
|
|||||||
LDAPUsersRead = 'ldap.user:read',
|
LDAPUsersRead = 'ldap.user:read',
|
||||||
LDAPUsersSync = 'ldap.user:sync',
|
LDAPUsersSync = 'ldap.user:sync',
|
||||||
LDAPStatusRead = 'ldap.status:read',
|
LDAPStatusRead = 'ldap.status:read',
|
||||||
|
DataSourcesExplore = 'datasources:explore',
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user