From 134a3def037789ca4cae5cdcdcbd1f12eeb15852 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Thu, 27 May 2021 14:20:57 -0700 Subject: [PATCH] [Actions] Converted `rejectUnauthorized` config usages to `verificationMode`. (#100179) * [Actions] Converted `rejectUnauthorized` config usages to `verificationMode`. * added new verificationMode config options for tls, proxy tls and custom hosts * added unit tests * added unit tests * added kibana docker * Apply suggestions from code review Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update alert-action-settings.asciidoc * Apply suggestions from code review Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com> * removed legacyRegectUnauthorized logic from getNodeTLSOptions * added deprecations * fixed doc links * fixed docs * Update x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com> * [DOCS] Fixes build error * fixed deprecations to set custom message * fixed doc * changed to not throw exception on non existing verification mode * added tests * fixed tests * fixed tests * added integration tests for legacy rejectUnauthorized fale * fixed tests Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com> Co-authored-by: lcawl Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/settings/alert-action-settings.asciidoc | 46 +++- .../resources/base/bin/kibana-docker | 2 + .../actions/server/actions_client.test.ts | 8 +- .../actions/server/actions_config.mock.ts | 4 +- .../actions/server/actions_config.test.ts | 63 +++++- .../plugins/actions/server/actions_config.ts | 14 +- .../server/builtin_action_types/email.test.ts | 4 +- .../lib/axios_utils.test.ts | 28 ++- .../lib/axios_utils_connection.test.ts | 26 ++- .../lib/get_custom_agents.test.ts | 75 ++++--- .../lib/get_custom_agents.ts | 23 +- .../lib/get_node_tls_options.test.ts | 70 ++++++ .../lib/get_node_tls_options.ts | 57 +++++ .../lib/send_email.test.ts | 22 +- .../builtin_action_types/lib/send_email.ts | 25 ++- .../server/builtin_action_types/slack.test.ts | 20 +- .../server/builtin_action_types/teams.test.ts | 4 +- .../builtin_action_types/webhook.test.ts | 4 +- x-pack/plugins/actions/server/config.test.ts | 38 ++++ x-pack/plugins/actions/server/config.ts | 31 +++ x-pack/plugins/actions/server/index.ts | 34 ++- x-pack/plugins/actions/server/types.ts | 6 +- .../alerting_api_integration/common/config.ts | 13 +- .../spaces_only/config.ts | 2 +- .../spaces_only_legacy/config.ts | 19 ++ .../spaces_only_legacy/scenarios.ts | 35 +++ .../actions/builtin_action_types/webhook.ts | 201 ++++++++++++++++++ .../spaces_only_legacy/tests/index.ts | 33 +++ 28 files changed, 800 insertions(+), 107 deletions(-) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/lib/get_node_tls_options.test.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/lib/get_node_tls_options.ts create mode 100644 x-pack/test/alerting_api_integration/spaces_only_legacy/config.ts create mode 100644 x-pack/test/alerting_api_integration/spaces_only_legacy/scenarios.ts create mode 100644 x-pack/test/alerting_api_integration/spaces_only_legacy/tests/actions/builtin_action_types/webhook.ts create mode 100644 x-pack/test/alerting_api_integration/spaces_only_legacy/tests/index.ts diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index 50ed0d2652c6f..71f141d1ed5d6 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -41,7 +41,7 @@ You can configure the following settings in the `kibana.yml` file. [cols="2*<"] |=== | `xpack.actions.enabled` - | Feature toggle that enables Actions in {kib}. Defaults to `true`. + | Feature toggle that enables Actions in {kib}. Default: `true`. | `xpack.actions.allowedHosts` {ess-icon} | A list of hostnames that {kib} is allowed to connect to when built-in actions are triggered. It defaults to `[*]`, allowing any host, but keep in mind the potential for SSRF attacks when hosts are not explicitly added to the allowed hosts. An empty list `[]` can be used to block built-in actions from making any external connections. + @@ -50,7 +50,7 @@ You can configure the following settings in the `kibana.yml` file. | `xpack.actions.customHostSettings` {ess-icon} | A list of custom host settings to override existing global settings. - Defaults to an empty list. + + Default: an empty list. + + Each entry in the list must have a `url` property, to associate a connection type (mail or https), hostname and port with the remaining options in the @@ -70,6 +70,7 @@ You can configure the following settings in the `kibana.yml` file. xpack.actions.customHostSettings: - url: smtp://mail.example.com:465 tls: + verificationMode: 'full' certificateAuthoritiesFiles: [ 'one.crt' ] certificateAuthoritiesData: | -----BEGIN CERTIFICATE----- @@ -79,7 +80,9 @@ xpack.actions.customHostSettings: requireTLS: true - url: https://webhook.example.com tls: + // legacy rejectUnauthorized: false + verificationMode: 'none' -- [cols="2*<"] @@ -115,10 +118,16 @@ xpack.actions.customHostSettings: | `xpack.actions.customHostSettings[n]` `.tls.rejectUnauthorized` {ess-icon} - | A boolean value indicating whether to bypass server certificate validation. + | Deprecated. Use <> instead. A boolean value indicating whether to bypass server certificate validation. Overrides the general `xpack.actions.rejectUnauthorized` configuration for requests made for this hostname/port. +|[[action-config-custom-host-verification-mode]] `xpack.actions.customHostSettings[n]` +`.tls.verificationMode` + | Controls the verification of the server certificate that {hosted-ems} receives when making an outbound SSL/TLS connection to the host server. Valid values are `full`, `certificate`, and `none`. + Use `full` to perform hostname verification, `certificate` to skip hostname verification, and `none` to skip verification. Default: `full`. <>. Overrides the general `xpack.actions.tls.verificationMode` configuration + for requests made for this hostname/port. + | `xpack.actions.customHostSettings[n]` `.tls.certificateAuthoritiesFiles` | A file name or list of file names of PEM-encoded certificate files to use @@ -137,10 +146,10 @@ xpack.actions.customHostSettings: | `xpack.actions` `.preconfiguredAlertHistoryEsIndex` {ess-icon} - | Enables a preconfigured alert history {es} <> connector. Defaults to `false`. + | Enables a preconfigured alert history {es} <> connector. Default: `false`. | `xpack.actions.preconfigured` - | Specifies preconfigured connector IDs and configs. Defaults to {}. + | Specifies preconfigured connector IDs and configs. Default: {}. | `xpack.actions.proxyUrl` {ess-icon} | Specifies the proxy URL to use, if using a proxy for actions. By default, no proxy is used. @@ -152,27 +161,44 @@ xpack.actions.customHostSettings: | Specifies hostnames which should only use the proxy, if using a proxy for actions. The value is an array of hostnames as strings. By default, no hosts will use the proxy, but if an action's hostname is in this list, the proxy will be used. The settings `xpack.actions.proxyBypassHosts` and `xpack.actions.proxyOnlyHosts` cannot be used at the same time. | `xpack.actions.proxyHeaders` {ess-icon} - | Specifies HTTP headers for the proxy, if using a proxy for actions. Defaults to {}. + | Specifies HTTP headers for the proxy, if using a proxy for actions. Default: {}. a|`xpack.actions.` `proxyRejectUnauthorizedCertificates` {ess-icon} - | Set to `false` to bypass certificate validation for the proxy, if using a proxy for actions. Defaults to `true`. + | Deprecated. Use <> instead. Set to `false` to bypass certificate validation for the proxy, if using a proxy for actions. Default: `true`. + +|[[action-config-proxy-verification-mode]] +`xpack.actions[n]` +`.tls.proxyVerificationMode` {ess-icon} +| Controls the verification for the proxy server certificate that {hosted-ems} receives when making an outbound SSL/TLS connection to the proxy server. Valid values are `full`, `certificate`, and `none`. +Use `full` to perform hostname verification, `certificate` to skip hostname verification, and `none` to skip verification. Default: `full`. <>. | `xpack.actions.rejectUnauthorized` {ess-icon} - | Set to `false` to bypass certificate validation for actions. Defaults to `true`. + + | Deprecated. Use <> instead. Set to `false` to bypass certificate validation for actions. Default: `true`. + + As an alternative to setting `xpack.actions.rejectUnauthorized`, you can use the setting `xpack.actions.customHostSettings` to set TLS options for specific servers. +|[[action-config-verification-mode]] +`xpack.actions[n]` +`.tls.verificationMode` {ess-icon} +| Controls the verification for the server certificate that {hosted-ems} receives when making an outbound SSL/TLS connection for actions. Valid values are `full`, `certificate`, and `none`. + Use `full` to perform hostname verification, `certificate` to skip hostname verification, and `none` to skip verification. Default: `full`. <>. + + + + As an alternative to setting `xpack.actions.tls.verificationMode`, you can use the setting + `xpack.actions.customHostSettings` to set TLS options for specific servers. + + + | `xpack.actions.maxResponseContentLength` {ess-icon} - | Specifies the max number of bytes of the http response for requests to external resources. Defaults to 1000000 (1MB). + | Specifies the max number of bytes of the http response for requests to external resources. Default: 1000000 (1MB). | `xpack.actions.responseTimeout` {ess-icon} | Specifies the time allowed for requests to external resources. Requests that take longer are aborted. The time is formatted as: + + `[ms,s,m,h,d,w,M,Y]` + + - For example, `20m`, `24h`, `7d`, `1w`. Defaults to `60s`. + For example, `20m`, `24h`, `7d`, `1w`. Default: `60s`. |=== diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index 47b5888da4ce8..a1838c571ea0b 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -175,6 +175,8 @@ kibana_vars=( xpack.actions.rejectUnauthorized xpack.actions.maxResponseContentLength xpack.actions.responseTimeout + xpack.actions.tls.verificationMode + xpack.actions.tls.proxyVerificationMode xpack.alerts.healthCheck.interval xpack.alerts.invalidateApiKeysTask.interval xpack.alerts.invalidateApiKeysTask.removalDelay diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 30108a0777819..3b91b07eb30f4 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -417,8 +417,8 @@ describe('create()', () => { allowedHosts: ['*'], preconfiguredAlertHistoryEsIndex: false, preconfigured: {}, - proxyRejectUnauthorizedCertificates: true, - rejectUnauthorized: true, + proxyRejectUnauthorizedCertificates: true, // legacy + rejectUnauthorized: true, // legacy proxyBypassHosts: undefined, proxyOnlyHosts: undefined, maxResponseContentLength: new ByteSizeValue(1000000), @@ -429,6 +429,10 @@ describe('create()', () => { idleInterval: schema.duration().validate('1h'), pageSize: 100, }, + tls: { + verificationMode: 'full', + proxyVerificationMode: 'full', + }, }); const localActionTypeRegistryParams = { diff --git a/x-pack/plugins/actions/server/actions_config.mock.ts b/x-pack/plugins/actions/server/actions_config.mock.ts index fbd9a8cddbdcb..19a43951377b6 100644 --- a/x-pack/plugins/actions/server/actions_config.mock.ts +++ b/x-pack/plugins/actions/server/actions_config.mock.ts @@ -15,7 +15,9 @@ const createActionsConfigMock = () => { ensureHostnameAllowed: jest.fn().mockReturnValue({}), ensureUriAllowed: jest.fn().mockReturnValue({}), ensureActionTypeEnabled: jest.fn().mockReturnValue({}), - isRejectUnauthorizedCertificatesEnabled: jest.fn().mockReturnValue(true), + getTLSSettings: jest.fn().mockReturnValue({ + verificationMode: 'full', + }), getProxySettings: jest.fn().mockReturnValue(undefined), getResponseSettings: jest.fn().mockReturnValue({ maxContentLength: 1000000, diff --git a/x-pack/plugins/actions/server/actions_config.test.ts b/x-pack/plugins/actions/server/actions_config.test.ts index 925e77ca85fb2..93dad226e0c99 100644 --- a/x-pack/plugins/actions/server/actions_config.test.ts +++ b/x-pack/plugins/actions/server/actions_config.test.ts @@ -27,8 +27,8 @@ const defaultActionsConfig: ActionsConfig = { enabledActionTypes: [], preconfiguredAlertHistoryEsIndex: false, preconfigured: {}, - proxyRejectUnauthorizedCertificates: true, - rejectUnauthorized: true, + proxyRejectUnauthorizedCertificates: true, // legacy + rejectUnauthorized: true, // legacy maxResponseContentLength: new ByteSizeValue(1000000), responseTimeout: moment.duration(60000), cleanupFailedExecutionsTask: { @@ -37,6 +37,10 @@ const defaultActionsConfig: ActionsConfig = { idleInterval: schema.duration().validate('1h'), pageSize: 100, }, + tls: { + proxyVerificationMode: 'full', + verificationMode: 'full', + }, }; describe('ensureUriAllowed', () => { @@ -305,22 +309,45 @@ describe('getProxySettings', () => { expect(proxySettings?.proxyUrl).toBe(config.proxyUrl); }); - test('returns proxyRejectUnauthorizedCertificates', () => { + test('returns proper verificationMode values, beased on the legacy config option proxyRejectUnauthorizedCertificates', () => { const configTrue: ActionsConfig = { ...defaultActionsConfig, proxyUrl: 'https://proxy.elastic.co', proxyRejectUnauthorizedCertificates: true, }; let proxySettings = getActionsConfigurationUtilities(configTrue).getProxySettings(); - expect(proxySettings?.proxyRejectUnauthorizedCertificates).toBe(true); + expect(proxySettings?.proxyTLSSettings.verificationMode).toBe('full'); const configFalse: ActionsConfig = { ...defaultActionsConfig, proxyUrl: 'https://proxy.elastic.co', proxyRejectUnauthorizedCertificates: false, + tls: {}, + }; + proxySettings = getActionsConfigurationUtilities(configFalse).getProxySettings(); + expect(proxySettings?.proxyTLSSettings.verificationMode).toBe('none'); + }); + + test('returns proper verificationMode value, based on the TLS proxy configuration', () => { + const configTrue: ActionsConfig = { + ...defaultActionsConfig, + proxyUrl: 'https://proxy.elastic.co', + tls: { + proxyVerificationMode: 'full', + }, + }; + let proxySettings = getActionsConfigurationUtilities(configTrue).getProxySettings(); + expect(proxySettings?.proxyTLSSettings.verificationMode).toBe('full'); + + const configFalse: ActionsConfig = { + ...defaultActionsConfig, + proxyUrl: 'https://proxy.elastic.co', + tls: { + proxyVerificationMode: 'none', + }, }; proxySettings = getActionsConfigurationUtilities(configFalse).getProxySettings(); - expect(proxySettings?.proxyRejectUnauthorizedCertificates).toBe(false); + expect(proxySettings?.proxyTLSSettings.verificationMode).toBe('none'); }); test('returns proxy headers', () => { @@ -406,13 +433,13 @@ describe('getProxySettings', () => { { url: 'https://elastic.co', tls: { - rejectUnauthorized: true, + verificationMode: 'full', }, }, { url: 'smtp://elastic.co:123', tls: { - rejectUnauthorized: false, + verificationMode: 'none', }, smtp: { ignoreTLS: true, @@ -437,3 +464,25 @@ describe('getProxySettings', () => { expect(chs).toEqual(undefined); }); }); + +describe('getTLSSettings', () => { + test('returns proper verificationMode value, based on the TLS proxy configuration', () => { + const configTrue: ActionsConfig = { + ...defaultActionsConfig, + tls: { + verificationMode: 'full', + }, + }; + let tlsSettings = getActionsConfigurationUtilities(configTrue).getTLSSettings(); + expect(tlsSettings.verificationMode).toBe('full'); + + const configFalse: ActionsConfig = { + ...defaultActionsConfig, + tls: { + verificationMode: 'none', + }, + }; + tlsSettings = getActionsConfigurationUtilities(configFalse).getTLSSettings(); + expect(tlsSettings.verificationMode).toBe('none'); + }); +}); diff --git a/x-pack/plugins/actions/server/actions_config.ts b/x-pack/plugins/actions/server/actions_config.ts index b8cd5878a8972..d25101f8279f8 100644 --- a/x-pack/plugins/actions/server/actions_config.ts +++ b/x-pack/plugins/actions/server/actions_config.ts @@ -14,7 +14,8 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { ActionsConfig, AllowedHosts, EnabledActionTypes, CustomHostSettings } from './config'; import { getCanonicalCustomHostUrl } from './lib/custom_host_settings'; import { ActionTypeDisabledError } from './lib'; -import { ProxySettings, ResponseSettings } from './types'; +import { ProxySettings, ResponseSettings, TLSSettings } from './types'; +import { getTLSSettingsFromConfig } from './builtin_action_types/lib/get_node_tls_options'; export { AllowedHosts, EnabledActionTypes } from './config'; @@ -30,7 +31,7 @@ export interface ActionsConfigurationUtilities { ensureHostnameAllowed: (hostname: string) => void; ensureUriAllowed: (uri: string) => void; ensureActionTypeEnabled: (actionType: string) => void; - isRejectUnauthorizedCertificatesEnabled: () => boolean; + getTLSSettings: () => TLSSettings; getProxySettings: () => undefined | ProxySettings; getResponseSettings: () => ResponseSettings; getCustomHostSettings: (targetUrl: string) => CustomHostSettings | undefined; @@ -93,7 +94,10 @@ function getProxySettingsFromConfig(config: ActionsConfig): undefined | ProxySet proxyBypassHosts: arrayAsSet(config.proxyBypassHosts), proxyOnlyHosts: arrayAsSet(config.proxyOnlyHosts), proxyHeaders: config.proxyHeaders, - proxyRejectUnauthorizedCertificates: config.proxyRejectUnauthorizedCertificates, + proxyTLSSettings: getTLSSettingsFromConfig( + config.tls?.proxyVerificationMode, + config.proxyRejectUnauthorizedCertificates + ), }; } @@ -142,8 +146,8 @@ export function getActionsConfigurationUtilities( isActionTypeEnabled, getProxySettings: () => getProxySettingsFromConfig(config), getResponseSettings: () => getResponseSettingsFromConfig(config), - // returns the global rejectUnauthorized setting - isRejectUnauthorizedCertificatesEnabled: () => config.rejectUnauthorized, + getTLSSettings: () => + getTLSSettingsFromConfig(config.tls?.verificationMode, config.rejectUnauthorized), ensureUriAllowed(uri: string) { if (!isUriAllowed(uri)) { throw new Error(allowListErrorMessage(AllowListingField.URL, uri)); diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts index 5747b4bbb28f4..98ea436b17f3e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts @@ -285,9 +285,9 @@ describe('execute()', () => { "getCustomHostSettings": [MockFunction], "getProxySettings": [MockFunction], "getResponseSettings": [MockFunction], + "getTLSSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], - "isRejectUnauthorizedCertificatesEnabled": [MockFunction], "isUriAllowed": [MockFunction], }, "content": Object { @@ -346,9 +346,9 @@ describe('execute()', () => { "getCustomHostSettings": [MockFunction], "getProxySettings": [MockFunction], "getResponseSettings": [MockFunction], + "getTLSSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], - "isRejectUnauthorizedCertificatesEnabled": [MockFunction], "isUriAllowed": [MockFunction], }, "content": Object { diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts index edc9429e4fac6..ccd5a044971df 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts @@ -18,7 +18,7 @@ import { getCustomAgents } from './get_custom_agents'; const TestUrl = 'https://elastic.co/foo/bar/baz'; const logger = loggingSystemMock.create().get() as jest.Mocked; -const configurationUtilities = actionsConfigMock.create(); +let configurationUtilities = actionsConfigMock.create(); jest.mock('axios'); const axiosMock = (axios as unknown) as jest.Mock; @@ -42,6 +42,7 @@ describe('request', () => { headers: { 'content-type': 'application/json' }, data: { incidentId: '123' }, })); + configurationUtilities = actionsConfigMock.create(); configurationUtilities.getResponseSettings.mockReturnValue({ maxContentLength: 1000000, timeout: 360000, @@ -74,7 +75,9 @@ describe('request', () => { test('it have been called with proper proxy agent for a valid url', async () => { configurationUtilities.getProxySettings.mockReturnValue({ - proxyRejectUnauthorizedCertificates: true, + proxyTLSSettings: { + verificationMode: 'full', + }, proxyUrl: 'https://localhost:1212', proxyBypassHosts: undefined, proxyOnlyHosts: undefined, @@ -107,7 +110,9 @@ describe('request', () => { test('it have been called with proper proxy agent for an invalid url', async () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: ':nope:', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: undefined, proxyOnlyHosts: undefined, }); @@ -136,7 +141,9 @@ describe('request', () => { test('it bypasses with proxyBypassHosts when expected', async () => { configurationUtilities.getProxySettings.mockReturnValue({ - proxyRejectUnauthorizedCertificates: true, + proxyTLSSettings: { + verificationMode: 'full', + }, proxyUrl: 'https://elastic.proxy.co', proxyBypassHosts: new Set(['elastic.co']), proxyOnlyHosts: undefined, @@ -157,7 +164,9 @@ describe('request', () => { test('it does not bypass with proxyBypassHosts when expected', async () => { configurationUtilities.getProxySettings.mockReturnValue({ - proxyRejectUnauthorizedCertificates: true, + proxyTLSSettings: { + verificationMode: 'full', + }, proxyUrl: 'https://elastic.proxy.co', proxyBypassHosts: new Set(['not-elastic.co']), proxyOnlyHosts: undefined, @@ -178,7 +187,9 @@ describe('request', () => { test('it proxies with proxyOnlyHosts when expected', async () => { configurationUtilities.getProxySettings.mockReturnValue({ - proxyRejectUnauthorizedCertificates: true, + proxyTLSSettings: { + verificationMode: 'full', + }, proxyUrl: 'https://elastic.proxy.co', proxyBypassHosts: undefined, proxyOnlyHosts: new Set(['elastic.co']), @@ -199,7 +210,9 @@ describe('request', () => { test('it does not proxy with proxyOnlyHosts when expected', async () => { configurationUtilities.getProxySettings.mockReturnValue({ - proxyRejectUnauthorizedCertificates: true, + proxyTLSSettings: { + verificationMode: 'full', + }, proxyUrl: 'https://elastic.proxy.co', proxyBypassHosts: undefined, proxyOnlyHosts: new Set(['not-elastic.co']), @@ -252,6 +265,7 @@ describe('patch', () => { status: 200, headers: { 'content-type': 'application/json' }, })); + configurationUtilities = actionsConfigMock.create(); configurationUtilities.getResponseSettings.mockReturnValue({ maxContentLength: 1000000, timeout: 360000, diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils_connection.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils_connection.test.ts index 80bf51e19c379..235fca005e225 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils_connection.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils_connection.test.ts @@ -81,23 +81,25 @@ describe('axios connections', () => { await expect(fn()).rejects.toThrow('certificate'); }); - test('it works with rejectUnauthorized false config', async () => { + test('it works with verificationMode "none" config', async () => { const { url, server } = await createServer(true); testServer = server; const configurationUtilities = getACUfromConfig({ - rejectUnauthorized: false, + tls: { + verificationMode: 'none', + }, }); const res = await request({ axios, url, logger, configurationUtilities }); expect(res.status).toBe(200); }); - test('it works with rejectUnauthorized custom host config', async () => { + test('it works with verificationMode "none" for custom host config', async () => { const { url, server } = await createServer(true); testServer = server; const configurationUtilities = getACUfromConfig({ - customHostSettings: [{ url, tls: { rejectUnauthorized: false } }], + customHostSettings: [{ url, tls: { verificationMode: 'none' } }], }); const res = await request({ axios, url, logger, configurationUtilities }); expect(res.status).toBe(200); @@ -125,7 +127,7 @@ describe('axios connections', () => { await expect(fn()).rejects.toThrow('certificate'); }); - test('it works with incorrect ca in custom host config but rejectUnauthorized false', async () => { + test('it works with incorrect ca in custom host config but verificationMode "none"', async () => { const { url, server } = await createServer(true); testServer = server; @@ -135,7 +137,7 @@ describe('axios connections', () => { url, tls: { certificateAuthoritiesData: CA, - rejectUnauthorized: false, + verificationMode: 'none', }, }, ], @@ -144,12 +146,14 @@ describe('axios connections', () => { expect(res.status).toBe(200); }); - test('it works with incorrect ca in custom host config but rejectUnauthorized config true', async () => { + test('it works with incorrect ca in custom host config but verificationMode config "full"', async () => { const { url, server } = await createServer(true); testServer = server; const configurationUtilities = getACUfromConfig({ - rejectUnauthorized: false, + tls: { + verificationMode: 'none', + }, customHostSettings: [ { url, @@ -169,7 +173,7 @@ describe('axios connections', () => { testServer = server; const configurationUtilities = getACUfromConfig({ - customHostSettings: [{ url: otherUrl, tls: { rejectUnauthorized: false } }], + customHostSettings: [{ url: otherUrl, tls: { verificationMode: 'none' } }], }); const fn = async () => await request({ axios, url, logger, configurationUtilities }); await expect(fn()).rejects.toThrow('certificate'); @@ -251,6 +255,10 @@ const BaseActionsConfig: ActionsConfig = { proxyUrl: undefined, proxyHeaders: undefined, proxyRejectUnauthorizedCertificates: true, + tls: { + proxyVerificationMode: 'full', + verificationMode: 'full', + }, proxyBypassHosts: undefined, proxyOnlyHosts: undefined, rejectUnauthorized: true, diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts index 805c22806ce4c..8b4abe86e271a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts @@ -20,16 +20,19 @@ const targetUrlCanonical = `https://${targetHost}:443`; const nonMatchingUrl = `https://${targetHost}m/foo/bar/baz`; describe('getCustomAgents', () => { - const configurationUtilities = actionsConfigMock.create(); + let configurationUtilities = actionsConfigMock.create(); beforeEach(() => { jest.resetAllMocks(); + configurationUtilities = actionsConfigMock.create(); }); test('get agents for valid proxy URL', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: undefined, proxyOnlyHosts: undefined, }); @@ -41,7 +44,9 @@ describe('getCustomAgents', () => { test('return default agents for invalid proxy URL', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: ':nope: not a valid URL', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: undefined, proxyOnlyHosts: undefined, }); @@ -59,7 +64,9 @@ describe('getCustomAgents', () => { test('returns non-proxy agents for matching proxyBypassHosts', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: new Set([targetHost]), proxyOnlyHosts: undefined, }); @@ -71,7 +78,9 @@ describe('getCustomAgents', () => { test('returns proxy agents for non-matching proxyBypassHosts', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: new Set([targetHost]), proxyOnlyHosts: undefined, }); @@ -87,7 +96,9 @@ describe('getCustomAgents', () => { test('returns proxy agents for matching proxyOnlyHosts', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: undefined, proxyOnlyHosts: new Set([targetHost]), }); @@ -99,7 +110,9 @@ describe('getCustomAgents', () => { test('returns non-proxy agents for non-matching proxyOnlyHosts', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: undefined, proxyOnlyHosts: new Set([targetHost]), }); @@ -116,7 +129,7 @@ describe('getCustomAgents', () => { configurationUtilities.getCustomHostSettings.mockReturnValue({ url: targetUrlCanonical, tls: { - rejectUnauthorized: false, + verificationMode: 'none', certificateAuthoritiesData: 'ca data here', }, }); @@ -128,14 +141,16 @@ describe('getCustomAgents', () => { test('handles custom host settings with proxy', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: undefined, proxyOnlyHosts: undefined, }); configurationUtilities.getCustomHostSettings.mockReturnValue({ url: targetUrlCanonical, tls: { - rejectUnauthorized: false, + verificationMode: 'none', certificateAuthoritiesData: 'ca data here', }, }); @@ -147,12 +162,14 @@ describe('getCustomAgents', () => { expect(httpsAgent?.options.rejectUnauthorized).toBe(false); }); - test('handles overriding global rejectUnauthorized false', () => { - configurationUtilities.isRejectUnauthorizedCertificatesEnabled.mockReturnValue(false); + test('handles overriding global verificationMode "none"', () => { + configurationUtilities.getTLSSettings.mockReturnValue({ + verificationMode: 'none', + }); configurationUtilities.getCustomHostSettings.mockReturnValue({ url: targetUrlCanonical, tls: { - rejectUnauthorized: true, + verificationMode: 'certificate', }, }); @@ -163,12 +180,14 @@ describe('getCustomAgents', () => { expect(httpsAgent?.options.rejectUnauthorized).toBeTruthy(); }); - test('handles overriding global rejectUnauthorized true', () => { - configurationUtilities.isRejectUnauthorizedCertificatesEnabled.mockReturnValue(true); + test('handles overriding global verificationMode "full"', () => { + configurationUtilities.getTLSSettings.mockReturnValue({ + verificationMode: 'full', + }); configurationUtilities.getCustomHostSettings.mockReturnValue({ url: targetUrlCanonical, tls: { - rejectUnauthorized: false, + verificationMode: 'none', }, }); @@ -179,19 +198,23 @@ describe('getCustomAgents', () => { expect(httpsAgent?.options.rejectUnauthorized).toBeFalsy(); }); - test('handles overriding global rejectUnauthorized false with a proxy', () => { - configurationUtilities.isRejectUnauthorizedCertificatesEnabled.mockReturnValue(false); + test('handles overriding global verificationMode "none" with a proxy', () => { + configurationUtilities.getTLSSettings.mockReturnValue({ + verificationMode: 'none', + }); configurationUtilities.getCustomHostSettings.mockReturnValue({ url: targetUrlCanonical, tls: { - rejectUnauthorized: true, + verificationMode: 'full', }, }); configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', // note: this setting doesn't come into play, it's for the connection to // the proxy, not the target url - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: undefined, proxyOnlyHosts: undefined, }); @@ -202,19 +225,23 @@ describe('getCustomAgents', () => { expect(httpsAgent?.options.rejectUnauthorized).toBeTruthy(); }); - test('handles overriding global rejectUnauthorized true with a proxy', () => { - configurationUtilities.isRejectUnauthorizedCertificatesEnabled.mockReturnValue(true); + test('handles overriding global verificationMode "full" with a proxy', () => { + configurationUtilities.getTLSSettings.mockReturnValue({ + verificationMode: 'full', + }); configurationUtilities.getCustomHostSettings.mockReturnValue({ url: targetUrlCanonical, tls: { - rejectUnauthorized: false, + verificationMode: 'none', }, }); configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', // note: this setting doesn't come into play, it's for the connection to // the proxy, not the target url - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: undefined, proxyOnlyHosts: undefined, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts index 6ec926004e73e..a327ee3ffe931 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts @@ -11,6 +11,7 @@ import HttpProxyAgent from 'http-proxy-agent'; import { HttpsProxyAgent } from 'https-proxy-agent'; import { Logger } from '../../../../../../src/core/server'; import { ActionsConfigurationUtilities } from '../../actions_config'; +import { getNodeTLSOptions, getTLSSettingsFromConfig } from './get_node_tls_options'; interface GetCustomAgentsResponse { httpAgent: HttpAgent | undefined; @@ -22,12 +23,14 @@ export function getCustomAgents( logger: Logger, url: string ): GetCustomAgentsResponse { + const generalTLSSettings = configurationUtilities.getTLSSettings(); + const agentTLSOptions = getNodeTLSOptions(logger, generalTLSSettings.verificationMode); // the default for rejectUnauthorized is the global setting, which can // be overridden (below) with a custom host setting const defaultAgents = { httpAgent: undefined, httpsAgent: new HttpsAgent({ - rejectUnauthorized: configurationUtilities.isRejectUnauthorizedCertificatesEnabled(), + ...agentTLSOptions, }), }; @@ -50,10 +53,18 @@ export function getCustomAgents( agentOptions.ca = tlsSettings.certificateAuthoritiesData; } + const tlsSettingsFromConfig = getTLSSettingsFromConfig( + tlsSettings.verificationMode, + tlsSettings.rejectUnauthorized + ); // see: src/core/server/elasticsearch/legacy/elasticsearch_client_config.ts // This is where the global rejectUnauthorized is overridden by a custom host - if (tlsSettings.rejectUnauthorized !== undefined) { - agentOptions.rejectUnauthorized = tlsSettings.rejectUnauthorized; + const customHostNodeTLSOptions = getNodeTLSOptions( + logger, + tlsSettingsFromConfig.verificationMode + ); + if (customHostNodeTLSOptions.rejectUnauthorized !== undefined) { + agentOptions.rejectUnauthorized = customHostNodeTLSOptions.rejectUnauthorized; } } @@ -96,6 +107,10 @@ export function getCustomAgents( return defaultAgents; } + const proxyNodeTLSOptions = getNodeTLSOptions( + logger, + proxySettings.proxyTLSSettings.verificationMode + ); // At this point, we are going to use a proxy, so we need new agents. // We will though, copy over the calculated tls options from above, into // the https agent. @@ -106,7 +121,7 @@ export function getCustomAgents( protocol: proxyUrl.protocol, headers: proxySettings.proxyHeaders, // do not fail on invalid certs if value is false - rejectUnauthorized: proxySettings.proxyRejectUnauthorizedCertificates, + ...proxyNodeTLSOptions, }) as unknown) as HttpsAgent; // vsCode wasn't convinced HttpsProxyAgent is an https.Agent, so we convinced it diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_tls_options.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_tls_options.test.ts new file mode 100644 index 0000000000000..7d131985053f1 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_tls_options.test.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { getNodeTLSOptions, getTLSSettingsFromConfig } from './get_node_tls_options'; +import { Logger } from '../../../../../../src/core/server'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; + +const logger = loggingSystemMock.create().get() as jest.Mocked; + +describe('getNodeTLSOptions', () => { + test('get node.js TLS options: rejectUnauthorized eql true for the verification mode "full"', () => { + const nodeOption = getNodeTLSOptions(logger, 'full'); + expect(nodeOption).toMatchObject({ + rejectUnauthorized: true, + }); + }); + + test('get node.js TLS options: rejectUnauthorized eql true for the verification mode "certificate"', () => { + const nodeOption = getNodeTLSOptions(logger, 'certificate'); + expect(nodeOption.checkServerIdentity).not.toBeNull(); + expect(nodeOption.rejectUnauthorized).toBeTruthy(); + }); + + test('get node.js TLS options: rejectUnauthorized eql false for the verification mode "none"', () => { + const nodeOption = getNodeTLSOptions(logger, 'none'); + expect(nodeOption).toMatchObject({ + rejectUnauthorized: false, + }); + }); + + test('get node.js TLS options: rejectUnauthorized eql true for the verification mode value which does not exist, the logger called with the proper warning message', () => { + const nodeOption = getNodeTLSOptions(logger, 'notexist'); + expect(loggingSystemMock.collect(logger).warn).toMatchInlineSnapshot(` + Array [ + Array [ + "Unknown ssl verificationMode: notexist", + ], + ] + `); + expect(nodeOption).toMatchObject({ + rejectUnauthorized: true, + }); + }); +}); + +describe('getTLSSettingsFromConfig', () => { + test('get verificationMode eql "none" if legacy rejectUnauthorized eql false', () => { + const nodeOption = getTLSSettingsFromConfig(undefined, false); + expect(nodeOption).toMatchObject({ + verificationMode: 'none', + }); + }); + + test('get verificationMode eql "none" if legacy rejectUnauthorized eql true', () => { + const nodeOption = getTLSSettingsFromConfig(undefined, true); + expect(nodeOption).toMatchObject({ + verificationMode: 'full', + }); + }); + + test('get verificationMode eql "certificate", ignore rejectUnauthorized', () => { + const nodeOption = getTLSSettingsFromConfig('certificate', false); + expect(nodeOption).toMatchObject({ + verificationMode: 'certificate', + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_tls_options.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_tls_options.ts new file mode 100644 index 0000000000000..423e9756b13f8 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_tls_options.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PeerCertificate } from 'tls'; +import { TLSSettings } from '../../types'; +import { Logger } from '../../../../../../src/core/server'; + +export function getNodeTLSOptions( + logger: Logger, + verificationMode?: string +): { + rejectUnauthorized?: boolean; + checkServerIdentity?: ((host: string, cert: PeerCertificate) => Error | undefined) | undefined; +} { + const agentOptions: { + rejectUnauthorized?: boolean; + checkServerIdentity?: ((host: string, cert: PeerCertificate) => Error | undefined) | undefined; + } = {}; + if (!!verificationMode) { + switch (verificationMode) { + case 'none': + agentOptions.rejectUnauthorized = false; + break; + case 'certificate': + agentOptions.rejectUnauthorized = true; + // by default, NodeJS is checking the server identify + agentOptions.checkServerIdentity = () => undefined; + break; + case 'full': + agentOptions.rejectUnauthorized = true; + break; + default: { + logger.warn(`Unknown ssl verificationMode: ${verificationMode}`); + agentOptions.rejectUnauthorized = true; + } + } + // see: src/core/server/elasticsearch/legacy/elasticsearch_client_config.ts + // This is where the global rejectUnauthorized is overridden by a custom host + } + return agentOptions; +} + +export function getTLSSettingsFromConfig( + verificationMode?: 'none' | 'certificate' | 'full', + rejectUnauthorized?: boolean +): TLSSettings { + if (verificationMode) { + return { verificationMode }; + } else if (rejectUnauthorized !== undefined) { + return { verificationMode: rejectUnauthorized ? 'full' : 'none' }; + } + return {}; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts index cceeefde71dc2..9bdb2d9481142 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts @@ -76,7 +76,9 @@ describe('send_email module', () => { }, { proxyUrl: 'https://example.com', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: undefined, proxyOnlyHosts: undefined, } @@ -119,7 +121,7 @@ describe('send_email module', () => { `); }); - test('rejectUnauthorized default setting email using not secure host/port', async () => { + test('verificationMode default setting email using not secure host/port', async () => { const sendEmailOptions = getSendEmailOptions({ transport: { host: 'example.com', @@ -236,7 +238,9 @@ describe('send_email module', () => { }, { proxyUrl: 'https://proxy.com', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: new Set(['example.com']), proxyOnlyHosts: undefined, } @@ -268,7 +272,9 @@ describe('send_email module', () => { }, { proxyUrl: 'https://proxy.com', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: new Set(['not-example.com']), proxyOnlyHosts: undefined, } @@ -302,7 +308,9 @@ describe('send_email module', () => { }, { proxyUrl: 'https://proxy.com', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: undefined, proxyOnlyHosts: new Set(['example.com']), } @@ -336,7 +344,7 @@ describe('send_email module', () => { }, { proxyUrl: 'https://proxy.com', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: {}, proxyBypassHosts: undefined, proxyOnlyHosts: new Set(['not-example.com']), } @@ -453,7 +461,7 @@ describe('send_email module', () => { }, { proxyUrl: 'https://proxy.com', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: {}, proxyBypassHosts: undefined, proxyOnlyHosts: undefined, }, diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts index 005e73b1fc2f7..9f601840bc982 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts @@ -12,6 +12,7 @@ import { default as MarkdownIt } from 'markdown-it'; import { Logger } from '../../../../../../src/core/server'; import { ActionsConfigurationUtilities } from '../../actions_config'; import { CustomHostSettings } from '../../config'; +import { getNodeTLSOptions, getTLSSettingsFromConfig } from './get_node_tls_options'; // an email "service" which doesn't actually send, just returns what it would send export const JSON_TRANSPORT_SERVICE = '__json'; @@ -58,7 +59,7 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom // eslint-disable-next-line @typescript-eslint/no-explicit-any const transportConfig: Record = {}; const proxySettings = configurationUtilities.getProxySettings(); - const rejectUnauthorized = configurationUtilities.isRejectUnauthorizedCertificatesEnabled(); + const generalTLSSettings = configurationUtilities.getTLSSettings(); if (hasAuth && user != null && password != null) { transportConfig.auth = { @@ -91,10 +92,10 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom customHostSettings = configurationUtilities.getCustomHostSettings(`smtp://${host}:${port}`); if (proxySettings && useProxy) { - transportConfig.tls = { - // do not fail on invalid certs if value is false - rejectUnauthorized: proxySettings?.proxyRejectUnauthorizedCertificates, - }; + transportConfig.tls = getNodeTLSOptions( + logger, + proxySettings?.proxyTLSSettings.verificationMode + ); transportConfig.proxy = proxySettings.proxyUrl; transportConfig.headers = proxySettings.proxyHeaders; } else if (!transportConfig.secure && user == null && password == null) { @@ -103,7 +104,7 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom // authenticate rarely have valid certs; eg cloud proxy, and npm maildev transportConfig.tls = { rejectUnauthorized: false }; } else { - transportConfig.tls = { rejectUnauthorized }; + transportConfig.tls = getNodeTLSOptions(logger, generalTLSSettings.verificationMode); } // finally, allow customHostSettings to override some of the settings @@ -116,14 +117,16 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom if (tlsSettings?.certificateAuthoritiesData) { tlsConfig.ca = tlsSettings?.certificateAuthoritiesData; } - if (tlsSettings?.rejectUnauthorized !== undefined) { - tlsConfig.rejectUnauthorized = tlsSettings?.rejectUnauthorized; - } + const tlsSettingsFromConfig = getTLSSettingsFromConfig( + tlsSettings?.verificationMode, + tlsSettings?.rejectUnauthorized + ); + const nodeTLSOptions = getNodeTLSOptions(logger, tlsSettingsFromConfig.verificationMode); if (!transportConfig.tls) { - transportConfig.tls = tlsConfig; + transportConfig.tls = { ...tlsConfig, ...nodeTLSOptions }; } else { - transportConfig.tls = { ...transportConfig.tls, ...tlsConfig }; + transportConfig.tls = { ...transportConfig.tls, ...tlsConfig, ...nodeTLSOptions }; } if (smtpSettings?.ignoreTLS) { diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts index 76612696e8e58..4108424e26ac4 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts @@ -194,7 +194,9 @@ describe('execute()', () => { const configurationUtilities = actionsConfigMock.create(); configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: undefined, proxyOnlyHosts: undefined, }); @@ -219,7 +221,9 @@ describe('execute()', () => { const configurationUtilities = actionsConfigMock.create(); configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: new Set(['example.com']), proxyOnlyHosts: undefined, }); @@ -244,7 +248,9 @@ describe('execute()', () => { const configurationUtilities = actionsConfigMock.create(); configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: new Set(['not-example.com']), proxyOnlyHosts: undefined, }); @@ -269,7 +275,9 @@ describe('execute()', () => { const configurationUtilities = actionsConfigMock.create(); configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: undefined, proxyOnlyHosts: new Set(['example.com']), }); @@ -294,7 +302,9 @@ describe('execute()', () => { const configurationUtilities = actionsConfigMock.create(); configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: undefined, proxyOnlyHosts: new Set(['not-example.com']), }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts b/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts index 95088fa5f7965..bf34789e03fae 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts @@ -170,9 +170,9 @@ describe('execute()', () => { "getCustomHostSettings": [MockFunction], "getProxySettings": [MockFunction], "getResponseSettings": [MockFunction], + "getTLSSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], - "isRejectUnauthorizedCertificatesEnabled": [MockFunction], "isUriAllowed": [MockFunction], }, "data": Object { @@ -234,9 +234,9 @@ describe('execute()', () => { "getCustomHostSettings": [MockFunction], "getProxySettings": [MockFunction], "getResponseSettings": [MockFunction], + "getTLSSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], - "isRejectUnauthorizedCertificatesEnabled": [MockFunction], "isUriAllowed": [MockFunction], }, "data": Object { diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts index 00e56303dbe22..b2c865c2f5374 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts @@ -293,9 +293,9 @@ describe('execute()', () => { "getCustomHostSettings": [MockFunction], "getProxySettings": [MockFunction], "getResponseSettings": [MockFunction], + "getTLSSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], - "isRejectUnauthorizedCertificatesEnabled": [MockFunction], "isUriAllowed": [MockFunction], }, "data": "some data", @@ -386,9 +386,9 @@ describe('execute()', () => { "getCustomHostSettings": [MockFunction], "getProxySettings": [MockFunction], "getResponseSettings": [MockFunction], + "getTLSSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], - "isRejectUnauthorizedCertificatesEnabled": [MockFunction], "isUriAllowed": [MockFunction], }, "data": "some data", diff --git a/x-pack/plugins/actions/server/config.test.ts b/x-pack/plugins/actions/server/config.test.ts index 4c4fd143369e1..9774bfb05d4ff 100644 --- a/x-pack/plugins/actions/server/config.test.ts +++ b/x-pack/plugins/actions/server/config.test.ts @@ -177,6 +177,44 @@ describe('config validation', () => { `"[customHostSettings.0.url]: expected value of type [string] but got [undefined]"` ); }); + + test('action with tls configuration', () => { + const config: Record = { + tls: { + verificationMode: 'none', + proxyVerificationMode: 'none', + }, + }; + expect(configSchema.validate(config)).toMatchInlineSnapshot(` + Object { + "allowedHosts": Array [ + "*", + ], + "cleanupFailedExecutionsTask": Object { + "cleanupInterval": "PT5M", + "enabled": true, + "idleInterval": "PT1H", + "pageSize": 100, + }, + "enabled": true, + "enabledActionTypes": Array [ + "*", + ], + "maxResponseContentLength": ByteSizeValue { + "valueInBytes": 1048576, + }, + "preconfigured": Object {}, + "preconfiguredAlertHistoryEsIndex": false, + "proxyRejectUnauthorizedCertificates": true, + "rejectUnauthorized": true, + "responseTimeout": "PT1M", + "tls": Object { + "proxyVerificationMode": "none", + "verificationMode": "none", + }, + } + `); + }); }); // object creator that ensures we can create a property named __proto__ on an diff --git a/x-pack/plugins/actions/server/config.ts b/x-pack/plugins/actions/server/config.ts index 0dc1aed68f4d0..8859a2d8881a2 100644 --- a/x-pack/plugins/actions/server/config.ts +++ b/x-pack/plugins/actions/server/config.ts @@ -33,7 +33,16 @@ const customHostSettingsSchema = schema.object({ ), tls: schema.maybe( schema.object({ + /** + * @deprecated in favor of `verificationMode` + **/ rejectUnauthorized: schema.maybe(schema.boolean()), + verificationMode: schema.maybe( + schema.oneOf( + [schema.literal('none'), schema.literal('certificate'), schema.literal('full')], + { defaultValue: 'full' } + ) + ), certificateAuthoritiesFiles: schema.maybe( schema.oneOf([ schema.string({ minLength: 1 }), @@ -68,10 +77,32 @@ export const configSchema = schema.object({ }), proxyUrl: schema.maybe(schema.string()), proxyHeaders: schema.maybe(schema.recordOf(schema.string(), schema.string())), + /** + * @deprecated in favor of `tls.proxyVerificationMode` + **/ proxyRejectUnauthorizedCertificates: schema.boolean({ defaultValue: true }), proxyBypassHosts: schema.maybe(schema.arrayOf(schema.string({ hostname: true }))), proxyOnlyHosts: schema.maybe(schema.arrayOf(schema.string({ hostname: true }))), + /** + * @deprecated in favor of `tls.verificationMode` + **/ rejectUnauthorized: schema.boolean({ defaultValue: true }), + tls: schema.maybe( + schema.object({ + verificationMode: schema.maybe( + schema.oneOf( + [schema.literal('none'), schema.literal('certificate'), schema.literal('full')], + { defaultValue: 'full' } + ) + ), + proxyVerificationMode: schema.maybe( + schema.oneOf( + [schema.literal('none'), schema.literal('certificate'), schema.literal('full')], + { defaultValue: 'full' } + ) + ), + }) + ), maxResponseContentLength: schema.byteSize({ defaultValue: '1mb' }), responseTimeout: schema.duration({ defaultValue: '60s' }), customHostSettings: schema.maybe(schema.arrayOf(customHostSettingsSchema)), diff --git a/x-pack/plugins/actions/server/index.ts b/x-pack/plugins/actions/server/index.ts index 99c6326d60e26..6a0f06b34d670 100644 --- a/x-pack/plugins/actions/server/index.ts +++ b/x-pack/plugins/actions/server/index.ts @@ -8,7 +8,7 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import { PluginInitializerContext, PluginConfigDescriptor } from '../../../../src/core/server'; import { ActionsPlugin } from './plugin'; -import { configSchema, ActionsConfig } from './config'; +import { configSchema, ActionsConfig, CustomHostSettings } from './config'; import { ActionsClient as ActionsClientClass } from './actions_client'; import { ActionsAuthorization as ActionsAuthorizationClass } from './authorization/actions_authorization'; @@ -57,7 +57,37 @@ export const plugin = (initContext: PluginInitializerContext) => new ActionsPlug export const config: PluginConfigDescriptor = { schema: configSchema, - deprecations: ({ renameFromRoot }) => [ + deprecations: ({ renameFromRoot, unused }) => [ renameFromRoot('xpack.actions.whitelistedHosts', 'xpack.actions.allowedHosts'), + (settings, fromPath, addDeprecation) => { + const customHostSettings = settings?.xpack?.actions?.customHostSettings ?? []; + if ( + customHostSettings.find( + (customHostSchema: CustomHostSettings) => + !!customHostSchema.tls && !!customHostSchema.tls.rejectUnauthorized + ) + ) { + addDeprecation({ + message: + '`xpack.actions.customHostSettings[].tls.rejectUnauthorized` is deprecated. Use `xpack.actions.customHostSettings[].tls.verificationMode` instead, with the setting `verificationMode:full` eql to `rejectUnauthorized:true`, and `verificationMode:none` eql to `rejectUnauthorized:false`.', + }); + } + }, + (settings, fromPath, addDeprecation) => { + if (!!settings?.xpack?.actions?.rejectUnauthorized) { + addDeprecation({ + message: + '`xpack.actions.rejectUnauthorized` is deprecated. Use `xpack.actions.verificationMode` instead, with the setting `verificationMode:full` eql to `rejectUnauthorized:true`, and `verificationMode:none` eql to `rejectUnauthorized:false`.', + }); + } + }, + (settings, fromPath, addDeprecation) => { + if (!!settings?.xpack?.actions?.proxyRejectUnauthorizedCertificates) { + addDeprecation({ + message: + '`xpack.actions.proxyRejectUnauthorizedCertificates` is deprecated. Use `xpack.actions.proxyVerificationMode` instead, with the setting `proxyVerificationMode:full` eql to `rejectUnauthorized:true`, and `proxyVerificationMode:none` eql to `rejectUnauthorized:false`.', + }); + } + }, ], }; diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index ea22e90dfed40..c8c9967afca1a 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -142,10 +142,14 @@ export interface ProxySettings { proxyBypassHosts: Set | undefined; proxyOnlyHosts: Set | undefined; proxyHeaders?: Record; - proxyRejectUnauthorizedCertificates: boolean; + proxyTLSSettings: TLSSettings; } export interface ResponseSettings { maxContentLength: number; timeout: number; } + +export interface TLSSettings { + verificationMode?: 'none' | 'certificate' | 'full'; +} diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 8647c5951b7f3..c56e8adfbe34f 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -19,10 +19,11 @@ interface CreateTestConfigOptions { disabledPlugins?: string[]; ssl?: boolean; enableActionsProxy: boolean; - rejectUnauthorized?: boolean; + verificationMode?: 'full' | 'none' | 'certificate'; publicBaseUrl?: boolean; preconfiguredAlertHistoryEsIndex?: boolean; customizeLocalHostTls?: boolean; + rejectUnauthorized?: boolean; // legacy } // test.not-enabled is specifically not enabled @@ -49,9 +50,10 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) license = 'trial', disabledPlugins = [], ssl = false, - rejectUnauthorized = true, + verificationMode = 'full', preconfiguredAlertHistoryEsIndex = false, customizeLocalHostTls = false, + rejectUnauthorized = true, // legacy } = options; return async ({ readConfigFile }: FtrConfigProviderContext) => { @@ -101,19 +103,19 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) { url: tlsWebhookServers.rejectUnauthorizedFalse, tls: { - rejectUnauthorized: false, + verificationMode: 'none', }, }, { url: tlsWebhookServers.rejectUnauthorizedTrue, tls: { - rejectUnauthorized: true, + verificationMode: 'full', }, }, { url: tlsWebhookServers.caFile, tls: { - rejectUnauthorized: true, + verificationMode: 'certificate', certificateAuthoritiesFiles: [CA_CERT_PATH], }, }, @@ -151,6 +153,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) '--xpack.alerting.invalidateApiKeysTask.interval="15s"', `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, `--xpack.actions.rejectUnauthorized=${rejectUnauthorized}`, + `--xpack.actions.tls.verificationMode=${verificationMode}`, ...actionsProxyUrl, ...customHostSettings, '--xpack.eventLog.logEntries=true', diff --git a/x-pack/test/alerting_api_integration/spaces_only/config.ts b/x-pack/test/alerting_api_integration/spaces_only/config.ts index 3b3a15b6d62e4..788d9d0698a19 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/config.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/config.ts @@ -12,7 +12,7 @@ export default createTestConfig('spaces_only', { disabledPlugins: ['security'], license: 'trial', enableActionsProxy: false, - rejectUnauthorized: false, + verificationMode: 'none', customizeLocalHostTls: true, preconfiguredAlertHistoryEsIndex: true, }); diff --git a/x-pack/test/alerting_api_integration/spaces_only_legacy/config.ts b/x-pack/test/alerting_api_integration/spaces_only_legacy/config.ts new file mode 100644 index 0000000000000..511e97b96e35d --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only_legacy/config.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('spaces_only', { + disabledPlugins: ['security'], + license: 'trial', + enableActionsProxy: false, + rejectUnauthorized: false, + verificationMode: undefined, + customizeLocalHostTls: true, + preconfiguredAlertHistoryEsIndex: true, +}); diff --git a/x-pack/test/alerting_api_integration/spaces_only_legacy/scenarios.ts b/x-pack/test/alerting_api_integration/spaces_only_legacy/scenarios.ts new file mode 100644 index 0000000000000..5c00ad2f4f70f --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only_legacy/scenarios.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Space } from '../common/types'; + +const Space1: Space = { + id: 'space1', + namespace: 'space1', + name: 'Space 1', + disabledFeatures: [], +}; + +const Other: Space = { + id: 'other', + namespace: 'other', + name: 'Other', + disabledFeatures: [], +}; + +const Default: Space = { + id: 'default', + namespace: undefined, + name: 'Default', + disabledFeatures: [], +}; + +export const Spaces = { + space1: Space1, + other: Other, + default: Default, +}; diff --git a/x-pack/test/alerting_api_integration/spaces_only_legacy/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/spaces_only_legacy/tests/actions/builtin_action_types/webhook.ts new file mode 100644 index 0000000000000..4af33136cd42c --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only_legacy/tests/actions/builtin_action_types/webhook.ts @@ -0,0 +1,201 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import http from 'http'; +import https from 'https'; +import getPort from 'get-port'; +import expect from '@kbn/expect'; +import { URL, format as formatUrl } from 'url'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { + getWebhookServer, + getHttpsWebhookServer, +} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +import { createTlsWebhookServer } from '../../../../common/lib/get_tls_webhook_servers'; + +// eslint-disable-next-line import/no-default-export +export default function webhookTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + async function createWebhookAction( + webhookSimulatorURL: string, + config: Record> = {} + ): Promise { + const url = formatUrl(new URL(webhookSimulatorURL), { auth: false }); + const composedConfig = { + headers: { + 'Content-Type': 'text/plain', + }, + ...config, + url, + }; + + const { body: createdAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'test') + .send({ + name: 'A generic Webhook action', + actionTypeId: '.webhook', + secrets: {}, + config: composedConfig, + }) + .expect(200); + + return createdAction.id; + } + + async function getPortOfConnector(connectorId: string): Promise { + const response = await supertest.get(`/api/actions/connectors`).expect(200); + const connector = response.body.find((conn: { id: string }) => conn.id === connectorId); + if (connector === undefined) { + throw new Error(`unable to find connector with id ${connectorId}`); + } + + // server URL is the connector name + const url = connector.name; + const parsedUrl = new URL(url); + return parsedUrl.port; + } + + describe('webhook action', () => { + describe('with http endpoint', () => { + let webhookSimulatorURL: string = ''; + let webhookServer: http.Server; + before(async () => { + webhookServer = await getWebhookServer(); + const availablePort = await getPort({ port: 9000 }); + webhookServer.listen(availablePort); + webhookSimulatorURL = `http://localhost:${availablePort}`; + }); + + it('webhook can be executed without username and password', async () => { + const webhookActionId = await createWebhookAction(webhookSimulatorURL); + const { body: result } = await supertest + .post(`/api/actions/action/${webhookActionId}/_execute`) + .set('kbn-xsrf', 'test') + .send({ + params: { + body: 'success', + }, + }) + .expect(200); + + expect(result.status).to.eql('ok'); + }); + + after(() => { + webhookServer.close(); + }); + }); + + describe('with https endpoint and rejectUnauthorized=false', () => { + let webhookSimulatorURL: string = ''; + let webhookServer: https.Server; + + before(async () => { + webhookServer = await getHttpsWebhookServer(); + const availablePort = await getPort({ port: getPort.makeRange(9000, 9100) }); + webhookServer.listen(availablePort); + webhookSimulatorURL = `https://localhost:${availablePort}`; + }); + + it('should support the POST method against webhook target', async () => { + const webhookActionId = await createWebhookAction(webhookSimulatorURL, { method: 'post' }); + const { body: result } = await supertest + .post(`/api/actions/action/${webhookActionId}/_execute`) + .set('kbn-xsrf', 'test') + .send({ + params: { + body: 'success_post_method', + }, + }) + .expect(200); + + expect(result.status).to.eql('ok'); + }); + + after(() => { + webhookServer.close(); + }); + }); + + describe('tls customization', () => { + it('should handle the xpack.actions.rejectUnauthorized: false', async () => { + const connectorId = 'custom.tls.noCustom'; + const port = await getPortOfConnector(connectorId); + const server = await createTlsWebhookServer(port); + const { status, body } = await supertest + .post(`/api/actions/connector/${connectorId}/_execute`) + .set('kbn-xsrf', 'test') + .send({ + params: { + body: 'foo', + }, + }); + expect(status).to.eql(200); + server.close(); + + expect(body.status).to.eql('ok'); + }); + + it('should handle the customized rejectUnauthorized: false', async () => { + const connectorId = 'custom.tls.rejectUnauthorizedFalse'; + const port = await getPortOfConnector(connectorId); + const server = await createTlsWebhookServer(port); + const { status, body } = await supertest + .post(`/api/actions/connector/custom.tls.rejectUnauthorizedFalse/_execute`) + .set('kbn-xsrf', 'test') + .send({ + params: { + body: 'foo', + }, + }); + expect(status).to.eql(200); + server.close(); + + expect(body.status).to.eql('ok'); + }); + + it('should handle the customized rejectUnauthorized: true', async () => { + const connectorId = 'custom.tls.rejectUnauthorizedTrue'; + const port = await getPortOfConnector(connectorId); + const server = await createTlsWebhookServer(port); + const { status, body } = await supertest + .post(`/api/actions/connector/custom.tls.rejectUnauthorizedTrue/_execute`) + .set('kbn-xsrf', 'test') + .send({ + params: { + body: 'foo', + }, + }); + expect(status).to.eql(200); + server.close(); + + expect(body.status).to.eql('error'); + expect(body.service_message.indexOf('certificate')).to.be.greaterThan(0); + }); + + it('should handle the customized ca file', async () => { + const connectorId = 'custom.tls.caFile'; + const port = await getPortOfConnector(connectorId); + const server = await createTlsWebhookServer(port); + const { status, body } = await supertest + .post(`/api/actions/connector/custom.tls.caFile/_execute`) + .set('kbn-xsrf', 'test') + .send({ + params: { + body: 'foo', + }, + }); + expect(status).to.eql(200); + server.close(); + + expect(body.status).to.eql('ok'); + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only_legacy/tests/index.ts b/x-pack/test/alerting_api_integration/spaces_only_legacy/tests/index.ts new file mode 100644 index 0000000000000..a5a046dcbbe86 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only_legacy/tests/index.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { Spaces } from '../scenarios'; + +// eslint-disable-next-line import/no-default-export +export default function alertingApiIntegrationTests({ loadTestFile }: FtrProviderContext) { + describe('alerting api integration spaces only legacy configuration', function () { + this.tags('ciGroup12'); + + loadTestFile(require.resolve('./actions/builtin_action_types/webhook')); + }); +} + +export async function buildUp(getService: FtrProviderContext['getService']) { + const spacesService = getService('spaces'); + for (const space of Object.values(Spaces)) { + if (space.id === 'default') continue; + + const { id, name, disabledFeatures } = space; + await spacesService.create({ id, name, disabledFeatures }); + } +} + +export async function tearDown(getService: FtrProviderContext['getService']) { + const esArchiver = getService('esArchiver'); + await esArchiver.unload('empty_kibana'); +}