Skip to content

Commit

Permalink
Fleet Privileges Display (elastic#204402)
Browse files Browse the repository at this point in the history
## 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 <[email protected]>
  • Loading branch information
2 people authored and CAWilson94 committed Jan 10, 2025
1 parent e279292 commit 893303a
Show file tree
Hide file tree
Showing 8 changed files with 764 additions and 127 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export const FeatureTableExpandedRow = ({
onChange={(updatedPrivileges) => onChange(feature.id, updatedPrivileges)}
selectedFeaturePrivileges={selectedFeaturePrivileges}
disabled={disabled || !isCustomizing || isDisabledDueToSpaceSelection}
allSpacesSelected={allSpacesSelected}
/>
</EuiFlexItem>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ interface Props {
onChange: (selectedPrivileges: string[]) => void;
disabled?: boolean;
categoryId?: string;
allSpacesSelected?: boolean;
}

export const SubFeatureForm = (props: Props) => {
Expand Down Expand Up @@ -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) => {
Expand All @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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>
);
}
};
Loading

0 comments on commit 893303a

Please sign in to comment.