[ResponseOps] provide config to turn off email action footer (#154919)

resolves https://github.com/elastic/kibana/issues/135402

Allows deployments to not have the default footer added to alerting
emails via the new `xpack.actions.enableFooterInEmail` config setting.
The default value is `true`, which renders the footer.  Setting the
value to `false` will cause no footer to be rendered.

Also changes the footer separator from `--` to `---`, which renders
nicer in HTML, as a `<hr>` element.
This commit is contained in:
Patrick Mueller 2023-04-17 11:19:16 -04:00 committed by GitHub
parent 70d5dad847
commit bcfe4b0005
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 96 additions and 20 deletions

View file

@ -130,6 +130,9 @@ 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.
`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.
`xpack.actions.enabledActionTypes` {ess-icon}::
A list of action types that are enabled. It defaults to `[*]`, enabling all types. The names for built-in {kib} action types are prefixed with a `.` and include: `.email`, `.index`, `.jira`, `.opsgenie`, `.pagerduty`, `.resilient`, `.server-log`, `.servicenow`, .`servicenow-itom`, `.servicenow-sir`, `.slack`, `.swimlane`, `.teams`, `.tines`, `.torq`, `.xmatters`, and `.webhook`. An empty list `[]` will disable all action types.
+

View file

@ -580,6 +580,7 @@ describe('create()', () => {
verificationMode: 'full',
proxyVerificationMode: 'full',
},
enableFooterInEmail: true,
});
const localActionTypeRegistryParams = {

View file

@ -27,6 +27,7 @@ const createActionsConfigMock = () => {
getMicrosoftGraphApiUrl: jest.fn().mockReturnValue(undefined),
validateEmailAddresses: jest.fn().mockReturnValue(undefined),
getMaxAttempts: jest.fn().mockReturnValue(3),
enableFooterInEmail: jest.fn().mockReturnValue(true),
};
return mocked;
};

View file

@ -33,6 +33,7 @@ const defaultActionsConfig: ActionsConfig = {
proxyVerificationMode: 'full',
verificationMode: 'full',
},
enableFooterInEmail: true,
};
describe('ensureUriAllowed', () => {

View file

@ -53,6 +53,7 @@ export interface ActionsConfigurationUtilities {
addresses: string[],
options?: ValidateEmailAddressesOptions
): string | undefined;
enableFooterInEmail: () => boolean;
}
function allowListErrorMessage(field: AllowListingField, value: string) {
@ -215,5 +216,6 @@ export function getActionsConfigurationUtilities(
DEFAULT_MAX_ATTEMPTS
);
},
enableFooterInEmail: () => config.enableFooterInEmail,
};
}

View file

@ -23,6 +23,7 @@ describe('config validation', () => {
"allowedHosts": Array [
"*",
],
"enableFooterInEmail": true,
"enabledActionTypes": Array [
"*",
],
@ -57,6 +58,7 @@ describe('config validation', () => {
"allowedHosts": Array [
"*",
],
"enableFooterInEmail": true,
"enabledActionTypes": Array [
"*",
],
@ -198,6 +200,7 @@ describe('config validation', () => {
"allowedHosts": Array [
"*",
],
"enableFooterInEmail": true,
"enabledActionTypes": Array [
"*",
],

View file

@ -125,6 +125,7 @@ export const configSchema = schema.object({
connectorTypeOverrides: schema.maybe(schema.arrayOf(connectorTypeSchema)),
})
),
enableFooterInEmail: schema.boolean({ defaultValue: true }),
});
export type ActionsConfig = TypeOf<typeof configSchema>;

View file

@ -580,6 +580,7 @@ const BaseActionsConfig: ActionsConfig = {
maxResponseContentLength: ByteSizeValue.parse('1mb'),
responseTimeout: momentDuration(1000 * 30),
customHostSettings: undefined,
enableFooterInEmail: true,
};
function getACUfromConfig(config: Partial<ActionsConfig> = {}): ActionsConfigurationUtilities {

View file

@ -592,6 +592,7 @@ const BaseActionsConfig: ActionsConfig = {
maxResponseContentLength: ByteSizeValue.parse('1mb'),
responseTimeout: momentDuration(1000 * 30),
customHostSettings: undefined,
enableFooterInEmail: true,
};
function getACUfromConfig(config: Partial<ActionsConfig> = {}): ActionsConfigurationUtilities {

View file

@ -73,6 +73,7 @@ describe('custom_host_settings', () => {
rejectUnauthorized: true,
maxResponseContentLength: new ByteSizeValue(1000000),
responseTimeout: moment.duration(60000),
enableFooterInEmail: true,
};
test('ensure it copies over the config parts that it does not touch', () => {

View file

@ -46,6 +46,7 @@ describe('Actions Plugin', () => {
rejectUnauthorized: true,
maxResponseContentLength: new ByteSizeValue(1000000),
responseTimeout: moment.duration(60000),
enableFooterInEmail: true,
});
plugin = new ActionsPlugin(context);
coreSetup = coreMock.createSetup();
@ -218,6 +219,7 @@ describe('Actions Plugin', () => {
rejectUnauthorized: true,
maxResponseContentLength: new ByteSizeValue(1000000),
responseTimeout: moment.duration('60s'),
enableFooterInEmail: true,
...overrides,
};
}
@ -273,6 +275,7 @@ describe('Actions Plugin', () => {
rejectUnauthorized: true,
maxResponseContentLength: new ByteSizeValue(1000000),
responseTimeout: moment.duration(60000),
enableFooterInEmail: true,
});
plugin = new ActionsPlugin(context);
coreSetup = coreMock.createSetup();
@ -341,6 +344,7 @@ describe('Actions Plugin', () => {
rejectUnauthorized: true,
maxResponseContentLength: new ByteSizeValue(1000000),
responseTimeout: moment.duration('60s'),
enableFooterInEmail: true,
...overrides,
};
}

View file

@ -523,6 +523,10 @@ describe('execute()', () => {
logger: mockedLogger,
};
beforeEach(() => {
executorOptions.configurationUtilities = actionsConfigMock.create();
});
test('ensure parameters are as expected', async () => {
sendEmailMock.mockReset();
const result = await connectorType.executor(executorOptions);
@ -540,7 +544,7 @@ describe('execute()', () => {
"content": Object {
"message": "a message to you
--
---
This message was sent by Elastic.",
"subject": "the subject",
@ -591,7 +595,7 @@ describe('execute()', () => {
"content": Object {
"message": "a message to you
--
---
This message was sent by Elastic.",
"subject": "the subject",
@ -642,7 +646,7 @@ describe('execute()', () => {
"content": Object {
"message": "a message to you
--
---
This message was sent by Elastic.",
"subject": "the subject",
@ -738,6 +742,28 @@ describe('execute()', () => {
`);
});
test('provides no footer link when enableFooterInEmail is false', async () => {
const customExecutorOptions: EmailConnectorTypeExecutorOptions = {
...executorOptions,
configurationUtilities: {
...configurationUtilities,
enableFooterInEmail: jest.fn().mockReturnValue(false),
},
};
const connectorTypeWithPublicUrl = getConnectorType({
publicBaseUrl: 'https://localhost:1234/foo/bar',
});
await connectorTypeWithPublicUrl.executor(customExecutorOptions);
expect(customExecutorOptions.configurationUtilities.enableFooterInEmail).toHaveBeenCalledTimes(
1
);
const sendMailCall = sendEmailMock.mock.calls[0][1];
expect(sendMailCall.content.message).toMatchInlineSnapshot(`"a message to you"`);
});
test('provides a footer link to Elastic when publicBaseUrl is defined', async () => {
const connectorTypeWithPublicUrl = getConnectorType({
publicBaseUrl: 'https://localhost:1234/foo/bar',
@ -750,7 +776,7 @@ describe('execute()', () => {
expect(sendMailCall.content.message).toMatchInlineSnapshot(`
"a message to you
--
---
This message was sent by Elastic. [Go to Elastic](https://localhost:1234/foo/bar)."
`);
@ -779,7 +805,7 @@ describe('execute()', () => {
expect(sendMailCall.content.message).toMatchInlineSnapshot(`
"a message to you
--
---
This message was sent by Elastic. [View this in Elastic](https://localhost:1234/foo/bar/my/app)."
`);

View file

@ -54,7 +54,7 @@ export const ELASTIC_CLOUD_SERVICE: SMTPConnection.Options = {
secure: false,
};
const EMAIL_FOOTER_DIVIDER = '\n\n--\n\n';
const EMAIL_FOOTER_DIVIDER = '\n\n---\n\n';
const ConfigSchemaProps = {
service: schema.string({ defaultValue: 'other' }),
@ -319,10 +319,14 @@ async function executor(
transport.service = config.service;
}
const footerMessage = getFooterMessage({
publicBaseUrl,
kibanaFooterLink: params.kibanaFooterLink,
});
let actualMessage = params.message;
if (configurationUtilities.enableFooterInEmail()) {
const footerMessage = getFooterMessage({
publicBaseUrl,
kibanaFooterLink: params.kibanaFooterLink,
});
actualMessage = `${params.message}${EMAIL_FOOTER_DIVIDER}${footerMessage}`;
}
const sendEmailOptions: SendEmailOptions = {
connectorId: actionId,
@ -335,7 +339,7 @@ async function executor(
},
content: {
subject: params.subject,
message: `${params.message}${EMAIL_FOOTER_DIVIDER}${footerMessage}`,
message: actualMessage,
},
hasAuth: config.hasAuth,
configurationUtilities,

View file

@ -27,6 +27,7 @@ interface CreateTestConfigOptions {
testFiles?: string[];
reportName?: string;
useDedicatedTaskRunner: boolean;
enableFooterInEmail?: boolean;
}
// test.not-enabled is specifically not enabled
@ -75,6 +76,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
testFiles = undefined,
reportName = undefined,
useDedicatedTaskRunner,
enableFooterInEmail = true,
} = options;
return async ({ readConfigFile }: FtrConfigProviderContext) => {
@ -173,6 +175,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
'some.non.existent.com',
'smtp.live.com',
])}`,
`--xpack.actions.enableFooterInEmail=${enableFooterInEmail}`,
'--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"',
'--xpack.alerting.invalidateApiKeysTask.interval="15s"',
'--xpack.alerting.healthCheck.interval="1s"',

View file

@ -43,7 +43,7 @@ export function initPlugin(router: IRouter, path: string) {
cc: null,
bcc: null,
subject: 'email-subject',
html: `<p>email-message</p>\n<p>--</p>\n<p>This message was sent by Elastic. <a href=\"https://localhost:5601\">Go to Elastic</a>.</p>\n`,
html: `<p>email-message</p>\n<hr>\n<p>This message was sent by Elastic. <a href=\"https://localhost:5601\">Go to Elastic</a>.</p>\n`,
text: 'email-message\n\n--\n\nThis message was sent by Elastic. [Go to Elastic](https://localhost:5601).',
headers: {},
},

View file

@ -124,8 +124,8 @@ export default function emailTest({ getService }: FtrProviderContext) {
cc: null,
bcc: null,
subject: 'email-subject',
html: `<p>email-message</p>\n<p>--</p>\n<p>This message was sent by Elastic. <a href=\"https://localhost:5601\">Go to Elastic</a>.</p>\n`,
text: 'email-message\n\n--\n\nThis message was sent by Elastic. [Go to Elastic](https://localhost:5601).',
html: `<p>email-message</p>\n<hr>\n<p>This message was sent by Elastic. <a href=\"https://localhost:5601\">Go to Elastic</a>.</p>\n`,
text: 'email-message\n\n---\n\nThis message was sent by Elastic. [Go to Elastic](https://localhost:5601).',
headers: {},
},
});
@ -147,10 +147,10 @@ export default function emailTest({ getService }: FtrProviderContext) {
.then((resp: any) => {
const { text, html } = resp.body.data.message;
expect(text).to.eql(
'_italic_ **bold** https://elastic.co link\n\n--\n\nThis message was sent by Elastic. [Go to Elastic](https://localhost:5601).'
'_italic_ **bold** https://elastic.co link\n\n---\n\nThis message was sent by Elastic. [Go to Elastic](https://localhost:5601).'
);
expect(html).to.eql(
`<p><em>italic</em> <strong>bold</strong> <a href="https://elastic.co">https://elastic.co</a> link</p>\n<p>--</p>\n<p>This message was sent by Elastic. <a href=\"https://localhost:5601\">Go to Elastic</a>.</p>\n`
`<p><em>italic</em> <strong>bold</strong> <a href="https://elastic.co">https://elastic.co</a> link</p>\n<hr>\n<p>This message was sent by Elastic. <a href=\"https://localhost:5601\">Go to Elastic</a>.</p>\n`
);
});
});
@ -174,10 +174,10 @@ export default function emailTest({ getService }: FtrProviderContext) {
.then((resp: any) => {
const { text, html } = resp.body.data.message;
expect(text).to.eql(
'message\n\n--\n\nThis message was sent by Elastic. [View my path in Elastic](https://localhost:5601/my/path).'
'message\n\n---\n\nThis message was sent by Elastic. [View my path in Elastic](https://localhost:5601/my/path).'
);
expect(html).to.eql(
`<p>message</p>\n<p>--</p>\n<p>This message was sent by Elastic. <a href=\"https://localhost:5601/my/path\">View my path in Elastic</a>.</p>\n`
`<p>message</p>\n<hr>\n<p>This message was sent by Elastic. <a href=\"https://localhost:5601/my/path\">View my path in Elastic</a>.</p>\n`
);
});
});
@ -325,8 +325,8 @@ export default function emailTest({ getService }: FtrProviderContext) {
cc: null,
bcc: null,
subject: 'email-subject',
html: `<p>email-message</p>\n<p>--</p>\n<p>This message was sent by Elastic. <a href=\"https://localhost:5601\">Go to Elastic</a>.</p>\n`,
text: 'email-message\n\n--\n\nThis message was sent by Elastic. [Go to Elastic](https://localhost:5601).',
html: `<p>email-message</p>\n<hr>\n<p>This message was sent by Elastic. <a href=\"https://localhost:5601\">Go to Elastic</a>.</p>\n`,
text: 'email-message\n\n---\n\nThis message was sent by Elastic. [Go to Elastic](https://localhost:5601).',
headers: {},
},
});

