From 636baadfa278bf831ad21457be4abde4b9c837c9 Mon Sep 17 00:00:00 2001 From: Konrad Szwarc Date: Mon, 23 Sep 2024 14:00:15 +0200 Subject: [PATCH] [EDR Workflows] The host isolation exception tab is hidden on the basic license if no artifacts (#192562) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR updates how the Host Isolation Exceptions tab is displayed based on the user’s permissions and license. The tab is always visible to platinum+ users. For lower-tier licenses, a check is performed: if a user has previously defined host isolation exceptions, they will see the tab and be able to view or remove existing exceptions. If they haven’t, the tab will be hidden, and the functionality will be inaccessible. Previously, even if a user didn’t have access to host isolation exceptions, they could still see and enter the Host Isolation Exceptions tab. To test locally: ESS: 1. Start ES + Kibana the regular way, with the default `trial` license. 2. Add HIE 3. Downgrade license (https://github.com/elastic/pzl-es-tools) 4. Verify that the license had been downgraded Serverless: 1. Start Serverless ES `yarn es serverless --clean --teardown --kill -E xpack.security.authc.api_key.enabled=true -E http.host=0.0.0.0 --projectType security` 2. Start Serverless Kibana `yarn serverless-security` 3. Add HIE 4. Modify `config/serverless.security.yml` to security and endpoint essential 5. Wait for Kibana to reload ESS: https://github.com/user-attachments/assets/75527af7-9d06-4da7-9e86-6ce6b22ac147 Serverless: https://github.com/user-attachments/assets/e89bd642-9e99-4a22-8b42-5997f7333ea6 --------- Co-authored-by: Ash <1849116+ashokaditya@users.noreply.github.com> --- ..._host_isolation_exceptions_access.test.tsx | 84 ++++++++++ .../use_host_isolation_exceptions_access.tsx | 43 +++++ .../services/blocklists_api_client.ts | 6 +- .../pages/event_filters/service/api_client.ts | 6 +- .../host_isolation_exceptions_api_client.ts | 6 +- .../pages/policy/view/policy_details.test.tsx | 154 ++++++++++++------ .../pages/policy/view/tabs/policy_tabs.tsx | 42 +++-- .../pages/trusted_apps/service/api_client.ts | 6 +- 8 files changed, 273 insertions(+), 74 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/hooks/artifacts/use_host_isolation_exceptions_access.test.tsx create mode 100644 x-pack/plugins/security_solution/public/management/hooks/artifacts/use_host_isolation_exceptions_access.tsx diff --git a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_host_isolation_exceptions_access.test.tsx b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_host_isolation_exceptions_access.test.tsx new file mode 100644 index 00000000000000..2bd673f68095fd --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_host_isolation_exceptions_access.test.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { useHostIsolationExceptionsAccess } from './use_host_isolation_exceptions_access'; +import { checkArtifactHasData } from '../../services/exceptions_list/check_artifact_has_data'; + +jest.mock('../../services/exceptions_list/check_artifact_has_data', () => ({ + checkArtifactHasData: jest.fn(), +})); + +const mockArtifactHasData = (hasData = true) => { + (checkArtifactHasData as jest.Mock).mockResolvedValueOnce(hasData); +}; + +describe('useHostIsolationExceptionsAccess', () => { + const mockApiClient = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const setupHook = (canAccess: boolean, canRead: boolean) => { + return renderHook(() => useHostIsolationExceptionsAccess(canAccess, canRead, mockApiClient)); + }; + + test('should set access to true if canAccessHostIsolationExceptions is true', async () => { + const { result, waitFor } = setupHook(true, false); + + await waitFor(() => expect(result.current.hasAccessToHostIsolationExceptions).toBe(true)); + }); + + test('should check for artifact data if canReadHostIsolationExceptions is true and canAccessHostIsolationExceptions is false', async () => { + mockArtifactHasData(); + + const { result, waitFor } = setupHook(false, true); + + await waitFor(() => { + expect(checkArtifactHasData).toHaveBeenCalledWith(mockApiClient()); + expect(result.current.hasAccessToHostIsolationExceptions).toBe(true); + }); + }); + + test('should set access to false if canReadHostIsolationExceptions is true but no artifact data exists', async () => { + mockArtifactHasData(false); + + const { result, waitFor } = setupHook(false, true); + + await waitFor(() => { + expect(checkArtifactHasData).toHaveBeenCalledWith(mockApiClient()); + expect(result.current.hasAccessToHostIsolationExceptions).toBe(false); + }); + }); + + test('should set access to false if neither canAccessHostIsolationExceptions nor canReadHostIsolationExceptions is true', async () => { + const { result, waitFor } = setupHook(false, false); + await waitFor(() => { + expect(result.current.hasAccessToHostIsolationExceptions).toBe(false); + }); + }); + + test('should not call checkArtifactHasData if canAccessHostIsolationExceptions is true', async () => { + const { result, waitFor } = setupHook(true, true); + + await waitFor(() => { + expect(checkArtifactHasData).not.toHaveBeenCalled(); + expect(result.current.hasAccessToHostIsolationExceptions).toBe(true); + }); + }); + + test('should set loading state correctly while checking access', async () => { + const { result, waitFor } = setupHook(false, true); + + expect(result.current.isHostIsolationExceptionsAccessLoading).toBe(true); + + await waitFor(() => { + expect(result.current.isHostIsolationExceptionsAccessLoading).toBe(false); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_host_isolation_exceptions_access.tsx b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_host_isolation_exceptions_access.tsx new file mode 100644 index 00000000000000..daccba91bccf0d --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_host_isolation_exceptions_access.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useState } from 'react'; +import { checkArtifactHasData } from '../../services/exceptions_list/check_artifact_has_data'; +import type { ExceptionsListApiClient } from '../../services/exceptions_list/exceptions_list_api_client'; + +export const useHostIsolationExceptionsAccess = ( + canAccessHostIsolationExceptions: boolean, + canReadHostIsolationExceptions: boolean, + getApiClient: () => ExceptionsListApiClient +): { + hasAccessToHostIsolationExceptions: boolean; + isHostIsolationExceptionsAccessLoading: boolean; +} => { + const [hasAccess, setHasAccess] = useState(null); + + useEffect(() => { + (async () => { + // Host isolation exceptions is a paid feature and therefore: + // canAccessHostIsolationExceptions signifies if the user has required license to access the feature. + // canReadHostIsolationExceptions, however, is a privilege that allows the user to read and delete the data even if the license is not sufficient (downgrade scenario). + // In such cases, the tab should be visible only if there is existing data. + if (canAccessHostIsolationExceptions) { + setHasAccess(true); + } else if (canReadHostIsolationExceptions) { + const result = await checkArtifactHasData(getApiClient()); + setHasAccess(result); + } else { + setHasAccess(false); + } + })(); + }, [canAccessHostIsolationExceptions, canReadHostIsolationExceptions, getApiClient]); + + return { + hasAccessToHostIsolationExceptions: !!hasAccess, + isHostIsolationExceptionsAccessLoading: hasAccess === null, + }; +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/blocklist/services/blocklists_api_client.ts b/x-pack/plugins/security_solution/public/management/pages/blocklist/services/blocklists_api_client.ts index 017301c55a018a..eb47a4d22ae2c4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/blocklist/services/blocklists_api_client.ts +++ b/x-pack/plugins/security_solution/public/management/pages/blocklist/services/blocklists_api_client.ts @@ -10,7 +10,7 @@ import type { ExceptionListItemSchema, UpdateExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; -import { ENDPOINT_BLOCKLISTS_LIST_ID } from '@kbn/securitysolution-list-constants'; +import { ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants'; import type { HttpStart } from '@kbn/core/public'; import type { ConditionEntry } from '../../../../../common/endpoint/types'; @@ -46,7 +46,7 @@ export class BlocklistsApiClient extends ExceptionsListApiClient { constructor(http: HttpStart) { super( http, - ENDPOINT_BLOCKLISTS_LIST_ID, + ENDPOINT_ARTIFACT_LISTS.blocklists.id, BLOCKLISTS_LIST_DEFINITION, readTransform, writeTransform @@ -56,7 +56,7 @@ export class BlocklistsApiClient extends ExceptionsListApiClient { public static getInstance(http: HttpStart): ExceptionsListApiClient { return super.getInstance( http, - ENDPOINT_BLOCKLISTS_LIST_ID, + ENDPOINT_ARTIFACT_LISTS.blocklists.id, BLOCKLISTS_LIST_DEFINITION, readTransform, writeTransform diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/service/api_client.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/service/api_client.ts index a69e54bf7776de..e7c5e53e34274c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/service/api_client.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/service/api_client.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '@kbn/securitysolution-list-constants'; +import { ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants'; import type { HttpStart } from '@kbn/core/public'; import type { CreateExceptionListItemSchema, @@ -33,7 +33,7 @@ export class EventFiltersApiClient extends ExceptionsListApiClient { constructor(http: HttpStart) { super( http, - ENDPOINT_EVENT_FILTERS_LIST_ID, + ENDPOINT_ARTIFACT_LISTS.eventFilters.id, EVENT_FILTER_LIST_DEFINITION, undefined, writeTransform @@ -43,7 +43,7 @@ export class EventFiltersApiClient extends ExceptionsListApiClient { public static getInstance(http: HttpStart): ExceptionsListApiClient { return super.getInstance( http, - ENDPOINT_EVENT_FILTERS_LIST_ID, + ENDPOINT_ARTIFACT_LISTS.eventFilters.id, EVENT_FILTER_LIST_DEFINITION, undefined, writeTransform diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/host_isolation_exceptions_api_client.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/host_isolation_exceptions_api_client.ts index eea09ceaf91cab..94bdab6af651e6 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/host_isolation_exceptions_api_client.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/host_isolation_exceptions_api_client.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID } from '@kbn/securitysolution-list-constants'; +import { ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants'; import type { HttpStart } from '@kbn/core/public'; import { ExceptionsListApiClient } from '../../services/exceptions_list/exceptions_list_api_client'; import { HOST_ISOLATION_EXCEPTIONS_LIST_DEFINITION } from './constants'; @@ -19,7 +19,7 @@ export class HostIsolationExceptionsApiClient extends ExceptionsListApiClient { constructor(http: HttpStart) { super( http, - ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID, + ENDPOINT_ARTIFACT_LISTS.hostIsolationExceptions.id, HOST_ISOLATION_EXCEPTIONS_LIST_DEFINITION ); } @@ -27,7 +27,7 @@ export class HostIsolationExceptionsApiClient extends ExceptionsListApiClient { public static getInstance(http: HttpStart): ExceptionsListApiClient { return super.getInstance( http, - ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID, + ENDPOINT_ARTIFACT_LISTS.hostIsolationExceptions.id, HOST_ISOLATION_EXCEPTIONS_LIST_DEFINITION ); } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx index a41cd65f9db3c2..ffcee81a3cb8f0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx @@ -30,12 +30,15 @@ import { PolicyDetails } from './policy_details'; import { APP_UI_ID } from '../../../../../common/constants'; import { createLicenseServiceMock } from '../../../../../common/license/mocks'; import { licenseService as licenseServiceMocked } from '../../../../common/hooks/__mocks__/use_license'; +import { useHostIsolationExceptionsAccess } from '../../../hooks/artifacts/use_host_isolation_exceptions_access'; jest.mock('../../../../common/components/user_privileges'); jest.mock('../../../../common/hooks/use_license'); +jest.mock('../../../hooks/artifacts/use_host_isolation_exceptions_access'); const useUserPrivilegesMock = useUserPrivileges as jest.Mock; const useLicenseMock = _useLicense as jest.Mock; +const useHostIsolationExceptionsAccessMock = useHostIsolationExceptionsAccess as jest.Mock; describe('Policy Details', () => { const policyDetailsPathUrl = getPolicyDetailPath('1'); @@ -100,11 +103,15 @@ describe('Policy Details', () => { policyPackagePolicy.policy_id = policyPackagePolicy.policy_ids[0]; const policyListApiHandlers = policyListApiPathHandlers(); + useHostIsolationExceptionsAccessMock.mockReturnValue({ + hasAccessToHostIsolationExceptions: true, + isHostIsolationExceptionsAccessLoading: false, + }); http.get.mockImplementation((...args) => { const [path] = args; if (typeof path === 'string') { - // GET datasouce + // GET datasource if (path === `${PACKAGE_POLICY_API_ROOT}/1`) { asyncActions = asyncActions.then(async (): Promise => sleep()); return Promise.resolve({ @@ -135,6 +142,34 @@ describe('Policy Details', () => { history.push(policyDetailsPathUrl); }); + it('should NOT display tabs when Host Isolation Exceptions access is loading', async () => { + useHostIsolationExceptionsAccessMock.mockReturnValue({ + hasAccessToHostIsolationExceptions: false, + isHostIsolationExceptionsAccessLoading: true, + }); + policyView = render(); + await asyncActions; + policyView.update(); + const tab = policyView.find('[data-test-subj="policyTabs"]'); + expect(tab).toHaveLength(0); + const loader = policyView.find('span[data-test-subj="privilegesLoading"]'); + expect(loader).toHaveLength(1); + }); + + it('should display tabs when Host Isolation Exceptions access is not loading', async () => { + useHostIsolationExceptionsAccessMock.mockReturnValue({ + hasAccessToHostIsolationExceptions: true, + isHostIsolationExceptionsAccessLoading: false, + }); + policyView = render(); + await asyncActions; + policyView.update(); + const tab = policyView.find('div[data-test-subj="policyTabs"]'); + expect(tab).toHaveLength(1); + const loader = policyView.find('div[data-test-subj="privilegesLoading"]'); + expect(loader).toHaveLength(0); + }); + it('should NOT display timeline', async () => { policyView = render(); await asyncActions; @@ -210,13 +245,24 @@ describe('Policy Details', () => { expect(eventFiltersTab.text()).toBe('Event filters'); }); - it('should display the host isolation exceptions tab', async () => { + it('should display the host isolation exceptions tab if user have access', async () => { policyView = render(); await asyncActions; policyView.update(); - const tab = policyView.find('button#hostIsolationExceptions'); + const tab = policyView.find('button[data-test-subj="policyHostIsolationExceptionsTab"]'); expect(tab).toHaveLength(1); - expect(tab.text()).toBe('Host isolation exceptions'); + }); + + it("shouldn't display the host isolation exceptions tab when user doesn't have access", async () => { + useHostIsolationExceptionsAccessMock.mockReturnValue({ + hasAccessToHostIsolationExceptions: false, + isHostIsolationExceptionsAccessLoading: false, + }); + policyView = render(); + await asyncActions; + policyView.update(); + const tab = policyView.find('button[data-test-subj="policyHostIsolationExceptionsTab"]'); + expect(tab).toHaveLength(0); }); it('should display the protection updates tab', async () => { @@ -234,6 +280,10 @@ describe('Policy Details', () => { licenseServiceMock.isEnterprise.mockReturnValue(false); useLicenseMock.mockReturnValue(licenseServiceMock); + useHostIsolationExceptionsAccessMock.mockReturnValue({ + hasAccessToHostIsolationExceptions: false, + isHostIsolationExceptionsAccessLoading: false, + }); }); afterEach(() => { @@ -247,55 +297,55 @@ describe('Policy Details', () => { const tab = policyView.find('button#protectionUpdates'); expect(tab).toHaveLength(0); }); - }); - describe('without required permissions', () => { - const renderWithPrivilege = async (privilege: string) => { - useUserPrivilegesMock.mockReturnValue({ - endpointPrivileges: { - loading: false, - [privilege]: false, - }, - }); - policyView = render(); - await asyncActions; - policyView.update(); - }; - - it.each([ - ['trusted apps', 'canReadTrustedApplications', 'trustedApps'], - ['event filters', 'canReadEventFilters', 'eventFilters'], - ['host isolation exeptions', 'canReadHostIsolationExceptions', 'hostIsolationExceptions'], - ['blocklist', 'canReadBlocklist', 'blocklists'], - ])( - 'should not display the %s tab with no privileges', - async (_: string, privilege: string, selector: string) => { - await renderWithPrivilege(privilege); - expect(policyView.find(`button#${selector}`)).toHaveLength(0); - } - ); - - it.each([ - ['trusted apps', 'canReadTrustedApplications', getPolicyTrustedAppsPath('1')], - ['event filters', 'canReadEventFilters', getPolicyEventFiltersPath('1')], - [ - 'host isolation exeptions', - 'canReadHostIsolationExceptions', - getPolicyHostIsolationExceptionsPath('1'), - ], - ['blocklist', 'canReadBlocklist', getPolicyBlocklistsPath('1')], - ])( - 'should redirect to policy details when no %s required privileges', - async (_: string, privilege: string, path: string) => { - history.push(path); - await renderWithPrivilege(privilege); - expect(history.location.pathname).toBe(policyDetailsPathUrl); - expect(coreStart.notifications.toasts.addDanger).toHaveBeenCalledTimes(1); - expect(coreStart.notifications.toasts.addDanger).toHaveBeenCalledWith( - 'You do not have the required Kibana permissions to use the given artifact.' - ); - } - ); + describe('without required permissions', () => { + const renderWithoutPrivilege = async (privilege: string) => { + useUserPrivilegesMock.mockReturnValue({ + endpointPrivileges: { + loading: false, + [privilege]: false, + }, + }); + policyView = render(); + await asyncActions; + policyView.update(); + }; + + it.each([ + ['trusted apps', 'canReadTrustedApplications', 'trustedApps'], + ['event filters', 'canReadEventFilters', 'eventFilters'], + ['host isolation exeptions', 'canReadHostIsolationExceptions', 'hostIsolationExceptions'], + ['blocklist', 'canReadBlocklist', 'blocklists'], + ])( + 'should not display the %s tab with no privileges', + async (_: string, privilege: string, selector: string) => { + await renderWithoutPrivilege(privilege); + expect(policyView.find(`button#${selector}`)).toHaveLength(0); + } + ); + + it.each([ + ['trusted apps', 'canReadTrustedApplications', getPolicyTrustedAppsPath('1')], + ['event filters', 'canReadEventFilters', getPolicyEventFiltersPath('1')], + [ + 'host isolation exeptions', + 'canReadHostIsolationExceptions', + getPolicyHostIsolationExceptionsPath('1'), + ], + ['blocklist', 'canReadBlocklist', getPolicyBlocklistsPath('1')], + ])( + 'should redirect to policy details when no %s required privileges', + async (_: string, privilege: string, path: string) => { + history.push(path); + await renderWithoutPrivilege(privilege); + expect(history.location.pathname).toBe(policyDetailsPathUrl); + expect(coreStart.notifications.toasts.addDanger).toHaveBeenCalledTimes(1); + expect(coreStart.notifications.toasts.addDanger).toHaveBeenCalledWith( + 'You do not have the required Kibana permissions to use the given artifact.' + ); + } + ); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx index cb480615d27a50..c350f91c914d6a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx @@ -58,6 +58,7 @@ import { SEARCHABLE_FIELDS as EVENT_FILTERS_SEARCHABLE_FIELDS } from '../../../e import { SEARCHABLE_FIELDS as HOST_ISOLATION_EXCEPTIONS_SEARCHABLE_FIELDS } from '../../../host_isolation_exceptions/constants'; import { SEARCHABLE_FIELDS as BLOCKLISTS_SEARCHABLE_FIELDS } from '../../../blocklist/constants'; import type { PolicyDetailsRouteState } from '../../../../../../common/endpoint/types'; +import { useHostIsolationExceptionsAccess } from '../../../../hooks/artifacts/use_host_isolation_exceptions_access'; enum PolicyTabKeys { SETTINGS = 'settings', @@ -118,11 +119,12 @@ export const PolicyTabs = React.memo(() => { canWriteTrustedApplications, canReadEventFilters, canWriteEventFilters, + canAccessHostIsolationExceptions, canReadHostIsolationExceptions, canWriteHostIsolationExceptions, canReadBlocklist, canWriteBlocklist, - loading: privilegesLoading, + loading: isPrivilegesLoading, } = useUserPrivileges().endpointPrivileges; const { state: routeState = {} } = useLocation(); @@ -131,12 +133,34 @@ export const PolicyTabs = React.memo(() => { ); const isEnterprise = useLicense().isEnterprise(); const isProtectionUpdatesEnabled = isEnterprise && isProtectionUpdatesFeatureEnabled; + + const getHostIsolationExceptionsApiClientInstance = useCallback( + () => HostIsolationExceptionsApiClient.getInstance(http), + [http] + ); + + const { hasAccessToHostIsolationExceptions, isHostIsolationExceptionsAccessLoading } = + useHostIsolationExceptionsAccess( + canAccessHostIsolationExceptions, + canReadHostIsolationExceptions, + getHostIsolationExceptionsApiClientInstance + ); + // move the user out of this route if they can't access it useEffect(() => { + if (isHostIsolationExceptionsAccessLoading || isPrivilegesLoading) { + return; + } + + const redirectHostIsolationException = + isInHostIsolationExceptionsTab && + (!canReadHostIsolationExceptions || + (!isHostIsolationExceptionsAccessLoading && !hasAccessToHostIsolationExceptions)); + if ( (isInTrustedAppsTab && !canReadTrustedApplications) || (isInEventFiltersTab && !canReadEventFilters) || - (isInHostIsolationExceptionsTab && !canReadHostIsolationExceptions) || + redirectHostIsolationException || (isInBlocklistsTab && !canReadBlocklist) ) { history.replace(getPolicyDetailPath(policyId)); @@ -152,12 +176,15 @@ export const PolicyTabs = React.memo(() => { canReadEventFilters, canReadHostIsolationExceptions, canReadTrustedApplications, + hasAccessToHostIsolationExceptions, history, + isHostIsolationExceptionsAccessLoading, isInBlocklistsTab, isInEventFiltersTab, isInHostIsolationExceptionsTab, isInProtectionUpdatesTab, isInTrustedAppsTab, + isPrivilegesLoading, policyId, toasts, ]); @@ -172,11 +199,6 @@ export const PolicyTabs = React.memo(() => { [http] ); - const getHostIsolationExceptionsApiClientInstance = useCallback( - () => HostIsolationExceptionsApiClient.getInstance(http), - [http] - ); - const getBlocklistsApiClientInstance = useCallback( () => BlocklistsApiClient.getInstance(http), [http] @@ -298,7 +320,7 @@ export const PolicyTabs = React.memo(() => { 'data-test-subj': 'policyEventFiltersTab', } : undefined, - [PolicyTabKeys.HOST_ISOLATION_EXCEPTIONS]: canReadHostIsolationExceptions + [PolicyTabKeys.HOST_ISOLATION_EXCEPTIONS]: hasAccessToHostIsolationExceptions ? { id: PolicyTabKeys.HOST_ISOLATION_EXCEPTIONS, name: i18n.translate( @@ -379,7 +401,7 @@ export const PolicyTabs = React.memo(() => { canReadEventFilters, getEventFiltersApiClientInstance, canWriteEventFilters, - canReadHostIsolationExceptions, + hasAccessToHostIsolationExceptions, getHostIsolationExceptionsApiClientInstance, canWriteHostIsolationExceptions, canReadBlocklist, @@ -485,7 +507,7 @@ export const PolicyTabs = React.memo(() => { }, [changeTab, unsavedChangesModal.nextTab]); // show loader for privileges validation - if (privilegesLoading) { + if (isPrivilegesLoading || isHostIsolationExceptionsAccessLoading) { return ; } diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/api_client.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/api_client.ts index 9ace94955d30f3..85672ea0c8b035 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/api_client.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/api_client.ts @@ -10,7 +10,7 @@ import type { ExceptionListItemSchema, UpdateExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; -import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '@kbn/securitysolution-list-constants'; +import { ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants'; import type { HttpStart } from '@kbn/core/public'; import type { ConditionEntry } from '../../../../../common/endpoint/types'; @@ -46,7 +46,7 @@ export class TrustedAppsApiClient extends ExceptionsListApiClient { constructor(http: HttpStart) { super( http, - ENDPOINT_TRUSTED_APPS_LIST_ID, + ENDPOINT_ARTIFACT_LISTS.trustedApps.id, TRUSTED_APPS_EXCEPTION_LIST_DEFINITION, readTransform, writeTransform @@ -56,7 +56,7 @@ export class TrustedAppsApiClient extends ExceptionsListApiClient { public static getInstance(http: HttpStart): ExceptionsListApiClient { return super.getInstance( http, - ENDPOINT_TRUSTED_APPS_LIST_ID, + ENDPOINT_ARTIFACT_LISTS.trustedApps.id, TRUSTED_APPS_EXCEPTION_LIST_DEFINITION, readTransform, writeTransform