mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[Security Solution][Investigations] - Disable adding notes when read only (#133905)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
81e8208258
commit
b41bc6643d
9 changed files with 196 additions and 21 deletions
|
@ -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(
|
||||||
|
<TestProviders>
|
||||||
|
<AddEventNoteAction
|
||||||
|
showNotes={false}
|
||||||
|
timelineType={TimelineType.default}
|
||||||
|
toggleShowNotes={jest.fn}
|
||||||
|
/>
|
||||||
|
</TestProviders>
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<TestProviders>
|
||||||
|
<AddEventNoteAction
|
||||||
|
showNotes={false}
|
||||||
|
timelineType={TimelineType.default}
|
||||||
|
toggleShowNotes={jest.fn}
|
||||||
|
/>
|
||||||
|
</TestProviders>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('timeline-notes-button-small')).not.toHaveClass(
|
||||||
|
'euiButtonIcon-isDisabled'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -11,6 +11,7 @@ import { TimelineType } from '../../../../../../common/types/timeline';
|
||||||
import * as i18n from '../translations';
|
import * as i18n from '../translations';
|
||||||
import { NotesButton } from '../../properties/helpers';
|
import { NotesButton } from '../../properties/helpers';
|
||||||
import { ActionIconItem } from './action_icon_item';
|
import { ActionIconItem } from './action_icon_item';
|
||||||
|
import { useUserPrivileges } from '../../../../../common/components/user_privileges';
|
||||||
|
|
||||||
interface AddEventNoteActionProps {
|
interface AddEventNoteActionProps {
|
||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
|
@ -24,20 +25,25 @@ const AddEventNoteActionComponent: React.FC<AddEventNoteActionProps> = ({
|
||||||
showNotes,
|
showNotes,
|
||||||
timelineType,
|
timelineType,
|
||||||
toggleShowNotes,
|
toggleShowNotes,
|
||||||
}) => (
|
}) => {
|
||||||
<ActionIconItem>
|
const { kibanaSecuritySolutionsPrivileges } = useUserPrivileges();
|
||||||
<NotesButton
|
|
||||||
ariaLabel={ariaLabel}
|
return (
|
||||||
data-test-subj="add-note"
|
<ActionIconItem>
|
||||||
showNotes={showNotes}
|
<NotesButton
|
||||||
timelineType={timelineType}
|
ariaLabel={ariaLabel}
|
||||||
toggleShowNotes={toggleShowNotes}
|
data-test-subj="add-note"
|
||||||
toolTip={
|
isDisabled={kibanaSecuritySolutionsPrivileges.crud === false}
|
||||||
timelineType === TimelineType.template ? i18n.NOTES_DISABLE_TOOLTIP : i18n.NOTES_TOOLTIP
|
showNotes={showNotes}
|
||||||
}
|
timelineType={timelineType}
|
||||||
/>
|
toggleShowNotes={toggleShowNotes}
|
||||||
</ActionIconItem>
|
toolTip={
|
||||||
);
|
timelineType === TimelineType.template ? i18n.NOTES_DISABLE_TOOLTIP : i18n.NOTES_TOOLTIP
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ActionIconItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
AddEventNoteActionComponent.displayName = 'AddEventNoteActionComponent';
|
AddEventNoteActionComponent.displayName = 'AddEventNoteActionComponent';
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
<TestProviders>
|
||||||
|
<PinEventAction
|
||||||
|
isAlert={false}
|
||||||
|
noteIds={[]}
|
||||||
|
onPinClicked={jest.fn}
|
||||||
|
eventIsPinned={false}
|
||||||
|
timelineType={TimelineType.default}
|
||||||
|
/>
|
||||||
|
</TestProviders>
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<TestProviders>
|
||||||
|
<PinEventAction
|
||||||
|
isAlert={false}
|
||||||
|
noteIds={[]}
|
||||||
|
onPinClicked={jest.fn}
|
||||||
|
eventIsPinned={false}
|
||||||
|
timelineType={TimelineType.default}
|
||||||
|
/>
|
||||||
|
</TestProviders>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('pin')).not.toHaveClass('euiButtonIcon-isDisabled');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -13,6 +13,7 @@ import { EventsTdContent } from '../../styles';
|
||||||
import { eventHasNotes, getPinTooltip } from '../helpers';
|
import { eventHasNotes, getPinTooltip } from '../helpers';
|
||||||
import { Pin } from '../../pin';
|
import { Pin } from '../../pin';
|
||||||
import { TimelineType } from '../../../../../../common/types/timeline';
|
import { TimelineType } from '../../../../../../common/types/timeline';
|
||||||
|
import { useUserPrivileges } from '../../../../../common/components/user_privileges';
|
||||||
|
|
||||||
interface PinEventActionProps {
|
interface PinEventActionProps {
|
||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
|
@ -31,6 +32,7 @@ const PinEventActionComponent: React.FC<PinEventActionProps> = ({
|
||||||
eventIsPinned,
|
eventIsPinned,
|
||||||
timelineType,
|
timelineType,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { kibanaSecuritySolutionsPrivileges } = useUserPrivileges();
|
||||||
const tooltipContent = useMemo(
|
const tooltipContent = useMemo(
|
||||||
() =>
|
() =>
|
||||||
getPinTooltip({
|
getPinTooltip({
|
||||||
|
@ -50,6 +52,7 @@ const PinEventActionComponent: React.FC<PinEventActionProps> = ({
|
||||||
ariaLabel={ariaLabel}
|
ariaLabel={ariaLabel}
|
||||||
allowUnpinning={!eventHasNotes(noteIds)}
|
allowUnpinning={!eventHasNotes(noteIds)}
|
||||||
data-test-subj="pin-event"
|
data-test-subj="pin-event"
|
||||||
|
isDisabled={kibanaSecuritySolutionsPrivileges.crud === false}
|
||||||
isAlert={isAlert}
|
isAlert={isAlert}
|
||||||
onClick={onPinClicked}
|
onClick={onPinClicked}
|
||||||
pinned={eventIsPinned}
|
pinned={eventIsPinned}
|
||||||
|
|
|
@ -28,6 +28,17 @@ jest.mock('../../../../../common/hooks/use_selector', () => ({
|
||||||
useShallowEqualSelector: jest.fn(),
|
useShallowEqualSelector: jest.fn(),
|
||||||
useDeepEqualSelector: 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', () => ({
|
jest.mock('../../../../../common/lib/kibana', () => ({
|
||||||
useKibana: () => ({
|
useKibana: () => ({
|
||||||
services: {
|
services: {
|
||||||
|
|
|
@ -36,6 +36,17 @@ import { createStore, State } from '../../../../common/store';
|
||||||
|
|
||||||
jest.mock('../../../../common/lib/kibana/hooks');
|
jest.mock('../../../../common/lib/kibana/hooks');
|
||||||
jest.mock('../../../../common/hooks/use_app_toasts');
|
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', () => {
|
jest.mock('../../../../common/lib/kibana', () => {
|
||||||
const originalModule = jest.requireActual('../../../../common/lib/kibana');
|
const originalModule = jest.requireActual('../../../../common/lib/kibana');
|
||||||
const mockCasesContract = jest.requireActual('@kbn/cases-plugin/public/mocks');
|
const mockCasesContract = jest.requireActual('@kbn/cases-plugin/public/mocks');
|
||||||
|
@ -225,7 +236,7 @@ describe('Body', () => {
|
||||||
mockDispatch.mockClear();
|
mockDispatch.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Add a Note to an event', () => {
|
test('Add a note to an event', () => {
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<TestProviders>
|
<TestProviders>
|
||||||
<StatefulBody {...props} />
|
<StatefulBody {...props} />
|
||||||
|
@ -257,7 +268,7 @@ describe('Body', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Add two Note to an event', () => {
|
test('Add two notes to an event', () => {
|
||||||
const { storage } = createSecuritySolutionStorageMock();
|
const { storage } = createSecuritySolutionStorageMock();
|
||||||
const state: State = {
|
const state: State = {
|
||||||
...mockGlobalState,
|
...mockGlobalState,
|
||||||
|
|
|
@ -39,6 +39,7 @@ import { getTimelineNoteSelector } from './selectors';
|
||||||
import { DetailsPanel } from '../../side_panel';
|
import { DetailsPanel } from '../../side_panel';
|
||||||
import { getScrollToTopSelector } from '../tabs_content/selectors';
|
import { getScrollToTopSelector } from '../tabs_content/selectors';
|
||||||
import { useScrollToTop } from '../../../../common/components/scroll_to_top';
|
import { useScrollToTop } from '../../../../common/components/scroll_to_top';
|
||||||
|
import { useUserPrivileges } from '../../../../common/components/user_privileges';
|
||||||
|
|
||||||
const FullWidthFlexGroup = styled(EuiFlexGroup)`
|
const FullWidthFlexGroup = styled(EuiFlexGroup)`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -131,6 +132,7 @@ interface NotesTabContentProps {
|
||||||
|
|
||||||
const NotesTabContentComponent: React.FC<NotesTabContentProps> = ({ timelineId }) => {
|
const NotesTabContentComponent: React.FC<NotesTabContentProps> = ({ timelineId }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const { kibanaSecuritySolutionsPrivileges } = useUserPrivileges();
|
||||||
|
|
||||||
const getScrollToTop = useMemo(() => getScrollToTopSelector(), []);
|
const getScrollToTop = useMemo(() => getScrollToTopSelector(), []);
|
||||||
const scrollToTop = useShallowEqualSelector((state) => getScrollToTop(state, timelineId));
|
const scrollToTop = useShallowEqualSelector((state) => getScrollToTop(state, timelineId));
|
||||||
|
@ -239,7 +241,7 @@ const NotesTabContentComponent: React.FC<NotesTabContentProps> = ({ timelineId }
|
||||||
showTimelineDescription
|
showTimelineDescription
|
||||||
/>
|
/>
|
||||||
<EuiSpacer size="s" />
|
<EuiSpacer size="s" />
|
||||||
{!isImmutable && (
|
{!isImmutable && kibanaSecuritySolutionsPrivileges.crud === true && (
|
||||||
<AddNote
|
<AddNote
|
||||||
associateNote={associateNote}
|
associateNote={associateNote}
|
||||||
newNote={newNote}
|
newNote={newNote}
|
||||||
|
|
|
@ -21,6 +21,7 @@ interface Props {
|
||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
allowUnpinning: boolean;
|
allowUnpinning: boolean;
|
||||||
isAlert: boolean;
|
isAlert: boolean;
|
||||||
|
isDisabled?: boolean;
|
||||||
timelineType?: TimelineTypeLiteral;
|
timelineType?: TimelineTypeLiteral;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
pinned: boolean;
|
pinned: boolean;
|
||||||
|
@ -45,7 +46,7 @@ export const getDefaultAriaLabel = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Pin = React.memo<Props>(
|
export const Pin = React.memo<Props>(
|
||||||
({ ariaLabel, allowUnpinning, isAlert, onClick = noop, pinned, timelineType }) => {
|
({ ariaLabel, allowUnpinning, isAlert, isDisabled, onClick = noop, pinned, timelineType }) => {
|
||||||
const isTemplate = timelineType === TimelineType.template;
|
const isTemplate = timelineType === TimelineType.template;
|
||||||
const defaultAriaLabel = getDefaultAriaLabel({
|
const defaultAriaLabel = getDefaultAriaLabel({
|
||||||
isAlert,
|
isAlert,
|
||||||
|
@ -60,7 +61,7 @@ export const Pin = React.memo<Props>(
|
||||||
data-test-subj="pin"
|
data-test-subj="pin"
|
||||||
iconType={getPinIcon(pinned)}
|
iconType={getPinIcon(pinned)}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
isDisabled={isTemplate || !allowUnpinning}
|
isDisabled={isDisabled || isTemplate || !allowUnpinning}
|
||||||
size="s"
|
size="s"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -91,6 +91,7 @@ NewTimeline.displayName = 'NewTimeline';
|
||||||
|
|
||||||
interface NotesButtonProps {
|
interface NotesButtonProps {
|
||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
|
isDisabled?: boolean;
|
||||||
showNotes: boolean;
|
showNotes: boolean;
|
||||||
toggleShowNotes: () => void;
|
toggleShowNotes: () => void;
|
||||||
toolTip?: string;
|
toolTip?: string;
|
||||||
|
@ -99,6 +100,7 @@ interface NotesButtonProps {
|
||||||
|
|
||||||
interface SmallNotesButtonProps {
|
interface SmallNotesButtonProps {
|
||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
|
isDisabled?: boolean;
|
||||||
toggleShowNotes: () => void;
|
toggleShowNotes: () => void;
|
||||||
timelineType: TimelineTypeLiteral;
|
timelineType: TimelineTypeLiteral;
|
||||||
}
|
}
|
||||||
|
@ -106,7 +108,7 @@ interface SmallNotesButtonProps {
|
||||||
export const NOTES_BUTTON_CLASS_NAME = 'notes-button';
|
export const NOTES_BUTTON_CLASS_NAME = 'notes-button';
|
||||||
|
|
||||||
const SmallNotesButton = React.memo<SmallNotesButtonProps>(
|
const SmallNotesButton = React.memo<SmallNotesButtonProps>(
|
||||||
({ ariaLabel = i18n.NOTES, toggleShowNotes, timelineType }) => {
|
({ ariaLabel = i18n.NOTES, isDisabled, toggleShowNotes, timelineType }) => {
|
||||||
const isTemplate = timelineType === TimelineType.template;
|
const isTemplate = timelineType === TimelineType.template;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -114,6 +116,7 @@ const SmallNotesButton = React.memo<SmallNotesButtonProps>(
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
className={NOTES_BUTTON_CLASS_NAME}
|
className={NOTES_BUTTON_CLASS_NAME}
|
||||||
data-test-subj="timeline-notes-button-small"
|
data-test-subj="timeline-notes-button-small"
|
||||||
|
disabled={isDisabled}
|
||||||
iconType="editorComment"
|
iconType="editorComment"
|
||||||
onClick={toggleShowNotes}
|
onClick={toggleShowNotes}
|
||||||
size="s"
|
size="s"
|
||||||
|
@ -125,10 +128,11 @@ const SmallNotesButton = React.memo<SmallNotesButtonProps>(
|
||||||
SmallNotesButton.displayName = 'SmallNotesButton';
|
SmallNotesButton.displayName = 'SmallNotesButton';
|
||||||
|
|
||||||
export const NotesButton = React.memo<NotesButtonProps>(
|
export const NotesButton = React.memo<NotesButtonProps>(
|
||||||
({ ariaLabel, showNotes, timelineType, toggleShowNotes, toolTip }) =>
|
({ ariaLabel, isDisabled, showNotes, timelineType, toggleShowNotes, toolTip }) =>
|
||||||
showNotes ? (
|
showNotes ? (
|
||||||
<SmallNotesButton
|
<SmallNotesButton
|
||||||
ariaLabel={ariaLabel}
|
ariaLabel={ariaLabel}
|
||||||
|
isDisabled={isDisabled}
|
||||||
toggleShowNotes={toggleShowNotes}
|
toggleShowNotes={toggleShowNotes}
|
||||||
timelineType={timelineType}
|
timelineType={timelineType}
|
||||||
/>
|
/>
|
||||||
|
@ -136,6 +140,7 @@ export const NotesButton = React.memo<NotesButtonProps>(
|
||||||
<EuiToolTip content={toolTip || ''} data-test-subj="timeline-notes-tool-tip">
|
<EuiToolTip content={toolTip || ''} data-test-subj="timeline-notes-tool-tip">
|
||||||
<SmallNotesButton
|
<SmallNotesButton
|
||||||
ariaLabel={ariaLabel}
|
ariaLabel={ariaLabel}
|
||||||
|
isDisabled={isDisabled}
|
||||||
toggleShowNotes={toggleShowNotes}
|
toggleShowNotes={toggleShowNotes}
|
||||||
timelineType={timelineType}
|
timelineType={timelineType}
|
||||||
/>
|
/>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue