From 2a93a372ec8571e9c650bab6751af979e3d2274a Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Thu, 13 Oct 2022 09:54:15 -0400 Subject: [PATCH 01/16] Adding unsecured actions client --- .../create_unsecured_execute_function.ts | 160 ++++++++++++++++++ x-pack/plugins/actions/server/plugin.ts | 36 ++++ .../unsecured_actions_client.ts | 44 +++++ ...nsecured_actions_client_access_registry.ts | 24 +++ x-pack/plugins/alerting/server/plugin.ts | 17 ++ 5 files changed, 281 insertions(+) create mode 100644 x-pack/plugins/actions/server/create_unsecured_execute_function.ts create mode 100644 x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.ts create mode 100644 x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client_access_registry.ts diff --git a/x-pack/plugins/actions/server/create_unsecured_execute_function.ts b/x-pack/plugins/actions/server/create_unsecured_execute_function.ts new file mode 100644 index 0000000000000..b21f1a04ccb60 --- /dev/null +++ b/x-pack/plugins/actions/server/create_unsecured_execute_function.ts @@ -0,0 +1,160 @@ +/* + * 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 { compact } from 'lodash'; +import { ISavedObjectsRepository, SavedObjectsBulkResponse } from '@kbn/core/server'; +import { TaskManagerStartContract } from '@kbn/task-manager-plugin/server'; +import { + ActionTypeRegistryContract as ConnectorTypeRegistryContract, + PreConfiguredAction as PreconfiguredConnector, +} from './types'; +import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from './constants/saved_objects'; +import { ExecuteOptions as ActionExecutorOptions } from './lib/action_executor'; +import { extractSavedObjectReferences, isSavedObjectExecutionSource } from './lib'; +import { RelatedSavedObjects } from './lib/related_saved_objects'; + +interface CreateBulkUnsecuredExecuteFunctionOptions { + taskManager: TaskManagerStartContract; + isESOCanEncrypt: boolean; + connectorTypeRegistry: ConnectorTypeRegistryContract; + preconfiguredConnectors: PreconfiguredConnector[]; +} + +export interface ExecuteOptions extends Pick { + id: string; + spaceId: string; + apiKey: string | null; + executionId: string; + consumer?: string; + relatedSavedObjects?: RelatedSavedObjects; +} + +export interface ActionTaskParams extends Pick { + actionId: string; + apiKey: string | null; + executionId: string; + consumer?: string; + relatedSavedObjects?: RelatedSavedObjects; +} + +export type BulkUnsecuredExecutionEnqueuer = ( + internalSavedObjectsRepository: ISavedObjectsRepository, + actionsToExectute: ExecuteOptions[] +) => Promise; + +export function createBulkUnsecuredExecutionEnqueuerFunction({ + taskManager, + connectorTypeRegistry, + isESOCanEncrypt, + preconfiguredConnectors, +}: CreateBulkUnsecuredExecuteFunctionOptions): BulkUnsecuredExecutionEnqueuer { + return async function execute( + internalSavedObjectsRepository: ISavedObjectsRepository, + actionsToExecute: ExecuteOptions[] + ) { + if (!isESOCanEncrypt) { + throw new Error( + `Unable to execute actions because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.` + ); + } + + const connectorTypeIds: Record = {}; + const spaceIds: Record = {}; + const connectorIds = [...new Set(actionsToExecute.map((action) => action.id))]; + + const notPreconfiguredConnectors = connectorIds.filter( + (connectorId) => + preconfiguredConnectors.find((connector) => connector.id === connectorId) == null + ); + + if (notPreconfiguredConnectors.length > 0) { + // log warning or throw error? + } + + const connectors: PreconfiguredConnector[] = compact( + connectorIds.map((connectorId) => + preconfiguredConnectors.find((pConnector) => pConnector.id === connectorId) + ) + ); + + connectors.forEach((connector) => { + const { id, actionTypeId } = connector; + if (!connectorTypeRegistry.isActionExecutable(id, actionTypeId, { notifyUsage: true })) { + connectorTypeRegistry.ensureActionTypeEnabled(actionTypeId); + } + + connectorTypeIds[id] = actionTypeId; + }); + + const actions = await Promise.all( + actionsToExecute.map(async (actionToExecute) => { + // Get saved object references from action ID and relatedSavedObjects + const { references, relatedSavedObjectWithRefs } = extractSavedObjectReferences( + actionToExecute.id, + true, + actionToExecute.relatedSavedObjects + ); + const executionSourceReference = executionSourceAsSavedObjectReferences( + actionToExecute.source + ); + + const taskReferences = []; + if (executionSourceReference.references) { + taskReferences.push(...executionSourceReference.references); + } + if (references) { + taskReferences.push(...references); + } + + spaceIds[actionToExecute.id] = actionToExecute.spaceId; + + return { + type: ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, + attributes: { + actionId: actionToExecute.id, + params: actionToExecute.params, + apiKey: actionToExecute.apiKey, + executionId: actionToExecute.executionId, + consumer: actionToExecute.consumer, + relatedSavedObjects: relatedSavedObjectWithRefs, + }, + references: taskReferences, + }; + }) + ); + + const actionTaskParamsRecords: SavedObjectsBulkResponse = + await internalSavedObjectsRepository.bulkCreate(actions); + + const taskInstances = actionTaskParamsRecords.saved_objects.map((so) => { + const actionId = so.attributes.actionId; + return { + taskType: `actions:${connectorTypeIds[actionId]}`, + params: { + spaceId: spaceIds[actionId], + actionTaskParamsId: so.id, + }, + state: {}, + scope: ['actions'], + }; + }); + await taskManager.bulkSchedule(taskInstances); + }; +} + +function executionSourceAsSavedObjectReferences(executionSource: ActionExecutorOptions['source']) { + return isSavedObjectExecutionSource(executionSource) + ? { + references: [ + { + name: 'source', + ...executionSource.source, + }, + ], + } + : {}; +} diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index fa70e0dc71354..319bd9c0ffce6 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -101,6 +101,9 @@ import { createSubActionConnectorFramework } from './sub_action_framework'; import { IServiceAbstract, SubActionConnectorType } from './sub_action_framework/types'; import { SubActionConnector } from './sub_action_framework/sub_action_connector'; import { CaseConnector } from './sub_action_framework/case'; +import { UnsecuredActionsClientAccessRegistry } from './unsecured_actions_client/unsecured_actions_client_access_registry'; +import { UnsecuredActionsClient } from './unsecured_actions_client/unsecured_actions_client'; +import { createBulkUnsecuredExecutionEnqueuerFunction } from './create_unsecured_execute_function'; export interface PluginSetupContract { registerType< @@ -117,6 +120,7 @@ export interface PluginSetupContract { >( connector: SubActionConnectorType ): void; + registerUnsecuredActionsClientAccess(featureId: string): void; isPreconfiguredConnector(connectorId: string): boolean; getSubActionConnectorClass: () => IServiceAbstract; getCaseConnectorClass: () => IServiceAbstract; @@ -138,6 +142,8 @@ export interface PluginStartContract { preconfiguredActions: PreConfiguredAction[]; + getUnsecuredActionsClient(): Promise>; + renderActionParameterTemplates( actionTypeId: string, actionId: string, @@ -188,6 +194,7 @@ export class ActionsPlugin implements Plugin { subActionFramework.registerConnector(connector); }, + registerUnsecuredActionsClientAccess: (featureId: string) => { + this.unsecuredActionsClientAccessRegistry?.register(featureId); + }, isPreconfiguredConnector: (connectorId: string): boolean => { return !!this.preconfiguredActions.find( (preconfigured) => preconfigured.id === connectorId @@ -452,6 +464,29 @@ export class ActionsPlugin implements Plugin { + if (isESOCanEncrypt !== true) { + throw new Error( + `Unable to create unsecured actions client because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.` + ); + } + + const internalSavedObjectsRepository = core.savedObjects.createInternalRepository([ + ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, + ]); + + return new UnsecuredActionsClient({ + internalSavedObjectsRepository, + unsecuredActionsClientAccessRegistry: this.unsecuredActionsClientAccessRegistry!, + executionEnqueuer: createBulkUnsecuredExecutionEnqueuerFunction({ + taskManager: plugins.taskManager, + connectorTypeRegistry: actionTypeRegistry!, + isESOCanEncrypt: isESOCanEncrypt!, + preconfiguredConnectors: preconfiguredActions, + }), + }); + }; + // Ensure the public API cannot be used to circumvent authorization // using our legacy exemption mechanism by passing in a legacy SO // as authorizationContext which would then set a Legacy AuthorizationMode @@ -532,6 +567,7 @@ export class ActionsPlugin implements Plugin renderActionParameterTemplates(actionTypeRegistry, ...args), diff --git a/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.ts b/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.ts new file mode 100644 index 0000000000000..7ff0c3d72c8e7 --- /dev/null +++ b/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.ts @@ -0,0 +1,44 @@ +/* + * 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 { ISavedObjectsRepository } from '@kbn/core/server'; +import { UnsecuredActionsClientAccessRegistry } from './unsecured_actions_client_access_registry'; +import { + BulkUnsecuredExecutionEnqueuer, + ExecuteOptions, +} from '../create_unsecured_execute_function'; + +export interface UnsecuredActionsClientOpts { + unsecuredActionsClientAccessRegistry: UnsecuredActionsClientAccessRegistry; + internalSavedObjectsRepository: ISavedObjectsRepository; + executionEnqueuer: BulkUnsecuredExecutionEnqueuer; +} + +export class UnsecuredActionsClient { + private readonly unsecuredActionsClientAccessRegistry: UnsecuredActionsClientAccessRegistry; + private readonly internalSavedObjectsRepository: ISavedObjectsRepository; + private readonly executionEnqueuer: BulkUnsecuredExecutionEnqueuer; + + constructor(params: UnsecuredActionsClientOpts) { + this.unsecuredActionsClientAccessRegistry = params.unsecuredActionsClientAccessRegistry; + this.executionEnqueuer = params.executionEnqueuer; + this.internalSavedObjectsRepository = params.internalSavedObjectsRepository; + } + + public async bulkEnqueueExecution( + requesterId: string, + actionsToExecute: ExecuteOptions[] + ): Promise { + // Check that requesterId is allowed + if (!this.unsecuredActionsClientAccessRegistry.has(requesterId)) { + throw new Error( + `${requesterId} feature is not registered for UnsecuredActionsClient access.` + ); + } + return this.executionEnqueuer(this.internalSavedObjectsRepository, actionsToExecute); + } +} diff --git a/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client_access_registry.ts b/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client_access_registry.ts new file mode 100644 index 0000000000000..39975a9b7fb25 --- /dev/null +++ b/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client_access_registry.ts @@ -0,0 +1,24 @@ +/* + * 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 class UnsecuredActionsClientAccessRegistry { + private readonly allowedFeatureIds: Map = new Map(); + + /** + * Returns if the access registry has the given feature id registered + */ + public has(id: string) { + return this.allowedFeatureIds.has(id); + } + + /** + * Registers feature id to the access registry + */ + public register(id: string) { + this.allowedFeatureIds.set(id, true); + } +} diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 7450dcb1a45d0..e437e9b7d0e65 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -219,6 +219,8 @@ export class AlertingPlugin { this.eventLogService = plugins.eventLog; plugins.eventLog.registerProviderActions(EVENT_LOG_PROVIDER, Object.values(EVENT_LOG_ACTIONS)); + plugins.actions.registerUnsecuredActionsClientAccess('alerting'); + const ruleTypeRegistry = new RuleTypeRegistry({ logger: this.logger, taskManager: plugins.taskManager, @@ -462,6 +464,21 @@ export class AlertingPlugin { scheduleAlertingHealthCheck(this.logger, this.config, plugins.taskManager); scheduleApiKeyInvalidatorTask(this.telemetryLogger, this.config, plugins.taskManager); + plugins.actions.getUnsecuredActionsClient().then((unsecuredActionsClient) => { + unsecuredActionsClient.bulkEnqueueExecution('alerting', [ + { + id: 'gmail', + params: { + to: ['xxxxxx'], + subject: 'hello from Kibana!', + message: 'does this work??', + }, + spaceId: 'default', + apiKey: null, + executionId: 'abc', + }, + ]); + }); return { listTypes: ruleTypeRegistry!.list.bind(this.ruleTypeRegistry!), getAlertingAuthorizationWithRequest, From 2fe5ce579c72935b0c2a57228292407e3e5c9050 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Thu, 13 Oct 2022 11:46:08 -0400 Subject: [PATCH 02/16] Removing isESOCanEncrypt check --- .../create_unsecured_execute_function.ts | 30 +++++++++---------- .../actions/server/lib/action_executor.ts | 15 ++++++---- x-pack/plugins/actions/server/plugin.ts | 7 ----- x-pack/plugins/alerting/server/plugin.ts | 2 -- 4 files changed, 23 insertions(+), 31 deletions(-) diff --git a/x-pack/plugins/actions/server/create_unsecured_execute_function.ts b/x-pack/plugins/actions/server/create_unsecured_execute_function.ts index b21f1a04ccb60..acc4d01e7c1a2 100644 --- a/x-pack/plugins/actions/server/create_unsecured_execute_function.ts +++ b/x-pack/plugins/actions/server/create_unsecured_execute_function.ts @@ -17,17 +17,15 @@ import { ExecuteOptions as ActionExecutorOptions } from './lib/action_executor'; import { extractSavedObjectReferences, isSavedObjectExecutionSource } from './lib'; import { RelatedSavedObjects } from './lib/related_saved_objects'; +const ALLOWED_CONNECTOR_TYPE_IDS = ['.email', '.slack']; interface CreateBulkUnsecuredExecuteFunctionOptions { taskManager: TaskManagerStartContract; - isESOCanEncrypt: boolean; connectorTypeRegistry: ConnectorTypeRegistryContract; preconfiguredConnectors: PreconfiguredConnector[]; } export interface ExecuteOptions extends Pick { id: string; - spaceId: string; - apiKey: string | null; executionId: string; consumer?: string; relatedSavedObjects?: RelatedSavedObjects; @@ -49,21 +47,13 @@ export type BulkUnsecuredExecutionEnqueuer = ( export function createBulkUnsecuredExecutionEnqueuerFunction({ taskManager, connectorTypeRegistry, - isESOCanEncrypt, preconfiguredConnectors, }: CreateBulkUnsecuredExecuteFunctionOptions): BulkUnsecuredExecutionEnqueuer { return async function execute( internalSavedObjectsRepository: ISavedObjectsRepository, actionsToExecute: ExecuteOptions[] ) { - if (!isESOCanEncrypt) { - throw new Error( - `Unable to execute actions because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.` - ); - } - const connectorTypeIds: Record = {}; - const spaceIds: Record = {}; const connectorIds = [...new Set(actionsToExecute.map((action) => action.id))]; const notPreconfiguredConnectors = connectorIds.filter( @@ -72,7 +62,11 @@ export function createBulkUnsecuredExecutionEnqueuerFunction({ ); if (notPreconfiguredConnectors.length > 0) { - // log warning or throw error? + throw new Error( + `${notPreconfiguredConnectors.join( + ',' + )} are not preconfigured connectors and can't be scheduled for unsecured actions execution` + ); } const connectors: PreconfiguredConnector[] = compact( @@ -87,6 +81,12 @@ export function createBulkUnsecuredExecutionEnqueuerFunction({ connectorTypeRegistry.ensureActionTypeEnabled(actionTypeId); } + if (!ALLOWED_CONNECTOR_TYPE_IDS.includes(actionTypeId)) { + throw new Error( + `${actionTypeId} actions cannot be scheduled for unsecured actions execution` + ); + } + connectorTypeIds[id] = actionTypeId; }); @@ -110,14 +110,12 @@ export function createBulkUnsecuredExecutionEnqueuerFunction({ taskReferences.push(...references); } - spaceIds[actionToExecute.id] = actionToExecute.spaceId; - return { type: ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, attributes: { actionId: actionToExecute.id, params: actionToExecute.params, - apiKey: actionToExecute.apiKey, + apiKey: null, executionId: actionToExecute.executionId, consumer: actionToExecute.consumer, relatedSavedObjects: relatedSavedObjectWithRefs, @@ -135,7 +133,7 @@ export function createBulkUnsecuredExecutionEnqueuerFunction({ return { taskType: `actions:${connectorTypeIds[actionId]}`, params: { - spaceId: spaceIds[actionId], + spaceId: 'default', actionTaskParamsId: so.id, }, state: {}, diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index 00a3980833ef2..f778b8aabfd41 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -105,12 +105,6 @@ export class ActionExecutor { throw new Error('ActionExecutor not initialized'); } - if (!this.isESOCanEncrypt) { - throw new Error( - `Unable to execute action because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.` - ); - } - return withSpan( { name: `execute_action`, @@ -137,6 +131,7 @@ export class ActionExecutor { const actionInfo = await getActionInfoInternal( await getActionsClientWithRequest(request, source), + this.isESOCanEncrypt, encryptedSavedObjectsClient, preconfiguredActions, actionId, @@ -319,6 +314,7 @@ export class ActionExecutor { if (!this.actionInfo || this.actionInfo.actionId !== actionId) { this.actionInfo = await getActionInfoInternal( await getActionsClientWithRequest(request, source), + this.isESOCanEncrypt, encryptedSavedObjectsClient, preconfiguredActions, actionId, @@ -370,6 +366,7 @@ interface ActionInfo { async function getActionInfoInternal( actionsClient: PublicMethodsOf, + isESOCanEncrypt: boolean, encryptedSavedObjectsClient: EncryptedSavedObjectsClient, preconfiguredActions: PreConfiguredAction[], actionId: string, @@ -389,6 +386,12 @@ async function getActionInfoInternal( }; } + if (!isESOCanEncrypt) { + throw new Error( + `Unable to execute action because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.` + ); + } + // if not pre-configured action, should be a saved object // ensure user can read the action before processing const { actionTypeId, config, name } = await actionsClient.get({ id: actionId }); diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 319bd9c0ffce6..e5eb5f8dcaa8d 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -465,12 +465,6 @@ export class ActionsPlugin implements Plugin { - if (isESOCanEncrypt !== true) { - throw new Error( - `Unable to create unsecured actions client because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.` - ); - } - const internalSavedObjectsRepository = core.savedObjects.createInternalRepository([ ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, ]); @@ -481,7 +475,6 @@ export class ActionsPlugin implements Plugin Date: Thu, 13 Oct 2022 12:11:29 -0400 Subject: [PATCH 03/16] Only getting actions client when needed in executor --- .../actions/server/lib/action_executor.ts | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index f778b8aabfd41..f230920cfeea0 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -130,12 +130,14 @@ export class ActionExecutor { const namespace = spaceId && spaceId !== 'default' ? { namespace: spaceId } : {}; const actionInfo = await getActionInfoInternal( - await getActionsClientWithRequest(request, source), + getActionsClientWithRequest, + request, this.isESOCanEncrypt, encryptedSavedObjectsClient, preconfiguredActions, actionId, - namespace.namespace + namespace.namespace, + source ); const { actionTypeId, name, config, secrets } = actionInfo; @@ -313,12 +315,14 @@ export class ActionExecutor { const namespace = spaceId && spaceId !== 'default' ? { namespace: spaceId } : {}; if (!this.actionInfo || this.actionInfo.actionId !== actionId) { this.actionInfo = await getActionInfoInternal( - await getActionsClientWithRequest(request, source), + getActionsClientWithRequest, + request, this.isESOCanEncrypt, encryptedSavedObjectsClient, preconfiguredActions, actionId, - namespace.namespace + namespace.namespace, + source ); } const task = taskInfo @@ -364,13 +368,18 @@ interface ActionInfo { actionId: string; } -async function getActionInfoInternal( - actionsClient: PublicMethodsOf, +async function getActionInfoInternal( + getActionsClientWithRequest: ( + request: KibanaRequest, + authorizationContext?: ActionExecutionSource + ) => Promise>, + request: KibanaRequest, isESOCanEncrypt: boolean, encryptedSavedObjectsClient: EncryptedSavedObjectsClient, preconfiguredActions: PreConfiguredAction[], actionId: string, - namespace: string | undefined + namespace: string | undefined, + source?: ActionExecutionSource ): Promise { // check to see if it's a pre-configured action first const pcAction = preconfiguredActions.find( @@ -392,6 +401,8 @@ async function getActionInfoInternal( ); } + const actionsClient = await getActionsClientWithRequest(request, source); + // if not pre-configured action, should be a saved object // ensure user can read the action before processing const { actionTypeId, config, name } = await actionsClient.get({ id: actionId }); From 00c611d9c15ef68a905b6a47ec0b6d0193df1263 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Fri, 14 Oct 2022 09:57:41 -0400 Subject: [PATCH 04/16] Changing to feature id allowlist. Adding unit tests --- .../create_unsecured_execute_function.test.ts | 499 ++++++++++++++++++ .../create_unsecured_execute_function.ts | 3 - .../server/lib/action_executor.test.ts | 241 ++++++++- x-pack/plugins/actions/server/mocks.ts | 2 + x-pack/plugins/actions/server/plugin.ts | 9 - .../unsecured_actions_client.mock.ts | 25 + .../unsecured_actions_client.test.ts | 64 +++ .../unsecured_actions_client.ts | 12 +- ...nsecured_actions_client_access_registry.ts | 24 - 9 files changed, 836 insertions(+), 43 deletions(-) create mode 100644 x-pack/plugins/actions/server/create_unsecured_execute_function.test.ts create mode 100644 x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.mock.ts create mode 100644 x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.test.ts delete mode 100644 x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client_access_registry.ts diff --git a/x-pack/plugins/actions/server/create_unsecured_execute_function.test.ts b/x-pack/plugins/actions/server/create_unsecured_execute_function.test.ts new file mode 100644 index 0000000000000..d4d52b3d1236b --- /dev/null +++ b/x-pack/plugins/actions/server/create_unsecured_execute_function.test.ts @@ -0,0 +1,499 @@ +/* + * 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 uuid from 'uuid'; +import { savedObjectsRepositoryMock } from '@kbn/core/server/mocks'; +import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; +import { createBulkUnsecuredExecutionEnqueuerFunction } from './create_unsecured_execute_function'; +import { actionTypeRegistryMock } from './action_type_registry.mock'; +import { asSavedObjectExecutionSource } from './lib/action_execution_source'; + +const mockTaskManager = taskManagerMock.createStart(); +const internalSavedObjectsRepository = savedObjectsRepositoryMock.create(); + +beforeEach(() => jest.resetAllMocks()); + +describe('bulkExecute()', () => { + test('schedules the actions with all given parameters with a preconfigured connector', async () => { + const executeFn = createBulkUnsecuredExecutionEnqueuerFunction({ + taskManager: mockTaskManager, + connectorTypeRegistry: actionTypeRegistryMock.create(), + preconfiguredConnectors: [ + { + id: '123', + actionTypeId: '.email', + config: {}, + isPreconfigured: true, + isDeprecated: false, + name: 'x', + secrets: {}, + }, + ], + }); + + internalSavedObjectsRepository.bulkCreate.mockResolvedValueOnce({ + saved_objects: [ + { + id: '234', + type: 'action_task_params', + attributes: { + actionId: '123', + }, + references: [], + }, + { + id: '345', + type: 'action_task_params', + attributes: { + actionId: '123', + }, + references: [], + }, + ], + }); + await executeFn(internalSavedObjectsRepository, [ + { + id: '123', + params: { baz: false }, + executionId: '123abc', + }, + { + id: '123', + params: { baz: true }, + executionId: '234xyz', + }, + ]); + expect(mockTaskManager.bulkSchedule).toHaveBeenCalledTimes(1); + expect(mockTaskManager.bulkSchedule.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "params": Object { + "actionTaskParamsId": "234", + "spaceId": "default", + }, + "scope": Array [ + "actions", + ], + "state": Object {}, + "taskType": "actions:.email", + }, + Object { + "params": Object { + "actionTaskParamsId": "345", + "spaceId": "default", + }, + "scope": Array [ + "actions", + ], + "state": Object {}, + "taskType": "actions:.email", + }, + ], + ] + `); + + expect(internalSavedObjectsRepository.bulkCreate).toHaveBeenCalledWith([ + { + type: 'action_task_params', + attributes: { + actionId: '123', + params: { baz: false }, + executionId: '123abc', + apiKey: null, + }, + references: [], + }, + { + type: 'action_task_params', + attributes: { + actionId: '123', + params: { baz: true }, + executionId: '234xyz', + apiKey: null, + }, + references: [], + }, + ]); + }); + + test('schedules the actions with all given parameters with a preconfigured connector and source specified', async () => { + const sourceUuid = uuid.v4(); + const source = { type: 'alert', id: sourceUuid }; + const executeFn = createBulkUnsecuredExecutionEnqueuerFunction({ + taskManager: mockTaskManager, + connectorTypeRegistry: actionTypeRegistryMock.create(), + preconfiguredConnectors: [ + { + id: '123', + actionTypeId: '.email', + config: {}, + isPreconfigured: true, + isDeprecated: false, + name: 'x', + secrets: {}, + }, + ], + }); + + internalSavedObjectsRepository.bulkCreate.mockResolvedValueOnce({ + saved_objects: [ + { + id: '234', + type: 'action_task_params', + attributes: { + actionId: '123', + }, + references: [ + { + id: sourceUuid, + name: 'source', + type: 'alert', + }, + ], + }, + { + id: '345', + type: 'action_task_params', + attributes: { + actionId: '123', + }, + references: [], + }, + ], + }); + await executeFn(internalSavedObjectsRepository, [ + { + id: '123', + params: { baz: false }, + executionId: '123abc', + source: asSavedObjectExecutionSource(source), + }, + { + id: '123', + params: { baz: true }, + executionId: '234xyz', + }, + ]); + expect(mockTaskManager.bulkSchedule).toHaveBeenCalledTimes(1); + expect(mockTaskManager.bulkSchedule.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "params": Object { + "actionTaskParamsId": "234", + "spaceId": "default", + }, + "scope": Array [ + "actions", + ], + "state": Object {}, + "taskType": "actions:.email", + }, + Object { + "params": Object { + "actionTaskParamsId": "345", + "spaceId": "default", + }, + "scope": Array [ + "actions", + ], + "state": Object {}, + "taskType": "actions:.email", + }, + ], + ] + `); + + expect(internalSavedObjectsRepository.bulkCreate).toHaveBeenCalledWith([ + { + type: 'action_task_params', + attributes: { + actionId: '123', + params: { baz: false }, + executionId: '123abc', + apiKey: null, + }, + references: [ + { + id: sourceUuid, + name: 'source', + type: 'alert', + }, + ], + }, + { + type: 'action_task_params', + attributes: { + actionId: '123', + params: { baz: true }, + executionId: '234xyz', + apiKey: null, + }, + references: [], + }, + ]); + }); + + test('schedules the actions with all given parameters with a preconfigured connector and relatedSavedObjects specified', async () => { + const sourceUuid = uuid.v4(); + const source = { type: 'alert', id: sourceUuid }; + const executeFn = createBulkUnsecuredExecutionEnqueuerFunction({ + taskManager: mockTaskManager, + connectorTypeRegistry: actionTypeRegistryMock.create(), + preconfiguredConnectors: [ + { + id: '123', + actionTypeId: '.email', + config: {}, + isPreconfigured: true, + isDeprecated: false, + name: 'x', + secrets: {}, + }, + ], + }); + + internalSavedObjectsRepository.bulkCreate.mockResolvedValueOnce({ + saved_objects: [ + { + id: '234', + type: 'action_task_params', + attributes: { + actionId: '123', + }, + references: [ + { + id: sourceUuid, + name: 'source', + type: 'alert', + }, + ], + }, + { + id: '345', + type: 'action_task_params', + attributes: { + actionId: '123', + }, + references: [ + { + id: 'some-id', + name: 'related_some-type_0', + type: 'some-type', + }, + ], + }, + ], + }); + await executeFn(internalSavedObjectsRepository, [ + { + id: '123', + params: { baz: false }, + executionId: '123abc', + source: asSavedObjectExecutionSource(source), + }, + { + id: '123', + params: { baz: true }, + executionId: '234xyz', + relatedSavedObjects: [ + { + id: 'some-id', + namespace: 'some-namespace', + type: 'some-type', + }, + ], + }, + ]); + expect(mockTaskManager.bulkSchedule).toHaveBeenCalledTimes(1); + expect(mockTaskManager.bulkSchedule.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "params": Object { + "actionTaskParamsId": "234", + "spaceId": "default", + }, + "scope": Array [ + "actions", + ], + "state": Object {}, + "taskType": "actions:.email", + }, + Object { + "params": Object { + "actionTaskParamsId": "345", + "spaceId": "default", + }, + "scope": Array [ + "actions", + ], + "state": Object {}, + "taskType": "actions:.email", + }, + ], + ] + `); + + expect(internalSavedObjectsRepository.bulkCreate).toHaveBeenCalledWith([ + { + type: 'action_task_params', + attributes: { + actionId: '123', + params: { baz: false }, + executionId: '123abc', + apiKey: null, + }, + references: [ + { + id: sourceUuid, + name: 'source', + type: 'alert', + }, + ], + }, + { + type: 'action_task_params', + attributes: { + actionId: '123', + params: { baz: true }, + executionId: '234xyz', + apiKey: null, + relatedSavedObjects: [ + { + id: 'related_some-type_0', + namespace: 'some-namespace', + type: 'some-type', + }, + ], + }, + references: [ + { + id: 'some-id', + name: 'related_some-type_0', + type: 'some-type', + }, + ], + }, + ]); + }); + + test('throws when scheduling action using non preconfigured connector', async () => { + const executeFn = createBulkUnsecuredExecutionEnqueuerFunction({ + taskManager: mockTaskManager, + connectorTypeRegistry: actionTypeRegistryMock.create(), + preconfiguredConnectors: [ + { + id: '123', + actionTypeId: '.email', + config: {}, + isPreconfigured: true, + isDeprecated: false, + name: 'x', + secrets: {}, + }, + ], + }); + await expect( + executeFn(internalSavedObjectsRepository, [ + { + id: '123', + params: { baz: false }, + executionId: '123abc', + }, + { + id: 'not-preconfigured', + params: { baz: true }, + executionId: '234xyz', + }, + ]) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"not-preconfigured are not preconfigured connectors and can't be scheduled for unsecured actions execution"` + ); + }); + + test('throws when connector type is not enabled', async () => { + const mockedConnectorTypeRegistry = actionTypeRegistryMock.create(); + const executeFn = createBulkUnsecuredExecutionEnqueuerFunction({ + taskManager: mockTaskManager, + connectorTypeRegistry: mockedConnectorTypeRegistry, + preconfiguredConnectors: [ + { + id: '123', + actionTypeId: '.email', + config: {}, + isPreconfigured: true, + isDeprecated: false, + name: 'x', + secrets: {}, + }, + ], + }); + mockedConnectorTypeRegistry.ensureActionTypeEnabled.mockImplementation(() => { + throw new Error('Fail'); + }); + + await expect( + executeFn(internalSavedObjectsRepository, [ + { + id: '123', + params: { baz: false }, + executionId: '123abc', + }, + { + id: '123', + params: { baz: true }, + executionId: '234xyz', + }, + ]) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Fail"`); + }); + + test('throws when scheduling action using non allow-listed preconfigured connector', async () => { + const executeFn = createBulkUnsecuredExecutionEnqueuerFunction({ + taskManager: mockTaskManager, + connectorTypeRegistry: actionTypeRegistryMock.create(), + preconfiguredConnectors: [ + { + id: '123', + actionTypeId: '.email', + config: {}, + isPreconfigured: true, + isDeprecated: false, + name: 'x', + secrets: {}, + }, + { + id: '456', + actionTypeId: 'not-in-allowlist', + config: {}, + isPreconfigured: true, + isDeprecated: false, + name: 'x', + secrets: {}, + }, + ], + }); + await expect( + executeFn(internalSavedObjectsRepository, [ + { + id: '123', + params: { baz: false }, + executionId: '123abc', + }, + { + id: '456', + params: { baz: true }, + executionId: '234xyz', + }, + ]) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"not-in-allowlist actions cannot be scheduled for unsecured actions execution"` + ); + }); +}); diff --git a/x-pack/plugins/actions/server/create_unsecured_execute_function.ts b/x-pack/plugins/actions/server/create_unsecured_execute_function.ts index acc4d01e7c1a2..45c0eb5c60288 100644 --- a/x-pack/plugins/actions/server/create_unsecured_execute_function.ts +++ b/x-pack/plugins/actions/server/create_unsecured_execute_function.ts @@ -27,7 +27,6 @@ interface CreateBulkUnsecuredExecuteFunctionOptions { export interface ExecuteOptions extends Pick { id: string; executionId: string; - consumer?: string; relatedSavedObjects?: RelatedSavedObjects; } @@ -35,7 +34,6 @@ export interface ActionTaskParams extends Pick actionId: string; apiKey: string | null; executionId: string; - consumer?: string; relatedSavedObjects?: RelatedSavedObjects; } @@ -117,7 +115,6 @@ export function createBulkUnsecuredExecutionEnqueuerFunction({ params: actionToExecute.params, apiKey: null, executionId: actionToExecute.executionId, - consumer: actionToExecute.consumer, relatedSavedObjects: relatedSavedObjectWithRefs, }, references: taskReferences, diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts index 54405b03e7c02..e0ef54601fc82 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.test.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts @@ -45,7 +45,21 @@ actionExecutor.initialize({ actionTypeRegistry, encryptedSavedObjectsClient, eventLogger, - preconfiguredActions: [], + preconfiguredActions: [ + { + id: 'preconfigured', + name: 'Preconfigured', + actionTypeId: 'test', + config: { + bar: 'preconfigured', + }, + secrets: { + apiKey: 'abc', + }, + isPreconfigured: true, + isDeprecated: false, + }, + ], }); beforeEach(() => { @@ -179,6 +193,106 @@ test('successfully executes', async () => { `); }); +test('successfully executes with preconfigured connector', async () => { + const actionType: jest.Mocked = { + id: 'test', + name: 'Test', + minimumLicenseRequired: 'basic', + supportedFeatureIds: ['alerting'], + executor: jest.fn(), + }; + + actionTypeRegistry.get.mockReturnValueOnce(actionType); + await actionExecutor.execute({ ...executeParams, actionId: 'preconfigured' }); + + expect(actionsClient.get).not.toHaveBeenCalled(); + expect(encryptedSavedObjectsClient.getDecryptedAsInternalUser).not.toHaveBeenCalled(); + + expect(actionTypeRegistry.get).toHaveBeenCalledWith('test'); + expect(actionTypeRegistry.isActionExecutable).toHaveBeenCalledWith('preconfigured', 'test', { + notifyUsage: true, + }); + + expect(actionType.executor).toHaveBeenCalledWith({ + actionId: 'preconfigured', + services: expect.anything(), + config: { + bar: 'preconfigured', + }, + secrets: { + apiKey: 'abc', + }, + params: { foo: true }, + }); + + expect(loggerMock.debug).toBeCalledWith('executing action test:preconfigured: Preconfigured'); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "execute-start", + "kind": "action", + }, + "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "123abc", + }, + }, + }, + "saved_objects": Array [ + Object { + "id": "preconfigured", + "namespace": "some-namespace", + "rel": "primary", + "type": "action", + "type_id": "test", + }, + ], + "space_ids": Array [ + "some-namespace", + ], + }, + "message": "action started: test:preconfigured: Preconfigured", + }, + ], + Array [ + Object { + "event": Object { + "action": "execute", + "kind": "action", + "outcome": "success", + }, + "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "123abc", + }, + }, + }, + "saved_objects": Array [ + Object { + "id": "preconfigured", + "namespace": "some-namespace", + "rel": "primary", + "type": "action", + "type_id": "test", + }, + ], + "space_ids": Array [ + "some-namespace", + ], + }, + "message": "action executed: test:preconfigured: Preconfigured", + }, + ], + ] + `); +}); + test('successfully executes as a task', async () => { const actionType: jest.Mocked = { id: 'test', @@ -504,6 +618,131 @@ test('throws an error when passing isESOCanEncrypt with value of false', async ( ); }); +test('should not throw error if action is preconfigured and isESOCanEncrypt is false', async () => { + const customActionExecutor = new ActionExecutor({ isESOCanEncrypt: false }); + customActionExecutor.initialize({ + logger: loggingSystemMock.create().get(), + spaces: spacesMock, + getActionsClientWithRequest, + getServices: () => services, + actionTypeRegistry, + encryptedSavedObjectsClient, + eventLogger: eventLoggerMock.create(), + preconfiguredActions: [ + { + id: 'preconfigured', + name: 'Preconfigured', + actionTypeId: 'test', + config: { + bar: 'preconfigured', + }, + secrets: { + apiKey: 'abc', + }, + isPreconfigured: true, + isDeprecated: false, + }, + ], + }); + const actionType: jest.Mocked = { + id: 'test', + name: 'Test', + minimumLicenseRequired: 'basic', + supportedFeatureIds: ['alerting'], + executor: jest.fn(), + }; + + actionTypeRegistry.get.mockReturnValueOnce(actionType); + await actionExecutor.execute({ ...executeParams, actionId: 'preconfigured' }); + + expect(actionsClient.get).not.toHaveBeenCalled(); + expect(encryptedSavedObjectsClient.getDecryptedAsInternalUser).not.toHaveBeenCalled(); + + expect(actionTypeRegistry.get).toHaveBeenCalledWith('test'); + expect(actionTypeRegistry.isActionExecutable).toHaveBeenCalledWith('preconfigured', 'test', { + notifyUsage: true, + }); + + expect(actionType.executor).toHaveBeenCalledWith({ + actionId: 'preconfigured', + services: expect.anything(), + config: { + bar: 'preconfigured', + }, + secrets: { + apiKey: 'abc', + }, + params: { foo: true }, + }); + + expect(loggerMock.debug).toBeCalledWith('executing action test:preconfigured: Preconfigured'); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "execute-start", + "kind": "action", + }, + "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "123abc", + }, + }, + }, + "saved_objects": Array [ + Object { + "id": "preconfigured", + "namespace": "some-namespace", + "rel": "primary", + "type": "action", + "type_id": "test", + }, + ], + "space_ids": Array [ + "some-namespace", + ], + }, + "message": "action started: test:preconfigured: Preconfigured", + }, + ], + Array [ + Object { + "event": Object { + "action": "execute", + "kind": "action", + "outcome": "success", + }, + "kibana": Object { + "alert": Object { + "rule": Object { + "execution": Object { + "uuid": "123abc", + }, + }, + }, + "saved_objects": Array [ + Object { + "id": "preconfigured", + "namespace": "some-namespace", + "rel": "primary", + "type": "action", + "type_id": "test", + }, + ], + "space_ids": Array [ + "some-namespace", + ], + }, + "message": "action executed: test:preconfigured: Preconfigured", + }, + ], + ] + `); +}); + test('does not log warning when alert executor succeeds', async () => { const executorMock = setupActionExecutorMock(); executorMock.mockResolvedValue({ diff --git a/x-pack/plugins/actions/server/mocks.ts b/x-pack/plugins/actions/server/mocks.ts index 3b8155818452f..4d5846de9528f 100644 --- a/x-pack/plugins/actions/server/mocks.ts +++ b/x-pack/plugins/actions/server/mocks.ts @@ -17,6 +17,7 @@ import { PluginSetupContract, PluginStartContract, renderActionParameterTemplate import { Services } from './types'; import { actionsAuthorizationMock } from './authorization/actions_authorization.mock'; import { ConnectorTokenClient } from './lib/connector_token_client'; +import { unsecuredActionsClientMock } from './unsecured_actions_client/unsecured_actions_client.mock'; export { actionsAuthorizationMock }; export { actionsClientMock }; const logger = loggingSystemMock.create().get() as jest.Mocked; @@ -38,6 +39,7 @@ const createStartMock = () => { isActionTypeEnabled: jest.fn(), isActionExecutable: jest.fn(), getActionsClientWithRequest: jest.fn().mockResolvedValue(actionsClientMock.create()), + getUnsecuredActionsClient: jest.fn().mockResolvedValue(unsecuredActionsClientMock.create()), getActionsAuthorizationWithRequest: jest .fn() .mockReturnValue(actionsAuthorizationMock.create()), diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index e5eb5f8dcaa8d..a5183148b14a7 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -101,7 +101,6 @@ import { createSubActionConnectorFramework } from './sub_action_framework'; import { IServiceAbstract, SubActionConnectorType } from './sub_action_framework/types'; import { SubActionConnector } from './sub_action_framework/sub_action_connector'; import { CaseConnector } from './sub_action_framework/case'; -import { UnsecuredActionsClientAccessRegistry } from './unsecured_actions_client/unsecured_actions_client_access_registry'; import { UnsecuredActionsClient } from './unsecured_actions_client/unsecured_actions_client'; import { createBulkUnsecuredExecutionEnqueuerFunction } from './create_unsecured_execute_function'; @@ -120,7 +119,6 @@ export interface PluginSetupContract { >( connector: SubActionConnectorType ): void; - registerUnsecuredActionsClientAccess(featureId: string): void; isPreconfiguredConnector(connectorId: string): boolean; getSubActionConnectorClass: () => IServiceAbstract; getCaseConnectorClass: () => IServiceAbstract; @@ -194,7 +192,6 @@ export class ActionsPlugin implements Plugin { subActionFramework.registerConnector(connector); }, - registerUnsecuredActionsClientAccess: (featureId: string) => { - this.unsecuredActionsClientAccessRegistry?.register(featureId); - }, isPreconfiguredConnector: (connectorId: string): boolean => { return !!this.preconfiguredActions.find( (preconfigured) => preconfigured.id === connectorId @@ -471,7 +463,6 @@ export class ActionsPlugin implements Plugin; +export type UnsecuredActionsClientMock = jest.Mocked; + +const createUnsecuredActionsClientMock = () => { + const mocked: UnsecuredActionsClientMock = { + bulkEnqueueExecution: jest.fn(), + }; + return mocked; +}; + +export const unsecuredActionsClientMock: { + create: () => UnsecuredActionsClientMock; +} = { + create: createUnsecuredActionsClientMock, +}; diff --git a/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.test.ts b/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.test.ts new file mode 100644 index 0000000000000..c863e943b8dc0 --- /dev/null +++ b/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.test.ts @@ -0,0 +1,64 @@ +/* + * 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 { UnsecuredActionsClient } from './unsecured_actions_client'; +import { savedObjectsRepositoryMock } from '@kbn/core/server/mocks'; + +const internalSavedObjectsRepository = savedObjectsRepositoryMock.create(); +const executionEnqueuer = jest.fn(); + +let unsecuredActionsClient: UnsecuredActionsClient; + +beforeEach(() => { + jest.resetAllMocks(); + unsecuredActionsClient = new UnsecuredActionsClient({ + internalSavedObjectsRepository, + executionEnqueuer, + }); +}); + +describe('bulkEnqueueExecution()', () => { + test('throws error when enqueuing execution with not allowed requester id', async () => { + const opts = [ + { + id: 'preconfigured1', + params: {}, + executionId: '123abc', + }, + { + id: 'preconfigured2', + params: {}, + executionId: '456def', + }, + ]; + await expect( + unsecuredActionsClient.bulkEnqueueExecution('badId', opts) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"\\"badId\\" feature is not allow-listed for UnsecuredActionsClient access."` + ); + }); + + test('calls the executionEnqueuer with the appropriate parameters', async () => { + const opts = [ + { + id: 'preconfigured1', + params: {}, + executionId: '123abc', + }, + { + id: 'preconfigured2', + params: {}, + executionId: '456def', + }, + ]; + await expect( + unsecuredActionsClient.bulkEnqueueExecution('notifications', opts) + ).resolves.toMatchInlineSnapshot(`undefined`); + + expect(executionEnqueuer).toHaveBeenCalledWith(internalSavedObjectsRepository, opts); + }); +}); diff --git a/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.ts b/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.ts index 7ff0c3d72c8e7..3ec5d15910952 100644 --- a/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.ts +++ b/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.ts @@ -6,25 +6,25 @@ */ import { ISavedObjectsRepository } from '@kbn/core/server'; -import { UnsecuredActionsClientAccessRegistry } from './unsecured_actions_client_access_registry'; import { BulkUnsecuredExecutionEnqueuer, ExecuteOptions, } from '../create_unsecured_execute_function'; +// allowlist for features wanting access to the unsecured actions client +// which allows actions to be enqueued for execution without a user request +const ALLOWED_REQUESTER_IDS = ['notifications', 'alerting']; + export interface UnsecuredActionsClientOpts { - unsecuredActionsClientAccessRegistry: UnsecuredActionsClientAccessRegistry; internalSavedObjectsRepository: ISavedObjectsRepository; executionEnqueuer: BulkUnsecuredExecutionEnqueuer; } export class UnsecuredActionsClient { - private readonly unsecuredActionsClientAccessRegistry: UnsecuredActionsClientAccessRegistry; private readonly internalSavedObjectsRepository: ISavedObjectsRepository; private readonly executionEnqueuer: BulkUnsecuredExecutionEnqueuer; constructor(params: UnsecuredActionsClientOpts) { - this.unsecuredActionsClientAccessRegistry = params.unsecuredActionsClientAccessRegistry; this.executionEnqueuer = params.executionEnqueuer; this.internalSavedObjectsRepository = params.internalSavedObjectsRepository; } @@ -34,9 +34,9 @@ export class UnsecuredActionsClient { actionsToExecute: ExecuteOptions[] ): Promise { // Check that requesterId is allowed - if (!this.unsecuredActionsClientAccessRegistry.has(requesterId)) { + if (!ALLOWED_REQUESTER_IDS.includes(requesterId)) { throw new Error( - `${requesterId} feature is not registered for UnsecuredActionsClient access.` + `"${requesterId}" feature is not allow-listed for UnsecuredActionsClient access.` ); } return this.executionEnqueuer(this.internalSavedObjectsRepository, actionsToExecute); diff --git a/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client_access_registry.ts b/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client_access_registry.ts deleted file mode 100644 index 39975a9b7fb25..0000000000000 --- a/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client_access_registry.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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 class UnsecuredActionsClientAccessRegistry { - private readonly allowedFeatureIds: Map = new Map(); - - /** - * Returns if the access registry has the given feature id registered - */ - public has(id: string) { - return this.allowedFeatureIds.has(id); - } - - /** - * Registers feature id to the access registry - */ - public register(id: string) { - this.allowedFeatureIds.set(id, true); - } -} From baf051ab58db193e51174eed215ce5e7371feb3a Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Fri, 14 Oct 2022 10:21:16 -0400 Subject: [PATCH 05/16] Removing execution id --- .../actions/server/create_execute_function.ts | 2 +- .../create_unsecured_execute_function.test.ts | 12 --------- .../create_unsecured_execute_function.ts | 5 +--- .../create_action_event_log_record_object.ts | 26 +++++++++---------- x-pack/plugins/alerting/server/plugin.ts | 3 --- 5 files changed, 15 insertions(+), 33 deletions(-) diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts index 8f4c4eee61e84..b37f618e64461 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -34,7 +34,7 @@ export interface ExecuteOptions extends Pick { +interface ActionTaskParams extends Pick { actionId: string; apiKey: string | null; executionId: string; diff --git a/x-pack/plugins/actions/server/create_unsecured_execute_function.test.ts b/x-pack/plugins/actions/server/create_unsecured_execute_function.test.ts index d4d52b3d1236b..591755b7df40b 100644 --- a/x-pack/plugins/actions/server/create_unsecured_execute_function.test.ts +++ b/x-pack/plugins/actions/server/create_unsecured_execute_function.test.ts @@ -59,12 +59,10 @@ describe('bulkExecute()', () => { { id: '123', params: { baz: false }, - executionId: '123abc', }, { id: '123', params: { baz: true }, - executionId: '234xyz', }, ]); expect(mockTaskManager.bulkSchedule).toHaveBeenCalledTimes(1); @@ -170,13 +168,11 @@ describe('bulkExecute()', () => { { id: '123', params: { baz: false }, - executionId: '123abc', source: asSavedObjectExecutionSource(source), }, { id: '123', params: { baz: true }, - executionId: '234xyz', }, ]); expect(mockTaskManager.bulkSchedule).toHaveBeenCalledTimes(1); @@ -294,13 +290,11 @@ describe('bulkExecute()', () => { { id: '123', params: { baz: false }, - executionId: '123abc', source: asSavedObjectExecutionSource(source), }, { id: '123', params: { baz: true }, - executionId: '234xyz', relatedSavedObjects: [ { id: 'some-id', @@ -404,12 +398,10 @@ describe('bulkExecute()', () => { { id: '123', params: { baz: false }, - executionId: '123abc', }, { id: 'not-preconfigured', params: { baz: true }, - executionId: '234xyz', }, ]) ).rejects.toThrowErrorMatchingInlineSnapshot( @@ -443,12 +435,10 @@ describe('bulkExecute()', () => { { id: '123', params: { baz: false }, - executionId: '123abc', }, { id: '123', params: { baz: true }, - executionId: '234xyz', }, ]) ).rejects.toThrowErrorMatchingInlineSnapshot(`"Fail"`); @@ -484,12 +474,10 @@ describe('bulkExecute()', () => { { id: '123', params: { baz: false }, - executionId: '123abc', }, { id: '456', params: { baz: true }, - executionId: '234xyz', }, ]) ).rejects.toThrowErrorMatchingInlineSnapshot( diff --git a/x-pack/plugins/actions/server/create_unsecured_execute_function.ts b/x-pack/plugins/actions/server/create_unsecured_execute_function.ts index 45c0eb5c60288..0f044cd78e06e 100644 --- a/x-pack/plugins/actions/server/create_unsecured_execute_function.ts +++ b/x-pack/plugins/actions/server/create_unsecured_execute_function.ts @@ -26,14 +26,12 @@ interface CreateBulkUnsecuredExecuteFunctionOptions { export interface ExecuteOptions extends Pick { id: string; - executionId: string; relatedSavedObjects?: RelatedSavedObjects; } -export interface ActionTaskParams extends Pick { +interface ActionTaskParams extends Pick { actionId: string; apiKey: string | null; - executionId: string; relatedSavedObjects?: RelatedSavedObjects; } @@ -114,7 +112,6 @@ export function createBulkUnsecuredExecutionEnqueuerFunction({ actionId: actionToExecute.id, params: actionToExecute.params, apiKey: null, - executionId: actionToExecute.executionId, relatedSavedObjects: relatedSavedObjectWithRefs, }, references: taskReferences, diff --git a/x-pack/plugins/actions/server/lib/create_action_event_log_record_object.ts b/x-pack/plugins/actions/server/lib/create_action_event_log_record_object.ts index 5d556398dc668..2632ead26a477 100644 --- a/x-pack/plugins/actions/server/lib/create_action_event_log_record_object.ts +++ b/x-pack/plugins/actions/server/lib/create_action_event_log_record_object.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { set } from 'lodash'; +import { isEmpty, set } from 'lodash'; import { IEvent, SAVED_OBJECT_REL_PRIMARY } from '@kbn/event-log-plugin/server'; import { RelatedSavedObjects } from './related_saved_objects'; @@ -38,6 +38,17 @@ export function createActionEventLogRecordObject(params: CreateActionEventLogRec const { action, message, task, namespace, executionId, spaceId, consumer, relatedSavedObjects } = params; + const kibanaAlertRule = { + ...(consumer ? { consumer } : {}), + ...(executionId + ? { + execution: { + uuid: executionId, + }, + } + : {}), + }; + const event: Event = { ...(params.timestamp ? { '@timestamp': params.timestamp } : {}), event: { @@ -45,18 +56,7 @@ export function createActionEventLogRecordObject(params: CreateActionEventLogRec kind: 'action', }, kibana: { - alert: { - rule: { - ...(consumer ? { consumer } : {}), - ...(executionId - ? { - execution: { - uuid: executionId, - }, - } - : {}), - }, - }, + ...(!isEmpty(kibanaAlertRule) ? { alert: { rule: kibanaAlertRule } } : {}), saved_objects: params.savedObjects.map((so) => ({ ...(so.relation ? { rel: so.relation } : {}), type: so.type, diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index a303814dd261c..d2c58dddc879f 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -219,8 +219,6 @@ export class AlertingPlugin { this.eventLogService = plugins.eventLog; plugins.eventLog.registerProviderActions(EVENT_LOG_PROVIDER, Object.values(EVENT_LOG_ACTIONS)); - plugins.actions.registerUnsecuredActionsClientAccess('alerting'); - const ruleTypeRegistry = new RuleTypeRegistry({ logger: this.logger, taskManager: plugins.taskManager, @@ -473,7 +471,6 @@ export class AlertingPlugin { subject: 'hello from Kibana!', message: 'does this work??', }, - executionId: 'abc', }, ]); }); From 96b696f44746865796706b57e4b6bc791efd5316 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Fri, 14 Oct 2022 11:05:11 -0400 Subject: [PATCH 06/16] Cleanup --- ...ate_action_event_log_record_object.test.ts | 37 +++++++++++++++++++ .../unsecured_actions_client.ts | 2 +- x-pack/plugins/alerting/server/plugin.ts | 12 ------ 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/actions/server/lib/create_action_event_log_record_object.test.ts b/x-pack/plugins/actions/server/lib/create_action_event_log_record_object.test.ts index 72cbda1312b9a..69eca915cc721 100644 --- a/x-pack/plugins/actions/server/lib/create_action_event_log_record_object.test.ts +++ b/x-pack/plugins/actions/server/lib/create_action_event_log_record_object.test.ts @@ -109,6 +109,43 @@ describe('createActionEventLogRecordObject', () => { }); }); + test('created action event "execute" with no kibana.alert.rule fields', async () => { + expect( + createActionEventLogRecordObject({ + actionId: '1', + name: 'test name', + action: 'execute', + message: 'action execution start', + namespace: 'default', + savedObjects: [ + { + id: '2', + type: 'action', + typeId: '.email', + relation: 'primary', + }, + ], + }) + ).toStrictEqual({ + event: { + action: 'execute', + kind: 'action', + }, + kibana: { + saved_objects: [ + { + id: '2', + namespace: 'default', + rel: 'primary', + type: 'action', + type_id: '.email', + }, + ], + }, + message: 'action execution start', + }); + }); + test('created action event "execute-timeout"', async () => { expect( createActionEventLogRecordObject({ diff --git a/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.ts b/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.ts index 3ec5d15910952..45980a6412390 100644 --- a/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.ts +++ b/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.ts @@ -13,7 +13,7 @@ import { // allowlist for features wanting access to the unsecured actions client // which allows actions to be enqueued for execution without a user request -const ALLOWED_REQUESTER_IDS = ['notifications', 'alerting']; +const ALLOWED_REQUESTER_IDS = ['notifications']; export interface UnsecuredActionsClientOpts { internalSavedObjectsRepository: ISavedObjectsRepository; diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index d2c58dddc879f..7450dcb1a45d0 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -462,18 +462,6 @@ export class AlertingPlugin { scheduleAlertingHealthCheck(this.logger, this.config, plugins.taskManager); scheduleApiKeyInvalidatorTask(this.telemetryLogger, this.config, plugins.taskManager); - plugins.actions.getUnsecuredActionsClient().then((unsecuredActionsClient) => { - unsecuredActionsClient.bulkEnqueueExecution('alerting', [ - { - id: 'gmail', - params: { - to: ['xxxxxx'], - subject: 'hello from Kibana!', - message: 'does this work??', - }, - }, - ]); - }); return { listTypes: ruleTypeRegistry!.list.bind(this.ruleTypeRegistry!), getAlertingAuthorizationWithRequest, From 6cf7a3aa19b50e0865fdb810b63e6866f8b3a6c9 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Fri, 14 Oct 2022 11:59:22 -0400 Subject: [PATCH 07/16] Fixing unit tests --- .../server/create_unsecured_execute_function.test.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/x-pack/plugins/actions/server/create_unsecured_execute_function.test.ts b/x-pack/plugins/actions/server/create_unsecured_execute_function.test.ts index 591755b7df40b..ea8407b956276 100644 --- a/x-pack/plugins/actions/server/create_unsecured_execute_function.test.ts +++ b/x-pack/plugins/actions/server/create_unsecured_execute_function.test.ts @@ -101,7 +101,6 @@ describe('bulkExecute()', () => { attributes: { actionId: '123', params: { baz: false }, - executionId: '123abc', apiKey: null, }, references: [], @@ -111,7 +110,6 @@ describe('bulkExecute()', () => { attributes: { actionId: '123', params: { baz: true }, - executionId: '234xyz', apiKey: null, }, references: [], @@ -211,7 +209,6 @@ describe('bulkExecute()', () => { attributes: { actionId: '123', params: { baz: false }, - executionId: '123abc', apiKey: null, }, references: [ @@ -227,7 +224,6 @@ describe('bulkExecute()', () => { attributes: { actionId: '123', params: { baz: true }, - executionId: '234xyz', apiKey: null, }, references: [], @@ -340,7 +336,6 @@ describe('bulkExecute()', () => { attributes: { actionId: '123', params: { baz: false }, - executionId: '123abc', apiKey: null, }, references: [ @@ -356,7 +351,6 @@ describe('bulkExecute()', () => { attributes: { actionId: '123', params: { baz: true }, - executionId: '234xyz', apiKey: null, relatedSavedObjects: [ { From 8865a8090f6edf3d35fe46487451c113ac321fe5 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Mon, 17 Oct 2022 09:55:45 -0400 Subject: [PATCH 08/16] Removing slack from allowlist --- .../actions/server/create_unsecured_execute_function.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/actions/server/create_unsecured_execute_function.ts b/x-pack/plugins/actions/server/create_unsecured_execute_function.ts index 0f044cd78e06e..2bd291899ceef 100644 --- a/x-pack/plugins/actions/server/create_unsecured_execute_function.ts +++ b/x-pack/plugins/actions/server/create_unsecured_execute_function.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { compact } from 'lodash'; import { ISavedObjectsRepository, SavedObjectsBulkResponse } from '@kbn/core/server'; import { TaskManagerStartContract } from '@kbn/task-manager-plugin/server'; import { @@ -17,7 +16,7 @@ import { ExecuteOptions as ActionExecutorOptions } from './lib/action_executor'; import { extractSavedObjectReferences, isSavedObjectExecutionSource } from './lib'; import { RelatedSavedObjects } from './lib/related_saved_objects'; -const ALLOWED_CONNECTOR_TYPE_IDS = ['.email', '.slack']; +const ALLOWED_CONNECTOR_TYPE_IDS = ['.email']; interface CreateBulkUnsecuredExecuteFunctionOptions { taskManager: TaskManagerStartContract; connectorTypeRegistry: ConnectorTypeRegistryContract; @@ -65,11 +64,11 @@ export function createBulkUnsecuredExecutionEnqueuerFunction({ ); } - const connectors: PreconfiguredConnector[] = compact( - connectorIds.map((connectorId) => + const connectors: PreconfiguredConnector[] = connectorIds + .map((connectorId) => preconfiguredConnectors.find((pConnector) => pConnector.id === connectorId) ) - ); + .filter(Boolean) as PreconfiguredConnector[]; connectors.forEach((connector) => { const { id, actionTypeId } = connector; From 7c884aea7ce047dd97e1ea066c1fe27a95d7ef88 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Mon, 17 Oct 2022 10:58:04 -0400 Subject: [PATCH 09/16] Make getUnsecuredActionsClient synchronous --- x-pack/plugins/actions/server/plugin.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index a5183148b14a7..8248dee862394 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -140,7 +140,7 @@ export interface PluginStartContract { preconfiguredActions: PreConfiguredAction[]; - getUnsecuredActionsClient(): Promise>; + getUnsecuredActionsClient(): PublicMethodsOf; renderActionParameterTemplates( actionTypeId: string, @@ -456,7 +456,7 @@ export class ActionsPlugin implements Plugin { + const getUnsecuredActionsClient = () => { const internalSavedObjectsRepository = core.savedObjects.createInternalRepository([ ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, ]); From f4c18514c141bc54ce1f4bd3ad40b5ab16864687 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Mon, 17 Oct 2022 11:24:26 -0400 Subject: [PATCH 10/16] Add comment --- .../plugins/actions/server/create_unsecured_execute_function.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/actions/server/create_unsecured_execute_function.ts b/x-pack/plugins/actions/server/create_unsecured_execute_function.ts index 2bd291899ceef..ea98ee6e9f2c0 100644 --- a/x-pack/plugins/actions/server/create_unsecured_execute_function.ts +++ b/x-pack/plugins/actions/server/create_unsecured_execute_function.ts @@ -16,6 +16,8 @@ import { ExecuteOptions as ActionExecutorOptions } from './lib/action_executor'; import { extractSavedObjectReferences, isSavedObjectExecutionSource } from './lib'; import { RelatedSavedObjects } from './lib/related_saved_objects'; +// This allowlist should only contain connector types that don't require API keys for +// execution. const ALLOWED_CONNECTOR_TYPE_IDS = ['.email']; interface CreateBulkUnsecuredExecuteFunctionOptions { taskManager: TaskManagerStartContract; From 47f1ca99a61e7cc7a4338431094c514ec6b56d15 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Tue, 18 Oct 2022 10:55:34 -0400 Subject: [PATCH 11/16] Adding functional tests --- .../unsecured_actions_client.ts | 6 +- .../alerting_api_integration/common/config.ts | 12 ++ .../actions_simulators/server/plugin.ts | 32 ++- .../server/unsecured_actions_simulation.ts | 48 +++++ .../spaces_only/tests/actions/index.ts | 1 + .../actions/schedule_unsecured_action.ts | 191 ++++++++++++++++++ 6 files changed, 279 insertions(+), 11 deletions(-) create mode 100644 x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/unsecured_actions_simulation.ts create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/actions/schedule_unsecured_action.ts diff --git a/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.ts b/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.ts index 45980a6412390..beb5c9d648cdc 100644 --- a/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.ts +++ b/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.ts @@ -13,7 +13,11 @@ import { // allowlist for features wanting access to the unsecured actions client // which allows actions to be enqueued for execution without a user request -const ALLOWED_REQUESTER_IDS = ['notifications']; +const ALLOWED_REQUESTER_IDS = [ + 'notifications', + // For functional testing + 'functional_tester', +]; export interface UnsecuredActionsClientOpts { internalSavedObjectsRepository: ISavedObjectsRepository; diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 090e48f7a8a2d..d2831b61799f5 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -197,6 +197,18 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) ])}`, `--xpack.actions.preconfiguredAlertHistoryEsIndex=${preconfiguredAlertHistoryEsIndex}`, `--xpack.actions.preconfigured=${JSON.stringify({ + 'my-test-email': { + actionTypeId: '.email', + name: 'TestEmail#xyz', + config: { + from: 'me@test.com', + service: '__json', + }, + secrets: { + user: 'user', + password: 'password', + }, + }, 'my-slack1': { actionTypeId: '.slack', name: 'Slack#xyz', diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts index ae874d942e75b..ec5bad5ab3ae7 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts @@ -7,10 +7,13 @@ import http from 'http'; import https from 'https'; -import { Plugin, CoreSetup, IRouter } from '@kbn/core/server'; +import { Plugin, CoreSetup, CoreStart, IRouter } from '@kbn/core/server'; import { EncryptedSavedObjectsPluginStart } from '@kbn/encrypted-saved-objects-plugin/server'; import { PluginSetupContract as FeaturesPluginSetup } from '@kbn/features-plugin/server'; -import { PluginSetupContract as ActionsPluginSetupContract } from '@kbn/actions-plugin/server/plugin'; +import { + PluginSetupContract as ActionsPluginSetupContract, + PluginStartContract as ActionsPluginStartContract, +} from '@kbn/actions-plugin/server/plugin'; import { ActionType } from '@kbn/actions-plugin/server'; import { initPlugin as initPagerduty } from './pagerduty_simulation'; import { initPlugin as initSwimlane } from './swimlane_simulation'; @@ -22,6 +25,7 @@ import { initPlugin as initSlack } from './slack_simulation'; import { initPlugin as initWebhook } from './webhook_simulation'; import { initPlugin as initMSExchange } from './ms_exchage_server_simulation'; import { initPlugin as initXmatters } from './xmatters_simulation'; +import { initPlugin as initUnsecuredAction } from './unsecured_actions_simulation'; export const NAME = 'actions-FTS-external-service-simulators'; @@ -84,9 +88,12 @@ interface FixtureSetupDeps { interface FixtureStartDeps { encryptedSavedObjects: EncryptedSavedObjectsPluginStart; + actions: ActionsPluginStartContract; } export class FixturePlugin implements Plugin { + private router: IRouter; + public setup(core: CoreSetup, { features, actions }: FixtureSetupDeps) { // this action is specifically NOT enabled in ../../config.ts const notEnabledActionType: ActionType = { @@ -126,19 +133,24 @@ export class FixturePlugin implements Plugin, + res: KibanaResponseFactory + ): Promise> { + const { body } = req; + + try { + const unsecuredActionsClient = actionsStart.getUnsecuredActionsClient(); + const { requesterId, id, params } = body; + await unsecuredActionsClient.bulkEnqueueExecution(requesterId, [{ id, params }]); + + return res.ok({ body: { status: 'success' } }); + } catch (err) { + return res.ok({ body: { status: 'error', error: `${err}` } }); + } + } + ); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts index 866f13ed5294c..b4dbb42e8f993 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts @@ -28,6 +28,7 @@ export default function actionsTests({ loadTestFile, getService }: FtrProviderCo loadTestFile(require.resolve('./connector_types/stack/webhook')); loadTestFile(require.resolve('./connector_types/stack/preconfigured_alert_history_connector')); loadTestFile(require.resolve('./type_not_enabled')); + loadTestFile(require.resolve('./schedule_unsecured_action')); // note that this test will destroy existing spaces loadTestFile(require.resolve('./migrations')); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/schedule_unsecured_action.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/schedule_unsecured_action.ts new file mode 100644 index 0000000000000..9a5719b7fa700 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/schedule_unsecured_action.ts @@ -0,0 +1,191 @@ +/* + * 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 expect from '@kbn/expect'; +import type { SearchTotalHits } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { Spaces } from '../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { getUrlPrefix, ObjectRemover } from '../../../common/lib'; + +// eslint-disable-next-line import/no-default-export +export default function createUnsecuredActionTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + const es = getService('es'); + const retry = getService('retry'); + + describe('schedule unsecured action', () => { + const objectRemover = new ObjectRemover(supertest); + + // need to wait for kibanaServer to settle ... + before(() => { + kibanaServer.resolveUrl(`/api/sample_unsecured_action`); + }); + + after(() => objectRemover.removeAll()); + + it('should successfully schedule email action', async () => { + const testStart = new Date().toISOString(); + const { body: result } = await supertest + .post(`/api/sample_unsecured_action`) + .set('kbn-xsrf', 'xxx') + .send({ + requesterId: 'functional_tester', + id: 'my-test-email', + params: { + to: ['you@test.com'], + subject: 'hello from Kibana!', + message: 'does this work??', + }, + }) + .expect(200); + expect(result.status).to.eql('success'); + + await retry.try(async () => { + const searchResult = await es.search({ + index: '.kibana-event-log*', + body: { + query: { + bool: { + filter: [ + { + term: { + 'event.provider': { + value: 'actions', + }, + }, + }, + { + term: { + 'event.action': 'execute', + }, + }, + { + range: { + '@timestamp': { + gte: testStart, + }, + }, + }, + { + nested: { + path: 'kibana.saved_objects', + query: { + bool: { + filter: [ + { + term: { + 'kibana.saved_objects.id': { + value: 'my-test-email', + }, + }, + }, + { + term: { + 'kibana.saved_objects.type': 'action', + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }, + }); + expect((searchResult.hits.total as SearchTotalHits).value).to.eql(1); + + const hit = searchResult.hits.hits[0]; + // @ts-expect-error _source: unknown + expect(hit?._source?.event?.outcome).to.eql('success'); + // @ts-expect-error _source: unknown + expect(hit?._source?.message).to.eql( + `action executed: .email:my-test-email: TestEmail#xyz` + ); + }); + }); + + it('should not allow scheduling email action from unallowed requester', async () => { + const { body: result } = await supertest + .post(`/api/sample_unsecured_action`) + .set('kbn-xsrf', 'xxx') + .send({ + requesterId: 'not_allowed', + id: 'my-test-email', + params: { + to: ['you@test.com'], + subject: 'hello from Kibana!', + message: 'does this work??', + }, + }) + .expect(200); + expect(result.status).to.eql('error'); + expect(result.error).to.eql( + `Error: "not_allowed" feature is not allow-listed for UnsecuredActionsClient access.` + ); + }); + + it('should not allow scheduling action from unallowed connector types', async () => { + const { body: result } = await supertest + .post(`/api/sample_unsecured_action`) + .set('kbn-xsrf', 'xxx') + .send({ + requesterId: 'functional_tester', + id: 'my-slack1', + params: { + message: 'does this work??', + }, + }) + .expect(200); + expect(result.status).to.eql('error'); + expect(result.error).to.eql( + `Error: .slack actions cannot be scheduled for unsecured actions execution` + ); + }); + + it('should not allow scheduling action from non preconfigured connectors', async () => { + const response = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My email action', + connector_type_id: '.email', + config: { + from: 'me@test.com', + service: '__json', + }, + secrets: { + user: 'user', + password: 'password', + }, + }); + expect(response.status).to.eql(200); + + const connectorId = response.body.id; + objectRemover.add(Spaces.space1.id, connectorId, 'action', 'actions'); + const { body: result } = await supertest + .post(`/api/sample_unsecured_action`) + .set('kbn-xsrf', 'xxx') + .send({ + requesterId: 'functional_tester', + id: connectorId, + params: { + to: ['you@test.com'], + subject: 'hello from Kibana!', + message: 'does this work??', + }, + }) + .expect(200); + expect(result.status).to.eql('error'); + expect(result.error).to.eql( + `Error: ${connectorId} are not preconfigured connectors and can't be scheduled for unsecured actions execution` + ); + }); + }); +} From c87baee6375a3d900ae4dc24010c213d3a1ba007 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Tue, 18 Oct 2022 11:28:38 -0400 Subject: [PATCH 12/16] Fixing types --- .../fixtures/plugins/actions_simulators/server/plugin.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts index ec5bad5ab3ae7..40090208207ac 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts @@ -92,7 +92,7 @@ interface FixtureStartDeps { } export class FixturePlugin implements Plugin { - private router: IRouter; + private router?: IRouter; public setup(core: CoreSetup, { features, actions }: FixtureSetupDeps) { // this action is specifically NOT enabled in ../../config.ts @@ -150,7 +150,7 @@ export class FixturePlugin implements Plugin Date: Tue, 18 Oct 2022 13:23:10 -0400 Subject: [PATCH 13/16] Fixing tests --- .../server/lib/action_executor.test.ts | 2 ++ .../group2/tests/actions/get_all.ts | 24 +++++++++++++++++++ .../alerting_and_actions_telemetry.ts | 2 +- .../spaces_only/tests/actions/get_all.ts | 24 +++++++++++++++++++ 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts index 73a5b66c40c54..0bcb9cf527548 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.test.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts @@ -227,6 +227,7 @@ test('successfully executes with preconfigured connector', async () => { apiKey: 'abc', }, params: { foo: true }, + logger: loggerMock, }); expect(loggerMock.debug).toBeCalledWith('executing action test:preconfigured: Preconfigured'); @@ -678,6 +679,7 @@ test('should not throw error if action is preconfigured and isESOCanEncrypt is f apiKey: 'abc', }, params: { foo: true }, + logger: loggerMock, }); expect(loggerMock.debug).toBeCalledWith('executing action test:preconfigured: Preconfigured'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get_all.ts index 69f618c804eb1..d4bfe3cbdd704 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get_all.ts @@ -127,6 +127,14 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { name: 'Test:_Preconfigured_Index_Record', referenced_by_count: 0, }, + { + id: 'my-test-email', + is_preconfigured: true, + is_deprecated: false, + connector_type_id: '.email', + name: 'TestEmail#xyz', + referenced_by_count: 0, + }, ]); break; default: @@ -262,6 +270,14 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { name: 'Test:_Preconfigured_Index_Record', referenced_by_count: 0, }, + { + id: 'my-test-email', + is_preconfigured: true, + is_deprecated: false, + connector_type_id: '.email', + name: 'TestEmail#xyz', + referenced_by_count: 0, + }, ]); break; default: @@ -361,6 +377,14 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { name: 'Test:_Preconfigured_Index_Record', referenced_by_count: 0, }, + { + id: 'my-test-email', + is_preconfigured: true, + is_deprecated: false, + connector_type_id: '.email', + name: 'TestEmail#xyz', + referenced_by_count: 0, + }, ]); break; default: diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/alerting_and_actions_telemetry.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/alerting_and_actions_telemetry.ts index b4cb36ab59d85..707f15534e663 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/alerting_and_actions_telemetry.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/alerting_and_actions_telemetry.ts @@ -571,7 +571,7 @@ export default function createAlertingAndActionsTelemetryTests({ getService }: F expect(taskState).not.to.be(undefined); actionsTelemetry = JSON.parse(taskState!); expect(actionsTelemetry.runs).to.equal(2); - expect(actionsTelemetry.count_total).to.equal(19); + expect(actionsTelemetry.count_total).to.equal(20); }); // request alerting telemetry task to run diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts index 0632f48ed6e8d..7846c9512867e 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts @@ -115,6 +115,14 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { name: 'Test:_Preconfigured_Index_Record', referenced_by_count: 0, }, + { + id: 'my-test-email', + is_preconfigured: true, + is_deprecated: false, + connector_type_id: '.email', + name: 'TestEmail#xyz', + referenced_by_count: 0, + }, ]); }); @@ -202,6 +210,14 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { name: 'Test:_Preconfigured_Index_Record', referenced_by_count: 0, }, + { + id: 'my-test-email', + is_preconfigured: true, + is_deprecated: false, + connector_type_id: '.email', + name: 'TestEmail#xyz', + referenced_by_count: 0, + }, ]); }); @@ -302,6 +318,14 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { name: 'Test:_Preconfigured_Index_Record', referencedByCount: 0, }, + { + id: 'my-test-email', + isPreconfigured: true, + isDeprecated: false, + actionTypeId: '.email', + name: 'TestEmail#xyz', + referencedByCount: 0, + }, ]); }); }); From 026646a630e613d17e00da254fba6875ad2335de Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Wed, 19 Oct 2022 10:39:05 -0400 Subject: [PATCH 14/16] Removing unnecessary Promise.all --- .../actions/server/create_execute_function.ts | 69 +++++++++---------- .../create_unsecured_execute_function.ts | 59 ++++++++-------- 2 files changed, 61 insertions(+), 67 deletions(-) diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts index b37f618e64461..30923134235fe 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -176,43 +176,40 @@ export function createBulkExecutionEnqueuerFunction({ connectorIsPreconfigured[id] = isPreconfigured; }); - const actions = await Promise.all( - actionsToExecute.map(async (actionToExecute) => { - // Get saved object references from action ID and relatedSavedObjects - const { references, relatedSavedObjectWithRefs } = extractSavedObjectReferences( - actionToExecute.id, - connectorIsPreconfigured[actionToExecute.id], - actionToExecute.relatedSavedObjects - ); - const executionSourceReference = executionSourceAsSavedObjectReferences( - actionToExecute.source - ); - - const taskReferences = []; - if (executionSourceReference.references) { - taskReferences.push(...executionSourceReference.references); - } - if (references) { - taskReferences.push(...references); - } - - spaceIds[actionToExecute.id] = actionToExecute.spaceId; - - return { - type: ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, - attributes: { - actionId: actionToExecute.id, - params: actionToExecute.params, - apiKey: actionToExecute.apiKey, - executionId: actionToExecute.executionId, - consumer: actionToExecute.consumer, - relatedSavedObjects: relatedSavedObjectWithRefs, - }, - references: taskReferences, - }; - }) - ); + const actions = actionsToExecute.map((actionToExecute) => { + // Get saved object references from action ID and relatedSavedObjects + const { references, relatedSavedObjectWithRefs } = extractSavedObjectReferences( + actionToExecute.id, + connectorIsPreconfigured[actionToExecute.id], + actionToExecute.relatedSavedObjects + ); + const executionSourceReference = executionSourceAsSavedObjectReferences( + actionToExecute.source + ); + + const taskReferences = []; + if (executionSourceReference.references) { + taskReferences.push(...executionSourceReference.references); + } + if (references) { + taskReferences.push(...references); + } + spaceIds[actionToExecute.id] = actionToExecute.spaceId; + + return { + type: ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, + attributes: { + actionId: actionToExecute.id, + params: actionToExecute.params, + apiKey: actionToExecute.apiKey, + executionId: actionToExecute.executionId, + consumer: actionToExecute.consumer, + relatedSavedObjects: relatedSavedObjectWithRefs, + }, + references: taskReferences, + }; + }); const actionTaskParamsRecords: SavedObjectsBulkResponse = await unsecuredSavedObjectsClient.bulkCreate(actions); const taskInstances = actionTaskParamsRecords.saved_objects.map((so) => { diff --git a/x-pack/plugins/actions/server/create_unsecured_execute_function.ts b/x-pack/plugins/actions/server/create_unsecured_execute_function.ts index ea98ee6e9f2c0..231a298de3db2 100644 --- a/x-pack/plugins/actions/server/create_unsecured_execute_function.ts +++ b/x-pack/plugins/actions/server/create_unsecured_execute_function.ts @@ -87,39 +87,36 @@ export function createBulkUnsecuredExecutionEnqueuerFunction({ connectorTypeIds[id] = actionTypeId; }); - const actions = await Promise.all( - actionsToExecute.map(async (actionToExecute) => { - // Get saved object references from action ID and relatedSavedObjects - const { references, relatedSavedObjectWithRefs } = extractSavedObjectReferences( - actionToExecute.id, - true, - actionToExecute.relatedSavedObjects - ); - const executionSourceReference = executionSourceAsSavedObjectReferences( - actionToExecute.source - ); + const actions = actionsToExecute.map((actionToExecute) => { + // Get saved object references from action ID and relatedSavedObjects + const { references, relatedSavedObjectWithRefs } = extractSavedObjectReferences( + actionToExecute.id, + true, + actionToExecute.relatedSavedObjects + ); + const executionSourceReference = executionSourceAsSavedObjectReferences( + actionToExecute.source + ); - const taskReferences = []; - if (executionSourceReference.references) { - taskReferences.push(...executionSourceReference.references); - } - if (references) { - taskReferences.push(...references); - } - - return { - type: ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, - attributes: { - actionId: actionToExecute.id, - params: actionToExecute.params, - apiKey: null, - relatedSavedObjects: relatedSavedObjectWithRefs, - }, - references: taskReferences, - }; - }) - ); + const taskReferences = []; + if (executionSourceReference.references) { + taskReferences.push(...executionSourceReference.references); + } + if (references) { + taskReferences.push(...references); + } + return { + type: ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, + attributes: { + actionId: actionToExecute.id, + params: actionToExecute.params, + apiKey: null, + relatedSavedObjects: relatedSavedObjectWithRefs, + }, + references: taskReferences, + }; + }); const actionTaskParamsRecords: SavedObjectsBulkResponse = await internalSavedObjectsRepository.bulkCreate(actions); From 802dfa01fba0d4cbd2ec14a0114432a8b45021a3 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Wed, 19 Oct 2022 11:44:42 -0400 Subject: [PATCH 15/16] Cleanup --- .../actions/server/create_execute_function.ts | 12 ++++-------- .../server/create_unsecured_execute_function.ts | 10 ++++------ 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts index 30923134235fe..19447bf8e79e5 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -16,7 +16,6 @@ import { import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from './constants/saved_objects'; import { ExecuteOptions as ActionExecutorOptions } from './lib/action_executor'; import { extractSavedObjectReferences, isSavedObjectExecutionSource } from './lib'; -import { RelatedSavedObjects } from './lib/related_saved_objects'; interface CreateExecuteFunctionOptions { taskManager: TaskManagerStartContract; @@ -25,21 +24,18 @@ interface CreateExecuteFunctionOptions { preconfiguredActions: PreConfiguredAction[]; } -export interface ExecuteOptions extends Pick { +export interface ExecuteOptions + extends Pick { id: string; spaceId: string; apiKey: string | null; executionId: string; - consumer?: string; - relatedSavedObjects?: RelatedSavedObjects; } -interface ActionTaskParams extends Pick { - actionId: string; +interface ActionTaskParams + extends Pick { apiKey: string | null; executionId: string; - consumer?: string; - relatedSavedObjects?: RelatedSavedObjects; } export interface GetConnectorsResult { diff --git a/x-pack/plugins/actions/server/create_unsecured_execute_function.ts b/x-pack/plugins/actions/server/create_unsecured_execute_function.ts index 231a298de3db2..4670601ecff83 100644 --- a/x-pack/plugins/actions/server/create_unsecured_execute_function.ts +++ b/x-pack/plugins/actions/server/create_unsecured_execute_function.ts @@ -14,7 +14,6 @@ import { import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from './constants/saved_objects'; import { ExecuteOptions as ActionExecutorOptions } from './lib/action_executor'; import { extractSavedObjectReferences, isSavedObjectExecutionSource } from './lib'; -import { RelatedSavedObjects } from './lib/related_saved_objects'; // This allowlist should only contain connector types that don't require API keys for // execution. @@ -25,15 +24,14 @@ interface CreateBulkUnsecuredExecuteFunctionOptions { preconfiguredConnectors: PreconfiguredConnector[]; } -export interface ExecuteOptions extends Pick { +export interface ExecuteOptions + extends Pick { id: string; - relatedSavedObjects?: RelatedSavedObjects; } -interface ActionTaskParams extends Pick { - actionId: string; +interface ActionTaskParams + extends Pick { apiKey: string | null; - relatedSavedObjects?: RelatedSavedObjects; } export type BulkUnsecuredExecutionEnqueuer = ( From 943ac4929a65910189ef5625730f3deeeabd367c Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Wed, 26 Oct 2022 08:32:41 -0400 Subject: [PATCH 16/16] PR feedback --- x-pack/plugins/actions/server/index.ts | 2 ++ x-pack/plugins/actions/server/plugin.ts | 7 +++-- .../unsecured_actions_client.mock.ts | 10 ++----- .../unsecured_actions_client.ts | 4 +++ .../actions_simulators/server/plugin.ts | 29 ++++++++----------- .../server/unsecured_actions_simulation.ts | 8 +++-- 6 files changed, 31 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/actions/server/index.ts b/x-pack/plugins/actions/server/index.ts index 1c7a66978ffb3..2713ee17463e4 100644 --- a/x-pack/plugins/actions/server/index.ts +++ b/x-pack/plugins/actions/server/index.ts @@ -12,6 +12,8 @@ import { configSchema, ActionsConfig, CustomHostSettings } from './config'; import { ActionsClient as ActionsClientClass } from './actions_client'; import { ActionsAuthorization as ActionsAuthorizationClass } from './authorization/actions_authorization'; +export type { IUnsecuredActionsClient } from './unsecured_actions_client/unsecured_actions_client'; +export { UnsecuredActionsClient } from './unsecured_actions_client/unsecured_actions_client'; export type ActionsClient = PublicMethodsOf; export type ActionsAuthorization = PublicMethodsOf; diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 934b6b45f80ac..e24ac8247bfd8 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -101,7 +101,10 @@ import { createSubActionConnectorFramework } from './sub_action_framework'; import { IServiceAbstract, SubActionConnectorType } from './sub_action_framework/types'; import { SubActionConnector } from './sub_action_framework/sub_action_connector'; import { CaseConnector } from './sub_action_framework/case'; -import { UnsecuredActionsClient } from './unsecured_actions_client/unsecured_actions_client'; +import { + IUnsecuredActionsClient, + UnsecuredActionsClient, +} from './unsecured_actions_client/unsecured_actions_client'; import { createBulkUnsecuredExecutionEnqueuerFunction } from './create_unsecured_execute_function'; export interface PluginSetupContract { @@ -140,7 +143,7 @@ export interface PluginStartContract { preconfiguredActions: PreConfiguredAction[]; - getUnsecuredActionsClient(): PublicMethodsOf; + getUnsecuredActionsClient(): IUnsecuredActionsClient; renderActionParameterTemplates( actionTypeId: string, diff --git a/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.mock.ts b/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.mock.ts index 62dd1a8e199b2..eb8d4de53e7f3 100644 --- a/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.mock.ts +++ b/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.mock.ts @@ -5,11 +5,9 @@ * 2.0. */ -import type { PublicMethodsOf } from '@kbn/utility-types'; -import { UnsecuredActionsClient } from './unsecured_actions_client'; +import { IUnsecuredActionsClient } from './unsecured_actions_client'; -type UnsecuredActionsClientContract = PublicMethodsOf; -export type UnsecuredActionsClientMock = jest.Mocked; +export type UnsecuredActionsClientMock = jest.Mocked; const createUnsecuredActionsClientMock = () => { const mocked: UnsecuredActionsClientMock = { @@ -18,8 +16,6 @@ const createUnsecuredActionsClientMock = () => { return mocked; }; -export const unsecuredActionsClientMock: { - create: () => UnsecuredActionsClientMock; -} = { +export const unsecuredActionsClientMock = { create: createUnsecuredActionsClientMock, }; diff --git a/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.ts b/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.ts index beb5c9d648cdc..333490389013a 100644 --- a/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.ts +++ b/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.ts @@ -24,6 +24,10 @@ export interface UnsecuredActionsClientOpts { executionEnqueuer: BulkUnsecuredExecutionEnqueuer; } +export interface IUnsecuredActionsClient { + bulkEnqueueExecution: (requesterId: string, actionsToExecute: ExecuteOptions[]) => Promise; +} + export class UnsecuredActionsClient { private readonly internalSavedObjectsRepository: ISavedObjectsRepository; private readonly executionEnqueuer: BulkUnsecuredExecutionEnqueuer; diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts index 40090208207ac..316d1916b4af2 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts @@ -7,7 +7,7 @@ import http from 'http'; import https from 'https'; -import { Plugin, CoreSetup, CoreStart, IRouter } from '@kbn/core/server'; +import { Plugin, CoreSetup } from '@kbn/core/server'; import { EncryptedSavedObjectsPluginStart } from '@kbn/encrypted-saved-objects-plugin/server'; import { PluginSetupContract as FeaturesPluginSetup } from '@kbn/features-plugin/server'; import { @@ -86,14 +86,12 @@ interface FixtureSetupDeps { features: FeaturesPluginSetup; } -interface FixtureStartDeps { +export interface FixtureStartDeps { encryptedSavedObjects: EncryptedSavedObjectsPluginStart; actions: ActionsPluginStartContract; } export class FixturePlugin implements Plugin { - private router?: IRouter; - public setup(core: CoreSetup, { features, actions }: FixtureSetupDeps) { // this action is specifically NOT enabled in ../../config.ts const notEnabledActionType: ActionType = { @@ -133,24 +131,21 @@ export class FixturePlugin implements Plugin) { router.post( { path: `/api/sample_unsecured_action`, @@ -32,10 +33,11 @@ export function initPlugin(router: IRouter, actionsStart: ActionsPluginStartCont req: KibanaRequest, res: KibanaResponseFactory ): Promise> { + const [_, { actions }] = await coreSetup.getStartServices(); const { body } = req; try { - const unsecuredActionsClient = actionsStart.getUnsecuredActionsClient(); + const unsecuredActionsClient = actions.getUnsecuredActionsClient(); const { requesterId, id, params } = body; await unsecuredActionsClient.bulkEnqueueExecution(requesterId, [{ id, params }]);