From 92b97196d2a1c95c53b9fae32856d8a653c7bfb8 Mon Sep 17 00:00:00 2001 From: machadoum Date: Tue, 12 Nov 2024 13:55:52 +0100 Subject: [PATCH 1/3] Add Risk score missing privileges callout to enablement flyout --- .../components/dashboard_panels.tsx | 58 +++++++++---------- .../components/enablement_modal.tsx | 24 ++++++-- 2 files changed, 46 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_panels.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_panels.tsx index d70eb9fe34b51..cdc947512aee0 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_panels.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_panels.tsx @@ -200,37 +200,35 @@ const EntityStoreDashboardPanelsComponent = () => { )} - {(entityStore.status === 'not_installed' || entityStore.status === 'stopped') && - !isRiskScoreAvailable && ( - // TODO: Move modal inside EnableEntityStore component, eliminating the onEnable prop in favour of forwarding the riskScoreEnabled status - setModalState({ visible: true })} - loadingRiskEngine={riskEngineInitializing} - /> - )} + {entityStore.status === 'not_installed' && !isRiskScoreAvailable && ( + // TODO: Move modal inside EnableEntityStore component, eliminating the onEnable prop in favour of forwarding the riskScoreEnabled status + setModalState({ visible: true })} + loadingRiskEngine={riskEngineInitializing} + /> + )} - {(entityStore.status === 'not_installed' || entityStore.status === 'stopped') && - isRiskScoreAvailable && ( - <> - - - setModalState({ - visible: true, - }) - } - /> - - - - - - - - - )} + {entityStore.status === 'not_installed' && isRiskScoreAvailable && ( + <> + + + setModalState({ + visible: true, + }) + } + /> + + + + + + + + + )} } checked={enablements.riskScore} - disabled={riskScore.disabled || false} + disabled={ + riskScore.disabled || + (!riskEnginePrivileges.isLoading && !riskEnginePrivileges?.hasAllRequiredPrivileges) + } onChange={() => setEnablements((prev) => ({ ...prev, riskScore: !prev.riskScore }))} /> + {!riskEnginePrivileges.isLoading && !riskEnginePrivileges.hasAllRequiredPrivileges && ( + + + + )} {ENABLEMENT_DESCRIPTION_RISK_ENGINE_ONLY} - setEnablements((prev) => ({ ...prev, entityStore: !prev.entityStore })) @@ -119,9 +131,9 @@ export const EntityStoreEnablementModal: React.FC - {!privileges || privileges.has_all_required ? null : ( + {!entityEnginePrivileges || entityEnginePrivileges.has_all_required ? null : ( - + )} From b6c2437deeefa7c5b59e0eaab917e64b282bc61c Mon Sep 17 00:00:00 2001 From: machadoum Date: Wed, 13 Nov 2024 10:26:15 +0100 Subject: [PATCH 2/3] Undo merge conflict --- .../components/dashboard_panels.tsx | 58 ++++++++++--------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_panels.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_panels.tsx index cdc947512aee0..d70eb9fe34b51 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_panels.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_panels.tsx @@ -200,35 +200,37 @@ const EntityStoreDashboardPanelsComponent = () => { )} - {entityStore.status === 'not_installed' && !isRiskScoreAvailable && ( - // TODO: Move modal inside EnableEntityStore component, eliminating the onEnable prop in favour of forwarding the riskScoreEnabled status - setModalState({ visible: true })} - loadingRiskEngine={riskEngineInitializing} - /> - )} + {(entityStore.status === 'not_installed' || entityStore.status === 'stopped') && + !isRiskScoreAvailable && ( + // TODO: Move modal inside EnableEntityStore component, eliminating the onEnable prop in favour of forwarding the riskScoreEnabled status + setModalState({ visible: true })} + loadingRiskEngine={riskEngineInitializing} + /> + )} - {entityStore.status === 'not_installed' && isRiskScoreAvailable && ( - <> - - - setModalState({ - visible: true, - }) - } - /> - - - - - - - - - )} + {(entityStore.status === 'not_installed' || entityStore.status === 'stopped') && + isRiskScoreAvailable && ( + <> + + + setModalState({ + visible: true, + }) + } + /> + + + + + + + + + )} Date: Wed, 13 Nov 2024 14:25:31 +0100 Subject: [PATCH 3/3] Add unit test --- .../components/enablement_modal.test.tsx | 175 +++++++++++++----- 1 file changed, 131 insertions(+), 44 deletions(-) diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/enablement_modal.test.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/enablement_modal.test.tsx index 360e28ec41518..cccccb175ff19 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/enablement_modal.test.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/enablement_modal.test.tsx @@ -8,13 +8,21 @@ import React from 'react'; import { fireEvent, render, screen } from '@testing-library/react'; import { EntityStoreEnablementModal } from './enablement_modal'; -import { useEntityEnginePrivileges } from '../hooks/use_entity_engine_privileges'; import { TestProviders } from '../../../../common/mock'; +import type { EntityAnalyticsPrivileges } from '../../../../../common/api/entity_analytics'; +import type { RiskEngineMissingPrivilegesResponse } from '../../../hooks/use_missing_risk_engine_privileges'; const mockToggle = jest.fn(); const mockEnableStore = jest.fn(() => jest.fn()); + +const mockUseEntityEnginePrivileges = jest.fn(); jest.mock('../hooks/use_entity_engine_privileges', () => ({ - useEntityEnginePrivileges: jest.fn(), + useEntityEnginePrivileges: () => mockUseEntityEnginePrivileges(), +})); + +const mockUseMissingRiskEnginePrivileges = jest.fn(); +jest.mock('../../../hooks/use_missing_risk_engine_privileges', () => ({ + useMissingRiskEnginePrivileges: () => mockUseMissingRiskEnginePrivileges(), })); const defaultProps = { @@ -25,6 +33,50 @@ const defaultProps = { entityStore: { disabled: false, checked: false }, }; +const allEntityEnginePrivileges: EntityAnalyticsPrivileges = { + has_all_required: true, + privileges: { + elasticsearch: { + cluster: { + manage_enrich: true, + }, + index: { 'logs-*': { read: false, view_index_metadata: true } }, + }, + kibana: { + 'saved_object:entity-engine-status/all': true, + }, + }, +}; + +const missingEntityEnginePrivileges: EntityAnalyticsPrivileges = { + has_all_required: false, + privileges: { + elasticsearch: { + cluster: { + manage_enrich: false, + }, + index: { 'logs-*': { read: false, view_index_metadata: false } }, + }, + kibana: { + 'saved_object:entity-engine-status/all': false, + }, + }, +}; + +const allRiskEnginePrivileges: RiskEngineMissingPrivilegesResponse = { + hasAllRequiredPrivileges: true, + isLoading: false, +}; + +const missingRiskEnginePrivileges: RiskEngineMissingPrivilegesResponse = { + isLoading: false, + hasAllRequiredPrivileges: false, + missingPrivileges: { + clusterPrivileges: [], + indexPrivileges: [], + }, +}; + const renderComponent = (props = defaultProps) => { return render(, { wrapper: TestProviders }); }; @@ -32,58 +84,93 @@ const renderComponent = (props = defaultProps) => { describe('EntityStoreEnablementModal', () => { beforeEach(() => { jest.clearAllMocks(); - (useEntityEnginePrivileges as jest.Mock).mockReturnValue({ - data: { - privileges: { - elasticsearch: { - index: {}, - }, - kibana: {}, - }, - }, - isLoading: false, - }); }); - it('should render the modal when visible is true', () => { - renderComponent(); - expect(screen.getByRole('dialog')).toBeInTheDocument(); - }); + describe('with all privileges', () => { + beforeEach(() => { + mockUseEntityEnginePrivileges.mockReturnValue({ + data: allEntityEnginePrivileges, + isLoading: false, + }); - it('should not render the modal when visible is false', () => { - renderComponent({ ...defaultProps, visible: false }); - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); - }); + mockUseMissingRiskEnginePrivileges.mockReturnValue(allRiskEnginePrivileges); + }); - it('should call toggle function when cancel button is clicked', () => { - renderComponent(); - fireEvent.click(screen.getByText('Cancel')); - expect(mockToggle).toHaveBeenCalledWith(false); - }); + it('should render the modal when visible is true', () => { + renderComponent(); + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }); - it('should call enableStore function when enable button is clicked', () => { - renderComponent({ - ...defaultProps, - riskScore: { ...defaultProps.riskScore, checked: true }, - entityStore: { ...defaultProps.entityStore, checked: true }, + it('should not render the modal when visible is false', () => { + renderComponent({ ...defaultProps, visible: false }); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); }); - fireEvent.click(screen.getByText('Enable')); - expect(mockEnableStore).toHaveBeenCalledWith({ riskScore: true, entityStore: true }); - }); - it('should display proceed warning when no enablement options are selected', () => { - renderComponent(); - expect(screen.getByText('Please enable at least one option to proceed.')).toBeInTheDocument(); + it('should call toggle function when cancel button is clicked', () => { + renderComponent(); + fireEvent.click(screen.getByText('Cancel')); + expect(mockToggle).toHaveBeenCalledWith(false); + }); + + it('should call enableStore function when enable button is clicked', () => { + renderComponent({ + ...defaultProps, + riskScore: { ...defaultProps.riskScore, checked: true }, + entityStore: { ...defaultProps.entityStore, checked: true }, + }); + fireEvent.click(screen.getByText('Enable')); + expect(mockEnableStore).toHaveBeenCalledWith({ riskScore: true, entityStore: true }); + }); + + it('should display proceed warning when no enablement options are selected', () => { + renderComponent(); + expect(screen.getByText('Please enable at least one option to proceed.')).toBeInTheDocument(); + }); + + it('should disable the enable button when enablementOptions are false', () => { + renderComponent({ + ...defaultProps, + riskScore: { ...defaultProps.riskScore, checked: false }, + entityStore: { ...defaultProps.entityStore, checked: false }, + }); + + const enableButton = screen.getByRole('button', { name: /Enable/i }); + expect(enableButton).toBeDisabled(); + }); + + it('should not show entity engine missing privileges warning when no missing privileges', () => { + renderComponent(); + expect( + screen.queryByTestId('callout-missing-entity-store-privileges') + ).not.toBeInTheDocument(); + }); + + it('should not show risk engine missing privileges warning when no missing privileges', () => { + renderComponent(); + expect( + screen.queryByTestId('callout-missing-risk-engine-privileges') + ).not.toBeInTheDocument(); + }); }); - it('should disable the enable button when enablementOptions are false', () => { - renderComponent({ - ...defaultProps, - riskScore: { ...defaultProps.riskScore, checked: false }, - entityStore: { ...defaultProps.entityStore, checked: false }, + describe('with no privileges', () => { + beforeEach(() => { + mockUseEntityEnginePrivileges.mockReturnValue({ + data: missingEntityEnginePrivileges, + isLoading: false, + }); + + mockUseMissingRiskEnginePrivileges.mockReturnValue(missingRiskEnginePrivileges); + }); + + it('should show entity engine missing privileges warning when missing privileges', () => { + renderComponent(); + expect(screen.getByTestId('callout-missing-entity-store-privileges')).toBeInTheDocument(); }); - const enableButton = screen.getByRole('button', { name: /Enable/i }); - expect(enableButton).toBeDisabled(); + it('should show risk engine missing privileges warning when missing privileges', () => { + renderComponent(); + expect(screen.getByTestId('callout-missing-risk-engine-privileges')).toBeInTheDocument(); + }); }); });