Skip to content

Commit

Permalink
[Fleet] moved root privilege callout with data stream info to create/…
Browse files Browse the repository at this point in the history
…edit package policy page (elastic#184190)

## Summary

Address feedback in
elastic#184119 (comment)

Relates elastic/ingest-dev#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="https://github.com/elastic/kibana/assets/90178898/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="https://github.com/elastic/kibana/assets/90178898/793ace68-7618-482e-a200-6b831d293c99">

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

<img width="876" alt="image"
src="https://github.com/elastic/kibana/assets/90178898/902f7d3c-ddbc-4131-a19d-341aa1209430">
<img width="878" alt="image"
src="https://github.com/elastic/kibana/assets/90178898/085e32df-033d-41ca-9805-5414854d9750">

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

<img width="1135" alt="image"
src="https://github.com/elastic/kibana/assets/90178898/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 <[email protected]>
  • Loading branch information
juliaElastic and kibanamachine authored May 28, 2024
1 parent 5a74376 commit 8ecee1f
Show file tree
Hide file tree
Showing 8 changed files with 75 additions and 241 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -316,117 +316,39 @@ 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 })
);
await act(async () => {
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 })
);
await act(async () => {
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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 &&
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ export type PackagePolicyFormState =
| 'VALID'
| 'INVALID'
| 'CONFIRM'
| 'CONFIRM_UNPRIVILEGED'
| 'LOADING'
| 'SUBMITTED'
| 'SUBMITTED_NO_AGENTS'
Expand Down
Loading

0 comments on commit 8ecee1f

Please sign in to comment.