diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts index 7147483998d982..132510ea0ce84e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts @@ -55,6 +55,7 @@ describe('config validation', () => { const config: Record = { service: 'gmail', from: 'bob@example.com', + hasAuth: true, }; expect(validateConfig(actionType, config)).toEqual({ ...config, @@ -66,6 +67,7 @@ describe('config validation', () => { delete config.service; config.host = 'elastic.co'; config.port = 8080; + config.hasAuth = true; expect(validateConfig(actionType, config)).toEqual({ ...config, service: null, @@ -233,6 +235,7 @@ describe('execute()', () => { port: 42, secure: true, from: 'bob@example.com', + hasAuth: true, }; const secrets: ActionTypeSecretsType = { user: 'bob', @@ -269,6 +272,7 @@ describe('execute()', () => { "message": "a message to you", "subject": "the subject", }, + "hasAuth": true, "proxySettings": undefined, "routing": Object { "bcc": Array [ @@ -298,6 +302,7 @@ describe('execute()', () => { port: 42, secure: true, from: 'bob@example.com', + hasAuth: false, }; const secrets: ActionTypeSecretsType = { user: null, @@ -327,6 +332,7 @@ describe('execute()', () => { "message": "a message to you", "subject": "the subject", }, + "hasAuth": false, "proxySettings": undefined, "routing": Object { "bcc": Array [ @@ -356,6 +362,7 @@ describe('execute()', () => { port: 42, secure: true, from: 'bob@example.com', + hasAuth: false, }; const secrets: ActionTypeSecretsType = { user: null, diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.ts b/x-pack/plugins/actions/server/builtin_action_types/email.ts index 6fd2d694b06f78..be2664887d9433 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.ts @@ -36,6 +36,7 @@ const ConfigSchemaProps = { port: schema.nullable(portSchema()), secure: schema.nullable(schema.boolean()), from: schema.string(), + hasAuth: schema.boolean({ defaultValue: true }), }; const ConfigSchema = schema.object(ConfigSchemaProps); @@ -185,6 +186,7 @@ async function executor( message: params.message, }, proxySettings: execOptions.proxySettings, + hasAuth: config.hasAuth, }; let result; diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts index b6c4a4ea882e5a..a1c4041628bd51 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts @@ -64,7 +64,7 @@ describe('send_email module', () => { }); test('handles unauthenticated email using not secure host/port', async () => { - const sendEmailOptions = getSendEmailOptions( + const sendEmailOptions = getSendEmailOptionsNoAuth( { transport: { host: 'example.com', @@ -76,12 +76,7 @@ describe('send_email module', () => { proxyRejectUnauthorizedCertificates: false, } ); - // @ts-expect-error - delete sendEmailOptions.transport.service; - // @ts-expect-error - delete sendEmailOptions.transport.user; - // @ts-expect-error - delete sendEmailOptions.transport.password; + const result = await sendEmail(mockLogger, sendEmailOptions); expect(result).toBe(sendMailMockResult); expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(` @@ -248,5 +243,31 @@ function getSendEmailOptions( password: 'changeme', }, proxySettings, + hasAuth: true, + }; +} + +function getSendEmailOptionsNoAuth( + { content = {}, routing = {}, transport = {} } = {}, + proxySettings?: ProxySettings +) { + return { + content: { + ...content, + message: 'a message', + subject: 'a subject', + }, + routing: { + ...routing, + from: 'fred@example.com', + to: ['jim@example.com'], + cc: ['bob@example.com', 'robert@example.com'], + bcc: [], + }, + transport: { + ...transport, + }, + proxySettings, + hasAuth: false, }; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts index dead8fee63d4ff..f3cdf82bfe8cd0 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts @@ -20,6 +20,7 @@ export interface SendEmailOptions { content: Content; proxySettings?: ProxySettings; rejectUnauthorized?: boolean; + hasAuth: boolean; } // config validation ensures either service is set or host/port are set @@ -46,14 +47,14 @@ export interface Content { // send an email export async function sendEmail(logger: Logger, options: SendEmailOptions): Promise { - const { transport, routing, content, proxySettings, rejectUnauthorized } = options; + const { transport, routing, content, proxySettings, rejectUnauthorized, hasAuth } = options; const { service, host, port, secure, user, password } = transport; const { from, to, cc, bcc } = routing; const { subject, message } = content; const transportConfig: Record = {}; - if (user != null && password != null) { + if (hasAuth && user != null && password != null) { transportConfig.auth = { user, pass: password, diff --git a/x-pack/plugins/actions/server/saved_objects/migrations.test.ts b/x-pack/plugins/actions/server/saved_objects/migrations.test.ts index d577f0c8bbc6c2..1fa5889e77cb04 100644 --- a/x-pack/plugins/actions/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/actions/server/saved_objects/migrations.test.ts @@ -21,6 +21,20 @@ describe('7.10.0', () => { ); }); + test('add hasAuth config property for .email actions', () => { + const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0']; + const action = getMockDataForEmail({}); + expect(migration710(action, context)).toMatchObject({ + ...action, + attributes: { + ...action.attributes, + config: { + hasAuth: true, + }, + }, + }); + }); + test('rename cases configuration object', () => { const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0']; const action = getMockData({}); @@ -36,6 +50,22 @@ describe('7.10.0', () => { }); }); +function getMockDataForEmail( + overwrites: Record = {} +): SavedObjectUnsanitizedDoc { + return { + attributes: { + name: 'abc', + actionTypeId: '.email', + config: {}, + secrets: { user: 'test', password: '123' }, + ...overwrites, + }, + id: uuid.v4(), + type: 'action', + }; +} + function getMockData( overwrites: Record = {} ): SavedObjectUnsanitizedDoc { diff --git a/x-pack/plugins/actions/server/saved_objects/migrations.ts b/x-pack/plugins/actions/server/saved_objects/migrations.ts index 0006d88c44149d..993beef8d9b2bd 100644 --- a/x-pack/plugins/actions/server/saved_objects/migrations.ts +++ b/x-pack/plugins/actions/server/saved_objects/migrations.ts @@ -3,40 +3,87 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - import { SavedObjectMigrationMap, SavedObjectUnsanitizedDoc, SavedObjectMigrationFn, + SavedObjectMigrationContext, } from '../../../../../src/core/server'; import { RawAction } from '../types'; import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; +type ActionMigration = ( + doc: SavedObjectUnsanitizedDoc +) => SavedObjectUnsanitizedDoc; + export function getMigrations( encryptedSavedObjects: EncryptedSavedObjectsPluginSetup ): SavedObjectMigrationMap { - return { '7.10.0': renameCasesConfigurationObject(encryptedSavedObjects) }; + const migrationActions = encryptedSavedObjects.createMigration( + (doc): doc is SavedObjectUnsanitizedDoc => + !!doc.attributes.config?.casesConfiguration || doc.attributes.actionTypeId === '.email', + pipeMigrations(renameCasesConfigurationObject, addHasAuthConfigurationObject) + ); + + return { + '7.10.0': executeMigrationWithErrorHandling(migrationActions, '7.10.0'), + }; } -const renameCasesConfigurationObject = ( - encryptedSavedObjects: EncryptedSavedObjectsPluginSetup -): SavedObjectMigrationFn => { - return encryptedSavedObjects.createMigration( - (doc): doc is SavedObjectUnsanitizedDoc => - !!doc.attributes.config?.casesConfiguration, - (doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc => { - const { casesConfiguration, ...restConfiguration } = doc.attributes.config; - - return { - ...doc, - attributes: { - ...doc.attributes, - config: { - ...restConfiguration, - incidentConfiguration: casesConfiguration, - }, - }, - }; +function executeMigrationWithErrorHandling( + migrationFunc: SavedObjectMigrationFn, + version: string +) { + return (doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) => { + try { + return migrationFunc(doc, context); + } catch (ex) { + context.log.error( + `encryptedSavedObject ${version} migration failed for action ${doc.id} with error: ${ex.message}`, + { actionDocument: doc } + ); } - ); + return doc; + }; +} + +function renameCasesConfigurationObject( + doc: SavedObjectUnsanitizedDoc +): SavedObjectUnsanitizedDoc { + if (!doc.attributes.config?.casesConfiguration) { + return doc; + } + const { casesConfiguration, ...restConfiguration } = doc.attributes.config; + + return { + ...doc, + attributes: { + ...doc.attributes, + config: { + ...restConfiguration, + incidentConfiguration: casesConfiguration, + }, + }, + }; +} + +const addHasAuthConfigurationObject = ( + doc: SavedObjectUnsanitizedDoc +): SavedObjectUnsanitizedDoc => { + const hasAuth = !!doc.attributes.secrets.user || !!doc.attributes.secrets.password; + return { + ...doc, + attributes: { + ...doc.attributes, + config: { + ...doc.attributes.config, + hasAuth, + }, + }, + }; }; + +function pipeMigrations(...migrations: ActionMigration[]): ActionMigration { + return (doc: SavedObjectUnsanitizedDoc) => + migrations.reduce((migratedDoc, nextMigration) => nextMigration(migratedDoc), doc); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx index e823e848f52c2c..ae698f2304e4ec 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx @@ -43,6 +43,7 @@ describe('connector validation', () => { port: 2323, host: 'localhost', test: 'test', + hasAuth: true, }, } as EmailActionConnector; @@ -72,6 +73,7 @@ describe('connector validation', () => { port: 2323, host: 'localhost', test: 'test', + hasAuth: false, }, } as EmailActionConnector; @@ -96,6 +98,7 @@ describe('connector validation', () => { name: 'email', config: { from: 'test@test.com', + hasAuth: true, }, } as EmailActionConnector; @@ -124,6 +127,7 @@ describe('connector validation', () => { port: 2323, host: 'localhost', test: 'test', + hasAuth: true, }, } as EmailActionConnector; @@ -152,6 +156,7 @@ describe('connector validation', () => { port: 2323, host: 'localhost', test: 'test', + hasAuth: true, }, } as EmailActionConnector; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx index 3e8e71991a5944..b75d809f6a3273 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx @@ -75,6 +75,26 @@ export function getActionType(): ActionTypeModel { name: 'email', config: { from: 'test@test.com', + hasAuth: true, }, } as EmailActionConnector; const wrapper = mountWithIntl( @@ -42,4 +43,35 @@ describe('EmailActionConnectorFields renders', () => { expect(wrapper.find('[data-test-subj="emailUserInput"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="emailPasswordInput"]').length > 0).toBeTruthy(); }); + + test('secret connector fields is not rendered when hasAuth false', () => { + const actionConnector = { + secrets: {}, + id: 'test', + actionTypeId: '.email', + name: 'email', + config: { + from: 'test@test.com', + hasAuth: false, + }, + } as EmailActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="emailFromInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="emailFromInput"]').first().prop('value')).toBe( + 'test@test.com' + ); + expect(wrapper.find('[data-test-subj="emailHostInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="emailPortInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="emailUserInput"]').length > 0).toBeFalsy(); + expect(wrapper.find('[data-test-subj="emailPasswordInput"]').length > 0).toBeFalsy(); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx index 4ef9c2e0d4d2e7..1e92e9fc2519c5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment } from 'react'; +import React, { Fragment, useEffect } from 'react'; import { EuiFieldText, EuiFlexItem, @@ -12,6 +12,9 @@ import { EuiFieldPassword, EuiSwitch, EuiFormRow, + EuiTitle, + EuiSpacer, + EuiCallOut, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -22,8 +25,14 @@ import { EmailActionConnector } from '../types'; export const EmailActionConnectorFields: React.FunctionComponent> = ({ action, editActionConfig, editActionSecrets, errors, readOnly, docLinks }) => { - const { from, host, port, secure } = action.config; + const { from, host, port, secure, hasAuth } = action.config; const { user, password } = action.secrets; + useEffect(() => { + if (!action.id) { + editActionConfig('hasAuth', true); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); return ( @@ -160,60 +169,116 @@ export const EmailActionConnectorFields: React.FunctionComponent - + - 0} + + +

+ +

+
+ + - 0} - name="user" - readOnly={readOnly} - value={user || ''} - data-test-subj="emailUserInput" - onChange={(e) => { - editActionSecrets('user', nullableString(e.target.value)); - }} - /> -
-
- - 0} - label={i18n.translate( - 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.passwordFieldLabel', - { - defaultMessage: 'Password', + disabled={readOnly} + checked={hasAuth} + onChange={(e) => { + editActionConfig('hasAuth', e.target.checked); + if (!e.target.checked) { + editActionSecrets('user', null); + editActionSecrets('password', null); } - )} - > - 0} - name="password" - value={password || ''} - data-test-subj="emailPasswordInput" - onChange={(e) => { - editActionSecrets('password', nullableString(e.target.value)); - }} - /> - + }} + />
+ {hasAuth ? ( + <> + {action.id ? ( + <> + + + + + ) : null} + + + 0 && user !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.userTextFieldLabel', + { + defaultMessage: 'Username', + } + )} + > + 0 && user !== undefined} + name="user" + readOnly={readOnly} + value={user || ''} + data-test-subj="emailUserInput" + onChange={(e) => { + editActionSecrets('user', nullableString(e.target.value)); + }} + onBlur={() => { + if (!user) { + editActionSecrets('user', ''); + } + }} + /> + + + + 0 && password !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.passwordFieldLabel', + { + defaultMessage: 'Password', + } + )} + > + 0 && password !== undefined} + name="password" + value={password || ''} + data-test-subj="emailPasswordInput" + onChange={(e) => { + editActionSecrets('password', nullableString(e.target.value)); + }} + onBlur={() => { + if (!password) { + editActionSecrets('password', ''); + } + }} + /> + + + + + ) : null}
); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts index f6bb08148b3cbf..958d77a11c883f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts @@ -69,6 +69,7 @@ export interface EmailConfig { host: string; port: number; secure?: boolean; + hasAuth: boolean; } export interface EmailSecrets { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx index ef6621f98fac2d..f91bd7382b61cc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx @@ -15,6 +15,7 @@ import { EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem, + EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -169,26 +170,37 @@ export const ActionConnectorForm = ({ {FieldsComponent !== null ? ( - - - - - - } - > - - + <> + +

+ +

+
+ + + + + + + } + > + + + ) : null} ); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts index 329bd3433d3880..f6b0f06a6722ec 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts @@ -25,6 +25,7 @@ export default function emailTest({ getService }: FtrProviderContext) { config: { service: '__json', from: 'bob@example.com', + hasAuth: true, }, secrets: { user: 'bob', @@ -41,6 +42,7 @@ export default function emailTest({ getService }: FtrProviderContext) { actionTypeId: '.email', config: { service: '__json', + hasAuth: true, host: null, port: null, secure: null, @@ -62,6 +64,7 @@ export default function emailTest({ getService }: FtrProviderContext) { config: { from: 'bob@example.com', service: '__json', + hasAuth: true, host: null, port: null, secure: null,