Skip to content

Commit

Permalink
[Security Solution] Allow to remove a host isolation exception from a…
Browse files Browse the repository at this point in the history
… policy (elastic#121280)
  • Loading branch information
academo authored Dec 21, 2021
1 parent 85c9136 commit 123980d
Show file tree
Hide file tree
Showing 4 changed files with 351 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* 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 { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import { act, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import uuid from 'uuid';
import { getExceptionListItemSchemaMock } from '../../../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
import {
AppContextTestRender,
createAppRootMockRenderer,
} from '../../../../../../common/mock/endpoint';
import { getPolicyHostIsolationExceptionsPath } from '../../../../../common/routing';
import { updateOneHostIsolationExceptionItem } from '../../../../host_isolation_exceptions/service';
import { PolicyHostIsolationExceptionsDeleteModal } from './delete_modal';

jest.mock('../../../../host_isolation_exceptions/service');

const updateOneHostIsolationExceptionItemMock = updateOneHostIsolationExceptionItem as jest.Mock;

describe('Policy details host isolation exceptions delete modal', () => {
let policyId: string;
let render: () => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<typeof render>;
let history: AppContextTestRender['history'];
let mockedContext: AppContextTestRender;
let exception: ExceptionListItemSchema;
let onCancel: () => void;

beforeEach(() => {
policyId = uuid.v4();
mockedContext = createAppRootMockRenderer();
exception = getExceptionListItemSchemaMock();
onCancel = jest.fn();
updateOneHostIsolationExceptionItemMock.mockClear();
({ history } = mockedContext);
render = () =>
(renderResult = mockedContext.render(
<PolicyHostIsolationExceptionsDeleteModal
policyId={policyId}
exception={exception}
onCancel={onCancel}
/>
));

act(() => {
history.push(getPolicyHostIsolationExceptionsPath(policyId));
});
});

it('should render with enabled buttons', () => {
render();
expect(renderResult.getByTestId('confirmModalCancelButton')).toBeEnabled();
expect(renderResult.getByTestId('confirmModalConfirmButton')).toBeEnabled();
});

it('should disable the submit button while deleting ', async () => {
updateOneHostIsolationExceptionItemMock.mockImplementation(() => {
return new Promise((resolve) => setImmediate(resolve));
});
render();
const confirmButton = renderResult.getByTestId('confirmModalConfirmButton');
userEvent.click(confirmButton);

await waitFor(() => {
expect(confirmButton).toBeDisabled();
});
});

it('should call the API with the removed policy from the exception tags', async () => {
exception.tags = ['policy:1234', 'policy:4321', `policy:${policyId}`, 'not-a-policy-tag'];
render();
const confirmButton = renderResult.getByTestId('confirmModalConfirmButton');
userEvent.click(confirmButton);

await waitFor(() => {
expect(updateOneHostIsolationExceptionItemMock).toHaveBeenCalledWith(
mockedContext.coreStart.http,
expect.objectContaining({
id: exception.id,
tags: ['policy:1234', 'policy:4321', 'not-a-policy-tag'],
})
);
});
});

it('should show a success toast if the operation was success', async () => {
updateOneHostIsolationExceptionItemMock.mockReturnValue('all good');
render();
const confirmButton = renderResult.getByTestId('confirmModalConfirmButton');
userEvent.click(confirmButton);

await waitFor(() => {
expect(updateOneHostIsolationExceptionItemMock).toHaveBeenCalled();
});

expect(mockedContext.coreStart.notifications.toasts.addSuccess).toHaveBeenCalled();
});

it('should show an error toast if the operation failed', async () => {
const error = new Error('the server is too far away');
updateOneHostIsolationExceptionItemMock.mockRejectedValue(error);
render();
const confirmButton = renderResult.getByTestId('confirmModalConfirmButton');
userEvent.click(confirmButton);

await waitFor(() => {
expect(updateOneHostIsolationExceptionItemMock).toHaveBeenCalled();
});

expect(mockedContext.coreStart.notifications.toasts.addError).toHaveBeenCalledWith(error, {
title: 'Error while attempt to remove host isolation exception',
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* 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 { EuiCallOut, EuiConfirmModal, EuiSpacer, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import React from 'react';
import { useMutation, useQueryClient } from 'react-query';
import { useHttp, useToasts } from '../../../../../../common/lib/kibana';
import { ServerApiError } from '../../../../../../common/types';
import { updateOneHostIsolationExceptionItem } from '../../../../host_isolation_exceptions/service';

export const PolicyHostIsolationExceptionsDeleteModal = ({
policyId,
exception,
onCancel,
}: {
policyId: string;
exception: ExceptionListItemSchema;
onCancel: () => void;
}) => {
const toasts = useToasts();
const http = useHttp();
const queryClient = useQueryClient();

const onDeleteError = (error: ServerApiError) => {
toasts.addError(error as unknown as Error, {
title: i18n.translate(
'xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.removeDialog.errorToastTitle',
{
defaultMessage: 'Error while attempt to remove host isolation exception',
}
),
});
onCancel();
};

const onDeleteSuccess = () => {
queryClient.invalidateQueries(['endpointSpecificPolicies']);
queryClient.invalidateQueries(['hostIsolationExceptions']);
toasts.addSuccess({
title: i18n.translate(
'xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.removeDialog.successToastTitle',
{ defaultMessage: 'Successfully removed' }
),
text: i18n.translate(
'xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.removeDialog.successToastText',
{
defaultMessage: '"{exception}" has been removed from policy',
values: { exception: exception.name },
}
),
});
onCancel();
};

const mutation = useMutation(
async () => {
const modifiedException = {
...exception,
tags: exception.tags.filter((tag) => tag !== `policy:${policyId}`),
};
return updateOneHostIsolationExceptionItem(http, modifiedException);
},
{
onSuccess: onDeleteSuccess,
onError: onDeleteError,
}
);

const handleModalConfirm = () => {
mutation.mutate();
};

const handleCancel = () => {
if (!mutation.isLoading) {
onCancel();
}
};

return (
<EuiConfirmModal
onCancel={handleCancel}
onConfirm={handleModalConfirm}
title={i18n.translate(
'xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.removeDialog.title',
{ defaultMessage: 'Remove host isolation exception from policy' }
)}
cancelButtonText={i18n.translate(
'xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.removeDialog.cancelLabel',
{ defaultMessage: 'Cancel' }
)}
confirmButtonText={i18n.translate(
'xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.removeDialog.confirmLabel',
{
defaultMessage: 'Remove from policy',
}
)}
isLoading={mutation.isLoading}
data-test-subj={'remove-from-policy-dialog'}
>
<EuiCallOut color="warning" iconType="help">
<p>
<FormattedMessage
id="xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.removeDialog.messageCallout"
defaultMessage="This host isolation exception will be removed only from this policy and can still be found and managed from the host isolation exceptions page."
/>
</p>
</EuiCallOut>

<EuiSpacer />

<EuiText size="s">
<p>
<FormattedMessage
id="xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.removeDialog.message"
defaultMessage="Are you sure you wish to continue?"
/>
</p>
</EuiText>
</EuiConfirmModal>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@

import { FoundExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import { act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import uuid from 'uuid';
import { getPolicyHostIsolationExceptionsPath } from '../../../../../common/routing';
import { getExceptionListItemSchemaMock } from '../../../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
import { getFoundExceptionListItemSchemaMock } from '../../../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock';
import {
AppContextTestRender,
createAppRootMockRenderer,
} from '../../../../../../common/mock/endpoint';
import { getPolicyHostIsolationExceptionsPath } from '../../../../../common/routing';
import { PolicyHostIsolationExceptionsList } from './list';
import userEvent from '@testing-library/user-event';

const emptyList = {
data: [],
Expand Down Expand Up @@ -91,4 +92,56 @@ describe('Policy details host isolation exceptions tab', () => {
userEvent.type(renderResult.getByTestId('searchField'), 'search me{enter}');
expect(history.location.search).toBe('?filter=search%20me');
});

it('should disable the "remove from policy" option to global exceptions', () => {
const testException = getExceptionListItemSchemaMock({ tags: ['policy:all'] });
const exceptions = {
...emptyList,
data: [testException],
total: 1,
};
render(exceptions);
// click the actions button
userEvent.click(
renderResult.getByTestId('hostIsolationExceptions-collapsed-list-card-header-actions-button')
);
expect(renderResult.getByTestId('remove-from-policy-action')).toBeDisabled();
});

it('should enable the "remove from policy" option to policy-specific exceptions ', () => {
const testException = getExceptionListItemSchemaMock({
tags: [`policy:${policyId}`, 'policy:1234', 'not-a-policy-tag'],
});
const exceptions = {
...emptyList,
data: [testException],
total: 1,
};
render(exceptions);
// click the actions button
userEvent.click(
renderResult.getByTestId('hostIsolationExceptions-collapsed-list-card-header-actions-button')
);
expect(renderResult.getByTestId('remove-from-policy-action')).toBeEnabled();
});

it('should render the delete dialog when the "remove from policy" button is clicked', () => {
const testException = getExceptionListItemSchemaMock({
tags: [`policy:${policyId}`, 'policy:1234', 'not-a-policy-tag'],
});
const exceptions = {
...emptyList,
data: [testException],
total: 1,
};
render(exceptions);
// click the actions button
userEvent.click(
renderResult.getByTestId('hostIsolationExceptions-collapsed-list-card-header-actions-button')
);
userEvent.click(renderResult.getByTestId('remove-from-policy-action'));

// check the dialog is there
expect(renderResult.getByTestId('remove-from-policy-dialog')).toBeTruthy();
});
});
Loading

0 comments on commit 123980d

Please sign in to comment.