kibana/x-pack/plugins/actions/server/builtin_action_types/email.ts
Mike Côté da8ce374cf
Make xpack.actions.rejectUnauthorized setting work (#88690)
* Remove ActionsConfigType due to being a duplicate

* Fix rejectUnauthorized not being configured

* Move proxySettings to configurationUtilities

* Fix isAxiosError check to code

* Add functional test

* Remove comment

* Close webhook server

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
2021-01-28 13:44:25 -05:00

296 lines
8.8 KiB
TypeScript

/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { curry } from 'lodash';
import { i18n } from '@kbn/i18n';
import { schema, TypeOf } from '@kbn/config-schema';
import nodemailerGetService from 'nodemailer/lib/well-known';
import { sendEmail, JSON_TRANSPORT_SERVICE, SendEmailOptions, Transport } from './lib/send_email';
import { portSchema } from './lib/schemas';
import { Logger } from '../../../../../src/core/server';
import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types';
import { ActionsConfigurationUtilities } from '../actions_config';
import { renderMustacheString, renderMustacheObject } from '../lib/mustache_renderer';
export type EmailActionType = ActionType<
ActionTypeConfigType,
ActionTypeSecretsType,
ActionParamsType,
unknown
>;
export type EmailActionTypeExecutorOptions = ActionTypeExecutorOptions<
ActionTypeConfigType,
ActionTypeSecretsType,
ActionParamsType
>;
// config definition
export type ActionTypeConfigType = TypeOf<typeof ConfigSchema>;
const EMAIL_FOOTER_DIVIDER = '\n\n--\n\n';
const ConfigSchemaProps = {
service: schema.nullable(schema.string()),
host: schema.nullable(schema.string()),
port: schema.nullable(portSchema()),
secure: schema.nullable(schema.boolean()),
from: schema.string(),
hasAuth: schema.boolean({ defaultValue: true }),
};
const ConfigSchema = schema.object(ConfigSchemaProps);
function validateConfig(
configurationUtilities: ActionsConfigurationUtilities,
configObject: ActionTypeConfigType
): string | void {
const config = configObject;
// Make sure service is set, or if not, both host/port must be set.
// If service is set, host/port are ignored, when the email is sent.
// Note, not currently making these message translated, as will be
// emitted alongside messages from @kbn/config-schema, which does not
// translate messages.
if (config.service === JSON_TRANSPORT_SERVICE) {
return;
} else if (config.service == null) {
if (config.host == null && config.port == null) {
return 'either [service] or [host]/[port] is required';
}
if (config.host == null) {
return '[host] is required if [service] is not provided';
}
if (config.port == null) {
return '[port] is required if [service] is not provided';
}
if (!configurationUtilities.isHostnameAllowed(config.host)) {
return `[host] value '${config.host}' is not in the allowedHosts configuration`;
}
} else {
const host = getServiceNameHost(config.service);
if (host == null) {
return `[service] value '${config.service}' is not valid`;
}
if (!configurationUtilities.isHostnameAllowed(host)) {
return `[service] value '${config.service}' resolves to host '${host}' which is not in the allowedHosts configuration`;
}
}
}
// secrets definition
export type ActionTypeSecretsType = TypeOf<typeof SecretsSchema>;
const SecretsSchema = schema.object({
user: schema.nullable(schema.string()),
password: schema.nullable(schema.string()),
});
// params definition
export type ActionParamsType = TypeOf<typeof ParamsSchema>;
const ParamsSchema = schema.object(
{
to: schema.arrayOf(schema.string(), { defaultValue: [] }),
cc: schema.arrayOf(schema.string(), { defaultValue: [] }),
bcc: schema.arrayOf(schema.string(), { defaultValue: [] }),
subject: schema.string(),
message: schema.string(),
// kibanaFooterLink isn't inteded for users to set, this is here to be able to programatically
// provide a more contextual URL in the footer (ex: URL to the alert details page)
kibanaFooterLink: schema.object({
path: schema.string({ defaultValue: '/' }),
text: schema.string({
defaultValue: i18n.translate('xpack.actions.builtin.email.kibanaFooterLinkText', {
defaultMessage: 'Go to Kibana',
}),
}),
}),
},
{
validate: validateParams,
}
);
function validateParams(paramsObject: unknown): string | void {
// avoids circular reference ...
const params = paramsObject as ActionParamsType;
const { to, cc, bcc } = params;
const addrs = to.length + cc.length + bcc.length;
if (addrs === 0) {
return 'no [to], [cc], or [bcc] entries';
}
}
interface GetActionTypeParams {
logger: Logger;
publicBaseUrl?: string;
configurationUtilities: ActionsConfigurationUtilities;
}
// action type definition
export const ActionTypeId = '.email';
export function getActionType(params: GetActionTypeParams): EmailActionType {
const { logger, publicBaseUrl, configurationUtilities } = params;
return {
id: ActionTypeId,
minimumLicenseRequired: 'gold',
name: i18n.translate('xpack.actions.builtin.emailTitle', {
defaultMessage: 'Email',
}),
validate: {
config: schema.object(ConfigSchemaProps, {
validate: curry(validateConfig)(configurationUtilities),
}),
secrets: SecretsSchema,
params: ParamsSchema,
},
renderParameterTemplates,
executor: curry(executor)({ logger, publicBaseUrl, configurationUtilities }),
};
}
function renderParameterTemplates(
params: ActionParamsType,
variables: Record<string, unknown>
): ActionParamsType {
return {
// most of the params need no escaping
...renderMustacheObject(params, variables),
// message however, needs to escaped as markdown
message: renderMustacheString(params.message, variables, 'markdown'),
};
}
// action executor
async function executor(
{
logger,
publicBaseUrl,
configurationUtilities,
}: {
logger: GetActionTypeParams['logger'];
publicBaseUrl: GetActionTypeParams['publicBaseUrl'];
configurationUtilities: ActionsConfigurationUtilities;
},
execOptions: EmailActionTypeExecutorOptions
): Promise<ActionTypeExecutorResult<unknown>> {
const actionId = execOptions.actionId;
const config = execOptions.config;
const secrets = execOptions.secrets;
const params = execOptions.params;
const transport: Transport = {};
if (secrets.user != null) {
transport.user = secrets.user;
}
if (secrets.password != null) {
transport.password = secrets.password;
}
if (config.service !== null) {
transport.service = config.service;
} else {
// already validated service or host/port is not null ...
transport.host = config.host!;
transport.port = config.port!;
transport.secure = getSecureValue(config.secure, config.port);
}
const footerMessage = getFooterMessage({
publicBaseUrl,
kibanaFooterLink: params.kibanaFooterLink,
});
const sendEmailOptions: SendEmailOptions = {
transport,
routing: {
from: config.from,
to: params.to,
cc: params.cc,
bcc: params.bcc,
},
content: {
subject: params.subject,
message: `${params.message}${EMAIL_FOOTER_DIVIDER}${footerMessage}`,
},
hasAuth: config.hasAuth,
configurationUtilities,
};
let result;
try {
result = await sendEmail(logger, sendEmailOptions);
} catch (err) {
const message = i18n.translate('xpack.actions.builtin.email.errorSendingErrorMessage', {
defaultMessage: 'error sending email',
});
return {
status: 'error',
actionId,
message,
serviceMessage: err.message,
};
}
return { status: 'ok', data: result, actionId };
}
// utilities
function getServiceNameHost(service: string): string | null {
const serviceEntry = nodemailerGetService(service);
if (serviceEntry === false) return null;
// in theory this won't happen, but it's JS, so just to be safe ...
if (serviceEntry == null) return null;
return serviceEntry.host || null;
}
// Returns the secure value - whether to use TLS or not.
// Respect value if not null | undefined.
// Otherwise, if the port is 465, return true, otherwise return false.
// Based on data here:
// - https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json
function getSecureValue(secure: boolean | null | undefined, port: number | null): boolean {
if (secure != null) return secure;
if (port === 465) return true;
return false;
}
function getFooterMessage({
publicBaseUrl,
kibanaFooterLink,
}: {
publicBaseUrl: GetActionTypeParams['publicBaseUrl'];
kibanaFooterLink: ActionParamsType['kibanaFooterLink'];
}) {
if (!publicBaseUrl) {
return i18n.translate('xpack.actions.builtin.email.sentByKibanaMessage', {
defaultMessage: 'This message was sent by Kibana.',
});
}
return i18n.translate('xpack.actions.builtin.email.customViewInKibanaMessage', {
defaultMessage: 'This message was sent by Kibana. [{kibanaFooterLinkText}]({link}).',
values: {
kibanaFooterLinkText: kibanaFooterLink.text,
link: `${publicBaseUrl}${kibanaFooterLink.path === '/' ? '' : kibanaFooterLink.path}`,
},
});
}