From 3709de64d614533571da8e190e751c51f3484073 Mon Sep 17 00:00:00 2001
From: Joe Reuter
Date: Wed, 22 Jul 2020 12:14:59 +0200
Subject: [PATCH 01/49] [Lens] Legend config (#70619)
---
.../workspace_panel_wrapper.tsx | 2 +-
.../pie_visualization/pie_visualization.tsx | 6 +-
.../pie_visualization/register_expression.tsx | 6 +
.../render_function.test.tsx | 7 +
.../pie_visualization/render_function.tsx | 3 +
.../pie_visualization/settings_widget.scss | 3 -
.../pie_visualization/settings_widget.tsx | 230 --------------
.../public/pie_visualization/to_expression.ts | 1 +
.../public/pie_visualization/toolbar.scss | 3 +
.../lens/public/pie_visualization/toolbar.tsx | 281 ++++++++++++++++++
.../lens/public/pie_visualization/types.ts | 1 +
.../__snapshots__/to_expression.test.ts.snap | 1 +
.../public/xy_visualization/to_expression.ts | 3 +
.../lens/public/xy_visualization/types.ts | 16 +
.../xy_visualization/xy_config_panel.tsx | 94 ++++++
.../xy_visualization/xy_expression.test.tsx | 67 +++++
.../public/xy_visualization/xy_expression.tsx | 6 +-
.../translations/translations/ja-JP.json | 6 +-
.../translations/translations/zh-CN.json | 6 +-
19 files changed, 498 insertions(+), 244 deletions(-)
delete mode 100644 x-pack/plugins/lens/public/pie_visualization/settings_widget.scss
delete mode 100644 x-pack/plugins/lens/public/pie_visualization/settings_widget.tsx
create mode 100644 x-pack/plugins/lens/public/pie_visualization/toolbar.scss
create mode 100644 x-pack/plugins/lens/public/pie_visualization/toolbar.tsx
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx
index f6e15002ca66c..411488abce77f 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx
@@ -63,7 +63,7 @@ export function WorkspacePanelWrapper({
clearStagedPreview: false,
});
},
- [dispatch]
+ [dispatch, activeVisualization]
);
return (
<>
diff --git a/x-pack/plugins/lens/public/pie_visualization/pie_visualization.tsx b/x-pack/plugins/lens/public/pie_visualization/pie_visualization.tsx
index 4f0c081d8be00..369ab28293fbc 100644
--- a/x-pack/plugins/lens/public/pie_visualization/pie_visualization.tsx
+++ b/x-pack/plugins/lens/public/pie_visualization/pie_visualization.tsx
@@ -13,7 +13,7 @@ import { toExpression, toPreviewExpression } from './to_expression';
import { LayerState, PieVisualizationState } from './types';
import { suggestions } from './suggestions';
import { CHART_NAMES, MAX_PIE_BUCKETS, MAX_TREEMAP_BUCKETS } from './constants';
-import { SettingsWidget } from './settings_widget';
+import { PieToolbar } from './toolbar';
function newLayerState(layerId: string): LayerState {
return {
@@ -204,10 +204,10 @@ export const pieVisualization: Visualization
-
+
,
domElement
);
diff --git a/x-pack/plugins/lens/public/pie_visualization/register_expression.tsx b/x-pack/plugins/lens/public/pie_visualization/register_expression.tsx
index cea84db8b2794..89d93ab79233f 100644
--- a/x-pack/plugins/lens/public/pie_visualization/register_expression.tsx
+++ b/x-pack/plugins/lens/public/pie_visualization/register_expression.tsx
@@ -7,6 +7,7 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { i18n } from '@kbn/i18n';
+import { Position } from '@elastic/charts';
import { I18nProvider } from '@kbn/i18n/react';
import {
IInterpreterRenderHandlers,
@@ -73,6 +74,11 @@ export const pie: ExpressionFunctionDefinition<
types: ['boolean'],
help: '',
},
+ legendPosition: {
+ types: ['string'],
+ options: [Position.Top, Position.Right, Position.Bottom, Position.Left],
+ help: '',
+ },
percentDecimals: {
types: ['number'],
help: '',
diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx
index cfbeb27efb3d0..38ef44a2fef18 100644
--- a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx
+++ b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx
@@ -65,6 +65,13 @@ describe('PieVisualization component', () => {
};
}
+ test('it shows legend on correct side', () => {
+ const component = shallow(
+
+ );
+ expect(component.find(Settings).prop('legendPosition')).toEqual('top');
+ });
+
test('it shows legend for 2 groups using default legendDisplay', () => {
const component = shallow();
expect(component.find(Settings).prop('showLegend')).toEqual(true);
diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx
index f349cc4dfd648..79986986b217d 100644
--- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx
+++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx
@@ -22,6 +22,7 @@ import {
PartitionFillLabel,
RecursivePartial,
LayerValue,
+ Position,
} from '@elastic/charts';
import { FormatFactory, LensFilterEvent } from '../types';
import { VisualizationContainer } from '../visualization_container';
@@ -55,6 +56,7 @@ export function PieComponent(
numberDisplay,
categoryDisplay,
legendDisplay,
+ legendPosition,
nestedLegend,
percentDecimals,
hideLabels,
@@ -237,6 +239,7 @@ export function PieComponent(
(legendDisplay === 'show' ||
(legendDisplay === 'default' && columnGroups.length > 1 && shape !== 'treemap'))
}
+ legendPosition={legendPosition || Position.Right}
legendMaxDepth={nestedLegend ? undefined : 1 /* Color is based only on first layer */}
onElementClick={(args) => {
const context = getFilterContext(
diff --git a/x-pack/plugins/lens/public/pie_visualization/settings_widget.scss b/x-pack/plugins/lens/public/pie_visualization/settings_widget.scss
deleted file mode 100644
index 4fa328d8a904d..0000000000000
--- a/x-pack/plugins/lens/public/pie_visualization/settings_widget.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-.lnsPieSettingsWidget {
- min-width: $euiSizeXL * 10;
-}
diff --git a/x-pack/plugins/lens/public/pie_visualization/settings_widget.tsx b/x-pack/plugins/lens/public/pie_visualization/settings_widget.tsx
deleted file mode 100644
index e5fde12f74b42..0000000000000
--- a/x-pack/plugins/lens/public/pie_visualization/settings_widget.tsx
+++ /dev/null
@@ -1,230 +0,0 @@
-/*
- * 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 {
- EuiForm,
- EuiFormRow,
- EuiSuperSelect,
- EuiRange,
- EuiSwitch,
- EuiHorizontalRule,
- EuiSpacer,
- EuiButtonGroup,
-} from '@elastic/eui';
-import { DEFAULT_PERCENT_DECIMALS } from './constants';
-import { PieVisualizationState, SharedLayerState } from './types';
-import { VisualizationLayerWidgetProps } from '../types';
-import './settings_widget.scss';
-
-const numberOptions: Array<{ value: SharedLayerState['numberDisplay']; inputDisplay: string }> = [
- {
- value: 'hidden',
- inputDisplay: i18n.translate('xpack.lens.pieChart.hiddenNumbersLabel', {
- defaultMessage: 'Hide from chart',
- }),
- },
- {
- value: 'percent',
- inputDisplay: i18n.translate('xpack.lens.pieChart.showPercentValuesLabel', {
- defaultMessage: 'Show percent',
- }),
- },
- {
- value: 'value',
- inputDisplay: i18n.translate('xpack.lens.pieChart.showFormatterValuesLabel', {
- defaultMessage: 'Show value',
- }),
- },
-];
-
-const categoryOptions: Array<{
- value: SharedLayerState['categoryDisplay'];
- inputDisplay: string;
-}> = [
- {
- value: 'default',
- inputDisplay: i18n.translate('xpack.lens.pieChart.showCategoriesLabel', {
- defaultMessage: 'Inside or outside',
- }),
- },
- {
- value: 'inside',
- inputDisplay: i18n.translate('xpack.lens.pieChart.fitInsideOnlyLabel', {
- defaultMessage: 'Inside only',
- }),
- },
- {
- value: 'hide',
- inputDisplay: i18n.translate('xpack.lens.pieChart.categoriesInLegendLabel', {
- defaultMessage: 'Hide labels',
- }),
- },
-];
-
-const categoryOptionsTreemap: Array<{
- value: SharedLayerState['categoryDisplay'];
- inputDisplay: string;
-}> = [
- {
- value: 'default',
- inputDisplay: i18n.translate('xpack.lens.pieChart.showTreemapCategoriesLabel', {
- defaultMessage: 'Show labels',
- }),
- },
- {
- value: 'hide',
- inputDisplay: i18n.translate('xpack.lens.pieChart.categoriesInLegendLabel', {
- defaultMessage: 'Hide labels',
- }),
- },
-];
-
-const legendOptions: Array<{
- value: SharedLayerState['legendDisplay'];
- label: string;
- id: string;
-}> = [
- {
- id: 'pieLegendDisplay-default',
- value: 'default',
- label: i18n.translate('xpack.lens.pieChart.defaultLegendLabel', {
- defaultMessage: 'auto',
- }),
- },
- {
- id: 'pieLegendDisplay-show',
- value: 'show',
- label: i18n.translate('xpack.lens.pieChart.alwaysShowLegendLabel', {
- defaultMessage: 'show',
- }),
- },
- {
- id: 'pieLegendDisplay-hide',
- value: 'hide',
- label: i18n.translate('xpack.lens.pieChart.hideLegendLabel', {
- defaultMessage: 'hide',
- }),
- },
-];
-
-export function SettingsWidget(props: VisualizationLayerWidgetProps) {
- const { state, setState } = props;
- const layer = state.layers[0];
- if (!layer) {
- return null;
- }
-
- return (
-
-
- {
- setState({
- ...state,
- layers: [{ ...layer, categoryDisplay: option }],
- });
- }}
- />
-
-
- {
- setState({
- ...state,
- layers: [{ ...layer, numberDisplay: option }],
- });
- }}
- />
-
-
-
- {
- setState({
- ...state,
- layers: [{ ...layer, percentDecimals: Number(e.currentTarget.value) }],
- });
- }}
- />
-
-
-
-
- value === layer.legendDisplay)!.id}
- onChange={(optionId) => {
- setState({
- ...state,
- layers: [
- {
- ...layer,
- legendDisplay: legendOptions.find(({ id }) => id === optionId)!.value,
- },
- ],
- });
- }}
- buttonSize="compressed"
- isFullWidth
- />
-
-
- {
- setState({ ...state, layers: [{ ...layer, nestedLegend: !layer.nestedLegend }] });
- }}
- />
-
-
-
- );
-}
diff --git a/x-pack/plugins/lens/public/pie_visualization/to_expression.ts b/x-pack/plugins/lens/public/pie_visualization/to_expression.ts
index cf9d311dfd504..fbc47e8bfb00f 100644
--- a/x-pack/plugins/lens/public/pie_visualization/to_expression.ts
+++ b/x-pack/plugins/lens/public/pie_visualization/to_expression.ts
@@ -41,6 +41,7 @@ function expressionHelper(
numberDisplay: [layer.numberDisplay],
categoryDisplay: [layer.categoryDisplay],
legendDisplay: [layer.legendDisplay],
+ legendPosition: [layer.legendPosition || 'right'],
percentDecimals: [layer.percentDecimals ?? DEFAULT_PERCENT_DECIMALS],
nestedLegend: [!!layer.nestedLegend],
},
diff --git a/x-pack/plugins/lens/public/pie_visualization/toolbar.scss b/x-pack/plugins/lens/public/pie_visualization/toolbar.scss
new file mode 100644
index 0000000000000..3cfbe6480c61b
--- /dev/null
+++ b/x-pack/plugins/lens/public/pie_visualization/toolbar.scss
@@ -0,0 +1,3 @@
+.lnsPieToolbar__popover {
+ width: $euiFormMaxWidth;
+}
diff --git a/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx b/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx
new file mode 100644
index 0000000000000..9c3d0d0f34814
--- /dev/null
+++ b/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx
@@ -0,0 +1,281 @@
+/*
+ * 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 './toolbar.scss';
+import React, { useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiPopover,
+ EuiSelect,
+ EuiFormRow,
+ EuiSuperSelect,
+ EuiRange,
+ EuiSwitch,
+ EuiHorizontalRule,
+ EuiSpacer,
+ EuiButtonGroup,
+} from '@elastic/eui';
+import { Position } from '@elastic/charts';
+import { DEFAULT_PERCENT_DECIMALS } from './constants';
+import { PieVisualizationState, SharedLayerState } from './types';
+import { VisualizationToolbarProps } from '../types';
+import { ToolbarButton } from '../toolbar_button';
+
+const numberOptions: Array<{ value: SharedLayerState['numberDisplay']; inputDisplay: string }> = [
+ {
+ value: 'hidden',
+ inputDisplay: i18n.translate('xpack.lens.pieChart.hiddenNumbersLabel', {
+ defaultMessage: 'Hide from chart',
+ }),
+ },
+ {
+ value: 'percent',
+ inputDisplay: i18n.translate('xpack.lens.pieChart.showPercentValuesLabel', {
+ defaultMessage: 'Show percent',
+ }),
+ },
+ {
+ value: 'value',
+ inputDisplay: i18n.translate('xpack.lens.pieChart.showFormatterValuesLabel', {
+ defaultMessage: 'Show value',
+ }),
+ },
+];
+
+const categoryOptions: Array<{
+ value: SharedLayerState['categoryDisplay'];
+ inputDisplay: string;
+}> = [
+ {
+ value: 'default',
+ inputDisplay: i18n.translate('xpack.lens.pieChart.showCategoriesLabel', {
+ defaultMessage: 'Inside or outside',
+ }),
+ },
+ {
+ value: 'inside',
+ inputDisplay: i18n.translate('xpack.lens.pieChart.fitInsideOnlyLabel', {
+ defaultMessage: 'Inside only',
+ }),
+ },
+ {
+ value: 'hide',
+ inputDisplay: i18n.translate('xpack.lens.pieChart.categoriesInLegendLabel', {
+ defaultMessage: 'Hide labels',
+ }),
+ },
+];
+
+const categoryOptionsTreemap: Array<{
+ value: SharedLayerState['categoryDisplay'];
+ inputDisplay: string;
+}> = [
+ {
+ value: 'default',
+ inputDisplay: i18n.translate('xpack.lens.pieChart.showTreemapCategoriesLabel', {
+ defaultMessage: 'Show labels',
+ }),
+ },
+ {
+ value: 'hide',
+ inputDisplay: i18n.translate('xpack.lens.pieChart.categoriesInLegendLabel', {
+ defaultMessage: 'Hide labels',
+ }),
+ },
+];
+
+const legendOptions: Array<{
+ value: SharedLayerState['legendDisplay'];
+ label: string;
+ id: string;
+}> = [
+ {
+ id: 'pieLegendDisplay-default',
+ value: 'default',
+ label: i18n.translate('xpack.lens.pieChart.legendVisibility.auto', {
+ defaultMessage: 'auto',
+ }),
+ },
+ {
+ id: 'pieLegendDisplay-show',
+ value: 'show',
+ label: i18n.translate('xpack.lens.pieChart.legendVisibility.show', {
+ defaultMessage: 'show',
+ }),
+ },
+ {
+ id: 'pieLegendDisplay-hide',
+ value: 'hide',
+ label: i18n.translate('xpack.lens.pieChart.legendVisibility.hide', {
+ defaultMessage: 'hide',
+ }),
+ },
+];
+
+export function PieToolbar(props: VisualizationToolbarProps) {
+ const [open, setOpen] = useState(false);
+ const { state, setState } = props;
+ const layer = state.layers[0];
+ if (!layer) {
+ return null;
+ }
+ return (
+
+
+ {
+ setOpen(!open);
+ }}
+ >
+ {i18n.translate('xpack.lens.pieChart.settingsLabel', { defaultMessage: 'Settings' })}
+
+ }
+ isOpen={open}
+ closePopover={() => {
+ setOpen(false);
+ }}
+ anchorPosition="downRight"
+ >
+
+ {
+ setState({
+ ...state,
+ layers: [{ ...layer, categoryDisplay: option }],
+ });
+ }}
+ />
+
+
+ {
+ setState({
+ ...state,
+ layers: [{ ...layer, numberDisplay: option }],
+ });
+ }}
+ />
+
+
+
+ {
+ setState({
+ ...state,
+ layers: [{ ...layer, percentDecimals: Number(e.currentTarget.value) }],
+ });
+ }}
+ />
+
+
+
+
+ value === layer.legendDisplay)!.id}
+ onChange={(optionId) => {
+ setState({
+ ...state,
+ layers: [
+ {
+ ...layer,
+ legendDisplay: legendOptions.find(({ id }) => id === optionId)!.value,
+ },
+ ],
+ });
+ }}
+ buttonSize="compressed"
+ isFullWidth
+ />
+
+
+ {
+ setState({ ...state, layers: [{ ...layer, nestedLegend: !layer.nestedLegend }] });
+ }}
+ />
+
+
+
+ {
+ setState({
+ ...state,
+ layers: [{ ...layer, legendPosition: e.target.value as Position }],
+ });
+ }}
+ />
+
+
+
+
+ );
+}
diff --git a/x-pack/plugins/lens/public/pie_visualization/types.ts b/x-pack/plugins/lens/public/pie_visualization/types.ts
index 60b6564248640..74156bce2ea70 100644
--- a/x-pack/plugins/lens/public/pie_visualization/types.ts
+++ b/x-pack/plugins/lens/public/pie_visualization/types.ts
@@ -13,6 +13,7 @@ export interface SharedLayerState {
numberDisplay: 'hidden' | 'percent' | 'value';
categoryDisplay: 'default' | 'inside' | 'hide';
legendDisplay: 'default' | 'show' | 'hide';
+ legendPosition?: 'left' | 'right' | 'top' | 'bottom';
nestedLegend?: boolean;
percentDecimals?: number;
}
diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap
index d7d76bdd1f44a..b5783803b803c 100644
--- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap
+++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap
@@ -64,6 +64,7 @@ Object {
"position": Array [
"bottom",
],
+ "showSingleSeries": Array [],
},
"function": "lens_xy_legendConfig",
"type": "function",
diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts
index 3b9406cedd499..b17704b38cdec 100644
--- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts
+++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts
@@ -127,6 +127,9 @@ export const buildExpression = (
function: 'lens_xy_legendConfig',
arguments: {
isVisible: [state.legend.isVisible],
+ showSingleSeries: state.legend.showSingleSeries
+ ? [state.legend.showSingleSeries]
+ : [],
position: [state.legend.position],
},
},
diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts
index 08f29c65b26d0..605119535d1f0 100644
--- a/x-pack/plugins/lens/public/xy_visualization/types.ts
+++ b/x-pack/plugins/lens/public/xy_visualization/types.ts
@@ -19,8 +19,18 @@ import { VisualizationType } from '../index';
import { FittingFunction } from './fitting_functions';
export interface LegendConfig {
+ /**
+ * Flag whether the legend should be shown. If there is just a single series, it will be hidden
+ */
isVisible: boolean;
+ /**
+ * Position of the legend relative to the chart
+ */
position: Position;
+ /**
+ * Flag whether the legend should be shown even with just a single series
+ */
+ showSingleSeries?: boolean;
}
type LegendConfigResult = LegendConfig & { type: 'lens_xy_legendConfig' };
@@ -50,6 +60,12 @@ export const legendConfig: ExpressionFunctionDefinition<
defaultMessage: 'Specifies the legend position.',
}),
},
+ showSingleSeries: {
+ types: ['boolean'],
+ help: i18n.translate('xpack.lens.xyChart.showSingleSeries.help', {
+ defaultMessage: 'Specifies whether a legend with just a single entry should be shown',
+ }),
+ },
},
fn: function fn(input: unknown, args: LegendConfig) {
return {
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx
index d22b3ec0a44a6..59c4b393df467 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx
@@ -7,6 +7,7 @@
import './xy_config_panel.scss';
import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
+import { Position } from '@elastic/charts';
import { debounce } from 'lodash';
import {
EuiButtonGroup,
@@ -16,12 +17,14 @@ import {
EuiFormRow,
EuiPopover,
EuiText,
+ EuiSelect,
htmlIdGenerator,
EuiForm,
EuiColorPicker,
EuiColorPickerProps,
EuiToolTip,
EuiIcon,
+ EuiHorizontalRule,
} from '@elastic/eui';
import {
VisualizationLayerWidgetProps,
@@ -46,6 +49,30 @@ function updateLayer(state: State, layer: UnwrapArray, index: n
};
}
+const legendOptions: Array<{ id: string; value: 'auto' | 'show' | 'hide'; label: string }> = [
+ {
+ id: `xy_legend_auto`,
+ value: 'auto',
+ label: i18n.translate('xpack.lens.xyChart.legendVisibility.auto', {
+ defaultMessage: 'auto',
+ }),
+ },
+ {
+ id: `xy_legend_show`,
+ value: 'show',
+ label: i18n.translate('xpack.lens.xyChart.legendVisibility.show', {
+ defaultMessage: 'show',
+ }),
+ },
+ {
+ id: `xy_legend_hide`,
+ value: 'hide',
+ label: i18n.translate('xpack.lens.xyChart.legendVisibility.hide', {
+ defaultMessage: 'hide',
+ }),
+ },
+];
+
export function LayerContextMenu(props: VisualizationLayerWidgetProps) {
const { state, layerId } = props;
const horizontalOnly = isHorizontalChart(state.layers);
@@ -95,6 +122,12 @@ export function XyToolbar(props: VisualizationToolbarProps) {
const hasNonBarSeries = props.state?.layers.some(
(layer) => layer.seriesType === 'line' || layer.seriesType === 'area'
);
+ const legendMode =
+ props.state?.legend.isVisible && !props.state?.legend.showSingleSeries
+ ? 'auto'
+ : !props.state?.legend.isVisible
+ ? 'hide'
+ : 'show';
return (
@@ -157,6 +190,67 @@ export function XyToolbar(props: VisualizationToolbarProps) {
/>
+
+
+ value === legendMode)!.id}
+ onChange={(optionId) => {
+ const newMode = legendOptions.find(({ id }) => id === optionId)!.value;
+ if (newMode === 'auto') {
+ props.setState({
+ ...props.state,
+ legend: { ...props.state.legend, isVisible: true, showSingleSeries: false },
+ });
+ } else if (newMode === 'show') {
+ props.setState({
+ ...props.state,
+ legend: { ...props.state.legend, isVisible: true, showSingleSeries: true },
+ });
+ } else if (newMode === 'hide') {
+ props.setState({
+ ...props.state,
+ legend: { ...props.state.legend, isVisible: false, showSingleSeries: false },
+ });
+ }
+ }}
+ />
+
+
+ {
+ props.setState({
+ ...props.state,
+ legend: { ...props.state.legend, position: e.target.value as Position },
+ });
+ }}
+ />
+
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx
index b7a50b3af640c..c880cbb641e5d 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx
@@ -1556,6 +1556,73 @@ describe('xy_expression', () => {
expect(component.find(Settings).prop('showLegend')).toEqual(true);
});
+ test('it should always show legend if showSingleSeries is set', () => {
+ const { data, args } = sampleArgs();
+
+ const component = shallow(
+
+ );
+
+ expect(component.find(Settings).prop('showLegend')).toEqual(true);
+ });
+
+ test('it not show legend if isVisible is set to false', () => {
+ const { data, args } = sampleArgs();
+
+ const component = shallow(
+
+ );
+
+ expect(component.find(Settings).prop('showLegend')).toEqual(false);
+ });
+
+ test('it should show legend on right side', () => {
+ const { data, args } = sampleArgs();
+
+ const component = shallow(
+
+ );
+
+ expect(component.find(Settings).prop('legendPosition')).toEqual('top');
+ });
+
test('it should apply the fitting function to all non-bar series', () => {
const data: LensMultiTable = {
type: 'lens_multitable',
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx
index 3ab12aa0879b0..871b626d62560 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx
@@ -282,7 +282,11 @@ export function XYChart({
return (
Date: Wed, 22 Jul 2020 12:15:39 +0200
Subject: [PATCH 02/49] do not pass title as part of tsvb request (#72619)
---
src/plugins/visualizations/public/legacy/build_pipeline.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.ts b/src/plugins/visualizations/public/legacy/build_pipeline.ts
index e74a83d91fabf..d3fe814f3b010 100644
--- a/src/plugins/visualizations/public/legacy/build_pipeline.ts
+++ b/src/plugins/visualizations/public/legacy/build_pipeline.ts
@@ -255,7 +255,7 @@ export const buildPipelineVisFunction: BuildPipelineVisFunction = {
input_control_vis: (params) => {
return `input_control_vis ${prepareJson('visConfig', params)}`;
},
- metrics: (params, schemas, uiState = {}) => {
+ metrics: ({ title, ...params }, schemas, uiState = {}) => {
const paramsJson = prepareJson('params', params);
const uiStateJson = prepareJson('uiState', uiState);
From cb0405eeaecbc85986a52462605b54671bd343ee Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Cau=C3=AA=20Marcondes?=
<55978943+cauemarcondes@users.noreply.github.com>
Date: Wed, 22 Jul 2020 13:30:52 +0100
Subject: [PATCH 03/49] [Observability] filter "hasData" api by processor event
(#72810)
* filtering hasdata by processor event
* adding api test
---
.../lib/observability_overview/has_data.ts | 21 +-
.../observability_overview/data.json.gz | Bin 0 -> 377 bytes
.../observability_overview/mappings.json | 4229 +++++++++++++++++
.../apm_api_integration/basic/tests/index.ts | 5 +
.../tests/observability_overview/has_data.ts | 41 +
.../observability_overview.ts | 47 +
6 files changed, 4342 insertions(+), 1 deletion(-)
create mode 100644 x-pack/test/apm_api_integration/basic/fixtures/es_archiver/observability_overview/data.json.gz
create mode 100644 x-pack/test/apm_api_integration/basic/fixtures/es_archiver/observability_overview/mappings.json
create mode 100644 x-pack/test/apm_api_integration/basic/tests/observability_overview/has_data.ts
create mode 100644 x-pack/test/apm_api_integration/basic/tests/observability_overview/observability_overview.ts
diff --git a/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts b/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts
index 73cc2d273ec69..fc7445ab4a225 100644
--- a/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts
+++ b/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts
@@ -3,6 +3,8 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
+import { PROCESSOR_EVENT } from '../../../common/elasticsearch_fieldnames';
+import { ProcessorEvent } from '../../../common/processor_event';
import { Setup } from '../helpers/setup_request';
export async function hasData({ setup }: { setup: Setup }) {
@@ -15,7 +17,24 @@ export async function hasData({ setup }: { setup: Setup }) {
indices['apm_oss.metricsIndices'],
],
terminateAfter: 1,
- size: 0,
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter: [
+ {
+ terms: {
+ [PROCESSOR_EVENT]: [
+ ProcessorEvent.error,
+ ProcessorEvent.metric,
+ ProcessorEvent.transaction,
+ ],
+ },
+ },
+ ],
+ },
+ },
+ },
};
const response = await client.search(params);
diff --git a/x-pack/test/apm_api_integration/basic/fixtures/es_archiver/observability_overview/data.json.gz b/x-pack/test/apm_api_integration/basic/fixtures/es_archiver/observability_overview/data.json.gz
new file mode 100644
index 0000000000000000000000000000000000000000..23602666f3b43ddab67671239a717cd706c6a0f2
GIT binary patch
literal 377
zcmV-<0fzn`iwFoj4j5km17u-zVJ>QOZ*Bl>Qn7BrFcjSR3X~Z~wiBB;Q&p)0U04_@
zstUc>rld;jC=RF<;@@jSQbJ*|Ab{Oun!-Y>O7n>*rXJxj6$9VdeJigW
zJo40)wRRoUO|S_HggK&Og?XN`Jmqmp*t*wyzLstz4{z43E3FA?60;abed%anUS
z{g}p&8^rH<{*h-C<1u6S1Yv{yY_o^yp4a=JwyELEhCs5rw3^mR?VSA|SHF {
+ describe('when data is not loaded', () => {
+ it('returns false when there is no data', async () => {
+ const response = await supertest.get('/api/apm/observability_overview/has_data');
+ expect(response.status).to.be(200);
+ expect(response.body).to.eql(false);
+ });
+ });
+ describe('when only onboarding data is loaded', () => {
+ before(() => esArchiver.load('observability_overview'));
+ after(() => esArchiver.unload('observability_overview'));
+ it('returns false when there is only onboarding data', async () => {
+ const response = await supertest.get('/api/apm/observability_overview/has_data');
+ expect(response.status).to.be(200);
+ expect(response.body).to.eql(false);
+ });
+ });
+ describe('when data is loaded', () => {
+ before(() => esArchiver.load('8.0.0'));
+ after(() => esArchiver.unload('8.0.0'));
+
+ it('returns true when there is at least one document on transaction, error or metrics indices', async () => {
+ const response = await supertest.get('/api/apm/observability_overview/has_data');
+ expect(response.status).to.be(200);
+ expect(response.body).to.eql(true);
+ });
+ });
+ });
+}
diff --git a/x-pack/test/apm_api_integration/basic/tests/observability_overview/observability_overview.ts b/x-pack/test/apm_api_integration/basic/tests/observability_overview/observability_overview.ts
new file mode 100644
index 0000000000000..bd8b0c6126faa
--- /dev/null
+++ b/x-pack/test/apm_api_integration/basic/tests/observability_overview/observability_overview.ts
@@ -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 expect from '@kbn/expect';
+import { FtrProviderContext } from '../../../common/ftr_provider_context';
+
+export default function ApiTest({ getService }: FtrProviderContext) {
+ const supertest = getService('supertest');
+ const esArchiver = getService('esArchiver');
+
+ // url parameters
+ const start = encodeURIComponent('2020-06-29T06:00:00.000Z');
+ const end = encodeURIComponent('2020-06-29T10:00:00.000Z');
+ const bucketSize = '60s';
+
+ describe('Observability overview', () => {
+ describe('when data is not loaded', () => {
+ it('handles the empty state', async () => {
+ const response = await supertest.get(
+ `/api/apm/observability_overview?start=${start}&end=${end}&bucketSize=${bucketSize}`
+ );
+ expect(response.status).to.be(200);
+ expect(response.body).to.eql({ serviceCount: 0, transactionCoordinates: [] });
+ });
+ });
+ describe('when data is loaded', () => {
+ before(() => esArchiver.load('8.0.0'));
+ after(() => esArchiver.unload('8.0.0'));
+
+ it('returns the service count and transaction coordinates', async () => {
+ const response = await supertest.get(
+ `/api/apm/observability_overview?start=${start}&end=${end}&bucketSize=${bucketSize}`
+ );
+ expect(response.status).to.be(200);
+ expect(response.body).to.eql({
+ serviceCount: 3,
+ transactionCoordinates: [
+ { x: 1593413220000, y: 0.016666666666666666 },
+ { x: 1593413280000, y: 1.0458333333333334 },
+ ],
+ });
+ });
+ });
+ });
+}
From a41633d8c5a3581df83f2e95dfcf33f76793fab6 Mon Sep 17 00:00:00 2001
From: Gidi Meir Morris
Date: Wed, 22 Jul 2020 13:39:33 +0100
Subject: [PATCH 04/49] [Task Manager] Addresses flaky test introduced by
buffered store (#72815)
Removed unused functionality which we weren't using anyway and was causing some flaky behaviour.
---
.../server/lib/bulk_operation_buffer.test.ts | 44 +------------------
.../server/lib/bulk_operation_buffer.ts | 11 +----
2 files changed, 3 insertions(+), 52 deletions(-)
diff --git a/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.test.ts b/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.test.ts
index 1994e5f749371..2c6d2b64f5d44 100644
--- a/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.test.ts
+++ b/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.test.ts
@@ -33,7 +33,7 @@ function errorAttempts(task: TaskInstance): Err {
+describe('Bulk Operation Buffer', () => {
describe('createBuffer()', () => {
test('batches up multiple Operation calls', async () => {
const bulkUpdate: jest.Mocked> = jest.fn(
@@ -54,48 +54,6 @@ describe.skip('Bulk Operation Buffer', () => {
expect(bulkUpdate).toHaveBeenCalledWith([task1, task2]);
});
- test('batch updates are executed at most by the next Event Loop tick by default', async () => {
- const bulkUpdate: jest.Mocked> = jest.fn((tasks) => {
- return Promise.resolve(tasks.map(incrementAttempts));
- });
-
- const bufferedUpdate = createBuffer(bulkUpdate);
-
- const task1 = createTask();
- const task2 = createTask();
- const task3 = createTask();
- const task4 = createTask();
- const task5 = createTask();
- const task6 = createTask();
-
- return new Promise((resolve) => {
- Promise.all([bufferedUpdate(task1), bufferedUpdate(task2)]).then((_) => {
- expect(bulkUpdate).toHaveBeenCalledTimes(1);
- expect(bulkUpdate).toHaveBeenCalledWith([task1, task2]);
- expect(bulkUpdate).not.toHaveBeenCalledWith([task3, task4]);
- });
-
- setTimeout(() => {
- // on next tick
- setTimeout(() => {
- // on next tick
- expect(bulkUpdate).toHaveBeenCalledTimes(2);
- Promise.all([bufferedUpdate(task5), bufferedUpdate(task6)]).then((_) => {
- expect(bulkUpdate).toHaveBeenCalledTimes(3);
- expect(bulkUpdate).toHaveBeenCalledWith([task5, task6]);
- resolve();
- });
- }, 0);
-
- expect(bulkUpdate).toHaveBeenCalledTimes(1);
- Promise.all([bufferedUpdate(task3), bufferedUpdate(task4)]).then((_) => {
- expect(bulkUpdate).toHaveBeenCalledTimes(2);
- expect(bulkUpdate).toHaveBeenCalledWith([task3, task4]);
- });
- }, 0);
- });
- });
-
test('batch updates can be customised to execute after a certain period', async () => {
const bulkUpdate: jest.Mocked> = jest.fn((tasks) => {
return Promise.resolve(tasks.map(incrementAttempts));
diff --git a/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.ts b/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.ts
index fca7ce02e0cd7..c8e5b837fa36c 100644
--- a/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.ts
+++ b/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.ts
@@ -93,11 +93,8 @@ export function createBuffer {
setTimeout(resolve, ms);
From 4dcf719edbf74b944b12180030d5540de1f74b47 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Cau=C3=AA=20Marcondes?=
<55978943+cauemarcondes@users.noreply.github.com>
Date: Wed, 22 Jul 2020 14:01:29 +0100
Subject: [PATCH 05/49] Adding api test for transaction_groups /breakdown and
/avg_duration_by_browser (#72623)
* adding api test for transaction_groups /breakdown and /avg_duration_by_browser
* adding filter by transaction name
* adding filter by transaction name
* addressing pr comments
* fixing TS issue
Co-authored-by: Elastic Machine
---
.../avg_duration_by_browser/fetcher.ts | 8 +-
.../avg_duration_by_browser/index.ts | 1 +
.../apm/server/routes/transaction_groups.ts | 3 +-
.../apm_api_integration/basic/tests/index.ts | 2 +
.../avg_duration_by_browser.ts | 53 ++
.../tests/transaction_groups/breakdown.ts | 67 ++
.../expectation/avg_duration_by_browser.json | 735 ++++++++++++++++++
..._duration_by_browser_transaction_name.json | 731 +++++++++++++++++
.../expectation/breakdown.json | 202 +++++
.../breakdown_transaction_name.json | 55 ++
10 files changed, 1855 insertions(+), 2 deletions(-)
create mode 100644 x-pack/test/apm_api_integration/basic/tests/transaction_groups/avg_duration_by_browser.ts
create mode 100644 x-pack/test/apm_api_integration/basic/tests/transaction_groups/breakdown.ts
create mode 100644 x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/avg_duration_by_browser.json
create mode 100644 x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/avg_duration_by_browser_transaction_name.json
create mode 100644 x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown.json
create mode 100644 x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown_transaction_name.json
diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts
index e3d688b694380..b4d98ec41fc2d 100644
--- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts
+++ b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts
@@ -12,6 +12,7 @@ import {
TRANSACTION_TYPE,
USER_AGENT_NAME,
TRANSACTION_DURATION,
+ TRANSACTION_NAME,
} from '../../../../common/elasticsearch_fieldnames';
import { rangeFilter } from '../../../../common/utils/range_filter';
import { getBucketSize } from '../../helpers/get_bucket_size';
@@ -23,15 +24,20 @@ export type ESResponse = PromiseReturnType;
export function fetcher(options: Options) {
const { end, client, indices, start, uiFiltersES } = options.setup;
- const { serviceName } = options;
+ const { serviceName, transactionName } = options;
const { intervalString } = getBucketSize(start, end, 'auto');
+ const transactionNameFilter = transactionName
+ ? [{ term: { [TRANSACTION_NAME]: transactionName } }]
+ : [];
+
const filter: ESFilter[] = [
{ term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } },
{ term: { [SERVICE_NAME]: serviceName } },
{ term: { [TRANSACTION_TYPE]: TRANSACTION_PAGE_LOAD } },
{ range: rangeFilter(start, end) },
...uiFiltersES,
+ ...transactionNameFilter,
];
const params = {
diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/index.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/index.ts
index 000890f52ebe6..e3a0d9e26142a 100644
--- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/index.ts
+++ b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/index.ts
@@ -16,6 +16,7 @@ import { transformer } from './transformer';
export interface Options {
serviceName: string;
setup: Setup & SetupTimeRange & SetupUIFilters;
+ transactionName?: string;
}
export type AvgDurationByBrowserAPIResponse = Array<{
diff --git a/x-pack/plugins/apm/server/routes/transaction_groups.ts b/x-pack/plugins/apm/server/routes/transaction_groups.ts
index 813d757c7c33e..c667ce4f07e93 100644
--- a/x-pack/plugins/apm/server/routes/transaction_groups.ts
+++ b/x-pack/plugins/apm/server/routes/transaction_groups.ts
@@ -164,7 +164,6 @@ export const transactionGroupsAvgDurationByBrowser = createRoute(() => ({
}),
query: t.intersection([
t.partial({
- transactionType: t.string,
transactionName: t.string,
}),
uiFiltersRt,
@@ -174,10 +173,12 @@ export const transactionGroupsAvgDurationByBrowser = createRoute(() => ({
handler: async ({ context, request }) => {
const setup = await setupRequest(context, request);
const { serviceName } = context.params.path;
+ const { transactionName } = context.params.query;
return getTransactionAvgDurationByBrowser({
serviceName,
setup,
+ transactionName,
});
},
}));
diff --git a/x-pack/test/apm_api_integration/basic/tests/index.ts b/x-pack/test/apm_api_integration/basic/tests/index.ts
index a1950f1c0947f..b6a32ace5db56 100644
--- a/x-pack/test/apm_api_integration/basic/tests/index.ts
+++ b/x-pack/test/apm_api_integration/basic/tests/index.ts
@@ -35,6 +35,8 @@ export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderCont
loadTestFile(require.resolve('./transaction_groups/top_transaction_groups'));
loadTestFile(require.resolve('./transaction_groups/transaction_charts'));
loadTestFile(require.resolve('./transaction_groups/error_rate'));
+ loadTestFile(require.resolve('./transaction_groups/breakdown'));
+ loadTestFile(require.resolve('./transaction_groups/avg_duration_by_browser'));
});
describe('Observability overview', function () {
diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/avg_duration_by_browser.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/avg_duration_by_browser.ts
new file mode 100644
index 0000000000000..690935ddc7f6a
--- /dev/null
+++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/avg_duration_by_browser.ts
@@ -0,0 +1,53 @@
+/*
+ * 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 expect from '@kbn/expect';
+import { FtrProviderContext } from '../../../common/ftr_provider_context';
+import expectedAvgDurationByBrowser from './expectation/avg_duration_by_browser.json';
+import expectedAvgDurationByBrowserWithTransactionName from './expectation/avg_duration_by_browser_transaction_name.json';
+
+export default function ApiTest({ getService }: FtrProviderContext) {
+ const supertest = getService('supertest');
+ const esArchiver = getService('esArchiver');
+
+ const start = encodeURIComponent('2020-06-29T06:45:00.000Z');
+ const end = encodeURIComponent('2020-06-29T06:49:00.000Z');
+ const transactionName = '/products';
+ const uiFilters = encodeURIComponent(JSON.stringify({}));
+
+ describe('Average duration by browser', () => {
+ describe('when data is not loaded', () => {
+ it('handles the empty state', async () => {
+ const response = await supertest.get(
+ `/api/apm/services/client/transaction_groups/avg_duration_by_browser?start=${start}&end=${end}&uiFilters=${uiFilters}`
+ );
+ expect(response.status).to.be(200);
+ expect(response.body).to.eql([]);
+ });
+ });
+
+ describe('when data is loaded', () => {
+ before(() => esArchiver.load('8.0.0'));
+ after(() => esArchiver.unload('8.0.0'));
+
+ it('returns the average duration by browser', async () => {
+ const response = await supertest.get(
+ `/api/apm/services/client/transaction_groups/avg_duration_by_browser?start=${start}&end=${end}&uiFilters=${uiFilters}`
+ );
+
+ expect(response.status).to.be(200);
+ expect(response.body).to.eql(expectedAvgDurationByBrowser);
+ });
+ it('returns the average duration by browser filtering by transaction name', async () => {
+ const response = await supertest.get(
+ `/api/apm/services/client/transaction_groups/avg_duration_by_browser?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionName=${transactionName}`
+ );
+
+ expect(response.status).to.be(200);
+ expect(response.body).to.eql(expectedAvgDurationByBrowserWithTransactionName);
+ });
+ });
+ });
+}
diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/breakdown.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/breakdown.ts
new file mode 100644
index 0000000000000..5b61112a374c1
--- /dev/null
+++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/breakdown.ts
@@ -0,0 +1,67 @@
+/*
+ * 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 expect from '@kbn/expect';
+import { FtrProviderContext } from '../../../common/ftr_provider_context';
+import expectedBreakdown from './expectation/breakdown.json';
+import expectedBreakdownWithTransactionName from './expectation/breakdown_transaction_name.json';
+
+export default function ApiTest({ getService }: FtrProviderContext) {
+ const supertest = getService('supertest');
+ const esArchiver = getService('esArchiver');
+
+ const start = encodeURIComponent('2020-06-29T06:45:00.000Z');
+ const end = encodeURIComponent('2020-06-29T06:49:00.000Z');
+ const transactionType = 'request';
+ const transactionName = 'GET /api';
+ const uiFilters = encodeURIComponent(JSON.stringify({}));
+
+ describe('Breakdown', () => {
+ describe('when data is not loaded', () => {
+ it('handles the empty state', async () => {
+ const response = await supertest.get(
+ `/api/apm/services/opbeans-node/transaction_groups/breakdown?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=${transactionType}`
+ );
+ expect(response.status).to.be(200);
+ expect(response.body).to.eql({ kpis: [], timeseries: [] });
+ });
+ });
+
+ describe('when data is loaded', () => {
+ before(() => esArchiver.load('8.0.0'));
+ after(() => esArchiver.unload('8.0.0'));
+
+ it('returns the transaction breakdown for a service', async () => {
+ const response = await supertest.get(
+ `/api/apm/services/opbeans-node/transaction_groups/breakdown?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=${transactionType}`
+ );
+
+ expect(response.status).to.be(200);
+ expect(response.body).to.eql(expectedBreakdown);
+ });
+ it('returns the transaction breakdown for a transaction group', async () => {
+ const response = await supertest.get(
+ `/api/apm/services/opbeans-node/transaction_groups/breakdown?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=${transactionType}&transactionName=${transactionName}`
+ );
+
+ expect(response.status).to.be(200);
+ expect(response.body).to.eql(expectedBreakdownWithTransactionName);
+ });
+ it('returns the top 4 by percentage and sorts them by name', async () => {
+ const response = await supertest.get(
+ `/api/apm/services/opbeans-node/transaction_groups/breakdown?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=${transactionType}`
+ );
+
+ expect(response.status).to.be(200);
+ expect(response.body.kpis.map((kpi: { name: string }) => kpi.name)).to.eql([
+ 'app',
+ 'http',
+ 'postgresql',
+ 'redis',
+ ]);
+ });
+ });
+ });
+}
diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/avg_duration_by_browser.json b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/avg_duration_by_browser.json
new file mode 100644
index 0000000000000..cd53af3bf7080
--- /dev/null
+++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/avg_duration_by_browser.json
@@ -0,0 +1,735 @@
+[
+ {
+ "title":"HeadlessChrome",
+ "data":[
+ {
+ "x":1593413100000
+ },
+ {
+ "x":1593413101000
+ },
+ {
+ "x":1593413102000
+ },
+ {
+ "x":1593413103000
+ },
+ {
+ "x":1593413104000
+ },
+ {
+ "x":1593413105000
+ },
+ {
+ "x":1593413106000
+ },
+ {
+ "x":1593413107000
+ },
+ {
+ "x":1593413108000
+ },
+ {
+ "x":1593413109000
+ },
+ {
+ "x":1593413110000
+ },
+ {
+ "x":1593413111000
+ },
+ {
+ "x":1593413112000
+ },
+ {
+ "x":1593413113000
+ },
+ {
+ "x":1593413114000
+ },
+ {
+ "x":1593413115000
+ },
+ {
+ "x":1593413116000
+ },
+ {
+ "x":1593413117000
+ },
+ {
+ "x":1593413118000
+ },
+ {
+ "x":1593413119000
+ },
+ {
+ "x":1593413120000
+ },
+ {
+ "x":1593413121000
+ },
+ {
+ "x":1593413122000
+ },
+ {
+ "x":1593413123000
+ },
+ {
+ "x":1593413124000
+ },
+ {
+ "x":1593413125000
+ },
+ {
+ "x":1593413126000
+ },
+ {
+ "x":1593413127000
+ },
+ {
+ "x":1593413128000
+ },
+ {
+ "x":1593413129000
+ },
+ {
+ "x":1593413130000
+ },
+ {
+ "x":1593413131000
+ },
+ {
+ "x":1593413132000
+ },
+ {
+ "x":1593413133000
+ },
+ {
+ "x":1593413134000
+ },
+ {
+ "x":1593413135000
+ },
+ {
+ "x":1593413136000
+ },
+ {
+ "x":1593413137000
+ },
+ {
+ "x":1593413138000
+ },
+ {
+ "x":1593413139000
+ },
+ {
+ "x":1593413140000
+ },
+ {
+ "x":1593413141000
+ },
+ {
+ "x":1593413142000
+ },
+ {
+ "x":1593413143000
+ },
+ {
+ "x":1593413144000
+ },
+ {
+ "x":1593413145000
+ },
+ {
+ "x":1593413146000
+ },
+ {
+ "x":1593413147000
+ },
+ {
+ "x":1593413148000
+ },
+ {
+ "x":1593413149000
+ },
+ {
+ "x":1593413150000
+ },
+ {
+ "x":1593413151000
+ },
+ {
+ "x":1593413152000
+ },
+ {
+ "x":1593413153000
+ },
+ {
+ "x":1593413154000
+ },
+ {
+ "x":1593413155000
+ },
+ {
+ "x":1593413156000
+ },
+ {
+ "x":1593413157000
+ },
+ {
+ "x":1593413158000
+ },
+ {
+ "x":1593413159000
+ },
+ {
+ "x":1593413160000
+ },
+ {
+ "x":1593413161000
+ },
+ {
+ "x":1593413162000
+ },
+ {
+ "x":1593413163000
+ },
+ {
+ "x":1593413164000
+ },
+ {
+ "x":1593413165000
+ },
+ {
+ "x":1593413166000
+ },
+ {
+ "x":1593413167000
+ },
+ {
+ "x":1593413168000
+ },
+ {
+ "x":1593413169000
+ },
+ {
+ "x":1593413170000
+ },
+ {
+ "x":1593413171000
+ },
+ {
+ "x":1593413172000
+ },
+ {
+ "x":1593413173000
+ },
+ {
+ "x":1593413174000
+ },
+ {
+ "x":1593413175000
+ },
+ {
+ "x":1593413176000
+ },
+ {
+ "x":1593413177000
+ },
+ {
+ "x":1593413178000
+ },
+ {
+ "x":1593413179000
+ },
+ {
+ "x":1593413180000
+ },
+ {
+ "x":1593413181000
+ },
+ {
+ "x":1593413182000
+ },
+ {
+ "x":1593413183000
+ },
+ {
+ "x":1593413184000
+ },
+ {
+ "x":1593413185000
+ },
+ {
+ "x":1593413186000
+ },
+ {
+ "x":1593413187000
+ },
+ {
+ "x":1593413188000
+ },
+ {
+ "x":1593413189000
+ },
+ {
+ "x":1593413190000
+ },
+ {
+ "x":1593413191000
+ },
+ {
+ "x":1593413192000
+ },
+ {
+ "x":1593413193000
+ },
+ {
+ "x":1593413194000
+ },
+ {
+ "x":1593413195000
+ },
+ {
+ "x":1593413196000
+ },
+ {
+ "x":1593413197000
+ },
+ {
+ "x":1593413198000
+ },
+ {
+ "x":1593413199000
+ },
+ {
+ "x":1593413200000
+ },
+ {
+ "x":1593413201000
+ },
+ {
+ "x":1593413202000
+ },
+ {
+ "x":1593413203000
+ },
+ {
+ "x":1593413204000
+ },
+ {
+ "x":1593413205000
+ },
+ {
+ "x":1593413206000
+ },
+ {
+ "x":1593413207000
+ },
+ {
+ "x":1593413208000
+ },
+ {
+ "x":1593413209000
+ },
+ {
+ "x":1593413210000
+ },
+ {
+ "x":1593413211000
+ },
+ {
+ "x":1593413212000
+ },
+ {
+ "x":1593413213000
+ },
+ {
+ "x":1593413214000
+ },
+ {
+ "x":1593413215000
+ },
+ {
+ "x":1593413216000
+ },
+ {
+ "x":1593413217000
+ },
+ {
+ "x":1593413218000
+ },
+ {
+ "x":1593413219000
+ },
+ {
+ "x":1593413220000
+ },
+ {
+ "x":1593413221000
+ },
+ {
+ "x":1593413222000
+ },
+ {
+ "x":1593413223000
+ },
+ {
+ "x":1593413224000
+ },
+ {
+ "x":1593413225000
+ },
+ {
+ "x":1593413226000
+ },
+ {
+ "x":1593413227000
+ },
+ {
+ "x":1593413228000
+ },
+ {
+ "x":1593413229000
+ },
+ {
+ "x":1593413230000
+ },
+ {
+ "x":1593413231000
+ },
+ {
+ "x":1593413232000
+ },
+ {
+ "x":1593413233000
+ },
+ {
+ "x":1593413234000
+ },
+ {
+ "x":1593413235000
+ },
+ {
+ "x":1593413236000
+ },
+ {
+ "x":1593413237000
+ },
+ {
+ "x":1593413238000
+ },
+ {
+ "x":1593413239000
+ },
+ {
+ "x":1593413240000
+ },
+ {
+ "x":1593413241000
+ },
+ {
+ "x":1593413242000
+ },
+ {
+ "x":1593413243000
+ },
+ {
+ "x":1593413244000
+ },
+ {
+ "x":1593413245000
+ },
+ {
+ "x":1593413246000
+ },
+ {
+ "x":1593413247000
+ },
+ {
+ "x":1593413248000
+ },
+ {
+ "x":1593413249000
+ },
+ {
+ "x":1593413250000
+ },
+ {
+ "x":1593413251000
+ },
+ {
+ "x":1593413252000
+ },
+ {
+ "x":1593413253000
+ },
+ {
+ "x":1593413254000
+ },
+ {
+ "x":1593413255000
+ },
+ {
+ "x":1593413256000
+ },
+ {
+ "x":1593413257000
+ },
+ {
+ "x":1593413258000
+ },
+ {
+ "x":1593413259000
+ },
+ {
+ "x":1593413260000
+ },
+ {
+ "x":1593413261000
+ },
+ {
+ "x":1593413262000
+ },
+ {
+ "x":1593413263000
+ },
+ {
+ "x":1593413264000
+ },
+ {
+ "x":1593413265000
+ },
+ {
+ "x":1593413266000
+ },
+ {
+ "x":1593413267000
+ },
+ {
+ "x":1593413268000
+ },
+ {
+ "x":1593413269000
+ },
+ {
+ "x":1593413270000
+ },
+ {
+ "x":1593413271000
+ },
+ {
+ "x":1593413272000
+ },
+ {
+ "x":1593413273000
+ },
+ {
+ "x":1593413274000
+ },
+ {
+ "x":1593413275000
+ },
+ {
+ "x":1593413276000
+ },
+ {
+ "x":1593413277000
+ },
+ {
+ "x":1593413278000
+ },
+ {
+ "x":1593413279000
+ },
+ {
+ "x":1593413280000
+ },
+ {
+ "x":1593413281000
+ },
+ {
+ "x":1593413282000
+ },
+ {
+ "x":1593413283000
+ },
+ {
+ "x":1593413284000
+ },
+ {
+ "x":1593413285000
+ },
+ {
+ "x":1593413286000
+ },
+ {
+ "x":1593413287000,
+ "y":342000
+ },
+ {
+ "x":1593413288000
+ },
+ {
+ "x":1593413289000
+ },
+ {
+ "x":1593413290000
+ },
+ {
+ "x":1593413291000
+ },
+ {
+ "x":1593413292000
+ },
+ {
+ "x":1593413293000
+ },
+ {
+ "x":1593413294000
+ },
+ {
+ "x":1593413295000
+ },
+ {
+ "x":1593413296000
+ },
+ {
+ "x":1593413297000
+ },
+ {
+ "x":1593413298000,
+ "y":173000
+ },
+ {
+ "x":1593413299000
+ },
+ {
+ "x":1593413300000
+ },
+ {
+ "x":1593413301000,
+ "y":109000
+ },
+ {
+ "x":1593413302000
+ },
+ {
+ "x":1593413303000
+ },
+ {
+ "x":1593413304000
+ },
+ {
+ "x":1593413305000
+ },
+ {
+ "x":1593413306000
+ },
+ {
+ "x":1593413307000
+ },
+ {
+ "x":1593413308000
+ },
+ {
+ "x":1593413309000
+ },
+ {
+ "x":1593413310000
+ },
+ {
+ "x":1593413311000
+ },
+ {
+ "x":1593413312000
+ },
+ {
+ "x":1593413313000
+ },
+ {
+ "x":1593413314000
+ },
+ {
+ "x":1593413315000
+ },
+ {
+ "x":1593413316000
+ },
+ {
+ "x":1593413317000
+ },
+ {
+ "x":1593413318000,
+ "y":140000
+ },
+ {
+ "x":1593413319000
+ },
+ {
+ "x":1593413320000
+ },
+ {
+ "x":1593413321000
+ },
+ {
+ "x":1593413322000
+ },
+ {
+ "x":1593413323000
+ },
+ {
+ "x":1593413324000
+ },
+ {
+ "x":1593413325000
+ },
+ {
+ "x":1593413326000
+ },
+ {
+ "x":1593413327000
+ },
+ {
+ "x":1593413328000,
+ "y":77000
+ },
+ {
+ "x":1593413329000
+ },
+ {
+ "x":1593413330000
+ },
+ {
+ "x":1593413331000
+ },
+ {
+ "x":1593413332000
+ },
+ {
+ "x":1593413333000
+ },
+ {
+ "x":1593413334000
+ },
+ {
+ "x":1593413335000
+ },
+ {
+ "x":1593413336000
+ },
+ {
+ "x":1593413337000
+ },
+ {
+ "x":1593413338000
+ },
+ {
+ "x":1593413339000
+ },
+ {
+ "x":1593413340000
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/avg_duration_by_browser_transaction_name.json b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/avg_duration_by_browser_transaction_name.json
new file mode 100644
index 0000000000000..107302831d55f
--- /dev/null
+++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/avg_duration_by_browser_transaction_name.json
@@ -0,0 +1,731 @@
+[
+ {
+ "title":"HeadlessChrome",
+ "data":[
+ {
+ "x":1593413100000
+ },
+ {
+ "x":1593413101000
+ },
+ {
+ "x":1593413102000
+ },
+ {
+ "x":1593413103000
+ },
+ {
+ "x":1593413104000
+ },
+ {
+ "x":1593413105000
+ },
+ {
+ "x":1593413106000
+ },
+ {
+ "x":1593413107000
+ },
+ {
+ "x":1593413108000
+ },
+ {
+ "x":1593413109000
+ },
+ {
+ "x":1593413110000
+ },
+ {
+ "x":1593413111000
+ },
+ {
+ "x":1593413112000
+ },
+ {
+ "x":1593413113000
+ },
+ {
+ "x":1593413114000
+ },
+ {
+ "x":1593413115000
+ },
+ {
+ "x":1593413116000
+ },
+ {
+ "x":1593413117000
+ },
+ {
+ "x":1593413118000
+ },
+ {
+ "x":1593413119000
+ },
+ {
+ "x":1593413120000
+ },
+ {
+ "x":1593413121000
+ },
+ {
+ "x":1593413122000
+ },
+ {
+ "x":1593413123000
+ },
+ {
+ "x":1593413124000
+ },
+ {
+ "x":1593413125000
+ },
+ {
+ "x":1593413126000
+ },
+ {
+ "x":1593413127000
+ },
+ {
+ "x":1593413128000
+ },
+ {
+ "x":1593413129000
+ },
+ {
+ "x":1593413130000
+ },
+ {
+ "x":1593413131000
+ },
+ {
+ "x":1593413132000
+ },
+ {
+ "x":1593413133000
+ },
+ {
+ "x":1593413134000
+ },
+ {
+ "x":1593413135000
+ },
+ {
+ "x":1593413136000
+ },
+ {
+ "x":1593413137000
+ },
+ {
+ "x":1593413138000
+ },
+ {
+ "x":1593413139000
+ },
+ {
+ "x":1593413140000
+ },
+ {
+ "x":1593413141000
+ },
+ {
+ "x":1593413142000
+ },
+ {
+ "x":1593413143000
+ },
+ {
+ "x":1593413144000
+ },
+ {
+ "x":1593413145000
+ },
+ {
+ "x":1593413146000
+ },
+ {
+ "x":1593413147000
+ },
+ {
+ "x":1593413148000
+ },
+ {
+ "x":1593413149000
+ },
+ {
+ "x":1593413150000
+ },
+ {
+ "x":1593413151000
+ },
+ {
+ "x":1593413152000
+ },
+ {
+ "x":1593413153000
+ },
+ {
+ "x":1593413154000
+ },
+ {
+ "x":1593413155000
+ },
+ {
+ "x":1593413156000
+ },
+ {
+ "x":1593413157000
+ },
+ {
+ "x":1593413158000
+ },
+ {
+ "x":1593413159000
+ },
+ {
+ "x":1593413160000
+ },
+ {
+ "x":1593413161000
+ },
+ {
+ "x":1593413162000
+ },
+ {
+ "x":1593413163000
+ },
+ {
+ "x":1593413164000
+ },
+ {
+ "x":1593413165000
+ },
+ {
+ "x":1593413166000
+ },
+ {
+ "x":1593413167000
+ },
+ {
+ "x":1593413168000
+ },
+ {
+ "x":1593413169000
+ },
+ {
+ "x":1593413170000
+ },
+ {
+ "x":1593413171000
+ },
+ {
+ "x":1593413172000
+ },
+ {
+ "x":1593413173000
+ },
+ {
+ "x":1593413174000
+ },
+ {
+ "x":1593413175000
+ },
+ {
+ "x":1593413176000
+ },
+ {
+ "x":1593413177000
+ },
+ {
+ "x":1593413178000
+ },
+ {
+ "x":1593413179000
+ },
+ {
+ "x":1593413180000
+ },
+ {
+ "x":1593413181000
+ },
+ {
+ "x":1593413182000
+ },
+ {
+ "x":1593413183000
+ },
+ {
+ "x":1593413184000
+ },
+ {
+ "x":1593413185000
+ },
+ {
+ "x":1593413186000
+ },
+ {
+ "x":1593413187000
+ },
+ {
+ "x":1593413188000
+ },
+ {
+ "x":1593413189000
+ },
+ {
+ "x":1593413190000
+ },
+ {
+ "x":1593413191000
+ },
+ {
+ "x":1593413192000
+ },
+ {
+ "x":1593413193000
+ },
+ {
+ "x":1593413194000
+ },
+ {
+ "x":1593413195000
+ },
+ {
+ "x":1593413196000
+ },
+ {
+ "x":1593413197000
+ },
+ {
+ "x":1593413198000
+ },
+ {
+ "x":1593413199000
+ },
+ {
+ "x":1593413200000
+ },
+ {
+ "x":1593413201000
+ },
+ {
+ "x":1593413202000
+ },
+ {
+ "x":1593413203000
+ },
+ {
+ "x":1593413204000
+ },
+ {
+ "x":1593413205000
+ },
+ {
+ "x":1593413206000
+ },
+ {
+ "x":1593413207000
+ },
+ {
+ "x":1593413208000
+ },
+ {
+ "x":1593413209000
+ },
+ {
+ "x":1593413210000
+ },
+ {
+ "x":1593413211000
+ },
+ {
+ "x":1593413212000
+ },
+ {
+ "x":1593413213000
+ },
+ {
+ "x":1593413214000
+ },
+ {
+ "x":1593413215000
+ },
+ {
+ "x":1593413216000
+ },
+ {
+ "x":1593413217000
+ },
+ {
+ "x":1593413218000
+ },
+ {
+ "x":1593413219000
+ },
+ {
+ "x":1593413220000
+ },
+ {
+ "x":1593413221000
+ },
+ {
+ "x":1593413222000
+ },
+ {
+ "x":1593413223000
+ },
+ {
+ "x":1593413224000
+ },
+ {
+ "x":1593413225000
+ },
+ {
+ "x":1593413226000
+ },
+ {
+ "x":1593413227000
+ },
+ {
+ "x":1593413228000
+ },
+ {
+ "x":1593413229000
+ },
+ {
+ "x":1593413230000
+ },
+ {
+ "x":1593413231000
+ },
+ {
+ "x":1593413232000
+ },
+ {
+ "x":1593413233000
+ },
+ {
+ "x":1593413234000
+ },
+ {
+ "x":1593413235000
+ },
+ {
+ "x":1593413236000
+ },
+ {
+ "x":1593413237000
+ },
+ {
+ "x":1593413238000
+ },
+ {
+ "x":1593413239000
+ },
+ {
+ "x":1593413240000
+ },
+ {
+ "x":1593413241000
+ },
+ {
+ "x":1593413242000
+ },
+ {
+ "x":1593413243000
+ },
+ {
+ "x":1593413244000
+ },
+ {
+ "x":1593413245000
+ },
+ {
+ "x":1593413246000
+ },
+ {
+ "x":1593413247000
+ },
+ {
+ "x":1593413248000
+ },
+ {
+ "x":1593413249000
+ },
+ {
+ "x":1593413250000
+ },
+ {
+ "x":1593413251000
+ },
+ {
+ "x":1593413252000
+ },
+ {
+ "x":1593413253000
+ },
+ {
+ "x":1593413254000
+ },
+ {
+ "x":1593413255000
+ },
+ {
+ "x":1593413256000
+ },
+ {
+ "x":1593413257000
+ },
+ {
+ "x":1593413258000
+ },
+ {
+ "x":1593413259000
+ },
+ {
+ "x":1593413260000
+ },
+ {
+ "x":1593413261000
+ },
+ {
+ "x":1593413262000
+ },
+ {
+ "x":1593413263000
+ },
+ {
+ "x":1593413264000
+ },
+ {
+ "x":1593413265000
+ },
+ {
+ "x":1593413266000
+ },
+ {
+ "x":1593413267000
+ },
+ {
+ "x":1593413268000
+ },
+ {
+ "x":1593413269000
+ },
+ {
+ "x":1593413270000
+ },
+ {
+ "x":1593413271000
+ },
+ {
+ "x":1593413272000
+ },
+ {
+ "x":1593413273000
+ },
+ {
+ "x":1593413274000
+ },
+ {
+ "x":1593413275000
+ },
+ {
+ "x":1593413276000
+ },
+ {
+ "x":1593413277000
+ },
+ {
+ "x":1593413278000
+ },
+ {
+ "x":1593413279000
+ },
+ {
+ "x":1593413280000
+ },
+ {
+ "x":1593413281000
+ },
+ {
+ "x":1593413282000
+ },
+ {
+ "x":1593413283000
+ },
+ {
+ "x":1593413284000
+ },
+ {
+ "x":1593413285000
+ },
+ {
+ "x":1593413286000
+ },
+ {
+ "x":1593413287000
+ },
+ {
+ "x":1593413288000
+ },
+ {
+ "x":1593413289000
+ },
+ {
+ "x":1593413290000
+ },
+ {
+ "x":1593413291000
+ },
+ {
+ "x":1593413292000
+ },
+ {
+ "x":1593413293000
+ },
+ {
+ "x":1593413294000
+ },
+ {
+ "x":1593413295000
+ },
+ {
+ "x":1593413296000
+ },
+ {
+ "x":1593413297000
+ },
+ {
+ "x":1593413298000
+ },
+ {
+ "x":1593413299000
+ },
+ {
+ "x":1593413300000
+ },
+ {
+ "x":1593413301000
+ },
+ {
+ "x":1593413302000
+ },
+ {
+ "x":1593413303000
+ },
+ {
+ "x":1593413304000
+ },
+ {
+ "x":1593413305000
+ },
+ {
+ "x":1593413306000
+ },
+ {
+ "x":1593413307000
+ },
+ {
+ "x":1593413308000
+ },
+ {
+ "x":1593413309000
+ },
+ {
+ "x":1593413310000
+ },
+ {
+ "x":1593413311000
+ },
+ {
+ "x":1593413312000
+ },
+ {
+ "x":1593413313000
+ },
+ {
+ "x":1593413314000
+ },
+ {
+ "x":1593413315000
+ },
+ {
+ "x":1593413316000
+ },
+ {
+ "x":1593413317000
+ },
+ {
+ "x":1593413318000
+ },
+ {
+ "x":1593413319000
+ },
+ {
+ "x":1593413320000
+ },
+ {
+ "x":1593413321000
+ },
+ {
+ "x":1593413322000
+ },
+ {
+ "x":1593413323000
+ },
+ {
+ "x":1593413324000
+ },
+ {
+ "x":1593413325000
+ },
+ {
+ "x":1593413326000
+ },
+ {
+ "x":1593413327000
+ },
+ {
+ "x":1593413328000,
+ "y":77000
+ },
+ {
+ "x":1593413329000
+ },
+ {
+ "x":1593413330000
+ },
+ {
+ "x":1593413331000
+ },
+ {
+ "x":1593413332000
+ },
+ {
+ "x":1593413333000
+ },
+ {
+ "x":1593413334000
+ },
+ {
+ "x":1593413335000
+ },
+ {
+ "x":1593413336000
+ },
+ {
+ "x":1593413337000
+ },
+ {
+ "x":1593413338000
+ },
+ {
+ "x":1593413339000
+ },
+ {
+ "x":1593413340000
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown.json b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown.json
new file mode 100644
index 0000000000000..3b884a9eb7907
--- /dev/null
+++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown.json
@@ -0,0 +1,202 @@
+{
+ "kpis":[
+ {
+ "name":"app",
+ "percentage":0.16700861715223636,
+ "color":"#54b399"
+ },
+ {
+ "name":"http",
+ "percentage":0.7702092736971686,
+ "color":"#6092c0"
+ },
+ {
+ "name":"postgresql",
+ "percentage":0.0508822322527698,
+ "color":"#d36086"
+ },
+ {
+ "name":"redis",
+ "percentage":0.011899876897825195,
+ "color":"#9170b8"
+ }
+ ],
+ "timeseries":[
+ {
+ "title":"app",
+ "color":"#54b399",
+ "type":"areaStacked",
+ "data":[
+ {
+ "x":1593413100000,
+ "y":null
+ },
+ {
+ "x":1593413130000,
+ "y":null
+ },
+ {
+ "x":1593413160000,
+ "y":null
+ },
+ {
+ "x":1593413190000,
+ "y":null
+ },
+ {
+ "x":1593413220000,
+ "y":null
+ },
+ {
+ "x":1593413250000,
+ "y":null
+ },
+ {
+ "x":1593413280000,
+ "y":null
+ },
+ {
+ "x":1593413310000,
+ "y":0.16700861715223636
+ },
+ {
+ "x":1593413340000,
+ "y":null
+ }
+ ],
+ "hideLegend":true
+ },
+ {
+ "title":"http",
+ "color":"#6092c0",
+ "type":"areaStacked",
+ "data":[
+ {
+ "x":1593413100000,
+ "y":null
+ },
+ {
+ "x":1593413130000,
+ "y":null
+ },
+ {
+ "x":1593413160000,
+ "y":null
+ },
+ {
+ "x":1593413190000,
+ "y":null
+ },
+ {
+ "x":1593413220000,
+ "y":null
+ },
+ {
+ "x":1593413250000,
+ "y":null
+ },
+ {
+ "x":1593413280000,
+ "y":null
+ },
+ {
+ "x":1593413310000,
+ "y":0.7702092736971686
+ },
+ {
+ "x":1593413340000,
+ "y":null
+ }
+ ],
+ "hideLegend":true
+ },
+ {
+ "title":"postgresql",
+ "color":"#d36086",
+ "type":"areaStacked",
+ "data":[
+ {
+ "x":1593413100000,
+ "y":null
+ },
+ {
+ "x":1593413130000,
+ "y":null
+ },
+ {
+ "x":1593413160000,
+ "y":null
+ },
+ {
+ "x":1593413190000,
+ "y":null
+ },
+ {
+ "x":1593413220000,
+ "y":null
+ },
+ {
+ "x":1593413250000,
+ "y":null
+ },
+ {
+ "x":1593413280000,
+ "y":null
+ },
+ {
+ "x":1593413310000,
+ "y":0.0508822322527698
+ },
+ {
+ "x":1593413340000,
+ "y":null
+ }
+ ],
+ "hideLegend":true
+ },
+ {
+ "title":"redis",
+ "color":"#9170b8",
+ "type":"areaStacked",
+ "data":[
+ {
+ "x":1593413100000,
+ "y":null
+ },
+ {
+ "x":1593413130000,
+ "y":null
+ },
+ {
+ "x":1593413160000,
+ "y":null
+ },
+ {
+ "x":1593413190000,
+ "y":null
+ },
+ {
+ "x":1593413220000,
+ "y":null
+ },
+ {
+ "x":1593413250000,
+ "y":null
+ },
+ {
+ "x":1593413280000,
+ "y":null
+ },
+ {
+ "x":1593413310000,
+ "y":0.011899876897825195
+ },
+ {
+ "x":1593413340000,
+ "y":null
+ }
+ ],
+ "hideLegend":true
+ }
+ ]
+}
\ No newline at end of file
diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown_transaction_name.json b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown_transaction_name.json
new file mode 100644
index 0000000000000..b4f8e376d3609
--- /dev/null
+++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown_transaction_name.json
@@ -0,0 +1,55 @@
+{
+ "kpis":[
+ {
+ "name":"app",
+ "percentage":1,
+ "color":"#54b399"
+ }
+ ],
+ "timeseries":[
+ {
+ "title":"app",
+ "color":"#54b399",
+ "type":"areaStacked",
+ "data":[
+ {
+ "x":1593413100000,
+ "y":null
+ },
+ {
+ "x":1593413130000,
+ "y":null
+ },
+ {
+ "x":1593413160000,
+ "y":null
+ },
+ {
+ "x":1593413190000,
+ "y":null
+ },
+ {
+ "x":1593413220000,
+ "y":null
+ },
+ {
+ "x":1593413250000,
+ "y":null
+ },
+ {
+ "x":1593413280000,
+ "y":null
+ },
+ {
+ "x":1593413310000,
+ "y":1
+ },
+ {
+ "x":1593413340000,
+ "y":null
+ }
+ ],
+ "hideLegend":true
+ }
+ ]
+}
\ No newline at end of file
From 82dd173b2a0cd0f69d793001afe0ee045724f3d1 Mon Sep 17 00:00:00 2001
From: Poff Poffenberger
Date: Wed, 22 Jul 2020 08:05:53 -0500
Subject: [PATCH 06/49] Use server basepath when creating reporting jobs
(#72722)
Co-authored-by: Elastic Machine
---
.../reporting/server/export_types/png/create_job/index.ts | 3 +--
.../server/export_types/printable_pdf/create_job/index.ts | 3 +--
2 files changed, 2 insertions(+), 4 deletions(-)
diff --git a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts
index b63f2a09041b3..9227354520b6e 100644
--- a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts
+++ b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts
@@ -13,7 +13,6 @@ export const scheduleTaskFnFactory: ScheduleTaskFnFactory> = function createJobFactoryFn(reporting) {
const config = reporting.getConfig();
- const setupDeps = reporting.getPluginSetupDeps();
const crypto = cryptoFactory(config.get('encryptionKey'));
return async function scheduleTask(
@@ -32,7 +31,7 @@ export const scheduleTaskFnFactory: ScheduleTaskFnFactory> = function createJobFactoryFn(reporting) {
const config = reporting.getConfig();
- const setupDeps = reporting.getPluginSetupDeps();
const crypto = cryptoFactory(config.get('encryptionKey'));
return async function scheduleTaskFn(
@@ -26,7 +25,7 @@ export const scheduleTaskFnFactory: ScheduleTaskFnFactory
Date: Wed, 22 Jul 2020 09:24:14 -0400
Subject: [PATCH 07/49] [Monitoring] Revert direct shipping code (#72505)
* Backout these changes
* Fix test
---
.../plugins/monitoring/server/config.test.ts | 35 --------
x-pack/plugins/monitoring/server/config.ts | 2 -
.../__tests__/bulk_uploader.js | 89 -------------------
.../server/kibana_monitoring/bulk_uploader.js | 25 +-----
.../lib/send_bulk_payload.js | 56 +-----------
5 files changed, 5 insertions(+), 202 deletions(-)
diff --git a/x-pack/plugins/monitoring/server/config.test.ts b/x-pack/plugins/monitoring/server/config.test.ts
index 16d52f684109e..32b8691bd6049 100644
--- a/x-pack/plugins/monitoring/server/config.test.ts
+++ b/x-pack/plugins/monitoring/server/config.test.ts
@@ -27,33 +27,6 @@ describe('config schema', () => {
},
"enabled": true,
},
- "elasticsearch": Object {
- "apiVersion": "master",
- "customHeaders": Object {},
- "healthCheck": Object {
- "delay": "PT2.5S",
- },
- "ignoreVersionMismatch": false,
- "logFetchCount": 10,
- "logQueries": false,
- "pingTimeout": "PT30S",
- "preserveHost": true,
- "requestHeadersWhitelist": Array [
- "authorization",
- ],
- "requestTimeout": "PT30S",
- "shardTimeout": "PT30S",
- "sniffInterval": false,
- "sniffOnConnectionFault": false,
- "sniffOnStart": false,
- "ssl": Object {
- "alwaysPresentCertificate": false,
- "keystore": Object {},
- "truststore": Object {},
- "verificationMode": "full",
- },
- "startupTimeout": "PT5S",
- },
"enabled": true,
"kibana": Object {
"collection": Object {
@@ -125,9 +98,6 @@ describe('createConfig()', () => {
it('should wrap in Elasticsearch config', async () => {
const config = createConfig(
configSchema.validate({
- elasticsearch: {
- hosts: 'http://localhost:9200',
- },
ui: {
elasticsearch: {
hosts: 'http://localhost:9200',
@@ -135,7 +105,6 @@ describe('createConfig()', () => {
},
})
);
- expect(config.elasticsearch.hosts).toEqual(['http://localhost:9200']);
expect(config.ui.elasticsearch.hosts).toEqual(['http://localhost:9200']);
});
@@ -147,9 +116,6 @@ describe('createConfig()', () => {
};
const config = createConfig(
configSchema.validate({
- elasticsearch: {
- ssl,
- },
ui: {
elasticsearch: {
ssl,
@@ -162,7 +128,6 @@ describe('createConfig()', () => {
key: 'contents-of-packages/kbn-dev-utils/certs/elasticsearch.key',
certificateAuthorities: ['contents-of-packages/kbn-dev-utils/certs/ca.crt'],
});
- expect(config.elasticsearch.ssl).toEqual(expected);
expect(config.ui.elasticsearch.ssl).toEqual(expected);
});
});
diff --git a/x-pack/plugins/monitoring/server/config.ts b/x-pack/plugins/monitoring/server/config.ts
index a430be8da6a5f..789211c43db31 100644
--- a/x-pack/plugins/monitoring/server/config.ts
+++ b/x-pack/plugins/monitoring/server/config.ts
@@ -21,7 +21,6 @@ export const monitoringElasticsearchConfigSchema = elasticsearchConfigSchema.ext
export const configSchema = schema.object({
enabled: schema.boolean({ defaultValue: true }),
- elasticsearch: monitoringElasticsearchConfigSchema,
ui: schema.object({
enabled: schema.boolean({ defaultValue: true }),
ccs: schema.object({
@@ -86,7 +85,6 @@ export type MonitoringConfig = ReturnType;
export function createConfig(config: TypeOf) {
return {
...config,
- elasticsearch: new ElasticsearchConfig(config.elasticsearch as ElasticsearchConfigType),
ui: {
...config.ui,
elasticsearch: new MonitoringElasticsearchConfig(config.ui.elasticsearch),
diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/__tests__/bulk_uploader.js b/x-pack/plugins/monitoring/server/kibana_monitoring/__tests__/bulk_uploader.js
index 3421f5d3830d6..da12bde966091 100644
--- a/x-pack/plugins/monitoring/server/kibana_monitoring/__tests__/bulk_uploader.js
+++ b/x-pack/plugins/monitoring/server/kibana_monitoring/__tests__/bulk_uploader.js
@@ -6,10 +6,8 @@
import { noop } from 'lodash';
import sinon from 'sinon';
-import moment from 'moment';
import expect from '@kbn/expect';
import { BulkUploader } from '../bulk_uploader';
-import { MONITORING_SYSTEM_API_VERSION } from '../../../common/constants';
const FETCH_INTERVAL = 300;
const CHECK_DELAY = 500;
@@ -314,92 +312,5 @@ describe('BulkUploader', () => {
done();
}, CHECK_DELAY);
});
-
- it('uses a direct connection to the monitoring cluster, when configured', (done) => {
- const dateInIndex = '2020.02.10';
- const oldNow = moment.now;
- moment.now = () => 1581310800000;
- const prodClusterUuid = '1sdfd5';
- const prodCluster = {
- callWithInternalUser: sinon
- .stub()
- .withArgs('monitoring.bulk')
- .callsFake((arg) => {
- let resolution = null;
- if (arg === 'info') {
- resolution = { cluster_uuid: prodClusterUuid };
- }
- return new Promise((resolve) => resolve(resolution));
- }),
- };
- const monitoringCluster = {
- callWithInternalUser: sinon
- .stub()
- .withArgs('bulk')
- .callsFake(() => {
- return new Promise((resolve) => setTimeout(resolve, CHECK_DELAY + 1));
- }),
- };
-
- const collectorFetch = sinon.stub().returns({
- type: 'kibana_stats',
- result: { type: 'kibana_stats', payload: { testData: 12345 } },
- });
-
- const collectors = new MockCollectorSet(server, [
- {
- fetch: collectorFetch,
- isReady: () => true,
- formatForBulkUpload: (result) => result,
- isUsageCollector: false,
- },
- ]);
- const customServer = {
- ...server,
- elasticsearchPlugin: {
- createCluster: () => monitoringCluster,
- getCluster: (name) => {
- if (name === 'admin' || name === 'data') {
- return prodCluster;
- }
- return monitoringCluster;
- },
- },
- config: {
- get: (key) => {
- if (key === 'monitoring.elasticsearch') {
- return {
- hosts: ['http://localhost:9200'],
- username: 'tester',
- password: 'testing',
- ssl: {},
- };
- }
- return null;
- },
- },
- };
- const kbnServerStatus = { toJSON: () => ({ overall: { state: 'green' } }) };
- const kbnServerVersion = 'master';
- const uploader = new BulkUploader({
- ...customServer,
- interval: FETCH_INTERVAL,
- kbnServerStatus,
- kbnServerVersion,
- });
- uploader.start(collectors);
- setTimeout(() => {
- uploader.stop();
- const firstCallArgs = monitoringCluster.callWithInternalUser.firstCall.args;
- expect(firstCallArgs[0]).to.be('bulk');
- expect(firstCallArgs[1].body[0].index._index).to.be(
- `.monitoring-kibana-${MONITORING_SYSTEM_API_VERSION}-${dateInIndex}`
- );
- expect(firstCallArgs[1].body[1].type).to.be('kibana_stats');
- expect(firstCallArgs[1].body[1].cluster_uuid).to.be(prodClusterUuid);
- moment.now = oldNow;
- done();
- }, CHECK_DELAY);
- });
});
});
diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js b/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js
index 6035837bac85d..b23b4fc888120 100644
--- a/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js
+++ b/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { defaultsDeep, uniq, compact, get } from 'lodash';
+import { defaultsDeep, uniq, compact } from 'lodash';
import {
TELEMETRY_COLLECTION_INTERVAL,
@@ -12,7 +12,6 @@ import {
} from '../../common/constants';
import { sendBulkPayload, monitoringBulk } from './lib';
-import { hasMonitoringCluster } from '../es_client/instantiate_client';
/*
* Handles internal Kibana stats collection and uploading data to Monitoring
@@ -31,13 +30,11 @@ import { hasMonitoringCluster } from '../es_client/instantiate_client';
* @param {Object} xpackInfo server.plugins.xpack_main.info object
*/
export class BulkUploader {
- constructor({ config, log, interval, elasticsearch, kibanaStats }) {
+ constructor({ log, interval, elasticsearch, kibanaStats }) {
if (typeof interval !== 'number') {
throw new Error('interval number of milliseconds is required');
}
- this._hasDirectConnectionToMonitoringCluster = false;
- this._productionClusterUuid = null;
this._timer = null;
// Hold sending and fetching usage until monitoring.bulk is successful. This means that we
// send usage data on the second tick. But would save a lot of bandwidth fetching usage on
@@ -54,15 +51,6 @@ export class BulkUploader {
plugins: [monitoringBulk],
});
- if (hasMonitoringCluster(config.elasticsearch)) {
- this._log.info(`Detected direct connection to monitoring cluster`);
- this._hasDirectConnectionToMonitoringCluster = true;
- this._cluster = elasticsearch.legacy.createClient('monitoring-direct', config.elasticsearch);
- elasticsearch.legacy.client.callAsInternalUser('info').then((data) => {
- this._productionClusterUuid = get(data, 'cluster_uuid');
- });
- }
-
this.kibanaStats = kibanaStats;
this.kibanaStatusGetter = null;
}
@@ -181,14 +169,7 @@ export class BulkUploader {
}
async _onPayload(payload) {
- return await sendBulkPayload(
- this._cluster,
- this._interval,
- payload,
- this._log,
- this._hasDirectConnectionToMonitoringCluster,
- this._productionClusterUuid
- );
+ return await sendBulkPayload(this._cluster, this._interval, payload, this._log);
}
getKibanaStats(type) {
diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/lib/send_bulk_payload.js b/x-pack/plugins/monitoring/server/kibana_monitoring/lib/send_bulk_payload.js
index 9607b45d7e408..66799e4aa651a 100644
--- a/x-pack/plugins/monitoring/server/kibana_monitoring/lib/send_bulk_payload.js
+++ b/x-pack/plugins/monitoring/server/kibana_monitoring/lib/send_bulk_payload.js
@@ -3,64 +3,12 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import moment from 'moment';
-import { chunk, get } from 'lodash';
-import {
- MONITORING_SYSTEM_API_VERSION,
- KIBANA_SYSTEM_ID,
- KIBANA_STATS_TYPE_MONITORING,
- KIBANA_SETTINGS_TYPE,
-} from '../../../common/constants';
-
-const SUPPORTED_TYPES = [KIBANA_STATS_TYPE_MONITORING, KIBANA_SETTINGS_TYPE];
-export function formatForNormalBulkEndpoint(payload, productionClusterUuid) {
- const dateSuffix = moment.utc().format('YYYY.MM.DD');
- return chunk(payload, 2).reduce((accum, chunk) => {
- const type = get(chunk[0], 'index._type');
- if (!type || !SUPPORTED_TYPES.includes(type)) {
- return accum;
- }
-
- const { timestamp } = chunk[1];
-
- accum.push({
- index: {
- _index: `.monitoring-kibana-${MONITORING_SYSTEM_API_VERSION}-${dateSuffix}`,
- },
- });
- accum.push({
- [type]: chunk[1],
- type,
- timestamp,
- cluster_uuid: productionClusterUuid,
- });
- return accum;
- }, []);
-}
+import { MONITORING_SYSTEM_API_VERSION, KIBANA_SYSTEM_ID } from '../../../common/constants';
/*
* Send the Kibana usage data to the ES Monitoring Bulk endpoint
*/
-export async function sendBulkPayload(
- cluster,
- interval,
- payload,
- log,
- hasDirectConnectionToMonitoringCluster = false,
- productionClusterUuid = null
-) {
- if (hasDirectConnectionToMonitoringCluster) {
- if (productionClusterUuid === null) {
- log.warn(
- `Unable to determine production cluster uuid to use for shipping monitoring data. Kibana monitoring data will appear in a standalone cluster in the Stack Monitoring UI.`
- );
- }
- const formattedPayload = formatForNormalBulkEndpoint(payload, productionClusterUuid);
- return await cluster.callAsInternalUser('bulk', {
- body: formattedPayload,
- });
- }
-
+export async function sendBulkPayload(cluster, interval, payload) {
return cluster.callAsInternalUser('monitoring.bulk', {
system_id: KIBANA_SYSTEM_ID,
system_api_version: MONITORING_SYSTEM_API_VERSION,
From 4abe864f106b4f5fe623e546a4ffdf1277239f58 Mon Sep 17 00:00:00 2001
From: Gidi Meir Morris
Date: Wed, 22 Jul 2020 14:45:57 +0100
Subject: [PATCH 08/49] Adds Role Based Access-Control to the Alerting & Action
plugins based on Kibana Feature Controls (#67157)
This PR adds _Role Based Access-Control_ to the Alerting framework & Actions feature using Kibana Feature Controls, addressing most of the Meta issue: https://github.com/elastic/kibana/issues/43994
This also closes https://github.com/elastic/kibana/issues/62438
This PR includes the following:
1. Adds `alerting` specific Security Actions (not to be confused with Alerting Actions) to the `security` plugin which allows us to assign alerting specific privileges to users of other plugins using the `features` plugin.
2. Removes the security wrapper from the savedObjectsClient in AlertsClient and instead plugs in the new AlertsAuthorization which performs the privilege checks on each api call made to the AlertsClient.
3. Adds privileges in each plugin that is already using the Alerting Framework which mirror (as closely as possible) the existing api-level tag-based privileges and plugs them into the AlertsClient.
4. Adds feature granted privileges arounds Actions (by relying on Saved Object privileges under the hood) and plugs them into the ActionsClient
5. Removes the legacy api-level tag-based privilege system from both the Alerts and Action HTTP APIs
---
examples/alerting_example/kibana.json | 2 +-
examples/alerting_example/server/plugin.ts | 38 +-
x-pack/plugins/actions/kibana.json | 4 +-
.../actions/server/actions_client.mock.ts | 1 +
.../actions/server/actions_client.test.ts | 541 ++++++-
.../plugins/actions/server/actions_client.ts | 53 +-
.../actions_authorization.mock.ts | 22 +
.../actions_authorization.test.ts | 191 +++
.../authorization/actions_authorization.ts | 59 +
.../server/authorization/audit_logger.mock.ts | 22 +
.../server/authorization/audit_logger.test.ts | 75 +
.../server/authorization/audit_logger.ts | 66 +
.../actions/server/create_execute_function.ts | 14 +-
x-pack/plugins/actions/server/feature.ts | 41 +
x-pack/plugins/actions/server/index.ts | 2 +
.../server/lib/action_executor.test.ts | 64 +-
.../actions/server/lib/action_executor.ts | 17 +-
.../server/lib/task_runner_factory.test.ts | 3 +-
.../actions/server/lib/task_runner_factory.ts | 7 +-
x-pack/plugins/actions/server/mocks.ts | 5 +
x-pack/plugins/actions/server/plugin.test.ts | 3 +
x-pack/plugins/actions/server/plugin.ts | 97 +-
.../actions/server/routes/create.test.ts | 7 -
.../plugins/actions/server/routes/create.ts | 3 -
.../actions/server/routes/delete.test.ts | 7 -
.../plugins/actions/server/routes/delete.ts | 3 -
.../actions/server/routes/execute.test.ts | 7 -
.../plugins/actions/server/routes/execute.ts | 3 -
.../plugins/actions/server/routes/get.test.ts | 7 -
x-pack/plugins/actions/server/routes/get.ts | 3 -
.../actions/server/routes/get_all.test.ts | 21 -
.../plugins/actions/server/routes/get_all.ts | 3 -
.../server/routes/list_action_types.test.ts | 38 +-
.../server/routes/list_action_types.ts | 6 +-
.../actions/server/routes/update.test.ts | 7 -
.../plugins/actions/server/routes/update.ts | 3 -
.../actions/server/saved_objects/index.ts | 11 +-
.../plugins/alerting_builtins/common/index.ts | 7 +
x-pack/plugins/alerting_builtins/kibana.json | 2 +-
.../alert_types/index_threshold/alert_type.ts | 6 +-
.../alerting_builtins/server/feature.ts | 49 +
.../plugins/alerting_builtins/server/index.ts | 1 +
.../alerting_builtins/server/plugin.test.ts | 12 +-
.../alerting_builtins/server/plugin.ts | 8 +-
.../plugins/alerting_builtins/server/types.ts | 2 +
x-pack/plugins/alerts/README.md | 109 +-
x-pack/plugins/alerts/common/index.ts | 1 +
x-pack/plugins/alerts/kibana.json | 2 +-
.../plugins/alerts/public/alert_api.test.ts | 8 +-
.../alert_navigation_registry.test.ts | 2 +-
.../alerts/server/alert_type_registry.test.ts | 80 +-
.../alerts/server/alert_type_registry.ts | 57 +-
.../alerts/server/alerts_client.mock.ts | 1 +
.../alerts/server/alerts_client.test.ts | 1414 ++++++++++++++---
x-pack/plugins/alerts/server/alerts_client.ts | 291 +++-
.../server/alerts_client_factory.test.ts | 119 +-
.../alerts/server/alerts_client_factory.ts | 38 +-
.../alerts_authorization.mock.ts | 25 +
.../alerts_authorization.test.ts | 1256 +++++++++++++++
.../authorization/alerts_authorization.ts | 457 ++++++
.../server/authorization/audit_logger.mock.ts | 24 +
.../server/authorization/audit_logger.test.ts | 311 ++++
.../server/authorization/audit_logger.ts | 117 ++
x-pack/plugins/alerts/server/index.ts | 2 +-
.../lib/validate_alert_type_params.test.ts | 6 +-
x-pack/plugins/alerts/server/plugin.test.ts | 34 +
x-pack/plugins/alerts/server/plugin.ts | 36 +-
.../alerts/server/routes/create.test.ts | 7 -
x-pack/plugins/alerts/server/routes/create.ts | 3 -
.../alerts/server/routes/delete.test.ts | 7 -
x-pack/plugins/alerts/server/routes/delete.ts | 3 -
.../alerts/server/routes/disable.test.ts | 7 -
.../plugins/alerts/server/routes/disable.ts | 3 -
.../alerts/server/routes/enable.test.ts | 7 -
x-pack/plugins/alerts/server/routes/enable.ts | 3 -
.../plugins/alerts/server/routes/find.test.ts | 7 -
x-pack/plugins/alerts/server/routes/find.ts | 5 +-
.../plugins/alerts/server/routes/get.test.ts | 7 -
x-pack/plugins/alerts/server/routes/get.ts | 3 -
.../server/routes/get_alert_state.test.ts | 21 -
.../alerts/server/routes/get_alert_state.ts | 3 -
.../server/routes/list_alert_types.test.ts | 66 +-
.../alerts/server/routes/list_alert_types.ts | 5 +-
.../alerts/server/routes/mute_all.test.ts | 7 -
.../plugins/alerts/server/routes/mute_all.ts | 3 -
.../server/routes/mute_instance.test.ts | 7 -
.../alerts/server/routes/mute_instance.ts | 3 -
.../alerts/server/routes/unmute_all.test.ts | 7 -
.../alerts/server/routes/unmute_all.ts | 3 -
.../server/routes/unmute_instance.test.ts | 7 -
.../alerts/server/routes/unmute_instance.ts | 3 -
.../alerts/server/routes/update.test.ts | 7 -
x-pack/plugins/alerts/server/routes/update.ts | 3 -
.../server/routes/update_api_key.test.ts | 7 -
.../alerts/server/routes/update_api_key.ts | 3 -
.../server/saved_objects/migrations.test.ts | 34 +
.../alerts/server/saved_objects/migrations.ts | 26 +-
.../create_execution_handler.test.ts | 2 +-
.../server/task_runner/task_runner.test.ts | 114 +-
.../alerts/server/task_runner/task_runner.ts | 85 +-
.../task_runner/task_runner_factory.test.ts | 6 +-
.../server/task_runner/task_runner_factory.ts | 4 +-
x-pack/plugins/apm/server/feature.ts | 50 +-
x-pack/plugins/features/common/feature.ts | 11 +
.../common/feature_kibana_privileges.ts | 28 +
.../__snapshots__/oss_features.test.ts.snap | 12 +
.../features/server/feature_registry.test.ts | 162 ++
.../plugins/features/server/feature_schema.ts | 43 +-
.../inventory/components/alert_flyout.tsx | 2 +-
.../components/alert_flyout.tsx | 2 +-
x-pack/plugins/infra/server/features.ts | 47 +-
...r_inventory_metric_threshold_alert_type.ts | 2 +-
.../register_metric_threshold_alert_type.ts | 2 +-
x-pack/plugins/monitoring/server/plugin.ts | 5 +
.../__snapshots__/alerting.test.ts.snap | 37 +
.../authorization/actions/actions.mock.ts | 35 +
.../server/authorization/actions/actions.ts | 3 +
.../authorization/actions/alerting.test.ts | 45 +
.../server/authorization/actions/alerting.ts | 31 +
.../disable_ui_capabilities.test.ts | 3 +
.../server/authorization/index.mock.ts | 5 +-
.../alerting.test.ts | 178 +++
.../feature_privilege_builder/alerting.ts | 42 +
.../feature_privilege_builder/index.ts | 2 +
.../feature_privilege_iterator.test.ts | 143 ++
.../feature_privilege_iterator.ts | 8 +
.../authorization/privileges/privileges.ts | 1 -
x-pack/plugins/security/server/mocks.ts | 1 +
x-pack/plugins/security/server/plugin.test.ts | 4 +
x-pack/plugins/security/server/plugin.ts | 6 +-
.../notifications/create_notifications.ts | 4 +-
.../detection_engine/rules/create_rules.ts | 4 +-
.../security_solution/server/plugin.ts | 59 +-
.../email/email_connector.test.tsx | 1 +
.../email/email_connector.tsx | 8 +-
.../es_index/es_index_connector.test.tsx | 1 +
.../es_index/es_index_connector.tsx | 5 +-
.../pagerduty/pagerduty_connectors.test.tsx | 1 +
.../pagerduty/pagerduty_connectors.tsx | 4 +-
.../servicenow/servicenow_connectors.test.tsx | 2 +
.../servicenow/servicenow_connectors.tsx | 5 +-
.../slack/slack_connectors.test.tsx | 1 +
.../slack/slack_connectors.tsx | 3 +-
.../webhook/webhook_connectors.test.tsx | 1 +
.../webhook/webhook_connectors.tsx | 9 +-
.../application/lib/action_variables.test.ts | 4 +-
.../public/application/lib/alert_api.test.ts | 4 +-
.../public/application/lib/capabilities.ts | 24 +-
.../action_connector_form.test.tsx | 7 +
.../action_connector_form.tsx | 9 +-
.../action_connector_form/action_form.tsx | 93 +-
.../connector_add_flyout.tsx | 1 +
.../connector_add_modal.test.tsx | 8 +-
.../connector_add_modal.tsx | 1 +
.../connector_edit_flyout.tsx | 1 +
.../actions_connectors_list.test.tsx | 42 +-
.../components/actions_connectors_list.tsx | 54 +-
.../components/alert_details.test.tsx | 187 ++-
.../components/alert_details.tsx | 27 +-
.../components/alert_instances.test.tsx | 7 +-
.../components/alert_instances.tsx | 8 +-
.../components/alert_instances_route.test.tsx | 6 +-
.../components/alert_instances_route.tsx | 9 +-
.../sections/alert_form/alert_add.test.tsx | 40 +-
.../sections/alert_form/alert_add.tsx | 3 +
.../sections/alert_form/alert_edit.tsx | 3 +
.../sections/alert_form/alert_form.test.tsx | 61 +-
.../sections/alert_form/alert_form.tsx | 86 +-
.../components/alerts_list.test.tsx | 27 +-
.../alerts_list/components/alerts_list.tsx | 96 +-
.../components/collapsed_item_actions.tsx | 15 +-
.../triggers_actions_ui/public/types.ts | 5 +-
x-pack/plugins/uptime/server/kibana.index.ts | 44 +-
.../actions_simulators/server/plugin.ts | 14 +-
.../fixtures/plugins/alerts/kibana.json | 2 +-
.../plugins/alerts/server/alert_types.ts | 18 +-
.../fixtures/plugins/alerts/server/plugin.ts | 44 +-
.../plugins/alerts_restricted/kibana.json | 10 +
.../plugins/alerts_restricted/package.json | 20 +
.../alerts_restricted/server/alert_types.ts | 33 +
.../plugins/alerts_restricted/server/index.ts | 9 +
.../alerts_restricted/server/plugin.ts | 62 +
.../common/lib/alert_utils.ts | 20 +-
.../common/lib/get_test_alert_data.ts | 2 +-
.../common/lib/index.ts | 6 +-
.../security_and_spaces/scenarios.ts | 96 +-
.../tests/actions/create.ts | 49 +-
.../tests/actions/delete.ts | 41 +-
.../tests/actions/execute.ts | 70 +-
.../security_and_spaces/tests/actions/get.ts | 31 +-
.../tests/actions/get_all.ts | 30 +-
.../tests/actions/list_action_types.ts | 10 +-
.../tests/actions/update.ts | 69 +-
.../tests/alerting/alerts.ts | 225 ++-
.../tests/alerting/create.ts | 266 +++-
.../tests/alerting/delete.ts | 221 ++-
.../tests/alerting/disable.ts | 227 ++-
.../tests/alerting/enable.ts | 239 ++-
.../tests/alerting/find.ts | 215 ++-
.../security_and_spaces/tests/alerting/get.ts | 188 ++-
.../tests/alerting/get_alert_state.ts | 90 +-
.../tests/alerting/index.ts | 2 +-
.../tests/alerting/list_alert_types.ts | 124 +-
.../tests/alerting/mute_all.ts | 237 ++-
.../tests/alerting/mute_instance.ts | 251 ++-
.../tests/alerting/unmute_all.ts | 252 ++-
.../tests/alerting/unmute_instance.ts | 258 ++-
.../tests/alerting/update.ts | 399 ++++-
.../tests/alerting/update_api_key.ts | 251 ++-
.../index_threshold/alert.ts | 2 +-
.../spaces_only/tests/alerting/create.ts | 28 +-
.../spaces_only/tests/alerting/find.ts | 2 +-
.../spaces_only/tests/alerting/get.ts | 2 +-
.../tests/alerting/get_alert_state.ts | 2 +-
.../tests/alerting/list_alert_types.ts | 7 +-
.../spaces_only/tests/alerting/migrations.ts | 9 +
.../spaces_only/tests/alerting/update.ts | 2 +-
.../apis/features/features/features.ts | 2 +
.../apis/security/privileges.ts | 2 +
.../apis/security/privileges_basic.ts | 2 +
.../functional/es_archives/alerts/data.json | 42 +
.../apps/triggers_actions_ui/alerts.ts | 4 +-
.../fixtures/plugins/alerts/kibana.json | 2 +-
.../fixtures/plugins/alerts/public/plugin.ts | 6 +-
.../fixtures/plugins/alerts/server/plugin.ts | 36 +-
.../services/alerting/alerts.ts | 6 +-
226 files changed, 10844 insertions(+), 1704 deletions(-)
create mode 100644 x-pack/plugins/actions/server/authorization/actions_authorization.mock.ts
create mode 100644 x-pack/plugins/actions/server/authorization/actions_authorization.test.ts
create mode 100644 x-pack/plugins/actions/server/authorization/actions_authorization.ts
create mode 100644 x-pack/plugins/actions/server/authorization/audit_logger.mock.ts
create mode 100644 x-pack/plugins/actions/server/authorization/audit_logger.test.ts
create mode 100644 x-pack/plugins/actions/server/authorization/audit_logger.ts
create mode 100644 x-pack/plugins/actions/server/feature.ts
create mode 100644 x-pack/plugins/alerting_builtins/common/index.ts
create mode 100644 x-pack/plugins/alerting_builtins/server/feature.ts
create mode 100644 x-pack/plugins/alerts/server/authorization/alerts_authorization.mock.ts
create mode 100644 x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts
create mode 100644 x-pack/plugins/alerts/server/authorization/alerts_authorization.ts
create mode 100644 x-pack/plugins/alerts/server/authorization/audit_logger.mock.ts
create mode 100644 x-pack/plugins/alerts/server/authorization/audit_logger.test.ts
create mode 100644 x-pack/plugins/alerts/server/authorization/audit_logger.ts
create mode 100644 x-pack/plugins/security/server/authorization/actions/__snapshots__/alerting.test.ts.snap
create mode 100644 x-pack/plugins/security/server/authorization/actions/actions.mock.ts
create mode 100644 x-pack/plugins/security/server/authorization/actions/alerting.test.ts
create mode 100644 x-pack/plugins/security/server/authorization/actions/alerting.ts
create mode 100644 x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts
create mode 100644 x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts
create mode 100644 x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/kibana.json
create mode 100644 x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/package.json
create mode 100644 x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/alert_types.ts
create mode 100644 x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/index.ts
create mode 100644 x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/plugin.ts
diff --git a/examples/alerting_example/kibana.json b/examples/alerting_example/kibana.json
index 6c04218ca45e2..a2691c5fdcab7 100644
--- a/examples/alerting_example/kibana.json
+++ b/examples/alerting_example/kibana.json
@@ -4,6 +4,6 @@
"kibanaVersion": "kibana",
"server": true,
"ui": true,
- "requiredPlugins": ["triggers_actions_ui", "charts", "data", "alerts", "actions", "developerExamples"],
+ "requiredPlugins": ["triggers_actions_ui", "charts", "data", "alerts", "actions", "features", "developerExamples"],
"optionalPlugins": []
}
diff --git a/examples/alerting_example/server/plugin.ts b/examples/alerting_example/server/plugin.ts
index cdb005feca35c..49352cc285693 100644
--- a/examples/alerting_example/server/plugin.ts
+++ b/examples/alerting_example/server/plugin.ts
@@ -18,20 +18,56 @@
*/
import { Plugin, CoreSetup } from 'kibana/server';
+import { i18n } from '@kbn/i18n';
import { PluginSetupContract as AlertingSetup } from '../../../x-pack/plugins/alerts/server';
+import { PluginSetupContract as FeaturesPluginSetup } from '../../../x-pack/plugins/features/server';
import { alertType as alwaysFiringAlert } from './alert_types/always_firing';
import { alertType as peopleInSpaceAlert } from './alert_types/astros';
+import { INDEX_THRESHOLD_ID } from '../../../x-pack/plugins/alerting_builtins/server';
+import { ALERTING_EXAMPLE_APP_ID } from '../common/constants';
// this plugin's dependendencies
export interface AlertingExampleDeps {
alerts: AlertingSetup;
+ features: FeaturesPluginSetup;
}
export class AlertingExamplePlugin implements Plugin {
- public setup(core: CoreSetup, { alerts }: AlertingExampleDeps) {
+ public setup(core: CoreSetup, { alerts, features }: AlertingExampleDeps) {
alerts.registerType(alwaysFiringAlert);
alerts.registerType(peopleInSpaceAlert);
+
+ features.registerFeature({
+ id: ALERTING_EXAMPLE_APP_ID,
+ name: i18n.translate('alertsExample.featureRegistry.alertsExampleFeatureName', {
+ defaultMessage: 'Alerts Example',
+ }),
+ app: [],
+ alerting: [alwaysFiringAlert.id, peopleInSpaceAlert.id, INDEX_THRESHOLD_ID],
+ privileges: {
+ all: {
+ alerting: {
+ all: [alwaysFiringAlert.id, peopleInSpaceAlert.id, INDEX_THRESHOLD_ID],
+ },
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ ui: ['alerting:show'],
+ },
+ read: {
+ alerting: {
+ read: [alwaysFiringAlert.id, peopleInSpaceAlert.id, INDEX_THRESHOLD_ID],
+ },
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ ui: ['alerting:show'],
+ },
+ },
+ });
}
public start() {}
diff --git a/x-pack/plugins/actions/kibana.json b/x-pack/plugins/actions/kibana.json
index 14ddb8257ff37..ef604a9cf6417 100644
--- a/x-pack/plugins/actions/kibana.json
+++ b/x-pack/plugins/actions/kibana.json
@@ -4,7 +4,7 @@
"version": "8.0.0",
"kibanaVersion": "kibana",
"configPath": ["xpack", "actions"],
- "requiredPlugins": ["licensing", "taskManager", "encryptedSavedObjects", "eventLog"],
- "optionalPlugins": ["usageCollection", "spaces"],
+ "requiredPlugins": ["licensing", "taskManager", "encryptedSavedObjects", "eventLog", "features"],
+ "optionalPlugins": ["usageCollection", "spaces", "security"],
"ui": false
}
diff --git a/x-pack/plugins/actions/server/actions_client.mock.ts b/x-pack/plugins/actions/server/actions_client.mock.ts
index efd044c7e2493..48122a5ce4e0f 100644
--- a/x-pack/plugins/actions/server/actions_client.mock.ts
+++ b/x-pack/plugins/actions/server/actions_client.mock.ts
@@ -19,6 +19,7 @@ const createActionsClientMock = () => {
getBulk: jest.fn(),
execute: jest.fn(),
enqueueExecution: jest.fn(),
+ listTypes: jest.fn(),
};
return mocked;
};
diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts
index 807d75cd0d701..90b989ac3b52e 100644
--- a/x-pack/plugins/actions/server/actions_client.test.ts
+++ b/x-pack/plugins/actions/server/actions_client.test.ts
@@ -22,11 +22,14 @@ import {
import { actionExecutorMock } from './lib/action_executor.mock';
import uuid from 'uuid';
import { KibanaRequest } from 'kibana/server';
+import { ActionsAuthorization } from './authorization/actions_authorization';
+import { actionsAuthorizationMock } from './authorization/actions_authorization.mock';
const defaultKibanaIndex = '.kibana';
-const savedObjectsClient = savedObjectsClientMock.create();
+const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const scopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient();
const actionExecutor = actionExecutorMock.create();
+const authorization = actionsAuthorizationMock.create();
const executionEnqueuer = jest.fn();
const request = {} as KibanaRequest;
@@ -55,17 +58,88 @@ beforeEach(() => {
actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
actionsClient = new ActionsClient({
actionTypeRegistry,
- savedObjectsClient,
+ unsecuredSavedObjectsClient,
scopedClusterClient,
defaultKibanaIndex,
preconfiguredActions: [],
actionExecutor,
executionEnqueuer,
request,
+ authorization: (authorization as unknown) as ActionsAuthorization,
});
});
describe('create()', () => {
+ describe('authorization', () => {
+ test('ensures user is authorised to create this type of action', async () => {
+ const savedObjectCreateResult = {
+ id: '1',
+ type: 'action',
+ attributes: {
+ name: 'my name',
+ actionTypeId: 'my-action-type',
+ config: {},
+ },
+ references: [],
+ };
+ actionTypeRegistry.register({
+ id: 'my-action-type',
+ name: 'My action type',
+ minimumLicenseRequired: 'basic',
+ executor,
+ });
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult);
+
+ await actionsClient.create({
+ action: {
+ name: 'my name',
+ actionTypeId: 'my-action-type',
+ config: {},
+ secrets: {},
+ },
+ });
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('create', 'my-action-type');
+ });
+
+ test('throws when user is not authorised to create this type of action', async () => {
+ const savedObjectCreateResult = {
+ id: '1',
+ type: 'action',
+ attributes: {
+ name: 'my name',
+ actionTypeId: 'my-action-type',
+ config: {},
+ },
+ references: [],
+ };
+ actionTypeRegistry.register({
+ id: 'my-action-type',
+ name: 'My action type',
+ minimumLicenseRequired: 'basic',
+ executor,
+ });
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult);
+
+ authorization.ensureAuthorized.mockRejectedValue(
+ new Error(`Unauthorized to create a "my-action-type" action`)
+ );
+
+ await expect(
+ actionsClient.create({
+ action: {
+ name: 'my name',
+ actionTypeId: 'my-action-type',
+ config: {},
+ secrets: {},
+ },
+ })
+ ).rejects.toMatchInlineSnapshot(`[Error: Unauthorized to create a "my-action-type" action]`);
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('create', 'my-action-type');
+ });
+ });
+
test('creates an action with all given properties', async () => {
const savedObjectCreateResult = {
id: '1',
@@ -83,7 +157,7 @@ describe('create()', () => {
minimumLicenseRequired: 'basic',
executor,
});
- savedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult);
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult);
const result = await actionsClient.create({
action: {
name: 'my name',
@@ -99,8 +173,8 @@ describe('create()', () => {
actionTypeId: 'my-action-type',
config: {},
});
- expect(savedObjectsClient.create).toHaveBeenCalledTimes(1);
- expect(savedObjectsClient.create.mock.calls[0]).toMatchInlineSnapshot(`
+ expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1);
+ expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"action",
Object {
@@ -161,7 +235,7 @@ describe('create()', () => {
minimumLicenseRequired: 'basic',
executor,
});
- savedObjectsClient.create.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: 'type',
attributes: {
@@ -199,8 +273,8 @@ describe('create()', () => {
c: true,
},
});
- expect(savedObjectsClient.create).toHaveBeenCalledTimes(1);
- expect(savedObjectsClient.create.mock.calls[0]).toMatchInlineSnapshot(`
+ expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1);
+ expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"action",
Object {
@@ -237,13 +311,14 @@ describe('create()', () => {
actionTypeRegistry = new ActionTypeRegistry(localActionTypeRegistryParams);
actionsClient = new ActionsClient({
actionTypeRegistry,
- savedObjectsClient,
+ unsecuredSavedObjectsClient,
scopedClusterClient,
defaultKibanaIndex,
preconfiguredActions: [],
actionExecutor,
executionEnqueuer,
request,
+ authorization: (authorization as unknown) as ActionsAuthorization,
});
const savedObjectCreateResult = {
@@ -262,7 +337,7 @@ describe('create()', () => {
minimumLicenseRequired: 'basic',
executor,
});
- savedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult);
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult);
await expect(
actionsClient.create({
@@ -298,7 +373,7 @@ describe('create()', () => {
mockedLicenseState.ensureLicenseForActionType.mockImplementation(() => {
throw new Error('Fail');
});
- savedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult);
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult);
await expect(
actionsClient.create({
action: {
@@ -313,8 +388,118 @@ describe('create()', () => {
});
describe('get()', () => {
- test('calls savedObjectsClient with id', async () => {
- savedObjectsClient.get.mockResolvedValueOnce({
+ describe('authorization', () => {
+ test('ensures user is authorised to get the type of action', async () => {
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
+ id: '1',
+ type: 'type',
+ attributes: {
+ name: 'my name',
+ actionTypeId: 'my-action-type',
+ config: {},
+ },
+ references: [],
+ });
+
+ await actionsClient.get({ id: '1' });
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
+ });
+
+ test('ensures user is authorised to get preconfigured type of action', async () => {
+ actionsClient = new ActionsClient({
+ actionTypeRegistry,
+ unsecuredSavedObjectsClient,
+ scopedClusterClient,
+ defaultKibanaIndex,
+ actionExecutor,
+ executionEnqueuer,
+ request,
+ authorization: (authorization as unknown) as ActionsAuthorization,
+ preconfiguredActions: [
+ {
+ id: 'testPreconfigured',
+ actionTypeId: 'my-action-type',
+ secrets: {
+ test: 'test1',
+ },
+ isPreconfigured: true,
+ name: 'test',
+ config: {
+ foo: 'bar',
+ },
+ },
+ ],
+ });
+
+ await actionsClient.get({ id: 'testPreconfigured' });
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
+ });
+
+ test('throws when user is not authorised to create the type of action', async () => {
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
+ id: '1',
+ type: 'type',
+ attributes: {
+ name: 'my name',
+ actionTypeId: 'my-action-type',
+ config: {},
+ },
+ references: [],
+ });
+
+ authorization.ensureAuthorized.mockRejectedValue(
+ new Error(`Unauthorized to get a "my-action-type" action`)
+ );
+
+ await expect(actionsClient.get({ id: '1' })).rejects.toMatchInlineSnapshot(
+ `[Error: Unauthorized to get a "my-action-type" action]`
+ );
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
+ });
+
+ test('throws when user is not authorised to create preconfigured of action', async () => {
+ actionsClient = new ActionsClient({
+ actionTypeRegistry,
+ unsecuredSavedObjectsClient,
+ scopedClusterClient,
+ defaultKibanaIndex,
+ actionExecutor,
+ executionEnqueuer,
+ request,
+ authorization: (authorization as unknown) as ActionsAuthorization,
+ preconfiguredActions: [
+ {
+ id: 'testPreconfigured',
+ actionTypeId: 'my-action-type',
+ secrets: {
+ test: 'test1',
+ },
+ isPreconfigured: true,
+ name: 'test',
+ config: {
+ foo: 'bar',
+ },
+ },
+ ],
+ });
+
+ authorization.ensureAuthorized.mockRejectedValue(
+ new Error(`Unauthorized to get a "my-action-type" action`)
+ );
+
+ await expect(actionsClient.get({ id: 'testPreconfigured' })).rejects.toMatchInlineSnapshot(
+ `[Error: Unauthorized to get a "my-action-type" action]`
+ );
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
+ });
+ });
+
+ test('calls unsecuredSavedObjectsClient with id', async () => {
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
type: 'type',
attributes: {},
@@ -325,8 +510,8 @@ describe('get()', () => {
id: '1',
isPreconfigured: false,
});
- expect(savedObjectsClient.get).toHaveBeenCalledTimes(1);
- expect(savedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(`
+ expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1);
+ expect(unsecuredSavedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"action",
"1",
@@ -337,12 +522,13 @@ describe('get()', () => {
test('return predefined action with id', async () => {
actionsClient = new ActionsClient({
actionTypeRegistry,
- savedObjectsClient,
+ unsecuredSavedObjectsClient,
scopedClusterClient,
defaultKibanaIndex,
actionExecutor,
executionEnqueuer,
request,
+ authorization: (authorization as unknown) as ActionsAuthorization,
preconfiguredActions: [
{
id: 'testPreconfigured',
@@ -366,12 +552,84 @@ describe('get()', () => {
isPreconfigured: true,
name: 'test',
});
- expect(savedObjectsClient.get).not.toHaveBeenCalled();
+ expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled();
});
});
describe('getAll()', () => {
- test('calls savedObjectsClient with parameters', async () => {
+ describe('authorization', () => {
+ function getAllOperation(): ReturnType {
+ const expectedResult = {
+ total: 1,
+ per_page: 10,
+ page: 1,
+ saved_objects: [
+ {
+ id: '1',
+ type: 'type',
+ attributes: {
+ name: 'test',
+ config: {
+ foo: 'bar',
+ },
+ },
+ score: 1,
+ references: [],
+ },
+ ],
+ };
+ unsecuredSavedObjectsClient.find.mockResolvedValueOnce(expectedResult);
+ scopedClusterClient.callAsInternalUser.mockResolvedValueOnce({
+ aggregations: {
+ '1': { doc_count: 6 },
+ testPreconfigured: { doc_count: 2 },
+ },
+ });
+
+ actionsClient = new ActionsClient({
+ actionTypeRegistry,
+ unsecuredSavedObjectsClient,
+ scopedClusterClient,
+ defaultKibanaIndex,
+ actionExecutor,
+ executionEnqueuer,
+ request,
+ authorization: (authorization as unknown) as ActionsAuthorization,
+ preconfiguredActions: [
+ {
+ id: 'testPreconfigured',
+ actionTypeId: '.slack',
+ secrets: {},
+ isPreconfigured: true,
+ name: 'test',
+ config: {
+ foo: 'bar',
+ },
+ },
+ ],
+ });
+ return actionsClient.getAll();
+ }
+
+ test('ensures user is authorised to get the type of action', async () => {
+ await getAllOperation();
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
+ });
+
+ test('throws when user is not authorised to create the type of action', async () => {
+ authorization.ensureAuthorized.mockRejectedValue(
+ new Error(`Unauthorized to get all actions`)
+ );
+
+ await expect(getAllOperation()).rejects.toMatchInlineSnapshot(
+ `[Error: Unauthorized to get all actions]`
+ );
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
+ });
+ });
+
+ test('calls unsecuredSavedObjectsClient with parameters', async () => {
const expectedResult = {
total: 1,
per_page: 10,
@@ -391,7 +649,7 @@ describe('getAll()', () => {
},
],
};
- savedObjectsClient.find.mockResolvedValueOnce(expectedResult);
+ unsecuredSavedObjectsClient.find.mockResolvedValueOnce(expectedResult);
scopedClusterClient.callAsInternalUser.mockResolvedValueOnce({
aggregations: {
'1': { doc_count: 6 },
@@ -401,12 +659,13 @@ describe('getAll()', () => {
actionsClient = new ActionsClient({
actionTypeRegistry,
- savedObjectsClient,
+ unsecuredSavedObjectsClient,
scopedClusterClient,
defaultKibanaIndex,
actionExecutor,
executionEnqueuer,
request,
+ authorization: (authorization as unknown) as ActionsAuthorization,
preconfiguredActions: [
{
id: 'testPreconfigured',
@@ -443,8 +702,76 @@ describe('getAll()', () => {
});
describe('getBulk()', () => {
- test('calls getBulk savedObjectsClient with parameters', async () => {
- savedObjectsClient.bulkGet.mockResolvedValueOnce({
+ describe('authorization', () => {
+ function getBulkOperation(): ReturnType {
+ unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({
+ saved_objects: [
+ {
+ id: '1',
+ type: 'action',
+ attributes: {
+ actionTypeId: 'test',
+ name: 'test',
+ config: {
+ foo: 'bar',
+ },
+ },
+ references: [],
+ },
+ ],
+ });
+ scopedClusterClient.callAsInternalUser.mockResolvedValueOnce({
+ aggregations: {
+ '1': { doc_count: 6 },
+ testPreconfigured: { doc_count: 2 },
+ },
+ });
+
+ actionsClient = new ActionsClient({
+ actionTypeRegistry,
+ unsecuredSavedObjectsClient,
+ scopedClusterClient,
+ defaultKibanaIndex,
+ actionExecutor,
+ executionEnqueuer,
+ request,
+ authorization: (authorization as unknown) as ActionsAuthorization,
+ preconfiguredActions: [
+ {
+ id: 'testPreconfigured',
+ actionTypeId: '.slack',
+ secrets: {},
+ isPreconfigured: true,
+ name: 'test',
+ config: {
+ foo: 'bar',
+ },
+ },
+ ],
+ });
+ return actionsClient.getBulk(['1', 'testPreconfigured']);
+ }
+
+ test('ensures user is authorised to get the type of action', async () => {
+ await getBulkOperation();
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
+ });
+
+ test('throws when user is not authorised to create the type of action', async () => {
+ authorization.ensureAuthorized.mockRejectedValue(
+ new Error(`Unauthorized to get all actions`)
+ );
+
+ await expect(getBulkOperation()).rejects.toMatchInlineSnapshot(
+ `[Error: Unauthorized to get all actions]`
+ );
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
+ });
+ });
+
+ test('calls getBulk unsecuredSavedObjectsClient with parameters', async () => {
+ unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({
saved_objects: [
{
id: '1',
@@ -469,12 +796,13 @@ describe('getBulk()', () => {
actionsClient = new ActionsClient({
actionTypeRegistry,
- savedObjectsClient,
+ unsecuredSavedObjectsClient,
scopedClusterClient,
defaultKibanaIndex,
actionExecutor,
executionEnqueuer,
request,
+ authorization: (authorization as unknown) as ActionsAuthorization,
preconfiguredActions: [
{
id: 'testPreconfigured',
@@ -514,13 +842,32 @@ describe('getBulk()', () => {
});
describe('delete()', () => {
- test('calls savedObjectsClient with id', async () => {
+ describe('authorization', () => {
+ test('ensures user is authorised to delete actions', async () => {
+ await actionsClient.delete({ id: '1' });
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('delete');
+ });
+
+ test('throws when user is not authorised to create the type of action', async () => {
+ authorization.ensureAuthorized.mockRejectedValue(
+ new Error(`Unauthorized to delete all actions`)
+ );
+
+ await expect(actionsClient.delete({ id: '1' })).rejects.toMatchInlineSnapshot(
+ `[Error: Unauthorized to delete all actions]`
+ );
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('delete');
+ });
+ });
+
+ test('calls unsecuredSavedObjectsClient with id', async () => {
const expectedResult = Symbol();
- savedObjectsClient.delete.mockResolvedValueOnce(expectedResult);
+ unsecuredSavedObjectsClient.delete.mockResolvedValueOnce(expectedResult);
const result = await actionsClient.delete({ id: '1' });
expect(result).toEqual(expectedResult);
- expect(savedObjectsClient.delete).toHaveBeenCalledTimes(1);
- expect(savedObjectsClient.delete.mock.calls[0]).toMatchInlineSnapshot(`
+ expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledTimes(1);
+ expect(unsecuredSavedObjectsClient.delete.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"action",
"1",
@@ -530,6 +877,60 @@ describe('delete()', () => {
});
describe('update()', () => {
+ describe('authorization', () => {
+ function updateOperation(): ReturnType {
+ actionTypeRegistry.register({
+ id: 'my-action-type',
+ name: 'My action type',
+ minimumLicenseRequired: 'basic',
+ executor,
+ });
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
+ id: '1',
+ type: 'action',
+ attributes: {
+ actionTypeId: 'my-action-type',
+ },
+ references: [],
+ });
+ unsecuredSavedObjectsClient.update.mockResolvedValueOnce({
+ id: 'my-action',
+ type: 'action',
+ attributes: {
+ actionTypeId: 'my-action-type',
+ name: 'my name',
+ config: {},
+ secrets: {},
+ },
+ references: [],
+ });
+ return actionsClient.update({
+ id: 'my-action',
+ action: {
+ name: 'my name',
+ config: {},
+ secrets: {},
+ },
+ });
+ }
+ test('ensures user is authorised to update actions', async () => {
+ await updateOperation();
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update');
+ });
+
+ test('throws when user is not authorised to create the type of action', async () => {
+ authorization.ensureAuthorized.mockRejectedValue(
+ new Error(`Unauthorized to update all actions`)
+ );
+
+ await expect(updateOperation()).rejects.toMatchInlineSnapshot(
+ `[Error: Unauthorized to update all actions]`
+ );
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update');
+ });
+ });
+
test('updates an action with all given properties', async () => {
actionTypeRegistry.register({
id: 'my-action-type',
@@ -537,7 +938,7 @@ describe('update()', () => {
minimumLicenseRequired: 'basic',
executor,
});
- savedObjectsClient.get.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
type: 'action',
attributes: {
@@ -545,7 +946,7 @@ describe('update()', () => {
},
references: [],
});
- savedObjectsClient.update.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.update.mockResolvedValueOnce({
id: 'my-action',
type: 'action',
attributes: {
@@ -571,8 +972,8 @@ describe('update()', () => {
name: 'my name',
config: {},
});
- expect(savedObjectsClient.update).toHaveBeenCalledTimes(1);
- expect(savedObjectsClient.update.mock.calls[0]).toMatchInlineSnapshot(`
+ expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1);
+ expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"action",
"my-action",
@@ -584,8 +985,8 @@ describe('update()', () => {
},
]
`);
- expect(savedObjectsClient.get).toHaveBeenCalledTimes(1);
- expect(savedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(`
+ expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1);
+ expect(unsecuredSavedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"action",
"my-action",
@@ -605,7 +1006,7 @@ describe('update()', () => {
},
executor,
});
- savedObjectsClient.get.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: 'my-action',
type: 'action',
attributes: {
@@ -634,7 +1035,7 @@ describe('update()', () => {
minimumLicenseRequired: 'basic',
executor,
});
- savedObjectsClient.get.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: 'my-action',
type: 'action',
attributes: {
@@ -642,7 +1043,7 @@ describe('update()', () => {
},
references: [],
});
- savedObjectsClient.update.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.update.mockResolvedValueOnce({
id: 'my-action',
type: 'action',
attributes: {
@@ -680,8 +1081,8 @@ describe('update()', () => {
c: true,
},
});
- expect(savedObjectsClient.update).toHaveBeenCalledTimes(1);
- expect(savedObjectsClient.update.mock.calls[0]).toMatchInlineSnapshot(`
+ expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1);
+ expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"action",
"my-action",
@@ -709,7 +1110,7 @@ describe('update()', () => {
mockedLicenseState.ensureLicenseForActionType.mockImplementation(() => {
throw new Error('Fail');
});
- savedObjectsClient.get.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
type: 'action',
attributes: {
@@ -717,7 +1118,7 @@ describe('update()', () => {
},
references: [],
});
- savedObjectsClient.update.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.update.mockResolvedValueOnce({
id: 'my-action',
type: 'action',
attributes: {
@@ -742,6 +1143,35 @@ describe('update()', () => {
});
describe('execute()', () => {
+ describe('authorization', () => {
+ test('ensures user is authorised to excecute actions', async () => {
+ await actionsClient.execute({
+ actionId: 'action-id',
+ params: {
+ name: 'my name',
+ },
+ });
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('execute');
+ });
+
+ test('throws when user is not authorised to create the type of action', async () => {
+ authorization.ensureAuthorized.mockRejectedValue(
+ new Error(`Unauthorized to execute all actions`)
+ );
+
+ await expect(
+ actionsClient.execute({
+ actionId: 'action-id',
+ params: {
+ name: 'my name',
+ },
+ })
+ ).rejects.toMatchInlineSnapshot(`[Error: Unauthorized to execute all actions]`);
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('execute');
+ });
+ });
+
test('calls the actionExecutor with the appropriate parameters', async () => {
const actionId = uuid.v4();
actionExecutor.execute.mockResolvedValue({ status: 'ok', actionId });
@@ -765,6 +1195,35 @@ describe('execute()', () => {
});
describe('enqueueExecution()', () => {
+ describe('authorization', () => {
+ test('ensures user is authorised to excecute actions', async () => {
+ await actionsClient.enqueueExecution({
+ id: uuid.v4(),
+ params: {},
+ spaceId: 'default',
+ apiKey: null,
+ });
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('execute');
+ });
+
+ test('throws when user is not authorised to create the type of action', async () => {
+ authorization.ensureAuthorized.mockRejectedValue(
+ new Error(`Unauthorized to execute all actions`)
+ );
+
+ await expect(
+ actionsClient.enqueueExecution({
+ id: uuid.v4(),
+ params: {},
+ spaceId: 'default',
+ apiKey: null,
+ })
+ ).rejects.toMatchInlineSnapshot(`[Error: Unauthorized to execute all actions]`);
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('execute');
+ });
+ });
+
test('calls the executionEnqueuer with the appropriate parameters', async () => {
const opts = {
id: uuid.v4(),
@@ -774,6 +1233,6 @@ describe('enqueueExecution()', () => {
};
await expect(actionsClient.enqueueExecution(opts)).resolves.toMatchInlineSnapshot(`undefined`);
- expect(executionEnqueuer).toHaveBeenCalledWith(savedObjectsClient, opts);
+ expect(executionEnqueuer).toHaveBeenCalledWith(unsecuredSavedObjectsClient, opts);
});
});
diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts
index 44f9cfd5c9e61..6744a8d111623 100644
--- a/x-pack/plugins/actions/server/actions_client.ts
+++ b/x-pack/plugins/actions/server/actions_client.ts
@@ -28,6 +28,8 @@ import {
ExecutionEnqueuer,
ExecuteOptions as EnqueueExecutionOptions,
} from './create_execute_function';
+import { ActionsAuthorization } from './authorization/actions_authorization';
+import { ActionType } from '../common';
// We are assuming there won't be many actions. This is why we will load
// all the actions in advance and assume the total count to not go over 10000.
@@ -52,11 +54,12 @@ interface ConstructorOptions {
defaultKibanaIndex: string;
scopedClusterClient: ILegacyScopedClusterClient;
actionTypeRegistry: ActionTypeRegistry;
- savedObjectsClient: SavedObjectsClientContract;
+ unsecuredSavedObjectsClient: SavedObjectsClientContract;
preconfiguredActions: PreConfiguredAction[];
actionExecutor: ActionExecutorContract;
executionEnqueuer: ExecutionEnqueuer;
request: KibanaRequest;
+ authorization: ActionsAuthorization;
}
interface UpdateOptions {
@@ -67,45 +70,51 @@ interface UpdateOptions {
export class ActionsClient {
private readonly defaultKibanaIndex: string;
private readonly scopedClusterClient: ILegacyScopedClusterClient;
- private readonly savedObjectsClient: SavedObjectsClientContract;
+ private readonly unsecuredSavedObjectsClient: SavedObjectsClientContract;
private readonly actionTypeRegistry: ActionTypeRegistry;
private readonly preconfiguredActions: PreConfiguredAction[];
private readonly actionExecutor: ActionExecutorContract;
private readonly request: KibanaRequest;
+ private readonly authorization: ActionsAuthorization;
private readonly executionEnqueuer: ExecutionEnqueuer;
constructor({
actionTypeRegistry,
defaultKibanaIndex,
scopedClusterClient,
- savedObjectsClient,
+ unsecuredSavedObjectsClient,
preconfiguredActions,
actionExecutor,
executionEnqueuer,
request,
+ authorization,
}: ConstructorOptions) {
this.actionTypeRegistry = actionTypeRegistry;
- this.savedObjectsClient = savedObjectsClient;
+ this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient;
this.scopedClusterClient = scopedClusterClient;
this.defaultKibanaIndex = defaultKibanaIndex;
this.preconfiguredActions = preconfiguredActions;
this.actionExecutor = actionExecutor;
this.executionEnqueuer = executionEnqueuer;
this.request = request;
+ this.authorization = authorization;
}
/**
* Create an action
*/
- public async create({ action }: CreateOptions): Promise {
- const { actionTypeId, name, config, secrets } = action;
+ public async create({
+ action: { actionTypeId, name, config, secrets },
+ }: CreateOptions): Promise {
+ await this.authorization.ensureAuthorized('create', actionTypeId);
+
const actionType = this.actionTypeRegistry.get(actionTypeId);
const validatedActionTypeConfig = validateConfig(actionType, config);
const validatedActionTypeSecrets = validateSecrets(actionType, secrets);
this.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);
- const result = await this.savedObjectsClient.create('action', {
+ const result = await this.unsecuredSavedObjectsClient.create('action', {
actionTypeId,
name,
config: validatedActionTypeConfig as SavedObjectAttributes,
@@ -125,6 +134,8 @@ export class ActionsClient {
* Update action
*/
public async update({ id, action }: UpdateOptions): Promise {
+ await this.authorization.ensureAuthorized('update');
+
if (
this.preconfiguredActions.find((preconfiguredAction) => preconfiguredAction.id === id) !==
undefined
@@ -139,7 +150,7 @@ export class ActionsClient {
'update'
);
}
- const existingObject = await this.savedObjectsClient.get('action', id);
+ const existingObject = await this.unsecuredSavedObjectsClient.get('action', id);
const { actionTypeId } = existingObject.attributes;
const { name, config, secrets } = action;
const actionType = this.actionTypeRegistry.get(actionTypeId);
@@ -148,7 +159,7 @@ export class ActionsClient {
this.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);
- const result = await this.savedObjectsClient.update('action', id, {
+ const result = await this.unsecuredSavedObjectsClient.update('action', id, {
actionTypeId,
name,
config: validatedActionTypeConfig as SavedObjectAttributes,
@@ -168,6 +179,8 @@ export class ActionsClient {
* Get an action
*/
public async get({ id }: { id: string }): Promise {
+ await this.authorization.ensureAuthorized('get');
+
const preconfiguredActionsList = this.preconfiguredActions.find(
(preconfiguredAction) => preconfiguredAction.id === id
);
@@ -179,7 +192,7 @@ export class ActionsClient {
isPreconfigured: true,
};
}
- const result = await this.savedObjectsClient.get('action', id);
+ const result = await this.unsecuredSavedObjectsClient.get('action', id);
return {
id,
@@ -194,8 +207,10 @@ export class ActionsClient {
* Get all actions with preconfigured list
*/
public async getAll(): Promise {
+ await this.authorization.ensureAuthorized('get');
+
const savedObjectsActions = (
- await this.savedObjectsClient.find({
+ await this.unsecuredSavedObjectsClient.find({
perPage: MAX_ACTIONS_RETURNED,
type: 'action',
})
@@ -221,6 +236,8 @@ export class ActionsClient {
* Get bulk actions with preconfigured list
*/
public async getBulk(ids: string[]): Promise {
+ await this.authorization.ensureAuthorized('get');
+
const actionResults = new Array();
for (const actionId of ids) {
const action = this.preconfiguredActions.find(
@@ -242,7 +259,7 @@ export class ActionsClient {
];
const bulkGetOpts = actionSavedObjectsIds.map((id) => ({ id, type: 'action' }));
- const bulkGetResult = await this.savedObjectsClient.bulkGet(bulkGetOpts);
+ const bulkGetResult = await this.unsecuredSavedObjectsClient.bulkGet(bulkGetOpts);
for (const action of bulkGetResult.saved_objects) {
if (action.error) {
@@ -259,6 +276,8 @@ export class ActionsClient {
* Delete action
*/
public async delete({ id }: { id: string }) {
+ await this.authorization.ensureAuthorized('delete');
+
if (
this.preconfiguredActions.find((preconfiguredAction) => preconfiguredAction.id === id) !==
undefined
@@ -273,18 +292,24 @@ export class ActionsClient {
'delete'
);
}
- return await this.savedObjectsClient.delete('action', id);
+ return await this.unsecuredSavedObjectsClient.delete('action', id);
}
public async execute({
actionId,
params,
}: Omit): Promise {
+ await this.authorization.ensureAuthorized('execute');
return this.actionExecutor.execute({ actionId, params, request: this.request });
}
public async enqueueExecution(options: EnqueueExecutionOptions): Promise {
- return this.executionEnqueuer(this.savedObjectsClient, options);
+ await this.authorization.ensureAuthorized('execute');
+ return this.executionEnqueuer(this.unsecuredSavedObjectsClient, options);
+ }
+
+ public async listTypes(): Promise {
+ return this.actionTypeRegistry.list();
}
}
diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.mock.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.mock.ts
new file mode 100644
index 0000000000000..6b55c18241c55
--- /dev/null
+++ b/x-pack/plugins/actions/server/authorization/actions_authorization.mock.ts
@@ -0,0 +1,22 @@
+/*
+ * 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 { ActionsAuthorization } from './actions_authorization';
+
+export type ActionsAuthorizationMock = jest.Mocked>;
+
+const createActionsAuthorizationMock = () => {
+ const mocked: ActionsAuthorizationMock = {
+ ensureAuthorized: jest.fn(),
+ };
+ return mocked;
+};
+
+export const actionsAuthorizationMock: {
+ create: () => ActionsAuthorizationMock;
+} = {
+ create: createActionsAuthorizationMock,
+};
diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts
new file mode 100644
index 0000000000000..a48124cdbcb6a
--- /dev/null
+++ b/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts
@@ -0,0 +1,191 @@
+/*
+ * 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 { KibanaRequest } from 'kibana/server';
+import { securityMock } from '../../../../plugins/security/server/mocks';
+import { ActionsAuthorization } from './actions_authorization';
+import { actionsAuthorizationAuditLoggerMock } from './audit_logger.mock';
+import { ActionsAuthorizationAuditLogger, AuthorizationResult } from './audit_logger';
+import { ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from '../saved_objects';
+
+const request = {} as KibanaRequest;
+
+const auditLogger = actionsAuthorizationAuditLoggerMock.create();
+const realAuditLogger = new ActionsAuthorizationAuditLogger();
+
+const mockAuthorizationAction = (type: string, operation: string) => `${type}/${operation}`;
+function mockSecurity() {
+ const security = securityMock.createSetup();
+ const authorization = security.authz;
+ // typescript is having trouble inferring jest's automocking
+ (authorization.actions.savedObject.get as jest.MockedFunction<
+ typeof authorization.actions.savedObject.get
+ >).mockImplementation(mockAuthorizationAction);
+ authorization.mode.useRbacForRequest.mockReturnValue(true);
+ return { authorization };
+}
+
+beforeEach(() => {
+ jest.resetAllMocks();
+ auditLogger.actionsAuthorizationFailure.mockImplementation((username, ...args) =>
+ realAuditLogger.getAuthorizationMessage(AuthorizationResult.Unauthorized, ...args)
+ );
+ auditLogger.actionsAuthorizationSuccess.mockImplementation((username, ...args) =>
+ realAuditLogger.getAuthorizationMessage(AuthorizationResult.Authorized, ...args)
+ );
+});
+
+describe('ensureAuthorized', () => {
+ test('is a no-op when there is no authorization api', async () => {
+ const actionsAuthorization = new ActionsAuthorization({
+ request,
+ auditLogger,
+ });
+
+ await actionsAuthorization.ensureAuthorized('create', 'myType');
+ });
+
+ test('is a no-op when the security license is disabled', async () => {
+ const { authorization } = mockSecurity();
+ authorization.mode.useRbacForRequest.mockReturnValue(false);
+ const actionsAuthorization = new ActionsAuthorization({
+ request,
+ authorization,
+ auditLogger,
+ });
+
+ await actionsAuthorization.ensureAuthorized('create', 'myType');
+ });
+
+ test('ensures the user has privileges to use the operation on the Actions Saved Object type', async () => {
+ const { authorization } = mockSecurity();
+ const checkPrivileges: jest.MockedFunction> = jest.fn();
+ authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges);
+ const actionsAuthorization = new ActionsAuthorization({
+ request,
+ authorization,
+ auditLogger,
+ });
+
+ checkPrivileges.mockResolvedValueOnce({
+ username: 'some-user',
+ hasAllRequested: true,
+ privileges: [
+ {
+ privilege: mockAuthorizationAction('myType', 'create'),
+ authorized: true,
+ },
+ ],
+ });
+
+ await actionsAuthorization.ensureAuthorized('create', 'myType');
+
+ expect(authorization.actions.savedObject.get).toHaveBeenCalledWith('action', 'create');
+ expect(checkPrivileges).toHaveBeenCalledWith(mockAuthorizationAction('action', 'create'));
+
+ expect(auditLogger.actionsAuthorizationSuccess).toHaveBeenCalledTimes(1);
+ expect(auditLogger.actionsAuthorizationFailure).not.toHaveBeenCalled();
+ expect(auditLogger.actionsAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ "some-user",
+ "create",
+ "myType",
+ ]
+ `);
+ });
+
+ test('ensures the user has privileges to execute an Actions Saved Object type', async () => {
+ const { authorization } = mockSecurity();
+ const checkPrivileges: jest.MockedFunction> = jest.fn();
+ authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges);
+ const actionsAuthorization = new ActionsAuthorization({
+ request,
+ authorization,
+ auditLogger,
+ });
+
+ checkPrivileges.mockResolvedValueOnce({
+ username: 'some-user',
+ hasAllRequested: true,
+ privileges: [
+ {
+ privilege: mockAuthorizationAction('myType', 'execute'),
+ authorized: true,
+ },
+ ],
+ });
+
+ await actionsAuthorization.ensureAuthorized('execute', 'myType');
+
+ expect(authorization.actions.savedObject.get).toHaveBeenCalledWith(
+ ACTION_SAVED_OBJECT_TYPE,
+ 'get'
+ );
+ expect(authorization.actions.savedObject.get).toHaveBeenCalledWith(
+ ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
+ 'create'
+ );
+ expect(checkPrivileges).toHaveBeenCalledWith([
+ mockAuthorizationAction(ACTION_SAVED_OBJECT_TYPE, 'get'),
+ mockAuthorizationAction(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, 'create'),
+ ]);
+
+ expect(auditLogger.actionsAuthorizationSuccess).toHaveBeenCalledTimes(1);
+ expect(auditLogger.actionsAuthorizationFailure).not.toHaveBeenCalled();
+ expect(auditLogger.actionsAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ "some-user",
+ "execute",
+ "myType",
+ ]
+ `);
+ });
+
+ test('throws if user lacks the required privieleges', async () => {
+ const { authorization } = mockSecurity();
+ const checkPrivileges: jest.MockedFunction> = jest.fn();
+ authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges);
+ const actionsAuthorization = new ActionsAuthorization({
+ request,
+ authorization,
+ auditLogger,
+ });
+
+ checkPrivileges.mockResolvedValueOnce({
+ username: 'some-user',
+ hasAllRequested: false,
+ privileges: [
+ {
+ privilege: mockAuthorizationAction('myType', 'create'),
+ authorized: false,
+ },
+ {
+ privilege: mockAuthorizationAction('myOtherType', 'create'),
+ authorized: true,
+ },
+ ],
+ });
+
+ await expect(
+ actionsAuthorization.ensureAuthorized('create', 'myType')
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`"Unauthorized to create a \\"myType\\" action"`);
+
+ expect(auditLogger.actionsAuthorizationSuccess).not.toHaveBeenCalled();
+ expect(auditLogger.actionsAuthorizationFailure).toHaveBeenCalledTimes(1);
+ expect(auditLogger.actionsAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ "some-user",
+ "create",
+ "myType",
+ ]
+ `);
+ });
+});
diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.ts
new file mode 100644
index 0000000000000..da5a5a1cdc3eb
--- /dev/null
+++ b/x-pack/plugins/actions/server/authorization/actions_authorization.ts
@@ -0,0 +1,59 @@
+/*
+ * 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 Boom from 'boom';
+import { KibanaRequest } from 'src/core/server';
+import { SecurityPluginSetup } from '../../../security/server';
+import { ActionsAuthorizationAuditLogger } from './audit_logger';
+import { ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from '../saved_objects';
+
+export interface ConstructorOptions {
+ request: KibanaRequest;
+ auditLogger: ActionsAuthorizationAuditLogger;
+ authorization?: SecurityPluginSetup['authz'];
+}
+
+const operationAlias: Record<
+ string,
+ (authorization: SecurityPluginSetup['authz']) => string | string[]
+> = {
+ execute: (authorization) => [
+ authorization.actions.savedObject.get(ACTION_SAVED_OBJECT_TYPE, 'get'),
+ authorization.actions.savedObject.get(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, 'create'),
+ ],
+ list: (authorization) => authorization.actions.savedObject.get(ACTION_SAVED_OBJECT_TYPE, 'find'),
+};
+
+export class ActionsAuthorization {
+ private readonly request: KibanaRequest;
+ private readonly authorization?: SecurityPluginSetup['authz'];
+ private readonly auditLogger: ActionsAuthorizationAuditLogger;
+
+ constructor({ request, authorization, auditLogger }: ConstructorOptions) {
+ this.request = request;
+ this.authorization = authorization;
+ this.auditLogger = auditLogger;
+ }
+
+ public async ensureAuthorized(operation: string, actionTypeId?: string) {
+ const { authorization } = this;
+ if (authorization?.mode?.useRbacForRequest(this.request)) {
+ const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request);
+ const { hasAllRequested, username } = await checkPrivileges(
+ operationAlias[operation]
+ ? operationAlias[operation](authorization)
+ : authorization.actions.savedObject.get(ACTION_SAVED_OBJECT_TYPE, operation)
+ );
+ if (hasAllRequested) {
+ this.auditLogger.actionsAuthorizationSuccess(username, operation, actionTypeId);
+ } else {
+ throw Boom.forbidden(
+ this.auditLogger.actionsAuthorizationFailure(username, operation, actionTypeId)
+ );
+ }
+ }
+ }
+}
diff --git a/x-pack/plugins/actions/server/authorization/audit_logger.mock.ts b/x-pack/plugins/actions/server/authorization/audit_logger.mock.ts
new file mode 100644
index 0000000000000..95d4f4ebcd3aa
--- /dev/null
+++ b/x-pack/plugins/actions/server/authorization/audit_logger.mock.ts
@@ -0,0 +1,22 @@
+/*
+ * 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 { ActionsAuthorizationAuditLogger } from './audit_logger';
+
+const createActionsAuthorizationAuditLoggerMock = () => {
+ const mocked = ({
+ getAuthorizationMessage: jest.fn(),
+ actionsAuthorizationFailure: jest.fn(),
+ actionsAuthorizationSuccess: jest.fn(),
+ } as unknown) as jest.Mocked;
+ return mocked;
+};
+
+export const actionsAuthorizationAuditLoggerMock: {
+ create: () => jest.Mocked;
+} = {
+ create: createActionsAuthorizationAuditLoggerMock,
+};
diff --git a/x-pack/plugins/actions/server/authorization/audit_logger.test.ts b/x-pack/plugins/actions/server/authorization/audit_logger.test.ts
new file mode 100644
index 0000000000000..d700abdaa70ff
--- /dev/null
+++ b/x-pack/plugins/actions/server/authorization/audit_logger.test.ts
@@ -0,0 +1,75 @@
+/*
+ * 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 { ActionsAuthorizationAuditLogger } from './audit_logger';
+
+const createMockAuditLogger = () => {
+ return {
+ log: jest.fn(),
+ };
+};
+
+describe(`#constructor`, () => {
+ test('initializes a noop auditLogger if security logger is unavailable', () => {
+ const actionsAuditLogger = new ActionsAuthorizationAuditLogger(undefined);
+
+ const username = 'foo-user';
+ const actionTypeId = 'action-type-id';
+ const operation = 'create';
+ expect(() => {
+ actionsAuditLogger.actionsAuthorizationFailure(username, operation, actionTypeId);
+ actionsAuditLogger.actionsAuthorizationSuccess(username, operation, actionTypeId);
+ }).not.toThrow();
+ });
+});
+
+describe(`#actionsAuthorizationFailure`, () => {
+ test('logs auth failure', () => {
+ const auditLogger = createMockAuditLogger();
+ const actionsAuditLogger = new ActionsAuthorizationAuditLogger(auditLogger);
+ const username = 'foo-user';
+ const actionTypeId = 'action-type-id';
+ const operation = 'create';
+
+ actionsAuditLogger.actionsAuthorizationFailure(username, operation, actionTypeId);
+
+ expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ "actions_authorization_failure",
+ "foo-user Unauthorized to create a \\"action-type-id\\" action",
+ Object {
+ "actionTypeId": "action-type-id",
+ "operation": "create",
+ "username": "foo-user",
+ },
+ ]
+ `);
+ });
+});
+
+describe(`#savedObjectsAuthorizationSuccess`, () => {
+ test('logs auth success', () => {
+ const auditLogger = createMockAuditLogger();
+ const actionsAuditLogger = new ActionsAuthorizationAuditLogger(auditLogger);
+ const username = 'foo-user';
+ const actionTypeId = 'action-type-id';
+
+ const operation = 'create';
+
+ actionsAuditLogger.actionsAuthorizationSuccess(username, operation, actionTypeId);
+
+ expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ "actions_authorization_success",
+ "foo-user Authorized to create a \\"action-type-id\\" action",
+ Object {
+ "actionTypeId": "action-type-id",
+ "operation": "create",
+ "username": "foo-user",
+ },
+ ]
+ `);
+ });
+});
diff --git a/x-pack/plugins/actions/server/authorization/audit_logger.ts b/x-pack/plugins/actions/server/authorization/audit_logger.ts
new file mode 100644
index 0000000000000..7e0adc9206656
--- /dev/null
+++ b/x-pack/plugins/actions/server/authorization/audit_logger.ts
@@ -0,0 +1,66 @@
+/*
+ * 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 { AuditLogger } from '../../../security/server';
+
+export enum AuthorizationResult {
+ Unauthorized = 'Unauthorized',
+ Authorized = 'Authorized',
+}
+
+export class ActionsAuthorizationAuditLogger {
+ private readonly auditLogger: AuditLogger;
+
+ constructor(auditLogger: AuditLogger = { log() {} }) {
+ this.auditLogger = auditLogger;
+ }
+
+ public getAuthorizationMessage(
+ authorizationResult: AuthorizationResult,
+ operation: string,
+ actionTypeId?: string
+ ): string {
+ return `${authorizationResult} to ${operation} ${
+ actionTypeId ? `a "${actionTypeId}" action` : `actions`
+ }`;
+ }
+
+ public actionsAuthorizationFailure(
+ username: string,
+ operation: string,
+ actionTypeId?: string
+ ): string {
+ const message = this.getAuthorizationMessage(
+ AuthorizationResult.Unauthorized,
+ operation,
+ actionTypeId
+ );
+ this.auditLogger.log('actions_authorization_failure', `${username} ${message}`, {
+ username,
+ actionTypeId,
+ operation,
+ });
+ return message;
+ }
+
+ public actionsAuthorizationSuccess(
+ username: string,
+ operation: string,
+ actionTypeId?: string
+ ): string {
+ const message = this.getAuthorizationMessage(
+ AuthorizationResult.Authorized,
+ operation,
+ actionTypeId
+ );
+ this.auditLogger.log('actions_authorization_success', `${username} ${message}`, {
+ username,
+ actionTypeId,
+ operation,
+ });
+ return message;
+ }
+}
diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts
index 2bad33d56f228..85052eef93e05 100644
--- a/x-pack/plugins/actions/server/create_execute_function.ts
+++ b/x-pack/plugins/actions/server/create_execute_function.ts
@@ -7,6 +7,7 @@
import { SavedObjectsClientContract } from '../../../../src/core/server';
import { TaskManagerStartContract } from '../../task_manager/server';
import { RawAction, ActionTypeRegistryContract, PreConfiguredAction } from './types';
+import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from './saved_objects';
interface CreateExecuteFunctionOptions {
taskManager: TaskManagerStartContract;
@@ -49,11 +50,14 @@ export function createExecutionEnqueuerFunction({
actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);
}
- const actionTaskParamsRecord = await savedObjectsClient.create('action_task_params', {
- actionId: id,
- params,
- apiKey,
- });
+ const actionTaskParamsRecord = await savedObjectsClient.create(
+ ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
+ {
+ actionId: id,
+ params,
+ apiKey,
+ }
+ );
await taskManager.schedule({
taskType: `actions:${actionTypeId}`,
diff --git a/x-pack/plugins/actions/server/feature.ts b/x-pack/plugins/actions/server/feature.ts
new file mode 100644
index 0000000000000..c06acb6761454
--- /dev/null
+++ b/x-pack/plugins/actions/server/feature.ts
@@ -0,0 +1,41 @@
+/*
+ * 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';
+import { ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from './saved_objects';
+
+export const ACTIONS_FEATURE = {
+ id: 'actions',
+ name: i18n.translate('xpack.actions.featureRegistry.actionsFeatureName', {
+ defaultMessage: 'Actions',
+ }),
+ icon: 'bell',
+ navLinkId: 'actions',
+ app: [],
+ privileges: {
+ all: {
+ app: [],
+ api: [],
+ catalogue: [],
+ savedObject: {
+ all: [ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE],
+ read: [],
+ },
+ ui: ['show', 'execute', 'save', 'delete'],
+ },
+ read: {
+ app: [],
+ api: [],
+ catalogue: [],
+ savedObject: {
+ // action execution requires 'read' over `actions`, but 'all' over `action_task_params`
+ all: [ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE],
+ read: [ACTION_SAVED_OBJECT_TYPE],
+ },
+ ui: ['show', 'execute'],
+ },
+ },
+};
diff --git a/x-pack/plugins/actions/server/index.ts b/x-pack/plugins/actions/server/index.ts
index 88553c314112f..fef70c3a48455 100644
--- a/x-pack/plugins/actions/server/index.ts
+++ b/x-pack/plugins/actions/server/index.ts
@@ -8,8 +8,10 @@ import { PluginInitializerContext } from '../../../../src/core/server';
import { ActionsPlugin } from './plugin';
import { configSchema } from './config';
import { ActionsClient as ActionsClientClass } from './actions_client';
+import { ActionsAuthorization as ActionsAuthorizationClass } from './authorization/actions_authorization';
export type ActionsClient = PublicMethodsOf;
+export type ActionsAuthorization = PublicMethodsOf;
export {
ActionsPlugin,
diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts
index c8e6669275e11..65fd0646c639e 100644
--- a/x-pack/plugins/actions/server/lib/action_executor.test.ts
+++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts
@@ -9,15 +9,17 @@ import { schema } from '@kbn/config-schema';
import { ActionExecutor } from './action_executor';
import { actionTypeRegistryMock } from '../action_type_registry.mock';
import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks';
-import { loggingSystemMock, savedObjectsClientMock } from '../../../../../src/core/server/mocks';
+import { loggingSystemMock } from '../../../../../src/core/server/mocks';
import { eventLoggerMock } from '../../../event_log/server/mocks';
import { spacesServiceMock } from '../../../spaces/server/spaces_service/spaces_service.mock';
import { ActionType } from '../types';
-import { actionsMock } from '../mocks';
+import { actionsMock, actionsClientMock } from '../mocks';
+import { pick } from 'lodash';
const actionExecutor = new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false });
const services = actionsMock.createServices();
-const savedObjectsClientWithHidden = savedObjectsClientMock.create();
+
+const actionsClient = actionsClientMock.create();
const encryptedSavedObjectsClient = encryptedSavedObjectsMock.createClient();
const actionTypeRegistry = actionTypeRegistryMock.create();
@@ -30,11 +32,12 @@ const executeParams = {
};
const spacesMock = spacesServiceMock.createSetupContract();
+const getActionsClientWithRequest = jest.fn();
actionExecutor.initialize({
logger: loggingSystemMock.create().get(),
spaces: spacesMock,
getServices: () => services,
- getScopedSavedObjectsClient: () => savedObjectsClientWithHidden,
+ getActionsClientWithRequest,
actionTypeRegistry,
encryptedSavedObjectsClient,
eventLogger: eventLoggerMock.create(),
@@ -44,6 +47,7 @@ actionExecutor.initialize({
beforeEach(() => {
jest.resetAllMocks();
spacesMock.getSpaceId.mockReturnValue('some-namespace');
+ getActionsClientWithRequest.mockResolvedValue(actionsClient);
});
test('successfully executes', async () => {
@@ -67,7 +71,13 @@ test('successfully executes', async () => {
},
references: [],
};
- savedObjectsClientWithHidden.get.mockResolvedValueOnce(actionSavedObject);
+ const actionResult = {
+ id: actionSavedObject.id,
+ name: actionSavedObject.id,
+ ...pick(actionSavedObject.attributes, 'actionTypeId', 'config'),
+ isPreconfigured: false,
+ };
+ actionsClient.get.mockResolvedValueOnce(actionResult);
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject);
actionTypeRegistry.get.mockReturnValueOnce(actionType);
await actionExecutor.execute(executeParams);
@@ -108,7 +118,13 @@ test('provides empty config when config and / or secrets is empty', async () =>
},
references: [],
};
- savedObjectsClientWithHidden.get.mockResolvedValueOnce(actionSavedObject);
+ const actionResult = {
+ id: actionSavedObject.id,
+ name: actionSavedObject.id,
+ actionTypeId: actionSavedObject.attributes.actionTypeId,
+ isPreconfigured: false,
+ };
+ actionsClient.get.mockResolvedValueOnce(actionResult);
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject);
actionTypeRegistry.get.mockReturnValueOnce(actionType);
await actionExecutor.execute(executeParams);
@@ -138,7 +154,13 @@ test('throws an error when config is invalid', async () => {
},
references: [],
};
- savedObjectsClientWithHidden.get.mockResolvedValueOnce(actionSavedObject);
+ const actionResult = {
+ id: actionSavedObject.id,
+ name: actionSavedObject.id,
+ actionTypeId: actionSavedObject.attributes.actionTypeId,
+ isPreconfigured: false,
+ };
+ actionsClient.get.mockResolvedValueOnce(actionResult);
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject);
actionTypeRegistry.get.mockReturnValueOnce(actionType);
@@ -171,7 +193,13 @@ test('throws an error when params is invalid', async () => {
},
references: [],
};
- savedObjectsClientWithHidden.get.mockResolvedValueOnce(actionSavedObject);
+ const actionResult = {
+ id: actionSavedObject.id,
+ name: actionSavedObject.id,
+ actionTypeId: actionSavedObject.attributes.actionTypeId,
+ isPreconfigured: false,
+ };
+ actionsClient.get.mockResolvedValueOnce(actionResult);
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject);
actionTypeRegistry.get.mockReturnValueOnce(actionType);
@@ -185,7 +213,7 @@ test('throws an error when params is invalid', async () => {
});
test('throws an error when failing to load action through savedObjectsClient', async () => {
- savedObjectsClientWithHidden.get.mockRejectedValueOnce(new Error('No access'));
+ actionsClient.get.mockRejectedValueOnce(new Error('No access'));
await expect(actionExecutor.execute(executeParams)).rejects.toThrowErrorMatchingInlineSnapshot(
`"No access"`
);
@@ -206,7 +234,13 @@ test('throws an error if actionType is not enabled', async () => {
},
references: [],
};
- savedObjectsClientWithHidden.get.mockResolvedValueOnce(actionSavedObject);
+ const actionResult = {
+ id: actionSavedObject.id,
+ name: actionSavedObject.id,
+ actionTypeId: actionSavedObject.attributes.actionTypeId,
+ isPreconfigured: false,
+ };
+ actionsClient.get.mockResolvedValueOnce(actionResult);
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject);
actionTypeRegistry.get.mockReturnValueOnce(actionType);
actionTypeRegistry.ensureActionTypeEnabled.mockImplementationOnce(() => {
@@ -240,7 +274,13 @@ test('should not throws an error if actionType is preconfigured', async () => {
},
references: [],
};
- savedObjectsClientWithHidden.get.mockResolvedValueOnce(actionSavedObject);
+ const actionResult = {
+ id: actionSavedObject.id,
+ name: actionSavedObject.id,
+ ...pick(actionSavedObject.attributes, 'actionTypeId', 'config', 'secrets'),
+ isPreconfigured: false,
+ };
+ actionsClient.get.mockResolvedValueOnce(actionResult);
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject);
actionTypeRegistry.get.mockReturnValueOnce(actionType);
actionTypeRegistry.ensureActionTypeEnabled.mockImplementationOnce(() => {
@@ -268,7 +308,7 @@ test('throws an error when passing isESOUsingEphemeralEncryptionKey with value o
customActionExecutor.initialize({
logger: loggingSystemMock.create().get(),
spaces: spacesMock,
- getScopedSavedObjectsClient: () => savedObjectsClientWithHidden,
+ getActionsClientWithRequest,
getServices: () => services,
actionTypeRegistry,
encryptedSavedObjectsClient,
diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts
index 250bfc2752f1b..0e63cc8f5956e 100644
--- a/x-pack/plugins/actions/server/lib/action_executor.ts
+++ b/x-pack/plugins/actions/server/lib/action_executor.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { Logger, KibanaRequest, SavedObjectsClientContract } from '../../../../../src/core/server';
+import { Logger, KibanaRequest } from '../../../../../src/core/server';
import { validateParams, validateConfig, validateSecrets } from './validate_with_schema';
import {
ActionTypeExecutorResult,
@@ -15,14 +15,15 @@ import {
} from '../types';
import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server';
import { SpacesServiceSetup } from '../../../spaces/server';
-import { EVENT_LOG_ACTIONS } from '../plugin';
+import { EVENT_LOG_ACTIONS, PluginStartContract } from '../plugin';
import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server';
+import { ActionsClient } from '../actions_client';
export interface ActionExecutorContext {
logger: Logger;
spaces?: SpacesServiceSetup;
getServices: GetServicesFunction;
- getScopedSavedObjectsClient: (req: KibanaRequest) => SavedObjectsClientContract;
+ getActionsClientWithRequest: PluginStartContract['getActionsClientWithRequest'];
encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
actionTypeRegistry: ActionTypeRegistryContract;
eventLogger: IEventLogger;
@@ -76,7 +77,7 @@ export class ActionExecutor {
actionTypeRegistry,
eventLogger,
preconfiguredActions,
- getScopedSavedObjectsClient,
+ getActionsClientWithRequest,
} = this.actionExecutorContext!;
const services = getServices(request);
@@ -84,7 +85,7 @@ export class ActionExecutor {
const namespace = spaceId && spaceId !== 'default' ? { namespace: spaceId } : {};
const { actionTypeId, name, config, secrets } = await getActionInfo(
- getScopedSavedObjectsClient(request),
+ await getActionsClientWithRequest(request),
encryptedSavedObjectsClient,
preconfiguredActions,
actionId,
@@ -196,7 +197,7 @@ interface ActionInfo {
}
async function getActionInfo(
- savedObjectsClient: SavedObjectsClientContract,
+ actionsClient: PublicMethodsOf,
encryptedSavedObjectsClient: EncryptedSavedObjectsClient,
preconfiguredActions: PreConfiguredAction[],
actionId: string,
@@ -217,9 +218,7 @@ async function getActionInfo(
// if not pre-configured action, should be a saved object
// ensure user can read the action before processing
- const {
- attributes: { actionTypeId, config, name },
- } = await savedObjectsClient.get('action', actionId);
+ const { actionTypeId, config, name } = await actionsClient.get({ id: actionId });
const {
attributes: { secrets },
diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts
index 06cb84ad79a89..78522682054e1 100644
--- a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts
+++ b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts
@@ -15,6 +15,7 @@ import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/serv
import { savedObjectsClientMock, loggingSystemMock } from 'src/core/server/mocks';
import { eventLoggerMock } from '../../../event_log/server/mocks';
import { ActionTypeDisabledError } from './errors';
+import { actionsClientMock } from '../mocks';
const spaceIdToNamespace = jest.fn();
const actionTypeRegistry = actionTypeRegistryMock.create();
@@ -59,7 +60,7 @@ const actionExecutorInitializerParams = {
logger: loggingSystemMock.create().get(),
getServices: jest.fn().mockReturnValue(services),
actionTypeRegistry,
- getScopedSavedObjectsClient: () => savedObjectsClientMock.create(),
+ getActionsClientWithRequest: jest.fn(async () => actionsClientMock.create()),
encryptedSavedObjectsClient: mockedEncryptedSavedObjectsClient,
eventLogger: eventLoggerMock.create(),
preconfiguredActions: [],
diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.ts
index a962497f906a9..9204c41b9288c 100644
--- a/x-pack/plugins/actions/server/lib/task_runner_factory.ts
+++ b/x-pack/plugins/actions/server/lib/task_runner_factory.ts
@@ -17,6 +17,7 @@ import {
SpaceIdToNamespaceFunction,
ActionTypeExecutorResult,
} from '../types';
+import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from '../saved_objects';
export interface TaskRunnerContext {
logger: Logger;
@@ -66,7 +67,7 @@ export class TaskRunnerFactory {
const {
attributes: { actionId, params, apiKey },
} = await encryptedSavedObjectsClient.getDecryptedAsInternalUser(
- 'action_task_params',
+ ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
actionTaskParamsId,
{ namespace }
);
@@ -121,11 +122,11 @@ export class TaskRunnerFactory {
// Cleanup action_task_params object now that we're done with it
try {
const savedObjectsClient = getScopedSavedObjectsClient(fakeRequest);
- await savedObjectsClient.delete('action_task_params', actionTaskParamsId);
+ await savedObjectsClient.delete(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, actionTaskParamsId);
} catch (e) {
// Log error only, we shouldn't fail the task because of an error here (if ever there's retry logic)
logger.error(
- `Failed to cleanup action_task_params object [id="${actionTaskParamsId}"]: ${e.message}`
+ `Failed to cleanup ${ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE} object [id="${actionTaskParamsId}"]: ${e.message}`
);
}
},
diff --git a/x-pack/plugins/actions/server/mocks.ts b/x-pack/plugins/actions/server/mocks.ts
index b1e40dce811a0..e2f11abeefff2 100644
--- a/x-pack/plugins/actions/server/mocks.ts
+++ b/x-pack/plugins/actions/server/mocks.ts
@@ -11,7 +11,9 @@ import {
elasticsearchServiceMock,
savedObjectsClientMock,
} from '../../../../src/core/server/mocks';
+import { actionsAuthorizationMock } from './authorization/actions_authorization.mock';
+export { actionsAuthorizationMock };
export { actionsClientMock };
const createSetupMock = () => {
@@ -26,6 +28,9 @@ const createStartMock = () => {
isActionTypeEnabled: jest.fn(),
isActionExecutable: jest.fn(),
getActionsClientWithRequest: jest.fn().mockResolvedValue(actionsClientMock.create()),
+ getActionsAuthorizationWithRequest: jest
+ .fn()
+ .mockReturnValue(actionsAuthorizationMock.create()),
preconfiguredActions: [],
};
return mock;
diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts
index 1602b26559bed..ac4b332e7fd7a 100644
--- a/x-pack/plugins/actions/server/plugin.test.ts
+++ b/x-pack/plugins/actions/server/plugin.test.ts
@@ -8,6 +8,7 @@ import { PluginInitializerContext, RequestHandlerContext } from '../../../../src
import { coreMock, httpServerMock } from '../../../../src/core/server/mocks';
import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collection/server/mocks';
import { licensingMock } from '../../licensing/server/mocks';
+import { featuresPluginMock } from '../../features/server/mocks';
import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks';
import { taskManagerMock } from '../../task_manager/server/mocks';
import { eventLogMock } from '../../event_log/server/mocks';
@@ -43,6 +44,7 @@ describe('Actions Plugin', () => {
licensing: licensingMock.createSetup(),
eventLog: eventLogMock.createSetup(),
usageCollection: usageCollectionPluginMock.createSetupContract(),
+ features: featuresPluginMock.createSetup(),
};
});
@@ -200,6 +202,7 @@ describe('Actions Plugin', () => {
licensing: licensingMock.createSetup(),
eventLog: eventLogMock.createSetup(),
usageCollection: usageCollectionPluginMock.createSetupContract(),
+ features: featuresPluginMock.createSetup(),
};
pluginsStart = {
taskManager: taskManagerMock.createStart(),
diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts
index 114c85ae9f9da..5b8b25d02658b 100644
--- a/x-pack/plugins/actions/server/plugin.ts
+++ b/x-pack/plugins/actions/server/plugin.ts
@@ -29,6 +29,8 @@ import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_m
import { LicensingPluginSetup } from '../../licensing/server';
import { LICENSE_TYPE } from '../../licensing/common/types';
import { SpacesPluginSetup, SpacesServiceSetup } from '../../spaces/server';
+import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
+import { SecurityPluginSetup } from '../../security/server';
import { ActionsConfig } from './config';
import { Services, ActionType, PreConfiguredAction } from './types';
@@ -52,7 +54,14 @@ import {
} from './routes';
import { IEventLogger, IEventLogService } from '../../event_log/server';
import { initializeActionsTelemetry, scheduleActionsTelemetry } from './usage/task';
-import { setupSavedObjects } from './saved_objects';
+import {
+ setupSavedObjects,
+ ACTION_SAVED_OBJECT_TYPE,
+ ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
+} from './saved_objects';
+import { ACTIONS_FEATURE } from './feature';
+import { ActionsAuthorization } from './authorization/actions_authorization';
+import { ActionsAuthorizationAuditLogger } from './authorization/audit_logger';
const EVENT_LOG_PROVIDER = 'actions';
export const EVENT_LOG_ACTIONS = {
@@ -68,6 +77,7 @@ export interface PluginStartContract {
isActionTypeEnabled(id: string): boolean;
isActionExecutable(actionId: string, actionTypeId: string): boolean;
getActionsClientWithRequest(request: KibanaRequest): Promise>;
+ getActionsAuthorizationWithRequest(request: KibanaRequest): PublicMethodsOf;
preconfiguredActions: PreConfiguredAction[];
}
@@ -78,13 +88,15 @@ export interface ActionsPluginsSetup {
spaces?: SpacesPluginSetup;
eventLog: IEventLogService;
usageCollection?: UsageCollectionSetup;
+ security?: SecurityPluginSetup;
+ features: FeaturesPluginSetup;
}
export interface ActionsPluginsStart {
encryptedSavedObjects: EncryptedSavedObjectsPluginStart;
taskManager: TaskManagerStartContract;
}
-const includedHiddenTypes = ['action', 'action_task_params'];
+const includedHiddenTypes = [ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE];
export class ActionsPlugin implements Plugin, PluginStartContract> {
private readonly kibanaIndex: Promise;
@@ -97,6 +109,7 @@ export class ActionsPlugin implements Plugin, Plugi
private actionExecutor?: ActionExecutor;
private licenseState: ILicenseState | null = null;
private spaces?: SpacesServiceSetup;
+ private security?: SecurityPluginSetup;
private eventLogger?: IEventLogger;
private isESOUsingEphemeralEncryptionKey?: boolean;
private readonly telemetryLogger: Logger;
@@ -131,6 +144,7 @@ export class ActionsPlugin implements Plugin, Plugi
);
}
+ plugins.features.registerFeature(ACTIONS_FEATURE);
setupSavedObjects(core.savedObjects, plugins.encryptedSavedObjects);
plugins.eventLog.registerProviderActions(EVENT_LOG_PROVIDER, Object.values(EVENT_LOG_ACTIONS));
@@ -167,6 +181,7 @@ export class ActionsPlugin implements Plugin, Plugi
this.serverBasePath = core.http.basePath.serverBasePath;
this.actionExecutor = actionExecutor;
this.spaces = plugins.spaces?.spacesService;
+ this.security = plugins.security;
registerBuiltInActionTypes({
logger: this.logger,
@@ -227,16 +242,39 @@ export class ActionsPlugin implements Plugin, Plugi
kibanaIndex,
isESOUsingEphemeralEncryptionKey,
preconfiguredActions,
+ instantiateAuthorization,
} = this;
const encryptedSavedObjectsClient = plugins.encryptedSavedObjects.getClient({
includedHiddenTypes,
});
- const getScopedSavedObjectsClient = (request: KibanaRequest) =>
- core.savedObjects.getScopedClient(request, {
- includedHiddenTypes,
+ const getActionsClientWithRequest = async (request: KibanaRequest) => {
+ if (isESOUsingEphemeralEncryptionKey === true) {
+ throw new Error(
+ `Unable to create actions client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml`
+ );
+ }
+ return new ActionsClient({
+ unsecuredSavedObjectsClient: core.savedObjects.getScopedClient(request, {
+ excludedWrappers: ['security'],
+ includedHiddenTypes,
+ }),
+ actionTypeRegistry: actionTypeRegistry!,
+ defaultKibanaIndex: await kibanaIndex,
+ scopedClusterClient: core.elasticsearch.legacy.client.asScoped(request),
+ preconfiguredActions,
+ request,
+ authorization: instantiateAuthorization(request),
+ actionExecutor: actionExecutor!,
+ executionEnqueuer: createExecutionEnqueuerFunction({
+ taskManager: plugins.taskManager,
+ actionTypeRegistry: actionTypeRegistry!,
+ isESOUsingEphemeralEncryptionKey: isESOUsingEphemeralEncryptionKey!,
+ preconfiguredActions,
+ }),
});
+ };
const getScopedSavedObjectsClientWithoutAccessToActions = (request: KibanaRequest) =>
core.savedObjects.getScopedClient(request);
@@ -245,7 +283,7 @@ export class ActionsPlugin implements Plugin, Plugi
logger,
eventLogger: this.eventLogger!,
spaces: this.spaces,
- getScopedSavedObjectsClient,
+ getActionsClientWithRequest,
getServices: this.getServicesFactory(
getScopedSavedObjectsClientWithoutAccessToActions,
core.elasticsearch
@@ -261,7 +299,10 @@ export class ActionsPlugin implements Plugin, Plugi
encryptedSavedObjectsClient,
getBasePath: this.getBasePath,
spaceIdToNamespace: this.spaceIdToNamespace,
- getScopedSavedObjectsClient,
+ getScopedSavedObjectsClient: (request: KibanaRequest) =>
+ core.savedObjects.getScopedClient(request, {
+ includedHiddenTypes,
+ }),
});
scheduleActionsTelemetry(this.telemetryLogger, plugins.taskManager);
@@ -273,33 +314,24 @@ export class ActionsPlugin implements Plugin, Plugi
isActionExecutable: (actionId: string, actionTypeId: string) => {
return this.actionTypeRegistry!.isActionExecutable(actionId, actionTypeId);
},
- // Ability to get an actions client from legacy code
- async getActionsClientWithRequest(request: KibanaRequest) {
- if (isESOUsingEphemeralEncryptionKey === true) {
- throw new Error(
- `Unable to create actions client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml`
- );
- }
- return new ActionsClient({
- savedObjectsClient: getScopedSavedObjectsClient(request),
- actionTypeRegistry: actionTypeRegistry!,
- defaultKibanaIndex: await kibanaIndex,
- scopedClusterClient: core.elasticsearch.legacy.client.asScoped(request),
- preconfiguredActions,
- request,
- actionExecutor: actionExecutor!,
- executionEnqueuer: createExecutionEnqueuerFunction({
- taskManager: plugins.taskManager,
- actionTypeRegistry: actionTypeRegistry!,
- isESOUsingEphemeralEncryptionKey: isESOUsingEphemeralEncryptionKey!,
- preconfiguredActions,
- }),
- });
+ getActionsAuthorizationWithRequest(request: KibanaRequest) {
+ return instantiateAuthorization(request);
},
+ getActionsClientWithRequest,
preconfiguredActions,
};
}
+ private instantiateAuthorization = (request: KibanaRequest) => {
+ return new ActionsAuthorization({
+ request,
+ authorization: this.security?.authz,
+ auditLogger: new ActionsAuthorizationAuditLogger(
+ this.security?.audit.getLogger(ACTIONS_FEATURE.id)
+ ),
+ });
+ };
+
private getServicesFactory(
getScopedClient: (request: KibanaRequest) => SavedObjectsClientContract,
elasticsearch: ElasticsearchServiceStart
@@ -322,6 +354,7 @@ export class ActionsPlugin implements Plugin, Plugi
isESOUsingEphemeralEncryptionKey,
preconfiguredActions,
actionExecutor,
+ instantiateAuthorization,
} = this;
return async function actionsRouteHandlerContext(context, request) {
@@ -334,12 +367,16 @@ export class ActionsPlugin implements Plugin, Plugi
);
}
return new ActionsClient({
- savedObjectsClient: savedObjects.getScopedClient(request, { includedHiddenTypes }),
+ unsecuredSavedObjectsClient: savedObjects.getScopedClient(request, {
+ excludedWrappers: ['security'],
+ includedHiddenTypes,
+ }),
actionTypeRegistry: actionTypeRegistry!,
defaultKibanaIndex,
scopedClusterClient: context.core.elasticsearch.legacy.client,
preconfiguredActions,
request,
+ authorization: instantiateAuthorization(request),
actionExecutor: actionExecutor!,
executionEnqueuer: createExecutionEnqueuerFunction({
taskManager,
diff --git a/x-pack/plugins/actions/server/routes/create.test.ts b/x-pack/plugins/actions/server/routes/create.test.ts
index 940b8ecc61f4e..76f2a79c9f3ee 100644
--- a/x-pack/plugins/actions/server/routes/create.test.ts
+++ b/x-pack/plugins/actions/server/routes/create.test.ts
@@ -28,13 +28,6 @@ describe('createActionRoute', () => {
const [config, handler] = router.post.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/actions/action"`);
- expect(config.options).toMatchInlineSnapshot(`
- Object {
- "tags": Array [
- "access:actions-all",
- ],
- }
- `);
const createResult = {
id: '1',
diff --git a/x-pack/plugins/actions/server/routes/create.ts b/x-pack/plugins/actions/server/routes/create.ts
index 8135567157583..462d3f42b506c 100644
--- a/x-pack/plugins/actions/server/routes/create.ts
+++ b/x-pack/plugins/actions/server/routes/create.ts
@@ -30,9 +30,6 @@ export const createActionRoute = (router: IRouter, licenseState: ILicenseState)
validate: {
body: bodySchema,
},
- options: {
- tags: ['access:actions-all'],
- },
},
router.handleLegacyErrors(async function (
context: RequestHandlerContext,
diff --git a/x-pack/plugins/actions/server/routes/delete.test.ts b/x-pack/plugins/actions/server/routes/delete.test.ts
index 8d759f1a7565e..3bd2d93f255df 100644
--- a/x-pack/plugins/actions/server/routes/delete.test.ts
+++ b/x-pack/plugins/actions/server/routes/delete.test.ts
@@ -28,13 +28,6 @@ describe('deleteActionRoute', () => {
const [config, handler] = router.delete.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/actions/action/{id}"`);
- expect(config.options).toMatchInlineSnapshot(`
- Object {
- "tags": Array [
- "access:actions-all",
- ],
- }
- `);
const actionsClient = actionsClientMock.create();
actionsClient.delete.mockResolvedValueOnce({});
diff --git a/x-pack/plugins/actions/server/routes/delete.ts b/x-pack/plugins/actions/server/routes/delete.ts
index 9d4fa4019744c..a7303247e95b0 100644
--- a/x-pack/plugins/actions/server/routes/delete.ts
+++ b/x-pack/plugins/actions/server/routes/delete.ts
@@ -31,9 +31,6 @@ export const deleteActionRoute = (router: IRouter, licenseState: ILicenseState)
validate: {
params: paramSchema,
},
- options: {
- tags: ['access:actions-all'],
- },
},
router.handleLegacyErrors(async function (
context: RequestHandlerContext,
diff --git a/x-pack/plugins/actions/server/routes/execute.test.ts b/x-pack/plugins/actions/server/routes/execute.test.ts
index 6e8ebbf6f91cd..38fca656bef5a 100644
--- a/x-pack/plugins/actions/server/routes/execute.test.ts
+++ b/x-pack/plugins/actions/server/routes/execute.test.ts
@@ -53,13 +53,6 @@ describe('executeActionRoute', () => {
const [config, handler] = router.post.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/actions/action/{id}/_execute"`);
- expect(config.options).toMatchInlineSnapshot(`
- Object {
- "tags": Array [
- "access:actions-read",
- ],
- }
- `);
expect(await handler(context, req, res)).toEqual({ body: executeResult });
diff --git a/x-pack/plugins/actions/server/routes/execute.ts b/x-pack/plugins/actions/server/routes/execute.ts
index 28e6a54f5e92d..0d49d9a3a256e 100644
--- a/x-pack/plugins/actions/server/routes/execute.ts
+++ b/x-pack/plugins/actions/server/routes/execute.ts
@@ -32,9 +32,6 @@ export const executeActionRoute = (router: IRouter, licenseState: ILicenseState)
body: bodySchema,
params: paramSchema,
},
- options: {
- tags: ['access:actions-read'],
- },
},
router.handleLegacyErrors(async function (
context: RequestHandlerContext,
diff --git a/x-pack/plugins/actions/server/routes/get.test.ts b/x-pack/plugins/actions/server/routes/get.test.ts
index ee2586851366c..434bd6a9bc224 100644
--- a/x-pack/plugins/actions/server/routes/get.test.ts
+++ b/x-pack/plugins/actions/server/routes/get.test.ts
@@ -29,13 +29,6 @@ describe('getActionRoute', () => {
const [config, handler] = router.get.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/actions/action/{id}"`);
- expect(config.options).toMatchInlineSnapshot(`
- Object {
- "tags": Array [
- "access:actions-read",
- ],
- }
- `);
const getResult = {
id: '1',
diff --git a/x-pack/plugins/actions/server/routes/get.ts b/x-pack/plugins/actions/server/routes/get.ts
index 224de241c7374..33577fad87c04 100644
--- a/x-pack/plugins/actions/server/routes/get.ts
+++ b/x-pack/plugins/actions/server/routes/get.ts
@@ -26,9 +26,6 @@ export const getActionRoute = (router: IRouter, licenseState: ILicenseState) =>
validate: {
params: paramSchema,
},
- options: {
- tags: ['access:actions-read'],
- },
},
router.handleLegacyErrors(async function (
context: RequestHandlerContext,
diff --git a/x-pack/plugins/actions/server/routes/get_all.test.ts b/x-pack/plugins/actions/server/routes/get_all.test.ts
index 6550921278aa5..35db22d2da486 100644
--- a/x-pack/plugins/actions/server/routes/get_all.test.ts
+++ b/x-pack/plugins/actions/server/routes/get_all.test.ts
@@ -29,13 +29,6 @@ describe('getAllActionRoute', () => {
const [config, handler] = router.get.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/actions"`);
- expect(config.options).toMatchInlineSnapshot(`
- Object {
- "tags": Array [
- "access:actions-read",
- ],
- }
- `);
const actionsClient = actionsClientMock.create();
actionsClient.getAll.mockResolvedValueOnce([]);
@@ -64,13 +57,6 @@ describe('getAllActionRoute', () => {
const [config, handler] = router.get.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/actions"`);
- expect(config.options).toMatchInlineSnapshot(`
- Object {
- "tags": Array [
- "access:actions-read",
- ],
- }
- `);
const actionsClient = actionsClientMock.create();
actionsClient.getAll.mockResolvedValueOnce([]);
@@ -95,13 +81,6 @@ describe('getAllActionRoute', () => {
const [config, handler] = router.get.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/actions"`);
- expect(config.options).toMatchInlineSnapshot(`
- Object {
- "tags": Array [
- "access:actions-read",
- ],
- }
- `);
const actionsClient = actionsClientMock.create();
actionsClient.getAll.mockResolvedValueOnce([]);
diff --git a/x-pack/plugins/actions/server/routes/get_all.ts b/x-pack/plugins/actions/server/routes/get_all.ts
index 03a4a97855b6b..1b57f31d14a0d 100644
--- a/x-pack/plugins/actions/server/routes/get_all.ts
+++ b/x-pack/plugins/actions/server/routes/get_all.ts
@@ -19,9 +19,6 @@ export const getAllActionRoute = (router: IRouter, licenseState: ILicenseState)
{
path: `${BASE_ACTION_API_PATH}`,
validate: {},
- options: {
- tags: ['access:actions-read'],
- },
},
router.handleLegacyErrors(async function (
context: RequestHandlerContext,
diff --git a/x-pack/plugins/actions/server/routes/list_action_types.test.ts b/x-pack/plugins/actions/server/routes/list_action_types.test.ts
index f231efe1a07f3..982b64c339a5f 100644
--- a/x-pack/plugins/actions/server/routes/list_action_types.test.ts
+++ b/x-pack/plugins/actions/server/routes/list_action_types.test.ts
@@ -10,6 +10,7 @@ import { licenseStateMock } from '../lib/license_state.mock';
import { verifyApiAccess } from '../lib';
import { mockHandlerArguments } from './_mock_handler_arguments';
import { LicenseType } from '../../../../plugins/licensing/server';
+import { actionsClientMock } from '../mocks';
jest.mock('../lib/verify_api_access.ts', () => ({
verifyApiAccess: jest.fn(),
@@ -29,13 +30,6 @@ describe('listActionTypesRoute', () => {
const [config, handler] = router.get.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/actions/list_action_types"`);
- expect(config.options).toMatchInlineSnapshot(`
- Object {
- "tags": Array [
- "access:actions-read",
- ],
- }
- `);
const listTypes = [
{
@@ -48,7 +42,9 @@ describe('listActionTypesRoute', () => {
},
];
- const [context, req, res] = mockHandlerArguments({ listTypes }, {}, ['ok']);
+ const actionsClient = actionsClientMock.create();
+ actionsClient.listTypes.mockResolvedValueOnce(listTypes);
+ const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']);
expect(await handler(context, req, res)).toMatchInlineSnapshot(`
Object {
@@ -65,8 +61,6 @@ describe('listActionTypesRoute', () => {
}
`);
- expect(context.actions!.listTypes).toHaveBeenCalledTimes(1);
-
expect(res.ok).toHaveBeenCalledWith({
body: listTypes,
});
@@ -81,13 +75,6 @@ describe('listActionTypesRoute', () => {
const [config, handler] = router.get.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/actions/list_action_types"`);
- expect(config.options).toMatchInlineSnapshot(`
- Object {
- "tags": Array [
- "access:actions-read",
- ],
- }
- `);
const listTypes = [
{
@@ -100,8 +87,11 @@ describe('listActionTypesRoute', () => {
},
];
+ const actionsClient = actionsClientMock.create();
+ actionsClient.listTypes.mockResolvedValueOnce(listTypes);
+
const [context, req, res] = mockHandlerArguments(
- { listTypes },
+ { actionsClient },
{
params: { id: '1' },
},
@@ -126,13 +116,6 @@ describe('listActionTypesRoute', () => {
const [config, handler] = router.get.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/actions/list_action_types"`);
- expect(config.options).toMatchInlineSnapshot(`
- Object {
- "tags": Array [
- "access:actions-read",
- ],
- }
- `);
const listTypes = [
{
@@ -145,8 +128,11 @@ describe('listActionTypesRoute', () => {
},
];
+ const actionsClient = actionsClientMock.create();
+ actionsClient.listTypes.mockResolvedValueOnce(listTypes);
+
const [context, req, res] = mockHandlerArguments(
- { listTypes },
+ { actionsClient },
{
params: { id: '1' },
},
diff --git a/x-pack/plugins/actions/server/routes/list_action_types.ts b/x-pack/plugins/actions/server/routes/list_action_types.ts
index bfb5fabe127f3..c960a6bac6de0 100644
--- a/x-pack/plugins/actions/server/routes/list_action_types.ts
+++ b/x-pack/plugins/actions/server/routes/list_action_types.ts
@@ -19,9 +19,6 @@ export const listActionTypesRoute = (router: IRouter, licenseState: ILicenseStat
{
path: `${BASE_ACTION_API_PATH}/list_action_types`,
validate: {},
- options: {
- tags: ['access:actions-read'],
- },
},
router.handleLegacyErrors(async function (
context: RequestHandlerContext,
@@ -32,8 +29,9 @@ export const listActionTypesRoute = (router: IRouter, licenseState: ILicenseStat
if (!context.actions) {
return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' });
}
+ const actionsClient = context.actions.getActionsClient();
return res.ok({
- body: context.actions.listTypes(),
+ body: await actionsClient.listTypes(),
});
})
);
diff --git a/x-pack/plugins/actions/server/routes/update.test.ts b/x-pack/plugins/actions/server/routes/update.test.ts
index 323a52f2fc6e2..6d5b78650ba2a 100644
--- a/x-pack/plugins/actions/server/routes/update.test.ts
+++ b/x-pack/plugins/actions/server/routes/update.test.ts
@@ -28,13 +28,6 @@ describe('updateActionRoute', () => {
const [config, handler] = router.put.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/actions/action/{id}"`);
- expect(config.options).toMatchInlineSnapshot(`
- Object {
- "tags": Array [
- "access:actions-all",
- ],
- }
- `);
const updateResult = {
id: '1',
diff --git a/x-pack/plugins/actions/server/routes/update.ts b/x-pack/plugins/actions/server/routes/update.ts
index 1e107a4d6edb4..328ce74ef0b08 100644
--- a/x-pack/plugins/actions/server/routes/update.ts
+++ b/x-pack/plugins/actions/server/routes/update.ts
@@ -33,9 +33,6 @@ export const updateActionRoute = (router: IRouter, licenseState: ILicenseState)
body: bodySchema,
params: paramSchema,
},
- options: {
- tags: ['access:actions-all'],
- },
},
router.handleLegacyErrors(async function (
context: RequestHandlerContext,
diff --git a/x-pack/plugins/actions/server/saved_objects/index.ts b/x-pack/plugins/actions/server/saved_objects/index.ts
index d68c96a5e9270..54f186acc1ba5 100644
--- a/x-pack/plugins/actions/server/saved_objects/index.ts
+++ b/x-pack/plugins/actions/server/saved_objects/index.ts
@@ -8,12 +8,15 @@ import { SavedObjectsServiceSetup } from 'kibana/server';
import mappings from './mappings.json';
import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server';
+export const ACTION_SAVED_OBJECT_TYPE = 'action';
+export const ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE = 'action_task_params';
+
export function setupSavedObjects(
savedObjects: SavedObjectsServiceSetup,
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup
) {
savedObjects.registerType({
- name: 'action',
+ name: ACTION_SAVED_OBJECT_TYPE,
hidden: true,
namespaceType: 'single',
mappings: mappings.action,
@@ -24,19 +27,19 @@ export function setupSavedObjects(
// - `config` will be included in AAD
// - everything else excluded from AAD
encryptedSavedObjects.registerType({
- type: 'action',
+ type: ACTION_SAVED_OBJECT_TYPE,
attributesToEncrypt: new Set(['secrets']),
attributesToExcludeFromAAD: new Set(['name']),
});
savedObjects.registerType({
- name: 'action_task_params',
+ name: ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
hidden: true,
namespaceType: 'single',
mappings: mappings.action_task_params,
});
encryptedSavedObjects.registerType({
- type: 'action_task_params',
+ type: ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
attributesToEncrypt: new Set(['apiKey']),
});
}
diff --git a/x-pack/plugins/alerting_builtins/common/index.ts b/x-pack/plugins/alerting_builtins/common/index.ts
new file mode 100644
index 0000000000000..4f2c166669355
--- /dev/null
+++ b/x-pack/plugins/alerting_builtins/common/index.ts
@@ -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 const BUILT_IN_ALERTS_FEATURE_ID = 'builtInAlerts';
diff --git a/x-pack/plugins/alerting_builtins/kibana.json b/x-pack/plugins/alerting_builtins/kibana.json
index cc613d5247ef4..dd70e53604f16 100644
--- a/x-pack/plugins/alerting_builtins/kibana.json
+++ b/x-pack/plugins/alerting_builtins/kibana.json
@@ -3,7 +3,7 @@
"server": true,
"version": "8.0.0",
"kibanaVersion": "kibana",
- "requiredPlugins": ["alerts"],
+ "requiredPlugins": ["alerts", "features"],
"configPath": ["xpack", "alerting_builtins"],
"ui": false
}
diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type.ts
index 1a5da8a422b9e..153334cb64047 100644
--- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type.ts
+++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type.ts
@@ -9,11 +9,11 @@ import { AlertType, AlertExecutorOptions } from '../../types';
import { Params, ParamsSchema } from './alert_type_params';
import { BaseActionContext, addMessages } from './action_context';
import { TimeSeriesQuery } from './lib/time_series_query';
+import { Service } from '../../types';
+import { BUILT_IN_ALERTS_FEATURE_ID } from '../../../common';
export const ID = '.index-threshold';
-import { Service } from '../../types';
-
const ActionGroupId = 'threshold met';
const ComparatorFns = getComparatorFns();
export const ComparatorFnNames = new Set(ComparatorFns.keys());
@@ -85,7 +85,7 @@ export function getAlertType(service: Service): AlertType {
],
},
executor,
- producer: 'alerting',
+ producer: BUILT_IN_ALERTS_FEATURE_ID,
};
async function executor(options: AlertExecutorOptions) {
diff --git a/x-pack/plugins/alerting_builtins/server/feature.ts b/x-pack/plugins/alerting_builtins/server/feature.ts
new file mode 100644
index 0000000000000..669d2ba627059
--- /dev/null
+++ b/x-pack/plugins/alerting_builtins/server/feature.ts
@@ -0,0 +1,49 @@
+/*
+ * 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';
+import { ID as IndexThreshold } from './alert_types/index_threshold/alert_type';
+import { BUILT_IN_ALERTS_FEATURE_ID } from '../common';
+
+export const BUILT_IN_ALERTS_FEATURE = {
+ id: BUILT_IN_ALERTS_FEATURE_ID,
+ name: i18n.translate('xpack.alertingBuiltins.featureRegistry.actionsFeatureName', {
+ defaultMessage: 'Built-In Alerts',
+ }),
+ icon: 'bell',
+ app: [],
+ alerting: [IndexThreshold],
+ privileges: {
+ all: {
+ app: [],
+ catalogue: [],
+ alerting: {
+ all: [IndexThreshold],
+ read: [],
+ },
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ api: [],
+ ui: ['alerting:show'],
+ },
+ read: {
+ app: [],
+ catalogue: [],
+ alerting: {
+ all: [],
+ read: [IndexThreshold],
+ },
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ api: [],
+ ui: ['alerting:show'],
+ },
+ },
+};
diff --git a/x-pack/plugins/alerting_builtins/server/index.ts b/x-pack/plugins/alerting_builtins/server/index.ts
index 00613213d5aed..108393c0d1469 100644
--- a/x-pack/plugins/alerting_builtins/server/index.ts
+++ b/x-pack/plugins/alerting_builtins/server/index.ts
@@ -7,6 +7,7 @@
import { PluginInitializerContext } from 'src/core/server';
import { AlertingBuiltinsPlugin } from './plugin';
import { configSchema } from './config';
+export { ID as INDEX_THRESHOLD_ID } from './alert_types/index_threshold/alert_type';
export const plugin = (ctx: PluginInitializerContext) => new AlertingBuiltinsPlugin(ctx);
diff --git a/x-pack/plugins/alerting_builtins/server/plugin.test.ts b/x-pack/plugins/alerting_builtins/server/plugin.test.ts
index 71a904dcbab3d..15ad066523502 100644
--- a/x-pack/plugins/alerting_builtins/server/plugin.test.ts
+++ b/x-pack/plugins/alerting_builtins/server/plugin.test.ts
@@ -7,6 +7,8 @@
import { AlertingBuiltinsPlugin } from './plugin';
import { coreMock } from '../../../../src/core/server/mocks';
import { alertsMock } from '../../alerts/server/mocks';
+import { featuresPluginMock } from '../../features/server/mocks';
+import { BUILT_IN_ALERTS_FEATURE } from './feature';
describe('AlertingBuiltins Plugin', () => {
describe('setup()', () => {
@@ -22,7 +24,8 @@ describe('AlertingBuiltins Plugin', () => {
it('should register built-in alert types', async () => {
const alertingSetup = alertsMock.createSetup();
- await plugin.setup(coreSetup, { alerts: alertingSetup });
+ const featuresSetup = featuresPluginMock.createSetup();
+ await plugin.setup(coreSetup, { alerts: alertingSetup, features: featuresSetup });
expect(alertingSetup.registerType).toHaveBeenCalledTimes(1);
@@ -40,11 +43,16 @@ describe('AlertingBuiltins Plugin', () => {
"name": "Index threshold",
}
`);
+ expect(featuresSetup.registerFeature).toHaveBeenCalledWith(BUILT_IN_ALERTS_FEATURE);
});
it('should return a service in the expected shape', async () => {
const alertingSetup = alertsMock.createSetup();
- const service = await plugin.setup(coreSetup, { alerts: alertingSetup });
+ const featuresSetup = featuresPluginMock.createSetup();
+ const service = await plugin.setup(coreSetup, {
+ alerts: alertingSetup,
+ features: featuresSetup,
+ });
expect(typeof service.indexThreshold.timeSeriesQuery).toBe('function');
});
diff --git a/x-pack/plugins/alerting_builtins/server/plugin.ts b/x-pack/plugins/alerting_builtins/server/plugin.ts
index 12d1b080c7c63..41871c01bfb50 100644
--- a/x-pack/plugins/alerting_builtins/server/plugin.ts
+++ b/x-pack/plugins/alerting_builtins/server/plugin.ts
@@ -9,6 +9,7 @@ import { Plugin, Logger, CoreSetup, CoreStart, PluginInitializerContext } from '
import { Service, IService, AlertingBuiltinsDeps } from './types';
import { getService as getServiceIndexThreshold } from './alert_types/index_threshold';
import { registerBuiltInAlertTypes } from './alert_types';
+import { BUILT_IN_ALERTS_FEATURE } from './feature';
export class AlertingBuiltinsPlugin implements Plugin {
private readonly logger: Logger;
@@ -22,7 +23,12 @@ export class AlertingBuiltinsPlugin implements Plugin {
};
}
- public async setup(core: CoreSetup, { alerts }: AlertingBuiltinsDeps): Promise {
+ public async setup(
+ core: CoreSetup,
+ { alerts, features }: AlertingBuiltinsDeps
+ ): Promise {
+ features.registerFeature(BUILT_IN_ALERTS_FEATURE);
+
registerBuiltInAlertTypes({
service: this.service,
router: core.http.createRouter(),
diff --git a/x-pack/plugins/alerting_builtins/server/types.ts b/x-pack/plugins/alerting_builtins/server/types.ts
index 1fb5314ca4fd9..f3abc26be8dab 100644
--- a/x-pack/plugins/alerting_builtins/server/types.ts
+++ b/x-pack/plugins/alerting_builtins/server/types.ts
@@ -15,10 +15,12 @@ export {
AlertType,
AlertExecutorOptions,
} from '../../alerts/server';
+import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
// this plugin's dependendencies
export interface AlertingBuiltinsDeps {
alerts: AlertingSetup;
+ features: FeaturesPluginSetup;
}
// external service exposed through plugin setup/start
diff --git a/x-pack/plugins/alerts/README.md b/x-pack/plugins/alerts/README.md
index 0464ec78a4e9d..10568abbe3c72 100644
--- a/x-pack/plugins/alerts/README.md
+++ b/x-pack/plugins/alerts/README.md
@@ -18,6 +18,7 @@ Table of Contents
- [Methods](#methods)
- [Executor](#executor)
- [Example](#example)
+ - [Role Based Access-Control](#role-based-access-control)
- [Alert Navigation](#alert-navigation)
- [RESTful API](#restful-api)
- [`POST /api/alerts/alert`: Create alert](#post-apialert-create-alert)
@@ -58,7 +59,8 @@ A Kibana alert detects a condition and executes one or more actions when that co
## Usage
1. Develop and register an alert type (see alert types -> example).
-2. Create an alert using the RESTful API (see alerts -> create).
+2. Configure feature level privileges using RBAC
+3. Create an alert using the RESTful API (see alerts -> create).
## Limitations
@@ -293,6 +295,111 @@ server.newPlatform.setup.plugins.alerts.registerType({
});
```
+## Role Based Access-Control
+Once you have registered your AlertType, you need to grant your users privileges to use it.
+When registering a feature in Kibana you can specify multiple types of privileges which are granted to users when they're assigned certain roles.
+
+Assuming your feature introduces its own AlertTypes, you'll want to control which roles have all/read privileges for these AlertTypes when they're inside the feature.
+In addition, when users are inside your feature you might want to grant them access to AlertTypes from other features, such as built-in AlertTypes or AlertTypes provided by other features.
+
+You can control all of these abilities by assigning privileges to the Alerting Framework from within your own feature, for example:
+
+```typescript
+features.registerFeature({
+ id: 'my-application-id',
+ name: 'My Application',
+ app: [],
+ privileges: {
+ all: {
+ alerting: {
+ all: [
+ // grant `all` over our own types
+ 'my-application-id.my-alert-type',
+ 'my-application-id.my-restricted-alert-type',
+ // grant `all` over the built-in IndexThreshold
+ '.index-threshold',
+ // grant `all` over Uptime's TLS AlertType
+ 'xpack.uptime.alerts.actionGroups.tls'
+ ],
+ },
+ },
+ read: {
+ alerting: {
+ read: [
+ // grant `read` over our own type
+ 'my-application-id.my-alert-type',
+ // grant `read` over the built-in IndexThreshold
+ '.index-threshold',
+ // grant `read` over Uptime's TLS AlertType
+ 'xpack.uptime.alerts.actionGroups.tls'
+ ],
+ },
+ },
+ },
+});
+```
+
+In this example we can see the following:
+- Our feature grants any user who's assigned the `all` role in our feature the `all` role in the Alerting framework over every alert of the `my-application-id.my-alert-type` type which is created _inside_ the feature. What that means is that this privilege will allow the user to execute any of the `all` operations (listed below) on these alerts as long as their `consumer` is `my-application-id`. Below that you'll notice we've done the same with the `read` role, which is grants the Alerting Framework's `read` role privileges over these very same alerts.
+- In addition, our feature grants the same privileges over any alert of type `my-application-id.my-restricted-alert-type`, which is another hypothetical alertType registered by this feature. It's worth noting though that this type has been omitted from the `read` role. What this means is that only users with the `all` role will be able to interact with alerts of this type.
+- Next, lets look at the `.index-threshold` and `xpack.uptime.alerts.actionGroups.tls` types. These have been specified in both `read` and `all`, which means that all the users in the feature will gain privileges over alerts of these types (as long as their `consumer` is `my-application-id`). The difference between these two and the previous two is that they are _produced_ by other features! `.index-threshold` is a built-in type, provided by the _Built-In Alerts_ feature, and `xpack.uptime.alerts.actionGroups.tls` is an AlertType provided by the _Uptime_ feature. Specifying these type here tells the Alerting Framework that as far as the `my-application-id` feature is concerned, the user is privileged to use them (with `all` and `read` applied), but that isn't enough. Using another feature's AlertType is only possible if both the producer of the AlertType, and the consumer of the AlertType, explicitly grant privileges to do so. In this case, the _Built-In Alerts_ & _Uptime_ features would have to explicitly add these privileges to a role and this role would have to be granted to this user.
+
+It's important to note that any role can be granted a mix of `all` and `read` privileges accross multiple type, for example:
+
+```typescript
+features.registerFeature({
+ id: 'my-application-id',
+ name: 'My Application',
+ app: [],
+ privileges: {
+ all: {
+ alerting: {
+ all: [
+ 'my-application-id.my-alert-type',
+ 'my-application-id.my-restricted-alert-type'
+ ],
+ },
+ },
+ read: {
+ alerting: {
+ all: [
+ 'my-application-id.my-alert-type'
+ ]
+ read: [
+ 'my-application-id.my-restricted-alert-type'
+ ],
+ },
+ },
+ },
+});
+```
+
+In the above example, you note that instead of denying users with the `read` role any access to the `my-application-id.my-restricted-alert-type` type, we've decided that these users _should_ be granted `read` privileges over the _resitricted_ AlertType.
+As part of that same change, we also decided that not only should they be allowed to `read` the _restricted_ AlertType, but actually, despite having `read` privileges to the feature as a whole, we do actually want to allow them to create our basic 'my-application-id.my-alert-type' AlertType, as we consider it an extension of _reading_ data in our feature, rather than _writing_ it.
+
+### `read` privileges vs. `all` privileges
+When a user is granted the `read` role in the Alerting Framework, they will be able to execute the following api calls:
+- `get`
+- `getAlertState`
+- `find`
+
+When a user is granted the `all` role in the Alerting Framework, they will be able to execute all of the `read` privileged api calls, but in addition they'll be granted the following calls:
+- `create`
+- `delete`
+- `update`
+- `enable`
+- `disable`
+- `updateApiKey`
+- `muteAll`
+- `unmuteAll`
+- `muteInstance`
+- `unmuteInstance`
+
+Finally, all users, whether they're granted any role or not, are privileged to call the following:
+- `listAlertTypes`, but the output is limited to displaying the AlertTypes the user is perivileged to `get`
+
+Attempting to execute any operation the user isn't privileged to execute will result in an Authorization error thrown by the AlertsClient.
+
## Alert Navigation
When registering an Alert Type, you'll likely want to provide a way of viewing alerts of that type within your own plugin, or perhaps you want to provide a view for all alerts created from within your solution within your own UI.
diff --git a/x-pack/plugins/alerts/common/index.ts b/x-pack/plugins/alerts/common/index.ts
index 88a8da5a3e575..b839c07a9db89 100644
--- a/x-pack/plugins/alerts/common/index.ts
+++ b/x-pack/plugins/alerts/common/index.ts
@@ -21,3 +21,4 @@ export interface AlertingFrameworkHealth {
}
export const BASE_ALERT_API_PATH = '/api/alerts';
+export const ALERTS_FEATURE_ID = 'alerts';
diff --git a/x-pack/plugins/alerts/kibana.json b/x-pack/plugins/alerts/kibana.json
index eef61ff4b3d53..c0ab242831428 100644
--- a/x-pack/plugins/alerts/kibana.json
+++ b/x-pack/plugins/alerts/kibana.json
@@ -5,7 +5,7 @@
"version": "8.0.0",
"kibanaVersion": "kibana",
"configPath": ["xpack", "alerts"],
- "requiredPlugins": ["licensing", "taskManager", "encryptedSavedObjects", "actions", "eventLog"],
+ "requiredPlugins": ["licensing", "taskManager", "encryptedSavedObjects", "actions", "eventLog", "features"],
"optionalPlugins": ["usageCollection", "spaces", "security"],
"extraPublicDirs": ["common", "common/parse_duration"]
}
diff --git a/x-pack/plugins/alerts/public/alert_api.test.ts b/x-pack/plugins/alerts/public/alert_api.test.ts
index 45b9f5ba8fe2e..3ee67b79b7bda 100644
--- a/x-pack/plugins/alerts/public/alert_api.test.ts
+++ b/x-pack/plugins/alerts/public/alert_api.test.ts
@@ -22,7 +22,7 @@ describe('loadAlertTypes', () => {
actionVariables: ['var1'],
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
- producer: 'alerting',
+ producer: 'alerts',
},
];
http.get.mockResolvedValueOnce(resolvedValue);
@@ -45,7 +45,7 @@ describe('loadAlertType', () => {
actionVariables: ['var1'],
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
- producer: 'alerting',
+ producer: 'alerts',
};
http.get.mockResolvedValueOnce([alertType]);
@@ -65,7 +65,7 @@ describe('loadAlertType', () => {
actionVariables: [],
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
- producer: 'alerting',
+ producer: 'alerts',
};
http.get.mockResolvedValueOnce([alertType]);
@@ -80,7 +80,7 @@ describe('loadAlertType', () => {
actionVariables: [],
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
- producer: 'alerting',
+ producer: 'alerts',
},
]);
diff --git a/x-pack/plugins/alerts/public/alert_navigation_registry/alert_navigation_registry.test.ts b/x-pack/plugins/alerts/public/alert_navigation_registry/alert_navigation_registry.test.ts
index ff8a3a1c311c1..72c955923a0cc 100644
--- a/x-pack/plugins/alerts/public/alert_navigation_registry/alert_navigation_registry.test.ts
+++ b/x-pack/plugins/alerts/public/alert_navigation_registry/alert_navigation_registry.test.ts
@@ -16,7 +16,7 @@ const mockAlertType = (id: string): AlertType => ({
actionGroups: [],
actionVariables: [],
defaultActionGroupId: 'default',
- producer: 'alerting',
+ producer: 'alerts',
});
describe('AlertNavigationRegistry', () => {
diff --git a/x-pack/plugins/alerts/server/alert_type_registry.test.ts b/x-pack/plugins/alerts/server/alert_type_registry.test.ts
index 6d7cf621ab0ca..c740390713715 100644
--- a/x-pack/plugins/alerts/server/alert_type_registry.test.ts
+++ b/x-pack/plugins/alerts/server/alert_type_registry.test.ts
@@ -36,13 +36,67 @@ describe('has()', () => {
],
defaultActionGroupId: 'default',
executor: jest.fn(),
- producer: 'alerting',
+ producer: 'alerts',
});
expect(registry.has('foo')).toEqual(true);
});
});
describe('register()', () => {
+ test('throws if AlertType Id contains invalid characters', () => {
+ const alertType = {
+ id: 'test',
+ name: 'Test',
+ actionGroups: [
+ {
+ id: 'default',
+ name: 'Default',
+ },
+ ],
+ defaultActionGroupId: 'default',
+ executor: jest.fn(),
+ producer: 'alerts',
+ };
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const registry = new AlertTypeRegistry(alertTypeRegistryParams);
+
+ const invalidCharacters = [' ', ':', '*', '*', '/'];
+ for (const char of invalidCharacters) {
+ expect(() => registry.register({ ...alertType, id: `${alertType.id}${char}` })).toThrowError(
+ new Error(`expected AlertType Id not to include invalid character: ${char}`)
+ );
+ }
+
+ const [first, second] = invalidCharacters;
+ expect(() =>
+ registry.register({ ...alertType, id: `${first}${alertType.id}${second}` })
+ ).toThrowError(
+ new Error(`expected AlertType Id not to include invalid characters: ${first}, ${second}`)
+ );
+ });
+
+ test('throws if AlertType Id isnt a string', () => {
+ const alertType = {
+ id: (123 as unknown) as string,
+ name: 'Test',
+ actionGroups: [
+ {
+ id: 'default',
+ name: 'Default',
+ },
+ ],
+ defaultActionGroupId: 'default',
+ executor: jest.fn(),
+ producer: 'alerts',
+ };
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const registry = new AlertTypeRegistry(alertTypeRegistryParams);
+
+ expect(() => registry.register(alertType)).toThrowError(
+ new Error(`expected value of type [string] but got [number]`)
+ );
+ });
+
test('registers the executor with the task manager', () => {
const alertType = {
id: 'test',
@@ -55,7 +109,7 @@ describe('register()', () => {
],
defaultActionGroupId: 'default',
executor: jest.fn(),
- producer: 'alerting',
+ producer: 'alerts',
};
// eslint-disable-next-line @typescript-eslint/no-var-requires
const registry = new AlertTypeRegistry(alertTypeRegistryParams);
@@ -86,7 +140,7 @@ describe('register()', () => {
],
defaultActionGroupId: 'default',
executor: jest.fn(),
- producer: 'alerting',
+ producer: 'alerts',
};
const registry = new AlertTypeRegistry(alertTypeRegistryParams);
registry.register(alertType);
@@ -107,7 +161,7 @@ describe('register()', () => {
],
defaultActionGroupId: 'default',
executor: jest.fn(),
- producer: 'alerting',
+ producer: 'alerts',
});
expect(() =>
registry.register({
@@ -121,7 +175,7 @@ describe('register()', () => {
],
defaultActionGroupId: 'default',
executor: jest.fn(),
- producer: 'alerting',
+ producer: 'alerts',
})
).toThrowErrorMatchingInlineSnapshot(`"Alert type \\"test\\" is already registered."`);
});
@@ -141,7 +195,7 @@ describe('get()', () => {
],
defaultActionGroupId: 'default',
executor: jest.fn(),
- producer: 'alerting',
+ producer: 'alerts',
});
const alertType = registry.get('test');
expect(alertType).toMatchInlineSnapshot(`
@@ -160,7 +214,7 @@ describe('get()', () => {
"executor": [MockFunction],
"id": "test",
"name": "Test",
- "producer": "alerting",
+ "producer": "alerts",
}
`);
});
@@ -177,7 +231,7 @@ describe('list()', () => {
test('should return empty when nothing is registered', () => {
const registry = new AlertTypeRegistry(alertTypeRegistryParams);
const result = registry.list();
- expect(result).toMatchInlineSnapshot(`Array []`);
+ expect(result).toMatchInlineSnapshot(`Set {}`);
});
test('should return registered types', () => {
@@ -193,11 +247,11 @@ describe('list()', () => {
],
defaultActionGroupId: 'testActionGroup',
executor: jest.fn(),
- producer: 'alerting',
+ producer: 'alerts',
});
const result = registry.list();
expect(result).toMatchInlineSnapshot(`
- Array [
+ Set {
Object {
"actionGroups": Array [
Object {
@@ -212,9 +266,9 @@ describe('list()', () => {
"defaultActionGroupId": "testActionGroup",
"id": "test",
"name": "Test",
- "producer": "alerting",
+ "producer": "alerts",
},
- ]
+ }
`);
});
@@ -260,7 +314,7 @@ function alertTypeWithVariables(id: string, context: string, state: string): Ale
actionGroups: [],
defaultActionGroupId: id,
async executor() {},
- producer: 'alerting',
+ producer: 'alerts',
};
if (!context && !state) {
diff --git a/x-pack/plugins/alerts/server/alert_type_registry.ts b/x-pack/plugins/alerts/server/alert_type_registry.ts
index 8f36afe062aa5..c466d0e96382c 100644
--- a/x-pack/plugins/alerts/server/alert_type_registry.ts
+++ b/x-pack/plugins/alerts/server/alert_type_registry.ts
@@ -6,6 +6,8 @@
import Boom from 'boom';
import { i18n } from '@kbn/i18n';
+import { schema } from '@kbn/config-schema';
+import typeDetect from 'type-detect';
import { RunContext, TaskManagerSetupContract } from '../../task_manager/server';
import { TaskRunnerFactory } from './task_runner';
import { AlertType } from './types';
@@ -15,6 +17,34 @@ interface ConstructorOptions {
taskRunnerFactory: TaskRunnerFactory;
}
+export interface RegistryAlertType
+ extends Pick<
+ AlertType,
+ 'name' | 'actionGroups' | 'defaultActionGroupId' | 'actionVariables' | 'producer'
+ > {
+ id: string;
+}
+
+/**
+ * AlertType IDs are used as part of the authorization strings used to
+ * grant users privileged operations. There is a limited range of characters
+ * we can use in these auth strings, so we apply these same limitations to
+ * the AlertType Ids.
+ * If you wish to change this, please confer with the Kibana security team.
+ */
+const alertIdSchema = schema.string({
+ validate(value: string): string | void {
+ if (typeof value !== 'string') {
+ return `expected AlertType Id of type [string] but got [${typeDetect(value)}]`;
+ } else if (!value.match(/^[a-zA-Z0-9_\-\.]*$/)) {
+ const invalid = value.match(/[^a-zA-Z0-9_\-\.]+/g)!;
+ return `expected AlertType Id not to include invalid character${
+ invalid.length > 1 ? `s` : ``
+ }: ${invalid?.join(`, `)}`;
+ }
+ },
+});
+
export class AlertTypeRegistry {
private readonly taskManager: TaskManagerSetupContract;
private readonly alertTypes: Map = new Map();
@@ -41,7 +71,7 @@ export class AlertTypeRegistry {
);
}
alertType.actionVariables = normalizedActionVariables(alertType.actionVariables);
- this.alertTypes.set(alertType.id, { ...alertType });
+ this.alertTypes.set(alertIdSchema.validate(alertType.id), { ...alertType });
this.taskManager.registerTaskDefinitions({
[`alerting:${alertType.id}`]: {
title: alertType.name,
@@ -66,15 +96,22 @@ export class AlertTypeRegistry {
return this.alertTypes.get(id)!;
}
- public list() {
- return Array.from(this.alertTypes).map(([alertTypeId, alertType]) => ({
- id: alertTypeId,
- name: alertType.name,
- actionGroups: alertType.actionGroups,
- defaultActionGroupId: alertType.defaultActionGroupId,
- actionVariables: alertType.actionVariables,
- producer: alertType.producer,
- }));
+ public list(): Set {
+ return new Set(
+ Array.from(this.alertTypes).map(
+ ([id, { name, actionGroups, defaultActionGroupId, actionVariables, producer }]: [
+ string,
+ AlertType
+ ]) => ({
+ id,
+ name,
+ actionGroups,
+ defaultActionGroupId,
+ actionVariables,
+ producer,
+ })
+ )
+ );
}
}
diff --git a/x-pack/plugins/alerts/server/alerts_client.mock.ts b/x-pack/plugins/alerts/server/alerts_client.mock.ts
index 1848b3432ae5a..be70e441b6fc5 100644
--- a/x-pack/plugins/alerts/server/alerts_client.mock.ts
+++ b/x-pack/plugins/alerts/server/alerts_client.mock.ts
@@ -24,6 +24,7 @@ const createAlertsClientMock = () => {
unmuteAll: jest.fn(),
muteInstance: jest.fn(),
unmuteInstance: jest.fn(),
+ listAlertTypes: jest.fn(),
};
return mocked;
};
diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts
index d69d04f71ce9e..c25e040ad09ce 100644
--- a/x-pack/plugins/alerts/server/alerts_client.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client.test.ts
@@ -5,25 +5,32 @@
*/
import uuid from 'uuid';
import { schema } from '@kbn/config-schema';
-import { AlertsClient, CreateOptions } from './alerts_client';
+import { AlertsClient, CreateOptions, ConstructorOptions } from './alerts_client';
import { savedObjectsClientMock, loggingSystemMock } from '../../../../src/core/server/mocks';
import { taskManagerMock } from '../../task_manager/server/task_manager.mock';
import { alertTypeRegistryMock } from './alert_type_registry.mock';
+import { alertsAuthorizationMock } from './authorization/alerts_authorization.mock';
import { TaskStatus } from '../../task_manager/server';
import { IntervalSchedule } from './types';
import { resolvable } from './test_utils';
import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks';
-import { actionsClientMock } from '../../actions/server/mocks';
+import { actionsClientMock, actionsAuthorizationMock } from '../../actions/server/mocks';
+import { AlertsAuthorization } from './authorization/alerts_authorization';
+import { ActionsAuthorization } from '../../actions/server';
const taskManager = taskManagerMock.start();
const alertTypeRegistry = alertTypeRegistryMock.create();
-const savedObjectsClient = savedObjectsClientMock.create();
+const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
+const authorization = alertsAuthorizationMock.create();
+const actionsAuthorization = actionsAuthorizationMock.create();
-const alertsClientParams = {
+const alertsClientParams: jest.Mocked = {
taskManager,
alertTypeRegistry,
- savedObjectsClient,
+ unsecuredSavedObjectsClient,
+ authorization: (authorization as unknown) as AlertsAuthorization,
+ actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
getUserName: jest.fn(),
@@ -39,7 +46,11 @@ beforeEach(() => {
alertsClientParams.createAPIKey.mockResolvedValue({ apiKeysEnabled: false });
alertsClientParams.invalidateAPIKey.mockResolvedValue({
apiKeysEnabled: true,
- result: { error_count: 0 },
+ result: {
+ invalidated_api_keys: [],
+ previously_invalidated_api_keys: [],
+ error_count: 0,
+ },
});
alertsClientParams.getUserName.mockResolvedValue('elastic');
taskManager.runNow.mockResolvedValue({ id: '' });
@@ -71,6 +82,15 @@ beforeEach(() => {
},
]);
alertsClientParams.getActionsClient.mockResolvedValue(actionsClient);
+
+ alertTypeRegistry.get.mockImplementation((id) => ({
+ id: '123',
+ name: 'Test',
+ actionGroups: [{ id: 'default', name: 'Default' }],
+ defaultActionGroupId: 'default',
+ async executor() {},
+ producer: 'alerts',
+ }));
});
const mockedDate = new Date('2019-02-12T21:01:22.479Z');
@@ -114,19 +134,116 @@ describe('create()', () => {
beforeEach(() => {
alertsClient = new AlertsClient(alertsClientParams);
- alertTypeRegistry.get.mockReturnValue({
- id: '123',
- name: 'Test',
- actionGroups: [{ id: 'default', name: 'Default' }],
- defaultActionGroupId: 'default',
- async executor() {},
- producer: 'alerting',
+ });
+
+ describe('authorization', () => {
+ function tryToExecuteOperation(options: CreateOptions): Promise {
+ unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({
+ saved_objects: [
+ {
+ id: '1',
+ type: 'action',
+ attributes: {
+ actions: [],
+ actionTypeId: 'test',
+ },
+ references: [],
+ },
+ ],
+ });
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ alertTypeId: '123',
+ schedule: { interval: '10s' },
+ params: {
+ bar: true,
+ },
+ createdAt: '2019-02-12T21:01:22.479Z',
+ actions: [
+ {
+ group: 'default',
+ actionRef: 'action_0',
+ actionTypeId: 'test',
+ params: {
+ foo: true,
+ },
+ },
+ ],
+ },
+ references: [
+ {
+ name: 'action_0',
+ type: 'action',
+ id: '1',
+ },
+ ],
+ });
+ taskManager.schedule.mockResolvedValueOnce({
+ id: 'task-123',
+ taskType: 'alerting:123',
+ scheduledAt: new Date(),
+ attempts: 1,
+ status: TaskStatus.Idle,
+ runAt: new Date(),
+ startedAt: null,
+ retryAt: null,
+ state: {},
+ params: {},
+ ownerId: null,
+ });
+ unsecuredSavedObjectsClient.update.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ actions: [],
+ scheduledTaskId: 'task-123',
+ },
+ references: [
+ {
+ id: '1',
+ name: 'action_0',
+ type: 'action',
+ },
+ ],
+ });
+
+ return alertsClient.create(options);
+ }
+
+ test('ensures user is authorised to create this type of alert under the consumer', async () => {
+ const data = getMockData({
+ alertTypeId: 'myType',
+ consumer: 'myApp',
+ });
+
+ await tryToExecuteOperation({ data });
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'create');
+ });
+
+ test('throws when user is not authorised to create this type of alert', async () => {
+ const data = getMockData({
+ alertTypeId: 'myType',
+ consumer: 'myApp',
+ });
+
+ authorization.ensureAuthorized.mockRejectedValue(
+ new Error(`Unauthorized to create a "myType" alert for "myApp"`)
+ );
+
+ await expect(tryToExecuteOperation({ data })).rejects.toMatchInlineSnapshot(
+ `[Error: Unauthorized to create a "myType" alert for "myApp"]`
+ );
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'create');
});
});
test('creates an alert', async () => {
const data = getMockData();
- savedObjectsClient.create.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
@@ -168,10 +285,11 @@ describe('create()', () => {
params: {},
ownerId: null,
});
- savedObjectsClient.update.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.update.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
+ actions: [],
scheduledTaskId: 'task-123',
},
references: [
@@ -183,6 +301,7 @@ describe('create()', () => {
],
});
const result = await alertsClient.create({ data });
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('123', 'bar', 'create');
expect(result).toMatchInlineSnapshot(`
Object {
"actions": Array [
@@ -208,10 +327,10 @@ describe('create()', () => {
"updatedAt": 2019-02-12T21:01:22.479Z,
}
`);
- expect(savedObjectsClient.create).toHaveBeenCalledTimes(1);
- expect(savedObjectsClient.create.mock.calls[0]).toHaveLength(3);
- expect(savedObjectsClient.create.mock.calls[0][0]).toEqual('alert');
- expect(savedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(`
+ expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1);
+ expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3);
+ expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert');
+ expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(`
Object {
"actions": Array [
Object {
@@ -246,7 +365,7 @@ describe('create()', () => {
"updatedBy": "elastic",
}
`);
- expect(savedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(`
+ expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(`
Object {
"references": Array [
Object {
@@ -277,11 +396,11 @@ describe('create()', () => {
},
]
`);
- expect(savedObjectsClient.update).toHaveBeenCalledTimes(1);
- expect(savedObjectsClient.update.mock.calls[0]).toHaveLength(3);
- expect(savedObjectsClient.update.mock.calls[0][0]).toEqual('alert');
- expect(savedObjectsClient.update.mock.calls[0][1]).toEqual('1');
- expect(savedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(`
+ expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1);
+ expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toHaveLength(3);
+ expect(unsecuredSavedObjectsClient.update.mock.calls[0][0]).toEqual('alert');
+ expect(unsecuredSavedObjectsClient.update.mock.calls[0][1]).toEqual('1');
+ expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(`
Object {
"scheduledTaskId": "task-123",
}
@@ -314,7 +433,7 @@ describe('create()', () => {
},
],
});
- savedObjectsClient.create.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
@@ -382,10 +501,11 @@ describe('create()', () => {
params: {},
ownerId: null,
});
- savedObjectsClient.update.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.update.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
+ actions: [],
scheduledTaskId: 'task-123',
},
references: [],
@@ -436,19 +556,20 @@ describe('create()', () => {
test('creates a disabled alert', async () => {
const data = getMockData({ enabled: false });
- savedObjectsClient.bulkGet.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({
saved_objects: [
{
id: '1',
type: 'action',
attributes: {
+ actions: [],
actionTypeId: 'test',
},
references: [],
},
],
});
- savedObjectsClient.create.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
@@ -504,7 +625,7 @@ describe('create()', () => {
"updatedAt": 2019-02-12T21:01:22.479Z,
}
`);
- expect(savedObjectsClient.create).toHaveBeenCalledTimes(1);
+ expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1);
expect(taskManager.schedule).toHaveBeenCalledTimes(0);
});
@@ -527,7 +648,7 @@ describe('create()', () => {
}),
},
async executor() {},
- producer: 'alerting',
+ producer: 'alerts',
});
await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot(
`"params invalid: [param1]: expected value of type [string] but got [undefined]"`
@@ -542,25 +663,26 @@ describe('create()', () => {
await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot(
`"Test Error"`
);
- expect(savedObjectsClient.create).not.toHaveBeenCalled();
+ expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled();
expect(taskManager.schedule).not.toHaveBeenCalled();
});
test('throws error if create saved object fails', async () => {
const data = getMockData();
- savedObjectsClient.bulkGet.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({
saved_objects: [
{
id: '1',
type: 'action',
attributes: {
+ actions: [],
actionTypeId: 'test',
},
references: [],
},
],
});
- savedObjectsClient.create.mockRejectedValueOnce(new Error('Test failure'));
+ unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Test failure'));
await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot(
`"Test failure"`
);
@@ -569,19 +691,20 @@ describe('create()', () => {
test('attempts to remove saved object if scheduling failed', async () => {
const data = getMockData();
- savedObjectsClient.bulkGet.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({
saved_objects: [
{
id: '1',
type: 'action',
attributes: {
+ actions: [],
actionTypeId: 'test',
},
references: [],
},
],
});
- savedObjectsClient.create.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
@@ -610,12 +733,12 @@ describe('create()', () => {
],
});
taskManager.schedule.mockRejectedValueOnce(new Error('Test failure'));
- savedObjectsClient.delete.mockResolvedValueOnce({});
+ unsecuredSavedObjectsClient.delete.mockResolvedValueOnce({});
await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot(
`"Test failure"`
);
- expect(savedObjectsClient.delete).toHaveBeenCalledTimes(1);
- expect(savedObjectsClient.delete.mock.calls[0]).toMatchInlineSnapshot(`
+ expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledTimes(1);
+ expect(unsecuredSavedObjectsClient.delete.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"alert",
"1",
@@ -625,19 +748,20 @@ describe('create()', () => {
test('returns task manager error if cleanup fails, logs to console', async () => {
const data = getMockData();
- savedObjectsClient.bulkGet.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({
saved_objects: [
{
id: '1',
type: 'action',
attributes: {
+ actions: [],
actionTypeId: 'test',
},
references: [],
},
],
});
- savedObjectsClient.create.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
@@ -666,7 +790,9 @@ describe('create()', () => {
],
});
taskManager.schedule.mockRejectedValueOnce(new Error('Task manager error'));
- savedObjectsClient.delete.mockRejectedValueOnce(new Error('Saved object delete error'));
+ unsecuredSavedObjectsClient.delete.mockRejectedValueOnce(
+ new Error('Saved object delete error')
+ );
await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot(
`"Task manager error"`
);
@@ -689,21 +815,22 @@ describe('create()', () => {
const data = getMockData();
alertsClientParams.createAPIKey.mockResolvedValueOnce({
apiKeysEnabled: true,
- result: { id: '123', api_key: 'abc' },
+ result: { id: '123', name: '123', api_key: 'abc' },
});
- savedObjectsClient.bulkGet.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({
saved_objects: [
{
id: '1',
type: 'action',
attributes: {
+ actions: [],
actionTypeId: 'test',
},
references: [],
},
],
});
- savedObjectsClient.create.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
@@ -744,10 +871,11 @@ describe('create()', () => {
params: {},
ownerId: null,
});
- savedObjectsClient.update.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.update.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
+ actions: [],
scheduledTaskId: 'task-123',
},
references: [
@@ -761,7 +889,7 @@ describe('create()', () => {
await alertsClient.create({ data });
expect(alertsClientParams.createAPIKey).toHaveBeenCalledTimes(1);
- expect(savedObjectsClient.create).toHaveBeenCalledWith(
+ expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith(
'alert',
{
actions: [
@@ -802,19 +930,20 @@ describe('create()', () => {
test(`doesn't create API key for disabled alerts`, async () => {
const data = getMockData({ enabled: false });
- savedObjectsClient.bulkGet.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({
saved_objects: [
{
id: '1',
type: 'action',
attributes: {
+ actions: [],
actionTypeId: 'test',
},
references: [],
},
],
});
- savedObjectsClient.create.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
@@ -855,10 +984,11 @@ describe('create()', () => {
params: {},
ownerId: null,
});
- savedObjectsClient.update.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.update.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
+ actions: [],
scheduledTaskId: 'task-123',
},
references: [
@@ -872,7 +1002,7 @@ describe('create()', () => {
await alertsClient.create({ data });
expect(alertsClientParams.createAPIKey).not.toHaveBeenCalled();
- expect(savedObjectsClient.create).toHaveBeenCalledWith(
+ expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith(
'alert',
{
actions: [
@@ -918,9 +1048,21 @@ describe('enable()', () => {
id: '1',
type: 'alert',
attributes: {
+ consumer: 'myApp',
schedule: { interval: '10s' },
- alertTypeId: '2',
+ alertTypeId: 'myType',
enabled: false,
+ actions: [
+ {
+ group: 'default',
+ id: '1',
+ actionTypeId: '1',
+ actionRef: '1',
+ params: {
+ foo: true,
+ },
+ },
+ ],
},
version: '123',
references: [],
@@ -929,7 +1071,7 @@ describe('enable()', () => {
beforeEach(() => {
alertsClient = new AlertsClient(alertsClientParams);
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingAlert);
- savedObjectsClient.get.mockResolvedValue(existingAlert);
+ unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert);
alertsClientParams.createAPIKey.mockResolvedValue({
apiKeysEnabled: false,
});
@@ -948,31 +1090,85 @@ describe('enable()', () => {
});
});
+ describe('authorization', () => {
+ beforeEach(() => {
+ encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingAlert);
+ unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert);
+ alertsClientParams.createAPIKey.mockResolvedValue({
+ apiKeysEnabled: false,
+ });
+ taskManager.schedule.mockResolvedValue({
+ id: 'task-123',
+ scheduledAt: new Date(),
+ attempts: 0,
+ status: TaskStatus.Idle,
+ runAt: new Date(),
+ state: {},
+ params: {},
+ taskType: '',
+ startedAt: null,
+ retryAt: null,
+ ownerId: null,
+ });
+ });
+
+ test('ensures user is authorised to enable this type of alert under the consumer', async () => {
+ await alertsClient.enable({ id: '1' });
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'enable');
+ expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute');
+ });
+
+ test('throws when user is not authorised to enable this type of alert', async () => {
+ authorization.ensureAuthorized.mockRejectedValue(
+ new Error(`Unauthorized to enable a "myType" alert for "myApp"`)
+ );
+
+ await expect(alertsClient.enable({ id: '1' })).rejects.toMatchInlineSnapshot(
+ `[Error: Unauthorized to enable a "myType" alert for "myApp"]`
+ );
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'enable');
+ });
+ });
+
test('enables an alert', async () => {
await alertsClient.enable({ id: '1' });
- expect(savedObjectsClient.get).not.toHaveBeenCalled();
+ expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled();
expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', {
namespace: 'default',
});
expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled();
expect(alertsClientParams.createAPIKey).toHaveBeenCalled();
- expect(savedObjectsClient.update).toHaveBeenCalledWith(
+ expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith(
'alert',
'1',
{
schedule: { interval: '10s' },
- alertTypeId: '2',
+ alertTypeId: 'myType',
+ consumer: 'myApp',
enabled: true,
updatedBy: 'elastic',
apiKey: null,
apiKeyOwner: null,
+ actions: [
+ {
+ group: 'default',
+ id: '1',
+ actionTypeId: '1',
+ actionRef: '1',
+ params: {
+ foo: true,
+ },
+ },
+ ],
},
{
version: '123',
}
);
expect(taskManager.schedule).toHaveBeenCalledWith({
- taskType: `alerting:2`,
+ taskType: `alerting:myType`,
params: {
alertId: '1',
spaceId: 'default',
@@ -984,7 +1180,7 @@ describe('enable()', () => {
},
scope: ['alerting'],
});
- expect(savedObjectsClient.update).toHaveBeenCalledWith('alert', '1', {
+ expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith('alert', '1', {
scheduledTaskId: 'task-123',
});
});
@@ -999,7 +1195,7 @@ describe('enable()', () => {
});
await alertsClient.enable({ id: '1' });
- expect(savedObjectsClient.get).not.toHaveBeenCalled();
+ expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled();
expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', {
namespace: 'default',
});
@@ -1018,27 +1214,39 @@ describe('enable()', () => {
await alertsClient.enable({ id: '1' });
expect(alertsClientParams.getUserName).not.toHaveBeenCalled();
expect(alertsClientParams.createAPIKey).not.toHaveBeenCalled();
- expect(savedObjectsClient.update).not.toHaveBeenCalled();
+ expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled();
expect(taskManager.schedule).not.toHaveBeenCalled();
});
test('sets API key when createAPIKey returns one', async () => {
alertsClientParams.createAPIKey.mockResolvedValueOnce({
apiKeysEnabled: true,
- result: { id: '123', api_key: 'abc' },
+ result: { id: '123', name: '123', api_key: 'abc' },
});
await alertsClient.enable({ id: '1' });
- expect(savedObjectsClient.update).toHaveBeenCalledWith(
+ expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith(
'alert',
'1',
{
schedule: { interval: '10s' },
- alertTypeId: '2',
+ alertTypeId: 'myType',
+ consumer: 'myApp',
enabled: true,
apiKey: Buffer.from('123:abc').toString('base64'),
apiKeyOwner: 'elastic',
updatedBy: 'elastic',
+ actions: [
+ {
+ group: 'default',
+ id: '1',
+ actionTypeId: '1',
+ actionRef: '1',
+ params: {
+ foo: true,
+ },
+ },
+ ],
},
{
version: '123',
@@ -1050,7 +1258,7 @@ describe('enable()', () => {
encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail'));
await alertsClient.enable({ id: '1' });
- expect(savedObjectsClient.get).toHaveBeenCalledWith('alert', '1');
+ expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1');
expect(alertsClientParams.logger.error).toHaveBeenCalledWith(
'enable(): Failed to load API key to invalidate on alert 1: Fail'
);
@@ -1058,45 +1266,47 @@ describe('enable()', () => {
test('throws error when failing to load the saved object using SOC', async () => {
encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail'));
- savedObjectsClient.get.mockRejectedValueOnce(new Error('Fail to get'));
+ unsecuredSavedObjectsClient.get.mockRejectedValueOnce(new Error('Fail to get'));
await expect(alertsClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot(
`"Fail to get"`
);
expect(alertsClientParams.getUserName).not.toHaveBeenCalled();
expect(alertsClientParams.createAPIKey).not.toHaveBeenCalled();
- expect(savedObjectsClient.update).not.toHaveBeenCalled();
+ expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled();
expect(taskManager.schedule).not.toHaveBeenCalled();
});
test('throws error when failing to update the first time', async () => {
- savedObjectsClient.update.mockRejectedValueOnce(new Error('Fail to update'));
+ unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Fail to update'));
await expect(alertsClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot(
`"Fail to update"`
);
expect(alertsClientParams.getUserName).toHaveBeenCalled();
expect(alertsClientParams.createAPIKey).toHaveBeenCalled();
- expect(savedObjectsClient.update).toHaveBeenCalledTimes(1);
+ expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1);
expect(taskManager.schedule).not.toHaveBeenCalled();
});
test('throws error when failing to update the second time', async () => {
- savedObjectsClient.update.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.update.mockResolvedValueOnce({
...existingAlert,
attributes: {
...existingAlert.attributes,
enabled: true,
},
});
- savedObjectsClient.update.mockRejectedValueOnce(new Error('Fail to update second time'));
+ unsecuredSavedObjectsClient.update.mockRejectedValueOnce(
+ new Error('Fail to update second time')
+ );
await expect(alertsClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot(
`"Fail to update second time"`
);
expect(alertsClientParams.getUserName).toHaveBeenCalled();
expect(alertsClientParams.createAPIKey).toHaveBeenCalled();
- expect(savedObjectsClient.update).toHaveBeenCalledTimes(2);
+ expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(2);
expect(taskManager.schedule).toHaveBeenCalled();
});
@@ -1108,7 +1318,7 @@ describe('enable()', () => {
);
expect(alertsClientParams.getUserName).toHaveBeenCalled();
expect(alertsClientParams.createAPIKey).toHaveBeenCalled();
- expect(savedObjectsClient.update).toHaveBeenCalled();
+ expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled();
});
});
@@ -1118,10 +1328,22 @@ describe('disable()', () => {
id: '1',
type: 'alert',
attributes: {
+ consumer: 'myApp',
schedule: { interval: '10s' },
- alertTypeId: '2',
+ alertTypeId: 'myType',
enabled: true,
scheduledTaskId: 'task-123',
+ actions: [
+ {
+ group: 'default',
+ id: '1',
+ actionTypeId: '1',
+ actionRef: '1',
+ params: {
+ foo: true,
+ },
+ },
+ ],
},
version: '123',
references: [],
@@ -1136,27 +1358,59 @@ describe('disable()', () => {
beforeEach(() => {
alertsClient = new AlertsClient(alertsClientParams);
- savedObjectsClient.get.mockResolvedValue(existingAlert);
+ unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert);
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedAlert);
});
+ describe('authorization', () => {
+ test('ensures user is authorised to disable this type of alert under the consumer', async () => {
+ await alertsClient.disable({ id: '1' });
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'disable');
+ });
+
+ test('throws when user is not authorised to disable this type of alert', async () => {
+ authorization.ensureAuthorized.mockRejectedValue(
+ new Error(`Unauthorized to disable a "myType" alert for "myApp"`)
+ );
+
+ await expect(alertsClient.disable({ id: '1' })).rejects.toMatchInlineSnapshot(
+ `[Error: Unauthorized to disable a "myType" alert for "myApp"]`
+ );
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'disable');
+ });
+ });
+
test('disables an alert', async () => {
await alertsClient.disable({ id: '1' });
- expect(savedObjectsClient.get).not.toHaveBeenCalled();
+ expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled();
expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', {
namespace: 'default',
});
- expect(savedObjectsClient.update).toHaveBeenCalledWith(
+ expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith(
'alert',
'1',
{
+ consumer: 'myApp',
schedule: { interval: '10s' },
- alertTypeId: '2',
+ alertTypeId: 'myType',
apiKey: null,
apiKeyOwner: null,
enabled: false,
scheduledTaskId: null,
updatedBy: 'elastic',
+ actions: [
+ {
+ group: 'default',
+ id: '1',
+ actionTypeId: '1',
+ actionRef: '1',
+ params: {
+ foo: true,
+ },
+ },
+ ],
},
{
version: '123',
@@ -1170,21 +1424,33 @@ describe('disable()', () => {
encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail'));
await alertsClient.disable({ id: '1' });
- expect(savedObjectsClient.get).toHaveBeenCalledWith('alert', '1');
+ expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1');
expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', {
namespace: 'default',
});
- expect(savedObjectsClient.update).toHaveBeenCalledWith(
+ expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith(
'alert',
'1',
{
+ consumer: 'myApp',
schedule: { interval: '10s' },
- alertTypeId: '2',
+ alertTypeId: 'myType',
apiKey: null,
apiKeyOwner: null,
enabled: false,
scheduledTaskId: null,
updatedBy: 'elastic',
+ actions: [
+ {
+ group: 'default',
+ id: '1',
+ actionTypeId: '1',
+ actionRef: '1',
+ params: {
+ foo: true,
+ },
+ },
+ ],
},
{
version: '123',
@@ -1199,12 +1465,13 @@ describe('disable()', () => {
...existingDecryptedAlert,
attributes: {
...existingDecryptedAlert.attributes,
+ actions: [],
enabled: false,
},
});
await alertsClient.disable({ id: '1' });
- expect(savedObjectsClient.update).not.toHaveBeenCalled();
+ expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled();
expect(taskManager.remove).not.toHaveBeenCalled();
expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled();
});
@@ -1220,7 +1487,7 @@ describe('disable()', () => {
encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail'));
await alertsClient.disable({ id: '1' });
- expect(savedObjectsClient.update).toHaveBeenCalled();
+ expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled();
expect(taskManager.remove).toHaveBeenCalled();
expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled();
expect(alertsClientParams.logger.error).toHaveBeenCalledWith(
@@ -1228,8 +1495,8 @@ describe('disable()', () => {
);
});
- test('throws when savedObjectsClient update fails', async () => {
- savedObjectsClient.update.mockRejectedValueOnce(new Error('Failed to update'));
+ test('throws when unsecuredSavedObjectsClient update fails', async () => {
+ unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Failed to update'));
await expect(alertsClient.disable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot(
`"Failed to update"`
@@ -1257,52 +1524,181 @@ describe('disable()', () => {
describe('muteAll()', () => {
test('mutes an alert', async () => {
const alertsClient = new AlertsClient(alertsClientParams);
- savedObjectsClient.get.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
+ actions: [
+ {
+ group: 'default',
+ id: '1',
+ actionTypeId: '1',
+ actionRef: '1',
+ params: {
+ foo: true,
+ },
+ },
+ ],
muteAll: false,
},
references: [],
});
await alertsClient.muteAll({ id: '1' });
- expect(savedObjectsClient.update).toHaveBeenCalledWith('alert', '1', {
+ expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith('alert', '1', {
muteAll: true,
mutedInstanceIds: [],
updatedBy: 'elastic',
});
});
+
+ describe('authorization', () => {
+ beforeEach(() => {
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ actions: [
+ {
+ group: 'default',
+ id: '1',
+ actionTypeId: '1',
+ actionRef: '1',
+ params: {
+ foo: true,
+ },
+ },
+ ],
+ consumer: 'myApp',
+ schedule: { interval: '10s' },
+ alertTypeId: 'myType',
+ apiKey: null,
+ apiKeyOwner: null,
+ enabled: false,
+ scheduledTaskId: null,
+ updatedBy: 'elastic',
+ muteAll: false,
+ },
+ references: [],
+ });
+ });
+
+ test('ensures user is authorised to muteAll this type of alert under the consumer', async () => {
+ const alertsClient = new AlertsClient(alertsClientParams);
+ await alertsClient.muteAll({ id: '1' });
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'muteAll');
+ expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute');
+ });
+
+ test('throws when user is not authorised to muteAll this type of alert', async () => {
+ const alertsClient = new AlertsClient(alertsClientParams);
+ authorization.ensureAuthorized.mockRejectedValue(
+ new Error(`Unauthorized to muteAll a "myType" alert for "myApp"`)
+ );
+
+ await expect(alertsClient.muteAll({ id: '1' })).rejects.toMatchInlineSnapshot(
+ `[Error: Unauthorized to muteAll a "myType" alert for "myApp"]`
+ );
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'muteAll');
+ });
+ });
});
describe('unmuteAll()', () => {
test('unmutes an alert', async () => {
const alertsClient = new AlertsClient(alertsClientParams);
- savedObjectsClient.get.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
+ actions: [
+ {
+ group: 'default',
+ id: '1',
+ actionTypeId: '1',
+ actionRef: '1',
+ params: {
+ foo: true,
+ },
+ },
+ ],
muteAll: true,
},
references: [],
});
await alertsClient.unmuteAll({ id: '1' });
- expect(savedObjectsClient.update).toHaveBeenCalledWith('alert', '1', {
+ expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith('alert', '1', {
muteAll: false,
mutedInstanceIds: [],
updatedBy: 'elastic',
});
});
+
+ describe('authorization', () => {
+ beforeEach(() => {
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ actions: [
+ {
+ group: 'default',
+ id: '1',
+ actionTypeId: '1',
+ actionRef: '1',
+ params: {
+ foo: true,
+ },
+ },
+ ],
+ consumer: 'myApp',
+ schedule: { interval: '10s' },
+ alertTypeId: 'myType',
+ apiKey: null,
+ apiKeyOwner: null,
+ enabled: false,
+ scheduledTaskId: null,
+ updatedBy: 'elastic',
+ muteAll: false,
+ },
+ references: [],
+ });
+ });
+
+ test('ensures user is authorised to unmuteAll this type of alert under the consumer', async () => {
+ const alertsClient = new AlertsClient(alertsClientParams);
+ await alertsClient.unmuteAll({ id: '1' });
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'unmuteAll');
+ expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute');
+ });
+
+ test('throws when user is not authorised to unmuteAll this type of alert', async () => {
+ const alertsClient = new AlertsClient(alertsClientParams);
+ authorization.ensureAuthorized.mockRejectedValue(
+ new Error(`Unauthorized to unmuteAll a "myType" alert for "myApp"`)
+ );
+
+ await expect(alertsClient.unmuteAll({ id: '1' })).rejects.toMatchInlineSnapshot(
+ `[Error: Unauthorized to unmuteAll a "myType" alert for "myApp"]`
+ );
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'unmuteAll');
+ });
+ });
});
describe('muteInstance()', () => {
test('mutes an alert instance', async () => {
const alertsClient = new AlertsClient(alertsClientParams);
- savedObjectsClient.get.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
+ actions: [],
schedule: { interval: '10s' },
alertTypeId: '2',
enabled: true,
@@ -1314,7 +1710,7 @@ describe('muteInstance()', () => {
});
await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' });
- expect(savedObjectsClient.update).toHaveBeenCalledWith(
+ expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith(
'alert',
'1',
{
@@ -1327,10 +1723,11 @@ describe('muteInstance()', () => {
test('skips muting when alert instance already muted', async () => {
const alertsClient = new AlertsClient(alertsClientParams);
- savedObjectsClient.get.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
+ actions: [],
schedule: { interval: '10s' },
alertTypeId: '2',
enabled: true,
@@ -1341,15 +1738,16 @@ describe('muteInstance()', () => {
});
await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' });
- expect(savedObjectsClient.update).not.toHaveBeenCalled();
+ expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled();
});
test('skips muting when alert is muted', async () => {
const alertsClient = new AlertsClient(alertsClientParams);
- savedObjectsClient.get.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
+ actions: [],
schedule: { interval: '10s' },
alertTypeId: '2',
enabled: true,
@@ -1361,17 +1759,79 @@ describe('muteInstance()', () => {
});
await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' });
- expect(savedObjectsClient.update).not.toHaveBeenCalled();
+ expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled();
+ });
+
+ describe('authorization', () => {
+ beforeEach(() => {
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ actions: [
+ {
+ group: 'default',
+ id: '1',
+ actionTypeId: '1',
+ actionRef: '1',
+ params: {
+ foo: true,
+ },
+ },
+ ],
+ schedule: { interval: '10s' },
+ alertTypeId: 'myType',
+ consumer: 'myApp',
+ enabled: true,
+ scheduledTaskId: 'task-123',
+ mutedInstanceIds: [],
+ },
+ version: '123',
+ references: [],
+ });
+ });
+
+ test('ensures user is authorised to muteInstance this type of alert under the consumer', async () => {
+ const alertsClient = new AlertsClient(alertsClientParams);
+ await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' });
+
+ expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute');
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith(
+ 'myType',
+ 'myApp',
+ 'muteInstance'
+ );
+ });
+
+ test('throws when user is not authorised to muteInstance this type of alert', async () => {
+ const alertsClient = new AlertsClient(alertsClientParams);
+ authorization.ensureAuthorized.mockRejectedValue(
+ new Error(`Unauthorized to muteInstance a "myType" alert for "myApp"`)
+ );
+
+ await expect(
+ alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' })
+ ).rejects.toMatchInlineSnapshot(
+ `[Error: Unauthorized to muteInstance a "myType" alert for "myApp"]`
+ );
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith(
+ 'myType',
+ 'myApp',
+ 'muteInstance'
+ );
+ });
});
});
describe('unmuteInstance()', () => {
test('unmutes an alert instance', async () => {
const alertsClient = new AlertsClient(alertsClientParams);
- savedObjectsClient.get.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
+ actions: [],
schedule: { interval: '10s' },
alertTypeId: '2',
enabled: true,
@@ -1383,7 +1843,7 @@ describe('unmuteInstance()', () => {
});
await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' });
- expect(savedObjectsClient.update).toHaveBeenCalledWith(
+ expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith(
'alert',
'1',
{
@@ -1396,10 +1856,11 @@ describe('unmuteInstance()', () => {
test('skips unmuting when alert instance not muted', async () => {
const alertsClient = new AlertsClient(alertsClientParams);
- savedObjectsClient.get.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
+ actions: [],
schedule: { interval: '10s' },
alertTypeId: '2',
enabled: true,
@@ -1410,15 +1871,16 @@ describe('unmuteInstance()', () => {
});
await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' });
- expect(savedObjectsClient.update).not.toHaveBeenCalled();
+ expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled();
});
test('skips unmuting when alert is muted', async () => {
const alertsClient = new AlertsClient(alertsClientParams);
- savedObjectsClient.get.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
+ actions: [],
schedule: { interval: '10s' },
alertTypeId: '2',
enabled: true,
@@ -1430,14 +1892,75 @@ describe('unmuteInstance()', () => {
});
await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' });
- expect(savedObjectsClient.update).not.toHaveBeenCalled();
+ expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled();
+ });
+
+ describe('authorization', () => {
+ beforeEach(() => {
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ actions: [
+ {
+ group: 'default',
+ id: '1',
+ actionTypeId: '1',
+ actionRef: '1',
+ params: {
+ foo: true,
+ },
+ },
+ ],
+ alertTypeId: 'myType',
+ consumer: 'myApp',
+ schedule: { interval: '10s' },
+ enabled: true,
+ scheduledTaskId: 'task-123',
+ mutedInstanceIds: ['2'],
+ },
+ version: '123',
+ references: [],
+ });
+ });
+
+ test('ensures user is authorised to unmuteInstance this type of alert under the consumer', async () => {
+ const alertsClient = new AlertsClient(alertsClientParams);
+ await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' });
+
+ expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute');
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith(
+ 'myType',
+ 'myApp',
+ 'unmuteInstance'
+ );
+ });
+
+ test('throws when user is not authorised to unmuteInstance this type of alert', async () => {
+ const alertsClient = new AlertsClient(alertsClientParams);
+ authorization.ensureAuthorized.mockRejectedValue(
+ new Error(`Unauthorized to unmuteInstance a "myType" alert for "myApp"`)
+ );
+
+ await expect(
+ alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' })
+ ).rejects.toMatchInlineSnapshot(
+ `[Error: Unauthorized to unmuteInstance a "myType" alert for "myApp"]`
+ );
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith(
+ 'myType',
+ 'myApp',
+ 'unmuteInstance'
+ );
+ });
});
});
describe('get()', () => {
test('calls saved objects client with given params', async () => {
const alertsClient = new AlertsClient(alertsClientParams);
- savedObjectsClient.get.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
@@ -1446,6 +1969,7 @@ describe('get()', () => {
params: {
bar: true,
},
+ createdAt: new Date().toISOString(),
actions: [
{
group: 'default',
@@ -1488,8 +2012,8 @@ describe('get()', () => {
"updatedAt": 2019-02-12T21:01:22.479Z,
}
`);
- expect(savedObjectsClient.get).toHaveBeenCalledTimes(1);
- expect(savedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(`
+ expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1);
+ expect(unsecuredSavedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"alert",
"1",
@@ -1499,7 +2023,7 @@ describe('get()', () => {
test(`throws an error when references aren't found`, async () => {
const alertsClient = new AlertsClient(alertsClientParams);
- savedObjectsClient.get.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
@@ -1517,19 +2041,72 @@ describe('get()', () => {
},
},
],
- },
- references: [],
+ },
+ references: [],
+ });
+ await expect(alertsClient.get({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot(
+ `"Action reference \\"action_0\\" not found in alert id: 1"`
+ );
+ });
+
+ describe('authorization', () => {
+ beforeEach(() => {
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ alertTypeId: 'myType',
+ consumer: 'myApp',
+ schedule: { interval: '10s' },
+ params: {
+ bar: true,
+ },
+ actions: [
+ {
+ group: 'default',
+ actionRef: 'action_0',
+ params: {
+ foo: true,
+ },
+ },
+ ],
+ },
+ references: [
+ {
+ name: 'action_0',
+ type: 'action',
+ id: '1',
+ },
+ ],
+ });
+ });
+
+ test('ensures user is authorised to get this type of alert under the consumer', async () => {
+ const alertsClient = new AlertsClient(alertsClientParams);
+ await alertsClient.get({ id: '1' });
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'get');
+ });
+
+ test('throws when user is not authorised to get this type of alert', async () => {
+ const alertsClient = new AlertsClient(alertsClientParams);
+ authorization.ensureAuthorized.mockRejectedValue(
+ new Error(`Unauthorized to get a "myType" alert for "myApp"`)
+ );
+
+ await expect(alertsClient.get({ id: '1' })).rejects.toMatchInlineSnapshot(
+ `[Error: Unauthorized to get a "myType" alert for "myApp"]`
+ );
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'get');
});
- await expect(alertsClient.get({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot(
- `"Reference action_0 not found"`
- );
});
});
describe('getAlertState()', () => {
test('calls saved objects client with given params', async () => {
const alertsClient = new AlertsClient(alertsClientParams);
- savedObjectsClient.get.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
@@ -1572,8 +2149,8 @@ describe('getAlertState()', () => {
});
await alertsClient.getAlertState({ id: '1' });
- expect(savedObjectsClient.get).toHaveBeenCalledTimes(1);
- expect(savedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(`
+ expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1);
+ expect(unsecuredSavedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"alert",
"1",
@@ -1586,7 +2163,7 @@ describe('getAlertState()', () => {
const scheduledTaskId = 'task-123';
- savedObjectsClient.get.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
@@ -1638,12 +2215,103 @@ describe('getAlertState()', () => {
expect(taskManager.get).toHaveBeenCalledTimes(1);
expect(taskManager.get).toHaveBeenCalledWith(scheduledTaskId);
});
+
+ describe('authorization', () => {
+ beforeEach(() => {
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ alertTypeId: 'myType',
+ consumer: 'myApp',
+ schedule: { interval: '10s' },
+ params: {
+ bar: true,
+ },
+ actions: [
+ {
+ group: 'default',
+ actionRef: 'action_0',
+ params: {
+ foo: true,
+ },
+ },
+ ],
+ },
+ references: [
+ {
+ name: 'action_0',
+ type: 'action',
+ id: '1',
+ },
+ ],
+ });
+
+ taskManager.get.mockResolvedValueOnce({
+ id: '1',
+ taskType: 'alerting:123',
+ scheduledAt: new Date(),
+ attempts: 1,
+ status: TaskStatus.Idle,
+ runAt: new Date(),
+ startedAt: null,
+ retryAt: null,
+ state: {},
+ params: {},
+ ownerId: null,
+ });
+ });
+
+ test('ensures user is authorised to get this type of alert under the consumer', async () => {
+ const alertsClient = new AlertsClient(alertsClientParams);
+ await alertsClient.getAlertState({ id: '1' });
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith(
+ 'myType',
+ 'myApp',
+ 'getAlertState'
+ );
+ });
+
+ test('throws when user is not authorised to getAlertState this type of alert', async () => {
+ const alertsClient = new AlertsClient(alertsClientParams);
+ // `get` check
+ authorization.ensureAuthorized.mockResolvedValueOnce();
+ // `getAlertState` check
+ authorization.ensureAuthorized.mockRejectedValueOnce(
+ new Error(`Unauthorized to getAlertState a "myType" alert for "myApp"`)
+ );
+
+ await expect(alertsClient.getAlertState({ id: '1' })).rejects.toMatchInlineSnapshot(
+ `[Error: Unauthorized to getAlertState a "myType" alert for "myApp"]`
+ );
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith(
+ 'myType',
+ 'myApp',
+ 'getAlertState'
+ );
+ });
+ });
});
describe('find()', () => {
- test('calls saved objects client with given params', async () => {
- const alertsClient = new AlertsClient(alertsClientParams);
- savedObjectsClient.find.mockResolvedValueOnce({
+ const listedTypes = new Set([
+ {
+ actionGroups: [],
+ actionVariables: undefined,
+ defaultActionGroupId: 'default',
+ id: 'myType',
+ name: 'myType',
+ producer: 'myApp',
+ },
+ ]);
+ beforeEach(() => {
+ authorization.getFindAuthorizationFilter.mockResolvedValue({
+ ensureAlertTypeIsAuthorized() {},
+ logSuccessfulAuthorization() {},
+ });
+ unsecuredSavedObjectsClient.find.mockResolvedValueOnce({
total: 1,
per_page: 10,
page: 1,
@@ -1652,11 +2320,12 @@ describe('find()', () => {
id: '1',
type: 'alert',
attributes: {
- alertTypeId: '123',
+ alertTypeId: 'myType',
schedule: { interval: '10s' },
params: {
bar: true,
},
+ createdAt: new Date().toISOString(),
actions: [
{
group: 'default',
@@ -1678,6 +2347,25 @@ describe('find()', () => {
},
],
});
+ alertTypeRegistry.list.mockReturnValue(listedTypes);
+ authorization.filterByAlertTypeAuthorization.mockResolvedValue(
+ new Set([
+ {
+ id: 'myType',
+ name: 'Test',
+ actionGroups: [{ id: 'default', name: 'Default' }],
+ defaultActionGroupId: 'default',
+ producer: 'alerts',
+ authorizedConsumers: {
+ myApp: { read: true, all: true },
+ },
+ },
+ ])
+ );
+ });
+
+ test('calls saved objects client with given params', async () => {
+ const alertsClient = new AlertsClient(alertsClientParams);
const result = await alertsClient.find({ options: {} });
expect(result).toMatchInlineSnapshot(`
Object {
@@ -1692,7 +2380,7 @@ describe('find()', () => {
},
},
],
- "alertTypeId": "123",
+ "alertTypeId": "myType",
"createdAt": 2019-02-12T21:01:22.479Z,
"id": "1",
"params": Object {
@@ -1709,14 +2397,100 @@ describe('find()', () => {
"total": 1,
}
`);
- expect(savedObjectsClient.find).toHaveBeenCalledTimes(1);
- expect(savedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(`
- Array [
- Object {
- "type": "alert",
- },
- ]
- `);
+ expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(1);
+ expect(unsecuredSavedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "fields": undefined,
+ "type": "alert",
+ },
+ ]
+ `);
+ });
+
+ describe('authorization', () => {
+ test('ensures user is query filter types down to those the user is authorized to find', async () => {
+ authorization.getFindAuthorizationFilter.mockResolvedValue({
+ filter:
+ '((alert.attributes.alertTypeId:myType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myOtherType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myOtherType and alert.attributes.consumer:myOtherApp))',
+ ensureAlertTypeIsAuthorized() {},
+ logSuccessfulAuthorization() {},
+ });
+
+ const alertsClient = new AlertsClient(alertsClientParams);
+ await alertsClient.find({ options: { filter: 'someTerm' } });
+
+ const [options] = unsecuredSavedObjectsClient.find.mock.calls[0];
+ expect(options.filter).toMatchInlineSnapshot(
+ `"someTerm and ((alert.attributes.alertTypeId:myType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myOtherType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myOtherType and alert.attributes.consumer:myOtherApp))"`
+ );
+ expect(authorization.getFindAuthorizationFilter).toHaveBeenCalledTimes(1);
+ });
+
+ test('throws if user is not authorized to find any types', async () => {
+ const alertsClient = new AlertsClient(alertsClientParams);
+ authorization.getFindAuthorizationFilter.mockRejectedValue(new Error('not authorized'));
+ await expect(alertsClient.find({ options: {} })).rejects.toThrowErrorMatchingInlineSnapshot(
+ `"not authorized"`
+ );
+ });
+
+ test('ensures authorization even when the fields required to authorize are omitted from the find', async () => {
+ const ensureAlertTypeIsAuthorized = jest.fn();
+ const logSuccessfulAuthorization = jest.fn();
+ authorization.getFindAuthorizationFilter.mockResolvedValue({
+ filter: '',
+ ensureAlertTypeIsAuthorized,
+ logSuccessfulAuthorization,
+ });
+
+ unsecuredSavedObjectsClient.find.mockReset();
+ unsecuredSavedObjectsClient.find.mockResolvedValue({
+ total: 1,
+ per_page: 10,
+ page: 1,
+ saved_objects: [
+ {
+ id: '1',
+ type: 'alert',
+ attributes: {
+ actions: [],
+ alertTypeId: 'myType',
+ consumer: 'myApp',
+ tags: ['myTag'],
+ },
+ score: 1,
+ references: [],
+ },
+ ],
+ });
+
+ const alertsClient = new AlertsClient(alertsClientParams);
+ expect(await alertsClient.find({ options: { fields: ['tags'] } })).toMatchInlineSnapshot(`
+ Object {
+ "data": Array [
+ Object {
+ "actions": Array [],
+ "id": "1",
+ "schedule": undefined,
+ "tags": Array [
+ "myTag",
+ ],
+ },
+ ],
+ "page": 1,
+ "perPage": 10,
+ "total": 1,
+ }
+ `);
+
+ expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledWith({
+ fields: ['tags', 'alertTypeId', 'consumer'],
+ type: 'alert',
+ });
+ expect(ensureAlertTypeIsAuthorized).toHaveBeenCalledWith('myType', 'myApp');
+ expect(logSuccessfulAuthorization).toHaveBeenCalled();
+ });
});
});
@@ -1726,7 +2500,8 @@ describe('delete()', () => {
id: '1',
type: 'alert',
attributes: {
- alertTypeId: '123',
+ alertTypeId: 'myType',
+ consumer: 'myApp',
schedule: { interval: '10s' },
params: {
bar: true,
@@ -1735,6 +2510,7 @@ describe('delete()', () => {
actions: [
{
group: 'default',
+ actionTypeId: '.no-op',
actionRef: 'action_0',
params: {
foo: true,
@@ -1760,8 +2536,8 @@ describe('delete()', () => {
beforeEach(() => {
alertsClient = new AlertsClient(alertsClientParams);
- savedObjectsClient.get.mockResolvedValue(existingAlert);
- savedObjectsClient.delete.mockResolvedValue({
+ unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert);
+ unsecuredSavedObjectsClient.delete.mockResolvedValue({
success: true,
});
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedAlert);
@@ -1770,13 +2546,13 @@ describe('delete()', () => {
test('successfully removes an alert', async () => {
const result = await alertsClient.delete({ id: '1' });
expect(result).toEqual({ success: true });
- expect(savedObjectsClient.delete).toHaveBeenCalledWith('alert', '1');
+ expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledWith('alert', '1');
expect(taskManager.remove).toHaveBeenCalledWith('task-123');
expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' });
expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', {
namespace: 'default',
});
- expect(savedObjectsClient.get).not.toHaveBeenCalled();
+ expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled();
});
test('falls back to SOC.get when getDecryptedAsInternalUser throws an error', async () => {
@@ -1784,10 +2560,10 @@ describe('delete()', () => {
const result = await alertsClient.delete({ id: '1' });
expect(result).toEqual({ success: true });
- expect(savedObjectsClient.delete).toHaveBeenCalledWith('alert', '1');
+ expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledWith('alert', '1');
expect(taskManager.remove).toHaveBeenCalledWith('task-123');
expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled();
- expect(savedObjectsClient.get).toHaveBeenCalledWith('alert', '1');
+ expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1');
expect(alertsClientParams.logger.error).toHaveBeenCalledWith(
'delete(): Failed to load API key to invalidate on alert 1: Fail'
);
@@ -1839,9 +2615,9 @@ describe('delete()', () => {
);
});
- test('throws error when savedObjectsClient.get throws an error', async () => {
+ test('throws error when unsecuredSavedObjectsClient.get throws an error', async () => {
encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail'));
- savedObjectsClient.get.mockRejectedValue(new Error('SOC Fail'));
+ unsecuredSavedObjectsClient.get.mockRejectedValue(new Error('SOC Fail'));
await expect(alertsClient.delete({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot(
`"SOC Fail"`
@@ -1855,6 +2631,26 @@ describe('delete()', () => {
`"TM Fail"`
);
});
+
+ describe('authorization', () => {
+ test('ensures user is authorised to delete this type of alert under the consumer', async () => {
+ await alertsClient.delete({ id: '1' });
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'delete');
+ });
+
+ test('throws when user is not authorised to delete this type of alert', async () => {
+ authorization.ensureAuthorized.mockRejectedValue(
+ new Error(`Unauthorized to delete a "myType" alert for "myApp"`)
+ );
+
+ await expect(alertsClient.delete({ id: '1' })).rejects.toMatchInlineSnapshot(
+ `[Error: Unauthorized to delete a "myType" alert for "myApp"]`
+ );
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'delete');
+ });
+ });
});
describe('update()', () => {
@@ -1864,8 +2660,20 @@ describe('update()', () => {
type: 'alert',
attributes: {
enabled: true,
- alertTypeId: '123',
+ alertTypeId: 'myType',
+ consumer: 'myApp',
scheduledTaskId: 'task-123',
+ actions: [
+ {
+ group: 'default',
+ id: '1',
+ actionTypeId: '1',
+ actionRef: '1',
+ params: {
+ foo: true,
+ },
+ },
+ ],
},
references: [],
version: '123',
@@ -1880,7 +2688,7 @@ describe('update()', () => {
beforeEach(() => {
alertsClient = new AlertsClient(alertsClientParams);
- savedObjectsClient.get.mockResolvedValue(existingAlert);
+ unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert);
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedAlert);
alertTypeRegistry.get.mockReturnValue({
id: '123',
@@ -1888,12 +2696,12 @@ describe('update()', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
async executor() {},
- producer: 'alerting',
+ producer: 'alerts',
});
});
test('updates given parameters', async () => {
- savedObjectsClient.update.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.update.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
@@ -2029,12 +2837,12 @@ describe('update()', () => {
expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', {
namespace: 'default',
});
- expect(savedObjectsClient.get).not.toHaveBeenCalled();
- expect(savedObjectsClient.update).toHaveBeenCalledTimes(1);
- expect(savedObjectsClient.update.mock.calls[0]).toHaveLength(4);
- expect(savedObjectsClient.update.mock.calls[0][0]).toEqual('alert');
- expect(savedObjectsClient.update.mock.calls[0][1]).toEqual('1');
- expect(savedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(`
+ expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled();
+ expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1);
+ expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toHaveLength(4);
+ expect(unsecuredSavedObjectsClient.update.mock.calls[0][0]).toEqual('alert');
+ expect(unsecuredSavedObjectsClient.update.mock.calls[0][1]).toEqual('1');
+ expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(`
Object {
"actions": Array [
Object {
@@ -2062,9 +2870,10 @@ describe('update()', () => {
},
},
],
- "alertTypeId": "123",
+ "alertTypeId": "myType",
"apiKey": null,
"apiKeyOwner": null,
+ "consumer": "myApp",
"enabled": true,
"name": "abc",
"params": Object {
@@ -2081,7 +2890,7 @@ describe('update()', () => {
"updatedBy": "elastic",
}
`);
- expect(savedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(`
+ expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(`
Object {
"references": Array [
Object {
@@ -2106,12 +2915,13 @@ describe('update()', () => {
});
it('calls the createApiKey function', async () => {
- savedObjectsClient.bulkGet.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({
saved_objects: [
{
id: '1',
type: 'action',
attributes: {
+ actions: [],
actionTypeId: 'test',
},
references: [],
@@ -2120,9 +2930,9 @@ describe('update()', () => {
});
alertsClientParams.createAPIKey.mockResolvedValueOnce({
apiKeysEnabled: true,
- result: { id: '123', api_key: 'abc' },
+ result: { id: '123', name: '123', api_key: 'abc' },
});
- savedObjectsClient.update.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.update.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
@@ -2201,11 +3011,11 @@ describe('update()', () => {
"updatedAt": 2019-02-12T21:01:22.479Z,
}
`);
- expect(savedObjectsClient.update).toHaveBeenCalledTimes(1);
- expect(savedObjectsClient.update.mock.calls[0]).toHaveLength(4);
- expect(savedObjectsClient.update.mock.calls[0][0]).toEqual('alert');
- expect(savedObjectsClient.update.mock.calls[0][1]).toEqual('1');
- expect(savedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(`
+ expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1);
+ expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toHaveLength(4);
+ expect(unsecuredSavedObjectsClient.update.mock.calls[0][0]).toEqual('alert');
+ expect(unsecuredSavedObjectsClient.update.mock.calls[0][1]).toEqual('1');
+ expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(`
Object {
"actions": Array [
Object {
@@ -2217,9 +3027,10 @@ describe('update()', () => {
},
},
],
- "alertTypeId": "123",
+ "alertTypeId": "myType",
"apiKey": "MTIzOmFiYw==",
"apiKeyOwner": "elastic",
+ "consumer": "myApp",
"enabled": true,
"name": "abc",
"params": Object {
@@ -2236,7 +3047,7 @@ describe('update()', () => {
"updatedBy": "elastic",
}
`);
- expect(savedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(`
+ expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(`
Object {
"references": Array [
Object {
@@ -2258,19 +3069,20 @@ describe('update()', () => {
enabled: false,
},
});
- savedObjectsClient.bulkGet.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({
saved_objects: [
{
id: '1',
type: 'action',
attributes: {
+ actions: [],
actionTypeId: 'test',
},
references: [],
},
],
});
- savedObjectsClient.update.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.update.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
@@ -2350,11 +3162,11 @@ describe('update()', () => {
"updatedAt": 2019-02-12T21:01:22.479Z,
}
`);
- expect(savedObjectsClient.update).toHaveBeenCalledTimes(1);
- expect(savedObjectsClient.update.mock.calls[0]).toHaveLength(4);
- expect(savedObjectsClient.update.mock.calls[0][0]).toEqual('alert');
- expect(savedObjectsClient.update.mock.calls[0][1]).toEqual('1');
- expect(savedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(`
+ expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1);
+ expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toHaveLength(4);
+ expect(unsecuredSavedObjectsClient.update.mock.calls[0][0]).toEqual('alert');
+ expect(unsecuredSavedObjectsClient.update.mock.calls[0][1]).toEqual('1');
+ expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(`
Object {
"actions": Array [
Object {
@@ -2366,9 +3178,10 @@ describe('update()', () => {
},
},
],
- "alertTypeId": "123",
+ "alertTypeId": "myType",
"apiKey": null,
"apiKeyOwner": null,
+ "consumer": "myApp",
"enabled": false,
"name": "abc",
"params": Object {
@@ -2385,7 +3198,7 @@ describe('update()', () => {
"updatedBy": "elastic",
}
`);
- expect(savedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(`
+ expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(`
Object {
"references": Array [
Object {
@@ -2411,7 +3224,7 @@ describe('update()', () => {
}),
},
async executor() {},
- producer: 'alerting',
+ producer: 'alerts',
});
await expect(
alertsClient.update({
@@ -2442,19 +3255,20 @@ describe('update()', () => {
it('swallows error when invalidate API key throws', async () => {
alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail'));
- savedObjectsClient.bulkGet.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({
saved_objects: [
{
id: '1',
type: 'action',
attributes: {
+ actions: [],
actionTypeId: 'test',
},
references: [],
},
],
});
- savedObjectsClient.update.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.update.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
@@ -2511,12 +3325,13 @@ describe('update()', () => {
it('swallows error when getDecryptedAsInternalUser throws', async () => {
encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail'));
- savedObjectsClient.bulkGet.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({
saved_objects: [
{
id: '1',
type: 'action',
attributes: {
+ actions: [],
actionTypeId: 'test',
},
references: [],
@@ -2525,13 +3340,14 @@ describe('update()', () => {
id: '2',
type: 'action',
attributes: {
+ actions: [],
actionTypeId: 'test2',
},
references: [],
},
],
});
- savedObjectsClient.update.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.update.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
@@ -2623,7 +3439,7 @@ describe('update()', () => {
],
},
});
- expect(savedObjectsClient.get).toHaveBeenCalledWith('alert', '1');
+ expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1');
expect(alertsClientParams.logger.error).toHaveBeenCalledWith(
'update(): Failed to load API key to invalidate on alert 1: Fail'
);
@@ -2643,14 +3459,15 @@ describe('update()', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
async executor() {},
- producer: 'alerting',
+ producer: 'alerts',
});
- savedObjectsClient.bulkGet.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({
saved_objects: [
{
id: '1',
type: 'action',
attributes: {
+ actions: [],
actionTypeId: 'test',
},
references: [],
@@ -2661,6 +3478,7 @@ describe('update()', () => {
id: alertId,
type: 'alert',
attributes: {
+ actions: [],
enabled: true,
alertTypeId: '123',
schedule: currentSchedule,
@@ -2683,7 +3501,7 @@ describe('update()', () => {
params: {},
ownerId: null,
});
- savedObjectsClient.update.mockResolvedValueOnce({
+ unsecuredSavedObjectsClient.update.mockResolvedValueOnce({
id: alertId,
type: 'alert',
attributes: {
@@ -2852,6 +3670,73 @@ describe('update()', () => {
);
});
});
+
+ describe('authorization', () => {
+ beforeEach(() => {
+ unsecuredSavedObjectsClient.update.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ alertTypeId: 'myType',
+ consumer: 'myApp',
+ enabled: true,
+ schedule: { interval: '10s' },
+ params: {
+ bar: true,
+ },
+ actions: [],
+ scheduledTaskId: 'task-123',
+ createdAt: new Date().toISOString(),
+ },
+ updated_at: new Date().toISOString(),
+ references: [],
+ });
+ });
+
+ test('ensures user is authorised to update this type of alert under the consumer', async () => {
+ await alertsClient.update({
+ id: '1',
+ data: {
+ schedule: { interval: '10s' },
+ name: 'abc',
+ tags: ['foo'],
+ params: {
+ bar: true,
+ },
+ throttle: null,
+ actions: [],
+ },
+ });
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'update');
+ });
+
+ test('throws when user is not authorised to update this type of alert', async () => {
+ authorization.ensureAuthorized.mockRejectedValue(
+ new Error(`Unauthorized to update a "myType" alert for "myApp"`)
+ );
+
+ await expect(
+ alertsClient.update({
+ id: '1',
+ data: {
+ schedule: { interval: '10s' },
+ name: 'abc',
+ tags: ['foo'],
+ params: {
+ bar: true,
+ },
+ throttle: null,
+ actions: [],
+ },
+ })
+ ).rejects.toMatchInlineSnapshot(
+ `[Error: Unauthorized to update a "myType" alert for "myApp"]`
+ );
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'update');
+ });
+ });
});
describe('updateApiKey()', () => {
@@ -2861,8 +3746,20 @@ describe('updateApiKey()', () => {
type: 'alert',
attributes: {
schedule: { interval: '10s' },
- alertTypeId: '2',
+ alertTypeId: 'myType',
+ consumer: 'myApp',
enabled: true,
+ actions: [
+ {
+ group: 'default',
+ id: '1',
+ actionTypeId: '1',
+ actionRef: '1',
+ params: {
+ foo: true,
+ },
+ },
+ ],
},
version: '123',
references: [],
@@ -2877,30 +3774,42 @@ describe('updateApiKey()', () => {
beforeEach(() => {
alertsClient = new AlertsClient(alertsClientParams);
- savedObjectsClient.get.mockResolvedValue(existingAlert);
+ unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert);
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingEncryptedAlert);
alertsClientParams.createAPIKey.mockResolvedValueOnce({
apiKeysEnabled: true,
- result: { id: '234', api_key: 'abc' },
+ result: { id: '234', name: '123', api_key: 'abc' },
});
});
test('updates the API key for the alert', async () => {
await alertsClient.updateApiKey({ id: '1' });
- expect(savedObjectsClient.get).not.toHaveBeenCalled();
+ expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled();
expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', {
namespace: 'default',
});
- expect(savedObjectsClient.update).toHaveBeenCalledWith(
+ expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith(
'alert',
'1',
{
schedule: { interval: '10s' },
- alertTypeId: '2',
+ alertTypeId: 'myType',
+ consumer: 'myApp',
enabled: true,
apiKey: Buffer.from('234:abc').toString('base64'),
apiKeyOwner: 'elastic',
updatedBy: 'elastic',
+ actions: [
+ {
+ group: 'default',
+ id: '1',
+ actionTypeId: '1',
+ actionRef: '1',
+ params: {
+ foo: true,
+ },
+ },
+ ],
},
{ version: '123' }
);
@@ -2911,20 +3820,32 @@ describe('updateApiKey()', () => {
encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail'));
await alertsClient.updateApiKey({ id: '1' });
- expect(savedObjectsClient.get).toHaveBeenCalledWith('alert', '1');
+ expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1');
expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', {
namespace: 'default',
});
- expect(savedObjectsClient.update).toHaveBeenCalledWith(
+ expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith(
'alert',
'1',
{
schedule: { interval: '10s' },
- alertTypeId: '2',
+ alertTypeId: 'myType',
+ consumer: 'myApp',
enabled: true,
apiKey: Buffer.from('234:abc').toString('base64'),
apiKeyOwner: 'elastic',
updatedBy: 'elastic',
+ actions: [
+ {
+ group: 'default',
+ id: '1',
+ actionTypeId: '1',
+ actionRef: '1',
+ params: {
+ foo: true,
+ },
+ },
+ ],
},
{ version: '123' }
);
@@ -2938,7 +3859,7 @@ describe('updateApiKey()', () => {
expect(alertsClientParams.logger.error).toHaveBeenCalledWith(
'Failed to invalidate API Key: Fail'
);
- expect(savedObjectsClient.update).toHaveBeenCalled();
+ expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled();
});
test('swallows error when getting decrypted object throws', async () => {
@@ -2948,16 +3869,133 @@ describe('updateApiKey()', () => {
expect(alertsClientParams.logger.error).toHaveBeenCalledWith(
'updateApiKey(): Failed to load API key to invalidate on alert 1: Fail'
);
- expect(savedObjectsClient.update).toHaveBeenCalled();
+ expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled();
expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled();
});
- test('throws when savedObjectsClient update fails', async () => {
- savedObjectsClient.update.mockRejectedValueOnce(new Error('Fail'));
+ test('throws when unsecuredSavedObjectsClient update fails', async () => {
+ unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Fail'));
await expect(alertsClient.updateApiKey({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot(
`"Fail"`
);
expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled();
});
+
+ describe('authorization', () => {
+ test('ensures user is authorised to updateApiKey this type of alert under the consumer', async () => {
+ await alertsClient.updateApiKey({ id: '1' });
+
+ expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute');
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith(
+ 'myType',
+ 'myApp',
+ 'updateApiKey'
+ );
+ });
+
+ test('throws when user is not authorised to updateApiKey this type of alert', async () => {
+ authorization.ensureAuthorized.mockRejectedValue(
+ new Error(`Unauthorized to updateApiKey a "myType" alert for "myApp"`)
+ );
+
+ await expect(alertsClient.updateApiKey({ id: '1' })).rejects.toMatchInlineSnapshot(
+ `[Error: Unauthorized to updateApiKey a "myType" alert for "myApp"]`
+ );
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith(
+ 'myType',
+ 'myApp',
+ 'updateApiKey'
+ );
+ });
+ });
+});
+
+describe('listAlertTypes', () => {
+ let alertsClient: AlertsClient;
+ const alertingAlertType = {
+ actionGroups: [],
+ actionVariables: undefined,
+ defaultActionGroupId: 'default',
+ id: 'alertingAlertType',
+ name: 'alertingAlertType',
+ producer: 'alerts',
+ };
+ const myAppAlertType = {
+ actionGroups: [],
+ actionVariables: undefined,
+ defaultActionGroupId: 'default',
+ id: 'myAppAlertType',
+ name: 'myAppAlertType',
+ producer: 'myApp',
+ };
+ const setOfAlertTypes = new Set([myAppAlertType, alertingAlertType]);
+
+ const authorizedConsumers = {
+ alerts: { read: true, all: true },
+ myApp: { read: true, all: true },
+ myOtherApp: { read: true, all: true },
+ };
+
+ beforeEach(() => {
+ alertsClient = new AlertsClient(alertsClientParams);
+ });
+
+ test('should return a list of AlertTypes that exist in the registry', async () => {
+ alertTypeRegistry.list.mockReturnValue(setOfAlertTypes);
+ authorization.filterByAlertTypeAuthorization.mockResolvedValue(
+ new Set([
+ { ...myAppAlertType, authorizedConsumers },
+ { ...alertingAlertType, authorizedConsumers },
+ ])
+ );
+ expect(await alertsClient.listAlertTypes()).toEqual(
+ new Set([
+ { ...myAppAlertType, authorizedConsumers },
+ { ...alertingAlertType, authorizedConsumers },
+ ])
+ );
+ });
+
+ describe('authorization', () => {
+ const listedTypes = new Set([
+ {
+ actionGroups: [],
+ actionVariables: undefined,
+ defaultActionGroupId: 'default',
+ id: 'myType',
+ name: 'myType',
+ producer: 'myApp',
+ },
+ {
+ id: 'myOtherType',
+ name: 'Test',
+ actionGroups: [{ id: 'default', name: 'Default' }],
+ defaultActionGroupId: 'default',
+ producer: 'alerts',
+ },
+ ]);
+ beforeEach(() => {
+ alertTypeRegistry.list.mockReturnValue(listedTypes);
+ });
+
+ test('should return a list of AlertTypes that exist in the registry only if the user is authorised to get them', async () => {
+ const authorizedTypes = new Set([
+ {
+ id: 'myType',
+ name: 'Test',
+ actionGroups: [{ id: 'default', name: 'Default' }],
+ defaultActionGroupId: 'default',
+ producer: 'alerts',
+ authorizedConsumers: {
+ myApp: { read: true, all: true },
+ },
+ },
+ ]);
+ authorization.filterByAlertTypeAuthorization.mockResolvedValue(authorizedTypes);
+
+ expect(await alertsClient.listAlertTypes()).toEqual(authorizedTypes);
+ });
+ });
});
diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts
index e49745b186bb3..eec60f924bf38 100644
--- a/x-pack/plugins/alerts/server/alerts_client.ts
+++ b/x-pack/plugins/alerts/server/alerts_client.ts
@@ -5,7 +5,7 @@
*/
import Boom from 'boom';
-import { omit, isEqual, map, truncate } from 'lodash';
+import { omit, isEqual, map, uniq, pick, truncate } from 'lodash';
import { i18n } from '@kbn/i18n';
import {
Logger,
@@ -13,7 +13,7 @@ import {
SavedObjectReference,
SavedObject,
} from 'src/core/server';
-import { ActionsClient } from '../../actions/server';
+import { ActionsClient, ActionsAuthorization } from '../../actions/server';
import {
Alert,
PartialAlert,
@@ -35,7 +35,16 @@ import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/serve
import { TaskManagerStartContract } from '../../task_manager/server';
import { taskInstanceToAlertTaskInstance } from './task_runner/alert_task_instance';
import { deleteTaskIfItExists } from './lib/delete_task_if_it_exists';
+import { RegistryAlertType } from './alert_type_registry';
+import {
+ AlertsAuthorization,
+ WriteOperations,
+ ReadOperations,
+} from './authorization/alerts_authorization';
+export interface RegistryAlertTypeWithAuth extends RegistryAlertType {
+ authorizedConsumers: string[];
+}
type NormalizedAlertAction = Omit;
export type CreateAPIKeyResult =
| { apiKeysEnabled: false }
@@ -44,10 +53,12 @@ export type InvalidateAPIKeyResult =
| { apiKeysEnabled: false }
| { apiKeysEnabled: true; result: SecurityPluginInvalidateAPIKeyResult };
-interface ConstructorOptions {
+export interface ConstructorOptions {
logger: Logger;
taskManager: TaskManagerStartContract;
- savedObjectsClient: SavedObjectsClientContract;
+ unsecuredSavedObjectsClient: SavedObjectsClientContract;
+ authorization: AlertsAuthorization;
+ actionsAuthorization: ActionsAuthorization;
alertTypeRegistry: AlertTypeRegistry;
encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
spaceId?: string;
@@ -127,18 +138,21 @@ export class AlertsClient {
private readonly spaceId?: string;
private readonly namespace?: string;
private readonly taskManager: TaskManagerStartContract;
- private readonly savedObjectsClient: SavedObjectsClientContract;
+ private readonly unsecuredSavedObjectsClient: SavedObjectsClientContract;
+ private readonly authorization: AlertsAuthorization;
private readonly alertTypeRegistry: AlertTypeRegistry;
private readonly createAPIKey: (name: string) => Promise;
private readonly invalidateAPIKey: (
params: InvalidateAPIKeyParams
) => Promise;
private readonly getActionsClient: () => Promise;
+ private readonly actionsAuthorization: ActionsAuthorization;
encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
constructor({
alertTypeRegistry,
- savedObjectsClient,
+ unsecuredSavedObjectsClient,
+ authorization,
taskManager,
logger,
spaceId,
@@ -148,6 +162,7 @@ export class AlertsClient {
invalidateAPIKey,
encryptedSavedObjectsClient,
getActionsClient,
+ actionsAuthorization,
}: ConstructorOptions) {
this.logger = logger;
this.getUserName = getUserName;
@@ -155,16 +170,25 @@ export class AlertsClient {
this.namespace = namespace;
this.taskManager = taskManager;
this.alertTypeRegistry = alertTypeRegistry;
- this.savedObjectsClient = savedObjectsClient;
+ this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient;
+ this.authorization = authorization;
this.createAPIKey = createAPIKey;
this.invalidateAPIKey = invalidateAPIKey;
this.encryptedSavedObjectsClient = encryptedSavedObjectsClient;
this.getActionsClient = getActionsClient;
+ this.actionsAuthorization = actionsAuthorization;
}
public async create({ data, options }: CreateOptions): Promise {
+ await this.authorization.ensureAuthorized(
+ data.alertTypeId,
+ data.consumer,
+ WriteOperations.Create
+ );
+
// Throws an error if alert type isn't registered
const alertType = this.alertTypeRegistry.get(data.alertTypeId);
+
const validatedAlertTypeParams = validateAlertTypeParams(alertType, data.params);
const username = await this.getUserName();
@@ -186,7 +210,7 @@ export class AlertsClient {
muteAll: false,
mutedInstanceIds: [],
};
- const createdAlert = await this.savedObjectsClient.create('alert', rawAlert, {
+ const createdAlert = await this.unsecuredSavedObjectsClient.create('alert', rawAlert, {
...options,
references,
});
@@ -197,7 +221,7 @@ export class AlertsClient {
} catch (e) {
// Cleanup data, something went wrong scheduling the task
try {
- await this.savedObjectsClient.delete('alert', createdAlert.id);
+ await this.unsecuredSavedObjectsClient.delete('alert', createdAlert.id);
} catch (err) {
// Skip the cleanup error and throw the task manager error to avoid confusion
this.logger.error(
@@ -206,7 +230,7 @@ export class AlertsClient {
}
throw e;
}
- await this.savedObjectsClient.update('alert', createdAlert.id, {
+ await this.unsecuredSavedObjectsClient.update('alert', createdAlert.id, {
scheduledTaskId: scheduledTask.id,
});
createdAlert.attributes.scheduledTaskId = scheduledTask.id;
@@ -220,12 +244,22 @@ export class AlertsClient {
}
public async get({ id }: { id: string }): Promise {
- const result = await this.savedObjectsClient.get('alert', id);
+ const result = await this.unsecuredSavedObjectsClient.get('alert', id);
+ await this.authorization.ensureAuthorized(
+ result.attributes.alertTypeId,
+ result.attributes.consumer,
+ ReadOperations.Get
+ );
return this.getAlertFromRaw(result.id, result.attributes, result.updated_at, result.references);
}
public async getAlertState({ id }: { id: string }): Promise {
const alert = await this.get({ id });
+ await this.authorization.ensureAuthorized(
+ alert.alertTypeId,
+ alert.consumer,
+ ReadOperations.GetAlertState
+ );
if (alert.scheduledTaskId) {
const { state } = taskInstanceToAlertTaskInstance(
await this.taskManager.get(alert.scheduledTaskId),
@@ -235,30 +269,56 @@ export class AlertsClient {
}
}
- public async find({ options = {} }: { options: FindOptions }): Promise {
+ public async find({
+ options: { fields, ...options } = {},
+ }: { options?: FindOptions } = {}): Promise {
+ const {
+ filter: authorizationFilter,
+ ensureAlertTypeIsAuthorized,
+ logSuccessfulAuthorization,
+ } = await this.authorization.getFindAuthorizationFilter();
+
+ if (authorizationFilter) {
+ options.filter = options.filter
+ ? `${options.filter} and ${authorizationFilter}`
+ : authorizationFilter;
+ }
+
const {
page,
per_page: perPage,
total,
saved_objects: data,
- } = await this.savedObjectsClient.find({
+ } = await this.unsecuredSavedObjectsClient.find({
...options,
+ fields: fields ? this.includeFieldsRequiredForAuthentication(fields) : fields,
type: 'alert',
});
+ const authorizedData = data.map(({ id, attributes, updated_at, references }) => {
+ ensureAlertTypeIsAuthorized(attributes.alertTypeId, attributes.consumer);
+ return this.getAlertFromRaw(
+ id,
+ fields ? (pick(attributes, fields) as RawAlert) : attributes,
+ updated_at,
+ references
+ );
+ });
+
+ logSuccessfulAuthorization();
+
return {
page,
perPage,
total,
- data: data.map(({ id, attributes, updated_at, references }) =>
- this.getAlertFromRaw(id, attributes, updated_at, references)
- ),
+ data: authorizedData,
};
}
public async delete({ id }: { id: string }) {
let taskIdToRemove: string | undefined;
let apiKeyToInvalidate: string | null = null;
+ let attributes: RawAlert;
try {
const decryptedAlert = await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser<
@@ -266,17 +326,25 @@ export class AlertsClient {
>('alert', id, { namespace: this.namespace });
apiKeyToInvalidate = decryptedAlert.attributes.apiKey;
taskIdToRemove = decryptedAlert.attributes.scheduledTaskId;
+ attributes = decryptedAlert.attributes;
} catch (e) {
// We'll skip invalidating the API key since we failed to load the decrypted saved object
this.logger.error(
`delete(): Failed to load API key to invalidate on alert ${id}: ${e.message}`
);
// Still attempt to load the scheduledTaskId using SOC
- const alert = await this.savedObjectsClient.get('alert', id);
+ const alert = await this.unsecuredSavedObjectsClient.get('alert', id);
taskIdToRemove = alert.attributes.scheduledTaskId;
+ attributes = alert.attributes;
}
- const removeResult = await this.savedObjectsClient.delete('alert', id);
+ await this.authorization.ensureAuthorized(
+ attributes.alertTypeId,
+ attributes.consumer,
+ WriteOperations.Delete
+ );
+
+ const removeResult = await this.unsecuredSavedObjectsClient.delete('alert', id);
await Promise.all([
taskIdToRemove ? deleteTaskIfItExists(this.taskManager, taskIdToRemove) : null,
@@ -299,8 +367,13 @@ export class AlertsClient {
`update(): Failed to load API key to invalidate on alert ${id}: ${e.message}`
);
// Still attempt to load the object using SOC
- alertSavedObject = await this.savedObjectsClient.get('alert', id);
+ alertSavedObject = await this.unsecuredSavedObjectsClient.get('alert', id);
}
+ await this.authorization.ensureAuthorized(
+ alertSavedObject.attributes.alertTypeId,
+ alertSavedObject.attributes.consumer,
+ WriteOperations.Update
+ );
const updateResult = await this.updateAlert({ id, data }, alertSavedObject);
@@ -342,7 +415,7 @@ export class AlertsClient {
: null;
const apiKeyAttributes = this.apiKeyAsAlertAttributes(createdAPIKey, username);
- const updatedObject = await this.savedObjectsClient.update(
+ const updatedObject = await this.unsecuredSavedObjectsClient.update(
'alert',
id,
{
@@ -400,13 +473,22 @@ export class AlertsClient {
`updateApiKey(): Failed to load API key to invalidate on alert ${id}: ${e.message}`
);
// Still attempt to load the attributes and version using SOC
- const alert = await this.savedObjectsClient.get('alert', id);
+ const alert = await this.unsecuredSavedObjectsClient.get('alert', id);
attributes = alert.attributes;
version = alert.version;
}
+ await this.authorization.ensureAuthorized(
+ attributes.alertTypeId,
+ attributes.consumer,
+ WriteOperations.UpdateApiKey
+ );
+
+ if (attributes.actions.length) {
+ await this.actionsAuthorization.ensureAuthorized('execute');
+ }
const username = await this.getUserName();
- await this.savedObjectsClient.update(
+ await this.unsecuredSavedObjectsClient.update(
'alert',
id,
{
@@ -459,14 +541,24 @@ export class AlertsClient {
`enable(): Failed to load API key to invalidate on alert ${id}: ${e.message}`
);
// Still attempt to load the attributes and version using SOC
- const alert = await this.savedObjectsClient.get('alert', id);
+ const alert = await this.unsecuredSavedObjectsClient.get('alert', id);
attributes = alert.attributes;
version = alert.version;
}
+ await this.authorization.ensureAuthorized(
+ attributes.alertTypeId,
+ attributes.consumer,
+ WriteOperations.Enable
+ );
+
+ if (attributes.actions.length) {
+ await this.actionsAuthorization.ensureAuthorized('execute');
+ }
+
if (attributes.enabled === false) {
const username = await this.getUserName();
- await this.savedObjectsClient.update(
+ await this.unsecuredSavedObjectsClient.update(
'alert',
id,
{
@@ -483,7 +575,9 @@ export class AlertsClient {
{ version }
);
const scheduledTask = await this.scheduleAlert(id, attributes.alertTypeId);
- await this.savedObjectsClient.update('alert', id, { scheduledTaskId: scheduledTask.id });
+ await this.unsecuredSavedObjectsClient.update('alert', id, {
+ scheduledTaskId: scheduledTask.id,
+ });
if (apiKeyToInvalidate) {
await this.invalidateApiKey({ apiKey: apiKeyToInvalidate });
}
@@ -508,13 +602,19 @@ export class AlertsClient {
`disable(): Failed to load API key to invalidate on alert ${id}: ${e.message}`
);
// Still attempt to load the attributes and version using SOC
- const alert = await this.savedObjectsClient.get('alert', id);
+ const alert = await this.unsecuredSavedObjectsClient.get('alert', id);
attributes = alert.attributes;
version = alert.version;
}
+ await this.authorization.ensureAuthorized(
+ attributes.alertTypeId,
+ attributes.consumer,
+ WriteOperations.Disable
+ );
+
if (attributes.enabled === true) {
- await this.savedObjectsClient.update(
+ await this.unsecuredSavedObjectsClient.update(
'alert',
id,
{
@@ -538,7 +638,18 @@ export class AlertsClient {
}
public async muteAll({ id }: { id: string }) {
- await this.savedObjectsClient.update('alert', id, {
+ const { attributes } = await this.unsecuredSavedObjectsClient.get('alert', id);
+ await this.authorization.ensureAuthorized(
+ attributes.alertTypeId,
+ attributes.consumer,
+ WriteOperations.MuteAll
+ );
+
+ if (attributes.actions.length) {
+ await this.actionsAuthorization.ensureAuthorized('execute');
+ }
+
+ await this.unsecuredSavedObjectsClient.update('alert', id, {
muteAll: true,
mutedInstanceIds: [],
updatedBy: await this.getUserName(),
@@ -546,7 +657,18 @@ export class AlertsClient {
}
public async unmuteAll({ id }: { id: string }) {
- await this.savedObjectsClient.update('alert', id, {
+ const { attributes } = await this.unsecuredSavedObjectsClient.get('alert', id);
+ await this.authorization.ensureAuthorized(
+ attributes.alertTypeId,
+ attributes.consumer,
+ WriteOperations.UnmuteAll
+ );
+
+ if (attributes.actions.length) {
+ await this.actionsAuthorization.ensureAuthorized('execute');
+ }
+
+ await this.unsecuredSavedObjectsClient.update('alert', id, {
muteAll: false,
mutedInstanceIds: [],
updatedBy: await this.getUserName(),
@@ -554,11 +676,25 @@ export class AlertsClient {
}
public async muteInstance({ alertId, alertInstanceId }: MuteOptions) {
- const { attributes, version } = await this.savedObjectsClient.get('alert', alertId);
+ const { attributes, version } = await this.unsecuredSavedObjectsClient.get(
+ 'alert',
+ alertId
+ );
+
+ await this.authorization.ensureAuthorized(
+ attributes.alertTypeId,
+ attributes.consumer,
+ WriteOperations.MuteInstance
+ );
+
+ if (attributes.actions.length) {
+ await this.actionsAuthorization.ensureAuthorized('execute');
+ }
+
const mutedInstanceIds = attributes.mutedInstanceIds || [];
if (!attributes.muteAll && !mutedInstanceIds.includes(alertInstanceId)) {
mutedInstanceIds.push(alertInstanceId);
- await this.savedObjectsClient.update(
+ await this.unsecuredSavedObjectsClient.update(
'alert',
alertId,
{
@@ -577,10 +713,22 @@ export class AlertsClient {
alertId: string;
alertInstanceId: string;
}) {
- const { attributes, version } = await this.savedObjectsClient.get('alert', alertId);
+ const { attributes, version } = await this.unsecuredSavedObjectsClient.get(
+ 'alert',
+ alertId
+ );
+ await this.authorization.ensureAuthorized(
+ attributes.alertTypeId,
+ attributes.consumer,
+ WriteOperations.UnmuteInstance
+ );
+ if (attributes.actions.length) {
+ await this.actionsAuthorization.ensureAuthorized('execute');
+ }
+
const mutedInstanceIds = attributes.mutedInstanceIds || [];
if (!attributes.muteAll && mutedInstanceIds.includes(alertInstanceId)) {
- await this.savedObjectsClient.update(
+ await this.unsecuredSavedObjectsClient.update(
'alert',
alertId,
{
@@ -593,6 +741,13 @@ export class AlertsClient {
}
}
+ public async listAlertTypes() {
+ return await this.authorization.filterByAlertTypeAuthorization(this.alertTypeRegistry.list(), [
+ ReadOperations.Get,
+ WriteOperations.Create,
+ ]);
+ }
+
private async scheduleAlert(id: string, alertTypeId: string) {
return await this.taskManager.schedule({
taskType: `alerting:${alertTypeId}`,
@@ -610,13 +765,14 @@ export class AlertsClient {
}
private injectReferencesIntoActions(
+ alertId: string,
actions: RawAlert['actions'],
references: SavedObjectReference[]
) {
return actions.map((action) => {
const reference = references.find((ref) => ref.name === action.actionRef);
if (!reference) {
- throw new Error(`Reference ${action.actionRef} not found`);
+ throw new Error(`Action reference "${action.actionRef}" not found in alert id: ${alertId}`);
}
return {
...omit(action, 'actionRef'),
@@ -639,8 +795,8 @@ export class AlertsClient {
private getPartialAlertFromRaw(
id: string,
- rawAlert: Partial,
- updatedAt: SavedObject['updated_at'],
+ { createdAt, ...rawAlert }: Partial,
+ updatedAt: SavedObject['updated_at'] = createdAt,
references: SavedObjectReference[] | undefined
): PartialAlert {
return {
@@ -649,11 +805,11 @@ export class AlertsClient {
// we currently only support the Interval Schedule type
// Once we support additional types, this type signature will likely change
schedule: rawAlert.schedule as IntervalSchedule,
- updatedAt: updatedAt ? new Date(updatedAt) : new Date(rawAlert.createdAt!),
- createdAt: new Date(rawAlert.createdAt!),
actions: rawAlert.actions
- ? this.injectReferencesIntoActions(rawAlert.actions, references || [])
+ ? this.injectReferencesIntoActions(id, rawAlert.actions, references || [])
: [],
+ ...(updatedAt ? { updatedAt: new Date(updatedAt) } : {}),
+ ...(createdAt ? { createdAt: new Date(createdAt) } : {}),
};
}
@@ -679,38 +835,45 @@ export class AlertsClient {
private async denormalizeActions(
alertActions: NormalizedAlertAction[]
): Promise<{ actions: RawAlert['actions']; references: SavedObjectReference[] }> {
- const actionsClient = await this.getActionsClient();
- const actionIds = [...new Set(alertActions.map((alertAction) => alertAction.id))];
- const actionResults = await actionsClient.getBulk(actionIds);
const references: SavedObjectReference[] = [];
- const actions = alertActions.map(({ id, ...alertAction }, i) => {
- const actionResultValue = actionResults.find((action) => action.id === id);
- if (actionResultValue) {
- const actionRef = `action_${i}`;
- references.push({
- id,
- name: actionRef,
- type: 'action',
- });
- return {
- ...alertAction,
- actionRef,
- actionTypeId: actionResultValue.actionTypeId,
- };
- } else {
- return {
- ...alertAction,
- actionRef: '',
- actionTypeId: '',
- };
- }
- });
+ const actions: RawAlert['actions'] = [];
+ if (alertActions.length) {
+ const actionsClient = await this.getActionsClient();
+ const actionIds = [...new Set(alertActions.map((alertAction) => alertAction.id))];
+ const actionResults = await actionsClient.getBulk(actionIds);
+ alertActions.forEach(({ id, ...alertAction }, i) => {
+ const actionResultValue = actionResults.find((action) => action.id === id);
+ if (actionResultValue) {
+ const actionRef = `action_${i}`;
+ references.push({
+ id,
+ name: actionRef,
+ type: 'action',
+ });
+ actions.push({
+ ...alertAction,
+ actionRef,
+ actionTypeId: actionResultValue.actionTypeId,
+ });
+ } else {
+ actions.push({
+ ...alertAction,
+ actionRef: '',
+ actionTypeId: '',
+ });
+ }
+ });
+ }
return {
actions,
references,
};
}
+ private includeFieldsRequiredForAuthentication(fields: string[]): string[] {
+ return uniq([...fields, 'alertTypeId', 'consumer']);
+ }
+
private generateAPIKeyName(alertTypeId: string, alertName: string) {
return truncate(`Alerting: ${alertTypeId}/${alertName}`, { length: 256 });
}
diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts
index 128d54c10b66a..16b5af499bb90 100644
--- a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts
@@ -9,24 +9,39 @@ import { AlertsClientFactory, AlertsClientFactoryOpts } from './alerts_client_fa
import { alertTypeRegistryMock } from './alert_type_registry.mock';
import { taskManagerMock } from '../../task_manager/server/task_manager.mock';
import { KibanaRequest } from '../../../../src/core/server';
-import { loggingSystemMock, savedObjectsClientMock } from '../../../../src/core/server/mocks';
+import {
+ savedObjectsClientMock,
+ savedObjectsServiceMock,
+ loggingSystemMock,
+} from '../../../../src/core/server/mocks';
import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks';
import { AuthenticatedUser } from '../../../plugins/security/common/model';
import { securityMock } from '../../security/server/mocks';
-import { actionsMock } from '../../actions/server/mocks';
+import { PluginStartContract as ActionsStartContract } from '../../actions/server';
+import { actionsMock, actionsAuthorizationMock } from '../../actions/server/mocks';
+import { featuresPluginMock } from '../../features/server/mocks';
+import { AuditLogger } from '../../security/server';
+import { ALERTS_FEATURE_ID } from '../common';
jest.mock('./alerts_client');
+jest.mock('./authorization/alerts_authorization');
+jest.mock('./authorization/audit_logger');
const savedObjectsClient = savedObjectsClientMock.create();
+const savedObjectsService = savedObjectsServiceMock.createInternalStartContract();
+const features = featuresPluginMock.createStart();
+
const securityPluginSetup = securityMock.createSetup();
const alertsClientFactoryParams: jest.Mocked = {
logger: loggingSystemMock.create().get(),
taskManager: taskManagerMock.start(),
alertTypeRegistry: alertTypeRegistryMock.create(),
getSpaceId: jest.fn(),
+ getSpace: jest.fn(),
spaceIdToNamespace: jest.fn(),
encryptedSavedObjectsClient: encryptedSavedObjectsMock.createClient(),
actions: actionsMock.createStart(),
+ features,
};
const fakeRequest = ({
headers: {},
@@ -44,19 +59,101 @@ const fakeRequest = ({
getSavedObjectsClient: () => savedObjectsClient,
} as unknown) as Request;
+const actionsAuthorization = actionsAuthorizationMock.create();
+
beforeEach(() => {
jest.resetAllMocks();
+ alertsClientFactoryParams.actions = actionsMock.createStart();
+ (alertsClientFactoryParams.actions as jest.Mocked<
+ ActionsStartContract
+ >).getActionsAuthorizationWithRequest.mockReturnValue(actionsAuthorization);
alertsClientFactoryParams.getSpaceId.mockReturnValue('default');
alertsClientFactoryParams.spaceIdToNamespace.mockReturnValue('default');
});
+test('creates an alerts client with proper constructor arguments when security is enabled', async () => {
+ const factory = new AlertsClientFactory();
+ factory.initialize({ securityPluginSetup, ...alertsClientFactoryParams });
+ const request = KibanaRequest.from(fakeRequest);
+
+ const { AlertsAuthorizationAuditLogger } = jest.requireMock('./authorization/audit_logger');
+ savedObjectsService.getScopedClient.mockReturnValue(savedObjectsClient);
+
+ const logger = {
+ log: jest.fn(),
+ } as jest.Mocked;
+ securityPluginSetup.audit.getLogger.mockReturnValue(logger);
+
+ factory.create(request, savedObjectsService);
+
+ expect(savedObjectsService.getScopedClient).toHaveBeenCalledWith(request, {
+ excludedWrappers: ['security'],
+ includedHiddenTypes: ['alert'],
+ });
+
+ const { AlertsAuthorization } = jest.requireMock('./authorization/alerts_authorization');
+ expect(AlertsAuthorization).toHaveBeenCalledWith({
+ request,
+ authorization: securityPluginSetup.authz,
+ alertTypeRegistry: alertsClientFactoryParams.alertTypeRegistry,
+ features: alertsClientFactoryParams.features,
+ auditLogger: expect.any(AlertsAuthorizationAuditLogger),
+ getSpace: expect.any(Function),
+ });
+
+ expect(AlertsAuthorizationAuditLogger).toHaveBeenCalledWith(logger);
+ expect(securityPluginSetup.audit.getLogger).toHaveBeenCalledWith(ALERTS_FEATURE_ID);
+
+ expect(alertsClientFactoryParams.actions.getActionsAuthorizationWithRequest).toHaveBeenCalledWith(
+ request
+ );
+
+ expect(jest.requireMock('./alerts_client').AlertsClient).toHaveBeenCalledWith({
+ unsecuredSavedObjectsClient: savedObjectsClient,
+ authorization: expect.any(AlertsAuthorization),
+ actionsAuthorization,
+ logger: alertsClientFactoryParams.logger,
+ taskManager: alertsClientFactoryParams.taskManager,
+ alertTypeRegistry: alertsClientFactoryParams.alertTypeRegistry,
+ spaceId: 'default',
+ namespace: 'default',
+ getUserName: expect.any(Function),
+ getActionsClient: expect.any(Function),
+ createAPIKey: expect.any(Function),
+ invalidateAPIKey: expect.any(Function),
+ encryptedSavedObjectsClient: alertsClientFactoryParams.encryptedSavedObjectsClient,
+ });
+});
+
test('creates an alerts client with proper constructor arguments', async () => {
const factory = new AlertsClientFactory();
factory.initialize(alertsClientFactoryParams);
- factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient);
+ const request = KibanaRequest.from(fakeRequest);
+
+ savedObjectsService.getScopedClient.mockReturnValue(savedObjectsClient);
+
+ factory.create(request, savedObjectsService);
+
+ expect(savedObjectsService.getScopedClient).toHaveBeenCalledWith(request, {
+ excludedWrappers: ['security'],
+ includedHiddenTypes: ['alert'],
+ });
+
+ const { AlertsAuthorization } = jest.requireMock('./authorization/alerts_authorization');
+ const { AlertsAuthorizationAuditLogger } = jest.requireMock('./authorization/audit_logger');
+ expect(AlertsAuthorization).toHaveBeenCalledWith({
+ request,
+ authorization: undefined,
+ alertTypeRegistry: alertsClientFactoryParams.alertTypeRegistry,
+ features: alertsClientFactoryParams.features,
+ auditLogger: expect.any(AlertsAuthorizationAuditLogger),
+ getSpace: expect.any(Function),
+ });
expect(jest.requireMock('./alerts_client').AlertsClient).toHaveBeenCalledWith({
- savedObjectsClient,
+ unsecuredSavedObjectsClient: savedObjectsClient,
+ authorization: expect.any(AlertsAuthorization),
+ actionsAuthorization,
logger: alertsClientFactoryParams.logger,
taskManager: alertsClientFactoryParams.taskManager,
alertTypeRegistry: alertsClientFactoryParams.alertTypeRegistry,
@@ -73,7 +170,7 @@ test('creates an alerts client with proper constructor arguments', async () => {
test('getUserName() returns null when security is disabled', async () => {
const factory = new AlertsClientFactory();
factory.initialize(alertsClientFactoryParams);
- factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient);
+ factory.create(KibanaRequest.from(fakeRequest), savedObjectsService);
const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0];
const userNameResult = await constructorCall.getUserName();
@@ -86,7 +183,7 @@ test('getUserName() returns a name when security is enabled', async () => {
...alertsClientFactoryParams,
securityPluginSetup,
});
- factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient);
+ factory.create(KibanaRequest.from(fakeRequest), savedObjectsService);
const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0];
securityPluginSetup.authc.getCurrentUser.mockReturnValueOnce(({
@@ -99,7 +196,7 @@ test('getUserName() returns a name when security is enabled', async () => {
test('getActionsClient() returns ActionsClient', async () => {
const factory = new AlertsClientFactory();
factory.initialize(alertsClientFactoryParams);
- factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient);
+ factory.create(KibanaRequest.from(fakeRequest), savedObjectsService);
const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0];
const actionsClient = await constructorCall.getActionsClient();
@@ -109,7 +206,7 @@ test('getActionsClient() returns ActionsClient', async () => {
test('createAPIKey() returns { apiKeysEnabled: false } when security is disabled', async () => {
const factory = new AlertsClientFactory();
factory.initialize(alertsClientFactoryParams);
- factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient);
+ factory.create(KibanaRequest.from(fakeRequest), savedObjectsService);
const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0];
const createAPIKeyResult = await constructorCall.createAPIKey();
@@ -119,7 +216,7 @@ test('createAPIKey() returns { apiKeysEnabled: false } when security is disabled
test('createAPIKey() returns { apiKeysEnabled: false } when security is enabled but ES security is disabled', async () => {
const factory = new AlertsClientFactory();
factory.initialize(alertsClientFactoryParams);
- factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient);
+ factory.create(KibanaRequest.from(fakeRequest), savedObjectsService);
const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0];
securityPluginSetup.authc.grantAPIKeyAsInternalUser.mockResolvedValueOnce(null);
@@ -133,7 +230,7 @@ test('createAPIKey() returns an API key when security is enabled', async () => {
...alertsClientFactoryParams,
securityPluginSetup,
});
- factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient);
+ factory.create(KibanaRequest.from(fakeRequest), savedObjectsService);
const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0];
securityPluginSetup.authc.grantAPIKeyAsInternalUser.mockResolvedValueOnce({
@@ -154,7 +251,7 @@ test('createAPIKey() throws when security plugin createAPIKey throws an error',
...alertsClientFactoryParams,
securityPluginSetup,
});
- factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient);
+ factory.create(KibanaRequest.from(fakeRequest), savedObjectsService);
const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0];
securityPluginSetup.authc.grantAPIKeyAsInternalUser.mockRejectedValueOnce(
diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.ts b/x-pack/plugins/alerts/server/alerts_client_factory.ts
index 30fcd1b949f2b..79b0ccaf1f0bc 100644
--- a/x-pack/plugins/alerts/server/alerts_client_factory.ts
+++ b/x-pack/plugins/alerts/server/alerts_client_factory.ts
@@ -6,11 +6,16 @@
import { PluginStartContract as ActionsPluginStartContract } from '../../actions/server';
import { AlertsClient } from './alerts_client';
+import { ALERTS_FEATURE_ID } from '../common';
import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from './types';
-import { KibanaRequest, Logger, SavedObjectsClientContract } from '../../../../src/core/server';
+import { KibanaRequest, Logger, SavedObjectsServiceStart } from '../../../../src/core/server';
import { InvalidateAPIKeyParams, SecurityPluginSetup } from '../../security/server';
import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/server';
import { TaskManagerStartContract } from '../../task_manager/server';
+import { PluginStartContract as FeaturesPluginStart } from '../../features/server';
+import { AlertsAuthorization } from './authorization/alerts_authorization';
+import { AlertsAuthorizationAuditLogger } from './authorization/audit_logger';
+import { Space } from '../../spaces/server';
export interface AlertsClientFactoryOpts {
logger: Logger;
@@ -18,9 +23,11 @@ export interface AlertsClientFactoryOpts {
alertTypeRegistry: AlertTypeRegistry;
securityPluginSetup?: SecurityPluginSetup;
getSpaceId: (request: KibanaRequest) => string | undefined;
+ getSpace: (request: KibanaRequest) => Promise;
spaceIdToNamespace: SpaceIdToNamespaceFunction;
encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
actions: ActionsPluginStartContract;
+ features: FeaturesPluginStart;
}
export class AlertsClientFactory {
@@ -30,9 +37,11 @@ export class AlertsClientFactory {
private alertTypeRegistry!: AlertTypeRegistry;
private securityPluginSetup?: SecurityPluginSetup;
private getSpaceId!: (request: KibanaRequest) => string | undefined;
+ private getSpace!: (request: KibanaRequest) => Promise;
private spaceIdToNamespace!: SpaceIdToNamespaceFunction;
private encryptedSavedObjectsClient!: EncryptedSavedObjectsClient;
private actions!: ActionsPluginStartContract;
+ private features!: FeaturesPluginStart;
public initialize(options: AlertsClientFactoryOpts) {
if (this.isInitialized) {
@@ -41,26 +50,41 @@ export class AlertsClientFactory {
this.isInitialized = true;
this.logger = options.logger;
this.getSpaceId = options.getSpaceId;
+ this.getSpace = options.getSpace;
this.taskManager = options.taskManager;
this.alertTypeRegistry = options.alertTypeRegistry;
this.securityPluginSetup = options.securityPluginSetup;
this.spaceIdToNamespace = options.spaceIdToNamespace;
this.encryptedSavedObjectsClient = options.encryptedSavedObjectsClient;
this.actions = options.actions;
+ this.features = options.features;
}
- public create(
- request: KibanaRequest,
- savedObjectsClient: SavedObjectsClientContract
- ): AlertsClient {
- const { securityPluginSetup, actions } = this;
+ public create(request: KibanaRequest, savedObjects: SavedObjectsServiceStart): AlertsClient {
+ const { securityPluginSetup, actions, features } = this;
const spaceId = this.getSpaceId(request);
+ const authorization = new AlertsAuthorization({
+ authorization: securityPluginSetup?.authz,
+ request,
+ getSpace: this.getSpace,
+ alertTypeRegistry: this.alertTypeRegistry,
+ features: features!,
+ auditLogger: new AlertsAuthorizationAuditLogger(
+ securityPluginSetup?.audit.getLogger(ALERTS_FEATURE_ID)
+ ),
+ });
+
return new AlertsClient({
spaceId,
logger: this.logger,
taskManager: this.taskManager,
alertTypeRegistry: this.alertTypeRegistry,
- savedObjectsClient,
+ unsecuredSavedObjectsClient: savedObjects.getScopedClient(request, {
+ excludedWrappers: ['security'],
+ includedHiddenTypes: ['alert'],
+ }),
+ authorization,
+ actionsAuthorization: actions.getActionsAuthorizationWithRequest(request),
namespace: this.spaceIdToNamespace(spaceId),
encryptedSavedObjectsClient: this.encryptedSavedObjectsClient,
async getUserName() {
diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.mock.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.mock.ts
new file mode 100644
index 0000000000000..d7705f834ad41
--- /dev/null
+++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.mock.ts
@@ -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 { AlertsAuthorization } from './alerts_authorization';
+
+type Schema = PublicMethodsOf;
+export type AlertsAuthorizationMock = jest.Mocked;
+
+const createAlertsAuthorizationMock = () => {
+ const mocked: AlertsAuthorizationMock = {
+ ensureAuthorized: jest.fn(),
+ filterByAlertTypeAuthorization: jest.fn(),
+ getFindAuthorizationFilter: jest.fn(),
+ };
+ return mocked;
+};
+
+export const alertsAuthorizationMock: {
+ create: () => AlertsAuthorizationMock;
+} = {
+ create: createAlertsAuthorizationMock,
+};
diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts
new file mode 100644
index 0000000000000..b164d27ded648
--- /dev/null
+++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts
@@ -0,0 +1,1256 @@
+/*
+ * 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 { KibanaRequest } from 'kibana/server';
+import { alertTypeRegistryMock } from '../alert_type_registry.mock';
+import { securityMock } from '../../../../plugins/security/server/mocks';
+import { PluginStartContract as FeaturesStartContract, Feature } from '../../../features/server';
+import { featuresPluginMock } from '../../../features/server/mocks';
+import {
+ AlertsAuthorization,
+ ensureFieldIsSafeForQuery,
+ WriteOperations,
+ ReadOperations,
+} from './alerts_authorization';
+import { alertsAuthorizationAuditLoggerMock } from './audit_logger.mock';
+import { AlertsAuthorizationAuditLogger, AuthorizationResult } from './audit_logger';
+import uuid from 'uuid';
+
+const alertTypeRegistry = alertTypeRegistryMock.create();
+const features: jest.Mocked = featuresPluginMock.createStart();
+const request = {} as KibanaRequest;
+
+const auditLogger = alertsAuthorizationAuditLoggerMock.create();
+const realAuditLogger = new AlertsAuthorizationAuditLogger();
+
+const getSpace = jest.fn();
+
+const mockAuthorizationAction = (type: string, app: string, operation: string) =>
+ `${type}/${app}/${operation}`;
+function mockSecurity() {
+ const security = securityMock.createSetup();
+ const authorization = security.authz;
+ // typescript is having trouble inferring jest's automocking
+ (authorization.actions.alerting.get as jest.MockedFunction<
+ typeof authorization.actions.alerting.get
+ >).mockImplementation(mockAuthorizationAction);
+ authorization.mode.useRbacForRequest.mockReturnValue(true);
+ return { authorization };
+}
+
+function mockFeature(appName: string, typeName?: string) {
+ return new Feature({
+ id: appName,
+ name: appName,
+ app: [],
+ ...(typeName
+ ? {
+ alerting: [typeName],
+ }
+ : {}),
+ privileges: {
+ all: {
+ ...(typeName
+ ? {
+ alerting: {
+ all: [typeName],
+ },
+ }
+ : {}),
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ ui: [],
+ },
+ read: {
+ ...(typeName
+ ? {
+ alerting: {
+ read: [typeName],
+ },
+ }
+ : {}),
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ ui: [],
+ },
+ },
+ });
+}
+
+function mockFeatureWithSubFeature(appName: string, typeName: string) {
+ return new Feature({
+ id: appName,
+ name: appName,
+ app: [],
+ ...(typeName
+ ? {
+ alerting: [typeName],
+ }
+ : {}),
+ privileges: {
+ all: {
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ ui: [],
+ },
+ read: {
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ ui: [],
+ },
+ },
+ subFeatures: [
+ {
+ name: appName,
+ privilegeGroups: [
+ {
+ groupType: 'independent',
+ privileges: [
+ {
+ id: 'doSomethingAlertRelated',
+ name: 'sub feature alert',
+ includeIn: 'all',
+ alerting: {
+ all: [typeName],
+ },
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ ui: ['doSomethingAlertRelated'],
+ },
+ {
+ id: 'doSomethingAlertRelated',
+ name: 'sub feature alert',
+ includeIn: 'read',
+ alerting: {
+ read: [typeName],
+ },
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ ui: ['doSomethingAlertRelated'],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ });
+}
+
+const myAppFeature = mockFeature('myApp', 'myType');
+const myOtherAppFeature = mockFeature('myOtherApp', 'myType');
+const myAppWithSubFeature = mockFeatureWithSubFeature('myAppWithSubFeature', 'myType');
+const myFeatureWithoutAlerting = mockFeature('myOtherApp');
+
+beforeEach(() => {
+ jest.resetAllMocks();
+ auditLogger.alertsAuthorizationFailure.mockImplementation((username, ...args) =>
+ realAuditLogger.getAuthorizationMessage(AuthorizationResult.Unauthorized, ...args)
+ );
+ auditLogger.alertsAuthorizationSuccess.mockImplementation((username, ...args) =>
+ realAuditLogger.getAuthorizationMessage(AuthorizationResult.Authorized, ...args)
+ );
+ auditLogger.alertsUnscopedAuthorizationFailure.mockImplementation(
+ (username, operation) => `Unauthorized ${username}/${operation}`
+ );
+ alertTypeRegistry.get.mockImplementation((id) => ({
+ id,
+ name: 'My Alert Type',
+ actionGroups: [{ id: 'default', name: 'Default' }],
+ defaultActionGroupId: 'default',
+ async executor() {},
+ producer: 'myApp',
+ }));
+ features.getFeatures.mockReturnValue([
+ myAppFeature,
+ myOtherAppFeature,
+ myAppWithSubFeature,
+ myFeatureWithoutAlerting,
+ ]);
+ getSpace.mockResolvedValue(undefined);
+});
+
+describe('AlertsAuthorization', () => {
+ describe('constructor', () => {
+ test(`fetches the user's current space`, async () => {
+ const space = {
+ id: uuid.v4(),
+ name: uuid.v4(),
+ disabledFeatures: [],
+ };
+ getSpace.mockResolvedValue(space);
+
+ new AlertsAuthorization({
+ request,
+ alertTypeRegistry,
+ features,
+ auditLogger,
+ getSpace,
+ });
+
+ expect(getSpace).toHaveBeenCalledWith(request);
+ });
+ });
+
+ describe('ensureAuthorized', () => {
+ test('is a no-op when there is no authorization api', async () => {
+ const alertAuthorization = new AlertsAuthorization({
+ request,
+ alertTypeRegistry,
+ features,
+ auditLogger,
+ getSpace,
+ });
+
+ await alertAuthorization.ensureAuthorized('myType', 'myApp', WriteOperations.Create);
+
+ expect(alertTypeRegistry.get).toHaveBeenCalledTimes(0);
+ });
+
+ test('is a no-op when the security license is disabled', async () => {
+ const { authorization } = mockSecurity();
+ authorization.mode.useRbacForRequest.mockReturnValue(false);
+ const alertAuthorization = new AlertsAuthorization({
+ request,
+ alertTypeRegistry,
+ authorization,
+ features,
+ auditLogger,
+ getSpace,
+ });
+
+ await alertAuthorization.ensureAuthorized('myType', 'myApp', WriteOperations.Create);
+
+ expect(alertTypeRegistry.get).toHaveBeenCalledTimes(0);
+ });
+
+ test('ensures the user has privileges to execute the specified type, operation and consumer', async () => {
+ const { authorization } = mockSecurity();
+ const checkPrivileges: jest.MockedFunction> = jest.fn();
+ authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges);
+ const alertAuthorization = new AlertsAuthorization({
+ request,
+ authorization,
+ alertTypeRegistry,
+ features,
+ auditLogger,
+ getSpace,
+ });
+
+ checkPrivileges.mockResolvedValueOnce({
+ username: 'some-user',
+ hasAllRequested: true,
+ privileges: [],
+ });
+
+ await alertAuthorization.ensureAuthorized('myType', 'myApp', WriteOperations.Create);
+
+ expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType');
+
+ expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'create');
+ expect(checkPrivileges).toHaveBeenCalledWith([
+ mockAuthorizationAction('myType', 'myApp', 'create'),
+ ]);
+
+ expect(auditLogger.alertsAuthorizationSuccess).toHaveBeenCalledTimes(1);
+ expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled();
+ expect(auditLogger.alertsAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ "some-user",
+ "myType",
+ 0,
+ "myApp",
+ "create",
+ ]
+ `);
+ });
+
+ test('ensures the user has privileges to execute the specified type and operation without consumer when consumer is alerts', async () => {
+ const { authorization } = mockSecurity();
+ const checkPrivileges: jest.MockedFunction> = jest.fn();
+ authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges);
+ const alertAuthorization = new AlertsAuthorization({
+ request,
+ authorization,
+ alertTypeRegistry,
+ features,
+ auditLogger,
+ getSpace,
+ });
+
+ checkPrivileges.mockResolvedValueOnce({
+ username: 'some-user',
+ hasAllRequested: true,
+ privileges: [],
+ });
+
+ await alertAuthorization.ensureAuthorized('myType', 'alerts', WriteOperations.Create);
+
+ expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType');
+
+ expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'create');
+ expect(checkPrivileges).toHaveBeenCalledWith([
+ mockAuthorizationAction('myType', 'myApp', 'create'),
+ ]);
+
+ expect(auditLogger.alertsAuthorizationSuccess).toHaveBeenCalledTimes(1);
+ expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled();
+ expect(auditLogger.alertsAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ "some-user",
+ "myType",
+ 0,
+ "alerts",
+ "create",
+ ]
+ `);
+ });
+
+ test('ensures the user has privileges to execute the specified type, operation and producer when producer is different from consumer', async () => {
+ const { authorization } = mockSecurity();
+ const checkPrivileges: jest.MockedFunction> = jest.fn();
+ authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges);
+ checkPrivileges.mockResolvedValueOnce({
+ username: 'some-user',
+ hasAllRequested: true,
+ privileges: [],
+ });
+
+ const alertAuthorization = new AlertsAuthorization({
+ request,
+ authorization,
+ alertTypeRegistry,
+ features,
+ auditLogger,
+ getSpace,
+ });
+
+ await alertAuthorization.ensureAuthorized('myType', 'myOtherApp', WriteOperations.Create);
+
+ expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType');
+
+ expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'create');
+ expect(authorization.actions.alerting.get).toHaveBeenCalledWith(
+ 'myType',
+ 'myOtherApp',
+ 'create'
+ );
+ expect(checkPrivileges).toHaveBeenCalledWith([
+ mockAuthorizationAction('myType', 'myOtherApp', 'create'),
+ mockAuthorizationAction('myType', 'myApp', 'create'),
+ ]);
+
+ expect(auditLogger.alertsAuthorizationSuccess).toHaveBeenCalledTimes(1);
+ expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled();
+ expect(auditLogger.alertsAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ "some-user",
+ "myType",
+ 0,
+ "myOtherApp",
+ "create",
+ ]
+ `);
+ });
+
+ test('throws if user lacks the required privieleges for the consumer', async () => {
+ const { authorization } = mockSecurity();
+ const checkPrivileges: jest.MockedFunction> = jest.fn();
+ authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges);
+ const alertAuthorization = new AlertsAuthorization({
+ request,
+ authorization,
+ alertTypeRegistry,
+ features,
+ auditLogger,
+ getSpace,
+ });
+
+ checkPrivileges.mockResolvedValueOnce({
+ username: 'some-user',
+ hasAllRequested: false,
+ privileges: [
+ {
+ privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'),
+ authorized: false,
+ },
+ {
+ privilege: mockAuthorizationAction('myType', 'myApp', 'create'),
+ authorized: true,
+ },
+ ],
+ });
+
+ await expect(
+ alertAuthorization.ensureAuthorized('myType', 'myOtherApp', WriteOperations.Create)
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
+ `"Unauthorized to create a \\"myType\\" alert for \\"myOtherApp\\""`
+ );
+
+ expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled();
+ expect(auditLogger.alertsAuthorizationFailure).toHaveBeenCalledTimes(1);
+ expect(auditLogger.alertsAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ "some-user",
+ "myType",
+ 0,
+ "myOtherApp",
+ "create",
+ ]
+ `);
+ });
+
+ test('throws if user lacks the required privieleges for the producer', async () => {
+ const { authorization } = mockSecurity();
+ const checkPrivileges: jest.MockedFunction> = jest.fn();
+ authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges);
+ const alertAuthorization = new AlertsAuthorization({
+ request,
+ authorization,
+ alertTypeRegistry,
+ features,
+ auditLogger,
+ getSpace,
+ });
+
+ checkPrivileges.mockResolvedValueOnce({
+ username: 'some-user',
+ hasAllRequested: false,
+ privileges: [
+ {
+ privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'),
+ authorized: true,
+ },
+ {
+ privilege: mockAuthorizationAction('myType', 'myApp', 'create'),
+ authorized: false,
+ },
+ ],
+ });
+
+ await expect(
+ alertAuthorization.ensureAuthorized('myType', 'myOtherApp', WriteOperations.Create)
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
+ `"Unauthorized to create a \\"myType\\" alert by \\"myApp\\""`
+ );
+
+ expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled();
+ expect(auditLogger.alertsAuthorizationFailure).toHaveBeenCalledTimes(1);
+ expect(auditLogger.alertsAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ "some-user",
+ "myType",
+ 1,
+ "myApp",
+ "create",
+ ]
+ `);
+ });
+
+ test('throws if user lacks the required privieleges for both consumer and producer', async () => {
+ const { authorization } = mockSecurity();
+ const checkPrivileges: jest.MockedFunction> = jest.fn();
+ authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges);
+ const alertAuthorization = new AlertsAuthorization({
+ request,
+ authorization,
+ alertTypeRegistry,
+ features,
+ auditLogger,
+ getSpace,
+ });
+
+ checkPrivileges.mockResolvedValueOnce({
+ username: 'some-user',
+ hasAllRequested: false,
+ privileges: [
+ {
+ privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'),
+ authorized: false,
+ },
+ {
+ privilege: mockAuthorizationAction('myType', 'myApp', 'create'),
+ authorized: false,
+ },
+ ],
+ });
+
+ await expect(
+ alertAuthorization.ensureAuthorized('myType', 'myOtherApp', WriteOperations.Create)
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
+ `"Unauthorized to create a \\"myType\\" alert for \\"myOtherApp\\""`
+ );
+
+ expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled();
+ expect(auditLogger.alertsAuthorizationFailure).toHaveBeenCalledTimes(1);
+ expect(auditLogger.alertsAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ "some-user",
+ "myType",
+ 0,
+ "myOtherApp",
+ "create",
+ ]
+ `);
+ });
+ });
+
+ describe('getFindAuthorizationFilter', () => {
+ const myOtherAppAlertType = {
+ actionGroups: [],
+ actionVariables: undefined,
+ defaultActionGroupId: 'default',
+ id: 'myOtherAppAlertType',
+ name: 'myOtherAppAlertType',
+ producer: 'alerts',
+ };
+ const myAppAlertType = {
+ actionGroups: [],
+ actionVariables: undefined,
+ defaultActionGroupId: 'default',
+ id: 'myAppAlertType',
+ name: 'myAppAlertType',
+ producer: 'myApp',
+ };
+ const mySecondAppAlertType = {
+ actionGroups: [],
+ actionVariables: undefined,
+ defaultActionGroupId: 'default',
+ id: 'mySecondAppAlertType',
+ name: 'mySecondAppAlertType',
+ producer: 'myApp',
+ };
+ const setOfAlertTypes = new Set([myAppAlertType, myOtherAppAlertType, mySecondAppAlertType]);
+
+ test('omits filter when there is no authorization api', async () => {
+ const alertAuthorization = new AlertsAuthorization({
+ request,
+ alertTypeRegistry,
+ features,
+ auditLogger,
+ getSpace,
+ });
+
+ const {
+ filter,
+ ensureAlertTypeIsAuthorized,
+ } = await alertAuthorization.getFindAuthorizationFilter();
+
+ expect(() => ensureAlertTypeIsAuthorized('someMadeUpType', 'myApp')).not.toThrow();
+
+ expect(filter).toEqual(undefined);
+ });
+
+ test('ensureAlertTypeIsAuthorized is no-op when there is no authorization api', async () => {
+ const alertAuthorization = new AlertsAuthorization({
+ request,
+ alertTypeRegistry,
+ features,
+ auditLogger,
+ getSpace,
+ });
+
+ const { ensureAlertTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter();
+
+ ensureAlertTypeIsAuthorized('someMadeUpType', 'myApp');
+
+ expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled();
+ expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled();
+ });
+
+ test('creates a filter based on the privileged types', async () => {
+ const { authorization } = mockSecurity();
+ const checkPrivileges: jest.MockedFunction> = jest.fn();
+ authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges);
+ checkPrivileges.mockResolvedValueOnce({
+ username: 'some-user',
+ hasAllRequested: true,
+ privileges: [],
+ });
+
+ const alertAuthorization = new AlertsAuthorization({
+ request,
+ authorization,
+ alertTypeRegistry,
+ features,
+ auditLogger,
+ getSpace,
+ });
+ alertTypeRegistry.list.mockReturnValue(setOfAlertTypes);
+
+ expect((await alertAuthorization.getFindAuthorizationFilter()).filter).toMatchInlineSnapshot(
+ `"((alert.attributes.alertTypeId:myAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (alert.attributes.alertTypeId:myOtherAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (alert.attributes.alertTypeId:mySecondAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)))"`
+ );
+
+ expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled();
+ });
+
+ test('creates an `ensureAlertTypeIsAuthorized` function which throws if type is unauthorized', async () => {
+ const { authorization } = mockSecurity();
+ const checkPrivileges: jest.MockedFunction> = jest.fn();
+ authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges);
+ checkPrivileges.mockResolvedValueOnce({
+ username: 'some-user',
+ hasAllRequested: false,
+ privileges: [
+ {
+ privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'),
+ authorized: true,
+ },
+ {
+ privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'),
+ authorized: false,
+ },
+ {
+ privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'),
+ authorized: true,
+ },
+ {
+ privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'),
+ authorized: false,
+ },
+ ],
+ });
+
+ const alertAuthorization = new AlertsAuthorization({
+ request,
+ authorization,
+ alertTypeRegistry,
+ features,
+ auditLogger,
+ getSpace,
+ });
+ alertTypeRegistry.list.mockReturnValue(setOfAlertTypes);
+
+ const { ensureAlertTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter();
+ expect(() => {
+ ensureAlertTypeIsAuthorized('myAppAlertType', 'myOtherApp');
+ }).toThrowErrorMatchingInlineSnapshot(
+ `"Unauthorized to find a \\"myAppAlertType\\" alert for \\"myOtherApp\\""`
+ );
+
+ expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled();
+ expect(auditLogger.alertsAuthorizationFailure).toHaveBeenCalledTimes(1);
+ expect(auditLogger.alertsAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ "some-user",
+ "myAppAlertType",
+ 0,
+ "myOtherApp",
+ "find",
+ ]
+ `);
+ });
+
+ test('creates an `ensureAlertTypeIsAuthorized` function which is no-op if type is authorized', async () => {
+ const { authorization } = mockSecurity();
+ const checkPrivileges: jest.MockedFunction> = jest.fn();
+ authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges);
+ checkPrivileges.mockResolvedValueOnce({
+ username: 'some-user',
+ hasAllRequested: false,
+ privileges: [
+ {
+ privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'),
+ authorized: true,
+ },
+ {
+ privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'),
+ authorized: false,
+ },
+ {
+ privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'),
+ authorized: true,
+ },
+ {
+ privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'),
+ authorized: true,
+ },
+ ],
+ });
+
+ const alertAuthorization = new AlertsAuthorization({
+ request,
+ authorization,
+ alertTypeRegistry,
+ features,
+ auditLogger,
+ getSpace,
+ });
+ alertTypeRegistry.list.mockReturnValue(setOfAlertTypes);
+
+ const { ensureAlertTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter();
+ expect(() => {
+ ensureAlertTypeIsAuthorized('myAppAlertType', 'myOtherApp');
+ }).not.toThrow();
+
+ expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled();
+ expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled();
+ });
+
+ test('creates an `logSuccessfulAuthorization` function which logs every authorized type', async () => {
+ const { authorization } = mockSecurity();
+ const checkPrivileges: jest.MockedFunction> = jest.fn();
+ authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges);
+ checkPrivileges.mockResolvedValueOnce({
+ username: 'some-user',
+ hasAllRequested: false,
+ privileges: [
+ {
+ privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'),
+ authorized: true,
+ },
+ {
+ privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'),
+ authorized: false,
+ },
+ {
+ privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'),
+ authorized: true,
+ },
+ {
+ privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'),
+ authorized: true,
+ },
+ {
+ privilege: mockAuthorizationAction('mySecondAppAlertType', 'myApp', 'find'),
+ authorized: true,
+ },
+ {
+ privilege: mockAuthorizationAction('mySecondAppAlertType', 'myOtherApp', 'find'),
+ authorized: true,
+ },
+ ],
+ });
+
+ const alertAuthorization = new AlertsAuthorization({
+ request,
+ authorization,
+ alertTypeRegistry,
+ features,
+ auditLogger,
+ getSpace,
+ });
+ alertTypeRegistry.list.mockReturnValue(setOfAlertTypes);
+
+ const {
+ ensureAlertTypeIsAuthorized,
+ logSuccessfulAuthorization,
+ } = await alertAuthorization.getFindAuthorizationFilter();
+ expect(() => {
+ ensureAlertTypeIsAuthorized('myAppAlertType', 'myOtherApp');
+ ensureAlertTypeIsAuthorized('mySecondAppAlertType', 'myOtherApp');
+ ensureAlertTypeIsAuthorized('myAppAlertType', 'myOtherApp');
+ }).not.toThrow();
+
+ expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled();
+ expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled();
+
+ logSuccessfulAuthorization();
+
+ expect(auditLogger.alertsBulkAuthorizationSuccess).toHaveBeenCalledTimes(1);
+ expect(auditLogger.alertsBulkAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ "some-user",
+ Array [
+ Array [
+ "myAppAlertType",
+ "myOtherApp",
+ ],
+ Array [
+ "mySecondAppAlertType",
+ "myOtherApp",
+ ],
+ ],
+ 0,
+ "find",
+ ]
+ `);
+ });
+ });
+
+ describe('filterByAlertTypeAuthorization', () => {
+ const myOtherAppAlertType = {
+ actionGroups: [],
+ actionVariables: undefined,
+ defaultActionGroupId: 'default',
+ id: 'myOtherAppAlertType',
+ name: 'myOtherAppAlertType',
+ producer: 'myOtherApp',
+ };
+ const myAppAlertType = {
+ actionGroups: [],
+ actionVariables: undefined,
+ defaultActionGroupId: 'default',
+ id: 'myAppAlertType',
+ name: 'myAppAlertType',
+ producer: 'myApp',
+ };
+ const setOfAlertTypes = new Set([myAppAlertType, myOtherAppAlertType]);
+
+ test('augments a list of types with all features when there is no authorization api', async () => {
+ const alertAuthorization = new AlertsAuthorization({
+ request,
+ alertTypeRegistry,
+ features,
+ auditLogger,
+ getSpace,
+ });
+ alertTypeRegistry.list.mockReturnValue(setOfAlertTypes);
+
+ await expect(
+ alertAuthorization.filterByAlertTypeAuthorization(
+ new Set([myAppAlertType, myOtherAppAlertType]),
+ [WriteOperations.Create]
+ )
+ ).resolves.toMatchInlineSnapshot(`
+ Set {
+ Object {
+ "actionGroups": Array [],
+ "actionVariables": undefined,
+ "authorizedConsumers": Object {
+ "alerts": Object {
+ "all": true,
+ "read": true,
+ },
+ "myApp": Object {
+ "all": true,
+ "read": true,
+ },
+ "myAppWithSubFeature": Object {
+ "all": true,
+ "read": true,
+ },
+ "myOtherApp": Object {
+ "all": true,
+ "read": true,
+ },
+ },
+ "defaultActionGroupId": "default",
+ "id": "myAppAlertType",
+ "name": "myAppAlertType",
+ "producer": "myApp",
+ },
+ Object {
+ "actionGroups": Array [],
+ "actionVariables": undefined,
+ "authorizedConsumers": Object {
+ "alerts": Object {
+ "all": true,
+ "read": true,
+ },
+ "myApp": Object {
+ "all": true,
+ "read": true,
+ },
+ "myAppWithSubFeature": Object {
+ "all": true,
+ "read": true,
+ },
+ "myOtherApp": Object {
+ "all": true,
+ "read": true,
+ },
+ },
+ "defaultActionGroupId": "default",
+ "id": "myOtherAppAlertType",
+ "name": "myOtherAppAlertType",
+ "producer": "myOtherApp",
+ },
+ }
+ `);
+ });
+
+ test('augments a list of types with consumers under which the operation is authorized', async () => {
+ const { authorization } = mockSecurity();
+ const checkPrivileges: jest.MockedFunction> = jest.fn();
+ authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges);
+ checkPrivileges.mockResolvedValueOnce({
+ username: 'some-user',
+ hasAllRequested: false,
+ privileges: [
+ {
+ privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'),
+ authorized: true,
+ },
+ {
+ privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'),
+ authorized: false,
+ },
+ {
+ privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'),
+ authorized: true,
+ },
+ {
+ privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'),
+ authorized: true,
+ },
+ ],
+ });
+
+ const alertAuthorization = new AlertsAuthorization({
+ request,
+ authorization,
+ alertTypeRegistry,
+ features,
+ auditLogger,
+ getSpace,
+ });
+ alertTypeRegistry.list.mockReturnValue(setOfAlertTypes);
+
+ await expect(
+ alertAuthorization.filterByAlertTypeAuthorization(
+ new Set([myAppAlertType, myOtherAppAlertType]),
+ [WriteOperations.Create]
+ )
+ ).resolves.toMatchInlineSnapshot(`
+ Set {
+ Object {
+ "actionGroups": Array [],
+ "actionVariables": undefined,
+ "authorizedConsumers": Object {
+ "myApp": Object {
+ "all": true,
+ "read": true,
+ },
+ },
+ "defaultActionGroupId": "default",
+ "id": "myOtherAppAlertType",
+ "name": "myOtherAppAlertType",
+ "producer": "myOtherApp",
+ },
+ Object {
+ "actionGroups": Array [],
+ "actionVariables": undefined,
+ "authorizedConsumers": Object {
+ "alerts": Object {
+ "all": true,
+ "read": true,
+ },
+ "myApp": Object {
+ "all": true,
+ "read": true,
+ },
+ "myOtherApp": Object {
+ "all": true,
+ "read": true,
+ },
+ },
+ "defaultActionGroupId": "default",
+ "id": "myAppAlertType",
+ "name": "myAppAlertType",
+ "producer": "myApp",
+ },
+ }
+ `);
+ });
+
+ test('authorizes user under the Alerts consumer when they are authorized by the producer', async () => {
+ const { authorization } = mockSecurity();
+ const checkPrivileges: jest.MockedFunction> = jest.fn();
+ authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges);
+ checkPrivileges.mockResolvedValueOnce({
+ username: 'some-user',
+ hasAllRequested: false,
+ privileges: [
+ {
+ privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'),
+ authorized: true,
+ },
+ {
+ privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'),
+ authorized: false,
+ },
+ ],
+ });
+
+ const alertAuthorization = new AlertsAuthorization({
+ request,
+ authorization,
+ alertTypeRegistry,
+ features,
+ auditLogger,
+ getSpace,
+ });
+ alertTypeRegistry.list.mockReturnValue(setOfAlertTypes);
+
+ await expect(
+ alertAuthorization.filterByAlertTypeAuthorization(new Set([myAppAlertType]), [
+ WriteOperations.Create,
+ ])
+ ).resolves.toMatchInlineSnapshot(`
+ Set {
+ Object {
+ "actionGroups": Array [],
+ "actionVariables": undefined,
+ "authorizedConsumers": Object {
+ "alerts": Object {
+ "all": true,
+ "read": true,
+ },
+ "myApp": Object {
+ "all": true,
+ "read": true,
+ },
+ },
+ "defaultActionGroupId": "default",
+ "id": "myAppAlertType",
+ "name": "myAppAlertType",
+ "producer": "myApp",
+ },
+ }
+ `);
+ });
+
+ test('augments a list of types with consumers under which multiple operations are authorized', async () => {
+ const { authorization } = mockSecurity();
+ const checkPrivileges: jest.MockedFunction> = jest.fn();
+ authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges);
+ checkPrivileges.mockResolvedValueOnce({
+ username: 'some-user',
+ hasAllRequested: false,
+ privileges: [
+ {
+ privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'),
+ authorized: true,
+ },
+ {
+ privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'),
+ authorized: false,
+ },
+ {
+ privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'),
+ authorized: false,
+ },
+ {
+ privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'),
+ authorized: false,
+ },
+ {
+ privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'get'),
+ authorized: true,
+ },
+ {
+ privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'get'),
+ authorized: true,
+ },
+ {
+ privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'get'),
+ authorized: true,
+ },
+ {
+ privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'get'),
+ authorized: true,
+ },
+ ],
+ });
+
+ const alertAuthorization = new AlertsAuthorization({
+ request,
+ authorization,
+ alertTypeRegistry,
+ features,
+ auditLogger,
+ getSpace,
+ });
+ alertTypeRegistry.list.mockReturnValue(setOfAlertTypes);
+
+ await expect(
+ alertAuthorization.filterByAlertTypeAuthorization(
+ new Set([myAppAlertType, myOtherAppAlertType]),
+ [WriteOperations.Create, ReadOperations.Get]
+ )
+ ).resolves.toMatchInlineSnapshot(`
+ Set {
+ Object {
+ "actionGroups": Array [],
+ "actionVariables": undefined,
+ "authorizedConsumers": Object {
+ "alerts": Object {
+ "all": false,
+ "read": true,
+ },
+ "myApp": Object {
+ "all": true,
+ "read": true,
+ },
+ "myOtherApp": Object {
+ "all": false,
+ "read": true,
+ },
+ },
+ "defaultActionGroupId": "default",
+ "id": "myOtherAppAlertType",
+ "name": "myOtherAppAlertType",
+ "producer": "myOtherApp",
+ },
+ Object {
+ "actionGroups": Array [],
+ "actionVariables": undefined,
+ "authorizedConsumers": Object {
+ "alerts": Object {
+ "all": false,
+ "read": true,
+ },
+ "myApp": Object {
+ "all": false,
+ "read": true,
+ },
+ "myOtherApp": Object {
+ "all": false,
+ "read": true,
+ },
+ },
+ "defaultActionGroupId": "default",
+ "id": "myAppAlertType",
+ "name": "myAppAlertType",
+ "producer": "myApp",
+ },
+ }
+ `);
+ });
+
+ test('omits types which have no consumers under which the operation is authorized', async () => {
+ const { authorization } = mockSecurity();
+ const checkPrivileges: jest.MockedFunction> = jest.fn();
+ authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges);
+ checkPrivileges.mockResolvedValueOnce({
+ username: 'some-user',
+ hasAllRequested: false,
+ privileges: [
+ {
+ privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'),
+ authorized: true,
+ },
+ {
+ privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'),
+ authorized: true,
+ },
+ {
+ privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'),
+ authorized: false,
+ },
+ {
+ privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'),
+ authorized: false,
+ },
+ ],
+ });
+
+ const alertAuthorization = new AlertsAuthorization({
+ request,
+ authorization,
+ alertTypeRegistry,
+ features,
+ auditLogger,
+ getSpace,
+ });
+ alertTypeRegistry.list.mockReturnValue(setOfAlertTypes);
+
+ await expect(
+ alertAuthorization.filterByAlertTypeAuthorization(
+ new Set([myAppAlertType, myOtherAppAlertType]),
+ [WriteOperations.Create]
+ )
+ ).resolves.toMatchInlineSnapshot(`
+ Set {
+ Object {
+ "actionGroups": Array [],
+ "actionVariables": undefined,
+ "authorizedConsumers": Object {
+ "alerts": Object {
+ "all": true,
+ "read": true,
+ },
+ "myApp": Object {
+ "all": true,
+ "read": true,
+ },
+ "myOtherApp": Object {
+ "all": true,
+ "read": true,
+ },
+ },
+ "defaultActionGroupId": "default",
+ "id": "myOtherAppAlertType",
+ "name": "myOtherAppAlertType",
+ "producer": "myOtherApp",
+ },
+ }
+ `);
+ });
+ });
+
+ describe('ensureFieldIsSafeForQuery', () => {
+ test('throws if field contains character that isnt safe in a KQL query', () => {
+ expect(() => ensureFieldIsSafeForQuery('id', 'alert-*')).toThrowError(
+ `expected id not to include invalid character: *`
+ );
+
+ expect(() => ensureFieldIsSafeForQuery('id', '<=""')).toThrowError(
+ `expected id not to include invalid character: <=`
+ );
+
+ expect(() => ensureFieldIsSafeForQuery('id', '>=""')).toThrowError(
+ `expected id not to include invalid character: >=`
+ );
+
+ expect(() => ensureFieldIsSafeForQuery('id', '1 or alertid:123')).toThrowError(
+ `expected id not to include whitespace and invalid character: :`
+ );
+
+ expect(() => ensureFieldIsSafeForQuery('id', ') or alertid:123')).toThrowError(
+ `expected id not to include whitespace and invalid characters: ), :`
+ );
+
+ expect(() => ensureFieldIsSafeForQuery('id', 'some space')).toThrowError(
+ `expected id not to include whitespace`
+ );
+ });
+
+ test('doesnt throws if field is safe as part of a KQL query', () => {
+ expect(() => ensureFieldIsSafeForQuery('id', '123-0456-678')).not.toThrow();
+ });
+ });
+});
diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts
new file mode 100644
index 0000000000000..33a9a0bf0396e
--- /dev/null
+++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts
@@ -0,0 +1,457 @@
+/*
+ * 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 Boom from 'boom';
+import { map, mapValues, remove, fromPairs, has } from 'lodash';
+import { KibanaRequest } from 'src/core/server';
+import { ALERTS_FEATURE_ID } from '../../common';
+import { AlertTypeRegistry } from '../types';
+import { SecurityPluginSetup } from '../../../security/server';
+import { RegistryAlertType } from '../alert_type_registry';
+import { PluginStartContract as FeaturesPluginStart } from '../../../features/server';
+import { AlertsAuthorizationAuditLogger, ScopeType } from './audit_logger';
+import { Space } from '../../../spaces/server';
+
+export enum ReadOperations {
+ Get = 'get',
+ GetAlertState = 'getAlertState',
+ Find = 'find',
+}
+
+export enum WriteOperations {
+ Create = 'create',
+ Delete = 'delete',
+ Update = 'update',
+ UpdateApiKey = 'updateApiKey',
+ Enable = 'enable',
+ Disable = 'disable',
+ MuteAll = 'muteAll',
+ UnmuteAll = 'unmuteAll',
+ MuteInstance = 'muteInstance',
+ UnmuteInstance = 'unmuteInstance',
+}
+
+interface HasPrivileges {
+ read: boolean;
+ all: boolean;
+}
+type AuthorizedConsumers = Record;
+export interface RegistryAlertTypeWithAuth extends RegistryAlertType {
+ authorizedConsumers: AuthorizedConsumers;
+}
+
+type IsAuthorizedAtProducerLevel = boolean;
+
+export interface ConstructorOptions {
+ alertTypeRegistry: AlertTypeRegistry;
+ request: KibanaRequest;
+ features: FeaturesPluginStart;
+ getSpace: (request: KibanaRequest) => Promise;
+ auditLogger: AlertsAuthorizationAuditLogger;
+ authorization?: SecurityPluginSetup['authz'];
+}
+
+export class AlertsAuthorization {
+ private readonly alertTypeRegistry: AlertTypeRegistry;
+ private readonly request: KibanaRequest;
+ private readonly authorization?: SecurityPluginSetup['authz'];
+ private readonly auditLogger: AlertsAuthorizationAuditLogger;
+ private readonly featuresIds: Promise>;
+ private readonly allPossibleConsumers: Promise;
+
+ constructor({
+ alertTypeRegistry,
+ request,
+ authorization,
+ features,
+ auditLogger,
+ getSpace,
+ }: ConstructorOptions) {
+ this.request = request;
+ this.authorization = authorization;
+ this.alertTypeRegistry = alertTypeRegistry;
+ this.auditLogger = auditLogger;
+
+ this.featuresIds = getSpace(request)
+ .then((maybeSpace) => new Set(maybeSpace?.disabledFeatures ?? []))
+ .then(
+ (disabledFeatures) =>
+ new Set(
+ features
+ .getFeatures()
+ .filter(
+ ({ id, alerting }) =>
+ // ignore features which are disabled in the user's space
+ !disabledFeatures.has(id) &&
+ // ignore features which don't grant privileges to alerting
+ (alerting?.length ?? 0 > 0)
+ )
+ .map((feature) => feature.id)
+ )
+ )
+ .catch(() => {
+ // failing to fetch the space means the user is likely not privileged in the
+ // active space at all, which means that their list of features should be empty
+ return new Set();
+ });
+
+ this.allPossibleConsumers = this.featuresIds.then((featuresIds) =>
+ featuresIds.size
+ ? asAuthorizedConsumers([ALERTS_FEATURE_ID, ...featuresIds], {
+ read: true,
+ all: true,
+ })
+ : {}
+ );
+ }
+
+ private shouldCheckAuthorization(): boolean {
+ return this.authorization?.mode?.useRbacForRequest(this.request) ?? false;
+ }
+
+ public async ensureAuthorized(
+ alertTypeId: string,
+ consumer: string,
+ operation: ReadOperations | WriteOperations
+ ) {
+ const { authorization } = this;
+
+ const isAvailableConsumer = has(await this.allPossibleConsumers, consumer);
+ if (authorization && this.shouldCheckAuthorization()) {
+ const alertType = this.alertTypeRegistry.get(alertTypeId);
+ const requiredPrivilegesByScope = {
+ consumer: authorization.actions.alerting.get(alertTypeId, consumer, operation),
+ producer: authorization.actions.alerting.get(alertTypeId, alertType.producer, operation),
+ };
+
+ // We special case the Alerts Management `consumer` as we don't want to have to
+ // manually authorize each alert type in the management UI
+ const shouldAuthorizeConsumer = consumer !== ALERTS_FEATURE_ID;
+
+ const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request);
+ const { hasAllRequested, username, privileges } = await checkPrivileges(
+ shouldAuthorizeConsumer && consumer !== alertType.producer
+ ? [
+ // check for access at consumer level
+ requiredPrivilegesByScope.consumer,
+ // check for access at producer level
+ requiredPrivilegesByScope.producer,
+ ]
+ : [
+ // skip consumer privilege checks under `alerts` as all alert types can
+ // be created under `alerts` if you have producer level privileges
+ requiredPrivilegesByScope.producer,
+ ]
+ );
+
+ if (!isAvailableConsumer) {
+ /**
+ * Under most circumstances this would have been caught by `checkPrivileges` as
+ * a user can't have Privileges to an unknown consumer, but super users
+ * don't actually get "privilege checked" so the made up consumer *will* return
+ * as Privileged.
+ * This check will ensure we don't accidentally let these through
+ */
+ throw Boom.forbidden(
+ this.auditLogger.alertsAuthorizationFailure(
+ username,
+ alertTypeId,
+ ScopeType.Consumer,
+ consumer,
+ operation
+ )
+ );
+ }
+
+ if (hasAllRequested) {
+ this.auditLogger.alertsAuthorizationSuccess(
+ username,
+ alertTypeId,
+ ScopeType.Consumer,
+ consumer,
+ operation
+ );
+ } else {
+ const authorizedPrivileges = map(
+ privileges.filter((privilege) => privilege.authorized),
+ 'privilege'
+ );
+ const unauthorizedScopes = mapValues(
+ requiredPrivilegesByScope,
+ (privilege) => !authorizedPrivileges.includes(privilege)
+ );
+
+ const [unauthorizedScopeType, unauthorizedScope] =
+ shouldAuthorizeConsumer && unauthorizedScopes.consumer
+ ? [ScopeType.Consumer, consumer]
+ : [ScopeType.Producer, alertType.producer];
+
+ throw Boom.forbidden(
+ this.auditLogger.alertsAuthorizationFailure(
+ username,
+ alertTypeId,
+ unauthorizedScopeType,
+ unauthorizedScope,
+ operation
+ )
+ );
+ }
+ } else if (!isAvailableConsumer) {
+ throw Boom.forbidden(
+ this.auditLogger.alertsAuthorizationFailure(
+ '',
+ alertTypeId,
+ ScopeType.Consumer,
+ consumer,
+ operation
+ )
+ );
+ }
+ }
+
+ public async getFindAuthorizationFilter(): Promise<{
+ filter?: string;
+ ensureAlertTypeIsAuthorized: (alertTypeId: string, consumer: string) => void;
+ logSuccessfulAuthorization: () => void;
+ }> {
+ if (this.authorization && this.shouldCheckAuthorization()) {
+ const {
+ username,
+ authorizedAlertTypes,
+ } = await this.augmentAlertTypesWithAuthorization(this.alertTypeRegistry.list(), [
+ ReadOperations.Find,
+ ]);
+
+ if (!authorizedAlertTypes.size) {
+ throw Boom.forbidden(
+ this.auditLogger.alertsUnscopedAuthorizationFailure(username!, 'find')
+ );
+ }
+
+ const authorizedAlertTypeIdsToConsumers = new Set(
+ [...authorizedAlertTypes].reduce((alertTypeIdConsumerPairs, alertType) => {
+ for (const consumer of Object.keys(alertType.authorizedConsumers)) {
+ alertTypeIdConsumerPairs.push(`${alertType.id}/${consumer}`);
+ }
+ return alertTypeIdConsumerPairs;
+ }, [])
+ );
+
+ const authorizedEntries: Map> = new Map();
+ return {
+ filter: `(${this.asFiltersByAlertTypeAndConsumer(authorizedAlertTypes).join(' or ')})`,
+ ensureAlertTypeIsAuthorized: (alertTypeId: string, consumer: string) => {
+ if (!authorizedAlertTypeIdsToConsumers.has(`${alertTypeId}/${consumer}`)) {
+ throw Boom.forbidden(
+ this.auditLogger.alertsAuthorizationFailure(
+ username!,
+ alertTypeId,
+ ScopeType.Consumer,
+ consumer,
+ 'find'
+ )
+ );
+ } else {
+ if (authorizedEntries.has(alertTypeId)) {
+ authorizedEntries.get(alertTypeId)!.add(consumer);
+ } else {
+ authorizedEntries.set(alertTypeId, new Set([consumer]));
+ }
+ }
+ },
+ logSuccessfulAuthorization: () => {
+ if (authorizedEntries.size) {
+ this.auditLogger.alertsBulkAuthorizationSuccess(
+ username!,
+ [...authorizedEntries.entries()].reduce>(
+ (authorizedPairs, [alertTypeId, consumers]) => {
+ for (const consumer of consumers) {
+ authorizedPairs.push([alertTypeId, consumer]);
+ }
+ return authorizedPairs;
+ },
+ []
+ ),
+ ScopeType.Consumer,
+ 'find'
+ );
+ }
+ },
+ };
+ }
+ return {
+ ensureAlertTypeIsAuthorized: (alertTypeId: string, consumer: string) => {},
+ logSuccessfulAuthorization: () => {},
+ };
+ }
+
+ public async filterByAlertTypeAuthorization(
+ alertTypes: Set,
+ operations: Array
+ ): Promise> {
+ const { authorizedAlertTypes } = await this.augmentAlertTypesWithAuthorization(
+ alertTypes,
+ operations
+ );
+ return authorizedAlertTypes;
+ }
+
+ private async augmentAlertTypesWithAuthorization(
+ alertTypes: Set,
+ operations: Array
+ ): Promise<{
+ username?: string;
+ hasAllRequested: boolean;
+ authorizedAlertTypes: Set;
+ }> {
+ const featuresIds = await this.featuresIds;
+ if (this.authorization && this.shouldCheckAuthorization()) {
+ const checkPrivileges = this.authorization.checkPrivilegesDynamicallyWithRequest(
+ this.request
+ );
+
+ // add an empty `authorizedConsumers` array on each alertType
+ const alertTypesWithAuthorization = this.augmentWithAuthorizedConsumers(alertTypes, {});
+
+ // map from privilege to alertType which we can refer back to when analyzing the result
+ // of checkPrivileges
+ const privilegeToAlertType = new Map<
+ string,
+ [RegistryAlertTypeWithAuth, string, HasPrivileges, IsAuthorizedAtProducerLevel]
+ >();
+ // as we can't ask ES for the user's individual privileges we need to ask for each feature
+ // and alertType in the system whether this user has this privilege
+ for (const alertType of alertTypesWithAuthorization) {
+ for (const feature of featuresIds) {
+ for (const operation of operations) {
+ privilegeToAlertType.set(
+ this.authorization!.actions.alerting.get(alertType.id, feature, operation),
+ [
+ alertType,
+ feature,
+ hasPrivilegeByOperation(operation),
+ alertType.producer === feature,
+ ]
+ );
+ }
+ }
+ }
+
+ const { username, hasAllRequested, privileges } = await checkPrivileges([
+ ...privilegeToAlertType.keys(),
+ ]);
+
+ return {
+ username,
+ hasAllRequested,
+ authorizedAlertTypes: hasAllRequested
+ ? // has access to all features
+ this.augmentWithAuthorizedConsumers(alertTypes, await this.allPossibleConsumers)
+ : // only has some of the required privileges
+ privileges.reduce((authorizedAlertTypes, { authorized, privilege }) => {
+ if (authorized && privilegeToAlertType.has(privilege)) {
+ const [
+ alertType,
+ feature,
+ hasPrivileges,
+ isAuthorizedAtProducerLevel,
+ ] = privilegeToAlertType.get(privilege)!;
+ alertType.authorizedConsumers[feature] = mergeHasPrivileges(
+ hasPrivileges,
+ alertType.authorizedConsumers[feature]
+ );
+
+ if (isAuthorizedAtProducerLevel) {
+ // granting privileges under the producer automatically authorized the Alerts Management UI as well
+ alertType.authorizedConsumers[ALERTS_FEATURE_ID] = mergeHasPrivileges(
+ hasPrivileges,
+ alertType.authorizedConsumers[ALERTS_FEATURE_ID]
+ );
+ }
+ authorizedAlertTypes.add(alertType);
+ }
+ return authorizedAlertTypes;
+ }, new Set()),
+ };
+ } else {
+ return {
+ hasAllRequested: true,
+ authorizedAlertTypes: this.augmentWithAuthorizedConsumers(
+ new Set([...alertTypes].filter((alertType) => featuresIds.has(alertType.producer))),
+ await this.allPossibleConsumers
+ ),
+ };
+ }
+ }
+
+ private augmentWithAuthorizedConsumers(
+ alertTypes: Set,
+ authorizedConsumers: AuthorizedConsumers
+ ): Set {
+ return new Set(
+ Array.from(alertTypes).map((alertType) => ({
+ ...alertType,
+ authorizedConsumers: { ...authorizedConsumers },
+ }))
+ );
+ }
+
+ private asFiltersByAlertTypeAndConsumer(alertTypes: Set): string[] {
+ return Array.from(alertTypes).reduce((filters, { id, authorizedConsumers }) => {
+ ensureFieldIsSafeForQuery('alertTypeId', id);
+ filters.push(
+ `(alert.attributes.alertTypeId:${id} and alert.attributes.consumer:(${Object.keys(
+ authorizedConsumers
+ )
+ .map((consumer) => {
+ ensureFieldIsSafeForQuery('alertTypeId', id);
+ return consumer;
+ })
+ .join(' or ')}))`
+ );
+ return filters;
+ }, []);
+ }
+}
+
+export function ensureFieldIsSafeForQuery(field: string, value: string): boolean {
+ const invalid = value.match(/([>=<\*:()]+|\s+)/g);
+ if (invalid) {
+ const whitespace = remove(invalid, (chars) => chars.trim().length === 0);
+ const errors = [];
+ if (whitespace.length) {
+ errors.push(`whitespace`);
+ }
+ if (invalid.length) {
+ errors.push(`invalid character${invalid.length > 1 ? `s` : ``}: ${invalid?.join(`, `)}`);
+ }
+ throw new Error(`expected ${field} not to include ${errors.join(' and ')}`);
+ }
+ return true;
+}
+
+function mergeHasPrivileges(left: HasPrivileges, right?: HasPrivileges): HasPrivileges {
+ return {
+ read: (left.read || right?.read) ?? false,
+ all: (left.all || right?.all) ?? false,
+ };
+}
+
+function hasPrivilegeByOperation(operation: ReadOperations | WriteOperations): HasPrivileges {
+ const read = Object.values(ReadOperations).includes((operation as unknown) as ReadOperations);
+ const all = Object.values(WriteOperations).includes((operation as unknown) as WriteOperations);
+ return {
+ read: read || all,
+ all,
+ };
+}
+
+function asAuthorizedConsumers(
+ consumers: string[],
+ hasPrivileges: HasPrivileges
+): AuthorizedConsumers {
+ return fromPairs(consumers.map((feature) => [feature, hasPrivileges]));
+}
diff --git a/x-pack/plugins/alerts/server/authorization/audit_logger.mock.ts b/x-pack/plugins/alerts/server/authorization/audit_logger.mock.ts
new file mode 100644
index 0000000000000..ca6a35b24bcac
--- /dev/null
+++ b/x-pack/plugins/alerts/server/authorization/audit_logger.mock.ts
@@ -0,0 +1,24 @@
+/*
+ * 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 { AlertsAuthorizationAuditLogger } from './audit_logger';
+
+const createAlertsAuthorizationAuditLoggerMock = () => {
+ const mocked = ({
+ getAuthorizationMessage: jest.fn(),
+ alertsAuthorizationFailure: jest.fn(),
+ alertsUnscopedAuthorizationFailure: jest.fn(),
+ alertsAuthorizationSuccess: jest.fn(),
+ alertsBulkAuthorizationSuccess: jest.fn(),
+ } as unknown) as jest.Mocked;
+ return mocked;
+};
+
+export const alertsAuthorizationAuditLoggerMock: {
+ create: () => jest.Mocked;
+} = {
+ create: createAlertsAuthorizationAuditLoggerMock,
+};
diff --git a/x-pack/plugins/alerts/server/authorization/audit_logger.test.ts b/x-pack/plugins/alerts/server/authorization/audit_logger.test.ts
new file mode 100644
index 0000000000000..40973a3a67a51
--- /dev/null
+++ b/x-pack/plugins/alerts/server/authorization/audit_logger.test.ts
@@ -0,0 +1,311 @@
+/*
+ * 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 { AlertsAuthorizationAuditLogger, ScopeType } from './audit_logger';
+
+const createMockAuditLogger = () => {
+ return {
+ log: jest.fn(),
+ };
+};
+
+describe(`#constructor`, () => {
+ test('initializes a noop auditLogger if security logger is unavailable', () => {
+ const alertsAuditLogger = new AlertsAuthorizationAuditLogger(undefined);
+
+ const username = 'foo-user';
+ const alertTypeId = 'alert-type-id';
+ const scopeType = ScopeType.Consumer;
+ const scope = 'myApp';
+ const operation = 'create';
+ expect(() => {
+ alertsAuditLogger.alertsAuthorizationFailure(
+ username,
+ alertTypeId,
+ scopeType,
+ scope,
+ operation
+ );
+
+ alertsAuditLogger.alertsAuthorizationSuccess(
+ username,
+ alertTypeId,
+ scopeType,
+ scope,
+ operation
+ );
+ }).not.toThrow();
+ });
+});
+
+describe(`#alertsUnscopedAuthorizationFailure`, () => {
+ test('logs auth failure of operation', () => {
+ const auditLogger = createMockAuditLogger();
+ const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger);
+ const username = 'foo-user';
+ const operation = 'create';
+
+ alertsAuditLogger.alertsUnscopedAuthorizationFailure(username, operation);
+
+ expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ "alerts_unscoped_authorization_failure",
+ "foo-user Unauthorized to create any alert types",
+ Object {
+ "operation": "create",
+ "username": "foo-user",
+ },
+ ]
+ `);
+ });
+
+ test('logs auth failure with producer scope', () => {
+ const auditLogger = createMockAuditLogger();
+ const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger);
+ const username = 'foo-user';
+ const alertTypeId = 'alert-type-id';
+ const scopeType = ScopeType.Producer;
+ const scope = 'myOtherApp';
+ const operation = 'create';
+
+ alertsAuditLogger.alertsAuthorizationFailure(
+ username,
+ alertTypeId,
+ scopeType,
+ scope,
+ operation
+ );
+
+ expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ "alerts_authorization_failure",
+ "foo-user Unauthorized to create a \\"alert-type-id\\" alert by \\"myOtherApp\\"",
+ Object {
+ "alertTypeId": "alert-type-id",
+ "operation": "create",
+ "scope": "myOtherApp",
+ "scopeType": 1,
+ "username": "foo-user",
+ },
+ ]
+ `);
+ });
+});
+
+describe(`#alertsAuthorizationFailure`, () => {
+ test('logs auth failure with consumer scope', () => {
+ const auditLogger = createMockAuditLogger();
+ const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger);
+ const username = 'foo-user';
+ const alertTypeId = 'alert-type-id';
+ const scopeType = ScopeType.Consumer;
+ const scope = 'myApp';
+ const operation = 'create';
+
+ alertsAuditLogger.alertsAuthorizationFailure(
+ username,
+ alertTypeId,
+ scopeType,
+ scope,
+ operation
+ );
+
+ expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ "alerts_authorization_failure",
+ "foo-user Unauthorized to create a \\"alert-type-id\\" alert for \\"myApp\\"",
+ Object {
+ "alertTypeId": "alert-type-id",
+ "operation": "create",
+ "scope": "myApp",
+ "scopeType": 0,
+ "username": "foo-user",
+ },
+ ]
+ `);
+ });
+
+ test('logs auth failure with producer scope', () => {
+ const auditLogger = createMockAuditLogger();
+ const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger);
+ const username = 'foo-user';
+ const alertTypeId = 'alert-type-id';
+ const scopeType = ScopeType.Producer;
+ const scope = 'myOtherApp';
+ const operation = 'create';
+
+ alertsAuditLogger.alertsAuthorizationFailure(
+ username,
+ alertTypeId,
+ scopeType,
+ scope,
+ operation
+ );
+
+ expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ "alerts_authorization_failure",
+ "foo-user Unauthorized to create a \\"alert-type-id\\" alert by \\"myOtherApp\\"",
+ Object {
+ "alertTypeId": "alert-type-id",
+ "operation": "create",
+ "scope": "myOtherApp",
+ "scopeType": 1,
+ "username": "foo-user",
+ },
+ ]
+ `);
+ });
+});
+
+describe(`#alertsBulkAuthorizationSuccess`, () => {
+ test('logs auth success with consumer scope', () => {
+ const auditLogger = createMockAuditLogger();
+ const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger);
+ const username = 'foo-user';
+ const scopeType = ScopeType.Consumer;
+ const authorizedEntries: Array<[string, string]> = [
+ ['alert-type-id', 'myApp'],
+ ['other-alert-type-id', 'myOtherApp'],
+ ];
+ const operation = 'create';
+
+ alertsAuditLogger.alertsBulkAuthorizationSuccess(
+ username,
+ authorizedEntries,
+ scopeType,
+ operation
+ );
+
+ expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ "alerts_authorization_success",
+ "foo-user Authorized to create: \\"alert-type-id\\" alert for \\"myApp\\", \\"other-alert-type-id\\" alert for \\"myOtherApp\\"",
+ Object {
+ "authorizedEntries": Array [
+ Array [
+ "alert-type-id",
+ "myApp",
+ ],
+ Array [
+ "other-alert-type-id",
+ "myOtherApp",
+ ],
+ ],
+ "operation": "create",
+ "scopeType": 0,
+ "username": "foo-user",
+ },
+ ]
+ `);
+ });
+
+ test('logs auth success with producer scope', () => {
+ const auditLogger = createMockAuditLogger();
+ const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger);
+ const username = 'foo-user';
+ const scopeType = ScopeType.Producer;
+ const authorizedEntries: Array<[string, string]> = [
+ ['alert-type-id', 'myApp'],
+ ['other-alert-type-id', 'myOtherApp'],
+ ];
+ const operation = 'create';
+
+ alertsAuditLogger.alertsBulkAuthorizationSuccess(
+ username,
+ authorizedEntries,
+ scopeType,
+ operation
+ );
+
+ expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ "alerts_authorization_success",
+ "foo-user Authorized to create: \\"alert-type-id\\" alert by \\"myApp\\", \\"other-alert-type-id\\" alert by \\"myOtherApp\\"",
+ Object {
+ "authorizedEntries": Array [
+ Array [
+ "alert-type-id",
+ "myApp",
+ ],
+ Array [
+ "other-alert-type-id",
+ "myOtherApp",
+ ],
+ ],
+ "operation": "create",
+ "scopeType": 1,
+ "username": "foo-user",
+ },
+ ]
+ `);
+ });
+});
+
+describe(`#savedObjectsAuthorizationSuccess`, () => {
+ test('logs auth success with consumer scope', () => {
+ const auditLogger = createMockAuditLogger();
+ const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger);
+ const username = 'foo-user';
+ const alertTypeId = 'alert-type-id';
+ const scopeType = ScopeType.Consumer;
+ const scope = 'myApp';
+ const operation = 'create';
+
+ alertsAuditLogger.alertsAuthorizationSuccess(
+ username,
+ alertTypeId,
+ scopeType,
+ scope,
+ operation
+ );
+
+ expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ "alerts_authorization_success",
+ "foo-user Authorized to create a \\"alert-type-id\\" alert for \\"myApp\\"",
+ Object {
+ "alertTypeId": "alert-type-id",
+ "operation": "create",
+ "scope": "myApp",
+ "scopeType": 0,
+ "username": "foo-user",
+ },
+ ]
+ `);
+ });
+
+ test('logs auth success with producer scope', () => {
+ const auditLogger = createMockAuditLogger();
+ const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger);
+ const username = 'foo-user';
+ const alertTypeId = 'alert-type-id';
+ const scopeType = ScopeType.Producer;
+ const scope = 'myOtherApp';
+ const operation = 'create';
+
+ alertsAuditLogger.alertsAuthorizationSuccess(
+ username,
+ alertTypeId,
+ scopeType,
+ scope,
+ operation
+ );
+
+ expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ "alerts_authorization_success",
+ "foo-user Authorized to create a \\"alert-type-id\\" alert by \\"myOtherApp\\"",
+ Object {
+ "alertTypeId": "alert-type-id",
+ "operation": "create",
+ "scope": "myOtherApp",
+ "scopeType": 1,
+ "username": "foo-user",
+ },
+ ]
+ `);
+ });
+});
diff --git a/x-pack/plugins/alerts/server/authorization/audit_logger.ts b/x-pack/plugins/alerts/server/authorization/audit_logger.ts
new file mode 100644
index 0000000000000..f930da2ce428c
--- /dev/null
+++ b/x-pack/plugins/alerts/server/authorization/audit_logger.ts
@@ -0,0 +1,117 @@
+/*
+ * 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 { AuditLogger } from '../../../security/server';
+
+export enum ScopeType {
+ Consumer,
+ Producer,
+}
+
+export enum AuthorizationResult {
+ Unauthorized = 'Unauthorized',
+ Authorized = 'Authorized',
+}
+
+export class AlertsAuthorizationAuditLogger {
+ private readonly auditLogger: AuditLogger;
+
+ constructor(auditLogger: AuditLogger = { log() {} }) {
+ this.auditLogger = auditLogger;
+ }
+
+ public getAuthorizationMessage(
+ authorizationResult: AuthorizationResult,
+ alertTypeId: string,
+ scopeType: ScopeType,
+ scope: string,
+ operation: string
+ ): string {
+ return `${authorizationResult} to ${operation} a "${alertTypeId}" alert ${
+ scopeType === ScopeType.Consumer ? `for "${scope}"` : `by "${scope}"`
+ }`;
+ }
+
+ public alertsAuthorizationFailure(
+ username: string,
+ alertTypeId: string,
+ scopeType: ScopeType,
+ scope: string,
+ operation: string
+ ): string {
+ const message = this.getAuthorizationMessage(
+ AuthorizationResult.Unauthorized,
+ alertTypeId,
+ scopeType,
+ scope,
+ operation
+ );
+ this.auditLogger.log('alerts_authorization_failure', `${username} ${message}`, {
+ username,
+ alertTypeId,
+ scopeType,
+ scope,
+ operation,
+ });
+ return message;
+ }
+
+ public alertsUnscopedAuthorizationFailure(username: string, operation: string): string {
+ const message = `Unauthorized to ${operation} any alert types`;
+ this.auditLogger.log('alerts_unscoped_authorization_failure', `${username} ${message}`, {
+ username,
+ operation,
+ });
+ return message;
+ }
+
+ public alertsAuthorizationSuccess(
+ username: string,
+ alertTypeId: string,
+ scopeType: ScopeType,
+ scope: string,
+ operation: string
+ ): string {
+ const message = this.getAuthorizationMessage(
+ AuthorizationResult.Authorized,
+ alertTypeId,
+ scopeType,
+ scope,
+ operation
+ );
+ this.auditLogger.log('alerts_authorization_success', `${username} ${message}`, {
+ username,
+ alertTypeId,
+ scopeType,
+ scope,
+ operation,
+ });
+ return message;
+ }
+
+ public alertsBulkAuthorizationSuccess(
+ username: string,
+ authorizedEntries: Array<[string, string]>,
+ scopeType: ScopeType,
+ operation: string
+ ): string {
+ const message = `${AuthorizationResult.Authorized} to ${operation}: ${authorizedEntries
+ .map(
+ ([alertTypeId, scope]) =>
+ `"${alertTypeId}" alert ${
+ scopeType === ScopeType.Consumer ? `for "${scope}"` : `by "${scope}"`
+ }`
+ )
+ .join(', ')}`;
+ this.auditLogger.log('alerts_authorization_success', `${username} ${message}`, {
+ username,
+ scopeType,
+ authorizedEntries,
+ operation,
+ });
+ return message;
+ }
+}
diff --git a/x-pack/plugins/alerts/server/index.ts b/x-pack/plugins/alerts/server/index.ts
index 727e38d9ba56b..515de771e7d6b 100644
--- a/x-pack/plugins/alerts/server/index.ts
+++ b/x-pack/plugins/alerts/server/index.ts
@@ -21,7 +21,7 @@ export {
PartialAlert,
} from './types';
export { PluginSetupContract, PluginStartContract } from './plugin';
-export { FindOptions, FindResult } from './alerts_client';
+export { FindResult } from './alerts_client';
export { AlertInstance } from './alert_instance';
export { parseDuration } from './lib';
diff --git a/x-pack/plugins/alerts/server/lib/validate_alert_type_params.test.ts b/x-pack/plugins/alerts/server/lib/validate_alert_type_params.test.ts
index d31b15030fd3a..1e6c26c02e65b 100644
--- a/x-pack/plugins/alerts/server/lib/validate_alert_type_params.test.ts
+++ b/x-pack/plugins/alerts/server/lib/validate_alert_type_params.test.ts
@@ -20,7 +20,7 @@ test('should return passed in params when validation not defined', () => {
],
defaultActionGroupId: 'default',
async executor() {},
- producer: 'alerting',
+ producer: 'alerts',
},
{
foo: true,
@@ -48,7 +48,7 @@ test('should validate and apply defaults when params is valid', () => {
}),
},
async executor() {},
- producer: 'alerting',
+ producer: 'alerts',
},
{ param1: 'value' }
);
@@ -77,7 +77,7 @@ test('should validate and throw error when params is invalid', () => {
}),
},
async executor() {},
- producer: 'alerting',
+ producer: 'alerts',
},
{}
)
diff --git a/x-pack/plugins/alerts/server/plugin.test.ts b/x-pack/plugins/alerts/server/plugin.test.ts
index 008a9bb804c5b..e65d195290259 100644
--- a/x-pack/plugins/alerts/server/plugin.test.ts
+++ b/x-pack/plugins/alerts/server/plugin.test.ts
@@ -11,6 +11,8 @@ import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/
import { taskManagerMock } from '../../task_manager/server/mocks';
import { eventLogServiceMock } from '../../event_log/server/event_log_service.mock';
import { KibanaRequest, CoreSetup } from 'kibana/server';
+import { featuresPluginMock } from '../../features/server/mocks';
+import { Feature } from '../../features/server';
describe('Alerting Plugin', () => {
describe('setup()', () => {
@@ -80,8 +82,10 @@ describe('Alerting Plugin', () => {
actions: {
execute: jest.fn(),
getActionsClientWithRequest: jest.fn(),
+ getActionsAuthorizationWithRequest: jest.fn(),
},
encryptedSavedObjects: encryptedSavedObjectsMock.createStart(),
+ features: mockFeatures(),
} as unknown) as AlertingPluginsStart
);
@@ -124,9 +128,11 @@ describe('Alerting Plugin', () => {
actions: {
execute: jest.fn(),
getActionsClientWithRequest: jest.fn(),
+ getActionsAuthorizationWithRequest: jest.fn(),
},
spaces: () => null,
encryptedSavedObjects: encryptedSavedObjectsMock.createStart(),
+ features: mockFeatures(),
} as unknown) as AlertingPluginsStart
);
@@ -150,3 +156,31 @@ describe('Alerting Plugin', () => {
});
});
});
+
+function mockFeatures() {
+ const features = featuresPluginMock.createSetup();
+ features.getFeatures.mockReturnValue([
+ new Feature({
+ id: 'appName',
+ name: 'appName',
+ app: [],
+ privileges: {
+ all: {
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ ui: [],
+ },
+ read: {
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ ui: [],
+ },
+ },
+ }),
+ ]);
+ return features;
+}
diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts
index 07ed021d8ca84..cf6e1c9aebba6 100644
--- a/x-pack/plugins/alerts/server/plugin.ts
+++ b/x-pack/plugins/alerts/server/plugin.ts
@@ -58,6 +58,7 @@ import { Services } from './types';
import { registerAlertsUsageCollector } from './usage';
import { initializeAlertingTelemetry, scheduleAlertingTelemetry } from './usage/task';
import { IEventLogger, IEventLogService } from '../../event_log/server';
+import { PluginStartContract as FeaturesPluginStart } from '../../features/server';
import { setupSavedObjects } from './saved_objects';
const EVENT_LOG_PROVIDER = 'alerting';
@@ -90,6 +91,7 @@ export interface AlertingPluginsStart {
actions: ActionsPluginStartContract;
taskManager: TaskManagerStartContract;
encryptedSavedObjects: EncryptedSavedObjectsPluginStart;
+ features: FeaturesPluginStart;
}
export class AlertingPlugin {
@@ -216,12 +218,26 @@ export class AlertingPlugin {
getSpaceId(request: KibanaRequest) {
return spaces?.getSpaceId(request);
},
+ async getSpace(request: KibanaRequest) {
+ return spaces?.getActiveSpace(request);
+ },
actions: plugins.actions,
+ features: plugins.features,
});
+ const getAlertsClientWithRequest = (request: KibanaRequest) => {
+ if (isESOUsingEphemeralEncryptionKey === true) {
+ throw new Error(
+ `Unable to create alerts client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml`
+ );
+ }
+ return alertsClientFactory!.create(request, core.savedObjects);
+ };
+
taskRunnerFactory.initialize({
logger,
getServices: this.getServicesFactory(core.savedObjects, core.elasticsearch),
+ getAlertsClientWithRequest,
spaceIdToNamespace: this.spaceIdToNamespace,
actionsPlugin: plugins.actions,
encryptedSavedObjectsClient,
@@ -233,18 +249,7 @@ export class AlertingPlugin {
return {
listTypes: alertTypeRegistry!.list.bind(this.alertTypeRegistry!),
- // Ability to get an alerts client from legacy code
- getAlertsClientWithRequest: (request: KibanaRequest) => {
- if (isESOUsingEphemeralEncryptionKey === true) {
- throw new Error(
- `Unable to create alerts client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml`
- );
- }
- return alertsClientFactory!.create(
- request,
- this.getScopedClientWithAlertSavedObjectType(core.savedObjects, request)
- );
- },
+ getAlertsClientWithRequest,
};
}
@@ -252,14 +257,11 @@ export class AlertingPlugin {
core: CoreSetup
): IContextProvider, 'alerting'> => {
const { alertTypeRegistry, alertsClientFactory } = this;
- return async (context, request) => {
+ return async function alertsRouteHandlerContext(context, request) {
const [{ savedObjects }] = await core.getStartServices();
return {
getAlertsClient: () => {
- return alertsClientFactory!.create(
- request,
- this.getScopedClientWithAlertSavedObjectType(savedObjects, request)
- );
+ return alertsClientFactory!.create(request, savedObjects);
},
listTypes: alertTypeRegistry!.list.bind(alertTypeRegistry!),
};
diff --git a/x-pack/plugins/alerts/server/routes/create.test.ts b/x-pack/plugins/alerts/server/routes/create.test.ts
index 9e941903eeaed..274acaf01c475 100644
--- a/x-pack/plugins/alerts/server/routes/create.test.ts
+++ b/x-pack/plugins/alerts/server/routes/create.test.ts
@@ -75,13 +75,6 @@ describe('createAlertRoute', () => {
const [config, handler] = router.post.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert"`);
- expect(config.options).toMatchInlineSnapshot(`
- Object {
- "tags": Array [
- "access:alerting-all",
- ],
- }
- `);
alertsClient.create.mockResolvedValueOnce(createResult);
diff --git a/x-pack/plugins/alerts/server/routes/create.ts b/x-pack/plugins/alerts/server/routes/create.ts
index 6238fca024e55..91a81f6d84b71 100644
--- a/x-pack/plugins/alerts/server/routes/create.ts
+++ b/x-pack/plugins/alerts/server/routes/create.ts
@@ -47,9 +47,6 @@ export const createAlertRoute = (router: IRouter, licenseState: LicenseState) =>
validate: {
body: bodySchema,
},
- options: {
- tags: ['access:alerting-all'],
- },
},
handleDisabledApiKeysError(
router.handleLegacyErrors(async function (
diff --git a/x-pack/plugins/alerts/server/routes/delete.test.ts b/x-pack/plugins/alerts/server/routes/delete.test.ts
index 9ba4e20312e17..d9c5aa2d59c87 100644
--- a/x-pack/plugins/alerts/server/routes/delete.test.ts
+++ b/x-pack/plugins/alerts/server/routes/delete.test.ts
@@ -30,13 +30,6 @@ describe('deleteAlertRoute', () => {
const [config, handler] = router.delete.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}"`);
- expect(config.options).toMatchInlineSnapshot(`
- Object {
- "tags": Array [
- "access:alerting-all",
- ],
- }
- `);
alertsClient.delete.mockResolvedValueOnce({});
diff --git a/x-pack/plugins/alerts/server/routes/delete.ts b/x-pack/plugins/alerts/server/routes/delete.ts
index 2034bd21fbed6..b073c59149171 100644
--- a/x-pack/plugins/alerts/server/routes/delete.ts
+++ b/x-pack/plugins/alerts/server/routes/delete.ts
@@ -27,9 +27,6 @@ export const deleteAlertRoute = (router: IRouter, licenseState: LicenseState) =>
validate: {
params: paramSchema,
},
- options: {
- tags: ['access:alerting-all'],
- },
},
router.handleLegacyErrors(async function (
context: RequestHandlerContext,
diff --git a/x-pack/plugins/alerts/server/routes/disable.test.ts b/x-pack/plugins/alerts/server/routes/disable.test.ts
index a82d09854a604..74f7b2eb8a570 100644
--- a/x-pack/plugins/alerts/server/routes/disable.test.ts
+++ b/x-pack/plugins/alerts/server/routes/disable.test.ts
@@ -30,13 +30,6 @@ describe('disableAlertRoute', () => {
const [config, handler] = router.post.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_disable"`);
- expect(config.options).toMatchInlineSnapshot(`
- Object {
- "tags": Array [
- "access:alerting-all",
- ],
- }
- `);
alertsClient.disable.mockResolvedValueOnce();
diff --git a/x-pack/plugins/alerts/server/routes/disable.ts b/x-pack/plugins/alerts/server/routes/disable.ts
index dfc5dfbdd5aa2..234f8ed959a5d 100644
--- a/x-pack/plugins/alerts/server/routes/disable.ts
+++ b/x-pack/plugins/alerts/server/routes/disable.ts
@@ -27,9 +27,6 @@ export const disableAlertRoute = (router: IRouter, licenseState: LicenseState) =
validate: {
params: paramSchema,
},
- options: {
- tags: ['access:alerting-all'],
- },
},
router.handleLegacyErrors(async function (
context: RequestHandlerContext,
diff --git a/x-pack/plugins/alerts/server/routes/enable.test.ts b/x-pack/plugins/alerts/server/routes/enable.test.ts
index 4ee3a12a59dc7..c9575ef87f767 100644
--- a/x-pack/plugins/alerts/server/routes/enable.test.ts
+++ b/x-pack/plugins/alerts/server/routes/enable.test.ts
@@ -29,13 +29,6 @@ describe('enableAlertRoute', () => {
const [config, handler] = router.post.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_enable"`);
- expect(config.options).toMatchInlineSnapshot(`
- Object {
- "tags": Array [
- "access:alerting-all",
- ],
- }
- `);
alertsClient.enable.mockResolvedValueOnce();
diff --git a/x-pack/plugins/alerts/server/routes/enable.ts b/x-pack/plugins/alerts/server/routes/enable.ts
index b6f86b97d6a3a..c162b4a9844b3 100644
--- a/x-pack/plugins/alerts/server/routes/enable.ts
+++ b/x-pack/plugins/alerts/server/routes/enable.ts
@@ -28,9 +28,6 @@ export const enableAlertRoute = (router: IRouter, licenseState: LicenseState) =>
validate: {
params: paramSchema,
},
- options: {
- tags: ['access:alerting-all'],
- },
},
handleDisabledApiKeysError(
router.handleLegacyErrors(async function (
diff --git a/x-pack/plugins/alerts/server/routes/find.test.ts b/x-pack/plugins/alerts/server/routes/find.test.ts
index f20ee0a54dcd9..46702f96a2e10 100644
--- a/x-pack/plugins/alerts/server/routes/find.test.ts
+++ b/x-pack/plugins/alerts/server/routes/find.test.ts
@@ -31,13 +31,6 @@ describe('findAlertRoute', () => {
const [config, handler] = router.get.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/_find"`);
- expect(config.options).toMatchInlineSnapshot(`
- Object {
- "tags": Array [
- "access:alerting-read",
- ],
- }
- `);
const findResult = {
page: 1,
diff --git a/x-pack/plugins/alerts/server/routes/find.ts b/x-pack/plugins/alerts/server/routes/find.ts
index 80c9c20eec7da..ef3b16dc9e517 100644
--- a/x-pack/plugins/alerts/server/routes/find.ts
+++ b/x-pack/plugins/alerts/server/routes/find.ts
@@ -16,7 +16,7 @@ import { LicenseState } from '../lib/license_state';
import { verifyApiAccess } from '../lib/license_api_access';
import { BASE_ALERT_API_PATH } from '../../common';
import { renameKeys } from './lib/rename_keys';
-import { FindOptions } from '..';
+import { FindOptions } from '../alerts_client';
// config definition
const querySchema = schema.object({
@@ -50,9 +50,6 @@ export const findAlertRoute = (router: IRouter, licenseState: LicenseState) => {
validate: {
query: querySchema,
},
- options: {
- tags: ['access:alerting-read'],
- },
},
router.handleLegacyErrors(async function (
context: RequestHandlerContext,
diff --git a/x-pack/plugins/alerts/server/routes/get.test.ts b/x-pack/plugins/alerts/server/routes/get.test.ts
index b11224ff4794e..8c4b06adf70f7 100644
--- a/x-pack/plugins/alerts/server/routes/get.test.ts
+++ b/x-pack/plugins/alerts/server/routes/get.test.ts
@@ -61,13 +61,6 @@ describe('getAlertRoute', () => {
const [config, handler] = router.get.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}"`);
- expect(config.options).toMatchInlineSnapshot(`
- Object {
- "tags": Array [
- "access:alerting-read",
- ],
- }
- `);
alertsClient.get.mockResolvedValueOnce(mockedAlert);
diff --git a/x-pack/plugins/alerts/server/routes/get.ts b/x-pack/plugins/alerts/server/routes/get.ts
index ae9ebe1299371..0f3fc4b2f3e41 100644
--- a/x-pack/plugins/alerts/server/routes/get.ts
+++ b/x-pack/plugins/alerts/server/routes/get.ts
@@ -27,9 +27,6 @@ export const getAlertRoute = (router: IRouter, licenseState: LicenseState) => {
validate: {
params: paramSchema,
},
- options: {
- tags: ['access:alerting-read'],
- },
},
router.handleLegacyErrors(async function (
context: RequestHandlerContext,
diff --git a/x-pack/plugins/alerts/server/routes/get_alert_state.test.ts b/x-pack/plugins/alerts/server/routes/get_alert_state.test.ts
index 8c9051093f85b..d5bf9737d39ab 100644
--- a/x-pack/plugins/alerts/server/routes/get_alert_state.test.ts
+++ b/x-pack/plugins/alerts/server/routes/get_alert_state.test.ts
@@ -48,13 +48,6 @@ describe('getAlertStateRoute', () => {
const [config, handler] = router.get.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/state"`);
- expect(config.options).toMatchInlineSnapshot(`
- Object {
- "tags": Array [
- "access:alerting-read",
- ],
- }
- `);
alertsClient.getAlertState.mockResolvedValueOnce(mockedAlertState);
@@ -91,13 +84,6 @@ describe('getAlertStateRoute', () => {
const [config, handler] = router.get.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/state"`);
- expect(config.options).toMatchInlineSnapshot(`
- Object {
- "tags": Array [
- "access:alerting-read",
- ],
- }
- `);
alertsClient.getAlertState.mockResolvedValueOnce(undefined);
@@ -134,13 +120,6 @@ describe('getAlertStateRoute', () => {
const [config, handler] = router.get.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/state"`);
- expect(config.options).toMatchInlineSnapshot(`
- Object {
- "tags": Array [
- "access:alerting-read",
- ],
- }
- `);
alertsClient.getAlertState = jest
.fn()
diff --git a/x-pack/plugins/alerts/server/routes/get_alert_state.ts b/x-pack/plugins/alerts/server/routes/get_alert_state.ts
index b27ae3758e1b9..089fc80fca355 100644
--- a/x-pack/plugins/alerts/server/routes/get_alert_state.ts
+++ b/x-pack/plugins/alerts/server/routes/get_alert_state.ts
@@ -27,9 +27,6 @@ export const getAlertStateRoute = (router: IRouter, licenseState: LicenseState)
validate: {
params: paramSchema,
},
- options: {
- tags: ['access:alerting-read'],
- },
},
router.handleLegacyErrors(async function (
context: RequestHandlerContext,
diff --git a/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts b/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts
index 3192154f6664c..af20dd6e202ba 100644
--- a/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts
+++ b/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts
@@ -9,6 +9,9 @@ import { httpServiceMock } from 'src/core/server/mocks';
import { mockLicenseState } from '../lib/license_state.mock';
import { verifyApiAccess } from '../lib/license_api_access';
import { mockHandlerArguments } from './_mock_handler_arguments';
+import { alertsClientMock } from '../alerts_client.mock';
+
+const alertsClient = alertsClientMock.create();
jest.mock('../lib/license_api_access.ts', () => ({
verifyApiAccess: jest.fn(),
@@ -28,13 +31,6 @@ describe('listAlertTypesRoute', () => {
const [config, handler] = router.get.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/list_alert_types"`);
- expect(config.options).toMatchInlineSnapshot(`
- Object {
- "tags": Array [
- "access:alerting-read",
- ],
- }
- `);
const listTypes = [
{
@@ -47,12 +43,17 @@ describe('listAlertTypesRoute', () => {
},
],
defaultActionGroupId: 'default',
- actionVariables: [],
+ authorizedConsumers: {},
+ actionVariables: {
+ context: [],
+ state: [],
+ },
producer: 'test',
},
];
+ alertsClient.listAlertTypes.mockResolvedValueOnce(new Set(listTypes));
- const [context, req, res] = mockHandlerArguments({ listTypes }, {}, ['ok']);
+ const [context, req, res] = mockHandlerArguments({ alertsClient }, {}, ['ok']);
expect(await handler(context, req, res)).toMatchInlineSnapshot(`
Object {
@@ -64,7 +65,11 @@ describe('listAlertTypesRoute', () => {
"name": "Default",
},
],
- "actionVariables": Array [],
+ "actionVariables": Object {
+ "context": Array [],
+ "state": Array [],
+ },
+ "authorizedConsumers": Object {},
"defaultActionGroupId": "default",
"id": "1",
"name": "name",
@@ -74,7 +79,7 @@ describe('listAlertTypesRoute', () => {
}
`);
- expect(context.alerting!.listTypes).toHaveBeenCalledTimes(1);
+ expect(alertsClient.listAlertTypes).toHaveBeenCalledTimes(1);
expect(res.ok).toHaveBeenCalledWith({
body: listTypes,
@@ -90,19 +95,11 @@ describe('listAlertTypesRoute', () => {
const [config, handler] = router.get.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/list_alert_types"`);
- expect(config.options).toMatchInlineSnapshot(`
- Object {
- "tags": Array [
- "access:alerting-read",
- ],
- }
- `);
const listTypes = [
{
id: '1',
name: 'name',
- enabled: true,
actionGroups: [
{
id: 'default',
@@ -110,13 +107,19 @@ describe('listAlertTypesRoute', () => {
},
],
defaultActionGroupId: 'default',
- actionVariables: [],
- producer: 'alerting',
+ authorizedConsumers: {},
+ actionVariables: {
+ context: [],
+ state: [],
+ },
+ producer: 'alerts',
},
];
+ alertsClient.listAlertTypes.mockResolvedValueOnce(new Set(listTypes));
+
const [context, req, res] = mockHandlerArguments(
- { listTypes },
+ { alertsClient },
{
params: { id: '1' },
},
@@ -141,13 +144,6 @@ describe('listAlertTypesRoute', () => {
const [config, handler] = router.get.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/list_alert_types"`);
- expect(config.options).toMatchInlineSnapshot(`
- Object {
- "tags": Array [
- "access:alerting-read",
- ],
- }
- `);
const listTypes = [
{
@@ -160,13 +156,19 @@ describe('listAlertTypesRoute', () => {
},
],
defaultActionGroupId: 'default',
- actionVariables: [],
- producer: 'alerting',
+ authorizedConsumers: {},
+ actionVariables: {
+ context: [],
+ state: [],
+ },
+ producer: 'alerts',
},
];
+ alertsClient.listAlertTypes.mockResolvedValueOnce(new Set(listTypes));
+
const [context, req, res] = mockHandlerArguments(
- { listTypes },
+ { alertsClient },
{
params: { id: '1' },
},
diff --git a/x-pack/plugins/alerts/server/routes/list_alert_types.ts b/x-pack/plugins/alerts/server/routes/list_alert_types.ts
index 51a4558108e29..bf516120fbe93 100644
--- a/x-pack/plugins/alerts/server/routes/list_alert_types.ts
+++ b/x-pack/plugins/alerts/server/routes/list_alert_types.ts
@@ -20,9 +20,6 @@ export const listAlertTypesRoute = (router: IRouter, licenseState: LicenseState)
{
path: `${BASE_ALERT_API_PATH}/list_alert_types`,
validate: {},
- options: {
- tags: ['access:alerting-read'],
- },
},
router.handleLegacyErrors(async function (
context: RequestHandlerContext,
@@ -34,7 +31,7 @@ export const listAlertTypesRoute = (router: IRouter, licenseState: LicenseState)
return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' });
}
return res.ok({
- body: context.alerting.listTypes(),
+ body: Array.from(await context.alerting.getAlertsClient().listAlertTypes()),
});
})
);
diff --git a/x-pack/plugins/alerts/server/routes/mute_all.test.ts b/x-pack/plugins/alerts/server/routes/mute_all.test.ts
index bcdb8cbd022ac..efa3cdebad8ff 100644
--- a/x-pack/plugins/alerts/server/routes/mute_all.test.ts
+++ b/x-pack/plugins/alerts/server/routes/mute_all.test.ts
@@ -29,13 +29,6 @@ describe('muteAllAlertRoute', () => {
const [config, handler] = router.post.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_mute_all"`);
- expect(config.options).toMatchInlineSnapshot(`
- Object {
- "tags": Array [
- "access:alerting-all",
- ],
- }
- `);
alertsClient.muteAll.mockResolvedValueOnce();
diff --git a/x-pack/plugins/alerts/server/routes/mute_all.ts b/x-pack/plugins/alerts/server/routes/mute_all.ts
index 5b05d7231c385..6735121d4edb0 100644
--- a/x-pack/plugins/alerts/server/routes/mute_all.ts
+++ b/x-pack/plugins/alerts/server/routes/mute_all.ts
@@ -27,9 +27,6 @@ export const muteAllAlertRoute = (router: IRouter, licenseState: LicenseState) =
validate: {
params: paramSchema,
},
- options: {
- tags: ['access:alerting-all'],
- },
},
router.handleLegacyErrors(async function (
context: RequestHandlerContext,
diff --git a/x-pack/plugins/alerts/server/routes/mute_instance.test.ts b/x-pack/plugins/alerts/server/routes/mute_instance.test.ts
index c382c12de21cd..6e700e4e3fd46 100644
--- a/x-pack/plugins/alerts/server/routes/mute_instance.test.ts
+++ b/x-pack/plugins/alerts/server/routes/mute_instance.test.ts
@@ -31,13 +31,6 @@ describe('muteAlertInstanceRoute', () => {
expect(config.path).toMatchInlineSnapshot(
`"/api/alerts/alert/{alert_id}/alert_instance/{alert_instance_id}/_mute"`
);
- expect(config.options).toMatchInlineSnapshot(`
- Object {
- "tags": Array [
- "access:alerting-all",
- ],
- }
- `);
alertsClient.muteInstance.mockResolvedValueOnce();
diff --git a/x-pack/plugins/alerts/server/routes/mute_instance.ts b/x-pack/plugins/alerts/server/routes/mute_instance.ts
index 00550f4af3418..5e2ffc7d519ed 100644
--- a/x-pack/plugins/alerts/server/routes/mute_instance.ts
+++ b/x-pack/plugins/alerts/server/routes/mute_instance.ts
@@ -30,9 +30,6 @@ export const muteAlertInstanceRoute = (router: IRouter, licenseState: LicenseSta
validate: {
params: paramSchema,
},
- options: {
- tags: ['access:alerting-all'],
- },
},
router.handleLegacyErrors(async function (
context: RequestHandlerContext,
diff --git a/x-pack/plugins/alerts/server/routes/unmute_all.test.ts b/x-pack/plugins/alerts/server/routes/unmute_all.test.ts
index e13af38fe4cb1..81fdc5bb4dd76 100644
--- a/x-pack/plugins/alerts/server/routes/unmute_all.test.ts
+++ b/x-pack/plugins/alerts/server/routes/unmute_all.test.ts
@@ -28,13 +28,6 @@ describe('unmuteAllAlertRoute', () => {
const [config, handler] = router.post.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_unmute_all"`);
- expect(config.options).toMatchInlineSnapshot(`
- Object {
- "tags": Array [
- "access:alerting-all",
- ],
- }
- `);
alertsClient.unmuteAll.mockResolvedValueOnce();
diff --git a/x-pack/plugins/alerts/server/routes/unmute_all.ts b/x-pack/plugins/alerts/server/routes/unmute_all.ts
index 1efc9ed40054e..a987380541696 100644
--- a/x-pack/plugins/alerts/server/routes/unmute_all.ts
+++ b/x-pack/plugins/alerts/server/routes/unmute_all.ts
@@ -27,9 +27,6 @@ export const unmuteAllAlertRoute = (router: IRouter, licenseState: LicenseState)
validate: {
params: paramSchema,
},
- options: {
- tags: ['access:alerting-all'],
- },
},
router.handleLegacyErrors(async function (
context: RequestHandlerContext,
diff --git a/x-pack/plugins/alerts/server/routes/unmute_instance.test.ts b/x-pack/plugins/alerts/server/routes/unmute_instance.test.ts
index b2e2f24e91de9..04e97dbe5e538 100644
--- a/x-pack/plugins/alerts/server/routes/unmute_instance.test.ts
+++ b/x-pack/plugins/alerts/server/routes/unmute_instance.test.ts
@@ -31,13 +31,6 @@ describe('unmuteAlertInstanceRoute', () => {
expect(config.path).toMatchInlineSnapshot(
`"/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute"`
);
- expect(config.options).toMatchInlineSnapshot(`
- Object {
- "tags": Array [
- "access:alerting-all",
- ],
- }
- `);
alertsClient.unmuteInstance.mockResolvedValueOnce();
diff --git a/x-pack/plugins/alerts/server/routes/unmute_instance.ts b/x-pack/plugins/alerts/server/routes/unmute_instance.ts
index 967f9f890c9fb..15b882e585804 100644
--- a/x-pack/plugins/alerts/server/routes/unmute_instance.ts
+++ b/x-pack/plugins/alerts/server/routes/unmute_instance.ts
@@ -28,9 +28,6 @@ export const unmuteAlertInstanceRoute = (router: IRouter, licenseState: LicenseS
validate: {
params: paramSchema,
},
- options: {
- tags: ['access:alerting-all'],
- },
},
router.handleLegacyErrors(async function (
context: RequestHandlerContext,
diff --git a/x-pack/plugins/alerts/server/routes/update.test.ts b/x-pack/plugins/alerts/server/routes/update.test.ts
index c7d23f2670b45..dedb08a9972c2 100644
--- a/x-pack/plugins/alerts/server/routes/update.test.ts
+++ b/x-pack/plugins/alerts/server/routes/update.test.ts
@@ -52,13 +52,6 @@ describe('updateAlertRoute', () => {
const [config, handler] = router.put.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}"`);
- expect(config.options).toMatchInlineSnapshot(`
- Object {
- "tags": Array [
- "access:alerting-all",
- ],
- }
- `);
alertsClient.update.mockResolvedValueOnce(mockedResponse);
diff --git a/x-pack/plugins/alerts/server/routes/update.ts b/x-pack/plugins/alerts/server/routes/update.ts
index 99b81dfc5b56e..9b2fe9a43810b 100644
--- a/x-pack/plugins/alerts/server/routes/update.ts
+++ b/x-pack/plugins/alerts/server/routes/update.ts
@@ -49,9 +49,6 @@ export const updateAlertRoute = (router: IRouter, licenseState: LicenseState) =>
body: bodySchema,
params: paramSchema,
},
- options: {
- tags: ['access:alerting-all'],
- },
},
handleDisabledApiKeysError(
router.handleLegacyErrors(async function (
diff --git a/x-pack/plugins/alerts/server/routes/update_api_key.test.ts b/x-pack/plugins/alerts/server/routes/update_api_key.test.ts
index babae59553b5b..5aa91d215be90 100644
--- a/x-pack/plugins/alerts/server/routes/update_api_key.test.ts
+++ b/x-pack/plugins/alerts/server/routes/update_api_key.test.ts
@@ -29,13 +29,6 @@ describe('updateApiKeyRoute', () => {
const [config, handler] = router.post.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_update_api_key"`);
- expect(config.options).toMatchInlineSnapshot(`
- Object {
- "tags": Array [
- "access:alerting-all",
- ],
- }
- `);
alertsClient.updateApiKey.mockResolvedValueOnce();
diff --git a/x-pack/plugins/alerts/server/routes/update_api_key.ts b/x-pack/plugins/alerts/server/routes/update_api_key.ts
index 4736351a25cbd..d44649b05b929 100644
--- a/x-pack/plugins/alerts/server/routes/update_api_key.ts
+++ b/x-pack/plugins/alerts/server/routes/update_api_key.ts
@@ -28,9 +28,6 @@ export const updateApiKeyRoute = (router: IRouter, licenseState: LicenseState) =
validate: {
params: paramSchema,
},
- options: {
- tags: ['access:alerting-all'],
- },
},
handleDisabledApiKeysError(
router.handleLegacyErrors(async function (
diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts
index 38cda5a9a0f7c..19f4e918b7862 100644
--- a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts
+++ b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts
@@ -47,6 +47,40 @@ describe('7.9.0', () => {
});
});
+describe('7.10.0', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ encryptedSavedObjectsSetup.createMigration.mockImplementation(
+ (shouldMigrateWhenPredicate, migration) => migration
+ );
+ });
+
+ test('changes nothing on alerts by other plugins', () => {
+ const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0'];
+ const alert = getMockData({});
+ expect(migration710(alert, { log })).toMatchObject(alert);
+
+ expect(encryptedSavedObjectsSetup.createMigration).toHaveBeenCalledWith(
+ expect.any(Function),
+ expect.any(Function)
+ );
+ });
+
+ test('migrates the consumer for metrics', () => {
+ const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0'];
+ const alert = getMockData({
+ consumer: 'metrics',
+ });
+ expect(migration710(alert, { log })).toMatchObject({
+ ...alert,
+ attributes: {
+ ...alert.attributes,
+ consumer: 'infrastructure',
+ },
+ });
+ });
+});
+
function getMockData(
overwrites: Record = {}
): SavedObjectUnsanitizedDoc {
diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.ts
index 142102dd711c7..57a4005887093 100644
--- a/x-pack/plugins/alerts/server/saved_objects/migrations.ts
+++ b/x-pack/plugins/alerts/server/saved_objects/migrations.ts
@@ -15,23 +15,27 @@ export function getMigrations(
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup
): SavedObjectMigrationMap {
return {
- '7.9.0': changeAlertingConsumer(encryptedSavedObjects),
+ /**
+ * In v7.9.0 we changed the Alerting plugin so it uses the `consumer` value of `alerts`
+ * prior to that we were using `alerting` and we need to keep these in sync
+ */
+ '7.9.0': changeAlertingConsumer(encryptedSavedObjects, 'alerting', 'alerts'),
+ /**
+ * In v7.10.0 we changed the Matrics plugin so it uses the `consumer` value of `infrastructure`
+ * prior to that we were using `metrics` and we need to keep these in sync
+ */
+ '7.10.0': changeAlertingConsumer(encryptedSavedObjects, 'metrics', 'infrastructure'),
};
}
-/**
- * In v7.9.0 we changed the Alerting plugin so it uses the `consumer` value of `alerts`
- * prior to that we were using `alerting` and we need to keep these in sync
- */
function changeAlertingConsumer(
- encryptedSavedObjects: EncryptedSavedObjectsPluginSetup
+ encryptedSavedObjects: EncryptedSavedObjectsPluginSetup,
+ from: string,
+ to: string
): SavedObjectMigrationFn {
- const consumerMigration = new Map();
- consumerMigration.set('alerting', 'alerts');
-
return encryptedSavedObjects.createMigration(
function shouldBeMigrated(doc): doc is SavedObjectUnsanitizedDoc {
- return consumerMigration.has(doc.attributes.consumer);
+ return doc.attributes.consumer === from;
},
(doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc => {
const {
@@ -41,7 +45,7 @@ function changeAlertingConsumer(
...doc,
attributes: {
...doc.attributes,
- consumer: consumerMigration.get(consumer) ?? consumer,
+ consumer: consumer === from ? to : consumer,
},
};
}
diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts
index 3b1948c5e7ad7..3ea40fe4c3086 100644
--- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts
+++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts
@@ -20,7 +20,7 @@ const alertType: AlertType = {
],
defaultActionGroupId: 'default',
executor: jest.fn(),
- producer: 'alerting',
+ producer: 'alerts',
};
const actionsClient = actionsClientMock.create();
diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts
index 7a031c6671fd0..4abe58de5a904 100644
--- a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts
+++ b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts
@@ -14,7 +14,7 @@ import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/serv
import { loggingSystemMock } from '../../../../../src/core/server/mocks';
import { PluginStartContract as ActionsPluginStart } from '../../../actions/server';
import { actionsMock, actionsClientMock } from '../../../actions/server/mocks';
-import { alertsMock } from '../mocks';
+import { alertsMock, alertsClientMock } from '../mocks';
import { eventLoggerMock } from '../../../event_log/server/event_logger.mock';
import { IEventLogger } from '../../../event_log/server';
import { SavedObjectsErrorHelpers } from '../../../../../src/core/server';
@@ -25,7 +25,7 @@ const alertType = {
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
executor: jest.fn(),
- producer: 'alerting',
+ producer: 'alerts',
};
let fakeTimer: sinon.SinonFakeTimers;
@@ -56,8 +56,8 @@ describe('Task Runner', () => {
const encryptedSavedObjectsClient = encryptedSavedObjectsMock.createClient();
const services = alertsMock.createAlertServices();
- const savedObjectsClient = services.savedObjectsClient;
const actionsClient = actionsClientMock.create();
+ const alertsClient = alertsClientMock.create();
const taskRunnerFactoryInitializerParams: jest.Mocked & {
actionsPlugin: jest.Mocked;
@@ -65,6 +65,7 @@ describe('Task Runner', () => {
} = {
getServices: jest.fn().mockReturnValue(services),
actionsPlugin: actionsMock.createStart(),
+ getAlertsClientWithRequest: jest.fn().mockReturnValue(alertsClient),
encryptedSavedObjectsClient,
logger: loggingSystemMock.create().get(),
spaceIdToNamespace: jest.fn().mockReturnValue(undefined),
@@ -74,34 +75,31 @@ describe('Task Runner', () => {
const mockedAlertTypeSavedObject = {
id: '1',
- type: 'alert',
- attributes: {
- enabled: true,
- alertTypeId: '123',
- schedule: { interval: '10s' },
- name: 'alert-name',
- tags: ['alert-', '-tags'],
- createdBy: 'alert-creator',
- updatedBy: 'alert-updater',
- mutedInstanceIds: [],
- params: {
- bar: true,
- },
- actions: [
- {
- group: 'default',
- actionRef: 'action_0',
- params: {
- foo: true,
- },
- },
- ],
+ consumer: 'bar',
+ createdAt: new Date('2019-02-12T21:01:22.479Z'),
+ updatedAt: new Date('2019-02-12T21:01:22.479Z'),
+ throttle: null,
+ muteAll: false,
+ enabled: true,
+ alertTypeId: '123',
+ apiKeyOwner: 'elastic',
+ schedule: { interval: '10s' },
+ name: 'alert-name',
+ tags: ['alert-', '-tags'],
+ createdBy: 'alert-creator',
+ updatedBy: 'alert-updater',
+ mutedInstanceIds: [],
+ params: {
+ bar: true,
},
- references: [
+ actions: [
{
- name: 'action_0',
- type: 'action',
+ group: 'default',
id: '1',
+ actionTypeId: 'action',
+ params: {
+ foo: true,
+ },
},
],
};
@@ -109,6 +107,7 @@ describe('Task Runner', () => {
beforeEach(() => {
jest.resetAllMocks();
taskRunnerFactoryInitializerParams.getServices.mockReturnValue(services);
+ taskRunnerFactoryInitializerParams.getAlertsClientWithRequest.mockReturnValue(alertsClient);
taskRunnerFactoryInitializerParams.actionsPlugin.getActionsClientWithRequest.mockResolvedValue(
actionsClient
);
@@ -126,7 +125,7 @@ describe('Task Runner', () => {
},
taskRunnerFactoryInitializerParams
);
- savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
+ alertsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({
id: '1',
type: 'alert',
@@ -200,7 +199,7 @@ describe('Task Runner', () => {
mockedTaskInstance,
taskRunnerFactoryInitializerParams
);
- savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
+ alertsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({
id: '1',
type: 'alert',
@@ -285,7 +284,7 @@ describe('Task Runner', () => {
],
},
message:
- "alert: test:1: 'alert-name' instanceId: '1' scheduled actionGroup: 'default' action: undefined:1",
+ "alert: test:1: 'alert-name' instanceId: '1' scheduled actionGroup: 'default' action: action:1",
});
});
@@ -302,7 +301,7 @@ describe('Task Runner', () => {
mockedTaskInstance,
taskRunnerFactoryInitializerParams
);
- savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
+ alertsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({
id: '1',
type: 'alert',
@@ -412,7 +411,7 @@ describe('Task Runner', () => {
},
],
},
- "message": "alert: test:1: 'alert-name' instanceId: '1' scheduled actionGroup: 'default' action: undefined:1",
+ "message": "alert: test:1: 'alert-name' instanceId: '1' scheduled actionGroup: 'default' action: action:1",
},
],
]
@@ -439,7 +438,7 @@ describe('Task Runner', () => {
},
taskRunnerFactoryInitializerParams
);
- savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
+ alertsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({
id: '1',
type: 'alert',
@@ -526,7 +525,7 @@ describe('Task Runner', () => {
mockedTaskInstance,
taskRunnerFactoryInitializerParams
);
- savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
+ alertsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({
id: '1',
type: 'alert',
@@ -548,44 +547,13 @@ describe('Task Runner', () => {
);
});
- test('throws error if reference not found', async () => {
- const taskRunner = new TaskRunner(
- alertType,
- mockedTaskInstance,
- taskRunnerFactoryInitializerParams
- );
- savedObjectsClient.get.mockResolvedValueOnce({
- ...mockedAlertTypeSavedObject,
- references: [],
- });
- encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({
- id: '1',
- type: 'alert',
- attributes: {
- apiKey: Buffer.from('123:abc').toString('base64'),
- },
- references: [],
- });
- expect(await taskRunner.run()).toMatchInlineSnapshot(`
- Object {
- "runAt": 1970-01-01T00:00:10.000Z,
- "state": Object {
- "previousStartedAt": 1970-01-01T00:00:00.000Z,
- },
- }
- `);
- expect(taskRunnerFactoryInitializerParams.logger.error).toHaveBeenCalledWith(
- `Executing Alert \"1\" has resulted in Error: Action reference \"action_0\" not found in alert id: 1`
- );
- });
-
test('uses API key when provided', async () => {
const taskRunner = new TaskRunner(
alertType,
mockedTaskInstance,
taskRunnerFactoryInitializerParams
);
- savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
+ alertsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({
id: '1',
type: 'alert',
@@ -621,7 +589,7 @@ describe('Task Runner', () => {
mockedTaskInstance,
taskRunnerFactoryInitializerParams
);
- savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
+ alertsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({
id: '1',
type: 'alert',
@@ -660,7 +628,7 @@ describe('Task Runner', () => {
taskRunnerFactoryInitializerParams
);
- savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
+ alertsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({
id: '1',
type: 'alert',
@@ -722,7 +690,7 @@ describe('Task Runner', () => {
taskRunnerFactoryInitializerParams
);
- savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
+ alertsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
const runnerResult = await taskRunner.run();
@@ -747,7 +715,7 @@ describe('Task Runner', () => {
taskRunnerFactoryInitializerParams
);
- savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
+ alertsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({
id: '1',
type: 'alert',
@@ -770,7 +738,7 @@ describe('Task Runner', () => {
});
test('recovers gracefully when the Alert Task Runner throws an exception when fetching attributes', async () => {
- savedObjectsClient.get.mockImplementation(() => {
+ alertsClient.get.mockImplementation(() => {
throw new Error('OMG');
});
@@ -802,7 +770,7 @@ describe('Task Runner', () => {
});
test('avoids rescheduling a failed Alert Task Runner when it throws due to failing to fetch the alert', async () => {
- savedObjectsClient.get.mockImplementation(() => {
+ alertsClient.get.mockImplementation(() => {
throw SavedObjectsErrorHelpers.createGenericNotFoundError('task', '1');
});
diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts
index 3c66b57bb9416..e4d04a005c986 100644
--- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts
+++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts
@@ -5,7 +5,7 @@
*/
import { pickBy, mapValues, omit, without } from 'lodash';
-import { Logger, SavedObject, KibanaRequest } from '../../../../../src/core/server';
+import { Logger, KibanaRequest } from '../../../../../src/core/server';
import { TaskRunnerContext } from './task_runner_factory';
import { ConcreteTaskInstance } from '../../../task_manager/server';
import { createExecutionHandler } from './create_execution_handler';
@@ -17,15 +17,18 @@ import {
RawAlert,
IntervalSchedule,
Services,
- AlertInfoParams,
- AlertTaskState,
RawAlertInstance,
+ AlertTaskState,
+ Alert,
+ AlertExecutorOptions,
+ SanitizedAlert,
} from '../types';
import { promiseResult, map, Resultable, asOk, asErr, resolveErr } from '../lib/result_type';
import { taskInstanceToAlertTaskInstance } from './alert_task_instance';
import { EVENT_LOG_ACTIONS } from '../plugin';
import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server';
import { isAlertSavedObjectNotFoundError } from '../lib/is_alert_not_found_error';
+import { AlertsClient } from '../alerts_client';
const FALLBACK_RETRY_INTERVAL: IntervalSchedule = { interval: '5m' };
@@ -93,8 +96,12 @@ export class TaskRunner {
} as unknown) as KibanaRequest;
}
- async getServicesWithSpaceLevelPermissions(spaceId: string, apiKey: string | null) {
- return this.context.getServices(this.getFakeKibanaRequest(spaceId, apiKey));
+ private getServicesWithSpaceLevelPermissions(
+ spaceId: string,
+ apiKey: string | null
+ ): [Services, PublicMethodsOf] {
+ const request = this.getFakeKibanaRequest(spaceId, apiKey);
+ return [this.context.getServices(request), this.context.getAlertsClientWithRequest(request)];
}
private getExecutionHandler(
@@ -103,21 +110,8 @@ export class TaskRunner {
tags: string[] | undefined,
spaceId: string,
apiKey: string | null,
- actions: RawAlert['actions'],
- references: SavedObject['references']
+ actions: Alert['actions']
) {
- // Inject ids into actions
- const actionsWithIds = actions.map((action) => {
- const actionReference = references.find((obj) => obj.name === action.actionRef);
- if (!actionReference) {
- throw new Error(`Action reference "${action.actionRef}" not found in alert id: ${alertId}`);
- }
- return {
- ...action,
- id: actionReference.id,
- };
- });
-
return createExecutionHandler({
alertId,
alertName,
@@ -125,7 +119,7 @@ export class TaskRunner {
logger: this.logger,
actionsPlugin: this.context.actionsPlugin,
apiKey,
- actions: actionsWithIds,
+ actions,
spaceId,
alertType: this.alertType,
eventLogger: this.context.eventLogger,
@@ -146,20 +140,12 @@ export class TaskRunner {
async executeAlertInstances(
services: Services,
- alertInfoParams: AlertInfoParams,
+ alert: SanitizedAlert,
+ params: AlertExecutorOptions['params'],
executionHandler: ReturnType,
spaceId: string
): Promise {
- const {
- params,
- throttle,
- muteAll,
- mutedInstanceIds,
- name,
- tags,
- createdBy,
- updatedBy,
- } = alertInfoParams;
+ const { throttle, muteAll, mutedInstanceIds, name, tags, createdBy, updatedBy } = alert;
const {
params: { alertId },
state: { alertInstances: alertRawInstances = {}, alertTypeState = {}, previousStartedAt },
@@ -262,33 +248,22 @@ export class TaskRunner {
};
}
- async validateAndExecuteAlert(
- services: Services,
- apiKey: string | null,
- attributes: RawAlert,
- references: SavedObject['references']
- ) {
+ async validateAndExecuteAlert(services: Services, apiKey: string | null, alert: SanitizedAlert) {
const {
params: { alertId, spaceId },
} = this.taskInstance;
// Validate
- const params = validateAlertTypeParams(this.alertType, attributes.params);
+ const validatedParams = validateAlertTypeParams(this.alertType, alert.params);
const executionHandler = this.getExecutionHandler(
alertId,
- attributes.name,
- attributes.tags,
+ alert.name,
+ alert.tags,
spaceId,
apiKey,
- attributes.actions,
- references
- );
- return this.executeAlertInstances(
- services,
- { ...attributes, params },
- executionHandler,
- spaceId
+ alert.actions
);
+ return this.executeAlertInstances(services, alert, validatedParams, executionHandler, spaceId);
}
async loadAlertAttributesAndRun(): Promise> {
@@ -297,17 +272,17 @@ export class TaskRunner {
} = this.taskInstance;
const apiKey = await this.getApiKeyForAlertPermissions(alertId, spaceId);
- const services = await this.getServicesWithSpaceLevelPermissions(spaceId, apiKey);
+ const [services, alertsClient] = await this.getServicesWithSpaceLevelPermissions(
+ spaceId,
+ apiKey
+ );
// Ensure API key is still valid and user has access
- const { attributes, references } = await services.savedObjectsClient.get(
- 'alert',
- alertId
- );
+ const alert = await alertsClient.get({ id: alertId });
return {
state: await promiseResult(
- this.validateAndExecuteAlert(services, apiKey, attributes, references)
+ this.validateAndExecuteAlert(services, apiKey, alert)
),
runAt: asOk(
getNextRunAt(
@@ -315,7 +290,7 @@ export class TaskRunner {
// we do not currently have a good way of returning the type
// from SavedObjectsClient, and as we currenrtly require a schedule
// and we only support `interval`, we can cast this safely
- attributes.schedule as IntervalSchedule
+ alert.schedule
)
),
};
diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts
index 8f3e44b1cf42d..9af7d9ddc44eb 100644
--- a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts
+++ b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts
@@ -10,7 +10,7 @@ import { TaskRunnerContext, TaskRunnerFactory } from './task_runner_factory';
import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks';
import { loggingSystemMock } from '../../../../../src/core/server/mocks';
import { actionsMock } from '../../../actions/server/mocks';
-import { alertsMock } from '../mocks';
+import { alertsMock, alertsClientMock } from '../mocks';
import { eventLoggerMock } from '../../../event_log/server/event_logger.mock';
const alertType = {
@@ -19,7 +19,7 @@ const alertType = {
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
executor: jest.fn(),
- producer: 'alerting',
+ producer: 'alerts',
};
let fakeTimer: sinon.SinonFakeTimers;
@@ -52,9 +52,11 @@ describe('Task Runner Factory', () => {
const encryptedSavedObjectsPlugin = encryptedSavedObjectsMock.createStart();
const services = alertsMock.createAlertServices();
+ const alertsClient = alertsClientMock.create();
const taskRunnerFactoryInitializerParams: jest.Mocked = {
getServices: jest.fn().mockReturnValue(services),
+ getAlertsClientWithRequest: jest.fn().mockReturnValue(alertsClient),
actionsPlugin: actionsMock.createStart(),
encryptedSavedObjectsClient: encryptedSavedObjectsPlugin.getClient(),
logger: loggingSystemMock.create().get(),
diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts
index ca762cf2b2105..6f83e34cdbe03 100644
--- a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts
+++ b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts
@@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { Logger } from '../../../../../src/core/server';
+import { Logger, KibanaRequest } from '../../../../../src/core/server';
import { RunContext } from '../../../task_manager/server';
import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server';
import { PluginStartContract as ActionsPluginStartContract } from '../../../actions/server';
@@ -15,10 +15,12 @@ import {
} from '../types';
import { TaskRunner } from './task_runner';
import { IEventLogger } from '../../../event_log/server';
+import { AlertsClient } from '../alerts_client';
export interface TaskRunnerContext {
logger: Logger;
getServices: GetServicesFunction;
+ getAlertsClientWithRequest(request: KibanaRequest): PublicMethodsOf;
actionsPlugin: ActionsPluginStartContract;
eventLogger: IEventLogger;
encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts
index 80f722bae0868..38e75f75ad04b 100644
--- a/x-pack/plugins/apm/server/feature.ts
+++ b/x-pack/plugins/apm/server/feature.ts
@@ -5,6 +5,7 @@
*/
import { i18n } from '@kbn/i18n';
+import { AlertType } from '../common/alert_types';
export const APM_FEATURE = {
id: 'apm',
@@ -16,57 +17,34 @@ export const APM_FEATURE = {
navLinkId: 'apm',
app: ['apm', 'kibana'],
catalogue: ['apm'],
+ alerting: Object.values(AlertType),
// see x-pack/plugins/features/common/feature_kibana_privileges.ts
privileges: {
all: {
app: ['apm', 'kibana'],
- api: [
- 'apm',
- 'apm_write',
- 'actions-read',
- 'actions-all',
- 'alerting-read',
- 'alerting-all',
- ],
+ api: ['apm', 'apm_write'],
catalogue: ['apm'],
savedObject: {
- all: ['alert', 'action', 'action_task_params'],
+ all: [],
read: [],
},
- ui: [
- 'show',
- 'save',
- 'alerting:show',
- 'actions:show',
- 'alerting:save',
- 'actions:save',
- 'alerting:delete',
- 'actions:delete',
- ],
+ alerting: {
+ all: Object.values(AlertType),
+ },
+ ui: ['show', 'save', 'alerting:show', 'alerting:save'],
},
read: {
app: ['apm', 'kibana'],
- api: [
- 'apm',
- 'actions-read',
- 'actions-all',
- 'alerting-read',
- 'alerting-all',
- ],
+ api: ['apm'],
catalogue: ['apm'],
savedObject: {
- all: ['alert', 'action', 'action_task_params'],
+ all: [],
read: [],
},
- ui: [
- 'show',
- 'alerting:show',
- 'actions:show',
- 'alerting:save',
- 'actions:save',
- 'alerting:delete',
- 'actions:delete',
- ],
+ alerting: {
+ all: Object.values(AlertType),
+ },
+ ui: ['show', 'alerting:show', 'alerting:save'],
},
},
};
diff --git a/x-pack/plugins/features/common/feature.ts b/x-pack/plugins/features/common/feature.ts
index 4a293e0c962cc..1b700fb1a6ad0 100644
--- a/x-pack/plugins/features/common/feature.ts
+++ b/x-pack/plugins/features/common/feature.ts
@@ -94,6 +94,13 @@ export interface FeatureConfig {
*/
catalogue?: readonly string[];
+ /**
+ * If your feature grants access to specific Alert Types, you can specify them here to control visibility based on the current space.
+ * Include both Alert Types registered by the feature and external Alert Types such as built-in
+ * Alert Types and Alert Types provided by other features to which you wish to grant access.
+ */
+ alerting?: readonly string[];
+
/**
* Feature privilege definition.
*
@@ -179,6 +186,10 @@ export class Feature {
return this.config.privileges;
}
+ public get alerting() {
+ return this.config.alerting;
+ }
+
public get excludeFromBasePrivileges() {
return this.config.excludeFromBasePrivileges ?? false;
}
diff --git a/x-pack/plugins/features/common/feature_kibana_privileges.ts b/x-pack/plugins/features/common/feature_kibana_privileges.ts
index a9ba38e36f20b..c8faf75b348fd 100644
--- a/x-pack/plugins/features/common/feature_kibana_privileges.ts
+++ b/x-pack/plugins/features/common/feature_kibana_privileges.ts
@@ -75,6 +75,34 @@ export interface FeatureKibanaPrivileges {
*/
app?: readonly string[];
+ /**
+ * If your feature requires access to specific Alert Types, then specify your access needs here.
+ * Include both Alert Types registered by the feature and external Alert Types such as built-in
+ * Alert Types and Alert Types provided by other features to which you wish to grant access.
+ */
+ alerting?: {
+ /**
+ * List of alert types which users should have full read/write access to when granted this privilege.
+ * @example
+ * ```ts
+ * {
+ * all: ['my-alert-type-within-my-feature']
+ * }
+ * ```
+ */
+ all?: readonly string[];
+
+ /**
+ * List of alert types which users should have read-only access to when granted this privilege.
+ * @example
+ * ```ts
+ * {
+ * read: ['my-alert-type']
+ * }
+ * ```
+ */
+ read?: readonly string[];
+ };
/**
* If your feature requires access to specific saved objects, then specify your access needs here.
*/
diff --git a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap
index fe0a13fe702e5..2c98dc132f259 100644
--- a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap
+++ b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap
@@ -55,6 +55,10 @@ exports[`buildOSSFeatures returns the dashboard feature augmented with appropria
Array [
Object {
"privilege": Object {
+ "alerting": Object {
+ "all": Array [],
+ "read": Array [],
+ },
"api": Array [],
"app": Array [
"dashboards",
@@ -179,6 +183,10 @@ exports[`buildOSSFeatures returns the discover feature augmented with appropriat
Array [
Object {
"privilege": Object {
+ "alerting": Object {
+ "all": Array [],
+ "read": Array [],
+ },
"api": Array [],
"app": Array [
"discover",
@@ -403,6 +411,10 @@ exports[`buildOSSFeatures returns the visualize feature augmented with appropria
Array [
Object {
"privilege": Object {
+ "alerting": Object {
+ "all": Array [],
+ "read": Array [],
+ },
"api": Array [],
"app": Array [
"visualize",
diff --git a/x-pack/plugins/features/server/feature_registry.test.ts b/x-pack/plugins/features/server/feature_registry.test.ts
index 75022922917b3..f123068e41758 100644
--- a/x-pack/plugins/features/server/feature_registry.test.ts
+++ b/x-pack/plugins/features/server/feature_registry.test.ts
@@ -743,6 +743,168 @@ describe('FeatureRegistry', () => {
);
});
+ it(`prevents privileges from specifying alerting entries that don't exist at the root level`, () => {
+ const feature: FeatureConfig = {
+ id: 'test-feature',
+ name: 'Test Feature',
+ app: [],
+ alerting: ['bar'],
+ privileges: {
+ all: {
+ alerting: {
+ all: ['foo', 'bar'],
+ read: ['baz'],
+ },
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ ui: [],
+ app: [],
+ },
+ read: {
+ alerting: { read: ['foo', 'bar', 'baz'] },
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ ui: [],
+ app: [],
+ },
+ },
+ };
+
+ const featureRegistry = new FeatureRegistry();
+
+ expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot(
+ `"Feature privilege test-feature.all has unknown alerting entries: foo, baz"`
+ );
+ });
+
+ it(`prevents features from specifying alerting entries that don't exist at the privilege level`, () => {
+ const feature: FeatureConfig = {
+ id: 'test-feature',
+ name: 'Test Feature',
+ app: [],
+ alerting: ['foo', 'bar', 'baz'],
+ privileges: {
+ all: {
+ alerting: { all: ['foo'] },
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ ui: [],
+ app: [],
+ },
+ read: {
+ alerting: { all: ['foo'] },
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ ui: [],
+ app: [],
+ },
+ },
+ subFeatures: [
+ {
+ name: 'my sub feature',
+ privilegeGroups: [
+ {
+ groupType: 'independent',
+ privileges: [
+ {
+ id: 'cool-sub-feature-privilege',
+ name: 'cool privilege',
+ includeIn: 'none',
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ ui: [],
+ alerting: { all: ['bar'] },
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ };
+
+ const featureRegistry = new FeatureRegistry();
+
+ expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot(
+ `"Feature test-feature specifies alerting entries which are not granted to any privileges: baz"`
+ );
+ });
+
+ it(`prevents reserved privileges from specifying alerting entries that don't exist at the root level`, () => {
+ const feature: FeatureConfig = {
+ id: 'test-feature',
+ name: 'Test Feature',
+ app: [],
+ alerting: ['bar'],
+ privileges: null,
+ reserved: {
+ description: 'something',
+ privileges: [
+ {
+ id: 'reserved',
+ privilege: {
+ alerting: { all: ['foo', 'bar', 'baz'] },
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ ui: [],
+ app: [],
+ },
+ },
+ ],
+ },
+ };
+
+ const featureRegistry = new FeatureRegistry();
+
+ expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot(
+ `"Feature privilege test-feature.reserved has unknown alerting entries: foo, baz"`
+ );
+ });
+
+ it(`prevents features from specifying alerting entries that don't exist at the reserved privilege level`, () => {
+ const feature: FeatureConfig = {
+ id: 'test-feature',
+ name: 'Test Feature',
+ app: [],
+ alerting: ['foo', 'bar', 'baz'],
+ privileges: null,
+ reserved: {
+ description: 'something',
+ privileges: [
+ {
+ id: 'reserved',
+ privilege: {
+ alerting: { all: ['foo', 'bar'] },
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ ui: [],
+ app: [],
+ },
+ },
+ ],
+ },
+ };
+
+ const featureRegistry = new FeatureRegistry();
+
+ expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot(
+ `"Feature test-feature specifies alerting entries which are not granted to any privileges: baz"`
+ );
+ });
+
it(`prevents privileges from specifying management sections that don't exist at the root level`, () => {
const feature: FeatureConfig = {
id: 'test-feature',
diff --git a/x-pack/plugins/features/server/feature_schema.ts b/x-pack/plugins/features/server/feature_schema.ts
index c45788b511cde..95298603d706a 100644
--- a/x-pack/plugins/features/server/feature_schema.ts
+++ b/x-pack/plugins/features/server/feature_schema.ts
@@ -26,6 +26,7 @@ const managementSchema = Joi.object().pattern(
Joi.array().items(Joi.string().regex(uiCapabilitiesRegex))
);
const catalogueSchema = Joi.array().items(Joi.string().regex(uiCapabilitiesRegex));
+const alertingSchema = Joi.array().items(Joi.string());
const privilegeSchema = Joi.object({
excludeFromBasePrivileges: Joi.boolean(),
@@ -33,6 +34,10 @@ const privilegeSchema = Joi.object({
catalogue: catalogueSchema,
api: Joi.array().items(Joi.string()),
app: Joi.array().items(Joi.string()),
+ alerting: Joi.object({
+ all: alertingSchema,
+ read: alertingSchema,
+ }),
savedObject: Joi.object({
all: Joi.array().items(Joi.string()).required(),
read: Joi.array().items(Joi.string()).required(),
@@ -46,6 +51,10 @@ const subFeaturePrivilegeSchema = Joi.object({
includeIn: Joi.string().allow('all', 'read', 'none').required(),
management: managementSchema,
catalogue: catalogueSchema,
+ alerting: Joi.object({
+ all: alertingSchema,
+ read: alertingSchema,
+ }),
api: Joi.array().items(Joi.string()),
app: Joi.array().items(Joi.string()),
savedObject: Joi.object({
@@ -82,6 +91,7 @@ const schema = Joi.object({
app: Joi.array().items(Joi.string()).required(),
management: managementSchema,
catalogue: catalogueSchema,
+ alerting: alertingSchema,
privileges: Joi.object({
all: privilegeSchema,
read: privilegeSchema,
@@ -113,7 +123,7 @@ export function validateFeature(feature: FeatureConfig) {
throw validateResult.error;
}
// the following validation can't be enforced by the Joi schema, since it'd require us looking "up" the object graph for the list of valid value, which they explicitly forbid.
- const { app = [], management = {}, catalogue = [] } = feature;
+ const { app = [], management = {}, catalogue = [], alerting = [] } = feature;
const unseenApps = new Set(app);
@@ -126,6 +136,8 @@ export function validateFeature(feature: FeatureConfig) {
const unseenCatalogue = new Set(catalogue);
+ const unseenAlertTypes = new Set(alerting);
+
function validateAppEntry(privilegeId: string, entry: readonly string[] = []) {
entry.forEach((privilegeApp) => unseenApps.delete(privilegeApp));
@@ -152,6 +164,23 @@ export function validateFeature(feature: FeatureConfig) {
}
}
+ function validateAlertingEntry(privilegeId: string, entry: FeatureKibanaPrivileges['alerting']) {
+ const all = entry?.all ?? [];
+ const read = entry?.read ?? [];
+
+ all.forEach((privilegeAlertTypes) => unseenAlertTypes.delete(privilegeAlertTypes));
+ read.forEach((privilegeAlertTypes) => unseenAlertTypes.delete(privilegeAlertTypes));
+
+ const unknownAlertingEntries = difference([...all, ...read], alerting);
+ if (unknownAlertingEntries.length > 0) {
+ throw new Error(
+ `Feature privilege ${
+ feature.id
+ }.${privilegeId} has unknown alerting entries: ${unknownAlertingEntries.join(', ')}`
+ );
+ }
+ }
+
function validateManagementEntry(
privilegeId: string,
managementEntry: Record = {}
@@ -212,6 +241,7 @@ export function validateFeature(feature: FeatureConfig) {
validateCatalogueEntry(privilegeId, privilegeDefinition.catalogue);
validateManagementEntry(privilegeId, privilegeDefinition.management);
+ validateAlertingEntry(privilegeId, privilegeDefinition.alerting);
});
const subFeatureEntries = feature.subFeatures ?? [];
@@ -221,6 +251,7 @@ export function validateFeature(feature: FeatureConfig) {
validateAppEntry(subFeaturePrivilege.id, subFeaturePrivilege.app);
validateCatalogueEntry(subFeaturePrivilege.id, subFeaturePrivilege.catalogue);
validateManagementEntry(subFeaturePrivilege.id, subFeaturePrivilege.management);
+ validateAlertingEntry(subFeaturePrivilege.id, subFeaturePrivilege.alerting);
});
});
});
@@ -261,4 +292,14 @@ export function validateFeature(feature: FeatureConfig) {
)}`
);
}
+
+ if (unseenAlertTypes.size > 0) {
+ throw new Error(
+ `Feature ${
+ feature.id
+ } specifies alerting entries which are not granted to any privileges: ${Array.from(
+ unseenAlertTypes.values()
+ ).join(',')}`
+ );
+ }
}
diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx
index 7e85a2bdf7e9b..804ff9602c81c 100644
--- a/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx
+++ b/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx
@@ -44,7 +44,7 @@ export const AlertFlyout = (props: Props) => {
setAddFlyoutVisibility={props.setVisible}
alertTypeId={METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID}
canChangeTrigger={false}
- consumer={'metrics'}
+ consumer={'infrastructure'}
/>
)}
diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx
index b0c8cdb9d4195..b19a399b0e50d 100644
--- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx
+++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx
@@ -46,7 +46,7 @@ export const AlertFlyout = (props: Props) => {
setAddFlyoutVisibility={props.setVisible}
alertTypeId={METRIC_THRESHOLD_ALERT_TYPE_ID}
canChangeTrigger={false}
- consumer={'metrics'}
+ consumer={'infrastructure'}
/>
)}
diff --git a/x-pack/plugins/infra/server/features.ts b/x-pack/plugins/infra/server/features.ts
index fa228e03194a9..0de431186b151 100644
--- a/x-pack/plugins/infra/server/features.ts
+++ b/x-pack/plugins/infra/server/features.ts
@@ -5,6 +5,9 @@
*/
import { i18n } from '@kbn/i18n';
+import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID } from '../common/alerting/logs/types';
+import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from './lib/alerting/inventory_metric_threshold/types';
+import { METRIC_THRESHOLD_ALERT_TYPE_ID } from './lib/alerting/metric_threshold/types';
export const METRICS_FEATURE = {
id: 'infrastructure',
@@ -16,44 +19,33 @@ export const METRICS_FEATURE = {
navLinkId: 'metrics',
app: ['infra', 'kibana'],
catalogue: ['infraops'],
+ alerting: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID],
privileges: {
all: {
app: ['infra', 'kibana'],
catalogue: ['infraops'],
- api: ['infra', 'actions-read', 'actions-all', 'alerting-read', 'alerting-all'],
+ api: ['infra'],
savedObject: {
- all: ['infrastructure-ui-source', 'alert', 'action', 'action_task_params'],
+ all: ['infrastructure-ui-source'],
read: ['index-pattern'],
},
- ui: [
- 'show',
- 'configureSource',
- 'save',
- 'alerting:show',
- 'actions:show',
- 'alerting:save',
- 'actions:save',
- 'alerting:delete',
- 'actions:delete',
- ],
+ alerting: {
+ all: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID],
+ },
+ ui: ['show', 'configureSource', 'save', 'alerting:show'],
},
read: {
app: ['infra', 'kibana'],
catalogue: ['infraops'],
- api: ['infra', 'actions-read', 'actions-all', 'alerting-read', 'alerting-all'],
+ api: ['infra'],
savedObject: {
- all: ['alert', 'action', 'action_task_params'],
+ all: [],
read: ['infrastructure-ui-source', 'index-pattern'],
},
- ui: [
- 'show',
- 'alerting:show',
- 'actions:show',
- 'alerting:save',
- 'actions:save',
- 'alerting:delete',
- 'actions:delete',
- ],
+ alerting: {
+ all: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID],
+ },
+ ui: ['show', 'alerting:show'],
},
},
};
@@ -68,6 +60,7 @@ export const LOGS_FEATURE = {
navLinkId: 'logs',
app: ['infra', 'kibana'],
catalogue: ['infralogging'],
+ alerting: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID],
privileges: {
all: {
app: ['infra', 'kibana'],
@@ -77,12 +70,18 @@ export const LOGS_FEATURE = {
all: ['infrastructure-ui-source'],
read: [],
},
+ alerting: {
+ all: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID],
+ },
ui: ['show', 'configureSource', 'save'],
},
read: {
app: ['infra', 'kibana'],
catalogue: ['infralogging'],
api: ['infra'],
+ alerting: {
+ all: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID],
+ },
savedObject: {
all: [],
read: ['infrastructure-ui-source'],
diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts
index 85b38f48d9f22..fa5277cb09987 100644
--- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts
@@ -40,7 +40,7 @@ export const registerMetricInventoryThresholdAlertType = (libs: InfraBackendLibs
},
defaultActionGroupId: FIRED_ACTIONS.id,
actionGroups: [FIRED_ACTIONS],
- producer: 'metrics',
+ producer: 'infrastructure',
executor: createInventoryMetricThresholdExecutor(libs),
actionVariables: {
context: [
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts
index 529a1d176c437..51a127e9345b4 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts
@@ -117,6 +117,6 @@ export function registerMetricThresholdAlertType(libs: InfraBackendLibs) {
{ name: 'threshold', description: thresholdActionVariableDescription },
],
},
- producer: 'metrics',
+ producer: 'infrastructure',
};
}
diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts
index 39ec5fe1ffaa7..86022a0e863d5 100644
--- a/x-pack/plugins/monitoring/server/plugin.ts
+++ b/x-pack/plugins/monitoring/server/plugin.ts
@@ -25,6 +25,7 @@ import {
LOGGING_TAG,
KIBANA_MONITORING_LOGGING_TAG,
KIBANA_STATS_TYPE_MONITORING,
+ ALERTS,
} from '../common/constants';
import { MonitoringConfig, createConfig, configSchema } from './config';
// @ts-ignore
@@ -242,6 +243,7 @@ export class Plugin {
app: ['monitoring', 'kibana'],
catalogue: ['monitoring'],
privileges: null,
+ alerting: ALERTS,
reserved: {
description: i18n.translate('xpack.monitoring.feature.reserved.description', {
defaultMessage: 'To grant users access, you should also assign the monitoring_user role.',
@@ -256,6 +258,9 @@ export class Plugin {
all: [],
read: [],
},
+ alerting: {
+ all: ALERTS,
+ },
ui: [],
},
},
diff --git a/x-pack/plugins/security/server/authorization/actions/__snapshots__/alerting.test.ts.snap b/x-pack/plugins/security/server/authorization/actions/__snapshots__/alerting.test.ts.snap
new file mode 100644
index 0000000000000..afa907fe09837
--- /dev/null
+++ b/x-pack/plugins/security/server/authorization/actions/__snapshots__/alerting.test.ts.snap
@@ -0,0 +1,37 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`#get alertType of "" throws error 1`] = `"alertTypeId is required and must be a string"`;
+
+exports[`#get alertType of {} throws error 1`] = `"alertTypeId is required and must be a string"`;
+
+exports[`#get alertType of 1 throws error 1`] = `"alertTypeId is required and must be a string"`;
+
+exports[`#get alertType of null throws error 1`] = `"alertTypeId is required and must be a string"`;
+
+exports[`#get alertType of true throws error 1`] = `"alertTypeId is required and must be a string"`;
+
+exports[`#get alertType of undefined throws error 1`] = `"alertTypeId is required and must be a string"`;
+
+exports[`#get consumer of "" throws error 1`] = `"consumer is required and must be a string"`;
+
+exports[`#get consumer of {} throws error 1`] = `"consumer is required and must be a string"`;
+
+exports[`#get consumer of 1 throws error 1`] = `"consumer is required and must be a string"`;
+
+exports[`#get consumer of null throws error 1`] = `"consumer is required and must be a string"`;
+
+exports[`#get consumer of true throws error 1`] = `"consumer is required and must be a string"`;
+
+exports[`#get consumer of undefined throws error 1`] = `"consumer is required and must be a string"`;
+
+exports[`#get operation of "" throws error 1`] = `"operation is required and must be a string"`;
+
+exports[`#get operation of {} throws error 1`] = `"operation is required and must be a string"`;
+
+exports[`#get operation of 1 throws error 1`] = `"operation is required and must be a string"`;
+
+exports[`#get operation of null throws error 1`] = `"operation is required and must be a string"`;
+
+exports[`#get operation of true throws error 1`] = `"operation is required and must be a string"`;
+
+exports[`#get operation of undefined throws error 1`] = `"operation is required and must be a string"`;
diff --git a/x-pack/plugins/security/server/authorization/actions/actions.mock.ts b/x-pack/plugins/security/server/authorization/actions/actions.mock.ts
new file mode 100644
index 0000000000000..f41faaa3dd52c
--- /dev/null
+++ b/x-pack/plugins/security/server/authorization/actions/actions.mock.ts
@@ -0,0 +1,35 @@
+/*
+ * 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 { ApiActions } from './api';
+import { AppActions } from './app';
+import { SavedObjectActions } from './saved_object';
+import { SpaceActions } from './space';
+import { UIActions } from './ui';
+import { AlertingActions } from './alerting';
+import { Actions } from './actions';
+
+jest.mock('./api');
+jest.mock('./app');
+jest.mock('./saved_object');
+jest.mock('./space');
+jest.mock('./ui');
+jest.mock('./alerting');
+
+const create = (versionNumber: string) => {
+ const t = ({
+ api: new ApiActions(versionNumber),
+ app: new AppActions(versionNumber),
+ login: 'login:',
+ savedObject: new SavedObjectActions(versionNumber),
+ alerting: new AlertingActions(versionNumber),
+ space: new SpaceActions(versionNumber),
+ ui: new UIActions(versionNumber),
+ version: `version:${versionNumber}`,
+ } as unknown) as jest.Mocked;
+ return t;
+};
+
+export const actionsMock = { create };
diff --git a/x-pack/plugins/security/server/authorization/actions/actions.ts b/x-pack/plugins/security/server/authorization/actions/actions.ts
index 00293e88abe76..34258bdcf972d 100644
--- a/x-pack/plugins/security/server/authorization/actions/actions.ts
+++ b/x-pack/plugins/security/server/authorization/actions/actions.ts
@@ -9,6 +9,7 @@ import { AppActions } from './app';
import { SavedObjectActions } from './saved_object';
import { SpaceActions } from './space';
import { UIActions } from './ui';
+import { AlertingActions } from './alerting';
/** Actions are used to create the "actions" that are associated with Elasticsearch's
* application privileges, and are used to perform the authorization checks implemented
@@ -23,6 +24,8 @@ export class Actions {
public readonly savedObject = new SavedObjectActions(this.versionNumber);
+ public readonly alerting = new AlertingActions(this.versionNumber);
+
public readonly space = new SpaceActions(this.versionNumber);
public readonly ui = new UIActions(this.versionNumber);
diff --git a/x-pack/plugins/security/server/authorization/actions/alerting.test.ts b/x-pack/plugins/security/server/authorization/actions/alerting.test.ts
new file mode 100644
index 0000000000000..744543f38a914
--- /dev/null
+++ b/x-pack/plugins/security/server/authorization/actions/alerting.test.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 { AlertingActions } from './alerting';
+
+const version = '1.0.0-zeta1';
+
+describe('#get', () => {
+ [null, undefined, '', 1, true, {}].forEach((alertType: any) => {
+ test(`alertType of ${JSON.stringify(alertType)} throws error`, () => {
+ const alertingActions = new AlertingActions(version);
+ expect(() =>
+ alertingActions.get(alertType, 'consumer', 'foo-action')
+ ).toThrowErrorMatchingSnapshot();
+ });
+ });
+
+ [null, undefined, '', 1, true, {}].forEach((operation: any) => {
+ test(`operation of ${JSON.stringify(operation)} throws error`, () => {
+ const alertingActions = new AlertingActions(version);
+ expect(() =>
+ alertingActions.get('foo-alertType', 'consumer', operation)
+ ).toThrowErrorMatchingSnapshot();
+ });
+ });
+
+ [null, '', 1, true, undefined, {}].forEach((consumer: any) => {
+ test(`consumer of ${JSON.stringify(consumer)} throws error`, () => {
+ const alertingActions = new AlertingActions(version);
+ expect(() =>
+ alertingActions.get('foo-alertType', consumer, 'operation')
+ ).toThrowErrorMatchingSnapshot();
+ });
+ });
+
+ test('returns `alerting:${alertType}/${consumer}/${operation}`', () => {
+ const alertingActions = new AlertingActions(version);
+ expect(alertingActions.get('foo-alertType', 'consumer', 'bar-operation')).toBe(
+ 'alerting:1.0.0-zeta1:foo-alertType/consumer/bar-operation'
+ );
+ });
+});
diff --git a/x-pack/plugins/security/server/authorization/actions/alerting.ts b/x-pack/plugins/security/server/authorization/actions/alerting.ts
new file mode 100644
index 0000000000000..99d04efe6892d
--- /dev/null
+++ b/x-pack/plugins/security/server/authorization/actions/alerting.ts
@@ -0,0 +1,31 @@
+/*
+ * 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 { isString } from 'lodash';
+
+export class AlertingActions {
+ private readonly prefix: string;
+
+ constructor(versionNumber: string) {
+ this.prefix = `alerting:${versionNumber}:`;
+ }
+
+ public get(alertTypeId: string, consumer: string, operation: string): string {
+ if (!alertTypeId || !isString(alertTypeId)) {
+ throw new Error('alertTypeId is required and must be a string');
+ }
+
+ if (!operation || !isString(operation)) {
+ throw new Error('operation is required and must be a string');
+ }
+
+ if (!consumer || !isString(consumer)) {
+ throw new Error('consumer is required and must be a string');
+ }
+
+ return `${this.prefix}${alertTypeId}/${consumer}/${operation}`;
+ }
+}
diff --git a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts
index 45f55b34baf96..4aedac0757bc8 100644
--- a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts
+++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts
@@ -20,6 +20,9 @@ const mockRequest = httpServerMock.createKibanaRequest();
const createMockAuthz = (options: MockAuthzOptions) => {
const mock = authorizationMock.create({ version: '1.0.0-zeta1' });
+ // plug actual ui actions into mock Actions with
+ mock.actions = actions;
+
mock.checkPrivilegesDynamicallyWithRequest.mockImplementation((request) => {
expect(request).toBe(mockRequest);
diff --git a/x-pack/plugins/security/server/authorization/index.mock.ts b/x-pack/plugins/security/server/authorization/index.mock.ts
index 930ede4157723..62b254d132d9e 100644
--- a/x-pack/plugins/security/server/authorization/index.mock.ts
+++ b/x-pack/plugins/security/server/authorization/index.mock.ts
@@ -3,16 +3,15 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-
-import { Actions } from '.';
import { AuthorizationMode } from './mode';
+import { actionsMock } from './actions/actions.mock';
export const authorizationMock = {
create: ({
version = 'mock-version',
applicationName = 'mock-application',
}: { version?: string; applicationName?: string } = {}) => ({
- actions: new Actions(version),
+ actions: actionsMock.create(version),
checkPrivilegesWithRequest: jest.fn(),
checkPrivilegesDynamicallyWithRequest: jest.fn(),
checkSavedObjectsPrivilegesWithRequest: jest.fn(),
diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts
new file mode 100644
index 0000000000000..99d69602db137
--- /dev/null
+++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts
@@ -0,0 +1,178 @@
+/*
+ * 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 { Actions } from '../../actions';
+import { FeaturePrivilegeAlertingBuilder } from './alerting';
+import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server';
+
+const version = '1.0.0-zeta1';
+
+describe(`feature_privilege_builder`, () => {
+ describe(`alerting`, () => {
+ test('grants no privileges by default', () => {
+ const actions = new Actions(version);
+ const alertingFeaturePrivileges = new FeaturePrivilegeAlertingBuilder(actions);
+
+ const privilege: FeatureKibanaPrivileges = {
+ alerting: {
+ all: [],
+ read: [],
+ },
+
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ ui: [],
+ };
+
+ const feature = new Feature({
+ id: 'my-feature',
+ name: 'my-feature',
+ app: [],
+ privileges: {
+ all: privilege,
+ read: privilege,
+ },
+ });
+
+ expect(alertingFeaturePrivileges.getActions(privilege, feature)).toEqual([]);
+ });
+
+ describe(`within feature`, () => {
+ test('grants `read` privileges under feature consumer', () => {
+ const actions = new Actions(version);
+ const alertingFeaturePrivileges = new FeaturePrivilegeAlertingBuilder(actions);
+
+ const privilege: FeatureKibanaPrivileges = {
+ alerting: {
+ all: [],
+ read: ['alert-type'],
+ },
+
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ ui: [],
+ };
+
+ const feature = new Feature({
+ id: 'my-feature',
+ name: 'my-feature',
+ app: [],
+ privileges: {
+ all: privilege,
+ read: privilege,
+ },
+ });
+
+ expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(`
+ Array [
+ "alerting:1.0.0-zeta1:alert-type/my-feature/get",
+ "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertState",
+ "alerting:1.0.0-zeta1:alert-type/my-feature/find",
+ ]
+ `);
+ });
+
+ test('grants `all` privileges under feature consumer', () => {
+ const actions = new Actions(version);
+ const alertingFeaturePrivileges = new FeaturePrivilegeAlertingBuilder(actions);
+
+ const privilege: FeatureKibanaPrivileges = {
+ alerting: {
+ all: ['alert-type'],
+ read: [],
+ },
+
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ ui: [],
+ };
+
+ const feature = new Feature({
+ id: 'my-feature',
+ name: 'my-feature',
+ app: [],
+ privileges: {
+ all: privilege,
+ read: privilege,
+ },
+ });
+
+ expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(`
+ Array [
+ "alerting:1.0.0-zeta1:alert-type/my-feature/get",
+ "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertState",
+ "alerting:1.0.0-zeta1:alert-type/my-feature/find",
+ "alerting:1.0.0-zeta1:alert-type/my-feature/create",
+ "alerting:1.0.0-zeta1:alert-type/my-feature/delete",
+ "alerting:1.0.0-zeta1:alert-type/my-feature/update",
+ "alerting:1.0.0-zeta1:alert-type/my-feature/updateApiKey",
+ "alerting:1.0.0-zeta1:alert-type/my-feature/enable",
+ "alerting:1.0.0-zeta1:alert-type/my-feature/disable",
+ "alerting:1.0.0-zeta1:alert-type/my-feature/muteAll",
+ "alerting:1.0.0-zeta1:alert-type/my-feature/unmuteAll",
+ "alerting:1.0.0-zeta1:alert-type/my-feature/muteInstance",
+ "alerting:1.0.0-zeta1:alert-type/my-feature/unmuteInstance",
+ ]
+ `);
+ });
+
+ test('grants both `all` and `read` privileges under feature consumer', () => {
+ const actions = new Actions(version);
+ const alertingFeaturePrivileges = new FeaturePrivilegeAlertingBuilder(actions);
+
+ const privilege: FeatureKibanaPrivileges = {
+ alerting: {
+ all: ['alert-type'],
+ read: ['readonly-alert-type'],
+ },
+
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ ui: [],
+ };
+
+ const feature = new Feature({
+ id: 'my-feature',
+ name: 'my-feature',
+ app: [],
+ privileges: {
+ all: privilege,
+ read: privilege,
+ },
+ });
+
+ expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(`
+ Array [
+ "alerting:1.0.0-zeta1:alert-type/my-feature/get",
+ "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertState",
+ "alerting:1.0.0-zeta1:alert-type/my-feature/find",
+ "alerting:1.0.0-zeta1:alert-type/my-feature/create",
+ "alerting:1.0.0-zeta1:alert-type/my-feature/delete",
+ "alerting:1.0.0-zeta1:alert-type/my-feature/update",
+ "alerting:1.0.0-zeta1:alert-type/my-feature/updateApiKey",
+ "alerting:1.0.0-zeta1:alert-type/my-feature/enable",
+ "alerting:1.0.0-zeta1:alert-type/my-feature/disable",
+ "alerting:1.0.0-zeta1:alert-type/my-feature/muteAll",
+ "alerting:1.0.0-zeta1:alert-type/my-feature/unmuteAll",
+ "alerting:1.0.0-zeta1:alert-type/my-feature/muteInstance",
+ "alerting:1.0.0-zeta1:alert-type/my-feature/unmuteInstance",
+ "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/get",
+ "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/getAlertState",
+ "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/find",
+ ]
+ `);
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts
new file mode 100644
index 0000000000000..42dd7794ba184
--- /dev/null
+++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts
@@ -0,0 +1,42 @@
+/*
+ * 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 { uniq } from 'lodash';
+import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server';
+import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder';
+
+const readOperations: string[] = ['get', 'getAlertState', 'find'];
+const writeOperations: string[] = [
+ 'create',
+ 'delete',
+ 'update',
+ 'updateApiKey',
+ 'enable',
+ 'disable',
+ 'muteAll',
+ 'unmuteAll',
+ 'muteInstance',
+ 'unmuteInstance',
+];
+const allOperations: string[] = [...readOperations, ...writeOperations];
+
+export class FeaturePrivilegeAlertingBuilder extends BaseFeaturePrivilegeBuilder {
+ public getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: Feature): string[] {
+ const getAlertingPrivilege = (
+ operations: string[],
+ privilegedTypes: readonly string[],
+ consumer: string
+ ) =>
+ privilegedTypes.flatMap((type) =>
+ operations.map((operation) => this.actions.alerting.get(type, consumer, operation))
+ );
+
+ return uniq([
+ ...getAlertingPrivilege(allOperations, privilegeDefinition.alerting?.all ?? [], feature.id),
+ ...getAlertingPrivilege(readOperations, privilegeDefinition.alerting?.read ?? [], feature.id),
+ ]);
+ }
+}
diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts
index 3d6dfbdac0251..76b664cbbe2a7 100644
--- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts
+++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts
@@ -14,6 +14,7 @@ import { FeaturePrivilegeBuilder } from './feature_privilege_builder';
import { FeaturePrivilegeManagementBuilder } from './management';
import { FeaturePrivilegeNavlinkBuilder } from './navlink';
import { FeaturePrivilegeSavedObjectBuilder } from './saved_object';
+import { FeaturePrivilegeAlertingBuilder } from './alerting';
import { FeaturePrivilegeUIBuilder } from './ui';
export { FeaturePrivilegeBuilder };
@@ -26,6 +27,7 @@ export const featurePrivilegeBuilderFactory = (actions: Actions): FeaturePrivile
new FeaturePrivilegeNavlinkBuilder(actions),
new FeaturePrivilegeSavedObjectBuilder(actions),
new FeaturePrivilegeUIBuilder(actions),
+ new FeaturePrivilegeAlertingBuilder(actions),
];
return {
diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.test.ts
index 485783253d29d..bb1f0c33fdee9 100644
--- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.test.ts
+++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.test.ts
@@ -41,6 +41,10 @@ describe('featurePrivilegeIterator', () => {
all: ['all-type'],
read: ['read-type'],
},
+ alerting: {
+ all: ['alerting-all-type'],
+ read: ['alerting-read-type'],
+ },
ui: ['ui-action'],
},
read: {
@@ -54,6 +58,9 @@ describe('featurePrivilegeIterator', () => {
all: [],
read: ['read-type'],
},
+ alerting: {
+ read: ['alerting-read-type'],
+ },
ui: ['ui-action'],
},
},
@@ -80,6 +87,10 @@ describe('featurePrivilegeIterator', () => {
all: ['all-type'],
read: ['read-type'],
},
+ alerting: {
+ all: ['alerting-all-type'],
+ read: ['alerting-read-type'],
+ },
ui: ['ui-action'],
},
},
@@ -96,6 +107,9 @@ describe('featurePrivilegeIterator', () => {
all: [],
read: ['read-type'],
},
+ alerting: {
+ read: ['alerting-read-type'],
+ },
ui: ['ui-action'],
},
},
@@ -118,6 +132,10 @@ describe('featurePrivilegeIterator', () => {
all: ['all-type'],
read: ['read-type'],
},
+ alerting: {
+ all: ['alerting-all-type'],
+ read: ['alerting-read-type'],
+ },
ui: ['ui-action'],
},
read: {
@@ -131,6 +149,9 @@ describe('featurePrivilegeIterator', () => {
all: [],
read: ['read-type'],
},
+ alerting: {
+ read: ['alerting-read-type'],
+ },
ui: ['ui-action'],
},
},
@@ -158,6 +179,10 @@ describe('featurePrivilegeIterator', () => {
all: ['all-type'],
read: ['read-type'],
},
+ alerting: {
+ all: ['alerting-all-type'],
+ read: ['alerting-read-type'],
+ },
ui: ['ui-action'],
},
},
@@ -181,6 +206,10 @@ describe('featurePrivilegeIterator', () => {
all: ['all-type'],
read: ['read-type'],
},
+ alerting: {
+ all: ['alerting-all-type'],
+ read: ['alerting-read-type'],
+ },
ui: ['ui-action'],
},
read: {
@@ -194,6 +223,9 @@ describe('featurePrivilegeIterator', () => {
all: [],
read: ['read-type'],
},
+ alerting: {
+ read: ['alerting-read-type'],
+ },
ui: ['ui-action'],
},
},
@@ -218,6 +250,10 @@ describe('featurePrivilegeIterator', () => {
all: ['all-sub-type'],
read: ['read-sub-type'],
},
+ alerting: {
+ all: ['alerting-all-sub-type'],
+ read: ['alerting-read-sub-type'],
+ },
ui: ['ui-sub-type'],
},
],
@@ -247,6 +283,10 @@ describe('featurePrivilegeIterator', () => {
all: ['all-type'],
read: ['read-type'],
},
+ alerting: {
+ all: ['alerting-all-type'],
+ read: ['alerting-read-type'],
+ },
ui: ['ui-action'],
},
},
@@ -263,6 +303,9 @@ describe('featurePrivilegeIterator', () => {
all: [],
read: ['read-type'],
},
+ alerting: {
+ read: ['alerting-read-type'],
+ },
ui: ['ui-action'],
},
},
@@ -286,6 +329,10 @@ describe('featurePrivilegeIterator', () => {
all: ['all-type'],
read: ['read-type'],
},
+ alerting: {
+ all: ['alerting-all-type'],
+ read: ['alerting-read-type'],
+ },
ui: ['ui-action'],
},
read: {
@@ -299,6 +346,9 @@ describe('featurePrivilegeIterator', () => {
all: [],
read: ['read-type'],
},
+ alerting: {
+ read: ['alerting-read-type'],
+ },
ui: ['ui-action'],
},
},
@@ -323,6 +373,10 @@ describe('featurePrivilegeIterator', () => {
all: ['all-sub-type'],
read: ['read-sub-type'],
},
+ alerting: {
+ all: ['alerting-all-sub-type'],
+ read: ['alerting-read-sub-type'],
+ },
ui: ['ui-sub-type'],
},
],
@@ -352,6 +406,10 @@ describe('featurePrivilegeIterator', () => {
all: ['all-type'],
read: ['read-type'],
},
+ alerting: {
+ all: ['alerting-all-type'],
+ read: ['alerting-read-type'],
+ },
ui: ['ui-action'],
},
},
@@ -368,6 +426,9 @@ describe('featurePrivilegeIterator', () => {
all: [],
read: ['read-type'],
},
+ alerting: {
+ read: ['alerting-read-type'],
+ },
ui: ['ui-action'],
},
},
@@ -391,6 +452,10 @@ describe('featurePrivilegeIterator', () => {
all: ['all-type'],
read: ['read-type'],
},
+ alerting: {
+ all: ['alerting-all-type'],
+ read: ['alerting-read-type'],
+ },
ui: ['ui-action'],
},
read: {
@@ -404,6 +469,9 @@ describe('featurePrivilegeIterator', () => {
all: [],
read: ['read-type'],
},
+ alerting: {
+ read: ['alerting-read-type'],
+ },
ui: ['ui-action'],
},
},
@@ -429,6 +497,10 @@ describe('featurePrivilegeIterator', () => {
all: ['all-sub-type'],
read: ['read-sub-type'],
},
+ alerting: {
+ all: ['alerting-all-sub-type'],
+ read: ['alerting-read-sub-type'],
+ },
ui: ['ui-sub-type'],
},
],
@@ -459,6 +531,10 @@ describe('featurePrivilegeIterator', () => {
all: ['all-type', 'all-sub-type'],
read: ['read-type', 'read-sub-type'],
},
+ alerting: {
+ all: ['alerting-all-type', 'alerting-all-sub-type'],
+ read: ['alerting-read-type', 'alerting-read-sub-type'],
+ },
ui: ['ui-action', 'ui-sub-type'],
},
},
@@ -476,6 +552,10 @@ describe('featurePrivilegeIterator', () => {
all: ['all-sub-type'],
read: ['read-type', 'read-sub-type'],
},
+ alerting: {
+ all: ['alerting-all-sub-type'],
+ read: ['alerting-read-type', 'alerting-read-sub-type'],
+ },
ui: ['ui-action', 'ui-sub-type'],
},
},
@@ -499,6 +579,10 @@ describe('featurePrivilegeIterator', () => {
all: ['all-type'],
read: ['read-type'],
},
+ alerting: {
+ all: ['alerting-all-type'],
+ read: ['alerting-read-type'],
+ },
ui: ['ui-action'],
},
read: {
@@ -512,6 +596,9 @@ describe('featurePrivilegeIterator', () => {
all: [],
read: ['read-type'],
},
+ alerting: {
+ read: ['alerting-read-type'],
+ },
ui: ['ui-action'],
},
},
@@ -536,6 +623,9 @@ describe('featurePrivilegeIterator', () => {
all: [],
read: ['read-type'],
},
+ alerting: {
+ read: ['alerting-read-type'],
+ },
ui: ['ui-action'],
},
],
@@ -565,6 +655,10 @@ describe('featurePrivilegeIterator', () => {
all: ['all-type'],
read: ['read-type'],
},
+ alerting: {
+ all: ['alerting-all-type'],
+ read: ['alerting-read-type'],
+ },
ui: ['ui-action'],
},
},
@@ -581,6 +675,10 @@ describe('featurePrivilegeIterator', () => {
all: [],
read: ['read-type'],
},
+ alerting: {
+ all: [],
+ read: ['alerting-read-type'],
+ },
ui: ['ui-action'],
},
},
@@ -604,6 +702,10 @@ describe('featurePrivilegeIterator', () => {
all: ['all-type'],
read: ['read-type'],
},
+ alerting: {
+ all: ['alerting-all-type'],
+ read: ['alerting-read-type'],
+ },
ui: ['ui-action'],
},
read: {
@@ -617,6 +719,9 @@ describe('featurePrivilegeIterator', () => {
all: [],
read: ['read-type'],
},
+ alerting: {
+ read: ['alerting-read-type'],
+ },
ui: ['ui-action'],
},
},
@@ -642,6 +747,10 @@ describe('featurePrivilegeIterator', () => {
all: ['all-sub-type'],
read: ['read-sub-type'],
},
+ alerting: {
+ all: ['alerting-all-sub-type'],
+ read: ['alerting-read-sub-type'],
+ },
ui: ['ui-sub-type'],
},
],
@@ -672,6 +781,10 @@ describe('featurePrivilegeIterator', () => {
all: ['all-type', 'all-sub-type'],
read: ['read-type', 'read-sub-type'],
},
+ alerting: {
+ all: ['alerting-all-type', 'alerting-all-sub-type'],
+ read: ['alerting-read-type', 'alerting-read-sub-type'],
+ },
ui: ['ui-action', 'ui-sub-type'],
},
},
@@ -688,6 +801,9 @@ describe('featurePrivilegeIterator', () => {
all: [],
read: ['read-type'],
},
+ alerting: {
+ read: ['alerting-read-type'],
+ },
ui: ['ui-action'],
},
},
@@ -737,6 +853,10 @@ describe('featurePrivilegeIterator', () => {
all: ['all-sub-type'],
read: ['read-sub-type'],
},
+ alerting: {
+ all: ['alerting-all-sub-type'],
+ read: ['alerting-read-sub-type'],
+ },
ui: ['ui-sub-type'],
},
],
@@ -767,6 +887,10 @@ describe('featurePrivilegeIterator', () => {
all: ['all-sub-type'],
read: ['read-sub-type'],
},
+ alerting: {
+ all: ['alerting-all-sub-type'],
+ read: ['alerting-read-sub-type'],
+ },
ui: ['ui-sub-type'],
},
},
@@ -784,6 +908,10 @@ describe('featurePrivilegeIterator', () => {
all: ['all-sub-type'],
read: ['read-sub-type'],
},
+ alerting: {
+ all: ['alerting-all-sub-type'],
+ read: ['alerting-read-sub-type'],
+ },
ui: ['ui-sub-type'],
},
},
@@ -807,6 +935,10 @@ describe('featurePrivilegeIterator', () => {
all: ['all-type'],
read: ['read-type'],
},
+ alerting: {
+ all: ['alerting-all-type'],
+ read: ['alerting-read-type'],
+ },
ui: ['ui-action'],
},
read: {
@@ -820,6 +952,9 @@ describe('featurePrivilegeIterator', () => {
all: [],
read: ['read-type'],
},
+ alerting: {
+ read: ['alerting-read-type'],
+ },
ui: ['ui-action'],
},
},
@@ -867,6 +1002,10 @@ describe('featurePrivilegeIterator', () => {
all: ['all-type'],
read: ['read-type'],
},
+ alerting: {
+ all: ['alerting-all-type'],
+ read: ['alerting-read-type'],
+ },
ui: ['ui-action'],
},
},
@@ -883,6 +1022,10 @@ describe('featurePrivilegeIterator', () => {
all: [],
read: ['read-type'],
},
+ alerting: {
+ all: [],
+ read: ['alerting-read-type'],
+ },
ui: ['ui-action'],
},
},
diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.ts
index 029b2e77f7812..17c9464b14756 100644
--- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.ts
+++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.ts
@@ -72,6 +72,14 @@ function mergeWithSubFeatures(
mergedConfig.savedObject.read,
subFeaturePrivilege.savedObject.read
);
+
+ mergedConfig.alerting = {
+ all: mergeArrays(mergedConfig.alerting?.all ?? [], subFeaturePrivilege.alerting?.all ?? []),
+ read: mergeArrays(
+ mergedConfig.alerting?.read ?? [],
+ subFeaturePrivilege.alerting?.read ?? []
+ ),
+ };
}
return mergedConfig;
}
diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.ts
index f9ee5fc750127..5d8ef3f376cac 100644
--- a/x-pack/plugins/security/server/authorization/privileges/privileges.ts
+++ b/x-pack/plugins/security/server/authorization/privileges/privileges.ts
@@ -90,7 +90,6 @@ export function privilegesFactory(
delete featurePrivileges[feature.id];
}
}
-
return {
features: featurePrivileges,
global: {
diff --git a/x-pack/plugins/security/server/mocks.ts b/x-pack/plugins/security/server/mocks.ts
index c2d99433b0346..4ce0ec6e3c10e 100644
--- a/x-pack/plugins/security/server/mocks.ts
+++ b/x-pack/plugins/security/server/mocks.ts
@@ -17,6 +17,7 @@ function createSetupMock() {
authz: {
actions: mockAuthz.actions,
checkPrivilegesWithRequest: mockAuthz.checkPrivilegesWithRequest,
+ checkPrivilegesDynamicallyWithRequest: mockAuthz.checkPrivilegesDynamicallyWithRequest,
mode: mockAuthz.mode,
},
registerSpacesService: jest.fn(),
diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts
index 243bad0ec3e71..db015d246f591 100644
--- a/x-pack/plugins/security/server/plugin.test.ts
+++ b/x-pack/plugins/security/server/plugin.test.ts
@@ -69,6 +69,9 @@ describe('Security Plugin', () => {
},
"authz": Object {
"actions": Actions {
+ "alerting": AlertingActions {
+ "prefix": "alerting:version:",
+ },
"api": ApiActions {
"prefix": "api:version:",
},
@@ -88,6 +91,7 @@ describe('Security Plugin', () => {
"version": "version:version",
"versionNumber": "version",
},
+ "checkPrivilegesDynamicallyWithRequest": [Function],
"checkPrivilegesWithRequest": [Function],
"mode": Object {
"useRbacForRequest": [Function],
diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts
index c4b16c9eec872..1753eb7b62ed1 100644
--- a/x-pack/plugins/security/server/plugin.ts
+++ b/x-pack/plugins/security/server/plugin.ts
@@ -51,7 +51,10 @@ export interface SecurityPluginSetup {
| 'grantAPIKeyAsInternalUser'
| 'invalidateAPIKeyAsInternalUser'
>;
- authz: Pick;
+ authz: Pick<
+ AuthorizationServiceSetup,
+ 'actions' | 'checkPrivilegesDynamicallyWithRequest' | 'checkPrivilegesWithRequest' | 'mode'
+ >;
license: SecurityLicense;
audit: Pick;
@@ -206,6 +209,7 @@ export class Plugin {
authz: {
actions: authz.actions,
checkPrivilegesWithRequest: authz.checkPrivilegesWithRequest,
+ checkPrivilegesDynamicallyWithRequest: authz.checkPrivilegesDynamicallyWithRequest,
mode: authz.mode,
},
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/create_notifications.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/create_notifications.ts
index a472d8a4df4a4..8f6826cec5365 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/create_notifications.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/create_notifications.ts
@@ -5,7 +5,7 @@
*/
import { Alert } from '../../../../../alerts/common';
-import { APP_ID, NOTIFICATIONS_ID } from '../../../../common/constants';
+import { SERVER_APP_ID, NOTIFICATIONS_ID } from '../../../../common/constants';
import { CreateNotificationParams } from './types';
import { addTags } from './add_tags';
import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions';
@@ -23,7 +23,7 @@ export const createNotifications = async ({
name,
tags: addTags([], ruleAlertId),
alertTypeId: NOTIFICATIONS_ID,
- consumer: APP_ID,
+ consumer: SERVER_APP_ID,
params: {
ruleAlertId,
},
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts
index ad4038b05dbd3..0c67d9ca77146 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts
@@ -6,7 +6,7 @@
import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions';
import { Alert } from '../../../../../alerts/common';
-import { APP_ID, SIGNALS_ID } from '../../../../common/constants';
+import { SERVER_APP_ID, SIGNALS_ID } from '../../../../common/constants';
import { CreateRulesOptions } from './types';
import { addTags } from './add_tags';
@@ -57,7 +57,7 @@ export const createRules = async ({
name,
tags: addTags(tags, ruleId, immutable),
alertTypeId: SIGNALS_ID,
- consumer: APP_ID,
+ consumer: SERVER_APP_ID,
params: {
anomalyThreshold,
author,
diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts
index 611f35c6d402a..06cd3138ca564 100644
--- a/x-pack/plugins/security_solution/server/plugin.ts
+++ b/x-pack/plugins/security_solution/server/plugin.ts
@@ -40,7 +40,14 @@ import { initSavedObjects, savedObjectTypes } from './saved_objects';
import { AppClientFactory } from './client';
import { createConfig$, ConfigType } from './config';
import { initUiSettings } from './ui_settings';
-import { APP_ID, APP_ICON, SERVER_APP_ID, SecurityPageName } from '../common/constants';
+import {
+ APP_ID,
+ APP_ICON,
+ SERVER_APP_ID,
+ SecurityPageName,
+ SIGNALS_ID,
+ NOTIFICATIONS_ID,
+} from '../common/constants';
import { registerEndpointRoutes } from './endpoint/routes/metadata';
import { registerLimitedConcurrencyRoutes } from './endpoint/routes/limited_concurrency';
import { registerResolverRoutes } from './endpoint/routes/resolver';
@@ -167,23 +174,15 @@ export class Plugin implements IPlugin {
editActionConfig={() => {}}
editActionSecrets={() => {}}
docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart}
+ readOnly={false}
/>
);
expect(wrapper.find('[data-test-subj="emailFromInput"]').length > 0).toBeTruthy();
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx
index 8a15320d5de16..4ef9c2e0d4d2e 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx
@@ -21,7 +21,7 @@ import { EmailActionConnector } from '../types';
export const EmailActionConnectorFields: React.FunctionComponent> = ({ action, editActionConfig, editActionSecrets, errors, docLinks }) => {
+>> = ({ action, editActionConfig, editActionSecrets, errors, readOnly, docLinks }) => {
const { from, host, port, secure } = action.config;
const { user, password } = action.secrets;
@@ -54,6 +54,7 @@ export const EmailActionConnectorFields: React.FunctionComponent
0 && from !== undefined}
name="from"
value={from || ''}
@@ -86,6 +87,7 @@ export const EmailActionConnectorFields: React.FunctionComponent
0 && host !== undefined}
name="host"
value={host || ''}
@@ -121,6 +123,7 @@ export const EmailActionConnectorFields: React.FunctionComponent 0 && port !== undefined}
fullWidth
+ readOnly={readOnly}
name="port"
value={port || ''}
data-test-subj="emailPortInput"
@@ -145,6 +148,7 @@ export const EmailActionConnectorFields: React.FunctionComponent {
editActionConfig('secure', e.target.checked);
@@ -174,6 +178,7 @@ export const EmailActionConnectorFields: React.FunctionComponent 0}
name="user"
+ readOnly={readOnly}
value={user || ''}
data-test-subj="emailUserInput"
onChange={(e) => {
@@ -197,6 +202,7 @@ export const EmailActionConnectorFields: React.FunctionComponent
0}
name="password"
value={password || ''}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx
index 4cb397927b53e..f5f14cb041335 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx
@@ -88,6 +88,7 @@ describe('IndexActionConnectorFields renders', () => {
editActionSecrets={() => {}}
http={deps!.http}
docLinks={deps!.docLinks}
+ readOnly={false}
/>
);
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx
index 6fb078f3c808f..9cfb9f1dc25b2 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx
@@ -29,7 +29,7 @@ import {
const IndexActionConnectorFields: React.FunctionComponent> = ({ action, editActionConfig, errors, http, docLinks }) => {
+>> = ({ action, editActionConfig, errors, http, readOnly, docLinks }) => {
const { index, refresh, executionTimeField } = action.config;
const [hasTimeFieldCheckbox, setTimeFieldCheckboxState] = useState(
executionTimeField != null
@@ -115,6 +115,7 @@ const IndexActionConnectorFields: React.FunctionComponent {
editActionConfig('index', selected.length > 0 ? selected[0].value : '');
const indices = selected.map((s) => s.value as string);
@@ -145,6 +146,7 @@ const IndexActionConnectorFields: React.FunctionComponent {
editActionConfig('refresh', e.target.checked);
}}
@@ -172,6 +174,7 @@ const IndexActionConnectorFields: React.FunctionComponent {
setTimeFieldCheckboxState(!hasTimeFieldCheckbox);
// if changing from checked to not checked (hasTimeField === true),
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx
index 86730c0ab4ac7..53e68e6453690 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx
@@ -34,6 +34,7 @@ describe('PagerDutyActionConnectorFields renders', () => {
editActionConfig={() => {}}
editActionSecrets={() => {}}
docLinks={deps!.docLinks}
+ readOnly={false}
/>
);
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx
index 48da3f1778b48..6399e1f80984c 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx
@@ -12,7 +12,7 @@ import { PagerDutyActionConnector } from '.././types';
const PagerDutyActionConnectorFields: React.FunctionComponent> = ({ errors, action, editActionConfig, editActionSecrets, docLinks }) => {
+>> = ({ errors, action, editActionConfig, editActionSecrets, docLinks, readOnly }) => {
const { apiUrl } = action.config;
const { routingKey } = action.secrets;
return (
@@ -31,6 +31,7 @@ const PagerDutyActionConnectorFields: React.FunctionComponent) => {
editActionConfig('apiUrl', e.target.value);
@@ -69,6 +70,7 @@ const PagerDutyActionConnectorFields: React.FunctionComponent 0 && routingKey !== undefined}
name="routingKey"
+ readOnly={readOnly}
value={routingKey || ''}
data-test-subj="pagerdutyRoutingKeyInput"
onChange={(e: React.ChangeEvent) => {
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx
index 25381614a6c07..216e6967833b2 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx
@@ -34,6 +34,7 @@ describe('ServiceNowActionConnectorFields renders', () => {
editActionConfig={() => {}}
editActionSecrets={() => {}}
docLinks={deps!.docLinks}
+ readOnly={false}
/>
);
expect(
@@ -72,6 +73,7 @@ describe('ServiceNowActionConnectorFields renders', () => {
editActionConfig={() => {}}
editActionSecrets={() => {}}
docLinks={deps!.docLinks}
+ readOnly={false}
consumer={'case'}
/>
);
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx
index 139ef8fa58ff3..f99a276305d75 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx
@@ -25,7 +25,7 @@ import { FieldMapping } from './case_mappings/field_mapping';
const ServiceNowConnectorFields: React.FC> = ({ action, editActionSecrets, editActionConfig, errors, consumer, docLinks }) => {
+>> = ({ action, editActionSecrets, editActionConfig, errors, consumer, readOnly, docLinks }) => {
// TODO: remove incidentConfiguration later, when Case ServiceNow will move their fields to the level of action execution
const { apiUrl, incidentConfiguration, isCaseOwned } = action.config;
const mapping = incidentConfiguration ? incidentConfiguration.mapping : [];
@@ -97,6 +97,7 @@ const ServiceNowConnectorFields: React.FC
{
editActionConfig={() => {}}
editActionSecrets={() => {}}
docLinks={deps!.docLinks}
+ readOnly={false}
/>
);
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx
index b6efd9fa93266..aa3a1932eacdb 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx
@@ -12,7 +12,7 @@ import { SlackActionConnector } from '../types';
const SlackActionFields: React.FunctionComponent> = ({ action, editActionSecrets, errors, docLinks }) => {
+>> = ({ action, editActionSecrets, errors, readOnly, docLinks }) => {
const { webhookUrl } = action.secrets;
return (
@@ -44,6 +44,7 @@ const SlackActionFields: React.FunctionComponent 0 && webhookUrl !== undefined}
name="webhookUrl"
+ readOnly={readOnly}
placeholder="Example: https://hooks.slack.com/services"
value={webhookUrl || ''}
data-test-subj="slackWebhookUrlInput"
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx
index 3a2afff03c58f..7f2bed6c41f3b 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx
@@ -33,6 +33,7 @@ describe('WebhookActionConnectorFields renders', () => {
editActionConfig={() => {}}
editActionSecrets={() => {}}
docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart}
+ readOnly={false}
/>
);
expect(wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').length > 0).toBeTruthy();
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx
index 2321d5b4b5479..52160441adb5b 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx
@@ -30,7 +30,7 @@ const HTTP_VERBS = ['post', 'put'];
const WebhookActionConnectorFields: React.FunctionComponent> = ({ action, editActionConfig, editActionSecrets, errors }) => {
+>> = ({ action, editActionConfig, editActionSecrets, errors, readOnly }) => {
const { user, password } = action.secrets;
const { method, url, headers } = action.config;
@@ -128,6 +128,7 @@ const WebhookActionConnectorFields: React.FunctionComponent {
@@ -153,6 +154,7 @@ const WebhookActionConnectorFields: React.FunctionComponent {
@@ -222,6 +224,7 @@ const WebhookActionConnectorFields: React.FunctionComponent ({ text: verb.toUpperCase(), value: verb }))}
onChange={(e) => {
@@ -247,6 +250,7 @@ const WebhookActionConnectorFields: React.FunctionComponent 0 && url !== undefined}
fullWidth
+ readOnly={readOnly}
value={url || ''}
placeholder="https:// or http://"
data-test-subj="webhookUrlText"
@@ -280,6 +284,7 @@ const WebhookActionConnectorFields: React.FunctionComponent 0 && user !== undefined}
name="user"
+ readOnly={readOnly}
value={user || ''}
data-test-subj="webhookUserInput"
onChange={(e) => {
@@ -309,6 +314,7 @@ const WebhookActionConnectorFields: React.FunctionComponent 0 && password !== undefined}
value={password || ''}
data-test-subj="webhookPasswordInput"
@@ -328,6 +334,7 @@ const WebhookActionConnectorFields: React.FunctionComponent
jest.resetAllMocks());
@@ -183,6 +184,7 @@ function getAlertType(actionVariables: ActionVariables): AlertType {
actionVariables,
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
- producer: 'alerting',
+ authorizedConsumers: {},
+ producer: ALERTS_FEATURE_ID,
};
}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts
index 94d9166b40909..23caf2cfb31a8 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts
@@ -27,6 +27,7 @@ import {
health,
} from './alert_api';
import uuid from 'uuid';
+import { ALERTS_FEATURE_ID } from '../../../../alerts/common';
const http = httpServiceMock.createStartContract();
@@ -42,9 +43,10 @@ describe('loadAlertTypes', () => {
context: [{ name: 'var1', description: 'val1' }],
state: [{ name: 'var2', description: 'val2' }],
},
- producer: 'alerting',
+ producer: ALERTS_FEATURE_ID,
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
+ authorizedConsumers: {},
},
];
http.get.mockResolvedValueOnce(resolvedValue);
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts
index 82d03be41e1aa..065a782ee96a2 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts
@@ -4,6 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { BUILT_IN_ALERTS_FEATURE_ID } from '../../../../alerting_builtins/common';
+import { Alert, AlertType } from '../../types';
+
/**
* NOTE: Applications that want to show the alerting UIs will need to add
* check against their features here until we have a better solution. This
@@ -12,7 +15,7 @@
type Capabilities = Record;
-const apps = ['apm', 'siem', 'uptime', 'infrastructure'];
+const apps = ['apm', 'siem', 'uptime', 'infrastructure', 'actions', BUILT_IN_ALERTS_FEATURE_ID];
function hasCapability(capabilities: Capabilities, capability: string) {
return apps.some((app) => capabilities[app]?.[capability]);
@@ -23,8 +26,17 @@ function createCapabilityCheck(capability: string) {
}
export const hasShowAlertsCapability = createCapabilityCheck('alerting:show');
-export const hasShowActionsCapability = createCapabilityCheck('actions:show');
-export const hasSaveAlertsCapability = createCapabilityCheck('alerting:save');
-export const hasSaveActionsCapability = createCapabilityCheck('actions:save');
-export const hasDeleteAlertsCapability = createCapabilityCheck('alerting:delete');
-export const hasDeleteActionsCapability = createCapabilityCheck('actions:delete');
+
+export const hasShowActionsCapability = (capabilities: Capabilities) => capabilities?.actions?.show;
+export const hasSaveActionsCapability = (capabilities: Capabilities) => capabilities?.actions?.save;
+export const hasExecuteActionsCapability = (capabilities: Capabilities) =>
+ capabilities?.actions?.execute;
+export const hasDeleteActionsCapability = (capabilities: Capabilities) =>
+ capabilities?.actions?.delete;
+
+export function hasAllPrivilege(alert: Alert, alertType?: AlertType): boolean {
+ return alertType?.authorizedConsumers[alert.consumer]?.all ?? false;
+}
+export function hasReadPrivilege(alert: Alert, alertType?: AlertType): boolean {
+ return alertType?.authorizedConsumers[alert.consumer]?.read ?? false;
+}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx
index 17a1d929a0def..b7c9865cbd9d0 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx
@@ -15,10 +15,16 @@ describe('action_connector_form', () => {
let deps: any;
beforeAll(async () => {
const mocks = coreMock.createSetup();
+ const [
+ {
+ application: { capabilities },
+ },
+ ] = await mocks.getStartServices();
deps = {
http: mocks.http,
actionTypeRegistry: actionTypeRegistry as any,
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' },
+ capabilities,
};
});
@@ -56,6 +62,7 @@ describe('action_connector_form', () => {
http={deps!.http}
actionTypeRegistry={deps!.actionTypeRegistry}
docLinks={deps!.docLinks}
+ capabilities={deps!.capabilities}
/>
);
}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx
index 794f5548c4b44..ed4edb0229c2b 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx
@@ -18,10 +18,11 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { HttpSetup, DocLinksStart } from 'kibana/public';
+import { HttpSetup, ApplicationStart, DocLinksStart } from 'kibana/public';
import { ReducerAction } from './connector_reducer';
import { ActionConnector, IErrorObject, ActionTypeModel } from '../../../types';
import { TypeRegistry } from '../../type_registry';
+import { hasSaveActionsCapability } from '../../lib/capabilities';
export function validateBaseProperties(actionObject: ActionConnector) {
const validationResult = { errors: {} };
@@ -53,6 +54,7 @@ interface ActionConnectorProps {
http: HttpSetup;
actionTypeRegistry: TypeRegistry;
docLinks: DocLinksStart;
+ capabilities: ApplicationStart['capabilities'];
consumer?: string;
}
@@ -65,8 +67,11 @@ export const ActionConnectorForm = ({
http,
actionTypeRegistry,
docLinks,
+ capabilities,
consumer,
}: ActionConnectorProps) => {
+ const canSave = hasSaveActionsCapability(capabilities);
+
const setActionProperty = (key: string, value: any) => {
dispatch({ command: { type: 'setProperty' }, payload: { key, value } });
};
@@ -138,6 +143,7 @@ export const ActionConnectorForm = ({
>
0 && connector.name !== undefined}
name="name"
placeholder="Untitled"
@@ -167,6 +173,7 @@ export const ActionConnectorForm = ({
{
+ const canSave = hasSaveActionsCapability(capabilities);
+
const [addModalVisible, setAddModalVisibility] = useState(false);
const [activeActionItem, setActiveActionItem] = useState(
undefined
@@ -254,6 +257,7 @@ export const ActionForm = ({
/>
}
labelAppend={
+ canSave &&
actionTypesIndex &&
actionTypesIndex[actionConnector.actionTypeId].enabledInConfig ? (
+ );
return (
- actionItem.id === emptyId) ? (
- actionItem.id === emptyId) ? (
+ noConnectorsLabel
+ ) : (
+
+ )
+ }
+ actions={[
+ {
+ setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index });
+ setAddModalVisibility(true);
}}
- />
- ) : (
-
- )
- }
- actions={[
- {
- setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index });
- setAddModalVisibility(true);
- }}
- >
+ >
+
+ ,
+ ]}
+ />
+ ) : (
+
+
-
,
- ]}
- />
+
+
+ )}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx
index 60ec0cfa6955e..19ce653e465f1 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx
@@ -118,6 +118,7 @@ export const ConnectorAddFlyout = ({
actionTypeRegistry={actionTypeRegistry}
http={http}
docLinks={docLinks}
+ capabilities={capabilities}
consumer={consumer}
/>
);
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx
index 1b35b5636872d..3d621367fc40a 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx
@@ -26,10 +26,10 @@ describe('connector_add_modal', () => {
http: mocks.http,
capabilities: {
...capabilities,
- siem: {
- 'actions:show': true,
- 'actions:save': true,
- 'actions:delete': true,
+ actions: {
+ show: true,
+ save: true,
+ delete: true,
},
},
actionTypeRegistry: actionTypeRegistry as any,
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx
index 67c836fc12cf7..90abb986517d4 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx
@@ -166,6 +166,7 @@ export const ConnectorAddModal = ({
actionTypeRegistry={actionTypeRegistry}
docLinks={docLinks}
http={http}
+ capabilities={capabilities}
consumer={consumer}
/>
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx
index 52425a616aad4..ca75e730062ab 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx
@@ -195,6 +195,7 @@ export const ConnectorEditFlyout = ({
actionTypeRegistry={actionTypeRegistry}
http={http}
docLinks={docLinks}
+ capabilities={capabilities}
consumer={consumer}
/>
) : (
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx
index 23a7223f9c21b..c96e62df71ce4 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx
@@ -61,10 +61,10 @@ describe('actions_connectors_list component empty', () => {
navigateToApp,
capabilities: {
...capabilities,
- siem: {
- 'actions:show': true,
- 'actions:save': true,
- 'actions:delete': true,
+ actions: {
+ show: true,
+ save: true,
+ delete: true,
},
},
history: scopedHistoryMock.create(),
@@ -168,10 +168,10 @@ describe('actions_connectors_list component with items', () => {
navigateToApp,
capabilities: {
...capabilities,
- securitySolution: {
- 'actions:show': true,
- 'actions:save': true,
- 'actions:delete': true,
+ actions: {
+ show: true,
+ save: true,
+ delete: true,
},
},
history: scopedHistoryMock.create(),
@@ -256,10 +256,10 @@ describe('actions_connectors_list component empty with show only capability', ()
navigateToApp,
capabilities: {
...capabilities,
- securitySolution: {
- 'actions:show': true,
- 'actions:save': false,
- 'actions:delete': false,
+ actions: {
+ show: true,
+ save: false,
+ delete: false,
},
},
history: scopedHistoryMock.create(),
@@ -287,7 +287,7 @@ describe('actions_connectors_list component empty with show only capability', ()
it('renders no permissions to create connector', async () => {
await setup();
- expect(wrapper.find('[defaultMessage="No permissions to create connector"]')).toHaveLength(1);
+ expect(wrapper.find('[defaultMessage="No permissions to create connectors"]')).toHaveLength(1);
expect(wrapper.find('[data-test-subj="createActionButton"]')).toHaveLength(0);
});
});
@@ -345,10 +345,10 @@ describe('actions_connectors_list with show only capability', () => {
navigateToApp,
capabilities: {
...capabilities,
- securitySolution: {
- 'actions:show': true,
- 'actions:save': false,
- 'actions:delete': false,
+ actions: {
+ show: true,
+ save: false,
+ delete: false,
},
},
history: scopedHistoryMock.create(),
@@ -446,10 +446,10 @@ describe('actions_connectors_list component with disabled items', () => {
navigateToApp,
capabilities: {
...capabilities,
- securitySolution: {
- 'actions:show': true,
- 'actions:save': true,
- 'actions:delete': true,
+ actions: {
+ show: true,
+ save: true,
+ delete: true,
},
},
history: scopedHistoryMock.create(),
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx
index 5d52896cc628f..837529bfc938d 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx
@@ -17,6 +17,7 @@ import {
EuiBetaBadge,
EuiToolTip,
EuiButtonIcon,
+ EuiEmptyPrompt,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -324,30 +325,45 @@ export const ActionsConnectorsList: React.FunctionComponent = () => {
/>
,
],
- toolsRight: [
- setAddFlyoutVisibility(true)}
- >
-
- ,
- ],
+ toolsRight: canSave
+ ? [
+ setAddFlyoutVisibility(true)}
+ >
+
+ ,
+ ]
+ : [],
}}
/>
);
const noPermissionPrompt = (
-
-
-
+
+
+
+ }
+ body={
+
+
+
+ }
+ />
);
return (
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx
index d8f0d0b6b20a0..ccaa180de0edc 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx
@@ -20,6 +20,8 @@ import { i18n } from '@kbn/i18n';
import { ViewInApp } from './view_in_app';
import { PLUGIN } from '../../../constants/plugin';
import { coreMock } from 'src/core/public/mocks';
+import { ALERTS_FEATURE_ID } from '../../../../../../alerts/common';
+
const mockes = coreMock.createSetup();
jest.mock('../../../app_context', () => ({
@@ -29,8 +31,6 @@ jest.mock('../../../app_context', () => ({
get: jest.fn(() => ({})),
securitySolution: {
'alerting:show': true,
- 'alerting:save': true,
- 'alerting:delete': true,
},
},
actionTypeRegistry: jest.fn(),
@@ -66,7 +66,9 @@ jest.mock('react-router-dom', () => ({
}));
jest.mock('../../../lib/capabilities', () => ({
+ hasAllPrivilege: jest.fn(() => true),
hasSaveAlertsCapability: jest.fn(() => true),
+ hasExecuteActionsCapability: jest.fn(() => true),
}));
const mockAlertApis = {
@@ -77,6 +79,10 @@ const mockAlertApis = {
requestRefresh: jest.fn(),
};
+const authorizedConsumers = {
+ [ALERTS_FEATURE_ID]: { read: true, all: true },
+};
+
// const AlertDetails = withBulkAlertOperations(RawAlertDetails);
describe('alert_details', () => {
// mock Api handlers
@@ -89,7 +95,8 @@ describe('alert_details', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
- producer: 'alerting',
+ producer: ALERTS_FEATURE_ID,
+ authorizedConsumers,
};
expect(
@@ -127,7 +134,8 @@ describe('alert_details', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
- producer: 'alerting',
+ producer: ALERTS_FEATURE_ID,
+ authorizedConsumers,
};
expect(
@@ -156,7 +164,8 @@ describe('alert_details', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
- producer: 'alerting',
+ producer: ALERTS_FEATURE_ID,
+ authorizedConsumers,
};
const actionTypes: ActionType[] = [
@@ -209,7 +218,8 @@ describe('alert_details', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
- producer: 'alerting',
+ producer: ALERTS_FEATURE_ID,
+ authorizedConsumers,
};
const actionTypes: ActionType[] = [
{
@@ -267,7 +277,8 @@ describe('alert_details', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
- producer: 'alerting',
+ producer: ALERTS_FEATURE_ID,
+ authorizedConsumers,
};
expect(
@@ -286,7 +297,8 @@ describe('alert_details', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
- producer: 'alerting',
+ producer: ALERTS_FEATURE_ID,
+ authorizedConsumers,
};
expect(
@@ -314,7 +326,8 @@ describe('disable button', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
- producer: 'alerting',
+ producer: ALERTS_FEATURE_ID,
+ authorizedConsumers,
};
const enableButton = shallow(
@@ -341,7 +354,8 @@ describe('disable button', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
- producer: 'alerting',
+ producer: ALERTS_FEATURE_ID,
+ authorizedConsumers,
};
const enableButton = shallow(
@@ -368,7 +382,8 @@ describe('disable button', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
- producer: 'alerting',
+ producer: ALERTS_FEATURE_ID,
+ authorizedConsumers,
};
const disableAlert = jest.fn();
@@ -404,7 +419,8 @@ describe('disable button', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
- producer: 'alerting',
+ producer: ALERTS_FEATURE_ID,
+ authorizedConsumers,
};
const enableAlert = jest.fn();
@@ -443,7 +459,8 @@ describe('mute button', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
- producer: 'alerting',
+ producer: ALERTS_FEATURE_ID,
+ authorizedConsumers,
};
const enableButton = shallow(
@@ -471,7 +488,8 @@ describe('mute button', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
- producer: 'alerting',
+ producer: ALERTS_FEATURE_ID,
+ authorizedConsumers,
};
const enableButton = shallow(
@@ -499,7 +517,8 @@ describe('mute button', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
- producer: 'alerting',
+ producer: ALERTS_FEATURE_ID,
+ authorizedConsumers,
};
const muteAlert = jest.fn();
@@ -536,7 +555,8 @@ describe('mute button', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
- producer: 'alerting',
+ producer: ALERTS_FEATURE_ID,
+ authorizedConsumers,
};
const unmuteAlert = jest.fn();
@@ -573,7 +593,8 @@ describe('mute button', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
- producer: 'alerting',
+ producer: ALERTS_FEATURE_ID,
+ authorizedConsumers,
};
const enableButton = shallow(
@@ -590,6 +611,136 @@ describe('mute button', () => {
});
});
+describe('edit button', () => {
+ const actionTypes: ActionType[] = [
+ {
+ id: '.server-log',
+ name: 'Server log',
+ enabled: true,
+ enabledInConfig: true,
+ enabledInLicense: true,
+ minimumLicenseRequired: 'basic',
+ },
+ ];
+
+ it('should render an edit button when alert and actions are editable', () => {
+ const alert = mockAlert({
+ enabled: true,
+ muteAll: false,
+ actions: [
+ {
+ group: 'default',
+ id: uuid.v4(),
+ params: {},
+ actionTypeId: '.server-log',
+ },
+ ],
+ });
+
+ const alertType = {
+ id: '.noop',
+ name: 'No Op',
+ actionGroups: [{ id: 'default', name: 'Default' }],
+ actionVariables: { context: [], state: [] },
+ defaultActionGroupId: 'default',
+ producer: 'alerting',
+ authorizedConsumers,
+ };
+
+ expect(
+ shallow(
+
+ )
+ .find(EuiButtonEmpty)
+ .find('[name="edit"]')
+ .first()
+ .exists()
+ ).toBeTruthy();
+ });
+
+ it('should not render an edit button when alert editable but actions arent', () => {
+ const { hasExecuteActionsCapability } = jest.requireMock('../../../lib/capabilities');
+ hasExecuteActionsCapability.mockReturnValue(false);
+ const alert = mockAlert({
+ enabled: true,
+ muteAll: false,
+ actions: [
+ {
+ group: 'default',
+ id: uuid.v4(),
+ params: {},
+ actionTypeId: '.server-log',
+ },
+ ],
+ });
+
+ const alertType = {
+ id: '.noop',
+ name: 'No Op',
+ actionGroups: [{ id: 'default', name: 'Default' }],
+ actionVariables: { context: [], state: [] },
+ defaultActionGroupId: 'default',
+ producer: 'alerting',
+ authorizedConsumers,
+ };
+
+ expect(
+ shallow(
+
+ )
+ .find(EuiButtonEmpty)
+ .find('[name="edit"]')
+ .first()
+ .exists()
+ ).toBeFalsy();
+ });
+
+ it('should render an edit button when alert editable but actions arent when there are no actions on the alert', () => {
+ const { hasExecuteActionsCapability } = jest.requireMock('../../../lib/capabilities');
+ hasExecuteActionsCapability.mockReturnValue(false);
+ const alert = mockAlert({
+ enabled: true,
+ muteAll: false,
+ actions: [],
+ });
+
+ const alertType = {
+ id: '.noop',
+ name: 'No Op',
+ actionGroups: [{ id: 'default', name: 'Default' }],
+ actionVariables: { context: [], state: [] },
+ defaultActionGroupId: 'default',
+ producer: 'alerting',
+ authorizedConsumers,
+ };
+
+ expect(
+ shallow(
+
+ )
+ .find(EuiButtonEmpty)
+ .find('[name="edit"]')
+ .first()
+ .exists()
+ ).toBeTruthy();
+ });
+});
+
function mockAlert(overloads: Partial = {}): Alert {
return {
id: uuid.v4(),
@@ -597,7 +748,7 @@ function mockAlert(overloads: Partial = {}): Alert {
name: `alert-${uuid.v4()}`,
tags: [],
alertTypeId: '.noop',
- consumer: 'consumer',
+ consumer: ALERTS_FEATURE_ID,
schedule: { interval: '1m' },
actions: [],
params: {},
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx
index 66a7ac25d4a70..b1dd78ff59f34 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx
@@ -28,7 +28,7 @@ import {
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { useAppDependencies } from '../../../app_context';
-import { hasSaveAlertsCapability } from '../../../lib/capabilities';
+import { hasAllPrivilege, hasExecuteActionsCapability } from '../../../lib/capabilities';
import { Alert, AlertType, ActionType } from '../../../../types';
import {
ComponentOpts as BulkOperationsComponentOpts,
@@ -71,12 +71,20 @@ export const AlertDetails: React.FunctionComponent = ({
dataPlugin,
} = useAppDependencies();
- const canSave = hasSaveAlertsCapability(capabilities);
+ const canExecuteActions = hasExecuteActionsCapability(capabilities);
+ const canSaveAlert =
+ hasAllPrivilege(alert, alertType) &&
+ // if the alert has actions, can the user save the alert's action params
+ (canExecuteActions || (!canExecuteActions && alert.actions.length === 0));
+
const actionTypesByTypeId = keyBy(actionTypes, 'id');
const hasEditButton =
- canSave && alertTypeRegistry.has(alert.alertTypeId)
+ // can the user save the alert
+ canSaveAlert &&
+ // is this alert type editable from within Alerts Management
+ (alertTypeRegistry.has(alert.alertTypeId)
? !alertTypeRegistry.get(alert.alertTypeId).requiresAppContext
- : false;
+ : false);
const alertActions = alert.actions;
const uniqueActions = Array.from(new Set(alertActions.map((item: any) => item.actionTypeId)));
@@ -124,6 +132,7 @@ export const AlertDetails: React.FunctionComponent = ({
data-test-subj="openEditAlertFlyoutButton"
iconType="pencil"
onClick={() => setEditFlyoutVisibility(true)}
+ name="edit"
>
= ({
{
@@ -229,7 +238,7 @@ export const AlertDetails: React.FunctionComponent = ({
{
if (isMuted) {
@@ -255,7 +264,11 @@ export const AlertDetails: React.FunctionComponent = ({
{alert.enabled ? (
-
+
) : (
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.test.tsx
index 2531fd2625b4b..dd2ee48b7a620 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.test.tsx
@@ -52,7 +52,9 @@ describe('alert_instances', () => {
];
expect(
- shallow()
+ shallow(
+
+ )
.find(EuiBasicTable)
.prop('items')
).toEqual(instances);
@@ -68,6 +70,7 @@ describe('alert_instances', () => {
durationEpoch={fake2MinutesAgo.getTime()}
{...mockAPIs}
alert={alert}
+ readOnly={false}
alertState={alertState}
/>
)
@@ -95,6 +98,7 @@ describe('alert_instances', () => {
{
Promise;
durationEpoch?: number;
} & Pick;
export const alertInstancesTableColumns = (
- onMuteAction: (instance: AlertInstanceListItem) => Promise
+ onMuteAction: (instance: AlertInstanceListItem) => Promise,
+ readOnly: boolean
) => [
{
field: 'instance',
@@ -90,6 +92,7 @@ export const alertInstancesTableColumns = (
showLabel={false}
compressed={true}
checked={alertInstance.isMuted}
+ disabled={readOnly}
data-test-subj={`muteAlertInstanceButton_${alertInstance.instance}`}
onChange={() => onMuteAction(alertInstance)}
/>
@@ -109,6 +112,7 @@ function durationAsString(duration: Duration): string {
export function AlertInstances({
alert,
+ readOnly,
alertState: { alertInstances = {} },
muteAlertInstance,
unmuteAlertInstance,
@@ -162,7 +166,7 @@ export function AlertInstances({
cellProps={() => ({
'data-test-subj': 'cell',
})}
- columns={alertInstancesTableColumns(onMuteAction)}
+ columns={alertInstancesTableColumns(onMuteAction, readOnly)}
data-test-subj="alertInstancesList"
/>
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.test.tsx
index 9bff33e4aa69c..975856beba556 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.test.tsx
@@ -22,9 +22,9 @@ describe('alert_state_route', () => {
const alert = mockAlert();
expect(
- shallow().containsMatchingElement(
-
- )
+ shallow(
+
+ ).containsMatchingElement()
).toBeTruthy();
});
});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx
index a02b44523e26c..d8a7d18eb87a9 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx
@@ -18,11 +18,13 @@ import { AlertInstancesWithApi as AlertInstances } from './alert_instances';
type WithAlertStateProps = {
alert: Alert;
+ readOnly: boolean;
requestRefresh: () => Promise;
} & Pick;
export const AlertInstancesRoute: React.FunctionComponent = ({
alert,
+ readOnly,
requestRefresh,
loadAlertState,
}) => {
@@ -36,7 +38,12 @@ export const AlertInstancesRoute: React.FunctionComponent =
}, [alert]);
return alertState ? (
-
+
) : (
({
+ loadAlertTypes: jest.fn(),
+ health: jest.fn((async) => ({ isSufficientlySecure: true, hasPermanentEncryptionKey: true })),
+}));
+
const actionTypeRegistry = actionTypeRegistryMock.create();
const alertTypeRegistry = alertTypeRegistryMock.create();
@@ -42,6 +48,30 @@ describe('alert_add', () => {
async function setup() {
const mocks = coreMock.createSetup();
+ const { loadAlertTypes } = jest.requireMock('../../lib/alert_api');
+ const alertTypes = [
+ {
+ id: 'my-alert-type',
+ name: 'Test',
+ actionGroups: [
+ {
+ id: 'testActionGroup',
+ name: 'Test Action Group',
+ },
+ ],
+ defaultActionGroupId: 'testActionGroup',
+ producer: ALERTS_FEATURE_ID,
+ authorizedConsumers: {
+ [ALERTS_FEATURE_ID]: { read: true, all: true },
+ test: { read: true, all: true },
+ },
+ actionVariables: {
+ context: [],
+ state: [],
+ },
+ },
+ ];
+ loadAlertTypes.mockResolvedValue(alertTypes);
const [
{
application: { capabilities },
@@ -120,7 +150,11 @@ describe('alert_add', () => {
},
}}
>
-
{}} />
+ {}}
+ />
);
@@ -135,6 +169,10 @@ describe('alert_add', () => {
it('renders alert add flyout', async () => {
await setup();
+ await new Promise((resolve) => {
+ setTimeout(resolve, 1000);
+ });
+
expect(wrapper.find('[data-test-subj="addAlertFlyoutTitle"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="saveAlertButton"]').exists()).toBeTruthy();
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx
index 52c281761f2c1..20cbd42e34b67 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx
@@ -168,6 +168,9 @@ export const AlertAdd = ({
dispatch={dispatch}
errors={errors}
canChangeTrigger={canChangeTrigger}
+ operation={i18n.translate('xpack.triggersActionsUI.sections.alertAdd.operationName', {
+ defaultMessage: 'create',
+ })}
/>
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx
index 076f4b69fb496..f991cea9c009c 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx
@@ -156,6 +156,9 @@ export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => {
errors={errors}
canChangeTrigger={false}
setHasActionsDisabled={setHasActionsDisabled}
+ operation="i18n.translate('xpack.triggersActionsUI.sections.alertEdit.operationName', {
+ defaultMessage: 'edit',
+ })"
/>
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx
index c9ce2848c5670..6091519f5851e 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx
@@ -13,6 +13,8 @@ import { ValidationResult, Alert } from '../../../types';
import { AlertForm } from './alert_form';
import { AlertsContextProvider } from '../../context/alerts_context';
import { coreMock } from 'src/core/public/mocks';
+import { ALERTS_FEATURE_ID } from '../../../../../alerts/common';
+
const actionTypeRegistry = actionTypeRegistryMock.create();
const alertTypeRegistry = alertTypeRegistryMock.create();
jest.mock('../../lib/alert_api', () => ({
@@ -20,6 +22,10 @@ jest.mock('../../lib/alert_api', () => ({
}));
describe('alert_form', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
let deps: any;
const alertType = {
id: 'my-alert-type',
@@ -63,6 +69,26 @@ describe('alert_form', () => {
async function setup() {
const mocks = coreMock.createSetup();
+ const { loadAlertTypes } = jest.requireMock('../../lib/alert_api');
+ const alertTypes = [
+ {
+ id: 'my-alert-type',
+ name: 'Test',
+ actionGroups: [
+ {
+ id: 'testActionGroup',
+ name: 'Test Action Group',
+ },
+ ],
+ defaultActionGroupId: 'testActionGroup',
+ producer: ALERTS_FEATURE_ID,
+ authorizedConsumers: {
+ [ALERTS_FEATURE_ID]: { read: true, all: true },
+ test: { read: true, all: true },
+ },
+ },
+ ];
+ loadAlertTypes.mockResolvedValue(alertTypes);
const [
{
application: { capabilities },
@@ -85,7 +111,7 @@ describe('alert_form', () => {
const initialAlert = ({
name: 'test',
params: {},
- consumer: 'alerts',
+ consumer: ALERTS_FEATURE_ID,
schedule: {
interval: '1m',
},
@@ -111,7 +137,12 @@ describe('alert_form', () => {
capabilities: deps!.capabilities,
}}
>
- {}} errors={{ name: [], interval: [] }} />
+ {}}
+ errors={{ name: [], interval: [] }}
+ operation="create"
+ />
);
@@ -167,7 +198,11 @@ describe('alert_form', () => {
},
],
defaultActionGroupId: 'testActionGroup',
- producer: 'alerting',
+ producer: ALERTS_FEATURE_ID,
+ authorizedConsumers: {
+ [ALERTS_FEATURE_ID]: { read: true, all: true },
+ test: { read: true, all: true },
+ },
},
{
id: 'same-consumer-producer-alert-type',
@@ -180,6 +215,10 @@ describe('alert_form', () => {
],
defaultActionGroupId: 'testActionGroup',
producer: 'test',
+ authorizedConsumers: {
+ [ALERTS_FEATURE_ID]: { read: true, all: true },
+ test: { read: true, all: true },
+ },
},
]);
const mocks = coreMock.createSetup();
@@ -250,7 +289,12 @@ describe('alert_form', () => {
capabilities: deps!.capabilities,
}}
>
- {}} errors={{ name: [], interval: [] }} />
+ {}}
+ errors={{ name: [], interval: [] }}
+ operation="create"
+ />
);
@@ -302,7 +346,7 @@ describe('alert_form', () => {
name: 'test',
alertTypeId: alertType.id,
params: {},
- consumer: 'alerts',
+ consumer: ALERTS_FEATURE_ID,
schedule: {
interval: '1m',
},
@@ -328,7 +372,12 @@ describe('alert_form', () => {
capabilities: deps!.capabilities,
}}
>
- {}} errors={{ name: [], interval: [] }} />
+ {}}
+ errors={{ name: [], interval: [] }}
+ operation="create"
+ />
);
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx
index 874091b2bb7a8..47ec2c436ca50 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx
@@ -24,6 +24,7 @@ import {
EuiButtonIcon,
EuiHorizontalRule,
EuiLoadingSpinner,
+ EuiEmptyPrompt,
} from '@elastic/eui';
import { some, filter, map, fold } from 'fp-ts/lib/Option';
import { pipe } from 'fp-ts/lib/pipeable';
@@ -38,6 +39,8 @@ import { AlertTypeModel, Alert, IErrorObject, AlertAction, AlertTypeIndex } from
import { getTimeOptions } from '../../../common/lib/get_time_options';
import { useAlertsContext } from '../../context/alerts_context';
import { ActionForm } from '../action_connector_form';
+import { ALERTS_FEATURE_ID } from '../../../../../alerts/common';
+import { hasAllPrivilege, hasShowActionsCapability } from '../../lib/capabilities';
export function validateBaseProperties(alertObject: Alert) {
const validationResult = { errors: {} };
@@ -78,6 +81,7 @@ interface AlertFormProps {
errors: IErrorObject;
canChangeTrigger?: boolean; // to hide Change trigger button
setHasActionsDisabled?: (value: boolean) => void;
+ operation: string;
}
export const AlertForm = ({
@@ -86,6 +90,7 @@ export const AlertForm = ({
dispatch,
errors,
setHasActionsDisabled,
+ operation,
}: AlertFormProps) => {
const alertsContext = useAlertsContext();
const {
@@ -96,6 +101,7 @@ export const AlertForm = ({
docLinks,
capabilities,
} = alertsContext;
+ const canShowActions = hasShowActionsCapability(capabilities);
const [alertTypeModel, setAlertTypeModel] = useState(
alert.alertTypeId ? alertTypeRegistry.get(alert.alertTypeId) : null
@@ -121,12 +127,12 @@ export const AlertForm = ({
(async () => {
try {
const alertTypes = await loadAlertTypes({ http });
- const index: AlertTypeIndex = {};
+ const index: AlertTypeIndex = new Map();
for (const alertTypeItem of alertTypes) {
- index[alertTypeItem.id] = alertTypeItem;
+ index.set(alertTypeItem.id, alertTypeItem);
}
- if (alert.alertTypeId && index[alert.alertTypeId]) {
- setDefaultActionGroupId(index[alert.alertTypeId].defaultActionGroupId);
+ if (alert.alertTypeId && index.has(alert.alertTypeId)) {
+ setDefaultActionGroupId(index.get(alert.alertTypeId)!.defaultActionGroupId);
}
setAlertTypesIndex(index);
} catch (e) {
@@ -167,21 +173,21 @@ export const AlertForm = ({
? alertTypeModel.alertParamsExpression
: null;
- const alertTypeRegistryList =
- alert.consumer === 'alerts'
- ? alertTypeRegistry
- .list()
- .filter(
- (alertTypeRegistryItem: AlertTypeModel) => !alertTypeRegistryItem.requiresAppContext
- )
- : alertTypeRegistry
- .list()
- .filter(
- (alertTypeRegistryItem: AlertTypeModel) =>
- alertTypesIndex &&
- alertTypesIndex[alertTypeRegistryItem.id] &&
- alertTypesIndex[alertTypeRegistryItem.id].producer === alert.consumer
- );
+ const alertTypeRegistryList = alertTypesIndex
+ ? alertTypeRegistry
+ .list()
+ .filter(
+ (alertTypeRegistryItem: AlertTypeModel) =>
+ alertTypesIndex.has(alertTypeRegistryItem.id) &&
+ hasAllPrivilege(alert, alertTypesIndex.get(alertTypeRegistryItem.id))
+ )
+ .filter((alertTypeRegistryItem: AlertTypeModel) =>
+ alert.consumer === ALERTS_FEATURE_ID
+ ? !alertTypeRegistryItem.requiresAppContext
+ : alertTypesIndex.get(alertTypeRegistryItem.id)!.producer === alert.consumer
+ )
+ : [];
+
const alertTypeNodes = alertTypeRegistryList.map(function (item, index) {
return (
@@ -257,13 +263,13 @@ export const AlertForm = ({
/>
) : null}
- {defaultActionGroupId ? (
+ {canShowActions && defaultActionGroupId ? (
av.name
)
: undefined
@@ -487,7 +493,7 @@ export const AlertForm = ({
{alertTypeModel ? (
{alertTypeDetails}
- ) : (
+ ) : alertTypeNodes.length ? (
@@ -503,7 +509,37 @@ export const AlertForm = ({
{alertTypeNodes}
+ ) : (
+
)}
);
};
+
+const NoAuthorizedAlertTypes = ({ operation }: { operation: string }) => (
+
+
+
+ }
+ body={
+
+ }
+ />
+);
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx
index 69b0856297bb5..b8278aa701293 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx
@@ -17,6 +17,7 @@ import { AppContextProvider } from '../../../app_context';
import { chartPluginMock } from '../../../../../../../../src/plugins/charts/public/mocks';
import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks';
import { alertingPluginMock } from '../../../../../../alerts/public/mocks';
+import { ALERTS_FEATURE_ID } from '../../../../../../alerts/common';
jest.mock('../../../lib/action_connector_api', () => ({
loadActionTypes: jest.fn(),
@@ -47,6 +48,17 @@ const alertType = {
alertParamsExpression: () => null,
requiresAppContext: false,
};
+const alertTypeFromApi = {
+ id: 'test_alert_type',
+ name: 'some alert type',
+ actionGroups: [{ id: 'default', name: 'Default' }],
+ actionVariables: { context: [], state: [] },
+ defaultActionGroupId: 'default',
+ producer: ALERTS_FEATURE_ID,
+ authorizedConsumers: {
+ [ALERTS_FEATURE_ID]: { read: true, all: true },
+ },
+};
alertTypeRegistry.list.mockReturnValue([alertType]);
actionTypeRegistry.list.mockReturnValue([]);
@@ -73,7 +85,7 @@ describe('alerts_list component empty', () => {
name: 'Test2',
},
]);
- loadAlertTypes.mockResolvedValue([{ id: 'test_alert_type', name: 'some alert type' }]);
+ loadAlertTypes.mockResolvedValue([alertTypeFromApi]);
loadAllActions.mockResolvedValue([]);
const mockes = coreMock.createSetup();
@@ -98,8 +110,6 @@ describe('alerts_list component empty', () => {
...capabilities,
securitySolution: {
'alerting:show': true,
- 'alerting:save': true,
- 'alerting:delete': true,
},
},
history: scopedHistoryMock.create(),
@@ -193,7 +203,7 @@ describe('alerts_list component with items', () => {
name: 'Test2',
},
]);
- loadAlertTypes.mockResolvedValue([{ id: 'test_alert_type', name: 'some alert type' }]);
+ loadAlertTypes.mockResolvedValue([alertTypeFromApi]);
loadAllActions.mockResolvedValue([]);
const mockes = coreMock.createSetup();
const [
@@ -217,8 +227,6 @@ describe('alerts_list component with items', () => {
...capabilities,
securitySolution: {
'alerting:show': true,
- 'alerting:save': true,
- 'alerting:delete': true,
},
},
history: scopedHistoryMock.create(),
@@ -299,8 +307,6 @@ describe('alerts_list component empty with show only capability', () => {
...capabilities,
securitySolution: {
'alerting:show': true,
- 'alerting:save': false,
- 'alerting:delete': false,
},
},
history: scopedHistoryMock.create(),
@@ -390,7 +396,8 @@ describe('alerts_list with show only capability', () => {
name: 'Test2',
},
]);
- loadAlertTypes.mockResolvedValue([{ id: 'test_alert_type', name: 'some alert type' }]);
+
+ loadAlertTypes.mockResolvedValue([alertTypeFromApi]);
loadAllActions.mockResolvedValue([]);
const mockes = coreMock.createSetup();
const [
@@ -414,8 +421,6 @@ describe('alerts_list with show only capability', () => {
...capabilities,
securitySolution: {
'alerting:show': true,
- 'alerting:save': false,
- 'alerting:delete': false,
},
},
history: scopedHistoryMock.create(),
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx
index 2929ce6defeaf..8cb7afbda0e70 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx
@@ -18,6 +18,7 @@ import {
EuiSpacer,
EuiLink,
EuiLoadingSpinner,
+ EuiEmptyPrompt,
} from '@elastic/eui';
import { useHistory } from 'react-router-dom';
@@ -33,10 +34,12 @@ import { TypeFilter } from './type_filter';
import { ActionTypeFilter } from './action_type_filter';
import { loadAlerts, loadAlertTypes, deleteAlerts } from '../../../lib/alert_api';
import { loadActionTypes } from '../../../lib/action_connector_api';
-import { hasDeleteAlertsCapability, hasSaveAlertsCapability } from '../../../lib/capabilities';
+import { hasExecuteActionsCapability } from '../../../lib/capabilities';
import { routeToAlertDetails, DEFAULT_SEARCH_PAGE_SIZE } from '../../../constants';
import { DeleteModalConfirmation } from '../../../components/delete_modal_confirmation';
import { EmptyPrompt } from '../../../components/prompts/empty_prompt';
+import { ALERTS_FEATURE_ID } from '../../../../../../alerts/common';
+import { hasAllPrivilege } from '../../../lib/capabilities';
const ENTER_KEY = 13;
@@ -64,8 +67,7 @@ export const AlertsList: React.FunctionComponent = () => {
charts,
dataPlugin,
} = useAppDependencies();
- const canDelete = hasDeleteAlertsCapability(capabilities);
- const canSave = hasSaveAlertsCapability(capabilities);
+ const canExecuteActions = hasExecuteActionsCapability(capabilities);
const [actionTypes, setActionTypes] = useState([]);
const [selectedIds, setSelectedIds] = useState([]);
@@ -79,7 +81,7 @@ export const AlertsList: React.FunctionComponent = () => {
const [alertTypesState, setAlertTypesState] = useState({
isLoading: false,
isInitialized: false,
- data: {},
+ data: new Map(),
});
const [alertsState, setAlertsState] = useState({
isLoading: false,
@@ -98,9 +100,9 @@ export const AlertsList: React.FunctionComponent = () => {
try {
setAlertTypesState({ ...alertTypesState, isLoading: true });
const alertTypes = await loadAlertTypes({ http });
- const index: AlertTypeIndex = {};
+ const index: AlertTypeIndex = new Map();
for (const alertType of alertTypes) {
- index[alertType.id] = alertType;
+ index.set(alertType.id, alertType);
}
setAlertTypesState({ isLoading: false, data: index, isInitialized: true });
} catch (e) {
@@ -245,11 +247,16 @@ export const AlertsList: React.FunctionComponent = () => {
},
];
+ const authorizedAlertTypes = [...alertTypesState.data.values()];
+ const authorizedToCreateAnyAlerts = authorizedAlertTypes.some(
+ (alertType) => alertType.authorizedConsumers[ALERTS_FEATURE_ID]?.all
+ );
+
const toolsRight = [
setTypesFilter(types)}
- options={Object.values(alertTypesState.data)
+ options={authorizedAlertTypes
.map((alertType) => ({
value: alertType.id,
name: alertType.name,
@@ -263,7 +270,7 @@ export const AlertsList: React.FunctionComponent = () => {
/>,
];
- if (canSave) {
+ if (authorizedToCreateAnyAlerts) {
toolsRight.push(
{
);
}
+ const authorizedToModifySelectedAlerts = selectedIds.length
+ ? filterAlertsById(alertsState.data, selectedIds).every((selectedAlert) =>
+ hasAllPrivilege(selectedAlert, alertTypesState.data.get(selectedAlert.alertTypeId))
+ )
+ : false;
+
const table = (
- {selectedIds.length > 0 && canDelete && (
+ {selectedIds.length > 0 && authorizedToModifySelectedAlerts && (
setIsPerformingAction(true)}
onActionPerformed={() => {
@@ -337,7 +351,7 @@ export const AlertsList: React.FunctionComponent = () => {
items={
alertTypesState.isInitialized === false
? []
- : convertAlertsToTableItems(alertsState.data, alertTypesState.data)
+ : convertAlertsToTableItems(alertsState.data, alertTypesState.data, canExecuteActions)
}
itemId="id"
columns={alertsTableColumns}
@@ -354,15 +368,12 @@ export const AlertsList: React.FunctionComponent = () => {
/* Don't display alert count until we have the alert types initialized */
totalItemCount: alertTypesState.isInitialized === false ? 0 : alertsState.totalItemCount,
}}
- selection={
- canDelete
- ? {
- onSelectionChange(updatedSelectedItemsList: AlertTableItem[]) {
- setSelectedIds(updatedSelectedItemsList.map((item) => item.id));
- },
- }
- : undefined
- }
+ selection={{
+ selectable: (alert: AlertTableItem) => alert.isEditable,
+ onSelectionChange(updatedSelectedItemsList: AlertTableItem[]) {
+ setSelectedIds(updatedSelectedItemsList.map((item) => item.id));
+ },
+ }}
onChange={({ page: changedPage }: { page: Pagination }) => {
setPage(changedPage);
}}
@@ -370,7 +381,11 @@ export const AlertsList: React.FunctionComponent = () => {
);
- const loadedItems = convertAlertsToTableItems(alertsState.data, alertTypesState.data);
+ const loadedItems = convertAlertsToTableItems(
+ alertsState.data,
+ alertTypesState.data,
+ canExecuteActions
+ );
const isFilterApplied = !(
isEmpty(searchText) &&
@@ -421,8 +436,10 @@ export const AlertsList: React.FunctionComponent = () => {
- ) : (
+ ) : authorizedToCreateAnyAlerts ? (
setAlertFlyoutVisibility(true)} />
+ ) : (
+ noPermissionPrompt
)}
{
}}
>
@@ -448,15 +465,44 @@ export const AlertsList: React.FunctionComponent = () => {
);
};
+const noPermissionPrompt = (
+
+
+
+ }
+ body={
+
+
+
+ }
+ />
+);
+
function filterAlertsById(alerts: Alert[], ids: string[]): Alert[] {
return alerts.filter((alert) => ids.includes(alert.id));
}
-function convertAlertsToTableItems(alerts: Alert[], alertTypesIndex: AlertTypeIndex) {
+function convertAlertsToTableItems(
+ alerts: Alert[],
+ alertTypesIndex: AlertTypeIndex,
+ canExecuteActions: boolean
+) {
return alerts.map((alert) => ({
...alert,
actionsText: alert.actions.length,
tagsText: alert.tags.join(', '),
- alertType: alertTypesIndex[alert.alertTypeId]?.name ?? alert.alertTypeId,
+ alertType: alertTypesIndex.get(alert.alertTypeId)?.name ?? alert.alertTypeId,
+ isEditable:
+ hasAllPrivilege(alert, alertTypesIndex.get(alert.alertTypeId)) &&
+ (canExecuteActions || (!canExecuteActions && !alert.actions.length)),
}));
}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.tsx
index 2b746e5dea457..9279f8a1745fc 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.tsx
@@ -20,8 +20,6 @@ import {
} from '@elastic/eui';
import { AlertTableItem } from '../../../../types';
-import { useAppDependencies } from '../../../app_context';
-import { hasDeleteAlertsCapability, hasSaveAlertsCapability } from '../../../lib/capabilities';
import {
ComponentOpts as BulkOperationsComponentOpts,
withBulkAlertOperations,
@@ -43,16 +41,11 @@ export const CollapsedItemActions: React.FunctionComponent = ({
muteAlert,
setAlertsToDelete,
}: ComponentOpts) => {
- const { capabilities } = useAppDependencies();
-
- const canDelete = hasDeleteAlertsCapability(capabilities);
- const canSave = hasSaveAlertsCapability(capabilities);
-
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const button = (
setIsPopoverOpen(!isPopoverOpen)}
aria-label={i18n.translate(
@@ -75,7 +68,7 @@ export const CollapsedItemActions: React.FunctionComponent = ({
= ({
{
@@ -134,7 +127,7 @@ export const CollapsedItemActions: React.FunctionComponent = ({
setAlertsToDelete([item.id])}
>
diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts
index fe3bf98b03230..dd2b070956dbc 100644
--- a/x-pack/plugins/triggers_actions_ui/public/types.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/types.ts
@@ -19,7 +19,7 @@ export { Alert, AlertAction, AlertTaskState, RawAlertInstance, AlertingFramework
export { ActionType };
export type ActionTypeIndex = Record;
-export type AlertTypeIndex = Record;
+export type AlertTypeIndex = Map;
export type ActionTypeRegistryContract = PublicMethodsOf<
TypeRegistry>
>;
@@ -32,6 +32,7 @@ export interface ActionConnectorFieldsProps {
errors: IErrorObject;
docLinks: DocLinksStart;
http?: HttpSetup;
+ readOnly: boolean;
consumer?: string;
}
@@ -101,6 +102,7 @@ export interface AlertType {
actionGroups: ActionGroup[];
actionVariables: ActionVariables;
defaultActionGroupId: ActionGroup['id'];
+ authorizedConsumers: Record;
producer: string;
}
@@ -111,6 +113,7 @@ export type AlertWithoutId = Omit;
export interface AlertTableItem extends Alert {
alertType: AlertType['name'];
tagsText: string;
+ isEditable: boolean;
}
export interface AlertTypeParamsExpressionProps<
diff --git a/x-pack/plugins/uptime/server/kibana.index.ts b/x-pack/plugins/uptime/server/kibana.index.ts
index d68bbabe82b86..a2d5f58bbec14 100644
--- a/x-pack/plugins/uptime/server/kibana.index.ts
+++ b/x-pack/plugins/uptime/server/kibana.index.ts
@@ -35,51 +35,33 @@ export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCor
icon: 'uptimeApp',
app: ['uptime', 'kibana'],
catalogue: ['uptime'],
+ alerting: ['xpack.uptime.alerts.tls', 'xpack.uptime.alerts.monitorStatus'],
privileges: {
all: {
app: ['uptime', 'kibana'],
catalogue: ['uptime'],
- api: [
- 'uptime-read',
- 'uptime-write',
- 'actions-read',
- 'actions-all',
- 'alerting-read',
- 'alerting-all',
- ],
+ api: ['uptime-read', 'uptime-write'],
savedObject: {
- all: [umDynamicSettings.name, 'alert', 'action', 'action_task_params'],
+ all: [umDynamicSettings.name, 'alert'],
read: [],
},
- ui: [
- 'save',
- 'configureSettings',
- 'show',
- 'alerting:show',
- 'actions:show',
- 'alerting:save',
- 'actions:save',
- 'alerting:delete',
- 'actions:delete',
- ],
+ alerting: {
+ all: ['xpack.uptime.alerts.tls', 'xpack.uptime.alerts.monitorStatus'],
+ },
+ ui: ['save', 'configureSettings', 'show', 'alerting:show'],
},
read: {
app: ['uptime', 'kibana'],
catalogue: ['uptime'],
- api: ['uptime-read', 'actions-read', 'actions-all', 'alerting-read', 'alerting-all'],
+ api: ['uptime-read'],
savedObject: {
- all: ['alert', 'action', 'action_task_params'],
+ all: ['alert'],
read: [umDynamicSettings.name],
},
- ui: [
- 'show',
- 'alerting:show',
- 'actions:show',
- 'alerting:save',
- 'actions:save',
- 'alerting:delete',
- 'actions:delete',
- ],
+ alerting: {
+ all: ['xpack.uptime.alerts.tls', 'xpack.uptime.alerts.monitorStatus'],
+ },
+ ui: ['show', 'alerting:show'],
},
},
});
diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts
index b8b2cbdc03f39..cb1271494c294 100644
--- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts
+++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts
@@ -61,27 +61,27 @@ export class FixturePlugin implements Plugin {
public setup(core: CoreSetup, { features, actions, alerts }: FixtureSetupDeps) {
features.registerFeature({
- id: 'alerts',
+ id: 'alertsFixture',
name: 'Alerts',
app: ['alerts', 'kibana'],
+ alerting: [
+ 'test.always-firing',
+ 'test.cumulative-firing',
+ 'test.never-firing',
+ 'test.failing',
+ 'test.authorization',
+ 'test.validation',
+ 'test.onlyContextVariables',
+ 'test.onlyStateVariables',
+ 'test.noop',
+ 'test.unrestricted-noop',
+ ],
privileges: {
all: {
app: ['alerts', 'kibana'],
@@ -36,8 +48,21 @@ export class FixturePlugin implements Plugin,
+ { alerts }: Pick
+) {
+ const noopRestrictedAlertType: AlertType = {
+ id: 'test.restricted-noop',
+ name: 'Test: Restricted Noop',
+ actionGroups: [{ id: 'default', name: 'Default' }],
+ producer: 'alertsRestrictedFixture',
+ defaultActionGroupId: 'default',
+ async executor({ services, params, state }: AlertExecutorOptions) {},
+ };
+ const noopUnrestrictedAlertType: AlertType = {
+ id: 'test.unrestricted-noop',
+ name: 'Test: Unrestricted Noop',
+ actionGroups: [{ id: 'default', name: 'Default' }],
+ producer: 'alertsRestrictedFixture',
+ defaultActionGroupId: 'default',
+ async executor({ services, params, state }: AlertExecutorOptions) {},
+ };
+ alerts.registerType(noopRestrictedAlertType);
+ alerts.registerType(noopUnrestrictedAlertType);
+}
diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/index.ts
new file mode 100644
index 0000000000000..54d6de50cff4d
--- /dev/null
+++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/index.ts
@@ -0,0 +1,9 @@
+/*
+ * 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 { FixturePlugin } from './plugin';
+
+export const plugin = () => new FixturePlugin();
diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/plugin.ts
new file mode 100644
index 0000000000000..e297733fb47eb
--- /dev/null
+++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/plugin.ts
@@ -0,0 +1,62 @@
+/*
+ * 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 { Plugin, CoreSetup } from 'kibana/server';
+import { PluginSetupContract as ActionsPluginSetup } from '../../../../../../../plugins/actions/server/plugin';
+import { PluginSetupContract as AlertingPluginSetup } from '../../../../../../../plugins/alerts/server/plugin';
+import { EncryptedSavedObjectsPluginStart } from '../../../../../../../plugins/encrypted_saved_objects/server';
+import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../../plugins/features/server';
+import { defineAlertTypes } from './alert_types';
+
+export interface FixtureSetupDeps {
+ features: FeaturesPluginSetup;
+ actions: ActionsPluginSetup;
+ alerts: AlertingPluginSetup;
+}
+
+export interface FixtureStartDeps {
+ encryptedSavedObjects: EncryptedSavedObjectsPluginStart;
+}
+
+export class FixturePlugin implements Plugin {
+ public setup(core: CoreSetup, { features, alerts }: FixtureSetupDeps) {
+ features.registerFeature({
+ id: 'alertsRestrictedFixture',
+ name: 'AlertRestricted',
+ app: ['alerts', 'kibana'],
+ alerting: ['test.restricted-noop', 'test.unrestricted-noop', 'test.noop'],
+ privileges: {
+ all: {
+ app: ['alerts', 'kibana'],
+ savedObject: {
+ all: ['alert'],
+ read: [],
+ },
+ alerting: {
+ all: ['test.restricted-noop', 'test.unrestricted-noop', 'test.noop'],
+ },
+ ui: [],
+ },
+ read: {
+ app: ['alerts', 'kibana'],
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ alerting: {
+ read: ['test.restricted-noop', 'test.unrestricted-noop', 'test.noop'],
+ },
+ ui: [],
+ },
+ },
+ });
+
+ defineAlertTypes(core, { alerts });
+ }
+
+ public start() {}
+ public stop() {}
+}
diff --git a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts
index 708e7e1b75b58..a68f8de39d48e 100644
--- a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts
+++ b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts
@@ -252,7 +252,7 @@ export class AlertUtils {
throttle: '30s',
tags: [],
alertTypeId: 'test.failing',
- consumer: 'bar',
+ consumer: 'alertsFixture',
params: {
index: ES_TEST_INDEX_NAME,
reference,
@@ -267,6 +267,22 @@ export class AlertUtils {
}
}
+export function getConsumerUnauthorizedErrorMessage(
+ operation: string,
+ alertType: string,
+ consumer: string
+) {
+ return `Unauthorized to ${operation} a "${alertType}" alert for "${consumer}"`;
+}
+
+export function getProducerUnauthorizedErrorMessage(
+ operation: string,
+ alertType: string,
+ producer: string
+) {
+ return `Unauthorized to ${operation} a "${alertType}" alert by "${producer}"`;
+}
+
function getDefaultAlwaysFiringAlertData(reference: string, actionId: string) {
const messageTemplate = `
alertId: {{alertId}},
@@ -284,7 +300,7 @@ instanceStateValue: {{state.instanceStateValue}}
throttle: '1m',
tags: ['tag-A', 'tag-B'],
alertTypeId: 'test.always-firing',
- consumer: 'bar',
+ consumer: 'alertsFixture',
params: {
index: ES_TEST_INDEX_NAME,
reference,
diff --git a/x-pack/test/alerting_api_integration/common/lib/get_test_alert_data.ts b/x-pack/test/alerting_api_integration/common/lib/get_test_alert_data.ts
index 76f78809d5d11..2e7a4e325094c 100644
--- a/x-pack/test/alerting_api_integration/common/lib/get_test_alert_data.ts
+++ b/x-pack/test/alerting_api_integration/common/lib/get_test_alert_data.ts
@@ -10,7 +10,7 @@ export function getTestAlertData(overwrites = {}) {
name: 'abc',
tags: ['foo'],
alertTypeId: 'test.noop',
- consumer: 'bar',
+ consumer: 'alertsFixture',
schedule: { interval: '1m' },
throttle: '1m',
actions: [],
diff --git a/x-pack/test/alerting_api_integration/common/lib/index.ts b/x-pack/test/alerting_api_integration/common/lib/index.ts
index eae679cd38c11..94caf373c98d1 100644
--- a/x-pack/test/alerting_api_integration/common/lib/index.ts
+++ b/x-pack/test/alerting_api_integration/common/lib/index.ts
@@ -8,7 +8,11 @@ export { ObjectRemover } from './object_remover';
export { getUrlPrefix } from './space_test_utils';
export { ES_TEST_INDEX_NAME, ESTestIndexTool } from './es_test_index_tool';
export { getTestAlertData } from './get_test_alert_data';
-export { AlertUtils } from './alert_utils';
+export {
+ AlertUtils,
+ getConsumerUnauthorizedErrorMessage,
+ getProducerUnauthorizedErrorMessage,
+} from './alert_utils';
export { TaskManagerUtils } from './task_manager_utils';
export * from './test_assertions';
export { checkAAD } from './check_aad';
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts b/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts
index c72ee6a192bf2..2f57d05be4227 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts
@@ -47,8 +47,10 @@ const GlobalRead: User = {
kibana: [
{
feature: {
- alerts: ['read'],
actions: ['read'],
+ alertsFixture: ['read'],
+ alertsRestrictedFixture: ['read'],
+ actionsSimulators: ['read'],
},
spaces: ['*'],
},
@@ -75,8 +77,9 @@ const Space1All: User = {
kibana: [
{
feature: {
- alerts: ['all'],
actions: ['all'],
+ alertsFixture: ['all'],
+ actionsSimulators: ['all'],
},
spaces: ['space1'],
},
@@ -94,7 +97,71 @@ const Space1All: User = {
},
};
-export const Users: User[] = [NoKibanaPrivileges, Superuser, GlobalRead, Space1All];
+const Space1AllAlertingNoneActions: User = {
+ username: 'space_1_all_alerts_none_actions',
+ fullName: 'space_1_all_alerts_none_actions',
+ password: 'space_1_all_alerts_none_actions-password',
+ role: {
+ name: 'space_1_all_alerts_none_actions_role',
+ kibana: [
+ {
+ feature: {
+ alertsFixture: ['all'],
+ actionsSimulators: ['all'],
+ },
+ spaces: ['space1'],
+ },
+ ],
+ elasticsearch: {
+ // TODO: Remove once Elasticsearch doesn't require the permission for own keys
+ cluster: ['manage_api_key'],
+ indices: [
+ {
+ names: [`${ES_TEST_INDEX_NAME}*`],
+ privileges: ['all'],
+ },
+ ],
+ },
+ },
+};
+
+const Space1AllWithRestrictedFixture: User = {
+ username: 'space_1_all_with_restricted_fixture',
+ fullName: 'space_1_all_with_restricted_fixture',
+ password: 'space_1_all_with_restricted_fixture-password',
+ role: {
+ name: 'space_1_all_with_restricted_fixture_role',
+ kibana: [
+ {
+ feature: {
+ actions: ['all'],
+ alertsFixture: ['all'],
+ alertsRestrictedFixture: ['all'],
+ },
+ spaces: ['space1'],
+ },
+ ],
+ elasticsearch: {
+ // TODO: Remove once Elasticsearch doesn't require the permission for own keys
+ cluster: ['manage_api_key'],
+ indices: [
+ {
+ names: [`${ES_TEST_INDEX_NAME}*`],
+ privileges: ['all'],
+ },
+ ],
+ },
+ },
+};
+
+export const Users: User[] = [
+ NoKibanaPrivileges,
+ Superuser,
+ GlobalRead,
+ Space1All,
+ Space1AllWithRestrictedFixture,
+ Space1AllAlertingNoneActions,
+];
const Space1: Space = {
id: 'space1',
@@ -160,6 +227,23 @@ const Space1AllAtSpace1: Space1AllAtSpace1 = {
user: Space1All,
space: Space1,
};
+interface Space1AllWithRestrictedFixtureAtSpace1 extends Scenario {
+ id: 'space_1_all_with_restricted_fixture at space1';
+}
+const Space1AllWithRestrictedFixtureAtSpace1: Space1AllWithRestrictedFixtureAtSpace1 = {
+ id: 'space_1_all_with_restricted_fixture at space1',
+ user: Space1AllWithRestrictedFixture,
+ space: Space1,
+};
+
+interface Space1AllAlertingNoneActionsAtSpace1 extends Scenario {
+ id: 'space_1_all_alerts_none_actions at space1';
+}
+const Space1AllAlertingNoneActionsAtSpace1: Space1AllAlertingNoneActionsAtSpace1 = {
+ id: 'space_1_all_alerts_none_actions at space1',
+ user: Space1AllAlertingNoneActions,
+ space: Space1,
+};
interface Space1AllAtSpace2 extends Scenario {
id: 'space_1_all at space2';
@@ -175,11 +259,15 @@ export const UserAtSpaceScenarios: [
SuperuserAtSpace1,
GlobalReadAtSpace1,
Space1AllAtSpace1,
- Space1AllAtSpace2
+ Space1AllAtSpace2,
+ Space1AllWithRestrictedFixtureAtSpace1,
+ Space1AllAlertingNoneActionsAtSpace1
] = [
NoKibanaPrivilegesAtSpace1,
SuperuserAtSpace1,
GlobalReadAtSpace1,
Space1AllAtSpace1,
Space1AllAtSpace2,
+ Space1AllWithRestrictedFixtureAtSpace1,
+ Space1AllAlertingNoneActionsAtSpace1,
];
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts
index 69dcb7c813815..75609d58f7792 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts
@@ -41,16 +41,18 @@ export default function createActionTests({ getService }: FtrProviderContext) {
switch (scenario.id) {
case 'no_kibana_privileges at space1':
case 'global_read at space1':
+ case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ statusCode: 403,
+ error: 'Forbidden',
+ message: 'Unauthorized to create a "test.index-record" action',
});
break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(200);
objectRemover.add(space.id, response.body.id, 'action', 'actions');
expect(response.body).to.eql({
@@ -90,16 +92,18 @@ export default function createActionTests({ getService }: FtrProviderContext) {
switch (scenario.id) {
case 'no_kibana_privileges at space1':
case 'global_read at space1':
+ case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ statusCode: 403,
+ error: 'Forbidden',
+ message: 'Unauthorized to create a "test.unregistered-action-type" action',
});
break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(400);
expect(response.body).to.eql({
statusCode: 400,
@@ -122,16 +126,11 @@ export default function createActionTests({ getService }: FtrProviderContext) {
switch (scenario.id) {
case 'no_kibana_privileges at space1':
case 'global_read at space1':
+ case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
- expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
- });
- break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(400);
expect(response.body).to.eql({
statusCode: 400,
@@ -160,16 +159,18 @@ export default function createActionTests({ getService }: FtrProviderContext) {
switch (scenario.id) {
case 'no_kibana_privileges at space1':
case 'global_read at space1':
+ case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ statusCode: 403,
+ error: 'Forbidden',
+ message: 'Unauthorized to create a "test.index-record" action',
});
break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(400);
expect(response.body).to.eql({
statusCode: 400,
@@ -196,16 +197,18 @@ export default function createActionTests({ getService }: FtrProviderContext) {
switch (scenario.id) {
case 'no_kibana_privileges at space1':
case 'global_read at space1':
+ case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ statusCode: 403,
+ error: 'Forbidden',
+ message: 'Unauthorized to create a "test.not-enabled" action',
});
break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
statusCode: 403,
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/delete.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/delete.ts
index d96ffc5bb3be3..97c933f2ef8c5 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/delete.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/delete.ts
@@ -46,18 +46,20 @@ export default function deleteActionTests({ getService }: FtrProviderContext) {
switch (scenario.id) {
case 'no_kibana_privileges at space1':
+ case 'space_1_all_alerts_none_actions at space1':
case 'global_read at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ statusCode: 403,
+ error: 'Forbidden',
+ message: 'Unauthorized to delete actions',
});
objectRemover.add(space.id, createdAction.id, 'action', 'actions');
break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(204);
expect(response.body).to.eql('');
break;
@@ -88,19 +90,22 @@ export default function deleteActionTests({ getService }: FtrProviderContext) {
.auth(user.username, user.password)
.set('kbn-xsrf', 'foo');
- expect(response.statusCode).to.eql(404);
switch (scenario.id) {
case 'no_kibana_privileges at space1':
+ case 'space_1_all_alerts_none_actions at space1':
case 'global_read at space1':
case 'space_1_all at space2':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ statusCode: 403,
+ error: 'Forbidden',
+ message: 'Unauthorized to delete actions',
});
break;
case 'superuser at space1':
+ expect(response.statusCode).to.eql(404);
expect(response.body).to.eql({
statusCode: 404,
error: 'Not Found',
@@ -120,17 +125,19 @@ export default function deleteActionTests({ getService }: FtrProviderContext) {
switch (scenario.id) {
case 'no_kibana_privileges at space1':
+ case 'space_1_all_alerts_none_actions at space1':
case 'global_read at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ statusCode: 403,
+ error: 'Forbidden',
+ message: 'Unauthorized to delete actions',
});
break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(404);
break;
default:
@@ -146,17 +153,19 @@ export default function deleteActionTests({ getService }: FtrProviderContext) {
switch (scenario.id) {
case 'no_kibana_privileges at space1':
+ case 'space_1_all_alerts_none_actions at space1':
case 'global_read at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ statusCode: 403,
+ error: 'Forbidden',
+ message: 'Unauthorized to delete actions',
});
break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.body).to.eql({
statusCode: 400,
error: 'Bad Request',
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts
index 5d609d001ee5d..a45eee400b445 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts
@@ -77,17 +77,19 @@ export default function ({ getService }: FtrProviderContext) {
switch (scenario.id) {
case 'no_kibana_privileges at space1':
+ case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ statusCode: 403,
+ error: 'Forbidden',
+ message: 'Unauthorized to execute actions',
});
break;
case 'global_read at space1':
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(200);
expect(response.body).to.be.an('object');
const searchResult = await esTestIndexTool.search(
@@ -154,19 +156,22 @@ export default function ({ getService }: FtrProviderContext) {
},
});
- expect(response.statusCode).to.eql(404);
switch (scenario.id) {
case 'no_kibana_privileges at space1':
+ case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all at space2':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ statusCode: 403,
+ error: 'Forbidden',
+ message: 'Unauthorized to execute actions',
});
break;
case 'global_read at space1':
case 'superuser at space1':
+ expect(response.statusCode).to.eql(404);
expect(response.body).to.eql({
statusCode: 404,
error: 'Not Found',
@@ -224,17 +229,19 @@ export default function ({ getService }: FtrProviderContext) {
switch (scenario.id) {
case 'no_kibana_privileges at space1':
+ case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ statusCode: 403,
+ error: 'Forbidden',
+ message: 'Unauthorized to execute actions',
});
break;
case 'global_read at space1':
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(200);
expect(response.body).to.be.an('object');
const searchResult = await esTestIndexTool.search(
@@ -275,17 +282,19 @@ export default function ({ getService }: FtrProviderContext) {
switch (scenario.id) {
case 'no_kibana_privileges at space1':
+ case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ statusCode: 403,
+ error: 'Forbidden',
+ message: 'Unauthorized to execute actions',
});
break;
case 'global_read at space1':
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(404);
expect(response.body).to.eql({
statusCode: 404,
@@ -307,17 +316,12 @@ export default function ({ getService }: FtrProviderContext) {
switch (scenario.id) {
case 'no_kibana_privileges at space1':
+ case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
- expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
- });
- break;
case 'global_read at space1':
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(400);
expect(response.body).to.eql({
statusCode: 400,
@@ -383,17 +387,19 @@ export default function ({ getService }: FtrProviderContext) {
switch (scenario.id) {
case 'no_kibana_privileges at space1':
+ case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ statusCode: 403,
+ error: 'Forbidden',
+ message: 'Unauthorized to execute actions',
});
break;
case 'global_read at space1':
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(200);
break;
default:
@@ -431,16 +437,18 @@ export default function ({ getService }: FtrProviderContext) {
switch (scenario.id) {
case 'no_kibana_privileges at space1':
+ case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ statusCode: 403,
+ error: 'Forbidden',
+ message: 'Unauthorized to execute actions',
});
break;
case 'global_read at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(200);
searchResult = await esTestIndexTool.search('action:test.authorization', reference);
expect(searchResult.hits.total.value).to.eql(1);
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts
index c610ac670f690..fc08be3e30a6f 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts
@@ -45,17 +45,19 @@ export default function getActionTests({ getService }: FtrProviderContext) {
switch (scenario.id) {
case 'no_kibana_privileges at space1':
+ case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ statusCode: 403,
+ error: 'Forbidden',
+ message: 'Unauthorized to get actions',
});
break;
case 'global_read at space1':
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(200);
expect(response.body).to.eql({
id: createdAction.id,
@@ -93,19 +95,22 @@ export default function getActionTests({ getService }: FtrProviderContext) {
.get(`${getUrlPrefix('other')}/api/actions/action/${createdAction.id}`)
.auth(user.username, user.password);
- expect(response.statusCode).to.eql(404);
switch (scenario.id) {
case 'no_kibana_privileges at space1':
+ case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all at space2':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ statusCode: 403,
+ error: 'Forbidden',
+ message: 'Unauthorized to get actions',
});
break;
case 'global_read at space1':
case 'superuser at space1':
+ expect(response.statusCode).to.eql(404);
expect(response.body).to.eql({
statusCode: 404,
error: 'Not Found',
@@ -124,17 +129,19 @@ export default function getActionTests({ getService }: FtrProviderContext) {
switch (scenario.id) {
case 'no_kibana_privileges at space1':
+ case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ statusCode: 403,
+ error: 'Forbidden',
+ message: 'Unauthorized to get actions',
});
break;
case 'global_read at space1':
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(200);
expect(response.body).to.eql({
id: 'my-slack1',
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts
index 45491aa2d28fc..994072d5cb03c 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts
@@ -45,17 +45,19 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
switch (scenario.id) {
case 'no_kibana_privileges at space1':
+ case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ statusCode: 403,
+ error: 'Forbidden',
+ message: 'Unauthorized to get actions',
});
break;
case 'global_read at space1':
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(200);
expect(response.body).to.eql([
{
@@ -150,17 +152,19 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
switch (scenario.id) {
case 'no_kibana_privileges at space1':
+ case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ statusCode: 403,
+ error: 'Forbidden',
+ message: 'Unauthorized to get actions',
});
break;
case 'global_read at space1':
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(200);
expect(response.body).to.eql([
{
@@ -231,13 +235,15 @@ export default function getAllActionTests({ getService }: FtrProviderContext) {
switch (scenario.id) {
case 'no_kibana_privileges at space1':
+ case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all at space2':
case 'space_1_all at space1':
- expect(response.statusCode).to.eql(404);
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ statusCode: 403,
+ error: 'Forbidden',
+ message: 'Unauthorized to get actions',
});
break;
case 'global_read at space1':
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/list_action_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/list_action_types.ts
index 22c89a1a8148f..83b7077cbaadd 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/list_action_types.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/list_action_types.ts
@@ -28,19 +28,15 @@ export default function listActionTypesTests({ getService }: FtrProviderContext)
};
}
+ expect(response.statusCode).to.eql(200);
switch (scenario.id) {
case 'no_kibana_privileges at space1':
+ case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
- expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
- });
- break;
case 'global_read at space1':
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(200);
// Check for values explicitly in order to avoid this test failing each time plugins register
// a new action type
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/update.ts
index cb0e0efda0b1a..82b12e6ce9a22 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/update.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/update.ts
@@ -55,17 +55,19 @@ export default function updateActionTests({ getService }: FtrProviderContext) {
switch (scenario.id) {
case 'no_kibana_privileges at space1':
+ case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ statusCode: 403,
+ error: 'Forbidden',
+ message: 'Unauthorized to update actions',
});
break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(200);
expect(response.body).to.eql({
id: createdAction.id,
@@ -120,19 +122,22 @@ export default function updateActionTests({ getService }: FtrProviderContext) {
},
});
- expect(response.statusCode).to.eql(404);
switch (scenario.id) {
case 'no_kibana_privileges at space1':
+ case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all at space2':
case 'global_read at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ statusCode: 403,
+ error: 'Forbidden',
+ message: 'Unauthorized to update actions',
});
break;
case 'superuser at space1':
+ expect(response.statusCode).to.eql(404);
expect(response.body).to.eql({
statusCode: 404,
error: 'Not Found',
@@ -156,17 +161,12 @@ export default function updateActionTests({ getService }: FtrProviderContext) {
switch (scenario.id) {
case 'no_kibana_privileges at space1':
+ case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
- expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
- });
- break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(400);
expect(response.body).to.eql({
statusCode: 400,
@@ -196,17 +196,19 @@ export default function updateActionTests({ getService }: FtrProviderContext) {
switch (scenario.id) {
case 'no_kibana_privileges at space1':
+ case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ statusCode: 403,
+ error: 'Forbidden',
+ message: 'Unauthorized to update actions',
});
break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(404);
expect(response.body).to.eql({
statusCode: 404,
@@ -228,17 +230,12 @@ export default function updateActionTests({ getService }: FtrProviderContext) {
switch (scenario.id) {
case 'no_kibana_privileges at space1':
+ case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
- expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
- });
- break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(400);
expect(response.body).to.eql({
statusCode: 400,
@@ -285,17 +282,19 @@ export default function updateActionTests({ getService }: FtrProviderContext) {
switch (scenario.id) {
case 'no_kibana_privileges at space1':
+ case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ statusCode: 403,
+ error: 'Forbidden',
+ message: 'Unauthorized to update actions',
});
break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(400);
expect(response.body).to.eql({
statusCode: 400,
@@ -326,17 +325,19 @@ export default function updateActionTests({ getService }: FtrProviderContext) {
switch (scenario.id) {
case 'no_kibana_privileges at space1':
+ case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ statusCode: 403,
+ error: 'Forbidden',
+ message: 'Unauthorized to update actions',
});
break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.body).to.eql({
statusCode: 400,
error: 'Bad Request',
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts
index db1e59746162b..ffa9855478a05 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts
@@ -14,6 +14,7 @@ import {
getTestAlertData,
ObjectRemover,
AlertUtils,
+ getConsumerUnauthorizedErrorMessage,
TaskManagerUtils,
getEventLog,
} from '../../../common/lib';
@@ -88,15 +89,28 @@ export default function alertTests({ getService }: FtrProviderContext) {
case 'no_kibana_privileges at space1':
case 'global_read at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'create',
+ 'test.always-firing',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: `Unauthorized to get actions`,
+ statusCode: 403,
});
break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(200);
// Wait for the action to index a document before disabling the alert and waiting for tasks to finish
@@ -189,15 +203,28 @@ instanceStateValue: true
case 'no_kibana_privileges at space1':
case 'global_read at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'create',
+ 'test.always-firing',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: `Unauthorized to get actions`,
+ statusCode: 403,
});
break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(200);
// Wait for the action to index a document before disabling the alert and waiting for tasks to finish
@@ -376,15 +403,28 @@ instanceStateValue: true
case 'no_kibana_privileges at space1':
case 'global_read at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'create',
+ 'test.always-firing',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: `Unauthorized to get actions`,
+ statusCode: 403,
});
break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(200);
objectRemover.add(space.id, response.body.id, 'alert', 'alerts');
@@ -460,14 +500,20 @@ instanceStateValue: true
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'create',
+ 'test.authorization',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
});
break;
case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(200);
objectRemover.add(space.id, response.body.id, 'alert', 'alerts');
@@ -574,14 +620,19 @@ instanceStateValue: true
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'create',
+ 'test.always-firing',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
});
break;
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(200);
objectRemover.add(space.id, response.body.id, 'alert', 'alerts');
@@ -614,6 +665,14 @@ instanceStateValue: true
},
});
break;
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: `Unauthorized to get actions`,
+ statusCode: 403,
+ });
+ break;
case 'superuser at space1':
expect(response.statusCode).to.eql(200);
objectRemover.add(space.id, response.body.id, 'alert', 'alerts');
@@ -658,14 +717,27 @@ instanceStateValue: true
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'create',
+ 'test.always-firing',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: `Unauthorized to get actions`,
+ statusCode: 403,
});
break;
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
case 'superuser at space1':
expect(response.statusCode).to.eql(200);
// Wait until alerts scheduled actions 3 times before disabling the alert and waiting for tasks to finish
@@ -724,14 +796,27 @@ instanceStateValue: true
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'create',
+ 'test.always-firing',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: `Unauthorized to get actions`,
+ statusCode: 403,
});
break;
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
case 'superuser at space1':
expect(response.statusCode).to.eql(200);
// Wait for actions to execute twice before disabling the alert and waiting for tasks to finish
@@ -774,14 +859,27 @@ instanceStateValue: true
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'create',
+ 'test.always-firing',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: `Unauthorized to get actions`,
+ statusCode: 403,
});
break;
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
case 'superuser at space1':
expect(response.statusCode).to.eql(200);
// Actions should execute twice before widning things down
@@ -816,14 +914,27 @@ instanceStateValue: true
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'create',
+ 'test.always-firing',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: `Unauthorized to get actions`,
+ statusCode: 403,
});
break;
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
case 'superuser at space1':
await alertUtils.muteAll(response.body.id);
await alertUtils.enable(response.body.id);
@@ -861,14 +972,27 @@ instanceStateValue: true
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'create',
+ 'test.always-firing',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: `Unauthorized to get actions`,
+ statusCode: 403,
});
break;
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
case 'superuser at space1':
await alertUtils.muteInstance(response.body.id, '1');
await alertUtils.enable(response.body.id);
@@ -906,14 +1030,27 @@ instanceStateValue: true
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'create',
+ 'test.always-firing',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: `Unauthorized to get actions`,
+ statusCode: 403,
});
break;
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
case 'superuser at space1':
await alertUtils.muteInstance(response.body.id, '1');
await alertUtils.muteAll(response.body.id);
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts
index 4ca943f3e188a..8d7b9dec58cf1 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts
@@ -6,7 +6,14 @@
import expect from '@kbn/expect';
import { UserAtSpaceScenarios } from '../../scenarios';
-import { checkAAD, getTestAlertData, getUrlPrefix, ObjectRemover } from '../../../common/lib';
+import {
+ checkAAD,
+ getTestAlertData,
+ getConsumerUnauthorizedErrorMessage,
+ getUrlPrefix,
+ ObjectRemover,
+ getProducerUnauthorizedErrorMessage,
+} from '../../../common/lib';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
@@ -62,15 +69,28 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
case 'no_kibana_privileges at space1':
case 'global_read at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'create',
+ 'test.noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: `Unauthorized to get actions`,
+ statusCode: 403,
});
break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(200);
objectRemover.add(space.id, response.body.id, 'alert', 'alerts');
expect(response.body).to.eql({
@@ -87,7 +107,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
],
enabled: true,
alertTypeId: 'test.noop',
- consumer: 'bar',
+ consumer: 'alertsFixture',
params: {},
createdBy: user.username,
schedule: { interval: '1m' },
@@ -124,6 +144,174 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
}
});
+ it('should handle create alert request appropriately when consumer is the same as producer', async () => {
+ const response = await supertestWithoutAuth
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .auth(user.username, user.password)
+ .send(
+ getTestAlertData({
+ alertTypeId: 'test.restricted-noop',
+ consumer: 'alertsRestrictedFixture',
+ })
+ );
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'global_read at space1':
+ case 'space_1_all at space2':
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'create',
+ 'test.restricted-noop',
+ 'alertsRestrictedFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'superuser at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(200);
+ objectRemover.add(space.id, response.body.id, 'alert', 'alerts');
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
+ it('should handle create alert request appropriately when consumer is not the producer', async () => {
+ const response = await supertestWithoutAuth
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .auth(user.username, user.password)
+ .send(
+ getTestAlertData({ alertTypeId: 'test.unrestricted-noop', consumer: 'alertsFixture' })
+ );
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'global_read at space1':
+ case 'space_1_all at space2':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'create',
+ 'test.unrestricted-noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getProducerUnauthorizedErrorMessage(
+ 'create',
+ 'test.unrestricted-noop',
+ 'alertsRestrictedFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'superuser at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(200);
+ objectRemover.add(space.id, response.body.id, 'alert', 'alerts');
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
+ it('should handle create alert request appropriately when consumer is "alerts"', async () => {
+ const response = await supertestWithoutAuth
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .auth(user.username, user.password)
+ .send(
+ getTestAlertData({
+ alertTypeId: 'test.noop',
+ consumer: 'alerts',
+ })
+ );
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage('create', 'test.noop', 'alerts'),
+ statusCode: 403,
+ });
+ break;
+ case 'global_read at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getProducerUnauthorizedErrorMessage(
+ 'create',
+ 'test.noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'superuser at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(200);
+ objectRemover.add(space.id, response.body.id, 'alert', 'alerts');
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
+ it('should handle create alert request appropriately when consumer is unknown', async () => {
+ const response = await supertestWithoutAuth
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .auth(user.username, user.password)
+ .send(
+ getTestAlertData({
+ alertTypeId: 'test.noop',
+ consumer: 'some consumer patrick invented',
+ })
+ );
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'global_read at space1':
+ case 'space_1_all at space2':
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ case 'superuser at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'create',
+ 'test.noop',
+ 'some consumer patrick invented'
+ ),
+ statusCode: 403,
+ });
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
it('should handle create alert request appropriately when an alert is disabled ', async () => {
const response = await supertestWithoutAuth
.post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
@@ -135,15 +323,21 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
case 'no_kibana_privileges at space1':
case 'global_read at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'create',
+ 'test.noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
});
break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(200);
objectRemover.add(space.id, response.body.id, 'alert', 'alerts');
expect(response.body.scheduledTaskId).to.eql(undefined);
@@ -168,15 +362,10 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
case 'no_kibana_privileges at space1':
case 'global_read at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
- expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
- });
- break;
- case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ case 'superuser at space1':
expect(response.statusCode).to.eql(400);
expect(response.body).to.eql({
statusCode: 400,
@@ -200,15 +389,10 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
case 'no_kibana_privileges at space1':
case 'global_read at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
- expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
- });
- break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(400);
expect(response.body).to.eql({
statusCode: 400,
@@ -236,15 +420,21 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
case 'no_kibana_privileges at space1':
case 'global_read at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'create',
+ 'test.validation',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
});
break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(400);
expect(response.body).to.eql({
statusCode: 400,
@@ -269,15 +459,10 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
case 'no_kibana_privileges at space1':
case 'global_read at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
- expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
- });
- break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(400);
expect(response.body).to.eql({
error: 'Bad Request',
@@ -301,15 +486,10 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
case 'no_kibana_privileges at space1':
case 'global_read at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
- expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
- });
- break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(400);
expect(response.body).to.eql({
error: 'Bad Request',
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts
index 6f8ae010b9cd8..fc453c8da72e7 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts
@@ -6,7 +6,13 @@
import expect from '@kbn/expect';
import { UserAtSpaceScenarios } from '../../scenarios';
-import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib';
+import {
+ getUrlPrefix,
+ getTestAlertData,
+ getConsumerUnauthorizedErrorMessage,
+ getProducerUnauthorizedErrorMessage,
+ ObjectRemover,
+} from '../../../common/lib';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
@@ -46,11 +52,15 @@ export default function createDeleteTests({ getService }: FtrProviderContext) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'delete',
+ 'test.noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
});
objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
// Ensure task still exists
@@ -58,6 +68,185 @@ export default function createDeleteTests({ getService }: FtrProviderContext) {
break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(204);
+ expect(response.body).to.eql('');
+ try {
+ await getScheduledTask(createdAlert.scheduledTaskId);
+ throw new Error('Should have removed scheduled task');
+ } catch (e) {
+ expect(e.status).to.eql(404);
+ }
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
+ it('should handle delete alert request appropriately when consumer is the same as producer', async () => {
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ alertTypeId: 'test.restricted-noop',
+ consumer: 'alertsRestrictedFixture',
+ })
+ )
+ .expect(200);
+
+ const response = await supertestWithoutAuth
+ .delete(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
+ .set('kbn-xsrf', 'foo')
+ .auth(user.username, user.password);
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ case 'global_read at space1':
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'delete',
+ 'test.restricted-noop',
+ 'alertsRestrictedFixture'
+ ),
+ statusCode: 403,
+ });
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+ // Ensure task still exists
+ await getScheduledTask(createdAlert.scheduledTaskId);
+ break;
+ case 'superuser at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(204);
+ expect(response.body).to.eql('');
+ try {
+ await getScheduledTask(createdAlert.scheduledTaskId);
+ throw new Error('Should have removed scheduled task');
+ } catch (e) {
+ expect(e.status).to.eql(404);
+ }
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
+ it('should handle delete alert request appropriately when consumer is not the producer', async () => {
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({ alertTypeId: 'test.unrestricted-noop', consumer: 'alertsFixture' })
+ )
+ .expect(200);
+
+ const response = await supertestWithoutAuth
+ .delete(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
+ .set('kbn-xsrf', 'foo')
+ .auth(user.username, user.password);
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ case 'global_read at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'delete',
+ 'test.unrestricted-noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+ // Ensure task still exists
+ await getScheduledTask(createdAlert.scheduledTaskId);
+ break;
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getProducerUnauthorizedErrorMessage(
+ 'delete',
+ 'test.unrestricted-noop',
+ 'alertsRestrictedFixture'
+ ),
+ statusCode: 403,
+ });
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+ // Ensure task still exists
+ await getScheduledTask(createdAlert.scheduledTaskId);
+ break;
+ case 'superuser at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(204);
+ expect(response.body).to.eql('');
+ try {
+ await getScheduledTask(createdAlert.scheduledTaskId);
+ throw new Error('Should have removed scheduled task');
+ } catch (e) {
+ expect(e.status).to.eql(404);
+ }
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
+ it('should handle delete alert request appropriately when consumer is "alerts"', async () => {
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ alertTypeId: 'test.noop',
+ consumer: 'alerts',
+ })
+ )
+ .expect(200);
+
+ const response = await supertestWithoutAuth
+ .delete(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
+ .set('kbn-xsrf', 'foo')
+ .auth(user.username, user.password);
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage('delete', 'test.noop', 'alerts'),
+ statusCode: 403,
+ });
+ break;
+ case 'global_read at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getProducerUnauthorizedErrorMessage(
+ 'delete',
+ 'test.noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+ // Ensure task still exists
+ await getScheduledTask(createdAlert.scheduledTaskId);
+ break;
+ case 'superuser at space1':
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(204);
expect(response.body).to.eql('');
try {
@@ -91,12 +280,8 @@ export default function createDeleteTests({ getService }: FtrProviderContext) {
case 'space_1_all at space2':
case 'global_read at space1':
case 'space_1_all at space1':
- expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
- });
- break;
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
case 'superuser at space1':
expect(response.body).to.eql({
statusCode: 404,
@@ -137,11 +322,15 @@ export default function createDeleteTests({ getService }: FtrProviderContext) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'delete',
+ 'test.noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
});
objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
// Ensure task still exists
@@ -149,6 +338,8 @@ export default function createDeleteTests({ getService }: FtrProviderContext) {
break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(204);
expect(response.body).to.eql('');
try {
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts
index 589942a7ac11c..4e4f9053bd24f 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts
@@ -13,6 +13,8 @@ import {
getUrlPrefix,
getTestAlertData,
ObjectRemover,
+ getConsumerUnauthorizedErrorMessage,
+ getProducerUnauthorizedErrorMessage,
} from '../../../common/lib';
// eslint-disable-next-line import/no-default-export
@@ -39,10 +41,32 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte
describe(scenario.id, () => {
it('should handle disable alert request appropriately', async () => {
+ const { body: createdAction } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/actions/action`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ name: 'MY action',
+ actionTypeId: 'test.noop',
+ config: {},
+ secrets: {},
+ })
+ .expect(200);
+
const { body: createdAlert } = await supertest
.post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
.set('kbn-xsrf', 'foo')
- .send(getTestAlertData({ enabled: true }))
+ .send(
+ getTestAlertData({
+ enabled: true,
+ actions: [
+ {
+ id: createdAction.id,
+ group: 'default',
+ params: {},
+ },
+ ],
+ })
+ )
.expect(200);
objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
@@ -52,17 +76,23 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'disable',
+ 'test.noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
});
// Ensure task still exists
await getScheduledTask(createdAlert.scheduledTaskId);
break;
+ case 'space_1_all_alerts_none_actions at space1':
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(204);
expect(response.body).to.eql('');
try {
@@ -84,6 +114,171 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte
}
});
+ it('should handle disable alert request appropriately when consumer is the same as producer', async () => {
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ alertTypeId: 'test.restricted-noop',
+ consumer: 'alertsRestrictedFixture',
+ enabled: true,
+ })
+ )
+ .expect(200);
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+
+ const response = await alertUtils.getDisableRequest(createdAlert.id);
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ case 'global_read at space1':
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'disable',
+ 'test.restricted-noop',
+ 'alertsRestrictedFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'superuser at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(204);
+ expect(response.body).to.eql('');
+ try {
+ await getScheduledTask(createdAlert.scheduledTaskId);
+ throw new Error('Should have removed scheduled task');
+ } catch (e) {
+ expect(e.status).to.eql(404);
+ }
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
+ it('should handle disable alert request appropriately when consumer is not the producer', async () => {
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ alertTypeId: 'test.unrestricted-noop',
+ consumer: 'alertsFixture',
+ enabled: true,
+ })
+ )
+ .expect(200);
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+
+ const response = await alertUtils.getDisableRequest(createdAlert.id);
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ case 'global_read at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'disable',
+ 'test.unrestricted-noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getProducerUnauthorizedErrorMessage(
+ 'disable',
+ 'test.unrestricted-noop',
+ 'alertsRestrictedFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'superuser at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(204);
+ expect(response.body).to.eql('');
+ try {
+ await getScheduledTask(createdAlert.scheduledTaskId);
+ throw new Error('Should have removed scheduled task');
+ } catch (e) {
+ expect(e.status).to.eql(404);
+ }
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
+ it('should handle disable alert request appropriately when consumer is "alerts"', async () => {
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ alertTypeId: 'test.noop',
+ consumer: 'alerts',
+ enabled: true,
+ })
+ )
+ .expect(200);
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+
+ const response = await alertUtils.getDisableRequest(createdAlert.id);
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage('disable', 'test.noop', 'alerts'),
+ statusCode: 403,
+ });
+ break;
+ case 'global_read at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getProducerUnauthorizedErrorMessage(
+ 'disable',
+ 'test.noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'superuser at space1':
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(204);
+ expect(response.body).to.eql('');
+ try {
+ await getScheduledTask(createdAlert.scheduledTaskId);
+ throw new Error('Should have removed scheduled task');
+ } catch (e) {
+ expect(e.status).to.eql(404);
+ }
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
it('should still be able to disable alert when AAD is broken', async () => {
const { body: createdAlert } = await supertest
.post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
@@ -110,17 +305,23 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'disable',
+ 'test.noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
});
// Ensure task still exists
await getScheduledTask(createdAlert.scheduledTaskId);
break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(204);
expect(response.body).to.eql('');
try {
@@ -157,14 +358,10 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
- });
- break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.body).to.eql({
statusCode: 404,
error: 'Not Found',
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts
index 8cb0eeb0092a3..d7f6546bf34a9 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts
@@ -13,6 +13,8 @@ import {
getUrlPrefix,
getTestAlertData,
ObjectRemover,
+ getConsumerUnauthorizedErrorMessage,
+ getProducerUnauthorizedErrorMessage,
} from '../../../common/lib';
// eslint-disable-next-line import/no-default-export
@@ -39,10 +41,32 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex
describe(scenario.id, () => {
it('should handle enable alert request appropriately', async () => {
+ const { body: createdAction } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/actions/action`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ name: 'MY action',
+ actionTypeId: 'test.noop',
+ config: {},
+ secrets: {},
+ })
+ .expect(200);
+
const { body: createdAlert } = await supertest
.post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
.set('kbn-xsrf', 'foo')
- .send(getTestAlertData({ enabled: false }))
+ .send(
+ getTestAlertData({
+ enabled: false,
+ actions: [
+ {
+ id: createdAction.id,
+ group: 'default',
+ params: {},
+ },
+ ],
+ })
+ )
.expect(200);
objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
@@ -51,16 +75,40 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex
switch (scenario.id) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'enable',
+ 'test.noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'enable',
+ 'test.noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: `Unauthorized to execute actions`,
+ statusCode: 403,
});
break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(204);
expect(response.body).to.eql('');
const { body: updatedAlert } = await supertestWithoutAuth
@@ -89,6 +137,165 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex
}
});
+ it('should handle enable alert request appropriately when consumer is the same as producer', async () => {
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ alertTypeId: 'test.restricted-noop',
+ consumer: 'alertsRestrictedFixture',
+ enabled: false,
+ })
+ )
+ .expect(200);
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+
+ const response = await alertUtils.getEnableRequest(createdAlert.id);
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ case 'global_read at space1':
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'enable',
+ 'test.restricted-noop',
+ 'alertsRestrictedFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'superuser at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(204);
+ expect(response.body).to.eql('');
+ try {
+ await getScheduledTask(createdAlert.scheduledTaskId);
+ throw new Error('Should have removed scheduled task');
+ } catch (e) {
+ expect(e.status).to.eql(404);
+ }
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
+ it('should handle enable alert request appropriately when consumer is not the producer', async () => {
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ alertTypeId: 'test.unrestricted-noop',
+ consumer: 'alertsFixture',
+ enabled: false,
+ })
+ )
+ .expect(200);
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+
+ const response = await alertUtils.getEnableRequest(createdAlert.id);
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ case 'global_read at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'enable',
+ 'test.unrestricted-noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getProducerUnauthorizedErrorMessage(
+ 'enable',
+ 'test.unrestricted-noop',
+ 'alertsRestrictedFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'superuser at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(204);
+ expect(response.body).to.eql('');
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
+ it('should handle enable alert request appropriately when consumer is "alerts"', async () => {
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ alertTypeId: 'test.noop',
+ consumer: 'alerts',
+ enabled: false,
+ })
+ )
+ .expect(200);
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+
+ const response = await alertUtils.getEnableRequest(createdAlert.id);
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage('enable', 'test.noop', 'alerts'),
+ statusCode: 403,
+ });
+ break;
+ case 'global_read at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getProducerUnauthorizedErrorMessage(
+ 'enable',
+ 'test.noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'superuser at space1':
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(204);
+ expect(response.body).to.eql('');
+ try {
+ await getScheduledTask(createdAlert.scheduledTaskId);
+ throw new Error('Should have removed scheduled task');
+ } catch (e) {
+ expect(e.status).to.eql(404);
+ }
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
it('should still be able to enable alert when AAD is broken', async () => {
const { body: createdAlert } = await supertest
.post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
@@ -115,15 +322,21 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'enable',
+ 'test.noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
});
break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(204);
expect(response.body).to.eql('');
const { body: updatedAlert } = await supertestWithoutAuth
@@ -167,14 +380,10 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
- });
- break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.body).to.eql({
statusCode: 404,
error: 'Not Found',
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts
index 5fe9edeb91ec9..268212d4294d0 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts
@@ -5,6 +5,8 @@
*/
import expect from '@kbn/expect';
+import { chunk, omit } from 'lodash';
+import uuid from 'uuid';
import { UserAtSpaceScenarios } from '../../scenarios';
import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
@@ -41,16 +43,18 @@ export default function createFindTests({ getService }: FtrProviderContext) {
switch (scenario.id) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: `Unauthorized to find any alert types`,
+ statusCode: 403,
});
break;
case 'global_read at space1':
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(200);
expect(response.body.page).to.equal(1);
expect(response.body.perPage).to.be.greaterThan(0);
@@ -61,7 +65,7 @@ export default function createFindTests({ getService }: FtrProviderContext) {
name: 'abc',
tags: ['foo'],
alertTypeId: 'test.noop',
- consumer: 'bar',
+ consumer: 'alertsFixture',
schedule: { interval: '1m' },
enabled: true,
actions: [],
@@ -84,6 +88,108 @@ export default function createFindTests({ getService }: FtrProviderContext) {
}
});
+ it('should filter out types that the user is not authorized to `get` retaining pagination', async () => {
+ async function createNoOpAlert(overrides = {}) {
+ const alert = getTestAlertData(overrides);
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(alert)
+ .expect(200);
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+ return {
+ id: createdAlert.id,
+ alertTypeId: alert.alertTypeId,
+ };
+ }
+ function createRestrictedNoOpAlert() {
+ return createNoOpAlert({
+ alertTypeId: 'test.restricted-noop',
+ consumer: 'alertsRestrictedFixture',
+ });
+ }
+ function createUnrestrictedNoOpAlert() {
+ return createNoOpAlert({
+ alertTypeId: 'test.unrestricted-noop',
+ consumer: 'alertsFixture',
+ });
+ }
+ const allAlerts = [];
+ allAlerts.push(await createNoOpAlert());
+ allAlerts.push(await createNoOpAlert());
+ allAlerts.push(await createRestrictedNoOpAlert());
+ allAlerts.push(await createUnrestrictedNoOpAlert());
+ allAlerts.push(await createUnrestrictedNoOpAlert());
+ allAlerts.push(await createRestrictedNoOpAlert());
+ allAlerts.push(await createNoOpAlert());
+ allAlerts.push(await createNoOpAlert());
+
+ const perPage = 4;
+
+ const response = await supertestWithoutAuth
+ .get(
+ `${getUrlPrefix(space.id)}/api/alerts/_find?per_page=${perPage}&sort_field=createdAt`
+ )
+ .auth(user.username, user.password);
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: `Unauthorized to find any alert types`,
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(200);
+ expect(response.body.page).to.equal(1);
+ expect(response.body.perPage).to.be.equal(perPage);
+ expect(response.body.total).to.be.equal(6);
+ {
+ const [firstPage] = chunk(
+ allAlerts
+ .filter((alert) => alert.alertTypeId !== 'test.restricted-noop')
+ .map((alert) => alert.id),
+ perPage
+ );
+ expect(response.body.data.map((alert: any) => alert.id)).to.eql(firstPage);
+ }
+ break;
+ case 'global_read at space1':
+ case 'superuser at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(200);
+ expect(response.body.page).to.equal(1);
+ expect(response.body.perPage).to.be.equal(perPage);
+ expect(response.body.total).to.be.equal(8);
+
+ {
+ const [firstPage, secondPage] = chunk(
+ allAlerts.map((alert) => alert.id),
+ perPage
+ );
+ expect(response.body.data.map((alert: any) => alert.id)).to.eql(firstPage);
+
+ const secondResponse = await supertestWithoutAuth
+ .get(
+ `${getUrlPrefix(
+ space.id
+ )}/api/alerts/_find?per_page=${perPage}&sort_field=createdAt&page=2`
+ )
+ .auth(user.username, user.password);
+
+ expect(secondResponse.body.data.map((alert: any) => alert.id)).to.eql(secondPage);
+ }
+
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
it('should handle find alert request with filter appropriately', async () => {
const { body: createdAction } = await supertest
.post(`${getUrlPrefix(space.id)}/api/actions/action`)
@@ -125,16 +231,18 @@ export default function createFindTests({ getService }: FtrProviderContext) {
switch (scenario.id) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: `Unauthorized to find any alert types`,
+ statusCode: 403,
});
break;
case 'global_read at space1':
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(200);
expect(response.body.page).to.equal(1);
expect(response.body.perPage).to.be.greaterThan(0);
@@ -145,7 +253,7 @@ export default function createFindTests({ getService }: FtrProviderContext) {
name: 'abc',
tags: ['foo'],
alertTypeId: 'test.noop',
- consumer: 'bar',
+ consumer: 'alertsFixture',
schedule: { interval: '1m' },
enabled: false,
actions: [
@@ -174,6 +282,83 @@ export default function createFindTests({ getService }: FtrProviderContext) {
}
});
+ it('should handle find alert request with fields appropriately', async () => {
+ const myTag = uuid.v4();
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ enabled: false,
+ tags: [myTag],
+ alertTypeId: 'test.restricted-noop',
+ consumer: 'alertsRestrictedFixture',
+ })
+ )
+ .expect(200);
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+
+ // create another type with same tag
+ const { body: createdSecondAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ tags: [myTag],
+ alertTypeId: 'test.restricted-noop',
+ consumer: 'alertsRestrictedFixture',
+ })
+ )
+ .expect(200);
+ objectRemover.add(space.id, createdSecondAlert.id, 'alert', 'alerts');
+
+ const response = await supertestWithoutAuth
+ .get(
+ `${getUrlPrefix(
+ space.id
+ )}/api/alerts/_find?filter=alert.attributes.alertTypeId:test.restricted-noop&fields=["tags"]&sort_field=createdAt`
+ )
+ .auth(user.username, user.password);
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: `Unauthorized to find any alert types`,
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(200);
+ expect(response.body.data).to.eql([]);
+ break;
+ case 'global_read at space1':
+ case 'superuser at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(200);
+ expect(response.body.page).to.equal(1);
+ expect(response.body.perPage).to.be.greaterThan(0);
+ expect(response.body.total).to.be.greaterThan(0);
+ const [matchFirst, matchSecond] = response.body.data;
+ expect(omit(matchFirst, 'updatedAt')).to.eql({
+ id: createdAlert.id,
+ actions: [],
+ tags: [myTag],
+ });
+ expect(omit(matchSecond, 'updatedAt')).to.eql({
+ id: createdSecondAlert.id,
+ actions: [],
+ tags: [myTag],
+ });
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
it(`shouldn't find alert from another space`, async () => {
const { body: createdAlert } = await supertest
.post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
@@ -192,11 +377,13 @@ export default function createFindTests({ getService }: FtrProviderContext) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'space_1_all at space1':
- expect(response.statusCode).to.eql(404);
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: `Unauthorized to find any alert types`,
+ statusCode: 403,
});
break;
case 'global_read at space1':
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts
index a203b7d7c151b..1043ece08a2ac 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts
@@ -6,7 +6,13 @@
import expect from '@kbn/expect';
import { UserAtSpaceScenarios } from '../../scenarios';
-import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib';
+import {
+ getUrlPrefix,
+ getTestAlertData,
+ ObjectRemover,
+ getConsumerUnauthorizedErrorMessage,
+ getProducerUnauthorizedErrorMessage,
+} from '../../../common/lib';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
@@ -37,23 +43,25 @@ export default function createGetTests({ getService }: FtrProviderContext) {
switch (scenario.id) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage('get', 'test.noop', 'alertsFixture'),
+ statusCode: 403,
});
break;
case 'global_read at space1':
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(200);
expect(response.body).to.eql({
id: createdAlert.id,
name: 'abc',
tags: ['foo'],
alertTypeId: 'test.noop',
- consumer: 'bar',
+ consumer: 'alertsFixture',
schedule: { interval: '1m' },
enabled: true,
actions: [],
@@ -76,6 +84,157 @@ export default function createGetTests({ getService }: FtrProviderContext) {
}
});
+ it('should handle get alert request appropriately when consumer is the same as producer', async () => {
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ alertTypeId: 'test.restricted-noop',
+ consumer: 'alertsRestrictedFixture',
+ })
+ )
+ .expect(200);
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+
+ const response = await supertestWithoutAuth
+ .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
+ .auth(user.username, user.password);
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'get',
+ 'test.restricted-noop',
+ 'alertsRestrictedFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'global_read at space1':
+ case 'superuser at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(200);
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
+ it('should handle get alert request appropriately when consumer is not the producer', async () => {
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ alertTypeId: 'test.unrestricted-noop',
+ consumer: 'alertsFixture',
+ })
+ )
+ .expect(200);
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+
+ const response = await supertestWithoutAuth
+ .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
+ .auth(user.username, user.password);
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'get',
+ 'test.unrestricted-noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getProducerUnauthorizedErrorMessage(
+ 'get',
+ 'test.unrestricted-noop',
+ 'alertsRestrictedFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'superuser at space1':
+ case 'global_read at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(200);
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
+ it('should handle get alert request appropriately when consumer is "alerts"', async () => {
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ alertTypeId: 'test.restricted-noop',
+ consumer: 'alerts',
+ })
+ )
+ .expect(200);
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+
+ const response = await supertestWithoutAuth
+ .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
+ .auth(user.username, user.password);
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'get',
+ 'test.restricted-noop',
+ 'alerts'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getProducerUnauthorizedErrorMessage(
+ 'get',
+ 'test.restricted-noop',
+ 'alertsRestrictedFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'global_read at space1':
+ case 'superuser at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(200);
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
it(`shouldn't get alert from another space`, async () => {
const { body: createdAlert } = await supertest
.post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
@@ -93,12 +252,8 @@ export default function createGetTests({ getService }: FtrProviderContext) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'space_1_all at space1':
- expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
- });
- break;
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
case 'global_read at space1':
case 'superuser at space1':
expect(response.body).to.eql({
@@ -120,16 +275,11 @@ export default function createGetTests({ getService }: FtrProviderContext) {
switch (scenario.id) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
- expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
- });
- break;
case 'global_read at space1':
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(404);
expect(response.body).to.eql({
statusCode: 404,
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts
index fd071bd55b377..2e89aa2961c73 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts
@@ -5,7 +5,13 @@
*/
import expect from '@kbn/expect';
-import { getUrlPrefix, ObjectRemover, getTestAlertData } from '../../../common/lib';
+import {
+ getUrlPrefix,
+ ObjectRemover,
+ getTestAlertData,
+ getConsumerUnauthorizedErrorMessage,
+ getProducerUnauthorizedErrorMessage,
+} from '../../../common/lib';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
import { UserAtSpaceScenarios } from '../../scenarios';
@@ -37,16 +43,73 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont
switch (scenario.id) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage('get', 'test.noop', 'alertsFixture'),
+ statusCode: 403,
});
break;
case 'global_read at space1':
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(200);
+ expect(response.body).to.key('alertInstances', 'previousStartedAt');
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
+ it('should handle getAlertState alert request appropriately when unauthorized', async () => {
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ alertTypeId: 'test.unrestricted-noop',
+ consumer: 'alertsFixture',
+ })
+ )
+ .expect(200);
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+
+ const response = await supertestWithoutAuth
+ .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}/state`)
+ .auth(user.username, user.password);
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'get',
+ 'test.unrestricted-noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getProducerUnauthorizedErrorMessage(
+ 'get',
+ 'test.unrestricted-noop',
+ 'alertsRestrictedFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'global_read at space1':
+ case 'superuser at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(200);
expect(response.body).to.key('alertInstances', 'previousStartedAt');
break;
@@ -72,12 +135,8 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'space_1_all at space1':
- expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
- });
- break;
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
case 'global_read at space1':
case 'superuser at space1':
expect(response.body).to.eql({
@@ -99,16 +158,11 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont
switch (scenario.id) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
- expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
- });
- break;
case 'global_read at space1':
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(404);
expect(response.body).to.eql({
statusCode: 404,
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts
index f14f66f66fe2f..4cd5f0805121c 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts
@@ -9,11 +9,11 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default function alertingTests({ loadTestFile }: FtrProviderContext) {
describe('Alerts', () => {
+ loadTestFile(require.resolve('./find'));
loadTestFile(require.resolve('./create'));
loadTestFile(require.resolve('./delete'));
loadTestFile(require.resolve('./disable'));
loadTestFile(require.resolve('./enable'));
- loadTestFile(require.resolve('./find'));
loadTestFile(require.resolve('./get'));
loadTestFile(require.resolve('./get_alert_state'));
loadTestFile(require.resolve('./list_alert_types'));
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts
index dd31e2dbbb5b8..c3e5af0d1f771 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts
@@ -5,6 +5,7 @@
*/
import expect from '@kbn/expect';
+import { omit } from 'lodash';
import { UserAtSpaceScenarios } from '../../scenarios';
import { getUrlPrefix } from '../../../common/lib/space_test_utils';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
@@ -13,42 +14,125 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context';
export default function listAlertTypes({ getService }: FtrProviderContext) {
const supertestWithoutAuth = getService('supertestWithoutAuth');
+ const expectedNoOpType = {
+ actionGroups: [{ id: 'default', name: 'Default' }],
+ defaultActionGroupId: 'default',
+ id: 'test.noop',
+ name: 'Test: Noop',
+ actionVariables: {
+ state: [],
+ context: [],
+ },
+ producer: 'alertsFixture',
+ };
+
+ const expectedRestrictedNoOpType = {
+ actionGroups: [{ id: 'default', name: 'Default' }],
+ defaultActionGroupId: 'default',
+ id: 'test.restricted-noop',
+ name: 'Test: Restricted Noop',
+ actionVariables: {
+ state: [],
+ context: [],
+ },
+ producer: 'alertsRestrictedFixture',
+ };
+
describe('list_alert_types', () => {
for (const scenario of UserAtSpaceScenarios) {
const { user, space } = scenario;
describe(scenario.id, () => {
- it('should return 200 with list of alert types', async () => {
+ it('should return 200 with list of globally available alert types', async () => {
const response = await supertestWithoutAuth
.get(`${getUrlPrefix(space.id)}/api/alerts/list_alert_types`)
.auth(user.username, user.password);
+ expect(response.statusCode).to.eql(200);
+ const noOpAlertType = response.body.find(
+ (alertType: any) => alertType.id === 'test.noop'
+ );
+ const restrictedNoOpAlertType = response.body.find(
+ (alertType: any) => alertType.id === 'test.restricted-noop'
+ );
switch (scenario.id) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
- expect(response.statusCode).to.eql(404);
- expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ expect(response.body).to.eql([]);
+ break;
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(omit(noOpAlertType, 'authorizedConsumers')).to.eql(expectedNoOpType);
+ expect(restrictedNoOpAlertType).to.eql(undefined);
+ expect(noOpAlertType.authorizedConsumers).to.eql({
+ alerts: { read: true, all: true },
+ alertsFixture: { read: true, all: true },
});
break;
case 'global_read at space1':
+ expect(omit(noOpAlertType, 'authorizedConsumers')).to.eql(expectedNoOpType);
+ expect(noOpAlertType.authorizedConsumers.alertsFixture).to.eql({
+ read: true,
+ all: false,
+ });
+ expect(noOpAlertType.authorizedConsumers.alertsRestrictedFixture).to.eql({
+ read: true,
+ all: false,
+ });
+
+ expect(omit(restrictedNoOpAlertType, 'authorizedConsumers')).to.eql(
+ expectedRestrictedNoOpType
+ );
+ expect(Object.keys(restrictedNoOpAlertType.authorizedConsumers)).not.to.contain(
+ 'alertsFixture'
+ );
+ expect(restrictedNoOpAlertType.authorizedConsumers.alertsRestrictedFixture).to.eql({
+ read: true,
+ all: false,
+ });
+ break;
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(omit(noOpAlertType, 'authorizedConsumers')).to.eql(expectedNoOpType);
+ expect(noOpAlertType.authorizedConsumers.alertsFixture).to.eql({
+ read: true,
+ all: true,
+ });
+ expect(noOpAlertType.authorizedConsumers.alertsRestrictedFixture).to.eql({
+ read: true,
+ all: true,
+ });
+
+ expect(omit(restrictedNoOpAlertType, 'authorizedConsumers')).to.eql(
+ expectedRestrictedNoOpType
+ );
+ expect(Object.keys(restrictedNoOpAlertType.authorizedConsumers)).not.to.contain(
+ 'alertsFixture'
+ );
+ expect(restrictedNoOpAlertType.authorizedConsumers.alertsRestrictedFixture).to.eql({
+ read: true,
+ all: true,
+ });
+ break;
case 'superuser at space1':
- case 'space_1_all at space1':
- expect(response.statusCode).to.eql(200);
- const fixtureAlertType = response.body.find(
- (alertType: any) => alertType.id === 'test.noop'
+ expect(omit(noOpAlertType, 'authorizedConsumers')).to.eql(expectedNoOpType);
+ expect(noOpAlertType.authorizedConsumers.alertsFixture).to.eql({
+ read: true,
+ all: true,
+ });
+ expect(noOpAlertType.authorizedConsumers.alertsRestrictedFixture).to.eql({
+ read: true,
+ all: true,
+ });
+
+ expect(omit(restrictedNoOpAlertType, 'authorizedConsumers')).to.eql(
+ expectedRestrictedNoOpType
);
- expect(fixtureAlertType).to.eql({
- actionGroups: [{ id: 'default', name: 'Default' }],
- defaultActionGroupId: 'default',
- id: 'test.noop',
- name: 'Test: Noop',
- actionVariables: {
- state: [],
- context: [],
- },
- producer: 'alerting',
+ expect(noOpAlertType.authorizedConsumers.alertsFixture).to.eql({
+ read: true,
+ all: true,
+ });
+ expect(noOpAlertType.authorizedConsumers.alertsRestrictedFixture).to.eql({
+ read: true,
+ all: true,
});
break;
default:
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_all.ts
index 2416bc2ea1d12..a497affa266e4 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_all.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_all.ts
@@ -13,6 +13,8 @@ import {
getUrlPrefix,
getTestAlertData,
ObjectRemover,
+ getConsumerUnauthorizedErrorMessage,
+ getProducerUnauthorizedErrorMessage,
} from '../../../common/lib';
// eslint-disable-next-line import/no-default-export
@@ -31,10 +33,151 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext)
describe(scenario.id, () => {
it('should handle mute alert request appropriately', async () => {
+ const { body: createdAction } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/actions/action`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ name: 'MY action',
+ actionTypeId: 'test.noop',
+ config: {},
+ secrets: {},
+ })
+ .expect(200);
+
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ enabled: false,
+ actions: [
+ {
+ id: createdAction.id,
+ group: 'default',
+ params: {},
+ },
+ ],
+ })
+ )
+ .expect(200);
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+
+ const response = await alertUtils.getMuteAllRequest(createdAlert.id);
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ case 'global_read at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'muteAll',
+ 'test.noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: `Unauthorized to execute actions`,
+ statusCode: 403,
+ });
+ break;
+ case 'superuser at space1':
+ case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(204);
+ expect(response.body).to.eql('');
+ const { body: updatedAlert } = await supertestWithoutAuth
+ .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
+ .set('kbn-xsrf', 'foo')
+ .auth(user.username, user.password)
+ .expect(200);
+ expect(updatedAlert.muteAll).to.eql(true);
+ // Ensure AAD isn't broken
+ await checkAAD({
+ supertest,
+ spaceId: space.id,
+ type: 'alert',
+ id: createdAlert.id,
+ });
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
+ it('should handle mute alert request appropriately when consumer is the same as producer', async () => {
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ enabled: false,
+ alertTypeId: 'test.restricted-noop',
+ consumer: 'alertsRestrictedFixture',
+ })
+ )
+ .expect(200);
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+
+ const response = await alertUtils.getMuteAllRequest(createdAlert.id);
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ case 'global_read at space1':
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'muteAll',
+ 'test.restricted-noop',
+ 'alertsRestrictedFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'superuser at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(204);
+ expect(response.body).to.eql('');
+ const { body: updatedAlert } = await supertestWithoutAuth
+ .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
+ .set('kbn-xsrf', 'foo')
+ .auth(user.username, user.password)
+ .expect(200);
+ expect(updatedAlert.muteAll).to.eql(true);
+ // Ensure AAD isn't broken
+ await checkAAD({
+ supertest,
+ spaceId: space.id,
+ type: 'alert',
+ id: createdAlert.id,
+ });
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
+ it('should handle mute alert request appropriately when consumer is not the producer', async () => {
const { body: createdAlert } = await supertest
.post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
.set('kbn-xsrf', 'foo')
- .send(getTestAlertData({ enabled: false }))
+ .send(
+ getTestAlertData({
+ enabled: false,
+ alertTypeId: 'test.unrestricted-noop',
+ consumer: 'alertsFixture',
+ })
+ )
.expect(200);
objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
@@ -44,15 +187,99 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext)
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'muteAll',
+ 'test.unrestricted-noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: getProducerUnauthorizedErrorMessage(
+ 'muteAll',
+ 'test.unrestricted-noop',
+ 'alertsRestrictedFixture'
+ ),
+ statusCode: 403,
});
break;
case 'superuser at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(204);
+ expect(response.body).to.eql('');
+ const { body: updatedAlert } = await supertestWithoutAuth
+ .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
+ .set('kbn-xsrf', 'foo')
+ .auth(user.username, user.password)
+ .expect(200);
+ expect(updatedAlert.muteAll).to.eql(true);
+ // Ensure AAD isn't broken
+ await checkAAD({
+ supertest,
+ spaceId: space.id,
+ type: 'alert',
+ id: createdAlert.id,
+ });
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
+ it('should handle mute alert request appropriately when consumer is "alerts"', async () => {
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ enabled: false,
+ alertTypeId: 'test.restricted-noop',
+ consumer: 'alerts',
+ })
+ )
+ .expect(200);
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+
+ const response = await alertUtils.getMuteAllRequest(createdAlert.id);
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'muteAll',
+ 'test.restricted-noop',
+ 'alerts'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'global_read at space1':
case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getProducerUnauthorizedErrorMessage(
+ 'muteAll',
+ 'test.restricted-noop',
+ 'alertsRestrictedFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'superuser at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(204);
expect(response.body).to.eql('');
const { body: updatedAlert } = await supertestWithoutAuth
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_instance.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_instance.ts
index c59b9f4503a03..b4277479d8fd9 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_instance.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_instance.ts
@@ -13,6 +13,8 @@ import {
getUrlPrefix,
getTestAlertData,
ObjectRemover,
+ getConsumerUnauthorizedErrorMessage,
+ getProducerUnauthorizedErrorMessage,
} from '../../../common/lib';
// eslint-disable-next-line import/no-default-export
@@ -31,10 +33,32 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider
describe(scenario.id, () => {
it('should handle mute alert instance request appropriately', async () => {
+ const { body: createdAction } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/actions/action`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ name: 'MY action',
+ actionTypeId: 'test.noop',
+ config: {},
+ secrets: {},
+ })
+ .expect(200);
+
const { body: createdAlert } = await supertest
.post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
.set('kbn-xsrf', 'foo')
- .send(getTestAlertData({ enabled: false }))
+ .send(
+ getTestAlertData({
+ enabled: false,
+ actions: [
+ {
+ id: createdAction.id,
+ group: 'default',
+ params: {},
+ },
+ ],
+ })
+ )
.expect(200);
objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
@@ -44,15 +68,218 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'muteInstance',
+ 'test.noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: `Unauthorized to execute actions`,
+ statusCode: 403,
});
break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(204);
+ expect(response.body).to.eql('');
+ const { body: updatedAlert } = await supertestWithoutAuth
+ .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
+ .set('kbn-xsrf', 'foo')
+ .auth(user.username, user.password)
+ .expect(200);
+ expect(updatedAlert.mutedInstanceIds).to.eql(['1']);
+ // Ensure AAD isn't broken
+ await checkAAD({
+ supertest,
+ spaceId: space.id,
+ type: 'alert',
+ id: createdAlert.id,
+ });
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
+ it('should handle mute alert instance request appropriately when consumer is the same as producer', async () => {
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ enabled: false,
+ alertTypeId: 'test.restricted-noop',
+ consumer: 'alertsRestrictedFixture',
+ })
+ )
+ .expect(200);
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+
+ const response = await alertUtils.getMuteInstanceRequest(createdAlert.id, '1');
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ case 'global_read at space1':
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'muteInstance',
+ 'test.restricted-noop',
+ 'alertsRestrictedFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'superuser at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(204);
+ expect(response.body).to.eql('');
+ const { body: updatedAlert } = await supertestWithoutAuth
+ .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
+ .set('kbn-xsrf', 'foo')
+ .auth(user.username, user.password)
+ .expect(200);
+ expect(updatedAlert.mutedInstanceIds).to.eql(['1']);
+ // Ensure AAD isn't broken
+ await checkAAD({
+ supertest,
+ spaceId: space.id,
+ type: 'alert',
+ id: createdAlert.id,
+ });
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
+ it('should handle mute alert instance request appropriately when consumer is not the producer', async () => {
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ enabled: false,
+ alertTypeId: 'test.unrestricted-noop',
+ consumer: 'alertsFixture',
+ })
+ )
+ .expect(200);
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+
+ const response = await alertUtils.getMuteInstanceRequest(createdAlert.id, '1');
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ case 'global_read at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'muteInstance',
+ 'test.unrestricted-noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getProducerUnauthorizedErrorMessage(
+ 'muteInstance',
+ 'test.unrestricted-noop',
+ 'alertsRestrictedFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'superuser at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(204);
+ expect(response.body).to.eql('');
+ const { body: updatedAlert } = await supertestWithoutAuth
+ .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
+ .set('kbn-xsrf', 'foo')
+ .auth(user.username, user.password)
+ .expect(200);
+ expect(updatedAlert.mutedInstanceIds).to.eql(['1']);
+ // Ensure AAD isn't broken
+ await checkAAD({
+ supertest,
+ spaceId: space.id,
+ type: 'alert',
+ id: createdAlert.id,
+ });
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
+ it('should handle mute alert instance request appropriately when consumer is "alerts"', async () => {
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ enabled: false,
+ alertTypeId: 'test.restricted-noop',
+ consumer: 'alerts',
+ })
+ )
+ .expect(200);
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+
+ const response = await alertUtils.getMuteInstanceRequest(createdAlert.id, '1');
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'muteInstance',
+ 'test.restricted-noop',
+ 'alerts'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'global_read at space1':
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getProducerUnauthorizedErrorMessage(
+ 'muteInstance',
+ 'test.restricted-noop',
+ 'alertsRestrictedFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'superuser at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(204);
expect(response.body).to.eql('');
const { body: updatedAlert } = await supertestWithoutAuth
@@ -95,15 +322,21 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'muteInstance',
+ 'test.noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
});
break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(204);
expect(response.body).to.eql('');
const { body: updatedAlert } = await supertestWithoutAuth
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_all.ts
index fd22752ccc11a..46653900cb1c7 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_all.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_all.ts
@@ -13,6 +13,8 @@ import {
getUrlPrefix,
getTestAlertData,
ObjectRemover,
+ getConsumerUnauthorizedErrorMessage,
+ getProducerUnauthorizedErrorMessage,
} from '../../../common/lib';
// eslint-disable-next-line import/no-default-export
@@ -31,10 +33,32 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex
describe(scenario.id, () => {
it('should handle unmute alert request appropriately', async () => {
+ const { body: createdAction } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/actions/action`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ name: 'MY action',
+ actionTypeId: 'test.noop',
+ config: {},
+ secrets: {},
+ })
+ .expect(200);
+
const { body: createdAlert } = await supertest
.post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
.set('kbn-xsrf', 'foo')
- .send(getTestAlertData({ enabled: false }))
+ .send(
+ getTestAlertData({
+ enabled: false,
+ actions: [
+ {
+ id: createdAction.id,
+ group: 'default',
+ params: {},
+ },
+ ],
+ })
+ )
.expect(200);
objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
@@ -49,15 +73,233 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'unmuteAll',
+ 'test.noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: `Unauthorized to execute actions`,
+ statusCode: 403,
});
break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(204);
+ expect(response.body).to.eql('');
+ const { body: updatedAlert } = await supertestWithoutAuth
+ .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
+ .set('kbn-xsrf', 'foo')
+ .auth(user.username, user.password)
+ .expect(200);
+ expect(updatedAlert.muteAll).to.eql(false);
+ // Ensure AAD isn't broken
+ await checkAAD({
+ supertest,
+ spaceId: space.id,
+ type: 'alert',
+ id: createdAlert.id,
+ });
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
+ it('should handle unmute alert request appropriately when consumer is the same as producer', async () => {
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ enabled: false,
+ alertTypeId: 'test.restricted-noop',
+ consumer: 'alertsRestrictedFixture',
+ })
+ )
+ .expect(200);
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+
+ await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}/_mute_all`)
+ .set('kbn-xsrf', 'foo')
+ .expect(204, '');
+
+ const response = await alertUtils.getUnmuteAllRequest(createdAlert.id);
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ case 'global_read at space1':
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'unmuteAll',
+ 'test.restricted-noop',
+ 'alertsRestrictedFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'superuser at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(204);
+ expect(response.body).to.eql('');
+ const { body: updatedAlert } = await supertestWithoutAuth
+ .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
+ .set('kbn-xsrf', 'foo')
+ .auth(user.username, user.password)
+ .expect(200);
+ expect(updatedAlert.muteAll).to.eql(false);
+ // Ensure AAD isn't broken
+ await checkAAD({
+ supertest,
+ spaceId: space.id,
+ type: 'alert',
+ id: createdAlert.id,
+ });
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
+ it('should handle unmute alert request appropriately when consumer is not the producer', async () => {
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ enabled: false,
+ alertTypeId: 'test.unrestricted-noop',
+ consumer: 'alertsFixture',
+ })
+ )
+ .expect(200);
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+
+ await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}/_mute_all`)
+ .set('kbn-xsrf', 'foo')
+ .expect(204, '');
+
+ const response = await alertUtils.getUnmuteAllRequest(createdAlert.id);
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ case 'global_read at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'unmuteAll',
+ 'test.unrestricted-noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getProducerUnauthorizedErrorMessage(
+ 'unmuteAll',
+ 'test.unrestricted-noop',
+ 'alertsRestrictedFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'superuser at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(204);
+ expect(response.body).to.eql('');
+ const { body: updatedAlert } = await supertestWithoutAuth
+ .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
+ .set('kbn-xsrf', 'foo')
+ .auth(user.username, user.password)
+ .expect(200);
+ expect(updatedAlert.muteAll).to.eql(false);
+ // Ensure AAD isn't broken
+ await checkAAD({
+ supertest,
+ spaceId: space.id,
+ type: 'alert',
+ id: createdAlert.id,
+ });
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
+ it('should handle unmute alert request appropriately when consumer is "alerts"', async () => {
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ enabled: false,
+ alertTypeId: 'test.restricted-noop',
+ consumer: 'alerts',
+ })
+ )
+ .expect(200);
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+
+ await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}/_mute_all`)
+ .set('kbn-xsrf', 'foo')
+ .expect(204, '');
+
+ const response = await alertUtils.getUnmuteAllRequest(createdAlert.id);
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'unmuteAll',
+ 'test.restricted-noop',
+ 'alerts'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'global_read at space1':
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getProducerUnauthorizedErrorMessage(
+ 'unmuteAll',
+ 'test.restricted-noop',
+ 'alertsRestrictedFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'superuser at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(204);
expect(response.body).to.eql('');
const { body: updatedAlert } = await supertestWithoutAuth
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_instance.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_instance.ts
index 72b524282354a..2bc501c9a7c72 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_instance.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_instance.ts
@@ -13,6 +13,8 @@ import {
getUrlPrefix,
getTestAlertData,
ObjectRemover,
+ getConsumerUnauthorizedErrorMessage,
+ getProducerUnauthorizedErrorMessage,
} from '../../../common/lib';
// eslint-disable-next-line import/no-default-export
@@ -31,10 +33,32 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider
describe(scenario.id, () => {
it('should handle unmute alert instance request appropriately', async () => {
+ const { body: createdAction } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/actions/action`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ name: 'MY action',
+ actionTypeId: 'test.noop',
+ config: {},
+ secrets: {},
+ })
+ .expect(200);
+
const { body: createdAlert } = await supertest
.post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
.set('kbn-xsrf', 'foo')
- .send(getTestAlertData({ enabled: false }))
+ .send(
+ getTestAlertData({
+ enabled: false,
+ actions: [
+ {
+ id: createdAction.id,
+ group: 'default',
+ params: {},
+ },
+ ],
+ })
+ )
.expect(200);
objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
@@ -51,15 +75,239 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'unmuteInstance',
+ 'test.noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: `Unauthorized to execute actions`,
+ statusCode: 403,
});
break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(204);
+ expect(response.body).to.eql('');
+ const { body: updatedAlert } = await supertestWithoutAuth
+ .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
+ .set('kbn-xsrf', 'foo')
+ .auth(user.username, user.password)
+ .expect(200);
+ expect(updatedAlert.mutedInstanceIds).to.eql([]);
+ // Ensure AAD isn't broken
+ await checkAAD({
+ supertest,
+ spaceId: space.id,
+ type: 'alert',
+ id: createdAlert.id,
+ });
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
+ it('should handle unmute alert instance request appropriately when consumer is the same as producer', async () => {
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ enabled: false,
+ alertTypeId: 'test.restricted-noop',
+ consumer: 'alertsRestrictedFixture',
+ })
+ )
+ .expect(200);
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+
+ await supertest
+ .post(
+ `${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}/alert_instance/1/_mute`
+ )
+ .set('kbn-xsrf', 'foo')
+ .expect(204, '');
+
+ const response = await alertUtils.getUnmuteInstanceRequest(createdAlert.id, '1');
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ case 'global_read at space1':
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'unmuteInstance',
+ 'test.restricted-noop',
+ 'alertsRestrictedFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'superuser at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(204);
+ expect(response.body).to.eql('');
+ const { body: updatedAlert } = await supertestWithoutAuth
+ .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
+ .set('kbn-xsrf', 'foo')
+ .auth(user.username, user.password)
+ .expect(200);
+ expect(updatedAlert.mutedInstanceIds).to.eql([]);
+ // Ensure AAD isn't broken
+ await checkAAD({
+ supertest,
+ spaceId: space.id,
+ type: 'alert',
+ id: createdAlert.id,
+ });
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
+ it('should handle unmute alert instance request appropriately when consumer is not the producer', async () => {
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ enabled: false,
+ alertTypeId: 'test.unrestricted-noop',
+ consumer: 'alertsFixture',
+ })
+ )
+ .expect(200);
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+
+ await supertest
+ .post(
+ `${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}/alert_instance/1/_mute`
+ )
+ .set('kbn-xsrf', 'foo')
+ .expect(204, '');
+
+ const response = await alertUtils.getUnmuteInstanceRequest(createdAlert.id, '1');
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ case 'global_read at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'unmuteInstance',
+ 'test.unrestricted-noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getProducerUnauthorizedErrorMessage(
+ 'unmuteInstance',
+ 'test.unrestricted-noop',
+ 'alertsRestrictedFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'superuser at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(204);
+ expect(response.body).to.eql('');
+ const { body: updatedAlert } = await supertestWithoutAuth
+ .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
+ .set('kbn-xsrf', 'foo')
+ .auth(user.username, user.password)
+ .expect(200);
+ expect(updatedAlert.mutedInstanceIds).to.eql([]);
+ // Ensure AAD isn't broken
+ await checkAAD({
+ supertest,
+ spaceId: space.id,
+ type: 'alert',
+ id: createdAlert.id,
+ });
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
+ it('should handle unmute alert instance request appropriately when consumer is "alerts"', async () => {
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ enabled: false,
+ alertTypeId: 'test.restricted-noop',
+ consumer: 'alerts',
+ })
+ )
+ .expect(200);
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+
+ await supertest
+ .post(
+ `${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}/alert_instance/1/_mute`
+ )
+ .set('kbn-xsrf', 'foo')
+ .expect(204, '');
+
+ const response = await alertUtils.getUnmuteInstanceRequest(createdAlert.id, '1');
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'unmuteInstance',
+ 'test.restricted-noop',
+ 'alerts'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'global_read at space1':
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getProducerUnauthorizedErrorMessage(
+ 'unmuteInstance',
+ 'test.restricted-noop',
+ 'alertsRestrictedFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'superuser at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(204);
expect(response.body).to.eql('');
const { body: updatedAlert } = await supertestWithoutAuth
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts
index 2bcc035beb7a9..ab3a92d0b3f70 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts
@@ -13,6 +13,8 @@ import {
getTestAlertData,
ObjectRemover,
ensureDatetimeIsWithinRange,
+ getConsumerUnauthorizedErrorMessage,
+ getProducerUnauthorizedErrorMessage,
} from '../../../common/lib';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
@@ -38,6 +40,17 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
const { user, space } = scenario;
describe(scenario.id, () => {
it('should handle update alert request appropriately', async () => {
+ const { body: createdAction } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/actions/action`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ name: 'MY action',
+ actionTypeId: 'test.noop',
+ config: {},
+ secrets: {},
+ })
+ .expect(200);
+
const { body: createdAlert } = await supertest
.post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
.set('kbn-xsrf', 'foo')
@@ -52,7 +65,13 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
foo: true,
},
schedule: { interval: '12s' },
- actions: [],
+ actions: [
+ {
+ id: createdAction.id,
+ group: 'default',
+ params: {},
+ },
+ ],
throttle: '1m',
};
const response = await supertestWithoutAuth
@@ -65,21 +84,310 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'update',
+ 'test.noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: `Unauthorized to get actions`,
+ statusCode: 403,
});
break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(200);
expect(response.body).to.eql({
...updatedData,
id: createdAlert.id,
alertTypeId: 'test.noop',
- consumer: 'bar',
+ consumer: 'alertsFixture',
+ createdBy: 'elastic',
+ enabled: true,
+ updatedBy: user.username,
+ apiKeyOwner: user.username,
+ muteAll: false,
+ mutedInstanceIds: [],
+ actions: [
+ {
+ id: createdAction.id,
+ actionTypeId: 'test.noop',
+ group: 'default',
+ params: {},
+ },
+ ],
+ scheduledTaskId: createdAlert.scheduledTaskId,
+ createdAt: response.body.createdAt,
+ updatedAt: response.body.updatedAt,
+ });
+ expect(Date.parse(response.body.createdAt)).to.be.greaterThan(0);
+ expect(Date.parse(response.body.updatedAt)).to.be.greaterThan(0);
+ expect(Date.parse(response.body.updatedAt)).to.be.greaterThan(
+ Date.parse(response.body.createdAt)
+ );
+ // Ensure AAD isn't broken
+ await checkAAD({
+ supertest,
+ spaceId: space.id,
+ type: 'alert',
+ id: createdAlert.id,
+ });
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
+ it('should handle update alert request appropriately when consumer is the same as producer', async () => {
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ alertTypeId: 'test.restricted-noop',
+ consumer: 'alertsRestrictedFixture',
+ })
+ )
+ .expect(200);
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+
+ const updatedData = {
+ name: 'bcd',
+ tags: ['bar'],
+ params: {
+ foo: true,
+ },
+ schedule: { interval: '12s' },
+ actions: [],
+ throttle: '1m',
+ };
+ const response = await supertestWithoutAuth
+ .put(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
+ .set('kbn-xsrf', 'foo')
+ .auth(user.username, user.password)
+ .send(updatedData);
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ case 'global_read at space1':
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'update',
+ 'test.restricted-noop',
+ 'alertsRestrictedFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'superuser at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(200);
+ expect(response.body).to.eql({
+ ...updatedData,
+ id: createdAlert.id,
+ alertTypeId: 'test.restricted-noop',
+ consumer: 'alertsRestrictedFixture',
+ createdBy: 'elastic',
+ enabled: true,
+ updatedBy: user.username,
+ apiKeyOwner: user.username,
+ muteAll: false,
+ mutedInstanceIds: [],
+ scheduledTaskId: createdAlert.scheduledTaskId,
+ createdAt: response.body.createdAt,
+ updatedAt: response.body.updatedAt,
+ });
+ expect(Date.parse(response.body.createdAt)).to.be.greaterThan(0);
+ expect(Date.parse(response.body.updatedAt)).to.be.greaterThan(0);
+ expect(Date.parse(response.body.updatedAt)).to.be.greaterThan(
+ Date.parse(response.body.createdAt)
+ );
+ // Ensure AAD isn't broken
+ await checkAAD({
+ supertest,
+ spaceId: space.id,
+ type: 'alert',
+ id: createdAlert.id,
+ });
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
+ it('should handle update alert request appropriately when consumer is not the producer', async () => {
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ alertTypeId: 'test.unrestricted-noop',
+ consumer: 'alertsFixture',
+ })
+ )
+ .expect(200);
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+
+ const updatedData = {
+ name: 'bcd',
+ tags: ['bar'],
+ params: {
+ foo: true,
+ },
+ schedule: { interval: '12s' },
+ actions: [],
+ throttle: '1m',
+ };
+ const response = await supertestWithoutAuth
+ .put(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
+ .set('kbn-xsrf', 'foo')
+ .auth(user.username, user.password)
+ .send(updatedData);
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ case 'global_read at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'update',
+ 'test.unrestricted-noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getProducerUnauthorizedErrorMessage(
+ 'update',
+ 'test.unrestricted-noop',
+ 'alertsRestrictedFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'superuser at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(200);
+ expect(response.body).to.eql({
+ ...updatedData,
+ id: createdAlert.id,
+ alertTypeId: 'test.unrestricted-noop',
+ consumer: 'alertsFixture',
+ createdBy: 'elastic',
+ enabled: true,
+ updatedBy: user.username,
+ apiKeyOwner: user.username,
+ muteAll: false,
+ mutedInstanceIds: [],
+ scheduledTaskId: createdAlert.scheduledTaskId,
+ createdAt: response.body.createdAt,
+ updatedAt: response.body.updatedAt,
+ });
+ expect(Date.parse(response.body.createdAt)).to.be.greaterThan(0);
+ expect(Date.parse(response.body.updatedAt)).to.be.greaterThan(0);
+ expect(Date.parse(response.body.updatedAt)).to.be.greaterThan(
+ Date.parse(response.body.createdAt)
+ );
+ // Ensure AAD isn't broken
+ await checkAAD({
+ supertest,
+ spaceId: space.id,
+ type: 'alert',
+ id: createdAlert.id,
+ });
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
+ it('should handle update alert request appropriately when consumer is "alerts"', async () => {
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ alertTypeId: 'test.restricted-noop',
+ consumer: 'alerts',
+ })
+ )
+ .expect(200);
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+
+ const updatedData = {
+ name: 'bcd',
+ tags: ['bar'],
+ params: {
+ foo: true,
+ },
+ schedule: { interval: '12s' },
+ actions: [],
+ throttle: '1m',
+ };
+ const response = await supertestWithoutAuth
+ .put(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
+ .set('kbn-xsrf', 'foo')
+ .auth(user.username, user.password)
+ .send(updatedData);
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'update',
+ 'test.restricted-noop',
+ 'alerts'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'global_read at space1':
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getProducerUnauthorizedErrorMessage(
+ 'update',
+ 'test.restricted-noop',
+ 'alertsRestrictedFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'superuser at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(200);
+ expect(response.body).to.eql({
+ ...updatedData,
+ id: createdAlert.id,
+ alertTypeId: 'test.restricted-noop',
+ consumer: 'alerts',
createdBy: 'elastic',
enabled: true,
updatedBy: user.username,
@@ -148,21 +456,27 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'update',
+ 'test.noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
});
break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(200);
expect(response.body).to.eql({
...updatedData,
id: createdAlert.id,
alertTypeId: 'test.noop',
- consumer: 'bar',
+ consumer: 'alertsFixture',
createdBy: 'elastic',
enabled: true,
updatedBy: user.username,
@@ -220,12 +534,8 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
case 'space_1_all at space2':
case 'global_read at space1':
case 'space_1_all at space1':
- expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
- });
- break;
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
case 'superuser at space1':
expect(response.body).to.eql({
statusCode: 404,
@@ -266,15 +576,10 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
- expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
- });
- break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(400);
expect(response.body).to.eql({
statusCode: 400,
@@ -298,15 +603,10 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
- expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
- });
- break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(400);
expect(response.body).to.eql({
statusCode: 400,
@@ -351,15 +651,21 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'update',
+ 'test.validation',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
});
break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(400);
expect(response.body).to.eql({
statusCode: 400,
@@ -390,15 +696,10 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
- expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
- });
- break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(400);
expect(response.body).to.eql({
statusCode: 400,
@@ -450,15 +751,21 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'update',
+ 'test.noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
});
break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(200);
await retry.try(async () => {
const alertTask = (await getAlertingTaskById(createdAlert.scheduledTaskId)).docs[0];
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update_api_key.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update_api_key.ts
index bf72b970dc0f1..7dea591b895ee 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update_api_key.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update_api_key.ts
@@ -13,6 +13,8 @@ import {
getUrlPrefix,
getTestAlertData,
ObjectRemover,
+ getConsumerUnauthorizedErrorMessage,
+ getProducerUnauthorizedErrorMessage,
} from '../../../common/lib';
// eslint-disable-next-line import/no-default-export
@@ -31,28 +33,245 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte
describe(scenario.id, () => {
it('should handle update alert api key request appropriately', async () => {
+ const { body: createdAction } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/actions/action`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ name: 'MY action',
+ actionTypeId: 'test.noop',
+ config: {},
+ secrets: {},
+ })
+ .expect(200);
+
const { body: createdAlert } = await supertest
.post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
.set('kbn-xsrf', 'foo')
- .send(getTestAlertData())
+ .send(
+ getTestAlertData({
+ actions: [
+ {
+ id: createdAction.id,
+ group: 'default',
+ params: {},
+ },
+ ],
+ })
+ )
.expect(200);
objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
const response = await alertUtils.getUpdateApiKeyRequest(createdAlert.id);
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ case 'global_read at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'updateApiKey',
+ 'test.noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: `Unauthorized to execute actions`,
+ statusCode: 403,
+ });
+ break;
+ case 'superuser at space1':
+ case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(204);
+ expect(response.body).to.eql('');
+ const { body: updatedAlert } = await supertestWithoutAuth
+ .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
+ .set('kbn-xsrf', 'foo')
+ .auth(user.username, user.password)
+ .expect(200);
+ expect(updatedAlert.apiKeyOwner).to.eql(user.username);
+ // Ensure AAD isn't broken
+ await checkAAD({
+ supertest,
+ spaceId: space.id,
+ type: 'alert',
+ id: createdAlert.id,
+ });
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+ it('should handle update alert api key request appropriately when consumer is the same as producer', async () => {
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ alertTypeId: 'test.restricted-noop',
+ consumer: 'alertsRestrictedFixture',
+ })
+ )
+ .expect(200);
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+
+ const response = await alertUtils.getUpdateApiKeyRequest(createdAlert.id);
switch (scenario.id) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'updateApiKey',
+ 'test.restricted-noop',
+ 'alertsRestrictedFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'superuser at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(204);
+ expect(response.body).to.eql('');
+ const { body: updatedAlert } = await supertestWithoutAuth
+ .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
+ .set('kbn-xsrf', 'foo')
+ .auth(user.username, user.password)
+ .expect(200);
+ expect(updatedAlert.apiKeyOwner).to.eql(user.username);
+ // Ensure AAD isn't broken
+ await checkAAD({
+ supertest,
+ spaceId: space.id,
+ type: 'alert',
+ id: createdAlert.id,
+ });
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
+ it('should handle update alert api key request appropriately when consumer is not the producer', async () => {
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ alertTypeId: 'test.unrestricted-noop',
+ consumer: 'alertsFixture',
+ })
+ )
+ .expect(200);
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+
+ const response = await alertUtils.getUpdateApiKeyRequest(createdAlert.id);
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ case 'global_read at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'updateApiKey',
+ 'test.unrestricted-noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getProducerUnauthorizedErrorMessage(
+ 'updateApiKey',
+ 'test.unrestricted-noop',
+ 'alertsRestrictedFixture'
+ ),
+ statusCode: 403,
});
break;
case 'superuser at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(204);
+ expect(response.body).to.eql('');
+ const { body: updatedAlert } = await supertestWithoutAuth
+ .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
+ .set('kbn-xsrf', 'foo')
+ .auth(user.username, user.password)
+ .expect(200);
+ expect(updatedAlert.apiKeyOwner).to.eql(user.username);
+ // Ensure AAD isn't broken
+ await checkAAD({
+ supertest,
+ spaceId: space.id,
+ type: 'alert',
+ id: createdAlert.id,
+ });
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
+
+ it('should handle update alert api key request appropriately when consumer is "alerts"', async () => {
+ const { body: createdAlert } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ alertTypeId: 'test.restricted-noop',
+ consumer: 'alerts',
+ })
+ )
+ .expect(200);
+ objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
+
+ const response = await alertUtils.getUpdateApiKeyRequest(createdAlert.id);
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'space_1_all at space2':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'updateApiKey',
+ 'test.restricted-noop',
+ 'alerts'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'global_read at space1':
case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ expect(response.statusCode).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getProducerUnauthorizedErrorMessage(
+ 'updateApiKey',
+ 'test.restricted-noop',
+ 'alertsRestrictedFixture'
+ ),
+ statusCode: 403,
+ });
+ break;
+ case 'superuser at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(204);
expect(response.body).to.eql('');
const { body: updatedAlert } = await supertestWithoutAuth
@@ -100,15 +319,21 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.statusCode).to.eql(404);
+ expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'updateApiKey',
+ 'test.noop',
+ 'alertsFixture'
+ ),
+ statusCode: 403,
});
break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(204);
expect(response.body).to.eql('');
const { body: updatedAlert } = await supertestWithoutAuth
@@ -145,14 +370,10 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
- expect(response.body).to.eql({
- statusCode: 404,
- error: 'Not Found',
- message: 'Not Found',
- });
- break;
case 'superuser at space1':
case 'space_1_all at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
expect(response.body).to.eql({
statusCode: 404,
error: 'Not Found',
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts
index 8412c09eefcda..92db0458c0639 100644
--- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts
@@ -346,7 +346,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
.set('kbn-xsrf', 'foo')
.send({
name: params.name,
- consumer: 'function test',
+ consumer: 'alerts',
enabled: true,
alertTypeId: ALERT_TYPE_ID,
schedule: { interval: `${ALERT_INTERVAL_SECONDS}s` },
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts
index fa256712a012b..86775f77a7671 100644
--- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts
@@ -6,7 +6,13 @@
import expect from '@kbn/expect';
import { Spaces } from '../../scenarios';
-import { checkAAD, getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib';
+import {
+ checkAAD,
+ getUrlPrefix,
+ getTestAlertData,
+ ObjectRemover,
+ getConsumerUnauthorizedErrorMessage,
+} from '../../../common/lib';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
@@ -69,7 +75,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
],
enabled: true,
alertTypeId: 'test.noop',
- consumer: 'bar',
+ consumer: 'alertsFixture',
params: {},
createdBy: null,
schedule: { interval: '1m' },
@@ -102,6 +108,24 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
});
});
+ it('should handle create alert request appropriately when consumer is unknown', async () => {
+ const response = await supertest
+ .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(getTestAlertData({ consumer: 'some consumer patrick invented' }));
+
+ expect(response.status).to.eql(403);
+ expect(response.body).to.eql({
+ error: 'Forbidden',
+ message: getConsumerUnauthorizedErrorMessage(
+ 'create',
+ 'test.noop',
+ 'some consumer patrick invented'
+ ),
+ statusCode: 403,
+ });
+ });
+
it('should handle create alert request appropriately when an alert is disabled ', async () => {
const response = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`)
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts
index 06f27d666c3da..b28ce89b30472 100644
--- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts
@@ -42,7 +42,7 @@ export default function createFindTests({ getService }: FtrProviderContext) {
name: 'abc',
tags: ['foo'],
alertTypeId: 'test.noop',
- consumer: 'bar',
+ consumer: 'alertsFixture',
schedule: { interval: '1m' },
enabled: true,
actions: [],
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts
index ff671e16654b5..165eaa09126a8 100644
--- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts
@@ -36,7 +36,7 @@ export default function createGetTests({ getService }: FtrProviderContext) {
name: 'abc',
tags: ['foo'],
alertTypeId: 'test.noop',
- consumer: 'bar',
+ consumer: 'alertsFixture',
schedule: { interval: '1m' },
enabled: true,
actions: [],
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts
index d3f08d7c509a0..e3f87a9be00ba 100644
--- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts
@@ -44,7 +44,7 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont
name: 'abc',
tags: ['foo'],
alertTypeId: 'test.cumulative-firing',
- consumer: 'bar',
+ consumer: 'alertsFixture',
schedule: { interval: '5s' },
throttle: '5s',
actions: [],
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts
index aef87eefba2ad..dd09a14b4cb81 100644
--- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts
@@ -19,7 +19,9 @@ export default function listAlertTypes({ getService }: FtrProviderContext) {
`${getUrlPrefix(Spaces.space1.id)}/api/alerts/list_alert_types`
);
expect(response.status).to.eql(200);
- const fixtureAlertType = response.body.find((alertType: any) => alertType.id === 'test.noop');
+ const { authorizedConsumers, ...fixtureAlertType } = response.body.find(
+ (alertType: any) => alertType.id === 'test.noop'
+ );
expect(fixtureAlertType).to.eql({
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
@@ -29,8 +31,9 @@ export default function listAlertTypes({ getService }: FtrProviderContext) {
state: [],
context: [],
},
- producer: 'alerting',
+ producer: 'alertsFixture',
});
+ expect(Object.keys(authorizedConsumers)).to.contain('alertsFixture');
});
it('should return actionVariables with both context and state', async () => {
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts
index fc61f59d129d7..d0e1be12e762f 100644
--- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts
@@ -30,5 +30,14 @@ export default function createGetTests({ getService }: FtrProviderContext) {
expect(response.status).to.eql(200);
expect(response.body.consumer).to.equal('alerts');
});
+
+ it('7.10.0 migrates the `metrics` consumer to be the `infrastructure`', async () => {
+ const response = await supertest.get(
+ `${getUrlPrefix(``)}/api/alerts/alert/74f3e6d7-b7bb-477d-ac28-fdf248d5f2a4`
+ );
+
+ expect(response.status).to.eql(200);
+ expect(response.body.consumer).to.equal('infrastructure');
+ });
});
}
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts
index b01a1b140f2d6..9c8e6f6b8d94c 100644
--- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts
@@ -47,7 +47,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
id: createdAlert.id,
tags: ['bar'],
alertTypeId: 'test.noop',
- consumer: 'bar',
+ consumer: 'alertsFixture',
createdBy: null,
enabled: true,
updatedBy: null,
diff --git a/x-pack/test/api_integration/apis/features/features/features.ts b/x-pack/test/api_integration/apis/features/features/features.ts
index df6eca795f801..9c44bfeb810fa 100644
--- a/x-pack/test/api_integration/apis/features/features/features.ts
+++ b/x-pack/test/api_integration/apis/features/features/features.ts
@@ -97,6 +97,7 @@ export default function ({ getService }: FtrProviderContext) {
'visualize',
'dashboard',
'dev_tools',
+ 'actions',
'enterpriseSearch',
'advancedSettings',
'indexPatterns',
@@ -106,6 +107,7 @@ export default function ({ getService }: FtrProviderContext) {
'savedObjectsManagement',
'ml',
'apm',
+ 'builtInAlerts',
'canvas',
'infrastructure',
'logs',
diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts
index 59fcfae5db3cf..1ad25a11be879 100644
--- a/x-pack/test/api_integration/apis/security/privileges.ts
+++ b/x-pack/test/api_integration/apis/security/privileges.ts
@@ -38,6 +38,8 @@ export default function ({ getService }: FtrProviderContext) {
ml: ['all', 'read'],
siem: ['all', 'read'],
ingestManager: ['all', 'read'],
+ builtInAlerts: ['all', 'read'],
+ actions: ['all', 'read'],
},
global: ['all', 'read'],
space: ['all', 'read'],
diff --git a/x-pack/test/api_integration/apis/security/privileges_basic.ts b/x-pack/test/api_integration/apis/security/privileges_basic.ts
index 5c2a2875852d6..d5263aed26d0b 100644
--- a/x-pack/test/api_integration/apis/security/privileges_basic.ts
+++ b/x-pack/test/api_integration/apis/security/privileges_basic.ts
@@ -36,6 +36,8 @@ export default function ({ getService }: FtrProviderContext) {
ml: ['all', 'read'],
siem: ['all', 'read'],
ingestManager: ['all', 'read'],
+ builtInAlerts: ['all', 'read'],
+ actions: ['all', 'read'],
},
global: ['all', 'read'],
space: ['all', 'read'],
diff --git a/x-pack/test/functional/es_archives/alerts/data.json b/x-pack/test/functional/es_archives/alerts/data.json
index 3703473606ea2..cc246b0fe44da 100644
--- a/x-pack/test/functional/es_archives/alerts/data.json
+++ b/x-pack/test/functional/es_archives/alerts/data.json
@@ -38,4 +38,46 @@
"updated_at": "2020-06-17T15:35:39.839Z"
}
}
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "alert:74f3e6d7-b7bb-477d-ac28-fdf248d5f2a4",
+ "index": ".kibana_1",
+ "source": {
+ "alert": {
+ "actions": [
+ ],
+ "alertTypeId": "example.always-firing",
+ "apiKey": "XHcE1hfSJJCvu2oJrKErgbIbR7iu3XAX+c1kki8jESzWZNyBlD4+6yHhCDEx7rNLlP/Hvbut/V8N1BaQkaSpVpiNsW/UxshiCouqJ+cmQ9LbaYnca9eTTVUuPhbHwwsDjfYkakDPqW3gB8sonwZl6rpzZVacfp4=",
+ "apiKeyOwner": "elastic",
+ "consumer": "metrics",
+ "createdAt": "2020-06-17T15:35:38.497Z",
+ "createdBy": "elastic",
+ "enabled": true,
+ "muteAll": false,
+ "mutedInstanceIds": [
+ ],
+ "name": "always-firing-alert",
+ "params": {
+ },
+ "schedule": {
+ "interval": "1m"
+ },
+ "scheduledTaskId": "329798f0-b0b0-11ea-9510-fdf248d5f2a4",
+ "tags": [
+ ],
+ "throttle": null,
+ "updatedBy": "elastic"
+ },
+ "migrationVersion": {
+ "alert": "7.8.0"
+ },
+ "references": [
+ ],
+ "type": "alert",
+ "updated_at": "2020-06-17T15:35:39.839Z"
+ }
+ }
}
\ No newline at end of file
diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts
index 2225316bba80f..09c4156854506 100644
--- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts
@@ -28,7 +28,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
name: generateUniqueKey(),
tags: ['foo', 'bar'],
alertTypeId: 'test.noop',
- consumer: 'test',
+ consumer: 'alerts',
schedule: { interval: '1m' },
throttle: '1m',
actions: [],
@@ -372,7 +372,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await testSubjects.click('deleteAll');
await testSubjects.existOrFail('deleteIdsConfirmation');
await testSubjects.click('deleteIdsConfirmation > confirmModalConfirmButton');
- await testSubjects.missingOrFail('deleteIdsConfirmation');
+ await testSubjects.missingOrFail('deleteIdsConfirmation', { timeout: 5000 });
await pageObjects.common.closeToast();
diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/kibana.json b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/kibana.json
index 74f740f52a8b2..4ad7aa3126e88 100644
--- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/kibana.json
+++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/kibana.json
@@ -3,7 +3,7 @@
"version": "1.0.0",
"kibanaVersion": "kibana",
"configPath": ["xpack"],
- "requiredPlugins": ["alerts", "triggers_actions_ui"],
+ "requiredPlugins": ["alerts", "triggers_actions_ui", "features"],
"server": true,
"ui": true
}
diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/public/plugin.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/public/plugin.ts
index 2bc299ede930b..503c328017a9a 100644
--- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/public/plugin.ts
+++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/public/plugin.ts
@@ -21,7 +21,7 @@ export interface AlertingExamplePublicSetupDeps {
export class AlertingFixturePlugin implements Plugin {
public setup(core: CoreSetup, { alerts, triggers_actions_ui }: AlertingExamplePublicSetupDeps) {
alerts.registerNavigation(
- 'consumer-noop',
+ 'alerting_fixture',
'test.noop',
(alert: SanitizedAlert, alertType: AlertType) => `/alert/${alert.id}`
);
@@ -49,8 +49,8 @@ export class AlertingFixturePlugin implements Plugin {
- public setup(core: CoreSetup, { alerts }: AlertingExampleDeps) {
+ public setup(core: CoreSetup, { alerts, features }: AlertingExampleDeps) {
createNoopAlertType(alerts);
createAlwaysFiringAlertType(alerts);
+ features.registerFeature({
+ id: 'alerting_fixture',
+ name: 'alerting_fixture',
+ app: [],
+ alerting: ['test.always-firing', 'test.noop'],
+ privileges: {
+ all: {
+ alerting: {
+ all: ['test.always-firing', 'test.noop'],
+ },
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ ui: [],
+ },
+ read: {
+ alerting: {
+ all: ['test.always-firing', 'test.noop'],
+ },
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ ui: [],
+ },
+ },
+ });
}
public start() {}
@@ -32,7 +62,7 @@ function createNoopAlertType(alerts: AlertingSetup) {
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
async executor() {},
- producer: 'alerting',
+ producer: 'alerts',
};
alerts.registerType(noopAlertType);
}
@@ -46,7 +76,7 @@ function createAlwaysFiringAlertType(alerts: AlertingSetup) {
{ id: 'default', name: 'Default' },
{ id: 'other', name: 'Other' },
],
- producer: 'alerting',
+ producer: 'alerts',
async executor(alertExecutorOptions: any) {
const { services, state, params } = alertExecutorOptions;
diff --git a/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts b/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts
index 25f4c6a932d5e..23a4529139c53 100644
--- a/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts
+++ b/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts
@@ -43,7 +43,7 @@ export class Alerts {
name,
tags,
alertTypeId,
- consumer: consumer ?? 'bar',
+ consumer: consumer ?? 'alerts',
schedule: schedule ?? { interval: '1m' },
throttle: throttle ?? '1m',
actions: actions ?? [],
@@ -68,7 +68,7 @@ export class Alerts {
name,
tags: ['foo'],
alertTypeId: 'test.noop',
- consumer: 'consumer-noop',
+ consumer: 'alerting_fixture',
schedule: { interval: '1m' },
throttle: '1m',
actions: [],
@@ -101,7 +101,7 @@ export class Alerts {
name,
tags: ['foo'],
alertTypeId: 'test.always-firing',
- consumer: 'bar',
+ consumer: 'alerts',
schedule: { interval: '1m' },
throttle: '1m',
actions,
From f6bc61f2225cd3aa6ba901048b13d0745f21b972 Mon Sep 17 00:00:00 2001
From: Steph Milovic
Date: Wed, 22 Jul 2020 08:03:41 -0600
Subject: [PATCH 09/49] [Security solution] [Timeline] Bug fix for "Collapse
event" button (#72552)
---
.../__snapshots__/event_details.test.tsx.snap | 34 ++-----
.../event_details/event_details.test.tsx | 90 ++++-------------
.../event_details/event_details.tsx | 97 ++++++-------------
3 files changed, 59 insertions(+), 162 deletions(-)
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap
index ebaf60e7078f0..2ae621e71a725 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap
@@ -4,33 +4,6 @@ exports[`EventDetails rendering should match snapshot 1`] = `
-
-
-
-
- }
- closePopover={[Function]}
- display="inlineBlock"
- hasArrow={true}
- isOpen={false}
- ownFocus={false}
- panelPaddingSize="m"
- repositionOnScroll={true}
- />
-
+
+ Collapse event
+
`;
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx
index a5b44fd540c4b..01b0810830dd8 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx
@@ -11,7 +11,7 @@ import '../../mock/match_media';
import { mockDetailItemData, mockDetailItemDataId } from '../../mock/mock_detail_item';
import { TestProviders } from '../../mock/test_providers';
-import { EventDetails } from './event_details';
+import { EventDetails, View } from './event_details';
import { mockBrowserFields } from '../../containers/source/mock';
import { defaultHeaders } from '../../mock/header';
import { useMountAppended } from '../../utils/use_mount_appended';
@@ -20,47 +20,35 @@ jest.mock('../link_to');
describe('EventDetails', () => {
const mount = useMountAppended();
+ const onEventToggled = jest.fn();
+ const defaultProps = {
+ browserFields: mockBrowserFields,
+ columnHeaders: defaultHeaders,
+ data: mockDetailItemData,
+ id: mockDetailItemDataId,
+ view: 'table-view' as View,
+ onEventToggled,
+ onUpdateColumns: jest.fn(),
+ onViewSelected: jest.fn(),
+ timelineId: 'test',
+ toggleColumn: jest.fn(),
+ };
+ const wrapper = mount(
+
+
+
+ );
describe('rendering', () => {
test('should match snapshot', () => {
- const wrapper = shallow(
-
- );
- expect(wrapper).toMatchSnapshot();
+ const shallowWrap = shallow();
+ expect(shallowWrap).toMatchSnapshot();
});
});
describe('tabs', () => {
['Table', 'JSON View'].forEach((tab) => {
test(`it renders the ${tab} tab`, () => {
- const wrapper = mount(
-
-
-
- );
-
expect(
wrapper
.find('[data-test-subj="eventDetails"]')
@@ -71,48 +59,12 @@ describe('EventDetails', () => {
});
test('the Table tab is selected by default', () => {
- const wrapper = mount(
-
-
-
- );
-
expect(
wrapper.find('[data-test-subj="eventDetails"]').find('.euiTab-isSelected').first().text()
).toEqual('Table');
});
test('it invokes `onEventToggled` when the collapse button is clicked', () => {
- const onEventToggled = jest.fn();
-
- const wrapper = mount(
-
-
-
- );
-
wrapper.find('[data-test-subj="collapse"]').first().simulate('click');
wrapper.update();
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx
index 53ec14380d5bc..1cc50b7d951a2 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx
@@ -4,14 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { noop } from 'lodash/fp';
-import {
- EuiButtonIcon,
- EuiPopover,
- EuiTabbedContent,
- EuiTabbedContentTab,
- EuiToolTip,
-} from '@elastic/eui';
+import { EuiLink, EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui';
import React, { useMemo } from 'react';
import styled from 'styled-components';
@@ -26,22 +19,11 @@ import { COLLAPSE, COLLAPSE_EVENT } from '../../../timelines/components/timeline
export type View = 'table-view' | 'json-view';
-const PopoverContainer = styled.div`
- left: -40px;
- position: relative;
- top: 10px;
-
- .euiPopover {
- position: fixed;
- z-index: 10;
- }
+const CollapseLink = styled(EuiLink)`
+ margin: 20px 0;
`;
-const CollapseButton = styled(EuiButtonIcon)`
- border: 1px solid;
-`;
-
-CollapseButton.displayName = 'CollapseButton';
+CollapseLink.displayName = 'CollapseLink';
interface Props {
browserFields: BrowserFields;
@@ -75,59 +57,42 @@ export const EventDetails = React.memo(
timelineId,
toggleColumn,
}) => {
- const button = useMemo(
- () => (
-
-
-
- ),
- [onEventToggled]
+ const tabs: EuiTabbedContentTab[] = useMemo(
+ () => [
+ {
+ id: 'table-view',
+ name: i18n.TABLE,
+ content: (
+
+ ),
+ },
+ {
+ id: 'json-view',
+ name: i18n.JSON_VIEW,
+ content: ,
+ },
+ ],
+ [browserFields, columnHeaders, data, id, onUpdateColumns, timelineId, toggleColumn]
);
- const tabs: EuiTabbedContentTab[] = [
- {
- id: 'table-view',
- name: i18n.TABLE,
- content: (
-
- ),
- },
- {
- id: 'json-view',
- name: i18n.JSON_VIEW,
- content: ,
- },
- ];
-
return (
-
-
-
onViewSelected(e.id as View)}
/>
+
+ {COLLAPSE_EVENT}
+
);
}
From 10846cb361fca77002f1b48af4a091fd09784e7d Mon Sep 17 00:00:00 2001
From: gchaps <33642766+gchaps@users.noreply.github.com>
Date: Wed, 22 Jul 2020 08:00:53 -0700
Subject: [PATCH 10/49] [DOCS] Updates Management docs to match UI (#72514)
* [DOCS] Updates Management docs to match UI
* [DOCS] Incorporates review comments
Co-authored-by: Elastic Machine
---
.../alerts-and-actions-intro.asciidoc | 11 ++---
.../add-policy-to-index.asciidoc | 13 +++---
.../example-index-lifecycle-policy.asciidoc | 8 ++--
.../ingest-pipelines.asciidoc | 4 +-
docs/management/managing-beats.asciidoc | 7 ++--
docs/management/managing-ccr.asciidoc | 2 +-
docs/management/managing-indices.asciidoc | 20 ++++-----
docs/management/managing-licenses.asciidoc | 2 +-
.../managing-remote-clusters.asciidoc | 2 +-
.../create_and_manage_rollups.asciidoc | 11 +++--
.../snapshot-restore/index.asciidoc | 4 +-
.../upgrade-assistant/index.asciidoc | 42 +++++++++----------
docs/management/watcher-ui/index.asciidoc | 6 +--
docs/spaces/index.asciidoc | 3 +-
docs/user/reporting/index.asciidoc | 2 +-
docs/user/security/rbac_tutorial.asciidoc | 2 +-
16 files changed, 71 insertions(+), 68 deletions(-)
diff --git a/docs/management/alerting/alerts-and-actions-intro.asciidoc b/docs/management/alerting/alerts-and-actions-intro.asciidoc
index efc6a670af3e9..429d7915cc1c3 100644
--- a/docs/management/alerting/alerts-and-actions-intro.asciidoc
+++ b/docs/management/alerting/alerts-and-actions-intro.asciidoc
@@ -4,9 +4,10 @@
beta[]
-The *Alerts and Actions* UI lets you <> in a space, and provides tools to <> so that alerts can trigger actions like notification, indexing, and ticketing.
+The *Alerts and Actions* UI lets you <> in a space, and provides tools to <> so that alerts can trigger actions like notification, indexing, and ticketing.
-To manage alerting and connectors, open the menu, then go to *Stack Management > {kib} > Alerts and Actions*.
+To manage alerting and connectors, open the menu,
+then go to *Stack Management > Alerts and Insights > Alerts and Actions*.
[role="screenshot"]
image:management/alerting/images/alerts-and-actions-ui.png[Example alert listing in the Alerts and Actions UI]
@@ -14,12 +15,12 @@ image:management/alerting/images/alerts-and-actions-ui.png[Example alert listing
[NOTE]
============================================================================
Similar to dashboards, alerts and connectors reside in a <>.
-The *Alerts and Actions* UI only shows alerts and connectors for the current space.
+The *Alerts and Actions* UI only shows alerts and connectors for the current space.
============================================================================
[NOTE]
============================================================================
{es} also offers alerting capabilities through Watcher, which
-can be managed through the <>. See
+can be managed through the <>. See
<> for more information.
-============================================================================
\ No newline at end of file
+============================================================================
diff --git a/docs/management/index-lifecycle-policies/add-policy-to-index.asciidoc b/docs/management/index-lifecycle-policies/add-policy-to-index.asciidoc
index eb014a5165048..0fec62d895754 100644
--- a/docs/management/index-lifecycle-policies/add-policy-to-index.asciidoc
+++ b/docs/management/index-lifecycle-policies/add-policy-to-index.asciidoc
@@ -2,17 +2,16 @@
[[adding-policy-to-index]]
=== Adding a policy to an index
-To add a lifecycle policy to an index and view the status for indices
-managed by a policy, open the menu, then go to *Stack Management > {es} > Index Management*. This page lists your
-{es} indices, which you can filter by lifecycle status and lifecycle phase.
+To add a lifecycle policy to an index and view the status for indices
+managed by a policy, open the menu, then go to *Stack Management > Data > Index Management*.
+This page lists your
+{es} indices, which you can filter by lifecycle status and lifecycle phase.
To add a policy, select the index name and then select *Manage Index > Add lifecycle policy*.
-You’ll see the policy name, the phase the index is in, the current
-action, and if any errors occurred performing that action.
+You’ll see the policy name, the phase the index is in, the current
+action, and if any errors occurred performing that action.
To remove a policy from an index, select *Manage Index > Remove lifecycle policy*.
[role="screenshot"]
image::images/index_management_add_policy.png[][UI for adding a policy to an index]
-
-
diff --git a/docs/management/index-lifecycle-policies/example-index-lifecycle-policy.asciidoc b/docs/management/index-lifecycle-policies/example-index-lifecycle-policy.asciidoc
index 69e74d6538e4f..0097bf8c648f0 100644
--- a/docs/management/index-lifecycle-policies/example-index-lifecycle-policy.asciidoc
+++ b/docs/management/index-lifecycle-policies/example-index-lifecycle-policy.asciidoc
@@ -3,7 +3,7 @@
[[example-using-index-lifecycle-policy]]
=== Tutorial: Use {ilm-init} to manage {filebeat} time-based indices
-With {ilm} ({ilm-init}), you can create policies that perform actions automatically
+With {ilm} ({ilm-init}), you can create policies that perform actions automatically
on indices as they age and grow. {ilm-init} policies help you to manage
performance, resilience, and retention of your data during its lifecycle. This tutorial shows
you how to use {kib}’s *Index Lifecycle Policies* to modify and create {ilm-init}
@@ -59,7 +59,7 @@ output as described in {filebeat-ref}/filebeat-getting-started.html[Getting Star
{filebeat} includes a default {ilm-init} policy that enables rollover. {ilm-init}
is enabled automatically if you’re using the default `filebeat.yml` and index template.
-To view the default policy in {kib}, open the menu, go to * Stack Management > {es} > Index Lifecycle Policies*,
+To view the default policy in {kib}, open the menu, go to *Stack Management > Data > Index Lifecycle Policies*,
search for _filebeat_, and choose the _filebeat-version_ policy.
This policy initiates the rollover action when the index size reaches 50GB or
@@ -114,7 +114,7 @@ If meeting a specific retention time period is most important, you can create a
custom policy. For this option, you will use {filebeat} daily indices without
rollover.
-. To create a custom policy, open the menu, go to *Stack Management > {es} > Index Lifecycle Policies*, then click
+. To create a custom policy, open the menu, go to *Stack Management > Data > Index Lifecycle Policies*, then click
*Create policy*.
. Activate the warm phase and configure it as follows:
@@ -156,7 +156,7 @@ image::images/tutorial-ilm-custom-policy.png["Modify the custom policy to add a
[role="screenshot"]
image::images/tutorial-ilm-delete-phase-creation.png["Delete phase"]
-. To configure the index to use the new policy, open the menu, then go to *Stack Management > {es} > Index Lifecycle
+. To configure the index to use the new policy, open the menu, then go to *Stack Management > Data > Index Lifecycle
Policies*.
.. Find your {ilm-init} policy.
diff --git a/docs/management/ingest-pipelines/ingest-pipelines.asciidoc b/docs/management/ingest-pipelines/ingest-pipelines.asciidoc
index 8c259dae256d4..da2d3b8accac2 100644
--- a/docs/management/ingest-pipelines/ingest-pipelines.asciidoc
+++ b/docs/management/ingest-pipelines/ingest-pipelines.asciidoc
@@ -7,7 +7,7 @@ pipelines that perform common transformations and
enrichments on your data. For example, you might remove a field,
rename an existing field, or set a new field.
-You’ll find *Ingest Node Pipelines* in *Management > Elasticsearch*. With this feature, you can:
+You’ll find *Ingest Node Pipelines* in *Stack Management > Ingest*. With this feature, you can:
* View a list of your pipelines and drill down into details.
* Create a pipeline that defines a series of tasks, known as processors.
@@ -23,7 +23,7 @@ image:management/ingest-pipelines/images/ingest-pipeline-list.png["Ingest node p
The minimum required permissions to access *Ingest Node Pipelines* are
the `manage_pipeline` and `cluster:monitor/nodes/info` cluster privileges.
-You can add these privileges in *Management > Security > Roles*.
+You can add these privileges in *Stack Management > Security > Roles*.
[role="screenshot"]
image:management/ingest-pipelines/images/ingest-pipeline-privileges.png["Privileges required for Ingest Node Pipelines"]
diff --git a/docs/management/managing-beats.asciidoc b/docs/management/managing-beats.asciidoc
index d5a9c52feae23..678e160b99af0 100644
--- a/docs/management/managing-beats.asciidoc
+++ b/docs/management/managing-beats.asciidoc
@@ -4,7 +4,8 @@
include::{asciidoc-dir}/../../shared/discontinued.asciidoc[tag=cm-discontinued]
-To use the Central Management UI, open the menu, go to *Stack Management > {beats} > Central Management*, then define and
+To use {beats} Central Management UI, open the menu, go to *Stack Management > Ingest >
+{beats} Central Management*, then define and
manage configurations in a central location in {kib} and quickly deploy
configuration changes to all {beats} running across your enterprise. For more
about central management, see the related {beats} documentation:
@@ -17,8 +18,8 @@ about central management, see the related {beats} documentation:
This feature requires an Elastic license that includes {beats} central
management.
-Don't have a license? You can start a 30-day trial. Open the menu, go to
-*Stack Management > Elasticsearch > License Management*. At the end of the trial
+Don't have a license? You can start a 30-day trial. Open the menu,
+go to *Stack Management > Stack > License Management*. At the end of the trial
period, you can purchase a subscription to keep using central management. For
more information, see https://www.elastic.co/subscriptions and
<>.
diff --git a/docs/management/managing-ccr.asciidoc b/docs/management/managing-ccr.asciidoc
index 2df9addf74919..67193b3b5a037 100644
--- a/docs/management/managing-ccr.asciidoc
+++ b/docs/management/managing-ccr.asciidoc
@@ -7,7 +7,7 @@ remote clusters on a local cluster. {ref}/xpack-ccr.html[Cross-cluster replicati
is commonly used to provide remote backups for disaster recovery and for
geo-proximite copies of data.
-To get started, open the menu, then go to *Stack Management > Elasticsearch > Cross-Cluster Replication*.
+To get started, open the menu, then go to *Stack Management > Data > Cross-Cluster Replication*.
[role="screenshot"]
image::images/cross-cluster-replication-list-view.png[][Cross-cluster replication list view]
diff --git a/docs/management/managing-indices.asciidoc b/docs/management/managing-indices.asciidoc
index 4fc4ac1d37429..24cd094c877c6 100644
--- a/docs/management/managing-indices.asciidoc
+++ b/docs/management/managing-indices.asciidoc
@@ -13,7 +13,7 @@ the amount of bookkeeping when working with indices. Instead of manually
setting up your indices, you can create them automatically from a template,
ensuring that your settings, mappings, and aliases are consistently defined.
-To manage your indices, open the menu, then go to *Stack Management > {es} > Index Management*.
+To manage your indices, open the menu, then go to *Stack Management > Data > Index Management*.
[role="screenshot"]
image::images/management_index_labels.png[Index Management UI]
@@ -130,17 +130,17 @@ Alternatively, you can click the *Load JSON* link and define the mapping as JSON
[source,js]
----------------------------------
-{
+{
"properties": {
"geo": {
- "properties": {
- "coordinates": {
- "type": "geo_point"
- }
- }
- }
- }
-}
+ "properties": {
+ "coordinates": {
+ "type": "geo_point"
+ }
+ }
+ }
+ }
+}
----------------------------------
You can create additional mapping configurations in the *Dynamic templates* and
diff --git a/docs/management/managing-licenses.asciidoc b/docs/management/managing-licenses.asciidoc
index 99cfd12eeade9..25ae29036f656 100644
--- a/docs/management/managing-licenses.asciidoc
+++ b/docs/management/managing-licenses.asciidoc
@@ -7,7 +7,7 @@ with no expiration date. For the full list of features, refer to
If you want to try out the full set of features, you can activate a free 30-day
trial. To view the status of your license, start a trial, or install a new
-license, open the menu, then go to *Stack Management > {es} > License Management*.
+license, open the menu, then go to *Stack Management > Stack > License Management*.
NOTE: You can start a trial only if your cluster has not already activated a
trial license for the current major product version. For example, if you have
diff --git a/docs/management/managing-remote-clusters.asciidoc b/docs/management/managing-remote-clusters.asciidoc
index 8ccd27b65aed6..83895838efec6 100644
--- a/docs/management/managing-remote-clusters.asciidoc
+++ b/docs/management/managing-remote-clusters.asciidoc
@@ -6,7 +6,7 @@ connection from your cluster to other clusters. This functionality is
required for {ref}/xpack-ccr.html[cross-cluster replication] and
{ref}/modules-cross-cluster-search.html[cross-cluster search].
-To get started, open the menu, then go to *Stack Management > {es} > Remote Clusters*.
+To get started, open the menu, then go to *Stack Management > Data > Remote Clusters*.
[role="screenshot"]
image::images/remote-clusters-list-view.png[Remote Clusters list view, including Add a remote cluster button]
diff --git a/docs/management/rollups/create_and_manage_rollups.asciidoc b/docs/management/rollups/create_and_manage_rollups.asciidoc
index bbdc382d04b38..831b536f8c1cb 100644
--- a/docs/management/rollups/create_and_manage_rollups.asciidoc
+++ b/docs/management/rollups/create_and_manage_rollups.asciidoc
@@ -8,7 +8,7 @@ by an index pattern, and then rolls it into a new index. Rollup indices are a go
compactly store months or years of historical
data for use in visualizations and reports.
-To get started, open the menu, then go to *Stack Management > {es} > Rollup Jobs*. With this UI,
+To get started, open the menu, then go to *Stack Management > Data > Rollup Jobs*. With this UI,
you can:
* <>
@@ -130,8 +130,9 @@ Your next step is to visualize your rolled up data in a vertical bar chart.
Most visualizations support rolled up data, with the exception of Timelion and Vega visualizations.
-. Create the rollup index pattern in *Management > Index Patterns* so you can
-select your rolled up data for visualizations. Click *Create index pattern*, and select *Rollup index pattern* from the dropdown.
+. Go to *Stack Management > {kib} > Index Patterns*.
+
+. Click *Create index pattern*, and select *Rollup index pattern* from the dropdown.
+
[role="screenshot"]
image::images/management-rollup-index-pattern.png[][Create rollup index pattern]
@@ -144,7 +145,9 @@ is `rollup_logstash,kibana_sample_data_logs`. In this index pattern, `rollup_log
matches the rolled up index pattern and `kibana_sample_data_logs` matches the index
pattern for raw data.
-. Go to *Visualize* and create a vertical bar chart. Choose `rollup_logstash,kibana_sample_data_logs`
+. Go to *Visualize* and create a vertical bar chart.
+
+. Choose `rollup_logstash,kibana_sample_data_logs`
as your source to see both the raw and rolled up data.
+
[role="screenshot"]
diff --git a/docs/management/snapshot-restore/index.asciidoc b/docs/management/snapshot-restore/index.asciidoc
index a64b74069f978..1bf62522e245c 100644
--- a/docs/management/snapshot-restore/index.asciidoc
+++ b/docs/management/snapshot-restore/index.asciidoc
@@ -8,7 +8,7 @@ Snapshots are important because they provide a copy of your data in case
something goes wrong. If you need to roll back to an older version of your data,
you can restore a snapshot from the repository.
-To get started, open the menu, then go to *Stack Management > {es} > Snapshot and Restore*.
+To get started, open the menu, then go to *Stack Management > Data > Snapshot and Restore*.
With this UI, you can:
* Register a repository for storing your snapshots
@@ -191,7 +191,7 @@ your master and data nodes. You can do this in one of two ways:
Use *Snapshot and Restore* to register the repository where your snapshots
will live.
-. Open the menu, then go to *Stack Management > {es} > Snapshot and Restore*.
+. Open the menu, then go to *Stack Management > Data > Snapshot and Restore*.
. Click *Register a repository* in either the introductory message or *Repository view*.
. Enter a name for your repository, for example, `my_backup`.
. Select *Shared file system*.
diff --git a/docs/management/upgrade-assistant/index.asciidoc b/docs/management/upgrade-assistant/index.asciidoc
index ab6d0790ffa3f..c5fd6a3a555a1 100644
--- a/docs/management/upgrade-assistant/index.asciidoc
+++ b/docs/management/upgrade-assistant/index.asciidoc
@@ -2,50 +2,50 @@
[[upgrade-assistant]]
== Upgrade Assistant
-The Upgrade Assistant helps you prepare for your upgrade to the next major {es} version.
-For example, if you are using 6.8, the Upgrade Assistant helps you to upgrade to 7.0.
-To access the assistant, open the menu, then go to *Stack Management > {es} > Upgrade Assistant*.
+The Upgrade Assistant helps you prepare for your upgrade to the next major {es} version.
+For example, if you are using 6.8, the Upgrade Assistant helps you to upgrade to 7.0.
+To access the assistant, open the menu, then go to *Stack Management > Stack > Upgrade Assistant*.
-The assistant identifies the deprecated settings in your cluster and indices
-and guides you through the process of resolving issues, including reindexing.
+The assistant identifies the deprecated settings in your cluster and indices
+and guides you through the process of resolving issues, including reindexing.
-Before you upgrade, make sure that you are using the latest released minor
-version of {es} to see the most up-to-date deprecation issues.
+Before you upgrade, make sure that you are using the latest released minor
+version of {es} to see the most up-to-date deprecation issues.
For example, if you want to upgrade to to 7.0, make sure that you are using 6.8.
[float]
=== Reindexing
-The *Indices* page lists the indices that are incompatible with the next
+The *Indices* page lists the indices that are incompatible with the next
major version of {es}. You can initiate a reindex to resolve the issues.
[role="screenshot"]
image::images/management-upgrade-assistant-9.0.png[]
-For a preview of how the data will change during the reindex, select the
-index name. A warning appears if the index requires destructive changes.
-Back up your index, then proceed with the reindex by accepting each breaking change.
+For a preview of how the data will change during the reindex, select the
+index name. A warning appears if the index requires destructive changes.
+Back up your index, then proceed with the reindex by accepting each breaking change.
-You can follow the progress as the Upgrade Assistant makes the index read-only,
-creates a new index, reindexes the documents, and creates an alias that points
-from the old index to the new one.
+You can follow the progress as the Upgrade Assistant makes the index read-only,
+creates a new index, reindexes the documents, and creates an alias that points
+from the old index to the new one.
-If the reindexing fails or is cancelled, the changes are rolled back, the
-new index is deleted, and the original index becomes writable. An error
+If the reindexing fails or is cancelled, the changes are rolled back, the
+new index is deleted, and the original index becomes writable. An error
message explains the reason for the failure.
-You can reindex multiple indices at a time, but keep an eye on the
-{es} metrics, including CPU usage, memory pressure, and disk usage. If a
-metric is so high it affects query performance, cancel the reindex and
+You can reindex multiple indices at a time, but keep an eye on the
+{es} metrics, including CPU usage, memory pressure, and disk usage. If a
+metric is so high it affects query performance, cancel the reindex and
continue by reindexing fewer indices at a time.
Additional considerations:
* If you use {alert-features}, when you reindex the internal indices
-(`.watches`), the {watcher} process pauses and no alerts are triggered.
+(`.watches`), the {watcher} process pauses and no alerts are triggered.
* If you use {ml-features}, when you reindex the internal indices (`.ml-state`),
-the {ml} jobs pause and models are not trained or updated.
+the {ml} jobs pause and models are not trained or updated.
* If you use {security-features}, before you reindex the internal indices
(`.security*`), it is a good idea to create a temporary superuser account in the
diff --git a/docs/management/watcher-ui/index.asciidoc b/docs/management/watcher-ui/index.asciidoc
index fa3e0cce04fff..fbe5fcd5cd3a5 100644
--- a/docs/management/watcher-ui/index.asciidoc
+++ b/docs/management/watcher-ui/index.asciidoc
@@ -8,7 +8,8 @@ Watches are helpful for analyzing mission-critical and business-critical
streaming data. For example, you might watch application logs for performance
outages or audit access logs for security threats.
-To get started with the Watcher UI, open then menu, then go to *Stack Management > {es} > Watcher*.
+To get started with the Watcher UI, open then menu,
+then go to *Stack Management > Alerts and Insights > Watcher*.
With this UI, you can:
* <>
@@ -238,6 +239,3 @@ Refer to these examples for creating an advanced watch:
* {ref}/watch-cluster-status.html[Watch the status of an {es} cluster]
* {ref}/watching-meetup-data.html[Watch event data]
-
-
-
diff --git a/docs/spaces/index.asciidoc b/docs/spaces/index.asciidoc
index bbc213dc2050e..9e505b8bfe045 100644
--- a/docs/spaces/index.asciidoc
+++ b/docs/spaces/index.asciidoc
@@ -116,7 +116,8 @@ interface.
You can create a custom experience for users by configuring the {kib} landing page on a per-space basis.
The landing page can route users to a specific dashboard, application, or saved object as they enter each space.
-To configure the landing page, use the default route setting in < Advanced settings>>.
+To configure the landing page, use the default route setting in
+< {kib} > Advanced settings>>.
For example, you might set the default route to `/app/kibana#/dashboards`.
[role="screenshot"]
diff --git a/docs/user/reporting/index.asciidoc b/docs/user/reporting/index.asciidoc
index e4e4b461ac2bd..4f4d59315fafa 100644
--- a/docs/user/reporting/index.asciidoc
+++ b/docs/user/reporting/index.asciidoc
@@ -94,7 +94,7 @@ image::user/reporting/images/preserve-layout-switch.png["Share"]
[[manage-report-history]]
== View and manage report history
-For a list of your reports, open the menu, then go to *Stack Management > {kib} > Reporting*.
+For a list of your reports, open the menu, then go to *Stack Management > Alerts and Insights > Reporting*.
From this view, you can monitor the generation of a report and
download reports that you previously generated.
diff --git a/docs/user/security/rbac_tutorial.asciidoc b/docs/user/security/rbac_tutorial.asciidoc
index d7299f814b43c..3a4b2202201e2 100644
--- a/docs/user/security/rbac_tutorial.asciidoc
+++ b/docs/user/security/rbac_tutorial.asciidoc
@@ -90,7 +90,7 @@ image::security/images/role-space-visualization.png["Associate space"]
[float]
==== Create the developer user account with the proper roles
-. Open the menu, then go to *Stack Management > Users*.
+. Open the menu, then go to *Stack Management > Security > Users*.
. Click **Create user**, then give the user the `dev-mortgage`
and `monitoring-user` roles, which are required for *Stack Monitoring* users.
From 9655c87564366400fa41d8a1cafb257e8059d8fa Mon Sep 17 00:00:00 2001
From: Spencer
Date: Wed, 22 Jul 2020 08:02:39 -0700
Subject: [PATCH 11/49] [kbn/es-archiver] move to a package (#72318)
Co-authored-by: spalger
Co-authored-by: Elastic Machine
---
.github/CODEOWNERS | 2 +-
package.json | 1 +
.../src/run/run_with_commands.test.ts | 4 +-
.../src/run/run_with_commands.ts | 9 +-
packages/kbn-es-archiver/package.json | 17 ++
.../kbn-es-archiver/src}/actions/edit.ts | 2 +-
.../src}/actions/empty_kibana_index.ts | 0
.../kbn-es-archiver/src}/actions/index.ts | 0
.../kbn-es-archiver/src}/actions/load.ts | 2 +-
.../src}/actions/rebuild_all.ts | 2 +-
.../kbn-es-archiver/src}/actions/save.ts | 2 +-
.../kbn-es-archiver/src}/actions/unload.ts | 2 +-
packages/kbn-es-archiver/src/cli.ts | 244 ++++++++++++++++++
.../kbn-es-archiver/src}/es_archiver.ts | 0
.../kbn-es-archiver/src}/index.ts | 1 +
.../src}/lib/__tests__/stats.ts | 0
.../src}/lib/archives/__tests__/format.ts | 6 +-
.../src}/lib/archives/__tests__/parse.ts | 10 +-
.../src}/lib/archives/constants.ts | 0
.../src}/lib/archives/filenames.ts | 0
.../src}/lib/archives/format.ts | 2 +-
.../src}/lib/archives/index.ts | 0
.../src}/lib/archives/parse.ts | 8 +-
.../kbn-es-archiver/src}/lib/directory.ts | 0
.../__tests__/generate_doc_records_stream.ts | 6 +-
.../__tests__/index_doc_records_stream.ts | 2 +-
.../src}/lib/docs/__tests__/stubs.ts | 0
.../lib/docs/generate_doc_records_stream.ts | 0
.../kbn-es-archiver/src}/lib/docs/index.ts | 0
.../src}/lib/docs/index_doc_records_stream.ts | 0
.../kbn-es-archiver/src}/lib/index.ts | 0
.../indices/__tests__/create_index_stream.ts | 6 +-
.../indices/__tests__/delete_index_stream.ts | 2 +-
.../generate_index_records_stream.ts | 6 +-
.../src}/lib/indices/__tests__/stubs.ts | 0
.../src}/lib/indices/create_index_stream.ts | 0
.../src}/lib/indices/delete_index.ts | 0
.../src}/lib/indices/delete_index_stream.ts | 0
.../indices/generate_index_records_stream.ts | 0
.../kbn-es-archiver/src}/lib/indices/index.ts | 0
.../src}/lib/indices/kibana_index.ts | 2 +-
.../kbn-es-archiver/src}/lib/progress.ts | 0
.../__tests__/filter_records_stream.ts | 6 +-
.../src}/lib/records/filter_records_stream.ts | 0
.../kbn-es-archiver/src}/lib/records/index.ts | 0
.../kbn-es-archiver/src}/lib/stats.ts | 0
packages/kbn-es-archiver/src/lib/streams.ts | 20 ++
packages/kbn-es-archiver/tsconfig.json | 12 +
packages/kbn-es-archiver/yarn.lock | 1 +
.../src/functional_test_runner/index.ts | 2 +-
scripts/es_archiver.js | 2 +-
src/dev/build/tasks/copy_source_task.js | 1 -
.../ingestion_pipeline_painless.json | 2 +-
src/es_archiver/cli.ts | 183 -------------
src/es_archiver/cli_help.txt | 15 --
test/common/services/es_archiver.ts | 2 +-
.../app_search/engines.ts | 2 +-
.../common/suites/copy_to_space.ts | 2 +-
.../suites/resolve_copy_to_space_conflicts.ts | 2 +-
59 files changed, 336 insertions(+), 254 deletions(-)
create mode 100644 packages/kbn-es-archiver/package.json
rename {src/es_archiver => packages/kbn-es-archiver/src}/actions/edit.ts (97%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/actions/empty_kibana_index.ts (100%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/actions/index.ts (100%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/actions/load.ts (99%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/actions/rebuild_all.ts (97%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/actions/save.ts (96%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/actions/unload.ts (97%)
create mode 100644 packages/kbn-es-archiver/src/cli.ts
rename {src/es_archiver => packages/kbn-es-archiver/src}/es_archiver.ts (100%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/index.ts (97%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/lib/__tests__/stats.ts (100%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/lib/archives/__tests__/format.ts (96%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/lib/archives/__tests__/parse.ts (97%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/lib/archives/constants.ts (100%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/lib/archives/filenames.ts (100%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/lib/archives/format.ts (94%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/lib/archives/index.ts (100%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/lib/archives/parse.ts (87%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/lib/directory.ts (100%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/lib/docs/__tests__/generate_doc_records_stream.ts (97%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/lib/docs/__tests__/index_doc_records_stream.ts (99%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/lib/docs/__tests__/stubs.ts (100%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/lib/docs/generate_doc_records_stream.ts (100%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/lib/docs/index.ts (100%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/lib/docs/index_doc_records_stream.ts (100%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/lib/index.ts (100%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/lib/indices/__tests__/create_index_stream.ts (98%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/lib/indices/__tests__/delete_index_stream.ts (99%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/lib/indices/__tests__/generate_index_records_stream.ts (97%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/lib/indices/__tests__/stubs.ts (100%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/lib/indices/create_index_stream.ts (100%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/lib/indices/delete_index.ts (100%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/lib/indices/delete_index_stream.ts (100%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/lib/indices/generate_index_records_stream.ts (100%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/lib/indices/index.ts (100%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/lib/indices/kibana_index.ts (98%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/lib/progress.ts (100%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/lib/records/__tests__/filter_records_stream.ts (95%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/lib/records/filter_records_stream.ts (100%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/lib/records/index.ts (100%)
rename {src/es_archiver => packages/kbn-es-archiver/src}/lib/stats.ts (100%)
create mode 100644 packages/kbn-es-archiver/src/lib/streams.ts
create mode 100644 packages/kbn-es-archiver/tsconfig.json
create mode 120000 packages/kbn-es-archiver/yarn.lock
delete mode 100644 src/es_archiver/cli.ts
delete mode 100644 src/es_archiver/cli_help.txt
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 2ad82ded6cb38..f1a374445657f 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -115,7 +115,6 @@
/src/dev/ @elastic/kibana-operations
/src/setup_node_env/ @elastic/kibana-operations
/src/optimize/ @elastic/kibana-operations
-/src/es_archiver/ @elastic/kibana-operations
/packages/*eslint*/ @elastic/kibana-operations
/packages/*babel*/ @elastic/kibana-operations
/packages/kbn-dev-utils*/ @elastic/kibana-operations
@@ -124,6 +123,7 @@
/packages/kbn-pm/ @elastic/kibana-operations
/packages/kbn-test/ @elastic/kibana-operations
/packages/kbn-ui-shared-deps/ @elastic/kibana-operations
+/packages/kbn-es-archiver/ @elastic/kibana-operations
/src/legacy/server/keystore/ @elastic/kibana-operations
/src/legacy/server/pid/ @elastic/kibana-operations
/src/legacy/server/sass/ @elastic/kibana-operations
diff --git a/package.json b/package.json
index 2f3f95854df04..0d6bc8cc1fceb 100644
--- a/package.json
+++ b/package.json
@@ -300,6 +300,7 @@
"@elastic/makelogs": "^6.0.0",
"@kbn/dev-utils": "1.0.0",
"@kbn/es": "1.0.0",
+ "@kbn/es-archiver": "1.0.0",
"@kbn/eslint-import-resolver-kibana": "2.0.0",
"@kbn/eslint-plugin-eslint": "1.0.0",
"@kbn/expect": "1.0.0",
diff --git a/packages/kbn-dev-utils/src/run/run_with_commands.test.ts b/packages/kbn-dev-utils/src/run/run_with_commands.test.ts
index eb7708998751c..f6759b218bf32 100644
--- a/packages/kbn-dev-utils/src/run/run_with_commands.test.ts
+++ b/packages/kbn-dev-utils/src/run/run_with_commands.test.ts
@@ -61,9 +61,7 @@ it('extends the context using extendContext()', async () => {
expect(context.flags).toMatchInlineSnapshot(`
Object {
- "_": Array [
- "foo",
- ],
+ "_": Array [],
"debug": false,
"help": false,
"quiet": false,
diff --git a/packages/kbn-dev-utils/src/run/run_with_commands.ts b/packages/kbn-dev-utils/src/run/run_with_commands.ts
index 9fb069e4b2d35..ca56a17b545a7 100644
--- a/packages/kbn-dev-utils/src/run/run_with_commands.ts
+++ b/packages/kbn-dev-utils/src/run/run_with_commands.ts
@@ -91,6 +91,13 @@ export class RunWithCommands {
const commandFlagOptions = mergeFlagOptions(this.options.globalFlags, command.flags);
const commandFlags = getFlags(process.argv.slice(2), commandFlagOptions);
+ // strip command name plus "help" if we're actually executing the fake "help" command
+ if (isHelpCommand) {
+ commandFlags._.splice(0, 2);
+ } else {
+ commandFlags._.splice(0, 1);
+ }
+
const commandHelp = getCommandLevelHelp({
usage: this.options.usage,
globalFlagHelp: this.options.globalFlags?.help,
@@ -115,7 +122,7 @@ export class RunWithCommands {
log,
flags: commandFlags,
procRunner,
- addCleanupTask: cleanup.add,
+ addCleanupTask: cleanup.add.bind(cleanup),
};
const extendedContext = {
diff --git a/packages/kbn-es-archiver/package.json b/packages/kbn-es-archiver/package.json
new file mode 100644
index 0000000000000..13b5662519b19
--- /dev/null
+++ b/packages/kbn-es-archiver/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "@kbn/es-archiver",
+ "version": "1.0.0",
+ "license": "Apache-2.0",
+ "main": "target/index.js",
+ "scripts": {
+ "kbn:bootstrap": "tsc",
+ "kbn:watch": "tsc --watch"
+ },
+ "dependencies": {
+ "@kbn/dev-utils": "1.0.0",
+ "elasticsearch": "^16.7.0"
+ },
+ "devDependencies": {
+ "@types/elasticsearch": "^5.0.33"
+ }
+}
\ No newline at end of file
diff --git a/src/es_archiver/actions/edit.ts b/packages/kbn-es-archiver/src/actions/edit.ts
similarity index 97%
rename from src/es_archiver/actions/edit.ts
rename to packages/kbn-es-archiver/src/actions/edit.ts
index afa51a3b96477..1194637b1ff89 100644
--- a/src/es_archiver/actions/edit.ts
+++ b/packages/kbn-es-archiver/src/actions/edit.ts
@@ -24,7 +24,7 @@ import { promisify } from 'util';
import globby from 'globby';
import { ToolingLog } from '@kbn/dev-utils';
-import { createPromiseFromStreams } from '../../legacy/utils';
+import { createPromiseFromStreams } from '../lib/streams';
const unlinkAsync = promisify(Fs.unlink);
diff --git a/src/es_archiver/actions/empty_kibana_index.ts b/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts
similarity index 100%
rename from src/es_archiver/actions/empty_kibana_index.ts
rename to packages/kbn-es-archiver/src/actions/empty_kibana_index.ts
diff --git a/src/es_archiver/actions/index.ts b/packages/kbn-es-archiver/src/actions/index.ts
similarity index 100%
rename from src/es_archiver/actions/index.ts
rename to packages/kbn-es-archiver/src/actions/index.ts
diff --git a/src/es_archiver/actions/load.ts b/packages/kbn-es-archiver/src/actions/load.ts
similarity index 99%
rename from src/es_archiver/actions/load.ts
rename to packages/kbn-es-archiver/src/actions/load.ts
index 03de8f39a7c04..efb1fe9f9ea54 100644
--- a/src/es_archiver/actions/load.ts
+++ b/packages/kbn-es-archiver/src/actions/load.ts
@@ -23,7 +23,7 @@ import { Readable } from 'stream';
import { ToolingLog, KbnClient } from '@kbn/dev-utils';
import { Client } from 'elasticsearch';
-import { createPromiseFromStreams, concatStreamProviders } from '../../legacy/utils';
+import { createPromiseFromStreams, concatStreamProviders } from '../lib/streams';
import {
isGzip,
diff --git a/src/es_archiver/actions/rebuild_all.ts b/packages/kbn-es-archiver/src/actions/rebuild_all.ts
similarity index 97%
rename from src/es_archiver/actions/rebuild_all.ts
rename to packages/kbn-es-archiver/src/actions/rebuild_all.ts
index dfbd51300e04d..470a566a6eef0 100644
--- a/src/es_archiver/actions/rebuild_all.ts
+++ b/packages/kbn-es-archiver/src/actions/rebuild_all.ts
@@ -23,7 +23,7 @@ import { Readable, Writable } from 'stream';
import { fromNode } from 'bluebird';
import { ToolingLog } from '@kbn/dev-utils';
-import { createPromiseFromStreams } from '../../legacy/utils';
+import { createPromiseFromStreams } from '../lib/streams';
import {
prioritizeMappings,
readDirectory,
diff --git a/src/es_archiver/actions/save.ts b/packages/kbn-es-archiver/src/actions/save.ts
similarity index 96%
rename from src/es_archiver/actions/save.ts
rename to packages/kbn-es-archiver/src/actions/save.ts
index 7a3a9dd97c0ab..2f87cabadee6c 100644
--- a/src/es_archiver/actions/save.ts
+++ b/packages/kbn-es-archiver/src/actions/save.ts
@@ -23,7 +23,7 @@ import { Readable, Writable } from 'stream';
import { Client } from 'elasticsearch';
import { ToolingLog } from '@kbn/dev-utils';
-import { createListStream, createPromiseFromStreams } from '../../legacy/utils';
+import { createListStream, createPromiseFromStreams } from '../lib/streams';
import {
createStats,
createGenerateIndexRecordsStream,
diff --git a/src/es_archiver/actions/unload.ts b/packages/kbn-es-archiver/src/actions/unload.ts
similarity index 97%
rename from src/es_archiver/actions/unload.ts
rename to packages/kbn-es-archiver/src/actions/unload.ts
index 130a6b542b218..ae23ef21fb79f 100644
--- a/src/es_archiver/actions/unload.ts
+++ b/packages/kbn-es-archiver/src/actions/unload.ts
@@ -23,7 +23,7 @@ import { Readable, Writable } from 'stream';
import { Client } from 'elasticsearch';
import { ToolingLog, KbnClient } from '@kbn/dev-utils';
-import { createPromiseFromStreams } from '../../legacy/utils';
+import { createPromiseFromStreams } from '../lib/streams';
import {
isGzip,
createStats,
diff --git a/packages/kbn-es-archiver/src/cli.ts b/packages/kbn-es-archiver/src/cli.ts
new file mode 100644
index 0000000000000..1745bd862b434
--- /dev/null
+++ b/packages/kbn-es-archiver/src/cli.ts
@@ -0,0 +1,244 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/** ***********************************************************
+ *
+ * Run `node scripts/es_archiver --help` for usage information
+ *
+ *************************************************************/
+
+import Path from 'path';
+import Url from 'url';
+import readline from 'readline';
+
+import { RunWithCommands, createFlagError } from '@kbn/dev-utils';
+import { readConfigFile } from '@kbn/test';
+import legacyElasticsearch from 'elasticsearch';
+
+import { EsArchiver } from './es_archiver';
+
+const resolveConfigPath = (v: string) => Path.resolve(process.cwd(), v);
+const defaultConfigPath = resolveConfigPath('test/functional/config.js');
+
+export function runCli() {
+ new RunWithCommands({
+ description: 'CLI to manage archiving/restoring data in elasticsearch',
+ globalFlags: {
+ string: ['es-url', 'kibana-url', 'dir', 'config'],
+ help: `
+ --config path to an FTR config file that sets --es-url, --kibana-url, and --dir
+ default: ${defaultConfigPath}
+ --es-url url for Elasticsearch, prefer the --config flag
+ --kibana-url url for Kibana, prefer the --config flag
+ --dir where arechives are stored, prefer the --config flag
+ `,
+ },
+ async extendContext({ log, flags, addCleanupTask }) {
+ const configPath = flags.config || defaultConfigPath;
+ if (typeof configPath !== 'string') {
+ throw createFlagError('--config must be a string');
+ }
+ const config = await readConfigFile(log, Path.resolve(configPath));
+
+ let esUrl = flags['es-url'];
+ if (esUrl && typeof esUrl !== 'string') {
+ throw createFlagError('--es-url must be a string');
+ }
+ if (!esUrl && config) {
+ esUrl = Url.format(config.get('servers.elasticsearch'));
+ }
+ if (!esUrl) {
+ throw createFlagError('--es-url or --config must be defined');
+ }
+
+ let kibanaUrl = flags['kibana-url'];
+ if (kibanaUrl && typeof kibanaUrl !== 'string') {
+ throw createFlagError('--kibana-url must be a string');
+ }
+ if (!kibanaUrl && config) {
+ kibanaUrl = Url.format(config.get('servers.kibana'));
+ }
+ if (!kibanaUrl) {
+ throw createFlagError('--kibana-url or --config must be defined');
+ }
+
+ let dir = flags.dir;
+ if (dir && typeof dir !== 'string') {
+ throw createFlagError('--dir must be a string');
+ }
+ if (!dir && config) {
+ dir = Path.resolve(config.get('esArchiver.directory'));
+ }
+ if (!dir) {
+ throw createFlagError('--dir or --config must be defined');
+ }
+
+ const client = new legacyElasticsearch.Client({
+ host: esUrl,
+ log: flags.verbose ? 'trace' : [],
+ });
+ addCleanupTask(() => client.close());
+
+ const esArchiver = new EsArchiver({
+ log,
+ client,
+ dataDir: dir,
+ kibanaUrl,
+ });
+
+ return {
+ esArchiver,
+ };
+ },
+ })
+ .command({
+ name: 'save',
+ usage: 'save [name] [...indices]',
+ description: `
+ archive the [indices ...] into the --dir with [name]
+
+ Example:
+ Save all [logstash-*] indices from http://localhost:9200 to [snapshots/my_test_data] directory
+
+ WARNING: If the [my_test_data] snapshot exists it will be deleted!
+
+ $ node scripts/es_archiver save my_test_data logstash-* --dir snapshots
+ `,
+ flags: {
+ boolean: ['raw'],
+ help: `
+ --raw don't gzip the archives
+ `,
+ },
+ async run({ flags, esArchiver }) {
+ const [name, ...indices] = flags._;
+ if (!name) {
+ throw createFlagError('missing [name] argument');
+ }
+ if (!indices.length) {
+ throw createFlagError('missing [...indices] arguments');
+ }
+
+ const raw = flags.raw;
+ if (typeof raw !== 'boolean') {
+ throw createFlagError('--raw does not take a value');
+ }
+
+ await esArchiver.save(name, indices, { raw });
+ },
+ })
+ .command({
+ name: 'load',
+ usage: 'load [name]',
+ description: `
+ load the archive in --dir with [name]
+
+ Example:
+ Load the [my_test_data] snapshot from the archive directory and elasticsearch instance defined
+ in the [test/functional/config.js] config file
+
+ WARNING: If the indices exist already they will be deleted!
+
+ $ node scripts/es_archiver load my_test_data --config test/functional/config.js
+ `,
+ flags: {
+ boolean: ['use-create'],
+ help: `
+ --use-create use create instead of index for loading documents
+ `,
+ },
+ async run({ flags, esArchiver }) {
+ const [name] = flags._;
+ if (!name) {
+ throw createFlagError('missing [name] argument');
+ }
+ if (flags._.length > 1) {
+ throw createFlagError(`unknown extra arguments: [${flags._.slice(1).join(', ')}]`);
+ }
+
+ const useCreate = flags['use-create'];
+ if (typeof useCreate !== 'boolean') {
+ throw createFlagError('--use-create does not take a value');
+ }
+
+ await esArchiver.load(name, { useCreate });
+ },
+ })
+ .command({
+ name: 'unload',
+ usage: 'unload [name]',
+ description: 'remove indices created by the archive in --dir with [name]',
+ async run({ flags, esArchiver }) {
+ const [name] = flags._;
+ if (!name) {
+ throw createFlagError('missing [name] argument');
+ }
+ if (flags._.length > 1) {
+ throw createFlagError(`unknown extra arguments: [${flags._.slice(1).join(', ')}]`);
+ }
+
+ await esArchiver.unload(name);
+ },
+ })
+ .command({
+ name: 'edit',
+ usage: 'edit [prefix]',
+ description:
+ 'extract the archives under the prefix, wait for edits to be completed, and then recompress the archives',
+ async run({ flags, esArchiver }) {
+ const [prefix] = flags._;
+ if (!prefix) {
+ throw createFlagError('missing [prefix] argument');
+ }
+ if (flags._.length > 1) {
+ throw createFlagError(`unknown extra arguments: [${flags._.slice(1).join(', ')}]`);
+ }
+
+ await esArchiver.edit(prefix, async () => {
+ const rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout,
+ });
+
+ await new Promise((resolveInput) => {
+ rl.question(`Press enter when you're done`, () => {
+ rl.close();
+ resolveInput();
+ });
+ });
+ });
+ },
+ })
+ .command({
+ name: 'empty-kibana-index',
+ description:
+ '[internal] Delete any Kibana indices, and initialize the Kibana index as Kibana would do on startup.',
+ async run({ esArchiver }) {
+ await esArchiver.emptyKibanaIndex();
+ },
+ })
+ .command({
+ name: 'rebuild-all',
+ description: '[internal] read and write all archives in --dir to remove any inconsistencies',
+ async run({ esArchiver }) {
+ await esArchiver.rebuildAll();
+ },
+ })
+ .execute();
+}
diff --git a/src/es_archiver/es_archiver.ts b/packages/kbn-es-archiver/src/es_archiver.ts
similarity index 100%
rename from src/es_archiver/es_archiver.ts
rename to packages/kbn-es-archiver/src/es_archiver.ts
diff --git a/src/es_archiver/index.ts b/packages/kbn-es-archiver/src/index.ts
similarity index 97%
rename from src/es_archiver/index.ts
rename to packages/kbn-es-archiver/src/index.ts
index f7a579a98a42d..c00d457c939ce 100644
--- a/src/es_archiver/index.ts
+++ b/packages/kbn-es-archiver/src/index.ts
@@ -18,3 +18,4 @@
*/
export { EsArchiver } from './es_archiver';
+export * from './cli';
diff --git a/src/es_archiver/lib/__tests__/stats.ts b/packages/kbn-es-archiver/src/lib/__tests__/stats.ts
similarity index 100%
rename from src/es_archiver/lib/__tests__/stats.ts
rename to packages/kbn-es-archiver/src/lib/__tests__/stats.ts
diff --git a/src/es_archiver/lib/archives/__tests__/format.ts b/packages/kbn-es-archiver/src/lib/archives/__tests__/format.ts
similarity index 96%
rename from src/es_archiver/lib/archives/__tests__/format.ts
rename to packages/kbn-es-archiver/src/lib/archives/__tests__/format.ts
index f3829273ea808..044a0e82d9df2 100644
--- a/src/es_archiver/lib/archives/__tests__/format.ts
+++ b/packages/kbn-es-archiver/src/lib/archives/__tests__/format.ts
@@ -22,11 +22,7 @@ import { createGunzip } from 'zlib';
import expect from '@kbn/expect';
-import {
- createListStream,
- createPromiseFromStreams,
- createConcatStream,
-} from '../../../../legacy/utils';
+import { createListStream, createPromiseFromStreams, createConcatStream } from '../../streams';
import { createFormatArchiveStreams } from '../format';
diff --git a/src/es_archiver/lib/archives/__tests__/parse.ts b/packages/kbn-es-archiver/src/lib/archives/__tests__/parse.ts
similarity index 97%
rename from src/es_archiver/lib/archives/__tests__/parse.ts
rename to packages/kbn-es-archiver/src/lib/archives/__tests__/parse.ts
index 50cbdfe06f361..25b8fe46a81fc 100644
--- a/src/es_archiver/lib/archives/__tests__/parse.ts
+++ b/packages/kbn-es-archiver/src/lib/archives/__tests__/parse.ts
@@ -22,11 +22,7 @@ import { createGzip } from 'zlib';
import expect from '@kbn/expect';
-import {
- createConcatStream,
- createListStream,
- createPromiseFromStreams,
-} from '../../../../legacy/utils';
+import { createConcatStream, createListStream, createPromiseFromStreams } from '../../streams';
import { createParseArchiveStreams } from '../parse';
@@ -109,7 +105,7 @@ describe('esArchiver createParseArchiveStreams', () => {
Buffer.from('{"a": 2}\n\n'),
]),
...createParseArchiveStreams({ gzip: false }),
- createConcatStream(),
+ createConcatStream([]),
] as [Readable, ...Writable[]]);
throw new Error('should have failed');
} catch (err) {
@@ -172,7 +168,7 @@ describe('esArchiver createParseArchiveStreams', () => {
await createPromiseFromStreams([
createListStream([Buffer.from('{"a": 1}')]),
...createParseArchiveStreams({ gzip: true }),
- createConcatStream(),
+ createConcatStream([]),
] as [Readable, ...Writable[]]);
throw new Error('should have failed');
} catch (err) {
diff --git a/src/es_archiver/lib/archives/constants.ts b/packages/kbn-es-archiver/src/lib/archives/constants.ts
similarity index 100%
rename from src/es_archiver/lib/archives/constants.ts
rename to packages/kbn-es-archiver/src/lib/archives/constants.ts
diff --git a/src/es_archiver/lib/archives/filenames.ts b/packages/kbn-es-archiver/src/lib/archives/filenames.ts
similarity index 100%
rename from src/es_archiver/lib/archives/filenames.ts
rename to packages/kbn-es-archiver/src/lib/archives/filenames.ts
diff --git a/src/es_archiver/lib/archives/format.ts b/packages/kbn-es-archiver/src/lib/archives/format.ts
similarity index 94%
rename from src/es_archiver/lib/archives/format.ts
rename to packages/kbn-es-archiver/src/lib/archives/format.ts
index ac18147ad6948..3cd698c3f82c3 100644
--- a/src/es_archiver/lib/archives/format.ts
+++ b/packages/kbn-es-archiver/src/lib/archives/format.ts
@@ -21,7 +21,7 @@ import { createGzip, Z_BEST_COMPRESSION } from 'zlib';
import { PassThrough } from 'stream';
import stringify from 'json-stable-stringify';
-import { createMapStream, createIntersperseStream } from '../../../legacy/utils';
+import { createMapStream, createIntersperseStream } from '../streams';
import { RECORD_SEPARATOR } from './constants';
export function createFormatArchiveStreams({ gzip = false }: { gzip?: boolean } = {}) {
diff --git a/src/es_archiver/lib/archives/index.ts b/packages/kbn-es-archiver/src/lib/archives/index.ts
similarity index 100%
rename from src/es_archiver/lib/archives/index.ts
rename to packages/kbn-es-archiver/src/lib/archives/index.ts
diff --git a/src/es_archiver/lib/archives/parse.ts b/packages/kbn-es-archiver/src/lib/archives/parse.ts
similarity index 87%
rename from src/es_archiver/lib/archives/parse.ts
rename to packages/kbn-es-archiver/src/lib/archives/parse.ts
index 1d650815f9358..9236a618aa01a 100644
--- a/src/es_archiver/lib/archives/parse.ts
+++ b/packages/kbn-es-archiver/src/lib/archives/parse.ts
@@ -19,8 +19,12 @@
import { createGunzip } from 'zlib';
import { PassThrough } from 'stream';
-import { createFilterStream } from '../../../legacy/utils/streams/filter_stream';
-import { createSplitStream, createReplaceStream, createMapStream } from '../../../legacy/utils';
+import {
+ createFilterStream,
+ createSplitStream,
+ createReplaceStream,
+ createMapStream,
+} from '../streams';
import { RECORD_SEPARATOR } from './constants';
diff --git a/src/es_archiver/lib/directory.ts b/packages/kbn-es-archiver/src/lib/directory.ts
similarity index 100%
rename from src/es_archiver/lib/directory.ts
rename to packages/kbn-es-archiver/src/lib/directory.ts
diff --git a/src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.ts b/packages/kbn-es-archiver/src/lib/docs/__tests__/generate_doc_records_stream.ts
similarity index 97%
rename from src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.ts
rename to packages/kbn-es-archiver/src/lib/docs/__tests__/generate_doc_records_stream.ts
index 03599cdc9fbcf..2214f7ae9f2ea 100644
--- a/src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.ts
+++ b/packages/kbn-es-archiver/src/lib/docs/__tests__/generate_doc_records_stream.ts
@@ -21,11 +21,7 @@ import sinon from 'sinon';
import expect from '@kbn/expect';
import { delay } from 'bluebird';
-import {
- createListStream,
- createPromiseFromStreams,
- createConcatStream,
-} from '../../../../legacy/utils';
+import { createListStream, createPromiseFromStreams, createConcatStream } from '../../streams';
import { createGenerateDocRecordsStream } from '../generate_doc_records_stream';
import { Progress } from '../../progress';
diff --git a/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.ts b/packages/kbn-es-archiver/src/lib/docs/__tests__/index_doc_records_stream.ts
similarity index 99%
rename from src/es_archiver/lib/docs/__tests__/index_doc_records_stream.ts
rename to packages/kbn-es-archiver/src/lib/docs/__tests__/index_doc_records_stream.ts
index 35b068a691090..2b8eac5c27122 100644
--- a/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.ts
+++ b/packages/kbn-es-archiver/src/lib/docs/__tests__/index_doc_records_stream.ts
@@ -20,7 +20,7 @@
import expect from '@kbn/expect';
import { delay } from 'bluebird';
-import { createListStream, createPromiseFromStreams } from '../../../../legacy/utils';
+import { createListStream, createPromiseFromStreams } from '../../streams';
import { Progress } from '../../progress';
import { createIndexDocRecordsStream } from '../index_doc_records_stream';
diff --git a/src/es_archiver/lib/docs/__tests__/stubs.ts b/packages/kbn-es-archiver/src/lib/docs/__tests__/stubs.ts
similarity index 100%
rename from src/es_archiver/lib/docs/__tests__/stubs.ts
rename to packages/kbn-es-archiver/src/lib/docs/__tests__/stubs.ts
diff --git a/src/es_archiver/lib/docs/generate_doc_records_stream.ts b/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.ts
similarity index 100%
rename from src/es_archiver/lib/docs/generate_doc_records_stream.ts
rename to packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.ts
diff --git a/src/es_archiver/lib/docs/index.ts b/packages/kbn-es-archiver/src/lib/docs/index.ts
similarity index 100%
rename from src/es_archiver/lib/docs/index.ts
rename to packages/kbn-es-archiver/src/lib/docs/index.ts
diff --git a/src/es_archiver/lib/docs/index_doc_records_stream.ts b/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.ts
similarity index 100%
rename from src/es_archiver/lib/docs/index_doc_records_stream.ts
rename to packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.ts
diff --git a/src/es_archiver/lib/index.ts b/packages/kbn-es-archiver/src/lib/index.ts
similarity index 100%
rename from src/es_archiver/lib/index.ts
rename to packages/kbn-es-archiver/src/lib/index.ts
diff --git a/src/es_archiver/lib/indices/__tests__/create_index_stream.ts b/packages/kbn-es-archiver/src/lib/indices/__tests__/create_index_stream.ts
similarity index 98%
rename from src/es_archiver/lib/indices/__tests__/create_index_stream.ts
rename to packages/kbn-es-archiver/src/lib/indices/__tests__/create_index_stream.ts
index c90497eded88c..27c28b2229aec 100644
--- a/src/es_archiver/lib/indices/__tests__/create_index_stream.ts
+++ b/packages/kbn-es-archiver/src/lib/indices/__tests__/create_index_stream.ts
@@ -21,11 +21,7 @@ import expect from '@kbn/expect';
import sinon from 'sinon';
import Chance from 'chance';
-import {
- createPromiseFromStreams,
- createConcatStream,
- createListStream,
-} from '../../../../legacy/utils';
+import { createPromiseFromStreams, createConcatStream, createListStream } from '../../streams';
import { createCreateIndexStream } from '../create_index_stream';
diff --git a/src/es_archiver/lib/indices/__tests__/delete_index_stream.ts b/packages/kbn-es-archiver/src/lib/indices/__tests__/delete_index_stream.ts
similarity index 99%
rename from src/es_archiver/lib/indices/__tests__/delete_index_stream.ts
rename to packages/kbn-es-archiver/src/lib/indices/__tests__/delete_index_stream.ts
index 1c989ba158a29..551b744415c83 100644
--- a/src/es_archiver/lib/indices/__tests__/delete_index_stream.ts
+++ b/packages/kbn-es-archiver/src/lib/indices/__tests__/delete_index_stream.ts
@@ -19,7 +19,7 @@
import sinon from 'sinon';
-import { createListStream, createPromiseFromStreams } from '../../../../legacy/utils';
+import { createListStream, createPromiseFromStreams } from '../../streams';
import { createDeleteIndexStream } from '../delete_index_stream';
diff --git a/src/es_archiver/lib/indices/__tests__/generate_index_records_stream.ts b/packages/kbn-es-archiver/src/lib/indices/__tests__/generate_index_records_stream.ts
similarity index 97%
rename from src/es_archiver/lib/indices/__tests__/generate_index_records_stream.ts
rename to packages/kbn-es-archiver/src/lib/indices/__tests__/generate_index_records_stream.ts
index fe927483da7b0..cb3746c015dad 100644
--- a/src/es_archiver/lib/indices/__tests__/generate_index_records_stream.ts
+++ b/packages/kbn-es-archiver/src/lib/indices/__tests__/generate_index_records_stream.ts
@@ -20,11 +20,7 @@
import sinon from 'sinon';
import expect from '@kbn/expect';
-import {
- createListStream,
- createPromiseFromStreams,
- createConcatStream,
-} from '../../../../legacy/utils';
+import { createListStream, createPromiseFromStreams, createConcatStream } from '../../streams';
import { createStubClient, createStubStats } from './stubs';
diff --git a/src/es_archiver/lib/indices/__tests__/stubs.ts b/packages/kbn-es-archiver/src/lib/indices/__tests__/stubs.ts
similarity index 100%
rename from src/es_archiver/lib/indices/__tests__/stubs.ts
rename to packages/kbn-es-archiver/src/lib/indices/__tests__/stubs.ts
diff --git a/src/es_archiver/lib/indices/create_index_stream.ts b/packages/kbn-es-archiver/src/lib/indices/create_index_stream.ts
similarity index 100%
rename from src/es_archiver/lib/indices/create_index_stream.ts
rename to packages/kbn-es-archiver/src/lib/indices/create_index_stream.ts
diff --git a/src/es_archiver/lib/indices/delete_index.ts b/packages/kbn-es-archiver/src/lib/indices/delete_index.ts
similarity index 100%
rename from src/es_archiver/lib/indices/delete_index.ts
rename to packages/kbn-es-archiver/src/lib/indices/delete_index.ts
diff --git a/src/es_archiver/lib/indices/delete_index_stream.ts b/packages/kbn-es-archiver/src/lib/indices/delete_index_stream.ts
similarity index 100%
rename from src/es_archiver/lib/indices/delete_index_stream.ts
rename to packages/kbn-es-archiver/src/lib/indices/delete_index_stream.ts
diff --git a/src/es_archiver/lib/indices/generate_index_records_stream.ts b/packages/kbn-es-archiver/src/lib/indices/generate_index_records_stream.ts
similarity index 100%
rename from src/es_archiver/lib/indices/generate_index_records_stream.ts
rename to packages/kbn-es-archiver/src/lib/indices/generate_index_records_stream.ts
diff --git a/src/es_archiver/lib/indices/index.ts b/packages/kbn-es-archiver/src/lib/indices/index.ts
similarity index 100%
rename from src/es_archiver/lib/indices/index.ts
rename to packages/kbn-es-archiver/src/lib/indices/index.ts
diff --git a/src/es_archiver/lib/indices/kibana_index.ts b/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts
similarity index 98%
rename from src/es_archiver/lib/indices/kibana_index.ts
rename to packages/kbn-es-archiver/src/lib/indices/kibana_index.ts
index 1867f24d6f9ed..79e758f09ccf0 100644
--- a/src/es_archiver/lib/indices/kibana_index.ts
+++ b/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts
@@ -75,7 +75,7 @@ export async function migrateKibanaIndex({
},
} as any);
- return await kbnClient.savedObjects.migrate();
+ await kbnClient.savedObjects.migrate();
}
/**
diff --git a/src/es_archiver/lib/progress.ts b/packages/kbn-es-archiver/src/lib/progress.ts
similarity index 100%
rename from src/es_archiver/lib/progress.ts
rename to packages/kbn-es-archiver/src/lib/progress.ts
diff --git a/src/es_archiver/lib/records/__tests__/filter_records_stream.ts b/packages/kbn-es-archiver/src/lib/records/__tests__/filter_records_stream.ts
similarity index 95%
rename from src/es_archiver/lib/records/__tests__/filter_records_stream.ts
rename to packages/kbn-es-archiver/src/lib/records/__tests__/filter_records_stream.ts
index f4f9f32e239ea..b23ff2e4e52ac 100644
--- a/src/es_archiver/lib/records/__tests__/filter_records_stream.ts
+++ b/packages/kbn-es-archiver/src/lib/records/__tests__/filter_records_stream.ts
@@ -20,11 +20,7 @@
import Chance from 'chance';
import expect from '@kbn/expect';
-import {
- createListStream,
- createPromiseFromStreams,
- createConcatStream,
-} from '../../../../legacy/utils';
+import { createListStream, createPromiseFromStreams, createConcatStream } from '../../streams';
import { createFilterRecordsStream } from '../filter_records_stream';
diff --git a/src/es_archiver/lib/records/filter_records_stream.ts b/packages/kbn-es-archiver/src/lib/records/filter_records_stream.ts
similarity index 100%
rename from src/es_archiver/lib/records/filter_records_stream.ts
rename to packages/kbn-es-archiver/src/lib/records/filter_records_stream.ts
diff --git a/src/es_archiver/lib/records/index.ts b/packages/kbn-es-archiver/src/lib/records/index.ts
similarity index 100%
rename from src/es_archiver/lib/records/index.ts
rename to packages/kbn-es-archiver/src/lib/records/index.ts
diff --git a/src/es_archiver/lib/stats.ts b/packages/kbn-es-archiver/src/lib/stats.ts
similarity index 100%
rename from src/es_archiver/lib/stats.ts
rename to packages/kbn-es-archiver/src/lib/stats.ts
diff --git a/packages/kbn-es-archiver/src/lib/streams.ts b/packages/kbn-es-archiver/src/lib/streams.ts
new file mode 100644
index 0000000000000..a90afbe0c4d25
--- /dev/null
+++ b/packages/kbn-es-archiver/src/lib/streams.ts
@@ -0,0 +1,20 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export * from '../../../../src/legacy/utils/streams';
diff --git a/packages/kbn-es-archiver/tsconfig.json b/packages/kbn-es-archiver/tsconfig.json
new file mode 100644
index 0000000000000..6ffa64d91fba0
--- /dev/null
+++ b/packages/kbn-es-archiver/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./target",
+ "declaration": true,
+ "sourceMap": true,
+ "target": "ES2019"
+ },
+ "include": [
+ "src/**/*"
+ ]
+}
diff --git a/packages/kbn-es-archiver/yarn.lock b/packages/kbn-es-archiver/yarn.lock
new file mode 120000
index 0000000000000..3f82ebc9cdbae
--- /dev/null
+++ b/packages/kbn-es-archiver/yarn.lock
@@ -0,0 +1 @@
+../../yarn.lock
\ No newline at end of file
diff --git a/packages/kbn-test/src/functional_test_runner/index.ts b/packages/kbn-test/src/functional_test_runner/index.ts
index cf65ceb51df8e..b13c311350ff6 100644
--- a/packages/kbn-test/src/functional_test_runner/index.ts
+++ b/packages/kbn-test/src/functional_test_runner/index.ts
@@ -18,6 +18,6 @@
*/
export { FunctionalTestRunner } from './functional_test_runner';
-export { readConfigFile } from './lib';
+export { readConfigFile, Config } from './lib';
export { runFtrCli } from './cli';
export * from './lib/docker_servers';
diff --git a/scripts/es_archiver.js b/scripts/es_archiver.js
index faa8d9131240d..88674ce9eafb3 100755
--- a/scripts/es_archiver.js
+++ b/scripts/es_archiver.js
@@ -18,4 +18,4 @@
*/
require('../src/setup_node_env');
-require('../src/es_archiver/cli');
+require('@kbn/es-archiver').runCli();
diff --git a/src/dev/build/tasks/copy_source_task.js b/src/dev/build/tasks/copy_source_task.js
index e34f05bd6cfff..52809449ba338 100644
--- a/src/dev/build/tasks/copy_source_task.js
+++ b/src/dev/build/tasks/copy_source_task.js
@@ -37,7 +37,6 @@ export const CopySourceTask = {
'!src/legacy/core_plugins/console/public/tests/**',
'!src/cli/cluster/**',
'!src/cli/repl/**',
- '!src/es_archiver/**',
'!src/functional_test_runner/**',
'!src/dev/**',
'typings/**',
diff --git a/src/dev/code_coverage/ingest_coverage/team_assignment/ingestion_pipeline_painless.json b/src/dev/code_coverage/ingest_coverage/team_assignment/ingestion_pipeline_painless.json
index 18e88b47ec887..30e78635ec2e9 100644
--- a/src/dev/code_coverage/ingest_coverage/team_assignment/ingestion_pipeline_painless.json
+++ b/src/dev/code_coverage/ingest_coverage/team_assignment/ingestion_pipeline_painless.json
@@ -1 +1 @@
-{"description":"Kibana code coverage team assignments","processors":[{"script":{"lang":"painless","source":"\n String path = ctx.coveredFilePath; \n if (path.indexOf('src/legacy/core_plugins/kibana/') == 0) {\n\n if (path.indexOf('src/legacy/core_plugins/kibana/common/utils') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/migrations') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/dashboard/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/dev_tools/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/discover/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/home') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/home/np_ready/') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/local_application_service/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/management/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/lib') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/lib/management/saved_objects') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/routes/api/management/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/routes/api/import/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/routes/api/export/') == 0) ctx.team = 'kibana-platform';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('src/legacy/core_plugins/') == 0) {\n\n if (path.indexOf('src/legacy/core_plugins/apm_oss/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('src/legacy/core_plugins/console_legacy') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/elasticsearch') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/core_plugins/embeddable_api/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/input_control_vis') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/interpreter/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/kibana_react/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/newsfeed') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/legacy/core_plugins/region_map') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/legacy/core_plugins/status_page/public') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/testbed') == 0) ctx.team = 'kibana-platform';\n // else if (path.indexOf('src/legacy/core_plugins/tests_bundle/') == 0) ctx.team = 'kibana-platform';\n \n else if (path.indexOf('src/legacy/core_plugins/tile_map') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/legacy/core_plugins/timelion') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/ui_metric/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('src/legacy/core_plugins/vis_type_tagcloud') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/vis_type_vega') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/vis_type_vislib/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/visualizations/') == 0) ctx.team = 'kibana-app-arch';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('src/legacy/server/') == 0) {\n\n if (path.indexOf('src/legacy/server/config/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/http/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/i18n/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('src/legacy/server/index_patterns/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/server/keystore/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/legacy/server/logging/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/pid/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/legacy/server/sample_data/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/server/sass/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/legacy/server/saved_objects/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/status/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/url_shortening/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/server/utils/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/legacy/server/warnings/') == 0) ctx.team = 'kibana-operations';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('src/legacy/ui') == 0) {\n\n if (path.indexOf('src/legacy/ui/public/field_editor') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/ui/public/timefilter') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/ui/public/management') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/ui/public/state_management') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/ui/public/new_platform') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/plugin_discovery') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/chrome') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/notify') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/documentation_links') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/autoload') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/capabilities') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('src/legacy/ui/public/apm') == 0) ctx.team = 'apm-ui';\n\n } else if (path.indexOf('src/plugins/') == 0) {\n\n if (path.indexOf('src/plugins/advanced_settings/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/apm_oss/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('src/plugins/bfetch/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/charts/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/charts/public/static/color_maps') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/console/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('src/plugins/dashboard/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/data/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/dev_tools/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('src/plugins/discover/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/embeddable/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/es_ui_shared/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('src/plugins/expressions/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/home/public') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/plugins/home/server/tutorials') == 0) ctx.team = 'observability';\n else if (path.indexOf('src/plugins/home/server/services/') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/plugins/home/') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/plugins/index_pattern_management/public/service') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/index_pattern_management/public') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/input_control_vis/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/inspector/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/kibana_legacy/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/kibana_react/public/code_editor') == 0) ctx.team = 'kibana-canvas';\n else if (path.indexOf('src/plugins/kibana_react/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/kibana_utils/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/management/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/kibana_usage_collection/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('src/plugins/legacy_export/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/plugins/maps_legacy/') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/plugins/region_map/') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/plugins/tile_map/') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/plugins/timelion') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/navigation/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/newsfeed') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/plugins/saved_objects_management/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/plugins/saved_objects/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/share/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/status_page/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/plugins/telemetry') == 0) ctx.team = 'pulse';\n else if (path.indexOf('src/plugins/testbed/server/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/plugins/ui_actions/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/usage_collection/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('src/plugins/vis_default_editor') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/vis_type') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/visualizations/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/visualize/') == 0) ctx.team = 'kibana-app';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('x-pack/legacy/') == 0) {\n\n if (path.indexOf('x-pack/legacy/plugins/actions/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/legacy/plugins/alerting/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/legacy/plugins/apm/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('x-pack/legacy/plugins/beats_management/') == 0) ctx.team = 'beats';\n else if (path.indexOf('x-pack/legacy/plugins/canvas/') == 0) ctx.team = 'kibana-canvas';\n else if (path.indexOf('x-pack/legacy/plugins/cross_cluster_replication/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/dashboard_mode/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/legacy/plugins/encrypted_saved_objects/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/legacy/plugins/index_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/infra/') == 0) ctx.team = 'logs-metrics-ui';\n else if (path.indexOf('x-pack/legacy/plugins/ingest_manager/') == 0) ctx.team = 'ingest-management';\n else if (path.indexOf('x-pack/legacy/plugins/license_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/maps/') == 0) ctx.team = 'kibana-gis';\n else if (path.indexOf('x-pack/legacy/plugins/ml/') == 0) ctx.team = 'ml-ui';\n else if (path.indexOf('x-pack/legacy/plugins/monitoring/') == 0) ctx.team = 'stack-monitoring-ui';\n else if (path.indexOf('x-pack/legacy/plugins/reporting') == 0) ctx.team = 'kibana-reporting';\n else if (path.indexOf('x-pack/legacy/plugins/rollup/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/security/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/legacy/plugins/siem/') == 0) ctx.team = 'siem';\n else if (path.indexOf('x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules') == 0) ctx.team = 'security-intelligence-analytics';\n else if (path.indexOf('x-pack/legacy/plugins/snapshot_restore/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/spaces/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/legacy/plugins/task_manager') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/legacy/plugins/triggers_actions_ui/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/legacy/plugins/upgrade_assistant/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/uptime') == 0) ctx.team = 'uptime';\n else if (path.indexOf('x-pack/legacy/plugins/xpack_main/server/') == 0) ctx.team = 'kibana-platform';\n\n else if (path.indexOf('x-pack/legacy/server/lib/create_router/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/server/lib/check_license/') == 0) ctx.team = 'es-ui'; \n else if (path.indexOf('x-pack/legacy/server/lib/') == 0) ctx.team = 'kibana-platform'; \n else ctx.team = 'unknown';\n\n } else if (path.indexOf('x-pack/plugins/') == 0) {\n\n if (path.indexOf('x-pack/plugins/actions/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/advanced_ui_actions/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/alerts') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/alerting_builtins') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/apm/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('x-pack/plugins/beats_management/') == 0) ctx.team = 'beats';\n else if (path.indexOf('x-pack/plugins/canvas/') == 0) ctx.team = 'kibana-canvas';\n else if (path.indexOf('x-pack/plugins/case') == 0) ctx.team = 'siem';\n else if (path.indexOf('x-pack/plugins/cloud/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('x-pack/plugins/code/') == 0) ctx.team = 'code';\n else if (path.indexOf('x-pack/plugins/console_extensions/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/cross_cluster_replication/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/dashboard_enhanced') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/dashboard_mode') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/discover_enhanced') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/embeddable_enhanced') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/data_enhanced/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/drilldowns/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/encrypted_saved_objects/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/plugins/endpoint/') == 0) ctx.team = 'endpoint-app-team';\n else if (path.indexOf('x-pack/plugins/es_ui_shared/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/event_log/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/features/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('x-pack/plugins/file_upload') == 0) ctx.team = 'kibana-gis';\n else if (path.indexOf('x-pack/plugins/global_search') == 0) ctx.team = 'kibana-platform';\n \n else if (path.indexOf('x-pack/plugins/graph/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/grokdebugger/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/index_lifecycle_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/index_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/infra/') == 0) ctx.team = 'logs-metrics-ui';\n else if (path.indexOf('x-pack/plugins/ingest_manager/') == 0) ctx.team = 'ingest-management';\n else if (path.indexOf('x-pack/plugins/ingest_pipelines/') == 0) ctx.team = 'es-ui';\n \n else if (path.indexOf('x-pack/plugins/lens/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/license_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/licensing/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('x-pack/plugins/lists/') == 0) ctx.team = 'siem';\n else if (path.indexOf('x-pack/plugins/logstash') == 0) ctx.team = 'logstash';\n else if (path.indexOf('x-pack/plugins/maps/') == 0) ctx.team = 'kibana-gis';\n else if (path.indexOf('x-pack/plugins/maps_legacy_licensing') == 0) ctx.team = 'maps';\n else if (path.indexOf('x-pack/plugins/ml/') == 0) ctx.team = 'ml-ui';\n else if (path.indexOf('x-pack/plugins/monitoring') == 0) ctx.team = 'stack-monitoring-ui';\n else if (path.indexOf('x-pack/plugins/observability/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('x-pack/plugins/oss_telemetry/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('x-pack/plugins/painless_lab/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/remote_clusters/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/reporting') == 0) ctx.team = 'kibana-reporting';\n else if (path.indexOf('x-pack/plugins/rollup/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/searchprofiler/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/security/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/plugins/security_solution/') == 0) ctx.team = 'siem';\n \n else if (path.indexOf('x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules') == 0) ctx.team = 'security-intelligence-analytics';\n else if (path.indexOf('x-pack/plugins/siem/') == 0) ctx.team = 'siem';\n else if (path.indexOf('x-pack/plugins/snapshot_restore/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/spaces/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/plugins/task_manager/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/telemetry_collection_xpack/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('x-pack/plugins/transform/') == 0) ctx.team = 'ml-ui';\n else if (path.indexOf('x-pack/plugins/translations/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('x-pack/plugins/triggers_actions_ui/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/upgrade_assistant/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/ui_actions_enhanced') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/uptime') == 0) ctx.team = 'uptime';\n \n else if (path.indexOf('x-pack/plugins/watcher/') == 0) ctx.team = 'es-ui';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('packages') == 0) {\n\n if (path.indexOf('packages/kbn-analytics/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('packages/kbn-babel') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-config-schema/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('packages/elastic-datemath') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('packages/kbn-dev-utils') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-es/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-eslint') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-expect') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-i18n/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('packages/kbn-interpreter/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('packages/kbn-optimizer/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-pm/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-test/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-test-subj-selector/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-ui-framework/') == 0) ctx.team = 'kibana-design';\n else if (path.indexOf('packages/kbn-ui-shared-deps/') == 0) ctx.team = 'kibana-operations';\n else ctx.team = 'unknown';\n\n } else {\n\n if (path.indexOf('config/kibana.yml') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/apm.js') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('src/core/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/core/public/i18n/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('src/core/server/csp/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('src/dev/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/dev/i18n/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('src/dev/run_check_published_api_changes.ts') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/es_archiver/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/optimize/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/setup_node_env/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/test_utils/') == 0) ctx.team = 'kibana-operations'; \n else ctx.team = 'unknown';\n }"}}]}
+{"description":"Kibana code coverage team assignments","processors":[{"script":{"lang":"painless","source":"\n String path = ctx.coveredFilePath; \n if (path.indexOf('src/legacy/core_plugins/kibana/') == 0) {\n\n if (path.indexOf('src/legacy/core_plugins/kibana/common/utils') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/migrations') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/dashboard/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/dev_tools/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/discover/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/home') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/home/np_ready/') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/local_application_service/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/management/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/lib') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/lib/management/saved_objects') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/routes/api/management/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/routes/api/import/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/routes/api/export/') == 0) ctx.team = 'kibana-platform';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('src/legacy/core_plugins/') == 0) {\n\n if (path.indexOf('src/legacy/core_plugins/apm_oss/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('src/legacy/core_plugins/console_legacy') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/elasticsearch') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/core_plugins/embeddable_api/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/input_control_vis') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/interpreter/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/kibana_react/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/newsfeed') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/legacy/core_plugins/region_map') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/legacy/core_plugins/status_page/public') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/testbed') == 0) ctx.team = 'kibana-platform';\n // else if (path.indexOf('src/legacy/core_plugins/tests_bundle/') == 0) ctx.team = 'kibana-platform';\n \n else if (path.indexOf('src/legacy/core_plugins/tile_map') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/legacy/core_plugins/timelion') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/ui_metric/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('src/legacy/core_plugins/vis_type_tagcloud') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/vis_type_vega') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/vis_type_vislib/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/visualizations/') == 0) ctx.team = 'kibana-app-arch';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('src/legacy/server/') == 0) {\n\n if (path.indexOf('src/legacy/server/config/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/http/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/i18n/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('src/legacy/server/index_patterns/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/server/keystore/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/legacy/server/logging/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/pid/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/legacy/server/sample_data/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/server/sass/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/legacy/server/saved_objects/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/status/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/url_shortening/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/server/utils/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/legacy/server/warnings/') == 0) ctx.team = 'kibana-operations';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('src/legacy/ui') == 0) {\n\n if (path.indexOf('src/legacy/ui/public/field_editor') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/ui/public/timefilter') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/ui/public/management') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/ui/public/state_management') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/ui/public/new_platform') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/plugin_discovery') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/chrome') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/notify') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/documentation_links') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/autoload') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/capabilities') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('src/legacy/ui/public/apm') == 0) ctx.team = 'apm-ui';\n\n } else if (path.indexOf('src/plugins/') == 0) {\n\n if (path.indexOf('src/plugins/advanced_settings/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/apm_oss/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('src/plugins/bfetch/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/charts/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/charts/public/static/color_maps') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/console/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('src/plugins/dashboard/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/data/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/dev_tools/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('src/plugins/discover/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/embeddable/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/es_ui_shared/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('src/plugins/expressions/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/home/public') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/plugins/home/server/tutorials') == 0) ctx.team = 'observability';\n else if (path.indexOf('src/plugins/home/server/services/') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/plugins/home/') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/plugins/index_pattern_management/public/service') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/index_pattern_management/public') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/input_control_vis/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/inspector/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/kibana_legacy/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/kibana_react/public/code_editor') == 0) ctx.team = 'kibana-canvas';\n else if (path.indexOf('src/plugins/kibana_react/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/kibana_utils/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/management/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/kibana_usage_collection/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('src/plugins/legacy_export/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/plugins/maps_legacy/') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/plugins/region_map/') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/plugins/tile_map/') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/plugins/timelion') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/navigation/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/newsfeed') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/plugins/saved_objects_management/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/plugins/saved_objects/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/share/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/status_page/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/plugins/telemetry') == 0) ctx.team = 'pulse';\n else if (path.indexOf('src/plugins/testbed/server/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/plugins/ui_actions/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/usage_collection/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('src/plugins/vis_default_editor') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/vis_type') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/visualizations/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/visualize/') == 0) ctx.team = 'kibana-app';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('x-pack/legacy/') == 0) {\n\n if (path.indexOf('x-pack/legacy/plugins/actions/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/legacy/plugins/alerting/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/legacy/plugins/apm/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('x-pack/legacy/plugins/beats_management/') == 0) ctx.team = 'beats';\n else if (path.indexOf('x-pack/legacy/plugins/canvas/') == 0) ctx.team = 'kibana-canvas';\n else if (path.indexOf('x-pack/legacy/plugins/cross_cluster_replication/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/dashboard_mode/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/legacy/plugins/encrypted_saved_objects/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/legacy/plugins/index_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/infra/') == 0) ctx.team = 'logs-metrics-ui';\n else if (path.indexOf('x-pack/legacy/plugins/ingest_manager/') == 0) ctx.team = 'ingest-management';\n else if (path.indexOf('x-pack/legacy/plugins/license_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/maps/') == 0) ctx.team = 'kibana-gis';\n else if (path.indexOf('x-pack/legacy/plugins/ml/') == 0) ctx.team = 'ml-ui';\n else if (path.indexOf('x-pack/legacy/plugins/monitoring/') == 0) ctx.team = 'stack-monitoring-ui';\n else if (path.indexOf('x-pack/legacy/plugins/reporting') == 0) ctx.team = 'kibana-reporting';\n else if (path.indexOf('x-pack/legacy/plugins/rollup/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/security/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/legacy/plugins/siem/') == 0) ctx.team = 'siem';\n else if (path.indexOf('x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules') == 0) ctx.team = 'security-intelligence-analytics';\n else if (path.indexOf('x-pack/legacy/plugins/snapshot_restore/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/spaces/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/legacy/plugins/task_manager') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/legacy/plugins/triggers_actions_ui/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/legacy/plugins/upgrade_assistant/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/uptime') == 0) ctx.team = 'uptime';\n else if (path.indexOf('x-pack/legacy/plugins/xpack_main/server/') == 0) ctx.team = 'kibana-platform';\n\n else if (path.indexOf('x-pack/legacy/server/lib/create_router/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/server/lib/check_license/') == 0) ctx.team = 'es-ui'; \n else if (path.indexOf('x-pack/legacy/server/lib/') == 0) ctx.team = 'kibana-platform'; \n else ctx.team = 'unknown';\n\n } else if (path.indexOf('x-pack/plugins/') == 0) {\n\n if (path.indexOf('x-pack/plugins/actions/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/advanced_ui_actions/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/alerts') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/alerting_builtins') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/apm/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('x-pack/plugins/beats_management/') == 0) ctx.team = 'beats';\n else if (path.indexOf('x-pack/plugins/canvas/') == 0) ctx.team = 'kibana-canvas';\n else if (path.indexOf('x-pack/plugins/case') == 0) ctx.team = 'siem';\n else if (path.indexOf('x-pack/plugins/cloud/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('x-pack/plugins/code/') == 0) ctx.team = 'code';\n else if (path.indexOf('x-pack/plugins/console_extensions/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/cross_cluster_replication/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/dashboard_enhanced') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/dashboard_mode') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/discover_enhanced') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/embeddable_enhanced') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/data_enhanced/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/drilldowns/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/encrypted_saved_objects/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/plugins/endpoint/') == 0) ctx.team = 'endpoint-app-team';\n else if (path.indexOf('x-pack/plugins/es_ui_shared/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/event_log/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/features/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('x-pack/plugins/file_upload') == 0) ctx.team = 'kibana-gis';\n else if (path.indexOf('x-pack/plugins/global_search') == 0) ctx.team = 'kibana-platform';\n \n else if (path.indexOf('x-pack/plugins/graph/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/grokdebugger/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/index_lifecycle_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/index_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/infra/') == 0) ctx.team = 'logs-metrics-ui';\n else if (path.indexOf('x-pack/plugins/ingest_manager/') == 0) ctx.team = 'ingest-management';\n else if (path.indexOf('x-pack/plugins/ingest_pipelines/') == 0) ctx.team = 'es-ui';\n \n else if (path.indexOf('x-pack/plugins/lens/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/license_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/licensing/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('x-pack/plugins/lists/') == 0) ctx.team = 'siem';\n else if (path.indexOf('x-pack/plugins/logstash') == 0) ctx.team = 'logstash';\n else if (path.indexOf('x-pack/plugins/maps/') == 0) ctx.team = 'kibana-gis';\n else if (path.indexOf('x-pack/plugins/maps_legacy_licensing') == 0) ctx.team = 'maps';\n else if (path.indexOf('x-pack/plugins/ml/') == 0) ctx.team = 'ml-ui';\n else if (path.indexOf('x-pack/plugins/monitoring') == 0) ctx.team = 'stack-monitoring-ui';\n else if (path.indexOf('x-pack/plugins/observability/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('x-pack/plugins/oss_telemetry/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('x-pack/plugins/painless_lab/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/remote_clusters/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/reporting') == 0) ctx.team = 'kibana-reporting';\n else if (path.indexOf('x-pack/plugins/rollup/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/searchprofiler/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/security/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/plugins/security_solution/') == 0) ctx.team = 'siem';\n \n else if (path.indexOf('x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules') == 0) ctx.team = 'security-intelligence-analytics';\n else if (path.indexOf('x-pack/plugins/siem/') == 0) ctx.team = 'siem';\n else if (path.indexOf('x-pack/plugins/snapshot_restore/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/spaces/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/plugins/task_manager/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/telemetry_collection_xpack/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('x-pack/plugins/transform/') == 0) ctx.team = 'ml-ui';\n else if (path.indexOf('x-pack/plugins/translations/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('x-pack/plugins/triggers_actions_ui/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/upgrade_assistant/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/ui_actions_enhanced') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/uptime') == 0) ctx.team = 'uptime';\n \n else if (path.indexOf('x-pack/plugins/watcher/') == 0) ctx.team = 'es-ui';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('packages') == 0) {\n\n if (path.indexOf('packages/kbn-analytics/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('packages/kbn-babel') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-config-schema/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('packages/elastic-datemath') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('packages/kbn-dev-utils') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-es/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-eslint') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-expect') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-i18n/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('packages/kbn-interpreter/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('packages/kbn-optimizer/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-pm/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-test/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-test-subj-selector/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-ui-framework/') == 0) ctx.team = 'kibana-design';\n else if (path.indexOf('packages/kbn-ui-shared-deps/') == 0) ctx.team = 'kibana-operations';\n else ctx.team = 'unknown';\n\n } else {\n\n if (path.indexOf('config/kibana.yml') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/apm.js') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('src/core/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/core/public/i18n/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('src/core/server/csp/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('src/dev/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/dev/i18n/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('src/dev/run_check_published_api_changes.ts') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('packages/kbn-es-archiver/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/optimize/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/setup_node_env/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/test_utils/') == 0) ctx.team = 'kibana-operations'; \n else ctx.team = 'unknown';\n }"}}]}
diff --git a/src/es_archiver/cli.ts b/src/es_archiver/cli.ts
deleted file mode 100644
index 85e10b31a87ee..0000000000000
--- a/src/es_archiver/cli.ts
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-/** ***********************************************************
- *
- * Run `node scripts/es_archiver --help` for usage information
- *
- *************************************************************/
-
-import { resolve } from 'path';
-import { readFileSync } from 'fs';
-import { format as formatUrl } from 'url';
-import readline from 'readline';
-import { Command } from 'commander';
-import * as legacyElasticsearch from 'elasticsearch';
-
-import { ToolingLog } from '@kbn/dev-utils';
-import { readConfigFile } from '@kbn/test';
-
-import { EsArchiver } from './es_archiver';
-
-const cmd = new Command('node scripts/es_archiver');
-
-const resolveConfigPath = (v: string) => resolve(process.cwd(), v);
-const defaultConfigPath = resolveConfigPath('test/functional/config.js');
-
-cmd
- .description(`CLI to manage archiving/restoring data in elasticsearch`)
- .option('--es-url [url]', 'url for elasticsearch')
- .option(
- '--kibana-url [url]',
- 'url for kibana (only necessary if using "load" or "unload" methods)'
- )
- .option(`--dir [path]`, 'where archives are stored')
- .option('--verbose', 'turn on verbose logging')
- .option(
- '--config [path]',
- 'path to a functional test config file to use for default values',
- resolveConfigPath,
- defaultConfigPath
- )
- .on('--help', () => {
- // eslint-disable-next-line no-console
- console.log(readFileSync(resolve(__dirname, './cli_help.txt'), 'utf8'));
- });
-
-cmd
- .option('--raw', `don't gzip the archive`)
- .command('save ')
- .description('archive the into the --dir with ')
- .action((name, indices) => execute((archiver, { raw }) => archiver.save(name, indices, { raw })));
-
-cmd
- .option('--use-create', 'use create instead of index for loading documents')
- .command('load ')
- .description('load the archive in --dir with ')
- .action((name) => execute((archiver, { useCreate }) => archiver.load(name, { useCreate })));
-
-cmd
- .command('unload ')
- .description('remove indices created by the archive in --dir with ')
- .action((name) => execute((archiver) => archiver.unload(name)));
-
-cmd
- .command('empty-kibana-index')
- .description(
- '[internal] Delete any Kibana indices, and initialize the Kibana index as Kibana would do on startup.'
- )
- .action(() => execute((archiver) => archiver.emptyKibanaIndex()));
-
-cmd
- .command('edit [prefix]')
- .description(
- 'extract the archives under the prefix, wait for edits to be completed, and then recompress the archives'
- )
- .action((prefix) =>
- execute((archiver) =>
- archiver.edit(prefix, async () => {
- const rl = readline.createInterface({
- input: process.stdin,
- output: process.stdout,
- });
-
- await new Promise((resolveInput) => {
- rl.question(`Press enter when you're done`, () => {
- rl.close();
- resolveInput();
- });
- });
- })
- )
- );
-
-cmd
- .command('rebuild-all')
- .description('[internal] read and write all archives in --dir to remove any inconsistencies')
- .action(() => execute((archiver) => archiver.rebuildAll()));
-
-cmd.parse(process.argv);
-
-const missingCommand = cmd.args.every((a) => !((a as any) instanceof Command));
-if (missingCommand) {
- execute();
-}
-
-async function execute(fn?: (esArchiver: EsArchiver, command: Command) => void): Promise {
- try {
- const log = new ToolingLog({
- level: cmd.verbose ? 'debug' : 'info',
- writeTo: process.stdout,
- });
-
- if (cmd.config) {
- // load default values from the specified config file
- const config = await readConfigFile(log, resolve(cmd.config));
- if (!cmd.esUrl) cmd.esUrl = formatUrl(config.get('servers.elasticsearch'));
- if (!cmd.kibanaUrl) cmd.kibanaUrl = formatUrl(config.get('servers.kibana'));
- if (!cmd.dir) cmd.dir = config.get('esArchiver.directory');
- }
-
- // log and count all validation errors
- let errorCount = 0;
- const error = (msg: string) => {
- errorCount++;
- log.error(msg);
- };
-
- if (!fn) {
- error(`Unknown command "${cmd.args[0]}"`);
- }
-
- if (!cmd.esUrl) {
- error('You must specify either --es-url or --config flags');
- }
-
- if (!cmd.dir) {
- error('You must specify either --dir or --config flags');
- }
-
- // if there was a validation error display the help
- if (errorCount) {
- cmd.help();
- return;
- }
-
- // run!
- const client = new legacyElasticsearch.Client({
- host: cmd.esUrl,
- log: cmd.verbose ? 'trace' : [],
- });
-
- try {
- const esArchiver = new EsArchiver({
- log,
- client,
- dataDir: resolve(cmd.dir),
- kibanaUrl: cmd.kibanaUrl,
- });
- await fn!(esArchiver, cmd);
- } finally {
- await client.close();
- }
- } catch (err) {
- // eslint-disable-next-line no-console
- console.log('FATAL ERROR', err.stack);
- }
-}
diff --git a/src/es_archiver/cli_help.txt b/src/es_archiver/cli_help.txt
deleted file mode 100644
index 1e2f8e40824ba..0000000000000
--- a/src/es_archiver/cli_help.txt
+++ /dev/null
@@ -1,15 +0,0 @@
- Examples:
- Dump an index to disk:
- Save all `logstash-*` indices from http://localhost:9200 to `snapshots/my_test_data` directory
-
- WARNING: If the `my_test_data` snapshot exists it will be deleted!
-
- $ node scripts/es_archiver save my_test_data logstash-* --dir snapshots
-
- Load an index from disk
- Load the `my_test_data` snapshot from the archive directory and elasticsearch instance defined
- in the `test/functional/config.js` config file
-
- WARNING: If the indices exist already they will be deleted!
-
- $ node scripts/es_archiver load my_test_data --config test/functional/config.js
diff --git a/test/common/services/es_archiver.ts b/test/common/services/es_archiver.ts
index cfe0610414b4f..9c99445fa4827 100644
--- a/test/common/services/es_archiver.ts
+++ b/test/common/services/es_archiver.ts
@@ -18,9 +18,9 @@
*/
import { format as formatUrl } from 'url';
+import { EsArchiver } from '@kbn/es-archiver';
import { FtrProviderContext } from '../ftr_provider_context';
-import { EsArchiver } from '../../../src/es_archiver';
// @ts-ignore not TS yet
import * as KibanaServer from './kibana_server';
diff --git a/x-pack/test/functional_enterprise_search/apps/enterprise_search/with_host_configured/app_search/engines.ts b/x-pack/test/functional_enterprise_search/apps/enterprise_search/with_host_configured/app_search/engines.ts
index e4ebd61c0692a..1742ed443984b 100644
--- a/x-pack/test/functional_enterprise_search/apps/enterprise_search/with_host_configured/app_search/engines.ts
+++ b/x-pack/test/functional_enterprise_search/apps/enterprise_search/with_host_configured/app_search/engines.ts
@@ -5,7 +5,7 @@
*/
import expect from '@kbn/expect';
-import { EsArchiver } from 'src/es_archiver';
+import { EsArchiver } from '@kbn/es-archiver';
import { AppSearchService, IEngine } from '../../../../services/app_search_service';
import { Browser } from '../../../../../../../test/functional/services/common';
import { FtrProviderContext } from '../../../../ftr_provider_context';
diff --git a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts
index ebec70793e8fd..2dd4484ffcde8 100644
--- a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts
+++ b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts
@@ -6,7 +6,7 @@
import expect from '@kbn/expect';
import { SuperTest } from 'supertest';
-import { EsArchiver } from 'src/es_archiver';
+import { EsArchiver } from '@kbn/es-archiver';
import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants';
import { CopyResponse } from '../../../../plugins/spaces/server/lib/copy_to_spaces';
import { getUrlPrefix } from '../lib/space_test_utils';
diff --git a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts
index 3529d8f3ae9c9..6d80688b7a703 100644
--- a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts
+++ b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts
@@ -6,7 +6,7 @@
import expect from '@kbn/expect';
import { SuperTest } from 'supertest';
-import { EsArchiver } from 'src/es_archiver';
+import { EsArchiver } from '@kbn/es-archiver';
import { SavedObject } from 'src/core/server';
import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants';
import { CopyResponse } from '../../../../plugins/spaces/server/lib/copy_to_spaces';
From fa11161fd03ba401d020c540d54e8d2c528c92a0 Mon Sep 17 00:00:00 2001
From: Nathan Reese
Date: Wed, 22 Jul 2020 09:04:18 -0600
Subject: [PATCH 12/49] [Maps] fix zoom in/zoom out buttons are not visible in
dark mode (#72699)
Co-authored-by: Elastic Machine
---
x-pack/plugins/maps/public/_mapbox_hacks.scss | 18 ++++++++++--------
1 file changed, 10 insertions(+), 8 deletions(-)
diff --git a/x-pack/plugins/maps/public/_mapbox_hacks.scss b/x-pack/plugins/maps/public/_mapbox_hacks.scss
index a6436585847cb..9b2d93986e426 100644
--- a/x-pack/plugins/maps/public/_mapbox_hacks.scss
+++ b/x-pack/plugins/maps/public/_mapbox_hacks.scss
@@ -25,16 +25,18 @@
// Custom SVG as background for zoom controls based off of EUI glyphs plusInCircleFilled and minusInCircleFilled
// Also fixes dark mode
-.mapboxgl-ctrl-icon.mapboxgl-ctrl-zoom-in {
- background-repeat: no-repeat;
+.mapboxgl-ctrl-zoom-in .mapboxgl-ctrl-icon {
+ // sass-lint:disable-block no-important
+ background-repeat: no-repeat !important;
// sass-lint:disable-block quotes
- background-image: url("data:image/svg+xml,%0A%3Csvg width='15px' height='15px' viewBox='0 0 15 15' version='1.1' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='#{hexToRGB($euiTextColor)}' d='M8,7 L8,3.5 C8,3.22385763 7.77614237,3 7.5,3 C7.22385763,3 7,3.22385763 7,3.5 L7,7 L3.5,7 C3.22385763,7 3,7.22385763 3,7.5 C3,7.77614237 3.22385763,8 3.5,8 L7,8 L7,11.5 C7,11.7761424 7.22385763,12 7.5,12 C7.77614237,12 8,11.7761424 8,11.5 L8,8 L11.5,8 C11.7761424,8 12,7.77614237 12,7.5 C12,7.22385763 11.7761424,7 11.5,7 L8,7 Z M7.5,15 C3.35786438,15 0,11.6421356 0,7.5 C0,3.35786438 3.35786438,0 7.5,0 C11.6421356,0 15,3.35786438 15,7.5 C15,11.6421356 11.6421356,15 7.5,15 Z' /%3E%3C/svg%3E");
- background-position: center;
+ background-image: url("data:image/svg+xml,%0A%3Csvg width='15px' height='15px' viewBox='0 0 15 15' version='1.1' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='#{hexToRGB($euiTextColor)}' d='M8,7 L8,3.5 C8,3.22385763 7.77614237,3 7.5,3 C7.22385763,3 7,3.22385763 7,3.5 L7,7 L3.5,7 C3.22385763,7 3,7.22385763 3,7.5 C3,7.77614237 3.22385763,8 3.5,8 L7,8 L7,11.5 C7,11.7761424 7.22385763,12 7.5,12 C7.77614237,12 8,11.7761424 8,11.5 L8,8 L11.5,8 C11.7761424,8 12,7.77614237 12,7.5 C12,7.22385763 11.7761424,7 11.5,7 L8,7 Z M7.5,15 C3.35786438,15 0,11.6421356 0,7.5 C0,3.35786438 3.35786438,0 7.5,0 C11.6421356,0 15,3.35786438 15,7.5 C15,11.6421356 11.6421356,15 7.5,15 Z' /%3E%3C/svg%3E") !important;
+ background-position: center !important;
}
-.mapboxgl-ctrl-icon.mapboxgl-ctrl-zoom-out {
- background-repeat: no-repeat;
+.mapboxgl-ctrl-zoom-out .mapboxgl-ctrl-icon {
+ // sass-lint:disable-block no-important
+ background-repeat: no-repeat !important;
// sass-lint:disable-block quotes
- background-image: url("data:image/svg+xml,%0A%3Csvg width='15px' height='15px' viewBox='0 0 15 15' version='1.1' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='#{hexToRGB($euiTextColor)}' d='M7.5,0 C11.6355882,0 15,3.36441176 15,7.5 C15,11.6355882 11.6355882,15 7.5,15 C3.36441176,15 0,11.6355882 0,7.5 C0,3.36441176 3.36441176,0 7.5,0 Z M3.5,7 C3.22385763,7 3,7.22385763 3,7.5 C3,7.77614237 3.22385763,8 3.5,8 L11.5,8 C11.7761424,8 12,7.77614237 12,7.5 C12,7.22385763 11.7761424,7 11.5,7 L3.5,7 Z' /%3E%3C/svg%3E");
- background-position: center;
+ background-image: url("data:image/svg+xml,%0A%3Csvg width='15px' height='15px' viewBox='0 0 15 15' version='1.1' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='#{hexToRGB($euiTextColor)}' d='M7.5,0 C11.6355882,0 15,3.36441176 15,7.5 C15,11.6355882 11.6355882,15 7.5,15 C3.36441176,15 0,11.6355882 0,7.5 C0,3.36441176 3.36441176,0 7.5,0 Z M3.5,7 C3.22385763,7 3,7.22385763 3,7.5 C3,7.77614237 3.22385763,8 3.5,8 L11.5,8 C11.7761424,8 12,7.77614237 12,7.5 C12,7.22385763 11.7761424,7 11.5,7 L3.5,7 Z' /%3E%3C/svg%3E") !important;
+ background-position: center !important;
}
From 0bab77147aa867c736c5ab553f2af71e7d861cb6 Mon Sep 17 00:00:00 2001
From: Marshall Main <55718608+marshallmain@users.noreply.github.com>
Date: Wed, 22 Jul 2020 11:04:44 -0400
Subject: [PATCH 13/49] [Security Solution] Change query builder so N exception
items don't nest N levels deep (#72224)
* Change query builder so N exceptions don't nest N levels deep
* Fix tests and clarify function naming
* Rename evaluateEntry to buildEntry for consistency
* Remove duplicate tests
* more test fixes
* Add tests with multiple exception list items
* Chunk exception list items in query to support up to 1000000
Co-authored-by: Elastic Machine
---
.../build_exceptions_query.test.ts | 334 +++++-------
.../build_exceptions_query.ts | 101 ++--
.../detection_engine/get_query_filter.test.ts | 478 ++++++++++++++++--
.../detection_engine/get_query_filter.ts | 100 +++-
.../signals/get_filter.test.ts | 63 +--
5 files changed, 712 insertions(+), 364 deletions(-)
diff --git a/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.test.ts b/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.test.ts
index 1c7a2a5de6594..2cebaacc67681 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.test.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.test.ts
@@ -5,14 +5,13 @@
*/
import {
- buildQueryExceptions,
- buildExceptionItemEntries,
+ buildExceptionListQueries,
+ buildExceptionItem,
operatorBuilder,
buildExists,
buildMatch,
buildMatchAny,
- evaluateValues,
- formatQuery,
+ buildEntry,
getLanguageBooleanOperator,
buildNested,
} from './build_exceptions_query';
@@ -30,7 +29,6 @@ import { getEntryMatchAnyMock } from '../../../lists/common/schemas/types/entry_
import { getEntryExistsMock } from '../../../lists/common/schemas/types/entry_exists.mock';
describe('build_exceptions_query', () => {
- let exclude: boolean;
const makeMatchEntry = ({
field,
value = 'value-1',
@@ -97,10 +95,6 @@ describe('build_exceptions_query', () => {
operator: 'excluded',
});
- beforeEach(() => {
- exclude = true;
- });
-
describe('getLanguageBooleanOperator', () => {
test('it returns value as uppercase if language is "lucene"', () => {
const result = getLanguageBooleanOperator({ language: 'lucene', value: 'not' });
@@ -143,14 +137,14 @@ describe('build_exceptions_query', () => {
describe('kuery', () => {
test('it returns formatted wildcard string when operator is "excluded"', () => {
const query = buildExists({
- item: existsEntryWithExcluded,
+ entry: existsEntryWithExcluded,
language: 'kuery',
});
expect(query).toEqual('not host.name:*');
});
test('it returns formatted wildcard string when operator is "included"', () => {
const query = buildExists({
- item: existsEntryWithIncluded,
+ entry: existsEntryWithIncluded,
language: 'kuery',
});
expect(query).toEqual('host.name:*');
@@ -160,14 +154,14 @@ describe('build_exceptions_query', () => {
describe('lucene', () => {
test('it returns formatted wildcard string when operator is "excluded"', () => {
const query = buildExists({
- item: existsEntryWithExcluded,
+ entry: existsEntryWithExcluded,
language: 'lucene',
});
expect(query).toEqual('NOT _exists_host.name');
});
test('it returns formatted wildcard string when operator is "included"', () => {
const query = buildExists({
- item: existsEntryWithIncluded,
+ entry: existsEntryWithIncluded,
language: 'lucene',
});
expect(query).toEqual('_exists_host.name');
@@ -179,14 +173,14 @@ describe('build_exceptions_query', () => {
describe('kuery', () => {
test('it returns formatted string when operator is "included"', () => {
const query = buildMatch({
- item: matchEntryWithIncluded,
+ entry: matchEntryWithIncluded,
language: 'kuery',
});
expect(query).toEqual('host.name:"suricata"');
});
test('it returns formatted string when operator is "excluded"', () => {
const query = buildMatch({
- item: matchEntryWithExcluded,
+ entry: matchEntryWithExcluded,
language: 'kuery',
});
expect(query).toEqual('not host.name:"suricata"');
@@ -196,14 +190,14 @@ describe('build_exceptions_query', () => {
describe('lucene', () => {
test('it returns formatted string when operator is "included"', () => {
const query = buildMatch({
- item: matchEntryWithIncluded,
+ entry: matchEntryWithIncluded,
language: 'lucene',
});
expect(query).toEqual('host.name:"suricata"');
});
test('it returns formatted string when operator is "excluded"', () => {
const query = buildMatch({
- item: matchEntryWithExcluded,
+ entry: matchEntryWithExcluded,
language: 'lucene',
});
expect(query).toEqual('NOT host.name:"suricata"');
@@ -229,7 +223,7 @@ describe('build_exceptions_query', () => {
describe('kuery', () => {
test('it returns empty string if given an empty array for "values"', () => {
const exceptionSegment = buildMatchAny({
- item: entryWithIncludedAndNoValues,
+ entry: entryWithIncludedAndNoValues,
language: 'kuery',
});
expect(exceptionSegment).toEqual('');
@@ -237,7 +231,7 @@ describe('build_exceptions_query', () => {
test('it returns formatted string when "values" includes only one item', () => {
const exceptionSegment = buildMatchAny({
- item: entryWithIncludedAndOneValue,
+ entry: entryWithIncludedAndOneValue,
language: 'kuery',
});
@@ -246,7 +240,7 @@ describe('build_exceptions_query', () => {
test('it returns formatted string when operator is "included"', () => {
const exceptionSegment = buildMatchAny({
- item: matchAnyEntryWithIncludedAndTwoValues,
+ entry: matchAnyEntryWithIncludedAndTwoValues,
language: 'kuery',
});
@@ -255,7 +249,7 @@ describe('build_exceptions_query', () => {
test('it returns formatted string when operator is "excluded"', () => {
const exceptionSegment = buildMatchAny({
- item: entryWithExcludedAndTwoValues,
+ entry: entryWithExcludedAndTwoValues,
language: 'kuery',
});
@@ -266,7 +260,7 @@ describe('build_exceptions_query', () => {
describe('lucene', () => {
test('it returns formatted string when operator is "included"', () => {
const exceptionSegment = buildMatchAny({
- item: matchAnyEntryWithIncludedAndTwoValues,
+ entry: matchAnyEntryWithIncludedAndTwoValues,
language: 'lucene',
});
@@ -274,7 +268,7 @@ describe('build_exceptions_query', () => {
});
test('it returns formatted string when operator is "excluded"', () => {
const exceptionSegment = buildMatchAny({
- item: entryWithExcludedAndTwoValues,
+ entry: entryWithExcludedAndTwoValues,
language: 'lucene',
});
@@ -282,7 +276,7 @@ describe('build_exceptions_query', () => {
});
test('it returns formatted string when "values" includes only one item', () => {
const exceptionSegment = buildMatchAny({
- item: entryWithIncludedAndOneValue,
+ entry: entryWithIncludedAndOneValue,
language: 'lucene',
});
@@ -295,7 +289,7 @@ describe('build_exceptions_query', () => {
// NOTE: Only KQL supports nested
describe('kuery', () => {
test('it returns formatted query when one item in nested entry', () => {
- const item: EntryNested = {
+ const entry: EntryNested = {
field: 'parent',
type: 'nested',
entries: [
@@ -307,35 +301,35 @@ describe('build_exceptions_query', () => {
},
],
};
- const result = buildNested({ item, language: 'kuery' });
+ const result = buildNested({ entry, language: 'kuery' });
expect(result).toEqual('parent:{ nestedField:"value-1" }');
});
test('it returns formatted query when entry item is "exists"', () => {
- const item: EntryNested = {
+ const entry: EntryNested = {
field: 'parent',
type: 'nested',
entries: [{ ...getEntryExistsMock(), field: 'nestedField', operator: 'included' }],
};
- const result = buildNested({ item, language: 'kuery' });
+ const result = buildNested({ entry, language: 'kuery' });
expect(result).toEqual('parent:{ nestedField:* }');
});
test('it returns formatted query when entry item is "exists" and operator is "excluded"', () => {
- const item: EntryNested = {
+ const entry: EntryNested = {
field: 'parent',
type: 'nested',
entries: [{ ...getEntryExistsMock(), field: 'nestedField', operator: 'excluded' }],
};
- const result = buildNested({ item, language: 'kuery' });
+ const result = buildNested({ entry, language: 'kuery' });
expect(result).toEqual('parent:{ not nestedField:* }');
});
test('it returns formatted query when entry item is "match_any"', () => {
- const item: EntryNested = {
+ const entry: EntryNested = {
field: 'parent',
type: 'nested',
entries: [
@@ -347,13 +341,13 @@ describe('build_exceptions_query', () => {
},
],
};
- const result = buildNested({ item, language: 'kuery' });
+ const result = buildNested({ entry, language: 'kuery' });
expect(result).toEqual('parent:{ nestedField:("value1" or "value2") }');
});
test('it returns formatted query when entry item is "match_any" and operator is "excluded"', () => {
- const item: EntryNested = {
+ const entry: EntryNested = {
field: 'parent',
type: 'nested',
entries: [
@@ -365,13 +359,13 @@ describe('build_exceptions_query', () => {
},
],
};
- const result = buildNested({ item, language: 'kuery' });
+ const result = buildNested({ entry, language: 'kuery' });
expect(result).toEqual('parent:{ not nestedField:("value1" or "value2") }');
});
test('it returns formatted query when multiple items in nested entry', () => {
- const item: EntryNested = {
+ const entry: EntryNested = {
field: 'parent',
type: 'nested',
entries: [
@@ -389,34 +383,34 @@ describe('build_exceptions_query', () => {
},
],
};
- const result = buildNested({ item, language: 'kuery' });
+ const result = buildNested({ entry, language: 'kuery' });
expect(result).toEqual('parent:{ nestedField:"value-1" and nestedFieldB:"value-2" }');
});
});
});
- describe('evaluateValues', () => {
+ describe('buildEntry', () => {
describe('kuery', () => {
test('it returns formatted wildcard string when "type" is "exists"', () => {
- const result = evaluateValues({
- item: existsEntryWithIncluded,
+ const result = buildEntry({
+ entry: existsEntryWithIncluded,
language: 'kuery',
});
expect(result).toEqual('host.name:*');
});
test('it returns formatted string when "type" is "match"', () => {
- const result = evaluateValues({
- item: matchEntryWithIncluded,
+ const result = buildEntry({
+ entry: matchEntryWithIncluded,
language: 'kuery',
});
expect(result).toEqual('host.name:"suricata"');
});
test('it returns formatted string when "type" is "match_any"', () => {
- const result = evaluateValues({
- item: matchAnyEntryWithIncludedAndTwoValues,
+ const result = buildEntry({
+ entry: matchAnyEntryWithIncludedAndTwoValues,
language: 'kuery',
});
expect(result).toEqual('host.name:("suricata" or "auditd")');
@@ -424,95 +418,35 @@ describe('build_exceptions_query', () => {
});
describe('lucene', () => {
- describe('kuery', () => {
- test('it returns formatted wildcard string when "type" is "exists"', () => {
- const result = evaluateValues({
- item: existsEntryWithIncluded,
- language: 'lucene',
- });
- expect(result).toEqual('_exists_host.name');
- });
-
- test('it returns formatted string when "type" is "match"', () => {
- const result = evaluateValues({
- item: matchEntryWithIncluded,
- language: 'lucene',
- });
- expect(result).toEqual('host.name:"suricata"');
- });
-
- test('it returns formatted string when "type" is "match_any"', () => {
- const result = evaluateValues({
- item: matchAnyEntryWithIncludedAndTwoValues,
- language: 'lucene',
- });
- expect(result).toEqual('host.name:("suricata" OR "auditd")');
- });
- });
- });
- });
-
- describe('formatQuery', () => {
- describe('exclude is true', () => {
- describe('when query is empty string', () => {
- test('it returns empty string if "exceptions" is empty array', () => {
- const formattedQuery = formatQuery({ exceptions: [], language: 'kuery', exclude: true });
- expect(formattedQuery).toEqual('');
- });
-
- test('it returns expected query string when single exception in array', () => {
- const formattedQuery = formatQuery({
- exceptions: ['b:("value-1" or "value-2") and not c:*'],
- language: 'kuery',
- exclude: true,
- });
- expect(formattedQuery).toEqual('not ((b:("value-1" or "value-2") and not c:*))');
- });
- });
-
- test('it returns expected query string when multiple exceptions in array', () => {
- const formattedQuery = formatQuery({
- exceptions: ['b:("value-1" or "value-2") and not c:*', 'not d:*'],
- language: 'kuery',
- exclude: true,
+ test('it returns formatted wildcard string when "type" is "exists"', () => {
+ const result = buildEntry({
+ entry: existsEntryWithIncluded,
+ language: 'lucene',
});
- expect(formattedQuery).toEqual(
- 'not ((b:("value-1" or "value-2") and not c:*) or (not d:*))'
- );
+ expect(result).toEqual('_exists_host.name');
});
- });
- describe('exclude is false', () => {
- describe('when query is empty string', () => {
- test('it returns empty string if "exceptions" is empty array', () => {
- const formattedQuery = formatQuery({ exceptions: [], language: 'kuery', exclude: false });
- expect(formattedQuery).toEqual('');
- });
-
- test('it returns expected query string when single exception in array', () => {
- const formattedQuery = formatQuery({
- exceptions: ['b:("value-1" or "value-2") and not c:*'],
- language: 'kuery',
- exclude: false,
- });
- expect(formattedQuery).toEqual('(b:("value-1" or "value-2") and not c:*)');
+ test('it returns formatted string when "type" is "match"', () => {
+ const result = buildEntry({
+ entry: matchEntryWithIncluded,
+ language: 'lucene',
});
+ expect(result).toEqual('host.name:"suricata"');
});
- test('it returns expected query string when multiple exceptions in array', () => {
- const formattedQuery = formatQuery({
- exceptions: ['b:("value-1" or "value-2") and not c:*', 'not d:*'],
- language: 'kuery',
- exclude: false,
+ test('it returns formatted string when "type" is "match_any"', () => {
+ const result = buildEntry({
+ entry: matchAnyEntryWithIncludedAndTwoValues,
+ language: 'lucene',
});
- expect(formattedQuery).toEqual('(b:("value-1" or "value-2") and not c:*) or (not d:*)');
+ expect(result).toEqual('host.name:("suricata" OR "auditd")');
});
});
});
- describe('buildExceptionItemEntries', () => {
+ describe('buildExceptionItem', () => {
test('it returns empty string if empty lists array passed in', () => {
- const query = buildExceptionItemEntries({
+ const query = buildExceptionItem({
language: 'kuery',
entries: [],
});
@@ -525,7 +459,7 @@ describe('build_exceptions_query', () => {
makeMatchAnyEntry({ field: 'b' }),
makeMatchEntry({ field: 'c', operator: 'excluded', value: 'value-3' }),
];
- const query = buildExceptionItemEntries({
+ const query = buildExceptionItem({
language: 'kuery',
entries: payload,
});
@@ -545,7 +479,7 @@ describe('build_exceptions_query', () => {
],
},
];
- const query = buildExceptionItemEntries({
+ const query = buildExceptionItem({
language: 'kuery',
entries,
});
@@ -566,7 +500,7 @@ describe('build_exceptions_query', () => {
},
makeExistsEntry({ field: 'd' }),
];
- const query = buildExceptionItemEntries({
+ const query = buildExceptionItem({
language: 'kuery',
entries,
});
@@ -587,7 +521,7 @@ describe('build_exceptions_query', () => {
},
makeExistsEntry({ field: 'e', operator: 'excluded' }),
];
- const query = buildExceptionItemEntries({
+ const query = buildExceptionItem({
language: 'lucene',
entries,
});
@@ -599,7 +533,7 @@ describe('build_exceptions_query', () => {
describe('exists', () => {
test('it returns expected query when list includes single list item with operator of "included"', () => {
const entries: EntriesArray = [makeExistsEntry({ field: 'b' })];
- const query = buildExceptionItemEntries({
+ const query = buildExceptionItem({
language: 'kuery',
entries,
});
@@ -610,7 +544,7 @@ describe('build_exceptions_query', () => {
test('it returns expected query when list includes single list item with operator of "excluded"', () => {
const entries: EntriesArray = [makeExistsEntry({ field: 'b', operator: 'excluded' })];
- const query = buildExceptionItemEntries({
+ const query = buildExceptionItem({
language: 'kuery',
entries,
});
@@ -628,7 +562,7 @@ describe('build_exceptions_query', () => {
entries: [makeMatchEntry({ field: 'c', operator: 'included', value: 'value-1' })],
},
];
- const query = buildExceptionItemEntries({
+ const query = buildExceptionItem({
language: 'kuery',
entries,
});
@@ -650,7 +584,7 @@ describe('build_exceptions_query', () => {
},
makeExistsEntry({ field: 'e' }),
];
- const query = buildExceptionItemEntries({
+ const query = buildExceptionItem({
language: 'kuery',
entries,
});
@@ -663,7 +597,7 @@ describe('build_exceptions_query', () => {
describe('match', () => {
test('it returns expected query when list includes single list item with operator of "included"', () => {
const entries: EntriesArray = [makeMatchEntry({ field: 'b', value: 'value' })];
- const query = buildExceptionItemEntries({
+ const query = buildExceptionItem({
language: 'kuery',
entries,
});
@@ -676,7 +610,7 @@ describe('build_exceptions_query', () => {
const entries: EntriesArray = [
makeMatchEntry({ field: 'b', operator: 'excluded', value: 'value' }),
];
- const query = buildExceptionItemEntries({
+ const query = buildExceptionItem({
language: 'kuery',
entries,
});
@@ -694,7 +628,7 @@ describe('build_exceptions_query', () => {
entries: [makeMatchEntry({ field: 'c', operator: 'included', value: 'valueC' })],
},
];
- const query = buildExceptionItemEntries({
+ const query = buildExceptionItem({
language: 'kuery',
entries,
});
@@ -716,7 +650,7 @@ describe('build_exceptions_query', () => {
},
makeMatchEntry({ field: 'e', value: 'valueE' }),
];
- const query = buildExceptionItemEntries({
+ const query = buildExceptionItem({
language: 'kuery',
entries,
});
@@ -730,7 +664,7 @@ describe('build_exceptions_query', () => {
describe('match_any', () => {
test('it returns expected query when list includes single list item with operator of "included"', () => {
const entries: EntriesArray = [makeMatchAnyEntry({ field: 'b' })];
- const query = buildExceptionItemEntries({
+ const query = buildExceptionItem({
language: 'kuery',
entries,
});
@@ -741,7 +675,7 @@ describe('build_exceptions_query', () => {
test('it returns expected query when list includes single list item with operator of "excluded"', () => {
const entries: EntriesArray = [makeMatchAnyEntry({ field: 'b', operator: 'excluded' })];
- const query = buildExceptionItemEntries({
+ const query = buildExceptionItem({
language: 'kuery',
entries,
});
@@ -759,7 +693,7 @@ describe('build_exceptions_query', () => {
entries: [makeMatchEntry({ field: 'c', operator: 'excluded', value: 'valueC' })],
},
];
- const query = buildExceptionItemEntries({
+ const query = buildExceptionItem({
language: 'kuery',
entries,
});
@@ -773,7 +707,7 @@ describe('build_exceptions_query', () => {
makeMatchAnyEntry({ field: 'b' }),
makeMatchAnyEntry({ field: 'c' }),
];
- const query = buildExceptionItemEntries({
+ const query = buildExceptionItem({
language: 'kuery',
entries,
});
@@ -784,15 +718,15 @@ describe('build_exceptions_query', () => {
});
});
- describe('buildQueryExceptions', () => {
+ describe('buildExceptionListQueries', () => {
test('it returns empty array if lists is empty array', () => {
- const query = buildQueryExceptions({ language: 'kuery', lists: [] });
+ const query = buildExceptionListQueries({ language: 'kuery', lists: [] });
expect(query).toEqual([]);
});
test('it returns empty array if lists is undefined', () => {
- const query = buildQueryExceptions({ language: 'kuery', lists: undefined });
+ const query = buildExceptionListQueries({ language: 'kuery', lists: undefined });
expect(query).toEqual([]);
});
@@ -812,14 +746,24 @@ describe('build_exceptions_query', () => {
},
makeMatchAnyEntry({ field: 'e', operator: 'excluded' }),
];
- const query = buildQueryExceptions({
+ const queries = buildExceptionListQueries({
language: 'kuery',
lists: [payload, payload2],
});
- const expectedQuery =
- 'not ((some.parentField:{ nested.field:"some value" } and some.not.nested.field:"some value") or (b:("value-1" or "value-2") and parent:{ c:"valueC" and d:"valueD" } and not e:("value-1" or "value-2")))';
+ const expectedQueries = [
+ {
+ query:
+ 'some.parentField:{ nested.field:"some value" } and some.not.nested.field:"some value"',
+ language: 'kuery',
+ },
+ {
+ query:
+ 'b:("value-1" or "value-2") and parent:{ c:"valueC" and d:"valueD" } and not e:("value-1" or "value-2")',
+ language: 'kuery',
+ },
+ ];
- expect(query).toEqual([{ query: expectedQuery, language: 'kuery' }]);
+ expect(queries).toEqual(expectedQueries);
});
test('it returns expected query when lists exist and language is "lucene"', () => {
@@ -827,78 +771,58 @@ describe('build_exceptions_query', () => {
payload.entries = [makeMatchAnyEntry({ field: 'a' }), makeMatchAnyEntry({ field: 'b' })];
const payload2 = getExceptionListItemSchemaMock();
payload2.entries = [makeMatchAnyEntry({ field: 'c' }), makeMatchAnyEntry({ field: 'd' })];
- const query = buildQueryExceptions({
+ const queries = buildExceptionListQueries({
language: 'lucene',
lists: [payload, payload2],
});
- const expectedQuery =
- 'NOT ((a:("value-1" OR "value-2") AND b:("value-1" OR "value-2")) OR (c:("value-1" OR "value-2") AND d:("value-1" OR "value-2")))';
+ const expectedQueries = [
+ {
+ query: 'a:("value-1" OR "value-2") AND b:("value-1" OR "value-2")',
+ language: 'lucene',
+ },
+ {
+ query: 'c:("value-1" OR "value-2") AND d:("value-1" OR "value-2")',
+ language: 'lucene',
+ },
+ ];
- expect(query).toEqual([{ query: expectedQuery, language: 'lucene' }]);
+ expect(queries).toEqual(expectedQueries);
});
- describe('when "exclude" is false', () => {
- beforeEach(() => {
- exclude = false;
+ test('it builds correct queries for nested excluded fields', () => {
+ const payload = getExceptionListItemSchemaMock();
+ const payload2 = getExceptionListItemSchemaMock();
+ payload2.entries = [
+ makeMatchAnyEntry({ field: 'b' }),
+ {
+ field: 'parent',
+ type: 'nested',
+ entries: [
+ // TODO: these operators are not being respected. buildNested needs to be updated
+ makeMatchEntry({ field: 'c', operator: 'excluded', value: 'valueC' }),
+ makeMatchEntry({ field: 'd', operator: 'excluded', value: 'valueD' }),
+ ],
+ },
+ makeMatchAnyEntry({ field: 'e' }),
+ ];
+ const queries = buildExceptionListQueries({
+ language: 'kuery',
+ lists: [payload, payload2],
});
-
- test('it returns empty array if lists is empty array', () => {
- const query = buildQueryExceptions({
+ const expectedQueries = [
+ {
+ query:
+ 'some.parentField:{ nested.field:"some value" } and some.not.nested.field:"some value"',
language: 'kuery',
- lists: [],
- exclude,
- });
-
- expect(query).toEqual([]);
- });
-
- test('it returns empty array if lists is undefined', () => {
- const query = buildQueryExceptions({ language: 'kuery', lists: undefined, exclude });
-
- expect(query).toEqual([]);
- });
-
- test('it returns expected query when lists exist and language is "kuery"', () => {
- const payload = getExceptionListItemSchemaMock();
- const payload2 = getExceptionListItemSchemaMock();
- payload2.entries = [
- makeMatchAnyEntry({ field: 'b' }),
- {
- field: 'parent',
- type: 'nested',
- entries: [
- makeMatchEntry({ field: 'c', operator: 'excluded', value: 'valueC' }),
- makeMatchEntry({ field: 'd', operator: 'excluded', value: 'valueD' }),
- ],
- },
- makeMatchAnyEntry({ field: 'e' }),
- ];
- const query = buildQueryExceptions({
+ },
+ {
+ query:
+ 'b:("value-1" or "value-2") and parent:{ not c:"valueC" and not d:"valueD" } and e:("value-1" or "value-2")',
language: 'kuery',
- lists: [payload, payload2],
- exclude,
- });
- const expectedQuery =
- '(some.parentField:{ nested.field:"some value" } and some.not.nested.field:"some value") or (b:("value-1" or "value-2") and parent:{ not c:"valueC" and not d:"valueD" } and e:("value-1" or "value-2"))';
-
- expect(query).toEqual([{ query: expectedQuery, language: 'kuery' }]);
- });
-
- test('it returns expected query when lists exist and language is "lucene"', () => {
- const payload = getExceptionListItemSchemaMock();
- payload.entries = [makeMatchAnyEntry({ field: 'a' }), makeMatchAnyEntry({ field: 'b' })];
- const payload2 = getExceptionListItemSchemaMock();
- payload2.entries = [makeMatchAnyEntry({ field: 'c' }), makeMatchAnyEntry({ field: 'd' })];
- const query = buildQueryExceptions({
- language: 'lucene',
- lists: [payload, payload2],
- exclude,
- });
- const expectedQuery =
- '(a:("value-1" OR "value-2") AND b:("value-1" OR "value-2")) OR (c:("value-1" OR "value-2") AND d:("value-1" OR "value-2"))';
+ },
+ ];
- expect(query).toEqual([{ query: expectedQuery, language: 'lucene' }]);
- });
+ expect(queries).toEqual(expectedQueries);
});
});
});
diff --git a/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.ts b/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.ts
index ff492dcda3b66..c64d0b124b67a 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.ts
@@ -64,13 +64,13 @@ export const operatorBuilder = ({
};
export const buildExists = ({
- item,
+ entry,
language,
}: {
- item: EntryExists;
+ entry: EntryExists;
language: Language;
}): string => {
- const { operator, field } = item;
+ const { operator, field } = entry;
const exceptionOperator = operatorBuilder({ operator, language });
switch (language) {
@@ -84,26 +84,26 @@ export const buildExists = ({
};
export const buildMatch = ({
- item,
+ entry,
language,
}: {
- item: EntryMatch;
+ entry: EntryMatch;
language: Language;
}): string => {
- const { value, operator, field } = item;
+ const { value, operator, field } = entry;
const exceptionOperator = operatorBuilder({ operator, language });
return `${exceptionOperator}${field}:"${value}"`;
};
export const buildMatchAny = ({
- item,
+ entry,
language,
}: {
- item: EntryMatchAny;
+ entry: EntryMatchAny;
language: Language;
}): string => {
- const { value, operator, field } = item;
+ const { value, operator, field } = entry;
switch (value.length) {
case 0:
@@ -118,67 +118,40 @@ export const buildMatchAny = ({
};
export const buildNested = ({
- item,
+ entry,
language,
}: {
- item: EntryNested;
+ entry: EntryNested;
language: Language;
}): string => {
- const { field, entries } = item;
+ const { field, entries: subentries } = entry;
const and = getLanguageBooleanOperator({ language, value: 'and' });
- const values = entries.map((entry) => evaluateValues({ item: entry, language }));
+ const values = subentries.map((subentry) => buildEntry({ entry: subentry, language }));
return `${field}:{ ${values.join(` ${and} `)} }`;
};
-export const evaluateValues = ({
- item,
+export const buildEntry = ({
+ entry,
language,
}: {
- item: Entry | EntryNested;
+ entry: Entry | EntryNested;
language: Language;
}): string => {
- if (entriesExists.is(item)) {
- return buildExists({ item, language });
- } else if (entriesMatch.is(item)) {
- return buildMatch({ item, language });
- } else if (entriesMatchAny.is(item)) {
- return buildMatchAny({ item, language });
- } else if (entriesNested.is(item)) {
- return buildNested({ item, language });
+ if (entriesExists.is(entry)) {
+ return buildExists({ entry, language });
+ } else if (entriesMatch.is(entry)) {
+ return buildMatch({ entry, language });
+ } else if (entriesMatchAny.is(entry)) {
+ return buildMatchAny({ entry, language });
+ } else if (entriesNested.is(entry)) {
+ return buildNested({ entry, language });
} else {
return '';
}
};
-export const formatQuery = ({
- exceptions,
- language,
- exclude,
-}: {
- exceptions: string[];
- language: Language;
- exclude: boolean;
-}): string => {
- if (exceptions == null || (exceptions != null && exceptions.length === 0)) {
- return '';
- }
-
- const or = getLanguageBooleanOperator({ language, value: 'or' });
- const not = getLanguageBooleanOperator({ language, value: 'not' });
- const formattedExceptionItems = exceptions.map((exceptionItem, index) => {
- if (index === 0) {
- return `(${exceptionItem})`;
- }
-
- return `${or} (${exceptionItem})`;
- });
-
- const exceptionItemsQuery = formattedExceptionItems.join(' ');
- return exclude ? `${not} (${exceptionItemsQuery})` : exceptionItemsQuery;
-};
-
-export const buildExceptionItemEntries = ({
+export const buildExceptionItem = ({
entries,
language,
}: {
@@ -186,22 +159,19 @@ export const buildExceptionItemEntries = ({
language: Language;
}): string => {
const and = getLanguageBooleanOperator({ language, value: 'and' });
- const exceptionItemEntries = entries.reduce((accum, listItem) => {
- const exceptionSegment = evaluateValues({ item: listItem, language });
- return [...accum, exceptionSegment];
- }, []);
+ const exceptionItemEntries = entries.map((entry) => {
+ return buildEntry({ entry, language });
+ });
return exceptionItemEntries.join(` ${and} `);
};
-export const buildQueryExceptions = ({
+export const buildExceptionListQueries = ({
language,
lists,
- exclude = true,
}: {
language: Language;
lists: Array | undefined;
- exclude?: boolean;
}): DataQuery[] => {
if (lists == null || (lists != null && lists.length === 0)) {
return [];
@@ -211,7 +181,7 @@ export const buildQueryExceptions = ({
const { entries } = exceptionItem;
if (entries != null && entries.length > 0 && !hasLargeValueList(entries)) {
- return [...acc, buildExceptionItemEntries({ entries, language })];
+ return [...acc, buildExceptionItem({ entries, language })];
} else {
return acc;
}
@@ -220,12 +190,11 @@ export const buildQueryExceptions = ({
if (exceptionItems.length === 0) {
return [];
} else {
- const formattedQuery = formatQuery({ exceptions: exceptionItems, language, exclude });
- return [
- {
- query: formattedQuery,
+ return exceptionItems.map((exceptionItem) => {
+ return {
+ query: exceptionItem,
language,
- },
- ];
+ };
+ });
}
};
diff --git a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.test.ts b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.test.ts
index a8eb4e7bbb15b..72ef230a42342 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.test.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.test.ts
@@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { getQueryFilter } from './get_query_filter';
-import { Filter } from 'src/plugins/data/public';
+import { getQueryFilter, buildExceptionFilter } from './get_query_filter';
+import { Filter, EsQueryConfig } from 'src/plugins/data/public';
import { getExceptionListItemSchemaMock } from '../../../lists/common/schemas/response/exception_list_item_schema.mock';
describe('get_filter', () => {
@@ -363,49 +363,151 @@ describe('get_filter', () => {
bool: {
filter: [
{ bool: { minimum_should_match: 1, should: [{ match: { 'host.name': 'linux' } }] } },
+ ],
+ must: [],
+ must_not: [
{
bool: {
- must_not: {
- bool: {
- filter: [
- {
- nested: {
- path: 'some.parentField',
- query: {
- bool: {
- minimum_should_match: 1,
- should: [
- {
- match_phrase: {
- 'some.parentField.nested.field': 'some value',
+ should: [
+ {
+ bool: {
+ filter: [
+ {
+ nested: {
+ path: 'some.parentField',
+ query: {
+ bool: {
+ minimum_should_match: 1,
+ should: [
+ {
+ match_phrase: {
+ 'some.parentField.nested.field': 'some value',
+ },
},
+ ],
+ },
+ },
+ score_mode: 'none',
+ },
+ },
+ {
+ bool: {
+ minimum_should_match: 1,
+ should: [
+ {
+ match_phrase: {
+ 'some.not.nested.field': 'some value',
},
- ],
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ ],
+ should: [],
+ },
+ });
+ });
+
+ test('it should work with a list with multiple items', () => {
+ const esQuery = getQueryFilter(
+ 'host.name: linux',
+ 'kuery',
+ [],
+ ['auditbeat-*'],
+ [getExceptionListItemSchemaMock(), getExceptionListItemSchemaMock()]
+ );
+ expect(esQuery).toEqual({
+ bool: {
+ filter: [
+ { bool: { minimum_should_match: 1, should: [{ match: { 'host.name': 'linux' } }] } },
+ ],
+ must: [],
+ must_not: [
+ {
+ bool: {
+ should: [
+ {
+ bool: {
+ filter: [
+ {
+ nested: {
+ path: 'some.parentField',
+ query: {
+ bool: {
+ minimum_should_match: 1,
+ should: [
+ {
+ match_phrase: {
+ 'some.parentField.nested.field': 'some value',
+ },
+ },
+ ],
+ },
},
+ score_mode: 'none',
},
- score_mode: 'none',
},
- },
- {
- bool: {
- minimum_should_match: 1,
- should: [
- {
- match_phrase: {
- 'some.not.nested.field': 'some value',
+ {
+ bool: {
+ minimum_should_match: 1,
+ should: [
+ {
+ match_phrase: {
+ 'some.not.nested.field': 'some value',
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ bool: {
+ filter: [
+ {
+ nested: {
+ path: 'some.parentField',
+ query: {
+ bool: {
+ minimum_should_match: 1,
+ should: [
+ {
+ match_phrase: {
+ 'some.parentField.nested.field': 'some value',
+ },
+ },
+ ],
},
},
- ],
+ score_mode: 'none',
+ },
},
- },
- ],
+ {
+ bool: {
+ minimum_should_match: 1,
+ should: [
+ {
+ match_phrase: {
+ 'some.not.nested.field': 'some value',
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
},
- },
+ ],
},
},
],
- must: [],
- must_not: [],
should: [],
},
});
@@ -455,32 +557,137 @@ describe('get_filter', () => {
{ bool: { minimum_should_match: 1, should: [{ match: { 'host.name': 'linux' } }] } },
{
bool: {
- filter: [
+ should: [
{
- nested: {
- path: 'some.parentField',
- query: {
- bool: {
- minimum_should_match: 1,
- should: [
- {
- match_phrase: {
- 'some.parentField.nested.field': 'some value',
+ bool: {
+ filter: [
+ {
+ nested: {
+ path: 'some.parentField',
+ query: {
+ bool: {
+ minimum_should_match: 1,
+ should: [
+ {
+ match_phrase: {
+ 'some.parentField.nested.field': 'some value',
+ },
+ },
+ ],
},
},
- ],
+ score_mode: 'none',
+ },
},
- },
- score_mode: 'none',
+ {
+ bool: {
+ minimum_should_match: 1,
+ should: [
+ {
+ match_phrase: {
+ 'some.not.nested.field': 'some value',
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ ],
+ must: [],
+ must_not: [],
+ should: [],
+ },
+ });
+ });
+
+ test('it should work with a list with multiple items', () => {
+ const esQuery = getQueryFilter(
+ 'host.name: linux',
+ 'kuery',
+ [],
+ ['auditbeat-*'],
+ [getExceptionListItemSchemaMock(), getExceptionListItemSchemaMock()],
+ false
+ );
+ expect(esQuery).toEqual({
+ bool: {
+ filter: [
+ { bool: { minimum_should_match: 1, should: [{ match: { 'host.name': 'linux' } }] } },
+ {
+ bool: {
+ should: [
+ {
+ bool: {
+ filter: [
+ {
+ nested: {
+ path: 'some.parentField',
+ query: {
+ bool: {
+ minimum_should_match: 1,
+ should: [
+ {
+ match_phrase: {
+ 'some.parentField.nested.field': 'some value',
+ },
+ },
+ ],
+ },
+ },
+ score_mode: 'none',
+ },
+ },
+ {
+ bool: {
+ minimum_should_match: 1,
+ should: [
+ {
+ match_phrase: {
+ 'some.not.nested.field': 'some value',
+ },
+ },
+ ],
+ },
+ },
+ ],
},
},
{
bool: {
- minimum_should_match: 1,
- should: [
+ filter: [
{
- match_phrase: {
- 'some.not.nested.field': 'some value',
+ nested: {
+ path: 'some.parentField',
+ query: {
+ bool: {
+ minimum_should_match: 1,
+ should: [
+ {
+ match_phrase: {
+ 'some.parentField.nested.field': 'some value',
+ },
+ },
+ ],
+ },
+ },
+ score_mode: 'none',
+ },
+ },
+ {
+ bool: {
+ minimum_should_match: 1,
+ should: [
+ {
+ match_phrase: {
+ 'some.not.nested.field': 'some value',
+ },
+ },
+ ],
},
},
],
@@ -703,4 +910,179 @@ describe('get_filter', () => {
});
});
});
+
+ describe('buildExceptionFilter', () => {
+ const config: EsQueryConfig = {
+ allowLeadingWildcards: true,
+ queryStringOptions: { analyze_wildcard: true },
+ ignoreFilterIfFieldNotInIndex: false,
+ dateFormatTZ: 'Zulu',
+ };
+ test('it should build a filter without chunking exception items', () => {
+ const exceptionFilter = buildExceptionFilter(
+ [
+ { language: 'kuery', query: 'host.name: linux and some.field: value' },
+ { language: 'kuery', query: 'user.name: name' },
+ ],
+ {
+ fields: [],
+ title: 'auditbeat-*',
+ },
+ config,
+ true,
+ 2
+ );
+ expect(exceptionFilter).toEqual({
+ meta: {
+ alias: null,
+ negate: true,
+ disabled: false,
+ },
+ query: {
+ bool: {
+ should: [
+ {
+ bool: {
+ filter: [
+ {
+ bool: {
+ minimum_should_match: 1,
+ should: [
+ {
+ match: {
+ 'host.name': 'linux',
+ },
+ },
+ ],
+ },
+ },
+ {
+ bool: {
+ minimum_should_match: 1,
+ should: [
+ {
+ match: {
+ 'some.field': 'value',
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ bool: {
+ minimum_should_match: 1,
+ should: [
+ {
+ match: {
+ 'user.name': 'name',
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ });
+ });
+
+ test('it should properly chunk exception items', () => {
+ const exceptionFilter = buildExceptionFilter(
+ [
+ { language: 'kuery', query: 'host.name: linux and some.field: value' },
+ { language: 'kuery', query: 'user.name: name' },
+ { language: 'kuery', query: 'file.path: /safe/path' },
+ ],
+ {
+ fields: [],
+ title: 'auditbeat-*',
+ },
+ config,
+ true,
+ 2
+ );
+ expect(exceptionFilter).toEqual({
+ meta: {
+ alias: null,
+ negate: true,
+ disabled: false,
+ },
+ query: {
+ bool: {
+ should: [
+ {
+ bool: {
+ should: [
+ {
+ bool: {
+ filter: [
+ {
+ bool: {
+ minimum_should_match: 1,
+ should: [
+ {
+ match: {
+ 'host.name': 'linux',
+ },
+ },
+ ],
+ },
+ },
+ {
+ bool: {
+ minimum_should_match: 1,
+ should: [
+ {
+ match: {
+ 'some.field': 'value',
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ bool: {
+ minimum_should_match: 1,
+ should: [
+ {
+ match: {
+ 'user.name': 'name',
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ bool: {
+ should: [
+ {
+ bool: {
+ minimum_should_match: 1,
+ should: [
+ {
+ match: {
+ 'file.path': '/safe/path',
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ });
+ });
+ });
});
diff --git a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts
index a41589b5d0231..466a004c14c66 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts
@@ -6,20 +6,21 @@
import {
Filter,
+ Query,
IIndexPattern,
isFilterDisabled,
buildEsQuery,
- Query as DataQuery,
+ EsQueryConfig,
} from '../../../../../src/plugins/data/common';
import {
ExceptionListItemSchema,
CreateExceptionListItemSchema,
} from '../../../lists/common/schemas';
-import { buildQueryExceptions } from './build_exceptions_query';
-import { Query, Language, Index } from './schemas/common/schemas';
+import { buildExceptionListQueries } from './build_exceptions_query';
+import { Query as QueryString, Language, Index } from './schemas/common/schemas';
export const getQueryFilter = (
- query: Query,
+ query: QueryString,
language: Language,
filters: Array>,
index: Index,
@@ -31,7 +32,14 @@ export const getQueryFilter = (
title: index.join(),
};
- const initialQuery = [{ query, language }];
+ const config: EsQueryConfig = {
+ allowLeadingWildcards: true,
+ queryStringOptions: { analyze_wildcard: true },
+ ignoreFilterIfFieldNotInIndex: false,
+ dateFormatTZ: 'Zulu',
+ };
+
+ const enabledFilters = ((filters as unknown) as Filter[]).filter((f) => !isFilterDisabled(f));
/*
* Pinning exceptions to 'kuery' because lucene
* does not support nested queries, while our exceptions
@@ -39,16 +47,78 @@ export const getQueryFilter = (
* buildEsQuery, this allows us to offer nested queries
* regardless
*/
- const exceptions = buildQueryExceptions({ language: 'kuery', lists, exclude: excludeExceptions });
- const queries: DataQuery[] = [...initialQuery, ...exceptions];
+ const exceptionQueries = buildExceptionListQueries({ language: 'kuery', lists });
+ if (exceptionQueries.length > 0) {
+ // Assume that `indices.query.bool.max_clause_count` is at least 1024 (the default value),
+ // allowing us to make 1024-item chunks of exception list items.
+ // Discussion at https://issues.apache.org/jira/browse/LUCENE-4835 indicates that 1024 is a
+ // very conservative value.
+ const exceptionFilter = buildExceptionFilter(
+ exceptionQueries,
+ indexPattern,
+ config,
+ excludeExceptions,
+ 1024
+ );
+ enabledFilters.push(exceptionFilter);
+ }
+ const initialQuery = { query, language };
- const config = {
- allowLeadingWildcards: true,
- queryStringOptions: { analyze_wildcard: true },
- ignoreFilterIfFieldNotInIndex: false,
- dateFormatTZ: 'Zulu',
- };
+ return buildEsQuery(indexPattern, initialQuery, enabledFilters, config);
+};
- const enabledFilters = ((filters as unknown) as Filter[]).filter((f) => !isFilterDisabled(f));
- return buildEsQuery(indexPattern, queries, enabledFilters, config);
+export const buildExceptionFilter = (
+ exceptionQueries: Query[],
+ indexPattern: IIndexPattern,
+ config: EsQueryConfig,
+ excludeExceptions: boolean,
+ chunkSize: number
+) => {
+ const exceptionFilter: Filter = {
+ meta: {
+ alias: null,
+ negate: excludeExceptions,
+ disabled: false,
+ },
+ query: {
+ bool: {
+ should: undefined,
+ },
+ },
+ };
+ if (exceptionQueries.length <= chunkSize) {
+ const query = buildEsQuery(indexPattern, exceptionQueries, [], config);
+ exceptionFilter.query.bool.should = query.bool.filter;
+ } else {
+ const chunkedFilters: Filter[] = [];
+ for (let index = 0; index < exceptionQueries.length; index += chunkSize) {
+ const exceptionQueriesChunk = exceptionQueries.slice(index, index + chunkSize);
+ const esQueryChunk = buildEsQuery(indexPattern, exceptionQueriesChunk, [], config);
+ const filterChunk: Filter = {
+ meta: {
+ alias: null,
+ negate: false,
+ disabled: false,
+ },
+ query: {
+ bool: {
+ should: esQueryChunk.bool.filter,
+ },
+ },
+ };
+ chunkedFilters.push(filterChunk);
+ }
+ // Here we build a query with only the exceptions: it will put them all in the `filter` array
+ // of the resulting object, which would AND the exceptions together. When creating exceptionFilter,
+ // we move the `filter` array to `should` so they are OR'd together instead.
+ // This gets around the problem with buildEsQuery not allowing callers to specify whether queries passed in
+ // should be ANDed or ORed together.
+ exceptionFilter.query.bool.should = buildEsQuery(
+ indexPattern,
+ [],
+ chunkedFilters,
+ config
+ ).bool.filter;
+ }
+ return exceptionFilter;
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.test.ts
index a5740d7719f47..56768ec78745d 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.test.ts
@@ -209,49 +209,52 @@ describe('get_filter', () => {
minimum_should_match: 1,
},
},
+ ],
+ must_not: [
{
bool: {
- must_not: {
- bool: {
- filter: [
- {
- nested: {
- path: 'some.parentField',
- query: {
- bool: {
- should: [
- {
- match_phrase: {
- 'some.parentField.nested.field': 'some value',
+ should: [
+ {
+ bool: {
+ filter: [
+ {
+ nested: {
+ path: 'some.parentField',
+ query: {
+ bool: {
+ should: [
+ {
+ match_phrase: {
+ 'some.parentField.nested.field': 'some value',
+ },
},
- },
- ],
- minimum_should_match: 1,
+ ],
+ minimum_should_match: 1,
+ },
},
+ score_mode: 'none',
},
- score_mode: 'none',
},
- },
- {
- bool: {
- should: [
- {
- match_phrase: {
- 'some.not.nested.field': 'some value',
+ {
+ bool: {
+ should: [
+ {
+ match_phrase: {
+ 'some.not.nested.field': 'some value',
+ },
},
- },
- ],
- minimum_should_match: 1,
+ ],
+ minimum_should_match: 1,
+ },
},
- },
- ],
+ ],
+ },
},
- },
+ ],
},
},
],
should: [],
- must_not: [],
},
});
});
From 0a3170ffa09d890e1b4fd0dc024e7c9c13b1ce04 Mon Sep 17 00:00:00 2001
From: Matthias Wilhelm
Date: Wed, 22 Jul 2020 17:09:38 +0200
Subject: [PATCH 14/49] Unskip dashboard filter bar code coverage test (#72799)
---
test/functional/apps/dashboard/dashboard_filter_bar.js | 2 --
1 file changed, 2 deletions(-)
diff --git a/test/functional/apps/dashboard/dashboard_filter_bar.js b/test/functional/apps/dashboard/dashboard_filter_bar.js
index dd0318ea5c0d7..273779a42d3f9 100644
--- a/test/functional/apps/dashboard/dashboard_filter_bar.js
+++ b/test/functional/apps/dashboard/dashboard_filter_bar.js
@@ -172,8 +172,6 @@ export default function ({ getService, getPageObjects }) {
});
describe('saved search filtering', function () {
- // https://github.com/elastic/kibana/issues/47286#issuecomment-644687577
- this.tags('skipCoverage');
before(async () => {
await filterBar.ensureFieldEditorModalIsClosed();
await PageObjects.dashboard.gotoDashboardLandingPage();
From 9374a42a5c7b364337a9ee4fc53a8086c1f457b7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?=
Date: Wed, 22 Jul 2020 11:27:38 -0400
Subject: [PATCH 15/49] Allow larger difference in index threshold jest test
(#72506)
* Allow large difference in index threshold jest test
* Fix variable name
---
.../alert_types/index_threshold/lib/date_range_info.test.ts | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/date_range_info.test.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/date_range_info.test.ts
index 32a5845a7e65b..a0b726c2510c0 100644
--- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/date_range_info.test.ts
+++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/date_range_info.test.ts
@@ -132,10 +132,11 @@ describe('getRangeInfo', () => {
it('should handle no dateStart, dateEnd or interval specified', async () => {
const nowM0 = Date.now();
const nowM5 = nowM0 - 1000 * 60 * 5;
+ const digitPrecision = 1;
const info = getDateRangeInfo(BaseRangeQuery);
- expect(sloppyMilliDiff(nowM5, Date.parse(info.dateStart))).toBeCloseTo(0);
- expect(sloppyMilliDiff(nowM0, Date.parse(info.dateEnd))).toBeCloseTo(0);
+ expect(sloppyMilliDiff(nowM5, Date.parse(info.dateStart))).toBeCloseTo(0, digitPrecision);
+ expect(sloppyMilliDiff(nowM0, Date.parse(info.dateEnd))).toBeCloseTo(0, digitPrecision);
expect(info.dateRanges.length).toEqual(1);
expect(info.dateRanges[0].from).toEqual(info.dateStart);
expect(info.dateRanges[0].to).toEqual(info.dateEnd);
From b346253a7a9888adb9b397c3390fd9e96d2aad8f Mon Sep 17 00:00:00 2001
From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com>
Date: Wed, 22 Jul 2020 11:32:55 -0400
Subject: [PATCH 16/49] Adding aggregations for endpoint events (#72705)
---
.../server/lib/overview/query.dsl.ts | 142 ++++++++++++++++--
1 file changed, 126 insertions(+), 16 deletions(-)
diff --git a/x-pack/plugins/security_solution/server/lib/overview/query.dsl.ts b/x-pack/plugins/security_solution/server/lib/overview/query.dsl.ts
index 8ac8233a86b82..b6b1cfea394fd 100644
--- a/x-pack/plugins/security_solution/server/lib/overview/query.dsl.ts
+++ b/x-pack/plugins/security_solution/server/lib/overview/query.dsl.ts
@@ -142,57 +142,167 @@ export const buildOverviewHostQuery = ({
},
endgame_module: {
filter: {
- term: {
- 'event.module': 'endgame',
+ bool: {
+ should: [
+ {
+ term: { 'event.module': 'endpoint' },
+ },
+ {
+ term: {
+ 'event.module': 'endgame',
+ },
+ },
+ ],
},
},
aggs: {
dns_event_count: {
filter: {
- term: {
- 'endgame.event_type_full': 'dns_event',
+ bool: {
+ should: [
+ {
+ bool: {
+ filter: [
+ { term: { 'network.protocol': 'dns' } },
+ { term: { 'event.category': 'network' } },
+ ],
+ },
+ },
+ {
+ term: {
+ 'endgame.event_type_full': 'dns_event',
+ },
+ },
+ ],
},
},
},
file_event_count: {
filter: {
- term: {
- 'endgame.event_type_full': 'file_event',
+ bool: {
+ should: [
+ {
+ term: {
+ 'event.category': 'file',
+ },
+ },
+ {
+ term: {
+ 'endgame.event_type_full': 'file_event',
+ },
+ },
+ ],
},
},
},
image_load_event_count: {
filter: {
- term: {
- 'endgame.event_type_full': 'image_load_event',
+ bool: {
+ should: [
+ {
+ bool: {
+ should: [
+ {
+ term: {
+ 'event.category': 'library',
+ },
+ },
+ {
+ term: {
+ 'event.category': 'driver',
+ },
+ },
+ ],
+ },
+ },
+ {
+ term: {
+ 'endgame.event_type_full': 'image_load_event',
+ },
+ },
+ ],
},
},
},
network_event_count: {
filter: {
- term: {
- 'endgame.event_type_full': 'network_event',
+ bool: {
+ should: [
+ {
+ bool: {
+ filter: [
+ {
+ bool: {
+ must_not: {
+ term: { 'network.protocol': 'dns' },
+ },
+ },
+ },
+ {
+ term: { 'event.category': 'network' },
+ },
+ ],
+ },
+ },
+ {
+ term: {
+ 'endgame.event_type_full': 'network_event',
+ },
+ },
+ ],
},
},
},
process_event_count: {
filter: {
- term: {
- 'endgame.event_type_full': 'process_event',
+ bool: {
+ should: [
+ {
+ term: { 'event.category': 'process' },
+ },
+ {
+ term: {
+ 'endgame.event_type_full': 'process_event',
+ },
+ },
+ ],
},
},
},
registry_event: {
filter: {
- term: {
- 'endgame.event_type_full': 'registry_event',
+ bool: {
+ should: [
+ {
+ term: { 'event.category': 'registry' },
+ },
+ {
+ term: {
+ 'endgame.event_type_full': 'registry_event',
+ },
+ },
+ ],
},
},
},
security_event_count: {
filter: {
- term: {
- 'endgame.event_type_full': 'security_event',
+ bool: {
+ should: [
+ {
+ bool: {
+ filter: [
+ { term: { 'event.category': 'session' } },
+ { term: { 'event.category': 'authentication' } },
+ ],
+ },
+ },
+ {
+ term: {
+ 'endgame.event_type_full': 'security_event',
+ },
+ },
+ ],
},
},
},
From 3b809bea912cd97c7001774df627cac927d92ff6 Mon Sep 17 00:00:00 2001
From: Pierre Gayvallet
Date: Wed, 22 Jul 2020 17:38:08 +0200
Subject: [PATCH 17/49] add migration section for the new ES client (#71604)
* add migration guide for the new client
* add missing breaking changes
* add paragraph on header override
---
src/core/MIGRATION_EXAMPLES.md | 261 ++++++++++++++++++++++++++++++++-
1 file changed, 260 insertions(+), 1 deletion(-)
diff --git a/src/core/MIGRATION_EXAMPLES.md b/src/core/MIGRATION_EXAMPLES.md
index 6bb5a845ea2ab..d630fec652a37 100644
--- a/src/core/MIGRATION_EXAMPLES.md
+++ b/src/core/MIGRATION_EXAMPLES.md
@@ -27,6 +27,10 @@ APIs to their New Platform equivalents.
- [Changes in structure compared to legacy](#changes-in-structure-compared-to-legacy)
- [Remarks](#remarks)
- [UiSettings](#uisettings)
+ - [Elasticsearch client](#elasticsearch-client)
+ - [Client API Changes](#client-api-changes)
+ - [Accessing the client from a route handler](#accessing-the-client-from-a-route-handler)
+ - [Creating a custom client](#creating-a-custom-client)
## Configuration
@@ -1003,4 +1007,259 @@ setup(core: CoreSetup){
},
})
}
-```
\ No newline at end of file
+```
+
+## Elasticsearch client
+
+The new elasticsearch client is a thin wrapper around `@elastic/elasticsearch`'s `Client` class. Even if the API
+is quite close to the legacy client Kibana was previously using, there are some subtle changes to take into account
+during migration.
+
+[Official documentation](https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html)
+
+### Client API Changes
+
+The most significant changes for the consumers are the following:
+
+- internal / current user client accessors has been renamed and are now properties instead of functions
+ - `callAsInternalUser('ping')` -> `asInternalUser.ping()`
+ - `callAsCurrentUser('ping')` -> `asCurrentUser.ping()`
+
+- the API now reflects the `Client`'s instead of leveraging the string-based endpoint names the `LegacyAPICaller` was using
+
+before:
+
+```ts
+const body = await client.callAsInternalUser('indices.get', { index: 'id' });
+```
+
+after:
+
+```ts
+const { body } = await client.asInternalUser.indices.get({ index: 'id' });
+```
+
+- calling any ES endpoint now returns the whole response object instead of only the body payload
+
+before:
+
+```ts
+const body = await legacyClient.callAsInternalUser('get', { id: 'id' });
+```
+
+after:
+
+```ts
+const { body } = await client.asInternalUser.get({ id: 'id' });
+```
+
+Note that more information from the ES response is available:
+
+```ts
+const {
+ body, // response payload
+ statusCode, // http status code of the response
+ headers, // response headers
+ warnings, // warnings returned from ES
+ meta // meta information about the request, such as request parameters, number of attempts and so on
+} = await client.asInternalUser.get({ id: 'id' });
+```
+
+- all API methods are now generic to allow specifying the response body type
+
+before:
+
+```ts
+const body: GetResponse = await legacyClient.callAsInternalUser('get', { id: 'id' });
+```
+
+after:
+
+```ts
+// body is of type `GetResponse`
+const { body } = await client.asInternalUser.get({ id: 'id' });
+// fallback to `Record` if unspecified
+const { body } = await client.asInternalUser.get({ id: 'id' });
+```
+
+- the returned error types changed
+
+There are no longer specific errors for every HTTP status code (such as `BadRequest` or `NotFound`). A generic
+`ResponseError` with the specific `statusCode` is thrown instead.
+
+before:
+
+```ts
+import { errors } from 'elasticsearch';
+try {
+ await legacyClient.callAsInternalUser('ping');
+} catch(e) {
+ if(e instanceof errors.NotFound) {
+ // do something
+ }
+}
+```
+
+after:
+
+```ts
+import { errors } from '@elastic/elasticsearch';
+try {
+ await client.asInternalUser.ping();
+} catch(e) {
+ if(e instanceof errors.ResponseError && e.statusCode === 404) {
+ // do something
+ }
+ // also possible, as all errors got a name property with the name of the class,
+ // so this slightly better in term of performances
+ if(e.name === 'ResponseError' && e.statusCode === 404) {
+ // do something
+ }
+}
+```
+
+- the parameter property names changed from camelCase to snake_case
+
+Even if technically, the javascript client accepts both formats, the typescript definitions are only defining the snake_case
+properties.
+
+before:
+
+```ts
+legacyClient.callAsCurrentUser('get', {
+ id: 'id',
+ storedFields: ['some', 'fields'],
+})
+```
+
+after:
+
+```ts
+client.asCurrentUser.get({
+ id: 'id',
+ stored_fields: ['some', 'fields'],
+})
+```
+
+- the request abortion API changed
+
+All promises returned from the client API calls now have an `abort` method that can be used to cancel the request.
+
+before:
+
+```ts
+const controller = new AbortController();
+legacyClient.callAsCurrentUser('ping', {}, {
+ signal: controller.signal,
+})
+// later
+controller.abort();
+```
+
+after:
+
+```ts
+const request = client.asCurrentUser.ping();
+// later
+request.abort();
+```
+
+- it is now possible to override headers when performing specific API calls.
+
+Note that doing so is strongly discouraged due to potential side effects with the ES service internal
+behavior when scoping as the internal or as the current user.
+
+```ts
+const request = client.asCurrentUser.ping({}, {
+ headers: {
+ authorization: 'foo',
+ custom: 'bar',
+ }
+});
+```
+
+Please refer to the [Breaking changes list](https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/breaking-changes.html)
+for more information about the changes between the legacy and new client.
+
+### Accessing the client from a route handler
+
+Apart from the API format change, accessing the client from within a route handler
+did not change. As it was done for the legacy client, a preconfigured scoped client
+bound to the request is accessible using `core` context provider:
+
+before:
+
+```ts
+router.get(
+ {
+ path: '/my-route',
+ },
+ async (context, req, res) => {
+ const { client } = context.core.elasticsearch.legacy;
+ // call as current user
+ const res = await client.callAsCurrentUser('ping');
+ // call as internal user
+ const res2 = await client.callAsInternalUser('search', options);
+ return res.ok({ body: 'ok' });
+ }
+);
+```
+
+after:
+
+```ts
+router.get(
+ {
+ path: '/my-route',
+ },
+ async (context, req, res) => {
+ const { client } = context.core.elasticsearch;
+ // call as current user
+ const res = await client.asCurrentUser.ping();
+ // call as internal user
+ const res2 = await client.asInternalUser.search(options);
+ return res.ok({ body: 'ok' });
+ }
+);
+```
+
+### Creating a custom client
+
+Note that the `plugins` option is now longer available on the new client. As the API is now exhaustive, adding custom
+endpoints using plugins should no longer be necessary.
+
+The API to create custom clients did not change much:
+
+before:
+
+```ts
+const customClient = coreStart.elasticsearch.legacy.createClient('my-custom-client', customConfig);
+// do something with the client, such as
+await customClient.callAsInternalUser('ping');
+// custom client are closable
+customClient.close();
+```
+
+after:
+
+```ts
+const customClient = coreStart.elasticsearch.createClient('my-custom-client', customConfig);
+// do something with the client, such as
+await customClient.asInternalUser.ping();
+// custom client are closable
+customClient.close();
+```
+
+If, for any reasons, one still needs to reach an endpoint not listed on the client API, using `request.transport`
+is still possible:
+
+```ts
+const { body } = await client.asCurrentUser.transport.request({
+ method: 'get',
+ path: '/my-custom-endpoint',
+ body: { my: 'payload'},
+ querystring: { param: 'foo' }
+})
+```
+
+Remark: the new client creation API is now only available from the `start` contract of the elasticsearch service.
From 8305d9f77560a8ce23c0cce29f83495f0511a0bd Mon Sep 17 00:00:00 2001
From: spalger
Date: Wed, 22 Jul 2020 08:48:20 -0700
Subject: [PATCH 18/49] skip flaky suite (#72864)
---
.../task_manager/server/lib/bulk_operation_buffer.test.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.test.ts b/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.test.ts
index 2c6d2b64f5d44..3a21f622cec17 100644
--- a/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.test.ts
+++ b/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.test.ts
@@ -33,7 +33,8 @@ function errorAttempts(task: TaskInstance): Err {
+// FLAKY: https://github.com/elastic/kibana/issues/72864
+describe.skip('Bulk Operation Buffer', () => {
describe('createBuffer()', () => {
test('batches up multiple Operation calls', async () => {
const bulkUpdate: jest.Mocked> = jest.fn(
From b23b3d90248504274280bfebc0e218e2e0ba76aa Mon Sep 17 00:00:00 2001
From: Larry Gregory
Date: Wed, 22 Jul 2020 12:03:45 -0400
Subject: [PATCH 19/49] De-duplicates dashboard feature definition (#72834)
---
.../__snapshots__/oss_features.test.ts.snap | 2 +-
.../plugins/features/server/oss_features.ts | 2 +-
.../apis/short_urls/feature_controls.ts | 83 ++++++++++++++++++-
3 files changed, 83 insertions(+), 4 deletions(-)
diff --git a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap
index 2c98dc132f259..bfbc8b68c3d2c 100644
--- a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap
+++ b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap
@@ -71,8 +71,8 @@ Array [
"savedObject": Object {
"all": Array [
"dashboard",
- "url",
"query",
+ "url",
],
"read": Array [
"index-pattern",
diff --git a/x-pack/plugins/features/server/oss_features.ts b/x-pack/plugins/features/server/oss_features.ts
index 8d40f51df5760..9df042b45a32e 100644
--- a/x-pack/plugins/features/server/oss_features.ts
+++ b/x-pack/plugins/features/server/oss_features.ts
@@ -148,7 +148,7 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
app: ['dashboards', 'kibana'],
catalogue: ['dashboard'],
savedObject: {
- all: ['dashboard', 'url', 'query'],
+ all: ['dashboard', 'query'],
read: [
'index-pattern',
'search',
diff --git a/x-pack/test/api_integration/apis/short_urls/feature_controls.ts b/x-pack/test/api_integration/apis/short_urls/feature_controls.ts
index 958a19c9d871e..640c60572bf9f 100644
--- a/x-pack/test/api_integration/apis/short_urls/feature_controls.ts
+++ b/x-pack/test/api_integration/apis/short_urls/feature_controls.ts
@@ -24,30 +24,37 @@ export default function featureControlsTests({ getService }: FtrProviderContext)
{
featureId: 'discover',
canAccess: true,
+ canCreate: true,
},
{
featureId: 'dashboard',
canAccess: true,
+ canCreate: true,
},
{
featureId: 'visualize',
canAccess: true,
+ canCreate: true,
},
{
featureId: 'infrastructure',
canAccess: true,
+ canCreate: false,
},
{
featureId: 'canvas',
canAccess: true,
+ canCreate: false,
},
{
featureId: 'maps',
canAccess: true,
+ canCreate: false,
},
{
featureId: 'unknown-feature',
canAccess: false,
+ canCreate: false,
},
];
@@ -64,12 +71,46 @@ export default function featureControlsTests({ getService }: FtrProviderContext)
},
],
});
+ await security.role.create(`${feature.featureId}-minimal-role`, {
+ kibana: [
+ {
+ base: [],
+ feature: {
+ [feature.featureId]: ['minimal_all'],
+ },
+ spaces: ['*'],
+ },
+ ],
+ });
+ await security.role.create(`${feature.featureId}-minimal-shorten-role`, {
+ kibana: [
+ {
+ base: [],
+ feature: {
+ [feature.featureId]: ['minimal_read', 'url_create'],
+ },
+ spaces: ['*'],
+ },
+ ],
+ });
await security.user.create(`${feature.featureId}-user`, {
password: kibanaUserPassword,
roles: [`${feature.featureId}-role`],
full_name: 'a kibana user',
});
+
+ await security.user.create(`${feature.featureId}-minimal-user`, {
+ password: kibanaUserPassword,
+ roles: [`${feature.featureId}-minimal-role`],
+ full_name: 'a kibana user',
+ });
+
+ await security.user.create(`${feature.featureId}-minimal-shorten-user`, {
+ password: kibanaUserPassword,
+ roles: [`${feature.featureId}-minimal-shorten-role`],
+ full_name: 'a kibana user',
+ });
}
await security.user.create(kibanaUsername, {
@@ -89,8 +130,16 @@ export default function featureControlsTests({ getService }: FtrProviderContext)
});
after(async () => {
- const users = features.map((feature) => security.user.delete(`${feature.featureId}-user`));
- const roles = features.map((feature) => security.role.delete(`${feature.featureId}-role`));
+ const users = features.flatMap((feature) => [
+ security.user.delete(`${feature.featureId}-user`),
+ security.user.delete(`${feature.featureId}-minimal-user`),
+ security.user.delete(`${feature.featureId}-minimal-shorten-user`),
+ ]);
+ const roles = features.flatMap((feature) => [
+ security.role.delete(`${feature.featureId}-role`),
+ security.role.delete(`${feature.featureId}-minimal-role`),
+ security.role.delete(`${feature.featureId}-minimal-shorten-role`),
+ ]);
await Promise.all([...users, ...roles]);
await security.user.delete(kibanaUsername);
});
@@ -112,6 +161,36 @@ export default function featureControlsTests({ getService }: FtrProviderContext)
}
});
});
+
+ it(`users with "minimal_all" access to ${feature.featureId} should not be able to create short-urls`, async () => {
+ await supertest
+ .post(`/api/shorten_url`)
+ .auth(`${feature.featureId}-minimal-user`, kibanaUserPassword)
+ .set('kbn-xsrf', 'foo')
+ .send({ url: '/app/dashboard' })
+ .then((resp: Record) => {
+ expect(resp.status).to.eql(403);
+ expect(resp.body.message).to.eql('Unable to create url');
+ });
+ });
+
+ it(`users with "url_create" access to ${feature.featureId} ${
+ feature.canCreate ? 'should' : 'should not'
+ } be able to create short-urls`, async () => {
+ await supertest
+ .post(`/api/shorten_url`)
+ .auth(`${feature.featureId}-minimal-shorten-user`, kibanaUserPassword)
+ .set('kbn-xsrf', 'foo')
+ .send({ url: '/app/dashboard' })
+ .then((resp: Record) => {
+ if (feature.canCreate) {
+ expect(resp.status).to.eql(200);
+ } else {
+ expect(resp.status).to.eql(403);
+ expect(resp.body.message).to.eql('Unable to create url');
+ }
+ });
+ });
});
});
}
From b12d19f8faa230c117ec060f95d76dc62ac40ede Mon Sep 17 00:00:00 2001
From: spalger
Date: Wed, 22 Jul 2020 09:11:20 -0700
Subject: [PATCH 20/49] skip flaky suite (#72803)
---
.../security_and_spaces/tests/alerting/update.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts
index ab3a92d0b3f70..cac6355409ac9 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts
@@ -31,7 +31,8 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
.then((response: SupertestResponse) => response.body);
}
- describe('update', () => {
+ // FLAKY: https://github.com/elastic/kibana/issues/72803
+ describe.skip('update', () => {
const objectRemover = new ObjectRemover(supertest);
after(() => objectRemover.removeAll());
From 0ed55f2e182e18d43cba009a697b9d7dae225b8c Mon Sep 17 00:00:00 2001
From: Andrew Cholakian
Date: Wed, 22 Jul 2020 11:16:51 -0500
Subject: [PATCH 21/49] [Uptime] Stop indexing saved object fields. (#72787)
Fixes https://github.com/elastic/kibana/issues/72782
---
x-pack/plugins/uptime/server/lib/saved_objects.ts | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/x-pack/plugins/uptime/server/lib/saved_objects.ts b/x-pack/plugins/uptime/server/lib/saved_objects.ts
index 5a61eb859c28b..8024aba198058 100644
--- a/x-pack/plugins/uptime/server/lib/saved_objects.ts
+++ b/x-pack/plugins/uptime/server/lib/saved_objects.ts
@@ -22,7 +22,11 @@ export const umDynamicSettings: SavedObjectsType = {
hidden: false,
namespaceType: 'single',
mappings: {
+ dynamic: false,
properties: {
+ /* Leaving these commented to make it clear that these fields exist, even though we don't want them indexed.
+ When adding new fields please add them here. If they need to be searchable put them in the uncommented
+ part of properties.
heartbeatIndices: {
type: 'keyword',
},
@@ -32,6 +36,7 @@ export const umDynamicSettings: SavedObjectsType = {
certExpirationThreshold: {
type: 'long',
},
+ */
},
},
};
From fe5b4b81a2467f2cce3cc79d593411f2bda76a8b Mon Sep 17 00:00:00 2001
From: gchaps <33642766+gchaps@users.noreply.github.com>
Date: Wed, 22 Jul 2020 09:17:33 -0700
Subject: [PATCH 22/49] [DOCS] Updates content in Introduction (#72545)
* [DOCS] Updates content in Introduction
* Update docs/user/introduction.asciidoc
Co-authored-by: Kaarina Tungseth
* Update docs/user/introduction.asciidoc
Co-authored-by: Kaarina Tungseth
Co-authored-by: Kaarina Tungseth
---
docs/user/introduction.asciidoc | 35 +++++++++---------
.../images/intro-data-tutorial.png | Bin 155429 -> 120824 bytes
.../introduction/images/intro-help-icon.png | Bin 0 -> 772 bytes
.../user/introduction/images/intro-kibana.png | Bin 367348 -> 596958 bytes
.../introduction/images/intro-management.png | Bin 190000 -> 190393 bytes
5 files changed, 18 insertions(+), 17 deletions(-)
create mode 100644 docs/user/introduction/images/intro-help-icon.png
diff --git a/docs/user/introduction.asciidoc b/docs/user/introduction.asciidoc
index 6438098ad2c60..ff936fb4d5569 100644
--- a/docs/user/introduction.asciidoc
+++ b/docs/user/introduction.asciidoc
@@ -4,9 +4,9 @@
What is Kibana?
++++
-**_Explore and visualize your data and manage all things Elastic Stack._**
+**_Visualize and analyze your data and manage all things Elastic Stack._**
-Whether you’re a user or admin, {kib} makes your data actionable by providing
+Whether you’re an analyst or an admin, {kib} makes your data actionable by providing
three key functions. Kibana is:
* **An open-source analytics and visualization platform.**
@@ -24,20 +24,20 @@ image::images/intro-kibana.png[]
[float]
[[get-data-into-kibana]]
-=== Getting data into {kib}
+=== Add data
{kib} is designed to use {es} as a data source. Think of Elasticsearch as the engine that stores
and processes the data, with {kib} sitting on top.
-From the home page, {kib} provides these options for getting data in:
+From the home page, {kib} provides these options for adding data:
+* Import data using the
+https://www.elastic.co/blog/importing-csv-and-log-data-into-elasticsearch-with-file-data-visualizer[File Data visualizer].
* Set up a data flow to Elasticsearch using our built-in tutorials.
-(If a tutorial doesn’t exist for your data, go to the
+If a tutorial doesn’t exist for your data, go to the
{beats-ref}/beats-reference.html[Beats overview] to learn about other data shippers
-in the {beats} family.)
+in the {beats} family.
* <> and take {kib} for a test drive without loading data yourself.
-* Import static data using the
-https://www.elastic.co/blog/importing-csv-and-log-data-into-elasticsearch-with-file-data-visualizer[file upload feature].
* Index your data into Elasticsearch with {ref}/getting-started-index.html[REST APIs]
or https://www.elastic.co/guide/en/elasticsearch/client/index.html[client libraries].
+
@@ -47,9 +47,9 @@ image::images/intro-data-tutorial.png[Ways to get data in from the home page]
{kib} uses an
<> to tell it which {es} indices to explore.
-If you add sample data or run a built-in tutorial, you get an index pattern for free,
+If you add upload a file, run a built-in tutorial, or add sample data, you get an index pattern for free,
and are good to start exploring. If you load your own data, you can create
-an index pattern in <>.
+an index pattern in <>.
[float]
[[explore-and-query]]
@@ -84,14 +84,14 @@ image::images/intro-dashboard.png[]
{kib} also offers these visualization features:
* <> allows you to display your data in
-line charts, bar graphs, pie charts, histograms, and tables
-(just to name a few). It's also home to Lens, the drag-and-drop interface.
+charts, graphs, and tables
+(just to name a few). It's also home to Lens.
Visualize supports the ability to add interactive
-controls to your dashboard, and filter dashboard content in real time.
+controls to your dashboard, filter dashboard content in real time, and add your own images and logos for your brand.
* <