Skip to content

Commit

Permalink
[security solution][Endpoint] Refactor host isolation exceptions list…
Browse files Browse the repository at this point in the history
… page to now use `<ArtifactListPage/>` component (elastic#129099)

* Move Host Isolation Exceptions to `<ArtifactListPage>`
* New Generic FormatError component for formatting error Objects
* clean up un-used components ++ hooks
* EffectedPolicySelect support for `disabled` prop
* new ExceptionsList HTTP mocks
* additional test utilities for EffectedPolicySelect component
* New url routing utilities for Artifact List Page
* Refactor `useCanSeeHostIsolationExceptionsMenu()` hook to use generic `useSummaryArtifact()` hook
* add props to ArtifactListPage to allow for hiding actions like create, edit, delete
* Split out useUserPrivileges() mock implementation function for reuse and mock resets
* add generic `getFirstCard()` test utility to ArtifactListPage mocks
  • Loading branch information
paul-tavares authored and kertal committed May 24, 2022
1 parent a6f4ef4 commit f1a3edf
Show file tree
Hide file tree
Showing 53 changed files with 1,172 additions and 2,416 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
import { initialUserPrivilegesState, UserPrivilegesState } from '../user_privileges_context';
import { getEndpointPrivilegesInitialStateMock } from '../endpoint/mocks';

export const useUserPrivileges = jest.fn(() => {
export const getUserPrivilegesMockDefaultValue = () => {
const mockedPrivileges: UserPrivilegesState = {
...initialUserPrivilegesState(),
endpointPrivileges: getEndpointPrivilegesInitialStateMock(),
};

return mockedPrivileges;
});
};

export const useUserPrivileges = jest.fn(getUserPrivilegesMockDefaultValue);
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,20 @@
* 2.0.
*/

import React, { memo } from 'react';
import React, { memo, PropsWithChildren, useMemo } from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';

export const queryClient = new QueryClient();
export type ReactQueryClientProviderProps = PropsWithChildren<{
queryClient?: QueryClient;
}>;

export const ReactQueryClientProvider = memo(({ children }) => {
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
});
export const ReactQueryClientProvider = memo<ReactQueryClientProviderProps>(
({ queryClient, children }) => {
const client = useMemo(() => {
return queryClient || new QueryClient();
}, [queryClient]);
return <QueryClientProvider client={client}>{children}</QueryClientProvider>;
}
);

ReactQueryClientProvider.displayName = 'ReactQueryClientProvider';
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import { isEmpty } from 'lodash/fp';
// FIXME: Remove references to `querystring`
// eslint-disable-next-line import/no-nodejs-modules
import querystring from 'querystring';
import { generatePath } from 'react-router-dom';
Expand All @@ -14,26 +15,26 @@ import { ArtifactListPageUrlParams } from '../components/artifact_list_page';
import { paginationFromUrlParams } from '../components/hooks/use_url_pagination';
import { EndpointIndexUIQueryParams } from '../pages/endpoint_hosts/types';
import { EventFiltersPageLocation } from '../pages/event_filters/types';
import { HostIsolationExceptionsPageLocation } from '../pages/host_isolation_exceptions/types';
import { PolicyDetailsArtifactsPageLocation } from '../pages/policy/types';
import { TrustedAppsListPageLocation } from '../pages/trusted_apps/state';
import { AdministrationSubTab } from '../types';
import {
MANAGEMENT_DEFAULT_PAGE,
MANAGEMENT_DEFAULT_PAGE_SIZE,
MANAGEMENT_PAGE_SIZE_OPTIONS,
MANAGEMENT_ROUTING_BLOCKLIST_PATH,
MANAGEMENT_ROUTING_ENDPOINTS_PATH,
MANAGEMENT_ROUTING_EVENT_FILTERS_PATH,
MANAGEMENT_ROUTING_HOST_ISOLATION_EXCEPTIONS_PATH,
MANAGEMENT_ROUTING_POLICIES_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_BLOCKLISTS_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_EVENT_FILTERS_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_FORM_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_HOST_ISOLATION_EXCEPTIONS_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_EVENT_FILTERS_PATH,
MANAGEMENT_ROUTING_TRUSTED_APPS_PATH,
MANAGEMENT_ROUTING_BLOCKLIST_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_BLOCKLISTS_PATH,
} from './constants';
import { isDefaultOrMissing, getArtifactListPageUrlPath } from './url_routing';

// Taken from: https://github.com/microsoft/TypeScript/issues/12936#issuecomment-559034150
type ExactKeys<T1, T2> = Exclude<keyof T1, keyof T2> extends never ? T1 : never;
Expand Down Expand Up @@ -149,10 +150,6 @@ export const getPolicyEventFiltersPath = (
)}`;
};

const isDefaultOrMissing = <T>(value: T | undefined, defaultValue: T) => {
return value === undefined || value === defaultValue;
};

const normalizeTrustedAppsPageLocation = (
location?: Partial<TrustedAppsListPageLocation>
): Partial<TrustedAppsListPageLocation> => {
Expand Down Expand Up @@ -219,52 +216,6 @@ const normalizeEventFiltersPageLocation = (
}
};

const normalizBlocklistsPageLocation = (
location?: Partial<ArtifactListPageUrlParams>
): Partial<ArtifactListPageUrlParams> => {
if (location) {
return {
...(!isDefaultOrMissing(location.page, MANAGEMENT_DEFAULT_PAGE)
? { page: location.page }
: {}),
...(!isDefaultOrMissing(location.pageSize, MANAGEMENT_DEFAULT_PAGE_SIZE)
? { pageSize: location.pageSize }
: {}),
...(!isDefaultOrMissing(location.show, undefined) ? { show: location.show } : {}),
...(!isDefaultOrMissing(location.itemId, undefined) ? { id: location.itemId } : {}),
...(!isDefaultOrMissing(location.filter, '') ? { filter: location.filter } : ''),
...(!isDefaultOrMissing(location.includedPolicies, '')
? { includedPolicies: location.includedPolicies }
: ''),
};
} else {
return {};
}
};

const normalizeHostIsolationExceptionsPageLocation = (
location?: Partial<HostIsolationExceptionsPageLocation>
): 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 } : ''),
...(!isDefaultOrMissing(location.included_policies, '')
? { included_policies: location.included_policies }
: ''),
};
} 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 @@ -395,33 +346,14 @@ export const getEventFiltersListPath = (location?: Partial<EventFiltersPageLocat
)}`;
};

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

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

