Grouped features for role management (#78152)

* Grouped features for role management

* address PR feedback

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Larry Gregory 2020-10-02 08:45:28 -04:00 committed by GitHub
parent ea6bec6c9b
commit b9a79836f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 640 additions and 660 deletions

View file

@ -2,11 +2,11 @@
[[xpack-security-authorization]]
=== Granting access to {kib}
The Elastic Stack comes with the `kibana_admin` {ref}/built-in-roles.html[built-in role], which you can use to grant access to all Kibana features in all spaces. To grant users access to a subset of spaces or features, you can create a custom role that grants the desired Kibana privileges.
The Elastic Stack comes with the `kibana_admin` {ref}/built-in-roles.html[built-in role], which you can use to grant access to all {kib} features in all spaces. To grant users access to a subset of spaces or features, you can create a custom role that grants the desired {kib} privileges.
When you assign a user multiple roles, the user receives a union of the roles privileges. Therefore, assigning the `kibana_admin` role in addition to a custom role that grants Kibana privileges is ineffective because `kibana_admin` has access to all the features in all spaces.
When you assign a user multiple roles, the user receives a union of the roles privileges. Therefore, assigning the `kibana_admin` role in addition to a custom role that grants {kib} privileges is ineffective because `kibana_admin` has access to all the features in all spaces.
NOTE: When running multiple tenants of Kibana by changing the `kibana.index` in your `kibana.yml`, you cannot use `kibana_admin` to grant access. You must create custom roles that authorize the user for that specific tenant. Although multi-tenant installations are supported, the recommended approach to securing access to Kibana segments is to grant users access to specific spaces.
NOTE: When running multiple tenants of {kib} by changing the `kibana.index` in your `kibana.yml`, you cannot use `kibana_admin` to grant access. You must create custom roles that authorize the user for that specific tenant. Although multi-tenant installations are supported, the recommended approach to securing access to {kib} segments is to grant users access to specific spaces.
[role="xpack"]
[[xpack-kibana-role-management]]
@ -17,26 +17,26 @@ To create a role that grants {kib} privileges, open the menu, go to *Stack Manag
[[adding_kibana_privileges]]
==== Adding {kib} privileges
To assign {kib} privileges to the role, click **Add space privilege** in the Kibana section.
To assign {kib} privileges to the role, click **Add {kib} privilege** in the {kib} section.
[role="screenshot"]
image::user/security/images/add-space-privileges.png[Add space privileges]
image::user/security/images/add-space-privileges.png[Add {kib} privileges]
Open the **Spaces** selection control to specify whether to grant the role access to all spaces *** Global (all spaces)** or one or more individual spaces. If you select *** Global (all spaces)**, you cant select individual spaces until you clear your selection.
Use the **Privilege** menu to grant access to features. The default is **Custom**, which you can use to grant access to individual features. Otherwise, you can grant read and write access to all current and future features by selecting **All**, or grant read access to all current and future features by selecting **Read**.
When using the **Customize by feature** option, you can choose either **All**, **Read** or **None** for access to each feature. As new features are added to Kibana, roles that use the custom option do not automatically get access to the new features. You must manually update the roles.
When using the **Customize by feature** option, you can choose either **All**, **Read** or **None** for access to each feature. As new features are added to {kib}, roles that use the custom option do not automatically get access to the new features. You must manually update the roles.
NOTE: *{stack-monitor-app}* relies on built-in roles to grant access. When a
user is assigned the appropriate roles, the *{stack-monitor-app}* application is
available; otherwise, it is not visible.
To apply your changes, click **Create space privilege**. The space privilege shows up under the Kibana privileges section of the role.
To apply your changes, click **Add {kib} privilege**. The privilege shows up under the {kib} privileges section of the role.
[role="screenshot"]
image::user/security/images/create-space-privilege.png[Create space privilege]
image::user/security/images/create-space-privilege.png[Add {kib} privilege]
==== Feature availability
@ -64,9 +64,9 @@ Features are available to users when their roles grant access to the features, *
==== Assigning different privileges to different spaces
Using the same role, its possible to assign different privileges to different spaces. After youve added space privileges, click **Add space privilege**. If youve already added privileges for either *** Global (all spaces)** or an individual space, you will not be able to select these in the **Spaces** selection control.
Using the same role, its possible to assign different privileges to different spaces. After youve added privileges, click **Add {kib} privilege**. If youve already added privileges for either *** Global (all spaces)** or an individual space, you will not be able to select these in the **Spaces** selection control.
Additionally, if youve already assigned privileges at *** Global (all spaces)**, you are only able to assign additional privileges to individual spaces. Similar to the behavior of multiple roles granting the union of all privileges, space privileges are also a union. If youve already granted the user the **All** privilege at *** Global (all spaces)**, youre not able to restrict the role to only the **Read** privilege at an individual space.
Additionally, if youve already assigned privileges at *** Global (all spaces)**, you are only able to assign additional privileges to individual spaces. Similar to the behavior of multiple roles granting the union of all privileges, {kib} privileges are also a union. If youve already granted the user the **All** privilege at *** Global (all spaces)**, youre not able to restrict the role to only the **Read** privilege at an individual space.
==== Privilege summary
@ -78,39 +78,37 @@ image::user/security/images/view-privilege-summary.png[View privilege summary]
==== Example 1: Grant all access to Dashboard at an individual space
. Click **Add space privilege**.
. Click **Add {kib} privilege**.
. For **Spaces**, select an individual space.
. For **Privilege**, leave the default selection of **Custom**.
. For the Dashboard feature, select **All**
. Click **Create space privilege**.
. Click **Add {kib} privilege**.
[role="screenshot"]
image::user/security/images/privilege-example-1.png[Privilege example 1]
==== Example 2: Grant all access to one space and read access to another
. Click **Add space privilege**.
. Click **Add {kib} privilege**.
. For **Spaces**, select the first space.
. For **Privilege**, select **All**.
. Click **Create space privilege**.
. Click **Add space privilege**.
. Click **Add {kib} privilege**.
. For **Spaces**, select the second space.
. For **Privilege**, select **Read**.
. Click **Create space privilege**.
. Click **Add {kib} privilege**.
[role="screenshot"]
image::user/security/images/privilege-example-2.png[Privilege example 2]
==== Example 3: Grant read access to all spaces and write access to an individual space
. Click **Add space privilege**.
. Click **Add {kib} privilege**.
. For **Spaces**, select *** Global (all spaces)**.
. For **Privilege**, select **Read**.
. Click **Create space privilege**.
. Click **Add space privilege**.
. Click **Add {kib} privilege**.
. For **Spaces**, select the individual space.
. For **Privilege**, select **All**.
. Click **Create space privilege**.
. Click **Add {kib} privilege**.
[role="screenshot"]
image::user/security/images/privilege-example-3.png[Privilege example 3]

View file

@ -14,14 +14,15 @@ export const createFeature = (
excludeFromBaseAll?: boolean;
excludeFromBaseRead?: boolean;
privileges?: KibanaFeatureConfig['privileges'];
category?: KibanaFeatureConfig['category'];
}
) => {
const { excludeFromBaseAll, excludeFromBaseRead, privileges, ...rest } = config;
const { excludeFromBaseAll, excludeFromBaseRead, privileges, category, ...rest } = config;
return new KibanaFeature({
icon: 'discoverApp',
navLinkId: 'discover',
app: [],
category: { id: 'foo', label: 'foo' },
category: category ?? { id: 'foo', label: 'foo' },
catalogue: [],
privileges:
privileges === null

View file

@ -6,30 +6,37 @@
import { ReactWrapper } from 'enzyme';
import {
EuiTableRow,
EuiCheckbox,
EuiCheckboxProps,
EuiButtonGroup,
EuiButtonGroupProps,
} from '@elastic/eui';
import { EuiCheckbox, EuiCheckboxProps, EuiButtonGroup, EuiButtonGroupProps } from '@elastic/eui';
import { findTestSubject } from 'test_utils/find_test_subject';
import { EuiAccordion } from '@elastic/eui';
import { SubFeatureForm } from '../sub_feature_form';
export function getDisplayedFeaturePrivileges(wrapper: ReactWrapper<any>) {
const allExpanderButtons = findTestSubject(wrapper, 'expandFeaturePrivilegeRow');
const categoryExpander = findTestSubject(wrapper, 'featureCategoryButton_foo');
categoryExpander.simulate('click');
const allExpanderButtons = findTestSubject(wrapper, 'featureTableCell');
allExpanderButtons.forEach((button) => button.simulate('click'));
// each expanded row renders its own `EuiTableRow`, so there are 2 rows
// for each feature: one for the primary feature privilege, and one for the sub privilege form
const rows = wrapper.find(EuiTableRow);
const featurePrivilegeControls = wrapper
.find(EuiAccordion)
.filter('[data-test-subj="featurePrivilegeControls"]');
return rows.reduce((acc, row) => {
return featurePrivilegeControls.reduce((acc, featureControls) => {
const buttonGroup = featureControls
.find(EuiButtonGroup)
.filter('[data-test-subj="primaryFeaturePrivilegeControl"]');
const { name, idSelected } = buttonGroup.props();
expect(name).toBeDefined();
expect(idSelected).toBeDefined();
const featureId = name!.substr(`featurePrivilege_`.length);
const primaryFeaturePrivilege = idSelected!.substr(`${featureId}_`.length);
const subFeaturePrivileges = [];
const subFeatureForm = row.find(SubFeatureForm);
const subFeatureForm = featureControls.find(SubFeatureForm);
if (subFeatureForm.length > 0) {
const { featureId } = subFeatureForm.props();
const independentPrivileges = (subFeatureForm.find(EuiCheckbox) as ReactWrapper<
EuiCheckboxProps
>).reduce((acc2, checkbox) => {
@ -47,30 +54,15 @@ export function getDisplayedFeaturePrivileges(wrapper: ReactWrapper<any>) {
}, [] as string[]);
subFeaturePrivileges.push(...independentPrivileges, ...mutuallyExclusivePrivileges);
return {
...acc,
[featureId]: {
...acc[featureId],
subFeaturePrivileges,
},
};
} else {
const buttonGroup = row.find(EuiButtonGroup);
const { name, idSelected } = buttonGroup.props();
expect(name).toBeDefined();
expect(idSelected).toBeDefined();
const featureId = name!.substr(`featurePrivilege_`.length);
const primaryFeaturePrivilege = idSelected!.substr(`${featureId}_`.length);
return {
...acc,
[featureId]: {
...acc[featureId],
primaryFeaturePrivilege,
},
};
}
return {
...acc,
[featureId]: {
...acc[featureId],
primaryFeaturePrivilege,
subFeaturePrivileges,
},
};
}, {} as Record<string, { primaryFeaturePrivilege: string; subFeaturePrivileges: string[] }>);
}

View file

@ -6,7 +6,14 @@
import './change_all_privileges.scss';
import { EuiContextMenuItem, EuiContextMenuPanel, EuiLink, EuiPopover } from '@elastic/eui';
import {
EuiContextMenuItem,
EuiContextMenuPanel,
EuiLink,
EuiPopover,
EuiIcon,
EuiText,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import _ from 'lodash';
import React, { Component } from 'react';
@ -34,10 +41,13 @@ export class ChangeAllPrivilegesControl extends Component<Props, State> {
className={'secPrivilegeFeatureChangeAllLink'}
data-test-subj="changeAllPrivilegesButton"
>
<FormattedMessage
id="xpack.security.management.editRole.changeAllPrivilegesLink"
defaultMessage="(change all)"
/>
<EuiText size="xs">
<FormattedMessage
id="xpack.security.management.editRole.changeAllPrivilegesLink"
defaultMessage="Bulk actions"
/>{' '}
<EuiIcon size="s" type="arrowDown" />
</EuiText>
</EuiLink>
);

View file

@ -0,0 +1,5 @@
.subFeaturePrivilegeExpandedRegion {
background-color: $euiColorLightestShade;
padding-left: $euiSizeXXL;
padding-top: $euiSizeS;
}

View file

@ -13,7 +13,7 @@ import { createKibanaPrivileges } from '../../../../__fixtures__/kibana_privileg
import { PrivilegeFormCalculator } from '../privilege_form_calculator';
import { getDisplayedFeaturePrivileges } from './__fixtures__';
import { findTestSubject } from 'test_utils/find_test_subject';
import { FeatureTableExpandedRow } from './feature_table_expanded_row';
import { EuiAccordion } from '@elastic/eui';
const createRole = (kibana: Role['kibana'] = []): Role => {
return {
@ -86,18 +86,19 @@ describe('FeatureTable', () => {
expect(displayedPrivileges).toEqual({
excluded_from_base: {
primaryFeaturePrivilege: 'none',
...(canCustomizeSubFeaturePrivileges ? { subFeaturePrivileges: [] } : {}),
subFeaturePrivileges: [],
},
no_sub_features: {
primaryFeaturePrivilege: 'none',
subFeaturePrivileges: [],
},
with_excluded_sub_features: {
primaryFeaturePrivilege: 'none',
...(canCustomizeSubFeaturePrivileges ? { subFeaturePrivileges: [] } : {}),
subFeaturePrivileges: [],
},
with_sub_features: {
primaryFeaturePrivilege: 'none',
...(canCustomizeSubFeaturePrivileges ? { subFeaturePrivileges: [] } : {}),
subFeaturePrivileges: [],
},
});
});
@ -125,14 +126,15 @@ describe('FeatureTable', () => {
expect(displayedPrivileges).toEqual({
excluded_from_base: {
primaryFeaturePrivilege: 'none',
...(canCustomizeSubFeaturePrivileges ? { subFeaturePrivileges: [] } : {}),
subFeaturePrivileges: [],
},
no_sub_features: {
primaryFeaturePrivilege: 'all',
subFeaturePrivileges: [],
},
with_excluded_sub_features: {
primaryFeaturePrivilege: 'all',
...(canCustomizeSubFeaturePrivileges ? { subFeaturePrivileges: [] } : {}),
subFeaturePrivileges: [],
},
with_sub_features: {
primaryFeaturePrivilege: 'all',
@ -144,7 +146,7 @@ describe('FeatureTable', () => {
'cool_all',
],
}
: {}),
: { subFeaturePrivileges: [] }),
},
});
});
@ -175,14 +177,15 @@ describe('FeatureTable', () => {
expect(displayedPrivileges).toEqual({
excluded_from_base: {
primaryFeaturePrivilege: 'none',
...(canCustomizeSubFeaturePrivileges ? { subFeaturePrivileges: [] } : {}),
subFeaturePrivileges: [],
},
no_sub_features: {
primaryFeaturePrivilege: 'none',
subFeaturePrivileges: [],
},
with_excluded_sub_features: {
primaryFeaturePrivilege: 'none',
...(canCustomizeSubFeaturePrivileges ? { subFeaturePrivileges: [] } : {}),
subFeaturePrivileges: [],
},
with_sub_features: {
primaryFeaturePrivilege: 'all',
@ -194,7 +197,7 @@ describe('FeatureTable', () => {
'cool_all',
],
}
: {}),
: { subFeaturePrivileges: [] }),
},
});
});
@ -279,6 +282,7 @@ describe('FeatureTable', () => {
},
no_sub_features: {
primaryFeaturePrivilege: 'none',
subFeaturePrivileges: [],
},
with_excluded_sub_features: {
primaryFeaturePrivilege: 'none',
@ -313,43 +317,18 @@ describe('FeatureTable', () => {
});
kibanaFeatures.forEach((feature) => {
const rowExpander = findTestSubject(wrapper, `expandFeaturePrivilegeRow-${feature.id}`);
const { arrowDisplay } = wrapper
.find(EuiAccordion)
.filter(`#featurePrivilegeControls_${feature.id}`)
.props();
if (!feature.subFeatures || feature.subFeatures.length === 0) {
expect(rowExpander).toHaveLength(0);
expect(arrowDisplay).toEqual('none');
} else {
expect(rowExpander).toHaveLength(1);
expect(arrowDisplay).toEqual('left');
}
});
});
it('renders the <FeatureTableExpandedRow> when the row is expanded', () => {
const role = createRole([
{
spaces: ['*'],
base: ['read'],
feature: {},
},
{
spaces: ['foo'],
base: [],
feature: {},
},
]);
const { wrapper } = setup({
role,
features: kibanaFeatures,
privilegeIndex: 1,
calculateDisplayedPrivileges: false,
canCustomizeSubFeaturePrivileges: true,
});
expect(wrapper.find(FeatureTableExpandedRow)).toHaveLength(0);
findTestSubject(wrapper, 'expandFeaturePrivilegeRow').first().simulate('click');
expect(wrapper.find(FeatureTableExpandedRow)).toHaveLength(1);
});
it('renders with sub-feature privileges granted when primary feature privilege is "all"', () => {
const role = createRole([
{
@ -679,6 +658,7 @@ describe('FeatureTable', () => {
},
no_sub_features: {
primaryFeaturePrivilege: 'none',
subFeaturePrivileges: [],
},
with_excluded_sub_features: {
primaryFeaturePrivilege: 'none',
@ -716,15 +696,19 @@ describe('FeatureTable', () => {
expect(displayedPrivileges).toEqual({
excluded_from_base: {
primaryFeaturePrivilege: 'none',
subFeaturePrivileges: [],
},
no_sub_features: {
primaryFeaturePrivilege: 'none',
subFeaturePrivileges: [],
},
with_excluded_sub_features: {
primaryFeaturePrivilege: 'none',
subFeaturePrivileges: [],
},
with_sub_features: {
primaryFeaturePrivilege: 'none',
subFeaturePrivileges: [],
},
});
});
@ -750,15 +734,19 @@ describe('FeatureTable', () => {
expect(displayedPrivileges).toEqual({
excluded_from_base: {
primaryFeaturePrivilege: 'none',
subFeaturePrivileges: [],
},
no_sub_features: {
primaryFeaturePrivilege: 'none',
subFeaturePrivileges: [],
},
with_excluded_sub_features: {
primaryFeaturePrivilege: 'none',
subFeaturePrivileges: [],
},
with_sub_features: {
primaryFeaturePrivilege: 'none',
subFeaturePrivileges: [],
},
});
});
@ -843,6 +831,7 @@ describe('FeatureTable', () => {
expect(displayedPrivileges).toEqual({
reserved_feature: {
primaryFeaturePrivilege: 'none',
subFeaturePrivileges: [],
},
});
});
@ -873,16 +862,79 @@ describe('FeatureTable', () => {
expect(displayedPrivileges).toEqual({
excluded_from_base: {
primaryFeaturePrivilege: 'none',
subFeaturePrivileges: [],
},
no_sub_features: {
primaryFeaturePrivilege: 'none',
subFeaturePrivileges: [],
},
with_excluded_sub_features: {
primaryFeaturePrivilege: 'none',
subFeaturePrivileges: [],
},
with_sub_features: {
primaryFeaturePrivilege: 'none',
subFeaturePrivileges: [],
},
});
});
it('renders features by category, indicating how many features are granted within', async () => {
const role = createRole([
{
spaces: ['foo'],
base: [],
feature: {
feature_1: ['all'],
feature_3: ['all'],
feature_4: ['all'],
},
},
]);
const features = [
createFeature({
id: 'feature_1',
name: 'Feature1',
category: { id: 'foo', label: 'foo' },
}),
createFeature({
id: 'feature_2',
name: 'Feature2',
category: { id: 'foo', label: 'foo' },
}),
createFeature({
id: 'feature_3',
name: 'Feature3',
category: { id: 'bar', label: 'bar' },
}),
createFeature({
id: 'feature_4',
name: 'Feature4',
category: { id: 'bar', label: 'bar' },
}),
];
const { wrapper } = setup({
role,
features,
privilegeIndex: 0,
calculateDisplayedPrivileges: false,
canCustomizeSubFeaturePrivileges: false,
});
const fooCategory = findTestSubject(wrapper, 'featureCategory_foo');
const barCategory = findTestSubject(wrapper, 'featureCategory_bar');
expect(fooCategory).toHaveLength(1);
expect(barCategory).toHaveLength(1);
expect(findTestSubject(fooCategory, 'categoryLabel').text()).toMatchInlineSnapshot(
`"1 / 2 features granted"`
);
expect(findTestSubject(barCategory, 'categoryLabel').text()).toMatchInlineSnapshot(
`"2 / 2 features granted"`
);
});
});

View file

@ -5,24 +5,31 @@
*/
import {
EuiAccordionProps,
EuiButtonGroup,
EuiIconTip,
EuiInMemoryTable,
EuiText,
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiCallOut,
EuiHorizontalRule,
EuiAccordion,
EuiIcon,
EuiTitle,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import React, { Component } from 'react';
import React, { Component, ReactElement } from 'react';
import { AppCategory } from 'kibana/public';
import { Role } from '../../../../../../../common/model';
import { ChangeAllPrivilegesControl } from './change_all_privileges';
import { FeatureTableExpandedRow } from './feature_table_expanded_row';
import { NO_PRIVILEGE_VALUE } from '../constants';
import { PrivilegeFormCalculator } from '../privilege_form_calculator';
import { FeatureTableCell } from '../feature_table_cell';
import { KibanaPrivileges, SecuredFeature, KibanaPrivilege } from '../../../../model';
import { KibanaPrivileges, SecuredFeature } from '../../../../model';
import './feature_table.scss';
interface Props {
role: Role;
@ -35,88 +42,291 @@ interface Props {
disabled?: boolean;
}
interface State {
expandedFeatures: string[];
}
interface TableRow {
featureId: string;
feature: SecuredFeature;
inherited: KibanaPrivilege[];
effective: KibanaPrivilege[];
role: Role;
}
export class FeatureTable extends Component<Props, State> {
export class FeatureTable extends Component<Props, {}> {
public static defaultProps = {
privilegeIndex: -1,
showLocks: true,
};
private featureCategories: Map<string, SecuredFeature[]> = new Map();
constructor(props: Props) {
super(props);
this.state = {
expandedFeatures: [],
};
// features are static for the lifetime of the page, so this is safe to do here in a non-reactive manner
props.kibanaPrivileges
.getSecuredFeatures()
.filter((feature) => feature.privileges != null || feature.reserved != null)
.forEach((feature) => {
if (!this.featureCategories.has(feature.category.id)) {
this.featureCategories.set(feature.category.id, []);
}
this.featureCategories.get(feature.category.id)!.push(feature);
});
}
public render() {
const { role, kibanaPrivileges } = this.props;
const basePrivileges = this.props.kibanaPrivileges.getBasePrivileges(
this.props.role.kibana[this.props.privilegeIndex]
);
const featurePrivileges = kibanaPrivileges
.getSecuredFeatures()
.filter((feature) => feature.privileges != null || feature.reserved != null);
const accordions: Array<{ order: number; element: ReactElement }> = [];
this.featureCategories.forEach((featuresInCategory) => {
const { category } = featuresInCategory[0];
const items: TableRow[] = featurePrivileges
.sort((feature1, feature2) => {
if (feature1.reserved && !feature2.reserved) {
return 1;
const featureCount = featuresInCategory.length;
const grantedCount = featuresInCategory.filter(
(feature) =>
this.props.privilegeCalculator.getEffectivePrimaryFeaturePrivilege(
feature.id,
this.props.privilegeIndex
) != null
).length;
const canExpandCategory = true; // featuresInCategory.length > 1;
const buttonContent = (
<EuiFlexGroup
data-test-subj={`featureCategoryButton_${category.id}`}
alignItems={'center'}
responsive={false}
gutterSize="m"
>
{category.euiIconType ? (
<EuiFlexItem grow={false}>
<EuiIcon size="m" type={category.euiIconType} />
</EuiFlexItem>
) : null}
<EuiFlexItem grow={1}>
<EuiTitle size="xs">
<h4 className="eui-displayInlineBlock">{category.label}</h4>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
);
const label: string = i18n.translate(
'xpack.security.management.editRole.featureTable.featureAccordionSwitchLabel',
{
defaultMessage:
'{grantedCount} / {featureCount} {featureCount, plural, one {feature} other {features}} granted',
values: {
grantedCount,
featureCount,
},
}
);
const extraAction = (
<EuiText size="s" aria-hidden="true" color={'subdued'} data-test-subj="categoryLabel">
{label}
</EuiText>
);
if (feature2.reserved && !feature1.reserved) {
return -1;
}
const helpText = this.getCategoryHelpText(category);
return 0;
})
.map((feature) => {
return {
featureId: feature.id,
feature,
inherited: [],
effective: [],
role,
};
const accordion = (
<EuiAccordion
id={`featureCategory_${category.id}`}
data-test-subj={`featureCategory_${category.id}`}
key={category.id}
arrowDisplay={canExpandCategory ? 'left' : 'none'}
forceState={canExpandCategory ? undefined : 'closed'}
buttonContent={buttonContent}
extraAction={canExpandCategory ? extraAction : undefined}
>
<div>
<EuiSpacer size="s" />
{helpText && (
<>
<EuiCallOut iconType="iInCircle" size="s">
{helpText}
</EuiCallOut>
<EuiSpacer size="s" />
</>
)}
<EuiFlexGroup direction="column" gutterSize="s">
{featuresInCategory.map((feature) => (
<EuiFlexItem key={feature.id}>
{this.renderPrivilegeControlsForFeature(feature)}
</EuiFlexItem>
))}
</EuiFlexGroup>
</div>
</EuiAccordion>
);
accordions.push({
order: category.order ?? Number.MAX_SAFE_INTEGER,
element: accordion,
});
});
accordions.sort((a1, a2) => a1.order - a2.order);
return (
<EuiInMemoryTable
responsive={false}
columns={this.getColumns()}
itemId={'featureId'}
itemIdToExpandedRowMap={this.state.expandedFeatures.reduce((acc, featureId) => {
return {
...acc,
[featureId]: (
<FeatureTableExpandedRow
feature={featurePrivileges.find((f) => f.id === featureId)!}
privilegeIndex={this.props.privilegeIndex}
onChange={this.props.onChange}
privilegeCalculator={this.props.privilegeCalculator}
selectedFeaturePrivileges={
this.props.role.kibana[this.props.privilegeIndex].feature[featureId] ?? []
}
disabled={this.props.disabled}
<div>
<EuiFlexGroup alignItems={'flexEnd'}>
<EuiFlexItem>
<EuiText size="xs">
<b>
{i18n.translate(
'xpack.security.management.editRole.featureTable.featureVisibilityTitle',
{
defaultMessage: 'Customize feature privileges',
}
)}
</b>
</EuiText>
</EuiFlexItem>
{!this.props.disabled && (
<EuiFlexItem grow={false}>
<ChangeAllPrivilegesControl
privileges={basePrivileges}
onChange={this.onChangeAllFeaturePrivileges}
/>
),
};
}, {})}
items={items}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
<EuiHorizontalRule margin={'m'} />
{accordions.flatMap((a, idx) => [
a.element,
<EuiHorizontalRule key={`accordion-hr-${idx}`} margin={'m'} />,
])}
</div>
);
}
public onChange = (featureId: string) => (featurePrivilegeId: string) => {
private renderPrivilegeControlsForFeature = (feature: SecuredFeature) => {
const renderFeatureMarkup = (
buttonContent: EuiAccordionProps['buttonContent'],
extraAction: EuiAccordionProps['extraAction'],
warningIcon: JSX.Element
) => {
const { canCustomizeSubFeaturePrivileges } = this.props;
const hasSubFeaturePrivileges = feature.getSubFeaturePrivileges().length > 0;
return (
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>{warningIcon}</EuiFlexItem>
<EuiFlexItem>
<EuiAccordion
id={`featurePrivilegeControls_${feature.id}`}
data-test-subj="featurePrivilegeControls"
buttonContent={buttonContent}
extraAction={extraAction}
forceState={
canCustomizeSubFeaturePrivileges && hasSubFeaturePrivileges ? undefined : 'closed'
}
arrowDisplay={
canCustomizeSubFeaturePrivileges && hasSubFeaturePrivileges ? 'left' : 'none'
}
>
<div className="subFeaturePrivilegeExpandedRegion">
<FeatureTableExpandedRow
feature={feature}
privilegeIndex={this.props.privilegeIndex}
onChange={this.props.onChange}
privilegeCalculator={this.props.privilegeCalculator}
selectedFeaturePrivileges={
this.props.role.kibana[this.props.privilegeIndex].feature[feature.id] ?? []
}
disabled={this.props.disabled}
/>
</div>
</EuiAccordion>
</EuiFlexItem>
</EuiFlexGroup>
);
};
const primaryFeaturePrivileges = feature.getPrimaryFeaturePrivileges();
if (feature.reserved && primaryFeaturePrivileges.length === 0) {
const buttonContent = (
<>
{<EuiIcon type="empty" size="l" />} <FeatureTableCell feature={feature} />
</>
);
const extraAction = (
<EuiText size={'s'} data-test-subj="reservedFeatureDescription">
{feature.reserved.description}
</EuiText>
);
return renderFeatureMarkup(buttonContent, extraAction, <EuiIcon type="empty" />);
}
if (primaryFeaturePrivileges.length === 0) {
return null;
}
const selectedPrivilegeId = this.props.privilegeCalculator.getDisplayedPrimaryFeaturePrivilegeId(
feature.id,
this.props.privilegeIndex
);
const options = primaryFeaturePrivileges.map((privilege) => {
return {
id: `${feature.id}_${privilege.id}`,
label: privilege.name,
isDisabled: this.props.disabled,
};
});
options.push({
id: `${feature.id}_${NO_PRIVILEGE_VALUE}`,
label: 'None',
isDisabled: this.props.disabled,
});
let warningIcon = <EuiIconTip type="empty" content={null} />;
if (
this.props.privilegeCalculator.hasCustomizedSubFeaturePrivileges(
feature.id,
this.props.privilegeIndex
)
) {
warningIcon = (
<EuiIconTip
type="alert"
content={
<FormattedMessage
id="xpack.security.management.editRole.featureTable.privilegeCustomizationTooltip"
defaultMessage="Feature has customized sub-feature privileges. Expand this row for more information."
/>
}
/>
);
}
const { canCustomizeSubFeaturePrivileges } = this.props;
const hasSubFeaturePrivileges = feature.getSubFeaturePrivileges().length > 0;
const showAccordionArrow = canCustomizeSubFeaturePrivileges && hasSubFeaturePrivileges;
const buttonContent = (
<>
{!showAccordionArrow && <EuiIcon type="empty" size="l" />}{' '}
<FeatureTableCell feature={feature} />
</>
);
const extraAction = (
<EuiButtonGroup
name={`featurePrivilege_${feature.id}`}
data-test-subj={`primaryFeaturePrivilegeControl`}
isFullWidth={true}
options={options}
idSelected={`${feature.id}_${selectedPrivilegeId ?? NO_PRIVILEGE_VALUE}`}
onChange={this.onChange(feature.id)}
/>
);
return renderFeatureMarkup(buttonContent, extraAction, warningIcon);
};
private onChange = (featureId: string) => (featurePrivilegeId: string) => {
const privilege = featurePrivilegeId.substr(`${featureId}_`.length);
if (privilege === NO_PRIVILEGE_VALUE) {
this.props.onChange(featureId, []);
@ -125,163 +335,6 @@ export class FeatureTable extends Component<Props, State> {
}
};
private getColumns = () => {
const basePrivileges = this.props.kibanaPrivileges.getBasePrivileges(
this.props.role.kibana[this.props.privilegeIndex]
);
const columns = [];
if (this.props.canCustomizeSubFeaturePrivileges) {
columns.push({
width: '30px',
isExpander: true,
field: 'featureId',
name: '',
render: (featureId: string, record: TableRow) => {
const { feature } = record;
const hasSubFeaturePrivileges = feature.getSubFeaturePrivileges().length > 0;
if (!hasSubFeaturePrivileges) {
return null;
}
return (
<EuiButtonIcon
onClick={() => this.toggleExpandedFeature(featureId)}
data-test-subj={`expandFeaturePrivilegeRow expandFeaturePrivilegeRow-${featureId}`}
aria-label={this.state.expandedFeatures.includes(featureId) ? 'Collapse' : 'Expand'}
iconType={this.state.expandedFeatures.includes(featureId) ? 'arrowUp' : 'arrowDown'}
/>
);
},
});
}
columns.push(
{
field: 'feature',
width: '200px',
name: i18n.translate(
'xpack.security.management.editRole.featureTable.enabledRoleFeaturesFeatureColumnTitle',
{
defaultMessage: 'Feature',
}
),
render: (feature: SecuredFeature) => {
return <FeatureTableCell feature={feature} />;
},
},
{
field: 'privilege',
width: '200px',
name: (
<span>
<FormattedMessage
id="xpack.security.management.editRole.featureTable.enabledRoleFeaturesEnabledColumnTitle"
defaultMessage="Privilege"
/>
{!this.props.disabled && (
<ChangeAllPrivilegesControl
privileges={basePrivileges}
onChange={this.onChangeAllFeaturePrivileges}
/>
)}
</span>
),
mobileOptions: {
// Table isn't responsive, so skip rendering this for mobile. <ChangeAllPrivilegesControl /> isn't free...
header: false,
},
render: (roleEntry: Role, record: TableRow) => {
const { feature } = record;
const primaryFeaturePrivileges = feature.getPrimaryFeaturePrivileges();
if (feature.reserved && primaryFeaturePrivileges.length === 0) {
return (
<EuiText size={'s'} data-test-subj="reservedFeatureDescription">
{feature.reserved.description}
</EuiText>
);
}
if (primaryFeaturePrivileges.length === 0) {
return null;
}
const selectedPrivilegeId = this.props.privilegeCalculator.getDisplayedPrimaryFeaturePrivilegeId(
feature.id,
this.props.privilegeIndex
);
const options = primaryFeaturePrivileges.map((privilege) => {
return {
id: `${feature.id}_${privilege.id}`,
label: privilege.name,
isDisabled: this.props.disabled,
};
});
options.push({
id: `${feature.id}_${NO_PRIVILEGE_VALUE}`,
label: 'None',
isDisabled: this.props.disabled,
});
let warningIcon = <EuiIconTip type="empty" content={null} />;
if (
this.props.privilegeCalculator.hasCustomizedSubFeaturePrivileges(
feature.id,
this.props.privilegeIndex
)
) {
warningIcon = (
<EuiIconTip
type="iInCircle"
content={
<FormattedMessage
id="xpack.security.management.editRole.featureTable.privilegeCustomizationTooltip"
defaultMessage="Feature has customized sub-feature privileges. Expand this row for more information."
/>
}
/>
);
}
return (
<EuiFlexGroup alignItems="center" gutterSize="xs">
<EuiFlexItem grow={false}>{warningIcon}</EuiFlexItem>
<EuiFlexItem>
<EuiButtonGroup
name={`featurePrivilege_${feature.id}`}
data-test-subj={`primaryFeaturePrivilegeControl`}
buttonSize="compressed"
color={'primary'}
isFullWidth={true}
options={options}
idSelected={`${feature.id}_${selectedPrivilegeId ?? NO_PRIVILEGE_VALUE}`}
onChange={this.onChange(feature.id)}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
},
}
);
return columns;
};
private toggleExpandedFeature = (featureId: string) => {
if (this.state.expandedFeatures.includes(featureId)) {
this.setState({
expandedFeatures: this.state.expandedFeatures.filter((ef) => ef !== featureId),
});
} else {
this.setState({
expandedFeatures: [...this.state.expandedFeatures, featureId],
});
}
};
private onChangeAllFeaturePrivileges = (privilege: string) => {
if (privilege === NO_PRIVILEGE_VALUE) {
this.props.onChangeAll([]);
@ -289,4 +342,16 @@ export class FeatureTable extends Component<Props, State> {
this.props.onChangeAll([privilege]);
}
};
private getCategoryHelpText = (category: AppCategory) => {
if (category.id === 'management') {
return i18n.translate(
'xpack.security.management.editRole.featureTable.managementCategoryHelpText',
{
defaultMessage:
'Access to Stack Management is determined by both Elasticsearch and Kibana privileges, and cannot be explicitly disabled.',
}
);
}
};
}

View file

@ -1,4 +0,0 @@
.secPrivilegeFeatureIcon {
flex-shrink: 0;
margin-right: $euiSizeS;
}

View file

@ -9,10 +9,10 @@ import { createFeature } from '../../../../__fixtures__/kibana_features';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { FeatureTableCell } from '.';
import { SecuredFeature } from '../../../../model';
import { EuiIcon, EuiIconTip } from '@elastic/eui';
import { EuiIconTip } from '@elastic/eui';
describe('FeatureTableCell', () => {
it('renders an icon and feature name', () => {
it('renders the feature name', () => {
const feature = createFeature({
id: 'test-feature',
name: 'Test Feature',
@ -23,13 +23,10 @@ describe('FeatureTableCell', () => {
);
expect(wrapper.text()).toMatchInlineSnapshot(`"Test Feature "`);
expect(wrapper.find(EuiIcon).props()).toMatchObject({
type: feature.icon,
});
expect(wrapper.find(EuiIconTip)).toHaveLength(0);
});
it('renders an icon and feature name with tooltip when configured', () => {
it('renders a feature name with tooltip when configured', () => {
const feature = createFeature({
id: 'test-feature',
name: 'Test Feature',
@ -41,9 +38,7 @@ describe('FeatureTableCell', () => {
);
expect(wrapper.text()).toMatchInlineSnapshot(`"Test Feature "`);
expect(wrapper.find(EuiIcon).first().props()).toMatchObject({
type: feature.icon,
});
expect(wrapper.find(EuiIconTip).props().content).toMatchInlineSnapshot(`
<EuiText>
<p>

View file

@ -4,10 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import './feature_table_cell.scss';
import React from 'react';
import { EuiText, EuiIconTip, EuiIcon, IconType } from '@elastic/eui';
import { EuiText, EuiIconTip } from '@elastic/eui';
import { SecuredFeature } from '../../../../model';
interface Props {
@ -35,8 +33,7 @@ export const FeatureTableCell = ({ feature }: Props) => {
}
return (
<span>
<EuiIcon size="m" type={feature.icon as IconType} className="secPrivilegeFeatureIcon" />
<span data-test-subj={`featureTableCell`}>
{feature.name} {tooltipElement}
</span>
);

View file

@ -6,16 +6,9 @@
import React, { useState, Fragment } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiModal,
EuiButtonEmpty,
EuiOverlayMask,
EuiModalHeader,
EuiModalHeaderTitle,
EuiModalBody,
EuiModalFooter,
EuiButton,
} from '@elastic/eui';
import { EuiButtonEmpty, EuiOverlayMask, EuiButton } from '@elastic/eui';
import { EuiFlyout } from '@elastic/eui';
import { EuiFlyoutHeader, EuiTitle, EuiFlyoutBody, EuiFlyoutFooter } from '@elastic/eui';
import { Space } from '../../../../../../../../spaces/common/model/space';
import { Role } from '../../../../../../../common/model';
import { PrivilegeSummaryTable } from './privilege_summary_table';
@ -30,6 +23,9 @@ interface Props {
export const PrivilegeSummary = (props: Props) => {
const [isOpen, setIsOpen] = useState(false);
const numberOfPrivilegeDefinitions = props.role.kibana.length;
const flyoutSize = numberOfPrivilegeDefinitions > 5 ? 'l' : 'm';
return (
<Fragment>
<EuiButtonEmpty onClick={() => setIsOpen(true)} data-test-subj="viewPrivilegeSummaryButton">
@ -39,33 +35,35 @@ export const PrivilegeSummary = (props: Props) => {
/>
</EuiButtonEmpty>
{isOpen && (
<EuiOverlayMask>
<EuiModal onClose={() => setIsOpen(false)} maxWidth={false}>
<EuiModalHeader>
<EuiModalHeaderTitle>
<FormattedMessage
id="xpack.security.management.editRole.privilegeSummary.modalHeaderTitle"
defaultMessage="Privilege summary"
/>
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiOverlayMask headerZindexLocation="below">
<EuiFlyout onClose={() => setIsOpen(false)} size={flyoutSize}>
<EuiFlyoutHeader>
<EuiTitle size="m">
<h2>
<FormattedMessage
id="xpack.security.management.editRole.privilegeSummary.modalHeaderTitle"
defaultMessage="Privilege summary"
/>
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<PrivilegeSummaryTable
role={props.role}
spaces={props.spaces}
kibanaPrivileges={props.kibanaPrivileges}
canCustomizeSubFeaturePrivileges={props.canCustomizeSubFeaturePrivileges}
/>
</EuiModalBody>
<EuiModalFooter>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiButton onClick={() => setIsOpen(false)}>
<FormattedMessage
id="xpack.security.management.editRole.privilegeSummary.closeSummaryButtonText"
defaultMessage="Close"
/>
</EuiButton>
</EuiModalFooter>
</EuiModal>
</EuiFlyoutFooter>
</EuiFlyout>
</EuiOverlayMask>
)}
</Fragment>

View file

@ -4,14 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useState } from 'react';
import React, { useMemo, useState, Fragment } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiInMemoryTable,
EuiBasicTableColumn,
EuiButtonIcon,
EuiIcon,
EuiIconTip,
EuiSpacer,
EuiAccordion,
EuiTitle,
} from '@elastic/eui';
import { Space } from '../../../../../../../../spaces/common/model/space';
import { Role, RoleKibanaPrivilege } from '../../../../../../../common/model';
@ -39,6 +44,22 @@ function getColumnKey(entry: RoleKibanaPrivilege) {
export const PrivilegeSummaryTable = (props: Props) => {
const [expandedFeatures, setExpandedFeatures] = useState<string[]>([]);
const featureCategories = useMemo(() => {
const featureCategoryMap = new Map<string, SecuredFeature[]>();
props.kibanaPrivileges
.getSecuredFeatures()
.filter((feature) => feature.privileges != null || feature.reserved != null)
.forEach((feature) => {
if (!featureCategoryMap.has(feature.category.id)) {
featureCategoryMap.set(feature.category.id, []);
}
featureCategoryMap.get(feature.category.id)!.push(feature);
});
return featureCategoryMap;
}, [props.kibanaPrivileges]);
const calculator = new PrivilegeSummaryCalculator(props.kibanaPrivileges, props.role);
const toggleExpandedFeature = (featureId: string) => {
@ -140,35 +161,80 @@ export const PrivilegeSummaryTable = (props: Props) => {
};
}, {} as Record<string, EffectiveFeaturePrivileges>);
const items = props.kibanaPrivileges.getSecuredFeatures().map((feature) => {
return {
feature,
featureId: feature.id,
...privileges,
};
const accordions: any[] = [];
featureCategories.forEach((featuresInCategory) => {
const { category } = featuresInCategory[0];
const buttonContent = (
<EuiFlexGroup
data-test-subj={`featureCategoryButton_${category.id}`}
alignItems={'center'}
responsive={false}
gutterSize="m"
>
{category.euiIconType ? (
<EuiFlexItem grow={false}>
<EuiIcon size="m" type={category.euiIconType} />
</EuiFlexItem>
) : null}
<EuiFlexItem grow={1}>
<EuiTitle size="xs">
<h4 className="eui-displayInlineBlock">{category.label}</h4>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
);
const categoryItems = featuresInCategory.map((feature) => {
return {
feature,
featureId: feature.id,
...privileges,
};
});
accordions.push(
<EuiAccordion
id={`privilegeSummaryFeatureCategory_${category.id}`}
data-test-subj={`privilegeSummaryFeatureCategory_${category.id}`}
key={category.id}
buttonContent={buttonContent}
initialIsOpen={true}
>
<EuiInMemoryTable
columns={columns}
items={categoryItems}
itemId="featureId"
rowProps={(record) => {
return {
'data-test-subj': `summaryTableRow-${record.featureId}`,
};
}}
itemIdToExpandedRowMap={expandedFeatures.reduce((acc, featureId) => {
return {
...acc,
[featureId]: (
<PrivilegeSummaryExpandedRow
feature={props.kibanaPrivileges.getSecuredFeature(featureId)}
effectiveFeaturePrivileges={Object.values(privileges).map((p) => p[featureId])}
/>
),
};
}, {})}
/>
</EuiAccordion>
);
});
return (
<EuiInMemoryTable
columns={columns}
items={items}
itemId="featureId"
rowProps={(record) => {
return {
'data-test-subj': `summaryTableRow-${record.featureId}`,
};
}}
itemIdToExpandedRowMap={expandedFeatures.reduce((acc, featureId) => {
return {
...acc,
[featureId]: (
<PrivilegeSummaryExpandedRow
feature={props.kibanaPrivileges.getSecuredFeature(featureId)}
effectiveFeaturePrivileges={Object.values(privileges).map((p) => p[featureId])}
/>
),
};
}, {})}
/>
<>
{accordions.map((a, idx) => (
<Fragment key={idx}>
{a}
<EuiSpacer />
</Fragment>
))}
</>
);
};

View file

@ -43,7 +43,7 @@ const spaces = [
];
describe('SpaceColumnHeader', () => {
it('renders the Global privilege definition with a special label and popover control', () => {
it('renders the Global privilege definition with a special label', () => {
const wrapper = mountWithIntl(
<SpaceColumnHeader
spaces={spaces}
@ -55,10 +55,9 @@ describe('SpaceColumnHeader', () => {
/>
);
expect(wrapper.find(SpacesPopoverList)).toHaveLength(1);
// Snapshot includes space avatar (The first "G"), followed by the "Global" label,
// followed by the (all spaces) text as part of the SpacesPopoverList
expect(wrapper.text()).toMatchInlineSnapshot(`"G Global(all spaces)"`);
expect(wrapper.text()).toMatchInlineSnapshot(`"G All Spaces"`);
});
it('renders a placeholder space when the requested space no longer exists', () => {

View file

@ -39,17 +39,7 @@ export const SpaceColumnHeader = (props: Props) => {
<span>
<FormattedMessage
id="xpack.security.management.editRole.spacePrivilegeMatrix.globalSpaceName"
defaultMessage="Global"
/>
<br />
<SpacesPopoverList
spaces={props.spaces.filter((s) => s.id !== '*')}
buttonText={i18n.translate(
'xpack.security.management.editRole.spacePrivilegeMatrix.showAllSpacesLink',
{
defaultMessage: '(all spaces)',
}
)}
defaultMessage="All Spaces"
/>
</span>
)}

View file

@ -11,11 +11,11 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { PrivilegeSpaceForm } from './privilege_space_form';
import React from 'react';
import { Space } from '../../../../../../../../spaces/public';
import { EuiSuperSelect } from '@elastic/eui';
import { FeatureTable } from '../feature_table';
import { getDisplayedFeaturePrivileges } from '../feature_table/__fixtures__';
import { findTestSubject } from 'test_utils/find_test_subject';
import { SpaceSelector } from './space_selector';
import { EuiButtonGroup } from '@elastic/eui';
const createRole = (kibana: Role['kibana'] = []): Role => {
return {
@ -59,7 +59,9 @@ describe('PrivilegeSpaceForm', () => {
/>
);
expect(wrapper.find(EuiSuperSelect).props().valueOfSelected).toEqual(`basePrivilege_custom`);
expect(
wrapper.find(EuiButtonGroup).filter('[name="basePrivilegeButtonGroup"]').props().idSelected
).toEqual(`basePrivilege_custom`);
expect(wrapper.find(FeatureTable).props().disabled).toEqual(true);
expect(getDisplayedFeaturePrivileges(wrapper)).toMatchInlineSnapshot(`
Object {
@ -69,6 +71,7 @@ describe('PrivilegeSpaceForm', () => {
},
"no_sub_features": Object {
"primaryFeaturePrivilege": "none",
"subFeaturePrivileges": Array [],
},
"with_excluded_sub_features": Object {
"primaryFeaturePrivilege": "none",
@ -106,7 +109,9 @@ describe('PrivilegeSpaceForm', () => {
/>
);
expect(wrapper.find(EuiSuperSelect).props().valueOfSelected).toEqual(`basePrivilege_all`);
expect(
wrapper.find(EuiButtonGroup).filter('[name="basePrivilegeButtonGroup"]').props().idSelected
).toEqual(`basePrivilege_all`);
expect(wrapper.find(FeatureTable).props().disabled).toEqual(true);
expect(getDisplayedFeaturePrivileges(wrapper)).toMatchInlineSnapshot(`
Object {
@ -116,6 +121,7 @@ describe('PrivilegeSpaceForm', () => {
},
"no_sub_features": Object {
"primaryFeaturePrivilege": "all",
"subFeaturePrivileges": Array [],
},
"with_excluded_sub_features": Object {
"primaryFeaturePrivilege": "all",
@ -159,7 +165,9 @@ describe('PrivilegeSpaceForm', () => {
/>
);
expect(wrapper.find(EuiSuperSelect).props().valueOfSelected).toEqual(`basePrivilege_custom`);
expect(
wrapper.find(EuiButtonGroup).filter('[name="basePrivilegeButtonGroup"]').props().idSelected
).toEqual(`basePrivilege_custom`);
expect(wrapper.find(FeatureTable).props().disabled).toEqual(false);
expect(getDisplayedFeaturePrivileges(wrapper)).toMatchInlineSnapshot(`
Object {
@ -169,6 +177,7 @@ describe('PrivilegeSpaceForm', () => {
},
"no_sub_features": Object {
"primaryFeaturePrivilege": "none",
"subFeaturePrivileges": Array [],
},
"with_excluded_sub_features": Object {
"primaryFeaturePrivilege": "none",
@ -256,7 +265,10 @@ describe('PrivilegeSpaceForm', () => {
/>
);
expect(wrapper.find(EuiSuperSelect).props().valueOfSelected).toEqual(`basePrivilege_custom`);
expect(
wrapper.find(EuiButtonGroup).filter('[name="basePrivilegeButtonGroup"]').props().idSelected
).toEqual(`basePrivilege_custom`);
expect(wrapper.find(FeatureTable).props().disabled).toEqual(false);
expect(getDisplayedFeaturePrivileges(wrapper)).toMatchInlineSnapshot(`
Object {
@ -266,6 +278,7 @@ describe('PrivilegeSpaceForm', () => {
},
"no_sub_features": Object {
"primaryFeaturePrivilege": "none",
"subFeaturePrivileges": Array [],
},
"with_excluded_sub_features": Object {
"primaryFeaturePrivilege": "none",

View file

@ -18,7 +18,6 @@ import {
EuiFormRow,
EuiOverlayMask,
EuiSpacer,
EuiSuperSelect,
EuiText,
EuiTitle,
EuiErrorBoundary,
@ -26,6 +25,7 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { Component, Fragment } from 'react';
import { EuiButtonGroup } from '@elastic/eui';
import { Space } from '../../../../../../../../spaces/public';
import { Role, copyRole } from '../../../../../../../common/model';
import { SpaceSelector } from './space_selector';
@ -95,7 +95,7 @@ export class PrivilegeSpaceForm extends Component<Props, State> {
<h2>
<FormattedMessage
id="xpack.security.management.editRole.spacePrivilegeForm.modalTitle"
defaultMessage="Space privileges"
defaultMessage="Kibana privileges"
/>
</h2>
</EuiTitle>
@ -164,6 +164,13 @@ export class PrivilegeSpaceForm extends Component<Props, State> {
defaultMessage: 'Spaces',
}
)}
helpText={i18n.translate(
'xpack.security.management.editRole.spacePrivilegeForm.spaceSelectorFormHelpText',
{
defaultMessage:
'Select one or more Kibana spaces to which you wish to assign privileges.',
}
)}
>
<SpaceSelector
selectedSpaceIds={this.state.selectedSpaceIds}
@ -179,104 +186,46 @@ export class PrivilegeSpaceForm extends Component<Props, State> {
label={i18n.translate(
'xpack.security.management.editRole.spacePrivilegeForm.privilegeSelectorFormLabel',
{
defaultMessage: 'Privilege',
defaultMessage: 'Privileges for all features',
}
)}
helpText={i18n.translate(
'xpack.security.management.editRole.spacePrivilegeForm.privilegeSelectorFormHelpText',
{
defaultMessage:
'Assign the privilege level you wish to grant to all present and future features across this space.',
}
)}
>
<EuiSuperSelect
data-test-subj={'basePrivilegeComboBox'}
fullWidth
onChange={this.onSpaceBasePrivilegeChange}
<EuiButtonGroup
name={`basePrivilegeButtonGroup`}
data-test-subj={`basePrivilegeButtonGroup`}
isFullWidth={true}
color={'primary'}
options={[
{
value: 'basePrivilege_custom',
inputDisplay: (
<EuiText>
<FormattedMessage
id="xpack.security.management.editRole.spacePrivilegeForm.customPrivilegeDisplay"
defaultMessage="Custom"
/>
</EuiText>
),
dropdownDisplay: (
<EuiText>
<strong>
<FormattedMessage
id="xpack.security.management.editRole.spacePrivilegeForm.customPrivilegeDropdownDisplay"
defaultMessage="Custom"
/>
</strong>
<p>
<FormattedMessage
id="xpack.security.management.editRole.spacePrivilegeForm.customPrivilegeDetails"
defaultMessage="Customize access by feature in selected spaces."
/>
</p>
</EuiText>
),
id: 'basePrivilege_all',
label: 'All',
['data-test-subj']: 'basePrivilege_all',
},
{
value: 'basePrivilege_read',
inputDisplay: (
<EuiText>
<FormattedMessage
id="xpack.security.management.editRole.spacePrivilegeForm.readPrivilegeDisplay"
defaultMessage="Read"
/>
</EuiText>
),
dropdownDisplay: (
<EuiText>
<strong>
<FormattedMessage
id="xpack.security.management.editRole.spacePrivilegeForm.readPrivilegeDropdownDisplay"
defaultMessage="Read"
/>
</strong>
<p>
<FormattedMessage
id="xpack.security.management.editRole.spacePrivilegeForm.readPrivilegeDetails"
defaultMessage="Grant read-only access to all features in selected spaces."
/>
</p>
</EuiText>
),
id: 'basePrivilege_read',
label: 'Read',
['data-test-subj']: 'basePrivilege_read',
},
{
value: 'basePrivilege_all',
inputDisplay: (
<EuiText>
<FormattedMessage
id="xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDisplay"
defaultMessage="All"
/>
</EuiText>
),
dropdownDisplay: (
<EuiText>
<strong>
<FormattedMessage
id="xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDropdownDisplay"
defaultMessage="All"
/>
</strong>
<p>
<FormattedMessage
id="xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDetails"
defaultMessage="Grant full access to all features in selected spaces."
/>
</p>
</EuiText>
),
id: 'basePrivilege_custom',
label: 'Customize',
['data-test-subj']: 'basePrivilege_custom',
},
]}
hasDividers
valueOfSelected={this.getDisplayedBasePrivilege()}
disabled={!hasSelectedSpaces}
idSelected={this.getDisplayedBasePrivilege()}
isDisabled={!hasSelectedSpaces}
onChange={this.onSpaceBasePrivilegeChange}
/>
</EuiFormRow>
<EuiSpacer size="s" />
<EuiSpacer />
<EuiTitle size="xxs">
<h3>{this.getFeatureListLabel(this.state.selectedBasePrivilege.length > 0)}</h3>
@ -338,7 +287,7 @@ export class PrivilegeSpaceForm extends Component<Props, State> {
buttonText = (
<FormattedMessage
id="xpack.security.management.editRolespacePrivilegeForm.createPrivilegeButton"
defaultMessage="Create space privilege"
defaultMessage="Add Kibana privilege"
/>
);
}

View file

@ -23,7 +23,6 @@ import { FormattedMessage } from '@kbn/i18n/react';
import React, { Component } from 'react';
import { Space, getSpaceColor } from '../../../../../../../../spaces/public';
import { FeaturesPrivileges, Role, copyRole } from '../../../../../../../common/model';
import { SpacesPopoverList } from '../../../spaces_popover_list';
import { PrivilegeDisplay } from './privilege_display';
import { isGlobalPrivilegeDefinition } from '../../../privilege_utils';
import { PrivilegeFormCalculator } from '../privilege_form_calculator';
@ -118,19 +117,7 @@ export class PrivilegeSpaceTable extends Component<Props, State> {
const displayedSpaces = isExpanded ? spaces : spaces.slice(0, SPACES_DISPLAY_COUNT);
let button = null;
if (record.isGlobal) {
button = (
<SpacesPopoverList
spaces={this.props.displaySpaces.filter((s) => s.id !== '*')}
buttonText={i18n.translate(
'xpack.security.management.editRole.spacePrivilegeTable.showAllSpacesLink',
{
defaultMessage: 'show spaces',
}
)}
/>
);
} else if (spaces.length > displayedSpaces.length) {
if (spaces.length > displayedSpaces.length) {
button = (
<EuiButtonEmpty
size="xs"

View file

@ -50,7 +50,7 @@ export class SpaceAwarePrivilegeSection extends Component<Props, State> {
name: i18n.translate(
'xpack.security.management.editRole.spaceAwarePrivilegeForm.globalSpacesName',
{
defaultMessage: '* Global (all spaces)',
defaultMessage: '* All Spaces',
}
),
color: '#D3DAE6',
@ -198,7 +198,7 @@ export class SpaceAwarePrivilegeSection extends Component<Props, State> {
>
<FormattedMessage
id="xpack.security.management.editRole.spacePrivilegeSection.addSpacePrivilegeButton"
defaultMessage="Add space privilege"
defaultMessage="Add Kibana privilege"
/>
</EuiButton>
);

View file

@ -58,7 +58,7 @@ export class CustomizeSpace extends Component<Props, State> {
};
return (
<SectionPanel collapsible={false} title={panelTitle} description={panelTitle}>
<SectionPanel title={panelTitle} description={panelTitle}>
<EuiDescribedFormGroup
title={
<EuiTitle size="xs">

View file

@ -2,10 +2,8 @@
exports[`EnabledFeatures renders as expected 1`] = `
<SectionPanel
collapsible={false}
data-test-subj="enabled-features-panel"
description="Customize visible features"
initiallyCollapsed={false}
title={
<span>
<FormattedMessage

View file

@ -34,8 +34,6 @@ export class EnabledFeatures extends Component<Props, {}> {
return (
<SectionPanel
collapsible={false}
initiallyCollapsed={false}
title={this.getPanelTitle()}
description={description}
data-test-subj="enabled-features-panel"

View file

@ -1,4 +1,4 @@
.spcFeatureTableAccordionContent {
// Align accordion content with the feature category logo in the accordion's buttonContent
padding-left: $euiSizeXL;
}
}

View file

@ -24,17 +24,6 @@ exports[`it renders without blowing up 1`] = `
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiLink
aria-label="hide desc"
data-test-subj="show-hide-section-link"
onClick={[Function]}
>
hide
</EuiLink>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<p>

View file

@ -4,14 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiLink } from '@elastic/eui';
import React from 'react';
import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
import { SectionPanel } from './section_panel';
test('it renders without blowing up', () => {
const wrapper = shallowWithIntl(
<SectionPanel collapsible iconType="logoElasticsearch" title="Elasticsearch" description="desc">
<SectionPanel iconType="logoElasticsearch" title="Elasticsearch" description="desc">
<p>child</p>
</SectionPanel>
);
@ -19,9 +18,9 @@ test('it renders without blowing up', () => {
expect(wrapper).toMatchSnapshot();
});
test('it renders children by default', () => {
test('it renders children', () => {
const wrapper = mountWithIntl(
<SectionPanel collapsible iconType="logoElasticsearch" title="Elasticsearch" description="desc">
<SectionPanel iconType="logoElasticsearch" title="Elasticsearch" description="desc">
<p className="child">child 1</p>
<p className="child">child 2</p>
</SectionPanel>
@ -30,19 +29,3 @@ test('it renders children by default', () => {
expect(wrapper.find(SectionPanel)).toHaveLength(1);
expect(wrapper.find('.child')).toHaveLength(2);
});
test('it hides children when the "hide" link is clicked', () => {
const wrapper = mountWithIntl(
<SectionPanel collapsible iconType="logoElasticsearch" title="Elasticsearch" description="desc">
<p className="child">child 1</p>
<p className="child">child 2</p>
</SectionPanel>
);
expect(wrapper.find(SectionPanel)).toHaveLength(1);
expect(wrapper.find('.child')).toHaveLength(2);
wrapper.find(EuiLink).simulate('click');
expect(wrapper.find('.child')).toHaveLength(0);
});

View file

@ -8,39 +8,20 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiLink,
EuiPanel,
EuiSpacer,
EuiTitle,
IconType,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { Component, Fragment, ReactNode } from 'react';
interface Props {
iconType?: IconType;
title: string | ReactNode;
description: string;
collapsible: boolean;
initiallyCollapsed?: boolean;
}
interface State {
collapsed: boolean;
}
export class SectionPanel extends Component<Props, State> {
public state = {
collapsed: false,
};
constructor(props: Props) {
super(props);
this.state = {
collapsed: props.initiallyCollapsed || false,
};
}
export class SectionPanel extends Component<Props, {}> {
public render() {
return (
<EuiPanel>
@ -51,30 +32,6 @@ export class SectionPanel extends Component<Props, State> {
}
public getTitle = () => {
const showLinkText = i18n.translate('xpack.spaces.management.collapsiblePanel.showLinkText', {
defaultMessage: 'show',
});
const hideLinkText = i18n.translate('xpack.spaces.management.collapsiblePanel.hideLinkText', {
defaultMessage: 'hide',
});
const showLinkDescription = i18n.translate(
'xpack.spaces.management.collapsiblePanel.showLinkDescription',
{
defaultMessage: 'show {title}',
values: { title: this.props.description },
}
);
const hideLinkDescription = i18n.translate(
'xpack.spaces.management.collapsiblePanel.hideLinkDescription',
{
defaultMessage: 'hide {title}',
values: { title: this.props.description },
}
);
return (
<EuiFlexGroup alignItems={'baseline'} gutterSize="s" responsive={false}>
<EuiFlexItem grow={false}>
@ -93,26 +50,11 @@ export class SectionPanel extends Component<Props, State> {
</h2>
</EuiTitle>
</EuiFlexItem>
{this.props.collapsible && (
<EuiFlexItem grow={false}>
<EuiLink
data-test-subj="show-hide-section-link"
onClick={this.toggleCollapsed}
aria-label={this.state.collapsed ? showLinkDescription : hideLinkDescription}
>
{this.state.collapsed ? showLinkText : hideLinkText}
</EuiLink>
</EuiFlexItem>
)}
</EuiFlexGroup>
);
};
public getForm = () => {
if (this.state.collapsed) {
return null;
}
return (
<Fragment>
<EuiSpacer />
@ -120,10 +62,4 @@ export class SectionPanel extends Component<Props, State> {
</Fragment>
);
};
public toggleCollapsed = () => {
this.setState({
collapsed: !this.state.collapsed,
});
};
}

View file

@ -14302,8 +14302,6 @@
"xpack.security.management.editRole.elasticSearchPrivileges.manageRoleActionsDescription": "このロールがクラスターに対して実行できる操作を管理します。 ",
"xpack.security.management.editRole.elasticSearchPrivileges.runAsPrivilegesTitle": "権限として実行",
"xpack.security.management.editRole.featureTable.customizeSubFeaturePrivilegesSwitchLabel": "サブ機能権限をカスタマイズする",
"xpack.security.management.editRole.featureTable.enabledRoleFeaturesEnabledColumnTitle": "権限",
"xpack.security.management.editRole.featureTable.enabledRoleFeaturesFeatureColumnTitle": "機能",
"xpack.security.management.editRole.featureTable.privilegeCustomizationTooltip": "機能でサブ機能の権限がカスタマイズされています。この行を展開すると詳細が表示されます。",
"xpack.security.management.editRole.indexPrivilegeForm.deleteSpacePrivilegeAriaLabel": "インデックスの権限を削除",
"xpack.security.management.editRole.indexPrivilegeForm.grantedDocumentsQueryFormRowLabel": "提供されたドキュメントのクエリ",
@ -14345,35 +14343,24 @@
"xpack.security.management.editRole.spaceAwarePrivilegeForm.howToViewAllAvailableSpacesDescription": "利用可能なすべてのスペースを表示する権限がありません。",
"xpack.security.management.editRole.spaceAwarePrivilegeForm.insufficientPrivilegesDescription": "権限が不十分です",
"xpack.security.management.editRole.spaceAwarePrivilegeForm.kibanaAdminTitle": "kibana_admin",
"xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDetails": "選択されたスペースの全機能への完全アクセスを許可します。",
"xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDisplay": "すべて",
"xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDropdownDisplay": "すべて",
"xpack.security.management.editRole.spacePrivilegeForm.cancelButton": "キャンセル",
"xpack.security.management.editRole.spacePrivilegeForm.customizeFeaturePrivilegeDescription": "機能ごとに権限のレベルを上げます。機能によってはスペースごとに非表示になっているか、グローバルスペース権限による影響を受けているものもあります。",
"xpack.security.management.editRole.spacePrivilegeForm.customizeFeaturePrivileges": "機能ごとにカスタマイズ",
"xpack.security.management.editRole.spacePrivilegeForm.customPrivilegeDetails": "選択されたスペースの機能ごとにアクセスをカスタマイズします",
"xpack.security.management.editRole.spacePrivilegeForm.customPrivilegeDisplay": "カスタム",
"xpack.security.management.editRole.spacePrivilegeForm.customPrivilegeDropdownDisplay": "カスタム",
"xpack.security.management.editRole.spacePrivilegeForm.featurePrivilegeSummaryDescription": "機能によってはスペースごとに非表示になっているか、グローバルスペース権限による影響を受けているものもあります。",
"xpack.security.management.editRole.spacePrivilegeForm.globalPrivilegeNotice": "これらの権限はすべての現在および未来のスペースに適用されます。",
"xpack.security.management.editRole.spacePrivilegeForm.globalPrivilegeWarning": "グローバル権限の作成は他のスペース権限に影響を与える可能性があります。",
"xpack.security.management.editRole.spacePrivilegeForm.modalTitle": "スペース権限",
"xpack.security.management.editRole.spacePrivilegeForm.privilegeSelectorFormLabel": "権限",
"xpack.security.management.editRole.spacePrivilegeForm.readPrivilegeDetails": "選択されたスペースの全機能への読み込み専用アクセスを許可します。",
"xpack.security.management.editRole.spacePrivilegeForm.readPrivilegeDisplay": "読み込み",
"xpack.security.management.editRole.spacePrivilegeForm.readPrivilegeDropdownDisplay": "読み込み",
"xpack.security.management.editRole.spacePrivilegeForm.spaceSelectorFormLabel": "スペース",
"xpack.security.management.editRole.spacePrivilegeForm.summaryOfFeaturePrivileges": "機能権限のサマリー",
"xpack.security.management.editRole.spacePrivilegeForm.supersededWarning": "宣言された権限は、構成済みグローバル権限よりも許容度が低くなります。権限サマリーを表示すると有効な権限がわかります。",
"xpack.security.management.editRole.spacePrivilegeForm.supersededWarningTitle": "グローバル権限に置き換え",
"xpack.security.management.editRole.spacePrivilegeMatrix.globalSpaceName": "グローバル",
"xpack.security.management.editRole.spacePrivilegeMatrix.showAllSpacesLink": "(すべてのスペース)",
"xpack.security.management.editRole.spacePrivilegeMatrix.showNMoreSpacesLink": "他 {count} 件",
"xpack.security.management.editRole.spacePrivilegeSection.addSpacePrivilegeButton": "スペース権限を追加",
"xpack.security.management.editRole.spacePrivilegeSection.noAccessToKibanaTitle": "このロールは Kibana へのアクセスを許可しません",
"xpack.security.management.editRole.spacePrivilegeTable.deletePrivilegesLabel": "次のスペースの権限を削除: {spaceNames}",
"xpack.security.management.editRole.spacePrivilegeTable.editPrivilegesLabel": "次のスペースの権限を編集: {spaceNames}",
"xpack.security.management.editRole.spacePrivilegeTable.showAllSpacesLink": "スペースを表示",
"xpack.security.management.editRole.spacePrivilegeTable.showLessSpacesLink": "縮小表示",
"xpack.security.management.editRole.spacePrivilegeTable.showNMoreSpacesLink": "他 {count} 件",
"xpack.security.management.editRole.spacePrivilegeTable.supersededPrivilegeWarning": "権限は、構成されたグローバル権限に置き換わります。権限サマリーを表示すると有効な権限がわかります。",
@ -17360,10 +17347,6 @@
"xpack.spaces.management.advancedSettingsSubtitle.applyingSettingsOnPageToSpaceDescription": "このページの設定は、別途指定されていない限り {spaceName}’スペースに適用されます。’",
"xpack.spaces.management.advancedSettingsTitle.settingsTitle": "設定",
"xpack.spaces.management.breadcrumb": "スペース",
"xpack.spaces.management.collapsiblePanel.hideLinkDescription": "{title} を非表示",
"xpack.spaces.management.collapsiblePanel.hideLinkText": "非表示",
"xpack.spaces.management.collapsiblePanel.showLinkDescription": "{title} を表示",
"xpack.spaces.management.collapsiblePanel.showLinkText": "表示",
"xpack.spaces.management.confirmAlterActiveSpaceModal.cancelButton": "キャンセル",
"xpack.spaces.management.confirmAlterActiveSpaceModal.reloadWarningMessage": "このスペースで表示される機能を更新しました。保存後にページが更新されます。",
"xpack.spaces.management.confirmAlterActiveSpaceModal.title": "スペースの更新の確認",

View file

@ -14311,8 +14311,6 @@
"xpack.security.management.editRole.elasticSearchPrivileges.manageRoleActionsDescription": "管理此角色可以对您的集群执行的操作。 ",
"xpack.security.management.editRole.elasticSearchPrivileges.runAsPrivilegesTitle": "运行身份权限",
"xpack.security.management.editRole.featureTable.customizeSubFeaturePrivilegesSwitchLabel": "定制子功能权限",
"xpack.security.management.editRole.featureTable.enabledRoleFeaturesEnabledColumnTitle": "权限",
"xpack.security.management.editRole.featureTable.enabledRoleFeaturesFeatureColumnTitle": "功能",
"xpack.security.management.editRole.featureTable.privilegeCustomizationTooltip": "功能已定制子功能权限。展开此行以了解更多信息。",
"xpack.security.management.editRole.indexPrivilegeForm.deleteSpacePrivilegeAriaLabel": "删除索引权限",
"xpack.security.management.editRole.indexPrivilegeForm.grantedDocumentsQueryFormRowLabel": "已授权文档查询",
@ -14354,35 +14352,24 @@
"xpack.security.management.editRole.spaceAwarePrivilegeForm.howToViewAllAvailableSpacesDescription": "您无权查看所有可用工作区。",
"xpack.security.management.editRole.spaceAwarePrivilegeForm.insufficientPrivilegesDescription": "权限不足",
"xpack.security.management.editRole.spaceAwarePrivilegeForm.kibanaAdminTitle": "kibana_admin",
"xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDetails": "授予对选定工作区所有功能的完全访问权限。",
"xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDisplay": "全部",
"xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDropdownDisplay": "全部",
"xpack.security.management.editRole.spacePrivilegeForm.cancelButton": "取消",
"xpack.security.management.editRole.spacePrivilegeForm.customizeFeaturePrivilegeDescription": "按功能提高权限级别。某些功能可能被工作区隐藏或受全局工作区权限影响。",
"xpack.security.management.editRole.spacePrivilegeForm.customizeFeaturePrivileges": "按功能定制",
"xpack.security.management.editRole.spacePrivilegeForm.customPrivilegeDetails": "在选定工作区中按功能定制访问权限。",
"xpack.security.management.editRole.spacePrivilegeForm.customPrivilegeDisplay": "定制",
"xpack.security.management.editRole.spacePrivilegeForm.customPrivilegeDropdownDisplay": "定制",
"xpack.security.management.editRole.spacePrivilegeForm.featurePrivilegeSummaryDescription": "某些功能可能被工作区隐藏或受全局工作区权限影响。",
"xpack.security.management.editRole.spacePrivilegeForm.globalPrivilegeNotice": "这些权限将应用到所有当前和未来工作区。",
"xpack.security.management.editRole.spacePrivilegeForm.globalPrivilegeWarning": "创建全局权限可能会影响您的其他工作区权限。",
"xpack.security.management.editRole.spacePrivilegeForm.modalTitle": "工作区权限",
"xpack.security.management.editRole.spacePrivilegeForm.privilegeSelectorFormLabel": "权限",
"xpack.security.management.editRole.spacePrivilegeForm.readPrivilegeDetails": "授予对选定工作区所有功能的只读访问权限。",
"xpack.security.management.editRole.spacePrivilegeForm.readPrivilegeDisplay": "读取",
"xpack.security.management.editRole.spacePrivilegeForm.readPrivilegeDropdownDisplay": "读取",
"xpack.security.management.editRole.spacePrivilegeForm.spaceSelectorFormLabel": "工作区",
"xpack.security.management.editRole.spacePrivilegeForm.summaryOfFeaturePrivileges": "功能权限的摘要",
"xpack.security.management.editRole.spacePrivilegeForm.supersededWarning": "声明的权限相对配置的全局权限有较小的宽容度。查看权限摘要以查看有效的权限。",
"xpack.security.management.editRole.spacePrivilegeForm.supersededWarningTitle": "已由全局权限取代",
"xpack.security.management.editRole.spacePrivilegeMatrix.globalSpaceName": "全局",
"xpack.security.management.editRole.spacePrivilegeMatrix.showAllSpacesLink": "(所有工作区)",
"xpack.security.management.editRole.spacePrivilegeMatrix.showNMoreSpacesLink": "另外 {count} 个",
"xpack.security.management.editRole.spacePrivilegeSection.addSpacePrivilegeButton": "添加工作区权限",
"xpack.security.management.editRole.spacePrivilegeSection.noAccessToKibanaTitle": "此角色未授予对 Kibana 的访问权限",
"xpack.security.management.editRole.spacePrivilegeTable.deletePrivilegesLabel": "删除以下工作区的权限:{spaceNames}。",
"xpack.security.management.editRole.spacePrivilegeTable.editPrivilegesLabel": "编辑以下工作区的权限:{spaceNames}。",
"xpack.security.management.editRole.spacePrivilegeTable.showAllSpacesLink": "显示工作区",
"xpack.security.management.editRole.spacePrivilegeTable.showLessSpacesLink": "显示更少",
"xpack.security.management.editRole.spacePrivilegeTable.showNMoreSpacesLink": "另外 {count} 个",
"xpack.security.management.editRole.spacePrivilegeTable.supersededPrivilegeWarning": "权限已由配置的全局权限取代。查看权限摘要以查看有效的权限。",
@ -17370,10 +17357,6 @@
"xpack.spaces.management.advancedSettingsSubtitle.applyingSettingsOnPageToSpaceDescription": "除非已指定,否则此页面上的设置适用于 {spaceName} 空间。",
"xpack.spaces.management.advancedSettingsTitle.settingsTitle": "设置",
"xpack.spaces.management.breadcrumb": "工作区",
"xpack.spaces.management.collapsiblePanel.hideLinkDescription": "隐藏 {title}",
"xpack.spaces.management.collapsiblePanel.hideLinkText": "隐藏",
"xpack.spaces.management.collapsiblePanel.showLinkDescription": "显示 {title}",
"xpack.spaces.management.collapsiblePanel.showLinkText": "显示",
"xpack.spaces.management.confirmAlterActiveSpaceModal.cancelButton": "取消",
"xpack.spaces.management.confirmAlterActiveSpaceModal.reloadWarningMessage": "您已更新此工作区中的可见功能。保存后,您的页面将重新加载。",
"xpack.spaces.management.confirmAlterActiveSpaceModal.title": "确认更新工作区",

View file

@ -429,10 +429,7 @@ export function SecurityPageProvider({ getService, getPageObjects }: FtrProvider
const globalSpaceOption = await find.byCssSelector(`#spaceOption_\\*`);
await globalSpaceOption.click();
await testSubjects.click('basePrivilegeComboBox');
const privilegeOption = await find.byCssSelector(`#basePrivilege_${privilegeName}`);
await privilegeOption.click();
await testSubjects.click(`basePrivilege_${privilegeName}`);
await testSubjects.click('createSpacePrivilegeButton');
}