Skip to content

Commit

Permalink
[Security Solution] Disable ML rule's edit button link under basic li…
Browse files Browse the repository at this point in the history
…cense (#143260)

**Resolves:** [#139796](#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)
  • Loading branch information
maximpn authored Oct 21, 2022
1 parent 4349ea7 commit a670c7f
Show file tree
Hide file tree
Showing 19 changed files with 213 additions and 134 deletions.
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand All @@ -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,
},
Expand All @@ -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,
},
Expand All @@ -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,
},
Expand All @@ -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,
},
Expand All @@ -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,
},
Expand Down Expand Up @@ -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',
},
{
Expand All @@ -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',
},
],
Expand All @@ -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',
},
{
Expand All @@ -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',
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
) : (
Expand All @@ -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>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -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)}
Expand All @@ -85,7 +93,7 @@ const useEnabledColumn = ({ hasPermissions }: ColumnsProps): TableColumn => {
width: '95px',
sortable: true,
}),
[hasActionsPrivileges, hasMlPermissions, hasPermissions, loadingIds]
[hasActionsPrivileges, hasMlPermissions, hasCRUDPermissions, loadingIds]
);
};

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -292,23 +300,23 @@ export const useRulesColumns = ({ hasPermissions }: ColumnsProps): TableColumn[]
width: '65px',
},
enabledColumn,
...(hasPermissions ? [actionsColumn] : []),
...(hasCRUDPermissions ? [actionsColumn] : []),
],
[
actionsColumn,
enabledColumn,
hasPermissions,
hasCRUDPermissions,
isInMemorySorting,
ruleNameColumn,
showRelatedIntegrations,
]
);
};

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);
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit a670c7f

Please sign in to comment.