From a643f29e7f83dc61ddc929664f0f71da4bfd3951 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Mon, 7 Mar 2022 19:51:33 -0800 Subject: [PATCH] [Security Solution][Rules] - Remove rule selection for read only users (#126827) Resolves #126328 and #126314. (cherry picked from commit 18a53440825654d98cefa1ca002cc15c1beabdeb) --- .../missing_privileges_callout.spec.ts | 19 +----- .../all_rules_read_only.spec.ts | 60 +++++++++++++++++++ .../public/detections/pages/alerts/index.tsx | 24 +------- .../detections/pages/alerts/translations.ts | 7 --- .../rules/all/rules_tables.tsx | 2 +- .../public/exceptions/routes.tsx | 10 +++- .../public/exceptions/translations.ts | 15 +++++ .../security_solution/public/rules/routes.tsx | 34 +++++++---- .../public/rules/translations.ts | 15 +++++ .../security_solution/public/translations.ts | 12 ++++ .../public/use_readonly_header.ts | 37 ++++++++++++ .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 13 files changed, 175 insertions(+), 62 deletions(-) create mode 100644 x-pack/plugins/security_solution/cypress/integration/detection_rules/all_rules_read_only.spec.ts create mode 100644 x-pack/plugins/security_solution/public/exceptions/translations.ts create mode 100644 x-pack/plugins/security_solution/public/rules/translations.ts create mode 100644 x-pack/plugins/security_solution/public/translations.ts create mode 100644 x-pack/plugins/security_solution/public/use_readonly_header.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/missing_privileges_callout.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/missing_privileges_callout.spec.ts index 64377c03af6c4d..cf74ca338220bb 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/missing_privileges_callout.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/missing_privileges_callout.spec.ts @@ -72,24 +72,7 @@ describe('Detections > Callouts', () => { }); }); - context('On Rules Management page', () => { - beforeEach(() => { - loadPageAsReadOnlyUser(DETECTIONS_RULE_MANAGEMENT_URL); - }); - - it('We show one primary callout', () => { - waitForCallOutToBeShown(MISSING_PRIVILEGES_CALLOUT, 'primary'); - }); - - context('When a user clicks Dismiss on the callout', () => { - it('We hide it and persist the dismissal', () => { - waitForCallOutToBeShown(MISSING_PRIVILEGES_CALLOUT, 'primary'); - dismissCallOut(MISSING_PRIVILEGES_CALLOUT); - reloadPage(); - getCallOut(MISSING_PRIVILEGES_CALLOUT).should('not.exist'); - }); - }); - }); + // FYI: Rules Management check moved to ../detection_rules/all_rules_read_only.spec.ts context('On Rule Details page', () => { beforeEach(() => { diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/all_rules_read_only.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/all_rules_read_only.spec.ts new file mode 100644 index 00000000000000..845c5c2bca9d63 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/all_rules_read_only.spec.ts @@ -0,0 +1,60 @@ +/* + * 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 { ROLES } from '../../../common/test'; +import { getNewRule } from '../../objects/rule'; +import { + COLLAPSED_ACTION_BTN, + RULE_CHECKBOX, + RULE_NAME, +} from '../../screens/alerts_detection_rules'; +import { PAGE_TITLE } from '../../screens/common/page'; +import { waitForRulesTableToBeLoaded } from '../../tasks/alerts_detection_rules'; +import { createCustomRule } from '../../tasks/api_calls/rules'; +import { cleanKibana } from '../../tasks/common'; +import { dismissCallOut, getCallOut, waitForCallOutToBeShown } from '../../tasks/common/callouts'; +import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; +import { SECURITY_DETECTIONS_RULES_URL } from '../../urls/navigation'; + +const MISSING_PRIVILEGES_CALLOUT = 'missing-user-privileges'; + +describe('All rules - read only', () => { + before(() => { + cleanKibana(); + createCustomRule(getNewRule(), '1'); + loginAndWaitForPageWithoutDateRange(SECURITY_DETECTIONS_RULES_URL, ROLES.reader); + waitForRulesTableToBeLoaded(); + cy.get(RULE_NAME).should('have.text', getNewRule().name); + }); + + it('Does not display select boxes for rules', () => { + cy.get(RULE_CHECKBOX).should('not.exist'); + }); + + it('Does not display action options', () => { + // These are the 3 dots at the end of the row that opens up + // options to take action on the rule + cy.get(COLLAPSED_ACTION_BTN).should('not.exist'); + }); + + it('Displays missing privileges primary callout', () => { + waitForCallOutToBeShown(MISSING_PRIVILEGES_CALLOUT, 'primary'); + }); + + context('When a user clicks Dismiss on the callouts', () => { + it('We hide them and persist the dismissal', () => { + waitForCallOutToBeShown(MISSING_PRIVILEGES_CALLOUT, 'primary'); + + dismissCallOut(MISSING_PRIVILEGES_CALLOUT); + cy.reload(); + cy.get(PAGE_TITLE).should('be.visible'); + cy.get(RULE_NAME).should('have.text', getNewRule().name); + + getCallOut(MISSING_PRIVILEGES_CALLOUT).should('not.exist'); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/pages/alerts/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/alerts/index.tsx index 18952feee528bd..3cf344c691ccdd 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/alerts/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/alerts/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useEffect } from 'react'; +import React from 'react'; import { Route, Switch } from 'react-router-dom'; import { ALERTS_PATH, SecurityPageName } from '../../../../common/constants'; @@ -13,9 +13,8 @@ import { NotFoundPage } from '../../../app/404'; import * as i18n from './translations'; import { TrackApplicationView } from '../../../../../../../src/plugins/usage_collection/public'; import { DetectionEnginePage } from '../../pages/detection_engine/detection_engine'; -import { useKibana } from '../../../common/lib/kibana'; import { SpyRoute } from '../../../common/utils/route/spy_routes'; -import { useAlertsPrivileges } from '../../containers/detection_engine/alerts/use_alerts_privileges'; +import { useReadonlyHeader } from '../../../use_readonly_header'; const AlertsRoute = () => ( @@ -25,24 +24,7 @@ const AlertsRoute = () => ( ); const AlertsContainerComponent: React.FC = () => { - const { chrome } = useKibana().services; - const { hasIndexRead, hasIndexWrite } = useAlertsPrivileges(); - - useEffect(() => { - // if the user is read only then display the glasses badge in the global navigation header - if (!hasIndexWrite && hasIndexRead) { - chrome.setBadge({ - text: i18n.READ_ONLY_BADGE_TEXT, - tooltip: i18n.READ_ONLY_BADGE_TOOLTIP, - iconType: 'glasses', - }); - } - - // remove the icon after the component unmounts - return () => { - chrome.setBadge(); - }; - }, [chrome, hasIndexRead, hasIndexWrite]); + useReadonlyHeader(i18n.READ_ONLY_BADGE_TOOLTIP); return ( diff --git a/x-pack/plugins/security_solution/public/detections/pages/alerts/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/alerts/translations.ts index 734e93925e5365..de0b6a5f37d93c 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/alerts/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/alerts/translations.ts @@ -7,13 +7,6 @@ import { i18n } from '@kbn/i18n'; -export const READ_ONLY_BADGE_TEXT = i18n.translate( - 'xpack.securitySolution.alerts.badge.readOnly.text', - { - defaultMessage: 'Read only', - } -); - export const READ_ONLY_BADGE_TOOLTIP = i18n.translate( 'xpack.securitySolution.alerts.badge.readOnly.tooltip', { 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 1b8adb0bd11d36..c38c8e48928f11 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 @@ -390,7 +390,7 @@ export const RulesTables = React.memo( onChange={tableOnChangeCallback} pagination={paginationMemo} ref={tableRef} - selection={euiBasicTableSelectionProps} + selection={hasPermissions ? euiBasicTableSelectionProps : undefined} sorting={{ sort: { // EuiBasicTable has incorrect `sort.field` types which accept only `keyof Item` and reject fields in dot notation diff --git a/x-pack/plugins/security_solution/public/exceptions/routes.tsx b/x-pack/plugins/security_solution/public/exceptions/routes.tsx index a5b95ffa64d4d7..262db114ae84e4 100644 --- a/x-pack/plugins/security_solution/public/exceptions/routes.tsx +++ b/x-pack/plugins/security_solution/public/exceptions/routes.tsx @@ -7,11 +7,13 @@ import React from 'react'; import { Route, Switch } from 'react-router-dom'; +import * as i18n from './translations'; import { TrackApplicationView } from '../../../../../src/plugins/usage_collection/public'; import { EXCEPTIONS_PATH, SecurityPageName } from '../../common/constants'; import { ExceptionListsTable } from '../detections/pages/detection_engine/rules/all/exceptions/exceptions_table'; import { SpyRoute } from '../common/utils/route/spy_routes'; import { NotFoundPage } from '../app/404'; +import { useReadonlyHeader } from '../use_readonly_header'; const ExceptionsRoutes = () => { return ( @@ -22,7 +24,9 @@ const ExceptionsRoutes = () => { ); }; -const renderExceptionsRoutes = () => { +const ExceptionsContainerComponent: React.FC = () => { + useReadonlyHeader(i18n.READ_ONLY_BADGE_TOOLTIP); + return ( @@ -31,6 +35,10 @@ const renderExceptionsRoutes = () => { ); }; +const Exceptions = React.memo(ExceptionsContainerComponent); + +const renderExceptionsRoutes = () => ; + export const routes = [ { path: EXCEPTIONS_PATH, diff --git a/x-pack/plugins/security_solution/public/exceptions/translations.ts b/x-pack/plugins/security_solution/public/exceptions/translations.ts new file mode 100644 index 00000000000000..780ed23a64ffe5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/exceptions/translations.ts @@ -0,0 +1,15 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const READ_ONLY_BADGE_TOOLTIP = i18n.translate( + 'xpack.securitySolution.exceptions.badge.readOnly.tooltip', + { + defaultMessage: 'Unable to create, edit or delete exceptions', + } +); diff --git a/x-pack/plugins/security_solution/public/rules/routes.tsx b/x-pack/plugins/security_solution/public/rules/routes.tsx index fcb434ae760ed1..4172e75a3cd96f 100644 --- a/x-pack/plugins/security_solution/public/rules/routes.tsx +++ b/x-pack/plugins/security_solution/public/rules/routes.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { Route, Switch } from 'react-router-dom'; +import * as i18n from './translations'; import { TrackApplicationView } from '../../../../../src/plugins/usage_collection/public'; import { RULES_PATH, SecurityPageName } from '../../common/constants'; import { NotFoundPage } from '../app/404'; @@ -14,6 +15,7 @@ import { RulesPage } from '../detections/pages/detection_engine/rules'; import { CreateRulePage } from '../detections/pages/detection_engine/rules/create'; import { RuleDetailsPage } from '../detections/pages/detection_engine/rules/details'; import { EditRulePage } from '../detections/pages/detection_engine/rules/edit'; +import { useReadonlyHeader } from '../use_readonly_header'; const RulesSubRoutes = [ { @@ -38,18 +40,26 @@ const RulesSubRoutes = [ }, ]; -const renderRulesRoutes = () => ( - - - {RulesSubRoutes.map((route, index) => ( - - - - ))} - - - -); +const RulesContainerComponent: React.FC = () => { + useReadonlyHeader(i18n.READ_ONLY_BADGE_TOOLTIP); + + return ( + + + {RulesSubRoutes.map((route, index) => ( + + + + ))} + + + + ); +}; + +const Rules = React.memo(RulesContainerComponent); + +const renderRulesRoutes = () => ; export const routes = [ { diff --git a/x-pack/plugins/security_solution/public/rules/translations.ts b/x-pack/plugins/security_solution/public/rules/translations.ts new file mode 100644 index 00000000000000..2d2c5de70dba9b --- /dev/null +++ b/x-pack/plugins/security_solution/public/rules/translations.ts @@ -0,0 +1,15 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const READ_ONLY_BADGE_TOOLTIP = i18n.translate( + 'xpack.securitySolution.rules.badge.readOnly.tooltip', + { + defaultMessage: 'Unable to create, edit or delete rules', + } +); diff --git a/x-pack/plugins/security_solution/public/translations.ts b/x-pack/plugins/security_solution/public/translations.ts new file mode 100644 index 00000000000000..18c6ceea1f8377 --- /dev/null +++ b/x-pack/plugins/security_solution/public/translations.ts @@ -0,0 +1,12 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const READ_ONLY_BADGE_TEXT = i18n.translate('xpack.securitySolution.badge.readOnly.text', { + defaultMessage: 'Read only', +}); diff --git a/x-pack/plugins/security_solution/public/use_readonly_header.ts b/x-pack/plugins/security_solution/public/use_readonly_header.ts new file mode 100644 index 00000000000000..d48855b3971055 --- /dev/null +++ b/x-pack/plugins/security_solution/public/use_readonly_header.ts @@ -0,0 +1,37 @@ +/* + * 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 { useEffect } from 'react'; + +import * as i18n from './translations'; +import { useKibana } from './common/lib/kibana'; +import { useAlertsPrivileges } from './detections/containers/detection_engine/alerts/use_alerts_privileges'; + +/** + * This component places a read-only icon badge in the header + * if user only has read *Kibana* privileges, not individual data index + * privileges + */ +export function useReadonlyHeader(tooltip: string) { + const { hasKibanaREAD, hasKibanaCRUD } = useAlertsPrivileges(); + const chrome = useKibana().services.chrome; + + useEffect(() => { + if (hasKibanaREAD && !hasKibanaCRUD) { + chrome.setBadge({ + text: i18n.READ_ONLY_BADGE_TEXT, + tooltip, + iconType: 'glasses', + }); + } + + // remove the icon after the component unmounts + return () => { + chrome.setBadge(); + }; + }, [chrome, hasKibanaREAD, hasKibanaCRUD, tooltip]); +} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6f4ed3f3cb553e..e0e235c501acf4 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -22845,7 +22845,6 @@ "xpack.securitySolution.alertDetails.summary.readLess": "表示を減らす", "xpack.securitySolution.alertDetails.summary.readMore": "続きを読む", "xpack.securitySolution.alertDetails.threatIntel": "Threat Intel", - "xpack.securitySolution.alerts.badge.readOnly.text": "読み取り専用", "xpack.securitySolution.alerts.badge.readOnly.tooltip": "アラートを更新できません", "xpack.securitySolution.alerts.riskScoreMapping.defaultDescriptionLabel": "このルールで生成されたすべてのアラートのリスクスコアを選択します。", "xpack.securitySolution.alerts.riskScoreMapping.defaultRiskScoreTitle": "デフォルトリスクスコア", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 277a7645f00af5..bab3d7d83d9e9b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -22874,7 +22874,6 @@ "xpack.securitySolution.alertDetails.summary.readLess": "阅读更少内容", "xpack.securitySolution.alertDetails.summary.readMore": "阅读更多内容", "xpack.securitySolution.alertDetails.threatIntel": "威胁情报", - "xpack.securitySolution.alerts.badge.readOnly.text": "只读", "xpack.securitySolution.alerts.badge.readOnly.tooltip": "无法更新告警", "xpack.securitySolution.alerts.riskScoreMapping.defaultDescriptionLabel": "选择此规则生成的所有告警的风险分数。", "xpack.securitySolution.alerts.riskScoreMapping.defaultRiskScoreTitle": "默认风险分数",