View file

@ -21,4 +21,5 @@ export default createTestConfig('spaces_only', {
useDedicatedTaskRunner: true,
testFiles: [require.resolve('.')],
reportName: 'X-Pack Alerting API Integration Tests - Actions',
enableFooterInEmail: false,
});

View file

@ -54,6 +54,29 @@ export default function emailTest({ getService }: FtrProviderContext) {
}
});
it('does not have a footer', async () => {
const from = `bob@${EmailDomainAllowed}`;
const conn = await createConnector(from);
expect(conn.status).to.be(200);
const { id } = conn.body;
expect(id).to.be.a('string');
const to = EmailDomainsAllowed.map((domain) => `jeb@${domain}`).sort();
const cc = EmailDomainsAllowed.map((domain) => `jim@${domain}`).sort();
const bcc = EmailDomainsAllowed.map((domain) => `joe@${domain}`).sort();
const ccNames = cc.map((email) => `Jimmy Jack <${email}>`);
const run = await runConnector(id, to, ccNames, bcc);
expect(run.status).to.be(200);
const { status } = run.body || {};
expect(status).to.be('ok');
expect(run.body.data.message.text).to.be('email-message');
});
describe('fails for invalid email domains', () => {
it('in create when invalid "from" used', async () => {
const from = `bob@not.allowed`;