export const getHostIsolationExceptionsListPath = (
location?: Partial<HostIsolationExceptionsPageLocation>
location?: Partial<ArtifactListPageUrlParams>
): string => {
const path = generatePath(MANAGEMENT_ROUTING_HOST_ISOLATION_EXCEPTIONS_PATH, {
tabName: AdministrationSubTab.hostIsolationExceptions,
});

return `${path}${appendSearch(
querystring.stringify(normalizeHostIsolationExceptionsPageLocation(location))
)}`;
return getArtifactListPageUrlPath(path, location);
};

export const getPolicyHostIsolationExceptionsPath = (
Expand All @@ -442,7 +374,7 @@ export const getBlocklistsListPath = (location?: Partial<ArtifactListPageUrlPara
tabName: AdministrationSubTab.blocklist,
});

return `${path}${appendSearch(querystring.stringify(normalizBlocklistsPageLocation(location)))}`;
return getArtifactListPageUrlPath(path, location);
};

export const getPolicyBlocklistsPath = (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

// FIXME: Remove references to `querystring`
// eslint-disable-next-line import/no-nodejs-modules
import querystring from 'querystring';
import { ArtifactListPageUrlParams } from '../../components/artifact_list_page';
import {
isDefaultOrMissing,
extractFirstParamValue,
extractPageSizeNumber,
extractPageNumber,
} from './utils';
import { MANAGEMENT_DEFAULT_PAGE_SIZE } from '../constants';
import { appendSearch } from '../../../common/components/link_to/helpers';

const SHOW_PARAM_ALLOWED_VALUES: ReadonlyArray<Required<ArtifactListPageUrlParams>['show']> = [
'edit',
'create',
];

/**
* Normalizes the URL search params by dropping any that are either undefined or whose value is
* equal to the default value.
* @param urlSearchParams
*/
const normalizeArtifactListPageUrlSearchParams = (
urlSearchParams: Partial<ArtifactListPageUrlParams> = {}
): Partial<ArtifactListPageUrlParams> => {
return {
...(!isDefaultOrMissing(urlSearchParams.page, 1) ? { page: urlSearchParams.page } : {}),
...(!isDefaultOrMissing(urlSearchParams.pageSize, MANAGEMENT_DEFAULT_PAGE_SIZE)
? { pageSize: urlSearchParams.pageSize }
: {}),
...(!isDefaultOrMissing(urlSearchParams.show, undefined) ? { show: urlSearchParams.show } : {}),
...(!isDefaultOrMissing(urlSearchParams.itemId, undefined)
? { itemId: urlSearchParams.itemId }
: {}),
...(!isDefaultOrMissing(urlSearchParams.filter, '') ? { filter: urlSearchParams.filter } : ''),
...(!isDefaultOrMissing(urlSearchParams.includedPolicies, '')
? { includedPolicies: urlSearchParams.includedPolicies }
: ''),
};
};

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

return {
page: extractPageNumber(query),
pageSize: extractPageSizeNumber(query),
includedPolicies: extractFirstParamValue(query, 'includedPolicies'),
show:
showParamValue && SHOW_PARAM_ALLOWED_VALUES.includes(showParamValue)
? showParamValue
: undefined,
itemId: extractFirstParamValue(query, 'itemId'),
};
};

