mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution] [Detections] Disable edit button when user does not have actions privileges w/ rule + actions (#80220)
* disable edit button only when there is an action present on the rule to be edited, but the user attempting the edit does not have actions privileges * adds tooltip to explain why the edit rule button is disabled * prevent user from editing rules with actions on the all rules table * adds tooltip to appear on all rules table * updates tests for missing params and missing mock of useKibana * disable activate switch on all rules table and rule details page * remove as casting in favor of a boolean type guard to ensure actions.show capabilities are a boolean even though tye are typed as a boolean | Record * disable duplicate rule functionality for rules with actions * fix positioning of tooltips and add tooltip to rule duplicate button in overflow button * update tests * WIP - display bulk actions dropdown options as disabled + add tooltips describing why they are disabled * add eui tool tip as child of of each context menu item * PR feedback and utilize map of rule ids to rules to replace usage of array.finds * update snapshot * fix mocks * fix mocks * update wording with feedback from design team Co-authored-by: Patryk Kopycinski <contact@patrykkopycinski.com>
This commit is contained in:
parent
4c81b1a64b
commit
2f01a0911c
11 changed files with 368 additions and 75 deletions
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Rule } from '../../../detections/containers/detection_engine/rules';
|
||||
import * as i18n from '../../../detections/pages/detection_engine/rules/translations';
|
||||
import { isMlRule } from '../../../../common/machine_learning/helpers';
|
||||
import * as detectionI18n from '../../../detections/pages/detection_engine/translations';
|
||||
|
||||
export const isBoolean = (obj: unknown): obj is boolean => typeof obj === 'boolean';
|
||||
|
||||
export const canEditRuleWithActions = (
|
||||
rule: Rule | null | undefined,
|
||||
privileges:
|
||||
| boolean
|
||||
| Readonly<{
|
||||
[x: string]: boolean;
|
||||
}>
|
||||
): boolean => {
|
||||
if (rule == null) {
|
||||
return true;
|
||||
}
|
||||
if (rule.actions?.length > 0 && isBoolean(privileges)) {
|
||||
return privileges;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getToolTipContent = (
|
||||
rule: Rule | null | undefined,
|
||||
hasMlPermissions: boolean,
|
||||
hasReadActionsPrivileges:
|
||||
| boolean
|
||||
| Readonly<{
|
||||
[x: string]: boolean;
|
||||
}>
|
||||
): string | undefined => {
|
||||
if (rule == null) {
|
||||
return undefined;
|
||||
} else if (isMlRule(rule.type) && !hasMlPermissions) {
|
||||
return detectionI18n.ML_RULES_DISABLED_MESSAGE;
|
||||
} else if (!canEditRuleWithActions(rule, hasReadActionsPrivileges)) {
|
||||
return i18n.EDIT_RULE_SETTINGS_TOOLTIP;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
|
@ -40,7 +40,14 @@ exports[`RuleActionsOverflow snapshots renders correctly against snapshot 1`] =
|
|||
icon="copy"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Duplicate rule
|
||||
<EuiToolTip
|
||||
delay="regular"
|
||||
position="left"
|
||||
>
|
||||
<React.Fragment>
|
||||
Duplicate rule
|
||||
</React.Fragment>
|
||||
</EuiToolTip>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
data-test-subj="rules-details-export-rule"
|
||||
|
|
|
@ -29,7 +29,11 @@ describe('RuleActionsOverflow', () => {
|
|||
describe('snapshots', () => {
|
||||
test('renders correctly against snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<RuleActionsOverflow rule={mockRule('id')} userHasNoPermissions={false} />
|
||||
<RuleActionsOverflow
|
||||
rule={mockRule('id')}
|
||||
userHasNoPermissions={false}
|
||||
canDuplicateRuleWithActions={true}
|
||||
/>
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
@ -38,7 +42,11 @@ describe('RuleActionsOverflow', () => {
|
|||
describe('rules details menu panel', () => {
|
||||
test('there is at least one item when there is a rule within the rules-details-menu-panel', () => {
|
||||
const wrapper = mount(
|
||||
<RuleActionsOverflow rule={mockRule('id')} userHasNoPermissions={false} />
|
||||
<RuleActionsOverflow
|
||||
rule={mockRule('id')}
|
||||
userHasNoPermissions={false}
|
||||
canDuplicateRuleWithActions={true}
|
||||
/>
|
||||
);
|
||||
wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click');
|
||||
wrapper.update();
|
||||
|
@ -51,7 +59,13 @@ describe('RuleActionsOverflow', () => {
|
|||
});
|
||||
|
||||
test('items are empty when there is a null rule within the rules-details-menu-panel', () => {
|
||||
const wrapper = mount(<RuleActionsOverflow rule={null} userHasNoPermissions={false} />);
|
||||
const wrapper = mount(
|
||||
<RuleActionsOverflow
|
||||
rule={null}
|
||||
userHasNoPermissions={false}
|
||||
canDuplicateRuleWithActions={true}
|
||||
/>
|
||||
);
|
||||
wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click');
|
||||
wrapper.update();
|
||||
expect(
|
||||
|
@ -60,7 +74,13 @@ describe('RuleActionsOverflow', () => {
|
|||
});
|
||||
|
||||
test('items are empty when there is an undefined rule within the rules-details-menu-panel', () => {
|
||||
const wrapper = mount(<RuleActionsOverflow rule={null} userHasNoPermissions={false} />);
|
||||
const wrapper = mount(
|
||||
<RuleActionsOverflow
|
||||
rule={null}
|
||||
userHasNoPermissions={false}
|
||||
canDuplicateRuleWithActions={true}
|
||||
/>
|
||||
);
|
||||
wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click');
|
||||
wrapper.update();
|
||||
expect(
|
||||
|
@ -70,7 +90,11 @@ describe('RuleActionsOverflow', () => {
|
|||
|
||||
test('it opens the popover when rules-details-popover-button-icon is clicked', () => {
|
||||
const wrapper = mount(
|
||||
<RuleActionsOverflow rule={mockRule('id')} userHasNoPermissions={false} />
|
||||
<RuleActionsOverflow
|
||||
rule={mockRule('id')}
|
||||
userHasNoPermissions={false}
|
||||
canDuplicateRuleWithActions={true}
|
||||
/>
|
||||
);
|
||||
wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click');
|
||||
wrapper.update();
|
||||
|
@ -83,7 +107,11 @@ describe('RuleActionsOverflow', () => {
|
|||
describe('rules details pop over button icon', () => {
|
||||
test('it does not open the popover when rules-details-popover-button-icon is clicked when the user does not have permission', () => {
|
||||
const wrapper = mount(
|
||||
<RuleActionsOverflow rule={mockRule('id')} userHasNoPermissions={true} />
|
||||
<RuleActionsOverflow
|
||||
rule={mockRule('id')}
|
||||
userHasNoPermissions={true}
|
||||
canDuplicateRuleWithActions={true}
|
||||
/>
|
||||
);
|
||||
wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click');
|
||||
wrapper.update();
|
||||
|
@ -96,7 +124,13 @@ describe('RuleActionsOverflow', () => {
|
|||
describe('rules details duplicate rule', () => {
|
||||
test('it does not open the popover when rules-details-popover-button-icon is clicked and the user does not have permission', () => {
|
||||
const rule = mockRule('id');
|
||||
const wrapper = mount(<RuleActionsOverflow rule={rule} userHasNoPermissions={true} />);
|
||||
const wrapper = mount(
|
||||
<RuleActionsOverflow
|
||||
rule={rule}
|
||||
userHasNoPermissions={true}
|
||||
canDuplicateRuleWithActions={true}
|
||||
/>
|
||||
);
|
||||
wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click');
|
||||
wrapper.update();
|
||||
expect(wrapper.find('[data-test-subj="rules-details-delete-rule"] button').exists()).toEqual(
|
||||
|
@ -106,7 +140,11 @@ describe('RuleActionsOverflow', () => {
|
|||
|
||||
test('it opens the popover when rules-details-popover-button-icon is clicked', () => {
|
||||
const wrapper = mount(
|
||||
<RuleActionsOverflow rule={mockRule('id')} userHasNoPermissions={false} />
|
||||
<RuleActionsOverflow
|
||||
rule={mockRule('id')}
|
||||
userHasNoPermissions={false}
|
||||
canDuplicateRuleWithActions={true}
|
||||
/>
|
||||
);
|
||||
wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click');
|
||||
wrapper.update();
|
||||
|
@ -117,7 +155,11 @@ describe('RuleActionsOverflow', () => {
|
|||
|
||||
test('it closes the popover when rules-details-duplicate-rule is clicked', () => {
|
||||
const wrapper = mount(
|
||||
<RuleActionsOverflow rule={mockRule('id')} userHasNoPermissions={false} />
|
||||
<RuleActionsOverflow
|
||||
rule={mockRule('id')}
|
||||
userHasNoPermissions={false}
|
||||
canDuplicateRuleWithActions={true}
|
||||
/>
|
||||
);
|
||||
wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click');
|
||||
wrapper.update();
|
||||
|
@ -130,7 +172,11 @@ describe('RuleActionsOverflow', () => {
|
|||
|
||||
test('it calls duplicateRulesAction when rules-details-duplicate-rule is clicked', () => {
|
||||
const wrapper = mount(
|
||||
<RuleActionsOverflow rule={mockRule('id')} userHasNoPermissions={false} />
|
||||
<RuleActionsOverflow
|
||||
rule={mockRule('id')}
|
||||
userHasNoPermissions={false}
|
||||
canDuplicateRuleWithActions={true}
|
||||
/>
|
||||
);
|
||||
wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click');
|
||||
wrapper.update();
|
||||
|
@ -141,7 +187,13 @@ describe('RuleActionsOverflow', () => {
|
|||
|
||||
test('it calls duplicateRulesAction with the rule and rule.id when rules-details-duplicate-rule is clicked', () => {
|
||||
const rule = mockRule('id');
|
||||
const wrapper = mount(<RuleActionsOverflow rule={rule} userHasNoPermissions={false} />);
|
||||
const wrapper = mount(
|
||||
<RuleActionsOverflow
|
||||
rule={rule}
|
||||
userHasNoPermissions={false}
|
||||
canDuplicateRuleWithActions={true}
|
||||
/>
|
||||
);
|
||||
wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click');
|
||||
wrapper.update();
|
||||
wrapper.find('[data-test-subj="rules-details-duplicate-rule"] button').simulate('click');
|
||||
|
@ -158,7 +210,13 @@ describe('RuleActionsOverflow', () => {
|
|||
describe('rules details export rule', () => {
|
||||
test('it does not open the popover when rules-details-popover-button-icon is clicked and the user does not have permission', () => {
|
||||
const rule = mockRule('id');
|
||||
const wrapper = mount(<RuleActionsOverflow rule={rule} userHasNoPermissions={true} />);
|
||||
const wrapper = mount(
|
||||
<RuleActionsOverflow
|
||||
rule={rule}
|
||||
userHasNoPermissions={true}
|
||||
canDuplicateRuleWithActions={true}
|
||||
/>
|
||||
);
|
||||
wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click');
|
||||
wrapper.update();
|
||||
expect(wrapper.find('[data-test-subj="rules-details-export-rule"] button').exists()).toEqual(
|
||||
|
@ -168,7 +226,11 @@ describe('RuleActionsOverflow', () => {
|
|||
|
||||
test('it closes the popover when rules-details-export-rule is clicked', () => {
|
||||
const wrapper = mount(
|
||||
<RuleActionsOverflow rule={mockRule('id')} userHasNoPermissions={false} />
|
||||
<RuleActionsOverflow
|
||||
rule={mockRule('id')}
|
||||
userHasNoPermissions={false}
|
||||
canDuplicateRuleWithActions={true}
|
||||
/>
|
||||
);
|
||||
wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click');
|
||||
wrapper.update();
|
||||
|
@ -181,7 +243,13 @@ describe('RuleActionsOverflow', () => {
|
|||
|
||||
test('it sets the rule.rule_id on the generic downloader when rules-details-export-rule is clicked', () => {
|
||||
const rule = mockRule('id');
|
||||
const wrapper = mount(<RuleActionsOverflow rule={rule} userHasNoPermissions={false} />);
|
||||
const wrapper = mount(
|
||||
<RuleActionsOverflow
|
||||
rule={rule}
|
||||
userHasNoPermissions={false}
|
||||
canDuplicateRuleWithActions={true}
|
||||
/>
|
||||
);
|
||||
wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click');
|
||||
wrapper.update();
|
||||
wrapper.find('[data-test-subj="rules-details-export-rule"] button').simulate('click');
|
||||
|
@ -194,7 +262,13 @@ describe('RuleActionsOverflow', () => {
|
|||
test('it does not close the pop over on rules-details-export-rule when the rule is an immutable rule and the user does a click', () => {
|
||||
const rule = mockRule('id');
|
||||
rule.immutable = true;
|
||||
const wrapper = mount(<RuleActionsOverflow rule={rule} userHasNoPermissions={false} />);
|
||||
const wrapper = mount(
|
||||
<RuleActionsOverflow
|
||||
rule={rule}
|
||||
userHasNoPermissions={false}
|
||||
canDuplicateRuleWithActions={true}
|
||||
/>
|
||||
);
|
||||
wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click');
|
||||
wrapper.update();
|
||||
wrapper.find('[data-test-subj="rules-details-export-rule"] button').simulate('click');
|
||||
|
@ -207,7 +281,13 @@ describe('RuleActionsOverflow', () => {
|
|||
test('it does not set the rule.rule_id on rules-details-export-rule when the rule is an immutable rule', () => {
|
||||
const rule = mockRule('id');
|
||||
rule.immutable = true;
|
||||
const wrapper = mount(<RuleActionsOverflow rule={rule} userHasNoPermissions={false} />);
|
||||
const wrapper = mount(
|
||||
<RuleActionsOverflow
|
||||
rule={rule}
|
||||
userHasNoPermissions={false}
|
||||
canDuplicateRuleWithActions={true}
|
||||
/>
|
||||
);
|
||||
wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click');
|
||||
wrapper.update();
|
||||
wrapper.find('[data-test-subj="rules-details-export-rule"] button').simulate('click');
|
||||
|
@ -221,7 +301,13 @@ describe('RuleActionsOverflow', () => {
|
|||
describe('rules details delete rule', () => {
|
||||
test('it does not open the popover when rules-details-popover-button-icon is clicked and the user does not have permission', () => {
|
||||
const rule = mockRule('id');
|
||||
const wrapper = mount(<RuleActionsOverflow rule={rule} userHasNoPermissions={true} />);
|
||||
const wrapper = mount(
|
||||
<RuleActionsOverflow
|
||||
rule={rule}
|
||||
userHasNoPermissions={true}
|
||||
canDuplicateRuleWithActions={true}
|
||||
/>
|
||||
);
|
||||
wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click');
|
||||
wrapper.update();
|
||||
expect(wrapper.find('[data-test-subj="rules-details-delete-rule"] button').exists()).toEqual(
|
||||
|
@ -231,7 +317,11 @@ describe('RuleActionsOverflow', () => {
|
|||
|
||||
test('it closes the popover when rules-details-delete-rule is clicked', () => {
|
||||
const wrapper = mount(
|
||||
<RuleActionsOverflow rule={mockRule('id')} userHasNoPermissions={false} />
|
||||
<RuleActionsOverflow
|
||||
rule={mockRule('id')}
|
||||
userHasNoPermissions={false}
|
||||
canDuplicateRuleWithActions={true}
|
||||
/>
|
||||
);
|
||||
wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click');
|
||||
wrapper.update();
|
||||
|
@ -244,7 +334,11 @@ describe('RuleActionsOverflow', () => {
|
|||
|
||||
test('it calls deleteRulesAction when rules-details-delete-rule is clicked', () => {
|
||||
const wrapper = mount(
|
||||
<RuleActionsOverflow rule={mockRule('id')} userHasNoPermissions={false} />
|
||||
<RuleActionsOverflow
|
||||
rule={mockRule('id')}
|
||||
userHasNoPermissions={false}
|
||||
canDuplicateRuleWithActions={true}
|
||||
/>
|
||||
);
|
||||
wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click');
|
||||
wrapper.update();
|
||||
|
@ -255,7 +349,13 @@ describe('RuleActionsOverflow', () => {
|
|||
|
||||
test('it calls deleteRulesAction with the rule.id when rules-details-delete-rule is clicked', () => {
|
||||
const rule = mockRule('id');
|
||||
const wrapper = mount(<RuleActionsOverflow rule={rule} userHasNoPermissions={false} />);
|
||||
const wrapper = mount(
|
||||
<RuleActionsOverflow
|
||||
rule={rule}
|
||||
userHasNoPermissions={false}
|
||||
canDuplicateRuleWithActions={true}
|
||||
/>
|
||||
);
|
||||
wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click');
|
||||
wrapper.update();
|
||||
wrapper.find('[data-test-subj="rules-details-delete-rule"] button').simulate('click');
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
} from '../../../pages/detection_engine/rules/all/actions';
|
||||
import { GenericDownloader } from '../../../../common/components/generic_downloader';
|
||||
import { getRulesUrl } from '../../../../common/components/link_to/redirect_to_detection_engine';
|
||||
import { getToolTipContent } from '../../../../common/utils/privileges';
|
||||
|
||||
const MyEuiButtonIcon = styled(EuiButtonIcon)`
|
||||
&.euiButtonIcon {
|
||||
|
@ -41,6 +42,7 @@ const MyEuiButtonIcon = styled(EuiButtonIcon)`
|
|||
interface RuleActionsOverflowComponentProps {
|
||||
rule: Rule | null;
|
||||
userHasNoPermissions: boolean;
|
||||
canDuplicateRuleWithActions: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -49,6 +51,7 @@ interface RuleActionsOverflowComponentProps {
|
|||
const RuleActionsOverflowComponent = ({
|
||||
rule,
|
||||
userHasNoPermissions,
|
||||
canDuplicateRuleWithActions,
|
||||
}: RuleActionsOverflowComponentProps) => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const [rulesToExport, setRulesToExport] = useState<string[]>([]);
|
||||
|
@ -66,14 +69,19 @@ const RuleActionsOverflowComponent = ({
|
|||
<EuiContextMenuItem
|
||||
key={i18nActions.DUPLICATE_RULE}
|
||||
icon="copy"
|
||||
disabled={userHasNoPermissions}
|
||||
disabled={!canDuplicateRuleWithActions || userHasNoPermissions}
|
||||
data-test-subj="rules-details-duplicate-rule"
|
||||
onClick={async () => {
|
||||
setIsPopoverOpen(false);
|
||||
await duplicateRulesAction([rule], [rule.id], noop, dispatchToaster);
|
||||
}}
|
||||
>
|
||||
{i18nActions.DUPLICATE_RULE}
|
||||
<EuiToolTip
|
||||
position="left"
|
||||
content={getToolTipContent(rule, true, canDuplicateRuleWithActions)}
|
||||
>
|
||||
<>{i18nActions.DUPLICATE_RULE}</>
|
||||
</EuiToolTip>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
key={i18nActions.EXPORT_RULE}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiContextMenuItem } from '@elastic/eui';
|
||||
import { EuiContextMenuItem, EuiToolTip } from '@elastic/eui';
|
||||
import React, { Dispatch } from 'react';
|
||||
import * as i18n from '../translations';
|
||||
import { Action } from './reducer';
|
||||
|
@ -18,12 +18,14 @@ import { ActionToaster, displayWarningToast } from '../../../../../common/compon
|
|||
import { Rule } from '../../../../containers/detection_engine/rules';
|
||||
import * as detectionI18n from '../../translations';
|
||||
import { isMlRule } from '../../../../../../common/machine_learning/helpers';
|
||||
import { canEditRuleWithActions } from '../../../../../common/utils/privileges';
|
||||
|
||||
interface GetBatchItems {
|
||||
closePopover: () => void;
|
||||
dispatch: Dispatch<Action>;
|
||||
dispatchToaster: Dispatch<ActionToaster>;
|
||||
hasMlPermissions: boolean;
|
||||
hasActionsPrivileges: boolean;
|
||||
loadingRuleIds: string[];
|
||||
reFetchRules: (refreshPrePackagedRule?: boolean) => void;
|
||||
rules: Rule[];
|
||||
|
@ -39,31 +41,38 @@ export const getBatchItems = ({
|
|||
reFetchRules,
|
||||
rules,
|
||||
selectedRuleIds,
|
||||
hasActionsPrivileges,
|
||||
}: GetBatchItems) => {
|
||||
const containsEnabled = selectedRuleIds.some(
|
||||
(id) => rules.find((r) => r.id === id)?.enabled ?? false
|
||||
);
|
||||
const containsDisabled = selectedRuleIds.some(
|
||||
(id) => !rules.find((r) => r.id === id)?.enabled ?? false
|
||||
);
|
||||
const selectedRules = selectedRuleIds.reduce((acc, id) => {
|
||||
const found = rules.find((r) => r.id === id);
|
||||
if (found != null) {
|
||||
return { [id]: found, ...acc };
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, Rule>);
|
||||
|
||||
const containsEnabled = selectedRuleIds.some((id) => selectedRules[id]?.enabled ?? false);
|
||||
const containsDisabled = selectedRuleIds.some((id) => !selectedRules[id]?.enabled ?? false);
|
||||
const containsLoading = selectedRuleIds.some((id) => loadingRuleIds.includes(id));
|
||||
const containsImmutable = selectedRuleIds.some(
|
||||
(id) => rules.find((r) => r.id === id)?.immutable ?? false
|
||||
);
|
||||
const containsImmutable = selectedRuleIds.some((id) => selectedRules[id]?.immutable ?? false);
|
||||
|
||||
const missingActionPrivileges =
|
||||
!hasActionsPrivileges &&
|
||||
selectedRuleIds.some((id) => {
|
||||
return !canEditRuleWithActions(selectedRules[id], hasActionsPrivileges);
|
||||
});
|
||||
|
||||
return [
|
||||
<EuiContextMenuItem
|
||||
key={i18n.BATCH_ACTION_ACTIVATE_SELECTED}
|
||||
icon="checkInCircleFilled"
|
||||
disabled={containsLoading || !containsDisabled}
|
||||
disabled={missingActionPrivileges || containsLoading || !containsDisabled}
|
||||
onClick={async () => {
|
||||
closePopover();
|
||||
const deactivatedIds = selectedRuleIds.filter(
|
||||
(id) => !rules.find((r) => r.id === id)?.enabled ?? false
|
||||
);
|
||||
const deactivatedIds = selectedRuleIds.filter((id) => !selectedRules[id]?.enabled ?? false);
|
||||
|
||||
const deactivatedIdsNoML = deactivatedIds.filter(
|
||||
(id) => !isMlRule(rules.find((r) => r.id === id)?.type)
|
||||
(id) => !isMlRule(selectedRules[id]?.type)
|
||||
);
|
||||
|
||||
const mlRuleCount = deactivatedIds.length - deactivatedIdsNoML.length;
|
||||
|
@ -79,21 +88,29 @@ export const getBatchItems = ({
|
|||
);
|
||||
}}
|
||||
>
|
||||
{i18n.BATCH_ACTION_ACTIVATE_SELECTED}
|
||||
<EuiToolTip
|
||||
position="right"
|
||||
content={missingActionPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined}
|
||||
>
|
||||
<>{i18n.BATCH_ACTION_ACTIVATE_SELECTED}</>
|
||||
</EuiToolTip>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
key={i18n.BATCH_ACTION_DEACTIVATE_SELECTED}
|
||||
icon="crossInACircleFilled"
|
||||
disabled={containsLoading || !containsEnabled}
|
||||
disabled={missingActionPrivileges || containsLoading || !containsEnabled}
|
||||
onClick={async () => {
|
||||
closePopover();
|
||||
const activatedIds = selectedRuleIds.filter(
|
||||
(id) => rules.find((r) => r.id === id)?.enabled ?? false
|
||||
);
|
||||
const activatedIds = selectedRuleIds.filter((id) => selectedRules[id]?.enabled ?? false);
|
||||
await enableRulesAction(activatedIds, false, dispatch, dispatchToaster);
|
||||
}}
|
||||
>
|
||||
{i18n.BATCH_ACTION_DEACTIVATE_SELECTED}
|
||||
<EuiToolTip
|
||||
position="right"
|
||||
content={missingActionPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined}
|
||||
>
|
||||
<>{i18n.BATCH_ACTION_DEACTIVATE_SELECTED}</>
|
||||
</EuiToolTip>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
key={i18n.BATCH_ACTION_EXPORT_SELECTED}
|
||||
|
@ -109,10 +126,11 @@ export const getBatchItems = ({
|
|||
>
|
||||
{i18n.BATCH_ACTION_EXPORT_SELECTED}
|
||||
</EuiContextMenuItem>,
|
||||
|
||||
<EuiContextMenuItem
|
||||
key={i18n.BATCH_ACTION_DUPLICATE_SELECTED}
|
||||
icon="copy"
|
||||
disabled={containsLoading || selectedRuleIds.length === 0}
|
||||
disabled={missingActionPrivileges || containsLoading || selectedRuleIds.length === 0}
|
||||
onClick={async () => {
|
||||
closePopover();
|
||||
await duplicateRulesAction(
|
||||
|
@ -124,7 +142,12 @@ export const getBatchItems = ({
|
|||
reFetchRules(true);
|
||||
}}
|
||||
>
|
||||
{i18n.BATCH_ACTION_DUPLICATE_SELECTED}
|
||||
<EuiToolTip
|
||||
position="right"
|
||||
content={missingActionPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined}
|
||||
>
|
||||
<>{i18n.BATCH_ACTION_DUPLICATE_SELECTED}</>
|
||||
</EuiToolTip>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
data-test-subj="deleteRuleBulk"
|
||||
|
|
|
@ -52,7 +52,8 @@ describe('AllRulesTable Columns', () => {
|
|||
dispatch,
|
||||
dispatchToaster,
|
||||
history,
|
||||
reFetchRules
|
||||
reFetchRules,
|
||||
true
|
||||
)[1];
|
||||
await duplicateRulesActionObject.onClick(rule);
|
||||
expect(results).toEqual(['duplicateRulesAction', 'reFetchRules']);
|
||||
|
@ -73,7 +74,8 @@ describe('AllRulesTable Columns', () => {
|
|||
dispatch,
|
||||
dispatchToaster,
|
||||
history,
|
||||
reFetchRules
|
||||
reFetchRules,
|
||||
true
|
||||
)[3];
|
||||
await deleteRulesActionObject.onClick(rule);
|
||||
expect(results).toEqual(['deleteRulesAction', 'reFetchRules']);
|
||||
|
|
|
@ -35,27 +35,46 @@ import {
|
|||
} from './actions';
|
||||
import { Action } from './reducer';
|
||||
import { LocalizedDateTooltip } from '../../../../../common/components/localized_date_tooltip';
|
||||
import * as detectionI18n from '../../translations';
|
||||
import { LinkAnchor } from '../../../../../common/components/links';
|
||||
import { getToolTipContent, canEditRuleWithActions } from '../../../../../common/utils/privileges';
|
||||
import { TagsDisplay } from './tag_display';
|
||||
|
||||
export const getActions = (
|
||||
dispatch: React.Dispatch<Action>,
|
||||
dispatchToaster: Dispatch<ActionToaster>,
|
||||
history: H.History,
|
||||
reFetchRules: (refreshPrePackagedRule?: boolean) => void
|
||||
reFetchRules: (refreshPrePackagedRule?: boolean) => void,
|
||||
actionsPrivileges:
|
||||
| boolean
|
||||
| Readonly<{
|
||||
[x: string]: boolean;
|
||||
}>
|
||||
) => [
|
||||
{
|
||||
'data-test-subj': 'editRuleAction',
|
||||
description: i18n.EDIT_RULE_SETTINGS,
|
||||
name: !actionsPrivileges ? (
|
||||
<EuiToolTip position="left" content={i18n.EDIT_RULE_SETTINGS_TOOLTIP}>
|
||||
<>{i18n.EDIT_RULE_SETTINGS}</>
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
i18n.EDIT_RULE_SETTINGS
|
||||
),
|
||||
icon: 'controlsHorizontal',
|
||||
name: i18n.EDIT_RULE_SETTINGS,
|
||||
onClick: (rowItem: Rule) => editRuleAction(rowItem, history),
|
||||
enabled: (rowItem: Rule) => canEditRuleWithActions(rowItem, actionsPrivileges),
|
||||
},
|
||||
{
|
||||
description: i18n.DUPLICATE_RULE,
|
||||
icon: 'copy',
|
||||
name: i18n.DUPLICATE_RULE,
|
||||
name: !actionsPrivileges ? (
|
||||
<EuiToolTip position="left" content={i18n.EDIT_RULE_SETTINGS_TOOLTIP}>
|
||||
<>{i18n.DUPLICATE_RULE}</>
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
i18n.DUPLICATE_RULE
|
||||
),
|
||||
enabled: (rowItem: Rule) => canEditRuleWithActions(rowItem, actionsPrivileges),
|
||||
onClick: async (rowItem: Rule) => {
|
||||
await duplicateRulesAction([rowItem], [rowItem.id], dispatch, dispatchToaster);
|
||||
await reFetchRules(true);
|
||||
|
@ -97,6 +116,11 @@ interface GetColumns {
|
|||
hasNoPermissions: boolean;
|
||||
loadingRuleIds: string[];
|
||||
reFetchRules: (refreshPrePackagedRule?: boolean) => void;
|
||||
hasReadActionsPrivileges:
|
||||
| boolean
|
||||
| Readonly<{
|
||||
[x: string]: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const getColumns = ({
|
||||
|
@ -108,6 +132,7 @@ export const getColumns = ({
|
|||
hasNoPermissions,
|
||||
loadingRuleIds,
|
||||
reFetchRules,
|
||||
hasReadActionsPrivileges,
|
||||
}: GetColumns): RulesColumns[] => {
|
||||
const cols: RulesColumns[] = [
|
||||
{
|
||||
|
@ -227,11 +252,7 @@ export const getColumns = ({
|
|||
render: (value: Rule['enabled'], item: Rule) => (
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={
|
||||
isMlRule(item.type) && !hasMlPermissions
|
||||
? detectionI18n.ML_RULES_DISABLED_MESSAGE
|
||||
: undefined
|
||||
}
|
||||
content={getToolTipContent(item, hasMlPermissions, hasReadActionsPrivileges)}
|
||||
>
|
||||
<RuleSwitch
|
||||
data-test-subj="enabled"
|
||||
|
@ -239,7 +260,9 @@ export const getColumns = ({
|
|||
id={item.id}
|
||||
enabled={item.enabled}
|
||||
isDisabled={
|
||||
hasNoPermissions || (isMlRule(item.type) && !hasMlPermissions && !item.enabled)
|
||||
!canEditRuleWithActions(item, hasReadActionsPrivileges) ||
|
||||
hasNoPermissions ||
|
||||
(isMlRule(item.type) && !hasMlPermissions && !item.enabled)
|
||||
}
|
||||
isLoading={loadingRuleIds.includes(item.id)}
|
||||
/>
|
||||
|
@ -251,7 +274,13 @@ export const getColumns = ({
|
|||
];
|
||||
const actions: RulesColumns[] = [
|
||||
{
|
||||
actions: getActions(dispatch, dispatchToaster, history, reFetchRules),
|
||||
actions: getActions(
|
||||
dispatch,
|
||||
dispatchToaster,
|
||||
history,
|
||||
reFetchRules,
|
||||
hasReadActionsPrivileges
|
||||
),
|
||||
width: '40px',
|
||||
} as EuiTableActionsColumnType<Rule>,
|
||||
];
|
||||
|
|
|
@ -12,6 +12,7 @@ import '../../../../../common/mock/formatted_relative';
|
|||
import { TestProviders } from '../../../../../common/mock';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { AllRules } from './index';
|
||||
import { useKibana } from '../../../../../common/lib/kibana';
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
const original = jest.requireActual('react-router-dom');
|
||||
|
@ -25,6 +26,9 @@ jest.mock('react-router-dom', () => {
|
|||
});
|
||||
|
||||
jest.mock('../../../../../common/components/link_to');
|
||||
jest.mock('../../../../../common/lib/kibana');
|
||||
|
||||
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
|
||||
|
||||
jest.mock('./reducer', () => {
|
||||
return {
|
||||
|
@ -160,6 +164,14 @@ jest.mock('react-router-dom', () => {
|
|||
});
|
||||
|
||||
describe('AllRules', () => {
|
||||
beforeEach(() => {
|
||||
useKibanaMock().services.application.capabilities = {
|
||||
navLinks: {},
|
||||
management: {},
|
||||
catalogue: {},
|
||||
actions: { show: true },
|
||||
};
|
||||
});
|
||||
it('renders correctly', () => {
|
||||
const wrapper = shallow(
|
||||
<AllRules
|
||||
|
|
|
@ -34,6 +34,7 @@ import {
|
|||
UtilityBarSection,
|
||||
UtilityBarText,
|
||||
} from '../../../../../common/components/utility_bar';
|
||||
import { useKibana } from '../../../../../common/lib/kibana';
|
||||
import { useStateToaster } from '../../../../../common/components/toasters';
|
||||
import { Loader } from '../../../../../common/components/loader';
|
||||
import { Panel } from '../../../../../common/components/panel';
|
||||
|
@ -53,6 +54,7 @@ import { hasMlAdminPermissions } from '../../../../../../common/machine_learning
|
|||
import { hasMlLicense } from '../../../../../../common/machine_learning/has_ml_license';
|
||||
import { SecurityPageName } from '../../../../../app/types';
|
||||
import { useFormatUrl } from '../../../../../common/components/link_to';
|
||||
import { isBoolean } from '../../../../../common/utils/privileges';
|
||||
|
||||
const INITIAL_SORT_FIELD = 'enabled';
|
||||
const initialState: State = {
|
||||
|
@ -179,6 +181,17 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
rulesNotInstalled,
|
||||
rulesNotUpdated
|
||||
);
|
||||
const {
|
||||
services: {
|
||||
application: {
|
||||
capabilities: { actions },
|
||||
},
|
||||
},
|
||||
} = useKibana();
|
||||
|
||||
const hasActionsPrivileges = useMemo(() => (isBoolean(actions.show) ? actions.show : true), [
|
||||
actions,
|
||||
]);
|
||||
|
||||
const getBatchItemsPopoverContent = useCallback(
|
||||
(closePopover: () => void) => (
|
||||
|
@ -188,6 +201,7 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
dispatch,
|
||||
dispatchToaster,
|
||||
hasMlPermissions,
|
||||
hasActionsPrivileges,
|
||||
loadingRuleIds,
|
||||
selectedRuleIds,
|
||||
reFetchRules: reFetchRulesData,
|
||||
|
@ -203,6 +217,7 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
reFetchRulesData,
|
||||
rules,
|
||||
selectedRuleIds,
|
||||
hasActionsPrivileges,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -244,6 +259,7 @@ export const AllRules = React.memo<AllRulesProps>(
|
|||
? loadingRuleIds
|
||||
: [],
|
||||
reFetchRules: reFetchRulesData,
|
||||
hasReadActionsPrivileges: hasActionsPrivileges,
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
|
|
|
@ -23,6 +23,7 @@ import React, { FC, memo, useCallback, useEffect, useMemo, useState } from 'reac
|
|||
import { useParams, useHistory } from 'react-router-dom';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
|
||||
import { useKibana } from '../../../../../common/lib/kibana';
|
||||
import { TimelineId } from '../../../../../../common/types/timeline';
|
||||
import { UpdateDateRange } from '../../../../../common/components/charts/common';
|
||||
import { FiltersGlobal } from '../../../../../common/components/filters_global';
|
||||
|
@ -88,6 +89,12 @@ import { timelineDefaults } from '../../../../../timelines/store/timeline/defaul
|
|||
import { TimelineModel } from '../../../../../timelines/store/timeline/model';
|
||||
import { useSourcererScope } from '../../../../../common/containers/sourcerer';
|
||||
import { SourcererScopeName } from '../../../../../common/store/sourcerer/model';
|
||||
import {
|
||||
getToolTipContent,
|
||||
canEditRuleWithActions,
|
||||
isBoolean,
|
||||
} from '../../../../../common/utils/privileges';
|
||||
|
||||
import { AlertsHistogramOption } from '../../../../components/alerts_histogram_panel/types';
|
||||
|
||||
enum RuleDetailTabs {
|
||||
|
@ -166,6 +173,19 @@ export const RuleDetailsPageComponent: FC<PropsFromRedux> = ({
|
|||
// TODO: Refactor license check + hasMlAdminPermissions to common check
|
||||
const hasMlPermissions = hasMlLicense(mlCapabilities) && hasMlAdminPermissions(mlCapabilities);
|
||||
const ruleDetailTabs = getRuleDetailsTabs(rule);
|
||||
const {
|
||||
services: {
|
||||
application: {
|
||||
capabilities: { actions },
|
||||
},
|
||||
},
|
||||
} = useKibana();
|
||||
const hasActionsPrivileges = useMemo(() => {
|
||||
if (rule?.actions != null && rule?.actions.length > 0 && isBoolean(actions.show)) {
|
||||
return actions.show;
|
||||
}
|
||||
return true;
|
||||
}, [actions, rule?.actions]);
|
||||
|
||||
// persist rule until refresh is complete
|
||||
useEffect(() => {
|
||||
|
@ -297,6 +317,33 @@ export const RuleDetailsPageComponent: FC<PropsFromRedux> = ({
|
|||
[history, ruleId]
|
||||
);
|
||||
|
||||
const editRule = useMemo(() => {
|
||||
if (!hasActionsPrivileges) {
|
||||
return (
|
||||
<EuiToolTip position="top" content={ruleI18n.EDIT_RULE_SETTINGS_TOOLTIP}>
|
||||
<LinkButton
|
||||
onClick={goToEditRule}
|
||||
iconType="controlsHorizontal"
|
||||
isDisabled={true}
|
||||
href={formatUrl(getEditRuleUrl(ruleId ?? ''))}
|
||||
>
|
||||
{ruleI18n.EDIT_RULE_SETTINGS}
|
||||
</LinkButton>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<LinkButton
|
||||
onClick={goToEditRule}
|
||||
iconType="controlsHorizontal"
|
||||
isDisabled={userHasNoPermissions(canUserCRUD) ?? true}
|
||||
href={formatUrl(getEditRuleUrl(ruleId ?? ''))}
|
||||
>
|
||||
{ruleI18n.EDIT_RULE_SETTINGS}
|
||||
</LinkButton>
|
||||
);
|
||||
}, [canUserCRUD, formatUrl, goToEditRule, hasActionsPrivileges, ruleId]);
|
||||
|
||||
const onShowBuildingBlockAlertsChangedCallback = useCallback(
|
||||
(newShowBuildingBlockAlerts: boolean) => {
|
||||
setShowBuildingBlockAlerts(newShowBuildingBlockAlerts);
|
||||
|
@ -390,16 +437,14 @@ export const RuleDetailsPageComponent: FC<PropsFromRedux> = ({
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={
|
||||
isMlRule(rule?.type) && !hasMlPermissions
|
||||
? detectionI18n.ML_RULES_DISABLED_MESSAGE
|
||||
: undefined
|
||||
}
|
||||
content={getToolTipContent(rule, hasMlPermissions, hasActionsPrivileges)}
|
||||
>
|
||||
<RuleSwitch
|
||||
id={rule?.id ?? '-1'}
|
||||
isDisabled={
|
||||
userHasNoPermissions(canUserCRUD) || (!hasMlPermissions && !rule?.enabled)
|
||||
!canEditRuleWithActions(rule, hasActionsPrivileges) ||
|
||||
userHasNoPermissions(canUserCRUD) ||
|
||||
(!hasMlPermissions && !rule?.enabled)
|
||||
}
|
||||
enabled={rule?.enabled ?? false}
|
||||
optionLabel={i18n.ACTIVATE_RULE}
|
||||
|
@ -410,20 +455,15 @@ export const RuleDetailsPageComponent: FC<PropsFromRedux> = ({
|
|||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<LinkButton
|
||||
onClick={goToEditRule}
|
||||
iconType="controlsHorizontal"
|
||||
isDisabled={userHasNoPermissions(canUserCRUD) ?? true}
|
||||
href={formatUrl(getEditRuleUrl(ruleId ?? ''))}
|
||||
>
|
||||
{ruleI18n.EDIT_RULE_SETTINGS}
|
||||
</LinkButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{editRule}</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<RuleActionsOverflow
|
||||
rule={rule}
|
||||
userHasNoPermissions={userHasNoPermissions(canUserCRUD)}
|
||||
canDuplicateRuleWithActions={canEditRuleWithActions(
|
||||
rule,
|
||||
hasActionsPrivileges
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -214,6 +214,13 @@ export const EDIT_RULE_SETTINGS = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const EDIT_RULE_SETTINGS_TOOLTIP = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.allRules.actions.editRuleSettingsToolTip',
|
||||
{
|
||||
defaultMessage: 'You do not have Kibana Actions privileges',
|
||||
}
|
||||
);
|
||||
|
||||
export const DUPLICATE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateTitle',
|
||||
{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue