mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[9.0] [Security Solution][Endpoint] Disable Artifact card menu under space awareness conditions where user is not allowed to edit item under active space (#213820) (#214992)
# Backport This will backport the following commits from `main` to `9.0`: - [[Security Solution][Endpoint] Disable Artifact card menu under space awareness conditions where user is not allowed to edit item under active space (#213820)](https://github.com/elastic/kibana/pull/213820) <!--- 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-13T12:52:33Z","message":"[Security Solution][Endpoint] Disable Artifact card menu under space awareness conditions where user is not allowed to edit item under active space (#213820)\n\n## Summary\n\nThe following changes are being done to Artifact Card's Menu (which\ndisplays the option to Delete or Update the artifact) in support of\nspace awareness feature (currently behind Feature Flag:\n`endpointManagementSpaceAwarenessEnabled`):\n\n- Global Artifacts: If displaying a global artifact and user does not\nhave the new Global Artifact Management privilege - disable the Edit\nmenu icon and display a tooltip on hover\n- Per-Policy Artifacts: if displaying a per-policy artifact in a space\nother than one of the `ownerSpaceId` spaces that the artifact is\nassociated with and the user does not have the new Global Artifact\nManagement privilege - disable the Edit menu icon and display a tooltip\nwhen the user hover over that button\n\n\n> [!NOTE]\n> Changes were **NOT** done to Endpoint Exceptions with this PR.","sha":"2b9d2cff6cb9edd0fe639e82f8fe2e46591c7f0c","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","backport missing","Team:Defend Workflows","backport:prev-minor","v9.1.0"],"title":"[Security Solution][Endpoint] Disable Artifact card menu under space awareness conditions where user is not allowed to edit item under active space","number":213820,"url":"https://github.com/elastic/kibana/pull/213820","mergeCommit":{"message":"[Security Solution][Endpoint] Disable Artifact card menu under space awareness conditions where user is not allowed to edit item under active space (#213820)\n\n## Summary\n\nThe following changes are being done to Artifact Card's Menu (which\ndisplays the option to Delete or Update the artifact) in support of\nspace awareness feature (currently behind Feature Flag:\n`endpointManagementSpaceAwarenessEnabled`):\n\n- Global Artifacts: If displaying a global artifact and user does not\nhave the new Global Artifact Management privilege - disable the Edit\nmenu icon and display a tooltip on hover\n- Per-Policy Artifacts: if displaying a per-policy artifact in a space\nother than one of the `ownerSpaceId` spaces that the artifact is\nassociated with and the user does not have the new Global Artifact\nManagement privilege - disable the Edit menu icon and display a tooltip\nwhen the user hover over that button\n\n\n> [!NOTE]\n> Changes were **NOT** done to Endpoint Exceptions with this PR.","sha":"2b9d2cff6cb9edd0fe639e82f8fe2e46591c7f0c"}},"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/213820","number":213820,"mergeCommit":{"message":"[Security Solution][Endpoint] Disable Artifact card menu under space awareness conditions where user is not allowed to edit item under active space (#213820)\n\n## Summary\n\nThe following changes are being done to Artifact Card's Menu (which\ndisplays the option to Delete or Update the artifact) in support of\nspace awareness feature (currently behind Feature Flag:\n`endpointManagementSpaceAwarenessEnabled`):\n\n- Global Artifacts: If displaying a global artifact and user does not\nhave the new Global Artifact Management privilege - disable the Edit\nmenu icon and display a tooltip on hover\n- Per-Policy Artifacts: if displaying a per-policy artifact in a space\nother than one of the `ownerSpaceId` spaces that the artifact is\nassociated with and the user does not have the new Global Artifact\nManagement privilege - disable the Edit menu icon and display a tooltip\nwhen the user hover over that button\n\n\n> [!NOTE]\n> Changes were **NOT** done to Endpoint Exceptions with this PR.","sha":"2b9d2cff6cb9edd0fe639e82f8fe2e46591c7f0c"}}]}] BACKPORT-->
This commit is contained in:
parent
7f7a93e921
commit
c04009575a
12 changed files with 329 additions and 69 deletions
|
@ -27,6 +27,7 @@ import { PLUGIN_ID } from '@kbn/fleet-plugin/common';
|
|||
import type { UseBaseQueryResult } from '@tanstack/react-query';
|
||||
import ReactDOM from 'react-dom';
|
||||
import type { DeepReadonly } from 'utility-types';
|
||||
import { spacesPluginMock } from '@kbn/spaces-plugin/public/mocks';
|
||||
import type { UserPrivilegesState } from '../../components/user_privileges/user_privileges_context';
|
||||
import { getUserPrivilegesMockDefaultValue } from '../../components/user_privileges/__mocks__';
|
||||
import type { AppLinkItems } from '../../links/types';
|
||||
|
@ -134,7 +135,7 @@ export interface AppContextTestRender {
|
|||
store: Store<State>;
|
||||
history: ReturnType<typeof createMemoryHistory>;
|
||||
coreStart: ReturnType<typeof coreMock.createStart>;
|
||||
depsStart: Pick<StartPlugins, 'data' | 'fleet' | 'unifiedSearch'>;
|
||||
depsStart: Pick<StartPlugins, 'data' | 'fleet' | 'unifiedSearch' | 'spaces'>;
|
||||
startServices: StartServices;
|
||||
middlewareSpy: MiddlewareActionSpyHelper;
|
||||
/**
|
||||
|
@ -252,9 +253,20 @@ const experimentalFeaturesReducer: Reducer<State['app'], UpdateExperimentalFeatu
|
|||
export const createAppRootMockRenderer = (): AppContextTestRender => {
|
||||
const history = createMemoryHistory<never>();
|
||||
const coreStart = createCoreStartMock(history);
|
||||
const depsStart = depsStartMock();
|
||||
const middlewareSpy = createSpyMiddleware();
|
||||
const startServices: StartServices = createStartServicesMock(coreStart);
|
||||
const depsStart: AppContextTestRender['depsStart'] = {
|
||||
...depsStartMock(),
|
||||
spaces: spacesPluginMock.createStartContract(),
|
||||
};
|
||||
|
||||
(depsStart.spaces.getActiveSpace as jest.Mock).mockImplementation(async () => {
|
||||
return {
|
||||
id: 'default',
|
||||
name: 'default',
|
||||
disabledFeatures: [],
|
||||
};
|
||||
});
|
||||
|
||||
const storeReducer = {
|
||||
...SUB_PLUGINS_REDUCER,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Router } from '@kbn/shared-ux-router';
|
||||
import type { History } from 'history';
|
||||
|
@ -36,15 +36,21 @@ export const AppRootProvider = memo<{
|
|||
startServices: StartServices;
|
||||
queryClient: QueryClient;
|
||||
children: ReactNode | ReactNode[];
|
||||
}>(({ store, history, coreStart, queryClient, startServices, children }) => {
|
||||
}>(({ store, history, coreStart, depsStart, queryClient, startServices, children }) => {
|
||||
const { theme: themeStart } = coreStart;
|
||||
const theme = useObservable(themeStart.theme$, themeStart.getTheme());
|
||||
const isDarkMode = theme.darkMode;
|
||||
const services = useMemo(() => {
|
||||
return {
|
||||
...depsStart,
|
||||
...startServices,
|
||||
};
|
||||
}, [depsStart, startServices]);
|
||||
|
||||
return (
|
||||
<KibanaRenderContextProvider {...coreStart}>
|
||||
<Provider store={store}>
|
||||
<KibanaContextProvider services={startServices}>
|
||||
<KibanaContextProvider services={services}>
|
||||
<EuiThemeProvider darkMode={isDarkMode}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<UpsellingProvider upsellingService={startServices.upselling}>
|
||||
|
|
|
@ -5,9 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
import React, { memo, useCallback, useMemo, useState } from 'react';
|
||||
import type { EuiPopoverProps, EuiContextMenuPanelProps, EuiIconProps } from '@elastic/eui';
|
||||
import { EuiButtonIcon, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
|
||||
import { EuiToolTip, EuiButtonIcon, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import type { ContextMenuItemNavByRouterProps } from '../context_menu_with_router_support';
|
||||
|
@ -19,13 +20,23 @@ export interface ActionsContextMenuProps {
|
|||
/** Default icon is `boxesHorizontal` */
|
||||
icon?: EuiIconProps['type'];
|
||||
'data-test-subj'?: string;
|
||||
/** If menu button should be disabled */
|
||||
isDisabled?: boolean;
|
||||
/** If defined, then the disabled button will be wrapped in on-hover tooltip */
|
||||
disabledTooltip?: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a context menu behind a icon button (which defaults to the three horizontal dots icon)
|
||||
*/
|
||||
export const ActionsContextMenu = memo<ActionsContextMenuProps>(
|
||||
({ items, 'data-test-subj': dataTestSubj, icon = 'boxesHorizontal' }) => {
|
||||
({
|
||||
items,
|
||||
'data-test-subj': dataTestSubj,
|
||||
icon = 'boxesHorizontal',
|
||||
isDisabled = false,
|
||||
disabledTooltip,
|
||||
}) => {
|
||||
const getTestId = useTestIdGenerator(dataTestSubj);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
|
@ -53,22 +64,33 @@ export const ActionsContextMenu = memo<ActionsContextMenuProps>(
|
|||
});
|
||||
}, [handleCloseMenu, items]);
|
||||
|
||||
const menuButton = useMemo(() => {
|
||||
const button = (
|
||||
<EuiButtonIcon
|
||||
data-test-subj={getTestId('button')}
|
||||
iconType={icon}
|
||||
onClick={handleToggleMenu}
|
||||
isDisabled={isDisabled}
|
||||
aria-label={i18n.translate('xpack.securitySolution.actionsContextMenu.label', {
|
||||
defaultMessage: 'Open',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
||||
if (isDisabled && disabledTooltip) {
|
||||
return <EuiToolTip content={disabledTooltip}>{button}</EuiToolTip>;
|
||||
}
|
||||
|
||||
return button;
|
||||
}, [disabledTooltip, getTestId, handleToggleMenu, icon, isDisabled]);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
anchorPosition="downRight"
|
||||
panelPaddingSize="none"
|
||||
panelProps={panelProps}
|
||||
data-test-subj={dataTestSubj}
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
data-test-subj={getTestId('button')}
|
||||
iconType={icon}
|
||||
onClick={handleToggleMenu}
|
||||
aria-label={i18n.translate('xpack.securitySolution.actionsContextMenu.label', {
|
||||
defaultMessage: 'Open',
|
||||
})}
|
||||
/>
|
||||
}
|
||||
button={menuButton}
|
||||
isOpen={isOpen}
|
||||
closePopover={handleCloseMenu}
|
||||
>
|
||||
|
|
|
@ -6,14 +6,14 @@
|
|||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import type { AppContextTestRender } from '../../../common/mock/endpoint';
|
||||
import type { AppContextTestRender, UserPrivilegesMockSetter } from '../../../common/mock/endpoint';
|
||||
import { createAppRootMockRenderer } from '../../../common/mock/endpoint';
|
||||
import type {
|
||||
ArtifactEntryCardDecoratorProps,
|
||||
ArtifactEntryCardProps,
|
||||
} from './artifact_entry_card';
|
||||
import { ArtifactEntryCard } from './artifact_entry_card';
|
||||
import { act, fireEvent, getByTestId } from '@testing-library/react';
|
||||
import { act, fireEvent, getByTestId, waitFor } from '@testing-library/react';
|
||||
import type { AnyArtifact } from './types';
|
||||
import { isTrustedApp } from './utils';
|
||||
import { getTrustedAppProviderMock, getExceptionProviderMock } from './test_utils';
|
||||
|
@ -21,6 +21,12 @@ import { OS_LINUX, OS_MAC, OS_WINDOWS } from './components/translations';
|
|||
import type { TrustedApp } from '../../../../common/endpoint/types';
|
||||
import { useUserPrivileges } from '../../../common/components/user_privileges';
|
||||
import { getEndpointAuthzInitialStateMock } from '../../../../common/endpoint/service/authz/mocks';
|
||||
import { GLOBAL_ARTIFACT_TAG } from '../../../../common/endpoint/service/artifacts';
|
||||
import {
|
||||
buildPerPolicyTag,
|
||||
buildSpaceOwnerIdTag,
|
||||
} from '../../../../common/endpoint/service/artifacts/utils';
|
||||
import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
jest.mock('../../../common/components/user_privileges');
|
||||
const mockUserPrivileges = useUserPrivileges as jest.Mock;
|
||||
|
@ -286,4 +292,96 @@ describe.each([
|
|||
expect(passedItem).toBe(item);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and space awareness is enabled', () => {
|
||||
let authzMock: UserPrivilegesMockSetter;
|
||||
let actions: ArtifactEntryCardProps['actions'];
|
||||
|
||||
beforeEach(() => {
|
||||
actions = [
|
||||
{
|
||||
'data-test-subj': 'test-action',
|
||||
children: 'action one',
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
appTestContext.setExperimentalFlag({ endpointManagementSpaceAwarenessEnabled: true });
|
||||
authzMock = appTestContext.getUserPrivilegesMockSetter(mockUserPrivileges);
|
||||
authzMock.set({ canManageGlobalArtifacts: false });
|
||||
(item as ExceptionListItemSchema).tags = [GLOBAL_ARTIFACT_TAG, buildSpaceOwnerIdTag('foo')];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
authzMock.reset();
|
||||
});
|
||||
|
||||
it('should render menu if feature flag is disabled', () => {
|
||||
appTestContext.setExperimentalFlag({ endpointManagementSpaceAwarenessEnabled: false });
|
||||
render({ actions });
|
||||
|
||||
expect(
|
||||
(renderResult.getByTestId('testCard-header-actions-button') as HTMLButtonElement).disabled
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should disable card actions menu for global artifacts when user does not have global artifact privilege', () => {
|
||||
render({ actions });
|
||||
|
||||
expect(
|
||||
(renderResult.getByTestId('testCard-header-actions-button') as HTMLButtonElement).disabled
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should enable card actions menu for global artifacts when user has the global artifact privilege', () => {
|
||||
authzMock.set({ canManageGlobalArtifacts: true });
|
||||
render({ actions });
|
||||
|
||||
expect(
|
||||
(renderResult.getByTestId('testCard-header-actions-button') as HTMLButtonElement).disabled
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should disable card actions menu for per-policy artifacts not owned by active space', () => {
|
||||
(item as ExceptionListItemSchema).tags = [
|
||||
buildPerPolicyTag('abc'),
|
||||
buildSpaceOwnerIdTag('foo'),
|
||||
];
|
||||
render({ actions });
|
||||
|
||||
expect(
|
||||
(renderResult.getByTestId('testCard-header-actions-button') as HTMLButtonElement).disabled
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should enable card actions menu for per-policy artifacts when active space matches artifact owner space id', async () => {
|
||||
(item as ExceptionListItemSchema).tags = [
|
||||
buildPerPolicyTag('abc'),
|
||||
buildSpaceOwnerIdTag('default'),
|
||||
];
|
||||
render({ actions });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
(renderResult.getByTestId('testCard-header-actions-button') as HTMLButtonElement).disabled
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should enable card actions menu for per-policy artifacts when not owned by active space but user has global artifact privilege', async () => {
|
||||
authzMock.set({ canManageGlobalArtifacts: true });
|
||||
(item as ExceptionListItemSchema).tags = [
|
||||
buildPerPolicyTag('abc'),
|
||||
buildSpaceOwnerIdTag('foo'),
|
||||
];
|
||||
render({ actions });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
(renderResult.getByTestId('testCard-header-actions-button') as HTMLButtonElement).disabled
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -77,7 +77,7 @@ export const ArtifactEntryCard = memo<ArtifactEntryCardProps>(
|
|||
const policyNavLinks = usePolicyNavLinks(artifact, policies);
|
||||
|
||||
return (
|
||||
<CardContainerPanel {...commonProps} data-test-subj={dataTestSubj}>
|
||||
<CardContainerPanel {...commonProps} item={item} data-test-subj={dataTestSubj}>
|
||||
<CardSectionPanel className="top-section">
|
||||
<CardHeader
|
||||
name={artifact.name}
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
EuiButtonEmpty,
|
||||
} from '@elastic/eui';
|
||||
import styled from '@emotion/styled';
|
||||
import { CardArtifactProvider } from './components/card_artifact_context';
|
||||
import type { CriteriaConditionsProps } from './components/criteria_conditions';
|
||||
import { CriteriaConditions } from './components/criteria_conditions';
|
||||
import type { AnyArtifact } from './types';
|
||||
|
@ -108,42 +109,44 @@ export const ArtifactEntryCardMinified = memo(
|
|||
hasShadow={false}
|
||||
hasBorder
|
||||
>
|
||||
{cardTitle}
|
||||
<EuiSplitPanel.Inner paddingSize="s">
|
||||
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="s">
|
||||
<EuiTitle size="xxs">
|
||||
<h5 data-test-subj={getTestId('descriptionTitle')}>{DESCRIPTION_LABEL}</h5>
|
||||
</EuiTitle>
|
||||
<DescriptionField data-test-subj={getTestId('description')}>
|
||||
{artifact.description}
|
||||
</DescriptionField>
|
||||
</EuiPanel>
|
||||
<CardArtifactProvider item={item}>
|
||||
{cardTitle}
|
||||
<EuiSplitPanel.Inner paddingSize="s">
|
||||
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="s">
|
||||
<EuiTitle size="xxs">
|
||||
<h5 data-test-subj={getTestId('descriptionTitle')}>{DESCRIPTION_LABEL}</h5>
|
||||
</EuiTitle>
|
||||
<DescriptionField data-test-subj={getTestId('description')}>
|
||||
{artifact.description}
|
||||
</DescriptionField>
|
||||
</EuiPanel>
|
||||
|
||||
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="s">
|
||||
<EuiButtonEmpty
|
||||
data-test-subj={getTestId('collapse')}
|
||||
color="primary"
|
||||
size="s"
|
||||
flush="left"
|
||||
iconType={accordionTrigger === 'open' ? 'arrowUp' : 'arrowDown'}
|
||||
iconSide="right"
|
||||
iconSize="m"
|
||||
onClick={handleOnToggleAccordion}
|
||||
style={{ fontWeight: 400 }}
|
||||
>
|
||||
{getAccordionTitle()}
|
||||
</EuiButtonEmpty>
|
||||
<EuiAccordion id="showDetails" arrowDisplay="none" forceState={accordionTrigger}>
|
||||
{Decorator && <Decorator item={item} data-test-subj={getTestId('decorator')} />}
|
||||
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="s">
|
||||
<EuiButtonEmpty
|
||||
data-test-subj={getTestId('collapse')}
|
||||
color="primary"
|
||||
size="s"
|
||||
flush="left"
|
||||
iconType={accordionTrigger === 'open' ? 'arrowUp' : 'arrowDown'}
|
||||
iconSide="right"
|
||||
iconSize="m"
|
||||
onClick={handleOnToggleAccordion}
|
||||
style={{ fontWeight: 400 }}
|
||||
>
|
||||
{getAccordionTitle()}
|
||||
</EuiButtonEmpty>
|
||||
<EuiAccordion id="showDetails" arrowDisplay="none" forceState={accordionTrigger}>
|
||||
{Decorator && <Decorator item={item} data-test-subj={getTestId('decorator')} />}
|
||||
|
||||
<CriteriaConditions
|
||||
os={artifact.os as CriteriaConditionsProps['os']}
|
||||
entries={artifact.entries}
|
||||
data-test-subj={getTestId('criteriaConditions')}
|
||||
/>
|
||||
</EuiAccordion>
|
||||
</EuiPanel>
|
||||
</EuiSplitPanel.Inner>
|
||||
<CriteriaConditions
|
||||
os={artifact.os as CriteriaConditionsProps['os']}
|
||||
entries={artifact.entries}
|
||||
data-test-subj={getTestId('criteriaConditions')}
|
||||
/>
|
||||
</EuiAccordion>
|
||||
</EuiPanel>
|
||||
</EuiSplitPanel.Inner>
|
||||
</CardArtifactProvider>
|
||||
</CardContainerPanel>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import React, { memo } from 'react';
|
||||
import { EuiHorizontalRule } from '@elastic/eui';
|
||||
import type { AnyArtifact } from './types';
|
||||
import type { CommonArtifactEntryCardProps } from './artifact_entry_card';
|
||||
import { CardContainerPanel } from './components/card_container_panel';
|
||||
import { useNormalizedArtifact } from './hooks/use_normalized_artifact';
|
||||
|
@ -36,7 +37,7 @@ export const ArtifactEntryCollapsibleCard = memo<ArtifactEntryCollapsibleCardPro
|
|||
const getTestId = useTestIdGenerator(dataTestSubj);
|
||||
|
||||
return (
|
||||
<CardContainerPanel {...commonProps} data-test-subj={dataTestSubj}>
|
||||
<CardContainerPanel {...commonProps} item={item as AnyArtifact} data-test-subj={dataTestSubj}>
|
||||
<CardSectionPanel className="artifact-entry-collapsible-card">
|
||||
<CardCompressedHeader
|
||||
artifact={artifact}
|
||||
|
|
|
@ -5,11 +5,23 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
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 { 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 { useSpaceId } from '../../../../common/hooks/use_space_id';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
import { isArtifactGlobal } from '../../../../../common/endpoint/service/artifacts';
|
||||
import { useCardArtifact } from './card_artifact_context';
|
||||
import type { ActionsContextMenuProps } from '../../actions_context_menu';
|
||||
import { ActionsContextMenu } from '../../actions_context_menu';
|
||||
import { getArtifactOwnerSpaceIds } from '../../../../../common/endpoint/service/artifacts/utils';
|
||||
|
||||
export interface CardActionsFlexItemProps extends Pick<CommonProps, 'data-test-subj'> {
|
||||
/** If defined, then an overflow menu will be shown with the actions provided */
|
||||
|
@ -18,9 +30,51 @@ export interface CardActionsFlexItemProps extends Pick<CommonProps, 'data-test-s
|
|||
|
||||
export const CardActionsFlexItem = memo<CardActionsFlexItemProps>(
|
||||
({ actions, 'data-test-subj': dataTestSubj }) => {
|
||||
const item = useCardArtifact() as ExceptionListItemSchema;
|
||||
const canManageGlobalArtifacts =
|
||||
useUserPrivileges().endpointPrivileges.canManageGlobalArtifacts;
|
||||
const isGlobal = useMemo(() => isArtifactGlobal(item), [item]);
|
||||
const ownerSpaceIds = useMemo(() => getArtifactOwnerSpaceIds(item), [item]);
|
||||
const isSpacesEnabled = useIsExperimentalFeatureEnabled(
|
||||
'endpointManagementSpaceAwarenessEnabled'
|
||||
);
|
||||
const activeSpaceId = useSpaceId();
|
||||
|
||||
interface MenuButtonDisableOptions {
|
||||
isDisabled: boolean;
|
||||
disabledTooltip: ReactNode;
|
||||
}
|
||||
const { isDisabled, disabledTooltip } = useMemo<MenuButtonDisableOptions>(() => {
|
||||
if (!isSpacesEnabled || canManageGlobalArtifacts) {
|
||||
return { isDisabled: false, disabledTooltip: undefined };
|
||||
}
|
||||
|
||||
if (isGlobal) {
|
||||
return {
|
||||
isDisabled: true,
|
||||
disabledTooltip: MANAGEMENT_OF_GLOBAL_ARTIFACT_NOT_ALLOWED_MESSAGE,
|
||||
};
|
||||
}
|
||||
|
||||
if (!activeSpaceId || !ownerSpaceIds.includes(activeSpaceId)) {
|
||||
return {
|
||||
isDisabled: true,
|
||||
disabledTooltip: MANAGEMENT_OF_SHARED_PER_POLICY_ARTIFACT_NOT_ALLOWED_MESSAGE,
|
||||
};
|
||||
}
|
||||
|
||||
return { isDisabled: false, disabledTooltip: undefined };
|
||||
}, [activeSpaceId, canManageGlobalArtifacts, isGlobal, isSpacesEnabled, ownerSpaceIds]);
|
||||
|
||||
return actions && actions.length > 0 ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<ActionsContextMenu items={actions} icon="boxesHorizontal" data-test-subj={dataTestSubj} />
|
||||
<ActionsContextMenu
|
||||
items={actions}
|
||||
icon="boxesHorizontal"
|
||||
isDisabled={isDisabled}
|
||||
disabledTooltip={disabledTooltip}
|
||||
data-test-subj={dataTestSubj}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
) : null;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo, useContext, type PropsWithChildren } from 'react';
|
||||
import type { MaybeImmutable } from '../../../../../common/endpoint/types';
|
||||
import type { AnyArtifact } from '..';
|
||||
|
||||
const CardArtifactContext = React.createContext<MaybeImmutable<AnyArtifact> | undefined>(undefined);
|
||||
|
||||
export interface CardArtifactProviderProps extends PropsWithChildren {
|
||||
item: MaybeImmutable<AnyArtifact>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores and provides the Artifact item that is being rendered
|
||||
*/
|
||||
export const CardArtifactProvider = memo<CardArtifactProviderProps>(({ item, children }) => {
|
||||
return <CardArtifactContext.Provider value={item}>{children}</CardArtifactContext.Provider>;
|
||||
});
|
||||
CardArtifactProvider.displayName = 'CardArtifactProvider';
|
||||
|
||||
/**
|
||||
* Retrieve the artifact item (`ExceptionListItemSchema`) that is currently being rendered
|
||||
*/
|
||||
export const useCardArtifact = (): MaybeImmutable<AnyArtifact> => {
|
||||
const artifact = useContext(CardArtifactContext);
|
||||
|
||||
if (!artifact) {
|
||||
throw new Error('Card has not been initialized correctly - missing Artifact item');
|
||||
}
|
||||
|
||||
return artifact;
|
||||
};
|
|
@ -9,6 +9,9 @@ import styled from '@emotion/styled';
|
|||
import { EuiPanel } from '@elastic/eui';
|
||||
import type { EuiPanelProps } from '@elastic/eui/src/components/panel/panel';
|
||||
import React, { memo } from 'react';
|
||||
import type { MaybeImmutable } from '../../../../../common/endpoint/types';
|
||||
import { CardArtifactProvider } from './card_artifact_context';
|
||||
import type { AnyArtifact } from '..';
|
||||
|
||||
export const EuiPanelStyled = styled(EuiPanel)`
|
||||
&.artifactEntryCard + &.artifactEntryCard {
|
||||
|
@ -16,17 +19,23 @@ export const EuiPanelStyled = styled(EuiPanel)`
|
|||
}
|
||||
`;
|
||||
|
||||
export type CardContainerPanelProps = Exclude<EuiPanelProps, 'hasBorder' | 'paddingSize'>;
|
||||
export type CardContainerPanelProps = Exclude<EuiPanelProps, 'hasBorder' | 'paddingSize'> & {
|
||||
item: MaybeImmutable<AnyArtifact>;
|
||||
};
|
||||
|
||||
export const CardContainerPanel = memo<CardContainerPanelProps>(({ className, ...props }) => {
|
||||
return (
|
||||
<EuiPanelStyled
|
||||
{...props}
|
||||
hasBorder={true}
|
||||
paddingSize="none"
|
||||
className={`artifactEntryCard ${className ?? ''}`}
|
||||
/>
|
||||
);
|
||||
});
|
||||
export const CardContainerPanel = memo<CardContainerPanelProps>(
|
||||
({ className, item, children, ...props }) => {
|
||||
return (
|
||||
<EuiPanelStyled
|
||||
{...props}
|
||||
hasBorder={true}
|
||||
paddingSize="none"
|
||||
className={`artifactEntryCard ${className ?? ''}`}
|
||||
>
|
||||
<CardArtifactProvider item={item}>{children}</CardArtifactProvider>
|
||||
</EuiPanelStyled>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
CardContainerPanel.displayName = 'CardContainerPanel';
|
||||
|
|
|
@ -162,3 +162,16 @@ export const DESCRIPTION_LABEL = i18n.translate(
|
|||
defaultMessage: 'Description',
|
||||
}
|
||||
);
|
||||
|
||||
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',
|
||||
{
|
||||
defaultMessage:
|
||||
'Management of artifacts shared across multiple spaces is only allowed from the space where it was created from',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -883,19 +883,23 @@ interface GetOrCreateDefaultAgentPolicyOptions {
|
|||
kbnClient: KbnClient;
|
||||
log: ToolingLog;
|
||||
policyName?: string;
|
||||
overrides?: Partial<Omit<CreateAgentPolicyRequest['body'], 'name'>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a default Fleet Agent policy (if it does not yet exist) for testing. If
|
||||
* policy already exists, then it will be reused.
|
||||
* policy already exists, then it will be reused. It uses the policy name to find an
|
||||
* existing match.
|
||||
* @param kbnClient
|
||||
* @param log
|
||||
* @param policyName
|
||||
* @param overrides
|
||||
*/
|
||||
export const getOrCreateDefaultAgentPolicy = async ({
|
||||
kbnClient,
|
||||
log,
|
||||
policyName = DEFAULT_AGENT_POLICY_NAME,
|
||||
overrides = {},
|
||||
}: GetOrCreateDefaultAgentPolicyOptions): Promise<AgentPolicy> => {
|
||||
const existingPolicy = await fetchAgentPolicyList(kbnClient, {
|
||||
kuery: `${LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE}.name: "${policyName}"`,
|
||||
|
@ -919,6 +923,7 @@ export const getOrCreateDefaultAgentPolicy = async ({
|
|||
description: `Policy created by security solution tooling: ${__filename}`,
|
||||
namespace: spaceId,
|
||||
monitoring_enabled: ['logs', 'metrics'],
|
||||
...overrides,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue