Fleet Privileges Display (#204402)

## Summary

Fixed privileges display for features/subFeatures that require all
spaces.

### Before
Role privileges display for only `Default` space selected

<img width="728" alt="Screenshot 2024-12-17 at 13 32 17"
src="https://github.com/user-attachments/assets/151b7012-aa1a-430c-be22-cc91e64362e3"
/>

Privileges summary display for only `Default` space selected

<img width="471" alt="Screenshot 2024-12-17 at 13 32 50"
src="https://github.com/user-attachments/assets/964c2223-163d-4081-a37d-196f5df5df5c"
/>

### After
Role privileges display for only `Default` space selected

<img width="739" alt="Screenshot 2024-12-17 at 13 30 00"
src="https://github.com/user-attachments/assets/0f98a9d7-211d-46ec-82c6-25d29a44be6b"
/>

Privileges summary display for only `Default` space selected

<img width="569" alt="Screenshot 2024-12-17 at 13 30 19"
src="https://github.com/user-attachments/assets/932771fd-6486-4b7e-9de5-6cd34ab74dc9"
/>

### How to test
With `Default` space:
1. Navigate to Creating a new Role and assign Kibana privileges.
2. Set the Spaces to `Default` Space and the privilege level to All.
3. Navigate to Management category and verify that Fleet is set to
`None`.
4. Click on "View privilege summary" and verify that Fleet is set to
`None`.

With `*All Spaces`:
1. Navigate to Creating a new Role and assign Kibana privileges.
2. Set the Spaces to `*All Spaces` and the privilege level to All.
3. Navigate to Management category and verify that Fleet is set to `All`
4. Click on "View privilege summary" and verify that Fleet is set to
`All`


### Checklist

Check the PR satisfies following conditions. 

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

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

## Release Note
Fixed privileges display for features/subFeatures that require all
spaces.

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Elena Shostak 2025-01-09 14:22:51 +01:00 committed by GitHub
parent 13582aa458
commit d4196cd902
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 764 additions and 127 deletions

View file

@ -132,6 +132,7 @@ export const FeatureTableExpandedRow = ({
onChange={(updatedPrivileges) => onChange(feature.id, updatedPrivileges)}
selectedFeaturePrivileges={selectedFeaturePrivileges}
disabled={disabled || !isCustomizing || isDisabledDueToSpaceSelection}
allSpacesSelected={allSpacesSelected}
/>
</EuiFlexItem>
);

View file

@ -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(
<SubFeatureForm
featureId={feature.id}
subFeature={subFeature1}
selectedFeaturePrivileges={['cool_all']}
privilegeCalculator={calculator}
privilegeIndex={0}
onChange={onChange}
disabled={true}
allSpacesSelected={false}
/>
);
const buttonGroups = wrapper.find(EuiButtonGroup);
buttonGroups.every((button) => button.props().idSelected.id === 'none');
});
});

View file

