Skip to content

Commit

Permalink
[8.6] [Security Solution][Bug] Add privilege check in open timeline (#…
Browse files Browse the repository at this point in the history
…147964) (#148587)

# Backport

This will backport the following commits from `main` to `8.6`:
- [[Security Solution][Bug] Add privilege check in open timeline
(#147964)](#147964)

<!--- Backport version: 8.9.4 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT
[{"author":{"name":"christineweng","email":"18648970+christineweng@users.noreply.github.com"},"sourceCommit":{"committedDate":"2023-01-03T19:21:48Z","message":"[Security
Solution][Bug] Add privilege check in open timeline (#147964)\n\n##
Summary\r\n\r\nThis PR contains fixe
for\r\nhttps://github.com//issues/147544. On Timelines
page, a\r\nKibana read-only user was able to see and click on options to
create and\r\nduplicate timelines. This PR fixes this bug by checking
user privilege\r\n(have crud access) before showing timeline
actions.\r\n\r\n## After: \r\nUser with read only access to kibana
security solutions can: \r\n1) select timelines\r\n2) export
timelines\r\n3) export timelines in bulk\r\n\r\nUser with crud access to
kibana security solutions can: \r\n1) select timelines\r\n2) have the
options to modify timelines as before\r\n3) bulk actions include delete
timelines and export timelines\r\n4) see and click on 'import', ' Create
new timeline', 'Create new\r\ntimeline template' buttons\r\n\r\n### User
with read access but not crud access\r\n- Have access to export ('Export
selected'), cannot see 'Create new\r\ntimeline'
buttons\r\n\r\n![image](https://user-images.githubusercontent.com/18648970/209210913-0554bc4c-5c8e-45ae-8e27-54a7e33e3f8e.png)\r\n\r\n-
'Export selected' in bulk
actions\r\n\r\n![image](https://user-images.githubusercontent.com/18648970/209210992-f102d8d4-479f-4d0a-84c2-125cc754c5ce.png)\r\n\r\n\r\n![image](https://user-images.githubusercontent.com/18648970/209021998-fbe0b63d-8dfd-4098-9774-7423899a45e1.png)\r\n\r\n###
User with full
access\r\n\r\n![image](https://user-images.githubusercontent.com/18648970/209209755-b5e5ce2b-0af9-42c6-b1cc-64a2675bf19d.png)\r\n\r\n-
'Export selected' and 'Delete selected' available in bulk
actions\r\ndropdown\r\n\r\n![image](https://user-images.githubusercontent.com/18648970/210408773-0fc5b100-0f57-4526-9c8f-0aba1f1d0e76.png)\r\n\r\n###
Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\nCo-authored-by: Kibana Machine
<42973632+kibanamachine@users.noreply.github.com>","sha":"3abf705b10926d3c6221504dd5575b97d15c9a31","branchLabelMapping":{"^v8.7.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","backport
missing","Team:Threat Hunting","Team:
SecuritySolution","Feature:Timeline","Team:Threat
Hunting:Investigations","v8.6.0","v8.7.0"],"number":147964,"url":"#147964
Solution][Bug] Add privilege check in open timeline (#147964)\n\n##
Summary\r\n\r\nThis PR contains fixe
for\r\nhttps://github.com//issues/147544. On Timelines
page, a\r\nKibana read-only user was able to see and click on options to
create and\r\nduplicate timelines. This PR fixes this bug by checking
user privilege\r\n(have crud access) before showing timeline
actions.\r\n\r\n## After: \r\nUser with read only access to kibana
security solutions can: \r\n1) select timelines\r\n2) export
timelines\r\n3) export timelines in bulk\r\n\r\nUser with crud access to
kibana security solutions can: \r\n1) select timelines\r\n2) have the
options to modify timelines as before\r\n3) bulk actions include delete
timelines and export timelines\r\n4) see and click on 'import', ' Create
new timeline', 'Create new\r\ntimeline template' buttons\r\n\r\n### User
with read access but not crud access\r\n- Have access to export ('Export
selected'), cannot see 'Create new\r\ntimeline'
buttons\r\n\r\n![image](https://user-images.githubusercontent.com/18648970/209210913-0554bc4c-5c8e-45ae-8e27-54a7e33e3f8e.png)\r\n\r\n-
'Export selected' in bulk
actions\r\n\r\n![image](https://user-images.githubusercontent.com/18648970/209210992-f102d8d4-479f-4d0a-84c2-125cc754c5ce.png)\r\n\r\n\r\n![image](https://user-images.githubusercontent.com/18648970/209021998-fbe0b63d-8dfd-4098-9774-7423899a45e1.png)\r\n\r\n###
User with full
access\r\n\r\n![image](https://user-images.githubusercontent.com/18648970/209209755-b5e5ce2b-0af9-42c6-b1cc-64a2675bf19d.png)\r\n\r\n-
'Export selected' and 'Delete selected' available in bulk
actions\r\ndropdown\r\n\r\n![image](https://user-images.githubusercontent.com/18648970/210408773-0fc5b100-0f57-4526-9c8f-0aba1f1d0e76.png)\r\n\r\n###
Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\nCo-authored-by: Kibana Machine
<42973632+kibanamachine@users.noreply.github.com>","sha":"3abf705b10926d3c6221504dd5575b97d15c9a31"}},"sourceBranch":"main","suggestedTargetBranches":["8.6"],"targetPullRequestStates":[{"branch":"8.6","label":"v8.6.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.7.0","labelRegex":"^v8.7.0$","isSourceBranch":true,"state":"MERGED","url":"#147964
Solution][Bug] Add privilege check in open timeline (#147964)\n\n##
Summary\r\n\r\nThis PR contains fixe
for\r\nhttps://github.com//issues/147544. On Timelines
page, a\r\nKibana read-only user was able to see and click on options to
create and\r\nduplicate timelines. This PR fixes this bug by checking
user privilege\r\n(have crud access) before showing timeline
actions.\r\n\r\n## After: \r\nUser with read only access to kibana
security solutions can: \r\n1) select timelines\r\n2) export
timelines\r\n3) export timelines in bulk\r\n\r\nUser with crud access to
kibana security solutions can: \r\n1) select timelines\r\n2) have the
options to modify timelines as before\r\n3) bulk actions include delete
timelines and export timelines\r\n4) see and click on 'import', ' Create
new timeline', 'Create new\r\ntimeline template' buttons\r\n\r\n### User
with read access but not crud access\r\n- Have access to export ('Export
selected'), cannot see 'Create new\r\ntimeline'
buttons\r\n\r\n![image](https://user-images.githubusercontent.com/18648970/209210913-0554bc4c-5c8e-45ae-8e27-54a7e33e3f8e.png)\r\n\r\n-
'Export selected' in bulk
actions\r\n\r\n![image](https://user-images.githubusercontent.com/18648970/209210992-f102d8d4-479f-4d0a-84c2-125cc754c5ce.png)\r\n\r\n\r\n![image](https://user-images.githubusercontent.com/18648970/209021998-fbe0b63d-8dfd-4098-9774-7423899a45e1.png)\r\n\r\n###
User with full
access\r\n\r\n![image](https://user-images.githubusercontent.com/18648970/209209755-b5e5ce2b-0af9-42c6-b1cc-64a2675bf19d.png)\r\n\r\n-
'Export selected' and 'Delete selected' available in bulk
actions\r\ndropdown\r\n\r\n![image](https://user-images.githubusercontent.com/18648970/210408773-0fc5b100-0f57-4526-9c8f-0aba1f1d0e76.png)\r\n\r\n###
Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\nCo-authored-by: Kibana Machine
<42973632+kibanamachine@users.noreply.github.com>","sha":"3abf705b10926d3c6221504dd5575b97d15c9a31"}}]}]
BACKPORT-->

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
christineweng and kibanamachine committed Jan 9, 2023
1 parent 8debf86 commit d6a4111
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 64 deletions.
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 @@ -78,6 +79,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 @@ -88,6 +92,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 @@ -411,6 +450,25 @@ describe('OpenTimeline', () => {
).toEqual(['createFrom', 'duplicate', 'export', 'selectable', 'delete']);
});

test('it should NOT include createFrom, duplicate, 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} />
</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 @@ -440,6 +498,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 @@ -484,6 +545,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 @@ -499,6 +563,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 @@ -77,8 +77,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 @@ -148,23 +149,30 @@ export const OpenTimeline = React.memo<OpenTimelineProps>(
}, [setImportDataModalToggle, refetch]);

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

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;
}, [onDeleteSelected, deleteTimelines, timelineStatus]);
return timelineActions;
}
// user with read access should only see export
if (timelineStatus !== TimelineStatus.immutable) {
return ['export', 'selectable'];
}
return [];
}, [timelineStatus, onDeleteSelected, deleteTimelines, kibanaSecuritySolutionsPrivileges]);

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

