diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index 150bc768601f4..0e77b705075f2 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -582,4 +582,8 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, + 'enterpriseSearch:enableEnginesSection': { + type: 'boolean', + _meta: { description: 'Non-default value of setting.' }, + }, }; diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index 8a897721b6dc6..4365e2a1dd72e 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -155,4 +155,5 @@ export interface UsageStats { 'securitySolution:showRelatedIntegrations': boolean; 'visualization:visualize:legacyGaugeChartsLibrary': boolean; 'enterpriseSearch:enableBehavioralAnalyticsSection': boolean; + 'enterpriseSearch:enableEnginesSection': boolean; } diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index b59177259ece3..c2b0971827d14 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -9125,6 +9125,12 @@ "_meta": { "description": "Non-default value of setting." } + }, + "enterpriseSearch:enableEnginesSection": { + "type": "boolean", + "_meta": { + "description": "Non-default value of setting." + } } } }, diff --git a/x-pack/plugins/enterprise_search/common/ui_settings_keys.ts b/x-pack/plugins/enterprise_search/common/ui_settings_keys.ts index d06902fe04fab..1c6cec2ca9c15 100644 --- a/x-pack/plugins/enterprise_search/common/ui_settings_keys.ts +++ b/x-pack/plugins/enterprise_search/common/ui_settings_keys.ts @@ -7,3 +7,4 @@ export const enterpriseSearchFeatureId = 'enterpriseSearch'; export const enableBehavioralAnalyticsSection = 'enterpriseSearch:enableBehavioralAnalyticsSection'; +export const enableEnginesSection = 'enterpriseSearch:enableEnginesSection'; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx new file mode 100644 index 0000000000000..7292dad6db745 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx @@ -0,0 +1,33 @@ +/* + * 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 React from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { EnterpriseSearchContentPageTemplate } from '../layout/page_template'; + +export const EnginesList = () => { + return ( + +
+ + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_router.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_router.tsx new file mode 100644 index 0000000000000..14b0f424839d6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_router.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 React from 'react'; +import { Route, Switch } from 'react-router-dom'; + +import { useValues } from 'kea'; + +import { enableEnginesSection } from '../../../../../common/ui_settings_keys'; +import { KibanaLogic } from '../../../shared/kibana'; +import { ENGINES_PATH } from '../../routes'; + +import { NotFound } from '../not_found'; + +import { EnginesList } from './engines_list'; + +export const EnginesRouter: React.FC = () => { + const { uiSettings } = useValues(KibanaLogic); + const enginesSectionEnabled = uiSettings?.get(enableEnginesSection, false); + if (!enginesSectionEnabled) { + return ( + + + + + + ); + } + return ( + + + + + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/index.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/index.tsx index 4f73c141d17c6..b1fe9f6c2816d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/index.tsx @@ -17,11 +17,18 @@ import { HttpLogic } from '../shared/http'; import { KibanaLogic } from '../shared/kibana'; import { VersionMismatchPage } from '../shared/version_mismatch'; +import { EnginesRouter } from './components/engines/engines_router'; import { ErrorConnecting } from './components/error_connecting'; import { NotFound } from './components/not_found'; import { SearchIndicesRouter } from './components/search_indices'; import { Settings } from './components/settings'; -import { SETUP_GUIDE_PATH, ROOT_PATH, SEARCH_INDICES_PATH, SETTINGS_PATH } from './routes'; +import { + SETUP_GUIDE_PATH, + ROOT_PATH, + SEARCH_INDICES_PATH, + SETTINGS_PATH, + ENGINES_PATH, +} from './routes'; export const EnterpriseSearchContent: React.FC = (props) => { const { config } = useValues(KibanaLogic); @@ -74,6 +81,9 @@ export const EnterpriseSearchContentConfigured: React.FC + + + diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/routes.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/routes.ts index a980402119062..82a2af053773a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/routes.ts @@ -22,4 +22,6 @@ export const SEARCH_INDEX_TAB_PATH = `${SEARCH_INDEX_PATH}/:tabId`; export const SEARCH_INDEX_CRAWLER_DOMAIN_DETAIL_PATH = `${SEARCH_INDEX_PATH}/crawler/domains/:domainId`; export const SEARCH_INDEX_SELECT_CONNECTOR_PATH = `${SEARCH_INDEX_PATH}/select_connector`; +export const ENGINES_PATH = `${ROOT_PATH}engines`; + export const ML_MANAGE_TRAINED_MODELS_PATH = '/app/ml/trained_models'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx index bd367e7de8e7d..167e2850f1b85 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx @@ -13,7 +13,10 @@ import { setMockValues, mockKibanaValues } from '../../__mocks__/kea_logic'; import { ProductAccess } from '../../../../common/types'; -import { enableBehavioralAnalyticsSection } from '../../../../common/ui_settings_keys'; +import { + enableBehavioralAnalyticsSection, + enableEnginesSection, +} from '../../../../common/ui_settings_keys'; import { useEnterpriseSearchNav } from './nav'; @@ -53,6 +56,11 @@ describe('useEnterpriseSearchContentNav', () => { ], name: 'Content', }, + { + id: 'enterpiseSearchEngines', + name: 'Engines', + href: '/app/enterprise_search/content/engines', + }, { id: 'enterpriseSearchAnalytics', items: [ @@ -95,6 +103,7 @@ describe('useEnterpriseSearchContentNav', () => { enableBehavioralAnalyticsSection, false ); + expect(mockKibanaValues.uiSettings.get).toHaveBeenCalledWith(enableEnginesSection, false); }); it('excludes legacy products when the user has no access to them', () => { @@ -104,8 +113,12 @@ describe('useEnterpriseSearchContentNav', () => { }; setMockValues({ productAccess: noProductAccess }); + mockKibanaValues.uiSettings.get.mockReturnValue(false); - expect(useEnterpriseSearchNav()[3]).toEqual({ + const esNav = useEnterpriseSearchNav(); + const searchNav = esNav.find((item) => item.id === 'search'); + expect(searchNav).not.toBeUndefined(); + expect(searchNav).toEqual({ id: 'search', items: [ { @@ -131,7 +144,10 @@ describe('useEnterpriseSearchContentNav', () => { setMockValues({ productAccess: workplaceSearchProductAccess }); - expect(useEnterpriseSearchNav()[3]).toEqual({ + const esNav = useEnterpriseSearchNav(); + const searchNav = esNav.find((item) => item.id === 'search'); + expect(searchNav).not.toBeUndefined(); + expect(searchNav).toEqual({ id: 'search', items: [ { @@ -162,7 +178,10 @@ describe('useEnterpriseSearchContentNav', () => { setMockValues({ productAccess: appSearchProductAccess }); - expect(useEnterpriseSearchNav()[3]).toEqual({ + const esNav = useEnterpriseSearchNav(); + const searchNav = esNav.find((item) => item.id === 'search'); + expect(searchNav).not.toBeUndefined(); + expect(searchNav).toEqual({ id: 'search', items: [ { @@ -184,4 +203,50 @@ describe('useEnterpriseSearchContentNav', () => { name: 'Search', }); }); + + it('excludes analytics when feature flag is off', () => { + const fullProductAccess: ProductAccess = { + hasAppSearchAccess: true, + hasWorkplaceSearchAccess: true, + }; + setMockValues({ productAccess: fullProductAccess }); + + const esNav = useEnterpriseSearchNav(); + expect(esNav.find((item) => item.id === 'enterpriseSearchAnalytics')).toBeUndefined(); + }); + it('includes analytics when feature flag is off', () => { + const fullProductAccess: ProductAccess = { + hasAppSearchAccess: true, + hasWorkplaceSearchAccess: true, + }; + setMockValues({ productAccess: fullProductAccess }); + mockKibanaValues.uiSettings.get.mockReturnValueOnce(true).mockReturnValue(false); + + const esNav = useEnterpriseSearchNav(); + expect(esNav.find((item) => item.id === 'enterpriseSearchAnalytics')).not.toBeUndefined(); + }); + it('excludes engines when feature flag is off', () => { + const fullProductAccess: ProductAccess = { + hasAppSearchAccess: true, + hasWorkplaceSearchAccess: true, + }; + setMockValues({ productAccess: fullProductAccess }); + + const esNav = useEnterpriseSearchNav(); + expect(esNav.find((item) => item.id === 'enterpiseSearchEngines')).toBeUndefined(); + }); + it('includes engines when feature flag is on', () => { + const fullProductAccess: ProductAccess = { + hasAppSearchAccess: true, + hasWorkplaceSearchAccess: true, + }; + setMockValues({ productAccess: fullProductAccess }); + mockKibanaValues.uiSettings.get + .mockReturnValueOnce(false) + .mockReturnValueOnce(true) + .mockReturnValue(false); + + const esNav = useEnterpriseSearchNav(); + expect(esNav.find((item) => item.id === 'enterpiseSearchEngines')).not.toBeUndefined(); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx index 471c40668b4a3..4eee88ecd474e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx @@ -19,8 +19,15 @@ import { SEARCH_EXPERIENCES_PLUGIN, WORKPLACE_SEARCH_PLUGIN, } from '../../../../common/constants'; -import { enableBehavioralAnalyticsSection } from '../../../../common/ui_settings_keys'; -import { SEARCH_INDICES_PATH, SETTINGS_PATH } from '../../enterprise_search_content/routes'; +import { + enableBehavioralAnalyticsSection, + enableEnginesSection, +} from '../../../../common/ui_settings_keys'; +import { + ENGINES_PATH, + SEARCH_INDICES_PATH, + SETTINGS_PATH, +} from '../../enterprise_search_content/routes'; import { KibanaLogic } from '../kibana'; import { generateNavLink } from './nav_link_helpers'; @@ -29,6 +36,7 @@ export const useEnterpriseSearchNav = () => { const { productAccess, uiSettings } = useValues(KibanaLogic); const analyticsSectionEnabled = uiSettings?.get(enableBehavioralAnalyticsSection, false); + const enginesSectionEnabled = uiSettings?.get(enableEnginesSection, false); const navItems: Array> = [ { @@ -71,6 +79,21 @@ export const useEnterpriseSearchNav = () => { defaultMessage: 'Content', }), }, + ...(enginesSectionEnabled + ? [ + { + id: 'enterpiseSearchEngines', + name: i18n.translate('xpack.enterpriseSearch.nav.enginesTitle', { + defaultMessage: 'Engines', + }), + ...generateNavLink({ + shouldNotCreateHref: true, + shouldShowActiveForSubroutes: true, + to: ENTERPRISE_SEARCH_CONTENT_PLUGIN.URL + ENGINES_PATH, + }), + }, + ] + : []), ...(analyticsSectionEnabled ? [ { diff --git a/x-pack/plugins/enterprise_search/server/ui_settings.ts b/x-pack/plugins/enterprise_search/server/ui_settings.ts index 99fb5906a3050..c49d1287b1bf6 100644 --- a/x-pack/plugins/enterprise_search/server/ui_settings.ts +++ b/x-pack/plugins/enterprise_search/server/ui_settings.ts @@ -5,9 +5,25 @@ * 2.0. */ +import { schema } from '@kbn/config-schema'; import { UiSettingsParams } from '@kbn/core/types'; +import { i18n } from '@kbn/i18n'; +import { enterpriseSearchFeatureId, enableEnginesSection } from '../common/ui_settings_keys'; /** * uiSettings definitions for Enterprise Search */ -export const uiSettings: Record> = {}; +export const uiSettings: Record> = { + [enableEnginesSection]: { + category: [enterpriseSearchFeatureId], + description: i18n.translate('xpack.enterpriseSearch.uiSettings.engines.description', { + defaultMessage: 'Enable the new Engines section in Enterprise Search.', + }), + name: i18n.translate('xpack.enterpriseSearch.uiSettings.engines.name', { + defaultMessage: 'Enable Engines', + }), + requiresPageReload: false, + schema: schema.boolean(), + value: false, + }, +}; diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 9a48abc68f466..ac3a0e2f1bfc6 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -80,6 +80,16 @@ export const allowedExperimentalValues = Object.freeze({ * Enables the `get-file` endpoint response action */ responseActionGetFileEnabled: false, + + /** + * Keep DEPRECATED experimental flags that are documented to prevent failed upgrades. + * https://www.elastic.co/guide/en/security/current/user-risk-score.html + * https://www.elastic.co/guide/en/security/current/host-risk-score.html + * + * Issue: https://github.com/elastic/kibana/issues/146777 + */ + riskyHostsEnabled: false, // DEPRECATED + riskyUsersEnabled: false, // DEPRECATED }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/plugins/security_solution/public/management/links.test.ts b/x-pack/plugins/security_solution/public/management/links.test.ts index 83e8b43d674a7..aa3f3ba93e9da 100644 --- a/x-pack/plugins/security_solution/public/management/links.test.ts +++ b/x-pack/plugins/security_solution/public/management/links.test.ts @@ -12,10 +12,20 @@ import { SecurityPageName } from '../app/types'; import { calculateEndpointAuthz } from '../../common/endpoint/service/authz'; import type { StartPlugins } from '../types'; -import { links, getManagementFilteredLinks } from './links'; +import { getManagementFilteredLinks, links } from './links'; import { allowedExperimentalValues } from '../../common/experimental_features'; import { ExperimentalFeaturesService } from '../common/experimental_features_service'; import { getEndpointAuthzInitialStateMock } from '../../common/endpoint/service/authz/mocks'; +import { licenseService as _licenseService } from '../common/hooks/use_license'; +import type { LicenseService } from '../../common/license'; +import { createLicenseServiceMock } from '../../common/license/mocks'; +import type { FleetAuthz } from '@kbn/fleet-plugin/common'; +import { createFleetAuthzMock } from '@kbn/fleet-plugin/common'; +import type { DeepPartial } from '@kbn/utility-types'; +import { merge } from 'lodash'; +import { ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants'; + +jest.mock('../common/hooks/use_license'); jest.mock('../../common/endpoint/service/authz', () => { const originalModule = jest.requireActual('../../common/endpoint/service/authz'); @@ -27,9 +37,10 @@ jest.mock('../../common/endpoint/service/authz', () => { jest.mock('../common/lib/kibana'); +const licenseServiceMock = _licenseService as jest.Mocked; + describe('links', () => { let coreMockStarted: ReturnType; - let getPlugins: (roles: string[]) => StartPlugins; let fakeHttpServices: jest.Mocked; const getLinksWithout = (...excludedLinks: SecurityPageName[]) => ({ @@ -37,6 +48,22 @@ describe('links', () => { links: links.links?.filter((link) => !excludedLinks.includes(link.id)), }); + const getPlugins = ( + roles: string[], + fleetAuthzOverrides: DeepPartial = {} + ): StartPlugins => { + return { + security: { + authc: { + getCurrentUser: jest.fn().mockReturnValue({ roles }), + }, + }, + fleet: { + authz: merge(createFleetAuthzMock(), fleetAuthzOverrides), + }, + } as unknown as StartPlugins; + }; + beforeAll(() => { ExperimentalFeaturesService.init({ experimentalFeatures: { ...allowedExperimentalValues }, @@ -46,22 +73,11 @@ describe('links', () => { beforeEach(() => { coreMockStarted = coreMock.createStart(); fakeHttpServices = coreMockStarted.http as jest.Mocked; + }); + + afterEach(() => { fakeHttpServices.get.mockClear(); - getPlugins = (roles) => - ({ - security: { - authc: { - getCurrentUser: jest.fn().mockReturnValue({ roles }), - }, - }, - fleet: { - authz: { - fleet: { - all: true, - }, - }, - }, - } as unknown as StartPlugins); + Object.assign(licenseServiceMock, createLicenseServiceMock()); }); it('should return all links for user with all sub-feature privileges', async () => { @@ -88,146 +104,173 @@ describe('links', () => { }); }); - // todo: these tests should be updated, because in the end, showing/hiding HIE depends on nothing - // else but the mock return of `calculateEndpointAuthz`. - // These tests should check what is the value of `hasHostIsolationExceptions` which is passed to - // `calculateEndpointAuthz`. describe('Host Isolation Exception', () => { - it('should return all but HIE when NO isolation permission due to privilege', async () => { - (calculateEndpointAuthz as jest.Mock).mockReturnValue({ - canIsolateHost: false, - canUnIsolateHost: false, - canAccessEndpointManagement: true, - canReadActionsLogManagement: true, - canReadEndpointList: true, - canReadTrustedApplications: true, - canReadEventFilters: true, - }); + it('should NOT return HIE if `canReadHostIsolationExceptions` is false', async () => { + (calculateEndpointAuthz as jest.Mock).mockReturnValue( + getEndpointAuthzInitialStateMock({ canReadHostIsolationExceptions: false }) + ); const filteredLinks = await getManagementFilteredLinks( coreMockStarted, getPlugins(['superuser']) ); + expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.hostIsolationExceptions)); }); - it('should return all but HIE when NO isolation permission due to license and NO host isolation exceptions entry', async () => { - (calculateEndpointAuthz as jest.Mock).mockReturnValue({ - canIsolateHost: false, - canUnIsolateHost: true, - canAccessEndpointManagement: true, - canReadActionsLogManagement: true, - canReadEndpointList: true, - canReadTrustedApplications: true, - canReadEventFilters: true, - }); + it('should NOT return HIE if license is lower than Enterprise and NO HIE entries exist', async () => { + (calculateEndpointAuthz as jest.Mock).mockReturnValue( + getEndpointAuthzInitialStateMock({ canReadHostIsolationExceptions: false }) + ); + fakeHttpServices.get.mockResolvedValue({ total: 0 }); + licenseServiceMock.isPlatinumPlus.mockReturnValue(false); + ExperimentalFeaturesService.init({ + experimentalFeatures: { ...allowedExperimentalValues, endpointRbacEnabled: true }, + }); const filteredLinks = await getManagementFilteredLinks( coreMockStarted, - getPlugins(['superuser']) + getPlugins([], { + packagePrivileges: { + endpoint: { + actions: { + readHostIsolationExceptions: { + executePackageAction: true, + }, + }, + }, + }, + }) ); - expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.hostIsolationExceptions)); - }); - it('should return all but HIE when HAS isolation permission AND has HIE entry but not superuser', async () => { - (calculateEndpointAuthz as jest.Mock).mockReturnValue({ - canIsolateHost: false, - canUnIsolateHost: true, - canAccessEndpointManagement: false, - canReadActionsLogManagement: true, - canReadEndpointList: true, - canReadTrustedApplications: true, - canReadEventFilters: true, + expect(fakeHttpServices.get).toHaveBeenCalledWith('/api/exception_lists/items/_find', { + query: expect.objectContaining({ + list_id: [ENDPOINT_ARTIFACT_LISTS.hostIsolationExceptions.id], + }), }); - fakeHttpServices.get.mockResolvedValue({ total: 1 }); - - const filteredLinks = await getManagementFilteredLinks( - coreMockStarted, - getPlugins(['superuser']) + expect(calculateEndpointAuthz as jest.Mock).toHaveBeenLastCalledWith( + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + false ); expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.hostIsolationExceptions)); }); - it('should return all when NO isolation permission due to license but HAS at least one host isolation exceptions entry', async () => { + it('should return HIE if license is lower than Enterprise, but HIE entries exist', async () => { (calculateEndpointAuthz as jest.Mock).mockReturnValue( - getEndpointAuthzInitialStateMock({ - canIsolateHost: false, - }) + getEndpointAuthzInitialStateMock({ canReadHostIsolationExceptions: true }) ); - fakeHttpServices.get.mockResolvedValue({ total: 1 }); + + fakeHttpServices.get.mockResolvedValue({ total: 100 }); + licenseServiceMock.isPlatinumPlus.mockReturnValue(false); + ExperimentalFeaturesService.init({ + experimentalFeatures: { ...allowedExperimentalValues, endpointRbacEnabled: true }, + }); const filteredLinks = await getManagementFilteredLinks( coreMockStarted, - getPlugins(['superuser']) + getPlugins([], { + packagePrivileges: { + endpoint: { + actions: { + readHostIsolationExceptions: { + executePackageAction: true, + }, + }, + }, + }, + }) ); - expect(filteredLinks).toEqual(links); + + expect(fakeHttpServices.get).toHaveBeenCalledWith('/api/exception_lists/items/_find', { + query: expect.objectContaining({ + list_id: [ENDPOINT_ARTIFACT_LISTS.hostIsolationExceptions.id], + }), + }); + expect(calculateEndpointAuthz as jest.Mock).toHaveBeenLastCalledWith( + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + true + ); + expect(filteredLinks).toEqual(getLinksWithout()); }); + }); - it('should not affect showing Action Log if getting from HIE API throws error', async () => { - (calculateEndpointAuthz as jest.Mock).mockReturnValue({ - canIsolateHost: false, - canUnIsolateHost: true, - canReadActionsLogManagement: true, - canReadEndpointList: true, - canReadTrustedApplications: true, - canReadEventFilters: true, + // this can be removed after removing endpointRbacEnabled feature flag + describe('without endpointRbacEnabled', () => { + beforeAll(() => { + ExperimentalFeaturesService.init({ + experimentalFeatures: { ...allowedExperimentalValues, endpointRbacEnabled: false }, }); - fakeHttpServices.get.mockRejectedValue(new Error()); + }); - const filteredLinks = await getManagementFilteredLinks( - coreMockStarted, - getPlugins(['superuser']) - ); - expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.hostIsolationExceptions)); + it('shows Trusted Applications for non-superuser, too', async () => { + (calculateEndpointAuthz as jest.Mock).mockReturnValue(getEndpointAuthzInitialStateMock()); + + const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins([])); + + expect(filteredLinks).toEqual(links); }); + }); - it('should not affect hiding Action Log if getting from HIE API throws error', async () => { - (calculateEndpointAuthz as jest.Mock).mockReturnValue({ - canIsolateHost: false, - canUnIsolateHost: true, - canReadActionsLogManagement: false, - canReadEndpointList: true, - canReadTrustedApplications: true, - canReadEventFilters: true, + // this can be the default after removing endpointRbacEnabled feature flag + describe('with endpointRbacEnabled', () => { + beforeAll(() => { + ExperimentalFeaturesService.init({ + experimentalFeatures: { ...allowedExperimentalValues, endpointRbacEnabled: true }, }); - fakeHttpServices.get.mockRejectedValue(new Error()); + }); - const filteredLinks = await getManagementFilteredLinks( - coreMockStarted, - getPlugins(['superuser']) - ); - expect(filteredLinks).toEqual( - getLinksWithout( - SecurityPageName.hostIsolationExceptions, - SecurityPageName.responseActionsHistory - ) + it('should hide Trusted Applications for user without privilege', async () => { + (calculateEndpointAuthz as jest.Mock).mockReturnValue( + getEndpointAuthzInitialStateMock({ + canReadTrustedApplications: false, + }) ); + + const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins([])); + + expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.trustedApps)); }); - }); - it('should hide Trusted Applications for user without privilege', async () => { - (calculateEndpointAuthz as jest.Mock).mockReturnValue( - getEndpointAuthzInitialStateMock({ - canReadTrustedApplications: false, - }) - ); + it('should show Trusted Applications for user with privilege', async () => { + (calculateEndpointAuthz as jest.Mock).mockReturnValue(getEndpointAuthzInitialStateMock()); - const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins([])); + const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins([])); - expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.trustedApps)); - }); + expect(filteredLinks).toEqual(links); + }); + + it('should hide Event Filters for user without privilege', async () => { + (calculateEndpointAuthz as jest.Mock).mockReturnValue( + getEndpointAuthzInitialStateMock({ + canReadEventFilters: false, + }) + ); - it('should hide Event Filters for user without privilege', async () => { - (calculateEndpointAuthz as jest.Mock).mockReturnValue( - getEndpointAuthzInitialStateMock({ - canReadEventFilters: false, - }) - ); + const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins([])); - const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins([])); + expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.eventFilters)); + }); + + it('should NOT return policies if `canReadPolicyManagement` is `false`', async () => { + (calculateEndpointAuthz as jest.Mock).mockReturnValue( + getEndpointAuthzInitialStateMock({ + canReadPolicyManagement: false, + }) + ); - expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.eventFilters)); + const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins([])); + + expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.policies)); + }); }); describe('Endpoint List', () => { diff --git a/x-pack/plugins/security_solution/public/management/links.ts b/x-pack/plugins/security_solution/public/management/links.ts index fde26e39aead4..7004edb28fe47 100644 --- a/x-pack/plugins/security_solution/public/management/links.ts +++ b/x-pack/plugins/security_solution/public/management/links.ts @@ -61,7 +61,6 @@ import { IconSiemRules } from './icons/siem_rules'; import { IconTrustedApplications } from './icons/trusted_applications'; import { HostIsolationExceptionsApiClient } from './pages/host_isolation_exceptions/host_isolation_exceptions_api_client'; import { ExperimentalFeaturesService } from '../common/experimental_features_service'; -import { KibanaServices } from '../common/lib/kibana'; const categories = [ { @@ -269,7 +268,7 @@ export const getManagementFilteredLinks = async ( ) ) { hasHostIsolationExceptions = await checkArtifactHasData( - HostIsolationExceptionsApiClient.getInstance(KibanaServices.get().http) + HostIsolationExceptionsApiClient.getInstance(core.http) ); } @@ -279,6 +278,7 @@ export const getManagementFilteredLinks = async ( canReadEndpointList, canReadTrustedApplications, canReadEventFilters, + canReadPolicyManagement, } = fleetAuthz ? calculateEndpointAuthz( licenseService, @@ -294,6 +294,10 @@ export const getManagementFilteredLinks = async ( linksToExclude.push(SecurityPageName.endpoints); } + if (!canReadPolicyManagement) { + linksToExclude.push(SecurityPageName.policies); + } + if (!canReadActionsLogManagement) { linksToExclude.push(SecurityPageName.responseActionsHistory); } diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index ce53039039b3f..908f66fb3802c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -135,6 +135,7 @@ export const EndpointList = () => { const { canReadEndpointList, canAccessFleet, + canReadPolicyManagement, loading: endpointPrivilegesLoading, } = useUserPrivileges().endpointPrivileges; const { search } = useFormatUrl(SecurityPageName.administration); @@ -389,14 +390,18 @@ export const EndpointList = () => { return ( <> - - {policy.name} - + {canReadPolicyManagement ? ( + + {policy.name} + + ) : ( + <>{policy.name} + )} {policy.endpoint_policy_version && ( { ], }, ]; - }, [queryParams, search, getAppUrl, backToEndpointList, PAD_LEFT]); + }, [queryParams, search, getAppUrl, canReadPolicyManagement, backToEndpointList, PAD_LEFT]); const renderTableOrEmptyState = useMemo(() => { if (endpointsExist || areEndpointsEnrolling) { 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 c02969993e62d..76059dae08bc4 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,6 +11,7 @@ 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 { ConfigForm } from '../config_form'; @@ -41,6 +42,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 handleSwitchChange = useCallback( (event) => @@ -68,6 +70,7 @@ export const AntivirusRegistrationForm = memo(() => { label={TRANSLATIONS.label} checked={antivirusRegistrationEnabled} onChange={handleSwitchChange} + disabled={!canWritePolicyManagement} /> ); 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 525be79b7f785..e1c01c552468b 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,6 +11,7 @@ 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 { ConfigForm } from '../config_form'; @@ -33,6 +34,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 handleSwitchChange = useCallback( (event) => @@ -51,6 +53,7 @@ export const AttackSurfaceReductionForm = memo(() => { label={TRANSLATIONS.label} checked={credentialHardeningEnabled} onChange={handleSwitchChange} + disabled={!canWritePolicyManagement} /> ); 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 7ae421c5a253d..f497b59394b10 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,6 +19,7 @@ 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, @@ -75,6 +76,7 @@ const InnerEventsForm = ({ onValueSelection, supplementalOptions, }: EventsFormProps) => { + const { canWritePolicyManagement } = useUserPrivileges().endpointPrivileges; const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); const theme = useContext(ThemeContext); const countSelected = useCallback(() => { @@ -122,6 +124,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} /> ); })} @@ -165,7 +168,10 @@ 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={isDisabled ? isDisabled(policyDetailsConfig) : false} + disabled={ + !canWritePolicyManagement || + (isDisabled ? isDisabled(policyDetailsConfig) : false) + } /> {tooltipText && ( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/policy_endpoint_count.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/policy_endpoint_count.tsx index 7c0e3d470ec10..d5ba7d111d571 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/policy_endpoint_count.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/policy_endpoint_count.tsx @@ -16,10 +16,11 @@ import { getEndpointListPath, getPoliciesPath } from '../../../../common/routing import { APP_UI_ID } from '../../../../../../common/constants'; /** + * Returns a link component that navigates to the endpoint list page filtered by a specific policy + * * @param policyId * @param nonLinkCondition: boolean where the returned component is just text and not a link * - * Returns a link component that navigates to the endpoint list page filtered by a specific policy */ export const PolicyEndpointCount = memo< Omit & { 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 e6eab89e4f18f..44618a509d58b 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,6 +21,7 @@ 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 { AdvancedPolicySchema } from '../models/advanced_policy_schema'; @@ -145,6 +146,7 @@ const PolicyAdvanced = React.memo( lastSupportedVersion?: string; documentation: string; }) => { + const { canWritePolicyManagement } = useUserPrivileges().endpointPrivileges; const dispatch = useDispatch(); const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); const onChange = useCallback( @@ -196,6 +198,7 @@ const PolicyAdvanced = React.memo( fullWidth value={value as string} onChange={onChange} + disabled={!canWritePolicyManagement} /> 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 3a359b81f9bec..42b4328d8e794 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 @@ -16,18 +16,23 @@ import { createAppRootMockRenderer, resetReactDomCreatePortalMock, } from '../../../../../../common/mock/endpoint'; -import { getPolicyDetailPath, getEndpointListPath } from '../../../../../common/routing'; +import { getPolicyDetailPath, getPoliciesPath } from '../../../../../common/routing'; import { policyListApiPathHandlers } from '../../../store/test_mock_utils'; import { licenseService } from '../../../../../../common/hooks/use_license'; import { PACKAGE_POLICY_API_ROOT, AGENT_API_ROUTES } from '@kbn/fleet-plugin/common'; +import { useUserPrivileges as _useUserPrivileges } from '../../../../../../common/components/user_privileges'; +import { getUserPrivilegesMockDefaultValue } from '../../../../../../common/components/user_privileges/__mocks__'; jest.mock('../../../../../../common/hooks/use_license'); +jest.mock('../../../../../../common/components/user_privileges'); + +const useUserPrivilegesMock = _useUserPrivileges as jest.Mock; describe('Policy Form Layout', () => { type FindReactWrapperResponse = ReturnType['find']>; const policyDetailsPathUrl = getPolicyDetailPath('1'); - const endpointListPath = getEndpointListPath({ name: 'endpointList' }); + const policyListPath = getPoliciesPath(); const sleep = (ms = 100) => new Promise((wakeup) => setTimeout(wakeup, ms)); const generator = new EndpointDocGenerator(); let history: AppContextTestRender['history']; @@ -123,7 +128,7 @@ describe('Policy Form Layout', () => { const navigateToAppMockedCalls = coreStart.application.navigateToApp.mock.calls; expect(navigateToAppMockedCalls[navigateToAppMockedCalls.length - 1]).toEqual([ 'securitySolutionUI', - { path: endpointListPath }, + { path: policyListPath }, ]); }); it('should display save button', async () => { @@ -142,6 +147,7 @@ describe('Policy Form Layout', () => { expect(saveButton).toHaveLength(1); expect(saveButton.text()).toEqual('beta'); }); + describe('when the save button is clicked', () => { let saveButton: FindReactWrapperResponse; let confirmModal: FindReactWrapperResponse; @@ -237,6 +243,7 @@ describe('Policy Form Layout', () => { }); }); }); + describe('when the subscription tier is platinum or higher', () => { beforeEach(() => { (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(true); @@ -288,6 +295,7 @@ describe('Policy Form Layout', () => { expect(ransomware).toHaveLength(1); }); }); + describe('when the subscription tier is gold or lower', () => { beforeEach(() => { (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false); @@ -330,5 +338,40 @@ describe('Policy Form Layout', () => { expect(lockedCard).toHaveLength(4); }); }); + + describe('and user has only READ privilege', () => { + beforeEach(() => { + const mockedPrivileges = getUserPrivilegesMockDefaultValue(); + mockedPrivileges.endpointPrivileges.canWritePolicyManagement = false; + + useUserPrivilegesMock.mockReturnValue(mockedPrivileges); + + policyFormLayoutView = render(); + }); + + afterEach(() => { + useUserPrivilegesMock.mockImplementation(getUserPrivilegesMockDefaultValue); + }); + + it('should not display the Save button', () => { + expect( + policyFormLayoutView.find('EuiButton[data-test-subj="policyDetailsSaveButton"]') + ).toHaveLength(0); + }); + + it('should display all form controls as disabled', () => { + policyFormLayoutView + .find('button[data-test-subj="advancedPolicyButton"]') + .simulate('click'); + + const inputElements = policyFormLayoutView.find('input'); + + expect(inputElements.length).toBeGreaterThan(0); + + inputElements.forEach((element) => { + expect(element.prop('disabled')).toBe(true); + }); + }); + }); }); }); 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 e2a77dd5caa2e..ad9246b01a699 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 @@ -22,6 +22,8 @@ import { useDispatch } from 'react-redux'; 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 { policyDetails, @@ -32,7 +34,7 @@ import { import { useToasts, useKibana } from '../../../../../../common/lib/kibana'; import type { AppAction } from '../../../../../../common/store/actions'; -import { getEndpointListPath } from '../../../../../common/routing'; +import { getEndpointListPath, getPoliciesPath } from '../../../../../common/routing'; import { useNavigateToAppEventHandler } from '../../../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; import { APP_UI_ID } from '../../../../../../../common/constants'; import type { PolicyDetailsRouteState } from '../../../../../../../common/endpoint/types'; @@ -50,6 +52,7 @@ export const PolicyFormLayout = React.memo(() => { } = useKibana(); const toasts = useToasts(); const { state: locationRouteState } = useLocation(); + const { canWritePolicyManagement } = useUserPrivileges().endpointPrivileges; // Store values const policyItem = usePolicyDetailsSelector(policyDetails); @@ -61,12 +64,23 @@ export const PolicyFormLayout = React.memo(() => { const [showConfirm, setShowConfirm] = useState(false); const [routeState, setRouteState] = useState(); const policyName = policyItem?.name ?? ''; - const hostListRouterPath = getEndpointListPath({ name: 'endpointList' }); + const isPolicyListEnabled = useIsExperimentalFeatureEnabled('policyListEnabled'); const routingOnCancelNavigateTo = routeState?.onCancelNavigateTo; const navigateToAppArguments = useMemo((): Parameters => { - return routingOnCancelNavigateTo ?? [APP_UI_ID, { path: hostListRouterPath }]; - }, [hostListRouterPath, routingOnCancelNavigateTo]); + if (routingOnCancelNavigateTo) { + return routingOnCancelNavigateTo; + } + + return [ + APP_UI_ID, + { + path: isPolicyListEnabled + ? getPoliciesPath() + : getEndpointListPath({ name: 'endpointList' }), + }, + ]; + }, [isPolicyListEnabled, routingOnCancelNavigateTo]); // Handle showing update statuses useEffect(() => { @@ -167,20 +181,22 @@ export const PolicyFormLayout = React.memo(() => { - - - - - + {canWritePolicyManagement && ( + + + + + + )} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/protection_radio.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/protection_radio.tsx index b875f755b258e..8a1ff4204e23a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/protection_radio.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/protection_radio.tsx @@ -9,6 +9,7 @@ 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'; @@ -34,6 +35,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 handleRadioChange = useCallback(() => { if (policyDetailsConfig) { @@ -87,7 +89,7 @@ export const ProtectionRadio = React.memo( id={radioButtonId} checked={selected === protectionMode} onChange={handleRadioChange} - disabled={selected === ProtectionModes.off} + disabled={!canWritePolicyManagement || 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 75d79439a14fc..a564c2b2f83cb 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,6 +10,7 @@ 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'; @@ -40,6 +41,7 @@ export const ProtectionSwitch = React.memo( }) => { const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); const isPlatinumPlus = useLicense().isPlatinumPlus(); + const { canWritePolicyManagement } = useUserPrivileges().endpointPrivileges; const dispatch = useDispatch<(action: AppAction) => void>(); const selected = policyDetailsConfig && policyDetailsConfig.windows[protection].mode; @@ -123,6 +125,7 @@ export const ProtectionSwitch = React.memo( })} checked={selected !== ProtectionModes.off} onChange={handleSwitchChange} + disabled={!canWritePolicyManagement} /> ); } 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 2a9dd8bffb6d9..f08ec74c95b5a 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,6 +19,7 @@ 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'; @@ -36,6 +37,7 @@ export const UserNotification = React.memo( protection: PolicyProtection; osList: ImmutableArray>; }) => { + const { canWritePolicyManagement } = useUserPrivileges().endpointPrivileges; const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); const dispatch = useDispatch<(action: AppAction) => void>(); const selected = policyDetailsConfig && policyDetailsConfig.windows[protection].mode; @@ -139,7 +141,7 @@ export const UserNotification = React.memo( id={`${protection}UserNotificationCheckbox}`} onChange={handleUserNotificationCheckbox} checked={userNotificationSelected} - disabled={selected === ProtectionModes.off} + disabled={!canWritePolicyManagement || selected === ProtectionModes.off} label={i18n.translate('xpack.securitySolution.endpoint.policyDetail.notifyUser', { defaultMessage: 'Notify user', })} @@ -196,6 +198,7 @@ export const UserNotification = React.memo( value={userNotificationMessage} onChange={handleCustomUserNotification} fullWidth={true} + disabled={!canWritePolicyManagement} 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 00d4ddf2197c6..e6b69f21d26f3 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,6 +19,7 @@ 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 { @@ -59,6 +60,7 @@ export const MalwareProtections = React.memo(() => { defaultMessage: 'Blocklist enabled', } ); + const { canWritePolicyManagement } = useUserPrivileges().endpointPrivileges; const isPlatinumPlus = useLicense().isPlatinumPlus(); const dispatch = useDispatch<(action: AppAction) => void>(); const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); @@ -117,7 +119,9 @@ export const MalwareProtections = React.memo(() => { label={blocklistLabel} checked={policyDetailsConfig.windows[protection].blocklist} onChange={handleBlocklistSwitchChange} - disabled={policyDetailsConfig.windows[protection].mode === 'off'} + disabled={ + !canWritePolicyManagement || policyDetailsConfig.windows[protection].mode === 'off' + } /> diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx index 33fd25d0d15cf..ff2c194d435b4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx @@ -21,6 +21,7 @@ import { i18n } from '@kbn/i18n'; import { useLocation } from 'react-router-dom'; import type { CreatePackagePolicyRouteState } from '@kbn/fleet-plugin/public'; import { pagePathGetters } from '@kbn/fleet-plugin/public'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; import { AdministrationListPage } from '../../../components/administration_list_page'; import { FormattedDate } from '../../../../common/components/formatted_date'; import { EndpointPolicyLink } from '../../../components/endpoint_policy_link'; @@ -40,6 +41,7 @@ import { PolicyEndpointCount } from './components/policy_endpoint_count'; import { ManagementEmptyStateWrapper } from '../../../components/management_empty_state_wrapper'; export const PolicyList = memo(() => { + const { canReadEndpointList, loading: authLoading } = useUserPrivileges().endpointPrivileges; const { pagination, pageSizeOptions, setPagination } = useUrlPagination(); const { search } = useLocation(); const { getAppUrl } = useAppUrl(); @@ -266,7 +268,7 @@ export const PolicyList = memo(() => { className="eui-textTruncate" data-test-subj="policyEndpointCountLink" policyId={policy.id} - nonLinkCondition={count === 0} + nonLinkCondition={authLoading || !canReadEndpointList || count === 0} > {count} @@ -274,7 +276,7 @@ export const PolicyList = memo(() => { }, }, ]; - }, [policyIdToEndpointCount, backLink]); + }, [backLink, policyIdToEndpointCount, authLoading, canReadEndpointList]); const handleTableOnChange = useCallback( ({ page }: CriteriaWithPagination) => {