diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_analytics_risk_score/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_analytics_risk_score/index.tsx index 225aee1bfb34d..15b1e046ff54a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_analytics_risk_score/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_analytics_risk_score/index.tsx @@ -172,7 +172,7 @@ const EntityAnalyticsRiskScoresComponent = ({ setUpdatedAt(Date.now()); }, [isTableLoading, isKpiLoading]); // Update the time when data loads - const privileges = useMissingRiskEnginePrivileges(['read']); + const privileges = useMissingRiskEnginePrivileges({ readonly: true }); if (!isAuthorized) { return null; diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_enablement_panel.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_enablement_panel.tsx index 3b522b95b06d0..08981b76d075a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_enablement_panel.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_enablement_panel.tsx @@ -187,7 +187,7 @@ export const EnablementPanel: React.FC = ({ state } if ( riskEngineStatus !== RiskEngineStatusEnum.NOT_INSTALLED && - (entityStoreStatus === 'running' || entityStoreStatus === 'stopped') + entityStoreStatus !== 'not_installed' ) { return null; } @@ -224,14 +224,8 @@ export const EnablementPanel: React.FC = ({ state } visible={modal.visible} toggle={(visible) => setModalState({ visible })} enableStore={enableEntityStore} - riskScore={{ - canToggle: riskEngineStatus === RiskEngineStatusEnum.NOT_INSTALLED, - checked: riskEngineStatus === RiskEngineStatusEnum.NOT_INSTALLED, - }} - entityStore={{ - canToggle: entityStoreStatus !== 'running', - checked: entityStoreStatus === 'not_installed', - }} + riskEngineStatus={riskEngineStatus} + entityStoreStatus={entityStoreStatus} /> ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/enablement_modal.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/enablement_modal.tsx index 9347273ce5590..74b30d4263f60 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/enablement_modal.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/enablement_modal.tsx @@ -22,18 +22,20 @@ import { useEuiTheme, } from '@elastic/eui'; import { css } from '@emotion/react'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; +import type { RiskEngineStatus, StoreStatus } from '../../../../../common/api/entity_analytics'; +import { RiskEngineStatusEnum } from '../../../../../common/api/entity_analytics'; import { useContractComponents } from '../../../../common/hooks/use_contract_component'; import { ENABLEMENT_DESCRIPTION_RISK_ENGINE_ONLY, ENABLEMENT_DESCRIPTION_ENTITY_STORE_ONLY, ENABLEMENT_WARNING_SELECT_TO_PROCEED, } 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'; +import { useEntityEnginePrivileges } from '../hooks/use_entity_engine_privileges'; export interface Enablements { riskScore: boolean; @@ -44,51 +46,70 @@ interface EntityStoreEnablementModalProps { visible: boolean; toggle: (visible: boolean) => void; enableStore: (enablements: Enablements) => () => void; - riskScore: { - canToggle?: boolean; - checked?: boolean; - }; - entityStore: { - canToggle?: boolean; - checked?: boolean; - }; + riskEngineStatus?: RiskEngineStatus; + entityStoreStatus?: StoreStatus; } -const shouldAllowEnablement = ( - riskScoreEnabled: boolean, - entityStoreEnabled: boolean, +const isInstallButtonEnabled = ( + canInstallRiskScore: boolean, + canInstallEntityStore: boolean, userHasEnabled: Enablements ) => { - if (riskScoreEnabled) { - return userHasEnabled.entityStore; - } - if (entityStoreEnabled) { - return userHasEnabled.riskScore; + if (canInstallRiskScore || canInstallEntityStore) { + return userHasEnabled.riskScore || userHasEnabled.entityStore; } - return userHasEnabled.riskScore || userHasEnabled.entityStore; + + return false; }; export const EntityStoreEnablementModal: React.FC = ({ visible, toggle, enableStore, - riskScore, - entityStore, + riskEngineStatus, + entityStoreStatus, }) => { - const { euiTheme } = useEuiTheme(); - const [enablements, setEnablements] = useState({ - riskScore: !!riskScore.checked, - entityStore: !!entityStore.checked, - }); + const riskEnginePrivileges = useMissingRiskEnginePrivileges(); const { data: entityEnginePrivileges, isLoading: isLoadingEntityEnginePrivileges } = useEntityEnginePrivileges(); - const riskEnginePrivileges = useMissingRiskEnginePrivileges(); - const enablementOptions = shouldAllowEnablement( - !riskScore.canToggle, - !entityStore.canToggle, - enablements + const hasRiskScorePrivileges = !( + riskEnginePrivileges.isLoading || !riskEnginePrivileges?.hasAllRequiredPrivileges + ); + + const canInstallRiskScore = + hasRiskScorePrivileges && riskEngineStatus === RiskEngineStatusEnum.NOT_INSTALLED; + + const hasEntityStorePrivileges = !( + isLoadingEntityEnginePrivileges || !entityEnginePrivileges?.has_all_required ); + + const canInstallEntityStore = hasEntityStorePrivileges && entityStoreStatus === 'not_installed'; + + const { euiTheme } = useEuiTheme(); + const [toggleState, setToggleState] = useState({ + riskScore: false, + entityStore: false, + }); + + /** + * Update the toggle state when the install status changes because privileges are async. + * We automatically toggle the switch when the user can enable the engine. + * + */ + useEffect(() => { + setToggleState({ + riskScore: canInstallRiskScore, + entityStore: canInstallEntityStore, + }); + }, [canInstallRiskScore, canInstallEntityStore]); + + const isInstallButtonDisabled = !isInstallButtonEnabled( + canInstallRiskScore, + canInstallEntityStore, + toggleState + ); + const { AdditionalChargesMessage } = useContractComponents(); if (!visible) { @@ -127,12 +148,9 @@ export const EntityStoreEnablementModal: React.FC } - checked={enablements.riskScore} - disabled={ - !riskScore.canToggle || - (!riskEnginePrivileges.isLoading && !riskEnginePrivileges?.hasAllRequiredPrivileges) - } - onChange={() => setEnablements((prev) => ({ ...prev, riskScore: !prev.riskScore }))} + checked={toggleState.riskScore} + disabled={!canInstallRiskScore} + onChange={() => setToggleState((prev) => ({ ...prev, riskScore: !prev.riskScore }))} data-test-subj="enablementRiskScoreSwitch" /> @@ -154,13 +172,10 @@ export const EntityStoreEnablementModal: React.FC } - checked={enablements.entityStore} - disabled={ - !entityStore.canToggle || - (!isLoadingEntityEnginePrivileges && !entityEnginePrivileges?.has_all_required) - } + checked={toggleState.entityStore} + disabled={!canInstallEntityStore} onChange={() => - setEnablements((prev) => ({ ...prev, entityStore: !prev.entityStore })) + setToggleState((prev) => ({ ...prev, entityStore: !prev.entityStore })) } data-test-subj="enablementEntityStoreSwitch" /> @@ -179,15 +194,17 @@ export const EntityStoreEnablementModal: React.FC - {!enablementOptions ? {proceedWarning} : null} + {isInstallButtonDisabled && (canInstallRiskScore || canInstallEntityStore) ? ( + {proceedWarning} + ) : null} toggle(false)}>{'Cancel'} ({ from, to }), [from, to]); - const privileges = useMissingRiskEnginePrivileges(); + const privileges = useMissingRiskEnginePrivileges({ readonly: true }); const { data, inspect, isInspected, hasEngineBeenInstalled, loading, refetch, totalCount } = useRiskScore({ diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/hooks/use_missing_risk_engine_privileges.ts b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/hooks/use_missing_risk_engine_privileges.ts index 66ca7e604a438..40761dfa60b80 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/hooks/use_missing_risk_engine_privileges.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/hooks/use_missing_risk_engine_privileges.ts @@ -22,8 +22,14 @@ export type RiskEngineMissingPrivilegesResponse = hasAllRequiredPrivileges: false; }; +interface UseMissingRiskEnginePrivilegesParams { + /** + * If `true`, only read privileges are required. + */ + readonly: boolean; +} export const useMissingRiskEnginePrivileges = ( - required: NonEmptyArray = ['read', 'write'] + { readonly }: UseMissingRiskEnginePrivilegesParams = { readonly: false } ): RiskEngineMissingPrivilegesResponse => { const { data: privilegesResponse, isLoading } = useRiskEnginePrivileges(); @@ -41,16 +47,20 @@ export const useMissingRiskEnginePrivileges = ( }; } + const requiredIndexPrivileges: NonEmptyArray = readonly + ? ['read'] + : ['read', 'write']; + const { indexPrivileges, clusterPrivileges } = getMissingRiskEnginePrivileges( privilegesResponse.privileges, - required + requiredIndexPrivileges ); // privilegesResponse.has_all_required` is slightly misleading, it checks if it has *all* default required privileges. // Here we check if there are no missing privileges of the provided set of required privileges if ( indexPrivileges.every(([_, missingPrivileges]) => missingPrivileges.length === 0) && - clusterPrivileges.length === 0 + (readonly || clusterPrivileges.length === 0) // cluster privileges check is required for write operations ) { return { isLoading: false, @@ -66,5 +76,5 @@ export const useMissingRiskEnginePrivileges = ( clusterPrivileges, }, }; - }, [isLoading, privilegesResponse, required]); + }, [isLoading, privilegesResponse, readonly]); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/explore/hosts/pages/navigation/host_risk_score_tab_body.tsx b/x-pack/solutions/security/plugins/security_solution/public/explore/hosts/pages/navigation/host_risk_score_tab_body.tsx index 557e0ff68d724..d671d30410200 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/explore/hosts/pages/navigation/host_risk_score_tab_body.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/explore/hosts/pages/navigation/host_risk_score_tab_body.tsx @@ -63,7 +63,7 @@ export const HostRiskScoreQueryTabBody = ({ }, [toggleStatus]); const timerange = useMemo(() => ({ from, to }), [from, to]); - const privileges = useMissingRiskEnginePrivileges(); + const privileges = useMissingRiskEnginePrivileges({ readonly: true }); const { data, inspect, isInspected, hasEngineBeenInstalled, loading, refetch, totalCount } = useRiskScore({ filterQuery,