mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
Show sub-feature privileges when using the Basic license (#142020)
* Augments /api/security/privileges with optional respectLicenseLevel parameter for use by the edit_role_page. Implements fix for 125289 - Show sub-feature privileges when using the Basic license * Changed EuiTooltip to EuiIconTip. * Updated unit tests for feature table expanded row to include new property checks. * Renamed property to improve readability and reduce confusion. Fixed state of switch checked in sub-feature customization. * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * Fixed privilege get API default for 'respectLicenseLevel'. Updated privilege unit tests. * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * Uodated test description to match property name. * Updated privilege API integration tests to include new 'respectLicenseLevel' optional parameter. * Replaced empty fragment with undefined. Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Thom Heymann <190132+thomheymann@users.noreply.github.com>
This commit is contained in:
parent
b6885f21c6
commit
fb632caa33
11 changed files with 638 additions and 100 deletions
|
@ -141,7 +141,7 @@ function usePrivileges(
|
||||||
);
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Promise.all([
|
Promise.all([
|
||||||
privilegesAPIClient.getAll({ includeActions: true }),
|
privilegesAPIClient.getAll({ includeActions: true, respectLicenseLevel: false }),
|
||||||
privilegesAPIClient.getBuiltIn(),
|
privilegesAPIClient.getBuiltIn(),
|
||||||
]).then(
|
]).then(
|
||||||
([kibanaPrivileges, builtInESPrivileges]) =>
|
([kibanaPrivileges, builtInESPrivileges]) =>
|
||||||
|
|
|
@ -7,10 +7,11 @@
|
||||||
|
|
||||||
import './feature_table.scss';
|
import './feature_table.scss';
|
||||||
|
|
||||||
import type { EuiAccordionProps, EuiButtonGroupOptionProps } from '@elastic/eui';
|
|
||||||
import {
|
import {
|
||||||
EuiAccordion,
|
EuiAccordion,
|
||||||
|
EuiAccordionProps,
|
||||||
EuiButtonGroup,
|
EuiButtonGroup,
|
||||||
|
EuiButtonGroupOptionProps,
|
||||||
EuiCallOut,
|
EuiCallOut,
|
||||||
EuiFlexGroup,
|
EuiFlexGroup,
|
||||||
EuiFlexItem,
|
EuiFlexItem,
|
||||||
|
@ -216,7 +217,6 @@ export class FeatureTable extends Component<Props, State> {
|
||||||
extraAction: EuiAccordionProps['extraAction'],
|
extraAction: EuiAccordionProps['extraAction'],
|
||||||
infoIcon: JSX.Element
|
infoIcon: JSX.Element
|
||||||
) => {
|
) => {
|
||||||
const { canCustomizeSubFeaturePrivileges } = this.props;
|
|
||||||
const hasSubFeaturePrivileges = feature.getSubFeaturePrivileges().length > 0;
|
const hasSubFeaturePrivileges = feature.getSubFeaturePrivileges().length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -228,12 +228,8 @@ export class FeatureTable extends Component<Props, State> {
|
||||||
data-test-subj="featurePrivilegeControls"
|
data-test-subj="featurePrivilegeControls"
|
||||||
buttonContent={buttonContent}
|
buttonContent={buttonContent}
|
||||||
extraAction={extraAction}
|
extraAction={extraAction}
|
||||||
forceState={
|
forceState={hasSubFeaturePrivileges ? undefined : 'closed'}
|
||||||
canCustomizeSubFeaturePrivileges && hasSubFeaturePrivileges ? undefined : 'closed'
|
arrowDisplay={hasSubFeaturePrivileges ? 'left' : 'none'}
|
||||||
}
|
|
||||||
arrowDisplay={
|
|
||||||
canCustomizeSubFeaturePrivileges && hasSubFeaturePrivileges ? 'left' : 'none'
|
|
||||||
}
|
|
||||||
onToggle={(isOpen: boolean) => {
|
onToggle={(isOpen: boolean) => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
this.state.expandedPrivilegeControls.add(feature.id);
|
this.state.expandedPrivilegeControls.add(feature.id);
|
||||||
|
@ -256,6 +252,9 @@ export class FeatureTable extends Component<Props, State> {
|
||||||
this.props.role.kibana[this.props.privilegeIndex].feature[feature.id] ?? []
|
this.props.role.kibana[this.props.privilegeIndex].feature[feature.id] ?? []
|
||||||
}
|
}
|
||||||
disabled={this.props.disabled}
|
disabled={this.props.disabled}
|
||||||
|
licenseAllowsSubFeatPrivCustomization={
|
||||||
|
this.props.canCustomizeSubFeaturePrivileges
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</EuiAccordion>
|
</EuiAccordion>
|
||||||
|
@ -335,14 +334,10 @@ export class FeatureTable extends Component<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { canCustomizeSubFeaturePrivileges } = this.props;
|
|
||||||
const hasSubFeaturePrivileges = feature.getSubFeaturePrivileges().length > 0;
|
const hasSubFeaturePrivileges = feature.getSubFeaturePrivileges().length > 0;
|
||||||
|
|
||||||
const showAccordionArrow = canCustomizeSubFeaturePrivileges && hasSubFeaturePrivileges;
|
|
||||||
|
|
||||||
const buttonContent = (
|
const buttonContent = (
|
||||||
<>
|
<>
|
||||||
{!showAccordionArrow && <EuiIcon type="empty" size="l" />}{' '}
|
{!hasSubFeaturePrivileges && <EuiIcon type="empty" size="l" />}{' '}
|
||||||
<FeatureTableCell feature={feature} />
|
<FeatureTableCell feature={feature} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -25,6 +25,75 @@ const createRole = (kibana: Role['kibana'] = []): Role => {
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('FeatureTableExpandedRow', () => {
|
describe('FeatureTableExpandedRow', () => {
|
||||||
|
it('indicates sub-feature privileges are not customizable when licenseAllowsSubFeatPrivCustomization is false', () => {
|
||||||
|
const role = createRole([
|
||||||
|
{
|
||||||
|
base: [],
|
||||||
|
feature: {
|
||||||
|
with_sub_features: ['minimal_read'],
|
||||||
|
},
|
||||||
|
spaces: ['foo'],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||||
|
|
||||||
|
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||||
|
|
||||||
|
const feature = kibanaPrivileges.getSecuredFeature('with_sub_features');
|
||||||
|
|
||||||
|
const wrapper = mountWithIntl(
|
||||||
|
<FeatureTableExpandedRow
|
||||||
|
feature={feature}
|
||||||
|
privilegeIndex={0}
|
||||||
|
privilegeCalculator={calculator}
|
||||||
|
selectedFeaturePrivileges={['minimal_read']}
|
||||||
|
onChange={jest.fn()}
|
||||||
|
licenseAllowsSubFeatPrivCustomization={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
wrapper.find('EuiSwitch[data-test-subj="customizeSubFeaturePrivileges"]').props()
|
||||||
|
).toMatchObject({
|
||||||
|
disabled: true,
|
||||||
|
checked: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('EuiIconTip[data-test-subj="subFeaturesTip"]').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('indicates sub-feature privileges can be customized when licenseAllowsSubFeatPrivCustomization is true', () => {
|
||||||
|
const role = createRole([
|
||||||
|
{
|
||||||
|
base: [],
|
||||||
|
feature: {
|
||||||
|
with_sub_features: ['minimal_read'],
|
||||||
|
},
|
||||||
|
spaces: ['foo'],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures);
|
||||||
|
|
||||||
|
const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role);
|
||||||
|
|
||||||
|
const feature = kibanaPrivileges.getSecuredFeature('with_sub_features');
|
||||||
|
|
||||||
|
const wrapper = mountWithIntl(
|
||||||
|
<FeatureTableExpandedRow
|
||||||
|
feature={feature}
|
||||||
|
privilegeIndex={0}
|
||||||
|
privilegeCalculator={calculator}
|
||||||
|
selectedFeaturePrivileges={['none']}
|
||||||
|
onChange={jest.fn()}
|
||||||
|
licenseAllowsSubFeatPrivCustomization={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
wrapper.find('EuiIconTip[data-test-subj="cannotCustomizeSubFeaturesTooltip"]').length
|
||||||
|
).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
it('indicates sub-feature privileges are being customized if a minimal feature privilege is set', () => {
|
it('indicates sub-feature privileges are being customized if a minimal feature privilege is set', () => {
|
||||||
const role = createRole([
|
const role = createRole([
|
||||||
{
|
{
|
||||||
|
@ -48,6 +117,7 @@ describe('FeatureTableExpandedRow', () => {
|
||||||
privilegeCalculator={calculator}
|
privilegeCalculator={calculator}
|
||||||
selectedFeaturePrivileges={['minimal_read']}
|
selectedFeaturePrivileges={['minimal_read']}
|
||||||
onChange={jest.fn()}
|
onChange={jest.fn()}
|
||||||
|
licenseAllowsSubFeatPrivCustomization={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -82,6 +152,7 @@ describe('FeatureTableExpandedRow', () => {
|
||||||
privilegeCalculator={calculator}
|
privilegeCalculator={calculator}
|
||||||
selectedFeaturePrivileges={['read']}
|
selectedFeaturePrivileges={['read']}
|
||||||
onChange={jest.fn()}
|
onChange={jest.fn()}
|
||||||
|
licenseAllowsSubFeatPrivCustomization={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -114,6 +185,7 @@ describe('FeatureTableExpandedRow', () => {
|
||||||
privilegeCalculator={calculator}
|
privilegeCalculator={calculator}
|
||||||
selectedFeaturePrivileges={['read']}
|
selectedFeaturePrivileges={['read']}
|
||||||
onChange={jest.fn()}
|
onChange={jest.fn()}
|
||||||
|
licenseAllowsSubFeatPrivCustomization={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -150,6 +222,7 @@ describe('FeatureTableExpandedRow', () => {
|
||||||
privilegeCalculator={calculator}
|
privilegeCalculator={calculator}
|
||||||
selectedFeaturePrivileges={['read']}
|
selectedFeaturePrivileges={['read']}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
licenseAllowsSubFeatPrivCustomization={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -189,6 +262,7 @@ describe('FeatureTableExpandedRow', () => {
|
||||||
privilegeCalculator={calculator}
|
privilegeCalculator={calculator}
|
||||||
selectedFeaturePrivileges={['minimal_read', 'cool_read', 'cool_toggle_2']}
|
selectedFeaturePrivileges={['minimal_read', 'cool_read', 'cool_toggle_2']}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
licenseAllowsSubFeatPrivCustomization={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -6,9 +6,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { EuiSwitchEvent } from '@elastic/eui';
|
import type { EuiSwitchEvent } from '@elastic/eui';
|
||||||
import { EuiFlexGroup, EuiFlexItem, EuiSwitch } from '@elastic/eui';
|
import { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiSwitch } from '@elastic/eui';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
import { FormattedMessage } from '@kbn/i18n-react';
|
import { FormattedMessage } from '@kbn/i18n-react';
|
||||||
|
|
||||||
import type { SecuredFeature } from '../../../../model';
|
import type { SecuredFeature } from '../../../../model';
|
||||||
|
@ -21,6 +22,7 @@ interface Props {
|
||||||
privilegeIndex: number;
|
privilegeIndex: number;
|
||||||
selectedFeaturePrivileges: string[];
|
selectedFeaturePrivileges: string[];
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
licenseAllowsSubFeatPrivCustomization: boolean;
|
||||||
onChange: (featureId: string, featurePrivileges: string[]) => void;
|
onChange: (featureId: string, featurePrivileges: string[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,11 +33,13 @@ export const FeatureTableExpandedRow = ({
|
||||||
privilegeCalculator,
|
privilegeCalculator,
|
||||||
selectedFeaturePrivileges,
|
selectedFeaturePrivileges,
|
||||||
disabled,
|
disabled,
|
||||||
|
licenseAllowsSubFeatPrivCustomization,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [isCustomizing, setIsCustomizing] = useState(() => {
|
const [isCustomizing, setIsCustomizing] = useState(() => {
|
||||||
return feature
|
return (
|
||||||
.getMinimalFeaturePrivileges()
|
licenseAllowsSubFeatPrivCustomization &&
|
||||||
.some((p) => selectedFeaturePrivileges.includes(p.id));
|
feature.getMinimalFeaturePrivileges().some((p) => selectedFeaturePrivileges.includes(p.id))
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -43,10 +47,13 @@ export const FeatureTableExpandedRow = ({
|
||||||
.getMinimalFeaturePrivileges()
|
.getMinimalFeaturePrivileges()
|
||||||
.some((p) => selectedFeaturePrivileges.includes(p.id));
|
.some((p) => selectedFeaturePrivileges.includes(p.id));
|
||||||
|
|
||||||
if (!hasMinimalFeaturePrivilegeSelected && isCustomizing) {
|
if (
|
||||||
|
(!licenseAllowsSubFeatPrivCustomization || !hasMinimalFeaturePrivilegeSelected) &&
|
||||||
|
isCustomizing
|
||||||
|
) {
|
||||||
setIsCustomizing(false);
|
setIsCustomizing(false);
|
||||||
}
|
}
|
||||||
}, [feature, isCustomizing, selectedFeaturePrivileges]);
|
}, [feature, isCustomizing, selectedFeaturePrivileges, licenseAllowsSubFeatPrivCustomization]);
|
||||||
|
|
||||||
const onCustomizeSubFeatureChange = (e: EuiSwitchEvent) => {
|
const onCustomizeSubFeatureChange = (e: EuiSwitchEvent) => {
|
||||||
onChange(
|
onChange(
|
||||||
|
@ -63,21 +70,44 @@ export const FeatureTableExpandedRow = ({
|
||||||
return (
|
return (
|
||||||
<EuiFlexGroup direction="column">
|
<EuiFlexGroup direction="column">
|
||||||
<EuiFlexItem>
|
<EuiFlexItem>
|
||||||
<EuiSwitch
|
<div>
|
||||||
label={
|
<EuiSwitch
|
||||||
<FormattedMessage
|
label={
|
||||||
id="xpack.security.management.editRole.featureTable.customizeSubFeaturePrivilegesSwitchLabel"
|
<FormattedMessage
|
||||||
defaultMessage="Customize sub-feature privileges"
|
id="xpack.security.management.editRole.featureTable.customizeSubFeaturePrivilegesSwitchLabel"
|
||||||
|
defaultMessage="Customize sub-feature privileges"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
checked={isCustomizing}
|
||||||
|
onChange={onCustomizeSubFeatureChange}
|
||||||
|
data-test-subj="customizeSubFeaturePrivileges"
|
||||||
|
disabled={
|
||||||
|
disabled ||
|
||||||
|
!licenseAllowsSubFeatPrivCustomization ||
|
||||||
|
!privilegeCalculator.canCustomizeSubFeaturePrivileges(feature.id, privilegeIndex)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{licenseAllowsSubFeatPrivCustomization ? undefined : (
|
||||||
|
<EuiIconTip
|
||||||
|
data-test-subj="subFeaturesTip"
|
||||||
|
position="right"
|
||||||
|
aria-label="sub-feature-information-tip"
|
||||||
|
size="m"
|
||||||
|
type="iInCircle"
|
||||||
|
color="subdued"
|
||||||
|
iconProps={{
|
||||||
|
className: 'eui-alignTop',
|
||||||
|
}}
|
||||||
|
content={i18n.translate(
|
||||||
|
'xpack.security.management.editRole.featureTable.cannotCustomizeSubFeaturesTooltip',
|
||||||
|
{
|
||||||
|
defaultMessage:
|
||||||
|
'Customization of sub-feature privileges is a subscription feature.',
|
||||||
|
}
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
checked={isCustomizing}
|
</div>
|
||||||
onChange={onCustomizeSubFeatureChange}
|
|
||||||
data-test-subj="customizeSubFeaturePrivileges"
|
|
||||||
disabled={
|
|
||||||
disabled ||
|
|
||||||
!privilegeCalculator.canCustomizeSubFeaturePrivileges(feature.id, privilegeIndex)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
{feature.getSubFeatures().map((subFeature) => {
|
{feature.getSubFeatures().map((subFeature) => {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -547,6 +547,7 @@ export class PrivilegeSpaceForm extends Component<Props, State> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
role,
|
role,
|
||||||
privilegeCalculator: new PrivilegeFormCalculator(this.props.kibanaPrivileges, role),
|
privilegeCalculator: new PrivilegeFormCalculator(this.props.kibanaPrivileges, role),
|
||||||
|
|
|
@ -12,9 +12,19 @@ import type { BuiltinESPrivileges, RawKibanaPrivileges } from '../../../common/m
|
||||||
export class PrivilegesAPIClient {
|
export class PrivilegesAPIClient {
|
||||||
constructor(private readonly http: HttpStart) {}
|
constructor(private readonly http: HttpStart) {}
|
||||||
|
|
||||||
async getAll({ includeActions }: { includeActions: boolean }) {
|
/*
|
||||||
|
* respectLicenseLevel is an internal optional parameter soley for getting all sub-feature
|
||||||
|
* privilieges to use in the UI. It is not meant for any other use.
|
||||||
|
*/
|
||||||
|
async getAll({
|
||||||
|
includeActions,
|
||||||
|
respectLicenseLevel = true,
|
||||||
|
}: {
|
||||||
|
includeActions: boolean;
|
||||||
|
respectLicenseLevel: boolean;
|
||||||
|
}) {
|
||||||
return await this.http.get<RawKibanaPrivileges>('/api/security/privileges', {
|
return await this.http.get<RawKibanaPrivileges>('/api/security/privileges', {
|
||||||
query: { includeActions },
|
query: { includeActions, respectLicenseLevel },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1888,6 +1888,225 @@ describe('subFeatures', () => {
|
||||||
actions.ui.get('foo', 'sub-feature-ui'),
|
actions.ui.get('foo', 'sub-feature-ui'),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test(`should get the sub-feature privileges if 'respectLicenseLevel' is false`, () => {
|
||||||
|
const features: KibanaFeature[] = [
|
||||||
|
new KibanaFeature({
|
||||||
|
id: 'foo',
|
||||||
|
name: 'Foo KibanaFeature',
|
||||||
|
app: [],
|
||||||
|
category: { id: 'foo', label: 'foo' },
|
||||||
|
privileges: {
|
||||||
|
all: {
|
||||||
|
savedObject: {
|
||||||
|
all: [],
|
||||||
|
read: [],
|
||||||
|
},
|
||||||
|
ui: ['foo'],
|
||||||
|
},
|
||||||
|
read: {
|
||||||
|
savedObject: {
|
||||||
|
all: [],
|
||||||
|
read: [],
|
||||||
|
},
|
||||||
|
ui: ['foo'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
subFeatures: [
|
||||||
|
{
|
||||||
|
name: 'subFeature1',
|
||||||
|
privilegeGroups: [
|
||||||
|
{
|
||||||
|
groupType: 'independent',
|
||||||
|
privileges: [
|
||||||
|
{
|
||||||
|
id: 'subFeaturePriv1',
|
||||||
|
name: 'sub feature priv 1',
|
||||||
|
includeIn: 'read',
|
||||||
|
savedObject: {
|
||||||
|
all: ['all-sub-feature-type'],
|
||||||
|
read: ['read-sub-feature-type'],
|
||||||
|
},
|
||||||
|
ui: ['sub-feature-ui'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockFeaturesPlugin = featuresPluginMock.createSetup();
|
||||||
|
mockFeaturesPlugin.getKibanaFeatures.mockReturnValue(features);
|
||||||
|
const privileges = privilegesFactory(actions, mockFeaturesPlugin, mockLicenseServiceBasic);
|
||||||
|
|
||||||
|
const actual = privileges.get(false);
|
||||||
|
expect(actual.features).toHaveProperty(`foo.subFeaturePriv1`);
|
||||||
|
|
||||||
|
expect(actual.features).toHaveProperty(`foo.all`, [
|
||||||
|
actions.login,
|
||||||
|
actions.version,
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'get'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'find'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'create'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||||
|
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||||
|
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||||
|
actions.savedObject.get('read-sub-feature-type', 'find'),
|
||||||
|
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
|
||||||
|
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
|
||||||
|
actions.ui.get('foo', 'foo'),
|
||||||
|
actions.ui.get('foo', 'sub-feature-ui'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(actual.features).toHaveProperty(`foo.minimal_all`, [
|
||||||
|
actions.login,
|
||||||
|
actions.version,
|
||||||
|
actions.ui.get('foo', 'foo'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(actual.features).toHaveProperty(`foo.read`, [
|
||||||
|
actions.login,
|
||||||
|
actions.version,
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'get'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'find'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'create'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||||
|
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||||
|
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||||
|
actions.savedObject.get('read-sub-feature-type', 'find'),
|
||||||
|
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
|
||||||
|
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
|
||||||
|
actions.ui.get('foo', 'foo'),
|
||||||
|
actions.ui.get('foo', 'sub-feature-ui'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(actual.features).toHaveProperty(`foo.minimal_read`, [
|
||||||
|
actions.login,
|
||||||
|
actions.version,
|
||||||
|
actions.ui.get('foo', 'foo'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(actual).toHaveProperty('global.all', [
|
||||||
|
actions.login,
|
||||||
|
actions.version,
|
||||||
|
actions.api.get('decryptedTelemetry'),
|
||||||
|
actions.api.get('features'),
|
||||||
|
actions.api.get('taskManager'),
|
||||||
|
actions.space.manage,
|
||||||
|
actions.ui.get('spaces', 'manage'),
|
||||||
|
actions.ui.get('management', 'kibana', 'spaces'),
|
||||||
|
actions.ui.get('catalogue', 'spaces'),
|
||||||
|
actions.ui.get('enterpriseSearch', 'all'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'get'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'find'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'create'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||||
|
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||||
|
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||||
|
actions.savedObject.get('read-sub-feature-type', 'find'),
|
||||||
|
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
|
||||||
|
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
|
||||||
|
actions.ui.get('foo', 'foo'),
|
||||||
|
actions.ui.get('foo', 'sub-feature-ui'),
|
||||||
|
]);
|
||||||
|
expect(actual).toHaveProperty('global.read', [
|
||||||
|
actions.login,
|
||||||
|
actions.version,
|
||||||
|
actions.api.get('decryptedTelemetry'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'get'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'find'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'create'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||||
|
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||||
|
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||||
|
actions.savedObject.get('read-sub-feature-type', 'find'),
|
||||||
|
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
|
||||||
|
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
|
||||||
|
actions.ui.get('foo', 'foo'),
|
||||||
|
actions.ui.get('foo', 'sub-feature-ui'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(actual).toHaveProperty('space.all', [
|
||||||
|
actions.login,
|
||||||
|
actions.version,
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'get'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'find'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'create'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||||
|
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||||
|
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||||
|
actions.savedObject.get('read-sub-feature-type', 'find'),
|
||||||
|
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
|
||||||
|
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
|
||||||
|
actions.ui.get('foo', 'foo'),
|
||||||
|
actions.ui.get('foo', 'sub-feature-ui'),
|
||||||
|
]);
|
||||||
|
expect(actual).toHaveProperty('space.read', [
|
||||||
|
actions.login,
|
||||||
|
actions.version,
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'bulk_get'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'get'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'find'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'open_point_in_time'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'close_point_in_time'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'create'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'bulk_create'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||||
|
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||||
|
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||||
|
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||||
|
actions.savedObject.get('read-sub-feature-type', 'find'),
|
||||||
|
actions.savedObject.get('read-sub-feature-type', 'open_point_in_time'),
|
||||||
|
actions.savedObject.get('read-sub-feature-type', 'close_point_in_time'),
|
||||||
|
actions.ui.get('foo', 'foo'),
|
||||||
|
actions.ui.get('foo', 'sub-feature-ui'),
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`when license allows subfeatures, but not a specific sub feature`, () => {
|
describe(`when license allows subfeatures, but not a specific sub feature`, () => {
|
||||||
|
|
|
@ -18,7 +18,7 @@ import type { Actions } from '../actions';
|
||||||
import { featurePrivilegeBuilderFactory } from './feature_privilege_builder';
|
import { featurePrivilegeBuilderFactory } from './feature_privilege_builder';
|
||||||
|
|
||||||
export interface PrivilegesService {
|
export interface PrivilegesService {
|
||||||
get(): RawKibanaPrivileges;
|
get(respectLicenseLevel?: boolean): RawKibanaPrivileges;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function privilegesFactory(
|
export function privilegesFactory(
|
||||||
|
@ -29,7 +29,7 @@ export function privilegesFactory(
|
||||||
const featurePrivilegeBuilder = featurePrivilegeBuilderFactory(actions);
|
const featurePrivilegeBuilder = featurePrivilegeBuilderFactory(actions);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
get() {
|
get(respectLicenseLevel: boolean = true) {
|
||||||
const features = featuresService.getKibanaFeatures();
|
const features = featuresService.getKibanaFeatures();
|
||||||
const { allowSubFeaturePrivileges } = licenseService.getFeatures();
|
const { allowSubFeaturePrivileges } = licenseService.getFeatures();
|
||||||
const { hasAtLeast: licenseHasAtLeast } = licenseService;
|
const { hasAtLeast: licenseHasAtLeast } = licenseService;
|
||||||
|
@ -82,7 +82,10 @@ export function privilegesFactory(
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allowSubFeaturePrivileges && feature.subFeatures?.length > 0) {
|
if (
|
||||||
|
(!respectLicenseLevel || allowSubFeaturePrivileges) &&
|
||||||
|
feature.subFeatures?.length > 0
|
||||||
|
) {
|
||||||
for (const subFeaturePrivilege of featuresService.subFeaturePrivilegeIterator(
|
for (const subFeaturePrivilege of featuresService.subFeaturePrivilegeIterator(
|
||||||
feature,
|
feature,
|
||||||
licenseHasAtLeast
|
licenseHasAtLeast
|
||||||
|
|
|
@ -21,11 +21,15 @@ export function defineGetPrivilegesRoutes({ router, authz }: RouteDefinitionPara
|
||||||
includeActions: schema.maybe(
|
includeActions: schema.maybe(
|
||||||
schema.oneOf([schema.literal('true'), schema.literal('false')])
|
schema.oneOf([schema.literal('true'), schema.literal('false')])
|
||||||
),
|
),
|
||||||
|
respectLicenseLevel: schema.maybe(
|
||||||
|
schema.oneOf([schema.literal('true'), schema.literal('false')])
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
createLicensedRouteHandler((context, request, response) => {
|
createLicensedRouteHandler((context, request, response) => {
|
||||||
const privileges = authz.privileges.get();
|
const respectLicenseLevel = request.query.respectLicenseLevel !== 'false'; // if undefined resolve to true by default
|
||||||
|
const privileges = authz.privileges.get(respectLicenseLevel);
|
||||||
const includeActions = request.query.includeActions === 'true';
|
const includeActions = request.query.includeActions === 'true';
|
||||||
const privilegesResponseBody = includeActions
|
const privilegesResponseBody = includeActions
|
||||||
? privileges
|
? privileges
|
||||||
|
|
|
@ -14,70 +14,71 @@ import { FtrProviderContext } from '../../ftr_provider_context';
|
||||||
export default function ({ getService }: FtrProviderContext) {
|
export default function ({ getService }: FtrProviderContext) {
|
||||||
const supertest = getService('supertest');
|
const supertest = getService('supertest');
|
||||||
|
|
||||||
|
const expectedWithoutActions = {
|
||||||
|
global: ['all', 'read'],
|
||||||
|
space: ['all', 'read'],
|
||||||
|
features: {
|
||||||
|
graph: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
savedObjectsTagging: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
canvas: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
maps: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
generalCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'],
|
||||||
|
observabilityCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'],
|
||||||
|
fleetv2: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
fleet: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
actions: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
stackAlerts: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
ml: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
siem: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
uptime: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
securitySolutionCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'],
|
||||||
|
infrastructure: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
logs: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
apm: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
discover: [
|
||||||
|
'all',
|
||||||
|
'read',
|
||||||
|
'minimal_all',
|
||||||
|
'minimal_read',
|
||||||
|
'url_create',
|
||||||
|
'store_search_session',
|
||||||
|
],
|
||||||
|
visualize: ['all', 'read', 'minimal_all', 'minimal_read', 'url_create'],
|
||||||
|
dashboard: [
|
||||||
|
'all',
|
||||||
|
'read',
|
||||||
|
'minimal_all',
|
||||||
|
'minimal_read',
|
||||||
|
'url_create',
|
||||||
|
'store_search_session',
|
||||||
|
],
|
||||||
|
dev_tools: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
advancedSettings: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
indexPatterns: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
savedObjectsManagement: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
osquery: [
|
||||||
|
'all',
|
||||||
|
'read',
|
||||||
|
'minimal_all',
|
||||||
|
'minimal_read',
|
||||||
|
'live_queries_all',
|
||||||
|
'live_queries_read',
|
||||||
|
'run_saved_queries',
|
||||||
|
'saved_queries_all',
|
||||||
|
'saved_queries_read',
|
||||||
|
'packs_all',
|
||||||
|
'packs_read',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
reserved: ['fleet-setup', 'ml_user', 'ml_admin', 'ml_apm_user', 'monitoring'],
|
||||||
|
};
|
||||||
|
|
||||||
describe('Privileges', () => {
|
describe('Privileges', () => {
|
||||||
describe('GET /api/security/privileges', () => {
|
describe('GET /api/security/privileges', () => {
|
||||||
it('should return a privilege map with all known privileges, without actions', async () => {
|
it('should return a privilege map with all known privileges, without actions', async () => {
|
||||||
// If you're adding a privilege to the following, that's great!
|
// If you're adding a privilege to the following, that's great!
|
||||||
// If you're removing a privilege, this breaks backwards compatibility
|
// If you're removing a privilege, this breaks backwards compatibility
|
||||||
// Roles are associated with these privileges, and we shouldn't be removing them in a minor version.
|
// Roles are associated with these privileges, and we shouldn't be removing them in a minor version.
|
||||||
const expected = {
|
|
||||||
global: ['all', 'read'],
|
|
||||||
space: ['all', 'read'],
|
|
||||||
features: {
|
|
||||||
graph: ['all', 'read', 'minimal_all', 'minimal_read'],
|
|
||||||
savedObjectsTagging: ['all', 'read', 'minimal_all', 'minimal_read'],
|
|
||||||
canvas: ['all', 'read', 'minimal_all', 'minimal_read'],
|
|
||||||
maps: ['all', 'read', 'minimal_all', 'minimal_read'],
|
|
||||||
generalCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'],
|
|
||||||
observabilityCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'],
|
|
||||||
fleetv2: ['all', 'read', 'minimal_all', 'minimal_read'],
|
|
||||||
fleet: ['all', 'read', 'minimal_all', 'minimal_read'],
|
|
||||||
actions: ['all', 'read', 'minimal_all', 'minimal_read'],
|
|
||||||
stackAlerts: ['all', 'read', 'minimal_all', 'minimal_read'],
|
|
||||||
ml: ['all', 'read', 'minimal_all', 'minimal_read'],
|
|
||||||
siem: ['all', 'read', 'minimal_all', 'minimal_read'],
|
|
||||||
uptime: ['all', 'read', 'minimal_all', 'minimal_read'],
|
|
||||||
securitySolutionCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'],
|
|
||||||
infrastructure: ['all', 'read', 'minimal_all', 'minimal_read'],
|
|
||||||
logs: ['all', 'read', 'minimal_all', 'minimal_read'],
|
|
||||||
apm: ['all', 'read', 'minimal_all', 'minimal_read'],
|
|
||||||
discover: [
|
|
||||||
'all',
|
|
||||||
'read',
|
|
||||||
'minimal_all',
|
|
||||||
'minimal_read',
|
|
||||||
'url_create',
|
|
||||||
'store_search_session',
|
|
||||||
],
|
|
||||||
visualize: ['all', 'read', 'minimal_all', 'minimal_read', 'url_create'],
|
|
||||||
dashboard: [
|
|
||||||
'all',
|
|
||||||
'read',
|
|
||||||
'minimal_all',
|
|
||||||
'minimal_read',
|
|
||||||
'url_create',
|
|
||||||
'store_search_session',
|
|
||||||
],
|
|
||||||
dev_tools: ['all', 'read', 'minimal_all', 'minimal_read'],
|
|
||||||
advancedSettings: ['all', 'read', 'minimal_all', 'minimal_read'],
|
|
||||||
indexPatterns: ['all', 'read', 'minimal_all', 'minimal_read'],
|
|
||||||
savedObjectsManagement: ['all', 'read', 'minimal_all', 'minimal_read'],
|
|
||||||
osquery: [
|
|
||||||
'all',
|
|
||||||
'read',
|
|
||||||
'minimal_all',
|
|
||||||
'minimal_read',
|
|
||||||
'live_queries_all',
|
|
||||||
'live_queries_read',
|
|
||||||
'run_saved_queries',
|
|
||||||
'saved_queries_all',
|
|
||||||
'saved_queries_read',
|
|
||||||
'packs_all',
|
|
||||||
'packs_read',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
reserved: ['fleet-setup', 'ml_user', 'ml_admin', 'ml_apm_user', 'monitoring'],
|
|
||||||
};
|
|
||||||
|
|
||||||
await supertest
|
await supertest
|
||||||
.get('/api/security/privileges')
|
.get('/api/security/privileges')
|
||||||
|
@ -89,7 +90,7 @@ export default function ({ getService }: FtrProviderContext) {
|
||||||
// supertest uses assert.deepStrictEqual.
|
// supertest uses assert.deepStrictEqual.
|
||||||
// expect.js doesn't help us here.
|
// expect.js doesn't help us here.
|
||||||
// and lodash's isEqual doesn't know how to compare Sets.
|
// and lodash's isEqual doesn't know how to compare Sets.
|
||||||
const success = isEqualWith(res.body, expected, (value, other, key) => {
|
const success = isEqualWith(res.body, expectedWithoutActions, (value, other, key) => {
|
||||||
if (Array.isArray(value) && Array.isArray(other)) {
|
if (Array.isArray(value) && Array.isArray(other)) {
|
||||||
if (key === 'reserved') {
|
if (key === 'reserved') {
|
||||||
// order does not matter for the reserved privilege set.
|
// order does not matter for the reserved privilege set.
|
||||||
|
@ -106,7 +107,9 @@ export default function ({ getService }: FtrProviderContext) {
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Expected ${util.inspect(res.body)} to equal ${util.inspect(expected)}`
|
`Expected ${util.inspect(res.body)} to equal ${util.inspect(
|
||||||
|
expectedWithoutActions
|
||||||
|
)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -178,5 +181,115 @@ export default function ({ getService }: FtrProviderContext) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// In this non-Basic case, results should be exactly the same as not supplying the respectLicenseLevel flag
|
||||||
|
describe('GET /api/security/privileges?respectLicenseLevel=false', () => {
|
||||||
|
it('should return a privilege map with all known privileges, without actions', async () => {
|
||||||
|
// If you're adding a privilege to the following, that's great!
|
||||||
|
// If you're removing a privilege, this breaks backwards compatibility
|
||||||
|
// Roles are associated with these privileges, and we shouldn't be removing them in a minor version.
|
||||||
|
|
||||||
|
await supertest
|
||||||
|
.get('/api/security/privileges?respectLicenseLevel=false')
|
||||||
|
.set('kbn-xsrf', 'xxx')
|
||||||
|
.send()
|
||||||
|
.expect(200)
|
||||||
|
.expect((res: any) => {
|
||||||
|
// when comparing privileges, the order of the features doesn't matter (but the order of the privileges does)
|
||||||
|
// supertest uses assert.deepStrictEqual.
|
||||||
|
// expect.js doesn't help us here.
|
||||||
|
// and lodash's isEqual doesn't know how to compare Sets.
|
||||||
|
const success = isEqualWith(res.body, expectedWithoutActions, (value, other, key) => {
|
||||||
|
if (Array.isArray(value) && Array.isArray(other)) {
|
||||||
|
if (key === 'reserved') {
|
||||||
|
// order does not matter for the reserved privilege set.
|
||||||
|
return isEqual(value.sort(), other.sort());
|
||||||
|
}
|
||||||
|
// order matters for the rest, as the UI assumes they are returned in a descending order of permissiveness.
|
||||||
|
return isEqual(value, other);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lodash types aren't correct, `undefined` should be supported as a return value here and it
|
||||||
|
// has special meaning.
|
||||||
|
return undefined as any;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
throw new Error(
|
||||||
|
`Expected ${util.inspect(res.body)} to equal ${util.inspect(
|
||||||
|
expectedWithoutActions
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.expect(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// In this non-Basic case, results should be exactly the same as not supplying the respectLicenseLevel flag
|
||||||
|
describe('GET /api/security/privileges?includeActions=true&respectLicenseLevel=false', () => {
|
||||||
|
// The UI assumes that no wildcards are present when calculating the effective set of privileges.
|
||||||
|
// If this changes, then the "privilege calculators" will need revisiting to account for these wildcards.
|
||||||
|
it('should return a privilege map with actions which do not include wildcards', async () => {
|
||||||
|
await supertest
|
||||||
|
.get('/api/security/privileges?includeActions=true')
|
||||||
|
.set('kbn-xsrf', 'xxx')
|
||||||
|
.send()
|
||||||
|
.expect(200)
|
||||||
|
.expect((res: any) => {
|
||||||
|
const { features, global, space, reserved } = res.body as RawKibanaPrivileges;
|
||||||
|
expect(features).to.be.an('object');
|
||||||
|
expect(global).to.be.an('object');
|
||||||
|
expect(space).to.be.an('object');
|
||||||
|
expect(reserved).to.be.an('object');
|
||||||
|
|
||||||
|
Object.entries(features).forEach(([featureId, featurePrivs]) => {
|
||||||
|
Object.values(featurePrivs).forEach((actions) => {
|
||||||
|
expect(actions).to.be.an('array');
|
||||||
|
actions.forEach((action) => {
|
||||||
|
expect(action).to.be.a('string');
|
||||||
|
expect(action.indexOf('*')).to.eql(
|
||||||
|
-1,
|
||||||
|
`Feature ${featureId} with action ${action} cannot contain a wildcard`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.entries(global).forEach(([privilegeId, actions]) => {
|
||||||
|
expect(actions).to.be.an('array');
|
||||||
|
actions.forEach((action) => {
|
||||||
|
expect(action).to.be.a('string');
|
||||||
|
expect(action.indexOf('*')).to.eql(
|
||||||
|
-1,
|
||||||
|
`Global privilege ${privilegeId} with action ${action} cannot contain a wildcard`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.entries(space).forEach(([privilegeId, actions]) => {
|
||||||
|
expect(actions).to.be.an('array');
|
||||||
|
actions.forEach((action) => {
|
||||||
|
expect(action).to.be.a('string');
|
||||||
|
expect(action.indexOf('*')).to.eql(
|
||||||
|
-1,
|
||||||
|
`Space privilege ${privilegeId} with action ${action} cannot contain a wildcard`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.entries(reserved).forEach(([privilegeId, actions]) => {
|
||||||
|
expect(actions).to.be.an('array');
|
||||||
|
actions.forEach((action) => {
|
||||||
|
expect(action).to.be.a('string');
|
||||||
|
expect(action.indexOf('*')).to.eql(
|
||||||
|
-1,
|
||||||
|
`Reserved privilege ${privilegeId} with action ${action} cannot contain a wildcard`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,6 +79,95 @@ export default function ({ getService }: FtrProviderContext) {
|
||||||
})
|
})
|
||||||
.expect(200);
|
.expect(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should include sub-feature privileges when respectlicenseLevel is false', async () => {
|
||||||
|
const expected = {
|
||||||
|
global: ['all', 'read'],
|
||||||
|
space: ['all', 'read'],
|
||||||
|
features: {
|
||||||
|
graph: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
savedObjectsTagging: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
canvas: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
maps: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
generalCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'],
|
||||||
|
observabilityCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'],
|
||||||
|
fleetv2: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
fleet: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
actions: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
stackAlerts: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
ml: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
siem: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
uptime: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
securitySolutionCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'],
|
||||||
|
infrastructure: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
logs: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
apm: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
discover: [
|
||||||
|
'all',
|
||||||
|
'read',
|
||||||
|
'minimal_all',
|
||||||
|
'minimal_read',
|
||||||
|
'url_create',
|
||||||
|
'store_search_session',
|
||||||
|
],
|
||||||
|
visualize: ['all', 'read', 'minimal_all', 'minimal_read', 'url_create'],
|
||||||
|
dashboard: [
|
||||||
|
'all',
|
||||||
|
'read',
|
||||||
|
'minimal_all',
|
||||||
|
'minimal_read',
|
||||||
|
'url_create',
|
||||||
|
'store_search_session',
|
||||||
|
],
|
||||||
|
dev_tools: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
advancedSettings: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
indexPatterns: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
savedObjectsManagement: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
osquery: [
|
||||||
|
'all',
|
||||||
|
'read',
|
||||||
|
'minimal_all',
|
||||||
|
'minimal_read',
|
||||||
|
'live_queries_all',
|
||||||
|
'live_queries_read',
|
||||||
|
'run_saved_queries',
|
||||||
|
'saved_queries_all',
|
||||||
|
'saved_queries_read',
|
||||||
|
'packs_all',
|
||||||
|
'packs_read',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
reserved: ['fleet-setup', 'ml_user', 'ml_admin', 'ml_apm_user', 'monitoring'],
|
||||||
|
};
|
||||||
|
|
||||||
|
await supertest
|
||||||
|
.get('/api/security/privileges?respectLicenseLevel=false')
|
||||||
|
.set('kbn-xsrf', 'xxx')
|
||||||
|
.send()
|
||||||
|
.expect(200)
|
||||||
|
.expect((res: any) => {
|
||||||
|
// when comparing privileges, the order of the privileges doesn't matter.
|
||||||
|
// supertest uses assert.deepStrictEqual.
|
||||||
|
// expect.js doesn't help us here.
|
||||||
|
// and lodash's isEqual doesn't know how to compare Sets.
|
||||||
|
const success = isEqualWith(res.body, expected, (value, other, key) => {
|
||||||
|
if (Array.isArray(value) && Array.isArray(other)) {
|
||||||
|
return isEqual(value.sort(), other.sort());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lodash types aren't correct, `undefined` should be supported as a return value here and it
|
||||||
|
// has special meaning.
|
||||||
|
return undefined as any;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
throw new Error(
|
||||||
|
`Expected ${util.inspect(res.body)} to equal ${util.inspect(expected)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.expect(200);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue