Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[7.x] [Alerting] set correct parameter for unauthented email action (#63086) #63411

Merged
merged 1 commit into from
Apr 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 106 additions & 1 deletion x-pack/plugins/actions/server/builtin_action_types/email.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,14 @@ describe('execute()', () => {
services,
};
sendEmailMock.mockReset();
await actionType.executor(executorOptions);
const result = await actionType.executor(executorOptions);
expect(result).toMatchInlineSnapshot(`
Object {
"actionId": "some-id",
"data": undefined,
"status": "ok",
}
`);
expect(sendEmailMock.mock.calls[0][1]).toMatchInlineSnapshot(`
Object {
"content": Object {
Expand All @@ -282,4 +289,102 @@ describe('execute()', () => {
}
`);
});

test('parameters are as expected with no auth', async () => {
const config: ActionTypeConfigType = {
service: null,
host: 'a host',
port: 42,
secure: true,
from: '[email protected]',
};
const secrets: ActionTypeSecretsType = {
user: null,
password: null,
};
const params: ActionParamsType = {
to: ['[email protected]'],
cc: ['[email protected]'],
bcc: ['[email protected]'],
subject: 'the subject',
message: 'a message to you',
};

const actionId = 'some-id';
const executorOptions: ActionTypeExecutorOptions = {
actionId,
config,
params,
secrets,
services,
};
sendEmailMock.mockReset();
await actionType.executor(executorOptions);
expect(sendEmailMock.mock.calls[0][1]).toMatchInlineSnapshot(`
Object {
"content": Object {
"message": "a message to you",
"subject": "the subject",
},
"routing": Object {
"bcc": Array [
"[email protected]",
],
"cc": Array [
"[email protected]",
],
"from": "[email protected]",
"to": Array [
"[email protected]",
],
},
"transport": Object {
"host": "a host",
"port": 42,
"secure": true,
},
}
`);
});

test('returns expected result when an error is thrown', async () => {
const config: ActionTypeConfigType = {
service: null,
host: 'a host',
port: 42,
secure: true,
from: '[email protected]',
};
const secrets: ActionTypeSecretsType = {
user: null,
password: null,
};
const params: ActionParamsType = {
to: ['[email protected]'],
cc: ['[email protected]'],
bcc: ['[email protected]'],
subject: 'the subject',
message: 'a message to you',
};

const actionId = 'some-id';
const executorOptions: ActionTypeExecutorOptions = {
actionId,
config,
params,
secrets,
services,
};
sendEmailMock.mockReset();
sendEmailMock.mockRejectedValue(new Error('wops'));
const result = await actionType.executor(executorOptions);
expect(result).toMatchInlineSnapshot(`
Object {
"actionId": "some-id",
"message": "error sending email",
"serviceMessage": "wops",
"status": "error",
}
`);
});
});
11 changes: 6 additions & 5 deletions x-pack/plugins/actions/server/builtin_action_types/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n';
import { schema, TypeOf } from '@kbn/config-schema';
import nodemailerGetService from 'nodemailer/lib/well-known';

