diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.test.tsx new file mode 100644 index 0000000000000..1f4d0e9e8d12c --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.test.tsx @@ -0,0 +1,68 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { AddEventNoteAction } from './add_note_icon_item'; +import { useUserPrivileges } from '../../../../../common/components/user_privileges'; +import { getEndpointPrivilegesInitialStateMock } from '../../../../../common/components/user_privileges/endpoint/mocks'; +import { TestProviders } from '../../../../../common/mock'; +import { TimelineType } from '../../../../../../common/types'; + +jest.mock('../../../../../common/components/user_privileges'); +const useUserPrivilegesMock = useUserPrivileges as jest.Mock; + +describe('AddEventNoteAction', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('isDisabled', () => { + test('it disables the add note button when the user does NOT have crud privileges', () => { + useUserPrivilegesMock.mockReturnValue({ + kibanaSecuritySolutionsPrivileges: { crud: false, read: true }, + endpointPrivileges: getEndpointPrivilegesInitialStateMock(), + }); + + render( + + + + ); + + expect(screen.getByTestId('timeline-notes-button-small')).toHaveClass( + 'euiButtonIcon-isDisabled' + ); + }); + + test('it enables the add note button when the user has crud privileges', () => { + useUserPrivilegesMock.mockReturnValue({ + kibanaSecuritySolutionsPrivileges: { crud: true, read: true }, + endpointPrivileges: getEndpointPrivilegesInitialStateMock(), + }); + + render( + + + + ); + + expect(screen.getByTestId('timeline-notes-button-small')).not.toHaveClass( + 'euiButtonIcon-isDisabled' + ); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx index 22c40ba78b0fe..784685997e5ff 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx @@ -11,6 +11,7 @@ import { TimelineType } from '../../../../../../common/types/timeline'; import * as i18n from '../translations'; import { NotesButton } from '../../properties/helpers'; import { ActionIconItem } from './action_icon_item'; +import { useUserPrivileges } from '../../../../../common/components/user_privileges'; interface AddEventNoteActionProps { ariaLabel?: string; @@ -24,20 +25,25 @@ const AddEventNoteActionComponent: React.FC = ({ showNotes, timelineType, toggleShowNotes, -}) => ( - - - -); +}) => { + const { kibanaSecuritySolutionsPrivileges } = useUserPrivileges(); + + return ( + + + + ); +}; AddEventNoteActionComponent.displayName = 'AddEventNoteActionComponent'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.test.tsx new file mode 100644 index 0000000000000..e94c2ac2a38f0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.test.tsx @@ -0,0 +1,68 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { PinEventAction } from './pin_event_action'; +import { useUserPrivileges } from '../../../../../common/components/user_privileges'; +import { getEndpointPrivilegesInitialStateMock } from '../../../../../common/components/user_privileges/endpoint/mocks'; +import { TestProviders } from '../../../../../common/mock'; +import { TimelineType } from '../../../../../../common/types'; + +jest.mock('../../../../../common/components/user_privileges'); +const useUserPrivilegesMock = useUserPrivileges as jest.Mock; + +describe('PinEventAction', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('isDisabled', () => { + test('it disables the pin event button when the user does NOT have crud privileges', () => { + useUserPrivilegesMock.mockReturnValue({ + kibanaSecuritySolutionsPrivileges: { crud: false, read: true }, + endpointPrivileges: getEndpointPrivilegesInitialStateMock(), + }); + + render( + + + + ); + + expect(screen.getByTestId('pin')).toHaveClass('euiButtonIcon-isDisabled'); + }); + + test('it enables the pin event button when the user has crud privileges', () => { + useUserPrivilegesMock.mockReturnValue({ + kibanaSecuritySolutionsPrivileges: { crud: true, read: true }, + endpointPrivileges: getEndpointPrivilegesInitialStateMock(), + }); + + render( + + + + ); + + expect(screen.getByTestId('pin')).not.toHaveClass('euiButtonIcon-isDisabled'); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.tsx index 53970594c8c1c..d0294d3908590 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.tsx @@ -13,6 +13,7 @@ import { EventsTdContent } from '../../styles'; import { eventHasNotes, getPinTooltip } from '../helpers'; import { Pin } from '../../pin'; import { TimelineType } from '../../../../../../common/types/timeline'; +import { useUserPrivileges } from '../../../../../common/components/user_privileges'; interface PinEventActionProps { ariaLabel?: string; @@ -31,6 +32,7 @@ const PinEventActionComponent: React.FC = ({ eventIsPinned, timelineType, }) => { + const { kibanaSecuritySolutionsPrivileges } = useUserPrivileges(); const tooltipContent = useMemo( () => getPinTooltip({ @@ -50,6 +52,7 @@ const PinEventActionComponent: React.FC = ({ ariaLabel={ariaLabel} allowUnpinning={!eventHasNotes(noteIds)} data-test-subj="pin-event" + isDisabled={kibanaSecuritySolutionsPrivileges.crud === false} isAlert={isAlert} onClick={onPinClicked} pinned={eventIsPinned} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx index 334bd464f700f..59b331d4d7f11 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx @@ -28,6 +28,17 @@ jest.mock('../../../../../common/hooks/use_selector', () => ({ useShallowEqualSelector: jest.fn(), useDeepEqualSelector: jest.fn(), })); +jest.mock('../../../../../common/components/user_privileges', () => { + return { + useUserPrivileges: () => ({ + listPrivileges: { loading: false, error: undefined, result: undefined }, + detectionEnginePrivileges: { loading: false, error: undefined, result: undefined }, + endpointPrivileges: {}, + kibanaSecuritySolutionsPrivileges: { crud: true, read: true }, + }), + }; +}); + jest.mock('../../../../../common/lib/kibana', () => ({ useKibana: () => ({ services: { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index 200c9810d9fe6..f2045327a42f7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -36,6 +36,17 @@ import { createStore, State } from '../../../../common/store'; jest.mock('../../../../common/lib/kibana/hooks'); jest.mock('../../../../common/hooks/use_app_toasts'); +jest.mock('../../../../common/components/user_privileges', () => { + return { + useUserPrivileges: () => ({ + listPrivileges: { loading: false, error: undefined, result: undefined }, + detectionEnginePrivileges: { loading: false, error: undefined, result: undefined }, + endpointPrivileges: {}, + kibanaSecuritySolutionsPrivileges: { crud: true, read: true }, + }), + }; +}); + jest.mock('../../../../common/lib/kibana', () => { const originalModule = jest.requireActual('../../../../common/lib/kibana'); const mockCasesContract = jest.requireActual('@kbn/cases-plugin/public/mocks'); @@ -225,7 +236,7 @@ describe('Body', () => { mockDispatch.mockClear(); }); - test('Add a Note to an event', () => { + test('Add a note to an event', () => { const wrapper = mount( @@ -257,7 +268,7 @@ describe('Body', () => { ); }); - test('Add two Note to an event', () => { + test('Add two notes to an event', () => { const { storage } = createSecuritySolutionStorageMock(); const state: State = { ...mockGlobalState, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx index c2179abbb61df..7c25794a16c80 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx @@ -39,6 +39,7 @@ import { getTimelineNoteSelector } from './selectors'; import { DetailsPanel } from '../../side_panel'; import { getScrollToTopSelector } from '../tabs_content/selectors'; import { useScrollToTop } from '../../../../common/components/scroll_to_top'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; const FullWidthFlexGroup = styled(EuiFlexGroup)` width: 100%; @@ -131,6 +132,7 @@ interface NotesTabContentProps { const NotesTabContentComponent: React.FC = ({ timelineId }) => { const dispatch = useDispatch(); + const { kibanaSecuritySolutionsPrivileges } = useUserPrivileges(); const getScrollToTop = useMemo(() => getScrollToTopSelector(), []); const scrollToTop = useShallowEqualSelector((state) => getScrollToTop(state, timelineId)); @@ -239,7 +241,7 @@ const NotesTabContentComponent: React.FC = ({ timelineId } showTimelineDescription /> - {!isImmutable && ( + {!isImmutable && kibanaSecuritySolutionsPrivileges.crud === true && ( void; pinned: boolean; @@ -45,7 +46,7 @@ export const getDefaultAriaLabel = ({ }; export const Pin = React.memo( - ({ ariaLabel, allowUnpinning, isAlert, onClick = noop, pinned, timelineType }) => { + ({ ariaLabel, allowUnpinning, isAlert, isDisabled, onClick = noop, pinned, timelineType }) => { const isTemplate = timelineType === TimelineType.template; const defaultAriaLabel = getDefaultAriaLabel({ isAlert, @@ -60,7 +61,7 @@ export const Pin = React.memo( data-test-subj="pin" iconType={getPinIcon(pinned)} onClick={onClick} - isDisabled={isTemplate || !allowUnpinning} + isDisabled={isDisabled || isTemplate || !allowUnpinning} size="s" /> ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx index c59c0a15ff53d..ff0d8686bb9c3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx @@ -91,6 +91,7 @@ NewTimeline.displayName = 'NewTimeline'; interface NotesButtonProps { ariaLabel?: string; + isDisabled?: boolean; showNotes: boolean; toggleShowNotes: () => void; toolTip?: string; @@ -99,6 +100,7 @@ interface NotesButtonProps { interface SmallNotesButtonProps { ariaLabel?: string; + isDisabled?: boolean; toggleShowNotes: () => void; timelineType: TimelineTypeLiteral; } @@ -106,7 +108,7 @@ interface SmallNotesButtonProps { export const NOTES_BUTTON_CLASS_NAME = 'notes-button'; const SmallNotesButton = React.memo( - ({ ariaLabel = i18n.NOTES, toggleShowNotes, timelineType }) => { + ({ ariaLabel = i18n.NOTES, isDisabled, toggleShowNotes, timelineType }) => { const isTemplate = timelineType === TimelineType.template; return ( @@ -114,6 +116,7 @@ const SmallNotesButton = React.memo( aria-label={ariaLabel} className={NOTES_BUTTON_CLASS_NAME} data-test-subj="timeline-notes-button-small" + disabled={isDisabled} iconType="editorComment" onClick={toggleShowNotes} size="s" @@ -125,10 +128,11 @@ const SmallNotesButton = React.memo( SmallNotesButton.displayName = 'SmallNotesButton'; export const NotesButton = React.memo( - ({ ariaLabel, showNotes, timelineType, toggleShowNotes, toolTip }) => + ({ ariaLabel, isDisabled, showNotes, timelineType, toggleShowNotes, toolTip }) => showNotes ? ( @@ -136,6 +140,7 @@ export const NotesButton = React.memo(