Skip to content

Commit

Permalink
[SecuritySolution][Detections] Fix "Closing a signal silently fails w…
Browse files Browse the repository at this point in the history
…ith reduced privileges" (#86908)

## Summary

This PR introduces the following changes. If the user has insufficient write privileges on the signals index:

- we disable the status-changing actions on detection alerts ("Open alert", "Close Alert", "Mark in progress") in the context menu of an alert in alerts table
- we make sure to show the corresponding callout that tells about read-only access to detection alerts
- in the callout we provide links to docs for understanding why/how to fix
  • Loading branch information
banderror authored Jan 5, 2021
1 parent af4658f commit 933c6a5
Show file tree
Hide file tree
Showing 18 changed files with 265 additions and 66 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React, { FC, memo } from 'react';
import { useKibana } from '../../lib/kibana';
import { ExternalLink } from './external_link';
import { COMMON_ARIA_LABEL_ENDING } from './links_translations';

interface DocLinkProps {
guidePath?: string;
docPath: string;
linkText: string;
}

const DocLink: FC<DocLinkProps> = ({ guidePath = 'security', docPath, linkText }) => {
const { services } = useKibana();
const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = services.docLinks;

const url = `${ELASTIC_WEBSITE_URL}guide/en/${guidePath}/${DOC_LINK_VERSION}/${docPath}`;
const ariaLabel = `${linkText} - ${COMMON_ARIA_LABEL_ENDING}`;

return <ExternalLink url={url} text={linkText} ariaLabel={ariaLabel} />;
};

/**
* A simple text link to documentation.
*/
const DocLinkWrapper = memo(DocLink);

export { DocLinkWrapper as DocLink };
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React, { FC } from 'react';
import { EuiLink } from '@elastic/eui';

interface ExternalLinkProps {
url: string;
text: string;
ariaLabel?: string;
}

