[SecuritySolution] Update Save Timeline button behaviour (#136724)

* Update Save Timeline button behaviour based on the user privileges

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Jatin Kathuria 2022-07-22 17:30:08 +02:00 committed by GitHub
parent 678d59e8ec
commit a59ba34397
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 155 additions and 54 deletions

View file

@ -6,6 +6,7 @@
*/
import { getTimeline } from '../../objects/timeline';
import { ROLES } from '../../../common/test';
import {
LOCKED_ICON,
@ -19,6 +20,8 @@ import {
TIMELINE_PANEL,
TIMELINE_TAB_CONTENT_EQL,
TIMELINE_TAB_CONTENT_GRAPHS_NOTES,
EDIT_TIMELINE_BTN,
EDIT_TIMELINE_TOOLTIP,
} from '../../screens/timeline';
import { createTimelineTemplate } from '../../tasks/api_calls/timelines';
@ -48,6 +51,7 @@ describe('Create a timeline from a template', () => {
createTimelineTemplate(getTimeline());
visitWithoutDateRange(TIMELINE_TEMPLATES_URL);
});
it('Should have the same query and open the timeline modal', () => {
selectCustomTemplates();
expandEventAction();
@ -71,10 +75,30 @@ describe('Timelines', (): void => {
after(() => {
closeTimeline();
});
context('Privileges: CRUD', () => {
it('toggle create timeline ', () => {
createNewTimeline();
addNameAndDescriptionToTimeline(getTimeline());
cy.get(TIMELINE_PANEL).should('be.visible');
});
});
it('toggle create timeline ', () => {
createNewTimeline();
cy.get(TIMELINE_PANEL).should('be.visible');
context('Privileges: READ', () => {
before(() => {
login(ROLES.reader);
visit(OVERVIEW_URL, undefined, ROLES.reader);
});
it('should not be able to create/update timeline ', () => {
createNewTimeline();
cy.get(TIMELINE_PANEL).should('be.visible');
cy.get(EDIT_TIMELINE_BTN).should('be.disabled');
cy.get(EDIT_TIMELINE_BTN).first().trigger('mouseover', { force: true });
cy.get(EDIT_TIMELINE_TOOLTIP).should('be.visible');
cy.get(EDIT_TIMELINE_TOOLTIP).should(
'have.text',
'You can use Timeline to investigate events, but you do not have the required permissions to save timelines for future use. If you need to save timelines, contact your Kibana administrator.'
);
});
});
});
@ -84,6 +108,8 @@ describe('Timelines', (): void => {
});
before(() => {
login();
visit(OVERVIEW_URL);
openTimelineUsingToggle();
addNameAndDescriptionToTimeline(getTimeline());
populateTimeline();

View file

@ -263,3 +263,7 @@ export const TIMELINE_TAB_CONTENT_GRAPHS_NOTES =
export const TIMESTAMP_HOVER_ACTION_OVERFLOW_BTN = '[data-test-subj="more-actions-@timestamp"]';
export const USER_KPI = '[data-test-subj="siem-timeline-user-kpi"]';
export const EDIT_TIMELINE_BTN = '[data-test-subj="save-timeline-button-icon"]';
export const EDIT_TIMELINE_TOOLTIP = '[data-test-subj="save-timeline-btn-tooltip"]';

View file

@ -312,9 +312,15 @@ export const waitForPage = (url: string) => {
);
};
export const visit = (url: string, onBeforeLoadCallback?: (win: Cypress.AUTWindow) => void) => {
export const visit = (
url: string,
onBeforeLoadCallback?: (win: Cypress.AUTWindow) => void,
role?: ROLES
) => {
cy.visit(
`${url}?timerange=(global:(linkTo:!(timeline),timerange:(from:1547914976217,fromStr:'2019-01-19T16:22:56.217Z',kind:relative,to:1579537385745,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1547914976217,fromStr:'2019-01-19T16:22:56.217Z',kind:relative,to:1579537385745,toStr:now)))`,
`${
role ? getUrlWithRoute(role, url) : url
}?timerange=(global:(linkTo:!(timeline),timerange:(from:1547914976217,fromStr:'2019-01-19T16:22:56.217Z',kind:relative,to:1579537385745,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1547914976217,fromStr:'2019-01-19T16:22:56.217Z',kind:relative,to:1579537385745,toStr:now)))`,
{
onBeforeLoad(win) {
if (onBeforeLoadCallback) {

View file

@ -6,9 +6,18 @@
*/
import React from 'react';
import { mount } from 'enzyme';
import { render, fireEvent, waitFor, screen } from '@testing-library/react';
import type { SaveTimelineComponentProps } from './save_timeline_button';
import { SaveTimelineButton } from './save_timeline_button';
import { TestProviders } from '../../../../common/mock';
import { useUserPrivileges } from '../../../../common/components/user_privileges';
const TEST_ID = {
SAVE_TIMELINE_MODAL: 'save-timeline-modal',
SAVE_TIMELINE_BTN: 'save-timeline-button-icon',
SAVE_TIMELINE_TOOLTIP: 'save-timeline-tooltip',
};
jest.mock('react-redux', () => {
const actual = jest.requireActual('react-redux');
return {
@ -16,59 +25,100 @@ jest.mock('react-redux', () => {
useDispatch: jest.fn(),
};
});
jest.mock('../../../../common/lib/kibana');
jest.mock('./title_and_description');
jest.mock('../../../../common/components/user_privileges');
const props = {
initialFocus: 'title' as const,
timelineId: 'timeline-1',
toolTip: 'tooltip message',
};
const TestSaveTimelineButton = (_props: SaveTimelineComponentProps) => (
<TestProviders>
<SaveTimelineButton {..._props} />
</TestProviders>
);
jest.mock('raf', () => {
return jest.fn().mockImplementation((cb) => cb());
});
describe('SaveTimelineButton', () => {
const props = {
initialFocus: 'title' as const,
timelineId: 'timeline-1',
toolTip: 'tooltip message',
};
test('Show tooltip', () => {
const component = mount(
<TestProviders>
<SaveTimelineButton {...props} />
</TestProviders>
);
expect(component.find('[data-test-subj="save-timeline-btn-tooltip"]').exists()).toEqual(true);
beforeEach(() => {
jest.clearAllMocks();
});
test('Hide tooltip', () => {
const component = mount(
<TestProviders>
<SaveTimelineButton {...props} />
</TestProviders>
);
component.find('[data-test-subj="save-timeline-button-icon"]').first().simulate('click');
expect(component.find('[data-test-subj="save-timeline-btn-tooltip"]').exists()).toEqual(false);
// skipping this test because popover is not getting visible by RTL gestures.
//
// Raised a bug with eui team: https://github.com/elastic/eui/issues/6065
xit('Show tooltip', async () => {
render(<TestSaveTimelineButton {...props} />);
const saveTimelineIcon = screen.queryAllByTestId(TEST_ID.SAVE_TIMELINE_BTN)[0];
fireEvent.mouseOver(saveTimelineIcon);
jest.runAllTimers();
await waitFor(() => {
expect(screen.getByRole('tooltip')).toBeVisible();
});
});
test('should show a button with pencil icon', () => {
const component = mount(
<TestProviders>
<SaveTimelineButton {...props} />
</TestProviders>
it('should show a button with pencil icon', () => {
render(<TestSaveTimelineButton {...props} />);
expect(screen.getByTestId(TEST_ID.SAVE_TIMELINE_BTN).firstChild).toHaveAttribute(
'data-euiicon-type',
'pencil'
);
expect(
component.find('[data-test-subj="save-timeline-button-icon"]').first().prop('iconType')
).toEqual('pencil');
});
test('should not show a modal when showOverlay equals false', () => {
const component = mount(
it('should have edit timeline btn disabled with tooltip if user does not have write access', () => {
(useUserPrivileges as jest.Mock).mockReturnValue({
kibanaSecuritySolutionsPrivileges: { crud: false },
});
render(
<TestProviders>
<SaveTimelineButton {...props} />
</TestProviders>
);
expect(component.find('[data-test-subj="save-timeline-modal"]').exists()).toEqual(false);
expect(screen.getByTestId(TEST_ID.SAVE_TIMELINE_BTN)).toBeDisabled();
});
test('should show a modal when showOverlay equals true', () => {
const component = mount(
<TestProviders>
<SaveTimelineButton {...props} />
</TestProviders>
);
expect(component.find('[data-test-subj="save-timeline-btn-tooltip"]').exists()).toEqual(true);
expect(component.find('[data-test-subj="save-timeline-modal-comp"]').exists()).toEqual(false);
component.find('[data-test-subj="save-timeline-button-icon"]').first().simulate('click');
expect(component.find('[data-test-subj="save-timeline-btn-tooltip"]').exists()).toEqual(false);
expect(component.find('[data-test-subj="save-timeline-modal-comp"]').exists()).toEqual(true);
it('should not show modal if user does not have write access', async () => {
(useUserPrivileges as jest.Mock).mockReturnValue({
kibanaSecuritySolutionsPrivileges: { crud: false },
});
render(<TestSaveTimelineButton {...props} />);
expect(screen.queryByTestId(TEST_ID.SAVE_TIMELINE_MODAL)).not.toBeInTheDocument();
const saveTimelineIcon = screen.getByTestId(TEST_ID.SAVE_TIMELINE_BTN);
fireEvent.click(saveTimelineIcon);
await waitFor(() => {
expect(screen.queryAllByTestId(TEST_ID.SAVE_TIMELINE_MODAL)).toHaveLength(0);
});
});
it('should show a modal when user has crud privileges', async () => {
(useUserPrivileges as jest.Mock).mockReturnValue({
kibanaSecuritySolutionsPrivileges: { crud: true },
});
render(<TestSaveTimelineButton {...props} />);
expect(screen.queryByTestId(TEST_ID.SAVE_TIMELINE_MODAL)).not.toBeInTheDocument();
const saveTimelineIcon = screen.queryAllByTestId(TEST_ID.SAVE_TIMELINE_BTN)[0];
fireEvent.click(saveTimelineIcon);
await waitFor(() => {
expect(screen.queryByTestId(TEST_ID.SAVE_TIMELINE_TOOLTIP)).not.toBeInTheDocument();
expect(screen.queryAllByTestId(TEST_ID.SAVE_TIMELINE_MODAL)[0]).toBeVisible();
});
});
});

View file

@ -8,13 +8,14 @@
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import React, { useCallback, useMemo, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useUserPrivileges } from '../../../../common/components/user_privileges';
import { TimelineId } from '../../../../../common/types/timeline';
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
import { timelineActions } from '../../../store/timeline';
import { getTimelineSaveModalByIdSelector } from './selectors';
import { TimelineTitleAndDescription } from './title_and_description';
import { EDIT } from './translations';
import * as timelineTranslations from './translations';
export interface SaveTimelineComponentProps {
initialFocus: 'title' | 'description';
@ -45,23 +46,37 @@ export const SaveTimelineButton = React.memo<SaveTimelineComponentProps>(
setShowSaveTimelineOverlay(true);
}, [setShowSaveTimelineOverlay]);
// Case: 1
// check if user has crud privileges so that user can be allowed to edit the timeline
// Case: 2
// TODO: User may have Crud privileges but they may not have access to timeline index.
// Do we need to check that?
const {
kibanaSecuritySolutionsPrivileges: { crud: hasKibanaCrud },
} = useUserPrivileges();
const finalTooltipMsg = useMemo(
() => (hasKibanaCrud ? toolTip : timelineTranslations.CALL_OUT_UNAUTHORIZED_MSG),
[toolTip, hasKibanaCrud]
);
const saveTimelineButtonIcon = useMemo(
() => (
<EuiButtonIcon
aria-label={EDIT}
aria-label={timelineTranslations.EDIT}
isDisabled={!hasKibanaCrud}
onClick={openSaveTimeline}
iconType="pencil"
data-test-subj="save-timeline-button-icon"
/>
),
[openSaveTimeline]
[openSaveTimeline, hasKibanaCrud]
);
return (initialFocus === 'title' && show) || showSaveTimelineOverlay ? (
<>
{saveTimelineButtonIcon}
<TimelineTitleAndDescription
data-test-subj="save-timeline-modal-comp"
closeSaveTimeline={closeSaveTimeline}
initialFocus={initialFocus}
timelineId={timelineId}
@ -69,7 +84,7 @@ export const SaveTimelineButton = React.memo<SaveTimelineComponentProps>(
/>
</>
) : (
<EuiToolTip content={toolTip ?? ''} data-test-subj="save-timeline-btn-tooltip">
<EuiToolTip content={finalTooltipMsg ?? ''} data-test-subj="save-timeline-btn-tooltip">
{saveTimelineButtonIcon}
</EuiToolTip>
);