Access control: Dashboard and folder permissions frontend (#45094)

This commit is contained in:
Karl Persson 2022-03-03 15:18:27 +01:00 committed by GitHub
parent 4982ca3b1d
commit 7966e3583e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 147 additions and 18 deletions

View File

@ -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": [

View File

@ -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 });
}

View File

@ -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 }) => (

View File

@ -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}
/>
);
};

View File

@ -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({

View File

@ -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>
);

View File

@ -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) => {

View File

@ -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;

View File

@ -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);

View File

@ -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,
},

View File

@ -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>

View File

@ -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));
};

View File

@ -61,6 +61,7 @@ export function getLoadingNav(tabIndex: number): NavModel {
canSave: true,
canEdit: true,
canAdmin: true,
canDelete: true,
version: 0,
});

View File

@ -18,6 +18,7 @@ function getTestFolder(): FolderDTO {
canSave: true,
canEdit: true,
canAdmin: true,
canDelete: true,
version: 0,
};
}

View File

@ -9,6 +9,7 @@ export const initialState: FolderState = {
title: 'loading',
url: '',
canSave: false,
canDelete: false,
hasChanged: false,
version: 1,
permissions: [],

View File

@ -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',

View File

@ -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 {

View File

@ -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[];