export const getArtifactListPageUrlPath = (
/** The path to the desired page that is using the `<ArtifactListPage>` component */
path: string,
/** An optional set of url search params. These will be normalized prior to being appended to the url path */
searchParams: Partial<ArtifactListPageUrlParams> = {}
): string => {
return `${path}${appendSearch(
querystring.stringify(normalizeArtifactListPageUrlSearchParams(searchParams))
)}`;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export * from './utils';
export {
getArtifactListPageUrlPath,
extractArtifactListPageUrlSearchParams,
} from './artifact_list_page_routing';
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

// eslint-disable-next-line import/no-nodejs-modules
import querystring from 'querystring';
import { MANAGEMENT_DEFAULT_PAGE_SIZE, MANAGEMENT_PAGE_SIZE_OPTIONS } from '../constants';

/**
* Checks if a given value is either undefined or equal to the default value provided on input
* @param value
* @param defaultValue
*/
export const isDefaultOrMissing = <T>(value: T | undefined, defaultValue: T) => {
return value === undefined || value === defaultValue;
};

/**
* Given an object with url params, and a given key, return back only the first param value (case multiples were defined)
* @param query
* @param key
*/
export const extractFirstParamValue = (
query: querystring.ParsedUrlQuery,
key: string
): string | undefined => {
const value = query[key];

return Array.isArray(value) ? value[value.length - 1] : value;
};

/**
* Extracts the page number from a url query object `page` param and validates it.
* @param query
*/
export const extractPageNumber = (query: querystring.ParsedUrlQuery): number => {
const pageIndex = Number(extractFirstParamValue(query, 'page'));

return !Number.isFinite(pageIndex) || pageIndex < 1 ? 1 : pageIndex;
};

/**
* Extracts the page size from a url query object `pageSize` param and validates it
* @param query
*/
export const extractPageSizeNumber = (query: querystring.ParsedUrlQuery): number => {
const pageSize = Number(extractFirstParamValue(query, 'pageSize'));

return MANAGEMENT_PAGE_SIZE_OPTIONS.includes(pageSize) ? pageSize : MANAGEMENT_DEFAULT_PAGE_SIZE;
};
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,13 @@ describe('When using the ArtifactListPage component', () => {
});

describe('and data exists', () => {
let renderWithListData: () => Promise<ReturnType<typeof render>>;
let renderWithListData: (
props?: Partial<ArtifactListPageProps>
) => Promise<ReturnType<typeof render>>;

beforeEach(async () => {
renderWithListData = async () => {
render();
renderWithListData = async (props) => {
render(props);

await act(async () => {
await waitFor(() => {
Expand Down Expand Up @@ -163,6 +165,35 @@ describe('When using the ArtifactListPage component', () => {
expect(getByTestId('testPage-deleteModal')).toBeTruthy();
});
});

it.each([
['create button', 'testPage-pageAddButton', { allowCardCreateAction: false }],
['edit card action', 'testPage-card-cardEditAction', { allowCardEditAction: false }],
['delete card action', 'testPage-card-cardDeleteAction', { allowCardDeleteAction: false }],
])('should hide the %s', async (_, testId, renderProps) => {
const { queryByTestId } = await renderWithListData(
renderProps as Partial<ArtifactListPageProps>
);
await getFirstCard({ showActions: true });

expect(queryByTestId(testId)).toBeNull();
});

it.each([
['create', 'show=create'],
['edit', 'show=edit&itemId=123'],
])(
'should NOT show flyout if url has a show param of %s but the action is not allowed',
async (_, urlParam) => {
history.push(`somepage?${urlParam}`);
const { queryByTestId } = await renderWithListData({
allowCardCreateAction: false,
allowCardEditAction: false,
});

expect(queryByTestId('testPage-flyout')).toBeNull();
}
);
});

describe('and search bar is used', () => {
Expand Down
Loading

0 comments on commit f1a3edf

Please sign in to comment.