@ -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 (
<EuiButtonGroup
@ -187,7 +195,7 @@ export const SubFeatureForm = (props: Props) => {
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

View file

@ -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['featureId']>;
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 (
<div key={index}>
{privilegeGroup.privileges.map((privilege: SubFeaturePrivilege) => {
const isGranted = effectiveSubFeaturePrivileges.includes(privilege.id);
return (
<EuiFlexGroup gutterSize="s" data-test-subj="independentPrivilege" key={privilege.id}>
<EuiFlexItem grow={false}>
<EuiIconTip
type={isGranted ? 'check' : 'cross'}
color={isGranted ? 'primary' : 'danger'}
content={
isGranted
? i18n.translate(
'xpack.security.management.editRole.privilegeSummary.privilegeGrantedIconTip',
{ defaultMessage: 'Privilege is granted' }
)
: i18n.translate(
'xpack.security.management.editRole.privilegeSummary.privilegeNotGrantedIconTip',
{ defaultMessage: 'Privilege is not granted' }
)
}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiText size="s" data-test-subj="privilegeName">
{privilege.name}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
})}
</div>
);
},
[]
);
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 (
<EuiFlexGroup gutterSize="s" key={index} data-test-subj="mutexPrivilege">
<EuiFlexItem grow={false}>
<EuiIconTip
type={firstSelectedPrivilege ? 'check' : 'cross'}
color={firstSelectedPrivilege ? 'primary' : 'danger'}
content={firstSelectedPrivilege ? 'Privilege is granted' : 'Privilege is not granted'}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiText size="s" data-test-subj="privilegeName">
{firstSelectedPrivilege ?? 'None'}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
},
[]
);
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 (
<EuiFlexItem key={index} data-test-subj={`entry-${index}`}>
{subFeature.getPrivilegeGroups().map(
renderPrivilegeGroup(privs.subFeature, {
requireAllSpaces: subFeature.requireAllSpaces,
spaces,
})
)}
</EuiFlexItem>
);
});
},
[props.effectiveFeaturePrivileges, allSpacesEffectivePrivileges, renderPrivilegeGroup]
);
return (
<EuiFlexGroup direction="column">
{props.feature.getSubFeatures().map((subFeature) => {
@ -34,105 +170,11 @@ export const PrivilegeSummaryExpandedRow = (props: Props) => {
{subFeature.name}
</EuiText>
</EuiFlexItem>
{props.effectiveFeaturePrivileges.map((privs, index) => {
return (
<EuiFlexItem key={index} data-test-subj={`entry-${index}`}>
{subFeature.getPrivilegeGroups().map(renderPrivilegeGroup(privs.subFeature))}
</EuiFlexItem>
);
})}
{getEffectiveFeaturePrivileges(subFeature)}
</EuiFlexGroup>
</EuiFlexItem>
);
})}
</EuiFlexGroup>
);
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 (
<div key={index}>
{privilegeGroup.privileges.map((privilege: SubFeaturePrivilege) => {
const isGranted = effectiveSubFeaturePrivileges.includes(privilege.id);
return (
<EuiFlexGroup gutterSize="s" data-test-subj="independentPrivilege" key={privilege.id}>
<EuiFlexItem grow={false}>
<EuiIconTip
type={isGranted ? 'check' : 'cross'}
color={isGranted ? 'primary' : 'danger'}
content={
isGranted
? i18n.translate(
'xpack.security.management.editRole.privilegeSummary.privilegeGrantedIconTip',
{ defaultMessage: 'Privilege is granted' }
)
: i18n.translate(
'xpack.security.management.editRole.privilegeSummary.privilegeNotGrantedIconTip',
{ defaultMessage: 'Privilege is not granted' }
)
}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiText size="s" data-test-subj="privilegeName">
{privilege.name}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
})}
</div>
);
}
function renderMutuallyExclusivePrivilegeGroup(
effectiveSubFeaturePrivileges: string[],
privilegeGroup: SubFeaturePrivilegeGroup,
index: number
) {
const firstSelectedPrivilege = privilegeGroup.privileges.find((p) =>
effectiveSubFeaturePrivileges.includes(p.id)
)?.name;
return (
<EuiFlexGroup gutterSize="s" key={index} data-test-subj="mutexPrivilege">
<EuiFlexItem grow={false}>
<EuiIconTip
type={firstSelectedPrivilege ? 'check' : 'cross'}
color={firstSelectedPrivilege ? 'primary' : 'danger'}
content={firstSelectedPrivilege ? 'Privilege is granted' : 'Privilege is not granted'}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiText size="s" data-test-subj="privilegeName">
{firstSelectedPrivilege ?? 'None'}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
}
};

View file

@ -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'],
}),
},
});
});
});
});
});

View file

@ -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}
</span>
);
@ -178,12 +205,14 @@ export const PrivilegeSummaryTable = (props: PrivilegeSummaryTableProps) => {
}
columns.push(featureColumn, ...privilegeColumns);
const privileges = rawKibanaPrivileges.reduce((acc, entry) => {
const privileges = rawKibanaPrivileges.reduce<
Record<string, [string[], EffectiveFeaturePrivileges]>
>((acc, entry) => {
return {
...acc,
[getColumnKey(entry)]: calculator.getEffectiveFeaturePrivileges(entry),
[getColumnKey(entry)]: [entry.spaces, calculator.getEffectiveFeaturePrivileges(entry)],
};
}, {} as Record<string, EffectiveFeaturePrivileges>);
}, {});
const accordions: any[] = [];
@ -210,11 +239,15 @@ export const PrivilegeSummaryTable = (props: PrivilegeSummaryTableProps) => {
</EuiFlexGroup>
);
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]: (
<PrivilegeSummaryExpandedRow
feature={props.kibanaPrivileges.getSecuredFeature(featureId)}
effectiveFeaturePrivileges={Object.values(privileges).map((p) => p[featureId])}
effectiveFeaturePrivileges={Object.values(privileges).map(([spaces, privs]) => [
spaces,
privs[featureId],
])}
/>
),
};

View file

@ -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",

View file

@ -218,7 +218,7 @@ export class SpaceAwarePrivilegeSection extends Component<Props, State> {
const viewMatrixButton = (
<PrivilegeSummary
role={this.props.role}
spaces={this.getDisplaySpaces()}
spaces={this.getSelectedSpaces()}
kibanaPrivileges={this.props.kibanaPrivileges}
canCustomizeSubFeaturePrivileges={this.props.canCustomizeSubFeaturePrivileges}
spacesApiUi={this.props.spacesApiUi}
@ -240,6 +240,11 @@ export class SpaceAwarePrivilegeSection extends Component<Props, State> {
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(