Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ import { isTrustedApp } from './utils';
import { getTrustedAppProviderMock, getExceptionProviderMock } from './test_utils';
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';

jest.mock('../../../common/components/user_privileges');
const mockUserPrivileges = useUserPrivileges as jest.Mock;

describe.each([
['trusted apps', getTrustedAppProviderMock],
Expand Down Expand Up @@ -43,6 +48,12 @@ describe.each([
);
return renderResult;
};

mockUserPrivileges.mockReturnValue({ endpointPrivileges: getEndpointAuthzInitialStateMock() });
});

afterEach(() => {
mockUserPrivileges.mockReset();
});

it('should display title and who has created and updated it last', async () => {
Expand Down Expand Up @@ -205,21 +216,42 @@ describe.each([
).not.toBeNull();
});

it('should show popup menu with list of associated policies when clicked', async () => {
render({ policies });
await act(async () => {
await fireEvent.click(
renderResult.getByTestId('testCard-subHeader-effectScope-popupMenu-button')
describe('when clicked', () => {
it('should show popup menu with list of associated policies, with `View details` button when has Policy privilege', async () => {
render({ policies });
await act(async () => {
await fireEvent.click(
renderResult.getByTestId('testCard-subHeader-effectScope-popupMenu-button')
);
});

expect(
renderResult.getByTestId('testCard-subHeader-effectScope-popupMenu-popoverPanel')
).not.toBeNull();

expect(renderResult.getByTestId('policyMenuItem').textContent).toEqual(
'Policy oneView details'
);
});

expect(
renderResult.getByTestId('testCard-subHeader-effectScope-popupMenu-popoverPanel')
).not.toBeNull();
it('should show popup menu with list of associated policies, without `View details` button when does NOT have Policy privilege', async () => {
mockUserPrivileges.mockReturnValue({
endpointPrivileges: getEndpointAuthzInitialStateMock({ canReadPolicyManagement: false }),
});

expect(renderResult.getByTestId('policyMenuItem').textContent).toEqual(
'Policy oneView details'
);
render({ policies });
await act(async () => {
await fireEvent.click(
renderResult.getByTestId('testCard-subHeader-effectScope-popupMenu-button')
);
});

expect(
renderResult.getByTestId('testCard-subHeader-effectScope-popupMenu-popoverPanel')
).not.toBeNull();

expect(renderResult.getByTestId('policyMenuItem').textContent).toEqual('Policy one');
});
});

it('should display policy ID if no policy menu item found in `policies` prop', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ 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 { useUserPrivileges } from '../../../../common/components/user_privileges';
import {
GLOBAL_EFFECT_SCOPE,
POLICY_EFFECT_SCOPE,
Expand All @@ -25,7 +26,7 @@ import { useTestIdGenerator } from '../../../hooks/use_test_id_generator';
// the intent in this component was to also support to be able to display only text for artifacts
// by policy (>0), but **NOT** show the menu.
// So something like: `<EffectScope perPolicyCount={3} />`
// This should dispaly it as "Applied t o 3 policies", but NOT as a menu with links
// This should display it as "Applied to 3 policies", but NOT as a menu with links

const StyledWithContextMenuShiftedWrapper = styled('div')`
margin-left: -10px;
Expand All @@ -43,6 +44,7 @@ export interface EffectScopeProps extends Pick<CommonProps, 'data-test-subj'> {
export const EffectScope = memo<EffectScopeProps>(
({ policies, loadingPoliciesList = false, 'data-test-subj': dataTestSubj }) => {
const getTestId = useTestIdGenerator(dataTestSubj);
const { canReadPolicyManagement } = useUserPrivileges().endpointPrivileges;

const [icon, label] = useMemo(() => {
return policies
Expand Down Expand Up @@ -72,6 +74,7 @@ export const EffectScope = memo<EffectScopeProps>(
<WithContextMenu
policies={policies}
loadingPoliciesList={loadingPoliciesList}
canReadPolicies={canReadPolicyManagement}
data-test-subj={getTestId('popupMenu')}
>
{effectiveScopeLabel}
Expand All @@ -88,23 +91,31 @@ type WithContextMenuProps = Pick<CommonProps, 'data-test-subj'> &
PropsWithChildren<{
policies: Required<EffectScopeProps>['policies'];
}> & {
canReadPolicies: boolean;
loadingPoliciesList?: boolean;
};

export const WithContextMenu = memo<WithContextMenuProps>(
({ policies, loadingPoliciesList = false, children, 'data-test-subj': dataTestSubj }) => {
const WithContextMenu = memo<WithContextMenuProps>(
({
policies,
loadingPoliciesList = false,
canReadPolicies,
children,
'data-test-subj': dataTestSubj,
}) => {
const getTestId = useTestIdGenerator(dataTestSubj);

const hoverInfo = useMemo(
() => (
<StyledEuiButtonEmpty flush="right" size="s" iconSide="right" iconType="popout">
<FormattedMessage
id="xpack.securitySolution.contextMenuItemByRouter.viewDetails"
defaultMessage="View details"
/>
</StyledEuiButtonEmpty>
),
[]
() =>
canReadPolicies ? (
<StyledEuiButtonEmpty flush="right" size="s" iconSide="right" iconType="popout">
<FormattedMessage
id="xpack.securitySolution.contextMenuItemByRouter.viewDetails"
defaultMessage="View details"
/>
</StyledEuiButtonEmpty>
) : undefined,
[canReadPolicies]
);
return (
<ContextMenuWithRouterSupport
Expand All @@ -122,6 +133,7 @@ export const WithContextMenu = memo<WithContextMenuProps>(
</EuiButtonEmpty>
}
title={POLICY_EFFECT_SCOPE_TITLE(policies.length)}
isNavigationDisabled={!canReadPolicies}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import React, { memo, useMemo } from 'react';
import type { EuiContextMenuItemProps } from '@elastic/eui';
import { EuiContextMenuItem, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { EuiContextMenuItem, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import styled from 'styled-components';
import type { NavigateToAppOptions } from '@kbn/core/public';
import { useNavigateToAppEventHandler } from '../../../common/hooks/endpoint/use_navigate_to_app_event_handler';
Expand All @@ -26,6 +26,8 @@ export interface ContextMenuItemNavByRouterProps extends EuiContextMenuItemProps
textTruncate?: boolean;
/** Displays an additional info when hover an item */
hoverInfo?: React.ReactNode;
/** Disables navigation */
isNavigationDisabled?: boolean;
children: React.ReactNode;
}

Expand All @@ -43,7 +45,12 @@ const StyledEuiContextMenuItem = styled(EuiContextMenuItem)`

const StyledEuiFlexItem = styled('div')`
max-width: 50%;
padding-right: 10px;
padding-right: ${(props) => props.theme.eui.euiSizeS};
`;

const StyledEuiText = styled(EuiText)`
padding: ${(props) => props.theme.eui.euiSizeM};
line-height: ${(props) => props.theme.eui.euiFontSizeM};
`;

/**
Expand All @@ -59,6 +66,7 @@ export const ContextMenuItemNavByRouter = memo<ContextMenuItemNavByRouterProps>(
textTruncate,
hoverInfo,
children,
isNavigationDisabled = false,
...otherMenuItemProps
}) => {
const handleOnClickViaNavigateToApp = useNavigateToAppEventHandler(navigateAppId ?? '', {
Expand All @@ -79,32 +87,42 @@ export const ContextMenuItemNavByRouter = memo<ContextMenuItemNavByRouterProps>(
) : null;
}, [hoverInfo]);

return (
const content = textTruncate ? (
<>
<div
className="eui-textTruncate"
data-test-subj={getTestId('truncateWrapper')}
{
/* Add the html `title` prop if children is a string */
...('string' === typeof children ? { title: children } : {})
}
>
{children}
</div>
{hoverComponentInstance}
</>
) : (
<>
<EuiFlexItem>{children}</EuiFlexItem>
{hoverComponentInstance}
</>
);

return isNavigationDisabled ? (
<StyledEuiText
size="s"
className="eui-textTruncate"
data-test-subj={otherMenuItemProps['data-test-subj']}
>
{content}
</StyledEuiText>
) : (
<StyledEuiContextMenuItem
{...otherMenuItemProps}
onClick={navigateAppId ? handleOnClickViaNavigateToApp : onClick}
>
<EuiFlexGroup alignItems="center" gutterSize="none">
{textTruncate ? (
<>
<div
className="eui-textTruncate"
data-test-subj={getTestId('truncateWrapper')}
{
/* Add the html `title` prop if children is a string */
...('string' === typeof children ? { title: children } : {})
}
>
{children}
</div>
{hoverComponentInstance}
</>
) : (
<>
<EuiFlexItem>{children}</EuiFlexItem>
{hoverComponentInstance}
</>
)}
{content}
</EuiFlexGroup>
</StyledEuiContextMenuItem>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,16 @@ describe('When using the ContextMenuWithRouterSupport component', () => {
);
});

it('should NOT navigate after clicked if navigation is disabled', () => {
render({ isNavigationDisabled: true });
clickMenuTriggerButton();
act(() => {
fireEvent.click(renderResult.getByTestId('testMenu-item-1'));
});

expect(appTestContext.coreStart.application.navigateToApp).not.toHaveBeenCalled();
});

it('should display loading state', () => {
render({ loading: true });
clickMenuTriggerButton();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export interface ContextMenuWithRouterSupportProps
title?: string;
loading?: boolean;
hoverInfo?: React.ReactNode;
isNavigationDisabled?: boolean;
}

/**
Expand All @@ -58,6 +59,7 @@ export const ContextMenuWithRouterSupport = memo<ContextMenuWithRouterSupportPro
title,
loading = false,
hoverInfo,
isNavigationDisabled = false,
...commonProps
}) => {
const getTestId = useTestIdGenerator(commonProps['data-test-subj']);
Expand All @@ -84,6 +86,7 @@ export const ContextMenuWithRouterSupport = memo<ContextMenuWithRouterSupportPro
return (
<ContextMenuItemNavByRouter
{...itemProps}
isNavigationDisabled={isNavigationDisabled}
key={uuid.v4()}
data-test-subj={itemProps['data-test-subj'] ?? getTestId(`item-${index}`)}
textTruncate={Boolean(maxWidth) || itemProps.textTruncate}
Expand All @@ -97,7 +100,7 @@ export const ContextMenuWithRouterSupport = memo<ContextMenuWithRouterSupportPro
/>
);
});
}, [getTestId, handleCloseMenu, items, maxWidth, loading, hoverInfo]);
}, [items, loading, isNavigationDisabled, getTestId, maxWidth, hoverInfo, handleCloseMenu]);

type AdditionalPanelProps = Partial<
Omit<EuiContextMenuPanelProps & HTMLAttributes<HTMLDivElement>, 'style'>
Expand Down
24 changes: 18 additions & 6 deletions x-pack/plugins/security_solution/public/management/links.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,14 @@ describe('links', () => {
});
});

it('should return all links for user with all sub-feature privileges', async () => {
(calculateEndpointAuthz as jest.Mock).mockReturnValue(getEndpointAuthzInitialStateMock());

const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins([]));

expect(filteredLinks).toEqual(links);
});

it('should hide Trusted Applications for user without privilege', async () => {
(calculateEndpointAuthz as jest.Mock).mockReturnValue(
getEndpointAuthzInitialStateMock({
Expand All @@ -240,24 +248,28 @@ describe('links', () => {
expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.trustedApps));
});

it('should show Trusted Applications for user with privilege', async () => {
(calculateEndpointAuthz as jest.Mock).mockReturnValue(getEndpointAuthzInitialStateMock());
it('should hide Event Filters for user without privilege', async () => {
(calculateEndpointAuthz as jest.Mock).mockReturnValue(
getEndpointAuthzInitialStateMock({
canReadEventFilters: false,
})
);

const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins([]));

expect(filteredLinks).toEqual(links);
expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.eventFilters));
});

it('should hide Event Filters for user without privilege', async () => {
it('should hide Blocklist for user without privilege', async () => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you change this test from Event filters to Blocklist?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It must be some merge/diff magic. 🤷

Originally I had should show and should hide tests for Trusted Apps, but then figured that there's only need for one should show all test, and individual should hide tests for all artefacts. So I renamed the should show Trusted Apps test and moved it up in this commit: 00e8553

Then got some merge conflicts and stuff, so here we are, with a messed up diff, where should show Trusted Apps became should hide Event filters, and should hide Event filters became should hide Blocklists. So I think all should be fine, every test is here.

(calculateEndpointAuthz as jest.Mock).mockReturnValue(
getEndpointAuthzInitialStateMock({
canReadEventFilters: false,
canReadBlocklist: false,
})
);

const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins([]));

expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.eventFilters));
expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.blocklist));
});

it('should NOT return policies if `canReadPolicyManagement` is `false`', async () => {
Expand Down
5 changes: 5 additions & 0 deletions x-pack/plugins/security_solution/public/management/links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ export const getManagementFilteredLinks = async (
canReadEndpointList,
canReadTrustedApplications,
canReadEventFilters,
canReadBlocklist,
canReadPolicyManagement,
} = fleetAuthz
? calculateEndpointAuthz(
Expand Down Expand Up @@ -314,5 +315,9 @@ export const getManagementFilteredLinks = async (
linksToExclude.push(SecurityPageName.eventFilters);
}

if (!canReadBlocklist) {
linksToExclude.push(SecurityPageName.blocklist);
}

return excludeLinks(linksToExclude);
};
Loading