Skip to content

Commit

Permalink
[Roles] Added optional role description (elastic#183145)
Browse files Browse the repository at this point in the history
## Summary

1. Added optional role description field for Save/Edit Role page.
2. Added tooltip with description for roles ComboBox that we render on
the User and Role Mappings pages.
<details>
<summary>3. Updated <b>RolesGridPage </b>table responsive
setup.</summary>
  <br>
<table>
  <tr>
    <td>[Before] responsiveBreakpoint={false}</td>
     <td>[After] responsiveBreakpoint={true}</td>
  </tr>
  <tr>
<td><img alt="Before"
src="https://github.com/elastic/kibana/assets/165678770/d8290299-e8c8-4c00-abee-ad1fe909df1d"></td>
<td><img alt="After"
src="https://github.com/elastic/kibana/assets/165678770/917e2c78-6291-43f0-ab7e-7583c51fd69d"></td>
  </tr>
 </table>

</details>



https://github.com/elastic/kibana/assets/165678770/7035c05b-85c6-4da0-97d3-85f6d2dbc313


### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed.
[Report](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5960)
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)


### For maintainers

- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

__Fixes: https://github.com/elastic/kibana/issues/173570__

## Release note
Added optional role description field for Save/Edit Role page.

---------

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
elena-shostak and kibanamachine authored May 16, 2024
1 parent e53ff44 commit 4fade37
Show file tree
Hide file tree
Showing 12 changed files with 294 additions and 41 deletions.
2 changes: 2 additions & 0 deletions docs/api/role-management/get-all.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ The API returns the following:
[
{
"name": "my_kibana_role",
"description": "My kibana role description",
"metadata" : {
"version" : 1
},
Expand All @@ -55,6 +56,7 @@ The API returns the following:
},
{
"name": "my_admin_role",
"description": "My admin role description",
"metadata" : {
"version" : 1
},
Expand Down
1 change: 1 addition & 0 deletions docs/api/role-management/get.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ The API returns the following:
--------------------------------------------------
{
"name": "my_restricted_kibana_role",
"description": "My restricted kibana role description",
"metadata" : {
"version" : 1
},
Expand Down
8 changes: 8 additions & 0 deletions docs/api/role-management/put.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ To use the create or update role API, you must have the `manage_security` cluste
[[role-management-api-response-body]]
==== Request body

`description`::
(Optional, string) Description for the role.

`metadata`::
(Optional, object) In the `metadata` object, keys that begin with `_` are reserved for system usage.

Expand Down Expand Up @@ -74,6 +77,7 @@ Grant access to various features in all spaces:
--------------------------------------------------
$ curl -X PUT api/security/role/my_kibana_role
{
"description": "my_kibana_role_description",
"metadata": {
"version": 1
},
Expand Down Expand Up @@ -112,6 +116,7 @@ Grant dashboard-only access to only the Marketing space:
--------------------------------------------------
$ curl -X PUT api/security/role/my_kibana_role
{
"description": "Grants dashboard-only access to only the Marketing space.",
"metadata": {
"version": 1
},
Expand All @@ -138,6 +143,7 @@ Grant full access to all features in the Default space:
--------------------------------------------------
$ curl -X PUT api/security/role/my_kibana_role
{
"description": "Grants full access to all features in the Default space.",
"metadata": {
"version": 1
},
Expand All @@ -162,6 +168,7 @@ Grant different access to different spaces:
--------------------------------------------------
$ curl -X PUT api/security/role/my_kibana_role
{
"description": "Grants full access to discover and dashboard features in the default space. Grants read access in the marketing, and sales spaces.",
"metadata": {
"version": 1
},
Expand Down Expand Up @@ -193,6 +200,7 @@ Grant access to {kib} and {es}:
--------------------------------------------------
$ curl -X PUT api/security/role/my_kibana_role
{
"description": "Grants all cluster privileges and full access to index1 and index2. Grants full access to remote_index1 and remote_index2, and the monitor_enrich cluster privilege on remote_cluster1. Grants all Kibana privileges in the default space.",
"metadata": {
"version": 1
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ describe('RoleComboBox', () => {
},
{
name: 'deprecated_role',
description: 'Deprecated role description',
elasticsearch: { cluster: [], indices: [], run_as: [] },
kibana: [],
metadata: { _reserved: true, _deprecated: true },
Expand Down Expand Up @@ -72,6 +73,7 @@ describe('RoleComboBox', () => {
"label": "custom_role",
"value": Object {
"deprecatedReason": undefined,
"description": undefined,
"isAdmin": false,
"isDeprecated": false,
"isReserved": false,
Expand All @@ -89,6 +91,7 @@ describe('RoleComboBox', () => {
"label": "reserved_role",
"value": Object {
"deprecatedReason": undefined,
"description": undefined,
"isAdmin": false,
"isDeprecated": false,
"isReserved": true,
Expand All @@ -106,6 +109,7 @@ describe('RoleComboBox', () => {
"label": "some_admin",
"value": Object {
"deprecatedReason": undefined,
"description": undefined,
"isAdmin": true,
"isDeprecated": false,
"isReserved": true,
Expand All @@ -123,6 +127,7 @@ describe('RoleComboBox', () => {
"label": "some_system",
"value": Object {
"deprecatedReason": undefined,
"description": undefined,
"isAdmin": false,
"isDeprecated": false,
"isReserved": true,
Expand All @@ -140,6 +145,7 @@ describe('RoleComboBox', () => {
"label": "deprecated_role",
"value": Object {
"deprecatedReason": undefined,
"description": "Deprecated role description",
"isAdmin": false,
"isDeprecated": true,
"isReserved": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@
*/

import type { EuiComboBoxOptionOption, EuiComboBoxProps } from '@elastic/eui';
import { EuiBadge, EuiComboBox, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import {
EuiBadge,
EuiComboBox,
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiToolTip,
} from '@elastic/eui';
import React from 'react';

import { i18n } from '@kbn/i18n';
Expand Down Expand Up @@ -34,6 +41,7 @@ type Option = EuiComboBoxOptionOption<{
isSystem: boolean;
isAdmin: boolean;
deprecatedReason?: string;
description?: string;
}>;

export const RoleComboBox = (props: Props) => {
Expand All @@ -57,6 +65,7 @@ export const RoleComboBox = (props: Props) => {
isSystem,
isAdmin,
deprecatedReason: roleDefinition?.metadata?._deprecated_reason,
description: roleDefinition?.description,
},
};
};
Expand Down Expand Up @@ -134,7 +143,15 @@ export const RoleComboBox = (props: Props) => {
function renderOption(option: Option) {
return (
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center" responsive={false}>
<EuiFlexItem>{option.label}</EuiFlexItem>
<EuiFlexItem>
{option.value?.description ? (
<EuiToolTip position="left" content={option.value?.description}>
<EuiText size="s">{option.label}</EuiText>
</EuiToolTip>
) : (
<EuiText size="s">{option.label}</EuiText>
)}
</EuiFlexItem>
{option.value?.isDeprecated ? (
<EuiFlexItem grow={false}>
<EuiBadge color={option.color}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,69 @@ describe('<EditRolePage />', () => {
expectSaveFormButtons(wrapper);
});

it('can render a user defined role with description', async () => {
const wrapper = mountWithIntl(
<KibanaContextProvider services={coreStart}>
<EditRolePage
{...getProps({
action: 'edit',
spacesEnabled: false,
role: {
description: 'my custom role description',
name: 'my custom role',
metadata: {},
elasticsearch: { cluster: ['all'], indices: [], run_as: ['*'] },
kibana: [],
},
})}
/>
</KibanaContextProvider>
);

await waitForRender(wrapper);

expect(wrapper.find('input[data-test-subj="roleFormDescriptionInput"]').prop('value')).toBe(
'my custom role description'
);
expect(
wrapper.find('input[data-test-subj="roleFormDescriptionInput"]').prop('disabled')
).toBe(undefined);
expectSaveFormButtons(wrapper);
});

it('can render a reserved role with description', async () => {
const wrapper = mountWithIntl(
<KibanaContextProvider services={coreStart}>
<EditRolePage
{...getProps({
action: 'edit',
spacesEnabled: false,
role: {
description: 'my reserved role description',
name: 'my custom role',
metadata: {
_reserved: true,
},
elasticsearch: { cluster: ['all'], indices: [], run_as: ['*'] },
kibana: [],
},
})}
/>
</KibanaContextProvider>
);

await waitForRender(wrapper);

expect(wrapper.find('[data-test-subj="roleFormDescriptionTooltip"]')).toHaveLength(1);

expect(wrapper.find('input[data-test-subj="roleFormDescriptionInput"]').prop('value')).toBe(
'my reserved role description'
);
expect(
wrapper.find('input[data-test-subj="roleFormDescriptionInput"]').prop('disabled')
).toBe(true);
});

it('can render when creating a new role', async () => {
const wrapper = mountWithIntl(
<KibanaContextProvider services={coreStart}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
EuiSpacer,
EuiText,
EuiTitle,
EuiToolTip,
} from '@elastic/eui';
import type { ChangeEvent, FocusEvent, FunctionComponent, HTMLProps } from 'react';
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react';
Expand Down Expand Up @@ -211,6 +212,7 @@ function useRole(
? rolesAPIClient.getRole(roleName)
: Promise.resolve({
name: '',
description: '',
elasticsearch: { cluster: [], indices: [], run_as: [], remote_cluster: [] },
kibana: [],
_unrecognized_applications: [],
Expand Down Expand Up @@ -452,45 +454,82 @@ export const EditRolePage: FunctionComponent<Props> = ({
return null;
};

const getRoleName = () => {
const getRoleNameAndDescription = () => {
return (
<EuiPanel hasShadow={false} hasBorder={true}>
<EuiFormRow
data-test-subj={'roleNameFormRow'}
label={
<FormattedMessage
id="xpack.security.management.editRole.roleNameFormRowTitle"
defaultMessage="Role name"
/>
}
helpText={
!isEditingExistingRole ? (
<FormattedMessage
id="xpack.security.management.createRole.roleNameFormRowHelpText"
defaultMessage="Once the role is created you can no longer edit its name."
/>
) : !isRoleReserved ? (
<FormattedMessage
id="xpack.security.management.editRole.roleNameFormRowHelpText"
defaultMessage="A role's name cannot be changed once it has been created."
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow
data-test-subj={'roleNameFormRow'}
label={
<FormattedMessage
id="xpack.security.management.editRole.roleNameFormRowTitle"
defaultMessage="Role name"
/>
}
helpText={
!isEditingExistingRole ? (
<FormattedMessage
id="xpack.security.management.createRole.roleNameFormRowHelpText"
defaultMessage="Once the role is created you can no longer edit its name."
/>
) : !isRoleReserved ? (
<FormattedMessage
id="xpack.security.management.editRole.roleNameFormRowHelpText"
defaultMessage="A role's name cannot be changed once it has been created."
/>
) : undefined
}
{...validator.validateRoleName(role)}
{...(creatingRoleAlreadyExists
? { error: 'A role with this name already exists.', isInvalid: true }
: {})}
>
<EuiFieldText
name={'name'}
value={role.name || ''}
onChange={onNameChange}
onBlur={onNameBlur}
data-test-subj={'roleFormNameInput'}
disabled={isRoleReserved || isEditingExistingRole || isRoleReadOnly}
isInvalid={creatingRoleAlreadyExists}
/>
) : undefined
}
{...validator.validateRoleName(role)}
{...(creatingRoleAlreadyExists
? { error: 'A role with this name already exists.', isInvalid: true }
: {})}
>
<EuiFieldText
name={'name'}
value={role.name || ''}
onChange={onNameChange}
onBlur={onNameBlur}
data-test-subj={'roleFormNameInput'}
disabled={isRoleReserved || isEditingExistingRole || isRoleReadOnly}
isInvalid={creatingRoleAlreadyExists}
/>
</EuiFormRow>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow
data-test-subj="roleDescriptionFormRow"
label={
<FormattedMessage
id="xpack.security.management.editRole.roleDescriptionFormRowTitle"
defaultMessage="Role description"
/>
}
>
{isRoleReserved || isRoleReadOnly ? (
<EuiToolTip
content={role.description}
display="block"
data-test-subj="roleFormDescriptionTooltip"
>
<EuiFieldText
name="description"
value={role.description ?? ''}
data-test-subj="roleFormDescriptionInput"
disabled
/>
</EuiToolTip>
) : (
<EuiFieldText
name="description"
value={role.description ?? ''}
onChange={onDescriptionChange}
data-test-subj="roleFormDescriptionInput"
/>
)}
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
};
Expand All @@ -510,6 +549,12 @@ export const EditRolePage: FunctionComponent<Props> = ({
}
};

const onDescriptionChange = (e: ChangeEvent<HTMLInputElement>) =>
setRole({
...role,
description: e.target.value.trim().length ? e.target.value : undefined,
});

const getElasticsearchPrivileges = () => {
return (
<div>
Expand Down Expand Up @@ -787,7 +832,7 @@ export const EditRolePage: FunctionComponent<Props> = ({
</Fragment>
)}
<EuiSpacer />
{getRoleName()}
{getRoleNameAndDescription()}
{getElasticsearchPrivileges()}
{getKibanaPrivileges()}
<EuiSpacer />
Expand Down
Loading

0 comments on commit 4fade37

Please sign in to comment.