diff --git a/.buildkite/ftr_platform_stateful_configs.yml b/.buildkite/ftr_platform_stateful_configs.yml index ce7ce44d3bc5b..1cbdfa3d6507a 100644 --- a/.buildkite/ftr_platform_stateful_configs.yml +++ b/.buildkite/ftr_platform_stateful_configs.yml @@ -191,6 +191,7 @@ enabled: - x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/config.ts - x-pack/test/functional_with_es_ssl/apps/embeddable_alerts_table/config.ts - x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/config.ts + - x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/with_aws_ses_kibana_config/config.ts - x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/shared/config.ts - x-pack/test/functional/apps/advanced_settings/config.ts - x-pack/test/functional/apps/aiops/config.ts diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index a843af78aaad5..26288d723f18a 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -135,6 +135,14 @@ A list of allowed email domains which can be used with the email connector. When + WARNING: This feature is available in {kib} 7.17.4 and 8.3.0 onwards but is not supported in {kib} 8.0, 8.1 or 8.2. As such, this setting should be removed before upgrading from 7.17 to 8.0, 8.1 or 8.2. It is possible to configure the settings in 7.17.4 and then upgrade to 8.3.0 directly. +[[actions-config-email-services-ses-host]] `xpack.actions.email.services.ses.host` {ess-icon}:: +The SMTP endpoint for an Amazon Simple Email Service (SES) service provider that can be used by email connectors. ++ +WARNING: This setting alone is insufficient for overriding system defaults for the SES SMTP endpoint. You must also configure the `xpack.actions.email.services.ses.port` setting. + +[[actions-config-email-services-ses-port]] `xpack.actions.email.services.ses.port` {ess-icon}:: +The port number for an Amazon Simple Email Service (SES) service provider that can be used by email connectors. + `xpack.actions.enableFooterInEmail` {ess-icon}:: A boolean value indicating that a footer with a relevant link should be added to emails sent as alerting actions. Default: true. 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 d9763ef374d0a..7610d9d91ea71 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 @@ -216,6 +216,8 @@ kibana_vars=( xpack.actions.allowedHosts xpack.actions.customHostSettings xpack.actions.email.domain_allowlist + xpack.actions.email.services.ses.host + xpack.actions.email.services.ses.port xpack.actions.enableFooterInEmail xpack.actions.enabledActionTypes xpack.actions.maxResponseContentLength diff --git a/src/platform/test/plugin_functional/test_suites/core_plugins/rendering.ts b/src/platform/test/plugin_functional/test_suites/core_plugins/rendering.ts index 1bb53008a8041..2789eae1c0a99 100644 --- a/src/platform/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/src/platform/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -204,7 +204,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'vis_type_vislib.readOnly (boolean?|never)', 'vis_type_xy.readOnly (boolean?|never)', 'vis_type_vega.enableExternalUrls (boolean?)', - 'xpack.actions.email.domain_allowlist (array)', + 'xpack.actions.email.domain_allowlist (array?)', 'xpack.apm.serviceMapEnabled (boolean?)', 'xpack.apm.ui.enabled (boolean?)', 'xpack.apm.ui.maxTraceItems (number?)', diff --git a/x-pack/platform/plugins/shared/actions/server/actions_config.mock.ts b/x-pack/platform/plugins/shared/actions/server/actions_config.mock.ts index 6889d7db6dab5..5e3943e5b2cee 100644 --- a/x-pack/platform/plugins/shared/actions/server/actions_config.mock.ts +++ b/x-pack/platform/plugins/shared/actions/server/actions_config.mock.ts @@ -36,6 +36,7 @@ const createActionsConfigMock = () => { getMaxAttempts: jest.fn().mockReturnValue(3), enableFooterInEmail: jest.fn().mockReturnValue(true), getMaxQueued: jest.fn().mockReturnValue(1000), + getAwsSesConfig: jest.fn().mockReturnValue(null), }; return mocked; }; diff --git a/x-pack/platform/plugins/shared/actions/server/actions_config.test.ts b/x-pack/platform/plugins/shared/actions/server/actions_config.test.ts index 84ce85b8e219d..7a58ee2d08503 100644 --- a/x-pack/platform/plugins/shared/actions/server/actions_config.test.ts +++ b/x-pack/platform/plugins/shared/actions/server/actions_config.test.ts @@ -592,3 +592,34 @@ describe('getMaxQueued()', () => { expect(max).toEqual(1000000); }); }); + +describe('getAwsSesConfig()', () => { + test('returns null when no email config set', () => { + const acu = getActionsConfigurationUtilities(defaultActionsConfig); + expect(acu.getAwsSesConfig()).toEqual(null); + }); + + test('returns null when no email.services config set', () => { + const acu = getActionsConfigurationUtilities({ ...defaultActionsConfig, email: {} }); + expect(acu.getAwsSesConfig()).toEqual(null); + }); + + test('returns config if set', () => { + const acu = getActionsConfigurationUtilities({ + ...defaultActionsConfig, + email: { + services: { + ses: { + host: 'https://email.us-east-1.amazonaws.com', + port: 1234, + }, + }, + }, + }); + expect(acu.getAwsSesConfig()).toEqual({ + host: 'https://email.us-east-1.amazonaws.com', + port: 1234, + secure: true, + }); + }); +}); diff --git a/x-pack/platform/plugins/shared/actions/server/actions_config.ts b/x-pack/platform/plugins/shared/actions/server/actions_config.ts index 9febe0aa81600..6210901af6c10 100644 --- a/x-pack/platform/plugins/shared/actions/server/actions_config.ts +++ b/x-pack/platform/plugins/shared/actions/server/actions_config.ts @@ -15,7 +15,7 @@ import type { ActionsConfig, CustomHostSettings } from './config'; import { AllowedHosts, EnabledActionTypes, DEFAULT_QUEUED_MAX } from './config'; import { getCanonicalCustomHostUrl } from './lib/custom_host_settings'; import { ActionTypeDisabledError } from './lib'; -import type { ProxySettings, ResponseSettings, SSLSettings } from './types'; +import type { AwsSesConfig, ProxySettings, ResponseSettings, SSLSettings } from './types'; import { getSSLSettingsFromConfig } from './lib/get_node_ssl_options'; import type { ValidateEmailAddressesOptions } from '../common'; import { validateEmailAddresses, invalidEmailsAsMessage } from '../common'; @@ -55,6 +55,7 @@ export interface ActionsConfigurationUtilities { ): string | undefined; enableFooterInEmail: () => boolean; getMaxQueued: () => number; + getAwsSesConfig: () => AwsSesConfig; } function allowListErrorMessage(field: AllowListingField, value: string) { @@ -171,7 +172,7 @@ function validateEmails( addresses: string[], options: ValidateEmailAddressesOptions ): string | undefined { - if (config.email == null) { + if (config.email?.domain_allowlist == null) { return; } @@ -229,5 +230,16 @@ export function getActionsConfigurationUtilities( }, enableFooterInEmail: () => config.enableFooterInEmail, getMaxQueued: () => config.queued?.max || DEFAULT_QUEUED_MAX, + getAwsSesConfig: () => { + if (config.email?.services?.ses.host && config.email?.services?.ses.port) { + return { + host: config.email?.services?.ses.host, + port: config.email?.services?.ses.port, + secure: true, + }; + } + + return null; + }, }; } diff --git a/x-pack/platform/plugins/shared/actions/server/config.test.ts b/x-pack/platform/plugins/shared/actions/server/config.test.ts index ec145e238e7ed..67660410c4372 100644 --- a/x-pack/platform/plugins/shared/actions/server/config.test.ts +++ b/x-pack/platform/plugins/shared/actions/server/config.test.ts @@ -246,7 +246,7 @@ describe('config validation', () => { config.email = {}; expect(() => configSchema.validate(config)).toThrowErrorMatchingInlineSnapshot( - `"[email.domain_allowlist]: expected value of type [array] but got [undefined]"` + `"[email]: Email configuration requires either domain_allowlist or services.ses to be specified"` ); config.email = { domain_allowlist: [] }; @@ -257,6 +257,54 @@ describe('config validation', () => { result = configSchema.validate(config); expect(result.email?.domain_allowlist).toEqual(['a.com', 'b.c.com', 'd.e.f.com']); }); + + describe('email.services.ses', () => { + const config: Record = {}; + test('validates no email config at all', () => { + expect(configSchema.validate(config).email).toBe(undefined); + }); + + test('validates empty email config', () => { + config.email = {}; + expect(() => configSchema.validate(config)).toThrowErrorMatchingInlineSnapshot( + `"[email]: Email configuration requires either domain_allowlist or services.ses to be specified"` + ); + }); + + test('validates email config with empty services', () => { + config.email = { services: {} }; + expect(() => configSchema.validate(config)).toThrowErrorMatchingInlineSnapshot( + `"[email]: Email configuration requires either domain_allowlist or services.ses to be specified"` + ); + }); + + test('validates email config with empty ses service', () => { + config.email = { services: { ses: {} } }; + expect(() => configSchema.validate(config)).toThrowErrorMatchingInlineSnapshot( + `"[email]: Email configuration requires either domain_allowlist or services.ses to be specified"` + ); + }); + + test('validates ses config with host only', () => { + config.email = { services: { ses: { host: 'ses.host.com' } } }; + expect(() => configSchema.validate(config)).toThrowErrorMatchingInlineSnapshot( + `"[email]: Email configuration requires both services.ses.host and services.ses.port to be specified"` + ); + }); + + test('validates ses config with port only', () => { + config.email = { services: { ses: { port: 1 } } }; + expect(() => configSchema.validate(config)).toThrowErrorMatchingInlineSnapshot( + `"[email]: Email configuration requires both services.ses.host and services.ses.port to be specified"` + ); + }); + + test('validates ses service', () => { + config.email = { services: { ses: { host: 'ses.host.com', port: 1 } } }; + const result = configSchema.validate(config); + expect(result.email?.services?.ses).toEqual({ host: 'ses.host.com', port: 1 }); + }); + }); }); // object creator that ensures we can create a property named __proto__ on an diff --git a/x-pack/platform/plugins/shared/actions/server/config.ts b/x-pack/platform/plugins/shared/actions/server/config.ts index c0f0ca08c8ee7..aa840302469a7 100644 --- a/x-pack/platform/plugins/shared/actions/server/config.ts +++ b/x-pack/platform/plugins/shared/actions/server/config.ts @@ -133,9 +133,30 @@ export const configSchema = schema.object({ microsoftGraphApiScope: schema.string({ defaultValue: DEFAULT_MICROSOFT_GRAPH_API_SCOPE }), microsoftExchangeUrl: schema.string({ defaultValue: DEFAULT_MICROSOFT_EXCHANGE_URL }), email: schema.maybe( - schema.object({ - domain_allowlist: schema.arrayOf(schema.string()), - }) + schema.object( + { + domain_allowlist: schema.maybe(schema.arrayOf(schema.string())), + services: schema.maybe( + schema.object({ + ses: schema.object({ + host: schema.maybe(schema.string({ minLength: 1 })), + port: schema.maybe(schema.number({ min: 1, max: 65535 })), + }), + }) + ), + }, + { + validate: (obj) => { + if (!obj.domain_allowlist && !obj.services?.ses.host && !obj.services?.ses.port) { + return 'Email configuration requires either domain_allowlist or services.ses to be specified'; + } + + if (obj.services?.ses && (!obj.services.ses.host || !obj.services.ses.port)) { + return 'Email configuration requires both services.ses.host and services.ses.port to be specified'; + } + }, + } + ) ), run: schema.maybe( schema.object({ diff --git a/x-pack/platform/plugins/shared/actions/server/mocks.ts b/x-pack/platform/plugins/shared/actions/server/mocks.ts index b318e7b09f822..f2434049f886c 100644 --- a/x-pack/platform/plugins/shared/actions/server/mocks.ts +++ b/x-pack/platform/plugins/shared/actions/server/mocks.ts @@ -35,7 +35,9 @@ const createSetupMock = () => { getSubActionConnectorClass: jest.fn(), getCaseConnectorClass: jest.fn(), getActionsHealth: jest.fn(), - getActionsConfigurationUtilities: jest.fn(), + getActionsConfigurationUtilities: jest.fn().mockReturnValue({ + getAwsSesConfig: jest.fn(), + }), setEnabledConnectorTypes: jest.fn(), isActionTypeEnabled: jest.fn(), }; diff --git a/x-pack/platform/plugins/shared/actions/server/types.ts b/x-pack/platform/plugins/shared/actions/server/types.ts index f83008373be57..3a69cf2e19c20 100644 --- a/x-pack/platform/plugins/shared/actions/server/types.ts +++ b/x-pack/platform/plugins/shared/actions/server/types.ts @@ -299,3 +299,9 @@ export interface ConnectorToken extends SavedObjectAttributes { // This unallowlist should only contain connector types that require a request or API key for // execution. export const UNALLOWED_FOR_UNSECURE_EXECUTION_CONNECTOR_TYPE_IDS = ['.index']; + +export type AwsSesConfig = { + host: string; + port: number; + secure: boolean; +} | null; diff --git a/x-pack/platform/plugins/shared/stack_connectors/common/index.ts b/x-pack/platform/plugins/shared/stack_connectors/common/index.ts index 5f78f4fad2810..ec6b4b882203b 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/common/index.ts +++ b/x-pack/platform/plugins/shared/stack_connectors/common/index.ts @@ -10,6 +10,7 @@ export enum AdditionalEmailServices { ELASTIC_CLOUD = 'elastic_cloud', EXCHANGE = 'exchange_server', OTHER = 'other', + AWS_SES = 'ses', } export const INTERNAL_BASE_STACK_CONNECTORS_API_PATH = '/internal/stack_connectors'; diff --git a/x-pack/platform/plugins/shared/stack_connectors/common/types.ts b/x-pack/platform/plugins/shared/stack_connectors/common/types.ts deleted file mode 100644 index 6bc9ef7eb72e6..0000000000000 --- a/x-pack/platform/plugins/shared/stack_connectors/common/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export interface StackConnectorsConfigType { - enableExperimental: string[]; -} diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/plugin.ts b/x-pack/platform/plugins/shared/stack_connectors/public/plugin.ts index fdf8c32ba6260..c981a00081c36 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/public/plugin.ts +++ b/x-pack/platform/plugins/shared/stack_connectors/public/plugin.ts @@ -8,11 +8,11 @@ import type { CoreSetup, Plugin, PluginInitializerContext } from '@kbn/core/public'; import type { TriggersAndActionsUIPublicPluginSetup } from '@kbn/triggers-actions-ui-plugin/public'; import type { ActionsPublicPluginSetup } from '@kbn/actions-plugin/public'; +import type { ConfigSchema as StackConnectorsConfigType } from '../server/config'; import { registerConnectorTypes } from './connector_types'; import { ExperimentalFeaturesService } from './common/experimental_features_service'; import type { ExperimentalFeatures } from '../common/experimental_features'; import { parseExperimentalConfigValue } from '../common/experimental_features'; -import type { StackConnectorsConfigType } from '../common/types'; export type Setup = void; export type Start = void; diff --git a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/email/index.test.ts b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/email/index.test.ts index 7cea06db61cea..f9e060c9c5a11 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/email/index.test.ts +++ b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/email/index.test.ts @@ -33,6 +33,7 @@ import type { import { getConnectorType } from '.'; import type { ValidateEmailAddressesOptions } from '@kbn/actions-plugin/common'; import { ActionExecutionSourceType } from '@kbn/actions-plugin/server/types'; +import { AdditionalEmailServices } from '../../../common'; const sendEmailMock = sendEmail as jest.Mock; @@ -485,6 +486,23 @@ describe('params validation', () => { treatMustacheTemplatesAsValid: true, }); }); + + test('doesnt throws if both host and port do not match AWS SES config', () => { + expect(() => { + validateConfig( + connectorType, + { + service: AdditionalEmailServices.AWS_SES, + from: 'bob@example.com', + host: 'wrong-host', + port: 123, + secure: true, + hasAuth: true, + }, + { configurationUtilities } + ); + }).not.toThrowError(); + }); }); describe('execute()', () => { @@ -1260,6 +1278,144 @@ describe('execute()', () => { ] `); }); + + test('parameters are as expected when using ses service without ses kbn config', async () => { + const mockedActionsConfig = actionsConfigMock.create(); + const customExecutorOptions: EmailConnectorTypeExecutorOptions = { + ...executorOptions, + configurationUtilities: mockedActionsConfig, + config: { + ...config, + service: 'ses', + hasAuth: false, + }, + secrets: { + ...secrets, + user: null, + password: null, + }, + }; + + sendEmailMock.mockReset(); + await connectorType.executor(customExecutorOptions); + expect(sendEmailMock.mock.calls[0][1].transport).toStrictEqual({ + service: 'ses', + }); + }); + + test('parameters are as expected when using ses service and ses kbn config', async () => { + const mockedActionsConfig = actionsConfigMock.create(); + mockedActionsConfig.getAwsSesConfig = jest.fn().mockReturnValue({ + host: 'aws-ses-host', + port: 5555, + secure: true, + }); + const customExecutorOptions: EmailConnectorTypeExecutorOptions = { + ...executorOptions, + configurationUtilities: mockedActionsConfig, + config: { + ...config, + service: 'ses', + hasAuth: false, + }, + secrets: { + ...secrets, + user: null, + password: null, + }, + }; + + sendEmailMock.mockReset(); + await connectorType.executor(customExecutorOptions); + expect(sendEmailMock.mock.calls[0][1].transport).toStrictEqual({ + host: 'aws-ses-host', + port: 5555, + secure: true, + }); + }); +}); + +describe('validateConfig AWS SES specific checks', () => { + const awsSesHost = 'email-smtp.us-east-1.amazonaws.com'; + const awsSesPort = 465; + const awsSesConfig = { + host: awsSesHost, + port: awsSesPort, + secure: true, + }; + + let configUtilsWithSes: jest.Mocked; + + beforeEach(() => { + configUtilsWithSes = { + ...actionsConfigMock.create(), + getAwsSesConfig: jest.fn(() => awsSesConfig), + } as unknown as jest.Mocked; + }); + + test('throws if both host and port do not match AWS SES config', () => { + const config = { + service: AdditionalEmailServices.AWS_SES, + from: 'bob@example.com', + host: 'wrong-host', + port: 123, + secure: true, + hasAuth: true, + }; + expect(() => { + validateConfig(connectorType, config, { configurationUtilities: configUtilsWithSes }); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action type config: [ses.host]/[ses.port] does not match with the configured AWS SES host/port combination"` + ); + }); + + test('throws if host does not match AWS SES config', () => { + const config = { + service: AdditionalEmailServices.AWS_SES, + from: 'bob@example.com', + host: 'wrong-host', + port: awsSesPort, + secure: true, + hasAuth: true, + }; + expect(() => { + validateConfig(connectorType, config, { configurationUtilities: configUtilsWithSes }); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action type config: [ses.host] does not match with the configured AWS SES host"` + ); + }); + + test('throws if port does not match AWS SES config', () => { + const config = { + service: AdditionalEmailServices.AWS_SES, + from: 'bob@example.com', + host: awsSesHost, + port: 123, + secure: true, + hasAuth: true, + }; + expect(() => { + validateConfig(connectorType, config, { configurationUtilities: configUtilsWithSes }); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action type config: [ses.port] does not match with the configured AWS SES port"` + ); + }); + + test('throws if secure is not true for AWS SES', () => { + const config = { + service: AdditionalEmailServices.AWS_SES, + from: 'bob@example.com', + host: awsSesHost, + port: awsSesPort, + secure: false, + hasAuth: true, + }; + expect(() => { + validateConfig(connectorType, config, { configurationUtilities: configUtilsWithSes }); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action type config: [ses.secure] must be true for AWS SES"` + ); + }); }); function validateEmailAddressesImpl( diff --git a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/email/index.ts b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/email/index.ts index b09be9895ab84..189abf313eea6 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/email/index.ts +++ b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/email/index.ts @@ -81,6 +81,7 @@ function validateConfig( ) { const config = configObject; const { configurationUtilities } = validatorServices; + const awsSesConfig = configurationUtilities.getAwsSesConfig(); const emails = [config.from]; const invalidEmailsMessage = configurationUtilities.validateEmailAddresses(emails); @@ -106,6 +107,21 @@ function validateConfig( if (config.tenantId == null) { throw new Error('[tenantId] is required'); } + } else if (awsSesConfig && config.service === AdditionalEmailServices.AWS_SES) { + if (awsSesConfig.host !== config.host && awsSesConfig.port !== config.port) { + throw new Error( + '[ses.host]/[ses.port] does not match with the configured AWS SES host/port combination' + ); + } + if (awsSesConfig.host !== config.host) { + throw new Error('[ses.host] does not match with the configured AWS SES host'); + } + if (awsSesConfig.port !== config.port) { + throw new Error('[ses.port] does not match with the configured AWS SES port'); + } + if (awsSesConfig.secure !== config.secure) { + throw new Error(`[ses.secure] must be ${awsSesConfig.secure} for AWS SES`); + } } else if (CUSTOM_HOST_PORT_SERVICES.indexOf(config.service) >= 0) { // If configured `service` requires custom host/port/secure settings, validate that they are set if (config.host == null && config.port == null) { @@ -297,6 +313,7 @@ async function executor( connectorUsageCollector, } = execOptions; const connectorTokenClient = services.connectorTokenClient; + const awsSesConfig = configurationUtilities.getAwsSesConfig(); const emails = params.to.concat(params.cc).concat(params.bcc); let invalidEmailsMessage = configurationUtilities.validateEmailAddresses(emails); @@ -348,6 +365,10 @@ async function executor( if (config.oauthTokenUrl !== null) { transport.oauthTokenUrl = config.oauthTokenUrl; } + } else if (awsSesConfig && config.service === AdditionalEmailServices.AWS_SES) { + transport.host = awsSesConfig.host; + transport.port = awsSesConfig.port; + transport.secure = awsSesConfig.secure; } else if (CUSTOM_HOST_PORT_SERVICES.indexOf(config.service) >= 0) { // use configured host/port/secure values // already validated service or host/port is not null ... diff --git a/x-pack/platform/plugins/shared/stack_connectors/server/plugin.ts b/x-pack/platform/plugins/shared/stack_connectors/server/plugin.ts index 325b6b3a88a0c..dd9e2fdbf6880 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/server/plugin.ts +++ b/x-pack/platform/plugins/shared/stack_connectors/server/plugin.ts @@ -11,7 +11,7 @@ import { registerConnectorTypes } from './connector_types'; import { validSlackApiChannelsRoute, getWellKnownEmailServiceRoute } from './routes'; import type { ExperimentalFeatures } from '../common/experimental_features'; import { parseExperimentalConfigValue } from '../common/experimental_features'; -import type { StackConnectorsConfigType } from '../common/types'; +import type { ConfigSchema as StackConnectorsConfigType } from './config'; export interface ConnectorsPluginsSetup { actions: ActionsPluginSetupContract; } @@ -37,7 +37,8 @@ export class StackConnectorsPlugin const router = core.http.createRouter(); const { actions } = plugins; - getWellKnownEmailServiceRoute(router); + const awsSesConfig = actions.getActionsConfigurationUtilities().getAwsSesConfig(); + getWellKnownEmailServiceRoute(router, awsSesConfig); validSlackApiChannelsRoute(router, actions.getActionsConfigurationUtilities(), this.logger); registerConnectorTypes({ diff --git a/x-pack/platform/plugins/shared/stack_connectors/server/routes/get_well_known_email_service.test.ts b/x-pack/platform/plugins/shared/stack_connectors/server/routes/get_well_known_email_service.test.ts index ed8d827f6e237..670e5cb41239c 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/server/routes/get_well_known_email_service.test.ts +++ b/x-pack/platform/plugins/shared/stack_connectors/server/routes/get_well_known_email_service.test.ts @@ -12,7 +12,7 @@ describe('getWellKnownEmailServiceRoute', () => { it('returns config for well known email service', async () => { const router = httpServiceMock.createRouter(); - getWellKnownEmailServiceRoute(router); + getWellKnownEmailServiceRoute(router, null); const [config, handler] = router.get.mock.calls[0]; expect(config.path).toMatchInlineSnapshot( @@ -37,7 +37,7 @@ describe('getWellKnownEmailServiceRoute', () => { it('returns config for elastic cloud email service', async () => { const router = httpServiceMock.createRouter(); - getWellKnownEmailServiceRoute(router); + getWellKnownEmailServiceRoute(router, null); const [config, handler] = router.get.mock.calls[0]; expect(config.path).toMatchInlineSnapshot( @@ -63,7 +63,7 @@ describe('getWellKnownEmailServiceRoute', () => { it('returns empty for unknown service', async () => { const router = httpServiceMock.createRouter(); - getWellKnownEmailServiceRoute(router); + getWellKnownEmailServiceRoute(router, null); const [config, handler] = router.get.mock.calls[0]; expect(config.path).toMatchInlineSnapshot( @@ -80,4 +80,29 @@ describe('getWellKnownEmailServiceRoute', () => { body: {}, }); }); + + it('returns aws simple email service (ses) config if set', async () => { + const awsSesConfig = { + host: 'fake-email-smtp.us-east-1.amazonaws.com', + port: 1, + secure: true, + }; + const router = httpServiceMock.createRouter(); + getWellKnownEmailServiceRoute(router, awsSesConfig); + const [_, handler] = router.get.mock.calls[0]; + const mockResponse = httpServerMock.createResponseFactory(); + const mockRequest = httpServerMock.createKibanaRequest({ + params: { service: 'ses' }, + }); + + await handler({}, mockRequest, mockResponse); + + expect(mockResponse.ok).toHaveBeenCalledWith({ + body: { + host: awsSesConfig.host, + port: awsSesConfig.port, + secure: true, + }, + }); + }); }); diff --git a/x-pack/platform/plugins/shared/stack_connectors/server/routes/get_well_known_email_service.ts b/x-pack/platform/plugins/shared/stack_connectors/server/routes/get_well_known_email_service.ts index cfb240a18ecb5..1297a41244680 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/server/routes/get_well_known_email_service.ts +++ b/x-pack/platform/plugins/shared/stack_connectors/server/routes/get_well_known_email_service.ts @@ -15,6 +15,7 @@ import type { } from '@kbn/core/server'; import nodemailerGetService from 'nodemailer/lib/well-known'; import type SMTPConnection from 'nodemailer/lib/smtp-connection'; +import type { AwsSesConfig } from '@kbn/actions-plugin/server/types'; import { AdditionalEmailServices, INTERNAL_BASE_STACK_CONNECTORS_API_PATH } from '../../common'; import { ELASTIC_CLOUD_SERVICE } from '../connector_types/email'; @@ -22,7 +23,7 @@ const paramSchema = schema.object({ service: schema.string(), }); -export const getWellKnownEmailServiceRoute = (router: IRouter) => { +export const getWellKnownEmailServiceRoute = (router: IRouter, awsSesConfig: AwsSesConfig) => { router.get( { path: `${INTERNAL_BASE_STACK_CONNECTORS_API_PATH}/_email_config/{service}`, @@ -53,6 +54,12 @@ export const getWellKnownEmailServiceRoute = (router: IRouter) => { let response: SMTPConnection.Options = {}; if (service === AdditionalEmailServices.ELASTIC_CLOUD) { response = ELASTIC_CLOUD_SERVICE; + } else if (awsSesConfig && service === AdditionalEmailServices.AWS_SES) { + response = { + host: awsSesConfig.host, + port: awsSesConfig.port, + secure: true, + }; } else { const serviceEntry = nodemailerGetService(service); if (serviceEntry) { diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/with_aws_ses_kibana_config/config.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/with_aws_ses_kibana_config/config.ts new file mode 100644 index 0000000000000..2d5c7cf719cbc --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/with_aws_ses_kibana_config/config.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. + */ + +export const awsSesConfig = { + host: 'email-fips.ca-central-1.amazonaws.com', + port: 25439, +}; + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseConfig = await readConfigFile(require.resolve('../../../../config.base.ts')); + + return { + ...baseConfig.getAll(), + testFiles: [require.resolve('.')], + junit: { + reportName: 'Chrome X-Pack UI Functional Tests with ES SSL - AWS SES Kibana config', + }, + kbnTestServer: { + ...baseConfig.getAll().kbnTestServer, + serverArgs: [ + ...baseConfig.getAll().kbnTestServer.serverArgs, + `--xpack.actions.email.services.ses.host="${awsSesConfig.host}"`, + `--xpack.actions.email.services.ses.port=${awsSesConfig.port}`, + ], + }, + }; +} diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/with_aws_ses_kibana_config/email.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/with_aws_ses_kibana_config/email.ts new file mode 100644 index 0000000000000..950cf9ca27bf6 --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/with_aws_ses_kibana_config/email.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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { awsSesConfig } from './config'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const testSubjects = getService('testSubjects'); + const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']); + + describe('Email - with AWS SES Kibana config', () => { + beforeEach(async () => { + await pageObjects.common.navigateToApp('triggersActionsConnectors'); + }); + + it('should use the kibana config for aws ses', async () => { + await pageObjects.triggersActionsUI.clickCreateConnectorButton(); + await testSubjects.click('.email-card'); + await testSubjects.selectValue('emailServiceSelectInput', 'ses'); + + await testSubjects.waitForAttributeToChange('emailHostInput', 'value', awsSesConfig.host); + expect(await testSubjects.getAttribute('emailPortInput', 'value')).to.be( + awsSesConfig.port.toString() + ); + expect(await testSubjects.getAttribute('emailSecureSwitch', 'aria-checked')).to.be('true'); + }); + }); +}; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/with_aws_ses_kibana_config/index.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/with_aws_ses_kibana_config/index.ts new file mode 100644 index 0000000000000..e0fe99620cdb0 --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/with_aws_ses_kibana_config/index.ts @@ -0,0 +1,21 @@ +/* + * 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 type { FtrProviderContext } from '../../../../../alerting_api_integration/common/ftr_provider_context'; +import { + buildUp, + tearDown, +} from '../../../../../alerting_api_integration/spaces_only/tests/helpers'; + +export default function actionsTests({ loadTestFile, getService }: FtrProviderContext) { + describe('Connectors with AWS SES Kibana config', () => { + before(async () => buildUp(getService)); + after(async () => tearDown(getService)); + + loadTestFile(require.resolve('./email')); + }); +} diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/email.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/email.ts new file mode 100644 index 0000000000000..d4ea85dbd9feb --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/email.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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const testSubjects = getService('testSubjects'); + const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']); + + describe('Email', () => { + beforeEach(async () => { + await pageObjects.common.navigateToApp('triggersActionsConnectors'); + }); + + it('should use the kibana config for aws ses defaults', async () => { + await new Promise((resolve) => setTimeout(resolve, 10000)); + await pageObjects.triggersActionsUI.clickCreateConnectorButton(); + await testSubjects.click('.email-card'); + await testSubjects.selectValue('emailServiceSelectInput', 'ses'); + + await testSubjects.waitForAttributeToChange( + 'emailHostInput', + 'value', + 'email-smtp.us-east-1.amazonaws.com' + ); + expect(await testSubjects.getAttribute('emailPortInput', 'value')).to.be('465'); + expect(await testSubjects.getAttribute('emailSecureSwitch', 'aria-checked')).to.be('true'); + }); + }); +}; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts index 1c662b8c1819e..31520012d1229 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts @@ -18,6 +18,7 @@ export default ({ loadTestFile }: FtrProviderContext) => { loadTestFile(require.resolve('./rules_settings')); loadTestFile(require.resolve('./stack_alerts_page')); loadTestFile(require.resolve('./maintenance_windows')); + loadTestFile(require.resolve('./email')); loadTestFile(require.resolve('./alert_deletion')); }); };