diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.tsx index 0dae2cda807a0..aacbd8f2ef90b 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.tsx @@ -63,11 +63,17 @@ export class FeatureTable extends Component { const items: TableRow[] = features .sort((feature1, feature2) => { - if (feature1.reserved && !feature2.reserved) { + if ( + Object.keys(feature1.privileges).length === 0 && + Object.keys(feature2.privileges).length > 0 + ) { return 1; } - if (feature2.reserved && !feature1.reserved) { + if ( + Object.keys(feature2.privileges).length === 0 && + Object.keys(feature1.privileges).length > 0 + ) { return -1; } @@ -165,9 +171,9 @@ export class FeatureTable extends Component { ), render: (roleEntry: Role, record: TableRow) => { - const { id: featureId, reserved } = record.feature; + const { id: featureId, name: featureName, reserved, privileges } = record.feature; - if (reserved) { + if (reserved && Object.keys(privileges).length === 0) { return {reserved.description}; } @@ -194,8 +200,27 @@ export class FeatureTable extends Component { !this.props.disabled && (allowsNone || enabledFeaturePrivileges.length > 1); if (!canChangePrivilege) { + const assignedBasePrivilege = + this.props.role.kibana[this.props.spacesIndex].base.length > 0; + + const excludedFromBasePrivilegsTooltip = ( + + ); + return ( - + ); } diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__fixtures__/index.ts b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__fixtures__/index.ts new file mode 100644 index 0000000000000..09e449f61356f --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__fixtures__/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { rawKibanaPrivileges } from './raw_kibana_privileges'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__fixtures__/raw_kibana_privileges.ts b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__fixtures__/raw_kibana_privileges.ts new file mode 100644 index 0000000000000..54f9c33aafe28 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__fixtures__/raw_kibana_privileges.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RawKibanaPrivileges } from '../../../../../../../../../common/model'; + +export const rawKibanaPrivileges: RawKibanaPrivileges = { + global: { + all: ['normal-feature-all', 'normal-feature-read', 'just-global-all'], + read: ['normal-feature-read'], + }, + space: { + all: ['normal-feature-all', 'normal-feature-read'], + read: ['normal-feature-read'], + }, + reserved: {}, + features: { + normal: { + all: ['normal-feature-all', 'normal-feature-read'], + read: ['normal-feature-read'], + }, + excludedFromBase: { + all: ['excluded-from-base-all', 'excluded-from-base-read'], + read: ['excluded-from-base-read'], + }, + }, +}; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap index 88c05940a9a24..a11ad5cba226e 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap @@ -203,7 +203,7 @@ exports[` renders without crashing 1`] = ` "value": "basePrivilege_custom", }, Object { - "disabled": true, + "disabled": false, "dropdownDisplay": renders without crashing 1`] = ` Object { "base": Object { "canUnassign": true, - "privileges": Array [], + "privileges": Array [ + "all", + "read", + ], + }, + "feature": Object { + "excludedFromBase": Object { + "canUnassign": true, + "privileges": Array [ + "all", + "read", + ], + }, + "normal": Object { + "canUnassign": true, + "privileges": Array [ + "all", + "read", + ], + }, }, - "feature": Object {}, } } calculatedPrivileges={ @@ -300,7 +318,18 @@ exports[` renders without crashing 1`] = ` "actualPrivilegeSource": 40, "isDirectlyAssigned": true, }, - "feature": Object {}, + "feature": Object { + "excludedFromBase": Object { + "actualPrivilege": "none", + "actualPrivilegeSource": 30, + "isDirectlyAssigned": true, + }, + "normal": Object { + "actualPrivilege": "none", + "actualPrivilegeSource": 30, + "isDirectlyAssigned": true, + }, + }, "reserved": undefined, } } @@ -411,16 +440,63 @@ exports[` renders without crashing 1`] = ` kibanaPrivileges={ KibanaPrivileges { "rawKibanaPrivileges": Object { - "features": Object {}, - "global": Object {}, + "features": Object { + "excludedFromBase": Object { + "all": Array [ + "excluded-from-base-all", + "excluded-from-base-read", + ], + "read": Array [ + "excluded-from-base-read", + ], + }, + "normal": Object { + "all": Array [ + "normal-feature-all", + "normal-feature-read", + ], + "read": Array [ + "normal-feature-read", + ], + }, + }, + "global": Object { + "all": Array [ + "normal-feature-all", + "normal-feature-read", + "just-global-all", + ], + "read": Array [ + "normal-feature-read", + ], + }, "reserved": Object {}, - "space": Object {}, + "space": Object { + "all": Array [ + "normal-feature-all", + "normal-feature-read", + ], + "read": Array [ + "normal-feature-read", + ], + }, }, } } onChange={[Function]} onChangeAll={[Function]} - rankedFeaturePrivileges={Object {}} + rankedFeaturePrivileges={ + Object { + "excludedFromBase": Array [ + "all", + "read", + ], + "normal": Array [ + "all", + "read", + ], + } + } role={ Object { "elasticsearch": Object { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.test.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.test.tsx index 30f4afbe550ce..62e22050132fd 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.test.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiIconTip, EuiText } from '@elastic/eui'; +import { EuiIconTip, EuiText, EuiToolTip } from '@elastic/eui'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { PRIVILEGE_SOURCE } from '../../../../../../../lib/kibana_privilege_calculator'; @@ -26,7 +26,17 @@ describe('PrivilegeDisplay', () => { it('renders a privilege with tooltip, if provided', () => { const wrapper = mountWithIntl( - ahh} iconType={'asterisk'} /> + ahh} /> + ); + expect(wrapper.text().trim()).toEqual('All'); + expect(wrapper.find(EuiToolTip).props()).toMatchObject({ + content: ahh, + }); + }); + + it('renders a privilege with icon tooltip, if provided', () => { + const wrapper = mountWithIntl( + ahh} iconType={'asterisk'} /> ); expect(wrapper.text().trim()).toEqual('All'); expect(wrapper.find(EuiIconTip).props()).toMatchObject({ diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.tsx index af8a201cec716..a354155fcfb0a 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { EuiIcon, EuiIconTip, EuiText, IconType, PropsOf } from '@elastic/eui'; +import { EuiIcon, EuiIconTip, EuiText, IconType, PropsOf, EuiToolTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import _ from 'lodash'; import React, { ReactNode, SFC } from 'react'; @@ -17,6 +17,7 @@ interface Props extends PropsOf { privilege: string | string[] | undefined; explanation?: PrivilegeExplanation; iconType?: IconType; + iconTooltipContent?: ReactNode; tooltipContent?: ReactNode; } @@ -39,13 +40,19 @@ export const PrivilegeDisplay: SFC = (props: Props) => { }; const SimplePrivilegeDisplay: SFC = (props: Props) => { - const { privilege, iconType, tooltipContent, explanation, ...rest } = props; + const { privilege, iconType, iconTooltipContent, explanation, tooltipContent, ...rest } = props; - return ( + const text = ( - {getDisplayValue(privilege)} {getIconTip(iconType, tooltipContent)} + {getDisplayValue(privilege)} {getIconTip(iconType, iconTooltipContent)} ); + + if (tooltipContent) { + return {text}; + } + + return text; }; export const SupersededPrivilegeDisplay: SFC = (props: Props) => { @@ -56,7 +63,7 @@ export const SupersededPrivilegeDisplay: SFC = (props: Props) => { = (props: Props) => { const source = getReadablePrivilegeSource(explanation!.actualPrivilegeSource); - const tooltipContent = ( + const iconTooltipContent = ( = (props: Props) => { /> ); - return ; + return ( + + ); }; PrivilegeDisplay.defaultProps = { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx index 3ef291eafe690..2b7d87f663d72 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx @@ -5,15 +5,24 @@ */ import React from 'react'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { merge } from 'lodash'; +// @ts-ignore +import { findTestSubject } from '@elastic/eui/lib/test'; +import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers'; import { KibanaPrivileges } from '../../../../../../../../common/model'; import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; -import { RoleValidator } from '../../../../lib/validate_role'; import { PrivilegeSpaceForm } from './privilege_space_form'; +import { rawKibanaPrivileges } from './__fixtures__'; -const buildProps = (customProps = {}) => { - return { - mode: 'create' as any, +type RecursivePartial = { + [P in keyof T]?: RecursivePartial; +}; + +const buildProps = ( + overrides?: RecursivePartial +): PrivilegeSpaceForm['props'] => { + const kibanaPrivileges = new KibanaPrivileges(rawKibanaPrivileges); + const defaultProps: PrivilegeSpaceForm['props'] = { spaces: [ { id: 'default', @@ -29,20 +38,8 @@ const buildProps = (customProps = {}) => { disabledFeatures: [], }, ], - kibanaPrivileges: new KibanaPrivileges({ - features: {}, - global: {}, - space: {}, - reserved: {}, - }), - privilegeCalculatorFactory: new KibanaPrivilegeCalculatorFactory( - new KibanaPrivileges({ - global: {}, - features: {}, - space: {}, - reserved: {}, - }) - ), + kibanaPrivileges, + privilegeCalculatorFactory: new KibanaPrivilegeCalculatorFactory(kibanaPrivileges), features: [], role: { name: 'test role', @@ -51,26 +48,79 @@ const buildProps = (customProps = {}) => { indices: [] as any[], run_as: [] as string[], }, - kibana: [ - { - spaces: [], - base: [], - feature: {}, - }, - ], + kibana: [{ spaces: [], base: [], feature: {} }], }, onChange: jest.fn(), onCancel: jest.fn(), - onDelete: jest.fn(), - validator: new RoleValidator(), intl: {} as any, editingIndex: 0, - ...customProps, }; + return merge(defaultProps, overrides || {}); }; describe('', () => { it('renders without crashing', () => { expect(shallowWithIntl()).toMatchSnapshot(); }); + + it(`defaults to "Custom" for new global entries`, () => { + const props = buildProps({ + role: { + kibana: [ + { + spaces: ['*'], + base: [], + feature: {}, + }, + ], + }, + editingIndex: 0, + }); + const component = mountWithIntl(); + const basePrivilegeComboBox = findTestSubject(component, `basePrivilegeComboBox`); + expect(basePrivilegeComboBox.text()).toBe('Custom'); + }); + + it(`defaults to "Custom" for new space entries`, () => { + const props = buildProps({ + role: { + kibana: [ + { + spaces: ['space:default'], + base: [], + feature: {}, + }, + ], + }, + editingIndex: 0, + }); + const component = mountWithIntl(); + const basePrivilegeComboBox = findTestSubject(component, `basePrivilegeComboBox`); + expect(basePrivilegeComboBox.text()).toBe('Custom'); + }); + + describe('when an existing global all privilege', () => { + it(`defaults to "Custom" for new entries`, () => { + const props = buildProps({ + role: { + kibana: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + { + spaces: ['default'], + base: [], + feature: {}, + }, + ], + }, + editingIndex: 1, + }); + const component = mountWithIntl(); + const basePrivilegeComboBox = findTestSubject(component, `basePrivilegeComboBox`); + expect(basePrivilegeComboBox.text()).toBe('Custom'); + }); + }); }); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx index 89ad1de9a816a..e32fe04b19747 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx @@ -34,7 +34,7 @@ import { } from '../../../../../../../lib/kibana_privilege_calculator'; import { hasAssignedFeaturePrivileges } from '../../../../../../../lib/privilege_utils'; import { copyRole } from '../../../../../../../lib/role_utils'; -import { CUSTOM_PRIVILEGE_VALUE, NO_PRIVILEGE_VALUE } from '../../../../lib/constants'; +import { CUSTOM_PRIVILEGE_VALUE } from '../../../../lib/constants'; import { FeatureTable } from '../feature_table'; import { SpaceSelector } from './space_selector'; @@ -489,7 +489,7 @@ export class PrivilegeSpaceForm extends Component { if ( hasAssignedFeaturePrivileges(form) || - explanation.actualPrivilege === NO_PRIVILEGE_VALUE || + form.base.length === 0 || this.state.isCustomizingFeaturePrivileges ) { displayedBasePrivilege = CUSTOM_PRIVILEGE_VALUE; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx new file mode 100644 index 0000000000000..7a73687842540 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx @@ -0,0 +1,700 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiBadge, EuiInMemoryTable, EuiIconTip } from '@elastic/eui'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { ReactWrapper } from 'enzyme'; +import { PrivilegeSpaceTable } from './privilege_space_table'; +import { PrivilegeDisplay } from './privilege_display'; +import { KibanaPrivileges, Role, RoleKibanaPrivilege } from '../../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; +import { rawKibanaPrivileges } from './__fixtures__'; + +interface TableRow { + spaces: string[]; + privileges: { + summary: string; + }; +} + +const buildProps = (roleKibanaPrivileges: RoleKibanaPrivilege[]): PrivilegeSpaceTable['props'] => { + const kibanaPrivileges = new KibanaPrivileges(rawKibanaPrivileges); + return { + role: { + name: 'test role', + elasticsearch: { + cluster: ['all'], + indices: [] as any[], + run_as: [] as string[], + }, + kibana: roleKibanaPrivileges, + }, + privilegeCalculatorFactory: new KibanaPrivilegeCalculatorFactory(kibanaPrivileges), + onChange: (role: Role) => {}, + onEdit: (spacesIndex: number) => {}, + displaySpaces: [ + { + id: 'default', + name: 'Default', + description: '', + disabledFeatures: [], + _reserved: true, + }, + { + id: 'marketing', + name: 'Marketing', + description: '', + disabledFeatures: [], + }, + ], + intl: {} as any, + }; +}; + +const getTableFromComponent = ( + component: ReactWrapper, React.Component<{}, {}, any>> +): TableRow[] => { + const table = component.find(EuiInMemoryTable); + const rows = table.find('tr'); + const dataRows = rows.slice(1); + return dataRows.reduce( + (acc, row) => { + const cells = row.find('td'); + const spacesCell = cells.at(0); + const spacesBadge = spacesCell.find(EuiBadge); + const privilegesCell = cells.at(1); + const privilegesDisplay = privilegesCell.find(PrivilegeDisplay); + return [ + ...acc, + { + spaces: spacesBadge.map(badge => badge.text().trim()), + privileges: { + summary: privilegesDisplay.text().trim(), + overridden: privilegesDisplay.find(EuiIconTip).exists('[type="lock"]'), + }, + }, + ]; + }, + [] as TableRow[] + ); +}; + +describe('only global', () => { + it('base all', () => { + const props = buildProps([{ spaces: ['*'], base: ['all'], feature: {} }]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'All', overridden: false } }, + ]); + }); + + it('base read', () => { + const props = buildProps([{ spaces: ['*'], base: ['read'], feature: {} }]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Read', overridden: false } }, + ]); + }); + + it('normal feature privilege all', () => { + const props = buildProps([{ spaces: ['*'], base: [], feature: { normal: ['all'] } }]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('normal feature privilege read', () => { + const props = buildProps([{ spaces: ['*'], base: [], feature: { normal: ['read'] } }]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('excludedFromBase feature privilege all', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { excludedFromBase: ['read'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('excludedFromBase feature privilege read', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { excludedFromBase: ['read'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); +}); + +describe('only default and marketing space', () => { + it('base all', () => { + const props = buildProps([{ spaces: ['default', 'marketing'], base: ['all'], feature: {} }]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['Default', 'Marketing'], privileges: { summary: 'All', overridden: false } }, + ]); + }); + + it('base read', () => { + const props = buildProps([{ spaces: ['default', 'marketing'], base: ['read'], feature: {} }]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Read', overridden: false } }, + ]); + }); + + it('normal feature privilege all', () => { + const props = buildProps([ + { spaces: ['default', 'marketing'], base: [], feature: { normal: ['all'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('normal feature privilege read', () => { + const props = buildProps([ + { spaces: ['default', 'marketing'], base: [], feature: { normal: ['read'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('excludedFromBase feature privilege all', () => { + const props = buildProps([ + { spaces: ['default', 'marketing'], base: [], feature: { excludedFromBase: ['read'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('excludedFromBase feature privilege read', () => { + const props = buildProps([ + { spaces: ['default', 'marketing'], base: [], feature: { excludedFromBase: ['read'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); +}); + +describe('global base all', () => { + describe('default and marketing space', () => { + it('base all', () => { + const props = buildProps([ + { spaces: ['*'], base: ['all'], feature: {} }, + { spaces: ['default', 'marketing'], base: ['all'], feature: {} }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'All', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'All', overridden: true } }, + ]); + }); + + it('base read', () => { + const props = buildProps([ + { spaces: ['*'], base: ['all'], feature: {} }, + { spaces: ['default', 'marketing'], base: ['read'], feature: {} }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'All', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'All', overridden: true } }, + ]); + }); + + it('normal feature privilege all', () => { + const props = buildProps([ + { spaces: ['*'], base: ['all'], feature: {} }, + { spaces: ['default', 'marketing'], base: [], feature: { normal: ['all'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'All', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'All', overridden: true } }, + ]); + }); + + it('normal feature privilege read', () => { + const props = buildProps([ + { spaces: ['*'], base: ['all'], feature: {} }, + { spaces: ['default', 'marketing'], base: [], feature: { normal: ['read'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'All', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'All', overridden: true } }, + ]); + }); + + it('excludedFromBase feature privilege all', () => { + const props = buildProps([ + { spaces: ['*'], base: ['all'], feature: {} }, + { spaces: ['default', 'marketing'], base: [], feature: { excludedFromBase: ['read'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'All', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('excludedFromBase feature privilege read', () => { + const props = buildProps([ + { spaces: ['*'], base: ['all'], feature: {} }, + { spaces: ['default', 'marketing'], base: [], feature: { excludedFromBase: ['read'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'All', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + }); +}); + +describe('global base read', () => { + describe('default and marketing space', () => { + it('base all', () => { + const props = buildProps([ + { spaces: ['*'], base: ['read'], feature: {} }, + { spaces: ['default', 'marketing'], base: ['all'], feature: {} }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Read', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'All', overridden: false } }, + ]); + }); + + it('base read', () => { + const props = buildProps([ + { spaces: ['*'], base: ['read'], feature: {} }, + { spaces: ['default', 'marketing'], base: ['read'], feature: {} }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Read', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Read', overridden: true } }, + ]); + }); + + it('normal feature privilege all', () => { + const props = buildProps([ + { spaces: ['*'], base: ['read'], feature: {} }, + { spaces: ['default', 'marketing'], base: [], feature: { normal: ['all'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Read', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Read', overridden: true } }, + ]); + }); + + it('normal feature privilege read', () => { + const props = buildProps([ + { spaces: ['*'], base: ['read'], feature: {} }, + { spaces: ['default', 'marketing'], base: [], feature: { normal: ['read'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Read', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Read', overridden: true } }, + ]); + }); + + it('excludedFromBase feature privilege all', () => { + const props = buildProps([ + { spaces: ['*'], base: ['read'], feature: {} }, + { spaces: ['default', 'marketing'], base: [], feature: { excludedFromBase: ['read'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Read', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('excludedFromBase feature privilege read', () => { + const props = buildProps([ + { spaces: ['*'], base: ['read'], feature: {} }, + { spaces: ['default', 'marketing'], base: [], feature: { excludedFromBase: ['read'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Read', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + }); +}); + +describe('global normal feature privilege all', () => { + describe('default and marketing space', () => { + it('base all', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { normal: ['all'] } }, + { spaces: ['default', 'marketing'], base: ['all'], feature: {} }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'All', overridden: false } }, + ]); + }); + + it('base read', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { normal: ['all'] } }, + { spaces: ['default', 'marketing'], base: ['read'], feature: {} }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Read', overridden: false } }, + ]); + }); + + it('normal feature privilege all', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { normal: ['all'] } }, + { spaces: ['default', 'marketing'], base: [], feature: { normal: ['all'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('normal feature privilege read', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { normal: ['all'] } }, + { spaces: ['default', 'marketing'], base: [], feature: { normal: ['read'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('excludedFromBase feature privilege all', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { normal: ['all'] } }, + { spaces: ['default', 'marketing'], base: [], feature: { excludedFromBase: ['read'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('excludedFromBase feature privilege read', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { normal: ['all'] } }, + { spaces: ['default', 'marketing'], base: [], feature: { excludedFromBase: ['read'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + }); +}); + +describe('global normal feature privilege read', () => { + describe('default and marketing space', () => { + it('base all', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { normal: ['read'] } }, + { spaces: ['default', 'marketing'], base: ['all'], feature: {} }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'All', overridden: false } }, + ]); + }); + + it('base read', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { normal: ['read'] } }, + { spaces: ['default', 'marketing'], base: ['read'], feature: {} }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Read', overridden: false } }, + ]); + }); + + it('normal feature privilege all', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { normal: ['read'] } }, + { spaces: ['default', 'marketing'], base: [], feature: { normal: ['all'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('normal feature privilege read', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { normal: ['read'] } }, + { spaces: ['default', 'marketing'], base: [], feature: { normal: ['read'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('excludedFromBase feature privilege all', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { normal: ['read'] } }, + { spaces: ['default', 'marketing'], base: [], feature: { excludedFromBase: ['read'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('excludedFromBase feature privilege read', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { normal: ['read'] } }, + { spaces: ['default', 'marketing'], base: [], feature: { excludedFromBase: ['read'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + }); +}); + +describe('global excludedFromBase feature privilege all', () => { + describe('default and marketing space', () => { + it('base all', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { excludedFromBase: ['all'] } }, + { spaces: ['default', 'marketing'], base: ['all'], feature: {} }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'All', overridden: false } }, + ]); + }); + + it('base read', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { excludedFromBase: ['all'] } }, + { spaces: ['default', 'marketing'], base: ['read'], feature: {} }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Read', overridden: false } }, + ]); + }); + + it('normal feature privilege all', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { excludedFromBase: ['all'] } }, + { spaces: ['default', 'marketing'], base: [], feature: { normal: ['all'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('normal feature privilege read', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { excludedFromBase: ['all'] } }, + { spaces: ['default', 'marketing'], base: [], feature: { normal: ['read'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('excludedFromBase feature privilege all', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { excludedFromBase: ['all'] } }, + { spaces: ['default', 'marketing'], base: [], feature: { excludedFromBase: ['read'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('excludedFromBase feature privilege read', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { excludedFromBase: ['all'] } }, + { spaces: ['default', 'marketing'], base: [], feature: { excludedFromBase: ['read'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + }); +}); + +describe('global excludedFromBase feature privilege read', () => { + describe('default and marketing space', () => { + it('base all', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { excludedFromBase: ['read'] } }, + { spaces: ['default', 'marketing'], base: ['all'], feature: {} }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'All', overridden: false } }, + ]); + }); + + it('base read', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { excludedFromBase: ['read'] } }, + { spaces: ['default', 'marketing'], base: ['read'], feature: {} }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Read', overridden: false } }, + ]); + }); + + it('normal feature privilege all', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { excludedFromBase: ['read'] } }, + { spaces: ['default', 'marketing'], base: [], feature: { normal: ['all'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('normal feature privilege read', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { excludedFromBase: ['read'] } }, + { spaces: ['default', 'marketing'], base: [], feature: { normal: ['read'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('excludedFromBase feature privilege all', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { excludedFromBase: ['read'] } }, + { spaces: ['default', 'marketing'], base: [], feature: { excludedFromBase: ['read'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + + it('excludedFromBase feature privilege read', () => { + const props = buildProps([ + { spaces: ['*'], base: [], feature: { excludedFromBase: ['read'] } }, + { spaces: ['default', 'marketing'], base: [], feature: { excludedFromBase: ['read'] } }, + ]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: 'Custom', overridden: false } }, + { spaces: ['Default', 'Marketing'], privileges: { summary: 'Custom', overridden: false } }, + ]); + }); + }); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx index 1c8e5d3dcc536..f1157203059e5 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx @@ -23,11 +23,11 @@ import { } from '../../../../../../../../common/model'; import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; import { - hasAssignedFeaturePrivileges, isGlobalPrivilegeDefinition, + hasAssignedFeaturePrivileges, } from '../../../../../../../lib/privilege_utils'; import { copyRole } from '../../../../../../../lib/role_utils'; -import { CUSTOM_PRIVILEGE_VALUE } from '../../../../lib/constants'; +import { CUSTOM_PRIVILEGE_VALUE, NO_PRIVILEGE_VALUE } from '../../../../lib/constants'; import { SpacesPopoverList } from '../../../spaces_popover_list'; import { PrivilegeDisplay } from './privilege_display'; @@ -183,28 +183,45 @@ export class PrivilegeSpaceTable extends Component { field: 'privileges', name: 'Privileges', render: (privileges: RoleKibanaPrivilege, record: TableRow) => { - const hasCustomizations = hasAssignedFeaturePrivileges(privileges); const effectivePrivilege = effectivePrivileges[record.spacesIndex]; const basePrivilege = effectivePrivilege.base; - const isAllowedCustomizations = - allowedPrivileges[record.spacesIndex].base.privileges.length > 1; - - const showCustomize = hasCustomizations && isAllowedCustomizations; - if (effectivePrivilege.reserved != null && effectivePrivilege.reserved.length > 0) { return ; } else if (record.isGlobal) { return ( ); } else { + const hasNonSupersededCustomizations = Object.entries(privileges.feature).some( + ([featureId, featurePrivileges]) => { + const allowedFeaturePrivileges = + allowedPrivileges[record.spacesIndex].feature[featureId]; + return ( + allowedFeaturePrivileges && + allowedFeaturePrivileges.canUnassign && + allowedFeaturePrivileges.privileges.some(privilege => + featurePrivileges.includes(privilege) + ) + ); + } + ); + + const showCustom = + hasNonSupersededCustomizations || + (hasAssignedFeaturePrivileges(privileges) && + effectivePrivilege.base.actualPrivilege === NO_PRIVILEGE_VALUE); + return ( ); } diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/privileges/privileges.test.ts b/x-pack/legacy/plugins/security/server/lib/authorization/privileges/privileges.test.ts index dc93f5638646f..3243606c5721c 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/privileges/privileges.test.ts +++ b/x-pack/legacy/plugins/security/server/lib/authorization/privileges/privileges.test.ts @@ -637,6 +637,80 @@ describe('features', () => { ]); expect(actual).toHaveProperty(`${group}.read`, [actions.login, actions.version]); }); + + test('actions defined in a feature privilege with excludeFromBasePrivileges are not included in `all` or `read', () => { + const features: Feature[] = [ + { + id: 'foo', + name: 'Foo Feature', + excludeFromBasePrivileges: true, + icon: 'arrowDown', + navLinkId: 'kibana:foo', + app: [], + catalogue: ['ignore-me-1', 'ignore-me-2'], + management: { + foo: ['ignore-me-1', 'ignore-me-2'], + }, + privileges: { + bar: { + management: { + 'bar-management': ['bar-management-1'], + }, + catalogue: ['bar-catalogue-1'], + savedObject: { + all: ['bar-savedObject-all-1'], + read: ['bar-savedObject-read-1'], + }, + ui: ['bar-ui-1'], + }, + all: { + management: { + 'all-management': ['all-management-1'], + }, + catalogue: ['all-catalogue-1'], + savedObject: { + all: ['all-savedObject-all-1'], + read: ['all-savedObject-read-1'], + }, + ui: ['all-ui-1'], + }, + read: { + management: { + 'read-management': ['read-management-1'], + }, + catalogue: ['read-catalogue-1'], + savedObject: { + all: ['read-savedObject-all-1'], + read: ['read-savedObject-read-1'], + }, + ui: ['read-ui-1'], + }, + }, + }, + ]; + + const mockXPackMainPlugin = { + getFeatures: jest.fn().mockReturnValue(features), + }; + + const privileges = privilegesFactory(actions, mockXPackMainPlugin as any); + + const actual = privileges.get(); + expect(actual).toHaveProperty(`${group}.all`, [ + actions.login, + actions.version, + ...(expectGetFeatures ? [actions.api.get('features')] : []), + ...(expectManageSpaces + ? [ + actions.space.manage, + actions.ui.get('spaces', 'manage'), + actions.ui.get('management', 'kibana', 'spaces'), + ] + : []), + actions.allHack, + ]); + expect(actual).toHaveProperty(`${group}.read`, [actions.login, actions.version]); + }); }); }); diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/privileges/privileges.ts b/x-pack/legacy/plugins/security/server/lib/authorization/privileges/privileges.ts index 60c929854aed4..aa18a7d150b7e 100644 --- a/x-pack/legacy/plugins/security/server/lib/authorization/privileges/privileges.ts +++ b/x-pack/legacy/plugins/security/server/lib/authorization/privileges/privileges.ts @@ -21,10 +21,11 @@ export function privilegesFactory(actions: Actions, xpackMainPlugin: XPackMainPl return { get() { const features = xpackMainPlugin.getFeatures(); + const basePrivilegeFeatures = features.filter(feature => !feature.excludeFromBasePrivileges); const allActions = uniq( flatten( - features.map(feature => + basePrivilegeFeatures.map(feature => Object.values(feature.privileges).reduce((acc, privilege) => { return [...acc, ...featurePrivilegeBuilder.getActions(privilege, feature)]; }, []) @@ -34,7 +35,7 @@ export function privilegesFactory(actions: Actions, xpackMainPlugin: XPackMainPl const readActions = uniq( flatten( - features.map(feature => + basePrivilegeFeatures.map(feature => Object.entries(feature.privileges).reduce((acc, [privilegeId, privilege]) => { if (privilegeId !== 'read') { return acc; diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/feature_registry/feature_registry.test.ts b/x-pack/legacy/plugins/xpack_main/server/lib/feature_registry/feature_registry.test.ts index 5fceeac98d85b..0dac8c5586995 100644 --- a/x-pack/legacy/plugins/xpack_main/server/lib/feature_registry/feature_registry.test.ts +++ b/x-pack/legacy/plugins/xpack_main/server/lib/feature_registry/feature_registry.test.ts @@ -29,6 +29,7 @@ describe('FeatureRegistry', () => { const feature: Feature = { id: 'test-feature', name: 'Test Feature', + excludeFromBasePrivileges: true, icon: 'addDataApp', navLinkId: 'someNavLink', app: ['app1', 'app2'], diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/feature_registry/feature_registry.ts b/x-pack/legacy/plugins/xpack_main/server/lib/feature_registry/feature_registry.ts index d7d009ce97cb1..95f2ee7b3d384 100644 --- a/x-pack/legacy/plugins/xpack_main/server/lib/feature_registry/feature_registry.ts +++ b/x-pack/legacy/plugins/xpack_main/server/lib/feature_registry/feature_registry.ts @@ -138,6 +138,15 @@ export interface Feature = Privileges */ name: string; + /** + * Whether or not this feature should be excluded from the base privileges. + * This is primarily helpful when migrating applications with a "legacy" privileges model + * to use Kibana privileges. We don't want these features to be considered part of the `all` + * or `read` base privileges in a minor release if the user was previously granted access + * using an additional reserved role. + */ + excludeFromBasePrivileges?: boolean; + /** * Optional array of supported licenses. * If omitted, all licenses are allowed. @@ -249,6 +258,7 @@ const schema = Joi.object({ .invalid(...prohibitedFeatureIds) .required(), name: Joi.string().required(), + excludeFromBasePrivileges: Joi.boolean(), validLicenses: Joi.array().items(Joi.string().valid('basic', 'standard', 'gold', 'platinum')), icon: Joi.string(), description: Joi.string(),