Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks fine so far. The suggestion with permissions for e2e tests we have been making to each other is that we have 1 test which shows it being disabled for one role and then a second test which shows it being enabled for 1 more role.

This way if someone breaks the code so that it is disabled for all roles we can catch it.

Original file line number Diff line number Diff line change
Expand Up @@ -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"]';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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',
});
Expand Down Expand Up @@ -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');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: You don't need to interpolate here, you can just use '[data-test-subj="add-new-case-item"]'


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',
Expand Down Expand Up @@ -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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* 2.0.
*/

import { isEmpty } from 'lodash';
import React, { memo, useState, useCallback, useMemo } from 'react';
import {
EuiPopover,
Expand All @@ -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';
Expand All @@ -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;
Expand All @@ -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();

Expand Down Expand Up @@ -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>,
Expand All @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -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'}
/>,
]
: []),
Expand Down