[Alerting] allow email action to not require auth (#60839)

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

Currently, the built-in email action requires user/password properties to be
set in it's secrets parameters.  This PR changes that requirement, so they
are no longer required.
This commit is contained in:
Patrick Mueller 2020-03-23 19:02:28 -04:00 committed by GitHub
parent dc31736dd2
commit 72bc0eae32
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 185 additions and 47 deletions

View file

@ -184,12 +184,14 @@ describe('secrets validation', () => {
expect(validateSecrets(actionType, secrets)).toEqual(secrets);
});
test('secrets validation fails when secrets is not valid', () => {
expect(() => {
validateSecrets(actionType, {});
}).toThrowErrorMatchingInlineSnapshot(
`"error validating action type secrets: [user]: expected value of type [string] but got [undefined]"`
);
test('secrets validation succeeds when secrets props are null/undefined', () => {
const secrets: Record<string, any> = {
user: null,
password: null,
};
expect(validateSecrets(actionType, {})).toEqual(secrets);
expect(validateSecrets(actionType, { user: null })).toEqual(secrets);
expect(validateSecrets(actionType, { password: null })).toEqual(secrets);
});
});

View file

@ -10,7 +10,6 @@ import { schema, TypeOf } from '@kbn/config-schema';
import nodemailerGetService from 'nodemailer/lib/well-known';
import { sendEmail, JSON_TRANSPORT_SERVICE } from './lib/send_email';
import { nullableType } from './lib/nullable';
import { portSchema } from './lib/schemas';
import { Logger } from '../../../../../src/core/server';
import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types';
@ -20,10 +19,10 @@ import { ActionsConfigurationUtilities } from '../actions_config';
export type ActionTypeConfigType = TypeOf<typeof ConfigSchema>;
const ConfigSchemaProps = {
service: nullableType(schema.string()),
host: nullableType(schema.string()),
port: nullableType(portSchema()),
secure: nullableType(schema.boolean()),
service: schema.nullable(schema.string()),
host: schema.nullable(schema.string()),
port: schema.nullable(portSchema()),
secure: schema.nullable(schema.boolean()),
from: schema.string(),
};
@ -75,8 +74,8 @@ function validateConfig(
export type ActionTypeSecretsType = TypeOf<typeof SecretsSchema>;
const SecretsSchema = schema.object({
user: schema.string(),
password: schema.string(),
user: schema.nullable(schema.string()),
password: schema.nullable(schema.string()),
});
// params definition
@ -144,10 +143,14 @@ async function executor(
const secrets = execOptions.secrets as ActionTypeSecretsType;
const params = execOptions.params as ActionParamsType;
const transport: any = {
user: secrets.user,
password: secrets.password,
};
const transport: any = {};
if (secrets.user != null) {
transport.user = secrets.user;
}
if (secrets.password != null) {
transport.password = secrets.password;
}
if (config.service !== null) {
transport.service = config.service;

View file

@ -58,6 +58,33 @@ describe('connector validation', () => {
});
});
test('connector validation succeeds when connector config is valid with empty user/password', () => {
const actionConnector = {
secrets: {
user: null,
password: null,
},
id: 'test',
actionTypeId: '.email',
name: 'email',
config: {
from: 'test@test.com',
port: 2323,
host: 'localhost',
test: 'test',
},
} as EmailActionConnector;
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
errors: {
from: [],
port: [],
host: [],
user: [],
password: [],
},
});
});
test('connector validation fails when connector config is not valid', () => {
const actionConnector = {
secrets: {
@ -82,6 +109,60 @@ describe('connector validation', () => {
},
});
});
test('connector validation fails when user specified but not password', () => {
const actionConnector = {
secrets: {
user: 'user',
password: null,
},
id: 'test',
actionTypeId: '.email',
name: 'email',
config: {
from: 'test@test.com',
port: 2323,
host: 'localhost',
test: 'test',
},
} as EmailActionConnector;
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
errors: {
from: [],
port: [],
host: [],
user: [],
password: ['Password is required when username is used.'],
},
});
});
test('connector validation fails when password specified but not user', () => {
const actionConnector = {
secrets: {
user: null,
password: 'password',
},
id: 'test',
actionTypeId: '.email',
name: 'email',
config: {
from: 'test@test.com',
port: 2323,
host: 'localhost',
test: 'test',
},
} as EmailActionConnector;
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
errors: {
from: [],
port: [],
host: [],
user: ['Username is required when password is used.'],
password: [],
},
});
});
});
describe('action params validation', () => {

View file

@ -97,22 +97,22 @@ export function getActionType(): ActionTypeModel {
)
);
}
if (!action.secrets.user) {
errors.user.push(
i18n.translate(
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredUserText',
{
defaultMessage: 'Username is required.',
}
)
);
}
if (!action.secrets.password) {
if (action.secrets.user && !action.secrets.password) {
errors.password.push(
i18n.translate(
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPasswordText',
{
defaultMessage: 'Password is required.',
defaultMessage: 'Password is required when username is used.',
}
)
);
}
if (!action.secrets.user && action.secrets.password) {
errors.user.push(
i18n.translate(
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredUserText',
{
defaultMessage: 'Username is required when password is used.',
}
)
);
@ -303,7 +303,7 @@ const EmailActionConnectorFields: React.FunctionComponent<ActionConnectorFieldsP
id="emailUser"
fullWidth
error={errors.user}
isInvalid={errors.user.length > 0 && user !== undefined}
isInvalid={errors.user.length > 0}
label={i18n.translate(
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.userTextFieldLabel',
{
@ -313,17 +313,12 @@ const EmailActionConnectorFields: React.FunctionComponent<ActionConnectorFieldsP
>
<EuiFieldText
fullWidth
isInvalid={errors.user.length > 0 && user !== undefined}
isInvalid={errors.user.length > 0}
name="user"
value={user || ''}
data-test-subj="emailUserInput"
onChange={e => {
editActionSecrets('user', e.target.value);
}}
onBlur={() => {
if (!user) {
editActionSecrets('user', '');
}
editActionSecrets('user', nullableString(e.target.value));
}}
/>
</EuiFormRow>
@ -333,7 +328,7 @@ const EmailActionConnectorFields: React.FunctionComponent<ActionConnectorFieldsP
id="emailPassword"
fullWidth
error={errors.password}
isInvalid={errors.password.length > 0 && password !== undefined}
isInvalid={errors.password.length > 0}
label={i18n.translate(
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.passwordFieldLabel',
{
@ -343,17 +338,12 @@ const EmailActionConnectorFields: React.FunctionComponent<ActionConnectorFieldsP
>
<EuiFieldPassword
fullWidth
isInvalid={errors.password.length > 0 && password !== undefined}
isInvalid={errors.password.length > 0}
name="password"
value={password || ''}
data-test-subj="emailPasswordInput"
onChange={e => {
editActionSecrets('password', e.target.value);
}}
onBlur={() => {
if (!password) {
editActionSecrets('password', '');
}
editActionSecrets('password', nullableString(e.target.value));
}}
/>
</EuiFormRow>
@ -624,3 +614,9 @@ const EmailParamsFields: React.FunctionComponent<ActionParamsProps<EmailActionPa
</Fragment>
);
};
// if the string == null or is empty, return null, else return string
function nullableString(str: string | null | undefined) {
if (str == null || str.trim() === '') return null;
return str;
}

View file

@ -72,8 +72,8 @@ interface EmailConfig {
}
interface EmailSecrets {
user: string;
password: string;
user: string | null;
password: string | null;
}
export interface EmailActionConnector extends ActionConnector {

View file

@ -227,5 +227,61 @@ export default function emailTest({ getService }: FtrProviderContext) {
.expect(200);
expect(typeof createdAction.id).to.be('string');
});
it('should handle an email action with no auth', async () => {
const { body: createdAction } = await supertest
.post('/api/action')
.set('kbn-xsrf', 'foo')
.send({
name: 'An email action with no auth',
actionTypeId: '.email',
config: {
service: '__json',
from: 'jim@example.com',
},
})
.expect(200);
await supertest
.post(`/api/action/${createdAction.id}/_execute`)
.set('kbn-xsrf', 'foo')
.send({
params: {
to: ['kibana-action-test@elastic.co'],
subject: 'email-subject',
message: 'email-message',
},
})
.expect(200)
.then((resp: any) => {
expect(resp.body.data.message.messageId).to.be.a('string');
expect(resp.body.data.messageId).to.be.a('string');
delete resp.body.data.message.messageId;
delete resp.body.data.messageId;
expect(resp.body.data).to.eql({
envelope: {
from: 'jim@example.com',
to: ['kibana-action-test@elastic.co'],
},
message: {
from: { address: 'jim@example.com', name: '' },
to: [
{
address: 'kibana-action-test@elastic.co',
name: '',
},
],
cc: null,
bcc: null,
subject: 'email-subject',
html: '<p>email-message</p>\n',
text: 'email-message',
headers: {},
},
});
});
});
});
}