Expand Down
Expand Up @@ -26,12 +26,14 @@ export const getActionsColumns = ({
enableExportTimelineDownloader,
onOpenDeleteTimelineModal,
onOpenTimeline,
hasCrudAccess,
}: {
actionTimelineToShow: ActionTimelineToShow[];
deleteTimelines?: DeleteTimelines;
enableExportTimelineDownloader?: EnableExportTimelineDownloader;
onOpenDeleteTimelineModal?: OnOpenDeleteTimelineModal;
onOpenTimeline: OnOpenTimeline;
hasCrudAccess: boolean;
}): [TimelineActionsOverflowColumns] => {
const createTimelineFromTemplate = {
name: i18n.CREATE_TIMELINE_FROM_TEMPLATE,
Expand Down Expand Up @@ -134,7 +136,7 @@ export const getActionsColumns = ({

return [
{
width: '80px',
width: hasCrudAccess ? '80px' : '150px',
actions: [
createTimelineFromTemplate,
createTemplateFromTimeline,
Expand Down
Expand Up @@ -27,7 +27,7 @@ import { getExtendedColumns } from './extended_columns';
import { getIconHeaderColumns } from './icon_header_columns';
import type { TimelineTypeLiteralWithNull } from '../../../../../common/types/timeline';
import { TimelineStatus, TimelineType } from '../../../../../common/types/timeline';

import { useUserPrivileges } from '../../../../common/components/user_privileges';
// there are a number of type mismatches across this file
const EuiBasicTable: any = _EuiBasicTable; // eslint-disable-line @typescript-eslint/no-explicit-any

Expand Down Expand Up @@ -59,6 +59,7 @@ export const getTimelinesTableColumns = ({
onToggleShowNotes,
showExtendedColumns,
timelineType,
hasCrudAccess,
}: {
actionTimelineToShow: ActionTimelineToShow[];
deleteTimelines?: DeleteTimelines;
Expand All @@ -70,6 +71,7 @@ export const getTimelinesTableColumns = ({
onToggleShowNotes: OnToggleShowNotes;
showExtendedColumns: boolean;
timelineType: TimelineTypeLiteralWithNull;
hasCrudAccess: boolean;
}) => {
return [
...getCommonColumns({
Expand All @@ -87,6 +89,7 @@ export const getTimelinesTableColumns = ({
enableExportTimelineDownloader,
onOpenDeleteTimelineModal,
onOpenTimeline,
hasCrudAccess,
})
: []),
];
Expand Down Expand Up @@ -170,7 +173,7 @@ export const TimelinesTable = React.memo<TimelinesTableProps>(
onSelectionChange,
};
const basicTableProps = tableRef != null ? { ref: tableRef } : {};

const { kibanaSecuritySolutionsPrivileges } = useUserPrivileges();
const columns = useMemo(
() =>
getTimelinesTableColumns({
Expand All @@ -184,6 +187,7 @@ export const TimelinesTable = React.memo<TimelinesTableProps>(
onToggleShowNotes,
showExtendedColumns,
timelineType,
hasCrudAccess: kibanaSecuritySolutionsPrivileges.crud,
}),
[
actionTimelineToShow,
Expand All @@ -196,6 +200,7 @@ export const TimelinesTable = React.memo<TimelinesTableProps>(
onToggleShowNotes,
showExtendedColumns,
timelineType,
kibanaSecuritySolutionsPrivileges,
]
);

Expand Down

0 comments on commit d6a4111

Please sign in to comment.