[Security Solution][Endpoint] Require all spaces flag for sub features (#143733)

* Adds requireAllSpaces flag for subfeatures.

* fixes ts errors

* Adds unit test on sub features form UI

* Adds unit test for validateKibanaPrivileges function with subfeatures

* Fixes failing tests

* Rename some vars and reorder return null. Also skip two tests that are not working as expected

* Reorder if condition for performance optimisation

* Fixes unit test

* PR feedback - remove useMemo and use a function

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
David Sánchez 2022-10-26 15:35:45 +02:00 committed by GitHub
parent 0c1ae8c1c2
commit 217d2d0c4e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 609 additions and 22 deletions

View file

@ -16,6 +16,17 @@ export interface SubFeatureConfig {
/** Display name for this sub-feature */
name: string;
/**
* Whether or not this privilege should only be granted to `All Spaces *`. Should be used for features that do not
* support Spaces. Defaults to `false`.
*/
requireAllSpaces?: boolean;
/**
* Optional message to display on the Role Management screen when configuring permissions for this feature.
*/
privilegesTooltip?: string;
/** Collection of privilege groups */
privilegeGroups: readonly SubFeaturePrivilegeGroupConfig[];
}
@ -90,6 +101,10 @@ export class SubFeature {
return this.config.privilegeGroups;
}
public get requireAllSpaces() {
return this.config.requireAllSpaces ?? false;
}
public toRaw() {
return { ...this.config };
}

View file

@ -163,6 +163,8 @@ const kibanaMutuallyExclusiveSubFeaturePrivilegeSchema =
const kibanaSubFeatureSchema = schema.object({
name: schema.string(),
requireAllSpaces: schema.maybe(schema.boolean()),
privilegesTooltip: schema.maybe(schema.string()),
privilegeGroups: schema.maybe(
schema.arrayOf(
schema.oneOf([

View file

@ -226,4 +226,31 @@ export const kibanaFeatures = [
},
],
}),
createFeature({
id: 'with_require_all_spaces_sub_features',
name: 'Require all spaces Sub Features',
subFeatures: [
{
name: 'Require all spaces Sub Feature',
requireAllSpaces: true,
privilegeGroups: [
{
groupType: 'mutually_exclusive',
privileges: [
{
id: 'cool_toggle_1',
name: 'Cool toggle 1',
includeIn: 'read',
savedObject: {
all: [],
read: [],
},
ui: ['cool_toggle_1-ui'],
},
],
},
],
},
],
}),
];

View file

@ -107,6 +107,10 @@ describe('FeatureTable', () => {
primaryFeaturePrivilege: 'none',
subFeaturePrivileges: [],
},
with_require_all_spaces_sub_features: {
primaryFeaturePrivilege: 'none',
subFeaturePrivileges: [],
},
});
});
@ -157,6 +161,14 @@ describe('FeatureTable', () => {
}
: { subFeaturePrivileges: [] }),
},
with_require_all_spaces_sub_features: {
primaryFeaturePrivilege: 'all',
...(canCustomizeSubFeaturePrivileges
? {
subFeaturePrivileges: ['cool_toggle_1'],
}
: { subFeaturePrivileges: [] }),
},
});
});
@ -208,6 +220,10 @@ describe('FeatureTable', () => {
}
: { subFeaturePrivileges: [] }),
},
with_require_all_spaces_sub_features: {
primaryFeaturePrivilege: 'none',
subFeaturePrivileges: [],
},
});
});
@ -302,6 +318,10 @@ describe('FeatureTable', () => {
primaryFeaturePrivilege: 'read',
subFeaturePrivileges: ['cool_all'],
},
with_require_all_spaces_sub_features: {
primaryFeaturePrivilege: 'none',
subFeaturePrivileges: [],
},
});
});
@ -684,6 +704,10 @@ describe('FeatureTable', () => {
'cool_all',
],
},
with_require_all_spaces_sub_features: {
primaryFeaturePrivilege: 'none',
subFeaturePrivileges: [],
},
});
});
@ -722,6 +746,10 @@ describe('FeatureTable', () => {
primaryFeaturePrivilege: 'all',
subFeaturePrivileges: [],
},
with_require_all_spaces_sub_features: {
primaryFeaturePrivilege: 'none',
subFeaturePrivileges: [],
},
});
});
@ -760,6 +788,10 @@ describe('FeatureTable', () => {
primaryFeaturePrivilege: 'read',
subFeaturePrivileges: [],
},
with_require_all_spaces_sub_features: {
primaryFeaturePrivilege: 'none',
subFeaturePrivileges: [],
},
});
});
@ -888,6 +920,10 @@ describe('FeatureTable', () => {
primaryFeaturePrivilege: 'none',
subFeaturePrivileges: [],
},
with_require_all_spaces_sub_features: {
primaryFeaturePrivilege: 'none',
subFeaturePrivileges: [],
},
});
});

