[Actions] Microsoft Teams connector (#83169)

* First cut at adding teams connector

* Getting teams connector working

* Unit tests

* Updating docs

* PR comments

* PR comments

* Changing error to debug log

* Fixing imports

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
ymao1 2020-11-20 11:14:17 -05:00 committed by GitHub
parent 00e59512fa
commit 8ca1e93763
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1208 additions and 1 deletions

View file

@ -23,6 +23,9 @@ a| <<jira-action-type, Jira>>
| Create an incident in Jira.
a| <<teams-action-type, Microsoft Teams>>
| Send a message to a Microsoft Teams channel.
a| <<pagerduty-action-type, PagerDuty>>
@ -65,6 +68,7 @@ include::action-types/email.asciidoc[]
include::action-types/resilient.asciidoc[]
include::action-types/index.asciidoc[]
include::action-types/jira.asciidoc[]
include::action-types/teams.asciidoc[]
include::action-types/pagerduty.asciidoc[]
include::action-types/server-log.asciidoc[]
include::action-types/servicenow.asciidoc[]

View file

@ -0,0 +1,58 @@
[role="xpack"]
[[teams-action-type]]
=== Microsoft Teams action
The Microsoft Teams action type uses https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook[Incoming Webhooks].
[float]
[[teams-connector-configuration]]
==== Connector configuration
Microsoft Teams connectors have the following configuration properties:
Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action.
Webhook URL:: The URL of the incoming webhook. See https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook#add-an-incoming-webhook-to-a-teams-channel[Add Incoming Webhooks] for instructions on generating this URL. If you are using the <<action-settings, `xpack.actions.allowedHosts`>> setting, make sure the hostname is added to the allowed hosts.
[float]
[[Preconfigured-teams-configuration]]
==== Preconfigured action type
[source,text]
--
my-teams:
name: preconfigured-teams-action-type
actionTypeId: .teams
config:
webhookUrl: 'https://outlook.office.com/webhook/abcd@0123456/IncomingWebhook/abcdefgh/ijklmnopqrstuvwxyz'
--
`config` defines the action type specific to the configuration.
`config` contains
`webhookUrl`, a string that corresponds to *Webhook URL*.
[float]
[[teams-action-configuration]]
==== Action configuration
Microsoft Teams actions have the following properties:
Message:: The message text, converted to the `text` field in the Webhook JSON payload. Currently only the text field is supported. Markdown, images, and other advanced formatting are not yet supported.
[[configuring-teams]]
==== Configuring Microsoft Teams Accounts
You need a https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook[Microsoft Teams webhook URL] to
configure a Microsoft Teams action. To create a webhook
URL, add the **Incoming Webhook App** through the Microsoft Teams console:
. Log in to http://teams.microsoft.com[teams.microsoft.com] as a team administrator.
. Navigate to the Apps directory, search for and select the *Incoming Webhook* app.
. Choose _Add to team_ and select a team and channel for the app.
. Enter a name for your webhook and (optionally) upload a custom icon.
+
image::images/teams-add-webhook-integration.png[]
. Click *Create*.
. Copy the generated webhook URL so you can paste it into your Teams connector form.
+
image::images/teams-copy-webhook-url.png[]

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View file

@ -14,7 +14,15 @@ import { actionsConfigMock } from '../actions_config.mock';
import { licenseStateMock } from '../lib/license_state.mock';
import { licensingMock } from '../../../licensing/server/mocks';
const ACTION_TYPE_IDS = ['.index', '.email', '.pagerduty', '.server-log', '.slack', '.webhook'];
const ACTION_TYPE_IDS = [
'.index',
'.email',
'.pagerduty',
'.server-log',
'.slack',
'.teams',
'.webhook',
];
export function createActionTypeRegistry(): {
logger: jest.Mocked<Logger>;

View file

@ -17,6 +17,7 @@ import { getActionType as getWebhookActionType } from './webhook';
import { getActionType as getServiceNowActionType } from './servicenow';
import { getActionType as getJiraActionType } from './jira';
import { getActionType as getResilientActionType } from './resilient';
import { getActionType as getTeamsActionType } from './teams';
export function registerBuiltInActionTypes({
actionsConfigUtils: configurationUtilities,
@ -36,4 +37,5 @@ export function registerBuiltInActionTypes({
actionTypeRegistry.register(getServiceNowActionType({ logger, configurationUtilities }));
actionTypeRegistry.register(getJiraActionType({ logger, configurationUtilities }));
actionTypeRegistry.register(getResilientActionType({ logger, configurationUtilities }));
actionTypeRegistry.register(getTeamsActionType({ logger, configurationUtilities }));
}

View file

@ -0,0 +1,266 @@
/*
* 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 { Logger } from '../../../../../src/core/server';
import { Services } from '../types';
import { validateParams, validateSecrets } from '../lib';
import axios from 'axios';
import { ActionParamsType, ActionTypeSecretsType, getActionType, TeamsActionType } from './teams';
import { actionsConfigMock } from '../actions_config.mock';
import { actionsMock } from '../mocks';
import { createActionTypeRegistry } from './index.test';
import * as utils from './lib/axios_utils';
jest.mock('axios');
jest.mock('./lib/axios_utils', () => {
const originalUtils = jest.requireActual('./lib/axios_utils');
return {
...originalUtils,
request: jest.fn(),
patch: jest.fn(),
};
});
axios.create = jest.fn(() => axios);
const requestMock = utils.request as jest.Mock;
const ACTION_TYPE_ID = '.teams';
const services: Services = actionsMock.createServices();
let actionType: TeamsActionType;
let mockedLogger: jest.Mocked<Logger>;
beforeAll(() => {
const { logger, actionTypeRegistry } = createActionTypeRegistry();
actionType = actionTypeRegistry.get<{}, ActionTypeSecretsType, ActionParamsType>(ACTION_TYPE_ID);
mockedLogger = logger;
});
describe('action registration', () => {
test('returns action type', () => {
expect(actionType.id).toEqual(ACTION_TYPE_ID);
expect(actionType.name).toEqual('Microsoft Teams');
});
});
describe('validateParams()', () => {
test('should validate and pass when params is valid', () => {
expect(validateParams(actionType, { message: 'a message' })).toEqual({
message: 'a message',
});
});
test('should validate and throw error when params is invalid', () => {
expect(() => {
validateParams(actionType, {});
}).toThrowErrorMatchingInlineSnapshot(
`"error validating action params: [message]: expected value of type [string] but got [undefined]"`
);
expect(() => {
validateParams(actionType, { message: 1 });
}).toThrowErrorMatchingInlineSnapshot(
`"error validating action params: [message]: expected value of type [string] but got [number]"`
);
});
});
describe('validateActionTypeSecrets()', () => {
test('should validate and pass when config is valid', () => {
validateSecrets(actionType, {
webhookUrl: 'https://example.com',
});
});
test('should validate and throw error when config is invalid', () => {
expect(() => {
validateSecrets(actionType, {});
}).toThrowErrorMatchingInlineSnapshot(
`"error validating action type secrets: [webhookUrl]: expected value of type [string] but got [undefined]"`
);
expect(() => {
validateSecrets(actionType, { webhookUrl: 1 });
}).toThrowErrorMatchingInlineSnapshot(
`"error validating action type secrets: [webhookUrl]: expected value of type [string] but got [number]"`
);
expect(() => {
validateSecrets(actionType, { webhookUrl: 'fee-fi-fo-fum' });
}).toThrowErrorMatchingInlineSnapshot(
`"error validating action type secrets: error configuring teams action: unable to parse host name from webhookUrl"`
);
});
test('should validate and pass when the teams webhookUrl is added to allowedHosts', () => {
actionType = getActionType({
logger: mockedLogger,
configurationUtilities: {
...actionsConfigMock.create(),
ensureUriAllowed: (url) => {
expect(url).toEqual('https://outlook.office.com/');
},
},
});
expect(validateSecrets(actionType, { webhookUrl: 'https://outlook.office.com/' })).toEqual({
webhookUrl: 'https://outlook.office.com/',
});
});
test('config validation returns an error if the specified URL isnt added to allowedHosts', () => {
actionType = getActionType({
logger: mockedLogger,
configurationUtilities: {
...actionsConfigMock.create(),
ensureHostnameAllowed: () => {
throw new Error(`target hostname is not added to allowedHosts`);
},
},
});
expect(() => {
validateSecrets(actionType, { webhookUrl: 'https://outlook.office.com/' });
}).toThrowErrorMatchingInlineSnapshot(
`"error validating action type secrets: error configuring teams action: target hostname is not added to allowedHosts"`
);
});
});
describe('execute()', () => {
beforeAll(() => {
requestMock.mockReset();
actionType = getActionType({
logger: mockedLogger,
configurationUtilities: actionsConfigMock.create(),
});
});
beforeEach(() => {
requestMock.mockReset();
requestMock.mockResolvedValue({
status: 200,
statusText: '',
data: '',
headers: [],
config: {},
});
});
test('calls the mock executor with success', async () => {
const response = await actionType.executor({
actionId: 'some-id',
services,
config: {},
secrets: { webhookUrl: 'http://example.com' },
params: { message: 'this invocation should succeed' },
});
expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(`
Object {
"axios": undefined,
"data": Object {
"text": "this invocation should succeed",
},
"logger": Object {
"context": Array [],
"debug": [MockFunction] {
"calls": Array [
Array [
"response from teams action \\"some-id\\": [HTTP 200] ",
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
},
"error": [MockFunction],
"fatal": [MockFunction],
"get": [MockFunction],
"info": [MockFunction],
"log": [MockFunction],
"trace": [MockFunction],
"warn": [MockFunction],
},
"method": "post",
"proxySettings": undefined,
"url": "http://example.com",
}
`);
expect(response).toMatchInlineSnapshot(`
Object {
"actionId": "some-id",
"data": Object {
"text": "this invocation should succeed",
},
"status": "ok",
}
`);
});
test('calls the mock executor with success proxy', async () => {
const response = await actionType.executor({
actionId: 'some-id',
services,
config: {},
secrets: { webhookUrl: 'http://example.com' },
params: { message: 'this invocation should succeed' },
proxySettings: {
proxyUrl: 'https://someproxyhost',
proxyRejectUnauthorizedCertificates: false,
},
});
expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(`
Object {
"axios": undefined,
"data": Object {
"text": "this invocation should succeed",
},
"logger": Object {
"context": Array [],
"debug": [MockFunction] {
"calls": Array [
Array [
"response from teams action \\"some-id\\": [HTTP 200] ",
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
},
"error": [MockFunction],
"fatal": [MockFunction],
"get": [MockFunction],
"info": [MockFunction],
"log": [MockFunction],
"trace": [MockFunction],
"warn": [MockFunction],
},
"method": "post",
"proxySettings": Object {
"proxyRejectUnauthorizedCertificates": false,
"proxyUrl": "https://someproxyhost",
},
"url": "http://example.com",
}
`);
expect(response).toMatchInlineSnapshot(`
Object {
"actionId": "some-id",
"data": Object {
"text": "this invocation should succeed",
},
"status": "ok",
}
`);
});
});

View file

@ -0,0 +1,229 @@
/*
* 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 { URL } from 'url';
import { curry, isString } from 'lodash';
import axios, { AxiosError, AxiosResponse } from 'axios';
import { i18n } from '@kbn/i18n';
import { schema, TypeOf } from '@kbn/config-schema';
import { pipe } from 'fp-ts/lib/pipeable';
import { map, getOrElse } from 'fp-ts/lib/Option';
import { Logger } from '../../../../../src/core/server';
import { getRetryAfterIntervalFromHeaders } from './lib/http_rersponse_retry_header';
import { isOk, promiseResult, Result } from './lib/result_type';
import { request } from './lib/axios_utils';
import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types';
import { ActionsConfigurationUtilities } from '../actions_config';
export type TeamsActionType = ActionType<{}, ActionTypeSecretsType, ActionParamsType, unknown>;
export type TeamsActionTypeExecutorOptions = ActionTypeExecutorOptions<
{},
ActionTypeSecretsType,
ActionParamsType
>;
// secrets definition
export type ActionTypeSecretsType = TypeOf<typeof SecretsSchema>;
const secretsSchemaProps = {
webhookUrl: schema.string(),
};
const SecretsSchema = schema.object(secretsSchemaProps);
// params definition
export type ActionParamsType = TypeOf<typeof ParamsSchema>;
const ParamsSchema = schema.object({
message: schema.string({ minLength: 1 }),
});
// action type definition
export function getActionType({
logger,
configurationUtilities,
}: {
logger: Logger;
configurationUtilities: ActionsConfigurationUtilities;
}): TeamsActionType {
return {
id: '.teams',
minimumLicenseRequired: 'gold',
name: i18n.translate('xpack.actions.builtin.teamsTitle', {
defaultMessage: 'Microsoft Teams',
}),
validate: {
secrets: schema.object(secretsSchemaProps, {
validate: curry(validateActionTypeConfig)(configurationUtilities),
}),
params: ParamsSchema,
},
executor: curry(teamsExecutor)({ logger }),
};
}
function validateActionTypeConfig(
configurationUtilities: ActionsConfigurationUtilities,
secretsObject: ActionTypeSecretsType
) {
let url: URL;
try {
url = new URL(secretsObject.webhookUrl);
} catch (err) {
return i18n.translate('xpack.actions.builtin.teams.teamsConfigurationErrorNoHostname', {
defaultMessage: 'error configuring teams action: unable to parse host name from webhookUrl',
});
}
try {
configurationUtilities.ensureHostnameAllowed(url.hostname);
} catch (allowListError) {
return i18n.translate('xpack.actions.builtin.teams.teamsConfigurationError', {
defaultMessage: 'error configuring teams action: {message}',
values: {
message: allowListError.message,
},
});
}
}
// action executor
async function teamsExecutor(
{ logger }: { logger: Logger },
execOptions: TeamsActionTypeExecutorOptions
): Promise<ActionTypeExecutorResult<unknown>> {
const actionId = execOptions.actionId;
const secrets = execOptions.secrets;
const params = execOptions.params;
const { webhookUrl } = secrets;
const { message } = params;
const data = { text: message };
const axiosInstance = axios.create();
const result: Result<AxiosResponse, AxiosError> = await promiseResult(
request({
axios: axiosInstance,
method: 'post',
url: webhookUrl,
logger,
data,
proxySettings: execOptions.proxySettings,
})
);
if (isOk(result)) {
const {
value: { status, statusText, data: responseData, headers: responseHeaders },
} = result;
// Microsoft Teams connectors do not throw 429s. Rather they will return a 200 response
// with a 429 message in the response body when the rate limit is hit
// https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using#rate-limiting-for-connectors
if (isString(responseData) && responseData.includes('ErrorCode:ApplicationThrottled')) {
return pipe(
getRetryAfterIntervalFromHeaders(responseHeaders),
map((retry) => retryResultSeconds(actionId, message, retry)),
getOrElse(() => retryResult(actionId, message))
);
}
logger.debug(`response from teams action "${actionId}": [HTTP ${status}] ${statusText}`);
return successResult(actionId, data);
} else {
const { error } = result;
if (error.response) {
const { status, statusText } = error.response;
const serviceMessage = `[${status}] ${statusText}`;
logger.error(`error on ${actionId} Microsoft Teams event: ${serviceMessage}`);
// special handling for 5xx
if (status >= 500) {
return retryResult(actionId, serviceMessage);
}
return errorResultInvalid(actionId, serviceMessage);
}
logger.debug(`error on ${actionId} Microsoft Teams action: unexpected error`);
return errorResultUnexpectedError(actionId);
}
}
function successResult(actionId: string, data: unknown): ActionTypeExecutorResult<unknown> {
return { status: 'ok', data, actionId };
}
function errorResultUnexpectedError(actionId: string): ActionTypeExecutorResult<void> {
const errMessage = i18n.translate('xpack.actions.builtin.teams.unreachableErrorMessage', {
defaultMessage: 'error posting to Microsoft Teams, unexpected error',
});
return {
status: 'error',
message: errMessage,
actionId,
};
}
function errorResultInvalid(
actionId: string,
serviceMessage: string
): ActionTypeExecutorResult<void> {
const errMessage = i18n.translate('xpack.actions.builtin.teams.invalidResponseErrorMessage', {
defaultMessage: 'error posting to Microsoft Teams, invalid response',
});
return {
status: 'error',
message: errMessage,
actionId,
serviceMessage,
};
}
function retryResult(actionId: string, message: string): ActionTypeExecutorResult<void> {
const errMessage = i18n.translate(
'xpack.actions.builtin.teams.errorPostingRetryLaterErrorMessage',
{
defaultMessage: 'error posting a Microsoft Teams message, retry later',
}
);
return {
status: 'error',
message: errMessage,
retry: true,
actionId,
};
}
function retryResultSeconds(
actionId: string,
message: 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.teams.errorPostingRetryDateErrorMessage',
{
defaultMessage: 'error posting a Microsoft Teams message, retry at {retryString}',
values: {
retryString,
},
}
);
return {
status: 'error',
message: errMessage,
retry,
actionId,
serviceMessage: message,
};
}

View file

@ -15,6 +15,7 @@ import { ActionTypeModel } from '../../../types';
import { getServiceNowActionType } from './servicenow';
import { getJiraActionType } from './jira';
import { getResilientActionType } from './resilient';
import { getTeamsActionType } from './teams';
export function registerBuiltInActionTypes({
actionTypeRegistry,
@ -30,4 +31,5 @@ export function registerBuiltInActionTypes({
actionTypeRegistry.register(getServiceNowActionType());
actionTypeRegistry.register(getJiraActionType());
actionTypeRegistry.register(getResilientActionType());
actionTypeRegistry.register(getTeamsActionType());
}

View file

@ -0,0 +1,7 @@
/*
* 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.
*/
export { getActionType as getTeamsActionType } from './teams';

View file

@ -0,0 +1,131 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="256px" height="256px" viewBox="0 0 256 256" enable-background="new 0 0 256 256" xml:space="preserve"> <image id="image0" width="256" height="256" x="0" y="0"
xlink:href="data:image/png;base64,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" />
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -0,0 +1,123 @@
/*
* 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 { TypeRegistry } from '../../../type_registry';
import { registerBuiltInActionTypes } from '.././index';
import { ActionTypeModel } from '../../../../types';
import { TeamsActionConnector } from '../types';
const ACTION_TYPE_ID = '.teams';
let actionTypeModel: ActionTypeModel;
beforeAll(async () => {
const actionTypeRegistry = new TypeRegistry<ActionTypeModel>();
registerBuiltInActionTypes({ actionTypeRegistry });
const getResult = actionTypeRegistry.get(ACTION_TYPE_ID);
if (getResult !== null) {
actionTypeModel = getResult;
}
});
describe('actionTypeRegistry.get() works', () => {
test('action type static data is as expected', () => {
expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID);
});
});
describe('teams connector validation', () => {
test('connector validation succeeds when connector config is valid', () => {
const actionConnector = {
secrets: {
webhookUrl: 'https:\\test',
},
id: 'test',
actionTypeId: '.teams',
name: 'team',
config: {},
} as TeamsActionConnector;
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
errors: {
webhookUrl: [],
},
});
});
test('connector validation fails when connector config is not valid - empty webhook url', () => {
const actionConnector = {
secrets: {},
id: 'test',
actionTypeId: '.teams',
name: 'team',
config: {},
} as TeamsActionConnector;
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
errors: {
webhookUrl: ['Webhook URL is required.'],
},
});
});
test('connector validation fails when connector config is not valid - invalid webhook url', () => {
const actionConnector = {
secrets: {
webhookUrl: 'h',
},
id: 'test',
actionTypeId: '.teams',
name: 'team',
config: {},
} as TeamsActionConnector;
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
errors: {
webhookUrl: ['Webhook URL is invalid.'],
},
});
});
test('connector validation fails when connector config is not valid - invalid webhook url protocol', () => {
const actionConnector = {
secrets: {
webhookUrl: 'http://insecure',
},
id: 'test',
actionTypeId: '.teams',
name: 'team',
config: {},
} as TeamsActionConnector;
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
errors: {
webhookUrl: ['Webhook URL must start with https://.'],
},
});
});
});
describe('teams action params validation', () => {
test('if action params validation succeeds when action params is valid', () => {
const actionParams = {
message: 'message {test}',
};
expect(actionTypeModel.validateParams(actionParams)).toEqual({
errors: { message: [] },
});
});
test('params validation fails when message is not valid', () => {
const actionParams = {
message: '',
};
expect(actionTypeModel.validateParams(actionParams)).toEqual({
errors: {
message: ['Message is required.'],
},
});
});
});

View file

@ -0,0 +1,88 @@
/*
* 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 { lazy } from 'react';
import { i18n } from '@kbn/i18n';
import teamsSvg from './teams.svg';
import { ActionTypeModel, ValidationResult } from '../../../../types';
import { TeamsActionParams, TeamsSecrets, TeamsActionConnector } from '../types';
import { isValidUrl } from '../../../lib/value_validators';
export function getActionType(): ActionTypeModel<unknown, TeamsSecrets, TeamsActionParams> {
return {
id: '.teams',
iconClass: teamsSvg,
selectMessage: i18n.translate(
'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.selectMessageText',
{
defaultMessage: 'Send a message to a Microsoft Teams channel.',
}
),
actionTypeTitle: i18n.translate(
'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.actionTypeTitle',
{
defaultMessage: 'Send a message to a Microsoft Teams channel.',
}
),
validateConnector: (action: TeamsActionConnector): ValidationResult => {
const validationResult = { errors: {} };
const errors = {
webhookUrl: new Array<string>(),
};
validationResult.errors = errors;
if (!action.secrets.webhookUrl) {
errors.webhookUrl.push(
i18n.translate(
'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requiredWebhookUrlText',
{
defaultMessage: 'Webhook URL is required.',
}
)
);
} else if (action.secrets.webhookUrl) {
if (!isValidUrl(action.secrets.webhookUrl)) {
errors.webhookUrl.push(
i18n.translate(
'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.invalidWebhookUrlText',
{
defaultMessage: 'Webhook URL is invalid.',
}
)
);
} else if (!isValidUrl(action.secrets.webhookUrl, 'https:')) {
errors.webhookUrl.push(
i18n.translate(
'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requireHttpsWebhookUrlText',
{
defaultMessage: 'Webhook URL must start with https://.',
}
)
);
}
}
return validationResult;
},
validateParams: (actionParams: TeamsActionParams): ValidationResult => {
const validationResult = { errors: {} };
const errors = {
message: new Array<string>(),
};
validationResult.errors = errors;
if (!actionParams.message?.length) {
errors.message.push(
i18n.translate(
'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requiredMessageText',
{
defaultMessage: 'Message is required.',
}
)
);
}
return validationResult;
},
actionConnectorFields: lazy(() => import('./teams_connectors')),
actionParamsFields: lazy(() => import('./teams_params')),
};
}

View file

@ -0,0 +1,97 @@
/*
* 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 React from 'react';
import { mountWithIntl, nextTick } from '@kbn/test/jest';
import { act } from '@testing-library/react';
import { TeamsActionConnector } from '../types';
import TeamsActionFields from './teams_connectors';
import { DocLinksStart } from 'kibana/public';
describe('TeamsActionFields renders', () => {
test('all connector fields are rendered', async () => {
const actionConnector = {
secrets: {
webhookUrl: 'https:\\test',
},
id: 'test',
actionTypeId: '.teams',
name: 'teams',
config: {},
} as TeamsActionConnector;
const deps = {
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart,
};
const wrapper = mountWithIntl(
<TeamsActionFields
action={actionConnector}
errors={{ index: [], webhookUrl: [] }}
editActionConfig={() => {}}
editActionSecrets={() => {}}
docLinks={deps!.docLinks}
readOnly={false}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find('[data-test-subj="teamsWebhookUrlInput"]').length > 0).toBeTruthy();
expect(wrapper.find('[data-test-subj="teamsWebhookUrlInput"]').first().prop('value')).toBe(
'https:\\test'
);
});
test('should display a message on create to remember credentials', () => {
const actionConnector = {
actionTypeId: '.teams',
config: {},
secrets: {},
} as TeamsActionConnector;
const deps = {
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart,
};
const wrapper = mountWithIntl(
<TeamsActionFields
action={actionConnector}
errors={{ index: [], webhookUrl: [] }}
editActionConfig={() => {}}
editActionSecrets={() => {}}
docLinks={deps!.docLinks}
readOnly={false}
/>
);
expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0);
expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0);
});
test('should display a message on edit to re-enter credentials', () => {
const actionConnector = {
secrets: {
webhookUrl: 'http:\\test',
},
id: 'test',
actionTypeId: '.teams',
name: 'teams',
config: {},
} as TeamsActionConnector;
const deps = {
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart,
};
const wrapper = mountWithIntl(
<TeamsActionFields
action={actionConnector}
errors={{ index: [], webhookUrl: [] }}
editActionConfig={() => {}}
editActionSecrets={() => {}}
docLinks={deps!.docLinks}
readOnly={false}
/>
);
expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0);
expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0);
});
});

View file

@ -0,0 +1,100 @@
/*
* 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 React, { Fragment } from 'react';
import { EuiCallOut, EuiFieldText, EuiFormRow, EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { ActionConnectorFieldsProps } from '../../../../types';
import { TeamsActionConnector } from '../types';
const TeamsActionFields: React.FunctionComponent<ActionConnectorFieldsProps<
TeamsActionConnector
>> = ({ action, editActionSecrets, errors, readOnly, docLinks }) => {
const { webhookUrl } = action.secrets;
return (
<Fragment>
<EuiFormRow
id="webhookUrl"
fullWidth
helpText={
<EuiLink
href={`${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/teams-action-type.html#configuring-teams`}
target="_blank"
>
<FormattedMessage
id="xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.webhookUrlHelpLabel"
defaultMessage="Create a Microsoft Teams Webhook URL"
/>
</EuiLink>
}
error={errors.webhookUrl}
isInvalid={errors.webhookUrl.length > 0 && webhookUrl !== undefined}
label={i18n.translate(
'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.webhookUrlTextFieldLabel',
{
defaultMessage: 'Webhook URL',
}
)}
>
<Fragment>
{getEncryptedFieldNotifyLabel(!action.id)}
<EuiFieldText
fullWidth
isInvalid={errors.webhookUrl.length > 0 && webhookUrl !== undefined}
name="webhookUrl"
readOnly={readOnly}
value={webhookUrl || ''}
data-test-subj="teamsWebhookUrlInput"
onChange={(e) => {
editActionSecrets('webhookUrl', e.target.value);
}}
onBlur={() => {
if (!webhookUrl) {
editActionSecrets('webhookUrl', '');
}
}}
/>
</Fragment>
</EuiFormRow>
</Fragment>
);
};
function getEncryptedFieldNotifyLabel(isCreate: boolean) {
if (isCreate) {
return (
<Fragment>
<EuiSpacer size="s" />
<EuiText size="s" data-test-subj="rememberValuesMessage">
<FormattedMessage
id="xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.rememberValueLabel"
defaultMessage="Remember this value. You must reenter it each time you edit the connector."
/>
</EuiText>
<EuiSpacer size="s" />
</Fragment>
);
}
return (
<Fragment>
<EuiSpacer size="s" />
<EuiCallOut
size="s"
iconType="iInCircle"
data-test-subj="reenterValuesMessage"
title={i18n.translate(
'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.reenterValueLabel',
{ defaultMessage: 'This URL is encrypted. Please reenter a value for this field.' }
)}
/>
<EuiSpacer size="m" />
</Fragment>
);
}
// eslint-disable-next-line import/no-default-export
export { TeamsActionFields as default };

View file

@ -0,0 +1,35 @@
/*
* 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 React from 'react';
import { mountWithIntl } from '@kbn/test/jest';
import TeamsParamsFields from './teams_params';
import { DocLinksStart } from 'kibana/public';
import { coreMock } from 'src/core/public/mocks';
describe('TeamsParamsFields renders', () => {
test('all params fields is rendered', () => {
const mocks = coreMock.createSetup();
const actionParams = {
message: 'test message',
};
const wrapper = mountWithIntl(
<TeamsParamsFields
actionParams={actionParams}
errors={{ message: [] }}
editAction={() => {}}
index={0}
docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart}
toastNotifications={mocks.notifications.toasts}
http={mocks.http}
/>
);
expect(wrapper.find('[data-test-subj="messageTextArea"]').length > 0).toBeTruthy();
expect(wrapper.find('[data-test-subj="messageTextArea"]').first().prop('value')).toStrictEqual(
'test message'
);
});
});

View file

@ -0,0 +1,47 @@
/*
* 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 React, { useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import { ActionParamsProps } from '../../../../types';
import { TeamsActionParams } from '../types';
import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables';
const TeamsParamsFields: React.FunctionComponent<ActionParamsProps<TeamsActionParams>> = ({
actionParams,
editAction,
index,
errors,
messageVariables,
defaultMessage,
}) => {
const { message } = actionParams;
useEffect(() => {
if (!message && defaultMessage && defaultMessage.length > 0) {
editAction('message', defaultMessage, index);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<TextAreaWithMessageVariables
index={index}
editAction={editAction}
messageVariables={messageVariables}
paramsProperty={'message'}
inputTargetValue={message}
label={i18n.translate(
'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.messageTextAreaFieldLabel',
{
defaultMessage: 'Message',
}
)}
errors={errors.message as string[]}
/>
);
};
// eslint-disable-next-line import/no-default-export
export { TeamsParamsFields as default };

View file

@ -60,6 +60,10 @@ export interface SlackActionParams {
message: string;
}
export interface TeamsActionParams {
message: string;
}
export interface WebhookActionParams {
body?: string;
}
@ -119,3 +123,9 @@ export interface WebhookSecrets {
}
export type WebhookActionConnector = UserConfiguredActionConnector<WebhookConfig, WebhookSecrets>;
export interface TeamsSecrets {
webhookUrl: string;
}
export type TeamsActionConnector = UserConfiguredActionConnector<unknown, TeamsSecrets>;