From dd0af8d2852d750f463dc2cbeb8dda9d75c95363 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 17 Oct 2024 00:42:34 +1100 Subject: [PATCH 1/6] skip failing test suite (#196546) --- .../trial_license_complete_tier/engine_nondefault_spaces.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine_nondefault_spaces.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine_nondefault_spaces.ts index de949730d3d10..481f7aa4056f6 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine_nondefault_spaces.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine_nondefault_spaces.ts @@ -18,7 +18,8 @@ export default ({ getService }: FtrProviderContextWithSpaces) => { const supertest = getService('supertest'); const utils = EntityStoreUtils(getService, namespace); - describe('@ess Entity Store Engine APIs in non-default space', () => { + // Failing: See https://github.com/elastic/kibana/issues/196546 + describe.skip('@ess Entity Store Engine APIs in non-default space', () => { const dataView = dataViewRouteHelpersFactory(supertest, namespace); before(async () => { From e6569f0b2503968c4b8eadd9227dca96d69fd4e9 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 17 Oct 2024 00:43:08 +1100 Subject: [PATCH 2/6] skip failing test suite (#194305) --- .../functional/test_suites/common/discover/esql/_esql_view.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/esql/_esql_view.ts b/x-pack/test_serverless/functional/test_suites/common/discover/esql/_esql_view.ts index 1bedd0acd0cc4..89edce106f64e 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/esql/_esql_view.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/esql/_esql_view.ts @@ -35,7 +35,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { defaultIndex: 'logstash-*', }; - describe('discover esql view', function () { + // Failing: See https://github.com/elastic/kibana/issues/194305 + describe.skip('discover esql view', function () { // see details: https://github.com/elastic/kibana/issues/188816 this.tags(['failsOnMKI']); From 661458f9e9196b3618db30a68bbd6785606f1b59 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 17 Oct 2024 01:17:59 +1100 Subject: [PATCH 3/6] skip failing test suite (#196319) --- .../trial_license_complete_tier/init_and_status_apis.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/init_and_status_apis.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/init_and_status_apis.ts index 3224caa24d5e2..dd1fe34cd050a 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/init_and_status_apis.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/init_and_status_apis.ts @@ -30,7 +30,8 @@ export default ({ getService }: FtrProviderContext) => { const riskEngineRoutes = riskEngineRouteHelpersFactory(supertest); const log = getService('log'); - describe('@ess @serverless @serverlessQA init_and_status_apis', () => { + // Failing: See https://github.com/elastic/kibana/issues/196319 + describe.skip('@ess @serverless @serverlessQA init_and_status_apis', () => { before(async () => { await riskEngineRoutes.cleanUp(); }); From 50cf1b322e59419b54e19351c217c0b77af3aa0a Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 17 Oct 2024 01:20:48 +1100 Subject: [PATCH 4/6] skip failing test suite (#196153) --- .../apps/triggers_actions_ui/alert_create_flyout.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts index 3c39bd235bf97..121bb753e434b 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts @@ -94,7 +94,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await rules.common.cancelRuleCreation(); } - describe('create alert', function () { + // Failing: See https://github.com/elastic/kibana/issues/196153 + describe.skip('create alert', function () { let apmSynthtraceEsClient: ApmSynthtraceEsClient; before(async () => { await esArchiver.load( From 241a05aa68cbaa737f5ccd92c1e6f91252a3cc9b Mon Sep 17 00:00:00 2001 From: Alexi Doak <109488926+doakalexi@users.noreply.github.com> Date: Wed, 16 Oct 2024 07:25:41 -0700 Subject: [PATCH 5/6] [ResponseOps] Prepare the connector `execute` HTTP API for versioning (#194481) Towards https://github.com/elastic/response-ops-team/issues/125 ## Summary Preparing the `POST ${BASE_ACTION_API_PATH}/connector/{id}/_execute` HTTP API for versioning --------- Co-authored-by: Ying Mao --- .../routes/connector/apis/execute/index.ts | 21 +++++ .../connector/apis/execute/schemas/latest.ts | 8 ++ .../connector/apis/execute/schemas/v1.ts | 20 +++++ .../connector/apis/execute/types/latest.ts | 8 ++ .../routes/connector/apis/execute/types/v1.ts | 12 +++ .../common/routes/connector/response/index.ts | 9 +- .../connector/response/schemas/latest.ts | 1 + .../routes/connector/response/schemas/v1.ts | 52 +++++++++++ .../routes/connector/response/types/v1.ts | 12 +++ .../server/actions_client/actions_client.ts | 87 +++---------------- .../connector/methods/execute/execute.ts | 73 ++++++++++++++++ .../connector/methods/execute/index.ts | 8 ++ .../connector/methods/execute/types/index.ts | 8 ++ .../connector/methods/execute/types/types.ts | 10 +++ .../get_system_action_kibana_privileges.ts | 28 ++++++ .../actions/server/lib/is_preconfigured.ts | 14 +++ .../actions/server/lib/is_system_action.ts | 14 +++ .../{ => connector/execute}/execute.test.ts | 30 +++---- .../routes/{ => connector/execute}/execute.ts | 52 ++++------- .../server/routes/connector/execute/index.ts | 8 ++ .../connector/execute/transforms/index.ts | 10 +++ .../transform_connector_response/latest.ts | 8 ++ .../transform_connector_response/v1.ts | 21 +++++ x-pack/plugins/actions/server/routes/index.ts | 4 +- .../actions/connector_types/cases_webhook.ts | 8 +- .../tests/actions/connector_types/jira.ts | 8 +- .../tests/actions/connector_types/opsgenie.ts | 8 +- .../actions/connector_types/resilient.ts | 8 +- .../connector_types/servicenow_itom.ts | 8 +- .../connector_types/servicenow_itsm.ts | 8 +- .../actions/connector_types/servicenow_sir.ts | 8 +- .../tests/actions/connector_types/swimlane.ts | 8 +- .../tests/actions/connector_types/tines.ts | 8 +- 33 files changed, 426 insertions(+), 164 deletions(-) create mode 100644 x-pack/plugins/actions/common/routes/connector/apis/execute/index.ts create mode 100644 x-pack/plugins/actions/common/routes/connector/apis/execute/schemas/latest.ts create mode 100644 x-pack/plugins/actions/common/routes/connector/apis/execute/schemas/v1.ts create mode 100644 x-pack/plugins/actions/common/routes/connector/apis/execute/types/latest.ts create mode 100644 x-pack/plugins/actions/common/routes/connector/apis/execute/types/v1.ts create mode 100644 x-pack/plugins/actions/server/application/connector/methods/execute/execute.ts create mode 100644 x-pack/plugins/actions/server/application/connector/methods/execute/index.ts create mode 100644 x-pack/plugins/actions/server/application/connector/methods/execute/types/index.ts create mode 100644 x-pack/plugins/actions/server/application/connector/methods/execute/types/types.ts create mode 100644 x-pack/plugins/actions/server/lib/get_system_action_kibana_privileges.ts create mode 100644 x-pack/plugins/actions/server/lib/is_preconfigured.ts create mode 100644 x-pack/plugins/actions/server/lib/is_system_action.ts rename x-pack/plugins/actions/server/routes/{ => connector/execute}/execute.test.ts (85%) rename x-pack/plugins/actions/server/routes/{ => connector/execute}/execute.ts (61%) create mode 100644 x-pack/plugins/actions/server/routes/connector/execute/index.ts create mode 100644 x-pack/plugins/actions/server/routes/connector/execute/transforms/index.ts create mode 100644 x-pack/plugins/actions/server/routes/connector/execute/transforms/transform_connector_response/latest.ts create mode 100644 x-pack/plugins/actions/server/routes/connector/execute/transforms/transform_connector_response/v1.ts diff --git a/x-pack/plugins/actions/common/routes/connector/apis/execute/index.ts b/x-pack/plugins/actions/common/routes/connector/apis/execute/index.ts new file mode 100644 index 0000000000000..448428839336d --- /dev/null +++ b/x-pack/plugins/actions/common/routes/connector/apis/execute/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { + executeConnectorRequestParamsSchema, + executeConnectorRequestBodySchema, +} from './schemas/latest'; +export type { ExecuteConnectorRequestParams, ExecuteConnectorRequestBody } from './types/latest'; + +export { + executeConnectorRequestParamsSchema as executeConnectorRequestParamsSchemaV1, + executeConnectorRequestBodySchema as executeConnectorRequestBodySchemaV1, +} from './schemas/v1'; +export type { + ExecuteConnectorRequestParams as ExecuteConnectorRequestParamsV1, + ExecuteConnectorRequestBody as ExecuteConnectorRequestBodyV1, +} from './types/v1'; diff --git a/x-pack/plugins/actions/common/routes/connector/apis/execute/schemas/latest.ts b/x-pack/plugins/actions/common/routes/connector/apis/execute/schemas/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/plugins/actions/common/routes/connector/apis/execute/schemas/latest.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './v1'; diff --git a/x-pack/plugins/actions/common/routes/connector/apis/execute/schemas/v1.ts b/x-pack/plugins/actions/common/routes/connector/apis/execute/schemas/v1.ts new file mode 100644 index 0000000000000..1f41763a004a2 --- /dev/null +++ b/x-pack/plugins/actions/common/routes/connector/apis/execute/schemas/v1.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +export const executeConnectorRequestParamsSchema = schema.object({ + id: schema.string({ + meta: { + description: 'An identifier for the connector.', + }, + }), +}); + +export const executeConnectorRequestBodySchema = schema.object({ + params: schema.recordOf(schema.string(), schema.any()), +}); diff --git a/x-pack/plugins/actions/common/routes/connector/apis/execute/types/latest.ts b/x-pack/plugins/actions/common/routes/connector/apis/execute/types/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/plugins/actions/common/routes/connector/apis/execute/types/latest.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './v1'; diff --git a/x-pack/plugins/actions/common/routes/connector/apis/execute/types/v1.ts b/x-pack/plugins/actions/common/routes/connector/apis/execute/types/v1.ts new file mode 100644 index 0000000000000..cc1b6e4cdc196 --- /dev/null +++ b/x-pack/plugins/actions/common/routes/connector/apis/execute/types/v1.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { TypeOf } from '@kbn/config-schema'; +import { executeConnectorRequestParamsSchemaV1, executeConnectorRequestBodySchemaV1 } from '..'; + +export type ExecuteConnectorRequestParams = TypeOf; +export type ExecuteConnectorRequestBody = TypeOf; diff --git a/x-pack/plugins/actions/common/routes/connector/response/index.ts b/x-pack/plugins/actions/common/routes/connector/response/index.ts index c870698329052..3a58325a542ed 100644 --- a/x-pack/plugins/actions/common/routes/connector/response/index.ts +++ b/x-pack/plugins/actions/common/routes/connector/response/index.ts @@ -6,11 +6,16 @@ */ // Latest -export type { ConnectorResponse, AllConnectorsResponse } from './types/latest'; +export type { + ConnectorResponse, + AllConnectorsResponse, + ConnectorExecuteResponse, +} from './types/latest'; export { connectorResponseSchema, allConnectorsResponseSchema, connectorTypesResponseSchema, + connectorExecuteResponseSchema, } from './schemas/latest'; // v1 @@ -18,9 +23,11 @@ export type { ConnectorResponse as ConnectorResponseV1, AllConnectorsResponse as AllConnectorsResponseV1, ConnectorTypesResponse as ConnectorTypesResponseV1, + ConnectorExecuteResponse as ConnectorExecuteResponseV1, } from './types/v1'; export { connectorResponseSchema as connectorResponseSchemaV1, allConnectorsResponseSchema as connectorWithExtraFindDataSchemaV1, connectorTypesResponseSchema as connectorTypesResponseSchemaV1, + connectorExecuteResponseSchema as connectorExecuteResponseSchemaV1, } from './schemas/v1'; diff --git a/x-pack/plugins/actions/common/routes/connector/response/schemas/latest.ts b/x-pack/plugins/actions/common/routes/connector/response/schemas/latest.ts index bc4edc5be46d0..c89efd04bc485 100644 --- a/x-pack/plugins/actions/common/routes/connector/response/schemas/latest.ts +++ b/x-pack/plugins/actions/common/routes/connector/response/schemas/latest.ts @@ -8,3 +8,4 @@ export { connectorResponseSchema } from './v1'; export { allConnectorsResponseSchema } from './v1'; export { connectorTypesResponseSchema } from './v1'; +export { connectorExecuteResponseSchema } from './v1'; diff --git a/x-pack/plugins/actions/common/routes/connector/response/schemas/v1.ts b/x-pack/plugins/actions/common/routes/connector/response/schemas/v1.ts index 5c9b95ca8fc7b..096e2f2943d80 100644 --- a/x-pack/plugins/actions/common/routes/connector/response/schemas/v1.ts +++ b/x-pack/plugins/actions/common/routes/connector/response/schemas/v1.ts @@ -98,3 +98,55 @@ export const connectorTypesResponseSchema = schema.object({ meta: { description: 'Indicates whether the action is a system action.' }, }), }); + +export const connectorExecuteResponseSchema = schema.object({ + connector_id: schema.string({ + meta: { + description: 'The identifier for the connector.', + }, + }), + status: schema.oneOf([schema.literal('ok'), schema.literal('error')], { + meta: { + description: 'The outcome of the connector execution.', + }, + }), + message: schema.maybe( + schema.string({ + meta: { + description: 'The connector execution error message.', + }, + }) + ), + service_message: schema.maybe( + schema.string({ + meta: { + description: 'An error message that contains additional details.', + }, + }) + ), + data: schema.maybe( + schema.any({ + meta: { + description: 'The connector execution data.', + }, + }) + ), + retry: schema.maybe( + schema.nullable( + schema.oneOf([schema.boolean(), schema.string()], { + meta: { + description: + 'When the status is error, identifies whether the connector execution will retry .', + }, + }) + ) + ), + errorSource: schema.maybe( + schema.oneOf([schema.literal('user'), schema.literal('framework')], { + meta: { + description: + 'When the status is error, identifies whether the error is a framework error or a user error.', + }, + }) + ), +}); diff --git a/x-pack/plugins/actions/common/routes/connector/response/types/v1.ts b/x-pack/plugins/actions/common/routes/connector/response/types/v1.ts index 3bf7401d2d0e0..499cc2ec21d48 100644 --- a/x-pack/plugins/actions/common/routes/connector/response/types/v1.ts +++ b/x-pack/plugins/actions/common/routes/connector/response/types/v1.ts @@ -10,6 +10,7 @@ import { connectorResponseSchemaV1, connectorTypesResponseSchemaV1, allConnectorsResponseSchema, + connectorExecuteResponseSchema, } from '..'; type ConnectorResponseSchemaType = TypeOf; @@ -41,3 +42,14 @@ export interface ConnectorTypesResponse { supported_feature_ids: ConnectorTypesResponseSchemaType['supported_feature_ids']; is_system_action_type: ConnectorTypesResponseSchemaType['is_system_action_type']; } + +type ConnectorExecuteResponseSchemaType = TypeOf; +export interface ConnectorExecuteResponse { + connector_id: ConnectorExecuteResponseSchemaType['connector_id']; + status: ConnectorExecuteResponseSchemaType['status']; + message?: ConnectorExecuteResponseSchemaType['message']; + service_message?: ConnectorExecuteResponseSchemaType['service_message']; + data?: ConnectorExecuteResponseSchemaType['data']; + retry?: ConnectorExecuteResponseSchemaType['retry']; + errorSource?: ConnectorExecuteResponseSchemaType['errorSource']; +} diff --git a/x-pack/plugins/actions/server/actions_client/actions_client.ts b/x-pack/plugins/actions/server/actions_client/actions_client.ts index f485d82b2f120..edad072acbca6 100644 --- a/x-pack/plugins/actions/server/actions_client/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client/actions_client.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { v4 as uuidv4 } from 'uuid'; import Boom from '@hapi/boom'; import url from 'url'; import { UsageCounter } from '@kbn/usage-collection-plugin/server'; @@ -30,6 +29,7 @@ import { get } from '../application/connector/methods/get'; import { getAll, getAllSystemConnectors } from '../application/connector/methods/get_all'; import { update } from '../application/connector/methods/update'; import { listTypes } from '../application/connector/methods/list_types'; +import { execute } from '../application/connector/methods/execute'; import { GetGlobalExecutionKPIParams, GetGlobalExecutionLogParams, @@ -54,7 +54,6 @@ import { HookServices, } from '../types'; import { PreconfiguredActionDisabledModificationError } from '../lib/errors/preconfigured_action_disabled_modification'; -import { ExecuteOptions } from '../lib/action_executor'; import { ExecutionEnqueuer, ExecuteOptions as EnqueueExecutionOptions, @@ -96,6 +95,9 @@ import { connectorFromSavedObject, isConnectorDeprecated } from '../application/ import { ListTypesParams } from '../application/connector/methods/list_types/types'; import { ConnectorUpdateParams } from '../application/connector/methods/update/types'; import { ConnectorUpdate } from '../application/connector/methods/update/types/types'; +import { isPreconfigured } from '../lib/is_preconfigured'; +import { isSystemAction } from '../lib/is_system_action'; +import { ConnectorExecuteParams } from '../application/connector/methods/execute/types'; interface Action extends ConnectorUpdate { actionTypeId: string; @@ -649,75 +651,10 @@ export class ActionsClient { return result; } - private getSystemActionKibanaPrivileges(connectorId: string, params?: ExecuteOptions['params']) { - const inMemoryConnector = this.context.inMemoryConnectors.find( - (connector) => connector.id === connectorId - ); - - const additionalPrivileges = inMemoryConnector?.isSystemAction - ? this.context.actionTypeRegistry.getSystemActionKibanaPrivileges( - inMemoryConnector.actionTypeId, - params - ) - : []; - - return additionalPrivileges; - } - - public async execute({ - actionId, - params, - source, - relatedSavedObjects, - }: Omit): Promise< - ActionTypeExecutorResult - > { - const log = this.context.logger; - - if ( - (await getAuthorizationModeBySource(this.context.unsecuredSavedObjectsClient, source)) === - AuthorizationMode.RBAC - ) { - const additionalPrivileges = this.getSystemActionKibanaPrivileges(actionId, params); - let actionTypeId: string | undefined; - - try { - if (this.isPreconfigured(actionId) || this.isSystemAction(actionId)) { - const connector = this.context.inMemoryConnectors.find( - (inMemoryConnector) => inMemoryConnector.id === actionId - ); - - actionTypeId = connector?.actionTypeId; - } else { - // TODO: Optimize so we don't do another get on top of getAuthorizationModeBySource and within the actionExecutor.execute - const { attributes } = await this.context.unsecuredSavedObjectsClient.get( - 'action', - actionId - ); - - actionTypeId = attributes.actionTypeId; - } - } catch (err) { - log.debug(`Failed to retrieve actionTypeId for action [${actionId}]`, err); - } - - await this.context.authorization.ensureAuthorized({ - operation: 'execute', - additionalPrivileges, - actionTypeId, - }); - } else { - trackLegacyRBACExemption('execute', this.context.usageCounter); - } - - return this.context.actionExecutor.execute({ - actionId, - params, - source, - request: this.context.request, - relatedSavedObjects, - actionExecutionId: uuidv4(), - }); + public async execute( + connectorExecuteParams: ConnectorExecuteParams + ): Promise> { + return execute(this.context, connectorExecuteParams); } public async bulkEnqueueExecution( @@ -789,15 +726,11 @@ export class ActionsClient { } public isPreconfigured(connectorId: string): boolean { - return !!this.context.inMemoryConnectors.find( - (connector) => connector.isPreconfigured && connector.id === connectorId - ); + return isPreconfigured(this.context, connectorId); } public isSystemAction(connectorId: string): boolean { - return !!this.context.inMemoryConnectors.find( - (connector) => connector.isSystemAction && connector.id === connectorId - ); + return isSystemAction(this.context, connectorId); } public async getGlobalExecutionLogWithAuth({ diff --git a/x-pack/plugins/actions/server/application/connector/methods/execute/execute.ts b/x-pack/plugins/actions/server/application/connector/methods/execute/execute.ts new file mode 100644 index 0000000000000..f9922e0b61a8d --- /dev/null +++ b/x-pack/plugins/actions/server/application/connector/methods/execute/execute.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { v4 as uuidv4 } from 'uuid'; +import { RawAction, ActionTypeExecutorResult } from '../../../../types'; +import { getSystemActionKibanaPrivileges } from '../../../../lib/get_system_action_kibana_privileges'; +import { isPreconfigured } from '../../../../lib/is_preconfigured'; +import { isSystemAction } from '../../../../lib/is_system_action'; +import { + getAuthorizationModeBySource, + AuthorizationMode, +} from '../../../../authorization/get_authorization_mode_by_source'; +import { trackLegacyRBACExemption } from '../../../../lib/track_legacy_rbac_exemption'; +import { ConnectorExecuteParams } from './types'; +import { ACTION_SAVED_OBJECT_TYPE } from '../../../../constants/saved_objects'; +import { ActionsClientContext } from '../../../../actions_client'; + +export async function execute( + context: ActionsClientContext, + connectorExecuteParams: ConnectorExecuteParams +): Promise> { + const log = context.logger; + const { actionId, params, source, relatedSavedObjects } = connectorExecuteParams; + + if ( + (await getAuthorizationModeBySource(context.unsecuredSavedObjectsClient, source)) === + AuthorizationMode.RBAC + ) { + const additionalPrivileges = getSystemActionKibanaPrivileges(context, actionId, params); + let actionTypeId: string | undefined; + + try { + if (isPreconfigured(context, actionId) || isSystemAction(context, actionId)) { + const connector = context.inMemoryConnectors.find( + (inMemoryConnector) => inMemoryConnector.id === actionId + ); + + actionTypeId = connector?.actionTypeId; + } else { + // TODO: Optimize so we don't do another get on top of getAuthorizationModeBySource and within the actionExecutor.execute + const { attributes } = await context.unsecuredSavedObjectsClient.get( + ACTION_SAVED_OBJECT_TYPE, + actionId + ); + + actionTypeId = attributes.actionTypeId; + } + } catch (err) { + log.debug(`Failed to retrieve actionTypeId for action [${actionId}]`, err); + } + + await context.authorization.ensureAuthorized({ + operation: 'execute', + additionalPrivileges, + actionTypeId, + }); + } else { + trackLegacyRBACExemption('execute', context.usageCounter); + } + + return context.actionExecutor.execute({ + actionId, + params, + source, + request: context.request, + relatedSavedObjects, + actionExecutionId: uuidv4(), + }); +} diff --git a/x-pack/plugins/actions/server/application/connector/methods/execute/index.ts b/x-pack/plugins/actions/server/application/connector/methods/execute/index.ts new file mode 100644 index 0000000000000..21598e68a047c --- /dev/null +++ b/x-pack/plugins/actions/server/application/connector/methods/execute/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { execute } from './execute'; diff --git a/x-pack/plugins/actions/server/application/connector/methods/execute/types/index.ts b/x-pack/plugins/actions/server/application/connector/methods/execute/types/index.ts new file mode 100644 index 0000000000000..ff2bc6be97a80 --- /dev/null +++ b/x-pack/plugins/actions/server/application/connector/methods/execute/types/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type { ConnectorExecuteParams } from './types'; diff --git a/x-pack/plugins/actions/server/application/connector/methods/execute/types/types.ts b/x-pack/plugins/actions/server/application/connector/methods/execute/types/types.ts new file mode 100644 index 0000000000000..22aa019de599f --- /dev/null +++ b/x-pack/plugins/actions/server/application/connector/methods/execute/types/types.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ExecuteOptions } from '../../../../../lib/action_executor'; + +export type ConnectorExecuteParams = Omit; diff --git a/x-pack/plugins/actions/server/lib/get_system_action_kibana_privileges.ts b/x-pack/plugins/actions/server/lib/get_system_action_kibana_privileges.ts new file mode 100644 index 0000000000000..ef3b8ff853d17 --- /dev/null +++ b/x-pack/plugins/actions/server/lib/get_system_action_kibana_privileges.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ActionsClientContext } from '../actions_client'; +import { ExecuteOptions } from './action_executor'; + +export function getSystemActionKibanaPrivileges( + context: ActionsClientContext, + connectorId: string, + params?: ExecuteOptions['params'] +) { + const inMemoryConnector = context.inMemoryConnectors.find( + (connector) => connector.id === connectorId + ); + + const additionalPrivileges = inMemoryConnector?.isSystemAction + ? context.actionTypeRegistry.getSystemActionKibanaPrivileges( + inMemoryConnector.actionTypeId, + params + ) + : []; + + return additionalPrivileges; +} diff --git a/x-pack/plugins/actions/server/lib/is_preconfigured.ts b/x-pack/plugins/actions/server/lib/is_preconfigured.ts new file mode 100644 index 0000000000000..9f42c496d7cb2 --- /dev/null +++ b/x-pack/plugins/actions/server/lib/is_preconfigured.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ActionsClientContext } from '../actions_client'; + +export function isPreconfigured(context: ActionsClientContext, connectorId: string): boolean { + return !!context.inMemoryConnectors.find( + (connector) => connector.isPreconfigured && connector.id === connectorId + ); +} diff --git a/x-pack/plugins/actions/server/lib/is_system_action.ts b/x-pack/plugins/actions/server/lib/is_system_action.ts new file mode 100644 index 0000000000000..e21e1ee480df8 --- /dev/null +++ b/x-pack/plugins/actions/server/lib/is_system_action.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ActionsClientContext } from '../actions_client'; + +export function isSystemAction(context: ActionsClientContext, connectorId: string): boolean { + return !!context.inMemoryConnectors.find( + (connector) => connector.isSystemAction && connector.id === connectorId + ); +} diff --git a/x-pack/plugins/actions/server/routes/execute.test.ts b/x-pack/plugins/actions/server/routes/connector/execute/execute.test.ts similarity index 85% rename from x-pack/plugins/actions/server/routes/execute.test.ts rename to x-pack/plugins/actions/server/routes/connector/execute/execute.test.ts index 39319ff1dabf2..a9ae5e881f141 100644 --- a/x-pack/plugins/actions/server/routes/execute.test.ts +++ b/x-pack/plugins/actions/server/routes/connector/execute/execute.test.ts @@ -5,16 +5,16 @@ * 2.0. */ -import { executeActionRoute } from './execute'; +import { executeConnectorRoute } from './execute'; import { httpServiceMock } from '@kbn/core/server/mocks'; -import { licenseStateMock } from '../lib/license_state.mock'; -import { mockHandlerArguments } from './legacy/_mock_handler_arguments'; -import { asHttpRequestExecutionSource } from '../lib'; -import { actionsClientMock } from '../actions_client/actions_client.mock'; -import { ActionTypeExecutorResult } from '../types'; -import { verifyAccessAndContext } from './verify_access_and_context'; - -jest.mock('./verify_access_and_context', () => ({ +import { licenseStateMock } from '../../../lib/license_state.mock'; +import { mockHandlerArguments } from '../../legacy/_mock_handler_arguments'; +import { asHttpRequestExecutionSource } from '../../../lib'; +import { actionsClientMock } from '../../../actions_client/actions_client.mock'; +import { ActionTypeExecutorResult } from '../../../types'; +import { verifyAccessAndContext } from '../../verify_access_and_context'; + +jest.mock('../../verify_access_and_context', () => ({ verifyAccessAndContext: jest.fn(), })); @@ -23,7 +23,7 @@ beforeEach(() => { (verifyAccessAndContext as jest.Mock).mockImplementation((license, handler) => handler); }); -describe('executeActionRoute', () => { +describe('executeConnectorRoute', () => { beforeEach(() => { jest.clearAllMocks(); }); @@ -55,7 +55,7 @@ describe('executeActionRoute', () => { status: 'ok', }; - executeActionRoute(router, licenseState); + executeConnectorRoute(router, licenseState); const [config, handler] = router.post.mock.calls[0]; @@ -95,7 +95,7 @@ describe('executeActionRoute', () => { ['noContent'] ); - executeActionRoute(router, licenseState); + executeConnectorRoute(router, licenseState); const [, handler] = router.post.mock.calls[0]; @@ -131,7 +131,7 @@ describe('executeActionRoute', () => { ['ok'] ); - executeActionRoute(router, licenseState); + executeConnectorRoute(router, licenseState); const [, handler] = router.post.mock.calls[0]; @@ -163,7 +163,7 @@ describe('executeActionRoute', () => { ['ok'] ); - executeActionRoute(router, licenseState); + executeConnectorRoute(router, licenseState); const [, handler] = router.post.mock.calls[0]; @@ -192,7 +192,7 @@ describe('executeActionRoute', () => { ['ok'] ); - executeActionRoute(router, licenseState); + executeConnectorRoute(router, licenseState); const [_, handler] = router.post.mock.calls[0]; diff --git a/x-pack/plugins/actions/server/routes/execute.ts b/x-pack/plugins/actions/server/routes/connector/execute/execute.ts similarity index 61% rename from x-pack/plugins/actions/server/routes/execute.ts rename to x-pack/plugins/actions/server/routes/connector/execute/execute.ts index 74813a73474ac..ab5ed25ff5f78 100644 --- a/x-pack/plugins/actions/server/routes/execute.ts +++ b/x-pack/plugins/actions/server/routes/connector/execute/execute.ts @@ -5,37 +5,23 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; import { IRouter } from '@kbn/core/server'; -import { ILicenseState } from '../lib'; +import { ILicenseState } from '../../../lib'; -import { ActionTypeExecutorResult, ActionsRequestHandlerContext } from '../types'; -import { BASE_ACTION_API_PATH, RewriteResponseCase } from '../../common'; -import { asHttpRequestExecutionSource } from '../lib/action_execution_source'; -import { verifyAccessAndContext } from './verify_access_and_context'; -import { connectorResponseSchemaV1 } from '../../common/routes/connector/response'; +import { ActionTypeExecutorResult, ActionsRequestHandlerContext } from '../../../types'; +import { BASE_ACTION_API_PATH } from '../../../../common'; +import { asHttpRequestExecutionSource } from '../../../lib/action_execution_source'; +import { verifyAccessAndContext } from '../../verify_access_and_context'; +import { connectorResponseSchemaV1 } from '../../../../common/routes/connector/response'; +import { + executeConnectorRequestBodySchemaV1, + ExecuteConnectorRequestBodyV1, + executeConnectorRequestParamsSchemaV1, + ExecuteConnectorRequestParamsV1, +} from '../../../../common/routes/connector/apis/execute'; +import { transformExecuteConnectorResponseV1 } from './transforms'; -const paramSchema = schema.object({ - id: schema.string({ - meta: { description: 'An identifier for the connector.' }, - }), -}); - -const bodySchema = schema.object({ - params: schema.recordOf(schema.string(), schema.any()), -}); - -const rewriteBodyRes: RewriteResponseCase> = ({ - actionId, - serviceMessage, - ...res -}) => ({ - ...res, - connector_id: actionId, - ...(serviceMessage ? { service_message: serviceMessage } : {}), -}); - -export const executeActionRoute = ( +export const executeConnectorRoute = ( router: IRouter, licenseState: ILicenseState ) => { @@ -51,8 +37,8 @@ export const executeActionRoute = ( }, validate: { request: { - body: bodySchema, - params: paramSchema, + body: executeConnectorRequestBodySchemaV1, + params: executeConnectorRequestParamsSchemaV1, }, response: { 200: { @@ -65,8 +51,8 @@ export const executeActionRoute = ( router.handleLegacyErrors( verifyAccessAndContext(licenseState, async function (context, req, res) { const actionsClient = (await context.actions).getActionsClient(); - const { params } = req.body; - const { id } = req.params; + const { params }: ExecuteConnectorRequestBodyV1 = req.body; + const { id }: ExecuteConnectorRequestParamsV1 = req.params; if (actionsClient.isSystemAction(id)) { return res.badRequest({ body: 'Execution of system action is not allowed' }); @@ -81,7 +67,7 @@ export const executeActionRoute = ( return body ? res.ok({ - body: rewriteBodyRes(body), + body: transformExecuteConnectorResponseV1(body), }) : res.noContent(); }) diff --git a/x-pack/plugins/actions/server/routes/connector/execute/index.ts b/x-pack/plugins/actions/server/routes/connector/execute/index.ts new file mode 100644 index 0000000000000..6f5cb866722b7 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/connector/execute/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { executeConnectorRoute } from './execute'; diff --git a/x-pack/plugins/actions/server/routes/connector/execute/transforms/index.ts b/x-pack/plugins/actions/server/routes/connector/execute/transforms/index.ts new file mode 100644 index 0000000000000..5c245970ec914 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/connector/execute/transforms/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { transformExecuteConnectorResponse } from './transform_connector_response/latest'; + +export { transformExecuteConnectorResponse as transformExecuteConnectorResponseV1 } from './transform_connector_response/v1'; diff --git a/x-pack/plugins/actions/server/routes/connector/execute/transforms/transform_connector_response/latest.ts b/x-pack/plugins/actions/server/routes/connector/execute/transforms/transform_connector_response/latest.ts new file mode 100644 index 0000000000000..900d86f842fc6 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/connector/execute/transforms/transform_connector_response/latest.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { transformExecuteConnectorResponse } from './v1'; diff --git a/x-pack/plugins/actions/server/routes/connector/execute/transforms/transform_connector_response/v1.ts b/x-pack/plugins/actions/server/routes/connector/execute/transforms/transform_connector_response/v1.ts new file mode 100644 index 0000000000000..bc001cd9f9103 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/connector/execute/transforms/transform_connector_response/v1.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ConnectorExecuteResponseV1 } from '../../../../../../common/routes/connector/response'; +import { ActionTypeExecutorResult } from '../../../../../types'; + +export const transformExecuteConnectorResponse = ({ + actionId, + retry, + serviceMessage, + ...res +}: ActionTypeExecutorResult): ConnectorExecuteResponseV1 => ({ + ...res, + connector_id: actionId, + ...(retry && retry instanceof Date ? { retry: retry.toISOString() } : { retry }), + ...(serviceMessage ? { service_message: serviceMessage } : {}), +}); diff --git a/x-pack/plugins/actions/server/routes/index.ts b/x-pack/plugins/actions/server/routes/index.ts index cccca87d849e2..5ea804d1ce47e 100644 --- a/x-pack/plugins/actions/server/routes/index.ts +++ b/x-pack/plugins/actions/server/routes/index.ts @@ -15,7 +15,7 @@ import { ILicenseState } from '../lib'; import { ActionsRequestHandlerContext } from '../types'; import { createActionRoute } from './create'; import { deleteConnectorRoute } from './connector/delete'; -import { executeActionRoute } from './execute'; +import { executeConnectorRoute } from './connector/execute'; import { getConnectorRoute } from './connector/get'; import { updateConnectorRoute } from './connector/update'; import { getOAuthAccessToken } from './get_oauth_access_token'; @@ -42,7 +42,7 @@ export function defineRoutes(opts: RouteOptions) { getAllConnectorsRoute(router, licenseState); updateConnectorRoute(router, licenseState); listTypesRoute(router, licenseState); - executeActionRoute(router, licenseState); + executeConnectorRoute(router, licenseState); getGlobalExecutionLogRoute(router, licenseState); getGlobalExecutionKPIRoute(router, licenseState); 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 1ef7b170a4f0d..fcf0f2d84e755 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 @@ -246,12 +246,12 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { params: {}, }) .then((resp: any) => { - expect(Object.keys(resp.body)).to.eql([ - 'status', + expect(Object.keys(resp.body).sort()).to.eql([ + 'connector_id', + 'errorSource', 'message', 'retry', - 'errorSource', - 'connector_id', + 'status', ]); expect(resp.body.connector_id).to.eql(simulatedActionId); expect(resp.body.status).to.eql('error'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/jira.ts index 2268e379f441a..d41f8f1fcad71 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/jira.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/jira.ts @@ -236,12 +236,12 @@ export default function jiraTest({ getService }: FtrProviderContext) { params: {}, }) .then((resp: any) => { - expect(Object.keys(resp.body)).to.eql([ - 'status', + expect(Object.keys(resp.body).sort()).to.eql([ + 'connector_id', + 'errorSource', 'message', 'retry', - 'errorSource', - 'connector_id', + 'status', ]); expect(resp.body.connector_id).to.eql(simulatedActionId); expect(resp.body.status).to.eql('error'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/opsgenie.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/opsgenie.ts index bd315edfb0459..0c5f52862b9de 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/opsgenie.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/opsgenie.ts @@ -169,12 +169,12 @@ export default function opsgenieTest({ getService }: FtrProviderContext) { }); expect(200); - expect(Object.keys(body)).to.eql([ - 'status', + expect(Object.keys(body).sort()).to.eql([ + 'connector_id', + 'errorSource', 'message', 'retry', - 'errorSource', - 'connector_id', + 'status', ]); expect(body.connector_id).to.eql(opsgenieActionId); expect(body.status).to.eql('error'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/resilient.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/resilient.ts index 6dfb420463e9f..232668c24749c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/resilient.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/resilient.ts @@ -230,12 +230,12 @@ export default function resilientTest({ getService }: FtrProviderContext) { params: {}, }) .then((resp: any) => { - expect(Object.keys(resp.body)).to.eql([ - 'status', + expect(Object.keys(resp.body).sort()).to.eql([ + 'connector_id', + 'errorSource', 'message', 'retry', - 'errorSource', - 'connector_id', + 'status', ]); expect(resp.body.connector_id).to.eql(resilientActionId); expect(resp.body.status).to.eql('error'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/servicenow_itom.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/servicenow_itom.ts index 0f1748db4f5ef..c189580951495 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/servicenow_itom.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/servicenow_itom.ts @@ -416,12 +416,12 @@ export default function serviceNowITOMTest({ getService }: FtrProviderContext) { params: {}, }) .then((resp: any) => { - expect(Object.keys(resp.body)).to.eql([ - 'status', + expect(Object.keys(resp.body).sort()).to.eql([ + 'connector_id', + 'errorSource', 'message', 'retry', - 'errorSource', - 'connector_id', + 'status', ]); expect(resp.body.connector_id).to.eql(simulatedActionId); expect(resp.body.status).to.eql('error'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/servicenow_itsm.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/servicenow_itsm.ts index bc0f48f15caf5..1f4f01db068d9 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/servicenow_itsm.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/servicenow_itsm.ts @@ -452,12 +452,12 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) { params: {}, }) .then((resp: any) => { - expect(Object.keys(resp.body)).to.eql([ - 'status', + expect(Object.keys(resp.body).sort()).to.eql([ + 'connector_id', + 'errorSource', 'message', 'retry', - 'errorSource', - 'connector_id', + 'status', ]); expect(resp.body.connector_id).to.eql(simulatedActionId); expect(resp.body.status).to.eql('error'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/servicenow_sir.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/servicenow_sir.ts index 717a44a406712..527ea53bbd1d5 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/servicenow_sir.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/servicenow_sir.ts @@ -465,12 +465,12 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) { params: {}, }) .then((resp: any) => { - expect(Object.keys(resp.body)).to.eql([ - 'status', + expect(Object.keys(resp.body).sort()).to.eql([ + 'connector_id', + 'errorSource', 'message', 'retry', - 'errorSource', - 'connector_id', + 'status', ]); expect(resp.body.connector_id).to.eql(simulatedActionId); expect(resp.body.status).to.eql('error'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/swimlane.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/swimlane.ts index 4d91fdddf80dd..93c2e4bc973af 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/swimlane.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/swimlane.ts @@ -327,12 +327,12 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { params: {}, }) .then((resp: any) => { - expect(Object.keys(resp.body)).to.eql([ - 'status', + expect(Object.keys(resp.body).sort()).to.eql([ + 'connector_id', + 'errorSource', 'message', 'retry', - 'errorSource', - 'connector_id', + 'status', ]); expect(resp.body.connector_id).to.eql(simulatedActionId); expect(resp.body.status).to.eql('error'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/tines.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/tines.ts index 04971990f879e..25b3b4b35cc76 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/tines.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/tines.ts @@ -188,12 +188,12 @@ export default function tinesTest({ getService }: FtrProviderContext) { }); expect(200); - expect(Object.keys(body)).to.eql([ - 'status', + expect(Object.keys(body).sort()).to.eql([ + 'connector_id', + 'errorSource', 'message', 'retry', - 'errorSource', - 'connector_id', + 'status', ]); expect(body.connector_id).to.eql(tinesActionId); expect(body.status).to.eql('error'); From e34876aa6809a5ea79a836ce61e94192a4769d59 Mon Sep 17 00:00:00 2001 From: Viduni Wickramarachchi Date: Wed, 16 Oct 2024 10:42:09 -0400 Subject: [PATCH 6/6] [Obs AI Assistant] Pass function responses when copying conversation (#195635) Closes https://github.com/elastic/kibana/issues/181216 ## Summary ### Problem Function call arguments and responses are serialized separately. Therefore, when a conversation is copied, arguments and responses (`content` and `data`) appear as strings instead of JSON objects. This makes it harder to debug. ### Solution Deserialize the arguments and responses and include it in the copied conversation object. Example of original copied conversation:
Click to expand JSON ```json { "title": "", "messages": [ { "@timestamp": "2024-10-09T13:58:41.723Z", "message": { "role": "system", "content": "You are a helpful assistant for Elastic Observability...." } }, { "@timestamp": "2024-10-09T13:58:26.881Z", "message": { "role": "user", "content": "Give me examples of questions I can ask here." } }, { "@timestamp": "2024-10-09T13:58:26.965Z", "message": { "role": "assistant", "function_call": { "name": "context", "trigger": "assistant" }, "content": "" } }, { "@timestamp": "2024-10-09T13:58:27.063Z", "message": { "role": "user", "data": "{\"scores\":[],\"suggestions\":[]}", "name": "context", "content": "{\"screen_description\":\"The user is looking at http://localhost:5601/kyq/app/observability/overview?rangeFrom=now-15m&rangeTo=now. The current time range is 2024-10-09T13:40:00.288Z - 2024-10-09T13:55:00.288Z.\\n\\nThe user is viewing the Overview page which shows a summary of the following apps: {\\\"universal_profiling\\\":{\\\"hasData\\\":false,\\\"status\\\":\\\"success\\\"},\\\"uptime\\\":{\\\"hasData\\\":false,\\\"indices\\\":\\\"heartbeat-*\\\",\\\"status\\\":\\\"success\\\"},\\\"infra_metrics\\\":{\\\"hasData\\\":false,\\\"indices\\\":\\\"metrics-*,metricbeat-*\\\",\\\"status\\\":\\\"success\\\"},\\\"alert\\\":{\\\"hasData\\\":false,\\\"status\\\":\\\"success\\\"},\\\"apm\\\":{\\\"hasData\\\":false,\\\"indices\\\":{\\\"transaction\\\":\\\"traces-apm*,apm-*,traces-*.otel-*\\\",\\\"span\\\":\\\"traces-apm*,apm-*,traces-*.otel-*\\\",\\\"error\\\":\\\"logs-apm*,apm-*,logs-*.otel-*\\\",\\\"metric\\\":\\\"metrics-apm*,apm-*,metrics-*.otel-*\\\",\\\"onboarding\\\":\\\"apm-*\\\",\\\"sourcemap\\\":\\\"apm-*\\\"},\\\"status\\\":\\\"success\\\"},\\\"ux\\\":{\\\"hasData\\\":false,\\\"indices\\\":\\\"traces-apm*,apm-*,traces-*.otel-*,logs-apm*,apm-*,logs-*.otel-*,metrics-apm*,apm-*,metrics-*.otel-*\\\",\\\"status\\\":\\\"success\\\"},\\\"infra_logs\\\":{\\\"hasData\\\":false,\\\"indices\\\":\\\"logs-*-*,logs-*,filebeat-*,kibana_sample_data_logs*\\\",\\\"status\\\":\\\"success\\\"}}\",\"learnings\":[]}" } }, { "@timestamp": "2024-10-09T13:58:35.140Z", "message": { "role": "assistant", "function_call": { "name": "", "arguments": "", "trigger": "assistant" }, "content": "Sure, here are some examples of questions you can ask:\n\n1. \"What is the average response time for my services?\"\n2. \"Show me the error rate for my services.\"\n3. \"Are there any anomalies in my system?\"\n4. \"What are the top 5 services by transaction volume?\"\n5. \"Show me the logs for a specific service.\"\n6. \"Are there any alerts in my system?\"\n7. \"What is the CPU usage of my hosts?\"\n8. \"Show me the network traffic in my system.\"\n9. \"What is the disk usage of my hosts?\"\n10. \"Show me the memory usage of my containers.\"\n\nPlease note that the actual questions you can ask depend on the data you have in your system." } }, { "@timestamp": "2024-10-09T13:58:41.651Z", "message": { "role": "user", "content": "What are the top 5 services by transaction volume" } }, { "@timestamp": "2024-10-09T13:58:41.723Z", "message": { "role": "assistant", "function_call": { "name": "context", "trigger": "assistant" }, "content": "" } }, { "@timestamp": "2024-10-09T13:58:41.784Z", "message": { "role": "user", "data": "{\"scores\":[],\"suggestions\":[]}", "name": "context", "content": "{\"screen_description\":\"The user is looking at http://localhost:5601/kyq/app/observability/overview?rangeFrom=now-15m&rangeTo=now. The current time range is 2024-10-09T13:40:00.288Z - 2024-10-09T13:55:00.288Z.\\n\\nThe user is viewing the Overview page which shows a summary of the following apps: {\\\"universal_profiling\\\":{\\\"hasData\\\":false,\\\"status\\\":\\\"success\\\"},\\\"uptime\\\":{\\\"hasData\\\":false,\\\"indices\\\":\\\"heartbeat-*\\\",\\\"status\\\":\\\"success\\\"},\\\"infra_metrics\\\":{\\\"hasData\\\":false,\\\"indices\\\":\\\"metrics-*,metricbeat-*\\\",\\\"status\\\":\\\"success\\\"},\\\"alert\\\":{\\\"hasData\\\":false,\\\"status\\\":\\\"success\\\"},\\\"apm\\\":{\\\"hasData\\\":false,\\\"indices\\\":{\\\"transaction\\\":\\\"traces-apm*,apm-*,traces-*.otel-*\\\",\\\"span\\\":\\\"traces-apm*,apm-*,traces-*.otel-*\\\",\\\"error\\\":\\\"logs-apm*,apm-*,logs-*.otel-*\\\",\\\"metric\\\":\\\"metrics-apm*,apm-*,metrics-*.otel-*\\\",\\\"onboarding\\\":\\\"apm-*\\\",\\\"sourcemap\\\":\\\"apm-*\\\"},\\\"status\\\":\\\"success\\\"},\\\"ux\\\":{\\\"hasData\\\":false,\\\"indices\\\":\\\"traces-apm*,apm-*,traces-*.otel-*,logs-apm*,apm-*,logs-*.otel-*,metrics-apm*,apm-*,metrics-*.otel-*\\\",\\\"status\\\":\\\"success\\\"},\\\"infra_logs\\\":{\\\"hasData\\\":false,\\\"indices\\\":\\\"logs-*-*,logs-*,filebeat-*,kibana_sample_data_logs*\\\",\\\"status\\\":\\\"success\\\"}}\",\"learnings\":[]}" } }, { "@timestamp": "2024-10-09T13:58:43.370Z", "message": { "role": "assistant", "function_call": { "name": "get_dataset_info", "arguments": "{\n \"index\": \"traces-apm*,apm-*,traces-*.otel-*\"\n}", "trigger": "assistant" }, "content": "" } }, { "@timestamp": "2024-10-09T13:58:43.379Z", "message": { "role": "user", "name": "get_dataset_info", "content": "{\"indices\":[],\"fields\":[]}" } }, { "@timestamp": "2024-10-09T13:58:43.998Z", "message": { "role": "assistant", "function_call": { "name": "query", "arguments": "{}", "trigger": "assistant" }, "content": "" } }, { "@timestamp": "2024-10-09T13:58:47.572Z", "message": { "role": "user", "data": "{\"keywords\":[\"STATS\",\"SORT\",\"LIMIT\",\"COUNT\"],\"requestedDocumentation\":{\"STATS\":\"# STATS ... BY\\n\\nThe `STATS ... BY` command groups rows based on a common value and calculates one or more aggregated values over these grouped rows.\\n\\n## Syntax\\n\\n```esql\\nSTATS [column1 =] expression1[, ..., [columnN =] expressionN] [BY grouping_expression1[, ..., grouping_expressionN]]\\n```\\n\\n### Parameters\\n\\n#### columnX\\n\\nThe name for the aggregated value in the output. If not provided...."}}", "name": "query", "content": "{}" } }, { "@timestamp": "2024-10-09T13:58:51.481Z", "message": { "role": "assistant", "function_call": { "name": "execute_query", "arguments": "{\"query\":\"FROM traces-apm*,apm-*,traces-*.otel-* | STATS COUNT(*) BY service.name | SORT COUNT(*) DESC | LIMIT 5\"}", "trigger": "assistant" }, "content": "" } }, { "@timestamp": "2024-10-09T13:58:51.619Z", "message": { "role": "user", "name": "execute_query", "content": "{\"message\":\"The query failed to execute\",\"error\":{\"name\":\"ResponseError\",\"message\":\"verification_exception\\n\\tRoot causes:\\n\\t\\tverification_exception: Found 1 problem\\nline 1:1: Unknown index [traces-apm*,apm-*,traces-*.otel-*]\"}}" } }, { "@timestamp": "2024-10-09T13:58:56.600Z", "message": { "role": "assistant", "function_call": { "name": "", "arguments": "", "trigger": "assistant" }, "content": "I'm sorry, but it seems like there are no indices matching the pattern \"traces-apm*,apm-*,traces-*.otel-*\" in your system. Therefore, I'm unable to retrieve the top 5 services by transaction volume. Please ensure that your APM data is being ingested correctly into Elasticsearch." } } ] } ```

Copied conversation after deserializing:
Click to expand JSON ```json { "title": "", "messages": [ { "@timestamp": "2024-10-09T13:58:41.723Z", "message": { "role": "system", "content": "You are a helpful assistant for Elastic Observability. Your goal is to help the Elastic Observability users to quickly assess what is happening in their observed systems..." } }, { "@timestamp": "2024-10-09T13:58:26.881Z", "message": { "role": "user", "content": "Give me examples of questions I can ask here." } }, { "@timestamp": "2024-10-09T13:58:26.965Z", "message": { "role": "assistant", "function_call": { "name": "context", "trigger": "assistant" }, "content": "" } }, { "@timestamp": "2024-10-09T13:58:27.063Z", "message": { "role": "user", "data": { "scores": [], "suggestions": [] }, "name": "context", "content": { "screen_description": "The user is looking at http://localhost:5601/kyq/app/observability/overview?rangeFrom=now-15m&rangeTo=now. The current time range is 2024-10-09T13:40:00.288Z - 2024-10-09T13:55:00.288Z.\n\nThe user is viewing the Overview page which shows a summary of the following apps: {\"universal_profiling\":{\"hasData\":false,\"status\":\"success\"},\"uptime\":{\"hasData\":false,\"indices\":\"heartbeat-*\",\"status\":\"success\"},\"infra_metrics\":{\"hasData\":false,\"indices\":\"metrics-*,metricbeat-*\",\"status\":\"success\"},\"alert\":{\"hasData\":false,\"status\":\"success\"},\"apm\":{\"hasData\":false,\"indices\":{\"transaction\":\"traces-apm*,apm-*,traces-*.otel-*\",\"span\":\"traces-apm*,apm-*,traces-*.otel-*\",\"error\":\"logs-apm*,apm-*,logs-*.otel-*\",\"metric\":\"metrics-apm*,apm-*,metrics-*.otel-*\",\"onboarding\":\"apm-*\",\"sourcemap\":\"apm-*\"},\"status\":\"success\"},\"ux\":{\"hasData\":false,\"indices\":\"traces-apm*,apm-*,traces-*.otel-*,logs-apm*,apm-*,logs-*.otel-*,metrics-apm*,apm-*,metrics-*.otel-*\",\"status\":\"success\"},\"infra_logs\":{\"hasData\":false,\"indices\":\"logs-*-*,logs-*,filebeat-*,kibana_sample_data_logs*\",\"status\":\"success\"}}", "learnings": [] } } }, { "@timestamp": "2024-10-09T13:58:35.140Z", "message": { "role": "assistant", "function_call": { "name": "", "arguments": "", "trigger": "assistant" }, "content": "Sure, here are some examples of questions..." } }, { "@timestamp": "2024-10-09T13:58:41.651Z", "message": { "role": "user", "content": "What are the top 5 services by transaction volume" } }, { "@timestamp": "2024-10-09T13:58:41.723Z", "message": { "role": "assistant", "function_call": { "name": "context", "trigger": "assistant" }, "content": "" } }, { "@timestamp": "2024-10-09T13:58:41.784Z", "message": { "role": "user", "data": { "scores": [], "suggestions": [] }, "name": "context", "content": { "screen_description": "The user is looking at http://localhost:5601/kyq/app/observability/overview?rangeFrom=now-15m&rangeTo=now. The current time range is 2024-10-09T13:40:00.288Z - 2024-10-09T13:55:00.288Z.\n\nThe user is viewing the Overview page which shows a summary of the following apps: {\"universal_profiling\":{\"hasData\":false,\"status\":\"success\"},\"uptime\":{\"hasData\":false,\"indices\":\"heartbeat-*\",\"status\":\"success\"},\"infra_metrics\":{\"hasData\":false,\"indices\":\"metrics-*,metricbeat-*\",\"status\":\"success\"},\"alert\":{\"hasData\":false,\"status\":\"success\"},\"apm\":{\"hasData\":false,\"indices\":{\"transaction\":\"traces-apm*,apm-*,traces-*.otel-*\",\"span\":\"traces-apm*,apm-*,traces-*.otel-*\",\"error\":\"logs-apm*,apm-*,logs-*.otel-*\",\"metric\":\"metrics-apm*,apm-*,metrics-*.otel-*\",\"onboarding\":\"apm-*\",\"sourcemap\":\"apm-*\"},\"status\":\"success\"},\"ux\":{\"hasData\":false,\"indices\":\"traces-apm*,apm-*,traces-*.otel-*,logs-apm*,apm-*,logs-*.otel-*,metrics-apm*,apm-*,metrics-*.otel-*\",\"status\":\"success\"},\"infra_logs\":{\"hasData\":false,\"indices\":\"logs-*-*,logs-*,filebeat-*,kibana_sample_data_logs*\",\"status\":\"success\"}}", "learnings": [] } } }, { "@timestamp": "2024-10-09T13:58:43.370Z", "message": { "role": "assistant", "function_call": { "name": "get_dataset_info", "arguments": { "index": "traces-apm*,apm-*,traces-*.otel-*" }, "trigger": "assistant" }, "content": "" } }, { "@timestamp": "2024-10-09T13:58:43.379Z", "message": { "role": "user", "name": "get_dataset_info", "content": { "indices": [], "fields": [] } } }, { "@timestamp": "2024-10-09T13:58:43.998Z", "message": { "role": "assistant", "function_call": { "name": "query", "arguments": {}, "trigger": "assistant" }, "content": "" } }, { "@timestamp": "2024-10-09T13:58:47.572Z", "message": { "role": "user", "data": { "keywords": ["STATS", "SORT", "LIMIT", "COUNT"], "requestedDocumentation": { "STATS": "# STATS ... BY\n\nThe `STATS ... BY`...", "SORT": "# SORT\n\nThe SORT command...", "LIMIT": "# LIMIT\n\nThe LIMIT command...", "COUNT": "# COUNT\n\n....", "SYNTAX": "# ES|QL Syntax Guide....", "OVERVIEW": "## ES|QL Overview\n\n### ES|QL....", "OPERATORS": "# ES|QL Operators\n\nThis document...." } }, "name": "query", "content": {} } }, { "@timestamp": "2024-10-09T13:58:51.481Z", "message": { "role": "assistant", "function_call": { "name": "execute_query", "arguments": { "query": "FROM traces-apm*,apm-*,traces-*.otel-* | STATS COUNT(*) BY service.name | SORT COUNT(*) DESC | LIMIT 5" }, "trigger": "assistant" }, "content": "" } }, { "@timestamp": "2024-10-09T13:58:51.619Z", "message": { "role": "user", "name": "execute_query", "content": { "message": "The query failed to execute", "error": { "name": "ResponseError", "message": "verification_exception\n\tRoot causes:\n\t\tverification_exception: Found 1 problem\nline 1:1: Unknown index [traces-apm*,apm-*,traces-*.otel-*]" } } } }, { "@timestamp": "2024-10-09T13:58:56.600Z", "message": { "role": "assistant", "function_call": { "name": "", "arguments": "", "trigger": "assistant" }, "content": "I'm sorry, but it seems like there are no indices matching the pattern \"traces-apm*,apm-*,traces-*.otel-*\" in your system. Therefore, I'm unable to retrieve the top 5 services by transaction volume. Please ensure that your APM data is being ingested correctly into Elasticsearch." } } ] } ```
--- .../kbn-ai-assistant/src/chat/chat_body.tsx | 5 +- .../src/utils/deserialize_message.test.ts | 118 ++++++++++++++++++ .../src/utils/deserialize_message.ts | 35 ++++++ 3 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 x-pack/packages/kbn-ai-assistant/src/utils/deserialize_message.test.ts create mode 100644 x-pack/packages/kbn-ai-assistant/src/utils/deserialize_message.ts diff --git a/x-pack/packages/kbn-ai-assistant/src/chat/chat_body.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_body.tsx index c3989f6971fff..5b80a34e0bf7b 100644 --- a/x-pack/packages/kbn-ai-assistant/src/chat/chat_body.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_body.tsx @@ -45,6 +45,7 @@ import { SimulatedFunctionCallingCallout } from './simulated_function_calling_ca import { WelcomeMessage } from './welcome_message'; import { useLicense } from '../hooks/use_license'; import { PromptEditor } from '../prompt_editor/prompt_editor'; +import { deserializeMessage } from '../utils/deserialize_message'; const fullHeightClassName = css` height: 100%; @@ -226,9 +227,11 @@ export function ChatBody({ }); const handleCopyConversation = () => { + const deserializedMessages = (conversation.value?.messages ?? messages).map(deserializeMessage); + const content = JSON.stringify({ title: initialTitle, - messages: conversation.value?.messages ?? messages, + messages: deserializedMessages, }); navigator.clipboard?.writeText(content || ''); diff --git a/x-pack/packages/kbn-ai-assistant/src/utils/deserialize_message.test.ts b/x-pack/packages/kbn-ai-assistant/src/utils/deserialize_message.test.ts new file mode 100644 index 0000000000000..b2c067a3e9f10 --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/src/utils/deserialize_message.test.ts @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { cloneDeep } from 'lodash'; +import { Message, MessageRole } from '@kbn/observability-ai-assistant-plugin/common'; +import { deserializeMessage } from './deserialize_message'; +import { safeJsonParse } from './safe_json_parse'; + +jest.mock('lodash', () => ({ + cloneDeep: jest.fn(), +})); + +jest.mock('./safe_json_parse', () => ({ + safeJsonParse: jest.fn((value) => { + try { + return JSON.parse(value); + } catch { + return value; + } + }), +})); + +describe('deserializeMessage', () => { + const baseMessage: Message = { + '@timestamp': '2024-10-15T00:00:00Z', + message: { + role: MessageRole.User, + content: 'This is a message', + }, + }; + + beforeEach(() => { + (cloneDeep as jest.Mock).mockImplementation((obj) => JSON.parse(JSON.stringify(obj))); + }); + + it('should clone the original message', () => { + const message = { ...baseMessage }; + deserializeMessage(message); + + expect(cloneDeep).toHaveBeenCalledWith(message); + }); + + it('should deserialize function_call.arguments if it is a string', () => { + const messageWithFunctionCall: Message = { + ...baseMessage, + message: { + ...baseMessage.message, + function_call: { + name: 'testFunction', + arguments: '{"key": "value"}', + trigger: MessageRole.Assistant, + }, + }, + }; + + const result = deserializeMessage(messageWithFunctionCall); + + expect(safeJsonParse).toHaveBeenCalledWith('{"key": "value"}'); + expect(result.message.function_call!.arguments).toEqual({ key: 'value' }); + }); + + it('should deserialize message.content if it is a string', () => { + const messageWithContent: Message = { + ...baseMessage, + message: { + ...baseMessage.message, + name: 'testMessage', + content: '{"key": "value"}', + }, + }; + + const result = deserializeMessage(messageWithContent); + + expect(safeJsonParse).toHaveBeenCalledWith('{"key": "value"}'); + expect(result.message.content).toEqual({ key: 'value' }); + }); + + it('should deserialize message.data if it is a string', () => { + const messageWithData: Message = { + ...baseMessage, + message: { + ...baseMessage.message, + name: 'testMessage', + data: '{"key": "value"}', + }, + }; + + const result = deserializeMessage(messageWithData); + + expect(safeJsonParse).toHaveBeenCalledWith('{"key": "value"}'); + expect(result.message.data).toEqual({ key: 'value' }); + }); + + it('should return the copied message as is if no deserialization is needed', () => { + const messageWithoutSerialization: Message = { + ...baseMessage, + message: { + ...baseMessage.message, + function_call: { + name: 'testFunction', + arguments: '', + trigger: MessageRole.Assistant, + }, + content: '', + }, + }; + + const result = deserializeMessage(messageWithoutSerialization); + + expect(result.message.function_call!.name).toEqual('testFunction'); + expect(result.message.function_call!.arguments).toEqual(''); + expect(result.message.content).toEqual(''); + }); +}); diff --git a/x-pack/packages/kbn-ai-assistant/src/utils/deserialize_message.ts b/x-pack/packages/kbn-ai-assistant/src/utils/deserialize_message.ts new file mode 100644 index 0000000000000..445e6330981a9 --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/src/utils/deserialize_message.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { cloneDeep } from 'lodash'; +import type { Message } from '@kbn/observability-ai-assistant-plugin/common'; +import { safeJsonParse } from './safe_json_parse'; + +export const deserializeMessage = (message: Message): Message => { + const copiedMessage = cloneDeep(message); + + if ( + copiedMessage.message.function_call?.arguments && + typeof copiedMessage.message.function_call?.arguments === 'string' + ) { + copiedMessage.message.function_call.arguments = safeJsonParse( + copiedMessage.message.function_call.arguments ?? '{}' + ); + } + + if (copiedMessage.message.name) { + if (copiedMessage.message.content && typeof copiedMessage.message.content === 'string') { + copiedMessage.message.content = safeJsonParse(copiedMessage.message.content); + } + + if (copiedMessage.message.data && typeof copiedMessage.message.data === 'string') { + copiedMessage.message.data = safeJsonParse(copiedMessage.message.data); + } + } + + return copiedMessage; +};