[Security Solution][Endpoint] Adds additional endpoint privileges to the useUserPrivileges() hook (#115051)

* Adds new `canIsolateHost` and `canCreateArtifactsByPolicy` privileges for endpoint
* Refactor `useEndpointPrivileges` mocks to also provide a test function to return the full set of default privileges
* refactor useEndpointPrivileges tests to be more resilient to future changes
This commit is contained in:
Paul Tavares 2021-10-19 00:43:12 -04:00 committed by GitHub
parent e53f4d2f28
commit 57ff4a7172
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 169 additions and 118 deletions

View file

@ -1,18 +0,0 @@
/*
* 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.
*/
import { EndpointPrivileges } from '../use_endpoint_privileges';
export const useEndpointPrivileges = jest.fn(() => {
const endpointPrivilegesMock: EndpointPrivileges = {
loading: false,
canAccessFleet: true,
canAccessEndpointManagement: true,
isPlatinumPlus: true,
};
return endpointPrivilegesMock;
});

View file

@ -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.
*/
import { getEndpointPrivilegesInitialStateMock } from '../mocks';
export { getEndpointPrivilegesInitialState } from '../utils';
export const useEndpointPrivileges = jest.fn(getEndpointPrivilegesInitialStateMock);

View file

@ -0,0 +1,9 @@
/*
* 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 './use_endpoint_privileges';
export { getEndpointPrivilegesInitialState } from './utils';

View file

@ -0,0 +1,29 @@
/*
* 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.
*/
import type { EndpointPrivileges } from './use_endpoint_privileges';
import { getEndpointPrivilegesInitialState } from './utils';
export const getEndpointPrivilegesInitialStateMock = (
overrides: Partial<EndpointPrivileges> = {}
): EndpointPrivileges => {
// Get the initial state and set all permissions to `true` (enabled) for testing
const endpointPrivilegesMock: EndpointPrivileges = {
...(
Object.entries(getEndpointPrivilegesInitialState()) as Array<
[keyof EndpointPrivileges, boolean]
>
).reduce((mockPrivileges, [key, value]) => {
mockPrivileges[key] = !value;
return mockPrivileges;
}, {} as EndpointPrivileges),
...overrides,
};
return endpointPrivilegesMock;
};

View file

@ -6,16 +6,17 @@
*/
import { act, renderHook, RenderHookResult, RenderResult } from '@testing-library/react-hooks';
import { useHttp, useCurrentUser } from '../../lib/kibana';
import { useHttp, useCurrentUser } from '../../../lib/kibana';
import { EndpointPrivileges, useEndpointPrivileges } from './use_endpoint_privileges';
import { securityMock } from '../../../../../security/public/mocks';
import { appRoutesService } from '../../../../../fleet/common';
import { AuthenticatedUser } from '../../../../../security/common';
import { licenseService } from '../../hooks/use_license';
import { fleetGetCheckPermissionsHttpMock } from '../../../management/pages/mocks';
import { securityMock } from '../../../../../../security/public/mocks';
import { appRoutesService } from '../../../../../../fleet/common';
import { AuthenticatedUser } from '../../../../../../security/common';
import { licenseService } from '../../../hooks/use_license';
import { fleetGetCheckPermissionsHttpMock } from '../../../../management/pages/mocks';
import { getEndpointPrivilegesInitialStateMock } from './mocks';
jest.mock('../../lib/kibana');
jest.mock('../../hooks/use_license', () => {
jest.mock('../../../lib/kibana');
jest.mock('../../../hooks/use_license', () => {
const licenseServiceInstance = {
isPlatinumPlus: jest.fn(),
};
@ -27,6 +28,8 @@ jest.mock('../../hooks/use_license', () => {
};
});
const licenseServiceMock = licenseService as jest.Mocked<typeof licenseService>;
describe('When using useEndpointPrivileges hook', () => {
let authenticatedUser: AuthenticatedUser;
let fleetApiMock: ReturnType<typeof fleetGetCheckPermissionsHttpMock>;
@ -45,7 +48,7 @@ describe('When using useEndpointPrivileges hook', () => {
fleetApiMock = fleetGetCheckPermissionsHttpMock(
useHttp() as Parameters<typeof fleetGetCheckPermissionsHttpMock>[0]
);
(licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(true);
licenseServiceMock.isPlatinumPlus.mockReturnValue(true);
render = () => {
const hookRenderResponse = renderHook(() => useEndpointPrivileges());
@ -69,34 +72,31 @@ describe('When using useEndpointPrivileges hook', () => {
(useCurrentUser as jest.Mock).mockReturnValue(null);
const { rerender } = render();
expect(result.current).toEqual({
canAccessEndpointManagement: false,
canAccessFleet: false,
loading: true,
isPlatinumPlus: true,
});
expect(result.current).toEqual(
getEndpointPrivilegesInitialStateMock({
canAccessEndpointManagement: false,
canAccessFleet: false,
loading: true,
})
);
// Make user service available
(useCurrentUser as jest.Mock).mockReturnValue(authenticatedUser);
rerender();
expect(result.current).toEqual({
canAccessEndpointManagement: false,
canAccessFleet: false,
loading: true,
isPlatinumPlus: true,
});
expect(result.current).toEqual(
getEndpointPrivilegesInitialStateMock({
canAccessEndpointManagement: false,
canAccessFleet: false,
loading: true,
})
);
// Release the API response
await act(async () => {
fleetApiMock.waitForApi();
releaseApiResponse!();
});
expect(result.current).toEqual({
canAccessEndpointManagement: true,
canAccessFleet: true,
loading: false,
isPlatinumPlus: true,
});
expect(result.current).toEqual(getEndpointPrivilegesInitialStateMock());
});
it('should call Fleet permissions api to determine user privilege to fleet', async () => {
@ -113,12 +113,11 @@ describe('When using useEndpointPrivileges hook', () => {
render();
await waitForNextUpdate();
await fleetApiMock.waitForApi();
expect(result.current).toEqual({
canAccessEndpointManagement: false,
canAccessFleet: true, // this is only true here because I did not adjust the API mock
loading: false,
isPlatinumPlus: true,
});
expect(result.current).toEqual(
getEndpointPrivilegesInitialStateMock({
canAccessEndpointManagement: false,
})
);
});
it('should set privileges to false if fleet api check returns failure', async () => {
@ -130,11 +129,21 @@ describe('When using useEndpointPrivileges hook', () => {
render();
await waitForNextUpdate();
await fleetApiMock.waitForApi();
expect(result.current).toEqual({
canAccessEndpointManagement: false,
canAccessFleet: false,
loading: false,
isPlatinumPlus: true,
});
expect(result.current).toEqual(
getEndpointPrivilegesInitialStateMock({
canAccessEndpointManagement: false,
canAccessFleet: false,
})
);
});
it.each([['canIsolateHost'], ['canCreateArtifactsByPolicy']])(
'should set %s to false if license is not PlatinumPlus',
async (privilege) => {
licenseServiceMock.isPlatinumPlus.mockReturnValue(false);
render();
await waitForNextUpdate();
expect(result.current).toEqual(expect.objectContaining({ [privilege]: false }));
}
);
});

View file

@ -6,9 +6,9 @@
*/
import { useEffect, useMemo, useRef, useState } from 'react';
import { useCurrentUser, useHttp } from '../../lib/kibana';
import { appRoutesService, CheckPermissionsResponse } from '../../../../../fleet/common';
import { useLicense } from '../../hooks/use_license';
import { useCurrentUser, useHttp } from '../../../lib/kibana';
import { appRoutesService, CheckPermissionsResponse } from '../../../../../../fleet/common';
import { useLicense } from '../../../hooks/use_license';
export interface EndpointPrivileges {
loading: boolean;
@ -16,6 +16,11 @@ export interface EndpointPrivileges {
canAccessFleet: boolean;
/** If user has permissions to access Endpoint management (includes check to ensure they also have access to fleet) */
canAccessEndpointManagement: boolean;
/** if user has permissions to create Artifacts by Policy */
canCreateArtifactsByPolicy: boolean;
/** If user has permissions to use the Host isolation feature */
canIsolateHost: boolean;
/** @deprecated do not use. instead, use one of the other privileges defined */
isPlatinumPlus: boolean;
}
@ -29,7 +34,7 @@ export const useEndpointPrivileges = (): EndpointPrivileges => {
const http = useHttp();
const user = useCurrentUser();
const isMounted = useRef<boolean>(true);
const license = useLicense();
const isPlatinumPlusLicense = useLicense().isPlatinumPlus();
const [canAccessFleet, setCanAccessFleet] = useState<boolean>(false);
const [fleetCheckDone, setFleetCheckDone] = useState<boolean>(false);
@ -61,13 +66,19 @@ export const useEndpointPrivileges = (): EndpointPrivileges => {
}, [user?.roles]);
const privileges = useMemo(() => {
return {
const privilegeList: EndpointPrivileges = {
loading: !fleetCheckDone || !user,
canAccessFleet,
canAccessEndpointManagement: canAccessFleet && isSuperUser,
isPlatinumPlus: license.isPlatinumPlus(),
canCreateArtifactsByPolicy: isPlatinumPlusLicense,
canIsolateHost: isPlatinumPlusLicense,
// FIXME: Remove usages of the property below
/** @deprecated */
isPlatinumPlus: isPlatinumPlusLicense,
};
}, [canAccessFleet, fleetCheckDone, isSuperUser, user, license]);
return privilegeList;
}, [canAccessFleet, fleetCheckDone, isSuperUser, user, isPlatinumPlusLicense]);
// Capture if component is unmounted
useEffect(

View file

@ -0,0 +1,19 @@
/*
* 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.
*/
import { EndpointPrivileges } from './use_endpoint_privileges';
export const getEndpointPrivilegesInitialState = (): EndpointPrivileges => {
return {
loading: true,
canAccessFleet: false,
canAccessEndpointManagement: false,
canIsolateHost: false,
canCreateArtifactsByPolicy: false,
isPlatinumPlus: false,
};
};

View file

@ -11,9 +11,10 @@ import { DeepReadonly } from 'utility-types';
import { Capabilities } from '../../../../../../../src/core/public';
import { useFetchDetectionEnginePrivileges } from '../../../detections/components/user_privileges/use_fetch_detection_engine_privileges';
import { useFetchListPrivileges } from '../../../detections/components/user_privileges/use_fetch_list_privileges';
import { EndpointPrivileges, useEndpointPrivileges } from './use_endpoint_privileges';
import { EndpointPrivileges, useEndpointPrivileges } from './endpoint';
import { SERVER_APP_ID } from '../../../../common/constants';
import { getEndpointPrivilegesInitialState } from './endpoint/utils';
export interface UserPrivilegesState {
listPrivileges: ReturnType<typeof useFetchListPrivileges>;
detectionEnginePrivileges: ReturnType<typeof useFetchDetectionEnginePrivileges>;
@ -24,12 +25,7 @@ export interface UserPrivilegesState {
export const initialUserPrivilegesState = (): UserPrivilegesState => ({
listPrivileges: { loading: false, error: undefined, result: undefined },
detectionEnginePrivileges: { loading: false, error: undefined, result: undefined },
endpointPrivileges: {
loading: true,
canAccessEndpointManagement: false,
canAccessFleet: false,
isPlatinumPlus: false,
},
endpointPrivileges: getEndpointPrivilegesInitialState(),
kibanaSecuritySolutionsPrivileges: { crud: false, read: false },
});

View file

@ -17,7 +17,7 @@ import { UserPrivilegesProvider } from '../../../common/components/user_privileg
jest.mock('../../../common/lib/kibana');
jest.mock('../../containers/detection_engine/alerts/api');
jest.mock('../../../common/components/user_privileges/use_endpoint_privileges');
jest.mock('../../../common/components/user_privileges/endpoint/use_endpoint_privileges');
describe('useUserInfo', () => {
beforeAll(() => {

View file

@ -12,6 +12,7 @@ import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock';
import { useUserPrivileges } from '../../../../common/components/user_privileges';
import { Privilege } from './types';
import { UseAlertsPrivelegesReturn, useAlertsPrivileges } from './use_alerts_privileges';
import { getEndpointPrivilegesInitialStateMock } from '../../../../common/components/user_privileges/endpoint/mocks';
jest.mock('./api');
jest.mock('../../../../common/hooks/use_app_toasts');
@ -86,12 +87,11 @@ const userPrivilegesInitial: ReturnType<typeof useUserPrivileges> = {
result: undefined,
error: undefined,
},
endpointPrivileges: {
endpointPrivileges: getEndpointPrivilegesInitialStateMock({
loading: true,
canAccessEndpointManagement: false,
canAccessFleet: false,
isPlatinumPlus: true,
},
}),
kibanaSecuritySolutionsPrivileges: { crud: true, read: true },
};

View file

@ -13,7 +13,7 @@ import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
jest.mock('./api');
jest.mock('../../../../common/hooks/use_app_toasts');
jest.mock('../../../../common/components/user_privileges/use_endpoint_privileges');
jest.mock('../../../../common/components/user_privileges/endpoint/use_endpoint_privileges');
describe('useSignalIndex', () => {
let appToastsMock: jest.Mocked<ReturnType<typeof useAppToastsMock.create>>;

View file

@ -11,11 +11,12 @@ import { AppContextTestRender, createAppRootMockRenderer } from '../../../common
import {
EndpointPrivileges,
useEndpointPrivileges,
} from '../../../common/components/user_privileges/use_endpoint_privileges';
} from '../../../common/components/user_privileges/endpoint/use_endpoint_privileges';
import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data';
import { SearchExceptions, SearchExceptionsProps } from '.';
jest.mock('../../../common/components/user_privileges/use_endpoint_privileges');
import { getEndpointPrivilegesInitialStateMock } from '../../../common/components/user_privileges/endpoint/mocks';
jest.mock('../../../common/components/user_privileges/endpoint/use_endpoint_privileges');
let onSearchMock: jest.Mock;
const mockUseEndpointPrivileges = useEndpointPrivileges as jest.Mock;
@ -29,13 +30,11 @@ describe('Search exceptions', () => {
const loadedUserEndpointPrivilegesState = (
endpointOverrides: Partial<EndpointPrivileges> = {}
): EndpointPrivileges => ({
loading: false,
canAccessFleet: true,
canAccessEndpointManagement: true,
isPlatinumPlus: false,
...endpointOverrides,
});
): EndpointPrivileges =>
getEndpointPrivilegesInitialStateMock({
isPlatinumPlus: false,
...endpointOverrides,
});
beforeEach(() => {
onSearchMock = jest.fn();

View file

@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiFieldSearch, EuiButton } from '@elastic/e
import { i18n } from '@kbn/i18n';
import { PolicySelectionItem, PoliciesSelector } from '../policies_selector';
import { ImmutableArray, PolicyData } from '../../../../common/endpoint/types';
import { useEndpointPrivileges } from '../../../common/components/user_privileges/use_endpoint_privileges';
import { useEndpointPrivileges } from '../../../common/components/user_privileges/endpoint/use_endpoint_privileges';
export interface SearchExceptionsProps {
defaultValue?: string;

View file

@ -14,7 +14,7 @@ import { isFailedResourceState, isLoadedResourceState } from '../../../state';
// Needed to mock the data services used by the ExceptionItem component
jest.mock('../../../../common/lib/kibana');
jest.mock('../../../../common/components/user_privileges/use_endpoint_privileges');
jest.mock('../../../../common/components/user_privileges/endpoint/use_endpoint_privileges');
describe('When on the Event Filters List Page', () => {
let render: () => ReturnType<AppContextTestRender['render']>;

View file

@ -16,8 +16,7 @@ import { getHostIsolationExceptionItems } from '../service';
import { HostIsolationExceptionsList } from './host_isolation_exceptions_list';
import { useLicense } from '../../../../common/hooks/use_license';
jest.mock('../../../../common/components/user_privileges/use_endpoint_privileges');
jest.mock('../../../../common/components/user_privileges/endpoint/use_endpoint_privileges');
jest.mock('../service');
jest.mock('../../../../common/hooks/use_license');

View file

@ -10,7 +10,7 @@ import { EuiEmptyPrompt, EuiButton, EuiPageTemplate, EuiLink } from '@elastic/eu
import { FormattedMessage } from '@kbn/i18n/react';
import { usePolicyDetailsNavigateCallback } from '../../policy_hooks';
import { useGetLinkTo } from './use_policy_trusted_apps_empty_hooks';
import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/use_endpoint_privileges';
import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges';
interface CommonProps {
policyId: string;

View file

@ -21,7 +21,7 @@ import { createLoadedResourceState, isLoadedResourceState } from '../../../../..
import { getPolicyDetailsArtifactsListPath } from '../../../../../common/routing';
jest.mock('../../../../trusted_apps/service');
jest.mock('../../../../../../common/components/user_privileges/use_endpoint_privileges');
jest.mock('../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges');
let mockedContext: AppContextTestRender;
let waitForAction: MiddlewareActionSpyHelper['waitForAction'];

View file

@ -19,13 +19,11 @@ import { createLoadedResourceState, isLoadedResourceState } from '../../../../..
import { getPolicyDetailsArtifactsListPath } from '../../../../../common/routing';
import { EndpointDocGenerator } from '../../../../../../../common/endpoint/generate_data';
import { policyListApiPathHandlers } from '../../../store/test_mock_utils';
import {
EndpointPrivileges,
useEndpointPrivileges,
} from '../../../../../../common/components/user_privileges/use_endpoint_privileges';
import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges';
import { getEndpointPrivilegesInitialStateMock } from '../../../../../../common/components/user_privileges/endpoint/mocks';
jest.mock('../../../../trusted_apps/service');
jest.mock('../../../../../../common/components/user_privileges/use_endpoint_privileges');
jest.mock('../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges');
const mockUseEndpointPrivileges = useEndpointPrivileges as jest.Mock;
let mockedContext: AppContextTestRender;
@ -37,16 +35,6 @@ let http: typeof coreStart.http;
const generator = new EndpointDocGenerator();
describe('Policy trusted apps layout', () => {
const loadedUserEndpointPrivilegesState = (
endpointOverrides: Partial<EndpointPrivileges> = {}
): EndpointPrivileges => ({
loading: false,
canAccessFleet: true,
canAccessEndpointManagement: true,
isPlatinumPlus: true,
...endpointOverrides,
});
beforeEach(() => {
mockedContext = createAppRootMockRenderer();
http = mockedContext.coreStart.http;
@ -137,7 +125,7 @@ describe('Policy trusted apps layout', () => {
it('should hide assign button on empty state with unassigned policies when downgraded to a gold or below license', async () => {
mockUseEndpointPrivileges.mockReturnValue(
loadedUserEndpointPrivilegesState({
getEndpointPrivilegesInitialStateMock({
isPlatinumPlus: false,
})
);
@ -155,7 +143,7 @@ describe('Policy trusted apps layout', () => {
it('should hide the `Assign trusted applications` button when there is data and the license is downgraded to gold or below', async () => {
mockUseEndpointPrivileges.mockReturnValue(
loadedUserEndpointPrivilegesState({
getEndpointPrivilegesInitialStateMock({
isPlatinumPlus: false,
})
);

View file

@ -30,7 +30,7 @@ import {
import { usePolicyDetailsNavigateCallback, usePolicyDetailsSelector } from '../../policy_hooks';
import { PolicyTrustedAppsFlyout } from '../flyout';
import { PolicyTrustedAppsList } from '../list/policy_trusted_apps_list';
import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/use_endpoint_privileges';
import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges';
import { useAppUrl } from '../../../../../../common/lib/kibana';
import { APP_ID } from '../../../../../../../common/constants';
import { getTrustedAppsListPath } from '../../../../../common/routing';

View file

@ -24,9 +24,10 @@ import { APP_ID } from '../../../../../../../common/constants';
import {
EndpointPrivileges,
useEndpointPrivileges,
} from '../../../../../../common/components/user_privileges/use_endpoint_privileges';
} from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges';
import { getEndpointPrivilegesInitialStateMock } from '../../../../../../common/components/user_privileges/endpoint/mocks';
jest.mock('../../../../../../common/components/user_privileges/use_endpoint_privileges');
jest.mock('../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges');
const mockUseEndpointPrivileges = useEndpointPrivileges as jest.Mock;
describe('when rendering the PolicyTrustedAppsList', () => {
@ -43,10 +44,7 @@ describe('when rendering the PolicyTrustedAppsList', () => {
const loadedUserEndpointPrivilegesState = (
endpointOverrides: Partial<EndpointPrivileges> = {}
): EndpointPrivileges => ({
loading: false,
canAccessFleet: true,
canAccessEndpointManagement: true,
isPlatinumPlus: true,
...getEndpointPrivilegesInitialStateMock(),
...endpointOverrides,
});

View file

@ -38,7 +38,7 @@ import { ContextMenuItemNavByRouterProps } from '../../../../../components/conte
import { ArtifactEntryCollapsibleCardProps } from '../../../../../components/artifact_entry_card';
import { useTestIdGenerator } from '../../../../../components/hooks/use_test_id_generator';
import { RemoveTrustedAppFromPolicyModal } from './remove_trusted_app_from_policy_modal';
import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/use_endpoint_privileges';
import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges';
const DATA_TEST_SUBJ = 'policyTrustedAppsGrid';

View file

@ -52,7 +52,7 @@ jest.mock('../../../../common/hooks/use_license', () => {
};
});
jest.mock('../../../../common/components/user_privileges/use_endpoint_privileges');
jest.mock('../../../../common/components/user_privileges/endpoint/use_endpoint_privileges');
describe('When on the Trusted Apps Page', () => {
const expectedAboutInfo =

View file

@ -30,7 +30,7 @@ import {
mockCtiLinksResponse,
} from '../components/overview_cti_links/mock';
import { useCtiDashboardLinks } from '../containers/overview_cti_links';
import { EndpointPrivileges } from '../../common/components/user_privileges/use_endpoint_privileges';
import { EndpointPrivileges } from '../../common/components/user_privileges/endpoint/use_endpoint_privileges';
import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features';
import { useHostsRiskScore } from '../containers/overview_risky_host_links/use_hosts_risk_score';