[Security Solution][Endpoint] Fix Policy form being displayed as Read Only when displayed in Fleet pages (#147212)

## Summary

- Fix Policy form being displayed as Read Only when displayed in Fleet
pages

Tested:

With Role that DOES NOT have access to Security Solution

- Fleet - Agent Policies - Edit agent Policy - Edit endpoint package
policy edit form: 👍
- Integrations Endpoint Page - Policies - edit form: 👍 

With Role that does have `policy management` privilege to Security
Solution

- Policies - Edit policy form: 👍
This commit is contained in:
Paul Tavares 2022-12-14 09:36:14 -05:00 committed by GitHub
parent 412fa4285b
commit ac6f2fb782
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 194 additions and 201 deletions

View file

@ -35,7 +35,8 @@ export const useEndpointPrivileges = (): Immutable<EndpointPrivileges> => {
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<EndpointPrivileges> => {
const [hasHostIsolationExceptionsItems, setHasHostIsolationExceptionsItems] =
useState<boolean>(false);
const securitySolutionPermissions = calculatePermissionsFromCapabilities(
useKibana().services.application.capabilities
const securitySolutionPermissions = useMemo(
() => calculatePermissionsFromCapabilities(kibanaServices.application.capabilities),
[kibanaServices.application.capabilities]
);
const privileges = useMemo(() => {

View file

@ -154,6 +154,11 @@ export interface AppContextTestRender {
* @param flags
*/
setExperimentalFlag: (flags: Partial<ExperimentalFeatures>) => 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,
};
};

View file

@ -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}
/>
</ConfigForm>
);

View file

@ -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}
/>
</ConfigForm>
);

View file

@ -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 = <T extends OperatingSystem>({
onValueSelection,
supplementalOptions,
}: EventsFormProps<T>) => {
const { canWritePolicyManagement } = useUserPrivileges().endpointPrivileges;
const showEditableFormFields = useShowEditableFormFields();
const policyDetailsConfig = usePolicyDetailsSelector(policyConfig);
const theme = useContext(ThemeContext);
const countSelected = useCallback(() => {
@ -124,7 +123,7 @@ const InnerEventsForm = <T extends OperatingSystem>({
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 = <T extends OperatingSystem>({
checked={selection[protectionField]}
onChange={(event) => onValueSelection(protectionField, event.target.checked)}
disabled={
!canWritePolicyManagement ||
!showEditableFormFields ||
(isDisabled ? isDisabled(policyDetailsConfig) : false)
}
/>

View file

@ -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<RenderContextProvidersProps>(
({ store, depsStart, queryClient, children }) => {
const {
application: { capabilities },
} = useKibana().services;
return (
<ReduxStoreProvider store={store}>
<ReactQueryClientProvider queryClient={queryClient}>
<SecuritySolutionStartDependenciesContext.Provider value={depsStart}>
<CurrentLicense>{children}</CurrentLicense>
<UserPrivilegesProvider kibanaCapabilities={capabilities}>
<CurrentLicense>{children}</CurrentLicense>
</UserPrivilegesProvider>
</SecuritySolutionStartDependenciesContext.Provider>
</ReactQueryClientProvider>
</ReduxStoreProvider>

View file

@ -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<AppContextTestRender['render']>;
let renderResult: ReturnType<AppContextTestRender['render']>;
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(
<EndpointPackageCustomExtension
@ -44,69 +41,42 @@ describe('When displaying the EndpointPackageCustomExtension fleet UI extension'
});
afterEach(() => {
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,
}),
});
render();
expect(renderResult.queryByTestId(artifactCardTestId)).toBeNull();
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();
await waitFor(() => {
expect(renderResult.queryByTestId('hostIsolationExceptions-fleetCard')).toBeNull();
});
});
}
);
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();

View file

@ -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<PackageCustomExtensionComponentProps>((props) => {
const http = useHttp();
@ -115,7 +115,7 @@ export const EndpointPackageCustomExtension = memo<PackageCustomExtensionCompone
canReadEventFilters,
canReadTrustedApplications,
canReadHostIsolationExceptions,
} = useEndpointPrivileges();
} = useUserPrivileges().endpointPrivileges;
const userCanAccessContent = useCanAccessSomeArtifacts();

View file

@ -8,6 +8,7 @@
import React, { memo, useCallback, useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiLoadingContent, EuiSpacer, EuiText } from '@elastic/eui';
import { useUserPrivileges } from '../../../../../../../common/components/user_privileges';
import {
BLOCKLISTS_LABELS,
EVENT_FILTERS_LABELS,
@ -36,7 +37,6 @@ import { SEARCHABLE_FIELDS as EVENT_FILTERS_SEARCHABLE_FIELDS } from '../../../.
import { SEARCHABLE_FIELDS as HOST_ISOLATION_EXCEPTIONS_SEARCHABLE_FIELDS } from '../../../../../host_isolation_exceptions/constants';
import { SEARCHABLE_FIELDS as BLOCKLIST_SEARCHABLE_FIELDS } from '../../../../../blocklist/constants';
import { useHttp } from '../../../../../../../common/lib/kibana';
import { useEndpointPrivileges } from '../../../../../../../common/components/user_privileges/endpoint';
interface PolicyArtifactCardProps {
policyId: string;
@ -48,7 +48,7 @@ const TrustedAppsPolicyCard = memo<PolicyArtifactCardProps>(({ policyId }) => {
() => TrustedAppsApiClient.getInstance(http),
[http]
);
const { canReadPolicyManagement } = useEndpointPrivileges();
const { canReadPolicyManagement } = useUserPrivileges().endpointPrivileges;
const getArtifactPathHandler: FleetIntegrationArtifactCardProps['getArtifactsPath'] =
useCallback(() => {
@ -78,7 +78,7 @@ const EventFiltersPolicyCard = memo<PolicyArtifactCardProps>(({ policyId }) => {
() => EventFiltersApiClient.getInstance(http),
[http]
);
const { canReadPolicyManagement } = useEndpointPrivileges();
const { canReadPolicyManagement } = useUserPrivileges().endpointPrivileges;
const getArtifactPathHandler: FleetIntegrationArtifactCardProps['getArtifactsPath'] =
useCallback(() => {
@ -108,7 +108,7 @@ const HostIsolationExceptionsPolicyCard = memo<PolicyArtifactCardProps>(({ 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<PolicyArtifactCardProps>(({ 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<EndpointPolicyArtifactCardsProps
canReadEventFilters,
canReadTrustedApplications,
canReadHostIsolationExceptions,
} = useEndpointPrivileges();
} = useUserPrivileges().endpointPrivileges;
const canAccessArtifactContent = useCanAccessSomeArtifacts();
if (loading) {

View file

@ -72,7 +72,9 @@ export const FleetIntegrationArtifactsCard = memo<FleetIntegrationArtifactCardPr
{ policies: [policyId, 'all'] },
searchableFields,
{
onError: (error) => toasts.addDanger(labels.artifactsSummaryApiError(error.message)),
onError: (error) => {
toasts.addDanger(labels.artifactsSummaryApiError(error.message));
},
}
);

View file

@ -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`);
}
);
});

View file

@ -103,39 +103,37 @@ const WrappedPolicyDetailsForm = memo<{
return (
<div data-test-subj="endpointIntegrationPolicyForm">
<>
<EndpointPolicyArtifactCards policyId={policyId} />
<div>
<EuiText>
<h5>
<EndpointPolicyArtifactCards policyId={policyId} />
<div>
<EuiText>
<h5>
<FormattedMessage
id="xpack.securitySolution.endpoint.policyDetails.settings.title"
defaultMessage="Policy settings"
/>
</h5>
</EuiText>
<EuiSpacer size="s" />
{endpointDetailsLoadingError ? (
<EuiCallOut
title={
<FormattedMessage
id="xpack.securitySolution.endpoint.policyDetails.settings.title"
defaultMessage="Policy settings"
id="xpack.securitySolution.endpoint.policyDetails.loadError"
defaultMessage="Failed to load endpoint policy settings"
/>
</h5>
</EuiText>
<EuiSpacer size="s" />
{endpointDetailsLoadingError ? (
<EuiCallOut
title={
<FormattedMessage
id="xpack.securitySolution.endpoint.policyDetails.loadError"
defaultMessage="Failed to load endpoint policy settings"
/>
}
iconType="alert"
color="warning"
data-test-subj="endpiontPolicySettingsLoadingError"
>
{endpointDetailsLoadingError.message}
</EuiCallOut>
) : !endpointPolicyDetails ? (
<EuiLoadingSpinner size="l" className="essentialAnimation" />
) : (
<PolicyDetailsForm />
)}
</div>
</>
}
iconType="alert"
color="warning"
data-test-subj="endpiontPolicySettingsLoadingError"
>
{endpointDetailsLoadingError.message}
</EuiCallOut>
) : !endpointPolicyDetails ? (
<EuiLoadingSpinner size="l" className="essentialAnimation" />
) : (
<PolicyDetailsForm />
)}
</div>
</div>
);
});

View file

@ -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 (

View file

@ -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 (
<I18nProvider>
<EuiThemeProvider>
<KibanaContextProvider services={mockedContext.startServices}>
<RenderContextProviders
store={store}
depsStart={mockedContext.depsStart}
queryClient={queryClient}
>
<KibanaContextProvider services={startServices}>
<RenderContextProviders store={store} depsStart={depsStart} queryClient={queryClient}>
{children}
</RenderContextProviders>
</KibanaContextProvider>

View file

@ -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<string, unknown>, 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}
/>
</EuiFormRow>
</>

View file

@ -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 <EuiLoadingContent lines={5} />;
}
return (
<>

View file

@ -343,6 +343,7 @@ describe('Policy Form Layout', () => {
beforeEach(() => {
const mockedPrivileges = getUserPrivilegesMockDefaultValue();
mockedPrivileges.endpointPrivileges.canWritePolicyManagement = false;
mockedPrivileges.endpointPrivileges.canAccessFleet = false;
useUserPrivilegesMock.mockReturnValue(mockedPrivileges);

View file

@ -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<PolicyDetailsRouteState>();
const { canWritePolicyManagement } = useUserPrivileges().endpointPrivileges;
const showEditableFormFields = useShowEditableFormFields();
// Store values
const policyItem = usePolicyDetailsSelector(policyDetails);
@ -181,7 +180,7 @@ export const PolicyFormLayout = React.memo(() => {
</EuiButtonEmpty>
</EuiThemeProvider>
</EuiFlexItem>
{canWritePolicyManagement && (
{showEditableFormFields && (
<EuiFlexItem grow={false}>
<EuiButton
fill={true}

View file

@ -9,11 +9,10 @@ import React, { useCallback, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { cloneDeep } from 'lodash';
import { htmlIdGenerator, EuiRadio } 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 { MacPolicyProtection, LinuxPolicyProtection, PolicyProtection } from '../../../types';
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 { useLicense } from '../../../../../../common/hooks/use_license';
@ -35,7 +34,7 @@ export const ProtectionRadio = React.memo(
const radioButtonId = useMemo(() => 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}
/>
);
}

View file

@ -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}
/>
);
}

View file

@ -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<Partial<keyof UIPolicyConfig>>;
}) => {
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`}
/>
</>

View file

@ -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'
}
/>
</EuiFlexItem>

View file

@ -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]);
};