diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index c836386d00364..01e6bd51ea50b 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -390,12 +390,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 3c420704fcb34..3d7409d1613e1 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 7c539f74924cb..dd91d85cd8493 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 be78a4d747b85..5589a15932ecf 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 c0a01bc85e916..98721c5675824 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 6007851f87084..a09788e45c394 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 1cbf3949d20f8..73b1de224eb32 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 10713d72a3858..104439ca4401f 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 0430d712e6267..b4e0324f9fead 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 7b4176fb69dbf..7d9d431d1c1be 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 3053c88f1c9ef..e589969c50e54 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 513f51f644534..4aaecc8e9d7df 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 9b58c124d0205..a39aaf3a3e2d1 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 6767468509d25..7d9bf20e22ace 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 c532b76a904d5..f0259c739654b 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 510e2a3b94894..6a41bf9a8b459 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 3bacbe4f0911b..3bfc3d736cda6 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 c40e4ea79d1c3..1fa8f755cc7ee 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 94b34034cd8b2..6a6fb7d660cbb 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 3a2cdb5397ad8..6749c11c77036 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 73279cd0c2ff0..a5a9353d83cbc 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 e2b44d84a2b7f..cfc04663c6a4f 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 8a1c6d31ec07f..87280169c0960 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 b98e820b5f67d..841c96acdc3b1 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 5d5692685c81b..57614aa816ff2 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 a3447e730c28d..accee08a00c61 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 0000000000000..7193a80b94498 --- /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 0000000000000..325d79651196b --- /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": {} + } + } + } +}