Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

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 @@ -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.
Expand All @@ -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(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -310,6 +315,7 @@ export const createAppRootMockRenderer = (): AppContextTestRender => {
renderHook,
renderReactQueryHook,
setExperimentalFlag,
queryClient,
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }> = {
Expand Down Expand Up @@ -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) =>
Expand Down Expand Up @@ -70,7 +69,7 @@ export const AntivirusRegistrationForm = memo(() => {
label={TRANSLATIONS.label}
checked={antivirusRegistrationEnabled}
onChange={handleSwitchChange}
disabled={!canWritePolicyManagement}
disabled={!showEditableFormFields}
/>
</ConfigForm>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }> = {
Expand All @@ -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) =>
Expand All @@ -53,7 +52,7 @@ export const AttackSurfaceReductionForm = memo(() => {
label={TRANSLATIONS.label}
checked={credentialHardeningEnabled}
onChange={handleSwitchChange}
disabled={!canWritePolicyManagement}
disabled={!showEditableFormFields}
/>
</ConfigForm>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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}
/>
);
})}
Expand Down Expand Up @@ -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)
}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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
Expand All @@ -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,
}),
});
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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();
Expand Down Expand Up @@ -115,7 +115,7 @@ export const EndpointPackageCustomExtension = memo<PackageCustomExtensionCompone
canReadEventFilters,
canReadTrustedApplications,
canReadHostIsolationExceptions,
} = useEndpointPrivileges();
} = useUserPrivileges().endpointPrivileges;

const userCanAccessContent = useCanAccessSomeArtifacts();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand All @@ -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(() => {
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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(() => {
Expand All @@ -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(() => {
Expand Down Expand Up @@ -174,7 +174,7 @@ export const EndpointPolicyArtifactCards = memo<EndpointPolicyArtifactCardsProps
canReadEventFilters,
canReadTrustedApplications,
canReadHostIsolationExceptions,
} = useEndpointPrivileges();
} = useUserPrivileges().endpointPrivileges;
const canAccessArtifactContent = useCanAccessSomeArtifacts();

if (loading) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
},
}
);

Expand Down
Loading