kibana/x-pack/plugins/actions/server/builtin_action_types/webhook.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

307 lines
9.1 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 { i18n } from '@kbn/i18n';
import { curry, isString } from 'lodash';
import axios, { AxiosError, AxiosResponse } from 'axios';
import { schema, TypeOf } from '@kbn/config-schema';
import { pipe } from 'fp-ts/lib/pipeable';
import { map, getOrElse } from 'fp-ts/lib/Option';
import { getRetryAfterIntervalFromHeaders } from './lib/http_rersponse_retry_header';
import { nullableType } from './lib/nullable';
import { isOk, promiseResult, Result } from './lib/result_type';
import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types';
import { ActionsConfigurationUtilities } from '../actions_config';
import { Logger } from '../../../../../src/core/server';
import { request } from './lib/axios_utils';
import { renderMustacheString } from '../lib/mustache_renderer';
// config definition
export enum WebhookMethods {
POST = 'post',
PUT = 'put',
}
export type WebhookActionType = ActionType<
ActionTypeConfigType,
ActionTypeSecretsType,
ActionParamsType,
unknown
>;
export type WebhookActionTypeExecutorOptions = ActionTypeExecutorOptions<
ActionTypeConfigType,
ActionTypeSecretsType,
ActionParamsType
>;
const HeadersSchema = schema.recordOf(schema.string(), schema.string());
const configSchemaProps = {
url: schema.string(),
method: schema.oneOf([schema.literal(WebhookMethods.POST), schema.literal(WebhookMethods.PUT)], {
defaultValue: WebhookMethods.POST,
}),
headers: nullableType(HeadersSchema),
hasAuth: schema.boolean({ defaultValue: true }),
};
const ConfigSchema = schema.object(configSchemaProps);
export type ActionTypeConfigType = TypeOf<typeof ConfigSchema>;
// secrets definition
export type ActionTypeSecretsType = TypeOf<typeof SecretsSchema>;
const secretSchemaProps = {
user: schema.nullable(schema.string()),
password: schema.nullable(schema.string()),
};
const SecretsSchema = schema.object(secretSchemaProps, {
validate: (secrets) => {
// user and password must be set together (or not at all)
if (!secrets.password && !secrets.user) return;
if (secrets.password && secrets.user) return;
return i18n.translate('xpack.actions.builtin.webhook.invalidUsernamePassword', {
defaultMessage: 'both user and password must be specified',
});
},
});
// params definition
export type ActionParamsType = TypeOf<typeof ParamsSchema>;
const ParamsSchema = schema.object({
body: schema.maybe(schema.string()),
});
export const ActionTypeId = '.webhook';
// action type definition
export function getActionType({
logger,
configurationUtilities,
}: {
logger: Logger;
configurationUtilities: ActionsConfigurationUtilities;
}): WebhookActionType {
return {
id: ActionTypeId,
minimumLicenseRequired: 'gold',
name: i18n.translate('xpack.actions.builtin.webhookTitle', {
defaultMessage: 'Webhook',
}),
validate: {
config: schema.object(configSchemaProps, {
validate: curry(validateActionTypeConfig)(configurationUtilities),
}),
secrets: SecretsSchema,
params: ParamsSchema,
},
renderParameterTemplates,
executor: curry(executor)({ logger, configurationUtilities }),
};
}
function renderParameterTemplates(
params: ActionParamsType,
variables: Record<string, unknown>
): ActionParamsType {
if (!params.body) return params;
return {
body: renderMustacheString(params.body, variables, 'json'),
};
}
function validateActionTypeConfig(
configurationUtilities: ActionsConfigurationUtilities,
configObject: ActionTypeConfigType
) {
const configuredUrl = configObject.url;
try {
new URL(configuredUrl);
} catch (err) {
return i18n.translate('xpack.actions.builtin.webhook.webhookConfigurationErrorNoHostname', {
defaultMessage: 'error configuring webhook action: unable to parse url: {err}',
values: {
err,
},
});
}
try {
configurationUtilities.ensureUriAllowed(configuredUrl);
} catch (allowListError) {
return i18n.translate('xpack.actions.builtin.webhook.webhookConfigurationError', {
defaultMessage: 'error configuring webhook action: {message}',
values: {
message: allowListError.message,
},
});
}
}
// action executor
export async function executor(
{
logger,
configurationUtilities,
}: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities },
execOptions: WebhookActionTypeExecutorOptions
): Promise<ActionTypeExecutorResult<unknown>> {
const actionId = execOptions.actionId;
const { method, url, headers = {}, hasAuth } = execOptions.config;
const { body: data } = execOptions.params;
const secrets: ActionTypeSecretsType = execOptions.secrets;
const basicAuth =
hasAuth && isString(secrets.user) && isString(secrets.password)
? { auth: { username: secrets.user, password: secrets.password } }
: {};
const axiosInstance = axios.create();
const result: Result<AxiosResponse, AxiosError> = await promiseResult(
request({
axios: axiosInstance,
method,
url,
logger,
...basicAuth,
headers,
data,
configurationUtilities,
})
);
if (isOk(result)) {
const {
value: { status, statusText },
} = result;
logger.debug(`response from webhook action "${actionId}": [HTTP ${status}] ${statusText}`);
return successResult(actionId, data);
} else {
const { error } = result;
if (error.response) {
const {
status,
statusText,
headers: responseHeaders,
data: { message: responseMessage },
} = error.response;
const responseMessageAsSuffix = responseMessage ? `: ${responseMessage}` : '';
const message = `[${status}] ${statusText}${responseMessageAsSuffix}`;
logger.error(`error on ${actionId} webhook event: ${message}`);
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
// special handling for 5xx
if (status >= 500) {
return retryResult(actionId, message);
}
// special handling for rate limiting
if (status === 429) {
return pipe(
getRetryAfterIntervalFromHeaders(responseHeaders),
map((retry) => retryResultSeconds(actionId, message, retry)),
getOrElse(() => retryResult(actionId, message))
);
}
return errorResultInvalid(actionId, message);
} else if (error.code) {
const message = `[${error.code}] ${error.message}`;
logger.error(`error on ${actionId} webhook event: ${message}`);
return errorResultRequestFailed(actionId, message);
}
logger.error(`error on ${actionId} webhook action: unexpected error`);
return errorResultUnexpectedError(actionId);
}
}
// Action Executor Result w/ internationalisation
function successResult(actionId: string, data: unknown): ActionTypeExecutorResult<unknown> {
return { status: 'ok', data, actionId };
}
function errorResultInvalid(
actionId: string,
serviceMessage: string
): ActionTypeExecutorResult<void> {
const errMessage = i18n.translate('xpack.actions.builtin.webhook.invalidResponseErrorMessage', {
defaultMessage: 'error calling webhook, invalid response',
});
return {
status: 'error',
message: errMessage,
actionId,
serviceMessage,
};
}
function errorResultRequestFailed(
actionId: string,
serviceMessage: string
): ActionTypeExecutorResult<unknown> {
const errMessage = i18n.translate('xpack.actions.builtin.webhook.requestFailedErrorMessage', {
defaultMessage: 'error calling webhook, request failed',
});
return {
status: 'error',
message: errMessage,
actionId,
serviceMessage,
};
}
function errorResultUnexpectedError(actionId: string): ActionTypeExecutorResult<void> {
const errMessage = i18n.translate('xpack.actions.builtin.webhook.unreachableErrorMessage', {
defaultMessage: 'error calling webhook, unexpected error',
});
return {
status: 'error',
message: errMessage,
actionId,
};
}
function retryResult(actionId: string, serviceMessage: string): ActionTypeExecutorResult<void> {
const errMessage = i18n.translate(
'xpack.actions.builtin.webhook.invalidResponseRetryLaterErrorMessage',
{
defaultMessage: 'error calling webhook, retry later',
}
);
return {
status: 'error',
message: errMessage,
retry: true,
actionId,
serviceMessage,
};
}
function retryResultSeconds(
actionId: string,
serviceMessage: string,
retryAfter: number
): ActionTypeExecutorResult<void> {
const retryEpoch = Date.now() + retryAfter * 1000;
const retry = new Date(retryEpoch);
const retryString = retry.toISOString();
const errMessage = i18n.translate(
'xpack.actions.builtin.webhook.invalidResponseRetryDateErrorMessage',
{
defaultMessage: 'error calling webhook, retry at {retryString}',
values: {
retryString,
},
}
);
return {
status: 'error',
message: errMessage,
retry,
actionId,
serviceMessage,
};
}