/**
* A simplistic text link for opening external urls in a new browser tab.
*/
export const ExternalLink: FC<ExternalLinkProps> = ({ url, text, ariaLabel }) => {
return (
<EuiLink href={url} aria-label={ariaLabel} external target="_blank" rel="noopener">
{text}
</EuiLink>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

export * from './links_components';
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React from 'react';
import { DocLink } from './doc_link';
import * as i18n from './links_translations';

export const SecuritySolutionRequirementsLink = () => (
<DocLink
docPath={i18n.SOLUTION_REQUIREMENTS_LINK_PATH}
linkText={i18n.SOLUTION_REQUIREMENTS_LINK_TEXT}
/>
);

export const DetectionsRequirementsLink = () => (
<DocLink
docPath={i18n.DETECTIONS_REQUIREMENTS_LINK_PATH}
linkText={i18n.DETECTIONS_REQUIREMENTS_LINK_TEXT}
/>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { i18n } from '@kbn/i18n';

/**
* If a link's text is "Docs", its aria-label will be set to
* "Docs - ${COMMON_ARIA_LABEL_ENDING}".
*/
export const COMMON_ARIA_LABEL_ENDING = i18n.translate(
'xpack.securitySolution.documentationLinks.ariaLabelEnding',
{
defaultMessage: 'click to open documentation in a new tab',
}
);

export const SOLUTION_REQUIREMENTS_LINK_PATH = 'sec-requirements.html';
export const SOLUTION_REQUIREMENTS_LINK_TEXT = i18n.translate(
'xpack.securitySolution.documentationLinks.solutionRequirements.text',
{
defaultMessage: 'Elastic Security system requirements',
}
);

export const DETECTIONS_REQUIREMENTS_LINK_PATH = 'detections-permissions-section.html';
export const DETECTIONS_REQUIREMENTS_LINK_TEXT = i18n.translate(
'xpack.securitySolution.documentationLinks.detectionsRequirements.text',
{
defaultMessage: 'Detections prerequisites and requirements',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
setPopover(false);
}, []);
const [exceptionModalType, setOpenAddExceptionModal] = useState<ExceptionListType | null>(null);
const [{ canUserCRUD, hasIndexWrite }] = useUserData();
const [{ canUserCRUD, hasIndexWrite, hasIndexUpdateDelete }] = useUserData();

const isEndpointAlert = useMemo((): boolean => {
if (ecsRowData == null) {
Expand Down Expand Up @@ -218,7 +218,7 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
data-test-subj="open-alert-status"
id={FILTER_OPEN}
onClick={openAlertActionOnClick}
disabled={!canUserCRUD || !hasIndexWrite}
disabled={!canUserCRUD || !hasIndexUpdateDelete}
>
<EuiText size="m">{i18n.ACTION_OPEN_ALERT}</EuiText>
</EuiContextMenuItem>
Expand Down Expand Up @@ -251,7 +251,7 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
data-test-subj="close-alert-status"
id={FILTER_CLOSED}
onClick={closeAlertActionClick}
disabled={!canUserCRUD || !hasIndexWrite}
disabled={!canUserCRUD || !hasIndexUpdateDelete}
>
<EuiText size="m">{i18n.ACTION_CLOSE_ALERT}</EuiText>
</EuiContextMenuItem>
Expand Down Expand Up @@ -284,7 +284,7 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
data-test-subj="in-progress-alert-status"
id={FILTER_IN_PROGRESS}
onClick={inProgressAlertActionClick}
disabled={!canUserCRUD || !hasIndexWrite}
disabled={!canUserCRUD || !hasIndexUpdateDelete}
>
<EuiText size="m">{i18n.ACTION_IN_PROGRESS_ALERT}</EuiText>
</EuiContextMenuItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@ const readOnlyAccessToAlertsMessage: CallOutMessage = {
type: 'primary',
id: 'read-only-access-to-alerts',
title: i18n.READ_ONLY_ALERTS_CALLOUT_TITLE,
description: <p>{i18n.READ_ONLY_ALERTS_CALLOUT_MSG}</p>,
description: i18n.readOnlyAlertsCallOutBody(),
};

const ReadOnlyAlertsCallOutComponent = () => {
const [{ hasIndexWrite }] = useUserData();
const [{ hasIndexUpdateDelete }] = useUserData();

return (
<CallOutSwitcher
namespace="detections"
condition={hasIndexWrite != null && !hasIndexWrite}
condition={hasIndexUpdateDelete != null && !hasIndexUpdateDelete}
message={readOnlyAccessToAlertsMessage}
/>
);
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
SecuritySolutionRequirementsLink,
DetectionsRequirementsLink,
} from '../../../../common/components/links_to_docs';

export const READ_ONLY_ALERTS_CALLOUT_TITLE = i18n.translate(
'xpack.securitySolution.detectionEngine.readOnlyAlertsCallOut.messageTitle',
{
defaultMessage: 'You cannot change alert states',
}
);

export const readOnlyAlertsCallOutBody = () => (
<FormattedMessage
id="xpack.securitySolution.detectionEngine.readOnlyAlertsCallOut.messageBody.messageDetail"
defaultMessage="{essence} Related documentation: {docs}"
values={{
essence: (
<p>
<FormattedMessage
id="xpack.securitySolution.detectionEngine.readOnlyAlertsCallOut.messageBody.essenceDescription"
defaultMessage="You only have permissions to view alerts. If you need to update alert states (open or close alerts), contact your Kibana administrator."
/>
</p>
),
docs: (
<ul>
<li>
<DetectionsRequirementsLink />
</li>
<li>
<SecuritySolutionRequirementsLink />
</li>
</ul>
),
}}
/>
);
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const readOnlyAccessToRulesMessage: CallOutMessage = {
type: 'primary',
id: 'read-only-access-to-rules',
title: i18n.READ_ONLY_RULES_CALLOUT_TITLE,
description: <p>{i18n.READ_ONLY_RULES_CALLOUT_MSG}</p>,
description: i18n.readOnlyRulesCallOutBody(),
};

const ReadOnlyRulesCallOutComponent = () => {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
SecuritySolutionRequirementsLink,
DetectionsRequirementsLink,
} from '../../../../common/components/links_to_docs';

export const READ_ONLY_RULES_CALLOUT_TITLE = i18n.translate(
'xpack.securitySolution.detectionEngine.readOnlyRulesCallOut.messageTitle',
{
defaultMessage: 'Rule permissions required',
}
);

export const readOnlyRulesCallOutBody = () => (
<FormattedMessage
id="xpack.securitySolution.detectionEngine.readOnlyRulesCallOut.messageBody.messageDetail"
defaultMessage="{essence} Related documentation: {docs}"
values={{
essence: (
<p>
<FormattedMessage
id="xpack.securitySolution.detectionEngine.readOnlyRulesCallOut.messageBody.essenceDescription"
defaultMessage="You are currently missing the required permissions to create/edit detection engine rule. Please contact your administrator for further assistance."
/>
</p>
),
docs: (
<ul>
<li>
<DetectionsRequirementsLink />
</li>
<li>
<SecuritySolutionRequirementsLink />
</li>
</ul>
),
}}
/>
);
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ describe('useUserInfo', () => {
hasEncryptionKey: null,
hasIndexManage: null,
hasIndexWrite: null,
hasIndexUpdateDelete: null,
isAuthenticated: null,
isSignalIndexExists: null,
loading: true,
Expand Down
Loading

0 comments on commit 933c6a5

Please sign in to comment.