From 54c6144bbc9e041dad5c2fd9f296ea7d838cc614 Mon Sep 17 00:00:00 2001 From: Pablo Machado Date: Thu, 14 Nov 2024 09:40:27 +0100 Subject: [PATCH] [Securitysolution] Add Risk score missing privileges callout to enablement flyout (#199804) ## Summary The entity analytics enablement model doesn't show the missing privileges warning for the Entity Risk score. To fix it, I added to the model the same callout we display on the risk engine page. ### What is not included * Improvement to the risk engine callout * Fix risk engine callout bugs https://github.com/elastic/security-team/issues/11138 ### How to test it * Create a user with no privileges except to the security solution app and the `logs*` index * Login and open the entity analytics page with the non-privileged user * Click enable and check if the model displays the missing privileges callout for the risk engine ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../components/enablement_modal.test.tsx | 175 +++++++++++++----- .../components/enablement_modal.tsx | 24 ++- 2 files changed, 149 insertions(+), 50 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(); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/enablement_modal.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/enablement_modal.tsx index 73d65176d116b..4252f71ec4baa 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/enablement_modal.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/enablement_modal.tsx @@ -34,6 +34,8 @@ import { } from '../translations'; import { useEntityEnginePrivileges } from '../hooks/use_entity_engine_privileges'; import { MissingPrivilegesCallout } from './missing_privileges_callout'; +import { useMissingRiskEnginePrivileges } from '../../../hooks/use_missing_risk_engine_privileges'; +import { RiskEnginePrivilegesCallOut } from '../../risk_engine_privileges_callout'; export interface Enablements { riskScore: boolean; @@ -66,7 +68,9 @@ export const EntityStoreEnablementModal: React.FC } 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 })) @@ -136,9 +148,9 @@ export const EntityStoreEnablementModal: React.FC - {!privileges || privileges.has_all_required ? null : ( + {!entityEnginePrivileges || entityEnginePrivileges.has_all_required ? null : ( - + )}