mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Access control: Dashboard and folder permissions frontend (#45094)
This commit is contained in:
parent
4982ca3b1d
commit
7966e3583e
@ -269,7 +269,7 @@ exports[`no enzyme tests`] = {
|
||||
"public/app/features/explore/TimeSyncButton.test.tsx:4230066214": [
|
||||
[2, 17, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"public/app/features/folders/FolderSettingsPage.test.tsx:1751147194": [
|
||||
"public/app/features/folders/FolderSettingsPage.test.tsx:3884290298": [
|
||||
[2, 19, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"public/app/features/invites/InviteesTable.test.tsx:3077684439": [
|
||||
|
@ -7,7 +7,7 @@ import { selectors } from '@grafana/e2e-selectors';
|
||||
import appEvents from '../../app_events';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { createFolder, getFolderById, searchFolders } from 'app/features/manage-dashboards/state/actions';
|
||||
import { PermissionLevelString } from '../../../types';
|
||||
import { AccessControlAction, PermissionLevelString } from '../../../types';
|
||||
|
||||
export interface Props {
|
||||
onChange: ($folder: { title: string; id: number }) => void;
|
||||
@ -81,7 +81,12 @@ export class FolderPicker extends PureComponent<Props, State> {
|
||||
const searchHits = await searchFolders(query, permissionLevel);
|
||||
|
||||
const options: Array<SelectableValue<number>> = searchHits.map((hit) => ({ label: hit.title, value: hit.id }));
|
||||
if (contextSrv.isEditor && rootName?.toLowerCase().startsWith(query.toLowerCase()) && showRoot) {
|
||||
|
||||
const hasAccess =
|
||||
contextSrv.hasAccess(AccessControlAction.DashboardsWrite, contextSrv.isEditor) ||
|
||||
contextSrv.hasAccess(AccessControlAction.DashboardsCreate, contextSrv.isEditor);
|
||||
|
||||
if (hasAccess && rootName?.toLowerCase().startsWith(query.toLowerCase()) && showRoot) {
|
||||
options.unshift({ label: rootName, value: 0 });
|
||||
}
|
||||
|
||||
|
@ -200,7 +200,7 @@ class DashNav extends PureComponent<Props> {
|
||||
|
||||
renderRightActionsButton() {
|
||||
const { dashboard, onAddPanel, isFullscreen, kioskMode } = this.props;
|
||||
const { canEdit, showSettings } = dashboard.meta;
|
||||
const { canSave, canEdit, showSettings } = dashboard.meta;
|
||||
const { snapshot } = dashboard;
|
||||
const snapshotUrl = snapshot && snapshot.originalUrl;
|
||||
const buttons: ReactNode[] = [];
|
||||
@ -218,6 +218,9 @@ class DashNav extends PureComponent<Props> {
|
||||
|
||||
if (canEdit && !isFullscreen) {
|
||||
buttons.push(<ToolbarButton tooltip="Add panel" icon="panel-add" onClick={onAddPanel} key="button-panel-add" />);
|
||||
}
|
||||
|
||||
if (canSave && !isFullscreen) {
|
||||
buttons.push(
|
||||
<ModalsController key="button-save">
|
||||
{({ showModal, hideModal }) => (
|
||||
|
@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { DashboardModel } from '../../state';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { Permissions } from 'app/core/components/AccessControl';
|
||||
|
||||
interface Props {
|
||||
dashboard: DashboardModel;
|
||||
}
|
||||
|
||||
export const AccessControlDashboardPermissions = ({ dashboard }: Props) => {
|
||||
const canListUsers = contextSrv.hasPermission(AccessControlAction.OrgUsersRead);
|
||||
const canSetPermissions = contextSrv.hasPermission(AccessControlAction.DashboardsPermissionsWrite);
|
||||
|
||||
return (
|
||||
<Permissions
|
||||
resource={'dashboards'}
|
||||
resourceId={dashboard.uid}
|
||||
canListUsers={canListUsers}
|
||||
canSetPermissions={canSetPermissions}
|
||||
/>
|
||||
);
|
||||
};
|
@ -9,6 +9,7 @@ import { DashboardModel } from '../../state/DashboardModel';
|
||||
import { SaveDashboardAsButton, SaveDashboardButton } from '../SaveDashboard/SaveDashboardButton';
|
||||
import { VariableEditorContainer } from '../../../variables/editor/VariableEditorContainer';
|
||||
import { DashboardPermissions } from '../DashboardPermissions/DashboardPermissions';
|
||||
import { AccessControlDashboardPermissions } from '../DashboardPermissions/AccessControlDashboardPermissions';
|
||||
import { GeneralSettings } from './GeneralSettings';
|
||||
import { AnnotationsSettings } from './AnnotationsSettings';
|
||||
import { LinksSettings } from './LinksSettings';
|
||||
@ -16,6 +17,7 @@ import { VersionsSettings } from './VersionsSettings';
|
||||
import { JsonEditorSettings } from './JsonEditorSettings';
|
||||
import { GrafanaTheme2, locationUtil } from '@grafana/data';
|
||||
import { locationService, reportInteraction } from '@grafana/runtime';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
|
||||
export interface Props {
|
||||
dashboard: DashboardModel;
|
||||
@ -100,12 +102,21 @@ export function DashboardSettings({ dashboard, editview }: Props) {
|
||||
}
|
||||
|
||||
if (dashboard.id && dashboard.meta.canAdmin) {
|
||||
pages.push({
|
||||
title: 'Permissions',
|
||||
id: 'permissions',
|
||||
icon: 'lock',
|
||||
component: <DashboardPermissions dashboard={dashboard} />,
|
||||
});
|
||||
if (!config.featureToggles['accesscontrol']) {
|
||||
pages.push({
|
||||
title: 'Permissions',
|
||||
id: 'permissions',
|
||||
icon: 'lock',
|
||||
component: <DashboardPermissions dashboard={dashboard} />,
|
||||
});
|
||||
} else if (contextSrv.hasPermission(AccessControlAction.DashboardsPermissionsRead)) {
|
||||
pages.push({
|
||||
title: 'Permissions',
|
||||
id: 'permissions',
|
||||
icon: 'lock',
|
||||
component: <AccessControlDashboardPermissions dashboard={dashboard} />,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pages.push({
|
||||
|
@ -151,7 +151,7 @@ export function GeneralSettingsUnconnected({ dashboard, updateTimeZone, updateWe
|
||||
</CollapsableSection>
|
||||
|
||||
<div className="gf-form-button-row">
|
||||
{dashboard.meta.canSave && <DeleteDashboardButton dashboard={dashboard} />}
|
||||
{dashboard.meta.canDelete && <DeleteDashboardButton dashboard={dashboard} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -3,19 +3,23 @@ import useAsyncFn from 'react-use/lib/useAsyncFn';
|
||||
import { locationUtil } from '@grafana/data';
|
||||
import { SaveDashboardOptions } from './types';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||
import { DashboardModel } from 'app/features/dashboard/state';
|
||||
import { saveDashboard as saveDashboardApiCall } from 'app/features/manage-dashboards/state/actions';
|
||||
import { locationService, reportInteraction } from '@grafana/runtime';
|
||||
import { DashboardSavedEvent } from 'app/types/events';
|
||||
|
||||
const saveDashboard = (saveModel: any, options: SaveDashboardOptions, dashboard: DashboardModel) => {
|
||||
const saveDashboard = async (saveModel: any, options: SaveDashboardOptions, dashboard: DashboardModel) => {
|
||||
let folderId = options.folderId;
|
||||
if (folderId === undefined) {
|
||||
folderId = dashboard.meta.folderId ?? saveModel.folderId;
|
||||
}
|
||||
|
||||
return saveDashboardApiCall({ ...options, folderId, dashboard: saveModel });
|
||||
const result = await saveDashboardApiCall({ ...options, folderId, dashboard: saveModel });
|
||||
// fetch updated access control permissions
|
||||
await contextSrv.fetchUserPermissions();
|
||||
return result;
|
||||
};
|
||||
|
||||
export const useDashboardSave = (dashboard: DashboardModel) => {
|
||||
|
@ -223,7 +223,9 @@ export class DashboardModel implements TimeModel {
|
||||
meta.canSave = meta.canSave !== false;
|
||||
meta.canStar = meta.canStar !== false;
|
||||
meta.canEdit = meta.canEdit !== false;
|
||||
meta.showSettings = meta.canEdit;
|
||||
meta.canDelete = meta.canDelete !== false;
|
||||
|
||||
meta.showSettings = meta.canSave;
|
||||
meta.canMakeEditable = meta.canSave && !this.editable;
|
||||
meta.hasUnsavedFolderChange = false;
|
||||
|
||||
|
@ -0,0 +1,52 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Permissions } from 'app/core/components/AccessControl';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
|
||||
import Page from 'app/core/components/Page/Page';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { getLoadingNav } from './state/navModel';
|
||||
import { AccessControlAction, StoreState } from 'app/types';
|
||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||
import { getFolderByUid } from './state/actions';
|
||||
|
||||
interface RouteProps extends GrafanaRouteComponentProps<{ uid: string }> {}
|
||||
|
||||
function mapStateToProps(state: StoreState, props: RouteProps) {
|
||||
const uid = props.match.params.uid;
|
||||
return {
|
||||
uid: uid,
|
||||
navModel: getNavModel(state.navIndex, `folder-permissions-${uid}`, getLoadingNav(1)),
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
getFolderByUid,
|
||||
};
|
||||
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
export type Props = ConnectedProps<typeof connector>;
|
||||
|
||||
export const AccessControlFolderPermissions = ({ uid, getFolderByUid, navModel }: Props) => {
|
||||
useEffect(() => {
|
||||
getFolderByUid(uid);
|
||||
}, [getFolderByUid, uid]);
|
||||
|
||||
const canListUsers = contextSrv.hasPermission(AccessControlAction.OrgUsersRead);
|
||||
const canSetPermissions = contextSrv.hasPermission(AccessControlAction.FoldersPermissionsWrite);
|
||||
|
||||
return (
|
||||
<Page navModel={navModel}>
|
||||
<Page.Contents>
|
||||
<Permissions
|
||||
resource="folders"
|
||||
resourceId={uid}
|
||||
canListUsers={canListUsers}
|
||||
canSetPermissions={canSetPermissions}
|
||||
/>
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default connector(AccessControlFolderPermissions);
|
@ -16,6 +16,7 @@ const setup = (propOverrides?: object) => {
|
||||
uid: '1234',
|
||||
title: 'loading',
|
||||
canSave: true,
|
||||
canDelete: true,
|
||||
url: 'url',
|
||||
hasChanged: false,
|
||||
version: 1,
|
||||
@ -52,6 +53,7 @@ describe('Render', () => {
|
||||
uid: '1234',
|
||||
title: 'loading',
|
||||
canSave: true,
|
||||
canDelete: true,
|
||||
hasChanged: true,
|
||||
version: 1,
|
||||
},
|
||||
|
@ -103,7 +103,7 @@ export class FolderSettingsPage extends PureComponent<Props, State> {
|
||||
<Button type="submit" disabled={!folder.canSave || !folder.hasChanged}>
|
||||
Save
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={this.onDelete} disabled={!folder.canSave}>
|
||||
<Button variant="destructive" onClick={this.onDelete} disabled={!folder.canDelete}>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { locationUtil } from '@grafana/data';
|
||||
import { getBackendSrv, locationService } from '@grafana/runtime';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { FolderState, ThunkResult } from 'app/types';
|
||||
import { DashboardAcl, DashboardAclUpdateDTO, NewDashboardAclItem, PermissionLevel } from 'app/types/acl';
|
||||
@ -143,6 +144,7 @@ export function addFolderPermission(newItem: NewDashboardAclItem): ThunkResult<v
|
||||
export function createNewFolder(folderName: string): ThunkResult<void> {
|
||||
return async (dispatch) => {
|
||||
const newFolder = await getBackendSrv().post('/api/folders', { title: folderName });
|
||||
await contextSrv.fetchUserPermissions();
|
||||
dispatch(notifyApp(createSuccessNotification('Folder Created', 'OK')));
|
||||
locationService.push(locationUtil.stripBaseFromUrl(newFolder.url));
|
||||
};
|
||||
|
@ -61,6 +61,7 @@ export function getLoadingNav(tabIndex: number): NavModel {
|
||||
canSave: true,
|
||||
canEdit: true,
|
||||
canAdmin: true,
|
||||
canDelete: true,
|
||||
version: 0,
|
||||
});
|
||||
|
||||
|
@ -18,6 +18,7 @@ function getTestFolder(): FolderDTO {
|
||||
canSave: true,
|
||||
canEdit: true,
|
||||
canAdmin: true,
|
||||
canDelete: true,
|
||||
version: 0,
|
||||
};
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ export const initialState: FolderState = {
|
||||
title: 'loading',
|
||||
url: '',
|
||||
canSave: false,
|
||||
canDelete: false,
|
||||
hasChanged: false,
|
||||
version: 1,
|
||||
permissions: [],
|
||||
|
@ -124,9 +124,15 @@ export function getAppRoutes(): RouteDescriptor[] {
|
||||
},
|
||||
{
|
||||
path: '/dashboards/f/:uid/:slug/permissions',
|
||||
component: SafeDynamicImport(
|
||||
() => import(/* webpackChunkName: "FolderPermissions"*/ 'app/features/folders/FolderPermissions')
|
||||
),
|
||||
component:
|
||||
config.featureToggles['accesscontrol'] && contextSrv.hasPermission(AccessControlAction.FoldersPermissionsRead)
|
||||
? SafeDynamicImport(
|
||||
() =>
|
||||
import(/* webpackChunkName: "FolderPermissions"*/ 'app/features/folders/AccessControlFolderPermissions')
|
||||
)
|
||||
: SafeDynamicImport(
|
||||
() => import(/* webpackChunkName: "FolderPermissions"*/ 'app/features/folders/FolderPermissions')
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/dashboards/f/:uid/:slug/settings',
|
||||
|
@ -66,6 +66,20 @@ export enum AccessControlAction {
|
||||
ActionTeamsRolesAdd = 'teams.roles:add',
|
||||
ActionTeamsRolesRemove = 'teams.roles:remove',
|
||||
ActionUserRolesList = 'users.roles:list',
|
||||
|
||||
DashboardsRead = 'dashboards:read',
|
||||
DashboardsWrite = 'dashboards:write',
|
||||
DashboardsDelete = 'dashboards:delete',
|
||||
DashboardsCreate = 'dashboards:create',
|
||||
DashboardsPermissionsRead = 'dashboards.permissions:read',
|
||||
DashboardsPermissionsWrite = 'dashboards.permissions:read',
|
||||
|
||||
FoldersRead = 'folders:read',
|
||||
FoldersWrite = 'folders:read',
|
||||
FoldersDelete = 'folders:delete',
|
||||
FoldersCreate = 'folders:create',
|
||||
FoldersPermissionsRead = 'folders.permissions:read',
|
||||
FoldersPermissionsWrite = 'folders.permissions:read',
|
||||
}
|
||||
|
||||
export interface Role {
|
||||
|
@ -9,6 +9,7 @@ export interface FolderDTO {
|
||||
canSave: boolean;
|
||||
canEdit: boolean;
|
||||
canAdmin: boolean;
|
||||
canDelete: boolean;
|
||||
}
|
||||
|
||||
export interface FolderState {
|
||||
@ -17,6 +18,7 @@ export interface FolderState {
|
||||
title: string;
|
||||
url: string;
|
||||
canSave: boolean;
|
||||
canDelete: boolean;
|
||||
hasChanged: boolean;
|
||||
version: number;
|
||||
permissions: DashboardAcl[];
|
||||
|
Loading…
Reference in New Issue
Block a user