Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fleet Privileges Display #204402

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
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