mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[9.0] [Security Solution][Endpoint] Adjust Artifacts policy assignment component in support of spaces (#214487) (#215194)
# Backport This will backport the following commits from `main` to `9.0`: - [[Security Solution][Endpoint] Adjust Artifacts policy assignment component in support of spaces (#214487)](https://github.com/elastic/kibana/pull/214487) <!--- Backport version: 9.6.6 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Paul Tavares","email":"56442535+paul-tavares@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-03-19T15:12:59Z","message":"[Security Solution][Endpoint] Adjust Artifacts policy assignment component in support of spaces (#214487)\n\n## Summary\n\n\n### Fleet\n\n- Exposed API route for bulk get package policies via the routes service\n- Created and exposed type `BulkGetPackagePoliciesRequestBody`\n\n<br/>\n\n\n### Security Solution\n\nThe following changes were made to Endpoint Artifacts in support of\nspaces:\n\n> [!NOTE]\n> Space awareness is currently behind feature flag:\n`endpointManagementSpaceAwarenessEnabled`\n\n\n- The policy assignment component, which is displayed on artifact's\nCreate and Update forms, now:\n- Displays the count of policies (if any) that are associated with the\nartifact, but not currently accessible in the active space (screen\ncapture 1️⃣ )\n- When a user does NOT have the Global Artifact privilege, the `Global`\ntoggle selection will be disabled and a tooltip is displayed. This\nchange also applies to the create form where the default selection will\nbe per-policy and the global button will be disabled. (screen capture\n2️⃣ )\n- Artifact policy assignments that are not accessible in active space\nare preserved when submitting an update to the artifact\n- The component was also refactored a bit to simplify its list of props\n- Artifact card policy assignment menu was adjusted to show any policy\nthat is not accessible to the user as \"disabled\" along with a tooltip\n(screen capture 3️⃣ )\n- The update artifact API was changed (via server-side extension point)\nto not error when validating policies that are not accessible in active\nspace if they were already associated with the item being updated.\n- Fixes a bug in the Find artifacts API (impact only when spaces was\nenabled) where an invalid filter was created when there was no policies\ncurrently shared with active space.","sha":"e11c3ecea5119202800d121a73765e26a41ff0a1","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:Fleet","Team:Defend Workflows","backport:prev-minor","v9.1.0"],"title":"[Security Solution][Endpoint] Adjust Artifacts policy assignment component in support of spaces","number":214487,"url":"https://github.com/elastic/kibana/pull/214487","mergeCommit":{"message":"[Security Solution][Endpoint] Adjust Artifacts policy assignment component in support of spaces (#214487)\n\n## Summary\n\n\n### Fleet\n\n- Exposed API route for bulk get package policies via the routes service\n- Created and exposed type `BulkGetPackagePoliciesRequestBody`\n\n<br/>\n\n\n### Security Solution\n\nThe following changes were made to Endpoint Artifacts in support of\nspaces:\n\n> [!NOTE]\n> Space awareness is currently behind feature flag:\n`endpointManagementSpaceAwarenessEnabled`\n\n\n- The policy assignment component, which is displayed on artifact's\nCreate and Update forms, now:\n- Displays the count of policies (if any) that are associated with the\nartifact, but not currently accessible in the active space (screen\ncapture 1️⃣ )\n- When a user does NOT have the Global Artifact privilege, the `Global`\ntoggle selection will be disabled and a tooltip is displayed. This\nchange also applies to the create form where the default selection will\nbe per-policy and the global button will be disabled. (screen capture\n2️⃣ )\n- Artifact policy assignments that are not accessible in active space\nare preserved when submitting an update to the artifact\n- The component was also refactored a bit to simplify its list of props\n- Artifact card policy assignment menu was adjusted to show any policy\nthat is not accessible to the user as \"disabled\" along with a tooltip\n(screen capture 3️⃣ )\n- The update artifact API was changed (via server-side extension point)\nto not error when validating policies that are not accessible in active\nspace if they were already associated with the item being updated.\n- Fixes a bug in the Find artifacts API (impact only when spaces was\nenabled) where an invalid filter was created when there was no policies\ncurrently shared with active space.","sha":"e11c3ecea5119202800d121a73765e26a41ff0a1"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/214487","number":214487,"mergeCommit":{"message":"[Security Solution][Endpoint] Adjust Artifacts policy assignment component in support of spaces (#214487)\n\n## Summary\n\n\n### Fleet\n\n- Exposed API route for bulk get package policies via the routes service\n- Created and exposed type `BulkGetPackagePoliciesRequestBody`\n\n<br/>\n\n\n### Security Solution\n\nThe following changes were made to Endpoint Artifacts in support of\nspaces:\n\n> [!NOTE]\n> Space awareness is currently behind feature flag:\n`endpointManagementSpaceAwarenessEnabled`\n\n\n- The policy assignment component, which is displayed on artifact's\nCreate and Update forms, now:\n- Displays the count of policies (if any) that are associated with the\nartifact, but not currently accessible in the active space (screen\ncapture 1️⃣ )\n- When a user does NOT have the Global Artifact privilege, the `Global`\ntoggle selection will be disabled and a tooltip is displayed. This\nchange also applies to the create form where the default selection will\nbe per-policy and the global button will be disabled. (screen capture\n2️⃣ )\n- Artifact policy assignments that are not accessible in active space\nare preserved when submitting an update to the artifact\n- The component was also refactored a bit to simplify its list of props\n- Artifact card policy assignment menu was adjusted to show any policy\nthat is not accessible to the user as \"disabled\" along with a tooltip\n(screen capture 3️⃣ )\n- The update artifact API was changed (via server-side extension point)\nto not error when validating policies that are not accessible in active\nspace if they were already associated with the item being updated.\n- Fixes a bug in the Find artifacts API (impact only when spaces was\nenabled) where an invalid filter was created when there was no policies\ncurrently shared with active space.","sha":"e11c3ecea5119202800d121a73765e26a41ff0a1"}}]}] BACKPORT--> Co-authored-by: Paul Tavares <56442535+paul-tavares@users.noreply.github.com>
This commit is contained in:
parent
0d226d39c9
commit
943300d05e
37 changed files with 926 additions and 386 deletions
|
@ -155,6 +155,10 @@ export const packagePolicyRouteService = {
|
|||
getOrphanedIntegrationPoliciesPath: () => {
|
||||
return PACKAGE_POLICY_API_ROUTES.ORPHANED_INTEGRATION_POLICIES;
|
||||
},
|
||||
|
||||
getBulkGetPath: (): string => {
|
||||
return PACKAGE_POLICY_API_ROUTES.BULK_GET_PATTERN;
|
||||
},
|
||||
};
|
||||
|
||||
export const agentPolicyRouteService = {
|
||||
|
|
|
@ -5,6 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { TypeOf } from '@kbn/config-schema';
|
||||
|
||||
import type { BulkGetPackagePoliciesRequestSchema } from '../../../server/types';
|
||||
|
||||
import type {
|
||||
PackagePolicy,
|
||||
NewPackagePolicy,
|
||||
|
@ -21,6 +25,9 @@ export interface GetPackagePoliciesRequest {
|
|||
}
|
||||
|
||||
export type GetPackagePoliciesResponse = ListResult<PackagePolicy>;
|
||||
export type BulkGetPackagePoliciesRequestBody = TypeOf<
|
||||
typeof BulkGetPackagePoliciesRequestSchema.body
|
||||
>;
|
||||
export type BulkGetPackagePoliciesResponse = BulkGetResult<PackagePolicy>;
|
||||
|
||||
export interface GetOnePackagePolicyRequest {
|
||||
|
|
|
@ -27,11 +27,15 @@ export const isArtifactGlobal = (item: Partial<Pick<ExceptionListItemSchema, 'ta
|
|||
return (item.tags ?? []).includes(GLOBAL_ARTIFACT_TAG);
|
||||
};
|
||||
|
||||
export const isArtifactByPolicy = (item: Pick<ExceptionListItemSchema, 'tags'>): boolean => {
|
||||
export const isArtifactByPolicy = (
|
||||
item: Partial<Pick<ExceptionListItemSchema, 'tags'>>
|
||||
): boolean => {
|
||||
return !isArtifactGlobal(item);
|
||||
};
|
||||
|
||||
export const getPolicyIdsFromArtifact = (item: Pick<ExceptionListItemSchema, 'tags'>): string[] => {
|
||||
export const getPolicyIdsFromArtifact = (
|
||||
item: Partial<Pick<ExceptionListItemSchema, 'tags'>>
|
||||
): string[] => {
|
||||
const policyIds = [];
|
||||
const tags = item.tags ?? [];
|
||||
|
||||
|
|
|
@ -320,3 +320,15 @@ export const CONFIRM_WARNING_MODAL_LABELS = (entryType: string) => {
|
|||
),
|
||||
};
|
||||
};
|
||||
|
||||
export const NO_PRIVILEGE_FOR_MANAGEMENT_OF_GLOBAL_ARTIFACT_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.translations.noGlobalArtifactManagementAllowedMessage',
|
||||
{ defaultMessage: 'Management of global artifacts requires additional privilege' }
|
||||
);
|
||||
|
||||
export const ARTIFACT_POLICIES_NOT_ACCESSIBLE_IN_ACTIVE_SPACE_MESSAGE = (count: number): string =>
|
||||
i18n.translate('xpack.securitySolution.translations.artifactPoliciesNotAccessibleInActiveSpace', {
|
||||
defaultMessage:
|
||||
'This artifact is associated with {count} {count, plural, =1 {policy that is} other {policies that are}} not accessible in active space',
|
||||
values: { count },
|
||||
});
|
||||
|
|
|
@ -203,8 +203,9 @@ describe.each([
|
|||
|
||||
policies = {
|
||||
'policy-1': {
|
||||
children: 'Policy one',
|
||||
children: 'Policy one title',
|
||||
'data-test-subj': 'policyMenuItem',
|
||||
href: 'http://some/path/to/policy-1',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
@ -239,7 +240,11 @@ describe.each([
|
|||
).not.toBeNull();
|
||||
|
||||
expect(renderResult.getByTestId('policyMenuItem').textContent).toEqual(
|
||||
'Policy oneView details'
|
||||
'Policy one titleView details'
|
||||
);
|
||||
|
||||
expect((renderResult.getByTestId('policyMenuItem') as HTMLAnchorElement).href).toEqual(
|
||||
policies!['policy-1'].href
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -259,12 +264,12 @@ describe.each([
|
|||
renderResult.getByTestId('testCard-subHeader-effectScope-popupMenu-popoverPanel')
|
||||
).not.toBeNull();
|
||||
|
||||
expect(renderResult.getByTestId('policyMenuItem').textContent).toEqual('Policy one');
|
||||
expect(renderResult.getByTestId('policyMenuItem').textContent).toEqual('Policy one title');
|
||||
});
|
||||
});
|
||||
|
||||
it('should display policy ID if no policy menu item found in `policies` prop', async () => {
|
||||
render();
|
||||
it('should display disabled button with policy ID if no policy menu item found in `policies` prop', async () => {
|
||||
render(); // Important: no polices provided to component on input
|
||||
await act(async () => {
|
||||
await fireEvent.click(
|
||||
renderResult.getByTestId('testCard-subHeader-effectScope-popupMenu-button')
|
||||
|
@ -275,7 +280,18 @@ describe.each([
|
|||
renderResult.getByTestId('testCard-subHeader-effectScope-popupMenu-popoverPanel')
|
||||
).not.toBeNull();
|
||||
|
||||
expect(renderResult.getByText('policy-1').textContent).not.toBeNull();
|
||||
expect(
|
||||
renderResult.getByTestId('testCard-subHeader-effectScope-popupMenu-item-0-truncateWrapper')
|
||||
.textContent
|
||||
).toEqual('policy-1');
|
||||
|
||||
expect(
|
||||
(
|
||||
renderResult.getByTestId(
|
||||
'testCard-subHeader-effectScope-popupMenu-item-0'
|
||||
) as HTMLButtonElement
|
||||
).disabled
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should pass item to decorator function and display its result', () => {
|
||||
|
|
|
@ -10,11 +10,9 @@ import React, { memo, useMemo } from 'react';
|
|||
import type { CommonProps } from '@elastic/eui';
|
||||
import { EuiFlexItem } from '@elastic/eui';
|
||||
import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { NO_PRIVILEGE_FOR_MANAGEMENT_OF_GLOBAL_ARTIFACT_MESSAGE } from '../../../common/translations';
|
||||
import { useUserPrivileges } from '../../../../common/components/user_privileges';
|
||||
import {
|
||||
MANAGEMENT_OF_GLOBAL_ARTIFACT_NOT_ALLOWED_MESSAGE,
|
||||
MANAGEMENT_OF_SHARED_PER_POLICY_ARTIFACT_NOT_ALLOWED_MESSAGE,
|
||||
} from './translations';
|
||||
import { MANAGEMENT_OF_SHARED_PER_POLICY_ARTIFACT_NOT_ALLOWED_MESSAGE } from './translations';
|
||||
import { useSpaceId } from '../../../../common/hooks/use_space_id';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
import { isArtifactGlobal } from '../../../../../common/endpoint/service/artifacts';
|
||||
|
@ -52,7 +50,7 @@ export const CardActionsFlexItem = memo<CardActionsFlexItemProps>(
|
|||
if (isGlobal) {
|
||||
return {
|
||||
isDisabled: true,
|
||||
disabledTooltip: MANAGEMENT_OF_GLOBAL_ARTIFACT_NOT_ALLOWED_MESSAGE,
|
||||
disabledTooltip: NO_PRIVILEGE_FOR_MANAGEMENT_OF_GLOBAL_ARTIFACT_MESSAGE,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,8 @@ import type { CommonProps } from '@elastic/eui';
|
|||
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
import { useUserPrivileges } from '../../../../common/components/user_privileges';
|
||||
import {
|
||||
GLOBAL_EFFECT_SCOPE,
|
||||
|
@ -28,6 +30,16 @@ import { useTestIdGenerator } from '../../../hooks/use_test_id_generator';
|
|||
// So something like: `<EffectScope perPolicyCount={3} />`
|
||||
// This should display it as "Applied to 3 policies", but NOT as a menu with links
|
||||
|
||||
const POLICY_DETAILS_NOT_ACCESSIBLE = i18n.translate(
|
||||
'xpack.securitySolution.effectScope.policyDetailsNotAccessible',
|
||||
{ defaultMessage: 'Policy is no longer accessible' }
|
||||
);
|
||||
|
||||
const POLICY_DETAILS_NOT_ACCESSIBLE_IN_ACTIVE_SPACE = i18n.translate(
|
||||
'xpack.securitySolution.effectScope.policyDetailsNotAccessibleInActiveSpace',
|
||||
{ defaultMessage: 'Policy is not accessible from the current space' }
|
||||
);
|
||||
|
||||
const StyledWithContextMenuShiftedWrapper = styled('div')`
|
||||
margin-left: -10px;
|
||||
`;
|
||||
|
@ -104,29 +116,46 @@ const WithContextMenu = memo<WithContextMenuProps>(
|
|||
'data-test-subj': dataTestSubj,
|
||||
}) => {
|
||||
const getTestId = useTestIdGenerator(dataTestSubj);
|
||||
|
||||
const hoverInfo = useMemo(
|
||||
() =>
|
||||
canReadPolicies ? (
|
||||
<StyledEuiButtonEmpty flush="right" size="s" iconSide="right" iconType="popout">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.contextMenuItemByRouter.viewDetails"
|
||||
defaultMessage="View details"
|
||||
/>
|
||||
</StyledEuiButtonEmpty>
|
||||
) : undefined,
|
||||
[canReadPolicies]
|
||||
const isSpacesEnabled = useIsExperimentalFeatureEnabled(
|
||||
'endpointManagementSpaceAwarenessEnabled'
|
||||
);
|
||||
|
||||
const menuItems: ContextMenuItemNavByRouterProps[] = useMemo(() => {
|
||||
return policies.map((policyMenuItem) => {
|
||||
const hasHref = Boolean(policyMenuItem.href);
|
||||
|
||||
return {
|
||||
...policyMenuItem,
|
||||
hoverInfo:
|
||||
hasHref && canReadPolicies ? (
|
||||
<StyledEuiButtonEmpty flush="right" size="s" iconSide="right" iconType="popout">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.contextMenuItemByRouter.viewDetails"
|
||||
defaultMessage="View details"
|
||||
/>
|
||||
</StyledEuiButtonEmpty>
|
||||
) : undefined,
|
||||
disabled: !hasHref,
|
||||
toolTipContent: !hasHref ? (
|
||||
<>
|
||||
{isSpacesEnabled
|
||||
? POLICY_DETAILS_NOT_ACCESSIBLE_IN_ACTIVE_SPACE
|
||||
: POLICY_DETAILS_NOT_ACCESSIBLE}
|
||||
</>
|
||||
) : undefined,
|
||||
};
|
||||
});
|
||||
}, [canReadPolicies, isSpacesEnabled, policies]);
|
||||
|
||||
return (
|
||||
<ContextMenuWithRouterSupport
|
||||
maxHeight="235px"
|
||||
fixedWidth={true}
|
||||
panelPaddingSize="none"
|
||||
items={policies}
|
||||
items={menuItems}
|
||||
anchorPosition={policies.length > 1 ? 'rightCenter' : 'rightUp'}
|
||||
data-test-subj={dataTestSubj}
|
||||
loading={loadingPoliciesList}
|
||||
hoverInfo={hoverInfo}
|
||||
button={
|
||||
<EuiButtonEmpty size="xs" data-test-subj={getTestId('button')}>
|
||||
{children}
|
||||
|
|
|
@ -163,11 +163,6 @@ export const DESCRIPTION_LABEL = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const MANAGEMENT_OF_GLOBAL_ARTIFACT_NOT_ALLOWED_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.translations.noGlobalArtifactManagementAllowedMessage',
|
||||
{ defaultMessage: 'Management of global artifacts requires additional privilege' }
|
||||
);
|
||||
|
||||
export const MANAGEMENT_OF_SHARED_PER_POLICY_ARTIFACT_NOT_ALLOWED_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.translations.sharedPerPolicyArtifactNotAllowed',
|
||||
{
|
||||
|
|
|
@ -36,11 +36,19 @@ describe('When the flyout is opened in the ArtifactListPage component', () => {
|
|||
let getLastFormComponentProps: ReturnType<
|
||||
typeof getFormComponentMock
|
||||
>['getLastFormComponentProps'];
|
||||
let setExperimentalFlag: AppContextTestRender['setExperimentalFlag'];
|
||||
|
||||
beforeEach(() => {
|
||||
const renderSetup = getArtifactListPageRenderingSetup();
|
||||
|
||||
({ history, coreStart, mockedApi, FormComponentMock, getLastFormComponentProps } = renderSetup);
|
||||
({
|
||||
history,
|
||||
coreStart,
|
||||
mockedApi,
|
||||
FormComponentMock,
|
||||
getLastFormComponentProps,
|
||||
setExperimentalFlag,
|
||||
} = renderSetup);
|
||||
|
||||
history.push('somepage?show=create');
|
||||
|
||||
|
@ -116,6 +124,36 @@ describe('When the flyout is opened in the ArtifactListPage component', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should initialize form with a per-policy artifact when user does not have global artifact privilege and spaces is enabeld', async () => {
|
||||
setExperimentalFlag({ endpointManagementSpaceAwarenessEnabled: true });
|
||||
useUserPrivileges.mockReturnValue({
|
||||
...useUserPrivileges(),
|
||||
endpointPrivileges: getEndpointPrivilegesInitialStateMock({
|
||||
canManageGlobalArtifacts: false,
|
||||
}),
|
||||
});
|
||||
await render();
|
||||
|
||||
expect(FormComponentMock).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
item: {
|
||||
comments: [],
|
||||
description: '',
|
||||
entries: [],
|
||||
item_id: undefined,
|
||||
list_id: 'endpoint_trusted_apps',
|
||||
meta: expect.any(Object),
|
||||
name: '',
|
||||
namespace_type: 'agnostic',
|
||||
os_types: ['windows'],
|
||||
tags: [],
|
||||
type: 'simple',
|
||||
},
|
||||
}),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
|
||||
describe('and form data is valid', () => {
|
||||
beforeEach(async () => {
|
||||
const _renderAndWaitForFlyout = render;
|
||||
|
|
|
@ -27,6 +27,8 @@ import type { EuiFlyoutSize } from '@elastic/eui/src/components/flyout/flyout';
|
|||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
import { useIsMounted } from '@kbn/securitysolution-hook-utils';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { GLOBAL_ARTIFACT_TAG } from '../../../../../common/endpoint/service/artifacts';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
import { useMarkInsightAsRemediated } from '../hooks/use_mark_workflow_insight_as_remediated';
|
||||
import type { WorkflowInsightRouteState } from '../../../pages/endpoint_hosts/types';
|
||||
import { useUrlParams } from '../../../hooks/use_url_params';
|
||||
|
@ -47,6 +49,7 @@ import { useIsArtifactAllowedPerPolicyUsage } from '../hooks/use_is_artifact_all
|
|||
import { useGetArtifact } from '../../../hooks/artifacts';
|
||||
import type { PolicyData } from '../../../../../common/endpoint/types';
|
||||
import { ArtifactConfirmModal } from './artifact_confirm_modal';
|
||||
import { useUserPrivileges } from '../../../../common/components/user_privileges';
|
||||
|
||||
export const ARTIFACT_FLYOUT_LABELS = Object.freeze({
|
||||
flyoutEditTitle: i18n.translate('xpack.securitySolution.artifactListPage.flyoutEditTitle', {
|
||||
|
@ -211,6 +214,11 @@ export const ArtifactFlyout = memo<ArtifactFlyoutProps>(
|
|||
const setUrlParams = useSetUrlParams();
|
||||
const { urlParams } = useUrlParams<ArtifactListPageUrlParams>();
|
||||
const isMounted = useIsMounted();
|
||||
const isSpaceAwarenessEnabled = useIsExperimentalFeatureEnabled(
|
||||
'endpointManagementSpaceAwarenessEnabled'
|
||||
);
|
||||
const canManageGlobalArtifacts =
|
||||
useUserPrivileges().endpointPrivileges.canManageGlobalArtifacts;
|
||||
const labels = useMemo<typeof ARTIFACT_FLYOUT_LABELS>(() => {
|
||||
return {
|
||||
...ARTIFACT_FLYOUT_LABELS,
|
||||
|
@ -255,9 +263,18 @@ export const ArtifactFlyout = memo<ArtifactFlyoutProps>(
|
|||
enabled: false,
|
||||
});
|
||||
|
||||
const [formState, setFormState] = useState<ArtifactFormComponentOnChangeCallbackProps>(
|
||||
createFormInitialState.bind(null, apiClient.listId, item)
|
||||
);
|
||||
const [formState, setFormState] = useState<ArtifactFormComponentOnChangeCallbackProps>(() => {
|
||||
const initialFormState = createFormInitialState(apiClient.listId, item);
|
||||
|
||||
// for Create Mode: If user is not able to manage global artifacts then the initial item should be per-policy
|
||||
if (!item && isSpaceAwarenessEnabled && !canManageGlobalArtifacts) {
|
||||
initialFormState.item.tags = (initialFormState.item.tags ?? []).filter(
|
||||
(tag) => tag !== GLOBAL_ARTIFACT_TAG
|
||||
);
|
||||
}
|
||||
|
||||
return initialFormState;
|
||||
});
|
||||
const showExpiredLicenseBanner = useIsArtifactAllowedPerPolicyUsage(
|
||||
{ tags: formState.item.tags ?? [] },
|
||||
formMode
|
||||
|
|
|
@ -87,6 +87,7 @@ export interface ArtifactListPageRenderingSetup {
|
|||
) => ReturnType<AppContextTestRender['render']>;
|
||||
history: AppContextTestRender['history'];
|
||||
coreStart: AppContextTestRender['coreStart'];
|
||||
setExperimentalFlag: AppContextTestRender['setExperimentalFlag'];
|
||||
mockedApi: ReturnType<typeof trustedAppsAllHttpMocks>;
|
||||
FormComponentMock: ReturnType<typeof getFormComponentMock>['FormComponentMock'];
|
||||
getLastFormComponentProps: ReturnType<typeof getFormComponentMock>['getLastFormComponentProps'];
|
||||
|
@ -141,5 +142,6 @@ export const getArtifactListPageRenderingSetup = (): ArtifactListPageRenderingSe
|
|||
FormComponentMock,
|
||||
getLastFormComponentProps,
|
||||
getFirstCard: getCard,
|
||||
setExperimentalFlag: mockedContext.setExperimentalFlag,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -57,7 +57,6 @@ const StyledEuiText = styled(EuiText)`
|
|||
* Just like `EuiContextMenuItem`, but allows for additional props to be defined which will
|
||||
* allow navigation to a URL path via React Router
|
||||
*/
|
||||
|
||||
export const ContextMenuItemNavByRouter = memo<ContextMenuItemNavByRouterProps>(
|
||||
({
|
||||
navigateAppId,
|
||||
|
@ -74,10 +73,11 @@ export const ContextMenuItemNavByRouter = memo<ContextMenuItemNavByRouterProps>(
|
|||
onClick,
|
||||
});
|
||||
const getTestId = useTestIdGenerator(otherMenuItemProps['data-test-subj']);
|
||||
|
||||
const hoverComponentInstance = useMemo(() => {
|
||||
// If the `hoverInfo` is not an object (ex. text, number), then auto-add the text truncation className.
|
||||
// Adding this when the `hoverInfo` is a react component could cause issue, thus in htose cases, we
|
||||
// assume the componet will handle how the data is truncated (if applicable)
|
||||
// Adding this when the `hoverInfo` is a react component could cause issue, thus in those cases, we
|
||||
// assume the component will handle how the data is truncated (if applicable)
|
||||
const cssClassNames = `additional-info ${
|
||||
'object' !== typeof hoverInfo ? 'eui-textTruncate' : ''
|
||||
}`;
|
||||
|
|
|
@ -169,4 +169,13 @@ describe('When using the ContextMenuWithRouterSupport component', () => {
|
|||
clickMenuTriggerButton();
|
||||
expect(renderResult.getByTestId('testMenu-item-1').textContent).toEqual('click me 2');
|
||||
});
|
||||
|
||||
it('should display menu item `hoverInfo` when no `hoverInfo` is provided to menu component', () => {
|
||||
items[1].hoverInfo = 'item hover info here';
|
||||
render();
|
||||
clickMenuTriggerButton();
|
||||
expect(renderResult.getByTestId('testMenu-item-1').textContent).toEqual(
|
||||
'click me 2item hover info here'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,7 +16,8 @@ import { useTestIdGenerator } from '../../hooks/use_test_id_generator';
|
|||
|
||||
export interface ContextMenuWithRouterSupportProps
|
||||
extends CommonProps,
|
||||
Pick<EuiPopoverProps, 'button' | 'anchorPosition' | 'panelPaddingSize'> {
|
||||
Pick<EuiPopoverProps, 'button' | 'anchorPosition' | 'panelPaddingSize'>,
|
||||
Pick<ContextMenuItemNavByRouterProps, 'isNavigationDisabled'> {
|
||||
items: ContextMenuItemNavByRouterProps[];
|
||||
/**
|
||||
* The max width for the popup menu. Default is `32ch`.
|
||||
|
@ -38,8 +39,12 @@ export interface ContextMenuWithRouterSupportProps
|
|||
*/
|
||||
title?: string;
|
||||
loading?: boolean;
|
||||
hoverInfo?: React.ReactNode;
|
||||
isNavigationDisabled?: boolean;
|
||||
/**
|
||||
* Additional information to show on ALL menu items.
|
||||
* The content provided here will be applied to all menu items, thus overriding the
|
||||
* `hoverInfo` that may be defined on each one.
|
||||
*/
|
||||
hoverInfo?: ContextMenuItemNavByRouterProps['hoverInfo'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -90,7 +95,7 @@ export const ContextMenuWithRouterSupport = memo<ContextMenuWithRouterSupportPro
|
|||
key={uuidv4()}
|
||||
data-test-subj={itemProps['data-test-subj'] ?? getTestId(`item-${index}`)}
|
||||
textTruncate={Boolean(maxWidth) || itemProps.textTruncate}
|
||||
hoverInfo={hoverInfo}
|
||||
hoverInfo={hoverInfo || itemProps.hoverInfo}
|
||||
onClick={(ev) => {
|
||||
handleCloseMenu();
|
||||
if (itemProps.onClick) {
|
||||
|
|
|
@ -9,14 +9,25 @@ import type { EffectedPolicySelectProps } from './effected_policy_select';
|
|||
import { EffectedPolicySelect } from './effected_policy_select';
|
||||
import React from 'react';
|
||||
import { forceHTMLElementOffsetWidth } from './test_utils';
|
||||
import { fireEvent, act } from '@testing-library/react';
|
||||
import { fireEvent, act, waitFor } from '@testing-library/react';
|
||||
import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data';
|
||||
import type { AppContextTestRender } from '../../../common/mock/endpoint';
|
||||
import { createAppRootMockRenderer } from '../../../common/mock/endpoint';
|
||||
import { useUserPrivileges } from '../../../common/components/user_privileges';
|
||||
import { initialUserPrivilegesState } from '../../../common/components/user_privileges/user_privileges_context';
|
||||
import { ExceptionsListItemGenerator } from '../../../../common/endpoint/data_generators/exceptions_list_item_generator';
|
||||
import { GLOBAL_ARTIFACT_TAG } from '../../../../common/endpoint/service/artifacts';
|
||||
import { useLicense as _useLicense } from '../../../common/hooks/use_license';
|
||||
import type { LicenseService } from '../../../../common/license';
|
||||
import { buildPerPolicyTag } from '../../../../common/endpoint/service/artifacts/utils';
|
||||
import { ARTIFACT_POLICIES_NOT_ACCESSIBLE_IN_ACTIVE_SPACE_MESSAGE } from '../../common/translations';
|
||||
import { fleetBulkGetPackagePoliciesListHttpMock } from '../../mocks';
|
||||
import { FleetPackagePolicyGenerator } from '../../../../common/endpoint/data_generators/fleet_package_policy_generator';
|
||||
|
||||
jest.mock('../../../common/components/user_privileges');
|
||||
jest.mock('../../../common/hooks/use_license');
|
||||
|
||||
const useLicenseMock = _useLicense as jest.Mock;
|
||||
|
||||
describe('when using EffectedPolicySelect component', () => {
|
||||
const generator = new EndpointDocGenerator('effected-policy-select');
|
||||
|
@ -43,15 +54,19 @@ describe('when using EffectedPolicySelect component', () => {
|
|||
afterAll(() => resetHTMLElementOffsetWidth());
|
||||
|
||||
beforeEach(() => {
|
||||
mockedContext = createAppRootMockRenderer();
|
||||
|
||||
// Default props
|
||||
componentProps = {
|
||||
item: new ExceptionsListItemGenerator('seed').generateTrustedApp({
|
||||
tags: [GLOBAL_ARTIFACT_TAG],
|
||||
}),
|
||||
options: [],
|
||||
isGlobal: true,
|
||||
isPlatinumPlus: true,
|
||||
onChange: handleOnChange,
|
||||
'data-test-subj': 'test',
|
||||
};
|
||||
mockedContext = createAppRootMockRenderer();
|
||||
|
||||
(useLicenseMock() as jest.Mocked<LicenseService>).isPlatinumPlus.mockReturnValue(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -60,7 +75,8 @@ describe('when using EffectedPolicySelect component', () => {
|
|||
|
||||
describe('and no policy entries exist', () => {
|
||||
it('should display no options available message', () => {
|
||||
const { getByTestId } = render({ isGlobal: false });
|
||||
componentProps.item.tags = [];
|
||||
const { getByTestId } = render();
|
||||
const euiSelectableMessageElement =
|
||||
getByTestId('test-policiesSelectable').getElementsByClassName('euiSelectableMessage')[0];
|
||||
expect(euiSelectableMessageElement).not.toBeNull();
|
||||
|
@ -100,45 +116,44 @@ describe('when using EffectedPolicySelect component', () => {
|
|||
options: [policy],
|
||||
};
|
||||
|
||||
handleOnChange.mockImplementation((selection) => {
|
||||
handleOnChange.mockImplementation((updatedItem) => {
|
||||
componentProps = {
|
||||
...componentProps,
|
||||
...selection,
|
||||
item: updatedItem,
|
||||
};
|
||||
renderResult.rerender(<EffectedPolicySelect {...componentProps} />);
|
||||
});
|
||||
});
|
||||
|
||||
it('should display policies', () => {
|
||||
const { getByTestId } = render({ isGlobal: false });
|
||||
componentProps.item.tags = [];
|
||||
const { getByTestId } = render();
|
||||
expect(getByTestId(policyTestSubj));
|
||||
});
|
||||
|
||||
it('should hide policy items if global is checked', () => {
|
||||
const { queryByTestId } = render({ isGlobal: true });
|
||||
const { queryByTestId } = render();
|
||||
expect(queryByTestId(policyTestSubj)).toBeNull();
|
||||
});
|
||||
|
||||
it('should enable policy items if global is unchecked', async () => {
|
||||
const { getByTestId } = render({ isGlobal: false });
|
||||
it('should show policy items when user clicks per-policy', async () => {
|
||||
const { getByTestId } = render();
|
||||
selectPerPolicy();
|
||||
expect(getByTestId(policyTestSubj).getAttribute('aria-disabled')).toEqual('false');
|
||||
|
||||
expect(getByTestId(policyTestSubj)).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should call onChange with selection when global is toggled', () => {
|
||||
it('should call onChange with updated item', () => {
|
||||
render();
|
||||
|
||||
selectPerPolicy();
|
||||
expect(handleOnChange.mock.calls[0][0]).toEqual({
|
||||
isGlobal: false,
|
||||
selected: [],
|
||||
...componentProps.item,
|
||||
tags: [],
|
||||
});
|
||||
|
||||
selectGlobalPolicy();
|
||||
expect(handleOnChange.mock.calls[1][0]).toEqual({
|
||||
isGlobal: true,
|
||||
selected: [],
|
||||
});
|
||||
expect(handleOnChange.mock.calls[1][0]).toEqual(componentProps.item);
|
||||
});
|
||||
|
||||
it('should maintain policies selection even if global was checked, and user switched back to per policy', () => {
|
||||
|
@ -147,24 +162,28 @@ describe('when using EffectedPolicySelect component', () => {
|
|||
|
||||
selectPerPolicy();
|
||||
clickOnPolicy();
|
||||
expect(handleOnChange.mock.calls[1][0]).toEqual({
|
||||
isGlobal: false,
|
||||
selected: [componentProps.options[0]],
|
||||
});
|
||||
// FYI: If wondering why `componentProps.item` is being used successfully here and below:
|
||||
// its because `handlOnChange` is setup above to re-render the component everytime an update
|
||||
// is received, thus it will always reflect the latest state of the artifact
|
||||
expect(handleOnChange).toHaveBeenLastCalledWith(componentProps.item);
|
||||
|
||||
// Toggle isGlobal back to True
|
||||
selectGlobalPolicy();
|
||||
expect(handleOnChange.mock.calls[2][0]).toEqual({
|
||||
isGlobal: true,
|
||||
selected: [componentProps.options[0]],
|
||||
});
|
||||
expect(handleOnChange).toHaveBeenLastCalledWith(componentProps.item);
|
||||
});
|
||||
|
||||
it('should show loader only when by polocy selected', () => {
|
||||
const { queryByTestId } = render({ isLoading: true });
|
||||
it('should show loader only when by policy selected', () => {
|
||||
componentProps.isLoading = true;
|
||||
const { queryByTestId, getByTestId, rerender } = render();
|
||||
expect(queryByTestId('loading-spinner')).toBeNull();
|
||||
selectPerPolicy();
|
||||
expect(queryByTestId('loading-spinner')).not.toBeNull();
|
||||
|
||||
componentProps.item = {
|
||||
...componentProps.item,
|
||||
tags: [],
|
||||
};
|
||||
rerender(<EffectedPolicySelect {...componentProps} />);
|
||||
|
||||
expect(getByTestId('loading-spinner')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should hide policy link when no policy management privileges', () => {
|
||||
|
@ -176,7 +195,8 @@ describe('when using EffectedPolicySelect component', () => {
|
|||
canReadPolicyManagement: false,
|
||||
},
|
||||
});
|
||||
const { queryByTestId } = render({ isGlobal: false });
|
||||
componentProps.item.tags = [];
|
||||
const { queryByTestId } = render();
|
||||
expect(queryByTestId('test-policyLink')).toBeNull();
|
||||
});
|
||||
|
||||
|
@ -189,7 +209,8 @@ describe('when using EffectedPolicySelect component', () => {
|
|||
canReadPolicyManagement: true,
|
||||
},
|
||||
});
|
||||
const { getByTestId } = render({ isGlobal: false });
|
||||
componentProps.item.tags = [];
|
||||
const { getByTestId } = render();
|
||||
expect(getByTestId('test-policyLink'));
|
||||
});
|
||||
|
||||
|
@ -202,8 +223,57 @@ describe('when using EffectedPolicySelect component', () => {
|
|||
canReadPolicyManagement: true,
|
||||
},
|
||||
});
|
||||
const { getByTestId } = render({ isGlobal: false });
|
||||
componentProps.item.tags = [];
|
||||
const { getByTestId } = render();
|
||||
expect(getByTestId('test-policyLink'));
|
||||
});
|
||||
|
||||
describe('and space awareness is enabled', () => {
|
||||
let httpMocks: ReturnType<typeof fleetBulkGetPackagePoliciesListHttpMock>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockedContext.setExperimentalFlag({ endpointManagementSpaceAwarenessEnabled: true });
|
||||
componentProps.item.tags = [buildPerPolicyTag('321')];
|
||||
httpMocks = fleetBulkGetPackagePoliciesListHttpMock(mockedContext.coreStart.http);
|
||||
httpMocks.responseProvider.bulkPackagePolicies.mockReturnValue({
|
||||
items: [new FleetPackagePolicyGenerator('seed').generate({ id: 'abc123' })],
|
||||
});
|
||||
});
|
||||
|
||||
it('should display count of policies assigned to artifact that are not accessible in active space', async () => {
|
||||
const { getByTestId } = render();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('test-unAccessiblePoliciesCallout').textContent).toEqual(
|
||||
ARTIFACT_POLICIES_NOT_ACCESSIBLE_IN_ACTIVE_SPACE_MESSAGE(1)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should disable global button if user has no global artifact privilege', async () => {
|
||||
(useUserPrivileges as jest.Mock).mockReturnValue({
|
||||
...initialUserPrivilegesState(),
|
||||
endpointPrivileges: {
|
||||
loading: false,
|
||||
canManageGroupPolicies: false,
|
||||
},
|
||||
});
|
||||
const { getByTestId } = render();
|
||||
|
||||
expect((getByTestId('test-global') as HTMLButtonElement).disabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should preserve assignment to policies not currently accessible in active space', async () => {
|
||||
const { getByTestId } = render();
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('test-unAccessiblePoliciesCallout'));
|
||||
});
|
||||
clickOnPolicy();
|
||||
|
||||
expect(handleOnChange).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ tags: ['policy:321', 'policy:abc123'] })
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,9 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo, useCallback, useMemo } from 'react';
|
||||
import React, { memo, useCallback, useMemo, useState } from 'react';
|
||||
import type { EuiButtonGroupOptionProps, EuiSelectableProps } from '@elastic/eui';
|
||||
import {
|
||||
EuiCallOut,
|
||||
EuiButtonGroup,
|
||||
EuiCheckbox,
|
||||
EuiFlexGroup,
|
||||
|
@ -22,6 +23,18 @@ import { i18n } from '@kbn/i18n';
|
|||
import type { EuiSelectableOption } from '@elastic/eui/src/components/selectable/selectable_option';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import styled from '@emotion/styled';
|
||||
import type {
|
||||
CreateExceptionListItemSchema,
|
||||
ExceptionListItemSchema,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { useArtifactRestrictedPolicyAssignments } from '../../hooks/artifacts/use_artifact_restricted_policy_assignments';
|
||||
import { useGetUpdatedTags } from '../../hooks/artifacts';
|
||||
import { useLicense } from '../../../common/hooks/use_license';
|
||||
import {
|
||||
ARTIFACT_POLICIES_NOT_ACCESSIBLE_IN_ACTIVE_SPACE_MESSAGE,
|
||||
NO_PRIVILEGE_FOR_MANAGEMENT_OF_GLOBAL_ARTIFACT_MESSAGE,
|
||||
} from '../../common/translations';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
|
||||
import { useUserPrivileges } from '../../../common/components/user_privileges';
|
||||
import type { PolicyData } from '../../../../common/endpoint/types';
|
||||
import { LinkToApp } from '../../../common/components/endpoint/link_to_app';
|
||||
|
@ -29,6 +42,12 @@ import { getPolicyDetailPath } from '../../common/routing';
|
|||
import { useTestIdGenerator } from '../../hooks/use_test_id_generator';
|
||||
import { useAppUrl } from '../../../common/lib/kibana/hooks';
|
||||
import { Loader } from '../../../common/components/loader';
|
||||
import {
|
||||
getPolicyIdsFromArtifact,
|
||||
GLOBAL_ARTIFACT_TAG,
|
||||
isArtifactGlobal,
|
||||
} from '../../../../common/endpoint/service/artifacts';
|
||||
import { buildPerPolicyTag } from '../../../../common/endpoint/service/artifacts/utils';
|
||||
|
||||
const NOOP = () => {};
|
||||
const DEFAULT_LIST_PROPS: EuiSelectableProps['listProps'] = { bordered: true, showIcons: false };
|
||||
|
@ -82,57 +101,76 @@ export type EffectedPolicySelectProps = Omit<
|
|||
EuiSelectableProps<OptionPolicyData>,
|
||||
'onChange' | 'options' | 'children' | 'searchable'
|
||||
> & {
|
||||
item: ExceptionListItemSchema | CreateExceptionListItemSchema;
|
||||
options: PolicyData[];
|
||||
isGlobal: boolean;
|
||||
isPlatinumPlus: boolean;
|
||||
description?: string;
|
||||
onChange: (selection: EffectedPolicySelection) => void;
|
||||
selected?: PolicyData[];
|
||||
onChange: (updatedItem: ExceptionListItemSchema | CreateExceptionListItemSchema) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const EffectedPolicySelect = memo<EffectedPolicySelectProps>(
|
||||
({
|
||||
isGlobal,
|
||||
isPlatinumPlus,
|
||||
item,
|
||||
description,
|
||||
isLoading = false,
|
||||
onChange,
|
||||
listProps,
|
||||
options,
|
||||
selected = [],
|
||||
disabled = false,
|
||||
'data-test-subj': dataTestSubj,
|
||||
...otherSelectableProps
|
||||
}) => {
|
||||
const { getAppUrl } = useAppUrl();
|
||||
const { canReadPolicyManagement } = useUserPrivileges().endpointPrivileges;
|
||||
|
||||
const getTestId = useTestIdGenerator(dataTestSubj);
|
||||
const isPlatinumPlus = useLicense().isPlatinumPlus();
|
||||
const isSpaceAwarenessEnabled = useIsExperimentalFeatureEnabled(
|
||||
'endpointManagementSpaceAwarenessEnabled'
|
||||
);
|
||||
const canManageGlobalArtifacts =
|
||||
useUserPrivileges().endpointPrivileges.canManageGlobalArtifacts;
|
||||
const { getTagsUpdatedBy } = useGetUpdatedTags(item);
|
||||
const artifactRestrictedPolicyIds = useArtifactRestrictedPolicyAssignments(item);
|
||||
const [selectedPolicyIds, setSelectedPolicyIds] = useState(getPolicyIdsFromArtifact(item));
|
||||
|
||||
const toggleGlobal: EuiButtonGroupOptionProps[] = useMemo(
|
||||
() => [
|
||||
const isGlobal = useMemo(() => isArtifactGlobal(item), [item]);
|
||||
const selectedAssignmentType = useMemo(() => {
|
||||
if (isSpaceAwarenessEnabled) {
|
||||
return canManageGlobalArtifacts && isGlobal ? 'globalPolicy' : 'perPolicy';
|
||||
}
|
||||
|
||||
return isGlobal ? 'globalPolicy' : 'perPolicy';
|
||||
}, [canManageGlobalArtifacts, isGlobal, isSpaceAwarenessEnabled]);
|
||||
|
||||
const toggleGlobal: EuiButtonGroupOptionProps[] = useMemo(() => {
|
||||
const isGlobalButtonDisabled = !isSpaceAwarenessEnabled ? false : !canManageGlobalArtifacts;
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'globalPolicy',
|
||||
label: i18n.translate('xpack.securitySolution.endpoint.effectedPolicySelect.global', {
|
||||
defaultMessage: 'Global',
|
||||
}),
|
||||
iconType: isGlobal ? 'checkInCircleFilled' : 'empty',
|
||||
iconType: selectedAssignmentType === 'globalPolicy' ? 'checkInCircleFilled' : 'empty',
|
||||
'data-test-subj': getTestId('global'),
|
||||
isDisabled: isGlobalButtonDisabled,
|
||||
toolTipContent: isGlobalButtonDisabled
|
||||
? NO_PRIVILEGE_FOR_MANAGEMENT_OF_GLOBAL_ARTIFACT_MESSAGE
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
id: 'perPolicy',
|
||||
label: i18n.translate('xpack.securitySolution.endpoint.effectedPolicySelect.perPolicy', {
|
||||
defaultMessage: 'Per Policy',
|
||||
}),
|
||||
iconType: !isGlobal ? 'checkInCircleFilled' : 'empty',
|
||||
iconType: selectedAssignmentType === 'perPolicy' ? 'checkInCircleFilled' : 'empty',
|
||||
'data-test-subj': getTestId('perPolicy'),
|
||||
},
|
||||
],
|
||||
[getTestId, isGlobal]
|
||||
);
|
||||
];
|
||||
}, [canManageGlobalArtifacts, getTestId, isSpaceAwarenessEnabled, selectedAssignmentType]);
|
||||
|
||||
const selectableOptions: EffectedPolicyOption[] = useMemo(() => {
|
||||
const isPolicySelected = new Set<string>(selected.map((policy) => policy.id));
|
||||
const isPolicySelected = new Set<string>(selectedPolicyIds);
|
||||
|
||||
return options
|
||||
.map<EffectedPolicyOption>((policy) => ({
|
||||
|
@ -174,29 +212,48 @@ export const EffectedPolicySelect = memo<EffectedPolicySelectProps>(
|
|||
isGlobal,
|
||||
isPlatinumPlus,
|
||||
options,
|
||||
selected,
|
||||
selectedPolicyIds,
|
||||
]);
|
||||
|
||||
const handleOnPolicySelectChange = useCallback<
|
||||
Required<EuiSelectableProps<OptionPolicyData>>['onChange']
|
||||
>(
|
||||
(currentOptions) => {
|
||||
const newPolicyAssignmentTags: string[] =
|
||||
artifactRestrictedPolicyIds.policyIds.map(buildPerPolicyTag);
|
||||
const newPolicyIds: string[] = [];
|
||||
|
||||
for (const opt of currentOptions) {
|
||||
if (opt.checked) {
|
||||
newPolicyIds.push(opt.policy.id);
|
||||
newPolicyAssignmentTags.push(buildPerPolicyTag(opt.policy.id));
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedPolicyIds(newPolicyIds);
|
||||
onChange({
|
||||
isGlobal,
|
||||
selected: currentOptions.filter((opt) => opt.checked).map((opt) => opt.policy),
|
||||
...item,
|
||||
tags: getTagsUpdatedBy('policySelection', newPolicyAssignmentTags),
|
||||
});
|
||||
},
|
||||
[isGlobal, onChange]
|
||||
[artifactRestrictedPolicyIds.policyIds, getTagsUpdatedBy, item, onChange]
|
||||
);
|
||||
|
||||
const handleGlobalButtonChange = useCallback(
|
||||
(selectedId: string) => {
|
||||
onChange({
|
||||
isGlobal: selectedId === 'globalPolicy',
|
||||
selected,
|
||||
...item,
|
||||
tags: getTagsUpdatedBy(
|
||||
'policySelection',
|
||||
selectedId === 'globalPolicy'
|
||||
? [GLOBAL_ARTIFACT_TAG]
|
||||
: selectedPolicyIds
|
||||
.concat(artifactRestrictedPolicyIds.policyIds)
|
||||
.map(buildPerPolicyTag)
|
||||
),
|
||||
});
|
||||
},
|
||||
[onChange, selected]
|
||||
[artifactRestrictedPolicyIds.policyIds, getTagsUpdatedBy, item, onChange, selectedPolicyIds]
|
||||
);
|
||||
|
||||
const listBuilderCallback = useCallback<NonNullable<EuiSelectableProps['children']>>(
|
||||
|
@ -243,7 +300,7 @@ export const EffectedPolicySelect = memo<EffectedPolicySelectProps>(
|
|||
<StyledButtonGroup
|
||||
legend="Global Policy Toggle"
|
||||
options={toggleGlobal}
|
||||
idSelected={isGlobal ? 'globalPolicy' : 'perPolicy'}
|
||||
idSelected={selectedAssignmentType}
|
||||
onChange={handleGlobalButtonChange}
|
||||
color="primary"
|
||||
data-test-subj={getTestId('byPolicyGlobalButtonGroup')}
|
||||
|
@ -253,7 +310,8 @@ export const EffectedPolicySelect = memo<EffectedPolicySelectProps>(
|
|||
</StyledEuiFlexItemButtonGroup>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
{!isGlobal &&
|
||||
|
||||
{selectedAssignmentType === 'perPolicy' &&
|
||||
(isLoading ? (
|
||||
<Loader size="l" data-test-subj={getTestId('policiesLoader')} />
|
||||
) : (
|
||||
|
@ -273,9 +331,19 @@ export const EffectedPolicySelect = memo<EffectedPolicySelectProps>(
|
|||
</StyledEuiSelectable>
|
||||
</EuiFormRow>
|
||||
))}
|
||||
{artifactRestrictedPolicyIds.policyIds.length > 0 && !isGlobal && (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<EuiCallOut size="s" data-test-subj={getTestId('unAccessiblePoliciesCallout')}>
|
||||
{ARTIFACT_POLICIES_NOT_ACCESSIBLE_IN_ACTIVE_SPACE_MESSAGE(
|
||||
artifactRestrictedPolicyIds.policyIds.length
|
||||
)}
|
||||
</EuiCallOut>
|
||||
</>
|
||||
)}
|
||||
</EffectivePolicyFormContainer>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
EffectedPolicySelect.displayName = 'EffectedPolicySelect';
|
||||
EffectedPolicySelect.displayName = 'EffectedPolicySelectNew';
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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 type { ArtifactFormComponentProps } from '../../components/artifact_list_page';
|
||||
import type { AppContextTestRender } from '../../../common/mock/endpoint';
|
||||
import { createAppRootMockRenderer } from '../../../common/mock/endpoint';
|
||||
import { ExceptionsListItemGenerator } from '../../../../common/endpoint/data_generators/exceptions_list_item_generator';
|
||||
import { buildPerPolicyTag } from '../../../../common/endpoint/service/artifacts/utils';
|
||||
import type { ArtifactRestrictedPolicyAssignments } from './use_artifact_restricted_policy_assignments';
|
||||
import { useArtifactRestrictedPolicyAssignments } from './use_artifact_restricted_policy_assignments';
|
||||
import { fleetBulkGetPackagePoliciesListHttpMock } from '../../mocks';
|
||||
import { FleetPackagePolicyGenerator } from '../../../../common/endpoint/data_generators/fleet_package_policy_generator';
|
||||
import { waitFor } from '@testing-library/dom';
|
||||
import { packagePolicyRouteService } from '@kbn/fleet-plugin/common';
|
||||
import type { RenderHookResult } from '@testing-library/react';
|
||||
|
||||
describe('useArtifactRestrictedPolicyAssignments()', () => {
|
||||
let testContext: AppContextTestRender;
|
||||
let item: ArtifactFormComponentProps['item'];
|
||||
let renderHook: () => RenderHookResult<ArtifactRestrictedPolicyAssignments, unknown>;
|
||||
|
||||
beforeEach(() => {
|
||||
testContext = createAppRootMockRenderer();
|
||||
|
||||
renderHook = () => {
|
||||
return testContext.renderHook(() => {
|
||||
return useArtifactRestrictedPolicyAssignments(item);
|
||||
});
|
||||
};
|
||||
|
||||
testContext.setExperimentalFlag({ endpointManagementSpaceAwarenessEnabled: true });
|
||||
|
||||
item = new ExceptionsListItemGenerator('seed').generateTrustedApp({
|
||||
tags: [buildPerPolicyTag('1'), buildPerPolicyTag('2'), buildPerPolicyTag('3')],
|
||||
});
|
||||
|
||||
const fleetApiMocks = fleetBulkGetPackagePoliciesListHttpMock(testContext.coreStart.http);
|
||||
const fleetPackagePolicyGenerator = new FleetPackagePolicyGenerator('seed');
|
||||
fleetApiMocks.responseProvider.bulkPackagePolicies.mockReturnValue({
|
||||
items: [
|
||||
fleetPackagePolicyGenerator.generate({ id: '1' }),
|
||||
fleetPackagePolicyGenerator.generate({ id: '2' }),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty array when feature flag is disabled', () => {
|
||||
testContext.setExperimentalFlag({ endpointManagementSpaceAwarenessEnabled: false });
|
||||
const { result } = renderHook();
|
||||
|
||||
expect(result.current).toEqual({ isLoading: false, policyIds: [] });
|
||||
});
|
||||
|
||||
it('should set loading property to true while fetching policies', async () => {
|
||||
const { result } = renderHook();
|
||||
|
||||
expect(result.current).toEqual({ isLoading: true, policyIds: [] });
|
||||
});
|
||||
|
||||
it('should call fleet api with list of policy ids', async () => {
|
||||
renderHook();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(testContext.coreStart.http.post).toHaveBeenCalledWith(
|
||||
packagePolicyRouteService.getBulkGetPath(),
|
||||
expect.objectContaining({
|
||||
body: JSON.stringify({ ids: ['1', '2', '3'], ignoreMissing: true }),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return list of policies that were not found in fleet', async () => {
|
||||
const { result } = renderHook();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.policyIds).toEqual(['3']);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { keyBy } from 'lodash';
|
||||
import { useBulkFetchFleetIntegrationPolicies } from '../policy/use_bulk_fetch_fleet_integration_policies';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
|
||||
import { getPolicyIdsFromArtifact } from '../../../../common/endpoint/service/artifacts';
|
||||
|
||||
export interface ArtifactRestrictedPolicyAssignments {
|
||||
isLoading: boolean;
|
||||
/** The list of policy IDs assigned to the artifact that are NOT currently accessible in active space */
|
||||
policyIds: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an artifact item, hook will calculate if any of the policies assigned to it
|
||||
* are restricted (not accessible by the current user) in the active space.
|
||||
*
|
||||
* NOTE: this hook's logic is executed at most once per artifact
|
||||
*
|
||||
* @param item
|
||||
*/
|
||||
export const useArtifactRestrictedPolicyAssignments = (
|
||||
item: Partial<Pick<ExceptionListItemSchema, 'tags' | 'item_id'>>
|
||||
): ArtifactRestrictedPolicyAssignments => {
|
||||
const isSpaceAwarenessEnabled = useIsExperimentalFeatureEnabled(
|
||||
'endpointManagementSpaceAwarenessEnabled'
|
||||
);
|
||||
const [{ itemId, policies }, setOriginalItem] = useState<{ itemId: string; policies: string[] }>({
|
||||
itemId: item.item_id ?? '',
|
||||
policies: getPolicyIdsFromArtifact(item),
|
||||
});
|
||||
|
||||
const { data, isFetching } = useBulkFetchFleetIntegrationPolicies(
|
||||
{ ids: policies },
|
||||
{ enabled: isSpaceAwarenessEnabled && policies.length > 0 }
|
||||
);
|
||||
|
||||
const restrictedPolicyIds = useMemo(() => {
|
||||
if (!isSpaceAwarenessEnabled || !data?.items) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const policiesFoundById = keyBy(data.items, 'id');
|
||||
|
||||
return policies.filter((id) => !policiesFoundById[id]);
|
||||
}, [data?.items, isSpaceAwarenessEnabled, policies]);
|
||||
|
||||
useEffect(() => {
|
||||
if (item.item_id !== itemId) {
|
||||
setOriginalItem({
|
||||
itemId: item.item_id ?? '',
|
||||
policies: getPolicyIdsFromArtifact(item),
|
||||
});
|
||||
}
|
||||
}, [item, itemId]);
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
isLoading: isSpaceAwarenessEnabled && policies.length > 0 ? isFetching : false,
|
||||
policyIds: restrictedPolicyIds,
|
||||
};
|
||||
}, [isFetching, isSpaceAwarenessEnabled, policies.length, restrictedPolicyIds]);
|
||||
};
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* 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 type { ArtifactFormComponentProps } from '../../components/artifact_list_page';
|
||||
import { ExceptionsListItemGenerator } from '../../../../common/endpoint/data_generators/exceptions_list_item_generator';
|
||||
import { useLicense } from '../../../common/hooks/use_license';
|
||||
import type { LicenseService } from '../../../../common/license';
|
||||
import { buildPerPolicyTag } from '../../../../common/endpoint/service/artifacts/utils';
|
||||
import {
|
||||
createAppRootMockRenderer,
|
||||
type AppContextTestRender,
|
||||
} from '../../../common/mock/endpoint';
|
||||
import { useCanAssignArtifactPerPolicy } from './use_can_assign_artifact_per_policy';
|
||||
import { GLOBAL_ARTIFACT_TAG } from '../../../../common/endpoint/service/artifacts';
|
||||
|
||||
jest.mock('../../../common/hooks/use_license');
|
||||
|
||||
const useLicenseMock = useLicense as jest.Mock<jest.Mocked<LicenseService>>;
|
||||
|
||||
describe('useCanAssignArtifactPerPolicy()', () => {
|
||||
let item: ArtifactFormComponentProps['item'];
|
||||
let mode: ArtifactFormComponentProps['mode'];
|
||||
let hasItemBeenUpdated: boolean;
|
||||
let renderHook: () => ReturnType<AppContextTestRender['renderHook']>;
|
||||
|
||||
beforeEach(() => {
|
||||
const testContext = createAppRootMockRenderer();
|
||||
|
||||
renderHook = () => {
|
||||
return testContext.renderHook(() => {
|
||||
return useCanAssignArtifactPerPolicy(item, mode, hasItemBeenUpdated);
|
||||
});
|
||||
};
|
||||
|
||||
item = new ExceptionsListItemGenerator('seed').generateTrustedApp({
|
||||
tags: [buildPerPolicyTag('123')],
|
||||
});
|
||||
mode = 'edit';
|
||||
hasItemBeenUpdated = false;
|
||||
useLicenseMock().isPlatinumPlus.mockReturnValue(false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return `true` when license is platinum plus', () => {
|
||||
mode = 'create';
|
||||
useLicenseMock().isPlatinumPlus.mockReturnValue(true);
|
||||
const { result } = renderHook();
|
||||
|
||||
expect(result.current).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return `false` when license is not platinum and artifact is global', () => {
|
||||
mode = 'create';
|
||||
useLicenseMock().isPlatinumPlus.mockReturnValue(false);
|
||||
const { result } = renderHook();
|
||||
|
||||
expect(result.current).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return `true` when license is not platinum but artifact is currently per-policy', () => {
|
||||
const { result } = renderHook();
|
||||
|
||||
expect(result.current).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return `true` when license is not platinum and per-policy item was updated to global', () => {
|
||||
const { result, rerender } = renderHook();
|
||||
item.tags = [GLOBAL_ARTIFACT_TAG];
|
||||
hasItemBeenUpdated = true;
|
||||
rerender();
|
||||
|
||||
expect(result.current).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 { useEffect, useMemo, useState } from 'react';
|
||||
import { useLicense } from '../../../common/hooks/use_license';
|
||||
import {
|
||||
isArtifactByPolicy,
|
||||
isArtifactGlobal,
|
||||
} from '../../../../common/endpoint/service/artifacts';
|
||||
import type { ArtifactFormComponentProps } from '../../components/artifact_list_page';
|
||||
|
||||
/**
|
||||
* Calculates if by-policy assignments can be made to an artifact.
|
||||
*
|
||||
* Per-Policy assignment is a Platinum+ licensed feature only, but the component can
|
||||
* be displayed in down-grade conditions: meaning - when user downgrades the license,
|
||||
* we will still allow the component to be displayed in the UI so that user has the
|
||||
* ability to set the artifact to `global`.
|
||||
*/
|
||||
export const useCanAssignArtifactPerPolicy = (
|
||||
item: ArtifactFormComponentProps['item'],
|
||||
mode: ArtifactFormComponentProps['mode'],
|
||||
hasItemBeenUpdated: boolean
|
||||
): boolean => {
|
||||
const isPlatinumPlus = useLicense().isPlatinumPlus();
|
||||
const isGlobal = useMemo(() => isArtifactGlobal(item), [item]);
|
||||
const [wasByPolicy, setWasByPolicy] = useState(isArtifactByPolicy(item));
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasItemBeenUpdated && item.tags) {
|
||||
setWasByPolicy(!isArtifactGlobal({ tags: item.tags }));
|
||||
}
|
||||
}, [item.tags, hasItemBeenUpdated]);
|
||||
|
||||
return useMemo(() => {
|
||||
if (isPlatinumPlus) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mode !== 'edit') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isGlobal) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return wasByPolicy && hasItemBeenUpdated;
|
||||
}, [mode, isGlobal, hasItemBeenUpdated, isPlatinumPlus, wasByPolicy]);
|
||||
};
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* 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 { useQuery as _useQuery } from '@tanstack/react-query';
|
||||
import { useBulkFetchFleetIntegrationPolicies } from './use_bulk_fetch_fleet_integration_policies';
|
||||
import type { AppContextTestRender, ReactQueryHookRenderer } from '../../../common/mock/endpoint';
|
||||
import { createAppRootMockRenderer } from '../../../common/mock/endpoint';
|
||||
import { allFleetHttpMocks } from '../../mocks';
|
||||
import type { BulkGetPackagePoliciesRequestBody } from '@kbn/fleet-plugin/common/types';
|
||||
import { packagePolicyRouteService } from '@kbn/fleet-plugin/common';
|
||||
import type { Mutable } from 'utility-types';
|
||||
|
||||
const useQueryMock = _useQuery as jest.Mock;
|
||||
|
||||
jest.mock('@tanstack/react-query', () => {
|
||||
const actualReactQueryModule = jest.requireActual('@tanstack/react-query');
|
||||
|
||||
return {
|
||||
...actualReactQueryModule,
|
||||
useQuery: jest.fn((...args) => actualReactQueryModule.useQuery(...args)),
|
||||
};
|
||||
});
|
||||
|
||||
describe('useBulkFetchFleetIntegrationPolicies()', () => {
|
||||
type HookRenderer = ReactQueryHookRenderer<
|
||||
Parameters<typeof useBulkFetchFleetIntegrationPolicies>,
|
||||
ReturnType<typeof useBulkFetchFleetIntegrationPolicies>
|
||||
>;
|
||||
|
||||
let reqOptions: Mutable<BulkGetPackagePoliciesRequestBody>;
|
||||
let queryOptions: NonNullable<Parameters<typeof useBulkFetchFleetIntegrationPolicies>[1]>;
|
||||
let http: AppContextTestRender['coreStart']['http'];
|
||||
let renderHook: () => ReturnType<HookRenderer>;
|
||||
|
||||
beforeEach(() => {
|
||||
const testContext = createAppRootMockRenderer();
|
||||
|
||||
reqOptions = { ids: ['1'] };
|
||||
queryOptions = {};
|
||||
http = testContext.coreStart.http;
|
||||
allFleetHttpMocks(http);
|
||||
renderHook = () => {
|
||||
return (testContext.renderReactQueryHook as HookRenderer)(() =>
|
||||
useBulkFetchFleetIntegrationPolicies(reqOptions, queryOptions)
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
useQueryMock.mockClear();
|
||||
});
|
||||
|
||||
it('should call the correct fleet api with the query data provided', async () => {
|
||||
const { data } = await renderHook();
|
||||
|
||||
expect(http.post).toHaveBeenCalledWith(
|
||||
packagePolicyRouteService.getBulkGetPath(),
|
||||
expect.objectContaining({
|
||||
body: JSON.stringify({ ...reqOptions, ignoreMissing: true }),
|
||||
})
|
||||
);
|
||||
expect(data).toEqual(expect.objectContaining({ items: expect.any(Array) }));
|
||||
});
|
||||
|
||||
it('should allow use of `ignoreMissing` request property', async () => {
|
||||
reqOptions.ignoreMissing = false;
|
||||
await renderHook();
|
||||
|
||||
expect(http.post).toHaveBeenCalledWith(
|
||||
packagePolicyRouteService.getBulkGetPath(),
|
||||
expect.objectContaining({
|
||||
body: JSON.stringify(reqOptions),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow useQuery options overrides', async () => {
|
||||
queryOptions.queryKey = ['a', 'b'];
|
||||
queryOptions.retry = false;
|
||||
queryOptions.refetchInterval = 5;
|
||||
await renderHook();
|
||||
|
||||
expect(useQueryMock).toHaveBeenCalledWith(expect.objectContaining(queryOptions));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 type { QueryObserverResult } from '@tanstack/react-query';
|
||||
import { useQuery, type UseQueryOptions } from '@tanstack/react-query';
|
||||
import type { BulkGetPackagePoliciesResponse } from '@kbn/fleet-plugin/common';
|
||||
import { packagePolicyRouteService } from '@kbn/fleet-plugin/common';
|
||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
import type { BulkGetPackagePoliciesRequestBody } from '@kbn/fleet-plugin/common/types';
|
||||
import { useHttp } from '../../../common/lib/kibana';
|
||||
|
||||
/**
|
||||
* Retrieve multiple integration policies (aka: package policies) from fleet using their IDs
|
||||
* @param ids
|
||||
* @param ignoreMissing
|
||||
* @param options
|
||||
*/
|
||||
export const useBulkFetchFleetIntegrationPolicies = (
|
||||
{ ids, ignoreMissing = true }: BulkGetPackagePoliciesRequestBody,
|
||||
options: UseQueryOptions<BulkGetPackagePoliciesResponse, IHttpFetchError> = {}
|
||||
): QueryObserverResult<BulkGetPackagePoliciesResponse> => {
|
||||
const http = useHttp();
|
||||
|
||||
return useQuery<BulkGetPackagePoliciesResponse, IHttpFetchError>({
|
||||
queryKey: ['bulkFetchFleetIntegrationPolicies', ids, ignoreMissing],
|
||||
refetchOnWindowFocus: false,
|
||||
...options,
|
||||
queryFn: async () => {
|
||||
return http.post(packagePolicyRouteService.getBulkGetPath(), {
|
||||
body: JSON.stringify({ ids, ignoreMissing }),
|
||||
version: '2023-10-31',
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useState, useCallback, memo, useEffect, useRef } from 'react';
|
||||
import React, { useMemo, useState, useCallback, memo, useRef } from 'react';
|
||||
import type { EuiSuperSelectOption, EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
import {
|
||||
EuiForm,
|
||||
|
@ -29,6 +29,7 @@ import { isOneOfOperator, isOperator } from '@kbn/securitysolution-list-utils';
|
|||
import { uniq } from 'lodash';
|
||||
|
||||
import { ListOperatorEnum, ListOperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { useCanAssignArtifactPerPolicy } from '../../../../hooks/artifacts/use_can_assign_artifact_per_policy';
|
||||
import { FormattedError } from '../../../../components/formatted_error';
|
||||
import { OS_TITLES } from '../../../../common/translations';
|
||||
import type {
|
||||
|
@ -53,17 +54,10 @@ import {
|
|||
VALUE_LABEL_HELPER,
|
||||
SINGLE_VALUE_LABEL_HELPER,
|
||||
} from '../../translations';
|
||||
import type { EffectedPolicySelection } from '../../../../components/effected_policy_select';
|
||||
import type { EffectedPolicySelectProps } from '../../../../components/effected_policy_select';
|
||||
import { EffectedPolicySelect } from '../../../../components/effected_policy_select';
|
||||
import { useLicense } from '../../../../../common/hooks/use_license';
|
||||
import { isValidHash } from '../../../../../../common/endpoint/service/artifacts/validations';
|
||||
import {
|
||||
getArtifactTagsByPolicySelection,
|
||||
isArtifactGlobal,
|
||||
} from '../../../../../../common/endpoint/service/artifacts';
|
||||
import type { PolicyData } from '../../../../../../common/endpoint/types';
|
||||
import { useTestIdGenerator } from '../../../../hooks/use_test_id_generator';
|
||||
import { useGetUpdatedTags } from '../../../../hooks/artifacts';
|
||||
|
||||
const testIdPrefix = 'blocklist-form';
|
||||
|
||||
|
@ -120,37 +114,8 @@ export const BlockListForm = memo<ArtifactFormComponentProps>(
|
|||
const [valueVisited, setValueVisited] = useState({ value: false }); // Use object to trigger re-render
|
||||
const warningsRef = useRef<ItemValidation>({ name: {}, value: {} });
|
||||
const errorsRef = useRef<ItemValidation>({ name: {}, value: {} });
|
||||
const [selectedPolicies, setSelectedPolicies] = useState<PolicyData[]>([]);
|
||||
const isPlatinumPlus = useLicense().isPlatinumPlus();
|
||||
const isGlobal = useMemo(() => isArtifactGlobal(item), [item]);
|
||||
const [wasByPolicy, setWasByPolicy] = useState(!isArtifactGlobal(item));
|
||||
const [hasFormChanged, setHasFormChanged] = useState(false);
|
||||
const { getTagsUpdatedBy } = useGetUpdatedTags(item);
|
||||
|
||||
const showAssignmentSection = useMemo(() => {
|
||||
return (
|
||||
isPlatinumPlus ||
|
||||
(mode === 'edit' && (!isGlobal || (wasByPolicy && isGlobal && hasFormChanged)))
|
||||
);
|
||||
}, [mode, isGlobal, hasFormChanged, isPlatinumPlus, wasByPolicy]);
|
||||
|
||||
// set initial state of `wasByPolicy` that checks if the initial state of the exception was by policy or not
|
||||
useEffect(() => {
|
||||
if (!hasFormChanged && item.tags) {
|
||||
setWasByPolicy(!isArtifactGlobal({ tags: item.tags }));
|
||||
}
|
||||
}, [item.tags, hasFormChanged]);
|
||||
|
||||
// select policies if editing
|
||||
useEffect(() => {
|
||||
if (hasFormChanged) return;
|
||||
const policyIds = item.tags?.map((tag) => tag.split(':')[1]) ?? [];
|
||||
if (!policyIds.length) return;
|
||||
const policiesData = policies.filter((policy) => policyIds.includes(policy.id));
|
||||
|
||||
setSelectedPolicies(policiesData);
|
||||
}, [hasFormChanged, item.tags, policies]);
|
||||
|
||||
const showAssignmentSection = useCanAssignArtifactPerPolicy(item, mode, hasFormChanged);
|
||||
const getTestId = useTestIdGenerator(testIdPrefix);
|
||||
|
||||
const blocklistEntry = useMemo((): BlocklistEntry => {
|
||||
|
@ -547,23 +512,16 @@ export const BlockListForm = memo<ArtifactFormComponentProps>(
|
|||
[validateValues, onChange, item, blocklistEntry]
|
||||
);
|
||||
|
||||
const handleOnPolicyChange = useCallback(
|
||||
(change: EffectedPolicySelection) => {
|
||||
const tags = getTagsUpdatedBy('policySelection', getArtifactTagsByPolicySelection(change));
|
||||
const nextItem = { ...item, tags };
|
||||
|
||||
// Preserve old selected policies when switching to global
|
||||
if (!change.isGlobal) {
|
||||
setSelectedPolicies(change.selected);
|
||||
}
|
||||
validateValues(nextItem);
|
||||
const handleEffectedPolicyOnChange: EffectedPolicySelectProps['onChange'] = useCallback(
|
||||
(updatedItem) => {
|
||||
validateValues(updatedItem);
|
||||
onChange({
|
||||
isValid: isValid(errorsRef.current),
|
||||
item: nextItem,
|
||||
item: updatedItem,
|
||||
});
|
||||
setHasFormChanged(true);
|
||||
},
|
||||
[getTagsUpdatedBy, item, validateValues, onChange]
|
||||
[onChange, validateValues]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -710,11 +668,9 @@ export const BlockListForm = memo<ArtifactFormComponentProps>(
|
|||
<EuiHorizontalRule />
|
||||
<EuiFormRow fullWidth>
|
||||
<EffectedPolicySelect
|
||||
isGlobal={isGlobal}
|
||||
isPlatinumPlus={isPlatinumPlus}
|
||||
selected={selectedPolicies}
|
||||
item={item}
|
||||
options={policies}
|
||||
onChange={handleOnPolicyChange}
|
||||
onChange={handleEffectedPolicyOnChange}
|
||||
isLoading={policiesIsLoading}
|
||||
description={POLICY_SELECT_DESCRIPTION}
|
||||
data-test-subj={getTestId('effectedPolicies')}
|
||||
|
|
|
@ -41,6 +41,7 @@ import { OperatingSystem } from '@kbn/securitysolution-utils';
|
|||
import { getExceptionBuilderComponentLazy } from '@kbn/lists-plugin/public';
|
||||
import type { OnChangeProps } from '@kbn/lists-plugin/public';
|
||||
import type { ValueSuggestionsGetFn } from '@kbn/unified-search-plugin/public/autocomplete/providers/value_suggestion_provider';
|
||||
import { useCanAssignArtifactPerPolicy } from '../../../../hooks/artifacts/use_can_assign_artifact_per_policy';
|
||||
import { FormattedError } from '../../../../components/formatted_error';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
|
||||
import { useGetUpdatedTags } from '../../../../hooks/artifacts';
|
||||
|
@ -56,17 +57,10 @@ import {
|
|||
} from '../../../../../../common/endpoint/constants';
|
||||
import { useSuggestions } from '../../../../hooks/use_suggestions';
|
||||
import { useTestIdGenerator } from '../../../../hooks/use_test_id_generator';
|
||||
import type { PolicyData } from '../../../../../../common/endpoint/types';
|
||||
import { useFetchIndex } from '../../../../../common/containers/source';
|
||||
import { Loader } from '../../../../../common/components/loader';
|
||||
import { useLicense } from '../../../../../common/hooks/use_license';
|
||||
import { useKibana } from '../../../../../common/lib/kibana';
|
||||
import type { ArtifactFormComponentProps } from '../../../../components/artifact_list_page';
|
||||
import {
|
||||
isArtifactGlobal,
|
||||
getPolicyIdsFromArtifact,
|
||||
getArtifactTagsByPolicySelection,
|
||||
} from '../../../../../../common/endpoint/service/artifacts';
|
||||
|
||||
import {
|
||||
ABOUT_EVENT_FILTERS,
|
||||
|
@ -79,7 +73,7 @@ import {
|
|||
import { OS_TITLES, CONFIRM_WARNING_MODAL_LABELS } from '../../../../common/translations';
|
||||
import { ENDPOINT_EVENT_FILTERS_LIST_ID, EVENT_FILTER_LIST_TYPE } from '../../constants';
|
||||
|
||||
import type { EffectedPolicySelection } from '../../../../components/effected_policy_select';
|
||||
import type { EffectedPolicySelectProps } from '../../../../components/effected_policy_select';
|
||||
import { EffectedPolicySelect } from '../../../../components/effected_policy_select';
|
||||
import { ExceptionItemComments } from '../../../../../detection_engine/rule_exceptions/components/item_comments';
|
||||
import { EventFiltersApiClient } from '../../service/api_client';
|
||||
|
@ -166,10 +160,6 @@ export const EventFiltersForm: React.FC<ArtifactFormComponentProps & { allowSele
|
|||
const [newComment, setNewComment] = useState('');
|
||||
const [hasCommentError, setHasCommentError] = useState(false);
|
||||
const [hasBeenInputNameVisited, setHasBeenInputNameVisited] = useState(false);
|
||||
const [selectedPolicies, setSelectedPolicies] = useState<PolicyData[]>([]);
|
||||
const isPlatinumPlus = useLicense().isPlatinumPlus();
|
||||
const isGlobal = useMemo(() => isArtifactGlobal(exception), [exception]);
|
||||
const [wasByPolicy, setWasByPolicy] = useState(!isArtifactGlobal(exception));
|
||||
const [hasDuplicateFields, setHasDuplicateFields] = useState<boolean>(false);
|
||||
const [hasWildcardWithWrongOperator, setHasWildcardWithWrongOperator] = useState<boolean>(
|
||||
hasWrongOperatorWithWildcard([exception])
|
||||
|
@ -205,12 +195,7 @@ export const EventFiltersForm: React.FC<ArtifactFormComponentProps & { allowSele
|
|||
[]
|
||||
);
|
||||
|
||||
const showAssignmentSection = useMemo(() => {
|
||||
return (
|
||||
isPlatinumPlus ||
|
||||
(mode === 'edit' && (!isGlobal || (wasByPolicy && isGlobal && hasFormChanged)))
|
||||
);
|
||||
}, [mode, isGlobal, hasFormChanged, isPlatinumPlus, wasByPolicy]);
|
||||
const showAssignmentSection = useCanAssignArtifactPerPolicy(exception, mode, hasFormChanged);
|
||||
|
||||
const isFormValid = useMemo(() => {
|
||||
// verify that it has legit entries
|
||||
|
@ -259,24 +244,6 @@ export const EventFiltersForm: React.FC<ArtifactFormComponentProps & { allowSele
|
|||
]
|
||||
);
|
||||
|
||||
// set initial state of `wasByPolicy` that checks
|
||||
// if the initial state of the exception was by policy or not
|
||||
useEffect(() => {
|
||||
if (!hasFormChanged && exception.tags) {
|
||||
setWasByPolicy(!isArtifactGlobal({ tags: exception.tags }));
|
||||
}
|
||||
}, [exception.tags, hasFormChanged]);
|
||||
|
||||
// select policies if editing
|
||||
useEffect(() => {
|
||||
if (hasFormChanged) return;
|
||||
const policyIds = exception.tags ? getPolicyIdsFromArtifact({ tags: exception.tags }) : [];
|
||||
|
||||
if (!policyIds.length) return;
|
||||
const policiesData = policies.filter((policy) => policyIds.includes(policy.id));
|
||||
setSelectedPolicies(policiesData);
|
||||
}, [hasFormChanged, exception, policies]);
|
||||
|
||||
const eventFilterItem = useMemo<ArtifactFormComponentProps['item']>(() => {
|
||||
const ef: ArtifactFormComponentProps['item'] = exception;
|
||||
ef.entries = exception.entries.length
|
||||
|
@ -689,44 +656,27 @@ export const EventFiltersForm: React.FC<ArtifactFormComponentProps & { allowSele
|
|||
[allowSelectOs, exceptionBuilderComponentMemo, osInputMemo, filterTypeSubsection]
|
||||
);
|
||||
|
||||
// policy and handler
|
||||
const handleOnPolicyChange = useCallback(
|
||||
(change: EffectedPolicySelection) => {
|
||||
const policySelectionTags = getArtifactTagsByPolicySelection(change);
|
||||
|
||||
// Preserve old selected policies when switching to global
|
||||
if (!change.isGlobal) {
|
||||
setSelectedPolicies(change.selected);
|
||||
const handleEffectedPolicyOnChange: EffectedPolicySelectProps['onChange'] = useCallback(
|
||||
(updatedItem) => {
|
||||
processChanged({ tags: updatedItem.tags ?? [] });
|
||||
if (!hasFormChanged) {
|
||||
setHasFormChanged(true);
|
||||
}
|
||||
|
||||
const tags = getTagsUpdatedBy('policySelection', policySelectionTags);
|
||||
processChanged({ tags });
|
||||
if (!hasFormChanged) setHasFormChanged(true);
|
||||
},
|
||||
[getTagsUpdatedBy, processChanged, hasFormChanged]
|
||||
[hasFormChanged, processChanged]
|
||||
);
|
||||
|
||||
const policiesSection = useMemo(
|
||||
() => (
|
||||
<EffectedPolicySelect
|
||||
selected={selectedPolicies}
|
||||
item={exception}
|
||||
options={policies}
|
||||
isGlobal={isGlobal}
|
||||
isLoading={policiesIsLoading}
|
||||
isPlatinumPlus={isPlatinumPlus}
|
||||
onChange={handleOnPolicyChange}
|
||||
onChange={handleEffectedPolicyOnChange}
|
||||
data-test-subj={getTestId('effectedPolicies')}
|
||||
/>
|
||||
),
|
||||
[
|
||||
selectedPolicies,
|
||||
policies,
|
||||
isGlobal,
|
||||
policiesIsLoading,
|
||||
isPlatinumPlus,
|
||||
handleOnPolicyChange,
|
||||
getTestId,
|
||||
]
|
||||
[exception, policies, policiesIsLoading, handleEffectedPolicyOnChange, getTestId]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -19,16 +19,8 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTestIdGenerator } from '../../../../hooks/use_test_id_generator';
|
||||
import { isValidIPv4OrCIDR } from '../../../../../../common/endpoint/utils/is_valid_ip';
|
||||
import type {
|
||||
EffectedPolicySelection,
|
||||
EffectedPolicySelectProps,
|
||||
} from '../../../../components/effected_policy_select';
|
||||
import type { EffectedPolicySelectProps } from '../../../../components/effected_policy_select';
|
||||
import { EffectedPolicySelect } from '../../../../components/effected_policy_select';
|
||||
import {
|
||||
getArtifactTagsByPolicySelection,
|
||||
getEffectedPolicySelectionByTags,
|
||||
isArtifactGlobal,
|
||||
} from '../../../../../../common/endpoint/service/artifacts';
|
||||
import {
|
||||
DESCRIPTION_LABEL,
|
||||
DESCRIPTION_PLACEHOLDER,
|
||||
|
@ -41,7 +33,6 @@ import {
|
|||
} from './translations';
|
||||
import type { ArtifactFormComponentProps } from '../../../../components/artifact_list_page';
|
||||
import { FormattedError } from '../../../../components/formatted_error';
|
||||
import { useGetUpdatedTags } from '../../../../hooks/artifacts';
|
||||
|
||||
export const testIdPrefix = 'hostIsolationExceptions-form';
|
||||
|
||||
|
@ -68,12 +59,6 @@ export const HostIsolationExceptionsForm = memo<ArtifactFormComponentProps>(
|
|||
const [hasNameError, setHasNameError] = useState(!exception.name);
|
||||
const [hasIpError, setHasIpError] = useState(!ipEntry.value);
|
||||
const getTestId = useTestIdGenerator(testIdPrefix);
|
||||
const { getTagsUpdatedBy } = useGetUpdatedTags(exception);
|
||||
|
||||
const [selectedPolicies, setSelectedPolicies] = useState<EffectedPolicySelection>({
|
||||
isGlobal: isArtifactGlobal(exception),
|
||||
selected: [],
|
||||
});
|
||||
|
||||
const isFormContentValid = useMemo(() => {
|
||||
return !hasNameError && !hasIpError;
|
||||
|
@ -129,23 +114,11 @@ export const HostIsolationExceptionsForm = memo<ArtifactFormComponentProps>(
|
|||
[ipEntry, notifyOfChange]
|
||||
);
|
||||
|
||||
const handlePolicySelectChange: EffectedPolicySelectProps['onChange'] = useCallback(
|
||||
(selection) => {
|
||||
// preserve the previous selection between global and not global toggle
|
||||
if (selection.isGlobal) {
|
||||
setSelectedPolicies({ isGlobal: true, selected: selection.selected });
|
||||
} else {
|
||||
setSelectedPolicies(selection);
|
||||
}
|
||||
|
||||
const tags = getTagsUpdatedBy(
|
||||
'policySelection',
|
||||
getArtifactTagsByPolicySelection(selection)
|
||||
);
|
||||
|
||||
notifyOfChange({ tags });
|
||||
const handleEffectedPolicyOnChange: EffectedPolicySelectProps['onChange'] = useCallback(
|
||||
(updatedItem) => {
|
||||
notifyOfChange(updatedItem);
|
||||
},
|
||||
[getTagsUpdatedBy, notifyOfChange]
|
||||
[notifyOfChange]
|
||||
);
|
||||
|
||||
const handleOnDescriptionChange = useCallback(
|
||||
|
@ -235,13 +208,6 @@ export const HostIsolationExceptionsForm = memo<ArtifactFormComponentProps>(
|
|||
[disabled, exception.description, handleOnDescriptionChange]
|
||||
);
|
||||
|
||||
// set current policies if not previously selected
|
||||
useEffect(() => {
|
||||
if (selectedPolicies.selected.length === 0 && exception.tags) {
|
||||
setSelectedPolicies(getEffectedPolicySelectionByTags(exception.tags, policies));
|
||||
}
|
||||
}, [exception.tags, policies, selectedPolicies.selected.length]);
|
||||
|
||||
// Anytime the `notificyOfChange()` is re-defined, call it with current values.
|
||||
// This will happen
|
||||
useEffect(() => {
|
||||
|
@ -311,11 +277,9 @@ export const HostIsolationExceptionsForm = memo<ArtifactFormComponentProps>(
|
|||
isDisabled={disabled}
|
||||
>
|
||||
<EffectedPolicySelect
|
||||
isGlobal={selectedPolicies.isGlobal}
|
||||
isPlatinumPlus={true}
|
||||
selected={selectedPolicies.selected}
|
||||
item={exception}
|
||||
options={policies}
|
||||
onChange={handlePolicySelectChange}
|
||||
onChange={handleEffectedPolicyOnChange}
|
||||
data-test-subj={getTestId('effectedPolicies')}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
|
|
@ -20,10 +20,7 @@ import {
|
|||
exceptionsListAllHttpMocks,
|
||||
fleetGetEndpointPackagePolicyListHttpMock,
|
||||
} from '../../../../../mocks';
|
||||
import {
|
||||
clickOnEffectedPolicy,
|
||||
isEffectedPolicySelected,
|
||||
} from '../../../../../components/effected_policy_select/test_utils';
|
||||
import { isEffectedPolicySelected } from '../../../../../components/effected_policy_select/test_utils';
|
||||
import { BY_POLICY_ARTIFACT_TAG_PREFIX } from '../../../../../../../common/endpoint/service/artifacts';
|
||||
import type { HttpFetchOptionsWithPath, IHttpFetchError } from '@kbn/core/public';
|
||||
import { testIdPrefix } from '../form';
|
||||
|
@ -150,40 +147,6 @@ describe('When on the host isolation exceptions entry form', () => {
|
|||
.classList.contains('euiButtonGroupButton-isSelected')
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should show policy as selected when user clicks on it', async () => {
|
||||
await userEvent.click(
|
||||
renderResult.getByTestId('hostIsolationExceptions-form-effectedPolicies-perPolicy')
|
||||
);
|
||||
await clickOnEffectedPolicy(renderResult, testIdPrefix);
|
||||
|
||||
await expect(isEffectedPolicySelected(renderResult, testIdPrefix)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('should retain the previous policy selection when switching from per-policy to global', async () => {
|
||||
// move to per-policy and select the first
|
||||
await userEvent.click(
|
||||
renderResult.getByTestId('hostIsolationExceptions-form-effectedPolicies-perPolicy')
|
||||
);
|
||||
await clickOnEffectedPolicy(renderResult, testIdPrefix);
|
||||
|
||||
await expect(isEffectedPolicySelected(renderResult, testIdPrefix)).resolves.toBe(true);
|
||||
|
||||
// move back to global
|
||||
await userEvent.click(
|
||||
renderResult.getByTestId('hostIsolationExceptions-form-effectedPolicies-global')
|
||||
);
|
||||
|
||||
expect(
|
||||
renderResult.queryByTestId(`${testIdPrefix}-effectedPolicies-policiesSelectable`)
|
||||
).toBeFalsy();
|
||||
|
||||
// move back to per-policy
|
||||
await userEvent.click(
|
||||
renderResult.getByTestId('hostIsolationExceptions-form-effectedPolicies-perPolicy')
|
||||
);
|
||||
await expect(isEffectedPolicySelected(renderResult, testIdPrefix)).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and editing an existing exception with global policy', () => {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import type { ChangeEventHandler } from 'react';
|
||||
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { memo, useCallback, useMemo, useState } from 'react';
|
||||
import type { EuiFieldTextProps, EuiSuperSelectOption } from '@elastic/eui';
|
||||
import {
|
||||
EuiFieldText,
|
||||
|
@ -29,23 +29,17 @@ import {
|
|||
OperatingSystem,
|
||||
} from '@kbn/securitysolution-utils';
|
||||
import { WildCardWithWrongOperatorCallout } from '@kbn/securitysolution-exception-list-components';
|
||||
import { useGetUpdatedTags } from '../../../../hooks/artifacts';
|
||||
import { useCanAssignArtifactPerPolicy } from '../../../../hooks/artifacts/use_can_assign_artifact_per_policy';
|
||||
import { FormattedError } from '../../../../components/formatted_error';
|
||||
import type {
|
||||
TrustedAppConditionEntry,
|
||||
NewTrustedApp,
|
||||
PolicyData,
|
||||
} from '../../../../../../common/endpoint/types';
|
||||
import {
|
||||
isValidHash,
|
||||
getDuplicateFields,
|
||||
} from '../../../../../../common/endpoint/service/artifacts/validations';
|
||||
|
||||
import {
|
||||
isArtifactGlobal,
|
||||
getPolicyIdsFromArtifact,
|
||||
getArtifactTagsByPolicySelection,
|
||||
} from '../../../../../../common/endpoint/service/artifacts';
|
||||
import { isSignerFieldExcluded } from '../../state/type_guards';
|
||||
|
||||
import {
|
||||
|
@ -63,8 +57,7 @@ import { OS_TITLES, CONFIRM_WARNING_MODAL_LABELS } from '../../../../common/tran
|
|||
import type { LogicalConditionBuilderProps } from './logical_condition';
|
||||
import { LogicalConditionBuilder } from './logical_condition';
|
||||
import { useTestIdGenerator } from '../../../../hooks/use_test_id_generator';
|
||||
import { useLicense } from '../../../../../common/hooks/use_license';
|
||||
import type { EffectedPolicySelection } from '../../../../components/effected_policy_select';
|
||||
import type { EffectedPolicySelectProps } from '../../../../components/effected_policy_select';
|
||||
import { EffectedPolicySelect } from '../../../../components/effected_policy_select';
|
||||
import type { ArtifactFormComponentProps } from '../../../../components/artifact_list_page';
|
||||
import { TrustedAppsArtifactsDocsLink } from './artifacts_docs_link';
|
||||
|
@ -244,41 +237,8 @@ export const TrustedAppsForm = memo<ArtifactFormComponentProps>(
|
|||
[key in keyof NewTrustedApp]: boolean;
|
||||
}>
|
||||
>({});
|
||||
|
||||
const [selectedPolicies, setSelectedPolicies] = useState<PolicyData[]>([]);
|
||||
const isPlatinumPlus = useLicense().isPlatinumPlus();
|
||||
const isGlobal = useMemo(() => isArtifactGlobal(item), [item]);
|
||||
const [wasByPolicy, setWasByPolicy] = useState(!isArtifactGlobal(item));
|
||||
const [hasFormChanged, setHasFormChanged] = useState(false);
|
||||
const { getTagsUpdatedBy } = useGetUpdatedTags(item);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasFormChanged && item.tags) {
|
||||
setWasByPolicy(!isArtifactGlobal({ tags: item.tags }));
|
||||
}
|
||||
}, [item.tags, hasFormChanged]);
|
||||
|
||||
// select policies if editing
|
||||
useEffect(() => {
|
||||
if (hasFormChanged) {
|
||||
return;
|
||||
}
|
||||
const policyIds = item.tags ? getPolicyIdsFromArtifact({ tags: item.tags }) : [];
|
||||
if (!policyIds.length) {
|
||||
return;
|
||||
}
|
||||
const policiesData = policies.filter((policy) => policyIds.includes(policy.id));
|
||||
|
||||
setSelectedPolicies(policiesData);
|
||||
}, [hasFormChanged, item, policies]);
|
||||
|
||||
const showAssignmentSection = useMemo(() => {
|
||||
return (
|
||||
isPlatinumPlus ||
|
||||
(mode === 'edit' && (!isGlobal || (wasByPolicy && isGlobal && hasFormChanged)))
|
||||
);
|
||||
}, [mode, isGlobal, hasFormChanged, isPlatinumPlus, wasByPolicy]);
|
||||
|
||||
const showAssignmentSection = useCanAssignArtifactPerPolicy(item, mode, hasFormChanged);
|
||||
const [validationResult, setValidationResult] = useState<ValidationResult>(() =>
|
||||
validateValues(item)
|
||||
);
|
||||
|
@ -302,19 +262,12 @@ export const TrustedAppsForm = memo<ArtifactFormComponentProps>(
|
|||
[onChange]
|
||||
);
|
||||
|
||||
const handleOnPolicyChange = useCallback(
|
||||
(change: EffectedPolicySelection) => {
|
||||
const tags = getTagsUpdatedBy('policySelection', getArtifactTagsByPolicySelection(change));
|
||||
const nextItem = { ...item, tags };
|
||||
|
||||
// Preserve old selected policies when switching to global
|
||||
if (!change.isGlobal) {
|
||||
setSelectedPolicies(change.selected);
|
||||
}
|
||||
processChanged(nextItem);
|
||||
const handleEffectedPolicyOnChange: EffectedPolicySelectProps['onChange'] = useCallback(
|
||||
(updatedItem) => {
|
||||
processChanged(updatedItem);
|
||||
setHasFormChanged(true);
|
||||
},
|
||||
[getTagsUpdatedBy, item, processChanged]
|
||||
[processChanged]
|
||||
);
|
||||
|
||||
const handleOnNameOrDescriptionChange = useCallback<
|
||||
|
@ -589,14 +542,12 @@ export const TrustedAppsForm = memo<ArtifactFormComponentProps>(
|
|||
<EuiHorizontalRule />
|
||||
<EuiFormRow fullWidth data-test-subj={getTestId('policySelection')}>
|
||||
<EffectedPolicySelect
|
||||
isGlobal={isGlobal}
|
||||
isPlatinumPlus={isPlatinumPlus}
|
||||
selected={selectedPolicies}
|
||||
item={item}
|
||||
options={policies}
|
||||
onChange={handleOnPolicyChange}
|
||||
isLoading={policiesIsLoading}
|
||||
description={POLICY_SELECT_DESCRIPTION}
|
||||
data-test-subj={getTestId('effectedPolicies')}
|
||||
isLoading={policiesIsLoading}
|
||||
onChange={handleEffectedPolicyOnChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
|
|
|
@ -29,7 +29,7 @@ import { HOST_ISOLATION_EXCEPTIONS_LIST_DEFINITION } from '../../../public/manag
|
|||
import type { NewTrustedApp } from '../../../common/endpoint/types';
|
||||
import { newTrustedAppToCreateExceptionListItem } from '../../../public/management/pages/trusted_apps/service/mappers';
|
||||
|
||||
const ensureArtifactListExists = memoize(
|
||||
export const ensureArtifactListExists = memoize(
|
||||
async (
|
||||
kbnClient: KbnClient,
|
||||
artifactType: keyof typeof ENDPOINT_ARTIFACT_LISTS | 'endpointExceptions'
|
||||
|
@ -91,7 +91,7 @@ const ensureArtifactListExists = memoize(
|
|||
* @param kbnClient
|
||||
* @param data
|
||||
*/
|
||||
const createExceptionListItem = async (
|
||||
export const createExceptionListItem = async (
|
||||
kbnClient: KbnClient,
|
||||
data: CreateExceptionListItemSchema
|
||||
): Promise<ExceptionListItemSchema> => {
|
||||
|
|
|
@ -106,6 +106,36 @@ describe('Artifacts: setFindRequestFilterScopeToActiveSpace()', () => {
|
|||
) AND (somevalue:match-this)`);
|
||||
});
|
||||
|
||||
it('should inject additional filtering when there is no visible policies in active space', async () => {
|
||||
(
|
||||
endpointAppContextServices.getInternalFleetServices()
|
||||
.packagePolicy as jest.Mocked<PackagePolicyClient>
|
||||
).listIds.mockResolvedValue({
|
||||
items: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
});
|
||||
await setFindRequestFilterScopeToActiveSpace(
|
||||
endpointAppContextServices,
|
||||
kibanaRequest,
|
||||
findOptionsMock
|
||||
);
|
||||
|
||||
expect(findOptionsMock.filter).toEqual(`
|
||||
(
|
||||
(
|
||||
exception-list-agnostic.attributes.tags:("policy:all")
|
||||
)
|
||||
OR
|
||||
(
|
||||
NOT exception-list-agnostic.attributes.tags:"policy:*"
|
||||
AND
|
||||
exception-list-agnostic.attributes.tags:"ownerSpaceId:default"
|
||||
)
|
||||
)`);
|
||||
});
|
||||
|
||||
it('should inject additional filtering when using multi-list search format', async () => {
|
||||
findOptionsMock.listId = [
|
||||
ENDPOINT_ARTIFACT_LISTS.trustedApps.id,
|
||||
|
|
|
@ -70,7 +70,8 @@ export const setFindRequestFilterScopeToActiveSpace = async (
|
|||
: ` OR ${allEndpointPolicyIds
|
||||
.map((policyId) => `"${buildPerPolicyTag(policyId)}"`)
|
||||
.join(' OR ')}
|
||||
)
|
||||
)`
|
||||
}
|
||||
)
|
||||
OR
|
||||
(
|
||||
|
@ -78,8 +79,7 @@ export const setFindRequestFilterScopeToActiveSpace = async (
|
|||
AND
|
||||
exception-list-agnostic.attributes.tags:"${buildSpaceOwnerIdTag(spaceId)}"
|
||||
)
|
||||
)`
|
||||
}`;
|
||||
)`;
|
||||
|
||||
if (isSingleListFindOptions(findOptions)) {
|
||||
findOptions.filter = `${spaceVisibleDataFilter}${
|
||||
|
|
|
@ -241,6 +241,24 @@ describe('When using Artifacts Exceptions BaseValidator', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
describe('#validateByPolicyItem()', () => {
|
||||
let currentItem: ExceptionListItemSchema;
|
||||
|
||||
beforeEach(() => {
|
||||
currentItem = createExceptionListItemMock({
|
||||
tags: exceptionLikeItem.tags,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not error if policy is not returned by fleet for active space, but it is already associated with item', async () => {
|
||||
packagePolicyService.getByIDs.mockResolvedValue([]);
|
||||
|
||||
await expect(
|
||||
initValidator()._validateByPolicyItem(exceptionLikeItem, currentItem)
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#validateCreateOnwerSpaceIds()', () => {
|
||||
it('should error if adding an spaceOwnerId but has no global artifact management authz', async () => {
|
||||
setArtifactOwnerSpaceId(exceptionLikeItem, 'foo');
|
||||
|
|
|
@ -14,6 +14,7 @@ import { OperatingSystem } from '@kbn/securitysolution-utils';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import {} from '@kbn/lists-plugin/server/services/exception_lists/exception_list_client_types';
|
||||
import { groupBy } from 'lodash';
|
||||
import type { PackagePolicy } from '@kbn/fleet-plugin/common';
|
||||
import { stringify } from '../../../endpoint/utils/stringify';
|
||||
import { ENDPOINT_AUTHZ_ERROR_MESSAGE } from '../../../endpoint/errors';
|
||||
import {
|
||||
|
@ -175,7 +176,11 @@ export class BaseValidator {
|
|||
* Validates that by-policy artifacts is permitted and that each policy referenced in the item is valid
|
||||
* @protected
|
||||
*/
|
||||
protected async validateByPolicyItem(item: ExceptionItemLikeOptions): Promise<void> {
|
||||
protected async validateByPolicyItem(
|
||||
item: ExceptionItemLikeOptions,
|
||||
/** Should be provided when an existing item is being updated. Will `undefined` on create flows */
|
||||
currentItem?: ExceptionListItemSchema
|
||||
): Promise<void> {
|
||||
if (this.isItemByPolicy(item)) {
|
||||
const spaceId = this.endpointAppContext.experimentalFeatures
|
||||
.endpointManagementSpaceAwarenessEnabled
|
||||
|
@ -190,16 +195,17 @@ export class BaseValidator {
|
|||
return;
|
||||
}
|
||||
|
||||
const policiesFromFleet = await packagePolicy.getByIDs(soClient, policyIds, {
|
||||
ignoreMissing: true,
|
||||
});
|
||||
const policiesFromFleet: PackagePolicy[] =
|
||||
(await packagePolicy.getByIDs(soClient, policyIds, {
|
||||
ignoreMissing: true,
|
||||
})) ?? [];
|
||||
|
||||
this.logger.debug(
|
||||
() =>
|
||||
`Lookup of policy ids:\n[${policyIds.join(
|
||||
' | '
|
||||
)}] for space [${spaceId}] returned:\n${stringify(
|
||||
(policiesFromFleet ?? []).map((policy) => ({
|
||||
policiesFromFleet.map((policy) => ({
|
||||
id: policy.id,
|
||||
name: policy.name,
|
||||
spaceIds: policy.spaceIds,
|
||||
|
@ -207,16 +213,22 @@ export class BaseValidator {
|
|||
)}`
|
||||
);
|
||||
|
||||
if (!policiesFromFleet) {
|
||||
throw new EndpointArtifactExceptionValidationError(
|
||||
`invalid policy ids: ${policyIds.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
const invalidPolicyIds = policyIds.filter(
|
||||
let invalidPolicyIds: string[] = policyIds.filter(
|
||||
(policyId) => !policiesFromFleet.some((policy) => policyId === policy.id)
|
||||
);
|
||||
|
||||
if (
|
||||
this.endpointAppContext.experimentalFeatures.endpointManagementSpaceAwarenessEnabled &&
|
||||
invalidPolicyIds.length > 0 &&
|
||||
currentItem
|
||||
) {
|
||||
const currentItemPolicyIds = getPolicyIdsFromArtifact(currentItem);
|
||||
|
||||
// Check to see if the invalid policy IDs are ones that the current item (pre-update) already has,
|
||||
// which implies that they are valid, but not visible in the active space.
|
||||
invalidPolicyIds = invalidPolicyIds.filter((id) => !currentItemPolicyIds.includes(id));
|
||||
}
|
||||
|
||||
if (invalidPolicyIds.length) {
|
||||
throw new EndpointArtifactExceptionValidationError(
|
||||
`invalid policy ids: ${invalidPolicyIds.join(', ')}`
|
||||
|
@ -226,7 +238,7 @@ export class BaseValidator {
|
|||
}
|
||||
|
||||
/**
|
||||
* If the item being updated is `by policy`, method validates if anyting was changes in regard to
|
||||
* If the item being updated is `by policy`, method validates if anything was changes in regard to
|
||||
* the effected scope of the by policy settings.
|
||||
*
|
||||
* @param updatedItem
|
||||
|
|
|
@ -298,7 +298,7 @@ export class BlocklistValidator extends BaseValidator {
|
|||
}
|
||||
}
|
||||
|
||||
await this.validateByPolicyItem(updatedItem);
|
||||
await this.validateByPolicyItem(updatedItem, currentItem);
|
||||
await this.validateUpdateOwnerSpaceIds(updatedItem, currentItem);
|
||||
await this.validateCanUpdateItemInActiveSpace(_updatedItem, currentItem);
|
||||
|
||||
|
|
|
@ -84,7 +84,7 @@ export class EventFilterValidator extends BaseValidator {
|
|||
}
|
||||
}
|
||||
|
||||
await this.validateByPolicyItem(updatedItem);
|
||||
await this.validateByPolicyItem(updatedItem, currentItem);
|
||||
await this.validateUpdateOwnerSpaceIds(_updatedItem, currentItem);
|
||||
await this.validateCanUpdateItemInActiveSpace(_updatedItem, currentItem);
|
||||
|
||||
|
|
|
@ -92,7 +92,7 @@ export class HostIsolationExceptionsValidator extends BaseValidator {
|
|||
|
||||
await this.validateHasWritePrivilege();
|
||||
await this.validateHostIsolationData(updatedItem);
|
||||
await this.validateByPolicyItem(updatedItem);
|
||||
await this.validateByPolicyItem(updatedItem, currentItem);
|
||||
await this.validateUpdateOwnerSpaceIds(_updatedItem, currentItem);
|
||||
await this.validateCanUpdateItemInActiveSpace(_updatedItem, currentItem);
|
||||
|
||||
|
|
|
@ -40,8 +40,11 @@ export class BaseValidatorMock extends BaseValidator {
|
|||
return this.validateCanCreateByPolicyArtifacts(item);
|
||||
}
|
||||
|
||||
async _validateByPolicyItem(item: ExceptionItemLikeOptions): Promise<void> {
|
||||
return this.validateByPolicyItem(item);
|
||||
async _validateByPolicyItem(
|
||||
item: ExceptionItemLikeOptions,
|
||||
currentItem?: ExceptionListItemSchema
|
||||
): Promise<void> {
|
||||
return this.validateByPolicyItem(item, currentItem);
|
||||
}
|
||||
|
||||
_wasByPolicyEffectScopeChanged(
|
||||
|
|
|
@ -259,7 +259,7 @@ export class TrustedAppValidator extends BaseValidator {
|
|||
}
|
||||
}
|
||||
|
||||
await this.validateByPolicyItem(updatedItem);
|
||||
await this.validateByPolicyItem(updatedItem, currentItem);
|
||||
await this.validateUpdateOwnerSpaceIds(_updatedItem, currentItem);
|
||||
await this.validateCanUpdateItemInActiveSpace(_updatedItem, currentItem);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue