diff --git a/x-pack/platform/packages/private/security/ui_components/src/kibana_privilege_table/feature_table_expanded_row.tsx b/x-pack/platform/packages/private/security/ui_components/src/kibana_privilege_table/feature_table_expanded_row.tsx index 436119884f51d..ad14f76187236 100644 --- a/x-pack/platform/packages/private/security/ui_components/src/kibana_privilege_table/feature_table_expanded_row.tsx +++ b/x-pack/platform/packages/private/security/ui_components/src/kibana_privilege_table/feature_table_expanded_row.tsx @@ -132,6 +132,7 @@ export const FeatureTableExpandedRow = ({ onChange={(updatedPrivileges) => onChange(feature.id, updatedPrivileges)} selectedFeaturePrivileges={selectedFeaturePrivileges} disabled={disabled || !isCustomizing || isDisabledDueToSpaceSelection} + allSpacesSelected={allSpacesSelected} /> ); diff --git a/x-pack/platform/packages/private/security/ui_components/src/kibana_privilege_table/sub_feature_form.test.tsx b/x-pack/platform/packages/private/security/ui_components/src/kibana_privilege_table/sub_feature_form.test.tsx index 8f741f1d48f9d..9b0f3f17ffacd 100644 --- a/x-pack/platform/packages/private/security/ui_components/src/kibana_privilege_table/sub_feature_form.test.tsx +++ b/x-pack/platform/packages/private/security/ui_components/src/kibana_privilege_table/sub_feature_form.test.tsx @@ -301,4 +301,66 @@ describe('SubFeatureForm', () => { expect(wrapper.children()).toMatchInlineSnapshot(`null`); }); + + it('correctly renders privileges that require all spaces to be enabled', () => { + const role = createRole([ + { + base: [], + feature: { + with_sub_features: ['cool_all'], + }, + spaces: [], + }, + ]); + const feature = new KibanaFeature({ + id: 'test_feature', + name: 'test feature', + category: { id: 'test', label: 'test' }, + app: [], + privileges: { + all: { + savedObject: { all: [], read: [] }, + ui: [], + }, + read: { + savedObject: { all: [], read: [] }, + ui: [], + }, + }, + subFeatures: [ + { + name: 'subFeature1', + requireAllSpaces: true, + privilegeGroups: [ + { + groupType: 'independent', + privileges: [], + }, + ], + }, + ], + }); + const subFeature1 = new SecuredSubFeature(feature.toRaw().subFeatures![0]); + const kibanaPrivileges = createKibanaPrivileges([feature]); + const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role); + + const onChange = jest.fn(); + + const wrapper = mountWithIntl( + + ); + + const buttonGroups = wrapper.find(EuiButtonGroup); + + buttonGroups.every((button) => button.props().idSelected.id === 'none'); + }); }); diff --git a/x-pack/platform/packages/private/security/ui_components/src/kibana_privilege_table/sub_feature_form.tsx b/x-pack/platform/packages/private/security/ui_components/src/kibana_privilege_table/sub_feature_form.tsx index 21bf95955328b..f2def06147a7a 100644 --- a/x-pack/platform/packages/private/security/ui_components/src/kibana_privilege_table/sub_feature_form.tsx +++ b/x-pack/platform/packages/private/security/ui_components/src/kibana_privilege_table/sub_feature_form.tsx @@ -34,6 +34,7 @@ interface Props { onChange: (selectedPrivileges: string[]) => void; disabled?: boolean; categoryId?: string; + allSpacesSelected?: boolean; } export const SubFeatureForm = (props: Props) => { @@ -157,12 +158,18 @@ export const SubFeatureForm = (props: Props) => { privilegeGroup: SubFeaturePrivilegeGroup, index: number ) { + const nonePrivilege = { + id: NO_PRIVILEGE_VALUE, + label: 'None', + isDisabled: props.disabled, + }; + const firstSelectedPrivilege = props.privilegeCalculator.getSelectedMutuallyExclusiveSubFeaturePrivilege( props.featureId, privilegeGroup, props.privilegeIndex - ); + ) ?? nonePrivilege; const options = [ ...privilegeGroup.privileges.map((privilege, privilegeIndex) => { @@ -174,11 +181,12 @@ export const SubFeatureForm = (props: Props) => { }), ]; - options.push({ - id: NO_PRIVILEGE_VALUE, - label: 'None', - isDisabled: props.disabled, - }); + options.push(nonePrivilege); + + const idSelected = + props.subFeature.requireAllSpaces && !props.allSpacesSelected + ? nonePrivilege.id + : firstSelectedPrivilege.id; return ( { data-test-subj="mutexSubFeaturePrivilegeControl" isFullWidth options={options} - idSelected={firstSelectedPrivilege?.id ?? NO_PRIVILEGE_VALUE} + idSelected={idSelected} isDisabled={props.disabled} onChange={(selectedPrivilegeId: string) => { // Deselect all privileges which belong to this mutually-exclusive group diff --git a/x-pack/platform/plugins/shared/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/privilege_summary_expanded_row.tsx b/x-pack/platform/plugins/shared/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/privilege_summary_expanded_row.tsx index 83f1e26ad1284..a48352c839598 100644 --- a/x-pack/platform/plugins/shared/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/privilege_summary_expanded_row.tsx +++ b/x-pack/platform/plugins/shared/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/privilege_summary_expanded_row.tsx @@ -6,23 +6,159 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiText } from '@elastic/eui'; -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import type { SecuredFeature, + SecuredSubFeature, SubFeaturePrivilege, SubFeaturePrivilegeGroup, } from '@kbn/security-role-management-model'; import type { EffectiveFeaturePrivileges } from './privilege_summary_calculator'; +import { ALL_SPACES_ID } from '../../../../../../../common/constants'; + +type EffectivePrivilegesTuple = [string[], EffectiveFeaturePrivileges['featureId']]; interface Props { feature: SecuredFeature; - effectiveFeaturePrivileges: Array; + effectiveFeaturePrivileges: EffectivePrivilegesTuple[]; } export const PrivilegeSummaryExpandedRow = (props: Props) => { + const allSpacesEffectivePrivileges = useMemo( + () => props.effectiveFeaturePrivileges.find(([spaces]) => spaces.includes(ALL_SPACES_ID)), + [props.effectiveFeaturePrivileges] + ); + + const renderIndependentPrivilegeGroup = useCallback( + ( + effectiveSubFeaturePrivileges: string[], + privilegeGroup: SubFeaturePrivilegeGroup, + index: number + ) => { + return ( +
+ {privilegeGroup.privileges.map((privilege: SubFeaturePrivilege) => { + const isGranted = effectiveSubFeaturePrivileges.includes(privilege.id); + return ( + + + + + + + {privilege.name} + + + + ); + })} +
+ ); + }, + [] + ); + + const renderMutuallyExclusivePrivilegeGroup = useCallback( + ( + effectiveSubFeaturePrivileges: string[], + privilegeGroup: SubFeaturePrivilegeGroup, + index: number, + isDisabledDueToSpaceSelection: boolean + ) => { + const firstSelectedPrivilege = !isDisabledDueToSpaceSelection + ? privilegeGroup.privileges.find((p) => effectiveSubFeaturePrivileges.includes(p.id))?.name + : null; + + return ( + + + + + + + {firstSelectedPrivilege ?? 'None'} + + + + ); + }, + [] + ); + + const renderPrivilegeGroup = useCallback( + ( + effectiveSubFeaturePrivileges: string[], + { requireAllSpaces, spaces }: { requireAllSpaces: boolean; spaces: string[] } + ) => { + return (privilegeGroup: SubFeaturePrivilegeGroup, index: number) => { + const isDisabledDueToSpaceSelection = requireAllSpaces && !spaces.includes(ALL_SPACES_ID); + + switch (privilegeGroup.groupType) { + case 'independent': + return renderIndependentPrivilegeGroup( + effectiveSubFeaturePrivileges, + privilegeGroup, + index + ); + case 'mutually_exclusive': + return renderMutuallyExclusivePrivilegeGroup( + effectiveSubFeaturePrivileges, + privilegeGroup, + index, + isDisabledDueToSpaceSelection + ); + default: + throw new Error(`Unsupported privilege group type: ${privilegeGroup.groupType}`); + } + }; + }, + [renderIndependentPrivilegeGroup, renderMutuallyExclusivePrivilegeGroup] + ); + + const getEffectiveFeaturePrivileges = useCallback( + (subFeature: SecuredSubFeature) => { + return props.effectiveFeaturePrivileges.map((entry, index) => { + const [spaces, privs] = + subFeature.requireAllSpaces && allSpacesEffectivePrivileges + ? allSpacesEffectivePrivileges + : entry; + + return ( + + {subFeature.getPrivilegeGroups().map( + renderPrivilegeGroup(privs.subFeature, { + requireAllSpaces: subFeature.requireAllSpaces, + spaces, + }) + )} + + ); + }); + }, + [props.effectiveFeaturePrivileges, allSpacesEffectivePrivileges, renderPrivilegeGroup] + ); + return ( {props.feature.getSubFeatures().map((subFeature) => { @@ -34,105 +170,11 @@ export const PrivilegeSummaryExpandedRow = (props: Props) => { {subFeature.name} - {props.effectiveFeaturePrivileges.map((privs, index) => { - return ( - - {subFeature.getPrivilegeGroups().map(renderPrivilegeGroup(privs.subFeature))} - - ); - })} + {getEffectiveFeaturePrivileges(subFeature)} ); })} ); - - function renderPrivilegeGroup(effectiveSubFeaturePrivileges: string[]) { - return (privilegeGroup: SubFeaturePrivilegeGroup, index: number) => { - switch (privilegeGroup.groupType) { - case 'independent': - return renderIndependentPrivilegeGroup( - effectiveSubFeaturePrivileges, - privilegeGroup, - index - ); - case 'mutually_exclusive': - return renderMutuallyExclusivePrivilegeGroup( - effectiveSubFeaturePrivileges, - privilegeGroup, - index - ); - default: - throw new Error(`Unsupported privilege group type: ${privilegeGroup.groupType}`); - } - }; - } - - function renderIndependentPrivilegeGroup( - effectiveSubFeaturePrivileges: string[], - privilegeGroup: SubFeaturePrivilegeGroup, - index: number - ) { - return ( -
- {privilegeGroup.privileges.map((privilege: SubFeaturePrivilege) => { - const isGranted = effectiveSubFeaturePrivileges.includes(privilege.id); - return ( - - - - - - - {privilege.name} - - - - ); - })} -
- ); - } - - function renderMutuallyExclusivePrivilegeGroup( - effectiveSubFeaturePrivileges: string[], - privilegeGroup: SubFeaturePrivilegeGroup, - index: number - ) { - const firstSelectedPrivilege = privilegeGroup.privileges.find((p) => - effectiveSubFeaturePrivileges.includes(p.id) - )?.name; - - return ( - - - - - - - {firstSelectedPrivilege ?? 'None'} - - - - ); - } }; diff --git a/x-pack/platform/plugins/shared/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/privilege_summary_table.test.tsx b/x-pack/platform/plugins/shared/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/privilege_summary_table.test.tsx index e1ca5300ee9f7..f344cf29a479b 100644 --- a/x-pack/platform/plugins/shared/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/privilege_summary_table.test.tsx +++ b/x-pack/platform/plugins/shared/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/privilege_summary_table.test.tsx @@ -21,6 +21,7 @@ import { mountWithIntl } from '@kbn/test-jest-helpers'; import { getDisplayedFeaturePrivileges } from './__fixtures__'; import type { PrivilegeSummaryTableProps } from './privilege_summary_table'; import { PrivilegeSummaryTable } from './privilege_summary_table'; +import { ALL_SPACES_ID } from '../../../../../../../common/constants'; const createRole = (roleKibanaPrivileges: RoleKibanaPrivilege[]) => ({ name: 'some-role', @@ -431,7 +432,7 @@ describe('PrivilegeSummaryTable', () => { hasCustomizedSubFeaturePrivileges: false, primaryFeaturePrivilege: 'All', ...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, { - 'Require all spaces Sub Feature': ['Cool toggle 1'], + 'Require all spaces Sub Feature': [], }), }, }, @@ -440,7 +441,7 @@ describe('PrivilegeSummaryTable', () => { hasCustomizedSubFeaturePrivileges: false, primaryFeaturePrivilege: 'None', ...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, { - 'Require all spaces Sub Feature': ['Cool toggle 1'], + 'Require all spaces Sub Feature': [], }), }, }, @@ -642,7 +643,7 @@ describe('PrivilegeSummaryTable', () => { hasCustomizedSubFeaturePrivileges: false, primaryFeaturePrivilege: 'None', ...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, { - 'Require all spaces Sub Feature': ['Cool toggle 1'], + 'Require all spaces Sub Feature': [], }), }, }, @@ -874,7 +875,7 @@ describe('PrivilegeSummaryTable', () => { hasCustomizedSubFeaturePrivileges: false, primaryFeaturePrivilege: 'Read', ...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, { - 'Require all spaces Sub Feature': ['Cool toggle 1'], + 'Require all spaces Sub Feature': [], }), }, }, @@ -1023,6 +1024,138 @@ describe('PrivilegeSummaryTable', () => { }); }); + it('renders effective privileges when all spaces option is selected', async () => { + const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures, { + allowSubFeaturePrivileges, + }); + + const role = createRole([ + { + base: ['all'], + feature: { + with_sub_features: ['minimal_read', 'cool_all'], + }, + spaces: ['*'], + }, + { + base: [], + feature: { + with_sub_features: ['all'], + }, + spaces: ['default', 'space-1'], + }, + ]); + + const wrapper = await setup({ + spaces: [ + { + id: ALL_SPACES_ID, + name: '*All Spaces', + disabledFeatures: [], + }, + ], + kibanaPrivileges, + role, + canCustomizeSubFeaturePrivileges: allowSubFeaturePrivileges, + spacesApiUi, + }); + + const displayedPrivileges = getDisplayedFeaturePrivileges(wrapper, role); + + expect(displayedPrivileges).toEqual({ + excluded_from_base: { + '*': { + hasCustomizedSubFeaturePrivileges: false, + primaryFeaturePrivilege: 'None', + ...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, { + 'Cool Sub Feature': [], + }), + }, + 'default, space-1': { + hasCustomizedSubFeaturePrivileges: false, + primaryFeaturePrivilege: 'None', + ...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, { + 'Cool Sub Feature': [], + }), + }, + }, + no_sub_features: { + '*': { + hasCustomizedSubFeaturePrivileges: false, + primaryFeaturePrivilege: 'All', + }, + 'default, space-1': { + hasCustomizedSubFeaturePrivileges: false, + primaryFeaturePrivilege: 'All', + }, + }, + with_excluded_sub_features: { + '*': { + hasCustomizedSubFeaturePrivileges: false, + primaryFeaturePrivilege: 'All', + ...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, { + 'Excluded Sub Feature': [], + }), + }, + 'default, space-1': { + hasCustomizedSubFeaturePrivileges: false, + primaryFeaturePrivilege: 'All', + ...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, { + 'Excluded Sub Feature': [], + }), + }, + }, + with_sub_features: { + '*': { + hasCustomizedSubFeaturePrivileges: false, + primaryFeaturePrivilege: 'All', + ...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, { + 'Cool Sub Feature': ['Cool toggle 1', 'Cool toggle 2', 'All'], + }), + }, + 'default, space-1': { + hasCustomizedSubFeaturePrivileges: false, + primaryFeaturePrivilege: 'All', + ...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, { + 'Cool Sub Feature': ['Cool toggle 1', 'Cool toggle 2', 'All'], + }), + }, + }, + with_require_all_spaces_sub_features: { + '*': { + hasCustomizedSubFeaturePrivileges: false, + primaryFeaturePrivilege: 'All', + ...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'], + }), + }, + }, + with_require_all_spaces_for_feature_and_sub_features: { + '*': { + hasCustomizedSubFeaturePrivileges: false, + primaryFeaturePrivilege: 'All', + ...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'], + }), + }, + }, + }); + }); + it('renders effective privileges for a complex setup', async () => { const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures, { allowSubFeaturePrivileges, @@ -1180,7 +1313,7 @@ describe('PrivilegeSummaryTable', () => { hasCustomizedSubFeaturePrivileges: false, primaryFeaturePrivilege: 'None', ...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, { - 'Require all spaces Sub Feature': ['Cool toggle 1'], + 'Require all spaces Sub Feature': [], }), }, 'space-1, space-2': { @@ -1193,6 +1326,360 @@ describe('PrivilegeSummaryTable', () => { }, }); }); + + it('renders effective privileges for requireAllSpaces feature when all spaces is not selected', async () => { + const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures, { + allowSubFeaturePrivileges, + }); + + const role = createRole([ + { + base: [], + feature: { + with_sub_features: ['all'], + }, + spaces: ['default', 'space-1'], + }, + ]); + + const wrapper = await setup({ + spaces, + kibanaPrivileges, + role, + canCustomizeSubFeaturePrivileges: allowSubFeaturePrivileges, + spacesApiUi, + }); + + const displayedPrivileges = getDisplayedFeaturePrivileges(wrapper, role); + + expect(displayedPrivileges.with_require_all_spaces_sub_features).toEqual({ + 'default, space-1': { + hasCustomizedSubFeaturePrivileges: false, + primaryFeaturePrivilege: 'None', + ...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, { + 'Require all spaces Sub Feature': [], + }), + }, + }); + + expect(displayedPrivileges.with_require_all_spaces_for_feature_and_sub_features).toEqual({ + 'default, space-1': { + hasCustomizedSubFeaturePrivileges: false, + primaryFeaturePrivilege: 'None', + ...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, { + 'Require all spaces Sub Feature': [], + }), + }, + }); + }); + + it('renders effective privileges for requireAllSpaces feature when all spaces is selected without granting access', async () => { + const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures, { + allowSubFeaturePrivileges, + }); + + const role = createRole([ + { + base: [], + feature: { + with_sub_features: ['all'], + }, + spaces: ['*'], + }, + { + base: [], + feature: { + with_sub_features: ['all'], + }, + spaces: ['default', 'space-1'], + }, + ]); + + const wrapper = await setup({ + spaces: [ + { + id: ALL_SPACES_ID, + name: '*All Spaces', + disabledFeatures: [], + }, + ...spaces, + ], + kibanaPrivileges, + role, + canCustomizeSubFeaturePrivileges: allowSubFeaturePrivileges, + spacesApiUi, + }); + + const displayedPrivileges = getDisplayedFeaturePrivileges(wrapper, role); + + expect(displayedPrivileges.with_require_all_spaces_sub_features).toEqual({ + '*': { + 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': [], + }), + }, + }); + + expect(displayedPrivileges.with_require_all_spaces_for_feature_and_sub_features).toEqual({ + '*': { + 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': [], + }), + }, + }); + }); + + it('renders effective privileges for requireAllSpaces feature when all spaces grants read access', async () => { + const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures, { + allowSubFeaturePrivileges, + }); + + const role = createRole([ + { + base: [], + feature: { + with_require_all_spaces_for_feature_and_sub_features: ['read'], + }, + spaces: ['*'], + }, + { + base: ['all'], + feature: {}, + spaces: ['default'], + }, + ]); + + const wrapper = await setup({ + spaces: [ + { + id: ALL_SPACES_ID, + name: '*All Spaces', + disabledFeatures: [], + }, + ...spaces, + ], + kibanaPrivileges, + role, + canCustomizeSubFeaturePrivileges: allowSubFeaturePrivileges, + spacesApiUi, + }); + + const displayedPrivileges = getDisplayedFeaturePrivileges(wrapper, role); + + expect(displayedPrivileges.with_require_all_spaces_for_feature_and_sub_features).toEqual({ + '*': { + hasCustomizedSubFeaturePrivileges: false, + primaryFeaturePrivilege: 'Read', + ...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, { + 'Require all spaces Sub Feature': [], + }), + }, + default: { + hasCustomizedSubFeaturePrivileges: false, + primaryFeaturePrivilege: 'Read', + ...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, { + 'Require all spaces Sub Feature': [], + }), + }, + }); + }); + + it('renders effective privileges for requireAllSpaces feature when all spaces grants all access', async () => { + const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures, { + allowSubFeaturePrivileges, + }); + + const role = createRole([ + { + base: [], + feature: { + with_require_all_spaces_for_feature_and_sub_features: ['all'], + with_require_all_spaces_sub_features: ['all'], + }, + spaces: ['*'], + }, + { + base: ['read'], + feature: {}, + spaces: ['default'], + }, + ]); + + const wrapper = await setup({ + spaces: [ + { + id: ALL_SPACES_ID, + name: '*All Spaces', + disabledFeatures: [], + }, + ...spaces, + ], + kibanaPrivileges, + role, + canCustomizeSubFeaturePrivileges: allowSubFeaturePrivileges, + spacesApiUi, + }); + + const displayedPrivileges = getDisplayedFeaturePrivileges(wrapper, role); + + expect(displayedPrivileges.with_require_all_spaces_sub_features).toEqual({ + '*': { + hasCustomizedSubFeaturePrivileges: false, + primaryFeaturePrivilege: 'All', + ...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'], + }), + }, + }); + + expect(displayedPrivileges.with_require_all_spaces_for_feature_and_sub_features).toEqual({ + '*': { + hasCustomizedSubFeaturePrivileges: false, + primaryFeaturePrivilege: 'All', + ...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'], + }), + }, + }); + }); + + it('renders effective privileges for requireAllSpaces feature when all spaces grants base read access', async () => { + const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures, { + allowSubFeaturePrivileges, + }); + + const role = createRole([ + { + base: ['read'], + feature: {}, + spaces: ['*'], + }, + { + base: ['all'], + feature: {}, + spaces: ['default'], + }, + ]); + + const wrapper = await setup({ + spaces: [ + { + id: ALL_SPACES_ID, + name: '*All Spaces', + disabledFeatures: [], + }, + ...spaces, + ], + kibanaPrivileges, + role, + canCustomizeSubFeaturePrivileges: allowSubFeaturePrivileges, + spacesApiUi, + }); + + const displayedPrivileges = getDisplayedFeaturePrivileges(wrapper, role); + + expect(displayedPrivileges.with_require_all_spaces_for_feature_and_sub_features).toEqual({ + '*': { + hasCustomizedSubFeaturePrivileges: false, + primaryFeaturePrivilege: 'Read', + ...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, { + 'Require all spaces Sub Feature': [], + }), + }, + default: { + hasCustomizedSubFeaturePrivileges: false, + primaryFeaturePrivilege: 'Read', + ...maybeExpectSubFeaturePrivileges(allowSubFeaturePrivileges, { + 'Require all spaces Sub Feature': [], + }), + }, + }); + }); + + it('renders effective privileges for requireAllSpaces feature when all spaces grants base all access', async () => { + const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures, { + allowSubFeaturePrivileges, + }); + + const role = createRole([ + { + base: ['all'], + feature: {}, + spaces: ['*'], + }, + { + base: ['read'], + feature: {}, + spaces: ['default'], + }, + ]); + + const wrapper = await setup({ + spaces: [ + { + id: ALL_SPACES_ID, + name: '*All Spaces', + disabledFeatures: [], + }, + ...spaces, + ], + kibanaPrivileges, + role, + canCustomizeSubFeaturePrivileges: allowSubFeaturePrivileges, + spacesApiUi, + }); + + const displayedPrivileges = getDisplayedFeaturePrivileges(wrapper, role); + + expect(displayedPrivileges.with_require_all_spaces_for_feature_and_sub_features).toEqual({ + '*': { + hasCustomizedSubFeaturePrivileges: false, + primaryFeaturePrivilege: 'All', + ...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'], + }), + }, + }); + }); }); }); }); diff --git a/x-pack/platform/plugins/shared/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/privilege_summary_table.tsx b/x-pack/platform/plugins/shared/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/privilege_summary_table.tsx index 5dc856e5af5d9..43dc296cb4987 100644 --- a/x-pack/platform/plugins/shared/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/privilege_summary_table.tsx +++ b/x-pack/platform/plugins/shared/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/privilege_summary_table.tsx @@ -48,14 +48,31 @@ function getColumnKey(entry: RoleKibanaPrivilege) { return `privilege_entry_${entry.spaces.join('|')}`; } -function showPrivilege(allSpacesSelected: boolean, primaryFeature?: PrimaryFeaturePrivilege) { +function showPrivilege({ + allSpacesSelected, + primaryFeature, + globalPrimaryFeature, +}: { + allSpacesSelected: boolean; + primaryFeature?: PrimaryFeaturePrivilege; + globalPrimaryFeature?: PrimaryFeaturePrivilege; +}) { if ( primaryFeature?.name == null || primaryFeature?.disabled || - (primaryFeature.requireAllSpaces && !allSpacesSelected) + (primaryFeature?.requireAllSpaces && !allSpacesSelected) ) { return 'None'; } + + // If primary feature requires all spaces we cannot rely on primaryFeature.name. + // Example: + // primaryFeature: feature with requireAllSpaces in space-a has all privileges set to All + // globalPrimaryFeature: feature in *AllSpaces has privileges set to Read (this is the correct one to display) + if (primaryFeature?.requireAllSpaces && allSpacesSelected) { + return globalPrimaryFeature?.name ?? 'None'; + } + return primaryFeature?.name; } @@ -127,6 +144,15 @@ export const PrivilegeSummaryTable = (props: PrivilegeSummaryTableProps) => { } return 0; }); + + const globalRawPrivilege = rawKibanaPrivileges.find((entry) => + isGlobalPrivilegeDefinition(entry) + ); + + const globalPrivilege = globalRawPrivilege + ? calculator.getEffectiveFeaturePrivileges(globalRawPrivilege) + : null; + const privilegeColumns = rawKibanaPrivileges.map((entry) => { const key = getColumnKey(entry); return { @@ -161,10 +187,11 @@ export const PrivilegeSummaryTable = (props: PrivilegeSummaryTableProps) => { hasCustomizedSubFeaturePrivileges ? 'additionalPrivilegesGranted' : '' }`} > - {showPrivilege( - props.spaces.some((space) => space.id === ALL_SPACES_ID), - primary - )}{' '} + {showPrivilege({ + allSpacesSelected: props.spaces.some((space) => space.id === ALL_SPACES_ID), + primaryFeature: primary, + globalPrimaryFeature: globalPrivilege?.[record.featureId]?.primary, + })}{' '} {iconTip} ); @@ -178,12 +205,14 @@ export const PrivilegeSummaryTable = (props: PrivilegeSummaryTableProps) => { } columns.push(featureColumn, ...privilegeColumns); - const privileges = rawKibanaPrivileges.reduce((acc, entry) => { + const privileges = rawKibanaPrivileges.reduce< + Record + >((acc, entry) => { return { ...acc, - [getColumnKey(entry)]: calculator.getEffectiveFeaturePrivileges(entry), + [getColumnKey(entry)]: [entry.spaces, calculator.getEffectiveFeaturePrivileges(entry)], }; - }, {} as Record); + }, {}); const accordions: any[] = []; @@ -210,11 +239,15 @@ export const PrivilegeSummaryTable = (props: PrivilegeSummaryTableProps) => { ); + const categoryPrivileges = Object.fromEntries( + Object.entries(privileges).map(([key, [, featurePrivileges]]) => [key, featurePrivileges]) + ); + const categoryItems = featuresInCategory.map((feature) => { return { feature, featureId: feature.id, - ...privileges, + ...categoryPrivileges, }; }); @@ -241,7 +274,10 @@ export const PrivilegeSummaryTable = (props: PrivilegeSummaryTableProps) => { [featureId]: ( p[featureId])} + effectiveFeaturePrivileges={Object.values(privileges).map(([spaces, privs]) => [ + spaces, + privs[featureId], + ])} /> ), }; diff --git a/x-pack/platform/plugins/shared/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx b/x-pack/platform/plugins/shared/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx index 751db03939dda..d172c46e3a660 100644 --- a/x-pack/platform/plugins/shared/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx +++ b/x-pack/platform/plugins/shared/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx @@ -115,15 +115,11 @@ describe('PrivilegeSpaceForm', () => { }, "with_require_all_spaces_for_feature_and_sub_features": Object { "primaryFeaturePrivilege": "none", - "subFeaturePrivileges": Array [ - "cool_toggle_1", - ], + "subFeaturePrivileges": Array [], }, "with_require_all_spaces_sub_features": Object { "primaryFeaturePrivilege": "all", - "subFeaturePrivileges": Array [ - "cool_toggle_1", - ], + "subFeaturePrivileges": Array [], }, "with_sub_features": Object { "primaryFeaturePrivilege": "all", diff --git a/x-pack/platform/plugins/shared/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx b/x-pack/platform/plugins/shared/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx index 2bb3292932870..2773dd92753ce 100644 --- a/x-pack/platform/plugins/shared/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx +++ b/x-pack/platform/plugins/shared/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx @@ -218,7 +218,7 @@ export class SpaceAwarePrivilegeSection extends Component { const viewMatrixButton = ( { return [this.globalSpaceEntry, ...this.props.spaces]; }; + private getSelectedSpaces = () => + this.getDisplaySpaces().filter((space) => + this.props.role.kibana.some((entry) => entry.spaces.includes(space.id)) + ); + private getAvailableSpaces = (includeSpacesFromPrivilegeIndex: number = -1) => { const spacesToExclude = _.uniq( _.flatten(