From 283a8b8e3732d13e5b9409e65f8ab4ea15682793 Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Wed, 23 Oct 2024 14:26:50 +0100 Subject: [PATCH 1/6] initial commit --- .../stack_connectors/common/auth/constants.ts | 1 + .../cases_webhook/steps/get.tsx | 258 +++++++++++------ .../cases_webhook/translations.ts | 41 ++- .../cases_webhook/validator.ts | 65 ++++- .../cases_webhook/webhook_connectors.test.tsx | 85 ++++++ .../cases_webhook/webhook_connectors.tsx | 4 +- .../connector_types/cases_webhook/schema.ts | 7 + .../cases_webhook/service.test.ts | 269 ++++++++++++++++++ .../connector_types/cases_webhook/service.ts | 41 ++- 9 files changed, 671 insertions(+), 100 deletions(-) diff --git a/x-pack/plugins/stack_connectors/common/auth/constants.ts b/x-pack/plugins/stack_connectors/common/auth/constants.ts index bdd5b7352f921..ecf7637c956ee 100644 --- a/x-pack/plugins/stack_connectors/common/auth/constants.ts +++ b/x-pack/plugins/stack_connectors/common/auth/constants.ts @@ -19,4 +19,5 @@ export enum WebhookMethods { PATCH = 'patch', POST = 'post', PUT = 'put', + GET = 'get', } diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/steps/get.tsx b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/steps/get.tsx index e8f233408a4c9..c30feffa6bb68 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/steps/get.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/steps/get.tsx @@ -6,14 +6,27 @@ */ import React, { FunctionComponent } from 'react'; +import { css } from '@emotion/react'; import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { + FIELD_TYPES, + UseField, + useFormData, +} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { Field } from '@kbn/es-ui-shared-plugin/static/forms/components'; import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; -import { MustacheTextFieldWrapper } from '@kbn/triggers-actions-ui-plugin/public'; -import { containsExternalId, containsExternalIdOrTitle } from '../validator'; +import { JsonFieldWrapper, MustacheTextFieldWrapper } from '@kbn/triggers-actions-ui-plugin/public'; +import { WebhookMethods } from '../../../../common/auth/constants'; +import { + containsExternalIdForGet, + containsExternalIdForPost, + containsExternalIdOrTitle, + doesNotContainExternalIdForPost, + requiredJsonForPost, +} from '../validator'; import { urlVars, urlVarsExt } from '../action_variables'; import * as i18n from '../translations'; +import { HTTP_VERBS } from '../webhook_connectors'; const { emptyField, urlField } = fieldValidators; interface Props { @@ -21,88 +34,161 @@ interface Props { readOnly: boolean; } -export const GetStep: FunctionComponent = ({ display, readOnly }) => ( - - -

{i18n.STEP_3}

- -

{i18n.STEP_3_DESCRIPTION}

-
-
- - - - - - = ({ display, readOnly }) => { + const [{ config }] = useFormData({ + watch: ['config.getIncidentMethod'], + }); + const { getIncidentMethod = WebhookMethods.GET } = config ?? {}; + + return ( + + +

{i18n.STEP_3}

+ +

{i18n.STEP_3_DESCRIPTION}

+
+
+ + + + ({ + text: verb.toUpperCase(), + value: verb, + })), + readOnly, + }, + }} + /> + + + + + + {getIncidentMethod === WebhookMethods.POST ? ( + + + + ) : null} + + - - - + + + - - -
-); + }} + /> +
+
+
+ ); +}; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/translations.ts b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/translations.ts index 0b007e07cfd91..1d5baf7cef1de 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/translations.ts +++ b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/translations.ts @@ -178,13 +178,24 @@ export const ADD_CASES_VARIABLE = i18n.translate( defaultMessage: 'Add variable', } ); - +export const GET_INCIDENT_METHOD = i18n.translate( + 'xpack.stackConnectors.components.casesWebhook.getIncidentMethodTextFieldLabel', + { + defaultMessage: 'Get case method', + } +); export const GET_INCIDENT_URL = i18n.translate( 'xpack.stackConnectors.components.casesWebhook.getIncidentUrlTextFieldLabel', { defaultMessage: 'Get case URL', } ); +export const GET_METHOD_REQUIRED = i18n.translate( + 'xpack.stackConnectors.components.casesWebhook.error.requiredGetMethodText', + { + defaultMessage: 'Get case method is required.', + } +); export const GET_INCIDENT_URL_HELP = i18n.translate( 'xpack.stackConnectors.components.casesWebhook.getIncidentUrlHelp', { @@ -206,6 +217,34 @@ export const GET_INCIDENT_TITLE_KEY_HELP = i18n.translate( } ); +export const GET_INCIDENT_URL_POST_VALIDATION = (variable: string) => + i18n.translate('xpack.stackConnectors.components.casesWebhook.getIncidentUrlPostValidation', { + defaultMessage: '{variable} should not be in the URL for POST method.', + values: { variable }, + }); + +export const GET_INCIDENT_JSON_HELP = i18n.translate( + 'xpack.stackConnectors.components.casesWebhook.getIncidentJsonHelp', + { + defaultMessage: + 'JSON object to get a case. Use the variable selector to add cases data to the payload.', + } +); + +export const GET_INCIDENT_JSON = i18n.translate( + 'xpack.stackConnectors.components.casesWebhook.getIncidentJsonTextFieldLabel', + { + defaultMessage: 'Get case object', + } +); + +export const GET_INCIDENT_REQUIRED = i18n.translate( + 'xpack.stackConnectors.components.casesWebhook.error.requiredGetIncidentText', + { + defaultMessage: 'Get case object is required and must be valid JSON.', + } +); + export const EXTERNAL_INCIDENT_VIEW_URL = i18n.translate( 'xpack.stackConnectors.components.casesWebhook.viewIncidentUrlTextFieldLabel', { diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/validator.ts b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/validator.ts index d3d7f6dc8e612..9ce3265633336 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/validator.ts +++ b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/validator.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { isEmpty } from 'lodash'; import { ERROR_CODE } from '@kbn/es-ui-shared-plugin/static/forms/helpers/field_validators/types'; import { ValidationError, @@ -12,6 +13,7 @@ import { } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { containsChars, isUrl } from '@kbn/es-ui-shared-plugin/static/validators/string'; import { templateActionVariable } from '@kbn/triggers-actions-ui-plugin/public'; +import { WebhookMethods } from '../../../common/auth/constants'; import * as i18n from './translations'; import { casesVars, commentVars, urlVars, urlVarsExt } from './action_variables'; @@ -42,17 +44,42 @@ export const containsTitleAndDesc = } }; -export const containsExternalId = - () => +export const containsExternalIdForGet = + (method?: string) => (...args: Parameters): ReturnType> => { const [{ value, path }] = args; const id = templateActionVariable( urlVars.find((actionVariable) => actionVariable.name === 'external.system.id')! ); - return containsChars(id)(value as string).doesContain - ? undefined - : missingVariableErrorMessage(path, [id]); + + return method === WebhookMethods.GET && + value !== null && + !containsChars(id)(value as string).doesContain + ? missingVariableErrorMessage(path, [id]) + : undefined; + }; + +export const doesNotContainExternalIdForPost = + (method?: string) => + (...args: Parameters): ReturnType> => { + const [{ value, path }] = args; + + const id = templateActionVariable( + urlVars.find((actionVariable) => actionVariable.name === 'external.system.id')! + ); + + const error = { + code: errorCode, + path, + message: i18n.GET_INCIDENT_URL_POST_VALIDATION(id), + }; + + return method === WebhookMethods.POST && + value !== null && + containsChars(id)(value as string).doesContain + ? error + : undefined; }; export const containsExternalIdOrTitle = @@ -77,6 +104,34 @@ export const containsExternalIdOrTitle = return error; }; +export const requiredJsonForPost = + (method?: string) => + (...args: Parameters): ReturnType> => { + const [{ value, path }] = args; + + const error = { + code: errorCode, + path, + message: i18n.GET_INCIDENT_REQUIRED, + }; + + return method === WebhookMethods.POST && (value === null || isEmpty(value)) ? error : undefined; + }; + +export const containsExternalIdForPost = + (method?: string) => + (...args: Parameters): ReturnType> => { + const [{ value, path }] = args; + + const id = templateActionVariable( + urlVars.find((actionVariable) => actionVariable.name === 'external.system.id')! + ); + + return method === WebhookMethods.POST && !containsChars(id)(value as string).doesContain + ? missingVariableErrorMessage(path, [id]) + : undefined; + }; + export const containsCommentsOrEmpty = (message: string) => (...args: Parameters): ReturnType> => { diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_connectors.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_connectors.test.tsx index 8df473fef2ae8..d60bfdc4aea68 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_connectors.test.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_connectors.test.tsx @@ -42,6 +42,7 @@ const config = { headers: [{ key: 'content-type', value: 'text' }], viewIncidentUrl: 'https://coolsite.net/browse/{{{external.system.title}}}', getIncidentUrl: 'https://coolsite.net/rest/api/2/issue/{{{external.system.id}}}', + getIncidentMethod: 'get', updateIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', updateIncidentMethod: 'put', @@ -536,5 +537,89 @@ describe('CasesWebhookActionConnectorFields renders', () => { ).toBeInTheDocument(); } ); + + it('validates get incident json required correctly', async () => { + const connector = { + ...actionConnector, + config: { + ...actionConnector.config, + getIncidentUrl: 'https://coolsite.net/rest/api/2/issue', + getIncidentMethod: 'post', + headers: [], + }, + }; + + render( + + {}} + /> + + ); + + await userEvent.click(await screen.findByTestId('form-test-provide-submit')); + await waitFor(() => expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false })); + expect(await screen.findByText(i18n.GET_INCIDENT_REQUIRED)).toBeInTheDocument(); + }); + + it('validates get incident json variable correctly', async () => { + const connector = { + ...actionConnector, + config: { + ...actionConnector.config, + getIncidentUrl: 'https://coolsite.net/rest/api/2/issue', + getIncidentMethod: 'post', + getIncidentJson: '{"id": "wrong_external_id" }', + headers: [], + }, + }; + + render( + + {}} + /> + + ); + + await userEvent.click(await screen.findByTestId('form-test-provide-submit')); + await waitFor(() => expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false })); + expect( + await screen.findByText(i18n.MISSING_VARIABLES(['{{{external.system.id}}}'])) + ).toBeInTheDocument(); + }); + + it('validates get incident url with post correctly', async () => { + const connector = { + ...actionConnector, + config: { + ...actionConnector.config, + getIncidentUrl: 'https://coolsite.net/rest/api/2/issue/{{{external.system.id}}}', + getIncidentMethod: 'post', + getIncidentJson: '{"id": {{{external.system.id}}} }', + headers: [], + }, + }; + + render( + + {}} + /> + + ); + + await userEvent.click(await screen.findByTestId('form-test-provide-submit')); + await waitFor(() => expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false })); + expect( + await screen.findByText(i18n.GET_INCIDENT_URL_POST_VALIDATION('{{{external.system.id}}}')) + ).toBeInTheDocument(); + }); }); }); diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_connectors.tsx b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_connectors.tsx index 73e424901469a..5aaf56fa8dd90 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_connectors.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_connectors.tsx @@ -22,7 +22,7 @@ import { useKibana } from '@kbn/triggers-actions-ui-plugin/public'; import * as i18n from './translations'; import { AuthStep, CreateStep, GetStep, UpdateStep } from './steps'; -export const HTTP_VERBS = ['post', 'put', 'patch']; +export const HTTP_VERBS = ['post', 'put', 'patch', 'get']; const fields = { step1: [ 'config.hasAuth', @@ -38,7 +38,9 @@ const fields = { 'config.createIncidentResponseKey', ], step3: [ + 'config.getIncidentMethod', 'config.getIncidentUrl', + 'config.getIncidentJson', 'config.getIncidentResponseExternalTitleKey', 'config.viewIncidentUrl', ], diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/schema.ts b/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/schema.ts index 00b4fdc60a3ab..25b0d66e885b4 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/schema.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/schema.ts @@ -21,7 +21,14 @@ export const ExternalIncidentServiceConfiguration = { ), createIncidentJson: schema.string(), // stringified object createIncidentResponseKey: schema.string(), + getIncidentMethod: schema.oneOf( + [schema.literal(WebhookMethods.GET), schema.literal(WebhookMethods.POST)], + { + defaultValue: WebhookMethods.GET, + } + ), getIncidentUrl: schema.string(), + getIncidentJson: schema.nullable(schema.string()), getIncidentResponseExternalTitleKey: schema.string(), viewIncidentUrl: schema.string(), updateIncidentUrl: schema.string(), diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/service.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/service.test.ts index a44b34bf88fce..aaeca30be920a 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/service.test.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/service.test.ts @@ -47,6 +47,8 @@ const config: CasesWebhookPublicConfigurationType = { headers: { ['content-type']: 'application/json', foo: 'bar' }, viewIncidentUrl: 'https://coolsite.net/browse/{{{external.system.title}}}', getIncidentUrl: 'https://coolsite.net/issue/{{{external.system.id}}}', + getIncidentMethod: WebhookMethods.GET, + getIncidentJson: null, updateIncidentJson: '{"fields":{"title":{{{case.title}}},"description":{{{case.description}}},"tags":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', updateIncidentMethod: WebhookMethods.PUT, @@ -239,6 +241,7 @@ describe('Cases webhook service', () => { configurationUtilities, sslOverrides: defaultSSLOverrides, connectorUsageCollector: expect.any(ConnectorUsageCollector), + method: WebhookMethods.GET, }); }); @@ -282,6 +285,7 @@ describe('Cases webhook service', () => { "trace": [MockFunction], "warn": [MockFunction], }, + "method": "get", "sslOverrides": Object { "cert": Object { "data": Array [ @@ -440,6 +444,271 @@ describe('Cases webhook service', () => { '[Action][Webhook - Case Management]: Unable to get case with id 1. Error: Response is missing the expected field: key' ); }); + + it('it returns the incident correctly with POST', async () => { + const postService: ExternalService = createExternalService( + actionId, + { + config: { + ...config, + getIncidentMethod: WebhookMethods.POST, + getIncidentJson: '{"id": {{{external.system.id}}} }', + getIncidentUrl: 'https://coolsite.net/issue', + }, + secrets, + }, + logger, + configurationUtilities, + connectorUsageCollector + ); + + requestMock.mockImplementation(() => createAxiosResponse(axiosRes)); + const res = await postService.getIncident('1'); + expect(res).toEqual({ + id: '1', + title: 'CK-1', + }); + }); + + it('it should call request with correct arguments using POST', async () => { + const postService: ExternalService = createExternalService( + actionId, + { + config: { + ...config, + getIncidentMethod: WebhookMethods.POST, + getIncidentJson: '{"id": {{{external.system.id}}} }', + getIncidentUrl: 'https://coolsite.net/issue', + }, + secrets, + }, + logger, + configurationUtilities, + connectorUsageCollector + ); + + requestMock.mockImplementation(() => createAxiosResponse(axiosRes)); + + await postService.getIncident('1'); + expect(requestMock).toHaveBeenCalledWith({ + axios, + url: 'https://coolsite.net/issue', + logger, + configurationUtilities, + sslOverrides: defaultSSLOverrides, + connectorUsageCollector: expect.any(ConnectorUsageCollector), + method: WebhookMethods.POST, + data: '{"id": "1" }', + }); + }); + + it('it should call request with correct arguments when authType=SSL using POST', async () => { + const postSslService = createExternalService( + actionId, + { + config: { + ...sslConfig, + getIncidentMethod: WebhookMethods.POST, + getIncidentJson: '{"id": {{{external.system.id}}} }', + getIncidentUrl: 'https://coolsite.net/issue', + }, + secrets: sslSecrets, + }, + logger, + configurationUtilities, + connectorUsageCollector + ); + + requestMock.mockImplementation(() => createAxiosResponse(axiosRes)); + + await postSslService.getIncident('1'); + + // irrelevant snapshot content + delete requestMock.mock.calls[0][0].configurationUtilities; + expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(` + Object { + "axios": [Function], + "connectorUsageCollector": ConnectorUsageCollector { + "connectorId": "test-connector-id", + "logger": Object { + "context": Array [], + "debug": [MockFunction], + "error": [MockFunction], + "fatal": [MockFunction], + "get": [MockFunction], + "info": [MockFunction], + "isLevelEnabled": [MockFunction], + "log": [MockFunction], + "trace": [MockFunction], + "warn": [MockFunction], + }, + "usage": Object { + "requestBodyBytes": 0, + }, + }, + "data": "{\\"id\\": \\"1\\" }", + "logger": Object { + "context": Array [], + "debug": [MockFunction], + "error": [MockFunction], + "fatal": [MockFunction], + "get": [MockFunction], + "info": [MockFunction], + "isLevelEnabled": [MockFunction], + "log": [MockFunction], + "trace": [MockFunction], + "warn": [MockFunction], + }, + "method": "post", + "sslOverrides": Object { + "cert": Object { + "data": Array [ + 10, + 45, + 45, + 45, + 45, + 45, + 66, + 69, + 71, + 73, + 78, + 32, + 67, + 69, + 82, + 84, + 73, + 70, + 73, + 67, + 65, + 84, + 69, + 45, + 45, + 45, + 45, + 45, + 10, + 45, + 45, + 45, + 45, + 45, + 69, + 78, + 68, + 32, + 67, + 69, + 82, + 84, + 73, + 70, + 73, + 67, + 65, + 84, + 69, + 45, + 45, + 45, + 45, + 45, + 10, + ], + "type": "Buffer", + }, + "key": Object { + "data": Array [ + 10, + 45, + 45, + 45, + 45, + 45, + 66, + 69, + 71, + 73, + 78, + 32, + 80, + 82, + 73, + 86, + 65, + 84, + 69, + 32, + 75, + 69, + 89, + 45, + 45, + 45, + 45, + 45, + 10, + 45, + 45, + 45, + 45, + 45, + 69, + 78, + 68, + 32, + 80, + 82, + 73, + 86, + 65, + 84, + 69, + 32, + 75, + 69, + 89, + 45, + 45, + 45, + 45, + 45, + 10, + ], + "type": "Buffer", + }, + "passphrase": "foobar", + }, + "url": "https://coolsite.net/issue", + } + `); + }); + + it('it should throw if the request payload is not a valid JSON for POST', async () => { + const newService = createExternalService( + actionId, + { + config: { + ...config, + getIncidentMethod: WebhookMethods.POST, + getIncidentJson: '{"id": }', + getIncidentUrl: 'https://coolsite.net/issue', + }, + secrets, + }, + logger, + configurationUtilities, + connectorUsageCollector + ); + + await expect(newService.getIncident('1')).rejects.toThrow( + '[Action][Webhook - Case Management]: Unable to get case with id 1. Error: JSON Error: Get case JSON body must be valid JSON. ' + ); + }); }); describe('createIncident', () => { diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/service.ts b/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/service.ts index 170c63a1d4e5b..9db3d9eddd111 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/service.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/service.ts @@ -14,6 +14,7 @@ import { ActionsConfigurationUtilities } from '@kbn/actions-plugin/server/action import { combineHeadersWithBasicAuthHeader } from '@kbn/actions-plugin/server/lib'; import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types'; import { buildConnectorAuth, validateConnectorAuthConfiguration } from '../../../common/auth/utils'; +import { WebhookMethods } from '../../../common/auth/constants'; import { validateAndNormalizeUrl, validateJson } from './validators'; import { createServiceError, @@ -52,6 +53,8 @@ export const createExternalService = ( createIncidentUrl: createIncidentUrlConfig, getIncidentResponseExternalTitleKey, getIncidentUrl, + getIncidentMethod, + getIncidentJson, hasAuth, authType, headers, @@ -100,23 +103,44 @@ export const createExternalService = ( const getIncident = async (id: string): Promise => { try { - const getUrl = renderMustacheStringNoEscape(getIncidentUrl, { - external: { - system: { - id: encodeURIComponent(id), - }, - }, - }); + const getUrl = + getIncidentMethod === WebhookMethods.GET + ? renderMustacheStringNoEscape(getIncidentUrl, { + external: { + system: { + id: encodeURIComponent(id), + }, + }, + }) + : getIncidentUrl; const normalizedUrl = validateAndNormalizeUrl( `${getUrl}`, configurationUtilities, 'Get case URL' ); + + const json = + getIncidentMethod === WebhookMethods.POST && getIncidentJson + ? renderMustacheStringNoEscape(getIncidentJson, { + external: { + system: { + id: JSON.stringify(id), + }, + }, + }) + : null; + + if (json !== null) { + validateJson(json, 'Get case JSON body'); + } + const res = await request({ axios: axiosInstance, url: normalizedUrl, + method: getIncidentMethod, logger, + ...(getIncidentMethod === WebhookMethods.POST ? { data: json } : {}), configurationUtilities, sslOverrides, connectorUsageCollector, @@ -128,6 +152,7 @@ export const createExternalService = ( }); const title = getObjectValueByKeyAsString(res.data, getIncidentResponseExternalTitleKey)!; + return { id, title }; } catch (error) { throw createServiceError(error, `Unable to get case with id ${id}`); @@ -157,6 +182,7 @@ export const createExternalService = ( ); validateJson(json, 'Create case JSON body'); + const res: AxiosResponse = await request({ axios: axiosInstance, url: normalizedUrl, @@ -175,6 +201,7 @@ export const createExternalService = ( requiredAttributesToBeInTheResponse: [createIncidentResponseKey], }); const externalId = getObjectValueByKeyAsString(data, createIncidentResponseKey)!; + const insertedIncident = await getIncident(externalId); logger.debug(`response from webhook action "${actionId}": [HTTP ${status}] ${statusText}`); From 629a86746406d76cb53632f17151795d37831ca8 Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Thu, 24 Oct 2024 11:38:03 +0100 Subject: [PATCH 2/6] Fix tests --- .../connector_types.test.ts.snap | 72 ++++++++++++++ .../actions/connector_types/cases_webhook.ts | 98 ++++++++++++++++++- .../tests/trial/configure/get_connectors.ts | 2 + .../tests/trial/configure/get_connectors.ts | 2 + 4 files changed, 173 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap b/x-pack/plugins/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap index 94bc911557c21..e8b3cc7bebe2a 100644 --- a/x-pack/plugins/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap +++ b/x-pack/plugins/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap @@ -2251,6 +2251,78 @@ Object { ], "type": "string", }, + "getIncidentJson": Object { + "flags": Object { + "default": null, + "error": [Function], + "presence": "optional", + }, + "matches": Array [ + Object { + "schema": Object { + "flags": Object { + "error": [Function], + }, + "rules": Array [ + Object { + "args": Object { + "method": [Function], + }, + "name": "custom", + }, + ], + "type": "string", + }, + }, + Object { + "schema": Object { + "allow": Array [ + null, + ], + "flags": Object { + "error": [Function], + "only": true, + }, + "type": "any", + }, + }, + ], + "type": "alternatives", + }, + "getIncidentMethod": Object { + "flags": Object { + "default": "get", + "error": [Function], + "presence": "optional", + }, + "matches": Array [ + Object { + "schema": Object { + "allow": Array [ + "get", + ], + "flags": Object { + "error": [Function], + "only": true, + }, + "type": "any", + }, + }, + Object { + "schema": Object { + "allow": Array [ + "post", + ], + "flags": Object { + "error": [Function], + "only": true, + }, + "type": "any", + }, + }, + ], + "type": "alternatives", + }, "getIncidentResponseExternalTitleKey": Object { "flags": Object { "error": [Function], diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/cases_webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/cases_webhook.ts index fcf0f2d84e755..405153ec99c8f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/cases_webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/cases_webhook.ts @@ -38,6 +38,8 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { headers: { ['content-type']: 'application/json', ['kbn-xsrf']: 'abcd' }, viewIncidentUrl: 'https://coolsite.net/browse/{{{external.system.title}}}', getIncidentUrl: 'https://coolsite.net/rest/api/2/issue/{{{external.system.id}}}', + getIncidentMethod: 'get', + getIncidentJson: null, updateIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"labels":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', updateIncidentMethod: 'put', @@ -79,7 +81,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { }; let casesWebhookSimulatorURL: string = ''; - let simulatorConfig: Record>; + let simulatorConfig: Record>; describe('CasesWebhook', () => { before(() => { // use jira because cases webhook works with any third party case management system @@ -135,6 +137,53 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { config: simulatorConfig, }); }); + + it('should return 200 when creating a casesWebhook action with get case info using POST successfully', async () => { + const newConfig = { + ...simulatorConfig, + getIncidentMethod: 'post', + getIncidentJson: '{"id": {{{external.system.id}}} }', + getIncidentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue`, + }; + + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A casesWebhook action', + connector_type_id: '.cases-webhook', + config: newConfig, + secrets, + }) + .expect(200); + + expect(createdAction).to.eql({ + id: createdAction.id, + is_preconfigured: false, + is_system_action: false, + is_deprecated: false, + name: 'A casesWebhook action', + connector_type_id: '.cases-webhook', + is_missing_secrets: false, + config: newConfig, + }); + + const { body: fetchedAction } = await supertest + .get(`/api/actions/connector/${createdAction.id}`) + .expect(200); + + expect(fetchedAction).to.eql({ + id: fetchedAction.id, + is_preconfigured: false, + is_system_action: false, + is_deprecated: false, + name: 'A casesWebhook action', + connector_type_id: '.cases-webhook', + is_missing_secrets: false, + config: newConfig, + }); + }); + describe('400s for all required fields when missing', () => { requiredFields.forEach((field) => { it(`should respond with a 400 Bad Request when creating a casesWebhook action with no ${field}`, async () => { @@ -529,6 +578,53 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { }); expect(proxyHaveBeenCalled).to.equal(false); }); + + it('should respond with bad JSON error when get case JSON is bad', async () => { + const { body } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A casesWebhook simulator', + connector_type_id: '.cases-webhook', + config: { + ...simulatorConfig, + getIncidentJson: '{"id": "{{{external.system.id}}}" }', + getIncidentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue`, + getIncidentMethod: 'post', + }, + secrets, + }); + + simulatedActionId = body.id; + + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockCasesWebhook.params, + subActionParams: { + incident: { + title: 'success', + description: 'success', + }, + comments: [], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: true, + message: 'an error occurred while running the action', + errorSource: TaskErrorSource.FRAMEWORK, + service_message: + '[Action][Webhook - Case Management]: Unable to create case. Error: [Action][Webhook - Case Management]: Unable to get case with id 123. Error: JSON Error: Get case JSON body must be valid JSON. . ', + }); + }); + }); + after(() => { if (proxyServer) { proxyServer.close(); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts index d124047831e28..a6e98788e62a3 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts @@ -78,6 +78,8 @@ export default ({ getService }: FtrProviderContext): void => { headers: { [`content-type`]: 'application/json' }, viewIncidentUrl: 'http://some.non.existent.com/browse/{{{external.system.title}}}', getIncidentUrl: 'http://some.non.existent.com/{{{external.system.id}}}', + getIncidentMethod: 'get', + getIncidentJson: null, updateIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', updateIncidentMethod: 'put', diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/get_connectors.ts b/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/get_connectors.ts index 5ddc3df660142..5029c57d8aec1 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/get_connectors.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/get_connectors.ts @@ -110,6 +110,8 @@ export default ({ getService }: FtrProviderContext): void => { headers: { [`content-type`]: 'application/json' }, viewIncidentUrl: 'http://some.non.existent.com/browse/{{{external.system.title}}}', getIncidentUrl: 'http://some.non.existent.com/{{{external.system.id}}}', + getIncidentMethod: 'get', + getIncidentJson: null, updateIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', updateIncidentMethod: 'put', From d9d7949347f9b6d28719c79428ba9c16d2dc8b88 Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Thu, 24 Oct 2024 16:02:58 +0100 Subject: [PATCH 3/6] make system id optional in getIncidentUrl for POST --- .../cases_webhook/steps/get.tsx | 2 -- .../cases_webhook/translations.ts | 6 ---- .../cases_webhook/validator.ts | 22 --------------- .../cases_webhook/webhook_connectors.test.tsx | 28 +++++++++++++++---- .../connector_types/cases_webhook/service.ts | 17 +++++------ 5 files changed, 30 insertions(+), 45 deletions(-) diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/steps/get.tsx b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/steps/get.tsx index c30feffa6bb68..001769eff608c 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/steps/get.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/steps/get.tsx @@ -21,7 +21,6 @@ import { containsExternalIdForGet, containsExternalIdForPost, containsExternalIdOrTitle, - doesNotContainExternalIdForPost, requiredJsonForPost, } from '../validator'; import { urlVars, urlVarsExt } from '../action_variables'; @@ -91,7 +90,6 @@ export const GetStep: FunctionComponent = ({ display, readOnly }) => { validator: urlField(i18n.GET_INCIDENT_URL_REQUIRED), }, { validator: containsExternalIdForGet(getIncidentMethod) }, - { validator: doesNotContainExternalIdForPost(getIncidentMethod) }, ], helpText: i18n.GET_INCIDENT_URL_HELP, }} diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/translations.ts b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/translations.ts index 1d5baf7cef1de..8c44b6197ef9c 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/translations.ts +++ b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/translations.ts @@ -217,12 +217,6 @@ export const GET_INCIDENT_TITLE_KEY_HELP = i18n.translate( } ); -export const GET_INCIDENT_URL_POST_VALIDATION = (variable: string) => - i18n.translate('xpack.stackConnectors.components.casesWebhook.getIncidentUrlPostValidation', { - defaultMessage: '{variable} should not be in the URL for POST method.', - values: { variable }, - }); - export const GET_INCIDENT_JSON_HELP = i18n.translate( 'xpack.stackConnectors.components.casesWebhook.getIncidentJsonHelp', { diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/validator.ts b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/validator.ts index 9ce3265633336..a41ed25a2e8db 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/validator.ts +++ b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/validator.ts @@ -60,28 +60,6 @@ export const containsExternalIdForGet = : undefined; }; -export const doesNotContainExternalIdForPost = - (method?: string) => - (...args: Parameters): ReturnType> => { - const [{ value, path }] = args; - - const id = templateActionVariable( - urlVars.find((actionVariable) => actionVariable.name === 'external.system.id')! - ); - - const error = { - code: errorCode, - path, - message: i18n.GET_INCIDENT_URL_POST_VALIDATION(id), - }; - - return method === WebhookMethods.POST && - value !== null && - containsChars(id)(value as string).doesContain - ? error - : undefined; - }; - export const containsExternalIdOrTitle = () => (...args: Parameters): ReturnType> => { diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_connectors.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_connectors.test.tsx index d60bfdc4aea68..fd50ba8875188 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_connectors.test.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_connectors.test.tsx @@ -593,7 +593,7 @@ describe('CasesWebhookActionConnectorFields renders', () => { ).toBeInTheDocument(); }); - it('validates get incident url with post correctly', async () => { + it('validation succeeds get incident url with post correctly', async () => { const connector = { ...actionConnector, config: { @@ -605,6 +605,9 @@ describe('CasesWebhookActionConnectorFields renders', () => { }, }; + const { isPreconfigured, ...rest } = actionConnector; + const { headers, ...rest2 } = actionConnector.config; + render( { ); await userEvent.click(await screen.findByTestId('form-test-provide-submit')); - await waitFor(() => expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false })); - expect( - await screen.findByText(i18n.GET_INCIDENT_URL_POST_VALIDATION('{{{external.system.id}}}')) - ).toBeInTheDocument(); + + await waitFor(() => + expect(onSubmit).toHaveBeenCalledWith({ + data: { + __internal__: { + hasCA: false, + hasHeaders: true, + }, + ...rest, + config: { + ...rest2, + getIncidentUrl: 'https://coolsite.net/rest/api/2/issue/{{{external.system.id}}}', + getIncidentMethod: 'post', + getIncidentJson: '{"id": {{{external.system.id}}} }', + }, + }, + isValid: true, + }) + ); }); }); }); diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/service.ts b/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/service.ts index 9db3d9eddd111..9f14f494c9424 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/service.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/service.ts @@ -103,16 +103,13 @@ export const createExternalService = ( const getIncident = async (id: string): Promise => { try { - const getUrl = - getIncidentMethod === WebhookMethods.GET - ? renderMustacheStringNoEscape(getIncidentUrl, { - external: { - system: { - id: encodeURIComponent(id), - }, - }, - }) - : getIncidentUrl; + const getUrl = renderMustacheStringNoEscape(getIncidentUrl, { + external: { + system: { + id: encodeURIComponent(id), + }, + }, + }); const normalizedUrl = validateAndNormalizeUrl( `${getUrl}`, From 5eb601964137d0dc47db0640f312914da19a91cf Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Fri, 1 Nov 2024 10:16:56 +0000 Subject: [PATCH 4/6] use only get and post --- .../public/connector_types/cases_webhook/steps/get.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/steps/get.tsx b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/steps/get.tsx index 001769eff608c..39d12bc413b10 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/steps/get.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/steps/get.tsx @@ -25,7 +25,7 @@ import { } from '../validator'; import { urlVars, urlVarsExt } from '../action_variables'; import * as i18n from '../translations'; -import { HTTP_VERBS } from '../webhook_connectors'; + const { emptyField, urlField } = fieldValidators; interface Props { @@ -71,7 +71,7 @@ export const GetStep: FunctionComponent = ({ display, readOnly }) => { componentProps={{ euiFieldProps: { 'data-test-subj': 'webhookGetIncidentMethodSelect', - options: HTTP_VERBS.map((verb) => ({ + options: ['get', 'post'].map((verb) => ({ text: verb.toUpperCase(), value: verb, })), From eb46790f15b288f6c89db216150cc7a8bdc92a7d Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Wed, 6 Nov 2024 16:27:36 +0000 Subject: [PATCH 5/6] feedback 2 --- .../public/connector_types/cases_webhook/steps/get.tsx | 4 ++-- .../group2/tests/actions/connector_types/cases_webhook.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/steps/get.tsx b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/steps/get.tsx index 39d12bc413b10..f12e01fbeeb9a 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/steps/get.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/steps/get.tsx @@ -55,7 +55,7 @@ export const GetStep: FunctionComponent = ({ display, readOnly }) => { component={Field} config={{ label: i18n.GET_INCIDENT_METHOD, - defaultValue: 'get', + defaultValue: WebhookMethods.GET, type: FIELD_TYPES.SELECT, validations: [ { @@ -71,7 +71,7 @@ export const GetStep: FunctionComponent = ({ display, readOnly }) => { componentProps={{ euiFieldProps: { 'data-test-subj': 'webhookGetIncidentMethodSelect', - options: ['get', 'post'].map((verb) => ({ + options: [WebhookMethods.GET, WebhookMethods.POST].map((verb) => ({ text: verb.toUpperCase(), value: verb, })), diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/cases_webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/cases_webhook.ts index 2a82ed509fe60..b425db8569f50 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/cases_webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/cases_webhook.ts @@ -579,7 +579,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { expect(proxyHaveBeenCalled).to.equal(false); }); - it('should respond with bad JSON error when get case JSON is bad', async () => { + it('should respond with bad JSON error when get case POST JSON is bad', async () => { const { body } = await supertest .post('/api/actions/connector') .set('kbn-xsrf', 'foo') From 5ab48360718d8dda6fa858bc2b9ca0e215ae246b Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Thu, 7 Nov 2024 10:26:23 +0000 Subject: [PATCH 6/6] removed externalId validation for json --- .../cases_webhook/steps/get.tsx | 4 --- .../cases_webhook/validator.ts | 14 --------- .../cases_webhook/webhook_connectors.test.tsx | 29 ------------------- 3 files changed, 47 deletions(-) diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/steps/get.tsx b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/steps/get.tsx index f12e01fbeeb9a..5bf2689506ec4 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/steps/get.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/steps/get.tsx @@ -19,7 +19,6 @@ import { JsonFieldWrapper, MustacheTextFieldWrapper } from '@kbn/triggers-action import { WebhookMethods } from '../../../../common/auth/constants'; import { containsExternalIdForGet, - containsExternalIdForPost, containsExternalIdOrTitle, requiredJsonForPost, } from '../validator'; @@ -118,9 +117,6 @@ export const GetStep: FunctionComponent = ({ display, readOnly }) => { { validator: requiredJsonForPost(getIncidentMethod), }, - { - validator: containsExternalIdForPost(getIncidentMethod), - }, ], }} component={JsonFieldWrapper} diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/validator.ts b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/validator.ts index a41ed25a2e8db..d972c9bbd1f86 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/validator.ts +++ b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/validator.ts @@ -96,20 +96,6 @@ export const requiredJsonForPost = return method === WebhookMethods.POST && (value === null || isEmpty(value)) ? error : undefined; }; -export const containsExternalIdForPost = - (method?: string) => - (...args: Parameters): ReturnType> => { - const [{ value, path }] = args; - - const id = templateActionVariable( - urlVars.find((actionVariable) => actionVariable.name === 'external.system.id')! - ); - - return method === WebhookMethods.POST && !containsChars(id)(value as string).doesContain - ? missingVariableErrorMessage(path, [id]) - : undefined; - }; - export const containsCommentsOrEmpty = (message: string) => (...args: Parameters): ReturnType> => { diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_connectors.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_connectors.test.tsx index fd50ba8875188..713f2bd9e6f83 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_connectors.test.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_connectors.test.tsx @@ -564,35 +564,6 @@ describe('CasesWebhookActionConnectorFields renders', () => { expect(await screen.findByText(i18n.GET_INCIDENT_REQUIRED)).toBeInTheDocument(); }); - it('validates get incident json variable correctly', async () => { - const connector = { - ...actionConnector, - config: { - ...actionConnector.config, - getIncidentUrl: 'https://coolsite.net/rest/api/2/issue', - getIncidentMethod: 'post', - getIncidentJson: '{"id": "wrong_external_id" }', - headers: [], - }, - }; - - render( - - {}} - /> - - ); - - await userEvent.click(await screen.findByTestId('form-test-provide-submit')); - await waitFor(() => expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false })); - expect( - await screen.findByText(i18n.MISSING_VARIABLES(['{{{external.system.id}}}'])) - ).toBeInTheDocument(); - }); - it('validation succeeds get incident url with post correctly', async () => { const connector = { ...actionConnector,