[Security Solution][Rules] - Remove rule selection for read only users (#126827) (#127097)

Resolves #126328 and #126314.

(cherry picked from commit 18a5344082)

Co-authored-by: Yara Tercero <yctercero@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2022-03-08 15:06:39 -05:00 committed by GitHub
parent eb178a860b
commit c2d8771925
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 175 additions and 62 deletions

View file

@ -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(() => {

View file

@ -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');
});
});
});

View file

@ -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 = () => (
<TrackApplicationView viewId={SecurityPageName.alerts}>
@ -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 (
<Switch>

View file

@ -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',
{

View file

@ -390,7 +390,7 @@ export const RulesTables = React.memo<RulesTableProps>(
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

View file

@ -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 (
<Switch>
<Route path={EXCEPTIONS_PATH} exact component={ExceptionsRoutes} />
@ -31,6 +35,10 @@ const renderExceptionsRoutes = () => {
);
};
const Exceptions = React.memo(ExceptionsContainerComponent);
const renderExceptionsRoutes = () => <Exceptions />;
export const routes = [
{
path: EXCEPTIONS_PATH,

View file

@ -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',
}
);

View file

@ -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 = () => (
<TrackApplicationView viewId={SecurityPageName.rules}>
<Switch>
{RulesSubRoutes.map((route, index) => (
<Route key={`rules-route-${route.path}`} path={route.path} exact={route?.exact ?? false}>
<route.main />
</Route>
))}
<Route component={NotFoundPage} />
</Switch>
</TrackApplicationView>
);
const RulesContainerComponent: React.FC = () => {
useReadonlyHeader(i18n.READ_ONLY_BADGE_TOOLTIP);
return (
<TrackApplicationView viewId={SecurityPageName.rules}>
<Switch>
{RulesSubRoutes.map((route, index) => (
<Route key={`rules-route-${route.path}`} path={route.path} exact={route?.exact ?? false}>
<route.main />
</Route>
))}
<Route component={NotFoundPage} />
</Switch>
</TrackApplicationView>
);
};
const Rules = React.memo(RulesContainerComponent);
const renderRulesRoutes = () => <Rules />;
export const routes = [
{

View file

@ -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',
}
);

View file

@ -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',
});

View file

@ -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]);
}

View file

@ -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": "デフォルトリスクスコア",

View file

@ -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": "默认风险分数",