mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[Response Ops][Connectors][Email] xpack.actions.email.services.ses.host/port
kibana config (#221389)
## Summary Closes https://github.com/elastic/kibana/issues/220286 ## Release note New AWS SES Email configuration options `xpack.actions.email.services.ses.host` and `xpack.actions.email.services.ses.port`. --------- Co-authored-by: Lisa Cawley <lcawley@elastic.co>
This commit is contained in:
parent
52e3d1f228
commit
f5b6aa241f
25 changed files with 533 additions and 25 deletions
|
@ -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
|
||||
|
|
|
@ -134,6 +134,22 @@ $$$action-config-email-domain-allowlist$$$
|
|||
|
||||
Data type: `string`
|
||||
|
||||
`xpack.actions.email.services.ses.host` 
|
||||
: 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
|
||||
::::
|
||||
|
||||
Data type: `string`
|
||||
Default: `email-smtp.us-east-1.amazonaws.com`
|
||||
|
||||
`xpack.actions.email.services.ses.port` 
|
||||
: The port number for an Amazon Simple Email Service (SES) service provider that can be used by email connectors.
|
||||
|
||||
Data type: `int`
|
||||
Default: `465`
|
||||
|
||||
`xpack.actions.enableFooterInEmail` 
|
||||
: A boolean value indicating that a footer with a relevant link should be added to emails sent as alerting actions.
|
||||
|
||||
|
|
|
@ -281,6 +281,50 @@ groups:
|
|||
self: all
|
||||
ess: all
|
||||
# example: |
|
||||
|
||||
- setting: xpack.actions.email.services.ses.host
|
||||
id: action-config-email-services-ses-host
|
||||
description: |
|
||||
The SMTP endpoint for an Amazon Simple Email Service (SES) service provider that can be used by email connectors.
|
||||
# state: deprecated/hidden/tech-preview
|
||||
# deprecation_details: ""
|
||||
# note: ""
|
||||
# tip: ""
|
||||
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"
|
||||
# important: ""
|
||||
datatype: string
|
||||
default: "email-smtp.us-east-1.amazonaws.com"
|
||||
# options:
|
||||
# - option:
|
||||
# description: ""
|
||||
# type: static/dynamic
|
||||
applies_to:
|
||||
deployment:
|
||||
self: all
|
||||
ess: all
|
||||
# example: |
|
||||
|
||||
- setting: xpack.actions.email.services.ses.port
|
||||
id: action-config-email-services-ses-port
|
||||
description: |
|
||||
The port number for an Amazon Simple Email Service (SES) service provider that can be used by email connectors.
|
||||
# state: deprecated/hidden/tech-preview
|
||||
# deprecation_details: ""
|
||||
# note: ""
|
||||
# tip: ""
|
||||
# warning: ""
|
||||
# important: ""
|
||||
datatype: int
|
||||
default: 465
|
||||
# options:
|
||||
# - option:
|
||||
# description: ""
|
||||
# type: static/dynamic
|
||||
applies_to:
|
||||
deployment:
|
||||
self: all
|
||||
ess: all
|
||||
# example: |
|
||||
|
||||
- setting: xpack.actions.enableFooterInEmail
|
||||
# id:
|
||||
|
|
|
@ -212,6 +212,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
|
||||
|
|
|
@ -202,7 +202,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?)',
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -571,3 +571,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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
@ -168,7 +169,7 @@ function validateEmails(
|
|||
addresses: string[],
|
||||
options: ValidateEmailAddressesOptions
|
||||
): string | undefined {
|
||||
if (config.email == null) {
|
||||
if (config.email?.domain_allowlist == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -225,5 +226,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;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -238,7 +238,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: [] };
|
||||
|
@ -249,6 +249,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<string, unknown> = {};
|
||||
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
|
||||
|
|
|
@ -121,9 +121,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({
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
|
|
|
@ -281,3 +281,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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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[];
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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<ActionsConfigurationUtilities>;
|
||||
|
||||
beforeEach(() => {
|
||||
configUtilsWithSes = {
|
||||
...actionsConfigMock.create(),
|
||||
getAwsSesConfig: jest.fn(() => awsSesConfig),
|
||||
} as unknown as jest.Mocked<ActionsConfigurationUtilities>;
|
||||
});
|
||||
|
||||
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(
|
||||
|
|
|
@ -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 ...
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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}`,
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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'));
|
||||
});
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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'));
|
||||
});
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue