Skip to content

Commit

Permalink
[Security Solution] User can add new event filter from event filter l…
Browse files Browse the repository at this point in the history
…ist (elastic#98118)

* New flyout with event filters form

* Changes on event filters form to allow OS selector. Add new error on state for OS. Add created entry to the entries list

* Fixes typo

* Adds empty page with an add button that opens flyout. Alos added route and path management

* Fixes type and adds a TODO comment. Also removes ESlit rule for useCallback deps

* Fixes unit test. Adds consts for default page size and page index

* Fixes warning state update on an unmounted component

* Fixes infinite useEffect loop useFetchIndex hook because non memoized value

* Adds policy:all to eventFilter.tag and disables or button on exception builder

* Changes component name and simplify hook using useCallback without a custom callback

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
dasansol92 and kibanamachine committed Apr 28, 2021
1 parent a8a8dc8 commit 11458aa
Show file tree
Hide file tree
Showing 19 changed files with 680 additions and 63 deletions.
87 changes: 48 additions & 39 deletions x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import moment from 'moment-timezone';

import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useState, useRef } from 'react';
import { i18n } from '@kbn/i18n';

import { DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../../../common/constants';
Expand Down Expand Up @@ -51,57 +51,66 @@ export interface AuthenticatedElasticUser {
}

export const useCurrentUser = (): AuthenticatedElasticUser | null => {
const isMounted = useRef(false);
const [user, setUser] = useState<AuthenticatedElasticUser | null>(null);

const [, dispatchToaster] = useStateToaster();

const { security } = useKibana().services;

const fetchUser = useCallback(() => {
let didCancel = false;
const fetchData = async () => {
try {
if (security != null) {
const response = await security.authc.getCurrentUser();
const fetchUser = useCallback(
() => {
let didCancel = false;
const fetchData = async () => {
try {
if (security != null) {
const response = await security.authc.getCurrentUser();
if (!isMounted.current) return;
if (!didCancel) {
setUser(convertToCamelCase<AuthenticatedUser, AuthenticatedElasticUser>(response));
}
} else {
setUser({
username: i18n.translate('xpack.securitySolution.getCurrentUser.unknownUser', {
defaultMessage: 'Unknown',
}),
email: '',
fullName: '',
roles: [],
enabled: false,
authenticationRealm: { name: '', type: '' },
lookupRealm: { name: '', type: '' },
authenticationProvider: '',
});
}
} catch (error) {
if (!didCancel) {
setUser(convertToCamelCase<AuthenticatedUser, AuthenticatedElasticUser>(response));
errorToToaster({
title: i18n.translate('xpack.securitySolution.getCurrentUser.Error', {
defaultMessage: 'Error getting user',
}),
error: error.body && error.body.message ? new Error(error.body.message) : error,
dispatchToaster,
});
setUser(null);
}
} else {
setUser({
username: i18n.translate('xpack.securitySolution.getCurrentUser.unknownUser', {
defaultMessage: 'Unknown',
}),
email: '',
fullName: '',
roles: [],
enabled: false,
authenticationRealm: { name: '', type: '' },
lookupRealm: { name: '', type: '' },
authenticationProvider: '',
});
}
} catch (error) {
if (!didCancel) {
errorToToaster({
title: i18n.translate('xpack.securitySolution.getCurrentUser.Error', {
defaultMessage: 'Error getting user',
}),
error: error.body && error.body.message ? new Error(error.body.message) : error,
dispatchToaster,
});
setUser(null);
}
}
};
fetchData();
return () => {
didCancel = true;
};
};
fetchData();
return () => {
didCancel = true;
};
},
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [security]);
[security]
);

useEffect(() => {
isMounted.current = true;
fetchUser();
return () => {
isMounted.current = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return user;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { AdministrationSubTab } from '../types';
import { appendSearch } from '../../common/components/link_to/helpers';
import { EndpointIndexUIQueryParams } from '../pages/endpoint_hosts/types';
import { TrustedAppsListPageLocation } from '../pages/trusted_apps/state';
import { EventFiltersPageLocation } from '../pages/event_filters/state';
import { EventFiltersListPageUrlSearchParams } from '../pages/event_filters/types';

// Taken from: https://github.com/microsoft/TypeScript/issues/12936#issuecomment-559034150
Expand Down Expand Up @@ -118,6 +119,26 @@ const normalizeTrustedAppsPageLocation = (
}
};

const normalizeEventFiltersPageLocation = (
location?: Partial<EventFiltersPageLocation>
): Partial<EventFiltersPageLocation> => {
if (location) {
return {
...(!isDefaultOrMissing(location.page_index, MANAGEMENT_DEFAULT_PAGE)
? { page_index: location.page_index }
: {}),
...(!isDefaultOrMissing(location.page_size, MANAGEMENT_DEFAULT_PAGE_SIZE)
? { page_size: location.page_size }
: {}),
...(!isDefaultOrMissing(location.show, undefined) ? { show: location.show } : {}),
...(!isDefaultOrMissing(location.id, undefined) ? { id: location.id } : {}),
...(!isDefaultOrMissing(location.filter, '') ? { filter: location.filter } : ''),
};
} else {
return {};
}
};

/**
* Given an object with url params, and a given key, return back only the first param value (case multiples were defined)
* @param query
Expand Down Expand Up @@ -181,12 +202,27 @@ export const getTrustedAppsListPath = (location?: Partial<TrustedAppsListPageLoc
)}`;
};

export const extractEventFiltetrsPageLocation = (
query: querystring.ParsedUrlQuery
): EventFiltersPageLocation => {
const showParamValue = extractFirstParamValue(query, 'show') as EventFiltersPageLocation['show'];

return {
...extractListPaginationParams(query),
show:
showParamValue && ['edit', 'create'].includes(showParamValue) ? showParamValue : undefined,
id: extractFirstParamValue(query, 'id'),
};
};

export const getEventFiltersListPath = (
location?: Partial<EventFiltersListPageUrlSearchParams>
): string => {
const path = generatePath(MANAGEMENT_ROUTING_EVENT_FILTERS_PATH, {
tabName: AdministrationSubTab.eventFilters,
});

return `${path}${appendSearch(querystring.stringify(location))}`;
return `${path}${appendSearch(
querystring.stringify(normalizeEventFiltersPageLocation(location))
)}`;
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,23 @@

import { ExceptionListItemSchema, CreateExceptionListItemSchema } from '../../../../shared_imports';
import { AsyncResourceState } from '../../../state/async_resource_state';

export interface EventFiltersPageLocation {
page_index: number;
page_size: number;
show?: 'create' | 'edit';
/** Used for editing. The ID of the selected event filter */
id?: string;
filter: string;
}
export interface EventFiltersListPageState {
entries: ExceptionListItemSchema[];
form: {
entry: CreateExceptionListItemSchema | ExceptionListItemSchema | undefined;
hasNameError: boolean;
hasItemsError: boolean;
hasOSError: boolean;
submissionResourceState: AsyncResourceState<ExceptionListItemSchema>;
};
location: EventFiltersPageLocation;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type EventFiltersChangeForm = Action<'eventFiltersChangeForm'> & {
entry: ExceptionListItemSchema | CreateExceptionListItemSchema;
hasNameError?: boolean;
hasItemsError?: boolean;
hasOSError?: boolean;
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,20 @@
*/

import { EventFiltersListPageState } from '../state';
import { MANAGEMENT_DEFAULT_PAGE, MANAGEMENT_DEFAULT_PAGE_SIZE } from '../../../common/constants';

export const initialEventFiltersPageState = (): EventFiltersListPageState => ({
entries: [],
form: {
entry: undefined,
hasNameError: false,
hasItemsError: false,
hasOSError: false,
submissionResourceState: { type: 'UninitialisedResourceState' },
},
location: {
page_index: MANAGEMENT_DEFAULT_PAGE,
page_size: MANAGEMENT_DEFAULT_PAGE_SIZE,
filter: '',
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ describe('middleware', () => {
await spyMiddleware.waitForAction('eventFiltersFormStateChanged');
expect(store.getState()).toStrictEqual({
...initialState,
entries: [createdEventFilterEntryMock()],
form: {
...store.getState().form,
submissionResourceState: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ const eventFiltersCreate = async (
const sanitizedEntry = transformNewItemOutput(formEntry as CreateExceptionListItemSchema);

const exception = await eventFiltersService.addEventFilters(sanitizedEntry);
store.dispatch({
type: 'eventFiltersCreateSuccess',
payload: {
exception,
},
});
store.dispatch({
type: 'eventFiltersFormStateChanged',
payload: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,52 @@ describe('reducer', () => {
},
});
});

it('create is success when there is no entry on entries list', () => {
const result = eventFiltersPageReducer(initialState, {
type: 'eventFiltersCreateSuccess',
payload: {
exception: createdEventFilterEntryMock(),
},
});

expect(result).toStrictEqual({
...initialState,
entries: [createdEventFilterEntryMock()],
});
});

it('create is success when there there are entries on entries list', () => {
const customizedInitialState = {
...initialState,
entries: [createdEventFilterEntryMock(), createdEventFilterEntryMock()],
};
const result = eventFiltersPageReducer(customizedInitialState, {
type: 'eventFiltersCreateSuccess',
payload: {
exception: { ...createdEventFilterEntryMock(), meta: {} },
},
});

expect(result.entries).toHaveLength(3);
expect(result.entries[0]!.meta).not.toBeUndefined();
});
});
describe('UserChangedUrl', () => {
it('receives a url change with show=create', () => {
const result = eventFiltersPageReducer(initialState, {
type: 'userChangedUrl',
payload: { search: '?show=create', pathname: '/event_filters', hash: '' },
});

expect(result).toStrictEqual({
...initialState,
location: {
...initialState.location,
id: undefined,
show: 'create',
},
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,21 @@
* 2.0.
*/

// eslint-disable-next-line import/no-nodejs-modules
import { parse } from 'querystring';
import { matchPath } from 'react-router-dom';
import { ImmutableReducer } from '../../../../common/store';
import { Immutable } from '../../../../../common/endpoint/types';
import { AppAction } from '../../../../common/store/actions';
import { AppLocation, Immutable } from '../../../../../common/endpoint/types';
import { UserChangedUrl } from '../../../../common/store/routing/action';
import { MANAGEMENT_ROUTING_EVENT_FILTERS_PATH } from '../../../common/constants';
import { extractEventFiltetrsPageLocation } from '../../../common/routing';

import {
EventFiltersInitForm,
EventFiltersChangeForm,
EventFiltersFormStateChanged,
EventFiltersCreateSuccess,
} from './action';

import { EventFiltersListPageState } from '../state';
Expand All @@ -24,13 +31,23 @@ type CaseReducer<T extends AppAction> = (
action: Immutable<T>
) => Immutable<EventFiltersListPageState>;

const isEventFiltersPageLocation = (location: Immutable<AppLocation>) => {
return (
matchPath(location.pathname ?? '', {
path: MANAGEMENT_ROUTING_EVENT_FILTERS_PATH,
exact: true,
}) !== null
);
};

const eventFiltersInitForm: CaseReducer<EventFiltersInitForm> = (state, action) => {
return {
...state,
form: {
...state.form,
entry: action.payload.entry,
hasNameError: !action.payload.entry.name,
hasOSError: !action.payload.entry.os_types?.length,
submissionResourceState: {
type: 'UninitialisedResourceState',
},
Expand All @@ -52,6 +69,8 @@ const eventFiltersChangeForm: CaseReducer<EventFiltersChangeForm> = (state, acti
action.payload.hasNameError !== undefined
? action.payload.hasNameError
: state.form.hasNameError,
hasOSError:
action.payload.hasOSError !== undefined ? action.payload.hasOSError : state.form.hasOSError,
},
};
};
Expand All @@ -66,6 +85,22 @@ const eventFiltersFormStateChanged: CaseReducer<EventFiltersFormStateChanged> =
};
};

const eventFiltersCreateSuccess: CaseReducer<EventFiltersCreateSuccess> = (state, action) => {
return {
...state,
entries: [action.payload.exception, ...state.entries],
};
};

const userChangedUrl: CaseReducer<UserChangedUrl> = (state, action) => {
if (isEventFiltersPageLocation(action.payload)) {
const location = extractEventFiltetrsPageLocation(parse(action.payload.search.slice(1)));
return { ...state, location };
} else {
return state;
}
};

export const eventFiltersPageReducer: StateReducer = (
state = initialEventFiltersPageState(),
action
Expand All @@ -77,6 +112,10 @@ export const eventFiltersPageReducer: StateReducer = (
return eventFiltersChangeForm(state, action);
case 'eventFiltersFormStateChanged':
return eventFiltersFormStateChanged(state, action);
case 'eventFiltersCreateSuccess':
return eventFiltersCreateSuccess(state, action);
case 'userChangedUrl':
return userChangedUrl(state, action);
}

return state;
Expand Down
Loading

0 comments on commit 11458aa

Please sign in to comment.