[Fleet] moved root privilege callout with data stream info to create/edit package policy page (#184190)

## Summary

Address feedback in
https://github.com/elastic/kibana/pull/184119#issuecomment-2127689576

Relates https://github.com/elastic/ingest-dev/issues/3357

Moved root privileges callout with data streams from package policy
submit modal to the create/edit package policy page itself, so it is
more persistent than a modal window.

To verify:
- Go to System integration / Add integration
- Verify that the require root callout shows the data streams that
require root

<img width="974" alt="image"
src="bafdd556-c837-414d-8bbc-26a4463a8390">

- Go to System integration / Existing policies / Edit integration
- Verify that the require root callout is visible with data stream info

<img width="901" alt="image"
src="793ace68-7618-482e-a200-6b831d293c99">

- For package where all data streams require root, the callout is
unchanged.

<img width="876" alt="image"
src="902f7d3c-ddbc-4131-a19d-341aa1209430">
<img width="878" alt="image"
src="085e32df-033d-41ca-9805-5414854d9750">

- The require root callout is removed from the submit confirmation
modal.

<img width="1135" alt="image"
src="e360d74b-09d1-4a41-b2ff-f4a36656e3d4">


### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Julia Bardi 2024-05-28 21:57:06 +02:00 committed by GitHub
parent 5a74376da0
commit 8ecee1f7e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 75 additions and 241 deletions

View file

@ -11,7 +11,6 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import type { AgentPolicy } from '../../../types';
import { UnprivilegedAgentsCallout } from '../create_package_policy_page/single_page_layout/confirm_modal';
export const ConfirmDeployAgentPolicyModal: React.FunctionComponent<{
onConfirm: () => void;
@ -76,16 +75,6 @@ export const ConfirmDeployAgentPolicyModal: React.FunctionComponent<{
/>
</div>
</EuiCallOut>
{showUnprivilegedAgentsCallout && (
<>
<EuiSpacer size="m" />
<UnprivilegedAgentsCallout
agentPolicyName={agentPolicy.name}
unprivilegedAgentsCount={unprivilegedAgentsCount}
dataStreams={dataStreams ?? []}
/>
</>
)}
<EuiSpacer size="l" />
<FormattedMessage
id="xpack.fleet.agentPolicy.confirmModalDescription"

View file

@ -1,103 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiCallOut, EuiConfirmModal } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
export interface UnprivilegedConfirmModalProps {
onConfirm: () => void;
onCancel: () => void;
agentPolicyName: string;
unprivilegedAgentsCount: number;
dataStreams: Array<{ name: string; title: string }>;
}
export const UnprivilegedConfirmModal: React.FC<UnprivilegedConfirmModalProps> = ({
onConfirm,
onCancel,
agentPolicyName,
unprivilegedAgentsCount,
dataStreams,
}: UnprivilegedConfirmModalProps) => {
return (
<EuiConfirmModal
title={
<FormattedMessage
id="xpack.fleet.addIntegration.confirmModalTitle"
defaultMessage="Confirm add integration"
/>
}
onCancel={onCancel}
onConfirm={onConfirm}
cancelButtonText={
<FormattedMessage
id="xpack.fleet.addIntegration.confirmModal.cancelButtonLabel"
defaultMessage="Cancel"
/>
}
confirmButtonText={
<FormattedMessage
id="xpack.fleet.addIntegration.confirmModal.confirmButtonLabel"
defaultMessage="Add integration"
/>
}
buttonColor="warning"
>
<UnprivilegedAgentsCallout
unprivilegedAgentsCount={unprivilegedAgentsCount}
agentPolicyName={agentPolicyName}
dataStreams={dataStreams}
/>
</EuiConfirmModal>
);
};
export const UnprivilegedAgentsCallout: React.FC<{
agentPolicyName: string;
unprivilegedAgentsCount: number;
dataStreams: Array<{ name: string; title: string }>;
}> = ({ agentPolicyName, unprivilegedAgentsCount, dataStreams }) => {
return (
<EuiCallOut
color="warning"
iconType="warning"
title={i18n.translate('xpack.fleet.addIntegration.confirmModal.unprivilegedAgentsTitle', {
defaultMessage: 'Unprivileged agents enrolled to the selected policy',
})}
data-test-subj="unprivilegedAgentsCallout"
>
{dataStreams.length === 0 ? (
<FormattedMessage
id="xpack.fleet.addIntegration.confirmModal.unprivilegedAgentsMessage"
defaultMessage="This integration requires Elastic Agents to have root privileges. There {unprivilegedAgentsCount, plural, one {is # agent} other {are # agents}} running in an unprivileged mode using {agentPolicyName}. To ensure that all data required by the integration can be collected, re-enroll the {unprivilegedAgentsCount, plural, one {agent} other {agents}} using an account with root privileges."
values={{
unprivilegedAgentsCount,
agentPolicyName,
}}
/>
) : (
<>
<FormattedMessage
id="xpack.fleet.addIntegration.confirmModal.unprivilegedAgentsDataStreamsMessage"
defaultMessage="This integration has the following data streams that require Elastic Agents to have root privileges. There {unprivilegedAgentsCount, plural, one {is # agent} other {are # agents}} running in an unprivileged mode using {agentPolicyName}. To ensure that all data required by the integration can be collected, re-enroll the {unprivilegedAgentsCount, plural, one {agent} other {agents}} using an account with root privileges."
values={{
unprivilegedAgentsCount,
agentPolicyName,
}}
/>
<ul>
{dataStreams.map((item) => (
<li key={item.name}>{item.title}</li>
))}
</ul>
</>
)}
</EuiCallOut>
);
};

View file

@ -31,10 +31,7 @@ import {
PACKAGE_POLICY_SAVED_OBJECT_TYPE,
SO_SEARCH_LIMIT,
} from '../../../../../../../../common';
import {
getMaxPackageName,
isRootPrivilegesRequired,
} from '../../../../../../../../common/services';
import { getMaxPackageName } from '../../../../../../../../common/services';
import { useConfirmForceInstall } from '../../../../../../integrations/hooks';
import { validatePackagePolicy, validationHasErrors } from '../../services';
import type { PackagePolicyValidationResults } from '../../services';
@ -270,16 +267,6 @@ export function useOnSubmit({
setFormState('CONFIRM');
return;
}
if (
packageInfo &&
isRootPrivilegesRequired(packageInfo) &&
(agentPolicy?.unprivileged_agents ?? 0) > 0 &&
formState !== 'CONFIRM' &&
formState !== 'CONFIRM_UNPRIVILEGED'
) {
setFormState('CONFIRM_UNPRIVILEGED');
return;
}
let createdPolicy = overrideCreatedAgentPolicy;
if (selectedPolicyTab === SelectedPolicyTab.NEW && !overrideCreatedAgentPolicy) {
try {

View file

@ -316,7 +316,7 @@ describe('When on the package policy create page', () => {
(sendCreatePackagePolicy as jest.MockedFunction<any>).mockClear();
});
test('should show unprivileged warning modal on submit if conditions match', async () => {
test('should show root privileges callout on create page', async () => {
(useGetPackageInfoByKeyQuery as jest.Mock).mockReturnValue(
getMockPackageInfo({ requiresRoot: true })
);
@ -324,70 +324,15 @@ describe('When on the package policy create page', () => {
render('agent-policy-1');
});
let saveBtn: HTMLElement;
await waitFor(() => {
saveBtn = renderResult.getByText(/Save and continue/).closest('button')!;
expect(saveBtn).not.toBeDisabled();
});
await act(async () => {
fireEvent.click(saveBtn);
});
await waitFor(() => {
expect(
renderResult.getByText('Unprivileged agents enrolled to the selected policy')
).toBeInTheDocument();
expect(renderResult.getByTestId('unprivilegedAgentsCallout').textContent).toContain(
'This integration requires Elastic Agents to have root privileges. There is 1 agent running in an unprivileged mode using Agent policy 1. To ensure that all data required by the integration can be collected, re-enroll the agent using an account with root privileges.'
expect(renderResult.getByText('Requires root privileges')).toBeInTheDocument();
expect(renderResult.getByTestId('rootPrivilegesCallout').textContent).toContain(
'Elastic Agent needs to be run with root/administrator privileges for this integration.'
);
});
await waitFor(() => {
saveBtn = renderResult.getByText(/Add integration/).closest('button')!;
});
await act(async () => {
fireEvent.click(saveBtn);
});
expect(sendCreatePackagePolicy as jest.MockedFunction<any>).toHaveBeenCalledTimes(1);
});
test('should show unprivileged warning and agents modal on submit if conditions match', async () => {
(useGetPackageInfoByKeyQuery as jest.Mock).mockReturnValue(
getMockPackageInfo({ requiresRoot: true })
);
(sendGetAgentStatus as jest.MockedFunction<any>).mockResolvedValueOnce({
data: { results: { total: 1 } },
});
await act(async () => {
render('agent-policy-1');
});
await act(async () => {
fireEvent.click(renderResult.getByText(/Save and continue/).closest('button')!);
});
await waitFor(() => {
expect(renderResult.getByText('This action will update 1 agent')).toBeInTheDocument();
expect(
renderResult.getByText('Unprivileged agents enrolled to the selected policy')
).toBeInTheDocument();
expect(renderResult.getByTestId('unprivilegedAgentsCallout').textContent).toContain(
'This integration requires Elastic Agents to have root privileges. There is 1 agent running in an unprivileged mode using Agent policy 1. To ensure that all data required by the integration can be collected, re-enroll the agent using an account with root privileges.'
);
});
await act(async () => {
fireEvent.click(renderResult.getAllByText(/Save and deploy changes/)[1].closest('button')!);
});
expect(sendCreatePackagePolicy as jest.MockedFunction<any>).toHaveBeenCalled();
});
test('should show unprivileged warning modal with data streams on submit if conditions match', async () => {
test('should show root privileges callout with data streams on create page', async () => {
(useGetPackageInfoByKeyQuery as jest.Mock).mockReturnValue(
getMockPackageInfo({ dataStreamRequiresRoot: true })
);
@ -395,38 +340,15 @@ describe('When on the package policy create page', () => {
render('agent-policy-1');
});
let saveBtn: HTMLElement;
await waitFor(() => {
saveBtn = renderResult.getByText(/Save and continue/).closest('button')!;
expect(saveBtn).not.toBeDisabled();
});
await act(async () => {
fireEvent.click(saveBtn);
});
await waitFor(() => {
expect(
renderResult.getByText('Unprivileged agents enrolled to the selected policy')
).toBeInTheDocument();
expect(renderResult.getByTestId('unprivilegedAgentsCallout').textContent).toContain(
'This integration has the following data streams that require Elastic Agents to have root privileges. There is 1 agent running in an unprivileged mode using Agent policy 1. To ensure that all data required by the integration can be collected, re-enroll the agent using an account with root privileges.'
expect(renderResult.getByText('Requires root privileges')).toBeInTheDocument();
expect(renderResult.getByTestId('rootPrivilegesCallout').textContent).toContain(
'This integration has the following data streams that require Elastic Agents to have root privileges. To ensure that all data required by the integration can be collected, enroll agents using an account with root privileges.'
);
expect(renderResult.getByTestId('unprivilegedAgentsCallout').textContent).toContain(
expect(renderResult.getByTestId('rootPrivilegesCallout').textContent).toContain(
'Nginx access logs'
);
});
await waitFor(() => {
saveBtn = renderResult.getByText(/Add integration/).closest('button')!;
});
await act(async () => {
fireEvent.click(saveBtn);
});
expect(sendCreatePackagePolicy as jest.MockedFunction<any>).toHaveBeenCalledTimes(1);
});
test('should create package policy on submit when query param agent policy id is set', async () => {

View file

@ -79,7 +79,7 @@ import { useDevToolsRequest, useOnSubmit, useSetupTechnology } from './hooks';
import { PostInstallCloudFormationModal } from './components/cloud_security_posture/post_install_cloud_formation_modal';
import { PostInstallGoogleCloudShellModal } from './components/cloud_security_posture/post_install_google_cloud_shell_modal';
import { PostInstallAzureArmTemplateModal } from './components/cloud_security_posture/post_install_azure_arm_template_modal';
import { UnprivilegedConfirmModal } from './confirm_modal';
import { RootPrivilegesCallout } from './root_callout';
const StepsWithLessPadding = styled(EuiSteps)`
.euiStep__content {
@ -463,15 +463,6 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
dataStreams={rootPrivilegedDataStreams}
/>
)}
{formState === 'CONFIRM_UNPRIVILEGED' && agentPolicy ? (
<UnprivilegedConfirmModal
onCancel={() => setFormState('VALID')}
onConfirm={onSubmit}
unprivilegedAgentsCount={agentPolicy?.unprivileged_agents ?? 0}
agentPolicyName={agentPolicy?.name ?? ''}
dataStreams={rootPrivilegedDataStreams}
/>
) : null}
{formState === 'SUBMITTED_NO_AGENTS' &&
agentPolicy &&
packageInfo &&
@ -515,21 +506,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
)}
{packageInfo && isRootPrivilegesRequired(packageInfo) ? (
<>
<EuiCallOut
size="s"
color="warning"
title={
<FormattedMessage
id="xpack.fleet.createPackagePolicy.requireRootCalloutTitle"
defaultMessage="Requires root privileges"
/>
}
>
<FormattedMessage
id="xpack.fleet.createPackagePolicy.requireRootCalloutDescription"
defaultMessage="Elastic Agent needs to be run with root/administrator privileges for this integration."
/>
</EuiCallOut>
<RootPrivilegesCallout dataStreams={rootPrivilegedDataStreams} />
<EuiSpacer size="m" />
</>
) : null}

View file

@ -0,0 +1,49 @@
/*
* 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 } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import React from 'react';
interface Props {
dataStreams: Array<{ name: string; title: string }>;
}
export const RootPrivilegesCallout: React.FC<Props> = ({ dataStreams }) => {
return (
<EuiCallOut
size="m"
color="warning"
title={
<FormattedMessage
id="xpack.fleet.createPackagePolicy.requireRootCalloutTitle"
defaultMessage="Requires root privileges"
/>
}
data-test-subj="rootPrivilegesCallout"
>
{dataStreams.length === 0 ? (
<FormattedMessage
id="xpack.fleet.createPackagePolicy.requireRootCalloutDescription"
defaultMessage="Elastic Agent needs to be run with root/administrator privileges for this integration."
/>
) : (
<>
<FormattedMessage
id="xpack.fleet.addIntegration.confirmModal.unprivilegedAgentsDataStreamsMessage"
defaultMessage="This integration has the following data streams that require Elastic Agents to have root privileges. To ensure that all data required by the integration can be collected, enroll agents using an account with root privileges."
/>
<ul>
{dataStreams.map((item) => (
<li key={item.name}>{item.title}</li>
))}
</ul>
</>
)}
</EuiCallOut>
);
};

View file

@ -19,7 +19,6 @@ export type PackagePolicyFormState =
| 'VALID'
| 'INVALID'
| 'CONFIRM'
| 'CONFIRM_UNPRIVILEGED'
| 'LOADING'
| 'SUBMITTED'
| 'SUBMITTED_NO_AGENTS'

View file

@ -55,6 +55,13 @@ import type { PackagePolicyEditExtensionComponentProps } from '../../../types';
import { ExperimentalFeaturesService, pkgKeyFromPackageInfo } from '../../../services';
import { generateUpdatePackagePolicyDevToolsRequest } from '../services';
import {
getRootPrivilegedDataStreams,
isRootPrivilegesRequired,
} from '../../../../../../common/services';
import { RootPrivilegesCallout } from '../create_package_policy_page/single_page_layout/root_callout';
import { UpgradeStatusCallout } from './components';
import { usePackagePolicyWithRelatedData, useHistoryBlock } from './hooks';
import { getNewSecrets } from './utils';
@ -387,6 +394,7 @@ export const EditPackagePolicyForm = memo<{
),
[packagePolicyId, packagePolicy]
);
const rootPrivilegedDataStreams = packageInfo ? getRootPrivilegedDataStreams(packageInfo) : [];
return (
<CreatePackagePolicySinglePageLayout {...layoutProps} data-test-subj="editPackagePolicy">
@ -426,6 +434,12 @@ export const EditPackagePolicyForm = memo<{
onCancel={() => setFormState('VALID')}
/>
)}
{packageInfo && isRootPrivilegesRequired(packageInfo) ? (
<>
<RootPrivilegesCallout dataStreams={rootPrivilegedDataStreams} />
<EuiSpacer size="m" />
</>
) : null}
{isUpgrade && upgradeDryRunData && (
<>
<UpgradeStatusCallout dryRunData={upgradeDryRunData} newSecrets={newSecrets} />