Skip to content

Commit

Permalink
[7.x] [Security Solution] [Detections] Disable edit button when user …
Browse files Browse the repository at this point in the history
…does not have actions privileges w/ rule + actions (#80220) (#81055)

* 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>

Co-authored-by: Patryk Kopycinski <contact@patrykkopycinski.com>
  • Loading branch information
dhurley14 and patrykkopycinski authored Oct 20, 2020
1 parent 61a8146 commit 075efe4
Show file tree
Hide file tree
Showing 11 changed files with 368 additions and 75 deletions.
Original file line number Diff line number Diff line change
@@ -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;
}
};

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Expand All @@ -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();
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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(
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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');
Expand All @@ -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(
Expand All @@ -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();
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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(
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -41,6 +42,7 @@ const MyEuiButtonIcon = styled(EuiButtonIcon)`
interface RuleActionsOverflowComponentProps {
rule: Rule | null;
userHasNoPermissions: boolean;
canDuplicateRuleWithActions: boolean;
}

/**
Expand All @@ -49,6 +51,7 @@ interface RuleActionsOverflowComponentProps {
const RuleActionsOverflowComponent = ({
rule,
userHasNoPermissions,
canDuplicateRuleWithActions,
}: RuleActionsOverflowComponentProps) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const [rulesToExport, setRulesToExport] = useState<string[]>([]);
Expand All @@ -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}
Expand Down
Loading

0 comments on commit 075efe4

Please sign in to comment.