Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 7 commits into from
Mar 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,16 @@
* 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';
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}>
Expand All @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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} />
Expand All @@ -31,6 +35,10 @@ const renderExceptionsRoutes = () => {
);
};

const Exceptions = React.memo(ExceptionsContainerComponent);

const renderExceptionsRoutes = () => <Exceptions />;

export const routes = [
{
path: EXCEPTIONS_PATH,
Expand Down
Original file line number Diff line number Diff line change
@@ -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',
}
);
34 changes: 22 additions & 12 deletions x-pack/plugins/security_solution/public/rules/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
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';
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 = [
{
Expand All @@ -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 = [
{
Expand Down
15 changes: 15 additions & 0 deletions x-pack/plugins/security_solution/public/rules/translations.ts
Original file line number Diff line number Diff line change
@@ -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',
}
);
12 changes: 12 additions & 0 deletions x-pack/plugins/security_solution/public/translations.ts
Original file line number Diff line number Diff line change
@@ -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',
});
37 changes: 37 additions & 0 deletions x-pack/plugins/security_solution/public/use_readonly_header.ts
Original file line number Diff line number Diff line change
@@ -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]);
}
1 change: 0 additions & 1 deletion x-pack/plugins/translations/translations/ja-JP.json
Original file line number Diff line number Diff line change
Expand Up @@ -22657,7 +22657,6 @@
"xpack.securitySolution.alertDetails.summary.readLess": "表示を減らす",
"xpack.securitySolution.alertDetails.summary.readMore": "続きを読む",
"xpack.securitySolution.alertDetails.threatIntel": "Threat Intel",
"xpack.securitySolution.alerts.badge.readOnly.text": "読み取り専用",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to do anything about these translations? It looks like only the path has changed from xpack.securitySolution.alerts.badge.readOnly.text to xpack.securitySolution.badge.readOnly.text, but I don't know if that means the translation might still need to be updated.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I beliiieve that if the path changes it is treated as a new translation needed. I'll double check there.

"xpack.securitySolution.alerts.badge.readOnly.tooltip": "アラートを更新できません",
"xpack.securitySolution.alerts.riskScoreMapping.defaultDescriptionLabel": "このルールで生成されたすべてのアラートのリスクスコアを選択します。",
"xpack.securitySolution.alerts.riskScoreMapping.defaultRiskScoreTitle": "デフォルトリスクスコア",
Expand Down
1 change: 0 additions & 1 deletion x-pack/plugins/translations/translations/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -22686,7 +22686,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": "默认风险分数",
Expand Down