View file

@ -250,6 +250,7 @@ export class FeatureTable extends Component<Props, State> {
selectedFeaturePrivileges={
this.props.role.kibana[this.props.privilegeIndex].feature[feature.id] ?? []
}
allSpacesSelected={this.props.allSpacesSelected}
disabled={this.props.disabled}
licenseAllowsSubFeatPrivCustomization={
this.props.canCustomizeSubFeaturePrivileges

View file

@ -49,6 +49,7 @@ describe('FeatureTableExpandedRow', () => {
selectedFeaturePrivileges={['minimal_read']}
onChange={jest.fn()}
licenseAllowsSubFeatPrivCustomization={false}
allSpacesSelected={false}
/>
);
@ -86,6 +87,7 @@ describe('FeatureTableExpandedRow', () => {
selectedFeaturePrivileges={['none']}
onChange={jest.fn()}
licenseAllowsSubFeatPrivCustomization={true}
allSpacesSelected={false}
/>
);
@ -118,6 +120,7 @@ describe('FeatureTableExpandedRow', () => {
selectedFeaturePrivileges={['minimal_read']}
onChange={jest.fn()}
licenseAllowsSubFeatPrivCustomization={true}
allSpacesSelected={false}
/>
);
@ -153,6 +156,7 @@ describe('FeatureTableExpandedRow', () => {
selectedFeaturePrivileges={['read']}
onChange={jest.fn()}
licenseAllowsSubFeatPrivCustomization={true}
allSpacesSelected={false}
/>
);
@ -186,6 +190,7 @@ describe('FeatureTableExpandedRow', () => {
selectedFeaturePrivileges={['read']}
onChange={jest.fn()}
licenseAllowsSubFeatPrivCustomization={true}
allSpacesSelected={false}
/>
);
@ -223,6 +228,7 @@ describe('FeatureTableExpandedRow', () => {
selectedFeaturePrivileges={['read']}
onChange={onChange}
licenseAllowsSubFeatPrivCustomization={true}
allSpacesSelected={false}
/>
);
@ -263,6 +269,7 @@ describe('FeatureTableExpandedRow', () => {
selectedFeaturePrivileges={['minimal_read', 'cool_read', 'cool_toggle_2']}
onChange={onChange}
licenseAllowsSubFeatPrivCustomization={true}
allSpacesSelected={false}
/>
);
@ -272,4 +279,76 @@ describe('FeatureTableExpandedRow', () => {
expect(onChange).toHaveBeenCalledWith('with_sub_features', ['read']);
});
it('require all spaces enabled and allSpacesSelected is false: option is disabled', () => {
const role = createRole([
{
base: [],
feature: {
with_require_all_spaces_sub_features: ['cool_toggle_1'],
},
spaces: ['foo'],
},
]);
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
const feature = kibanaPrivileges.getSecuredFeature('with_require_all_spaces_sub_features');
const onChange = jest.fn();
const wrapper = mountWithIntl(
<FeatureTableExpandedRow
feature={feature}
privilegeIndex={0}
privilegeCalculator={calculator}
selectedFeaturePrivileges={['minimal_all']}
onChange={onChange}
licenseAllowsSubFeatPrivCustomization={true}
allSpacesSelected={false}
/>
);
act(() => {
findTestSubject(wrapper, 'customizeSubFeaturePrivileges').simulate('click');
});
const object = wrapper.find('SubFeatureForm');
expect(object.props()).toMatchObject({ disabled: true });
});
it('require all spaces enabled and allSpacesSelected is true: option is enabled', () => {
const role = createRole([
{
base: [],
feature: {
with_require_all_spaces_sub_features: ['cool_toggle_1'],
},
spaces: ['foo'],
},
]);
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
const feature = kibanaPrivileges.getSecuredFeature('with_require_all_spaces_sub_features');
const onChange = jest.fn();
const wrapper = mountWithIntl(
<FeatureTableExpandedRow
feature={feature}
privilegeIndex={0}
privilegeCalculator={calculator}
selectedFeaturePrivileges={['minimal_all']}
onChange={onChange}
licenseAllowsSubFeatPrivCustomization={true}
allSpacesSelected={true}
/>
);
act(() => {
findTestSubject(wrapper, 'customizeSubFeaturePrivileges').simulate('click');
});
const object = wrapper.find('SubFeatureForm');
expect(object.props()).toMatchObject({ disabled: false });
});
});

View file

@ -21,6 +21,7 @@ interface Props {
privilegeCalculator: PrivilegeFormCalculator;
privilegeIndex: number;
selectedFeaturePrivileges: string[];
allSpacesSelected: boolean;
disabled?: boolean;
licenseAllowsSubFeatPrivCustomization: boolean;
onChange: (featureId: string, featurePrivileges: string[]) => void;
@ -32,6 +33,7 @@ export const FeatureTableExpandedRow = ({
privilegeIndex,
privilegeCalculator,
selectedFeaturePrivileges,
allSpacesSelected,
disabled,
licenseAllowsSubFeatPrivCustomization,
}: Props) => {
@ -110,6 +112,8 @@ export const FeatureTableExpandedRow = ({
</div>
</EuiFlexItem>
{feature.getSubFeatures().map((subFeature) => {
const isDisabledDueToSpaceSelection = subFeature.requireAllSpaces && !allSpacesSelected;
return (
<EuiFlexItem key={subFeature.name}>
<SubFeatureForm
@ -119,7 +123,7 @@ export const FeatureTableExpandedRow = ({
subFeature={subFeature}
onChange={(updatedPrivileges) => onChange(feature.id, updatedPrivileges)}
selectedFeaturePrivileges={selectedFeaturePrivileges}
disabled={disabled || !isCustomizing}
disabled={disabled || !isCustomizing || isDisabledDueToSpaceSelection}
/>
</EuiFlexItem>
);

View file

@ -5,7 +5,14 @@
* 2.0.
*/
import { EuiButtonGroup, EuiCheckbox, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import {
EuiButtonGroup,
EuiCheckbox,
EuiFlexGroup,
EuiFlexItem,
EuiIconTip,
EuiText,
} from '@elastic/eui';
import React from 'react';
import { i18n } from '@kbn/i18n';
@ -33,6 +40,27 @@ export const SubFeatureForm = (props: Props) => {
.getPrivilegeGroups()
.filter((group) => group.privileges.length > 0);
const getTooltip = () => {
if (!props.subFeature.privilegesTooltip) {
return null;
}
const tooltipContent = (
<EuiText>
<p>{props.subFeature.privilegesTooltip}</p>
</EuiText>
);
return (
<EuiIconTip
iconProps={{
className: 'eui-alignTop',
}}
type="iInCircle"
color="subdued"
content={tooltipContent}
/>
);
};
if (groupsWithPrivileges.length === 0) {
return null;
}
@ -40,7 +68,9 @@ export const SubFeatureForm = (props: Props) => {
return (
<EuiFlexGroup>
<EuiFlexItem>
<EuiText size="s">{props.subFeature.name}</EuiText>
<EuiText size="s">
{props.subFeature.name} {getTooltip()}
</EuiText>
</EuiFlexItem>
<EuiFlexItem>{groupsWithPrivileges.map(renderPrivilegeGroup)}</EuiFlexItem>
</EuiFlexGroup>

View file

@ -53,6 +53,11 @@ describe('PrivilegeSummaryCalculator', () => {
primary: undefined,
subFeature: [],
},
with_require_all_spaces_sub_features: {
hasCustomizedSubFeaturePrivileges: false,
primary: undefined,
subFeature: [],
},
});
});
@ -99,6 +104,13 @@ describe('PrivilegeSummaryCalculator', () => {
}),
subFeature: ['cool_all', 'cool_read', 'cool_toggle_1', 'cool_toggle_2'],
},
with_require_all_spaces_sub_features: {
hasCustomizedSubFeaturePrivileges: false,
primary: expect.objectContaining({
id: 'all',
}),
subFeature: ['cool_toggle_1'],
},
});
});
@ -155,6 +167,13 @@ describe('PrivilegeSummaryCalculator', () => {
'cool_excluded_toggle',
],
},
with_require_all_spaces_sub_features: {
hasCustomizedSubFeaturePrivileges: false,
primary: expect.objectContaining({
id: 'all',
}),
subFeature: ['cool_toggle_1'],
},
});
});
@ -214,6 +233,13 @@ describe('PrivilegeSummaryCalculator', () => {
'cool_excluded_toggle',
],
},
with_require_all_spaces_sub_features: {
hasCustomizedSubFeaturePrivileges: false,
primary: expect.objectContaining({
id: 'all',
}),
subFeature: ['cool_toggle_1'],
},
});
});
@ -255,6 +281,13 @@ describe('PrivilegeSummaryCalculator', () => {
}),
subFeature: ['cool_all', 'cool_read', 'cool_toggle_1', 'cool_toggle_2'],
},
with_require_all_spaces_sub_features: {
hasCustomizedSubFeaturePrivileges: false,
primary: expect.objectContaining({
id: 'all',
}),
subFeature: ['cool_toggle_1'],
},
});
});
@ -294,6 +327,11 @@ describe('PrivilegeSummaryCalculator', () => {
primary: undefined,
subFeature: [],
},
with_require_all_spaces_sub_features: {
hasCustomizedSubFeaturePrivileges: false,
primary: undefined,
subFeature: [],
},
});
});
@ -333,6 +371,11 @@ describe('PrivilegeSummaryCalculator', () => {
primary: undefined,
subFeature: [],
},
with_require_all_spaces_sub_features: {
hasCustomizedSubFeaturePrivileges: false,
primary: undefined,
subFeature: [],
},
});
});
});

