Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution][Bug] Add privilege check in open timeline #147964

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -71,6 +71,33 @@ export const useEditTimelineBatchActions = ({
const getBatchItemsPopoverContent = useCallback(
(closePopover: () => void) => {
const disabled = selectedItems == null || selectedItems.length === 0;
const items = [];
if (selectedItems) {
items.push(
<EuiContextMenuItem
data-test-subj="export-timeline-action"
disabled={disabled}
icon="exportAction"
key="ExportItemKey"
onClick={handleEnableExportTimelineDownloader}
>
{i18n.EXPORT_SELECTED}
</EuiContextMenuItem>
);
}
if (deleteTimelines) {
items.push(
<EuiContextMenuItem
data-test-subj="delete-timeline-action"
disabled={disabled}
icon="trash"
key="DeleteItemKey"
onClick={handleOnOpenDeleteTimelineModal}
>
{i18n.DELETE_SELECTED}
</EuiContextMenuItem>
);
}
return (
<>
<EditTimelineActions
Expand All @@ -87,29 +114,7 @@ export const useEditTimelineBatchActions = ({
: selectedItems[0]?.title ?? ''
}
/>

<EuiContextMenuPanel
items={[
<EuiContextMenuItem
data-test-subj="export-timeline-action"
disabled={disabled}
icon="exportAction"
key="ExportItemKey"
onClick={handleEnableExportTimelineDownloader}
>
{i18n.EXPORT_SELECTED}
</EuiContextMenuItem>,
<EuiContextMenuItem
data-test-subj="delete-timeline-action"
disabled={disabled}
icon="trash"
key="DeleteItemKey"
onClick={handleOnOpenDeleteTimelineModal}
>
{i18n.DELETE_SELECTED}
</EuiContextMenuItem>,
]}
/>
<EuiContextMenuPanel items={items} />
</>
);
},
Expand Down
Expand Up @@ -29,6 +29,7 @@ import { TimelineTabsStyle } from './types';
import type { UseTimelineTypesArgs, UseTimelineTypesResult } from './use_timeline_types';
import { useTimelineTypes } from './use_timeline_types';
import { deleteTimelinesByIds } from '../../containers/api';
import { useUserPrivileges } from '../../../common/components/user_privileges';

jest.mock('react-router-dom', () => {
const originalModule = jest.requireActual('react-router-dom');
Expand Down Expand Up @@ -86,6 +87,9 @@ jest.mock('../../containers/api', () => ({
deleteTimelinesByIds: jest.fn(),
}));

jest.mock('../../../common/components/user_privileges');
const useUserPrivilegesMock = useUserPrivileges as jest.Mock;

describe('StatefulOpenTimeline', () => {
const title = 'All Timelines / Open Timelines';
let mockHistory: History[];
Expand All @@ -96,6 +100,9 @@ describe('StatefulOpenTimeline', () => {
tabName: TimelineType.default,
pageName: SecurityPageName.timelines,
});
useUserPrivilegesMock.mockReturnValue({
kibanaSecuritySolutionsPrivileges: { crud: true, read: true },
});
mockHistory = [];
(useHistory as jest.Mock).mockReturnValue(mockHistory);
(useGetAllTimeline as unknown as jest.Mock).mockReturnValue({
Expand Down
Expand Up @@ -20,6 +20,7 @@ import { OpenTimeline } from './open_timeline';
import { DEFAULT_SORT_DIRECTION, DEFAULT_SORT_FIELD } from './constants';
import { TimelineType, TimelineStatus } from '../../../../common/types/timeline';
import { getMockTheme } from '../../../common/lib/kibana/kibana_react.mock';
import { useUserPrivileges } from '../../../common/components/user_privileges';

jest.mock('../../../common/lib/kibana');

Expand All @@ -42,6 +43,9 @@ const mockTheme = getMockTheme({
},
});

jest.mock('../../../common/components/user_privileges');
const useUserPrivilegesMock = useUserPrivileges as jest.Mock;

describe('OpenTimeline', () => {
const title = 'All Timelines / Open Timelines';

Expand Down Expand Up @@ -102,6 +106,9 @@ describe('OpenTimeline', () => {
});

test('it shows the delete action columns when onDeleteSelected and deleteTimelines are specified', () => {
useUserPrivilegesMock.mockReturnValue({
kibanaSecuritySolutionsPrivileges: { crud: true, read: true },
});
const defaultProps = getDefaultTestProps(mockResults);
const wrapper = mountWithIntl(
<ThemeProvider theme={mockTheme}>
Expand Down Expand Up @@ -177,6 +184,29 @@ describe('OpenTimeline', () => {
expect(props.actionTimelineToShow).not.toContain('delete');
});

test('it does NOT show the delete action when user has read only access', () => {
const defaultProps = {
...getDefaultTestProps(mockResults),
onDeleteSelected: undefined,
deleteTimelines: undefined,
};
useUserPrivilegesMock.mockReturnValue({
kibanaSecuritySolutionsPrivileges: { crud: false, read: true },
});
const wrapper = mountWithIntl(
<ThemeProvider theme={mockTheme}>
<OpenTimeline {...defaultProps} />
</ThemeProvider>
);

const props = wrapper
.find('[data-test-subj="timelines-table"]')
.first()
.props() as TimelinesTableProps;

expect(props.actionTimelineToShow).not.toContain('delete');
});

test('it renders an empty string when the query is an empty string', () => {
const defaultProps = {
...getDefaultTestProps(mockResults),
Expand Down Expand Up @@ -324,6 +354,9 @@ describe('OpenTimeline', () => {
});

test('it should disable delete timeline if no timeline is selected', async () => {
useUserPrivilegesMock.mockReturnValue({
kibanaSecuritySolutionsPrivileges: { crud: true, read: true },
});
const defaultProps = {
...getDefaultTestProps(mockResults),
timelineStatus: null,
Expand Down Expand Up @@ -372,6 +405,9 @@ describe('OpenTimeline', () => {
});

test('it should enable delete timeline if a timeline is selected', async () => {
useUserPrivilegesMock.mockReturnValue({
kibanaSecuritySolutionsPrivileges: { crud: true, read: true },
});
const defaultProps = {
...getDefaultTestProps(mockResults),
timelineStatus: null,
Expand All @@ -396,6 +432,9 @@ describe('OpenTimeline', () => {
});

test("it should render a selectable timeline table if timelineStatus is active (selecting custom templates' tab)", () => {
useUserPrivilegesMock.mockReturnValue({
kibanaSecuritySolutionsPrivileges: { crud: true, read: true },
});
const defaultProps = {
...getDefaultTestProps(mockResults),
timelineStatus: TimelineStatus.active,
Expand All @@ -412,6 +451,9 @@ describe('OpenTimeline', () => {
});

test('it should include createRule in timeline actions if onCreateRule is passed', () => {
useUserPrivilegesMock.mockReturnValue({
kibanaSecuritySolutionsPrivileges: { crud: true, read: true },
});
const defaultProps = {
...getDefaultTestProps(mockResults),
timelineStatus: TimelineStatus.active,
Expand All @@ -427,6 +469,25 @@ describe('OpenTimeline', () => {
).toEqual(['createFrom', 'duplicate', 'createRule', 'export', 'selectable', 'delete']);
});

test('it should NOT include createFrom, duplicate, createRule, delete in timeline actions when user has read only access', () => {
const defaultProps = {
...getDefaultTestProps(mockResults),
timelineStatus: TimelineStatus.active,
};
useUserPrivilegesMock.mockReturnValue({
kibanaSecuritySolutionsPrivileges: { crud: false, read: true },
});
const wrapper = mountWithIntl(
<ThemeProvider theme={mockTheme}>
<OpenTimeline {...defaultProps} onCreateRule={jest.fn()} />
</ThemeProvider>
);

expect(
wrapper.find('[data-test-subj="timelines-table"]').first().prop('actionTimelineToShow')
).toEqual(['export', 'selectable']);
});

test("it should render selected count if timelineStatus is active (selecting custom templates' tab)", () => {
const defaultProps = {
...getDefaultTestProps(mockResults),
Expand Down Expand Up @@ -456,6 +517,9 @@ describe('OpenTimeline', () => {
});

test("it should not render a selectable timeline table if timelineStatus is immutable (selecting Elastic templates' tab)", () => {
useUserPrivilegesMock.mockReturnValue({
kibanaSecuritySolutionsPrivileges: { crud: true, read: true },
});
const defaultProps = {
...getDefaultTestProps(mockResults),
timelineStatus: TimelineStatus.immutable,
Expand Down Expand Up @@ -500,6 +564,9 @@ describe('OpenTimeline', () => {
});

test("it should render a selectable timeline table if timelineStatus is null (no template timelines' tab selected)", () => {
useUserPrivilegesMock.mockReturnValue({
kibanaSecuritySolutionsPrivileges: { crud: true, read: true },
});
const defaultProps = {
...getDefaultTestProps(mockResults),
timelineStatus: null,
Expand All @@ -515,6 +582,25 @@ describe('OpenTimeline', () => {
).toEqual(['createFrom', 'duplicate', 'export', 'selectable', 'delete']);
});

test("it should render a selectable timeline table if timelineStatus is null (no template timelines' tab selected) and user has read only access", () => {
const defaultProps = {
...getDefaultTestProps(mockResults),
timelineStatus: null,
};
useUserPrivilegesMock.mockReturnValue({
kibanaSecuritySolutionsPrivileges: { crud: false, read: true },
});
const wrapper = mountWithIntl(
<ThemeProvider theme={mockTheme}>
<OpenTimeline {...defaultProps} />
</ThemeProvider>
);

expect(
wrapper.find('[data-test-subj="timelines-table"]').first().prop('actionTimelineToShow')
).toEqual(['export', 'selectable']);
});

test("it should render selected count if timelineStatus is null (no template timelines' tab selected)", () => {
const defaultProps = {
...getDefaultTestProps(mockResults),
Expand Down
Expand Up @@ -21,7 +21,7 @@ import {
} from '../../../common/components/utility_bar';

import { importTimelines } from '../../containers/api';

import { useUserPrivileges } from '../../../common/components/user_privileges';
import { useEditTimelineBatchActions } from './edit_timeline_batch_actions';
import { useEditTimelineActions } from './edit_timeline_actions';
import { EditTimelineActions } from './export_timeline';
Expand Down Expand Up @@ -78,8 +78,9 @@ export const OpenTimeline = React.memo<OpenTimelineProps>(
onCompleteEditTimelineAction,
} = useEditTimelineActions();

const { kibanaSecuritySolutionsPrivileges } = useUserPrivileges();
const { getBatchItemsPopoverContent } = useEditTimelineBatchActions({
deleteTimelines,
deleteTimelines: kibanaSecuritySolutionsPrivileges.crud ? deleteTimelines : undefined,
selectedItems,
tableRef,
timelineType,
Expand Down Expand Up @@ -149,28 +150,41 @@ export const OpenTimeline = React.memo<OpenTimelineProps>(
}, [setImportDataModalToggle, refetch]);

const actionTimelineToShow = useMemo<ActionTimelineToShow[]>(() => {
const createRule: ActionTimelineToShow[] = ['createRule'];
const timelineActions: ActionTimelineToShow[] = [
'createFrom',
'duplicate',
...(onCreateRule != null ? createRule : []),
];
if (kibanaSecuritySolutionsPrivileges.crud) {
const createRule: ActionTimelineToShow[] = ['createRule'];
const timelineActions: ActionTimelineToShow[] = [
'createFrom',
'duplicate',
...(onCreateRule != null ? createRule : []),
];

if (timelineStatus !== TimelineStatus.immutable) {
timelineActions.push('export');
timelineActions.push('selectable');
}
if (timelineStatus !== TimelineStatus.immutable) {
timelineActions.push('export');
timelineActions.push('selectable');
}

if (
onDeleteSelected != null &&
deleteTimelines != null &&
timelineStatus !== TimelineStatus.immutable
) {
timelineActions.push('delete');
}
if (
onDeleteSelected != null &&
deleteTimelines != null &&
timelineStatus !== TimelineStatus.immutable
) {
timelineActions.push('delete');
}

return timelineActions;
}, [onCreateRule, timelineStatus, onDeleteSelected, deleteTimelines]);
return timelineActions;
}
// user with read access should only see export
if (timelineStatus !== TimelineStatus.immutable) {
return ['export', 'selectable'];
}
return [];
}, [
onCreateRule,
timelineStatus,
onDeleteSelected,
deleteTimelines,
kibanaSecuritySolutionsPrivileges,
]);

const SearchRowContent = useMemo(() => <>{templateTimelineFilter}</>, [templateTimelineFilter]);

Expand Down
Expand Up @@ -149,10 +149,9 @@ export const getActionsColumns = ({
'data-test-subj': 'create-rule-from-timeline',
available: () => actionTimelineToShow.includes('createRule') && onCreateRule != null,
};

return [
{
width: '80px',
width: '150px',
christineweng marked this conversation as resolved.
Show resolved Hide resolved
actions: [
createTimelineFromTemplate,
createTemplateFromTimeline,
Expand Down