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(