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] User can add new event filter from event filter list #98118

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for fixing this!

paul-tavares marked this conversation as resolved.
Show resolved Hide resolved
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 = (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 I wonder if we can create a generic utility that could work for any use case in parsing/normalizing url params, so that we did not have to keep cloning this code. maybe a utility that takes in the defaults ++ the actual (aka: location) value and returns back the location type of information. Likely would be a TS generic function so that it can work across use cases.

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 {
paul-tavares marked this conversation as resolved.
Show resolved Hide resolved
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