View file

@ -90,6 +90,15 @@ const expectNoPrivileges = (displayedPrivileges: any, expectSubFeatures: boolean
}),
},
},
with_require_all_spaces_sub_features: {
'*': {
hasCustomizedSubFeaturePrivileges: false,
primaryFeaturePrivilege: 'None',
...maybeExpectSubFeaturePrivileges(expectSubFeatures, {
'Require all spaces Sub Feature': [],
}),
},
},
});
};
@ -248,6 +257,15 @@ describe('PrivilegeSummaryTable', () => {
}),
},
},
with_require_all_spaces_sub_features: {
'*': {
hasCustomizedSubFeaturePrivileges: false,
primaryFeaturePrivilege: 'All',
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
'Require all spaces Sub Feature': ['Cool toggle 1'],
}),
},
},
});
});
@ -310,6 +328,15 @@ describe('PrivilegeSummaryTable', () => {
}),
},
},
with_require_all_spaces_sub_features: {
'*': {
hasCustomizedSubFeaturePrivileges: false,
primaryFeaturePrivilege: 'All',
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
'Require all spaces Sub Feature': ['Cool toggle 1'],
}),
},
},
});
});
@ -370,6 +397,15 @@ describe('PrivilegeSummaryTable', () => {
}),
},
},
with_require_all_spaces_sub_features: {
'default, space-1': {
hasCustomizedSubFeaturePrivileges: false,
primaryFeaturePrivilege: 'All',
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
'Require all spaces Sub Feature': ['Cool toggle 1'],
}),
},
},
});
});
@ -432,6 +468,15 @@ describe('PrivilegeSummaryTable', () => {
}),
},
},
with_require_all_spaces_sub_features: {
'default, space-1': {
hasCustomizedSubFeaturePrivileges: false,
primaryFeaturePrivilege: 'None',
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
'Require all spaces Sub Feature': [],
}),
},
},
});
});
@ -522,6 +567,22 @@ describe('PrivilegeSummaryTable', () => {
}),
},
},
with_require_all_spaces_sub_features: {
'*': {
hasCustomizedSubFeaturePrivileges: false,
primaryFeaturePrivilege: 'Read',
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
'Require all spaces Sub Feature': ['Cool toggle 1'],
}),
},
'default, space-1': {
hasCustomizedSubFeaturePrivileges: false,
primaryFeaturePrivilege: 'All',
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
'Require all spaces Sub Feature': ['Cool toggle 1'],
}),
},
},
});
});
@ -614,6 +675,22 @@ describe('PrivilegeSummaryTable', () => {
}),
},
},
with_require_all_spaces_sub_features: {
'*': {
hasCustomizedSubFeaturePrivileges: false,
primaryFeaturePrivilege: 'Read',
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
'Require all spaces Sub Feature': ['Cool toggle 1'],
}),
},
'default, space-1': {
hasCustomizedSubFeaturePrivileges: false,
primaryFeaturePrivilege: 'Read',
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
'Require all spaces Sub Feature': ['Cool toggle 1'],
}),
},
},
});
});
@ -706,6 +783,22 @@ describe('PrivilegeSummaryTable', () => {
}),
},
},
with_require_all_spaces_sub_features: {
'*': {
hasCustomizedSubFeaturePrivileges: false,
primaryFeaturePrivilege: 'None',
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
'Require all spaces Sub Feature': [],
}),
},
'default, space-1': {
hasCustomizedSubFeaturePrivileges: false,
primaryFeaturePrivilege: 'Read',
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
'Require all spaces Sub Feature': ['Cool toggle 1'],
}),
},
},
});
});
@ -800,6 +893,22 @@ describe('PrivilegeSummaryTable', () => {
}),
},
},
with_require_all_spaces_sub_features: {
'*': {
hasCustomizedSubFeaturePrivileges: false,
primaryFeaturePrivilege: 'None',
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
'Require all spaces Sub Feature': [],
}),
},
'default, space-1': {
hasCustomizedSubFeaturePrivileges: false,
primaryFeaturePrivilege: 'None',
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
'Require all spaces Sub Feature': [],
}),
},
},
});
});
@ -925,6 +1034,29 @@ describe('PrivilegeSummaryTable', () => {
}),
},
},
with_require_all_spaces_sub_features: {
'*': {
hasCustomizedSubFeaturePrivileges: false,
primaryFeaturePrivilege: 'Read',
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
'Require all spaces Sub Feature': ['Cool toggle 1'],
}),
},
default: {
hasCustomizedSubFeaturePrivileges: false,
primaryFeaturePrivilege: 'All',
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
'Require all spaces Sub Feature': ['Cool toggle 1'],
}),
},
'space-1, space-2': {
hasCustomizedSubFeaturePrivileges: false,
primaryFeaturePrivilege: 'Read',
...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, {
'Require all spaces Sub Feature': ['Cool toggle 1'],
}),
},
},
});
});
});