import { sendEmail, JSON_TRANSPORT_SERVICE } from './lib/send_email';
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';
Expand Down Expand Up @@ -143,7 +143,7 @@ async function executor(
const secrets = execOptions.secrets as ActionTypeSecretsType;
const params = execOptions.params as ActionParamsType;

const transport: any = {};
const transport: Transport = {};

if (secrets.user != null) {
transport.user = secrets.user;
Expand All @@ -155,12 +155,13 @@ async function executor(
if (config.service !== null) {
transport.service = config.service;
} else {
transport.host = config.host;
transport.port = config.port;
// 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 sendEmailOptions = {
const sendEmailOptions: SendEmailOptions = {
transport,
routing: {
from: config.from,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/*
* 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.
*/

jest.mock('nodemailer', () => ({
createTransport: jest.fn(),
}));

import { Logger } from '../../../../../../src/core/server';
import { sendEmail } from './send_email';
import { loggingServiceMock } from '../../../../../../src/core/server/mocks';
import nodemailer from 'nodemailer';

const createTransportMock = nodemailer.createTransport as jest.Mock;
const sendMailMockResult = { result: 'does not matter' };
const sendMailMock = jest.fn();

const mockLogger = loggingServiceMock.create().get() as jest.Mocked<Logger>;

describe('send_email module', () => {
beforeEach(() => {
jest.resetAllMocks();
createTransportMock.mockReturnValue({ sendMail: sendMailMock });
sendMailMock.mockResolvedValue(sendMailMockResult);
});

test('handles authenticated email using service', async () => {
const sendEmailOptions = getSendEmailOptions();
const result = await sendEmail(mockLogger, sendEmailOptions);
expect(result).toBe(sendMailMockResult);
expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Object {
"auth": Object {
"pass": "changeme",
"user": "elastic",
},
"service": "whatever",
},
]
`);
expect(sendMailMock.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Object {
"bcc": Array [],
"cc": Array [
"[email protected]",
"[email protected]",
],
"from": "[email protected]",
"html": "<p>a message</p>
",
"subject": "a subject",
"text": "a message",
"to": Array [
"[email protected]",
],
},
]
`);
});

test('handles unauthenticated email using not secure host/port', async () => {
const sendEmailOptions = getSendEmailOptions();
delete sendEmailOptions.transport.service;
delete sendEmailOptions.transport.user;
delete sendEmailOptions.transport.password;
sendEmailOptions.transport.host = 'example.com';
sendEmailOptions.transport.port = 1025;
const result = await sendEmail(mockLogger, sendEmailOptions);
expect(result).toBe(sendMailMockResult);
expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Object {
"host": "example.com",
"port": 1025,
"secure": false,
"tls": Object {
"rejectUnauthorized": false,
},
},
]
`);
expect(sendMailMock.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Object {
"bcc": Array [],
"cc": Array [
"[email protected]",
"[email protected]",
],
"from": "[email protected]",
"html": "<p>a message</p>
",
"subject": "a subject",
"text": "a message",
"to": Array [
"[email protected]",
],
},
]
`);
});

test('handles unauthenticated email using secure host/port', async () => {
const sendEmailOptions = getSendEmailOptions();
delete sendEmailOptions.transport.service;
delete sendEmailOptions.transport.user;
delete sendEmailOptions.transport.password;
sendEmailOptions.transport.host = 'example.com';
sendEmailOptions.transport.port = 1025;
sendEmailOptions.transport.secure = true;
const result = await sendEmail(mockLogger, sendEmailOptions);
expect(result).toBe(sendMailMockResult);
expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Object {
"host": "example.com",
"port": 1025,
"secure": true,
},
]
`);
expect(sendMailMock.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Object {
"bcc": Array [],
"cc": Array [
"[email protected]",
"[email protected]",
],
"from": "[email protected]",
"html": "<p>a message</p>
",
"subject": "a subject",
"text": "a message",
"to": Array [
"[email protected]",
],
},
]
`);
});

test('passes nodemailer exceptions to caller', async () => {
const sendEmailOptions = getSendEmailOptions();

sendMailMock.mockReset();
sendMailMock.mockRejectedValue(new Error('wops'));

await expect(sendEmail(mockLogger, sendEmailOptions)).rejects.toThrow('wops');
});
});

function getSendEmailOptions(): any {
return {
content: {
message: 'a message',
subject: 'a subject',
},
routing: {
from: '[email protected]',
to: ['[email protected]'],
cc: ['[email protected]', '[email protected]'],
bcc: [],
},
transport: {
service: 'whatever',
user: 'elastic',
password: 'changeme',
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,30 @@ import { Logger } from '../../../../../../src/core/server';
// an email "service" which doesn't actually send, just returns what it would send
export const JSON_TRANSPORT_SERVICE = '__json';

interface SendEmailOptions {
export interface SendEmailOptions {
transport: Transport;
routing: Routing;
content: Content;
}

// config validation ensures either service is set or host/port are set
interface Transport {
user: string;
password: string;
export interface Transport {
user?: string;
password?: string;
service?: string; // see: https://nodemailer.com/smtp/well-known/
host?: string;
port?: number;
secure?: boolean; // see: https://nodemailer.com/smtp/#tls-options
}

interface Routing {
export interface Routing {
from: string;
to: string[];
cc: string[];
bcc: string[];
}

interface Content {
export interface Content {
subject: string;
message: string;
}
Expand All @@ -49,12 +49,14 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom
const { from, to, cc, bcc } = routing;
const { subject, message } = content;

const transportConfig: Record<string, any> = {
auth: {
const transportConfig: Record<string, any> = {};

if (user != null && password != null) {
transportConfig.auth = {
user,
pass: password,
},
};
};
}

if (service === JSON_TRANSPORT_SERVICE) {
transportConfig.jsonTransport = true;
Expand Down