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": "批处理操作",