From 8833fe739d333223ac2dc61a25ca5ceac8fe4814 Mon Sep 17 00:00:00 2001 From: Thom Heymann <190132+thomheymann@users.noreply.github.com> Date: Sun, 30 Oct 2022 22:40:25 +0000 Subject: [PATCH] Add readonly view to role management (#143893) --- .../roles/edit_role/edit_role_page.test.tsx | 260 +++++++++++++----- .../roles/edit_role/edit_role_page.tsx | 26 +- .../elasticsearch_privileges.test.tsx.snap | 2 + .../privileges/es/cluster_privileges.test.tsx | 22 ++ .../privileges/es/cluster_privileges.tsx | 9 +- .../es/elasticsearch_privileges.test.tsx | 7 + .../es/elasticsearch_privileges.tsx | 41 +-- .../privileges/es/index_privileges.test.tsx | 46 ++++ .../privileges/es/index_privileges.tsx | 7 +- .../space_aware_privilege_section.test.tsx | 7 + .../space_aware_privilege_section.tsx | 31 ++- .../roles/roles_grid/roles_grid_page.test.tsx | 18 ++ .../roles/roles_grid/roles_grid_page.tsx | 65 +++-- .../roles/roles_management_app.test.tsx | 14 +- .../management/roles/roles_management_app.tsx | 8 + .../users/users_grid/users_grid_page.tsx | 1 + .../server/features/security_features.ts | 4 + 17 files changed, 421 insertions(+), 147 deletions(-) diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx index 429d4f1c925d2..e6d6403460e50 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx @@ -13,6 +13,7 @@ import type { Capabilities } from '@kbn/core/public'; import { coreMock, scopedHistoryMock } from '@kbn/core/public/mocks'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { KibanaFeature } from '@kbn/features-plugin/public'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import type { Space } from '@kbn/spaces-plugin/public'; import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; @@ -186,20 +187,63 @@ function getProps({ } describe('', () => { + const coreStart = coreMock.createStart(); + + beforeEach(() => { + coreStart.application.capabilities = { + ...coreStart.application.capabilities, + roles: { + save: true, + }, + }; + }); + describe('with spaces enabled', () => { + it('can render readonly view when not enough privileges', async () => { + coreStart.application.capabilities = { + ...coreStart.application.capabilities, + roles: { + save: false, + }, + }; + + const wrapper = mountWithIntl( + + + + ); + + await waitForRender(wrapper); + + expect(wrapper.find('input[data-test-subj="roleFormNameInput"]').prop('disabled')).toBe(true); + expectReadOnlyFormButtons(wrapper); + }); + it('can render a reserved role', async () => { const wrapper = mountWithIntl( - + + + ); await waitForRender(wrapper); @@ -207,22 +251,25 @@ describe('', () => { expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(1); expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expect(wrapper.find('input[data-test-subj="roleFormNameInput"]').prop('disabled')).toBe(true); expectReadOnlyFormButtons(wrapper); }); it('can render a user defined role', async () => { const wrapper = mountWithIntl( - + + + ); await waitForRender(wrapper); @@ -230,65 +277,100 @@ describe('', () => { expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expect(wrapper.find('input[data-test-subj="roleFormNameInput"]').prop('disabled')).toBe(true); expectSaveFormButtons(wrapper); }); it('can render when creating a new role', async () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + + + ); await waitForRender(wrapper); expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expect(wrapper.find('input[data-test-subj="roleFormNameInput"]').prop('disabled')).toBe( + false + ); expectSaveFormButtons(wrapper); }); + it('redirects back to roles when creating a new role without privileges', async () => { + coreStart.application.capabilities = { + ...coreStart.application.capabilities, + roles: { + save: false, + }, + }; + + const props = getProps({ action: 'edit' }); + const wrapper = mountWithIntl( + + + + ); + + await waitForRender(wrapper); + + expect(props.history.push).toHaveBeenCalledWith('/'); + }); + it('can render when cloning an existing role', async () => { const wrapper = mountWithIntl( - + + })} + /> + ); await waitForRender(wrapper); expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expect(wrapper.find('input[data-test-subj="roleFormNameInput"]').prop('disabled')).toBe( + false + ); expectSaveFormButtons(wrapper); }); it('renders an auth error when not authorized to manage spaces', async () => { const wrapper = mountWithIntl( - + + + ); await waitForRender(wrapper); @@ -305,19 +387,21 @@ describe('', () => { it('renders a partial read-only view when there is a transform error', async () => { const wrapper = mountWithIntl( - + + + ); await waitForRender(wrapper); @@ -331,7 +415,11 @@ describe('', () => { const error = { response: { status: 500 } }; const getFeatures = jest.fn().mockRejectedValue(error); const props = getProps({ action: 'edit' }); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + + + ); await waitForRender(wrapper); expect(props.fatalErrors.add).toHaveBeenLastCalledWith(error); @@ -342,7 +430,11 @@ describe('', () => { const error = { response: { status: 403 } }; const getFeatures = jest.fn().mockRejectedValue(error); const props = getProps({ action: 'edit' }); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + + + ); await waitForRender(wrapper); expect(props.fatalErrors.add).not.toHaveBeenCalled(); @@ -356,7 +448,9 @@ describe('', () => { dataViews.getTitles = jest.fn().mockRejectedValue({ response: { status: 403 } }); const wrapper = mountWithIntl( - + + + ); await waitForRender(wrapper); @@ -369,7 +463,11 @@ describe('', () => { describe('in create mode', () => { it('renders an error for existing role name', async () => { const props = getProps({ action: 'edit' }); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + + + ); await waitForRender(wrapper); @@ -389,7 +487,11 @@ describe('', () => { it('renders an error on save of existing role name', async () => { const props = getProps({ action: 'edit' }); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + + + ); props.rolesAPIClient.saveRole.mockRejectedValue({ body: { @@ -420,7 +522,11 @@ describe('', () => { it('does not render an error for new role name', async () => { const props = getProps({ action: 'edit' }); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + + + ); props.rolesAPIClient.getRole.mockRejectedValue(new Error('not found')); @@ -440,7 +546,11 @@ describe('', () => { it('does not render a notification on save of new role name', async () => { const props = getProps({ action: 'edit' }); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + + + ); props.rolesAPIClient.getRole.mockRejectedValue(new Error('not found')); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx index 9b35c7b1558f8..36d6815493c98 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx @@ -55,6 +55,7 @@ import { getExtendedRoleDeprecationNotice, prepareRoleClone, } from '../../../../common/model'; +import { useCapabilities } from '../../../components/use_capabilities'; import type { UserAPIClient } from '../../users'; import type { IndicesAPIClient } from '../indices_api_client'; import { KibanaPrivileges } from '../model'; @@ -124,7 +125,6 @@ function useIndexPatternsTitles( } fatalErrors.add(err); - throw err; }) .then((titles) => setIndexPatternsTitles(titles.filter(Boolean))); }, [fatalErrors, dataViews, notifications]); @@ -297,6 +297,7 @@ export const EditRolePage: FunctionComponent = ({ throw new Error('The dataViews plugin is required for this page, but it is not available'); } const backToRoleList = useCallback(() => history.push('/'), [history]); + const hasReadOnlyPrivileges = !useCapabilities('roles').save; // We should keep the same mutable instance of Validator for every re-render since we'll // eventually enable validation after the first time user tries to save a role. @@ -319,12 +320,19 @@ export const EditRolePage: FunctionComponent = ({ roleName ); + const isEditingExistingRole = !!roleName && action === 'edit'; + + useEffect(() => { + if (hasReadOnlyPrivileges && !isEditingExistingRole) { + backToRoleList(); + } + }, [hasReadOnlyPrivileges, isEditingExistingRole]); // eslint-disable-line react-hooks/exhaustive-deps + if (!role || !runAsUsers || !indexPatternsTitles || !privileges || !spaces || !features) { return null; } - const isEditingExistingRole = !!roleName && action === 'edit'; - const isRoleReadOnly = checkIfRoleReadOnly(role); + const isRoleReadOnly = hasReadOnlyPrivileges || checkIfRoleReadOnly(role); const isRoleReserved = checkIfRoleReserved(role); const isDeprecatedRole = checkIfRoleDeprecated(role); @@ -335,7 +343,7 @@ export const EditRolePage: FunctionComponent = ({ const props: HTMLProps = { tabIndex: 0, }; - if (isRoleReserved) { + if (isRoleReserved || isRoleReadOnly) { titleText = ( = ({ onChange={onNameChange} onBlur={onNameBlur} data-test-subj={'roleFormNameInput'} - readOnly={isRoleReserved || isEditingExistingRole} + disabled={isRoleReserved || isEditingExistingRole || isRoleReadOnly} isInvalid={creatingRoleAlreadyExists} /> @@ -491,10 +499,14 @@ export const EditRolePage: FunctionComponent = ({ const getReturnToRoleListButton = () => { return ( - + ); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap index 2c47d70b94100..e7cfef55274c0 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap @@ -52,6 +52,7 @@ exports[`it renders without crashing 1`] = ` "monitor", ] } + editable={true} onChange={[Function]} role={ Object { @@ -170,6 +171,7 @@ exports[`it renders without crashing 1`] = ` "index", ] } + editable={true} indexPatterns={Array []} indicesAPIClient={ Object { diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/cluster_privileges.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/cluster_privileges.test.tsx index 816e3f43c9ddc..b9b653e108738 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/cluster_privileges.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/cluster_privileges.test.tsx @@ -34,6 +34,28 @@ test('it renders without crashing', () => { expect(wrapper).toMatchSnapshot(); }); +test('it renders fields as disabled when not editable', () => { + const role: Role = { + name: '', + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + }; + + const wrapper = shallow( + + ); + expect(wrapper.find('EuiComboBox').prop('isDisabled')).toBe(true); +}); + test('it allows for custom cluster privileges', () => { const role: Role = { name: '', diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/cluster_privileges.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/cluster_privileges.tsx index 6d2643c4b6998..0e44970d8ef7a 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/cluster_privileges.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/cluster_privileges.tsx @@ -16,16 +16,21 @@ interface Props { role: Role; builtinClusterPrivileges: string[]; onChange: (privs: string[]) => void; + editable?: boolean; } export class ClusterPrivileges extends Component { + static defaultProps: Partial = { + editable: true, + }; + public render() { const availableClusterPrivileges = this.getAvailableClusterPrivileges(); return {this.buildComboBox(availableClusterPrivileges)}; } public buildComboBox = (items: string[]) => { - const role = this.props.role; + const { role, editable } = this.props; const options = items.map((i) => ({ label: i, @@ -41,7 +46,7 @@ export class ClusterPrivileges extends Component { selectedOptions={selectedOptions} onChange={this.onClusterPrivilegesChange} onCreateOption={this.onCreateCustomPrivilege} - isDisabled={isRoleReadOnly(role)} + isDisabled={isRoleReadOnly(role) || !editable} /> ); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.test.tsx index 717700cc82278..52feba5ae83ad 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.test.tsx @@ -66,3 +66,10 @@ test('it renders IndexPrivileges', () => { mountWithIntl().find(IndexPrivileges) ).toHaveLength(1); }); + +test('it renders fields as disabled when not editable', () => { + const wrapper = shallowWithIntl(); + expect(wrapper.find('EuiComboBox').prop('isDisabled')).toBe(true); + expect(wrapper.find('ClusterPrivileges').prop('editable')).toBe(false); + expect(wrapper.find('IndexPrivileges').prop('editable')).toBe(false); +}); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.tsx index 55b89d8832c75..045c4d924c426 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.tsx @@ -66,16 +66,6 @@ export class ElasticsearchPrivileges extends Component { builtinESPrivileges, } = this.props; - const indexProps = { - role, - indicesAPIClient, - validator, - indexPatterns, - license, - onChange, - availableIndexPrivileges: builtinESPrivileges.index, - }; - return ( { role={this.props.role} onChange={this.onClusterPrivilegesChange} builtinClusterPrivileges={builtinESPrivileges.cluster} + editable={editable} /> @@ -171,17 +162,27 @@ export class ElasticsearchPrivileges extends Component {

- - - + - {this.props.editable && ( - - - + {editable && ( + <> + + + + + )}
); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.test.tsx index cdc8ac71fd38d..91f761d1157fa 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.test.tsx @@ -100,3 +100,49 @@ test('it renders a IndexPrivilegeForm for each privilege on the role', async () await flushPromises(); expect(wrapper.find(IndexPrivilegeForm)).toHaveLength(1); }); + +test('it renders fields as disabled when not editable', async () => { + const license = licenseMock.create(); + license.getFeatures.mockReturnValue({ + allowRoleFieldLevelSecurity: true, + allowRoleDocumentLevelSecurity: true, + } as any); + + const indicesAPIClient = indicesAPIClientMock.create(); + indicesAPIClient.getFields.mockResolvedValue(['foo']); + + const props = { + role: { + name: '', + kibana: [], + elasticsearch: { + cluster: [], + indices: [ + { + names: ['foo*'], + privileges: ['all'], + query: '*', + field_security: { + grant: ['some_field'], + }, + }, + ], + run_as: [], + }, + }, + onChange: jest.fn(), + indexPatterns: [], + editable: false, + validator: new RoleValidator(), + availableIndexPrivileges: ['all', 'read', 'write', 'index'], + indicesAPIClient, + license, + }; + const wrapper = mountWithIntl( + + + + ); + await flushPromises(); + expect(wrapper.find('IndexPrivilegeForm').prop('isRoleReadOnly')).toBe(true); +}); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.tsx index d761992c275c9..8c8eafa6d6a2b 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.tsx @@ -24,6 +24,7 @@ interface Props { license: SecurityLicense; onChange: (role: Role) => void; validator: RoleValidator; + editable?: boolean; } interface State { @@ -33,6 +34,10 @@ interface State { } export class IndexPrivileges extends Component { + static defaultProps: Partial = { + editable: true, + }; + constructor(props: Props) { super(props); this.state = { @@ -54,7 +59,7 @@ export class IndexPrivileges extends Component { // doesn't permit FLS/DLS). allowDocumentLevelSecurity: allowRoleDocumentLevelSecurity || !isRoleEnabled(this.props.role), allowFieldLevelSecurity: allowRoleFieldLevelSecurity || !isRoleEnabled(this.props.role), - isRoleReadOnly: isRoleReadOnly(this.props.role), + isRoleReadOnly: !this.props.editable || isRoleReadOnly(this.props.role), }; const forms = indices.map((indexPrivilege: RoleIndexPrivilege, idx) => ( diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx index beaaf783f2f4d..8152a21bd931c 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx @@ -87,6 +87,13 @@ describe('', () => { expect(table).toHaveLength(0); }); + it('hides "Add space privilege" button if not editable', () => { + const props = buildProps(); + + const wrapper = mountWithIntl(); + expect(wrapper.find('button[data-test-subj="addSpacePrivilegeButton"]')).toHaveLength(0); + }); + it('Renders flyout after clicking "Add space privilege" button', () => { const props = buildProps({ role: { diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx index 84ba0c7020efb..b01f71e9aa4de 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx @@ -180,7 +180,7 @@ export class SpaceAwarePrivilegeSection extends Component { /> } - titleSize={'s'} + titleSize="xs" actions={this.getAvailablePrivilegeButtons(false)} /> ); @@ -194,20 +194,21 @@ export class SpaceAwarePrivilegeSection extends Component { return null; } - const addPrivilegeButton = ( - - - - ); + const addPrivilegeButton = + !hasAvailableSpaces || !this.props.editable ? null : ( + + + + ); if (!hasPrivilegesAssigned) { return addPrivilegeButton; diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx index ac0ce12aac3d9..57a3a5aa8ad03 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx @@ -234,4 +234,22 @@ describe('', () => { }, ]); }); + + it('hides controls when `readOnly` is enabled', async () => { + const wrapper = mountWithIntl( + + ); + const initialIconCount = wrapper.find(EuiIcon).length; + + await waitForRender(wrapper, (updatedWrapper) => { + return updatedWrapper.find(EuiIcon).length > initialIconCount; + }); + + expect(findTestSubject(wrapper, 'createRoleButton')).toHaveLength(0); + }); }); diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx index 89b7dea7cad1e..8b5631328f6f7 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx @@ -46,6 +46,7 @@ interface Props { notifications: NotificationsStart; rolesAPIClient: PublicMethodsOf; history: ScopedHistory; + readOnly?: boolean; } interface State { @@ -63,6 +64,10 @@ const getRoleManagementHref = (action: 'edit' | 'clone', roleName?: string) => { }; export class RolesGridPage extends Component { + static defaultProps: Partial = { + readOnly: false, + }; + private tableRef: React.RefObject>; constructor(props: Props) { super(props); @@ -106,19 +111,23 @@ export class RolesGridPage extends Component { defaultMessage="Apply roles to groups of users and manage permissions across the stack." /> } - rightSideItems={[ - - - , - ]} + rightSideItems={ + this.props.readOnly + ? undefined + : [ + + + , + ] + } /> @@ -139,11 +148,16 @@ export class RolesGridPage extends Component { responsive={false} columns={this.getColumnConfig()} hasActions={true} - selection={{ - selectable: (role: Role) => !role.metadata || !role.metadata._reserved, - selectableMessage: (selectable: boolean) => (!selectable ? 'Role is reserved' : ''), - onSelectionChange: (selection: Role[]) => this.setState({ selection }), - }} + selection={ + this.props.readOnly + ? undefined + : { + selectable: (role: Role) => !role.metadata || !role.metadata._reserved, + selectableMessage: (selectable: boolean) => + !selectable ? 'Role is reserved' : '', + onSelectionChange: (selection: Role[]) => this.setState({ selection }), + } + } pagination={{ initialPageSize: 20, pageSizeOptions: [10, 20, 30, 50, 100], @@ -188,7 +202,7 @@ export class RolesGridPage extends Component { }; private getColumnConfig = () => { - return [ + const config: Array> = [ { field: 'name', name: i18n.translate('xpack.security.management.roles.nameColumnName', { @@ -218,7 +232,10 @@ export class RolesGridPage extends Component { return this.getRoleStatusBadges(record); }, }, - { + ]; + + if (!this.props.readOnly) { + config.push({ name: i18n.translate('xpack.security.management.roles.actionsColumnName', { defaultMessage: 'Actions', }), @@ -289,7 +306,7 @@ export class RolesGridPage extends Component { }, { available: (role: Role) => !isRoleReadOnly(role), - enable: () => this.state.selection.length === 0, + enabled: () => this.state.selection.length === 0, isPrimary: true, render: (role: Role) => { const title = i18n.translate('xpack.security.management.roles.editRoleActionName', { @@ -321,8 +338,10 @@ export class RolesGridPage extends Component { }, }, ], - }, - ] as Array>; + }); + } + + return config; }; private getVisibleRoles = (roles: Role[], filter: string, includeReservedRoles: boolean) => { diff --git a/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx b/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx index a6e06351f38c9..f1536631a66e7 100644 --- a/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx @@ -32,6 +32,12 @@ async function mountApp(basePath: string, pathname: string) { const featuresStart = featuresPluginMock.createStart(); const coreStart = coreMock.createStart(); + coreStart.application.capabilities = { + ...coreStart.application.capabilities, + roles: { + save: true, + }, + }; let unmount: Unmount = noop; await act(async () => { @@ -84,7 +90,7 @@ describe('rolesManagementApp', () => { expect(docTitle.reset).not.toHaveBeenCalled(); expect(container).toMatchInlineSnapshot(`
- Roles Page: {"notifications":{"toasts":{}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/","search":"","hash":""}}} + Roles Page: {"notifications":{"toasts":{}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/","search":"","hash":""}},"readOnly":false}
`); @@ -106,7 +112,7 @@ describe('rolesManagementApp', () => { expect(docTitle.reset).not.toHaveBeenCalled(); expect(container).toMatchInlineSnapshot(`
- Role Edit Page: {"action":"edit","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"fieldCache":{},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{}},"docLinks":{},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit","search":"","hash":""}}} + Role Edit Page: {"action":"edit","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"fieldCache":{},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{}},"docLinks":{},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{},"roles":{"save":true}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit","search":"","hash":""}}}
`); @@ -133,7 +139,7 @@ describe('rolesManagementApp', () => { expect(docTitle.reset).not.toHaveBeenCalled(); expect(container).toMatchInlineSnapshot(`
- Role Edit Page: {"action":"edit","roleName":"role@name","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"fieldCache":{},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{}},"docLinks":{},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/role@name","search":"","hash":""}}} + Role Edit Page: {"action":"edit","roleName":"role@name","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"fieldCache":{},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{}},"docLinks":{},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{},"roles":{"save":true}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/role@name","search":"","hash":""}}}
`); @@ -160,7 +166,7 @@ describe('rolesManagementApp', () => { expect(docTitle.reset).not.toHaveBeenCalled(); expect(container).toMatchInlineSnapshot(`
- Role Edit Page: {"action":"clone","roleName":"someRoleName","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"fieldCache":{},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{}},"docLinks":{},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/clone/someRoleName","search":"","hash":""}}} + Role Edit Page: {"action":"clone","roleName":"someRoleName","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"fieldCache":{},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{}},"docLinks":{},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{},"roles":{"save":true}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/clone/someRoleName","search":"","hash":""}}}
`); diff --git a/x-pack/plugins/security/public/management/roles/roles_management_app.tsx b/x-pack/plugins/security/public/management/roles/roles_management_app.tsx index e0939d5cbf48b..30ddeb590042a 100644 --- a/x-pack/plugins/security/public/management/roles/roles_management_app.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_management_app.tsx @@ -21,6 +21,7 @@ import { createBreadcrumbsChangeHandler, } from '../../components/breadcrumb'; import type { PluginStartDependencies } from '../../plugin'; +import { ReadonlyBadge } from '../badges/readonly_badge'; import { tryDecodeURIComponent } from '../url_utils'; interface CreateParams { @@ -118,6 +119,12 @@ export const rolesManagementApp = Object.freeze({ + @@ -127,6 +134,7 @@ export const rolesManagementApp = Object.freeze({ notifications={notifications} rolesAPIClient={rolesAPIClient} history={history} + readOnly={!startServices.application.capabilities.roles.save} /> diff --git a/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx index 0649de83749cd..fa4fb04099b72 100644 --- a/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx +++ b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx @@ -74,6 +74,7 @@ export class UsersGridPage extends Component { isTableLoading: false, }; } + public componentDidMount() { this.loadUsersAndRoles(); } diff --git a/x-pack/plugins/security/server/features/security_features.ts b/x-pack/plugins/security/server/features/security_features.ts index 396f2d1640e1f..46184a845b66c 100644 --- a/x-pack/plugins/security/server/features/security_features.ts +++ b/x-pack/plugins/security/server/features/security_features.ts @@ -34,6 +34,10 @@ const rolesManagementFeature: ElasticsearchFeatureConfig = { privileges: [ { requiredClusterPrivileges: ['manage_security'], + ui: ['save'], + }, + { + requiredClusterPrivileges: ['read_security'], ui: [], }, ],