[SIEM] Allow Import timeline for authorised users (#61438) (#61528)

* allow users importing data if they are authorized

* rename props

* rename types

* hide import timeline btn if unauthorized
This commit is contained in:
Angela Chuang 2020-03-27 07:29:51 +00:00 committed by GitHub
parent 3486d44286
commit b863c0fb80
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 115 additions and 38 deletions

View file

@ -21,7 +21,7 @@ import {
} from '@elastic/eui';
import React, { useCallback, useState } from 'react';
import { ImportRulesResponse, ImportRulesProps } from '../../containers/detection_engine/rules';
import { ImportDataResponse, ImportDataProps } from '../../containers/detection_engine/rules';
import {
displayErrorToast,
displaySuccessToast,
@ -37,7 +37,7 @@ interface ImportDataModalProps {
errorMessage: string;
failedDetailed: (id: string, statusCode: number, message: string) => string;
importComplete: () => void;
importData: (arg: ImportRulesProps) => Promise<ImportRulesResponse>;
importData: (arg: ImportDataProps) => Promise<ImportDataResponse>;
showCheckBox: boolean;
showModal: boolean;
submitBtnText: string;
@ -75,7 +75,7 @@ export const ImportDataModalComponent = ({
closeModal();
}, [setIsImporting, setSelectedFiles, closeModal]);
const importRulesCallback = useCallback(async () => {
const importDataCallback = useCallback(async () => {
if (selectedFiles != null) {
setIsImporting(true);
const abortCtrl = new AbortController();
@ -152,7 +152,7 @@ export const ImportDataModalComponent = ({
<EuiModalFooter>
<EuiButtonEmpty onClick={handleCloseModal}>{i18n.CANCEL_BUTTON}</EuiButtonEmpty>
<EuiButton
onClick={importRulesCallback}
onClick={importDataCallback}
disabled={selectedFiles == null || isImporting}
fill
>

View file

@ -54,7 +54,7 @@ interface OwnProps<TCache = object> {
export type OpenTimelineOwnProps = OwnProps &
Pick<
OpenTimelineProps,
'defaultPageSize' | 'title' | 'importCompleteToggle' | 'setImportCompleteToggle'
'defaultPageSize' | 'title' | 'importDataModalToggle' | 'setImportDataModalToggle'
> &
PropsFromRedux;
@ -77,9 +77,9 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
defaultPageSize,
hideActions = [],
isModal = false,
importCompleteToggle,
importDataModalToggle,
onOpenTimeline,
setImportCompleteToggle,
setImportDataModalToggle,
timeline,
title,
updateTimeline,
@ -269,7 +269,7 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
defaultPageSize={defaultPageSize}
isLoading={loading}
itemIdToExpandedNotesRowMap={itemIdToExpandedNotesRowMap}
importCompleteToggle={importCompleteToggle}
importDataModalToggle={importDataModalToggle}
onAddTimelinesToFavorites={undefined}
onDeleteSelected={onDeleteSelected}
onlyFavorites={onlyFavorites}
@ -284,7 +284,7 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
query={search}
refetch={refetch}
searchResults={timelines}
setImportCompleteToggle={setImportCompleteToggle}
setImportDataModalToggle={setImportDataModalToggle}
selectedItems={selectedItems}
sortDirection={sortDirection}
sortField={sortField}

View file

@ -33,7 +33,7 @@ export const OpenTimeline = React.memo<OpenTimelineProps>(
defaultPageSize,
isLoading,
itemIdToExpandedNotesRowMap,
importCompleteToggle,
importDataModalToggle,
onAddTimelinesToFavorites,
onDeleteSelected,
onlyFavorites,
@ -50,7 +50,7 @@ export const OpenTimeline = React.memo<OpenTimelineProps>(
searchResults,
selectedItems,
sortDirection,
setImportCompleteToggle,
setImportDataModalToggle,
sortField,
title,
totalSearchResultsCount,
@ -103,18 +103,18 @@ export const OpenTimeline = React.memo<OpenTimelineProps>(
}, [refetch]);
const handleCloseModal = useCallback(() => {
if (setImportCompleteToggle != null) {
setImportCompleteToggle(false);
if (setImportDataModalToggle != null) {
setImportDataModalToggle(false);
}
}, [setImportCompleteToggle]);
}, [setImportDataModalToggle]);
const handleComplete = useCallback(() => {
if (setImportCompleteToggle != null) {
setImportCompleteToggle(false);
if (setImportDataModalToggle != null) {
setImportDataModalToggle(false);
}
if (refetch != null) {
refetch();
}
}, [setImportCompleteToggle, refetch]);
}, [setImportDataModalToggle, refetch]);
return (
<>
@ -136,7 +136,7 @@ export const OpenTimeline = React.memo<OpenTimelineProps>(
importData={importTimelines}
successMessage={i18n.SUCCESSFULLY_IMPORTED_TIMELINES}
showCheckBox={false}
showModal={importCompleteToggle ?? false}
showModal={importDataModalToggle ?? false}
submitBtnText={i18n.IMPORT_TIMELINE_BTN_TITLE}
subtitle={i18n.INITIAL_PROMPT_TEXT}
title={i18n.IMPORT_TIMELINE}

View file

@ -156,4 +156,72 @@ describe('#getActionsColumns', () => {
expect(onOpenTimeline).toBeCalledWith({ duplicate: true, timelineId: 'saved-timeline-11' });
});
test('it renders the export icon when enableExportTimelineDownloader is including the action export', () => {
const testProps: TimelinesTableProps = {
...getMockTimelinesTableProps(mockResults),
actionTimelineToShow: ['export'],
};
const wrapper = mountWithIntl(
<ThemeProvider theme={theme}>
<TimelinesTable {...testProps} />
</ThemeProvider>
);
expect(wrapper.find('[data-test-subj="export-timeline"]').exists()).toBe(true);
});
test('it renders No export icon when export is not included in the action ', () => {
const testProps: TimelinesTableProps = {
...getMockTimelinesTableProps(mockResults),
};
const wrapper = mountWithIntl(
<ThemeProvider theme={theme}>
<TimelinesTable {...testProps} />
</ThemeProvider>
);
expect(wrapper.find('[data-test-subj="export-timeline"]').exists()).toBe(false);
});
test('it renders a disabled the export button if the timeline does not have a saved object id', () => {
const missingSavedObjectId: OpenTimelineResult[] = [
omit('savedObjectId', { ...mockResults[0] }),
];
const testProps: TimelinesTableProps = {
...getMockTimelinesTableProps(missingSavedObjectId),
actionTimelineToShow: ['export'],
};
const wrapper = mountWithIntl(
<ThemeProvider theme={theme}>
<TimelinesTable {...testProps} />
</ThemeProvider>
);
const props = wrapper
.find('[data-test-subj="export-timeline"]')
.first()
.props() as EuiButtonIconProps;
expect(props.isDisabled).toBe(true);
});
test('it invokes enableExportTimelineDownloader with the expected params when the button is clicked', () => {
const enableExportTimelineDownloader = jest.fn();
const testProps: TimelinesTableProps = {
...getMockTimelinesTableProps(mockResults),
actionTimelineToShow: ['export'],
enableExportTimelineDownloader,
};
const wrapper = mountWithIntl(
<ThemeProvider theme={theme}>
<TimelinesTable {...testProps} />
</ThemeProvider>
);
wrapper
.find('[data-test-subj="export-timeline"]')
.first()
.simulate('click');
expect(enableExportTimelineDownloader).toBeCalledWith(mockResults[0]);
});
});

View file

@ -55,6 +55,7 @@ export const getActionsColumns = ({
},
enabled: ({ savedObjectId }: OpenTimelineResult) => savedObjectId != null,
description: i18n.EXPORT_SELECTED,
'data-test-subj': 'export-timeline',
};
const deleteTimelineColumn = {

View file

@ -121,7 +121,7 @@ export interface OpenTimelineProps {
/** Required by EuiTable for expandable rows: a map of `TimelineResult.savedObjectId` to rendered notes */
itemIdToExpandedNotesRowMap: Record<string, JSX.Element>;
/** Display import timelines modal*/
importCompleteToggle?: boolean;
importDataModalToggle?: boolean;
/** If this callback is specified, a "Favorite Selected" button will be displayed, and this callback will be invoked when the button is clicked */
onAddTimelinesToFavorites?: OnAddTimelinesToFavorites;
/** If this callback is specified, a "Delete Selected" button will be displayed, and this callback will be invoked when the button is clicked */
@ -153,7 +153,7 @@ export interface OpenTimelineProps {
/** the currently-selected timelines in the table */
selectedItems: OpenTimelineResult[];
/** Toggle export timelines modal*/
setImportCompleteToggle?: React.Dispatch<React.SetStateAction<boolean>>;
setImportDataModalToggle?: React.Dispatch<React.SetStateAction<boolean>>;
/** the requested sort direction of the query results */
sortDirection: 'asc' | 'desc';
/** the requested field to sort on */

View file

@ -15,10 +15,10 @@ import {
Rule,
FetchRuleProps,
BasicFetchProps,
ImportRulesProps,
ImportDataProps,
ExportDocumentsProps,
RuleStatusResponse,
ImportRulesResponse,
ImportDataResponse,
PrePackagedRulesStatusResponse,
BulkRuleResponse,
} from './types';
@ -204,11 +204,11 @@ export const importRules = async ({
fileToImport,
overwrite = false,
signal,
}: ImportRulesProps): Promise<ImportRulesResponse> => {
}: ImportDataProps): Promise<ImportDataResponse> => {
const formData = new FormData();
formData.append('file', fileToImport);
return KibanaServices.get().http.fetch<ImportRulesResponse>(
return KibanaServices.get().http.fetch<ImportDataResponse>(
`${DETECTION_ENGINE_RULES_URL}/_import`,
{
method: 'POST',

View file

@ -194,7 +194,7 @@ export interface BasicFetchProps {
signal: AbortSignal;
}
export interface ImportRulesProps {
export interface ImportDataProps {
fileToImport: File;
overwrite?: boolean;
signal: AbortSignal;
@ -208,7 +208,7 @@ export interface ImportRulesResponseError {
};
}
export interface ImportRulesResponse {
export interface ImportDataResponse {
success: boolean;
success_count: number;
errors: ImportRulesResponseError[];

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { ImportRulesProps, ImportRulesResponse } from '../../detection_engine/rules';
import { ImportDataProps, ImportDataResponse } from '../../detection_engine/rules';
import { KibanaServices } from '../../../lib/kibana';
import { TIMELINE_IMPORT_URL, TIMELINE_EXPORT_URL } from '../../../../common/constants';
import { ExportSelectedData } from '../../../components/generic_downloader';
@ -13,11 +13,11 @@ export const importTimelines = async ({
fileToImport,
overwrite = false,
signal,
}: ImportRulesProps): Promise<ImportRulesResponse> => {
}: ImportDataProps): Promise<ImportDataResponse> => {
const formData = new FormData();
formData.append('file', fileToImport);
return KibanaServices.get().http.fetch<ImportRulesResponse>(`${TIMELINE_IMPORT_URL}`, {
return KibanaServices.get().http.fetch<ImportDataResponse>(`${TIMELINE_IMPORT_URL}`, {
method: 'POST',
headers: { 'Content-Type': undefined },
query: { overwrite },

View file

@ -14,6 +14,7 @@ import { StatefulOpenTimeline } from '../../components/open_timeline';
import { WrapperPage } from '../../components/wrapper_page';
import { SpyRoute } from '../../utils/route/spy_routes';
import * as i18n from './translations';
import { useKibana } from '../../lib/kibana';
const TimelinesContainer = styled.div`
width: 100%;
@ -28,17 +29,24 @@ type OwnProps = TimelinesProps;
export const DEFAULT_SEARCH_RESULTS_PER_PAGE = 10;
const TimelinesPageComponent: React.FC<OwnProps> = ({ apolloClient }) => {
const [importCompleteToggle, setImportCompleteToggle] = useState<boolean>(false);
const [importDataModalToggle, setImportDataModalToggle] = useState<boolean>(false);
const onImportTimelineBtnClick = useCallback(() => {
setImportCompleteToggle(true);
}, [setImportCompleteToggle]);
setImportDataModalToggle(true);
}, [setImportDataModalToggle]);
const uiCapabilities = useKibana().services.application.capabilities;
const capabilitiesCanUserCRUD: boolean =
typeof uiCapabilities.siem.crud === 'boolean' ? uiCapabilities.siem.crud : false;
return (
<>
<WrapperPage>
<HeaderPage border title={i18n.PAGE_TITLE}>
<EuiButton iconType="indexOpen" onClick={onImportTimelineBtnClick}>
{i18n.ALL_TIMELINES_IMPORT_TIMELINE_TITLE}
</EuiButton>
{capabilitiesCanUserCRUD && (
<EuiButton iconType="indexOpen" onClick={onImportTimelineBtnClick}>
{i18n.ALL_TIMELINES_IMPORT_TIMELINE_TITLE}
</EuiButton>
)}
</HeaderPage>
<TimelinesContainer>
@ -46,8 +54,8 @@ const TimelinesPageComponent: React.FC<OwnProps> = ({ apolloClient }) => {
apolloClient={apolloClient}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
isModal={false}
importCompleteToggle={importCompleteToggle}
setImportCompleteToggle={setImportCompleteToggle}
importDataModalToggle={importDataModalToggle && capabilitiesCanUserCRUD}
setImportDataModalToggle={setImportDataModalToggle}
title={i18n.ALL_TIMELINES_PANEL_TITLE}
/>
</TimelinesContainer>

View file

@ -175,7 +175,7 @@ export default function({ getService }: FtrProviderContext) {
expect(version).to.not.be.empty();
});
it.skip('Update a timeline with a new title', async () => {
it('Update a timeline with a new title', async () => {
const titleToSaved = 'hello title';
const response = await createBasicTimeline(client, titleToSaved);
const { savedObjectId, version } = response.data && response.data.persistTimeline.timeline;