From 6e620ed444b2819285de73fc2eccf988d490339e Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Tue, 17 Dec 2019 12:27:04 -0500 Subject: [PATCH] adds per-actionType enablement via config xpack.actions.enabledActionTypes (#52967) (#53300) * adds per-actionType enablement via config xpack.actions.enabledTypes resolves: https://github.com/elastic/kibana/issues/52326 --- docs/setup/settings.asciidoc | 6 +- x-pack/legacy/plugins/actions/README.md | 3 + x-pack/legacy/plugins/actions/index.ts | 9 +- .../server/action_type_registry.mock.ts | 1 + .../server/action_type_registry.test.ts | 3 + .../actions/server/action_type_registry.ts | 21 ++- .../actions/server/actions_client.test.ts | 56 +++++++ .../plugins/actions/server/actions_client.ts | 7 + .../actions/server/actions_config.mock.ts | 2 + .../actions/server/actions_config.test.ts | 138 ++++++++++++++++-- .../plugins/actions/server/actions_config.ts | 53 +++++-- .../server/builtin_action_types/email.test.ts | 9 +- .../server/builtin_action_types/index.test.ts | 1 + .../server/lib/action_executor.test.ts | 33 +++++ .../actions/server/lib/action_executor.ts | 7 + .../legacy/plugins/actions/server/plugin.ts | 4 +- .../server/routes/list_action_types.test.ts | 1 + x-pack/legacy/plugins/actions/server/shim.ts | 1 + x-pack/legacy/plugins/actions/server/types.ts | 1 + .../alerting_api_integration/common/config.ts | 17 +++ .../common/fixtures/plugins/actions/index.ts | 11 ++ .../actions/builtin_action_types/pagerduty.ts | 2 +- .../actions/builtin_action_types/slack.ts | 2 +- .../actions/builtin_action_types/webhook.ts | 2 +- .../tests/actions/create.ts | 36 +++++ .../spaces_only/tests/actions/index.ts | 1 + .../tests/actions/type_not_enabled.ts | 109 ++++++++++++++ .../functional/es_archives/alerting/data.json | 15 ++ 28 files changed, 502 insertions(+), 49 deletions(-) create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts create mode 100644 x-pack/test/functional/es_archives/alerting/data.json diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index e5707a06d36d..0a45fde918be 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -396,12 +396,16 @@ Rollup user interface. `i18n.locale`:: *Default: en* Set this value to change the Kibana interface language. Valid locales are: `en`, `zh-CN`, `ja-JP`. +`xpack.actions.enabledActionTypes:`:: *Default: +[ {asterisk} ]+* Set this value +to an array of action types that are enabled. An element of `*` indicates all +action types registered are enabled. The action types provided by Kibana are: +`.server-log`, `.slack`, `.email`, `.index`, `.pagerduty`, `.webhook`. + `xpack.actions.whitelistedHosts:`:: *Default: +[ {asterisk} ]+* Set this value to an array of host names which actions such as email, slack, pagerduty, and webhook can connect to. An element of `*` indicates any host can be connected to. An empty array indicates no hosts can be connected to. - include::{docdir}/settings/apm-settings.asciidoc[] include::{docdir}/settings/dev-settings.asciidoc[] include::{docdir}/settings/graph-settings.asciidoc[] diff --git a/x-pack/legacy/plugins/actions/README.md b/x-pack/legacy/plugins/actions/README.md index 6efa979bc601..555b8c6830ba 100644 --- a/x-pack/legacy/plugins/actions/README.md +++ b/x-pack/legacy/plugins/actions/README.md @@ -34,6 +34,7 @@ Built-In-Actions are configured using the _xpack.actions_ namespoace under _kiba | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | | _xpack.actions._**enabled** | Feature toggle which enabled Actions in Kibana. | boolean | | _xpack.actions._**whitelistedHosts** | Which _hostnames_ are whitelisted for the Built-In-Action? This list should contain hostnames of every external service you wish to interact with using Webhooks, Email or any other built in Action. Note that you may use the string "\*" in place of a specific hostname to enable Kibana to target any URL, but keep in mind the potential use of such a feature to execute [SSRF](https://www.owasp.org/index.php/Server_Side_Request_Forgery) attacks from your server. | Array | +| _xpack.actions._**enabledActionTypes** | A list of _actionTypes_ id's that are enabled. A "\*" may be used as an element to indicate all registered actionTypes should be enabled. The actionTypes registered for Kibana are `.server-log`, `.slack`, `.email`, `.index`, `.pagerduty`, `.webhook`. Default: `["*"]` | Array | #### Whitelisting Built-in Action Types It is worth noting that the **whitelistedHosts** configuation applies to built-in action types (such as Slack, or PagerDuty) as well. @@ -49,8 +50,10 @@ This module provides a Utilities for interacting with the configuration. | --------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | isWhitelistedUri | _uri_: The URI you wish to validate is whitelisted | Validates whether the URI is whitelisted. This checks the configuration and validates that the hostname of the URI is in the list of whitelisted Hosts and returns `true` if it is whitelisted. If the configuration says that all URI's are whitelisted (using an "\*") then it will always return `true`. | Boolean | | isWhitelistedHostname | _hostname_: The Hostname you wish to validate is whitelisted | Validates whether the Hostname is whitelisted. This checks the configuration and validates that the hostname is in the list of whitelisted Hosts and returns `true` if it is whitelisted. If the configuration says that all Hostnames are whitelisted (using an "\*") then it will always return `true`. | Boolean | +| isActionTypeEnabled | _actionType_: The actionType to check to see if it's enabled | Returns true if the actionType is enabled, otherwise false. | Boolean | | ensureWhitelistedUri | _uri_: The URI you wish to validate is whitelisted | Validates whether the URI is whitelisted. This checks the configuration and validates that the hostname of the URI is in the list of whitelisted Hosts and throws an error if it is not whitelisted. If the configuration says that all URI's are whitelisted (using an "\*") then it will never throw. | No return value, throws if URI isn't whitelisted | | ensureWhitelistedHostname | _hostname_: The Hostname you wish to validate is whitelisted | Validates whether the Hostname is whitelisted. This checks the configuration and validates that the hostname is in the list of whitelisted Hosts and throws an error if it is not whitelisted. If the configuration says that all Hostnames are whitelisted (using an "\*") then it will never throw | No return value, throws if Hostname isn't whitelisted | +| ensureActionTypeEnabled | _actionType_: The actionType to check to see if it's enabled | Throws an error if the actionType is not enabled | No return value, throws if actionType isn't enabled | ## Action types diff --git a/x-pack/legacy/plugins/actions/index.ts b/x-pack/legacy/plugins/actions/index.ts index 7c539f74924c..dd91d85cd849 100644 --- a/x-pack/legacy/plugins/actions/index.ts +++ b/x-pack/legacy/plugins/actions/index.ts @@ -8,6 +8,7 @@ import { Legacy } from 'kibana'; import { Root } from 'joi'; import mappings from './mappings.json'; import { init } from './server'; +import { WhitelistedHosts, EnabledActionTypes } from './server/actions_config'; export { ActionsPlugin, @@ -38,10 +39,14 @@ export function actions(kibana: any) { .items( Joi.string() .hostname() - .allow('*') + .allow(WhitelistedHosts.Any) ) .sparse(false) - .default(['*']), + .default([WhitelistedHosts.Any]), + enabledActionTypes: Joi.array() + .items(Joi.string()) + .sparse(false) + .default([EnabledActionTypes.Any]), }) .default(); }, diff --git a/x-pack/legacy/plugins/actions/server/action_type_registry.mock.ts b/x-pack/legacy/plugins/actions/server/action_type_registry.mock.ts index be78a4d747b8..5589a15932ec 100644 --- a/x-pack/legacy/plugins/actions/server/action_type_registry.mock.ts +++ b/x-pack/legacy/plugins/actions/server/action_type_registry.mock.ts @@ -12,6 +12,7 @@ const createActionTypeRegistryMock = () => { register: jest.fn(), get: jest.fn(), list: jest.fn(), + ensureActionTypeEnabled: jest.fn(), }; return mocked; }; diff --git a/x-pack/legacy/plugins/actions/server/action_type_registry.test.ts b/x-pack/legacy/plugins/actions/server/action_type_registry.test.ts index c0a01bc85e91..98721c567582 100644 --- a/x-pack/legacy/plugins/actions/server/action_type_registry.test.ts +++ b/x-pack/legacy/plugins/actions/server/action_type_registry.test.ts @@ -8,11 +8,13 @@ import { taskManagerMock } from '../../task_manager/task_manager.mock'; import { ActionTypeRegistry } from './action_type_registry'; import { ExecutorType } from './types'; import { ActionExecutor, ExecutorError, TaskRunnerFactory } from './lib'; +import { configUtilsMock } from './actions_config.mock'; const mockTaskManager = taskManagerMock.create(); const actionTypeRegistryParams = { taskManager: mockTaskManager, taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor()), + actionsConfigUtils: configUtilsMock, }; beforeEach(() => jest.resetAllMocks()); @@ -123,6 +125,7 @@ describe('list()', () => { { id: 'my-action-type', name: 'My action type', + enabled: true, }, ]); }); diff --git a/x-pack/legacy/plugins/actions/server/action_type_registry.ts b/x-pack/legacy/plugins/actions/server/action_type_registry.ts index 6007851f8708..a09788e45c39 100644 --- a/x-pack/legacy/plugins/actions/server/action_type_registry.ts +++ b/x-pack/legacy/plugins/actions/server/action_type_registry.ts @@ -10,20 +10,23 @@ import { TaskManagerSetupContract } from './shim'; import { RunContext } from '../../task_manager'; import { ExecutorError, TaskRunnerFactory } from './lib'; import { ActionType } from './types'; - +import { ActionsConfigurationUtilities } from './actions_config'; interface ConstructorOptions { taskManager: TaskManagerSetupContract; taskRunnerFactory: TaskRunnerFactory; + actionsConfigUtils: ActionsConfigurationUtilities; } export class ActionTypeRegistry { private readonly taskManager: TaskManagerSetupContract; private readonly actionTypes: Map = new Map(); private readonly taskRunnerFactory: TaskRunnerFactory; + private readonly actionsConfigUtils: ActionsConfigurationUtilities; - constructor({ taskManager, taskRunnerFactory }: ConstructorOptions) { - this.taskManager = taskManager; - this.taskRunnerFactory = taskRunnerFactory; + constructor(constructorParams: ConstructorOptions) { + this.taskManager = constructorParams.taskManager; + this.taskRunnerFactory = constructorParams.taskRunnerFactory; + this.actionsConfigUtils = constructorParams.actionsConfigUtils; } /** @@ -33,6 +36,13 @@ export class ActionTypeRegistry { return this.actionTypes.has(id); } + /** + * Throws error if action type is not enabled. + */ + public ensureActionTypeEnabled(id: string) { + this.actionsConfigUtils.ensureActionTypeEnabled(id); + } + /** * Registers an action type to the action type registry */ @@ -86,12 +96,13 @@ export class ActionTypeRegistry { } /** - * Returns a list of registered action types [{ id, name }] + * Returns a list of registered action types [{ id, name, enabled }] */ public list() { return Array.from(this.actionTypes).map(([actionTypeId, actionType]) => ({ id: actionTypeId, name: actionType.name, + enabled: this.actionsConfigUtils.isActionTypeEnabled(actionTypeId), })); } } diff --git a/x-pack/legacy/plugins/actions/server/actions_client.test.ts b/x-pack/legacy/plugins/actions/server/actions_client.test.ts index 1cbf3949d20f..73b1de224eb3 100644 --- a/x-pack/legacy/plugins/actions/server/actions_client.test.ts +++ b/x-pack/legacy/plugins/actions/server/actions_client.test.ts @@ -11,6 +11,9 @@ import { ActionsClient } from './actions_client'; import { ExecutorType } from './types'; import { ActionExecutor, TaskRunnerFactory } from './lib'; import { taskManagerMock } from '../../task_manager/task_manager.mock'; +import { configUtilsMock } from './actions_config.mock'; +import { getActionsConfigurationUtilities } from './actions_config'; + import { elasticsearchServiceMock, savedObjectsClientMock, @@ -25,6 +28,7 @@ const mockTaskManager = taskManagerMock.create(); const actionTypeRegistryParams = { taskManager: mockTaskManager, taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor()), + actionsConfigUtils: configUtilsMock, }; let actionsClient: ActionsClient; @@ -190,6 +194,58 @@ describe('create()', () => { ] `); }); + + test('throws error creating action with disabled actionType', async () => { + const localConfigUtils = getActionsConfigurationUtilities({ + enabled: true, + enabledActionTypes: ['some-not-ignored-action-type'], + whitelistedHosts: ['*'], + }); + + const localActionTypeRegistryParams = { + taskManager: mockTaskManager, + taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor()), + actionsConfigUtils: localConfigUtils, + }; + + actionTypeRegistry = new ActionTypeRegistry(localActionTypeRegistryParams); + actionsClient = new ActionsClient({ + actionTypeRegistry, + savedObjectsClient, + scopedClusterClient, + defaultKibanaIndex, + }); + + const savedObjectCreateResult = { + id: '1', + type: 'type', + attributes: { + name: 'my name', + actionTypeId: 'my-action-type', + config: {}, + }, + references: [], + }; + actionTypeRegistry.register({ + id: 'my-action-type', + name: 'My action type', + executor, + }); + savedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult); + + await expect( + actionsClient.create({ + action: { + name: 'my name', + actionTypeId: 'my-action-type', + config: {}, + secrets: {}, + }, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"action type \\"my-action-type\\" is not enabled in the Kibana config xpack.actions.enabledActionTypes"` + ); + }); }); describe('get()', () => { diff --git a/x-pack/legacy/plugins/actions/server/actions_client.ts b/x-pack/legacy/plugins/actions/server/actions_client.ts index 10713d72a385..104439ca4401 100644 --- a/x-pack/legacy/plugins/actions/server/actions_client.ts +++ b/x-pack/legacy/plugins/actions/server/actions_client.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import Boom from 'boom'; import { IScopedClusterClient, SavedObjectsClientContract, @@ -92,6 +93,12 @@ export class ActionsClient { const validatedActionTypeConfig = validateConfig(actionType, config); const validatedActionTypeSecrets = validateSecrets(actionType, secrets); + try { + this.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); + } catch (err) { + throw Boom.badRequest(err.message); + } + const result = await this.savedObjectsClient.create('action', { actionTypeId, name, diff --git a/x-pack/legacy/plugins/actions/server/actions_config.mock.ts b/x-pack/legacy/plugins/actions/server/actions_config.mock.ts index 0430d712e626..b4e0324f9fea 100644 --- a/x-pack/legacy/plugins/actions/server/actions_config.mock.ts +++ b/x-pack/legacy/plugins/actions/server/actions_config.mock.ts @@ -9,6 +9,8 @@ import { ActionsConfigurationUtilities } from './actions_config'; export const configUtilsMock: ActionsConfigurationUtilities = { isWhitelistedHostname: _ => true, isWhitelistedUri: _ => true, + isActionTypeEnabled: _ => true, ensureWhitelistedHostname: _ => {}, ensureWhitelistedUri: _ => {}, + ensureActionTypeEnabled: _ => {}, }; diff --git a/x-pack/legacy/plugins/actions/server/actions_config.test.ts b/x-pack/legacy/plugins/actions/server/actions_config.test.ts index 7b4176fb69db..7d9d431d1c1b 100644 --- a/x-pack/legacy/plugins/actions/server/actions_config.test.ts +++ b/x-pack/legacy/plugins/actions/server/actions_config.test.ts @@ -5,13 +5,24 @@ */ import { ActionsConfigType } from './types'; -import { getActionsConfigurationUtilities, WhitelistedHosts } from './actions_config'; +import { + getActionsConfigurationUtilities, + WhitelistedHosts, + EnabledActionTypes, +} from './actions_config'; + +const DefaultActionsConfig: ActionsConfigType = { + enabled: false, + whitelistedHosts: [], + enabledActionTypes: [], +}; describe('ensureWhitelistedUri', () => { test('returns true when "any" hostnames are allowed', () => { const config: ActionsConfigType = { enabled: false, whitelistedHosts: [WhitelistedHosts.Any], + enabledActionTypes: [], }; expect( getActionsConfigurationUtilities(config).ensureWhitelistedUri( @@ -21,27 +32,31 @@ describe('ensureWhitelistedUri', () => { }); test('throws when the hostname in the requested uri is not in the whitelist', () => { - const config: ActionsConfigType = { enabled: false, whitelistedHosts: [] }; + const config: ActionsConfigType = DefaultActionsConfig; expect(() => getActionsConfigurationUtilities(config).ensureWhitelistedUri( 'https://github.com/elastic/kibana' ) ).toThrowErrorMatchingInlineSnapshot( - `"target url \\"https://github.com/elastic/kibana\\" is not in the Kibana whitelist"` + `"target url \\"https://github.com/elastic/kibana\\" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts"` ); }); test('throws when the uri cannot be parsed as a valid URI', () => { - const config: ActionsConfigType = { enabled: false, whitelistedHosts: [] }; + const config: ActionsConfigType = DefaultActionsConfig; expect(() => getActionsConfigurationUtilities(config).ensureWhitelistedUri('github.com/elastic') ).toThrowErrorMatchingInlineSnapshot( - `"target url \\"github.com/elastic\\" is not in the Kibana whitelist"` + `"target url \\"github.com/elastic\\" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts"` ); }); test('returns true when the hostname in the requested uri is in the whitelist', () => { - const config: ActionsConfigType = { enabled: false, whitelistedHosts: ['github.com'] }; + const config: ActionsConfigType = { + enabled: false, + whitelistedHosts: ['github.com'], + enabledActionTypes: [], + }; expect( getActionsConfigurationUtilities(config).ensureWhitelistedUri( 'https://github.com/elastic/kibana' @@ -55,6 +70,7 @@ describe('ensureWhitelistedHostname', () => { const config: ActionsConfigType = { enabled: false, whitelistedHosts: [WhitelistedHosts.Any], + enabledActionTypes: [], }; expect( getActionsConfigurationUtilities(config).ensureWhitelistedHostname('github.com') @@ -62,16 +78,20 @@ describe('ensureWhitelistedHostname', () => { }); test('throws when the hostname in the requested uri is not in the whitelist', () => { - const config: ActionsConfigType = { enabled: false, whitelistedHosts: [] }; + const config: ActionsConfigType = DefaultActionsConfig; expect(() => getActionsConfigurationUtilities(config).ensureWhitelistedHostname('github.com') ).toThrowErrorMatchingInlineSnapshot( - `"target hostname \\"github.com\\" is not in the Kibana whitelist"` + `"target hostname \\"github.com\\" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts"` ); }); test('returns true when the hostname in the requested uri is in the whitelist', () => { - const config: ActionsConfigType = { enabled: false, whitelistedHosts: ['github.com'] }; + const config: ActionsConfigType = { + enabled: false, + whitelistedHosts: ['github.com'], + enabledActionTypes: [], + }; expect( getActionsConfigurationUtilities(config).ensureWhitelistedHostname('github.com') ).toBeUndefined(); @@ -83,6 +103,7 @@ describe('isWhitelistedUri', () => { const config: ActionsConfigType = { enabled: false, whitelistedHosts: [WhitelistedHosts.Any], + enabledActionTypes: [], }; expect( getActionsConfigurationUtilities(config).isWhitelistedUri('https://github.com/elastic/kibana') @@ -90,21 +111,25 @@ describe('isWhitelistedUri', () => { }); test('throws when the hostname in the requested uri is not in the whitelist', () => { - const config: ActionsConfigType = { enabled: false, whitelistedHosts: [] }; + const config: ActionsConfigType = DefaultActionsConfig; expect( getActionsConfigurationUtilities(config).isWhitelistedUri('https://github.com/elastic/kibana') ).toEqual(false); }); test('throws when the uri cannot be parsed as a valid URI', () => { - const config: ActionsConfigType = { enabled: false, whitelistedHosts: [] }; + const config: ActionsConfigType = DefaultActionsConfig; expect(getActionsConfigurationUtilities(config).isWhitelistedUri('github.com/elastic')).toEqual( false ); }); test('returns true when the hostname in the requested uri is in the whitelist', () => { - const config: ActionsConfigType = { enabled: false, whitelistedHosts: ['github.com'] }; + const config: ActionsConfigType = { + enabled: false, + whitelistedHosts: ['github.com'], + enabledActionTypes: [], + }; expect( getActionsConfigurationUtilities(config).isWhitelistedUri('https://github.com/elastic/kibana') ).toEqual(true); @@ -116,6 +141,7 @@ describe('isWhitelistedHostname', () => { const config: ActionsConfigType = { enabled: false, whitelistedHosts: [WhitelistedHosts.Any], + enabledActionTypes: [], }; expect(getActionsConfigurationUtilities(config).isWhitelistedHostname('github.com')).toEqual( true @@ -123,16 +149,100 @@ describe('isWhitelistedHostname', () => { }); test('throws when the hostname in the requested uri is not in the whitelist', () => { - const config: ActionsConfigType = { enabled: false, whitelistedHosts: [] }; + const config: ActionsConfigType = DefaultActionsConfig; expect(getActionsConfigurationUtilities(config).isWhitelistedHostname('github.com')).toEqual( false ); }); test('returns true when the hostname in the requested uri is in the whitelist', () => { - const config: ActionsConfigType = { enabled: false, whitelistedHosts: ['github.com'] }; + const config: ActionsConfigType = { + enabled: false, + whitelistedHosts: ['github.com'], + enabledActionTypes: [], + }; expect(getActionsConfigurationUtilities(config).isWhitelistedHostname('github.com')).toEqual( true ); }); }); + +describe('isActionTypeEnabled', () => { + test('returns true when "any" actionTypes are allowed', () => { + const config: ActionsConfigType = { + enabled: false, + whitelistedHosts: [], + enabledActionTypes: ['ignore', EnabledActionTypes.Any], + }; + expect(getActionsConfigurationUtilities(config).isActionTypeEnabled('foo')).toEqual(true); + }); + + test('returns false when no actionType is allowed', () => { + const config: ActionsConfigType = { + enabled: false, + whitelistedHosts: [], + enabledActionTypes: [], + }; + expect(getActionsConfigurationUtilities(config).isActionTypeEnabled('foo')).toEqual(false); + }); + + test('returns false when the actionType is not in the enabled list', () => { + const config: ActionsConfigType = { + enabled: false, + whitelistedHosts: [], + enabledActionTypes: ['foo'], + }; + expect(getActionsConfigurationUtilities(config).isActionTypeEnabled('bar')).toEqual(false); + }); + + test('returns true when the actionType is in the enabled list', () => { + const config: ActionsConfigType = { + enabled: false, + whitelistedHosts: [], + enabledActionTypes: ['ignore', 'foo'], + }; + expect(getActionsConfigurationUtilities(config).isActionTypeEnabled('foo')).toEqual(true); + }); +}); + +describe('ensureActionTypeEnabled', () => { + test('does not throw when any actionType is allowed', () => { + const config: ActionsConfigType = { + enabled: false, + whitelistedHosts: [], + enabledActionTypes: ['ignore', EnabledActionTypes.Any], + }; + expect(getActionsConfigurationUtilities(config).ensureActionTypeEnabled('foo')).toBeUndefined(); + }); + + test('throws when no actionType is not allowed', () => { + const config: ActionsConfigType = DefaultActionsConfig; + expect(() => + getActionsConfigurationUtilities(config).ensureActionTypeEnabled('foo') + ).toThrowErrorMatchingInlineSnapshot( + `"action type \\"foo\\" is not enabled in the Kibana config xpack.actions.enabledActionTypes"` + ); + }); + + test('throws when actionType is not enabled', () => { + const config: ActionsConfigType = { + enabled: false, + whitelistedHosts: [], + enabledActionTypes: ['ignore'], + }; + expect(() => + getActionsConfigurationUtilities(config).ensureActionTypeEnabled('foo') + ).toThrowErrorMatchingInlineSnapshot( + `"action type \\"foo\\" is not enabled in the Kibana config xpack.actions.enabledActionTypes"` + ); + }); + + test('does not throw when actionType is enabled', () => { + const config: ActionsConfigType = { + enabled: false, + whitelistedHosts: [], + enabledActionTypes: ['ignore', 'foo'], + }; + expect(getActionsConfigurationUtilities(config).ensureActionTypeEnabled('foo')).toBeUndefined(); + }); +}); diff --git a/x-pack/legacy/plugins/actions/server/actions_config.ts b/x-pack/legacy/plugins/actions/server/actions_config.ts index 3053c88f1c9e..e589969c50e5 100644 --- a/x-pack/legacy/plugins/actions/server/actions_config.ts +++ b/x-pack/legacy/plugins/actions/server/actions_config.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { tryCatch, fromNullable, isSome, map, mapNullable, getOrElse } from 'fp-ts/lib/Option'; +import { tryCatch, map, mapNullable, getOrElse } from 'fp-ts/lib/Option'; import { URL } from 'url'; import { curry } from 'lodash'; import { pipe } from 'fp-ts/lib/pipeable'; @@ -16,6 +16,10 @@ export enum WhitelistedHosts { Any = '*', } +export enum EnabledActionTypes { + Any = '*', +} + enum WhitelistingField { url = 'url', hostname = 'hostname', @@ -24,13 +28,16 @@ enum WhitelistingField { export interface ActionsConfigurationUtilities { isWhitelistedHostname: (hostname: string) => boolean; isWhitelistedUri: (uri: string) => boolean; + isActionTypeEnabled: (actionType: string) => boolean; ensureWhitelistedHostname: (hostname: string) => void; ensureWhitelistedUri: (uri: string) => void; + ensureActionTypeEnabled: (actionType: string) => void; } function whitelistingErrorMessage(field: WhitelistingField, value: string) { return i18n.translate('xpack.actions.urlWhitelistConfigurationError', { - defaultMessage: 'target {field} "{value}" is not in the Kibana whitelist', + defaultMessage: + 'target {field} "{value}" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts', values: { value, field, @@ -38,22 +45,21 @@ function whitelistingErrorMessage(field: WhitelistingField, value: string) { }); } -function doesValueWhitelistAnyHostname(whitelistedHostname: string): boolean { - return whitelistedHostname === WhitelistedHosts.Any; +function disabledActionTypeErrorMessage(actionType: string) { + return i18n.translate('xpack.actions.disabledActionTypeError', { + defaultMessage: + 'action type "{actionType}" is not enabled in the Kibana config xpack.actions.enabledActionTypes', + values: { + actionType, + }, + }); } function isWhitelisted({ whitelistedHosts }: ActionsConfigType, hostname: string): boolean { - return ( - Array.isArray(whitelistedHosts) && - isSome( - fromNullable( - whitelistedHosts.find( - whitelistedHostname => - doesValueWhitelistAnyHostname(whitelistedHostname) || whitelistedHostname === hostname - ) - ) - ) - ); + const whitelisted = new Set(whitelistedHosts); + if (whitelisted.has(WhitelistedHosts.Any)) return true; + if (whitelisted.has(hostname)) return true; + return false; } function isWhitelistedHostnameInUri(config: ActionsConfigType, uri: string): boolean { @@ -65,14 +71,26 @@ function isWhitelistedHostnameInUri(config: ActionsConfigType, uri: string): boo ); } +function isActionTypeEnabledInConfig( + { enabledActionTypes }: ActionsConfigType, + actionType: string +): boolean { + const enabled = new Set(enabledActionTypes); + if (enabled.has(EnabledActionTypes.Any)) return true; + if (enabled.has(actionType)) return true; + return false; +} + export function getActionsConfigurationUtilities( config: ActionsConfigType ): ActionsConfigurationUtilities { const isWhitelistedHostname = curry(isWhitelisted)(config); const isWhitelistedUri = curry(isWhitelistedHostnameInUri)(config); + const isActionTypeEnabled = curry(isActionTypeEnabledInConfig)(config); return { isWhitelistedHostname, isWhitelistedUri, + isActionTypeEnabled, ensureWhitelistedUri(uri: string) { if (!isWhitelistedUri(uri)) { throw new Error(whitelistingErrorMessage(WhitelistingField.url, uri)); @@ -83,5 +101,10 @@ export function getActionsConfigurationUtilities( throw new Error(whitelistingErrorMessage(WhitelistingField.hostname, hostname)); } }, + ensureActionTypeEnabled(actionType: string) { + if (!isActionTypeEnabled(actionType)) { + throw new Error(disabledActionTypeErrorMessage(actionType)); + } + }, }; } diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/email.test.ts index 513f51f64453..4aaecc8e9d7d 100644 --- a/x-pack/legacy/plugins/actions/server/builtin_action_types/email.test.ts +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/email.test.ts @@ -12,7 +12,7 @@ import { Logger } from '../../../../../../src/core/server'; import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; import { ActionType, ActionTypeExecutorOptions } from '../types'; -import { ActionsConfigurationUtilities } from '../actions_config'; +import { configUtilsMock } from '../actions_config.mock'; import { validateConfig, validateSecrets, validateParams } from '../lib'; import { createActionTypeRegistry } from './index.test'; import { sendEmail } from './lib/send_email'; @@ -25,13 +25,6 @@ import { const sendEmailMock = sendEmail as jest.Mock; -const configUtilsMock: ActionsConfigurationUtilities = { - isWhitelistedHostname: _ => true, - isWhitelistedUri: _ => true, - ensureWhitelistedHostname: _ => {}, - ensureWhitelistedUri: _ => {}, -}; - const ACTION_TYPE_ID = '.email'; const NO_OP_FN = () => {}; diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/index.test.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/index.test.ts index 9b58c124d020..a39aaf3a3e2d 100644 --- a/x-pack/legacy/plugins/actions/server/builtin_action_types/index.test.ts +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/index.test.ts @@ -22,6 +22,7 @@ export function createActionTypeRegistry(): { const actionTypeRegistry = new ActionTypeRegistry({ taskManager: taskManagerMock.create(), taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor()), + actionsConfigUtils: configUtilsMock, }); registerBuiltInActionTypes({ logger, diff --git a/x-pack/legacy/plugins/actions/server/lib/action_executor.test.ts b/x-pack/legacy/plugins/actions/server/lib/action_executor.test.ts index 6767468509d2..7d9bf20e22ac 100644 --- a/x-pack/legacy/plugins/actions/server/lib/action_executor.test.ts +++ b/x-pack/legacy/plugins/actions/server/lib/action_executor.test.ts @@ -202,3 +202,36 @@ test('throws an error when failing to load action through savedObjectsClient', a `"No access"` ); }); + +test('returns an error if actionType is not enabled', async () => { + const actionType = { + id: 'test', + name: 'Test', + executor: jest.fn(), + }; + const actionSavedObject = { + id: '1', + type: 'action', + attributes: { + actionTypeId: 'test', + }, + references: [], + }; + savedObjectsClient.get.mockResolvedValueOnce(actionSavedObject); + encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject); + actionTypeRegistry.get.mockReturnValueOnce(actionType); + actionTypeRegistry.ensureActionTypeEnabled.mockImplementationOnce(() => { + throw new Error('not enabled for test'); + }); + const result = await actionExecutor.execute(executeParams); + + expect(actionTypeRegistry.ensureActionTypeEnabled).toHaveBeenCalledWith('test'); + expect(result).toMatchInlineSnapshot(` + Object { + "actionId": "1", + "message": "not enabled for test", + "retry": false, + "status": "error", + } + `); +}); diff --git a/x-pack/legacy/plugins/actions/server/lib/action_executor.ts b/x-pack/legacy/plugins/actions/server/lib/action_executor.ts index c532b76a904d..f0259c739654 100644 --- a/x-pack/legacy/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/legacy/plugins/actions/server/lib/action_executor.ts @@ -69,6 +69,13 @@ export class ActionExecutor { const { attributes: { actionTypeId, config, name }, } = await services.savedObjectsClient.get('action', actionId); + + try { + actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); + } catch (err) { + return { status: 'error', actionId, message: err.message, retry: false }; + } + // Only get encrypted attributes here, the remaining attributes can be fetched in // the savedObjectsClient call const { diff --git a/x-pack/legacy/plugins/actions/server/plugin.ts b/x-pack/legacy/plugins/actions/server/plugin.ts index 510e2a3b9489..6a41bf9a8b45 100644 --- a/x-pack/legacy/plugins/actions/server/plugin.ts +++ b/x-pack/legacy/plugins/actions/server/plugin.ts @@ -85,9 +85,11 @@ export class Plugin { const actionExecutor = new ActionExecutor(); const taskRunnerFactory = new TaskRunnerFactory(actionExecutor); + const actionsConfigUtils = getActionsConfigurationUtilities(config as ActionsConfigType); const actionTypeRegistry = new ActionTypeRegistry({ taskRunnerFactory, taskManager: plugins.task_manager, + actionsConfigUtils, }); this.taskRunnerFactory = taskRunnerFactory; this.actionTypeRegistry = actionTypeRegistry; @@ -97,7 +99,7 @@ export class Plugin { registerBuiltInActionTypes({ logger: this.logger, actionTypeRegistry, - actionsConfigUtils: getActionsConfigurationUtilities(config as ActionsConfigType), + actionsConfigUtils, }); // Routes diff --git a/x-pack/legacy/plugins/actions/server/routes/list_action_types.test.ts b/x-pack/legacy/plugins/actions/server/routes/list_action_types.test.ts index 3bacbe4f0911..3bfc3d736cda 100644 --- a/x-pack/legacy/plugins/actions/server/routes/list_action_types.test.ts +++ b/x-pack/legacy/plugins/actions/server/routes/list_action_types.test.ts @@ -23,6 +23,7 @@ it('calls the list function', async () => { { id: '1', name: 'One', + enabled: true, }, ]; diff --git a/x-pack/legacy/plugins/actions/server/shim.ts b/x-pack/legacy/plugins/actions/server/shim.ts index c40e4ea79d1c..1fa8f755cc7e 100644 --- a/x-pack/legacy/plugins/actions/server/shim.ts +++ b/x-pack/legacy/plugins/actions/server/shim.ts @@ -110,6 +110,7 @@ export function shim( return Rx.of({ enabled: server.config().get('xpack.actions.enabled') as boolean, whitelistedHosts: server.config().get('xpack.actions.whitelistedHosts') as string[], + enabledActionTypes: server.config().get('xpack.actions.enabledActionTypes') as string[], }) as Rx.Observable; }, }, diff --git a/x-pack/legacy/plugins/actions/server/types.ts b/x-pack/legacy/plugins/actions/server/types.ts index 94b34034cd8b..6a6fb7d660cb 100644 --- a/x-pack/legacy/plugins/actions/server/types.ts +++ b/x-pack/legacy/plugins/actions/server/types.ts @@ -27,6 +27,7 @@ export interface ActionsPlugin { export interface ActionsConfigType { enabled: boolean; whitelistedHosts: string[]; + enabledActionTypes: string[]; } // the parameters passed to an action type executor function diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 3a2cdb5397ad..6749c11c7703 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -16,6 +16,21 @@ interface CreateTestConfigOptions { ssl?: boolean; } +// test.not-enabled is specifically not enabled +const enabledActionTypes = [ + '.server-log', + '.slack', + '.email', + '.index', + '.pagerduty', + '.webhook', + 'test.noop', + 'test.index-record', + 'test.failing', + 'test.rate-limit', + 'test.authorization', +]; + // eslint-disable-next-line import/no-default-export export function createTestConfig(name: string, options: CreateTestConfigOptions) { const { license = 'trial', disabledPlugins = [], ssl = false } = options; @@ -57,6 +72,8 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) 'localhost', 'some.non.existent.com', ])}`, + `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, + '--xpack.alerting.enabled=true', ...disabledPlugins.map(key => `--xpack.${key}.enabled=false`), `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'alerts')}`, `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'actions')}`, diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/index.ts index 73279cd0c2ff..a5a9353d83cb 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/index.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/index.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import Hapi from 'hapi'; +import { ActionType } from '../../../../../../legacy/plugins/actions'; + import { initPlugin as initSlack } from './slack_simulation'; import { initPlugin as initWebhook } from './webhook_simulation'; import { initPlugin as initPagerduty } from './pagerduty_simulation'; @@ -32,6 +34,15 @@ export default function(kibana: any) { require: ['actions'], name: NAME, init: (server: Hapi.Server) => { + // this action is specifically NOT enabled in ../../config.ts + const notEnabledActionType: ActionType = { + id: 'test.not-enabled', + name: 'Test: Not Enabled', + async executor() { + return { status: 'ok', actionId: '' }; + }, + }; + server.plugins.actions!.setup.registerType(notEnabledActionType); server.plugins.xpack_main.registerFeature({ id: 'actions', name: 'Actions', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts index e2b44d84a2b7..cfc04663c6a4 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts @@ -111,7 +111,7 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'error validating action type config: error configuring pagerduty action: target url "https://events.pagerduty.com/v2/enqueue" is not in the Kibana whitelist', + 'error validating action type config: error configuring pagerduty action: target url "https://events.pagerduty.com/v2/enqueue" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts', }); }); }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts index 8a1c6d31ec07..87280169c096 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts @@ -103,7 +103,7 @@ export default function slackTest({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'error validating action type secrets: error configuring slack action: target url "http://slack.mynonexistent.com" is not in the Kibana whitelist', + 'error validating action type secrets: error configuring slack action: target url "http://slack.mynonexistent.com" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts', }); }); }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts index b98e820b5f67..841c96acdc3b 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts @@ -180,7 +180,7 @@ export default function webhookTest({ getService }: FtrProviderContext) { .expect(400); expect(result.error).to.eql('Bad Request'); - expect(result.message).to.match(/not in the Kibana whitelist/); + expect(result.message).to.match(/is not whitelisted in the Kibana config/); }); it('should handle unreachable webhook targets', async () => { 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 5d5692685c81..57614aa816ff 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 @@ -179,6 +179,42 @@ export default function createActionTests({ getService }: FtrProviderContext) { throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } }); + + it(`should handle create action requests for action types that are not enabled`, async () => { + const response = await supertestWithoutAuth + .post(`${getUrlPrefix(space.id)}/api/action`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .send({ + name: 'my name', + actionTypeId: 'test.not-enabled', + }); + + 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(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': + expect(response.statusCode).to.eql(400); + expect(response.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'action type "test.not-enabled" is not enabled in the Kibana config xpack.actions.enabledActionTypes', + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); }); } }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts index a3447e730c28..accee08a00c6 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts @@ -17,5 +17,6 @@ export default function actionsTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./update')); loadTestFile(require.resolve('./execute')); loadTestFile(require.resolve('./builtin_action_types/es_index')); + loadTestFile(require.resolve('./type_not_enabled')); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts new file mode 100644 index 000000000000..7193a80b9449 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts @@ -0,0 +1,109 @@ +/* + * 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'; + +const PREWRITTEN_ACTION_ID = 'uuid-actionId'; +const DISABLED_ACTION_TYPE = 'test.not-enabled'; + +// eslint-disable-next-line import/no-default-export +export default function typeNotEnabledTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('actionType not enabled', () => { + // loads action PREWRITTEN_ACTION_ID with actionType DISABLED_ACTION_TYPE + before(() => esArchiver.load('alerting')); + after(() => esArchiver.unload('alerting')); + + it('should handle create action with disabled actionType request appropriately', async () => { + const response = await supertest + .post(`/api/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action', + actionTypeId: DISABLED_ACTION_TYPE, + }); + + expect(response.statusCode).to.eql(400); + expect(response.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'action type "test.not-enabled" is not enabled in the Kibana config xpack.actions.enabledActionTypes', + }); + }); + + it(`should handle execute request with disabled actionType appropriately`, async () => { + const response = await supertest + .post(`/api/action/${PREWRITTEN_ACTION_ID}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: {}, + }); + + expect(response.statusCode).to.eql(200); + expect(response.body).to.eql({ + status: 'error', + retry: false, + actionId: PREWRITTEN_ACTION_ID, + message: + 'action type "test.not-enabled" is not enabled in the Kibana config xpack.actions.enabledActionTypes', + }); + }); + + it('should handle get action request with disabled actionType appropriately', async () => { + const response = await supertest.get(`/api/action/${PREWRITTEN_ACTION_ID}`); + + expect(response.statusCode).to.eql(200); + expect(response.body).to.eql({ + actionTypeId: 'test.not-enabled', + config: {}, + id: 'uuid-actionId', + name: 'an action created before test.not-enabled was disabled', + }); + }); + + it('should handle update action request with disabled actionType appropriately', async () => { + const responseUpdate = await supertest + .put(`/api/action/${PREWRITTEN_ACTION_ID}`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'an action created before test.not-enabled was disabled (updated)', + }); + + expect(responseUpdate.statusCode).to.eql(200); + expect(responseUpdate.body).to.eql({ + actionTypeId: 'test.not-enabled', + config: {}, + id: 'uuid-actionId', + name: 'an action created before test.not-enabled was disabled (updated)', + }); + + const response = await supertest.get(`/api/action/${PREWRITTEN_ACTION_ID}`); + expect(response.statusCode).to.eql(200); + expect(response.body).to.eql({ + actionTypeId: 'test.not-enabled', + config: {}, + id: 'uuid-actionId', + name: 'an action created before test.not-enabled was disabled (updated)', + }); + }); + + it('should handle delete action request with disabled actionType appropriately', async () => { + let response; + + response = await supertest + .delete(`/api/action/${PREWRITTEN_ACTION_ID}`) + .set('kbn-xsrf', 'foo'); + expect(response.statusCode).to.eql(204); + + response = await supertest.get(`/api/action/${PREWRITTEN_ACTION_ID}`); + expect(response.statusCode).to.eql(404); + }); + }); +} diff --git a/x-pack/test/functional/es_archives/alerting/data.json b/x-pack/test/functional/es_archives/alerting/data.json new file mode 100644 index 000000000000..325d79651196 --- /dev/null +++ b/x-pack/test/functional/es_archives/alerting/data.json @@ -0,0 +1,15 @@ +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "action:uuid-actionId", + "source": { + "type": "action", + "action": { + "actionTypeId": "test.not-enabled", + "name": "an action created before test.not-enabled was disabled", + "config": {} + } + } + } +}