[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:
David Sánchez 2022-12-20 15:00:14 +01:00 committed by GitHub
parent 6b29787e6f
commit 886289d206
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 336 additions and 191 deletions

View file

@ -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,

View file

@ -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>

View file

@ -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>

View file

@ -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({

View file

@ -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();
});
});
});

View file

@ -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}
/>

View file

@ -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}
/>

View file

@ -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,

View file

@ -67,6 +67,7 @@ export type PolicyArtifactsPageRequiredLabels = Pick<
| 'emptyUnassignedMessage'
| 'emptyUnassignedPrimaryActionButtonTitle'
| 'emptyUnassignedSecondaryActionButtonTitle'
| 'emptyUnassignedNoPrivilegesMessage'
| 'emptyUnexistingTitle'
| 'emptyUnexistingMessage'
| 'emptyUnexistingPrimaryActionButtonTitle'

View file

@ -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.'
);
}
);
});
});
});

View file

@ -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' }

View file

@ -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' }

View file

@ -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' }

View file

@ -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 (

View file

@ -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' }