diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.ts index c74deb9dec00e..7cbf5789df7b4 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.ts +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.ts @@ -35,7 +35,8 @@ export const useEndpointPrivileges = (): Immutable => { const http = useHttp(); const user = useCurrentUser(); - const fleetServicesFromUseKibana = useKibana().services.fleet; + const kibanaServices = useKibana().services; + const fleetServicesFromUseKibana = kibanaServices.fleet; // The `fleetServicesFromPluginStart` will be defined when this hooks called from a component // that is being rendered under the Fleet context (UI extensions). The `fleetServicesFromUseKibana` // above will be `undefined` in this case. @@ -56,8 +57,9 @@ export const useEndpointPrivileges = (): Immutable => { const [hasHostIsolationExceptionsItems, setHasHostIsolationExceptionsItems] = useState(false); - const securitySolutionPermissions = calculatePermissionsFromCapabilities( - useKibana().services.application.capabilities + const securitySolutionPermissions = useMemo( + () => calculatePermissionsFromCapabilities(kibanaServices.application.capabilities), + [kibanaServices.application.capabilities] ); const privileges = useMemo(() => { diff --git a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx index 4ed453a4ce0a3..196743d760bf7 100644 --- a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx @@ -154,6 +154,11 @@ export interface AppContextTestRender { * @param flags */ setExperimentalFlag: (flags: Partial) => void; + + /** + * The React Query client (setup to support jest testing) + */ + queryClient: QueryClient; } // Defined a private custom reducer that reacts to an action that enables us to update the @@ -310,6 +315,7 @@ export const createAppRootMockRenderer = (): AppContextTestRender => { renderHook, renderReactQueryHook, setExperimentalFlag, + queryClient, }; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/antivirus_registration_form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/antivirus_registration_form/index.tsx index 76059dae08bc4..24062bf00ef46 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/antivirus_registration_form/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/antivirus_registration_form/index.tsx @@ -11,9 +11,8 @@ import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiSwitch, EuiText } from '@elastic/eui'; import { OperatingSystem } from '@kbn/securitysolution-utils'; -import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; import { isAntivirusRegistrationEnabled } from '../../../store/policy_details/selectors'; -import { usePolicyDetailsSelector } from '../../policy_hooks'; +import { useShowEditableFormFields, usePolicyDetailsSelector } from '../../policy_hooks'; import { ConfigForm } from '../config_form'; const TRANSLATIONS: Readonly<{ [K in 'title' | 'description' | 'label']: string }> = { @@ -42,7 +41,7 @@ const TRANSLATIONS: Readonly<{ [K in 'title' | 'description' | 'label']: string export const AntivirusRegistrationForm = memo(() => { const antivirusRegistrationEnabled = usePolicyDetailsSelector(isAntivirusRegistrationEnabled); const dispatch = useDispatch(); - const { canWritePolicyManagement } = useUserPrivileges().endpointPrivileges; + const showEditableFormFields = useShowEditableFormFields(); const handleSwitchChange = useCallback( (event) => @@ -70,7 +69,7 @@ export const AntivirusRegistrationForm = memo(() => { label={TRANSLATIONS.label} checked={antivirusRegistrationEnabled} onChange={handleSwitchChange} - disabled={!canWritePolicyManagement} + disabled={!showEditableFormFields} /> ); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/attack_surface_reduction_form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/attack_surface_reduction_form/index.tsx index e1c01c552468b..235fef2aeee8d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/attack_surface_reduction_form/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/attack_surface_reduction_form/index.tsx @@ -11,9 +11,8 @@ import { i18n } from '@kbn/i18n'; import { EuiSwitch } from '@elastic/eui'; import { OperatingSystem } from '@kbn/securitysolution-utils'; -import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; import { isCredentialHardeningEnabled } from '../../../store/policy_details/selectors'; -import { usePolicyDetailsSelector } from '../../policy_hooks'; +import { useShowEditableFormFields, usePolicyDetailsSelector } from '../../policy_hooks'; import { ConfigForm } from '../config_form'; const TRANSLATIONS: Readonly<{ [K in 'title' | 'label']: string }> = { @@ -34,7 +33,7 @@ const TRANSLATIONS: Readonly<{ [K in 'title' | 'label']: string }> = { export const AttackSurfaceReductionForm = memo(() => { const credentialHardeningEnabled = usePolicyDetailsSelector(isCredentialHardeningEnabled); const dispatch = useDispatch(); - const { canWritePolicyManagement } = useUserPrivileges().endpointPrivileges; + const showEditableFormFields = useShowEditableFormFields(); const handleSwitchChange = useCallback( (event) => @@ -53,7 +52,7 @@ export const AttackSurfaceReductionForm = memo(() => { label={TRANSLATIONS.label} checked={credentialHardeningEnabled} onChange={handleSwitchChange} - disabled={!canWritePolicyManagement} + disabled={!showEditableFormFields} /> ); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/events_form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/events_form/index.tsx index f497b59394b10..a3a0f8c325b0e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/events_form/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/events_form/index.tsx @@ -19,12 +19,11 @@ import { } from '@elastic/eui'; import { OperatingSystem } from '@kbn/securitysolution-utils'; import { ThemeContext } from 'styled-components'; -import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; import type { PolicyOperatingSystem, UIPolicyConfig, } from '../../../../../../../common/endpoint/types'; -import { usePolicyDetailsSelector } from '../../policy_hooks'; +import { useShowEditableFormFields, usePolicyDetailsSelector } from '../../policy_hooks'; import { policyConfig } from '../../../store/policy_details/selectors'; import { ConfigForm, ConfigFormHeading } from '../config_form'; @@ -76,7 +75,7 @@ const InnerEventsForm = ({ onValueSelection, supplementalOptions, }: EventsFormProps) => { - const { canWritePolicyManagement } = useUserPrivileges().endpointPrivileges; + const showEditableFormFields = useShowEditableFormFields(); const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); const theme = useContext(ThemeContext); const countSelected = useCallback(() => { @@ -124,7 +123,7 @@ const InnerEventsForm = ({ data-test-subj={`policy${OPERATING_SYSTEM_TO_TEST_SUBJ[os]}Event_${protectionField}`} checked={selection[protectionField]} onChange={(event) => onValueSelection(protectionField, event.target.checked)} - disabled={!canWritePolicyManagement} + disabled={!showEditableFormFields} /> ); })} @@ -169,7 +168,7 @@ const InnerEventsForm = ({ checked={selection[protectionField]} onChange={(event) => onValueSelection(protectionField, event.target.checked)} disabled={ - !canWritePolicyManagement || + !showEditableFormFields || (isDisabled ? isDisabled(policyDetailsConfig) : false) } /> diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/components/with_security_context/render_context_providers.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/components/with_security_context/render_context_providers.tsx index 97698020983c1..ee0643ca2883a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/components/with_security_context/render_context_providers.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/components/with_security_context/render_context_providers.tsx @@ -9,11 +9,13 @@ import type { PropsWithChildren } from 'react'; import React, { memo } from 'react'; import { Provider as ReduxStoreProvider } from 'react-redux'; import type { Store } from 'redux'; +import { UserPrivilegesProvider } from '../../../../../../../common/components/user_privileges/user_privileges_context'; import type { SecuritySolutionQueryClient } from '../../../../../../../common/containers/query_client/query_client_provider'; import { ReactQueryClientProvider } from '../../../../../../../common/containers/query_client/query_client_provider'; import { SecuritySolutionStartDependenciesContext } from '../../../../../../../common/components/user_privileges/endpoint/security_solution_start_dependencies'; import { CurrentLicense } from '../../../../../../../common/components/current_license'; import type { StartPlugins } from '../../../../../../../types'; +import { useKibana } from '../../../../../../../common/lib/kibana'; export type RenderContextProvidersProps = PropsWithChildren<{ store: Store; @@ -23,11 +25,16 @@ export type RenderContextProvidersProps = PropsWithChildren<{ export const RenderContextProviders = memo( ({ store, depsStart, queryClient, children }) => { + const { + application: { capabilities }, + } = useKibana().services; return ( - {children} + + {children} + diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.test.tsx index 5681d2fa2fb34..0c3573ed78d5c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.test.tsx @@ -9,18 +9,16 @@ import React from 'react'; import type { AppContextTestRender } from '../../../../../../common/mock/endpoint'; import { createFleetContextRendererMock, generateFleetPackageInfo } from '../mocks'; import { EndpointPackageCustomExtension } from './endpoint_package_custom_extension'; -import { useEndpointPrivileges as _useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; import { getEndpointPrivilegesInitialStateMock } from '../../../../../../common/components/user_privileges/endpoint/mocks'; -import { exceptionsListAllHttpMocks } from '../../../../../mocks/exceptions_list_http_mocks'; -import { waitFor } from '@testing-library/react'; +import { useUserPrivileges as _useUserPrivileges } from '../../../../../../common/components/user_privileges'; +import { getUserPrivilegesMockDefaultValue } from '../../../../../../common/components/user_privileges/__mocks__'; -jest.mock('../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); -const useEndpointPrivilegesMock = _useEndpointPrivileges as jest.Mock; +jest.mock('../../../../../../common/components/user_privileges'); +const useUserPrivilegesMock = _useUserPrivileges as jest.Mock; describe('When displaying the EndpointPackageCustomExtension fleet UI extension', () => { let render: () => ReturnType; let renderResult: ReturnType; - let http: AppContextTestRender['coreStart']['http']; const artifactCards = Object.freeze([ 'trustedApps-fleetCard', 'eventFilters-fleetCard', @@ -30,7 +28,6 @@ describe('When displaying the EndpointPackageCustomExtension fleet UI extension' beforeEach(() => { const mockedTestContext = createFleetContextRendererMock(); - http = mockedTestContext.coreStart.http; render = () => { renderResult = mockedTestContext.render( { - useEndpointPrivilegesMock.mockImplementation(getEndpointPrivilegesInitialStateMock); + useUserPrivilegesMock.mockImplementation(getUserPrivilegesMockDefaultValue); }); - it('should show artifact cards', async () => { + it.each([...artifactCards])('should show artifact card: `%s`', (artifactCardtestId) => { render(); - await waitFor(() => { - artifactCards.forEach((artifactCard) => { - expect(renderResult.getByTestId(artifactCard)).toBeTruthy(); - }); - }); + expect(renderResult.getByTestId(artifactCardtestId)).toBeTruthy(); }); - it('should NOT show artifact cards if no endpoint management authz', async () => { - useEndpointPrivilegesMock.mockReturnValue({ - ...getEndpointPrivilegesInitialStateMock({ - canReadBlocklist: false, - canReadEventFilters: false, - canReadHostIsolationExceptions: false, - canReadTrustedApplications: false, - canIsolateHost: false, - }), - }); - render(); - - await waitFor(() => { - artifactCards.forEach((artifactCard) => { - expect(renderResult.queryByTestId(artifactCard)).toBeNull(); + it.each([...artifactCards])( + 'should NOT show artifact card if no endpoint management authz: %s', + (artifactCardTestId) => { + useUserPrivilegesMock.mockReturnValue({ + ...getUserPrivilegesMockDefaultValue(), + endpointPrivileges: getEndpointPrivilegesInitialStateMock({ + canReadBlocklist: false, + canReadEventFilters: false, + canReadHostIsolationExceptions: false, + canDeleteHostIsolationExceptions: false, + canReadTrustedApplications: false, + }), }); - expect(renderResult.queryByTestId('noPrivilegesPage')).toBeTruthy(); - }); - }); - - it('should show Host Isolations Exceptions if user has no authz but entries exist', async () => { - useEndpointPrivilegesMock.mockReturnValue({ - ...getEndpointPrivilegesInitialStateMock(), - canIsolateHost: false, - }); - // Mock APIs - exceptionsListAllHttpMocks(http); - render(); - await waitFor(() => { - expect(renderResult.getByTestId('hostIsolationExceptions-fleetCard')).toBeTruthy(); - }); - }); - - it('should NOT show Host Isolation Exceptions if user has no authz and no entries exist', async () => { - useEndpointPrivilegesMock.mockReturnValue({ - ...getEndpointPrivilegesInitialStateMock({ canReadHostIsolationExceptions: false }), - }); - render(); + render(); - await waitFor(() => { - expect(renderResult.queryByTestId('hostIsolationExceptions-fleetCard')).toBeNull(); - }); - }); + expect(renderResult.queryByTestId(artifactCardTestId)).toBeNull(); + expect(renderResult.queryByTestId('noPrivilegesPage')).toBeTruthy(); + } + ); it('should only show loading spinner if loading', () => { - useEndpointPrivilegesMock.mockReturnValue({ - ...getEndpointPrivilegesInitialStateMock(), - loading: true, + useUserPrivilegesMock.mockReturnValue({ + ...getUserPrivilegesMockDefaultValue(), + endpointPrivileges: getEndpointPrivilegesInitialStateMock({ loading: true }), }); + render(); expect(renderResult.getByTestId('endpointExtensionLoadingSpinner')).toBeInTheDocument(); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.tsx index 4266bff29a0c3..6e72dc260d64a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.tsx @@ -9,6 +9,7 @@ import type { ReactElement } from 'react'; import React, { memo, useMemo } from 'react'; import { EuiSpacer, EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import type { PackageCustomExtensionComponentProps } from '@kbn/fleet-plugin/public'; +import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; import { NoPrivileges } from '../../../../../../common/components/no_privileges'; import { useCanAccessSomeArtifacts } from '../hooks/use_can_access_some_artifacts'; import { useHttp } from '../../../../../../common/lib/kibana'; @@ -29,7 +30,6 @@ import { HOST_ISOLATION_EXCEPTIONS_LABELS, TRUSTED_APPS_LABELS, } from './translations'; -import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint'; const TrustedAppsArtifactCard = memo((props) => { const http = useHttp(); @@ -115,7 +115,7 @@ export const EndpointPackageCustomExtension = memo(({ policyId }) => { () => TrustedAppsApiClient.getInstance(http), [http] ); - const { canReadPolicyManagement } = useEndpointPrivileges(); + const { canReadPolicyManagement } = useUserPrivileges().endpointPrivileges; const getArtifactPathHandler: FleetIntegrationArtifactCardProps['getArtifactsPath'] = useCallback(() => { @@ -78,7 +78,7 @@ const EventFiltersPolicyCard = memo(({ policyId }) => { () => EventFiltersApiClient.getInstance(http), [http] ); - const { canReadPolicyManagement } = useEndpointPrivileges(); + const { canReadPolicyManagement } = useUserPrivileges().endpointPrivileges; const getArtifactPathHandler: FleetIntegrationArtifactCardProps['getArtifactsPath'] = useCallback(() => { @@ -108,7 +108,7 @@ const HostIsolationExceptionsPolicyCard = memo(({ polic () => HostIsolationExceptionsApiClient.getInstance(http), [http] ); - const { canReadPolicyManagement } = useEndpointPrivileges(); + const { canReadPolicyManagement } = useUserPrivileges().endpointPrivileges; const getArtifactPathHandler: FleetIntegrationArtifactCardProps['getArtifactsPath'] = useCallback(() => { @@ -135,7 +135,7 @@ HostIsolationExceptionsPolicyCard.displayName = 'HostIsolationExceptionsPolicyCa const BlocklistPolicyCard = memo(({ policyId }) => { const http = useHttp(); const blocklistsApiClientInstance = useMemo(() => BlocklistsApiClient.getInstance(http), [http]); - const { canReadPolicyManagement } = useEndpointPrivileges(); + const { canReadPolicyManagement } = useUserPrivileges().endpointPrivileges; const getArtifactPathHandler: FleetIntegrationArtifactCardProps['getArtifactsPath'] = useCallback(() => { @@ -174,7 +174,7 @@ export const EndpointPolicyArtifactCards = memo toasts.addDanger(labels.artifactsSummaryApiError(error.message)), + onError: (error) => { + toasts.addDanger(labels.artifactsSummaryApiError(error.message)); + }, } ); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension/endpoint_policy_edit_extension.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension/endpoint_policy_edit_extension.test.tsx index 75287dcc21900..71bad39304740 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension/endpoint_policy_edit_extension.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension/endpoint_policy_edit_extension.test.tsx @@ -6,24 +6,16 @@ */ import React from 'react'; -import { waitFor } from '@testing-library/react'; import type { PackagePolicy, NewPackagePolicy } from '@kbn/fleet-plugin/common'; -import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; import { getEndpointPrivilegesInitialStateMock } from '../../../../../../common/components/user_privileges/endpoint/mocks'; -import { composeHttpHandlerMocks } from '../../../../../../common/mock/endpoint/http_handler_mock_factory'; import type { AppContextTestRender } from '../../../../../../common/mock/endpoint'; -import { - fleetGetAgentStatusHttpMock, - fleetGetEndpointPackagePolicyHttpMock, -} from '../../../../../mocks'; import { EndpointPolicyEditExtension } from './endpoint_policy_edit_extension'; import { createFleetContextRendererMock } from '../mocks'; +import { getUserPrivilegesMockDefaultValue } from '../../../../../../common/components/user_privileges/__mocks__'; -jest.mock('../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); jest.mock('../../../../../../common/components/user_privileges'); -const useEndpointPrivilegesMock = useEndpointPrivileges as jest.Mock; const useUserPrivilegesMock = useUserPrivileges as jest.Mock; describe('When displaying the EndpointPolicyEditExtension fleet UI extension', () => { @@ -36,12 +28,7 @@ describe('When displaying the EndpointPolicyEditExtension fleet UI extension', ( ]); beforeEach(() => { - useEndpointPrivilegesMock.mockReturnValue(getEndpointPrivilegesInitialStateMock()); - useUserPrivilegesMock.mockReturnValue({ - endpointPrivileges: getEndpointPrivilegesInitialStateMock(), - }); const mockedTestContext = createFleetContextRendererMock(); - composeHttpHandlerMocks([fleetGetEndpointPackagePolicyHttpMock, fleetGetAgentStatusHttpMock]); render = () => mockedTestContext.render( @@ -53,34 +40,33 @@ describe('When displaying the EndpointPolicyEditExtension fleet UI extension', ( ); }); - it('should show artifact cards', async () => { - const renderResult = render(); - - await waitFor(() => { - artifactCards.forEach((artifactCard) => { - expect(renderResult.getByTestId(artifactCard)).toBeTruthy(); - }); - }); + afterEach(() => { + useUserPrivilegesMock.mockReturnValue(getUserPrivilegesMockDefaultValue()); }); - it('should NOT show artifact cards if no endpoint management authz', async () => { - useEndpointPrivilegesMock.mockReturnValue({ - ...getEndpointPrivilegesInitialStateMock({ - canReadTrustedApplications: false, - canReadEventFilters: false, - canReadBlocklist: false, - canReadHostIsolationExceptions: false, - }), - }); + it.each([...artifactCards])('should show artifact card `%s`', (artifactCardTestId) => { const renderResult = render(); - await waitFor(() => { - artifactCards.forEach((artifactCard) => { - expect(renderResult.queryByTestId(artifactCard)).toBeNull(); - }); - }); + expect(renderResult.getByTestId(artifactCardTestId)).toBeTruthy(); }); + it.each([...artifactCards])( + 'should NOT show artifact cards if no endpoint management authz: %s', + (artifactCardTestId) => { + useUserPrivilegesMock.mockReturnValue({ + endpointPrivileges: getEndpointPrivilegesInitialStateMock({ + canReadTrustedApplications: false, + canReadEventFilters: false, + canReadBlocklist: false, + canReadHostIsolationExceptions: false, + }), + }); + const renderResult = render(); + + expect(renderResult.queryByTestId(artifactCardTestId)).toBeNull(); + } + ); + it.each([ ['trustedApps', 'trusted_apps'], ['eventFilters', 'event_filters'], @@ -88,20 +74,18 @@ describe('When displaying the EndpointPolicyEditExtension fleet UI extension', ( ['blocklists', 'blocklist'], ])( 'should link to the %s list page if no Authz for policy management', - async (artifactTestIdPrefix, pageUrlName) => { - useEndpointPrivilegesMock.mockReturnValue({ - ...getEndpointPrivilegesInitialStateMock({ + (artifactTestIdPrefix, pageUrlName) => { + useUserPrivilegesMock.mockReturnValue({ + endpointPrivileges: getEndpointPrivilegesInitialStateMock({ canReadPolicyManagement: false, }), }); const { getByTestId } = render(); - await waitFor(() => { - expect( - getByTestId(`${artifactTestIdPrefix}-link-to-exceptions`).getAttribute('href') - ).toEqual(`/app/security/administration/${pageUrlName}?includedPolicies=someid%2Cglobal`); - }); + expect( + getByTestId(`${artifactTestIdPrefix}-link-to-exceptions`).getAttribute('href') + ).toEqual(`/app/security/administration/${pageUrlName}?includedPolicies=someid%2Cglobal`); } ); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension/endpoint_policy_edit_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension/endpoint_policy_edit_extension.tsx index 49a76f913a05b..bb629e3f445c8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension/endpoint_policy_edit_extension.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension/endpoint_policy_edit_extension.tsx @@ -103,39 +103,37 @@ const WrappedPolicyDetailsForm = memo<{ return (
- <> - -
- -
+ +
+ +
+ +
+
+ + {endpointDetailsLoadingError ? ( + -
-
- - {endpointDetailsLoadingError ? ( - - } - iconType="alert" - color="warning" - data-test-subj="endpiontPolicySettingsLoadingError" - > - {endpointDetailsLoadingError.message} - - ) : !endpointPolicyDetails ? ( - - ) : ( - - )} -
- + } + iconType="alert" + color="warning" + data-test-subj="endpiontPolicySettingsLoadingError" + > + {endpointDetailsLoadingError.message} + + ) : !endpointPolicyDetails ? ( + + ) : ( + + )} +
); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/hooks/use_can_access_some_artifacts.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/hooks/use_can_access_some_artifacts.ts index 1e79d12470e77..dae415025cf35 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/hooks/use_can_access_some_artifacts.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/hooks/use_can_access_some_artifacts.ts @@ -6,7 +6,7 @@ */ import { useMemo } from 'react'; -import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint'; +import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; /** * Checks to see if the current user can access at least one artifact page. @@ -18,7 +18,7 @@ export const useCanAccessSomeArtifacts = (): boolean => { canReadEventFilters, canReadTrustedApplications, canReadHostIsolationExceptions, - } = useEndpointPrivileges(); + } = useUserPrivileges().endpointPrivileges; return useMemo(() => { return ( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/mocks.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/mocks.tsx index fdc97bd608e7f..a43424b42982c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/mocks.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/mocks.tsx @@ -14,7 +14,7 @@ import { I18nProvider } from '@kbn/i18n-react'; import type { PackageInfo } from '@kbn/fleet-plugin/common/types'; import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import { SecuritySolutionQueryClient } from '../../../../../common/containers/query_client/query_client_provider'; +import { deepFreeze } from '@kbn/std'; import type { AppContextTestRender, UiRender } from '../../../../../common/mock/endpoint'; import { createAppRootMockRenderer } from '../../../../../common/mock/endpoint'; import { createFleetContextReduxStore } from './components/with_security_context/store'; @@ -61,9 +61,10 @@ export const createFleetContextRendererMock = (): AppContextTestRender => { }); const mockedContext = createAppRootMockRenderer(); + const { coreStart, depsStart, queryClient, startServices } = mockedContext; const store = createFleetContextReduxStore({ - coreStart: mockedContext.coreStart, - depsStart: mockedContext.depsStart, + coreStart, + depsStart, reducersObject: { management: managementReducer, app: (state, action: AppAction | UpdateExperimentalFeaturesTestAction) => { @@ -81,8 +82,6 @@ export const createFleetContextRendererMock = (): AppContextTestRender => { additionalMiddleware: [mockedContext.middlewareSpy.actionSpyMiddleware], }); - const queryClient = new SecuritySolutionQueryClient(); - const Wrapper: RenderOptions['wrapper'] = ({ children }) => { useEffect(() => { return () => { @@ -94,15 +93,16 @@ export const createFleetContextRendererMock = (): AppContextTestRender => { }; }, []); + startServices.application.capabilities = deepFreeze({ + ...startServices.application.capabilities, + siem: { show: true, crud: true }, + }); + return ( - - + + {children} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx index 44618a509d58b..731bccdd2e24c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx @@ -21,9 +21,8 @@ import { import { cloneDeep } from 'lodash'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; -import { useUserPrivileges } from '../../../../common/components/user_privileges'; import { policyConfig } from '../store/policy_details/selectors'; -import { usePolicyDetailsSelector } from './policy_hooks'; +import { useShowEditableFormFields, usePolicyDetailsSelector } from './policy_hooks'; import { AdvancedPolicySchema } from '../models/advanced_policy_schema'; function setValue(obj: Record, value: string, path: string[]) { @@ -146,7 +145,7 @@ const PolicyAdvanced = React.memo( lastSupportedVersion?: string; documentation: string; }) => { - const { canWritePolicyManagement } = useUserPrivileges().endpointPrivileges; + const showEditableFormFields = useShowEditableFormFields(); const dispatch = useDispatch(); const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); const onChange = useCallback( @@ -198,7 +197,7 @@ const PolicyAdvanced = React.memo( fullWidth value={value as string} onChange={onChange} - disabled={!canWritePolicyManagement} + disabled={!showEditableFormFields} /> diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx index 81deb57949fff..35997ae0c5106 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx @@ -5,10 +5,11 @@ * 2.0. */ -import { EuiButtonEmpty, EuiSpacer, EuiText } from '@elastic/eui'; +import { EuiButtonEmpty, EuiLoadingContent, EuiSpacer, EuiText } from '@elastic/eui'; import React, { memo, useCallback, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; import { MalwareProtections } from './policy_forms/protections/malware'; import { MemoryProtection } from './policy_forms/protections/memory'; import { BehaviorProtection } from './policy_forms/protections/behavior'; @@ -54,6 +55,11 @@ export const PolicyDetailsForm = memo(() => { setShowAdvancedPolicy(!showAdvancedPolicy); }, [showAdvancedPolicy]); const isPlatinumPlus = useLicense().isPlatinumPlus(); + const { loading: authzLoading } = useUserPrivileges().endpointPrivileges; + + if (authzLoading) { + return ; + } return ( <> diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.test.tsx index 42b4328d8e794..4ed6af2634c63 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.test.tsx @@ -343,6 +343,7 @@ describe('Policy Form Layout', () => { beforeEach(() => { const mockedPrivileges = getUserPrivilegesMockDefaultValue(); mockedPrivileges.endpointPrivileges.canWritePolicyManagement = false; + mockedPrivileges.endpointPrivileges.canAccessFleet = false; useUserPrivilegesMock.mockReturnValue(mockedPrivileges); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.tsx index ad9246b01a699..2b9a8f434f1cc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.tsx @@ -23,8 +23,7 @@ import { useLocation } from 'react-router-dom'; import type { ApplicationStart } from '@kbn/core/public'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; import { useIsExperimentalFeatureEnabled } from '../../../../../../common/hooks/use_experimental_features'; -import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; -import { usePolicyDetailsSelector } from '../../policy_hooks'; +import { useShowEditableFormFields, usePolicyDetailsSelector } from '../../policy_hooks'; import { policyDetails, agentStatusSummary, @@ -52,7 +51,7 @@ export const PolicyFormLayout = React.memo(() => { } = useKibana(); const toasts = useToasts(); const { state: locationRouteState } = useLocation(); - const { canWritePolicyManagement } = useUserPrivileges().endpointPrivileges; + const showEditableFormFields = useShowEditableFormFields(); // Store values const policyItem = usePolicyDetailsSelector(policyDetails); @@ -181,7 +180,7 @@ export const PolicyFormLayout = React.memo(() => { - {canWritePolicyManagement && ( + {showEditableFormFields && ( htmlIdGenerator()(), []); const selected = policyDetailsConfig && policyDetailsConfig.windows[protection].mode; const isPlatinumPlus = useLicense().isPlatinumPlus(); - const { canWritePolicyManagement } = useUserPrivileges().endpointPrivileges; + const showEditableFormFields = useShowEditableFormFields(); const handleRadioChange = useCallback(() => { if (policyDetailsConfig) { @@ -89,7 +88,7 @@ export const ProtectionRadio = React.memo( id={radioButtonId} checked={selected === protectionMode} onChange={handleRadioChange} - disabled={!canWritePolicyManagement || selected === ProtectionModes.off} + disabled={!showEditableFormFields || selected === ProtectionModes.off} /> ); } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/protection_switch.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/protection_switch.tsx index a564c2b2f83cb..3b25693410aff 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/protection_switch.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/protection_switch.tsx @@ -10,10 +10,9 @@ import { useDispatch } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { EuiSwitch } from '@elastic/eui'; import { cloneDeep } from 'lodash'; -import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; import { useLicense } from '../../../../../../common/hooks/use_license'; import { policyConfig } from '../../../store/policy_details/selectors'; -import { usePolicyDetailsSelector } from '../../policy_hooks'; +import { useShowEditableFormFields, usePolicyDetailsSelector } from '../../policy_hooks'; import type { AppAction } from '../../../../../../common/store/actions'; import type { ImmutableArray, @@ -41,7 +40,7 @@ export const ProtectionSwitch = React.memo( }) => { const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); const isPlatinumPlus = useLicense().isPlatinumPlus(); - const { canWritePolicyManagement } = useUserPrivileges().endpointPrivileges; + const showEditableFormFields = useShowEditableFormFields(); const dispatch = useDispatch<(action: AppAction) => void>(); const selected = policyDetailsConfig && policyDetailsConfig.windows[protection].mode; @@ -125,7 +124,7 @@ export const ProtectionSwitch = React.memo( })} checked={selected !== ProtectionModes.off} onChange={handleSwitchChange} - disabled={!canWritePolicyManagement} + disabled={!showEditableFormFields} /> ); } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/user_notification.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/user_notification.tsx index f08ec74c95b5a..4b6a2077f835e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/user_notification.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/user_notification.tsx @@ -19,12 +19,11 @@ import { EuiText, EuiTextArea, } from '@elastic/eui'; -import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; import type { ImmutableArray, UIPolicyConfig } from '../../../../../../../common/endpoint/types'; import { ProtectionModes } from '../../../../../../../common/endpoint/types'; import type { PolicyProtection, MacPolicyProtection, LinuxPolicyProtection } from '../../../types'; import { ConfigFormHeading } from '../../components/config_form'; -import { usePolicyDetailsSelector } from '../../policy_hooks'; +import { useShowEditableFormFields, usePolicyDetailsSelector } from '../../policy_hooks'; import { policyConfig } from '../../../store/policy_details/selectors'; import type { AppAction } from '../../../../../../common/store/actions'; import { SupportedVersionNotice } from './supported_version'; @@ -37,7 +36,7 @@ export const UserNotification = React.memo( protection: PolicyProtection; osList: ImmutableArray>; }) => { - const { canWritePolicyManagement } = useUserPrivileges().endpointPrivileges; + const showEditableFormFields = useShowEditableFormFields(); const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); const dispatch = useDispatch<(action: AppAction) => void>(); const selected = policyDetailsConfig && policyDetailsConfig.windows[protection].mode; @@ -141,7 +140,7 @@ export const UserNotification = React.memo( id={`${protection}UserNotificationCheckbox}`} onChange={handleUserNotificationCheckbox} checked={userNotificationSelected} - disabled={!canWritePolicyManagement || selected === ProtectionModes.off} + disabled={!showEditableFormFields || selected === ProtectionModes.off} label={i18n.translate('xpack.securitySolution.endpoint.policyDetail.notifyUser', { defaultMessage: 'Notify user', })} @@ -198,7 +197,7 @@ export const UserNotification = React.memo( value={userNotificationMessage} onChange={handleCustomUserNotification} fullWidth={true} - disabled={!canWritePolicyManagement} + disabled={!showEditableFormFields} data-test-subj={`${protection}UserNotificationCustomMessage`} /> diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx index e6b69f21d26f3..a9782c8a56a02 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx @@ -19,7 +19,6 @@ import { import { OperatingSystem } from '@kbn/securitysolution-utils'; import { useDispatch } from 'react-redux'; import { cloneDeep } from 'lodash'; -import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; import { APP_UI_ID } from '../../../../../../../common/constants'; import { SecurityPageName } from '../../../../../../app/types'; import type { @@ -36,7 +35,7 @@ import { RadioButtons } from '../components/radio_buttons'; import { UserNotification } from '../components/user_notification'; import { ProtectionSwitch } from '../components/protection_switch'; import { policyConfig } from '../../../store/policy_details/selectors'; -import { usePolicyDetailsSelector } from '../../policy_hooks'; +import { useShowEditableFormFields, usePolicyDetailsSelector } from '../../policy_hooks'; import type { AppAction } from '../../../../../../common/store/actions'; /** The Malware Protections form for policy details @@ -60,7 +59,7 @@ export const MalwareProtections = React.memo(() => { defaultMessage: 'Blocklist enabled', } ); - const { canWritePolicyManagement } = useUserPrivileges().endpointPrivileges; + const showEditableFormFields = useShowEditableFormFields(); const isPlatinumPlus = useLicense().isPlatinumPlus(); const dispatch = useDispatch<(action: AppAction) => void>(); const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); @@ -120,7 +119,7 @@ export const MalwareProtections = React.memo(() => { checked={policyDetailsConfig.windows[protection].blocklist} onChange={handleBlocklistSwitchChange} disabled={ - !canWritePolicyManagement || policyDetailsConfig.windows[protection].mode === 'off' + !showEditableFormFields || policyDetailsConfig.windows[protection].mode === 'off' } /> diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts index 9ffef5333ca93..001e3c5727a5c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { useHistory } from 'react-router-dom'; import { useSelector } from 'react-redux'; import { @@ -13,6 +13,8 @@ import { ENDPOINT_EVENT_FILTERS_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID, } from '@kbn/securitysolution-list-constants'; +import { useKibana } from '../../../../common/lib/kibana'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; import type { PolicyDetailsArtifactsPageLocation, PolicyDetailsState } from '../types'; import type { State } from '../../../../common/store'; import { @@ -26,7 +28,7 @@ import { getPolicyHostIsolationExceptionsPath, } from '../../../common/routing'; import { getCurrentArtifactsLocation, policyIdFromParams } from '../store/policy_details/selectors'; -import { POLICIES_PATH } from '../../../../../common/constants'; +import { APP_UI_ID, POLICIES_PATH } from '../../../../../common/constants'; /** * Narrows global state down to the PolicyDetailsState before calling the provided Policy Details Selector @@ -88,3 +90,27 @@ export const useIsPolicySettingsBarVisible = () => { window.location.pathname.includes('/settings') ); }; + +/** + * Indicates if user is granted Write access to Policy Management. This method differs from what + * `useUserPrivileges().endpointPrivileges.canWritePolicyManagement` in that it also checks if + * user has `canAccessFleet` if form is being displayed outside of Security Solution. + * This is to ensure that the Policy Form remains accessible when displayed inside of Fleet + * pages if the user does not have privileges to security solution policy management. + */ +export const useShowEditableFormFields = (): boolean => { + const { canWritePolicyManagement, canAccessFleet } = useUserPrivileges().endpointPrivileges; + const { getUrlForApp } = useKibana().services.application; + + const securitySolutionUrl = useMemo(() => { + return getUrlForApp(APP_UI_ID); + }, [getUrlForApp]); + + return useMemo(() => { + if (window.location.pathname.startsWith(securitySolutionUrl)) { + return canWritePolicyManagement; + } else { + return canAccessFleet; + } + }, [canAccessFleet, canWritePolicyManagement, securitySolutionUrl]); +};