diff --git a/x-pack/plugins/security_solution/public/common/utils/privileges/index.test.ts b/x-pack/plugins/security_solution/public/common/utils/privileges/index.test.ts new file mode 100644 index 0000000000000..34abe0dd52c9a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/utils/privileges/index.test.ts @@ -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(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/utils/privileges/index.ts b/x-pack/plugins/security_solution/public/common/utils/privileges/index.ts index de8ad087f27e6..c420630ee23ba 100644 --- a/x-pack/plugins/security_solution/public/common/utils/privileges/index.ts +++ b/x-pack/plugins/security_solution/public/common/utils/privileges/index.ts @@ -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; } diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx index beb8e8365d74e..404319ef75832 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx @@ -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 = ({ > <>{i18nActions.DUPLICATE_RULE} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions.tsx index 17443855a6abf..05dee7d20823b 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions.tsx @@ -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', }, ], diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx index d7908d0bbce66..98e552b1c174a 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx @@ -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; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_actions.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_actions.tsx index cfd83c3ab408f..0a10026194869 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_actions.tsx @@ -43,7 +43,7 @@ export const getRulesTableActions = ({ 'data-test-subj': 'editRuleAction', description: i18n.EDIT_RULE_SETTINGS, name: !actionsPrivileges ? ( - + <>{i18n.EDIT_RULE_SETTINGS} ) : ( @@ -59,7 +59,7 @@ export const getRulesTableActions = ({ description: i18n.DUPLICATE_RULE, icon: 'copy', name: !actionsPrivileges ? ( - + <>{i18n.DUPLICATE_RULE} ) : ( diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx index 9165bdea5c533..f577a71a6b6e5 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx @@ -179,8 +179,8 @@ export const RulesTables = React.memo( [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) { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx index c5032a1eb9b72..2fa479d01b5d2 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx @@ -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 | EuiTableActionsColumnType; 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) => ( { width: '95px', sortable: true, }), - [hasActionsPrivileges, hasMlPermissions, hasPermissions, loadingIds] + [hasActionsPrivileges, hasMlPermissions, hasCRUDPermissions, loadingIds] ); }; @@ -195,9 +203,9 @@ const useActionsColumn = (): EuiTableActionsColumnType => { ); }; -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$(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$(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, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx index 0a4914274d2e6..0ea11725ab27a 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx @@ -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(), diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/components/edit_rule_settings_button_link.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/components/edit_rule_settings_button_link.tsx new file mode 100644 index 0000000000000..6ef7ebb0cae4c --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/components/edit_rule_settings_button_link.tsx @@ -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 ( + + + {ruleI18n.EDIT_RULE_SETTINGS} + + + ); +} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index f962cd806cc07..99f8d0554a983 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -27,10 +27,10 @@ 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 { isMlRule } from '../../../../../../common/machine_learning/helpers'; import { SecuritySolutionTabNavigation } from '../../../../../common/components/navigation'; import { InputsModelId } from '../../../../../common/store/inputs/constants'; import { @@ -43,7 +43,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, @@ -67,7 +66,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'; @@ -76,8 +75,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 { timelineDefaults } from '../../../../../timelines/store/timeline/defaul 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. @@ -301,7 +300,6 @@ const RuleDetailsPageComponent: React.FC = ({ 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(FILTER_OPEN); const [dataViewOptions, setDataViewOptions] = useState<{ [x: string]: DataViewListItem }>({}); @@ -586,45 +584,6 @@ const RuleDetailsPageComponent: React.FC = ({ 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 ( - - - {ruleI18n.EDIT_RULE_SETTINGS} - - - ); - } - return ( - - {ruleI18n.EDIT_RULE_SETTINGS} - - ); - }, [isExistingRule, canUserCRUD, formatUrl, goToEditRule, hasActionsPrivileges, ruleId]); - const onShowBuildingBlockAlertsChangedCallback = useCallback( (newShowBuildingBlockAlerts: boolean) => { setShowBuildingBlockAlerts(newShowBuildingBlockAlerts); @@ -721,7 +680,12 @@ const RuleDetailsPageComponent: React.FC = ({ = ({ isDisabled={ !isExistingRule || !canEditRuleWithActions(rule, hasActionsPrivileges) || - !userHasPermissions(canUserCRUD) || + !hasUserCRUDPermission(canUserCRUD) || (!hasMlPermissions && !rule?.enabled) } enabled={isExistingRule && (rule?.enabled ?? false)} @@ -742,11 +706,26 @@ const RuleDetailsPageComponent: React.FC = ({ - {editRule} + + + { path: getDetectionEngineUrl(), }); return null; - } else if (!userHasPermissions(canUserCRUD)) { + } else if (!hasUserCRUDPermission(canUserCRUD)) { navigateToApp(APP_UI_ID, { deepLinkId: SecurityPageName.rules, path: getRuleDetailsUrl(ruleId ?? ''), diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx index 4392be5cdc409..f7251806f8f0b 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx @@ -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; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx index 36e00aaccc1ef..6995abd54e7fb 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx @@ -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; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx index 997f40e918cb2..57c0dee2834f7 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx @@ -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 = () => { {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} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts index 8b32821bc71f1..b9948ca90578b 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts @@ -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', { diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index d4698b863029c..5e598674e0c0c 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -27075,7 +27075,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", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index eee20465f90bc..d13339673a297 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -27050,7 +27050,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": "一斉アクション", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 3a603dd65fc69..ffd50fd9cbeea 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -27081,7 +27081,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": "批处理操作",