[Security Solution] Disable ML rule's edit button link under basic license (#143260)

**Resolves:** [#139796](https://github.com/elastic/kibana/issues/139796)

## Summary

It disables ML rule's edit button link under the basic license.

## Details

ML rules aren't available under the basic license but installable from the prebuilt rules. Having an active edit button makes the UX inconsistent. Disabling such a button under the basic license for ML rules improves UX though doesn't block a user from opening the rule editing page from the address bar.


Before:

https://user-images.githubusercontent.com/3775283/195552179-525f0423-3a62-4ab5-b1ef-0f5cafe2286e.mov

After:

https://user-images.githubusercontent.com/3775283/195551540-b95fabeb-4e50-4a26-ae42-1a72f53573dc.mov


### Checklist

- [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
This commit is contained in:
Maxim Palenov 2022-10-21 11:50:20 +03:00 committed by GitHub
parent 4349ea70ee
commit a670c7f376
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 213 additions and 134 deletions

View file

@ -0,0 +1,30 @@
/*
* 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 { hasUserCRUDPermission } from '.';
describe('privileges utils', () => {
describe('hasUserCRUDPermission', () => {
test("returns true when user's CRUD operations are null", () => {
const result = hasUserCRUDPermission(null);
expect(result).toBeTruthy();
});
test('returns false when user cannot CRUD', () => {
const result = hasUserCRUDPermission(false);
expect(result).toBeFalsy();
});
test('returns true when user can CRUD', () => {
const result = hasUserCRUDPermission(true);
expect(result).toBeTruthy();
});
});
});

View file

@ -6,7 +6,7 @@
*/
import type { Rule } from '../../../detections/containers/detection_engine/rules';
import * as i18n from '../../../detections/pages/detection_engine/rules/translations';
import * as i18nActions from '../../../detections/pages/detection_engine/rules/translations';
import { isMlRule } from '../../../../common/machine_learning/helpers';
import * as detectionI18n from '../../../detections/pages/detection_engine/translations';
@ -29,21 +29,28 @@ export const canEditRuleWithActions = (
return true;
};
export const getToolTipContent = (
// typed as null not undefined as the initial state for this value is null.
export const hasUserCRUDPermission = (canUserCRUD: boolean | null): boolean =>
canUserCRUD != null ? canUserCRUD : true;
export const explainLackOfPermission = (
rule: Rule | null | undefined,
hasMlPermissions: boolean,
hasReadActionsPrivileges:
| boolean
| Readonly<{
[x: string]: boolean;
}>
}>,
canUserCRUD: boolean | null
): string | undefined => {
if (rule == null) {
return undefined;
} else if (isMlRule(rule.type) && !hasMlPermissions) {
return detectionI18n.ML_RULES_DISABLED_MESSAGE;
} else if (!canEditRuleWithActions(rule, hasReadActionsPrivileges)) {
return i18n.EDIT_RULE_SETTINGS_TOOLTIP;
return i18nActions.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES;
} else if (!hasUserCRUDPermission(canUserCRUD)) {
return i18nActions.LACK_OF_KIBANA_SECURITY_PRIVILEGES;
} else {
return undefined;
}

View file

@ -23,7 +23,7 @@ import { useBoolState } from '../../../../common/hooks/use_bool_state';
import { SINGLE_RULE_ACTIONS } from '../../../../common/lib/apm/user_actions';
import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction';
import { useKibana } from '../../../../common/lib/kibana';
import { getToolTipContent } from '../../../../common/utils/privileges';
import { canEditRuleWithActions } from '../../../../common/utils/privileges';
import type { Rule } from '../../../containers/detection_engine/rules';
import {
executeRulesBulkAction,
@ -96,7 +96,11 @@ const RuleActionsOverflowComponent = ({
>
<EuiToolTip
position="left"
content={getToolTipContent(rule, true, canDuplicateRuleWithActions)}
content={
!canEditRuleWithActions(rule, canDuplicateRuleWithActions)
? i18nActions.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES
: undefined
}
>
<>{i18nActions.DUPLICATE_RULE}</>
</EuiToolTip>

View file

@ -332,7 +332,9 @@ export const useBulkActions = ({
disabled:
missingActionPrivileges || containsLoading || (!containsDisabled && !isAllSelected),
onClick: handleEnableAction,
toolTipContent: missingActionPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined,
toolTipContent: missingActionPrivileges
? i18n.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES
: undefined,
toolTipPosition: 'right',
icon: undefined,
},
@ -342,7 +344,9 @@ export const useBulkActions = ({
'data-test-subj': 'duplicateRuleBulk',
disabled: isEditDisabled,
onClick: handleDuplicateAction,
toolTipContent: missingActionPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined,
toolTipContent: missingActionPrivileges
? i18n.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES
: undefined,
toolTipPosition: 'right',
icon: undefined,
},
@ -366,7 +370,9 @@ export const useBulkActions = ({
'data-test-subj': 'addRuleActionsBulk',
disabled: !hasActionsPrivileges || isEditDisabled,
onClick: handleBulkEdit(BulkActionEditType.add_rule_actions),
toolTipContent: !hasActionsPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined,
toolTipContent: !hasActionsPrivileges
? i18n.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES
: undefined,
toolTipPosition: 'right',
icon: undefined,
},
@ -376,7 +382,9 @@ export const useBulkActions = ({
'data-test-subj': 'setScheduleBulk',
disabled: isEditDisabled,
onClick: handleBulkEdit(BulkActionEditType.set_schedule),
toolTipContent: missingActionPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined,
toolTipContent: missingActionPrivileges
? i18n.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES
: undefined,
toolTipPosition: 'right',
icon: undefined,
},
@ -386,7 +394,9 @@ export const useBulkActions = ({
'data-test-subj': 'applyTimelineTemplateBulk',
disabled: isEditDisabled,
onClick: handleBulkEdit(BulkActionEditType.set_timeline),
toolTipContent: missingActionPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined,
toolTipContent: missingActionPrivileges
? i18n.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES
: undefined,
toolTipPosition: 'right',
icon: undefined,
},
@ -405,7 +415,9 @@ export const useBulkActions = ({
disabled:
missingActionPrivileges || containsLoading || (!containsEnabled && !isAllSelected),
onClick: handleDisableActions,
toolTipContent: missingActionPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined,
toolTipContent: missingActionPrivileges
? i18n.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES
: undefined,
toolTipPosition: 'right',
icon: undefined,
},
@ -439,7 +451,9 @@ export const useBulkActions = ({
'data-test-subj': 'addTagsBulkEditRule',
onClick: handleBulkEdit(BulkActionEditType.add_tags),
disabled: isEditDisabled,
toolTipContent: missingActionPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined,
toolTipContent: missingActionPrivileges
? i18n.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES
: undefined,
toolTipPosition: 'right',
},
{
@ -448,7 +462,9 @@ export const useBulkActions = ({
'data-test-subj': 'deleteTagsBulkEditRule',
onClick: handleBulkEdit(BulkActionEditType.delete_tags),
disabled: isEditDisabled,
toolTipContent: missingActionPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined,
toolTipContent: missingActionPrivileges
? i18n.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES
: undefined,
toolTipPosition: 'right',
},
],
@ -463,7 +479,9 @@ export const useBulkActions = ({
'data-test-subj': 'addIndexPatternsBulkEditRule',
onClick: handleBulkEdit(BulkActionEditType.add_index_patterns),
disabled: isEditDisabled,
toolTipContent: missingActionPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined,
toolTipContent: missingActionPrivileges
? i18n.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES
: undefined,
toolTipPosition: 'right',
},
{
@ -472,7 +490,9 @@ export const useBulkActions = ({
'data-test-subj': 'deleteIndexPatternsBulkEditRule',
onClick: handleBulkEdit(BulkActionEditType.delete_index_patterns),
disabled: isEditDisabled,
toolTipContent: missingActionPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined,
toolTipContent: missingActionPrivileges
? i18n.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES
: undefined,
toolTipPosition: 'right',
},
],

View file

@ -19,6 +19,7 @@ import {
import type { NamespaceType, ExceptionListFilter } from '@kbn/securitysolution-io-ts-list-types';
import { useApi, useExceptionLists } from '@kbn/securitysolution-list-hooks';
import { hasUserCRUDPermission } from '../../../../../../common/utils/privileges';
import { useAppToasts } from '../../../../../../common/hooks/use_app_toasts';
import { AutoDownload } from '../../../../../../common/components/auto_download/auto_download';
import { useKibana } from '../../../../../../common/lib/kibana';
@ -36,7 +37,6 @@ import { ExceptionsSearchBar } from './exceptions_search_bar';
import { getSearchFilters } from '../helpers';
import { SecurityPageName } from '../../../../../../../common/constants';
import { useUserData } from '../../../../../components/user_info';
import { userHasPermissions } from '../../helpers';
import { useListsConfig } from '../../../../../containers/detection_engine/lists/use_lists_config';
import type { ExceptionsTableItem } from './types';
import { MissingPrivilegesCallOut } from '../../../../../components/callouts/missing_privileges_callout';
@ -63,7 +63,7 @@ const exceptionReferenceModalInitialState: ReferenceModalState = {
export const ExceptionListsTable = React.memo(() => {
const { formatUrl } = useFormatUrl(SecurityPageName.rules);
const [{ loading: userInfoLoading, canUserCRUD, canUserREAD }] = useUserData();
const hasPermissions = userHasPermissions(canUserCRUD);
const hasPermissions = hasUserCRUDPermission(canUserCRUD);
const { loading: listsConfigLoading } = useListsConfig();
const loading = userInfoLoading || listsConfigLoading;

View file

@ -43,7 +43,7 @@ export const getRulesTableActions = ({
'data-test-subj': 'editRuleAction',
description: i18n.EDIT_RULE_SETTINGS,
name: !actionsPrivileges ? (
<EuiToolTip position="left" content={i18n.EDIT_RULE_SETTINGS_TOOLTIP}>
<EuiToolTip position="left" content={i18n.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES}>
<>{i18n.EDIT_RULE_SETTINGS}</>
</EuiToolTip>
) : (
@ -59,7 +59,7 @@ export const getRulesTableActions = ({
description: i18n.DUPLICATE_RULE,
icon: 'copy',
name: !actionsPrivileges ? (
<EuiToolTip position="left" content={i18n.EDIT_RULE_SETTINGS_TOOLTIP}>
<EuiToolTip position="left" content={i18n.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES}>
<>{i18n.DUPLICATE_RULE}</>
</EuiToolTip>
) : (

View file

@ -179,8 +179,8 @@ export const RulesTables = React.memo<RulesTableProps>(
[setPage, setPerPage, setSortingOptions]
);
const rulesColumns = useRulesColumns({ hasPermissions });
const monitoringColumns = useMonitoringColumns({ hasPermissions });
const rulesColumns = useRulesColumns({ hasCRUDPermissions: hasPermissions });
const monitoringColumns = useMonitoringColumns({ hasCRUDPermissions: hasPermissions });
const handleCreatePrePackagedRules = useCallback(async () => {
if (createPrePackagedRules != null) {

View file

@ -22,7 +22,10 @@ import { FormattedRelativePreferenceDate } from '../../../../../common/component
import { getRuleDetailsTabUrl } from '../../../../../common/components/link_to/redirect_to_detection_engine';
import { PopoverItems } from '../../../../../common/components/popover_items';
import { useKibana, useUiSetting$ } from '../../../../../common/lib/kibana';
import { canEditRuleWithActions, getToolTipContent } from '../../../../../common/utils/privileges';
import {
canEditRuleWithActions,
explainLackOfPermission,
} from '../../../../../common/utils/privileges';
import { RuleSwitch } from '../../../../components/rules/rule_switch';
import { SeverityBadge } from '../../../../components/rules/severity_badge';
import type { Rule } from '../../../../containers/detection_engine/rules';
@ -48,10 +51,10 @@ import { RuleDetailTabs } from '../details';
export type TableColumn = EuiBasicTableColumn<Rule> | EuiTableActionsColumnType<Rule>;
interface ColumnsProps {
hasPermissions: boolean;
hasCRUDPermissions: boolean;
}
const useEnabledColumn = ({ hasPermissions }: ColumnsProps): TableColumn => {
const useEnabledColumn = ({ hasCRUDPermissions }: ColumnsProps): TableColumn => {
const hasMlPermissions = useHasMlPermissions();
const hasActionsPrivileges = useHasActionsPrivileges();
const { loadingRulesAction, loadingRuleIds } = useRulesTableContext().state;
@ -68,14 +71,19 @@ const useEnabledColumn = ({ hasPermissions }: ColumnsProps): TableColumn => {
render: (_, rule: Rule) => (
<EuiToolTip
position="top"
content={getToolTipContent(rule, hasMlPermissions, hasActionsPrivileges)}
content={explainLackOfPermission(
rule,
hasMlPermissions,
hasActionsPrivileges,
hasCRUDPermissions
)}
>
<RuleSwitch
id={rule.id}
enabled={rule.enabled}
isDisabled={
!canEditRuleWithActions(rule, hasActionsPrivileges) ||
!hasPermissions ||
!hasCRUDPermissions ||
(isMlRule(rule.type) && !hasMlPermissions && !rule.enabled)
}
isLoading={loadingIds.includes(rule.id)}
@ -85,7 +93,7 @@ const useEnabledColumn = ({ hasPermissions }: ColumnsProps): TableColumn => {
width: '95px',
sortable: true,
}),
[hasActionsPrivileges, hasMlPermissions, hasPermissions, loadingIds]
[hasActionsPrivileges, hasMlPermissions, hasCRUDPermissions, loadingIds]
);
};
@ -195,9 +203,9 @@ const useActionsColumn = (): EuiTableActionsColumnType<Rule> => {
);
};
export const useRulesColumns = ({ hasPermissions }: ColumnsProps): TableColumn[] => {
export const useRulesColumns = ({ hasCRUDPermissions }: ColumnsProps): TableColumn[] => {
const actionsColumn = useActionsColumn();
const enabledColumn = useEnabledColumn({ hasPermissions });
const enabledColumn = useEnabledColumn({ hasCRUDPermissions });
const ruleNameColumn = useRuleNameColumn();
const { isInMemorySorting } = useRulesTableContext().state;
const [showRelatedIntegrations] = useUiSetting$<boolean>(SHOW_RELATED_INTEGRATIONS_SETTING);
@ -292,12 +300,12 @@ export const useRulesColumns = ({ hasPermissions }: ColumnsProps): TableColumn[]
width: '65px',
},
enabledColumn,
...(hasPermissions ? [actionsColumn] : []),
...(hasCRUDPermissions ? [actionsColumn] : []),
],
[
actionsColumn,
enabledColumn,
hasPermissions,
hasCRUDPermissions,
isInMemorySorting,
ruleNameColumn,
showRelatedIntegrations,
@ -305,10 +313,10 @@ export const useRulesColumns = ({ hasPermissions }: ColumnsProps): TableColumn[]
);
};
export const useMonitoringColumns = ({ hasPermissions }: ColumnsProps): TableColumn[] => {
export const useMonitoringColumns = ({ hasCRUDPermissions }: ColumnsProps): TableColumn[] => {
const docLinks = useKibana().services.docLinks;
const actionsColumn = useActionsColumn();
const enabledColumn = useEnabledColumn({ hasPermissions });
const enabledColumn = useEnabledColumn({ hasCRUDPermissions });
const ruleNameColumn = useRuleNameColumn();
const { isInMemorySorting } = useRulesTableContext().state;
const [showRelatedIntegrations] = useUiSetting$<boolean>(SHOW_RELATED_INTEGRATIONS_SETTING);
@ -425,13 +433,13 @@ export const useMonitoringColumns = ({ hasPermissions }: ColumnsProps): TableCol
width: '16%',
},
enabledColumn,
...(hasPermissions ? [actionsColumn] : []),
...(hasCRUDPermissions ? [actionsColumn] : []),
],
[
actionsColumn,
docLinks.links.siem.troubleshootGaps,
enabledColumn,
hasPermissions,
hasCRUDPermissions,
isInMemorySorting,
ruleNameColumn,
showRelatedIntegrations,

View file

@ -18,6 +18,7 @@ import React, { useCallback, useRef, useState, useMemo, useEffect } from 'react'
import styled from 'styled-components';
import type { DataViewListItem } from '@kbn/data-views-plugin/common';
import { hasUserCRUDPermission } from '../../../../../common/utils/privileges';
import { isThreatMatchRule } from '../../../../../../common/detection_engine/utils';
import { useCreateRule } from '../../../../containers/detection_engine/rules';
import type { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request';
@ -38,12 +39,7 @@ import { StepAboutRule } from '../../../../components/rules/step_about_rule';
import { StepScheduleRule } from '../../../../components/rules/step_schedule_rule';
import { StepRuleActions } from '../../../../components/rules/step_rule_actions';
import * as RuleI18n from '../translations';
import {
redirectToDetections,
getActionMessageParams,
userHasPermissions,
MaxWidthEuiFlexItem,
} from '../helpers';
import { redirectToDetections, getActionMessageParams, MaxWidthEuiFlexItem } from '../helpers';
import type {
AboutStepRule,
DefineStepRule,
@ -364,7 +360,7 @@ const CreateRulePageComponent: React.FC = () => {
path: getDetectionEngineUrl(),
});
return null;
} else if (!userHasPermissions(canUserCRUD)) {
} else if (!hasUserCRUDPermission(canUserCRUD)) {
navigateToApp(APP_UI_ID, {
deepLinkId: SecurityPageName.rules,
path: getRulesUrl(),

View file

@ -0,0 +1,56 @@
/*
* 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, { useCallback } from 'react';
import { EuiToolTip } from '@elastic/eui';
import { useKibana } from '../../../../../../common/lib/kibana';
import { SecuritySolutionLinkButton } from '../../../../../../common/components/links';
import { APP_UI_ID } from '../../../../../../../common/constants';
import { SecurityPageName } from '../../../../../../app/types';
import { getEditRuleUrl } from '../../../../../../common/components/link_to/redirect_to_detection_engine';
import * as ruleI18n from '../../translations';
interface EditRuleSettingButtonLinkProps {
ruleId: string;
disabled: boolean;
disabledReason?: string;
}
export function EditRuleSettingButtonLink({
ruleId,
disabled = false,
disabledReason,
}: EditRuleSettingButtonLinkProps): JSX.Element {
const {
application: { navigateToApp },
} = useKibana().services;
const goToEditRule = useCallback(
(ev) => {
ev.preventDefault();
navigateToApp(APP_UI_ID, {
deepLinkId: SecurityPageName.rules,
path: getEditRuleUrl(ruleId),
});
},
[navigateToApp, ruleId]
);
return (
<EuiToolTip position="top" content={disabledReason}>
<SecuritySolutionLinkButton
data-test-subj="editRuleSettingsLink"
onClick={goToEditRule}
iconType="controlsHorizontal"
isDisabled={disabled}
deepLinkId={SecurityPageName.rules}
path={getEditRuleUrl(ruleId)}
>
{ruleI18n.EDIT_RULE_SETTINGS}
</SecuritySolutionLinkButton>
</EuiToolTip>
);
}

View file

@ -27,12 +27,12 @@ import type { ConnectedProps } from 'react-redux';
import { connect, useDispatch } from 'react-redux';
import styled from 'styled-components';
import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import type { Dispatch } from 'redux';
import { isTab } from '@kbn/timelines-plugin/public';
import type { DataViewListItem } from '@kbn/data-views-plugin/common';
import { tableDefaults } from '../../../../../common/store/data_table/defaults';
import { dataTableActions, dataTableSelectors } from '../../../../../common/store/data_table';
import { isMlRule } from '../../../../../../common/machine_learning/helpers';
import { SecuritySolutionTabNavigation } from '../../../../../common/components/navigation';
import { InputsModelId } from '../../../../../common/store/inputs/constants';
import {
@ -45,7 +45,6 @@ import type { UpdateDateRange } from '../../../../../common/components/charts/co
import { FiltersGlobal } from '../../../../../common/components/filters_global';
import { FormattedDate } from '../../../../../common/components/formatted_date';
import {
getEditRuleUrl,
getRulesUrl,
getDetectionEngineUrl,
getRuleDetailsTabUrl,
@ -69,7 +68,7 @@ import {
} from '../../../../components/alerts_table/default_config';
import { RuleSwitch } from '../../../../components/rules/rule_switch';
import { StepPanel } from '../../../../components/rules/step_panel';
import { getStepsData, redirectToDetections, userHasPermissions } from '../helpers';
import { getStepsData, redirectToDetections } from '../helpers';
import { useGlobalTime } from '../../../../../common/containers/use_global_time';
import { inputsSelectors } from '../../../../../common/store/inputs';
import { setAbsoluteRangeDatePicker } from '../../../../../common/store/inputs/actions';
@ -78,8 +77,6 @@ import { useMlCapabilities } from '../../../../../common/components/ml/hooks/use
import { hasMlAdminPermissions } from '../../../../../../common/machine_learning/has_ml_admin_permissions';
import { hasMlLicense } from '../../../../../../common/machine_learning/has_ml_license';
import { SecurityPageName } from '../../../../../app/types';
import { LinkButton } from '../../../../../common/components/links';
import { useFormatUrl } from '../../../../../common/components/link_to';
import {
APP_UI_ID,
DEFAULT_INDEX_KEY,
@ -97,9 +94,10 @@ import {
import { useSourcererDataView } from '../../../../../common/containers/sourcerer';
import { SourcererScopeName } from '../../../../../common/store/sourcerer/model';
import {
getToolTipContent,
explainLackOfPermission,
canEditRuleWithActions,
isBoolean,
hasUserCRUDPermission,
} from '../../../../../common/utils/privileges';
import {
@ -132,6 +130,7 @@ import { useSignalHelpers } from '../../../../../common/containers/sourcerer/use
import { HeaderPage } from '../../../../../common/components/header_page';
import { ExceptionsViewer } from '../../../../../detection_engine/rule_exceptions/components/all_exception_items_table';
import type { NavTab } from '../../../../../common/components/navigation/types';
import { EditRuleSettingButtonLink } from './components/edit_rule_settings_button_link';
/**
* Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space.
@ -299,7 +298,6 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
const [showBuildingBlockAlerts, setShowBuildingBlockAlerts] = useState(false);
const [showOnlyThreatIndicatorAlerts, setShowOnlyThreatIndicatorAlerts] = useState(false);
const mlCapabilities = useMlCapabilities();
const { formatUrl } = useFormatUrl(SecurityPageName.rules);
const { globalFullScreen } = useGlobalFullScreen();
const [filterGroup, setFilterGroup] = useState<Status>(FILTER_OPEN);
const [dataViewOptions, setDataViewOptions] = useState<{ [x: string]: DataViewListItem }>({});
@ -584,45 +582,6 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
setRule((currentRule) => (currentRule ? { ...currentRule, enabled } : currentRule));
}, []);
const goToEditRule = useCallback(
(ev) => {
ev.preventDefault();
navigateToApp(APP_UI_ID, {
deepLinkId: SecurityPageName.rules,
path: getEditRuleUrl(ruleId ?? ''),
});
},
[navigateToApp, ruleId]
);
const editRule = useMemo(() => {
if (!hasActionsPrivileges) {
return (
<EuiToolTip position="top" content={ruleI18n.EDIT_RULE_SETTINGS_TOOLTIP}>
<LinkButton
onClick={goToEditRule}
iconType="controlsHorizontal"
isDisabled={true}
href={formatUrl(getEditRuleUrl(ruleId ?? ''))}
>
{ruleI18n.EDIT_RULE_SETTINGS}
</LinkButton>
</EuiToolTip>
);
}
return (
<LinkButton
data-test-subj="editRuleSettingsLink"
onClick={goToEditRule}
iconType="controlsHorizontal"
isDisabled={!isExistingRule || !userHasPermissions(canUserCRUD)}
href={formatUrl(getEditRuleUrl(ruleId ?? ''))}
>
{ruleI18n.EDIT_RULE_SETTINGS}
</LinkButton>
);
}, [isExistingRule, canUserCRUD, formatUrl, goToEditRule, hasActionsPrivileges, ruleId]);
const onShowBuildingBlockAlertsChangedCallback = useCallback(
(newShowBuildingBlockAlerts: boolean) => {
setShowBuildingBlockAlerts(newShowBuildingBlockAlerts);
@ -719,7 +678,12 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
<EuiFlexItem grow={false}>
<EuiToolTip
position="top"
content={getToolTipContent(rule, hasMlPermissions, hasActionsPrivileges)}
content={explainLackOfPermission(
rule,
hasMlPermissions,
hasActionsPrivileges,
canUserCRUD
)}
>
<EuiFlexGroup>
<RuleSwitch
@ -727,7 +691,7 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
isDisabled={
!isExistingRule ||
!canEditRuleWithActions(rule, hasActionsPrivileges) ||
!userHasPermissions(canUserCRUD) ||
!hasUserCRUDPermission(canUserCRUD) ||
(!hasMlPermissions && !rule?.enabled)
}
enabled={isExistingRule && (rule?.enabled ?? false)}
@ -740,11 +704,26 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
<EuiFlexItem grow={false}>
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
<EuiFlexItem grow={false}>{editRule}</EuiFlexItem>
<EuiFlexItem grow={false}>
<EditRuleSettingButtonLink
ruleId={ruleId}
disabled={
!isExistingRule ||
!hasUserCRUDPermission(canUserCRUD) ||
(isMlRule(rule?.type) && !hasMlPermissions)
}
disabledReason={explainLackOfPermission(
rule,
hasMlPermissions,
hasActionsPrivileges,
canUserCRUD
)}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<RuleActionsOverflow
rule={rule}
userHasPermissions={isExistingRule && userHasPermissions(canUserCRUD)}
userHasPermissions={isExistingRule && hasUserCRUDPermission(canUserCRUD)}
canDuplicateRuleWithActions={canEditRuleWithActions(
rule,
hasActionsPrivileges

View file

@ -21,6 +21,7 @@ import { useParams } from 'react-router-dom';
import { noop } from 'lodash';
import type { DataViewListItem } from '@kbn/data-views-plugin/common';
import { hasUserCRUDPermission } from '../../../../../common/utils/privileges';
import type { UpdateRulesSchema } from '../../../../../../common/detection_engine/schemas/request';
import { useRule, useUpdateRule } from '../../../../containers/detection_engine/rules';
import { useListsConfig } from '../../../../containers/detection_engine/lists/use_lists_config';
@ -49,7 +50,6 @@ import {
getStepsData,
redirectToDetections,
getActionMessageParams,
userHasPermissions,
MaxWidthEuiFlexItem,
} from '../helpers';
import * as ruleI18n from '../translations';
@ -454,7 +454,7 @@ const EditRulePageComponent: FC = () => {
path: getDetectionEngineUrl(),
});
return null;
} else if (!userHasPermissions(canUserCRUD)) {
} else if (!hasUserCRUDPermission(canUserCRUD)) {
navigateToApp(APP_UI_ID, {
deepLinkId: SecurityPageName.rules,
path: getRuleDetailsUrl(ruleId ?? ''),

View file

@ -18,7 +18,6 @@ import {
getPrePackagedRuleStatus,
getPrePackagedTimelineStatus,
determineDetailsValue,
userHasPermissions,
fillEmptySeverityMappings,
} from './helpers';
import { mockRuleWithEverything, mockRule } from './all/__mocks__/mock';
@ -448,29 +447,6 @@ describe('rule helpers', () => {
});
});
describe('userHasPermissions', () => {
test("returns true when user's CRUD operations are null", () => {
const result: boolean = userHasPermissions(null);
const userHasPermissionsExpectedResult = true;
expect(result).toEqual(userHasPermissionsExpectedResult);
});
test('returns false when user cannot CRUD', () => {
const result: boolean = userHasPermissions(false);
const userHasPermissionsExpectedResult = false;
expect(result).toEqual(userHasPermissionsExpectedResult);
});
test('returns true when user can CRUD', () => {
const result: boolean = userHasPermissions(true);
const userHasPermissionsExpectedResult = true;
expect(result).toEqual(userHasPermissionsExpectedResult);
});
});
describe('getPrePackagedRuleStatus', () => {
test('ruleNotInstalled', () => {
const rulesInstalled = 0;

View file

@ -461,10 +461,6 @@ export const getActionMessageParams = memoizeOne((ruleType: Type | undefined): A
export const getAllActionMessageParams = () =>
transformRuleKeysToActionVariables(getAllRuleParamsKeys());
// typed as null not undefined as the initial state for this value is null.
export const userHasPermissions = (canUserCRUD: boolean | null): boolean =>
canUserCRUD != null ? canUserCRUD : true;
export const MaxWidthEuiFlexItem = styled(EuiFlexItem)`
max-width: 1000px;
overflow: hidden;

View file

@ -7,6 +7,7 @@
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
import React, { useCallback, useMemo, useState } from 'react';
import { hasUserCRUDPermission } from '../../../../common/utils/privileges';
import { MlJobUpgradeModal } from '../../../components/modals/ml_job_upgrade_modal';
import { affectedJobIds } from '../../../components/callouts/ml_job_compatibility_callout/affected_job_ids';
import { useInstalledSecurityJobs } from '../../../../common/components/ml/hooks/use_installed_security_jobs';
@ -26,7 +27,6 @@ import {
getPrePackagedRuleStatus,
getPrePackagedTimelineStatus,
redirectToDetections,
userHasPermissions,
} from './helpers';
import * as i18n from './translations';
import { SecurityPageName } from '../../../../app/types';
@ -126,7 +126,7 @@ const RulesPageComponent: React.FC = () => {
const loadPrebuiltRulesAndTemplatesButton = useMemo(
() =>
getLoadPrebuiltRulesAndTemplatesButton({
isDisabled: !userHasPermissions(canUserCRUD) || loading || loadingJobs,
isDisabled: !hasUserCRUDPermission(canUserCRUD) || loading || loadingJobs,
onClick: showMlJobUpgradeModal,
}),
[
@ -141,7 +141,7 @@ const RulesPageComponent: React.FC = () => {
const reloadPrebuiltRulesAndTemplatesButton = useMemo(
() =>
getReloadPrebuiltRulesAndTemplatesButton({
isDisabled: !userHasPermissions(canUserCRUD) || loading || loadingJobs,
isDisabled: !hasUserCRUDPermission(canUserCRUD) || loading || loadingJobs,
onClick: showMlJobUpgradeModal,
}),
[
@ -224,7 +224,7 @@ const RulesPageComponent: React.FC = () => {
<EuiButton
data-test-subj="rules-import-modal-button"
iconType="importAction"
isDisabled={!userHasPermissions(canUserCRUD) || loading}
isDisabled={!hasUserCRUDPermission(canUserCRUD) || loading}
onClick={showImportModal}
>
{i18n.IMPORT_RULE}
@ -235,7 +235,7 @@ const RulesPageComponent: React.FC = () => {
data-test-subj="create-new-rule"
fill
iconType="plusInCircle"
isDisabled={!userHasPermissions(canUserCRUD) || loading}
isDisabled={!hasUserCRUDPermission(canUserCRUD) || loading}
deepLinkId={SecurityPageName.rulesCreate}
>
{i18n.ADD_NEW_RULE}
@ -257,7 +257,7 @@ const RulesPageComponent: React.FC = () => {
createPrePackagedRules={createPrePackagedRules}
data-test-subj="all-rules"
loadingCreatePrePackagedRules={loadingCreatePrePackagedRules}
hasPermissions={userHasPermissions(canUserCRUD)}
hasPermissions={hasUserCRUDPermission(canUserCRUD)}
rulesCustomInstalled={rulesCustomInstalled}
rulesInstalled={rulesInstalled}
rulesNotInstalled={rulesNotInstalled}

View file

@ -484,13 +484,20 @@ export const EDIT_RULE_SETTINGS = i18n.translate(
}
);
export const EDIT_RULE_SETTINGS_TOOLTIP = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.allRules.actions.editRuleSettingsToolTip',
export const LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.allRules.actions.lackOfKibanaActionsFeaturePrivileges',
{
defaultMessage: 'You do not have Kibana Actions privileges',
}
);
export const LACK_OF_KIBANA_SECURITY_PRIVILEGES = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.allRules.actions.lackOfKibanaSecurityPrivileges',
{
defaultMessage: 'You do not have Kibana Security privileges',
}
);
export const DUPLICATE_RULE = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateRuleDescription',
{

View file

@ -27333,7 +27333,7 @@
"xpack.securitySolution.detectionEngine.rules.allRules.actions.deleteRuleDescription": "Supprimer la règle",
"xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateRuleDescription": "Dupliquer la règle",
"xpack.securitySolution.detectionEngine.rules.allRules.actions.editRuleSettingsDescription": "Modifier les paramètres de règles",
"xpack.securitySolution.detectionEngine.rules.allRules.actions.editRuleSettingsToolTip": "Vous ne disposez pas des privilèges d'actions Kibana",
"xpack.securitySolution.detectionEngine.rules.allRules.actions.lackOfKibanaActionsFeaturePrivileges": "Vous ne disposez pas des privilèges d'actions Kibana",
"xpack.securitySolution.detectionEngine.rules.allRules.actions.exportRuleDescription": "Exporter la règle",
"xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedImmutableTitle": "La sélection contient des règles immuables qui ne peuvent pas être supprimées",
"xpack.securitySolution.detectionEngine.rules.allRules.batchActionsTitle": "Actions groupées",

View file

@ -27308,7 +27308,7 @@
"xpack.securitySolution.detectionEngine.rules.allRules.actions.deleteRuleDescription": "ルールの削除",
"xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateRuleDescription": "ルールの複製",
"xpack.securitySolution.detectionEngine.rules.allRules.actions.editRuleSettingsDescription": "ルール設定の編集",
"xpack.securitySolution.detectionEngine.rules.allRules.actions.editRuleSettingsToolTip": "Kibana アクション特権がありません",
"xpack.securitySolution.detectionEngine.rules.allRules.actions.lackOfKibanaActionsFeaturePrivileges": "Kibana アクション特権がありません",
"xpack.securitySolution.detectionEngine.rules.allRules.actions.exportRuleDescription": "ルールのエクスポート",
"xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedImmutableTitle": "選択には削除できないイミュータブルルールがあります",
"xpack.securitySolution.detectionEngine.rules.allRules.batchActionsTitle": "一斉アクション",

View file

@ -27342,7 +27342,7 @@
"xpack.securitySolution.detectionEngine.rules.allRules.actions.deleteRuleDescription": "删除规则",
"xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateRuleDescription": "复制规则",
"xpack.securitySolution.detectionEngine.rules.allRules.actions.editRuleSettingsDescription": "编辑规则设置",
"xpack.securitySolution.detectionEngine.rules.allRules.actions.editRuleSettingsToolTip": "您没有 Kibana 操作权限",
"xpack.securitySolution.detectionEngine.rules.allRules.actions.lackOfKibanaActionsFeaturePrivileges": "您没有 Kibana 操作权限",
"xpack.securitySolution.detectionEngine.rules.allRules.actions.exportRuleDescription": "导出规则",
"xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedImmutableTitle": "选择内容包含无法删除的不可变规则",
"xpack.securitySolution.detectionEngine.rules.allRules.batchActionsTitle": "批处理操作",