Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[8.x] [SecuritySolution] Fix Risk score Insufficient privileges warning missing cluster privileges (#212405) #213377

Open
wants to merge 1 commit into
base: 8.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ const EntityAnalyticsRiskScoresComponent = <T extends EntityType>({

const refreshPage = useRefetchQueries();

const privileges = useMissingRiskEnginePrivileges(['read']);
const privileges = useMissingRiskEnginePrivileges({ readonly: true });

if (!isAuthorized) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,8 @@ export const EnablementPanel: React.FC<EnableEntityStorePanelProps> = ({ state }
: riskEngineStatus?.legacy_risk_engine_status;

if (
combinedRiskEngineStatus !== RiskEngineStatusEnum.NOT_INSTALLED &&
(entityStoreStatus === 'running' || entityStoreStatus === 'stopped')
riskEngineStatus !== RiskEngineStatusEnum.NOT_INSTALLED &&
entityStoreStatus !== 'not_installed'
) {
return null;
}
Expand Down Expand Up @@ -227,14 +227,8 @@ export const EnablementPanel: React.FC<EnableEntityStorePanelProps> = ({ state }
visible={modal.visible}
toggle={(visible) => setModalState({ visible })}
enableStore={enableEntityStore}
riskScore={{
canToggle: combinedRiskEngineStatus === RiskEngineStatusEnum.NOT_INSTALLED,
checked: combinedRiskEngineStatus === RiskEngineStatusEnum.NOT_INSTALLED,
}}
entityStore={{
canToggle: entityStoreStatus !== 'running',
checked: entityStoreStatus === 'not_installed',
}}
riskEngineStatus={riskEngineStatus}
entityStoreStatus={entityStoreStatus}
/>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { fireEvent, render, screen } from '@testing-library/react';
import type { EntityStoreEnablementModalProps } from './enablement_modal';
import { EntityStoreEnablementModal } from './enablement_modal';
import { TestProviders } from '../../../../common/mock';
import type { EntityAnalyticsPrivileges } from '../../../../../common/api/entity_analytics';
import {
type EntityAnalyticsPrivileges,
RiskEngineStatusEnum,
StoreStatusEnum,
} from '../../../../../common/api/entity_analytics';
import type { RiskEngineMissingPrivilegesResponse } from '../../../hooks/use_missing_risk_engine_privileges';

const mockToggle = jest.fn();
Expand All @@ -35,8 +40,8 @@ const defaultProps = {
visible: true,
toggle: mockToggle,
enableStore: mockEnableStore,
riskScore: { canToggle: false, checked: false },
entityStore: { canToggle: false, checked: false },
riskEngineStatus: RiskEngineStatusEnum.NOT_INSTALLED,
entityStoreStatus: StoreStatusEnum.not_installed,
};

const allEntityEnginePrivileges: EntityAnalyticsPrivileges = {
Expand Down Expand Up @@ -83,7 +88,7 @@ const missingRiskEnginePrivileges: RiskEngineMissingPrivilegesResponse = {
},
};

const renderComponent = async (props = defaultProps) => {
const renderComponent = async (props: EntityStoreEnablementModalProps = defaultProps) => {
await act(async () => {
return render(<EntityStoreEnablementModal {...props} />, { wrapper: TestProviders });
});
Expand Down Expand Up @@ -121,46 +126,45 @@ describe('EntityStoreEnablementModal', () => {
});

it('should call enableStore function when enable button is clicked', () => {
renderComponent({
...defaultProps,
riskScore: { ...defaultProps.riskScore, checked: true },
entityStore: { ...defaultProps.entityStore, checked: true },
});
renderComponent(defaultProps);
fireEvent.click(screen.getByText('Enable'));
expect(mockEnableStore).toHaveBeenCalledWith({ riskScore: true, entityStore: true });
});

it('should display proceed warning when no enablement options are selected', () => {
renderComponent();
fireEvent.click(screen.getByTestId('enablementEntityStoreSwitch')); // unselect entity store
fireEvent.click(screen.getByTestId('enablementRiskScoreSwitch')); // unselect risk engine
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 },
});
renderComponent();
fireEvent.click(screen.getByTestId('enablementEntityStoreSwitch')); // unselect entity store
fireEvent.click(screen.getByTestId('enablementRiskScoreSwitch')); // unselect risk engine

const enableButton = screen.getByRole('button', { name: /Enable/i });
expect(enableButton).toBeDisabled();
});

it('should show proceed warning when riskScore is enabled but entityStore is disabled and unchecked', () => {
it('should show proceed warning when riskScore is not installed and unchecked but entityStore is already running', () => {
renderComponent({
...defaultProps,
riskScore: { canToggle: false, checked: false }, // Enabled & Checked
entityStore: { canToggle: true, checked: false }, // Disabled & Unchecked
riskEngineStatus: RiskEngineStatusEnum.NOT_INSTALLED,
entityStoreStatus: StoreStatusEnum.running,
});
fireEvent.click(screen.getByTestId('enablementRiskScoreSwitch')); // unselect risk engine
expect(screen.getByText('Please enable at least one option to proceed.')).toBeInTheDocument();
});

it('should show proceed warning when entityStore is enabled but riskScore is disabled and unchecked', () => {
it('should show proceed warning when entityStore is not installed and unchecked but riskScore is already installed', () => {
renderComponent({
...defaultProps,
entityStore: { canToggle: false, checked: false }, // Enabled & Checked
riskScore: { canToggle: true, checked: false }, // Disabled & Unchecked
riskEngineStatus: RiskEngineStatusEnum.ENABLED,
entityStoreStatus: StoreStatusEnum.not_installed,
});
fireEvent.click(screen.getByTestId('enablementEntityStoreSwitch')); // unselect risk engine

expect(screen.getByText('Please enable at least one option to proceed.')).toBeInTheDocument();
});

Expand Down Expand Up @@ -209,5 +213,13 @@ describe('EntityStoreEnablementModal', () => {

expect(screen.queryByTestId('enablement-modal-test')).toBeInTheDocument();
});

it('should disabled the "enable" button', async () => {
renderComponent();
expect(screen.getByTestId('callout-missing-entity-store-privileges')).toBeInTheDocument();

const enableButton = screen.getByRole('button', { name: /Enable/i });
expect(enableButton).toBeDisabled();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,73 +22,94 @@ 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;
entityStore: boolean;
}

interface EntityStoreEnablementModalProps {
export 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<EntityStoreEnablementModalProps> = ({
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) {
Expand Down Expand Up @@ -127,12 +148,9 @@ export const EntityStoreEnablementModal: React.FC<EntityStoreEnablementModalProp
defaultMessage="Risk Score"
/>
}
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"
/>
</EuiFlexItem>
Expand All @@ -154,13 +172,10 @@ export const EntityStoreEnablementModal: React.FC<EntityStoreEnablementModalProp
defaultMessage="Entity Store"
/>
}
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"
/>
Expand All @@ -179,15 +194,17 @@ export const EntityStoreEnablementModal: React.FC<EntityStoreEnablementModalProp

<EuiModalFooter>
<EuiFlexGroup justifyContent="flexEnd" alignItems="center">
{!enablementOptions ? <EuiFlexItem>{proceedWarning}</EuiFlexItem> : null}
{isInstallButtonDisabled && (canInstallRiskScore || canInstallEntityStore) ? (
<EuiFlexItem>{proceedWarning}</EuiFlexItem>
) : null}
<EuiFlexItem grow={false}>
<EuiFlexGroup direction="row" justifyContent="flexEnd">
<EuiButtonEmpty onClick={() => toggle(false)}>{'Cancel'}</EuiButtonEmpty>
<EuiButton
onClick={enableStore(enablements)}
onClick={enableStore(toggleState)}
fill
isDisabled={!enablementOptions}
aria-disabled={!enablementOptions}
isDisabled={isInstallButtonDisabled}
aria-disabled={isInstallButtonDisabled}
data-test-subj="entityStoreEnablementModalButton"
>
<FormattedMessage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ const RiskDetailsTabBodyComponent: React.FC<
[setOverTimeToggleStatus]
);

const privileges = useMissingRiskEnginePrivileges();
const privileges = useMissingRiskEnginePrivileges({ readonly: true });

const RiskScoreUpsell = useUpsellingComponent('entity_analytics_panel');
if (RiskScoreUpsell) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export const UserRiskScoreQueryTabBody = ({

const timerange = useMemo(() => ({ from, to }), [from, to]);

const privileges = useMissingRiskEnginePrivileges();
const privileges = useMissingRiskEnginePrivileges({ readonly: true });

const {
data,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<RiskEngineIndexPrivilege> = ['read', 'write']
{ readonly }: UseMissingRiskEnginePrivilegesParams = { readonly: false }
): RiskEngineMissingPrivilegesResponse => {
const { data: privilegesResponse, isLoading } = useRiskEnginePrivileges();

Expand All @@ -41,14 +47,21 @@ export const useMissingRiskEnginePrivileges = (
};
}

const requiredIndexPrivileges: NonEmptyArray<RiskEngineIndexPrivilege> = 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)) {
if (
indexPrivileges.every(([_, missingPrivileges]) => missingPrivileges.length === 0) &&
(readonly || clusterPrivileges.length === 0) // cluster privileges check is required for write operations
) {
return {
isLoading: false,
hasAllRequiredPrivileges: true,
Expand All @@ -60,8 +73,8 @@ export const useMissingRiskEnginePrivileges = (
hasAllRequiredPrivileges: false,
missingPrivileges: {
indexPrivileges,
clusterPrivileges,
clusterPrivileges: readonly ? [] : clusterPrivileges, // cluster privileges are not required for readonly
},
};
}, [isLoading, privilegesResponse, required]);
}, [isLoading, privilegesResponse, readonly]);
};
Loading