mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Soution][Endpoint] Uses RBAC in policy details page for artifacts tabs (#147676)
## Summary - Hide artifact tabs when no read permissions. - Hide manage/add artifacts button actions when no write permissions. - Redirects user to policy details page when missing privileges and add a toast with error message. - Remove superuser check for `canCreateArtifactsByPolicy` privielge - Also updates and adds unit tests
This commit is contained in:
parent
6b29787e6f
commit
886289d206
15 changed files with 336 additions and 191 deletions
|
@ -218,7 +218,7 @@ export const calculateEndpointAuthz = (
|
|||
canReadSecuritySolution,
|
||||
canAccessFleet: fleetAuthz?.fleet.all ?? userRoles.includes('superuser'),
|
||||
canAccessEndpointManagement: hasEndpointManagementAccess,
|
||||
canCreateArtifactsByPolicy: hasEndpointManagementAccess && isPlatinumPlusLicense,
|
||||
canCreateArtifactsByPolicy: isPlatinumPlusLicense,
|
||||
canWriteEndpointList,
|
||||
canReadEndpointList,
|
||||
canWritePolicyManagement,
|
||||
|
|
|
@ -22,12 +22,21 @@ interface CommonProps {
|
|||
policyName: string;
|
||||
listId: string;
|
||||
labels: typeof POLICY_ARTIFACT_EMPTY_UNASSIGNED_LABELS;
|
||||
canWriteArtifact?: boolean;
|
||||
getPolicyArtifactsPath: (policyId: string) => string;
|
||||
getArtifactPath: (location?: Partial<ArtifactListPageUrlParams>) => string;
|
||||
}
|
||||
|
||||
export const PolicyArtifactsEmptyUnassigned = memo<CommonProps>(
|
||||
({ policyId, policyName, listId, labels, getPolicyArtifactsPath, getArtifactPath }) => {
|
||||
({
|
||||
policyId,
|
||||
policyName,
|
||||
listId,
|
||||
labels,
|
||||
canWriteArtifact = false,
|
||||
getPolicyArtifactsPath,
|
||||
getArtifactPath,
|
||||
}) => {
|
||||
const { canCreateArtifactsByPolicy } = useUserPrivileges().endpointPrivileges;
|
||||
const { onClickHandler, toRouteUrl } = useGetLinkTo(
|
||||
policyId,
|
||||
|
@ -50,24 +59,34 @@ export const PolicyArtifactsEmptyUnassigned = memo<CommonProps>(
|
|||
iconType="plusInCircle"
|
||||
data-test-subj="policy-artifacts-empty-unassigned"
|
||||
title={<h2>{labels.emptyUnassignedTitle}</h2>}
|
||||
body={labels.emptyUnassignedMessage(policyName)}
|
||||
body={
|
||||
canWriteArtifact
|
||||
? labels.emptyUnassignedMessage(policyName)
|
||||
: labels.emptyUnassignedNoPrivilegesMessage(policyName)
|
||||
}
|
||||
actions={[
|
||||
...(canCreateArtifactsByPolicy
|
||||
...(canCreateArtifactsByPolicy && canWriteArtifact
|
||||
? [
|
||||
<EuiButton
|
||||
color="primary"
|
||||
fill
|
||||
onClick={onClickPrimaryButtonHandler}
|
||||
data-test-subj="assign-artifacts-button"
|
||||
data-test-subj="unassigned-assign-artifacts-button"
|
||||
>
|
||||
{labels.emptyUnassignedPrimaryActionButtonTitle}
|
||||
</EuiButton>,
|
||||
]
|
||||
: []),
|
||||
// eslint-disable-next-line @elastic/eui/href-or-on-click
|
||||
<EuiLink onClick={onClickHandler} href={toRouteUrl}>
|
||||
{labels.emptyUnassignedSecondaryActionButtonTitle}
|
||||
</EuiLink>,
|
||||
canWriteArtifact ? (
|
||||
// eslint-disable-next-line @elastic/eui/href-or-on-click
|
||||
<EuiLink
|
||||
onClick={onClickHandler}
|
||||
href={toRouteUrl}
|
||||
data-test-subj="unassigned-manage-artifacts-button"
|
||||
>
|
||||
{labels.emptyUnassignedSecondaryActionButtonTitle}
|
||||
</EuiLink>
|
||||
) : null,
|
||||
]}
|
||||
/>
|
||||
</EuiPageTemplate>
|
||||
|
|
|
@ -19,12 +19,20 @@ interface CommonProps {
|
|||
policyId: string;
|
||||
policyName: string;
|
||||
labels: typeof POLICY_ARTIFACT_EMPTY_UNEXISTING_LABELS;
|
||||
canWriteArtifact?: boolean;
|
||||
getPolicyArtifactsPath: (policyId: string) => string;
|
||||
getArtifactPath: (location?: Partial<ArtifactListPageUrlParams>) => string;
|
||||
}
|
||||
|
||||
export const PolicyArtifactsEmptyUnexisting = memo<CommonProps>(
|
||||
({ policyId, policyName, labels, getPolicyArtifactsPath, getArtifactPath }) => {
|
||||
({
|
||||
policyId,
|
||||
policyName,
|
||||
labels,
|
||||
canWriteArtifact = false,
|
||||
getPolicyArtifactsPath,
|
||||
getArtifactPath,
|
||||
}) => {
|
||||
const { onClickHandler, toRouteUrl } = useGetLinkTo(
|
||||
policyId,
|
||||
policyName,
|
||||
|
@ -42,10 +50,18 @@ export const PolicyArtifactsEmptyUnexisting = memo<CommonProps>(
|
|||
title={<h2>{labels.emptyUnexistingTitle}</h2>}
|
||||
body={labels.emptyUnexistingMessage}
|
||||
actions={
|
||||
// eslint-disable-next-line @elastic/eui/href-or-on-click
|
||||
<EuiButton color="primary" fill onClick={onClickHandler} href={toRouteUrl}>
|
||||
{labels.emptyUnexistingPrimaryActionButtonTitle}
|
||||
</EuiButton>
|
||||
canWriteArtifact ? (
|
||||
// eslint-disable-next-line @elastic/eui/href-or-on-click
|
||||
<EuiButton
|
||||
color="primary"
|
||||
fill
|
||||
onClick={onClickHandler}
|
||||
href={toRouteUrl}
|
||||
data-test-subj="unexisting-manage-artifacts-button"
|
||||
>
|
||||
{labels.emptyUnexistingPrimaryActionButtonTitle}
|
||||
</EuiButton>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
</EuiPageTemplate>
|
||||
|
|
|
@ -30,6 +30,14 @@ export const POLICY_ARTIFACT_EMPTY_UNASSIGNED_LABELS = Object.freeze({
|
|||
defaultMessage: 'Manage artifacts',
|
||||
}
|
||||
),
|
||||
emptyUnassignedNoPrivilegesMessage: (policyName: string): string =>
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.endpoint.policy.artifacts.empty.unassigned.noPrivileges.content',
|
||||
{
|
||||
defaultMessage: 'There are currently no artifacts assigned to {policyName}.',
|
||||
values: { policyName },
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
export const POLICY_ARTIFACT_EMPTY_UNEXISTING_LABELS = Object.freeze({
|
||||
|
|
|
@ -20,19 +20,26 @@ import type { ImmutableObject, PolicyData } from '../../../../../../../common/en
|
|||
import { parsePoliciesAndFilterToKql } from '../../../../../common/utils';
|
||||
import { eventFiltersListQueryHttpMock } from '../../../../event_filters/test_utils';
|
||||
import { getFoundExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/found_exception_list_item_schema.mock';
|
||||
import { getEndpointPrivilegesInitialStateMock } from '../../../../../../common/components/user_privileges/endpoint/mocks';
|
||||
import { POLICY_ARTIFACT_EVENT_FILTERS_LABELS } from '../../tabs/event_filters_translations';
|
||||
import { EventFiltersApiClient } from '../../../../event_filters/service/api_client';
|
||||
import { SEARCHABLE_FIELDS as EVENT_FILTERS_SEARCHABLE_FIELDS } from '../../../../event_filters/constants';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useUserPrivileges } from '../../../../../../common/components/user_privileges';
|
||||
|
||||
let render: (externalPrivileges?: boolean) => Promise<ReturnType<AppContextTestRender['render']>>;
|
||||
jest.mock('../../../../../../common/components/user_privileges');
|
||||
|
||||
interface MockedAPIArgs {
|
||||
query: { filter: string };
|
||||
}
|
||||
|
||||
let render: (canWriteArtifact?: boolean) => Promise<ReturnType<AppContextTestRender['render']>>;
|
||||
let mockedContext: AppContextTestRender;
|
||||
let renderResult: ReturnType<AppContextTestRender['render']>;
|
||||
let policyItem: ImmutableObject<PolicyData>;
|
||||
const generator = new EndpointDocGenerator();
|
||||
let mockedApi: ReturnType<typeof eventFiltersListQueryHttpMock>;
|
||||
let history: AppContextTestRender['history'];
|
||||
const useUserPrivilegesMock = useUserPrivileges as jest.Mock;
|
||||
|
||||
const getEventFiltersLabels = () => ({
|
||||
...POLICY_ARTIFACT_EVENT_FILTERS_LABELS,
|
||||
|
@ -46,6 +53,9 @@ const getEventFiltersLabels = () => ({
|
|||
});
|
||||
|
||||
describe('Policy artifacts layout', () => {
|
||||
const isFilteredByPolicyQuery = (args?: { query: { filter: string } }) =>
|
||||
args && args.query.filter === parsePoliciesAndFilterToKql({ policies: [policyItem.id, 'all'] });
|
||||
|
||||
beforeEach(() => {
|
||||
mockedContext = createAppRootMockRenderer();
|
||||
mockedApi = eventFiltersListQueryHttpMock(mockedContext.coreStart.http);
|
||||
|
@ -53,10 +63,12 @@ describe('Policy artifacts layout', () => {
|
|||
policyItem = generator.generatePolicyPackagePolicy();
|
||||
({ history } = mockedContext);
|
||||
|
||||
getEndpointPrivilegesInitialStateMock({
|
||||
canCreateArtifactsByPolicy: true,
|
||||
useUserPrivilegesMock.mockReturnValue({
|
||||
endpointPrivileges: {
|
||||
canCreateArtifactsByPolicy: true,
|
||||
},
|
||||
});
|
||||
render = async (externalPrivileges = true) => {
|
||||
render = async (canWriteArtifact = true) => {
|
||||
await act(async () => {
|
||||
renderResult = mockedContext.render(
|
||||
<PolicyArtifactsLayout
|
||||
|
@ -68,7 +80,7 @@ describe('Policy artifacts layout', () => {
|
|||
searchableFields={EVENT_FILTERS_SEARCHABLE_FIELDS}
|
||||
getArtifactPath={getEventFiltersListPath}
|
||||
getPolicyArtifactsPath={getPolicyEventFiltersPath}
|
||||
externalPrivileges={externalPrivileges}
|
||||
canWriteArtifact={canWriteArtifact}
|
||||
/>
|
||||
);
|
||||
await waitFor(mockedApi.responseProvider.eventFiltersList);
|
||||
|
@ -104,18 +116,13 @@ describe('Policy artifacts layout', () => {
|
|||
});
|
||||
|
||||
it('should render layout with no assigned artifacts data when there are artifacts', async () => {
|
||||
mockedApi.responseProvider.eventFiltersList.mockImplementation(
|
||||
(args?: { query: { filter: string } }) => {
|
||||
if (
|
||||
!args ||
|
||||
args.query.filter !== parsePoliciesAndFilterToKql({ policies: [policyItem.id, 'all'] })
|
||||
) {
|
||||
return getFoundExceptionListItemSchemaMock(1);
|
||||
} else {
|
||||
return getFoundExceptionListItemSchemaMock(0);
|
||||
}
|
||||
mockedApi.responseProvider.eventFiltersList.mockImplementation((args?: MockedAPIArgs) => {
|
||||
if (!isFilteredByPolicyQuery(args)) {
|
||||
return getFoundExceptionListItemSchemaMock(1);
|
||||
} else {
|
||||
return getFoundExceptionListItemSchemaMock(0);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
await render();
|
||||
|
||||
|
@ -135,8 +142,10 @@ describe('Policy artifacts layout', () => {
|
|||
});
|
||||
|
||||
it('should hide `Assign artifacts to policy` on empty state with unassigned policies when downgraded to a gold or below license', async () => {
|
||||
getEndpointPrivilegesInitialStateMock({
|
||||
canCreateArtifactsByPolicy: false,
|
||||
useUserPrivilegesMock.mockReturnValue({
|
||||
endpointPrivileges: {
|
||||
canCreateArtifactsByPolicy: false,
|
||||
},
|
||||
});
|
||||
mockedApi.responseProvider.eventFiltersList.mockReturnValue(
|
||||
getFoundExceptionListItemSchemaMock(0)
|
||||
|
@ -148,8 +157,10 @@ describe('Policy artifacts layout', () => {
|
|||
});
|
||||
|
||||
it('should hide the `Assign artifacts to policy` button license is downgraded to gold or below', async () => {
|
||||
getEndpointPrivilegesInitialStateMock({
|
||||
canCreateArtifactsByPolicy: false,
|
||||
useUserPrivilegesMock.mockReturnValue({
|
||||
endpointPrivileges: {
|
||||
canCreateArtifactsByPolicy: false,
|
||||
},
|
||||
});
|
||||
mockedApi.responseProvider.eventFiltersList.mockReturnValue(
|
||||
getFoundExceptionListItemSchemaMock(5)
|
||||
|
@ -162,8 +173,10 @@ describe('Policy artifacts layout', () => {
|
|||
});
|
||||
|
||||
it('should hide the `Assign artifacts` flyout when license is downgraded to gold or below', async () => {
|
||||
getEndpointPrivilegesInitialStateMock({
|
||||
canCreateArtifactsByPolicy: false,
|
||||
useUserPrivilegesMock.mockReturnValue({
|
||||
endpointPrivileges: {
|
||||
canCreateArtifactsByPolicy: false,
|
||||
},
|
||||
});
|
||||
mockedApi.responseProvider.eventFiltersList.mockReturnValue(
|
||||
getFoundExceptionListItemSchemaMock(2)
|
||||
|
@ -185,5 +198,26 @@ describe('Policy artifacts layout', () => {
|
|||
await render(false);
|
||||
expect(renderResult.queryByTestId('artifacts-assign-button')).toBeNull();
|
||||
});
|
||||
it('should not display assign and manage artifacts buttons on empty state when there are artifacts', async () => {
|
||||
mockedApi.responseProvider.eventFiltersList.mockImplementation((args?: MockedAPIArgs) => {
|
||||
if (!isFilteredByPolicyQuery(args)) {
|
||||
return getFoundExceptionListItemSchemaMock(1);
|
||||
} else {
|
||||
return getFoundExceptionListItemSchemaMock(0);
|
||||
}
|
||||
});
|
||||
await render(false);
|
||||
expect(await renderResult.findByTestId('policy-artifacts-empty-unassigned')).not.toBeNull();
|
||||
expect(renderResult.queryByTestId('unassigned-assign-artifacts-button')).toBeNull();
|
||||
expect(renderResult.queryByTestId('unassigned-manage-artifacts-button')).toBeNull();
|
||||
});
|
||||
it('should not display manage artifacts button on empty state when there are no artifacts', async () => {
|
||||
mockedApi.responseProvider.eventFiltersList.mockReturnValue(
|
||||
getFoundExceptionListItemSchemaMock(0)
|
||||
);
|
||||
await render(false);
|
||||
expect(await renderResult.findByTestId('policy-artifacts-empty-unexisting')).not.toBeNull();
|
||||
expect(renderResult.queryByTestId('unexisting-manage-artifacts-button')).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -42,8 +42,8 @@ interface PolicyArtifactsLayoutProps {
|
|||
searchableFields: readonly string[];
|
||||
getArtifactPath: (location?: Partial<ArtifactListPageUrlParams>) => string;
|
||||
getPolicyArtifactsPath: (policyId: string) => string;
|
||||
/** A boolean to check extra privileges for restricted actions, true when it's allowed, false when not */
|
||||
externalPrivileges?: boolean;
|
||||
/** A boolean to check if has write artifact privilege or not */
|
||||
canWriteArtifact?: boolean;
|
||||
}
|
||||
export const PolicyArtifactsLayout = React.memo<PolicyArtifactsLayoutProps>(
|
||||
({
|
||||
|
@ -53,7 +53,7 @@ export const PolicyArtifactsLayout = React.memo<PolicyArtifactsLayoutProps>(
|
|||
searchableFields,
|
||||
getArtifactPath,
|
||||
getPolicyArtifactsPath,
|
||||
externalPrivileges = true,
|
||||
canWriteArtifact = false,
|
||||
}) => {
|
||||
const exceptionsListApiClient = useMemo(
|
||||
() => getExceptionsListApiClient(),
|
||||
|
@ -161,6 +161,7 @@ export const PolicyArtifactsLayout = React.memo<PolicyArtifactsLayoutProps>(
|
|||
policyName={policyItem.name}
|
||||
listId={exceptionsListApiClient.listId}
|
||||
labels={labels}
|
||||
canWriteArtifact={canWriteArtifact}
|
||||
getPolicyArtifactsPath={getPolicyArtifactsPath}
|
||||
getArtifactPath={getArtifactPath}
|
||||
/>
|
||||
|
@ -169,6 +170,7 @@ export const PolicyArtifactsLayout = React.memo<PolicyArtifactsLayoutProps>(
|
|||
policyId={policyItem.id}
|
||||
policyName={policyItem.name}
|
||||
labels={labels}
|
||||
canWriteArtifact={canWriteArtifact}
|
||||
getPolicyArtifactsPath={getPolicyArtifactsPath}
|
||||
getArtifactPath={getArtifactPath}
|
||||
/>
|
||||
|
@ -192,10 +194,10 @@ export const PolicyArtifactsLayout = React.memo<PolicyArtifactsLayoutProps>(
|
|||
</EuiText>
|
||||
</EuiPageHeaderSection>
|
||||
<EuiPageHeaderSection>
|
||||
{canCreateArtifactsByPolicy && externalPrivileges && assignToPolicyButton}
|
||||
{canCreateArtifactsByPolicy && canWriteArtifact && assignToPolicyButton}
|
||||
</EuiPageHeaderSection>
|
||||
</EuiPageHeader>
|
||||
{canCreateArtifactsByPolicy && externalPrivileges && urlParams.show === 'list' && (
|
||||
{canCreateArtifactsByPolicy && canWriteArtifact && urlParams.show === 'list' && (
|
||||
<PolicyArtifactsFlyout
|
||||
policyItem={policyItem}
|
||||
apiClient={exceptionsListApiClient}
|
||||
|
@ -228,7 +230,7 @@ export const PolicyArtifactsLayout = React.memo<PolicyArtifactsLayoutProps>(
|
|||
searchableFields={[...searchableFields]}
|
||||
labels={labels}
|
||||
onDeleteActionCallback={handleOnDeleteActionCallback}
|
||||
externalPrivileges={externalPrivileges}
|
||||
canWriteArtifact={canWriteArtifact}
|
||||
getPolicyArtifactsPath={getPolicyArtifactsPath}
|
||||
getArtifactPath={getArtifactPath}
|
||||
/>
|
||||
|
|
|
@ -39,7 +39,7 @@ const getDefaultQueryParameters = (customFilter: string | undefined = '') => ({
|
|||
jest.setTimeout(10000);
|
||||
|
||||
describe('Policy details artifacts list', () => {
|
||||
let render: (externalPrivileges?: boolean) => Promise<ReturnType<AppContextTestRender['render']>>;
|
||||
let render: (canWriteArtifact?: boolean) => Promise<ReturnType<AppContextTestRender['render']>>;
|
||||
let renderResult: ReturnType<AppContextTestRender['render']>;
|
||||
let history: AppContextTestRender['history'];
|
||||
let mockedContext: AppContextTestRender;
|
||||
|
@ -55,7 +55,7 @@ describe('Policy details artifacts list', () => {
|
|||
getEndpointPrivilegesInitialStateMock({
|
||||
canCreateArtifactsByPolicy: true,
|
||||
});
|
||||
render = async (externalPrivileges = true) => {
|
||||
render = async (canWriteArtifact = true) => {
|
||||
await act(async () => {
|
||||
renderResult = mockedContext.render(
|
||||
<PolicyArtifactsList
|
||||
|
@ -64,7 +64,7 @@ describe('Policy details artifacts list', () => {
|
|||
searchableFields={[...SEARCHABLE_FIELDS]}
|
||||
labels={POLICY_ARTIFACT_LIST_LABELS}
|
||||
onDeleteActionCallback={handleOnDeleteActionCallbackMock}
|
||||
externalPrivileges={externalPrivileges}
|
||||
canWriteArtifact={canWriteArtifact}
|
||||
getPolicyArtifactsPath={getPolicyEventFiltersPath}
|
||||
getArtifactPath={getEventFiltersListPath}
|
||||
/>
|
||||
|
|
|
@ -37,7 +37,7 @@ interface PolicyArtifactsListProps {
|
|||
getPolicyArtifactsPath: (policyId: string) => string;
|
||||
labels: typeof POLICY_ARTIFACT_LIST_LABELS;
|
||||
onDeleteActionCallback: (item: ExceptionListItemSchema) => void;
|
||||
externalPrivileges?: boolean;
|
||||
canWriteArtifact?: boolean;
|
||||
}
|
||||
|
||||
export const PolicyArtifactsList = React.memo<PolicyArtifactsListProps>(
|
||||
|
@ -49,7 +49,7 @@ export const PolicyArtifactsList = React.memo<PolicyArtifactsListProps>(
|
|||
getPolicyArtifactsPath,
|
||||
labels,
|
||||
onDeleteActionCallback,
|
||||
externalPrivileges = true,
|
||||
canWriteArtifact = false,
|
||||
}) => {
|
||||
useOldUrlSearchPaginationReplace();
|
||||
const { getAppUrl } = useAppUrl();
|
||||
|
@ -150,7 +150,7 @@ export const PolicyArtifactsList = React.memo<PolicyArtifactsListProps>(
|
|||
return {
|
||||
expanded: expandedItemsMap.get(item.id) || false,
|
||||
actions:
|
||||
canCreateArtifactsByPolicy && externalPrivileges
|
||||
canCreateArtifactsByPolicy && canWriteArtifact
|
||||
? [fullDetailsAction, deleteAction]
|
||||
: [fullDetailsAction],
|
||||
policies: artifactCardPolicies,
|
||||
|
@ -160,7 +160,7 @@ export const PolicyArtifactsList = React.memo<PolicyArtifactsListProps>(
|
|||
artifactCardPolicies,
|
||||
canCreateArtifactsByPolicy,
|
||||
expandedItemsMap,
|
||||
externalPrivileges,
|
||||
canWriteArtifact,
|
||||
getAppUrl,
|
||||
getArtifactPath,
|
||||
labels.listFullDetailsActionTitle,
|
||||
|
|
|
@ -67,6 +67,7 @@ export type PolicyArtifactsPageRequiredLabels = Pick<
|
|||
| 'emptyUnassignedMessage'
|
||||
| 'emptyUnassignedPrimaryActionButtonTitle'
|
||||
| 'emptyUnassignedSecondaryActionButtonTitle'
|
||||
| 'emptyUnassignedNoPrivilegesMessage'
|
||||
| 'emptyUnexistingTitle'
|
||||
| 'emptyUnexistingMessage'
|
||||
| 'emptyUnexistingPrimaryActionButtonTitle'
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { mount } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { AGENT_API_ROUTES, PACKAGE_POLICY_API_ROOT } from '@kbn/fleet-plugin/common';
|
||||
|
@ -16,12 +15,19 @@ import {
|
|||
createAppRootMockRenderer,
|
||||
resetReactDomCreatePortalMock,
|
||||
} from '../../../../common/mock/endpoint';
|
||||
import { getEndpointListPath, getPoliciesPath, getPolicyDetailPath } from '../../../common/routing';
|
||||
import {
|
||||
getEndpointListPath,
|
||||
getPoliciesPath,
|
||||
getPolicyBlocklistsPath,
|
||||
getPolicyDetailPath,
|
||||
getPolicyEventFiltersPath,
|
||||
getPolicyHostIsolationExceptionsPath,
|
||||
getPolicyTrustedAppsPath,
|
||||
} from '../../../common/routing';
|
||||
import { policyListApiPathHandlers } from '../store/test_mock_utils';
|
||||
import { PolicyDetails } from './policy_details';
|
||||
import { APP_UI_ID } from '../../../../../common/constants';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
import { exceptionsFindHttpMocks } from '../../../mocks/exceptions_list_http_mocks';
|
||||
|
||||
jest.mock('./policy_forms/components/policy_form_layout', () => ({
|
||||
PolicyFormLayout: () => <></>,
|
||||
|
@ -215,61 +221,53 @@ describe('Policy Details', () => {
|
|||
expect(tab.text()).toBe('Host isolation exceptions');
|
||||
});
|
||||
|
||||
describe('without canIsolateHost permissions', () => {
|
||||
let findExceptionsApiHttpMock: ReturnType<typeof exceptionsFindHttpMocks>;
|
||||
|
||||
beforeEach(() => {
|
||||
describe('without required permissions', () => {
|
||||
const renderWithPrivilege = async (privilege: string) => {
|
||||
useUserPrivilegesMock.mockReturnValue({
|
||||
endpointPrivileges: {
|
||||
loading: false,
|
||||
canIsolateHost: false,
|
||||
[privilege]: false,
|
||||
},
|
||||
});
|
||||
|
||||
findExceptionsApiHttpMock = exceptionsFindHttpMocks(http);
|
||||
});
|
||||
|
||||
it('should not display the host isolation exceptions tab with no privileges and no assigned exceptions', async () => {
|
||||
findExceptionsApiHttpMock.responseProvider.exceptionsFind.mockReturnValue({
|
||||
data: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
per_page: 100,
|
||||
});
|
||||
policyView = render();
|
||||
await asyncActions;
|
||||
policyView.update();
|
||||
await waitFor(() => {
|
||||
expect(findExceptionsApiHttpMock.responseProvider.exceptionsFind).toHaveBeenCalled();
|
||||
});
|
||||
expect(policyView.find('button#hostIsolationExceptions')).toHaveLength(0);
|
||||
});
|
||||
};
|
||||
|
||||
it('should not display the host isolation exceptions tab with no privileges and no data', async () => {
|
||||
findExceptionsApiHttpMock.responseProvider.exceptionsFind.mockReturnValue({
|
||||
data: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
per_page: 100,
|
||||
});
|
||||
policyView = render();
|
||||
await asyncActions;
|
||||
policyView.update();
|
||||
await waitFor(() => {
|
||||
expect(findExceptionsApiHttpMock.responseProvider.exceptionsFind).toHaveBeenCalled();
|
||||
});
|
||||
expect(policyView.find('button#hostIsolationExceptions')).toHaveLength(0);
|
||||
});
|
||||
it.each([
|
||||
['trusted apps', 'canReadTrustedApplications', 'trustedApps'],
|
||||
['event filters', 'canReadEventFilters', 'eventFilters'],
|
||||
['host isolation exeptions', 'canReadHostIsolationExceptions', 'hostIsolationExceptions'],
|
||||
['blocklist', 'canReadBlocklist', 'blocklists'],
|
||||
])(
|
||||
'should not display the %s tab with no privileges',
|
||||
async (_: string, privilege: string, selector: string) => {
|
||||
await renderWithPrivilege(privilege);
|
||||
expect(policyView.find(`button#${selector}`)).toHaveLength(0);
|
||||
}
|
||||
);
|
||||
|
||||
it('should display the host isolation exceptions tab with no privileges if there are assigned exceptions', async () => {
|
||||
policyView = render();
|
||||
await asyncActions;
|
||||
policyView.update();
|
||||
await waitFor(() => {
|
||||
expect(findExceptionsApiHttpMock.responseProvider.exceptionsFind).toHaveBeenCalled();
|
||||
});
|
||||
expect(policyView.find('button#hostIsolationExceptions')).toHaveLength(1);
|
||||
});
|
||||
it.each([
|
||||
['trusted apps', 'canReadTrustedApplications', getPolicyTrustedAppsPath('1')],
|
||||
['event filters', 'canReadEventFilters', getPolicyEventFiltersPath('1')],
|
||||
[
|
||||
'host isolation exeptions',
|
||||
'canReadHostIsolationExceptions',
|
||||
getPolicyHostIsolationExceptionsPath('1'),
|
||||
],
|
||||
['blocklist', 'canReadBlocklist', getPolicyBlocklistsPath('1')],
|
||||
])(
|
||||
'should redirect to policy details when no %s required privileges',
|
||||
async (_: string, privilege: string, path: string) => {
|
||||
history.push(path);
|
||||
await renderWithPrivilege(privilege);
|
||||
expect(history.location.pathname).toBe(policyDetailsPathUrl);
|
||||
expect(coreStart.notifications.toasts.addDanger).toHaveBeenCalledTimes(1);
|
||||
expect(coreStart.notifications.toasts.addDanger).toHaveBeenCalledWith(
|
||||
'You do not have the required Kibana permissions to use the given artifact.'
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -104,6 +104,14 @@ export const POLICY_ARTIFACT_BLOCKLISTS_LABELS = Object.freeze({
|
|||
defaultMessage: 'Manage blocklist entries',
|
||||
}
|
||||
),
|
||||
emptyUnassignedNoPrivilegesMessage: (policyName: string): string =>
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.endpoint.policy.blocklist.empty.unassigned.noPrivileges.content',
|
||||
{
|
||||
defaultMessage: 'There are currently no blocklist entries assigned to {policyName}.',
|
||||
values: { policyName },
|
||||
}
|
||||
),
|
||||
emptyUnexistingTitle: i18n.translate(
|
||||
'xpack.securitySolution.endpoint.policy.blocklist.empty.unexisting.title',
|
||||
{ defaultMessage: 'No blocklists entries exist' }
|
||||
|
|
|
@ -104,6 +104,14 @@ export const POLICY_ARTIFACT_EVENT_FILTERS_LABELS = Object.freeze({
|
|||
defaultMessage: 'Manage event filters',
|
||||
}
|
||||
),
|
||||
emptyUnassignedNoPrivilegesMessage: (policyName: string): string =>
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.endpoint.policy.eventFilters.empty.unassigned.noPrivileges.content',
|
||||
{
|
||||
defaultMessage: 'There are currently no event filters assigned to {policyName}',
|
||||
values: { policyName },
|
||||
}
|
||||
),
|
||||
emptyUnexistingTitle: i18n.translate(
|
||||
'xpack.securitySolution.endpoint.policy.eventFilters.empty.unexisting.title',
|
||||
{ defaultMessage: 'No event filters exist' }
|
||||
|
|
|
@ -110,6 +110,15 @@ export const POLICY_ARTIFACT_HOST_ISOLATION_EXCEPTIONS_LABELS = Object.freeze({
|
|||
defaultMessage: 'Manage host isolation exceptions',
|
||||
}
|
||||
),
|
||||
emptyUnassignedNoPrivilegesMessage: (policyName: string): string =>
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.endpoint.policy.hostIsolationException.empty.unassigned.noPrivileges.content',
|
||||
{
|
||||
defaultMessage:
|
||||
'There are currently no host isolation exceptions assigned to {policyName}.',
|
||||
values: { policyName },
|
||||
}
|
||||
),
|
||||
emptyUnexistingTitle: i18n.translate(
|
||||
'xpack.securitySolution.endpoint.policy.hostIsolationException.empty.unexisting.title',
|
||||
{ defaultMessage: 'No host isolation exceptions exist' }
|
||||
|
|
|
@ -24,7 +24,7 @@ import {
|
|||
getBlocklistsListPath,
|
||||
getPolicyBlocklistsPath,
|
||||
} from '../../../../common/routing';
|
||||
import { useHttp } from '../../../../../common/lib/kibana';
|
||||
import { useHttp, useToasts } from '../../../../../common/lib/kibana';
|
||||
import { ManagementPageLoader } from '../../../../components/management_page_loader';
|
||||
import {
|
||||
isOnHostIsolationExceptionsView,
|
||||
|
@ -51,7 +51,6 @@ import { SEARCHABLE_FIELDS as EVENT_FILTERS_SEARCHABLE_FIELDS } from '../../../e
|
|||
import { SEARCHABLE_FIELDS as HOST_ISOLATION_EXCEPTIONS_SEARCHABLE_FIELDS } from '../../../host_isolation_exceptions/constants';
|
||||
import { SEARCHABLE_FIELDS as BLOCKLISTS_SEARCHABLE_FIELDS } from '../../../blocklist/constants';
|
||||
import type { PolicyDetailsRouteState } from '../../../../../../common/endpoint/types';
|
||||
import { useListArtifact } from '../../../../hooks/artifacts';
|
||||
|
||||
enum PolicyTabKeys {
|
||||
SETTINGS = 'settings',
|
||||
|
@ -70,40 +69,57 @@ interface PolicyTab {
|
|||
export const PolicyTabs = React.memo(() => {
|
||||
const history = useHistory();
|
||||
const http = useHttp();
|
||||
const toasts = useToasts();
|
||||
|
||||
const isInSettingsTab = usePolicyDetailsSelector(isOnPolicyFormView);
|
||||
const isInTrustedAppsTab = usePolicyDetailsSelector(isOnPolicyTrustedAppsView);
|
||||
const isInEventFilters = usePolicyDetailsSelector(isOnPolicyEventFiltersView);
|
||||
const isInEventFiltersTab = usePolicyDetailsSelector(isOnPolicyEventFiltersView);
|
||||
const isInHostIsolationExceptionsTab = usePolicyDetailsSelector(isOnHostIsolationExceptionsView);
|
||||
const isInBlocklistsTab = usePolicyDetailsSelector(isOnBlocklistsView);
|
||||
const policyId = usePolicyDetailsSelector(policyIdFromParams);
|
||||
const policyItem = usePolicyDetailsSelector(policyDetails);
|
||||
const privileges = useUserPrivileges().endpointPrivileges;
|
||||
const {
|
||||
canReadTrustedApplications,
|
||||
canWriteTrustedApplications,
|
||||
canReadEventFilters,
|
||||
canWriteEventFilters,
|
||||
canReadHostIsolationExceptions,
|
||||
canWriteHostIsolationExceptions,
|
||||
canReadBlocklist,
|
||||
canWriteBlocklist,
|
||||
loading: privilegesLoading,
|
||||
} = useUserPrivileges().endpointPrivileges;
|
||||
const { state: routeState = {} } = useLocation<PolicyDetailsRouteState>();
|
||||
|
||||
const allPolicyHostIsolationExceptionsListRequest = useListArtifact(
|
||||
HostIsolationExceptionsApiClient.getInstance(http),
|
||||
{
|
||||
page: 1,
|
||||
perPage: 100,
|
||||
policies: [policyId, 'all'],
|
||||
},
|
||||
HOST_ISOLATION_EXCEPTIONS_SEARCHABLE_FIELDS,
|
||||
{
|
||||
enabled: !privileges.loading && !privileges.canIsolateHost,
|
||||
}
|
||||
);
|
||||
|
||||
const canSeeHostIsolationExceptions =
|
||||
privileges.canIsolateHost ||
|
||||
(allPolicyHostIsolationExceptionsListRequest.isFetched &&
|
||||
allPolicyHostIsolationExceptionsListRequest.data?.total !== 0);
|
||||
|
||||
// move the use out of this route if they can't access it
|
||||
// move the user out of this route if they can't access it
|
||||
useEffect(() => {
|
||||
if (isInHostIsolationExceptionsTab && !canSeeHostIsolationExceptions) {
|
||||
if (
|
||||
(isInTrustedAppsTab && !canReadTrustedApplications) ||
|
||||
(isInEventFiltersTab && !canReadEventFilters) ||
|
||||
(isInHostIsolationExceptionsTab && !canReadHostIsolationExceptions) ||
|
||||
(isInBlocklistsTab && !canReadBlocklist)
|
||||
) {
|
||||
history.replace(getPolicyDetailPath(policyId));
|
||||
toasts.addDanger(
|
||||
i18n.translate('xpack.securitySolution.policyDetails.missingArtifactAccess', {
|
||||
defaultMessage:
|
||||
'You do not have the required Kibana permissions to use the given artifact.',
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [canSeeHostIsolationExceptions, history, isInHostIsolationExceptionsTab, policyId]);
|
||||
}, [
|
||||
canReadBlocklist,
|
||||
canReadEventFilters,
|
||||
canReadHostIsolationExceptions,
|
||||
canReadTrustedApplications,
|
||||
history,
|
||||
isInBlocklistsTab,
|
||||
isInEventFiltersTab,
|
||||
isInHostIsolationExceptionsTab,
|
||||
isInTrustedAppsTab,
|
||||
policyId,
|
||||
toasts,
|
||||
]);
|
||||
|
||||
const getTrustedAppsApiClientInstance = useCallback(
|
||||
() => TrustedAppsApiClient.getInstance(http),
|
||||
|
@ -183,45 +199,57 @@ export const PolicyTabs = React.memo(() => {
|
|||
</>
|
||||
),
|
||||
},
|
||||
[PolicyTabKeys.TRUSTED_APPS]: {
|
||||
id: PolicyTabKeys.TRUSTED_APPS,
|
||||
name: i18n.translate('xpack.securitySolution.endpoint.policy.details.tabs.trustedApps', {
|
||||
defaultMessage: 'Trusted applications',
|
||||
}),
|
||||
content: (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<PolicyArtifactsLayout
|
||||
policyItem={policyItem}
|
||||
labels={trustedAppsLabels}
|
||||
getExceptionsListApiClient={getTrustedAppsApiClientInstance}
|
||||
searchableFields={TRUSTED_APPS_SEARCHABLE_FIELDS}
|
||||
getArtifactPath={getTrustedAppsListPath}
|
||||
getPolicyArtifactsPath={getPolicyDetailsArtifactsListPath}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
[PolicyTabKeys.EVENT_FILTERS]: {
|
||||
id: PolicyTabKeys.EVENT_FILTERS,
|
||||
name: i18n.translate('xpack.securitySolution.endpoint.policy.details.tabs.eventFilters', {
|
||||
defaultMessage: 'Event filters',
|
||||
}),
|
||||
content: (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<PolicyArtifactsLayout
|
||||
policyItem={policyItem}
|
||||
labels={eventFiltersLabels}
|
||||
getExceptionsListApiClient={getEventFiltersApiClientInstance}
|
||||
searchableFields={EVENT_FILTERS_SEARCHABLE_FIELDS}
|
||||
getArtifactPath={getEventFiltersListPath}
|
||||
getPolicyArtifactsPath={getPolicyEventFiltersPath}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
[PolicyTabKeys.HOST_ISOLATION_EXCEPTIONS]: canSeeHostIsolationExceptions
|
||||
[PolicyTabKeys.TRUSTED_APPS]: canReadTrustedApplications
|
||||
? {
|
||||
id: PolicyTabKeys.TRUSTED_APPS,
|
||||
name: i18n.translate(
|
||||
'xpack.securitySolution.endpoint.policy.details.tabs.trustedApps',
|
||||
{
|
||||
defaultMessage: 'Trusted applications',
|
||||
}
|
||||
),
|
||||
content: (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<PolicyArtifactsLayout
|
||||
policyItem={policyItem}
|
||||
labels={trustedAppsLabels}
|
||||
getExceptionsListApiClient={getTrustedAppsApiClientInstance}
|
||||
searchableFields={TRUSTED_APPS_SEARCHABLE_FIELDS}
|
||||
getArtifactPath={getTrustedAppsListPath}
|
||||
getPolicyArtifactsPath={getPolicyDetailsArtifactsListPath}
|
||||
canWriteArtifact={canWriteTrustedApplications}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
[PolicyTabKeys.EVENT_FILTERS]: canReadEventFilters
|
||||
? {
|
||||
id: PolicyTabKeys.EVENT_FILTERS,
|
||||
name: i18n.translate(
|
||||
'xpack.securitySolution.endpoint.policy.details.tabs.eventFilters',
|
||||
{
|
||||
defaultMessage: 'Event filters',
|
||||
}
|
||||
),
|
||||
content: (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<PolicyArtifactsLayout
|
||||
policyItem={policyItem}
|
||||
labels={eventFiltersLabels}
|
||||
getExceptionsListApiClient={getEventFiltersApiClientInstance}
|
||||
searchableFields={EVENT_FILTERS_SEARCHABLE_FIELDS}
|
||||
getArtifactPath={getEventFiltersListPath}
|
||||
getPolicyArtifactsPath={getPolicyEventFiltersPath}
|
||||
canWriteArtifact={canWriteEventFilters}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
[PolicyTabKeys.HOST_ISOLATION_EXCEPTIONS]: canReadHostIsolationExceptions
|
||||
? {
|
||||
id: PolicyTabKeys.HOST_ISOLATION_EXCEPTIONS,
|
||||
name: i18n.translate(
|
||||
|
@ -240,40 +268,49 @@ export const PolicyTabs = React.memo(() => {
|
|||
searchableFields={HOST_ISOLATION_EXCEPTIONS_SEARCHABLE_FIELDS}
|
||||
getArtifactPath={getHostIsolationExceptionsListPath}
|
||||
getPolicyArtifactsPath={getPolicyHostIsolationExceptionsPath}
|
||||
externalPrivileges={privileges.canIsolateHost}
|
||||
canWriteArtifact={canWriteHostIsolationExceptions}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
[PolicyTabKeys.BLOCKLISTS]: canReadBlocklist
|
||||
? {
|
||||
id: PolicyTabKeys.BLOCKLISTS,
|
||||
name: i18n.translate('xpack.securitySolution.endpoint.policy.details.tabs.blocklists', {
|
||||
defaultMessage: 'Blocklist',
|
||||
}),
|
||||
content: (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<PolicyArtifactsLayout
|
||||
policyItem={policyItem}
|
||||
labels={blocklistsLabels}
|
||||
getExceptionsListApiClient={getBlocklistsApiClientInstance}
|
||||
searchableFields={BLOCKLISTS_SEARCHABLE_FIELDS}
|
||||
getArtifactPath={getBlocklistsListPath}
|
||||
getPolicyArtifactsPath={getPolicyBlocklistsPath}
|
||||
canWriteArtifact={canWriteBlocklist}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
[PolicyTabKeys.BLOCKLISTS]: {
|
||||
id: PolicyTabKeys.BLOCKLISTS,
|
||||
name: i18n.translate('xpack.securitySolution.endpoint.policy.details.tabs.blocklists', {
|
||||
defaultMessage: 'Blocklist',
|
||||
}),
|
||||
content: (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<PolicyArtifactsLayout
|
||||
policyItem={policyItem}
|
||||
labels={blocklistsLabels}
|
||||
getExceptionsListApiClient={getBlocklistsApiClientInstance}
|
||||
searchableFields={BLOCKLISTS_SEARCHABLE_FIELDS}
|
||||
getArtifactPath={getBlocklistsListPath}
|
||||
getPolicyArtifactsPath={getPolicyBlocklistsPath}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
}, [
|
||||
canSeeHostIsolationExceptions,
|
||||
canReadTrustedApplications,
|
||||
canWriteTrustedApplications,
|
||||
canReadEventFilters,
|
||||
canWriteEventFilters,
|
||||
canReadHostIsolationExceptions,
|
||||
canWriteHostIsolationExceptions,
|
||||
canReadBlocklist,
|
||||
canWriteBlocklist,
|
||||
getEventFiltersApiClientInstance,
|
||||
getHostIsolationExceptionsApiClientInstance,
|
||||
getBlocklistsApiClientInstance,
|
||||
getTrustedAppsApiClientInstance,
|
||||
policyItem,
|
||||
privileges.canIsolateHost,
|
||||
]);
|
||||
|
||||
// convert tabs object into an array EuiTabbedContent can understand
|
||||
|
@ -290,7 +327,7 @@ export const PolicyTabs = React.memo(() => {
|
|||
selectedTab = tabs[PolicyTabKeys.SETTINGS];
|
||||
} else if (isInTrustedAppsTab) {
|
||||
selectedTab = tabs[PolicyTabKeys.TRUSTED_APPS];
|
||||
} else if (isInEventFilters) {
|
||||
} else if (isInEventFiltersTab) {
|
||||
selectedTab = tabs[PolicyTabKeys.EVENT_FILTERS];
|
||||
} else if (isInHostIsolationExceptionsTab) {
|
||||
selectedTab = tabs[PolicyTabKeys.HOST_ISOLATION_EXCEPTIONS];
|
||||
|
@ -303,7 +340,7 @@ export const PolicyTabs = React.memo(() => {
|
|||
tabs,
|
||||
isInSettingsTab,
|
||||
isInTrustedAppsTab,
|
||||
isInEventFilters,
|
||||
isInEventFiltersTab,
|
||||
isInHostIsolationExceptionsTab,
|
||||
isInBlocklistsTab,
|
||||
]);
|
||||
|
@ -334,11 +371,8 @@ export const PolicyTabs = React.memo(() => {
|
|||
);
|
||||
|
||||
// show loader for privileges validation
|
||||
if (
|
||||
isInHostIsolationExceptionsTab &&
|
||||
(privileges.loading || allPolicyHostIsolationExceptionsListRequest.isFetching)
|
||||
) {
|
||||
return <ManagementPageLoader data-test-subj="policyHostIsolationExceptionsTabLoading" />;
|
||||
if (privilegesLoading) {
|
||||
return <ManagementPageLoader data-test-subj="privilegesLoading" />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -104,6 +104,14 @@ export const POLICY_ARTIFACT_TRUSTED_APPS_LABELS = Object.freeze({
|
|||
defaultMessage: 'Manage trusted applications',
|
||||
}
|
||||
),
|
||||
emptyUnassignedNoPrivilegesMessage: (policyName: string): string =>
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.endpoint.policy.trustedApps.empty.unassigned.noPrivileges.content',
|
||||
{
|
||||
defaultMessage: 'There are currently no trusted applications assigned to {policyName}.',
|
||||
values: { policyName },
|
||||
}
|
||||
),
|
||||
emptyUnexistingTitle: i18n.translate(
|
||||
'xpack.securitySolution.endpoint.policy.trustedApps.empty.unexisting.title',
|
||||
{ defaultMessage: 'No trusted applications exist' }
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue