[SECURITY_SOLUTION][ENDPOINT] Add ability to view Trusted Apps from Ingest Integration Policy Edit page (#78854) (#79137)

* Refactor Callout shown in Ingest Edit Endpoint Integration Policy that display actions menu
* Add `backComponent` to `<HeaderPage>` to allow for custom back buttons
* Back button displayed on Trusted Apps List when route state is defined
This commit is contained in:
Paul Tavares 2020-10-01 13:29:05 -04:00 committed by GitHub
parent e239807b66
commit d0d80e0fbb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 325 additions and 62 deletions

View file

@ -5,6 +5,7 @@
*/
import { TypeOf } from '@kbn/config-schema';
import { ApplicationStart } from 'kibana/public';
import {
DeleteTrustedAppsRequestSchema,
GetTrustedAppsRequestSchema,
@ -65,3 +66,15 @@ export type TrustedApp = NewTrustedApp & {
created_at: string;
created_by: string;
};
/**
* Supported React-Router state for the Trusted Apps List page
*/
export interface TrustedAppsListPageRouteState {
/** Where the user should be redirected to when the `Back` button is clicked */
onBackButtonNavigateTo: Parameters<ApplicationStart['navigateToApp']>;
/** The URL for the `Back` button */
backButtonUrl?: string;
/** The label for the button */
backButtonLabel?: string;
}

View file

@ -71,6 +71,8 @@ interface BackOptions {
export interface HeaderPageProps extends HeaderProps {
backOptions?: BackOptions;
/** A component to be displayed as the back button. Used only if `backOption` is not defined */
backComponent?: React.ReactNode;
badgeOptions?: BadgeOptions;
children?: React.ReactNode;
draggableArguments?: DraggableArguments;
@ -83,6 +85,7 @@ export interface HeaderPageProps extends HeaderProps {
const HeaderPageComponent: React.FC<HeaderPageProps> = ({
backOptions,
backComponent,
badgeOptions,
border,
children,
@ -123,6 +126,8 @@ const HeaderPageComponent: React.FC<HeaderPageProps> = ({
</LinkBack>
)}
{!backOptions && backComponent && <>{backComponent}</>}
{titleNode || (
<Title
draggableArguments={draggableArguments}

View file

@ -27,10 +27,11 @@ interface AdministrationListPageProps {
title: React.ReactNode;
subtitle: React.ReactNode;
actions?: React.ReactNode;
headerBackComponent?: React.ReactNode;
}
export const AdministrationListPage: FC<AdministrationListPageProps & CommonProps> = memo(
({ beta, title, subtitle, actions, children, ...otherProps }) => {
({ beta, title, subtitle, actions, children, headerBackComponent, ...otherProps }) => {
const badgeOptions = !beta ? undefined : { beta: true, text: BETA_BADGE_LABEL };
return (
@ -39,6 +40,7 @@ export const AdministrationListPage: FC<AdministrationListPageProps & CommonProp
hideSourcerer={true}
title={title}
subtitle={subtitle}
backComponent={headerBackComponent}
badgeOptions={badgeOptions}
>
{actions}

View file

@ -4,17 +4,34 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { memo, useMemo } from 'react';
import React, { memo, useCallback, useMemo, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiCallOut, EuiText, EuiSpacer } from '@elastic/eui';
import { LinkToApp } from '../../../../../common/components/endpoint/link_to_app';
import {
EuiCallOut,
EuiText,
EuiSpacer,
EuiFlexGroup,
EuiFlexItem,
EuiContextMenuPanel,
EuiPopover,
EuiButton,
EuiContextMenuItem,
EuiContextMenuPanelProps,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import {
CustomConfigurePackagePolicyContent,
CustomConfigurePackagePolicyProps,
pagePathGetters,
} from '../../../../../../../ingest_manager/public';
import { getPolicyDetailPath } from '../../../../common/routing';
import { getPolicyDetailPath, getTrustedAppsListPath } from '../../../../common/routing';
import { MANAGEMENT_APP_ID } from '../../../../common/constants';
import { PolicyDetailsRouteState } from '../../../../../../common/endpoint/types';
import {
PolicyDetailsRouteState,
TrustedAppsListPageRouteState,
} from '../../../../../../common/endpoint/types';
import { useKibana } from '../../../../../common/lib/kibana';
import { useNavigateToAppEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_to_app_event_handler';
/**
* Exports Endpoint-specific package policy instructions
@ -26,27 +43,6 @@ export const ConfigureEndpointPackagePolicy = memo<CustomConfigurePackagePolicyC
packagePolicyId,
packagePolicy: { policy_id: agentPolicyId },
}: CustomConfigurePackagePolicyProps) => {
let policyUrl = '';
if (from === 'edit' && packagePolicyId) {
// Cannot use formalUrl here since the code is called in Ingest, which does not use redux
policyUrl = getPolicyDetailPath(packagePolicyId);
}
const policyDetailRouteState = useMemo((): undefined | PolicyDetailsRouteState => {
if (from !== 'edit') {
return undefined;
}
const navigateTo: PolicyDetailsRouteState['onSaveNavigateTo'] &
PolicyDetailsRouteState['onCancelNavigateTo'] = [
'ingestManager',
{ path: `#/policies/${agentPolicyId}/edit-integration/${packagePolicyId}` },
];
return {
onSaveNavigateTo: navigateTo,
onCancelNavigateTo: navigateTo,
};
}, [agentPolicyId, from, packagePolicyId]);
return (
<>
<EuiSpacer size="m" />
@ -55,39 +51,149 @@ export const ConfigureEndpointPackagePolicy = memo<CustomConfigurePackagePolicyC
iconType="iInCircle"
>
<EuiText size="s">
<p>
{from === 'edit' ? (
<FormattedMessage
id="xpack.securitySolution.endpoint.ingestManager.editPackagePolicy.endpointConfiguration"
defaultMessage="Click {advancedConfigOptionsLink} to edit advanced configuration options."
values={{
advancedConfigOptionsLink: (
<LinkToApp
data-test-subj="editLinkToPolicyDetails"
appId={MANAGEMENT_APP_ID}
appPath={policyUrl}
appState={policyDetailRouteState}
>
<FormattedMessage
id="xpack.securitySolution.endpoint.ingestManager.editPackagePolicy.endpointConfigurationLink"
defaultMessage="here"
/>
</LinkToApp>
),
}}
{from === 'edit' ? (
<>
<EditFlowMessage
agentPolicyId={agentPolicyId}
integrationPolicyId={packagePolicyId!}
/>
) : (
</>
) : (
<p>
<FormattedMessage
id="xpack.securitySolution.endpoint.ingestManager.createPackagePolicy.endpointConfiguration"
defaultMessage="We'll save your integration with our recommended defaults. You can change this later by editing the Endpoint Security integration within your agent policy."
/>
)}
</p>
</p>
)}
</EuiText>
</EuiCallOut>
</>
);
}
);
ConfigureEndpointPackagePolicy.displayName = 'ConfigureEndpointPackagePolicy';
const EditFlowMessage = memo<{
agentPolicyId: string;
integrationPolicyId: string;
}>(({ agentPolicyId, integrationPolicyId }) => {
const {
services: {
application: { getUrlForApp },
},
} = useKibana();
const [isMenuOpen, setIsMenuOpen] = useState<boolean>(false);
const navigateBackToIngest = useMemo<
PolicyDetailsRouteState['onSaveNavigateTo'] &
PolicyDetailsRouteState['onCancelNavigateTo'] &
TrustedAppsListPageRouteState['onBackButtonNavigateTo']
>(() => {
return [
'ingestManager',
{
path: `#${pagePathGetters.edit_integration({
policyId: agentPolicyId,
packagePolicyId: integrationPolicyId!,
})}`,
},
];
}, [agentPolicyId, integrationPolicyId]);
const handleClosePopup = useCallback(() => setIsMenuOpen(false), []);
const handleSecurityPolicyAction = useNavigateToAppEventHandler<PolicyDetailsRouteState>(
MANAGEMENT_APP_ID,
{
path: getPolicyDetailPath(integrationPolicyId),
state: {
onSaveNavigateTo: navigateBackToIngest,
onCancelNavigateTo: navigateBackToIngest,
},
}
);
const handleTrustedAppsAction = useNavigateToAppEventHandler<TrustedAppsListPageRouteState>(
MANAGEMENT_APP_ID,
{
path: getTrustedAppsListPath(),
state: {
backButtonUrl: navigateBackToIngest[1]?.path
? `${getUrlForApp('ingestManager')}${navigateBackToIngest[1].path}`
: undefined,
onBackButtonNavigateTo: navigateBackToIngest,
backButtonLabel: i18n.translate(
'xpack.securitySolution.endpoint.ingestManager.editPackagePolicy.trustedAppsMessageReturnBackLabel',
{ defaultMessage: 'Back to Edit Integration' }
),
},
}
);
const menuButton = useMemo(() => {
return (
<EuiButton
size="s"
iconType="arrowDown"
iconSide="right"
onClick={() => setIsMenuOpen((prevState) => !prevState)}
data-test-subj="endpointActions"
>
<FormattedMessage
id="xpack.securitySolution.endpoint.ingestManager.editPackagePolicy.menuButton"
defaultMessage="Actions"
/>
</EuiButton>
);
}, []);
const actionItems = useMemo<EuiContextMenuPanelProps['items']>(() => {
return [
<EuiContextMenuItem
key="policyDetails"
onClick={handleSecurityPolicyAction}
data-test-subj="securityPolicy"
>
<FormattedMessage
id="xpack.securitySolution.endpoint.ingestManager.editPackagePolicy.actionSecurityPolicy"
defaultMessage="Edit Security Policy"
/>
</EuiContextMenuItem>,
<EuiContextMenuItem
key="trustedApps"
onClick={handleTrustedAppsAction}
data-test-subj="trustedAppsAction"
>
<FormattedMessage
id="xpack.securitySolution.endpoint.ingestManager.editPackagePolicy.actionTrustedApps"
defaultMessage="View Trusted Applications"
/>
</EuiContextMenuItem>,
];
}, [handleSecurityPolicyAction, handleTrustedAppsAction]);
return (
<EuiFlexGroup>
<EuiFlexItem>
<FormattedMessage
id="xpack.securitySolution.endpoint.ingestManager.editPackagePolicy.message"
defaultMessage="More advanced configuration options can be found by selecting an action from the menu"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPopover
button={menuButton}
isOpen={isMenuOpen}
closePopover={handleClosePopup}
anchorPosition="downRight"
panelPaddingSize="s"
>
<EuiContextMenuPanel data-test-subj="endpointActionsMenuPanel" items={actionItems} />
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>
);
});
EditFlowMessage.displayName = 'EditFlowMessage';

View file

@ -3,10 +3,11 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { memo, useCallback } from 'react';
import React, { memo, useCallback, useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiButton } from '@elastic/eui';
import { useHistory } from 'react-router-dom';
import { EuiButton, EuiButtonEmpty } from '@elastic/eui';
import { useHistory, useLocation } from 'react-router-dom';
import styled from 'styled-components';
import { AdministrationListPage } from '../../../components/administration_list_page';
import { TrustedAppsList } from './trusted_apps_list';
import { TrustedAppDeletionDialog } from './trusted_app_deletion_dialog';
@ -15,9 +16,12 @@ import { CreateTrustedAppFlyout } from './components/create_trusted_app_flyout';
import { getTrustedAppsListPath } from '../../../common/routing';
import { useTrustedAppsSelector } from './hooks';
import { getListCurrentShowValue, getListUrlSearchParams } from '../store/selectors';
import { TrustedAppsListPageRouteState } from '../../../../../common/endpoint/types';
import { useNavigateToAppEventHandler } from '../../../../common/hooks/endpoint/use_navigate_to_app_event_handler';
export const TrustedAppsPage = memo(() => {
const history = useHistory();
const { state: routeState } = useLocation<TrustedAppsListPageRouteState | undefined>();
const urlParams = useTrustedAppsSelector(getListUrlSearchParams);
const showAddFlout = useTrustedAppsSelector(getListCurrentShowValue) === 'create';
const handleAddButtonClick = useCallback(() => {
@ -33,6 +37,15 @@ export const TrustedAppsPage = memo(() => {
history.push(getTrustedAppsListPath(paginationParamsOnly));
}, [history, urlParams]);
const backButton = useMemo(() => {
if (routeState && routeState.onBackButtonNavigateTo) {
return <BackToExternalAppButton {...routeState} />;
}
return null;
// FIXME: Route state is being deleted by some parent component
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const addButton = (
<EuiButton
fill
@ -50,6 +63,7 @@ export const TrustedAppsPage = memo(() => {
return (
<AdministrationListPage
data-test-subj="trustedAppsListPage"
beta={true}
title={
<FormattedMessage
@ -57,6 +71,7 @@ export const TrustedAppsPage = memo(() => {
defaultMessage="Trusted Applications"
/>
}
headerBackComponent={backButton}
subtitle={
<FormattedMessage
id="xpack.securitySolution.trustedapps.list.pageSubTitle"
@ -80,3 +95,43 @@ export const TrustedAppsPage = memo(() => {
});
TrustedAppsPage.displayName = 'TrustedAppsPage';
const EuiButtonEmptyStyled = styled(EuiButtonEmpty)`
margin-bottom: ${({ theme }) => theme.eui.euiSizeS};
.euiIcon {
width: ${({ theme }) => theme.eui.euiIconSizes.small};
height: ${({ theme }) => theme.eui.euiIconSizes.small};
}
.text {
font-size: ${({ theme }) => theme.eui.euiFontSizeXS};
}
`;
const BackToExternalAppButton = memo<TrustedAppsListPageRouteState>(
({ backButtonLabel, backButtonUrl, onBackButtonNavigateTo }) => {
const handleBackOnClick = useNavigateToAppEventHandler(...onBackButtonNavigateTo!);
return (
<EuiButtonEmptyStyled
flush="left"
size="xs"
iconType="arrowLeft"
href={backButtonUrl!}
onClick={handleBackOnClick}
textProps={{ className: 'text' }}
data-test-subj="backToOrigin"
>
{backButtonLabel || (
<FormattedMessage
id="xpack.securitySolution.trustedapps.list.backButton"
defaultMessage="Back"
/>
)}
</EuiButtonEmptyStyled>
);
}
);
BackToExternalAppButton.displayName = 'BackToExternalAppButton';

View file

@ -16,6 +16,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
'policy',
'endpointPageUtils',
'ingestManagerCreatePackagePolicy',
'trustedApps',
]);
const testSubjects = getService('testSubjects');
const policyTestResources = getService('policyTestResources');
@ -250,6 +251,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
});
});
describe('when on Ingest Policy Edit Package Policy page', async () => {
let policyInfo: PolicyTestResourceInfo;
beforeEach(async () => {
@ -265,16 +267,31 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await policyInfo.cleanup();
}
});
it('should show a link to Policy Details', async () => {
await testSubjects.existOrFail('editLinkToPolicyDetails');
it('should show callout', async () => {
await testSubjects.existOrFail('endpointPackagePolicy_edit');
});
it('should navigate to Policy Details when the link is clicked', async () => {
const linkToPolicy = await testSubjects.find('editLinkToPolicyDetails');
await linkToPolicy.click();
it('should show actions button with expected action items', async () => {
const actionsButton = await pageObjects.ingestManagerCreatePackagePolicy.findEndpointActionsButton();
await actionsButton.click();
const menuPanel = await testSubjects.find('endpointActionsMenuPanel');
const actionItems = await menuPanel.findAllByTagName<'button'>('button');
const expectedItems = ['Edit Security Policy', 'View Trusted Applications'];
for (const action of actionItems) {
const buttonText = await action.getVisibleText();
expect(buttonText).to.be(expectedItems.find((item) => item === buttonText));
}
});
it('should navigate to Policy Details when the edit security policy action is clicked', async () => {
await pageObjects.ingestManagerCreatePackagePolicy.selectEndpointAction('policy');
await pageObjects.policy.ensureIsOnDetailsPage();
});
it('should allow the user to navigate, edit, save Policy Details and be redirected back to ingest', async () => {
await (await testSubjects.find('editLinkToPolicyDetails')).click();
await pageObjects.ingestManagerCreatePackagePolicy.selectEndpointAction('policy');
await pageObjects.policy.ensureIsOnDetailsPage();
await pageObjects.endpointPageUtils.clickOnEuiCheckbox('policyWindowsEvent_dns');
await pageObjects.policy.confirmAndSave();
@ -282,11 +299,24 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await testSubjects.existOrFail('policyDetailsSuccessMessage');
await pageObjects.ingestManagerCreatePackagePolicy.ensureOnEditPageOrFail();
});
it('should navigate back to Ingest Policy Edit package page on click of cancel button', async () => {
await (await testSubjects.find('editLinkToPolicyDetails')).click();
await pageObjects.ingestManagerCreatePackagePolicy.selectEndpointAction('policy');
await (await pageObjects.policy.findCancelButton()).click();
await pageObjects.ingestManagerCreatePackagePolicy.ensureOnEditPageOrFail();
});
it('should navigate to Trusted Apps', async () => {
await pageObjects.ingestManagerCreatePackagePolicy.selectEndpointAction('trustedApps');
await pageObjects.trustedApps.ensureIsOnTrustedAppsListPage();
});
it('should show the back button on Trusted Apps Page and navigate back to fleet', async () => {
await pageObjects.ingestManagerCreatePackagePolicy.selectEndpointAction('trustedApps');
const backButton = await pageObjects.trustedApps.findTrustedAppsListPageBackButton();
await backButton.click();
await pageObjects.ingestManagerCreatePackagePolicy.ensureOnEditPageOrFail();
});
});
});
}

View file

@ -5,6 +5,7 @@
*/
import { FtrProviderContext } from '../ftr_provider_context';
import { WebElementWrapper } from '../../../../test/functional/services/lib/web_element_wrapper';
export function IngestManagerCreatePackagePolicy({
getService,
@ -13,6 +14,7 @@ export function IngestManagerCreatePackagePolicy({
const testSubjects = getService('testSubjects');
const find = getService('find');
const pageObjects = getPageObjects(['common']);
const browser = getService('browser');
return {
/**
@ -101,5 +103,38 @@ export function IngestManagerCreatePackagePolicy({
});
await this.ensureOnEditPageOrFail();
},
/**
* Returns the Endpoint Callout that is displayed on the Integration Policy create/edit pages
*/
async findEndpointActionsButton() {
const button = await testSubjects.find('endpointActions');
await this.scrollToCenterOfWindow(button);
return button;
},
/**
* Center a given Element on the Window viewport
* @param element
*/
async scrollToCenterOfWindow(element: WebElementWrapper) {
const [elementPosition, windowSize] = await Promise.all([
element.getPosition(),
browser.getWindowSize(),
]);
await browser.execute(
`document.scrollingElement.scrollTop = ${elementPosition.y - windowSize.height / 2}`
);
},
/**
* Will click on the given Endpoint Action (from the Actions dropdown)
* @param action
*/
async selectEndpointAction(action: 'policy' | 'trustedApps') {
await (await this.findEndpointActionsButton()).click();
const testSubjId = action === 'policy' ? 'securityPolicy' : 'trustedAppsAction';
await (await testSubjects.find(testSubjId)).click();
},
};
}

View file

@ -5,8 +5,9 @@
*/
import { FtrProviderContext } from '../ftr_provider_context';
export function TrustedAppsPageProvider({ getPageObjects }: FtrProviderContext) {
export function TrustedAppsPageProvider({ getService, getPageObjects }: FtrProviderContext) {
const pageObjects = getPageObjects(['common', 'header', 'endpointPageUtils']);
const testSubjects = getService('testSubjects');
return {
async navigateToTrustedAppsList(searchParams?: string) {
@ -16,5 +17,21 @@ export function TrustedAppsPageProvider({ getPageObjects }: FtrProviderContext)
);
await pageObjects.header.waitUntilLoadingHasFinished();
},
/**
* ensures that the Policy Page is the currently display view
*/
async ensureIsOnTrustedAppsListPage() {
await testSubjects.existOrFail('trustedAppsListPage');
},
/**
* Returns the Back button displayed on the Trusted Apps list page when page is loaded
* with route state that triggers return button to be displayed
*/
async findTrustedAppsListPageBackButton() {
await this.ensureIsOnTrustedAppsListPage();
return testSubjects.find('backToOrigin');
},
};
}