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,