View file

@ -79,6 +79,10 @@ describe('PrivilegeSpaceForm', () => {
"primaryFeaturePrivilege": "none",
"subFeaturePrivileges": Array [],
},
"with_require_all_spaces_sub_features": Object {
"primaryFeaturePrivilege": "none",
"subFeaturePrivileges": Array [],
},
"with_sub_features": Object {
"primaryFeaturePrivilege": "none",
"subFeaturePrivileges": Array [],
@ -129,6 +133,12 @@ describe('PrivilegeSpaceForm', () => {
"primaryFeaturePrivilege": "all",
"subFeaturePrivileges": Array [],
},
"with_require_all_spaces_sub_features": Object {
"primaryFeaturePrivilege": "all",
"subFeaturePrivileges": Array [
"cool_toggle_1",
],
},
"with_sub_features": Object {
"primaryFeaturePrivilege": "all",
"subFeaturePrivileges": Array [
@ -185,6 +195,10 @@ describe('PrivilegeSpaceForm', () => {
"primaryFeaturePrivilege": "none",
"subFeaturePrivileges": Array [],
},
"with_require_all_spaces_sub_features": Object {
"primaryFeaturePrivilege": "none",
"subFeaturePrivileges": Array [],
},
"with_sub_features": Object {
"primaryFeaturePrivilege": "read",
"subFeaturePrivileges": Array [
@ -286,6 +300,10 @@ describe('PrivilegeSpaceForm', () => {
"primaryFeaturePrivilege": "none",
"subFeaturePrivileges": Array [],
},
"with_require_all_spaces_sub_features": Object {
"primaryFeaturePrivilege": "none",
"subFeaturePrivileges": Array [],
},
"with_sub_features": Object {
"primaryFeaturePrivilege": "read",
"subFeaturePrivileges": Array [
@ -346,6 +364,7 @@ describe('PrivilegeSpaceForm', () => {
with_excluded_sub_features: ['read'],
no_sub_features: ['read'],
with_sub_features: ['read'],
with_require_all_spaces_sub_features: ['read'],
},
spaces: ['foo'],
},
@ -451,6 +470,7 @@ describe('PrivilegeSpaceForm', () => {
with_excluded_sub_features: ['read'],
no_sub_features: ['read'],
with_sub_features: ['read'],
with_require_all_spaces_sub_features: ['read'],
},
spaces: ['foo'],
},
@ -493,6 +513,7 @@ describe('PrivilegeSpaceForm', () => {
no_sub_features: ['all'],
no_sub_features_disabled_read: ['all'],
with_sub_features: ['all'],
with_require_all_spaces_sub_features: ['all'],
},
spaces: ['foo'],
},
@ -569,6 +590,7 @@ describe('PrivilegeSpaceForm', () => {
no_sub_features: ['read'],
no_sub_features_require_all_space: ['read'],
with_sub_features: ['read'],
with_require_all_spaces_sub_features: ['read'],
},
spaces: ['foo'],
},
@ -613,6 +635,7 @@ describe('PrivilegeSpaceForm', () => {
with_excluded_sub_features: ['all'],
no_sub_features: ['all'],
with_sub_features: ['all'],
with_require_all_spaces_sub_features: ['all'],
},
spaces: ['foo'],
},
@ -671,6 +694,7 @@ describe('PrivilegeSpaceForm', () => {
no_sub_features: ['all'],
no_sub_features_require_all_space: ['all'],
with_sub_features: ['all'],
with_require_all_spaces_sub_features: ['all'],
},
spaces: ['*'],
},

View file

@ -24,6 +24,7 @@ import {
EuiText,
EuiTitle,
} from '@elastic/eui';
import { remove } from 'lodash';
import React, { Component, Fragment } from 'react';
import { i18n } from '@kbn/i18n';
@ -472,9 +473,22 @@ export class PrivilegeSpaceForm extends Component<Props, State> {
const primaryFeaturePrivilege = securedFeature
?.getPrimaryFeaturePrivileges({ includeMinimalFeaturePrivileges: true })
.find((pfp) => privileges.includes(pfp.id)) ?? { disabled: false, requireAllSpaces: false };
const areAllSpacesSelected = selectedSpaceIds.includes(ALL_SPACES_ID);
if (securedFeature) {
securedFeature.getSubFeatures().forEach((subFeature) => {
subFeature.privileges.forEach((currentPrivilege) => {
if (privileges.includes(currentPrivilege.id)) {
if (subFeature.requireAllSpaces && !areAllSpacesSelected) {
remove(privileges, (privilege) => privilege === currentPrivilege.id);
}
}
});
});
}
const newFeaturePrivileges =
primaryFeaturePrivilege?.disabled ||
(primaryFeaturePrivilege?.requireAllSpaces && !selectedSpaceIds.includes(ALL_SPACES_ID))
(primaryFeaturePrivilege?.requireAllSpaces && !areAllSpacesSelected)
? [] // The primary feature privilege cannot be selected; remove that and any selected sub-feature privileges, too
: privileges;
return {
@ -536,7 +550,12 @@ export class PrivilegeSpaceForm extends Component<Props, State> {
newPrivileges = [nextFeaturePrivilege.id];
feature.getSubFeaturePrivileges().forEach((psf) => {
if (Array.isArray(privileges) && privileges.includes(psf.id)) {
newPrivileges.push(psf.id);
if (
!psf.requireAllSpaces ||
(psf.requireAllSpaces && this.state.selectedSpaceIds.includes(ALL_SPACES_ID))
) {
newPrivileges.push(psf.id);
}
}
});
}

View file

@ -13,6 +13,7 @@ import { SubFeaturePrivilegeGroup } from './sub_feature_privilege_group';
export class SecuredSubFeature extends SubFeature {
public readonly privileges: SubFeaturePrivilege[];
public readonly privilegesTooltip: string;
constructor(
config: SubFeatureConfig,
@ -20,6 +21,8 @@ export class SecuredSubFeature extends SubFeature {
) {
super(config);
this.privilegesTooltip = config.privilegesTooltip || '';
this.privileges = [];
for (const privilege of this.privilegeIterator()) {
this.privileges.push(privilege);

View file

@ -20,4 +20,8 @@ export class SubFeaturePrivilege extends KibanaPrivilege {
public get name() {
return this.subPrivilegeConfig.name;
}
public get requireAllSpaces() {
return this.subPrivilegeConfig.requireAllSpaces ?? false;
}
}

View file

@ -101,6 +101,22 @@ export const validateKibanaPrivileges = (
}
}
kibanaFeature.subFeatures.forEach((subFeature) => {
if (
subFeature.requireAllSpaces &&
!forAllSpaces &&
subFeature.privilegeGroups.some((group) =>
group.privileges.some((privilege) => feature.includes(privilege.id))
)
) {
errors.push(
`Sub-feature privilege [${kibanaFeature.name} - ${
subFeature.name
}] requires all spaces to be selected but received [${priv.spaces.join(',')}]`
);
}
});
return errors;
});
});

View file

@ -466,4 +466,86 @@ describe('validateKibanaPrivileges', () => {
`Feature [foo] does not support privilege [read].`,
]);
});
const fooSubFeature = new KibanaFeature({
id: 'foo',
name: 'Foo',
privileges: {
all: {
savedObject: {
all: [],
read: [],
},
ui: [],
},
read: {
disabled: true,
savedObject: {
all: [],
read: [],
},
ui: [],
},
},
subFeatures: [
{
name: 'Require All Spaces Enabled',
requireAllSpaces: true,
privilegeGroups: [
{
groupType: 'mutually_exclusive',
privileges: [
{
id: 'test',
name: 'foo',
includeIn: 'none',
ui: ['test-ui'],
savedObject: {
all: [],
read: [],
},
},
],
},
],
},
],
app: [],
category: { id: 'foo', label: 'foo' },
});
test('returns no error when subfeature requireAllSpaces enabled and all spaces selected', () => {
expect(
validateKibanaPrivileges(
[fooSubFeature],
[
{
spaces: ['*'],
base: [],
feature: {
foo: ['all', 'test'],
},
},
]
).validationErrors
).toEqual([]);
});
test('returns error when subfeature requireAllSpaces enabled but not all spaces selected', () => {
expect(
validateKibanaPrivileges(
[fooSubFeature],
[
{
spaces: ['foo-space'],
base: [],
feature: {
foo: ['all', 'test'],
},
},
]
).validationErrors
).toEqual([
'Sub-feature privilege [Foo - Require All Spaces Enabled] requires all spaces to be selected but received [foo-space]',
]);
});
});

View file

@ -185,6 +185,13 @@ export const getKibanaPrivilegesFeaturePrivileges = (
subFeatures: experimentalFeatures.endpointRbacEnabled
? [
{
requireAllSpaces: true,
privilegesTooltip: i18n.translate(
'xpack.securitySolution.featureRegistry.subFeatures.endpointList.privilegesTooltip',
{
defaultMessage: 'All Spaces is required for Endpoint List access.',
}
),
name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.endpointList', {
defaultMessage: 'Endpoint List',
}),
@ -195,7 +202,7 @@ export const getKibanaPrivilegesFeaturePrivileges = (
{
api: [`${APP_ID}-writeEndpointList`, `${APP_ID}-readEndpointList`],
id: 'endpoint_list_all',
includeIn: 'all',
includeIn: 'none',
name: 'All',
savedObject: {
all: [],
@ -206,7 +213,7 @@ export const getKibanaPrivilegesFeaturePrivileges = (
{
api: [`${APP_ID}-readEndpointList`],
id: 'endpoint_list_read',
includeIn: 'read',
includeIn: 'none',
name: 'Read',
savedObject: {
all: [],
@ -219,6 +226,13 @@ export const getKibanaPrivilegesFeaturePrivileges = (
],
},
{
requireAllSpaces: true,
privilegesTooltip: i18n.translate(
'xpack.securitySolution.featureRegistry.subFeatures.trustedApplications.privilegesTooltip',
{
defaultMessage: 'All Spaces is required for Trusted Applications access.',
}
),
name: i18n.translate(
'xpack.securitySolution.featureRegistry.subFeatures.trustedApplications',
{
@ -232,7 +246,7 @@ export const getKibanaPrivilegesFeaturePrivileges = (
{
api: [`${APP_ID}-writeTrustedApplications`, `${APP_ID}-readTrustedApplications`],
id: 'trusted_applications_all',
includeIn: 'all',
includeIn: 'none',
name: 'All',
savedObject: {
all: [],
@ -243,7 +257,7 @@ export const getKibanaPrivilegesFeaturePrivileges = (
{
api: [`${APP_ID}-readTrustedApplications`],
id: 'trusted_applications_read',
includeIn: 'read',
includeIn: 'none',
name: 'Read',
savedObject: {
all: [],
@ -256,6 +270,13 @@ export const getKibanaPrivilegesFeaturePrivileges = (
],
},
{
requireAllSpaces: true,
privilegesTooltip: i18n.translate(
'xpack.securitySolution.featureRegistry.subFeatures.hostIsolationExceptions.privilegesTooltip',
{
defaultMessage: 'All Spaces is required for Host Isolation Exceptions access.',
}
),
name: i18n.translate(
'xpack.securitySolution.featureRegistry.subFeatures.hostIsolationExceptions',
{
@ -272,7 +293,7 @@ export const getKibanaPrivilegesFeaturePrivileges = (
`${APP_ID}-readHostIsolationExceptions`,
],
id: 'host_isolation_exceptions_all',
includeIn: 'all',
includeIn: 'none',
name: 'All',
savedObject: {
all: [],
@ -283,7 +304,7 @@ export const getKibanaPrivilegesFeaturePrivileges = (
{
api: [`${APP_ID}-readHostIsolationExceptions`],
id: 'host_isolation_exceptions_read',
includeIn: 'read',
includeIn: 'none',
name: 'Read',
savedObject: {
all: [],
@ -296,6 +317,13 @@ export const getKibanaPrivilegesFeaturePrivileges = (
],
},
{
requireAllSpaces: true,
privilegesTooltip: i18n.translate(
'xpack.securitySolution.featureRegistry.subFeatures.blockList.privilegesTooltip',
{
defaultMessage: 'All Spaces is required for Blocklist access.',
}
),
name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.blockList', {
defaultMessage: 'Blocklist',
}),
@ -306,7 +334,7 @@ export const getKibanaPrivilegesFeaturePrivileges = (
{
api: [`${APP_ID}-writeBlocklist`, `${APP_ID}-readBlocklist`],
id: 'blocklist_all',
includeIn: 'all',
includeIn: 'none',
name: 'All',
savedObject: {
all: [],
@ -317,7 +345,7 @@ export const getKibanaPrivilegesFeaturePrivileges = (
{
api: [`${APP_ID}-readBlocklist`],
id: 'blocklist_read',
includeIn: 'read',
includeIn: 'none',
name: 'Read',
savedObject: {
all: [],
@ -330,6 +358,13 @@ export const getKibanaPrivilegesFeaturePrivileges = (
],
},
{
requireAllSpaces: true,
privilegesTooltip: i18n.translate(
'xpack.securitySolution.featureRegistry.subFeatures.eventFilters.privilegesTooltip',
{
defaultMessage: 'All Spaces is required for Event Filters access.',
}
),
name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.eventFilters', {
defaultMessage: 'Event Filters',
}),
@ -340,7 +375,7 @@ export const getKibanaPrivilegesFeaturePrivileges = (
{
api: [`${APP_ID}-writeEventFilters`, `${APP_ID}-readEventFilters`],
id: 'event_filters_all',
includeIn: 'all',
includeIn: 'none',
name: 'All',
savedObject: {
all: [],
@ -351,7 +386,7 @@ export const getKibanaPrivilegesFeaturePrivileges = (
{
api: [`${APP_ID}-readEventFilters`],
id: 'event_filters_read',
includeIn: 'read',
includeIn: 'none',
name: 'Read',
savedObject: {
all: [],
@ -364,6 +399,13 @@ export const getKibanaPrivilegesFeaturePrivileges = (
],
},
{
requireAllSpaces: true,
privilegesTooltip: i18n.translate(
'xpack.securitySolution.featureRegistry.subFeatures.policyManagement.privilegesTooltip',
{
defaultMessage: 'All Spaces is required for Policy Management access.',
}
),
name: i18n.translate(
'xpack.securitySolution.featureRegistry.subFeatures.policyManagement',
{
@ -377,7 +419,7 @@ export const getKibanaPrivilegesFeaturePrivileges = (
{
api: [`${APP_ID}-writePolicyManagement`, `${APP_ID}-readPolicyManagement`],
id: 'policy_management_all',
includeIn: 'all',
includeIn: 'none',
name: 'All',
savedObject: {
all: [],
@ -388,7 +430,7 @@ export const getKibanaPrivilegesFeaturePrivileges = (
{
api: [`${APP_ID}-readPolicyManagement`],
id: 'policy_management_read',
includeIn: 'read',
includeIn: 'none',
name: 'Read',
savedObject: {
all: [],
@ -401,6 +443,13 @@ export const getKibanaPrivilegesFeaturePrivileges = (
],
},
{
requireAllSpaces: true,
privilegesTooltip: i18n.translate(
'xpack.securitySolution.featureRegistry.subFeatures.actionsLogManagement.privilegesTooltip',
{
defaultMessage: 'All Spaces is required for Actions Log Management access.',
}
),
name: i18n.translate(
'xpack.securitySolution.featureRegistry.subFeatures.actionsLogManagement',
{
@ -417,7 +466,7 @@ export const getKibanaPrivilegesFeaturePrivileges = (
`${APP_ID}-readActionsLogManagement`,
],
id: 'actions_log_management_all',
includeIn: 'all',
includeIn: 'none',
name: 'All',
savedObject: {
all: [],
@ -428,7 +477,7 @@ export const getKibanaPrivilegesFeaturePrivileges = (
{
api: [`${APP_ID}-readActionsLogManagement`],
id: 'actions_log_management_read',
includeIn: 'read',
includeIn: 'none',
name: 'Read',
savedObject: {
all: [],
@ -441,6 +490,13 @@ export const getKibanaPrivilegesFeaturePrivileges = (
],
},
{
requireAllSpaces: true,
privilegesTooltip: i18n.translate(
'xpack.securitySolution.featureRegistry.subFeatures.hostIsolation.privilegesTooltip',
{
defaultMessage: 'All Spaces is required for Host Isolation access.',
}
),
name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.hostIsolation', {
defaultMessage: 'Host Isolation',
}),
@ -451,7 +507,7 @@ export const getKibanaPrivilegesFeaturePrivileges = (
{
api: [`${APP_ID}-writeHostIsolation`],
id: 'host_isolation_all',
includeIn: 'all',
includeIn: 'none',
name: 'All',
savedObject: {
all: [],
@ -464,6 +520,13 @@ export const getKibanaPrivilegesFeaturePrivileges = (
],
},
{
requireAllSpaces: true,
privilegesTooltip: i18n.translate(
'xpack.securitySolution.featureRegistry.subFeatures.processOperations.privilegesTooltip',
{
defaultMessage: 'All Spaces is required for Process Operations access.',
}
),
name: i18n.translate(
'xpack.securitySolution.featureRegistry.subFeatures.processOperations',
{
@ -477,7 +540,7 @@ export const getKibanaPrivilegesFeaturePrivileges = (
{
api: [`${APP_ID}-writeProcessOperations`],
id: 'process_operations_all',
includeIn: 'all',
includeIn: 'none',
name: 'All',
savedObject: {
all: [],
@ -490,6 +553,13 @@ export const getKibanaPrivilegesFeaturePrivileges = (
],
},
{
requireAllSpaces: true,
privilegesTooltip: i18n.translate(
'xpack.securitySolution.featureRegistry.subFeatures.fileOperations.privilegesTooltip',
{
defaultMessage: 'All Spaces is required for File Operations access.',
}
),
name: i18n.translate('xpack.securitySolution.featureRegistr.subFeatures.fileOperations', {
defaultMessage: 'File Operations',
}),
@ -500,7 +570,7 @@ export const getKibanaPrivilegesFeaturePrivileges = (
{
api: [`${APP_ID}-writeFileOperations`],
id: 'file_operations_all',
includeIn: 'all',
includeIn: 'none',
name: 'All',
savedObject: {
all: [],