[SecuritySolution][Case] Disable cases on detections in read-only mode (#93010)

* Disable cases on detetions on read-only mode

* Add cypress tests
This commit is contained in:
Christos Nasikas 2021-03-02 00:38:49 +02:00 committed by GitHub
parent 4739eab490
commit aa62a130ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 170 additions and 18 deletions

View file

@ -0,0 +1,60 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { newRule } from '../../objects/rule';
import { ROLES } from '../../../common/test';
import { waitForAlertsIndexToBeCreated, waitForAlertsPanelToBeLoaded } from '../../tasks/alerts';
import { createCustomRuleActivated } from '../../tasks/api_calls/rules';
import { cleanKibana } from '../../tasks/common';
import { waitForAlertsToPopulate } from '../../tasks/create_new_rule';
import { login, loginAndWaitForPage, waitForPageWithoutDateRange } from '../../tasks/login';
import { refreshPage } from '../../tasks/security_header';
import { DETECTIONS_URL } from '../../urls/navigation';
import { ATTACH_ALERT_TO_CASE_BUTTON } from '../../screens/alerts_detection_rules';
const loadDetectionsPage = (role: ROLES) => {
waitForPageWithoutDateRange(DETECTIONS_URL, role);
waitForAlertsToPopulate();
};
describe('Alerts timeline', () => {
before(() => {
// First we login as a privileged user to create alerts.
cleanKibana();
loginAndWaitForPage(DETECTIONS_URL, ROLES.platform_engineer);
waitForAlertsPanelToBeLoaded();
waitForAlertsIndexToBeCreated();
createCustomRuleActivated(newRule);
refreshPage();
waitForAlertsToPopulate();
// Then we login as read-only user to test.
login(ROLES.reader);
});
context('Privileges: read only', () => {
beforeEach(() => {
loadDetectionsPage(ROLES.reader);
});
it('should not allow user with read only privileges to attach alerts to cases', () => {
cy.get(ATTACH_ALERT_TO_CASE_BUTTON).first().should('be.disabled');
});
});
context('Privileges: can crud', () => {
beforeEach(() => {
loadDetectionsPage(ROLES.platform_engineer);
});
it('should allow a user with crud privileges to attach alerts to cases', () => {
cy.get(ATTACH_ALERT_TO_CASE_BUTTON).first().should('not.be.disabled');
});
});
});

View file

@ -5,6 +5,8 @@
* 2.0.
*/
export const ATTACH_ALERT_TO_CASE_BUTTON = '[data-test-subj="attach-alert-to-case-button"]';
export const BULK_ACTIONS_BTN = '[data-test-subj="bulkActions"] span';
export const CREATE_NEW_RULE_BTN = '[data-test-subj="create-new-rule"]';

View file

@ -10,7 +10,7 @@ import React, { ReactNode } from 'react';
import { mount } from 'enzyme';
import { EuiGlobalToastList } from '@elastic/eui';
import { useKibana } from '../../../common/lib/kibana';
import { useKibana, useGetUserSavedObjectPermissions } from '../../../common/lib/kibana';
import { useStateToaster } from '../../../common/components/toasters';
import { TestProviders } from '../../../common/mock';
import { usePostComment } from '../../containers/use_post_comment';
@ -113,8 +113,8 @@ describe('AddToCaseAction', () => {
ecsRowData: {
_id: 'test-id',
_index: 'test-index',
signal: { rule: { id: ['rule-id'], name: ['rule-name'], false_positives: [] } },
},
disabled: false,
};
const mockDispatchToaster = jest.fn();
@ -127,6 +127,10 @@ describe('AddToCaseAction', () => {
(useKibana as jest.Mock).mockReturnValue({
services: { application: { navigateToApp: mockNavigateToApp } },
});
(useGetUserSavedObjectPermissions as jest.Mock).mockReturnValue({
crud: true,
read: true,
});
});
it('it renders', async () => {
@ -181,8 +185,8 @@ describe('AddToCaseAction', () => {
alertId: 'test-id',
index: 'test-index',
rule: {
id: null,
name: null,
id: 'rule-id',
name: 'rule-name',
},
type: 'alert',
});
@ -218,7 +222,38 @@ describe('AddToCaseAction', () => {
alertId: 'test-id',
index: 'test-index',
rule: {
id: null,
id: 'rule-id',
name: 'rule-name',
},
type: 'alert',
});
});
it('it set rule information as null when missing', async () => {
const wrapper = mount(
<TestProviders>
<AddToCaseAction
{...props}
ecsRowData={{
_id: 'test-id',
_index: 'test-index',
signal: { rule: { id: ['rule-id'], false_positives: [] } },
}}
/>
</TestProviders>
);
wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().simulate('click');
wrapper.find(`[data-test-subj="add-new-case-item"]`).first().simulate('click');
wrapper.find(`[data-test-subj="form-context-on-success"]`).first().simulate('click');
expect(postComment.mock.calls[0][0].caseId).toBe('new-case');
expect(postComment.mock.calls[0][0].data).toEqual({
alertId: 'test-id',
index: 'test-index',
rule: {
id: 'rule-id',
name: null,
},
type: 'alert',
@ -291,4 +326,39 @@ describe('AddToCaseAction', () => {
path: '/selected-case',
});
});
it('disabled when event type is not supported', async () => {
const wrapper = mount(
<TestProviders>
<AddToCaseAction
{...props}
ecsRowData={{
_id: 'test-id',
_index: 'test-index',
}}
/>
</TestProviders>
);
expect(
wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().prop('disabled')
).toBeTruthy();
});
it('disabled when user does not have crud permissions', async () => {
(useGetUserSavedObjectPermissions as jest.Mock).mockReturnValue({
crud: false,
read: true,
});
const wrapper = mount(
<TestProviders>
<AddToCaseAction {...props} />
</TestProviders>
);
expect(
wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().prop('disabled')
).toBeTruthy();
});
});

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { isEmpty } from 'lodash';
import React, { memo, useState, useCallback, useMemo } from 'react';
import {
EuiPopover,
@ -22,7 +23,7 @@ import { usePostComment } from '../../containers/use_post_comment';
import { Case } from '../../containers/types';
import { useStateToaster } from '../../../common/components/toasters';
import { APP_ID } from '../../../../common/constants';
import { useKibana } from '../../../common/lib/kibana';
import { useGetUserSavedObjectPermissions, useKibana } from '../../../common/lib/kibana';
import { getCaseDetailsUrl } from '../../../common/components/link_to';
import { SecurityPageName } from '../../../app/types';
import { useAllCasesModal } from '../use_all_cases_modal';
@ -34,13 +35,11 @@ import { CreateCaseFlyout } from '../create/flyout';
interface AddToCaseActionProps {
ariaLabel?: string;
ecsRowData: Ecs;
disabled: boolean;
}
const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
ariaLabel = i18n.ACTION_ADD_TO_CASE_ARIA_LABEL,
ecsRowData,
disabled,
}) => {
const eventId = ecsRowData._id;
const eventIndex = ecsRowData._index;
@ -51,6 +50,16 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const openPopover = useCallback(() => setIsPopoverOpen(true), []);
const closePopover = useCallback(() => setIsPopoverOpen(false), []);
const userPermissions = useGetUserSavedObjectPermissions();
const isEventSupported = !isEmpty(ecsRowData.signal?.rule?.id);
const userCanCrud = userPermissions?.crud ?? false;
const isDisabled = !userCanCrud || !isEventSupported;
const tooltipContext = userCanCrud
? isEventSupported
? i18n.ACTION_ADD_TO_CASE_TOOLTIP
: i18n.UNSUPPORTED_EVENTS_MSG
: i18n.PERMISSIONS_MSG;
const { postComment } = usePostComment();
@ -137,7 +146,7 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
onClick={addNewCaseClick}
aria-label={i18n.ACTION_ADD_NEW_CASE}
data-test-subj="add-new-case-item"
disabled={disabled}
disabled={isDisabled}
>
<EuiText size="m">{i18n.ACTION_ADD_NEW_CASE}</EuiText>
</EuiContextMenuItem>,
@ -146,31 +155,28 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
onClick={addExistingCaseClick}
aria-label={i18n.ACTION_ADD_EXISTING_CASE}
data-test-subj="add-existing-case-menu-item"
disabled={disabled}
disabled={isDisabled}
>
<EuiText size="m">{i18n.ACTION_ADD_EXISTING_CASE}</EuiText>
</EuiContextMenuItem>,
],
[addExistingCaseClick, addNewCaseClick, disabled]
[addExistingCaseClick, addNewCaseClick, isDisabled]
);
const button = useMemo(
() => (
<EuiToolTip
data-test-subj="attach-alert-to-case-tooltip"
content={i18n.ACTION_ADD_TO_CASE_TOOLTIP}
>
<EuiToolTip data-test-subj="attach-alert-to-case-tooltip" content={tooltipContext}>
<EuiButtonIcon
aria-label={ariaLabel}
data-test-subj="attach-alert-to-case-button"
size="s"
iconType="folderClosed"
onClick={openPopover}
disabled={disabled}
disabled={isDisabled}
/>
</EuiToolTip>
),
[ariaLabel, disabled, openPopover]
[ariaLabel, isDisabled, openPopover, tooltipContext]
);
return (

View file

@ -61,3 +61,18 @@ export const VIEW_CASE = i18n.translate(
defaultMessage: 'View Case',
}
);
export const PERMISSIONS_MSG = i18n.translate(
'xpack.securitySolution.case.timeline.actions.permissionsMessage',
{
defaultMessage:
'You are currently missing the required permissions to attach alerts to cases. Please contact your administrator for further assistance.',
}
);
export const UNSUPPORTED_EVENTS_MSG = i18n.translate(
'xpack.securitySolution.case.timeline.actions.unsupportedEventsMessage',
{
defaultMessage: 'This event cannot be attached to a case',
}
);

View file

@ -175,7 +175,6 @@ export const EventColumnView = React.memo<Props>(
ariaLabel={i18n.ATTACH_ALERT_TO_CASE_FOR_ROW({ ariaRowindex, columnValues })}
key="attach-to-case"
ecsRowData={ecsData}
disabled={eventType !== 'signal'}
/>,
]
: []),