diff --git a/x-pack/legacy/plugins/actions/README.md b/x-pack/legacy/plugins/actions/README.md index 793792c3e202b..ef39034905b24 100644 --- a/x-pack/legacy/plugins/actions/README.md +++ b/x-pack/legacy/plugins/actions/README.md @@ -51,7 +51,7 @@ This module provides a Utilities for interacting with the configuration. ### Methods -**server.plugins.actions.registerType(options)** +**server.plugins.actions.setup.registerType(options)** The following table describes the properties of the `options` object. diff --git a/x-pack/legacy/plugins/actions/index.ts b/x-pack/legacy/plugins/actions/index.ts index d9e93d5cb4b45..7c4dd9f73c11f 100644 --- a/x-pack/legacy/plugins/actions/index.ts +++ b/x-pack/legacy/plugins/actions/index.ts @@ -9,7 +9,14 @@ import { Root } from 'joi'; import mappings from './mappings.json'; import { init } from './server'; -export { ActionsPlugin, ActionsClient, ActionType, ActionTypeExecutorOptions } from './server'; +export { + ActionsPlugin, + ActionsClient, + ActionType, + ActionTypeExecutorOptions, + PluginSetupContract, + PluginStartContract, +} from './server'; export function actions(kibana: any) { return new kibana.Plugin({ diff --git a/x-pack/legacy/plugins/actions/server/action_type_registry.test.ts b/x-pack/legacy/plugins/actions/server/action_type_registry.test.ts index 61fa59f9813ae..b4d73cc4759d7 100644 --- a/x-pack/legacy/plugins/actions/server/action_type_registry.test.ts +++ b/x-pack/legacy/plugins/actions/server/action_type_registry.test.ts @@ -4,33 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('./lib/get_create_task_runner_function', () => ({ - getCreateTaskRunnerFunction: jest.fn(), -})); - import { taskManagerMock } from '../../task_manager/task_manager.mock'; -import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/plugin.mock'; import { ActionTypeRegistry } from './action_type_registry'; import { ExecutorType } from './types'; -import { SavedObjectsClientMock } from '../../../../../src/core/server/mocks'; -import { ExecutorError } from './lib'; +import { ActionExecutor, ExecutorError, TaskRunnerFactory } from './lib'; const mockTaskManager = taskManagerMock.create(); - -function getServices() { - return { - log: jest.fn(), - callCluster: jest.fn(), - savedObjectsClient: SavedObjectsClientMock.create(), - }; -} const actionTypeRegistryParams = { - getServices, - isSecurityEnabled: true, taskManager: mockTaskManager, - encryptedSavedObjectsPlugin: encryptedSavedObjectsMock.create(), - spaceIdToNamespace: jest.fn().mockReturnValue(undefined), - getBasePath: jest.fn().mockReturnValue(undefined), + taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor()), }; beforeEach(() => jest.resetAllMocks()); @@ -41,9 +23,6 @@ const executor: ExecutorType = async options => { describe('register()', () => { test('able to register action types', () => { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const { getCreateTaskRunnerFunction } = require('./lib/get_create_task_runner_function'); - getCreateTaskRunnerFunction.mockReturnValueOnce(jest.fn()); const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); actionTypeRegistry.register({ id: 'my-action-type', @@ -56,7 +35,7 @@ describe('register()', () => { Array [ Object { "actions:my-action-type": Object { - "createTaskRunner": [MockFunction], + "createTaskRunner": [Function], "getRetry": [Function], "maxAttempts": 1, "title": "My action type", @@ -65,14 +44,6 @@ describe('register()', () => { }, ] `); - expect(getCreateTaskRunnerFunction).toHaveBeenCalledWith({ - actionTypeRegistry, - isSecurityEnabled: true, - encryptedSavedObjectsPlugin: actionTypeRegistryParams.encryptedSavedObjectsPlugin, - getServices: actionTypeRegistryParams.getServices, - getBasePath: actionTypeRegistryParams.getBasePath, - spaceIdToNamespace: actionTypeRegistryParams.spaceIdToNamespace, - }); }); test('throws error if action type already registered', () => { diff --git a/x-pack/legacy/plugins/actions/server/action_type_registry.ts b/x-pack/legacy/plugins/actions/server/action_type_registry.ts index 586f5d657001f..6007851f87084 100644 --- a/x-pack/legacy/plugins/actions/server/action_type_registry.ts +++ b/x-pack/legacy/plugins/actions/server/action_type_registry.ts @@ -6,47 +6,24 @@ import Boom from 'boom'; import { i18n } from '@kbn/i18n'; -import { TaskManager, TaskRunCreatorFunction } from '../../task_manager'; -import { getCreateTaskRunnerFunction, ExecutorError } from './lib'; -import { EncryptedSavedObjectsPlugin } from '../../encrypted_saved_objects'; -import { - ActionType, - GetBasePathFunction, - GetServicesFunction, - SpaceIdToNamespaceFunction, -} from './types'; +import { TaskManagerSetupContract } from './shim'; +import { RunContext } from '../../task_manager'; +import { ExecutorError, TaskRunnerFactory } from './lib'; +import { ActionType } from './types'; interface ConstructorOptions { - isSecurityEnabled: boolean; - taskManager: TaskManager; - getServices: GetServicesFunction; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin; - spaceIdToNamespace: SpaceIdToNamespaceFunction; - getBasePath: GetBasePathFunction; + taskManager: TaskManagerSetupContract; + taskRunnerFactory: TaskRunnerFactory; } export class ActionTypeRegistry { - private readonly taskRunCreatorFunction: TaskRunCreatorFunction; - private readonly taskManager: TaskManager; + private readonly taskManager: TaskManagerSetupContract; private readonly actionTypes: Map = new Map(); + private readonly taskRunnerFactory: TaskRunnerFactory; - constructor({ - getServices, - taskManager, - encryptedSavedObjectsPlugin, - spaceIdToNamespace, - getBasePath, - isSecurityEnabled, - }: ConstructorOptions) { + constructor({ taskManager, taskRunnerFactory }: ConstructorOptions) { this.taskManager = taskManager; - this.taskRunCreatorFunction = getCreateTaskRunnerFunction({ - isSecurityEnabled, - getServices, - actionTypeRegistry: this, - encryptedSavedObjectsPlugin, - spaceIdToNamespace, - getBasePath, - }); + this.taskRunnerFactory = taskRunnerFactory; } /** @@ -86,7 +63,7 @@ export class ActionTypeRegistry { // Don't retry other kinds of errors return false; }, - createTaskRunner: this.taskRunCreatorFunction, + createTaskRunner: (context: RunContext) => this.taskRunnerFactory.create(context), }, }); } diff --git a/x-pack/legacy/plugins/actions/server/actions_client.test.ts b/x-pack/legacy/plugins/actions/server/actions_client.test.ts index 6c1d3b2df6d47..b582d9f2a1b0d 100644 --- a/x-pack/legacy/plugins/actions/server/actions_client.test.ts +++ b/x-pack/legacy/plugins/actions/server/actions_client.test.ts @@ -9,31 +9,17 @@ import { schema } from '@kbn/config-schema'; import { ActionTypeRegistry } from './action_type_registry'; import { ActionsClient } from './actions_client'; import { ExecutorType } from './types'; +import { ActionExecutor, TaskRunnerFactory } from './lib'; import { taskManagerMock } from '../../task_manager/task_manager.mock'; import { SavedObjectsClientMock } from '../../../../../src/core/server/mocks'; -import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/plugin.mock'; const savedObjectsClient = SavedObjectsClientMock.create(); const mockTaskManager = taskManagerMock.create(); -const mockEncryptedSavedObjectsPlugin = encryptedSavedObjectsMock.create(); - -function getServices() { - return { - log: jest.fn(), - callCluster: jest.fn(), - savedObjectsClient: SavedObjectsClientMock.create(), - }; -} - const actionTypeRegistryParams = { - getServices, - isSecurityEnabled: true, taskManager: mockTaskManager, - encryptedSavedObjectsPlugin: mockEncryptedSavedObjectsPlugin, - spaceIdToNamespace: jest.fn().mockReturnValue(undefined), - getBasePath: jest.fn().mockReturnValue(undefined), + taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor()), }; const executor: ExecutorType = async options => { diff --git a/x-pack/legacy/plugins/actions/server/actions_config.test.ts b/x-pack/legacy/plugins/actions/server/actions_config.test.ts index 2fef6bfc539cc..7b4176fb69dbf 100644 --- a/x-pack/legacy/plugins/actions/server/actions_config.test.ts +++ b/x-pack/legacy/plugins/actions/server/actions_config.test.ts @@ -4,15 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - ActionsKibanaConfig, - getActionsConfigurationUtilities, - WhitelistedHosts, -} from './actions_config'; +import { ActionsConfigType } from './types'; +import { getActionsConfigurationUtilities, WhitelistedHosts } from './actions_config'; describe('ensureWhitelistedUri', () => { test('returns true when "any" hostnames are allowed', () => { - const config: ActionsKibanaConfig = { + const config: ActionsConfigType = { enabled: false, whitelistedHosts: [WhitelistedHosts.Any], }; @@ -24,7 +21,7 @@ describe('ensureWhitelistedUri', () => { }); test('throws when the hostname in the requested uri is not in the whitelist', () => { - const config: ActionsKibanaConfig = { enabled: false, whitelistedHosts: [] }; + const config: ActionsConfigType = { enabled: false, whitelistedHosts: [] }; expect(() => getActionsConfigurationUtilities(config).ensureWhitelistedUri( 'https://github.com/elastic/kibana' @@ -35,7 +32,7 @@ describe('ensureWhitelistedUri', () => { }); test('throws when the uri cannot be parsed as a valid URI', () => { - const config: ActionsKibanaConfig = { enabled: false, whitelistedHosts: [] }; + const config: ActionsConfigType = { enabled: false, whitelistedHosts: [] }; expect(() => getActionsConfigurationUtilities(config).ensureWhitelistedUri('github.com/elastic') ).toThrowErrorMatchingInlineSnapshot( @@ -44,7 +41,7 @@ describe('ensureWhitelistedUri', () => { }); test('returns true when the hostname in the requested uri is in the whitelist', () => { - const config: ActionsKibanaConfig = { enabled: false, whitelistedHosts: ['github.com'] }; + const config: ActionsConfigType = { enabled: false, whitelistedHosts: ['github.com'] }; expect( getActionsConfigurationUtilities(config).ensureWhitelistedUri( 'https://github.com/elastic/kibana' @@ -55,7 +52,7 @@ describe('ensureWhitelistedUri', () => { describe('ensureWhitelistedHostname', () => { test('returns true when "any" hostnames are allowed', () => { - const config: ActionsKibanaConfig = { + const config: ActionsConfigType = { enabled: false, whitelistedHosts: [WhitelistedHosts.Any], }; @@ -65,7 +62,7 @@ describe('ensureWhitelistedHostname', () => { }); test('throws when the hostname in the requested uri is not in the whitelist', () => { - const config: ActionsKibanaConfig = { enabled: false, whitelistedHosts: [] }; + const config: ActionsConfigType = { enabled: false, whitelistedHosts: [] }; expect(() => getActionsConfigurationUtilities(config).ensureWhitelistedHostname('github.com') ).toThrowErrorMatchingInlineSnapshot( @@ -74,7 +71,7 @@ describe('ensureWhitelistedHostname', () => { }); test('returns true when the hostname in the requested uri is in the whitelist', () => { - const config: ActionsKibanaConfig = { enabled: false, whitelistedHosts: ['github.com'] }; + const config: ActionsConfigType = { enabled: false, whitelistedHosts: ['github.com'] }; expect( getActionsConfigurationUtilities(config).ensureWhitelistedHostname('github.com') ).toBeUndefined(); @@ -83,7 +80,7 @@ describe('ensureWhitelistedHostname', () => { describe('isWhitelistedUri', () => { test('returns true when "any" hostnames are allowed', () => { - const config: ActionsKibanaConfig = { + const config: ActionsConfigType = { enabled: false, whitelistedHosts: [WhitelistedHosts.Any], }; @@ -93,21 +90,21 @@ describe('isWhitelistedUri', () => { }); test('throws when the hostname in the requested uri is not in the whitelist', () => { - const config: ActionsKibanaConfig = { enabled: false, whitelistedHosts: [] }; + const config: ActionsConfigType = { enabled: false, whitelistedHosts: [] }; expect( getActionsConfigurationUtilities(config).isWhitelistedUri('https://github.com/elastic/kibana') ).toEqual(false); }); test('throws when the uri cannot be parsed as a valid URI', () => { - const config: ActionsKibanaConfig = { enabled: false, whitelistedHosts: [] }; + const config: ActionsConfigType = { enabled: false, whitelistedHosts: [] }; expect(getActionsConfigurationUtilities(config).isWhitelistedUri('github.com/elastic')).toEqual( false ); }); test('returns true when the hostname in the requested uri is in the whitelist', () => { - const config: ActionsKibanaConfig = { enabled: false, whitelistedHosts: ['github.com'] }; + const config: ActionsConfigType = { enabled: false, whitelistedHosts: ['github.com'] }; expect( getActionsConfigurationUtilities(config).isWhitelistedUri('https://github.com/elastic/kibana') ).toEqual(true); @@ -116,7 +113,7 @@ describe('isWhitelistedUri', () => { describe('isWhitelistedHostname', () => { test('returns true when "any" hostnames are allowed', () => { - const config: ActionsKibanaConfig = { + const config: ActionsConfigType = { enabled: false, whitelistedHosts: [WhitelistedHosts.Any], }; @@ -126,14 +123,14 @@ describe('isWhitelistedHostname', () => { }); test('throws when the hostname in the requested uri is not in the whitelist', () => { - const config: ActionsKibanaConfig = { enabled: false, whitelistedHosts: [] }; + const config: ActionsConfigType = { enabled: false, whitelistedHosts: [] }; expect(getActionsConfigurationUtilities(config).isWhitelistedHostname('github.com')).toEqual( false ); }); test('returns true when the hostname in the requested uri is in the whitelist', () => { - const config: ActionsKibanaConfig = { enabled: false, whitelistedHosts: ['github.com'] }; + const config: ActionsConfigType = { enabled: false, whitelistedHosts: ['github.com'] }; expect(getActionsConfigurationUtilities(config).isWhitelistedHostname('github.com')).toEqual( true ); diff --git a/x-pack/legacy/plugins/actions/server/actions_config.ts b/x-pack/legacy/plugins/actions/server/actions_config.ts index 7706a05c9e285..0d33914920b21 100644 --- a/x-pack/legacy/plugins/actions/server/actions_config.ts +++ b/x-pack/legacy/plugins/actions/server/actions_config.ts @@ -10,6 +10,8 @@ import { URL } from 'url'; import { curry } from 'lodash'; import { pipe } from 'fp-ts/lib/pipeable'; +import { ActionsConfigType } from './types'; + export enum WhitelistedHosts { Any = '*', } @@ -19,11 +21,6 @@ enum WhitelistingField { hostname = 'hostname', } -export interface ActionsKibanaConfig { - enabled: boolean; - whitelistedHosts: string[]; -} - export interface ActionsConfigurationUtilities { isWhitelistedHostname: (hostname: string) => boolean; isWhitelistedUri: (uri: string) => boolean; @@ -45,7 +42,7 @@ function doesValueWhitelistAnyHostname(whitelistedHostname: string): boolean { return whitelistedHostname === WhitelistedHosts.Any; } -function isWhitelisted({ whitelistedHosts }: ActionsKibanaConfig, hostname: string): boolean { +function isWhitelisted({ whitelistedHosts }: ActionsConfigType, hostname: string): boolean { return ( Array.isArray(whitelistedHosts) && isSome( @@ -59,7 +56,7 @@ function isWhitelisted({ whitelistedHosts }: ActionsKibanaConfig, hostname: stri ); } -function isWhitelistedHostnameInUri(config: ActionsKibanaConfig, uri: string): boolean { +function isWhitelistedHostnameInUri(config: ActionsConfigType, uri: string): boolean { return pipe( tryCatch(() => new URL(uri)), map(url => url.hostname), @@ -69,7 +66,7 @@ function isWhitelistedHostnameInUri(config: ActionsKibanaConfig, uri: string): b } export function getActionsConfigurationUtilities( - config: ActionsKibanaConfig + config: ActionsConfigType ): ActionsConfigurationUtilities { const isWhitelistedHostname = curry(isWhitelisted)(config); const isWhitelistedUri = curry(isWhitelistedHostnameInUri)(config); diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/email.test.ts index a04a55bd29e23..99d1978ece72b 100644 --- a/x-pack/legacy/plugins/actions/server/builtin_action_types/email.test.ts +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/email.test.ts @@ -29,7 +29,7 @@ const services = { let actionType: ActionType; beforeAll(() => { - const actionTypeRegistry = createActionTypeRegistry(); + const { actionTypeRegistry } = createActionTypeRegistry(); actionType = actionTypeRegistry.get(ACTION_TYPE_ID); }); diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/email.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/email.ts index 70ffdb164a459..d54ffedcb71a9 100644 --- a/x-pack/legacy/plugins/actions/server/builtin_action_types/email.ts +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/email.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { curry } from 'lodash'; import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; import nodemailerServices from 'nodemailer/lib/well-known/services.json'; @@ -11,6 +12,7 @@ import nodemailerServices from 'nodemailer/lib/well-known/services.json'; import { sendEmail, JSON_TRANSPORT_SERVICE } from './lib/send_email'; import { nullableType } from './lib/nullable'; import { portSchema } from './lib/schemas'; +import { Logger } from '../../../../../../src/core/server'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; // config definition @@ -97,7 +99,7 @@ function validateParams(paramsObject: any): string | void { } // action type definition -export function getActionType(): ActionType { +export function getActionType({ logger }: { logger: Logger }): ActionType { return { id: '.email', name: 'email', @@ -106,18 +108,20 @@ export function getActionType(): ActionType { secrets: SecretsSchema, params: ParamsSchema, }, - executor, + executor: curry(executor)({ logger }), }; } // action executor -async function executor(execOptions: ActionTypeExecutorOptions): Promise { +async function executor( + { logger }: { logger: Logger }, + execOptions: ActionTypeExecutorOptions +): Promise { const id = execOptions.id; const config = execOptions.config as ActionTypeConfigType; const secrets = execOptions.secrets as ActionTypeSecretsType; const params = execOptions.params as ActionParamsType; - const services = execOptions.services; const transport: any = { user: secrets.user, @@ -149,7 +153,7 @@ async function executor(execOptions: ActionTypeExecutorOptions): Promise { - const actionTypeRegistry = createActionTypeRegistry(); + const { actionTypeRegistry } = createActionTypeRegistry(); actionType = actionTypeRegistry.get(ACTION_TYPE_ID); }); diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/es_index.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/es_index.ts index a60a3729e78fe..e5e1c63640dc2 100644 --- a/x-pack/legacy/plugins/actions/server/builtin_action_types/es_index.ts +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/es_index.ts @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { curry } from 'lodash'; import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; import { nullableType } from './lib/nullable'; +import { Logger } from '../../../../../../src/core/server'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; // config definition @@ -33,7 +35,7 @@ const ParamsSchema = schema.object({ }); // action type definition -export function getActionType(): ActionType { +export function getActionType({ logger }: { logger: Logger }): ActionType { return { id: '.index', name: 'index', @@ -41,13 +43,16 @@ export function getActionType(): ActionType { config: ConfigSchema, params: ParamsSchema, }, - executor, + executor: curry(executor)({ logger }), }; } // action executor -async function executor(execOptions: ActionTypeExecutorOptions): Promise { +async function executor( + { logger }: { logger: Logger }, + execOptions: ActionTypeExecutorOptions +): Promise { const id = execOptions.id; const config = execOptions.config as ActionTypeConfigType; const params = execOptions.params as ActionParamsType; @@ -67,10 +72,7 @@ async function executor(execOptions: ActionTypeExecutorOptions): Promise {}; const MOCK_KIBANA_CONFIG_UTILS: ActionsConfigurationUtilities = { isWhitelistedHostname: _ => true, isWhitelistedUri: _ => true, @@ -20,29 +20,21 @@ const MOCK_KIBANA_CONFIG_UTILS: ActionsConfigurationUtilities = { ensureWhitelistedUri: _ => {}, }; -const services = { - log: NO_OP_FN, - callCluster: jest.fn(), - savedObjectsClient: SavedObjectsClientMock.create(), -}; - -function getServices() { - return services; -} - -const mockEncryptedSavedObjectsPlugin = encryptedSavedObjectsMock.create(); - -export function createActionTypeRegistry(): ActionTypeRegistry { +export function createActionTypeRegistry(): { + logger: jest.Mocked; + actionTypeRegistry: ActionTypeRegistry; +} { + const logger = loggingServiceMock.create().get() as jest.Mocked; const actionTypeRegistry = new ActionTypeRegistry({ - getServices, - isSecurityEnabled: true, taskManager: taskManagerMock.create(), - encryptedSavedObjectsPlugin: mockEncryptedSavedObjectsPlugin, - spaceIdToNamespace: jest.fn().mockReturnValue(undefined), - getBasePath: jest.fn().mockReturnValue(undefined), + taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor()), + }); + registerBuiltInActionTypes({ + logger, + actionTypeRegistry, + actionsConfigUtils: MOCK_KIBANA_CONFIG_UTILS, }); - registerBuiltInActionTypes(actionTypeRegistry, MOCK_KIBANA_CONFIG_UTILS); - return actionTypeRegistry; + return { logger, actionTypeRegistry }; } beforeEach(() => { @@ -51,7 +43,7 @@ beforeEach(() => { describe('action is registered', () => { test('gets registered with builtin actions', () => { - const actionTypeRegistry = createActionTypeRegistry(); + const { actionTypeRegistry } = createActionTypeRegistry(); ACTION_TYPE_IDS.forEach(ACTION_TYPE_ID => expect(actionTypeRegistry.has(ACTION_TYPE_ID)).toEqual(true) ); diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/index.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/index.ts index 92e0ff7511860..be743c84f5dfe 100644 --- a/x-pack/legacy/plugins/actions/server/builtin_action_types/index.ts +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/index.ts @@ -6,6 +6,7 @@ import { ActionTypeRegistry } from '../action_type_registry'; import { ActionsConfigurationUtilities } from '../actions_config'; +import { Logger } from '../../../../../../src/core/server'; import { getActionType as getServerLogActionType } from './server_log'; import { getActionType as getSlackActionType } from './slack'; @@ -14,14 +15,21 @@ import { getActionType as getIndexActionType } from './es_index'; import { getActionType as getPagerDutyActionType } from './pagerduty'; import { getActionType as getWebhookActionType } from './webhook'; -export function registerBuiltInActionTypes( - actionTypeRegistry: ActionTypeRegistry, - actionsConfigUtils: ActionsConfigurationUtilities -) { - actionTypeRegistry.register(getServerLogActionType()); +export function registerBuiltInActionTypes({ + logger, + actionTypeRegistry, + actionsConfigUtils, +}: { + logger: Logger; + actionTypeRegistry: ActionTypeRegistry; + actionsConfigUtils: ActionsConfigurationUtilities; +}) { + actionTypeRegistry.register(getServerLogActionType({ logger })); actionTypeRegistry.register(getSlackActionType()); - actionTypeRegistry.register(getEmailActionType()); - actionTypeRegistry.register(getIndexActionType()); - actionTypeRegistry.register(getPagerDutyActionType()); - actionTypeRegistry.register(getWebhookActionType(actionsConfigUtils)); + actionTypeRegistry.register(getEmailActionType({ logger })); + actionTypeRegistry.register(getIndexActionType({ logger })); + actionTypeRegistry.register(getPagerDutyActionType({ logger })); + actionTypeRegistry.register( + getWebhookActionType({ logger, configurationUtilities: actionsConfigUtils }) + ); } diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/lib/send_email.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/lib/send_email.ts index e69dc3401f130..ab949dbe2722d 100644 --- a/x-pack/legacy/plugins/actions/server/builtin_action_types/lib/send_email.ts +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/lib/send_email.ts @@ -9,7 +9,7 @@ import nodemailer from 'nodemailer'; import { default as MarkdownIt } from 'markdown-it'; -import { Services } from '../../types'; +import { Logger } from '../../../../../../../src/core/server'; // an email "service" which doesn't actually send, just returns what it would send export const JSON_TRANSPORT_SERVICE = '__json'; @@ -43,7 +43,7 @@ interface Content { } // send an email -export async function sendEmail(services: Services, options: SendEmailOptions): Promise { +export async function sendEmail(logger: Logger, options: SendEmailOptions): Promise { const { transport, routing, content } = options; const { service, host, port, secure, user, password } = transport; const { from, to, cc, bcc } = routing; @@ -68,7 +68,7 @@ export async function sendEmail(services: Services, options: SendEmailOptions): } const nodemailerTransport = nodemailer.createTransport(transportConfig); - const messageHTML = htmlFromMarkdown(services, message); + const messageHTML = htmlFromMarkdown(logger, message); const email = { // email routing @@ -96,7 +96,7 @@ export async function sendEmail(services: Services, options: SendEmailOptions): } // try rendering markdown to html, return markdown on any kind of error -function htmlFromMarkdown(services: Services, markdown: string) { +function htmlFromMarkdown(logger: Logger, markdown: string) { try { const md = MarkdownIt({ linkify: true, @@ -104,7 +104,7 @@ function htmlFromMarkdown(services: Services, markdown: string) { return md.render(markdown); } catch (err) { - services.log(['debug', 'actions'], `error rendering markdown to html: ${err.message}`); + logger.debug(`error rendering markdown to html: ${err.message}`); return markdown; } diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.test.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.test.ts index cc2499932e785..2ca4cff46b51f 100644 --- a/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.test.ts +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.test.ts @@ -17,10 +17,8 @@ import { createActionTypeRegistry } from './index.test'; const postPagerdutyMock = postPagerduty as jest.Mock; const ACTION_TYPE_ID = '.pagerduty'; -const NO_OP_FN = () => {}; const services: Services = { - log: NO_OP_FN, callCluster: async (path: string, opts: any) => {}, savedObjectsClient: SavedObjectsClientMock.create(), }; @@ -28,14 +26,10 @@ const services: Services = { let actionType: ActionType; beforeAll(() => { - const actionTypeRegistry = createActionTypeRegistry(); + const { actionTypeRegistry } = createActionTypeRegistry(); actionType = actionTypeRegistry.get(ACTION_TYPE_ID); }); -beforeEach(() => { - services.log = NO_OP_FN; -}); - describe('get()', () => { test('should return correct action type', () => { expect(actionType.id).toEqual(ACTION_TYPE_ID); diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.ts index 7e043d5309c37..f3c5c77802447 100644 --- a/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.ts +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/pagerduty.ts @@ -4,9 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { curry } from 'lodash'; import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; import { postPagerduty } from './lib/post_pagerduty'; +import { Logger } from '../../../../../../src/core/server'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; // uses the PagerDuty Events API v2 @@ -84,7 +86,7 @@ function validateParams(paramsObject: any): string | void { } // action type definition -export function getActionType(): ActionType { +export function getActionType({ logger }: { logger: Logger }): ActionType { return { id: '.pagerduty', name: 'pagerduty', @@ -93,13 +95,16 @@ export function getActionType(): ActionType { secrets: SecretsSchema, params: ParamsSchema, }, - executor, + executor: curry(executor)({ logger }), }; } // action executor -async function executor(execOptions: ActionTypeExecutorOptions): Promise { +async function executor( + { logger }: { logger: Logger }, + execOptions: ActionTypeExecutorOptions +): Promise { const id = execOptions.id; const config = execOptions.config as ActionTypeConfigType; const secrets = execOptions.secrets as ActionTypeSecretsType; @@ -124,20 +129,14 @@ async function executor(execOptions: ActionTypeExecutorOptions): Promise {}; - -const services: Services = { - log: NO_OP_FN, - callCluster: async (path: string, opts: any) => {}, - savedObjectsClient: SavedObjectsClientMock.create(), -}; let actionType: ActionType; +let mockedLogger: jest.Mocked; beforeAll(() => { - const actionTypeRegistry = createActionTypeRegistry(); + const { logger, actionTypeRegistry } = createActionTypeRegistry(); actionType = actionTypeRegistry.get(ACTION_TYPE_ID); + mockedLogger = logger; expect(actionType).toBeTruthy(); }); -beforeEach(() => { - services.log = NO_OP_FN; -}); - describe('get()', () => { test('returns action type', () => { expect(actionType.id).toEqual(ACTION_TYPE_ID); @@ -39,18 +31,18 @@ describe('get()', () => { describe('validateParams()', () => { test('should validate and pass when params is valid', () => { - expect(validateParams(actionType, { message: 'a message' })).toEqual({ + expect(validateParams(actionType, { message: 'a message', level: 'info' })).toEqual({ message: 'a message', - tags: ['info', 'alerting'], + level: 'info', }); expect( validateParams(actionType, { message: 'a message', - tags: ['info', 'blorg'], + level: 'info', }) ).toEqual({ message: 'a message', - tags: ['info', 'blorg'], + level: 'info', }); }); @@ -68,54 +60,44 @@ describe('validateParams()', () => { ); expect(() => { - validateParams(actionType, { message: 'x', tags: 2 }); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action params: [tags]: expected value of type [array] but got [number]"` - ); + validateParams(actionType, { message: 'x', level: 2 }); + }).toThrowErrorMatchingInlineSnapshot(` +"error validating action params: [level]: types that failed validation: +- [level.0]: expected value to equal [trace] but got [2] +- [level.1]: expected value to equal [debug] but got [2] +- [level.2]: expected value to equal [info] but got [2] +- [level.3]: expected value to equal [warn] but got [2] +- [level.4]: expected value to equal [error] but got [2] +- [level.5]: expected value to equal [fatal] but got [2]" +`); expect(() => { - validateParams(actionType, { message: 'x', tags: [2] }); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action params: [tags.0]: expected value of type [string] but got [number]"` - ); + validateParams(actionType, { message: 'x', level: 'foo' }); + }).toThrowErrorMatchingInlineSnapshot(` +"error validating action params: [level]: types that failed validation: +- [level.0]: expected value to equal [trace] but got [foo] +- [level.1]: expected value to equal [debug] but got [foo] +- [level.2]: expected value to equal [info] but got [foo] +- [level.3]: expected value to equal [warn] but got [foo] +- [level.4]: expected value to equal [error] but got [foo] +- [level.5]: expected value to equal [fatal] but got [foo]" +`); }); }); describe('execute()', () => { test('calls the executor with proper params', async () => { - const mockLog = jest.fn().mockResolvedValueOnce({ success: true }); - - services.log = mockLog; const id = 'some-id'; await actionType.executor({ id, services: { - log: mockLog, callCluster: async (path: string, opts: any) => {}, savedObjectsClient: SavedObjectsClientMock.create(), }, - params: { message: 'message text here', tags: ['tag1', 'tag2'] }, + params: { message: 'message text here', level: 'info' }, config: {}, secrets: {}, }); - expect(mockLog).toMatchInlineSnapshot(` -[MockFunction] { - "calls": Array [ - Array [ - Array [ - "tag1", - "tag2", - ], - "message text here", - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], -} -`); + expect(mockedLogger.info).toHaveBeenCalledWith('message text here'); }); }); diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/server_log.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/server_log.ts index 9ec8e6051b6ca..fc9b5481bc33c 100644 --- a/x-pack/legacy/plugins/actions/server/builtin_action_types/server_log.ts +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/server_log.ts @@ -4,43 +4,52 @@ * you may not use this file except in compliance with the Elastic License. */ +import { curry } from 'lodash'; import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; +import { Logger } from '../../../../../../src/core/server'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; -const DEFAULT_TAGS = ['info', 'alerting']; - // params definition export type ActionParamsType = TypeOf; const ParamsSchema = schema.object({ message: schema.string(), - tags: schema.arrayOf(schema.string(), { defaultValue: DEFAULT_TAGS }), + level: schema.oneOf([ + schema.literal('trace'), + schema.literal('debug'), + schema.literal('info'), + schema.literal('warn'), + schema.literal('error'), + schema.literal('fatal'), + ]), }); // action type definition -export function getActionType(): ActionType { +export function getActionType({ logger }: { logger: Logger }): ActionType { return { id: '.server-log', name: 'server-log', validate: { params: ParamsSchema, }, - executor, + executor: curry(executor)({ logger }), }; } // action executor -async function executor(execOptions: ActionTypeExecutorOptions): Promise { +async function executor( + { logger }: { logger: Logger }, + execOptions: ActionTypeExecutorOptions +): Promise { const id = execOptions.id; const params = execOptions.params as ActionParamsType; - const services = execOptions.services; try { - services.log(params.tags, params.message); + logger[params.level](params.message); } catch (err) { const message = i18n.translate('xpack.actions.builtin.serverLog.errorLoggingErrorMessage', { defaultMessage: 'error in action "{id}" logging message: {errorMessage}', diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/slack.test.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/slack.test.ts index bae7897b44d7d..64607b45f2496 100644 --- a/x-pack/legacy/plugins/actions/server/builtin_action_types/slack.test.ts +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/slack.test.ts @@ -6,31 +6,21 @@ import { ActionType, Services, ActionTypeExecutorOptions } from '../types'; import { ActionTypeRegistry } from '../action_type_registry'; -import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/plugin.mock'; import { SavedObjectsClientMock } from '../../../../../../src/core/server/mocks'; -import { validateParams, validateSecrets } from '../lib'; +import { ActionExecutor, validateParams, validateSecrets, TaskRunnerFactory } from '../lib'; import { getActionType } from './slack'; import { taskManagerMock } from '../../../task_manager/task_manager.mock'; const ACTION_TYPE_ID = '.slack'; -const NO_OP_FN = () => {}; - const services: Services = { - log: NO_OP_FN, callCluster: async (path: string, opts: any) => {}, savedObjectsClient: SavedObjectsClientMock.create(), }; -function getServices(): Services { - return services; -} - let actionTypeRegistry: ActionTypeRegistry; let actionType: ActionType; -const mockEncryptedSavedObjectsPlugin = encryptedSavedObjectsMock.create(); - async function mockSlackExecutor(options: ActionTypeExecutorOptions): Promise { const { params } = options; const { message } = params; @@ -49,12 +39,8 @@ async function mockSlackExecutor(options: ActionTypeExecutorOptions): Promise { actionTypeRegistry = new ActionTypeRegistry({ - getServices, - isSecurityEnabled: true, taskManager: taskManagerMock.create(), - encryptedSavedObjectsPlugin: mockEncryptedSavedObjectsPlugin, - spaceIdToNamespace: jest.fn().mockReturnValue(undefined), - getBasePath: jest.fn().mockReturnValue(undefined), + taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor()), }); actionTypeRegistry.register(getActionType({ executor: mockSlackExecutor })); actionType = actionTypeRegistry.get(ACTION_TYPE_ID); diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/webhook.test.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/webhook.test.ts index 6fe1486be94b6..2d492eb451e80 100644 --- a/x-pack/legacy/plugins/actions/server/builtin_action_types/webhook.test.ts +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/webhook.test.ts @@ -9,6 +9,7 @@ import { validateConfig, validateSecrets, validateParams } from '../lib'; import { ActionsConfigurationUtilities } from '../actions_config'; import { ActionType } from '../types'; import { createActionTypeRegistry } from './index.test'; +import { Logger } from '../../../../../../src/core/server'; const ACTION_TYPE_ID = '.webhook'; const configUtilsMock: ActionsConfigurationUtilities = { @@ -19,10 +20,12 @@ const configUtilsMock: ActionsConfigurationUtilities = { }; let actionType: ActionType; +let mockedLogger: jest.Mocked; beforeAll(() => { - const actionTypeRegistry = createActionTypeRegistry(); + const { logger, actionTypeRegistry } = createActionTypeRegistry(); actionType = actionTypeRegistry.get(ACTION_TYPE_ID); + mockedLogger = logger; }); describe('actionType', () => { @@ -154,9 +157,12 @@ describe('config validation', () => { test('config validation returns an error if the specified URL isnt whitelisted', () => { actionType = getActionType({ - ...configUtilsMock, - ensureWhitelistedUri: _ => { - throw new Error(`target url is not whitelisted`); + logger: mockedLogger, + configurationUtilities: { + ...configUtilsMock, + ensureWhitelistedUri: _ => { + throw new Error(`target url is not whitelisted`); + }, }, }); diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/webhook.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/webhook.ts index 8dcfbddab822e..194570b462fe8 100644 --- a/x-pack/legacy/plugins/actions/server/builtin_action_types/webhook.ts +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/webhook.ts @@ -14,6 +14,7 @@ import { nullableType } from './lib/nullable'; import { isOk, promiseResult, Result } from './lib/result_type'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; import { ActionsConfigurationUtilities } from '../actions_config'; +import { Logger } from '../../../../../../src/core/server'; // config definition enum WebhookMethods { @@ -46,7 +47,13 @@ const ParamsSchema = schema.object({ }); // action type definition -export function getActionType(configurationUtilities: ActionsConfigurationUtilities): ActionType { +export function getActionType({ + logger, + configurationUtilities, +}: { + logger: Logger; + configurationUtilities: ActionsConfigurationUtilities; +}): ActionType { return { id: '.webhook', name: 'webhook', @@ -57,7 +64,7 @@ export function getActionType(configurationUtilities: ActionsConfigurationUtilit secrets: SecretsSchema, params: ParamsSchema, }, - executor: curry(executor)(configurationUtilities), + executor: curry(executor)({ logger }), }; } @@ -79,12 +86,9 @@ function valdiateActionTypeConfig( // action executor export async function executor( - configurationUtilities: ActionsConfigurationUtilities, + { logger }: { logger: Logger }, execOptions: ActionTypeExecutorOptions ): Promise { - const log = (level: string, msg: string) => - execOptions.services.log([level, 'actions', 'webhook'], msg); - const id = execOptions.id; const { method, url, headers = {} } = execOptions.config as ActionTypeConfigType; const { user: username, password } = execOptions.secrets as ActionTypeSecretsType; @@ -107,7 +111,7 @@ export async function executor( const { value: { status, statusText }, } = result; - log('debug', `response from webhook action "${id}": [HTTP ${status}] ${statusText}`); + logger.debug(`response from webhook action "${id}": [HTTP ${status}] ${statusText}`); return successResult(data); } else { @@ -116,7 +120,7 @@ export async function executor( if (error.response) { const { status, statusText, headers: responseHeaders } = error.response; const message = `[${status}] ${statusText}`; - log(`warn`, `error on ${id} webhook event: ${message}`); + logger.warn(`error on ${id} webhook event: ${message}`); // The request was made and the server responded with a status code // that falls out of the range of 2xx // special handling for 5xx @@ -138,7 +142,7 @@ export async function executor( const message = i18n.translate('xpack.actions.builtin.webhook.unreachableRemoteWebhook', { defaultMessage: 'Unreachable Remote Webhook, are you sure the address is correct?', }); - log(`warn`, `error on ${id} webhook action: ${message}`); + logger.warn(`error on ${id} webhook action: ${message}`); return errorResultUnreachable(id, message); } } diff --git a/x-pack/legacy/plugins/actions/server/create_execute_function.test.ts b/x-pack/legacy/plugins/actions/server/create_execute_function.test.ts index f2610ae0a4b17..eff7f673da98a 100644 --- a/x-pack/legacy/plugins/actions/server/create_execute_function.test.ts +++ b/x-pack/legacy/plugins/actions/server/create_execute_function.test.ts @@ -101,6 +101,16 @@ describe('execute()', () => { // base64 encoded "123:abc" authorization: 'ApiKey MTIzOmFiYw==', }, + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, }); }); @@ -135,6 +145,16 @@ describe('execute()', () => { expect(getScopedSavedObjectsClient).toHaveBeenCalledWith({ getBasePath: expect.anything(), headers: {}, + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, }); }); diff --git a/x-pack/legacy/plugins/actions/server/create_execute_function.ts b/x-pack/legacy/plugins/actions/server/create_execute_function.ts index a7e93d9f1ade8..c441b256dccb6 100644 --- a/x-pack/legacy/plugins/actions/server/create_execute_function.ts +++ b/x-pack/legacy/plugins/actions/server/create_execute_function.ts @@ -5,12 +5,12 @@ */ import { SavedObjectsClientContract } from 'src/core/server'; -import { TaskManager } from '../../task_manager'; +import { TaskManagerStartContract } from './shim'; import { GetBasePathFunction } from './types'; interface CreateExecuteFunctionOptions { isSecurityEnabled: boolean; - taskManager: TaskManager; + taskManager: TaskManagerStartContract; getScopedSavedObjectsClient: (request: any) => SavedObjectsClientContract; getBasePath: GetBasePathFunction; } @@ -42,6 +42,16 @@ export function createExecuteFunction({ const fakeRequest: any = { headers: requestHeaders, getBasePath: () => getBasePath(spaceId), + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, }; const savedObjectsClient = getScopedSavedObjectsClient(fakeRequest); diff --git a/x-pack/legacy/plugins/actions/server/index.ts b/x-pack/legacy/plugins/actions/server/index.ts index 4e1016b9eef43..faafb0db330cd 100644 --- a/x-pack/legacy/plugins/actions/server/index.ts +++ b/x-pack/legacy/plugins/actions/server/index.ts @@ -7,3 +7,4 @@ export { init } from './init'; export { ActionsPlugin, ActionTypeExecutorOptions, ActionType } from './types'; export { ActionsClient } from './actions_client'; +export { PluginSetupContract, PluginStartContract } from './plugin'; diff --git a/x-pack/legacy/plugins/actions/server/init.ts b/x-pack/legacy/plugins/actions/server/init.ts index 395284938fa1d..5eab3418467bc 100644 --- a/x-pack/legacy/plugins/actions/server/init.ts +++ b/x-pack/legacy/plugins/actions/server/init.ts @@ -4,152 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import Hapi from 'hapi'; -import { Legacy } from 'kibana'; -import { TaskManager } from '../../task_manager'; -import { ActionsClient } from './actions_client'; -import { ActionTypeRegistry } from './action_type_registry'; -import { createExecuteFunction } from './create_execute_function'; -import { ActionsPlugin, Services } from './types'; -import { ActionsKibanaConfig, getActionsConfigurationUtilities } from './actions_config'; -import { EncryptedSavedObjectsPlugin } from '../../encrypted_saved_objects'; -import { - createRoute, - deleteRoute, - findRoute, - getRoute, - updateRoute, - listActionTypesRoute, - executeRoute, -} from './routes'; -import { registerBuiltInActionTypes } from './builtin_action_types'; -import { SpacesPlugin } from '../../spaces'; -import { createOptionalPlugin } from '../../../server/lib/optional_plugin'; +import { Plugin } from './plugin'; +import { shim, Server } from './shim'; +import { ActionsPlugin } from './types'; -// Extend PluginProperties to indicate which plugins are guaranteed to exist -// due to being marked as dependencies -interface Plugins extends Hapi.PluginProperties { - task_manager: TaskManager; - encrypted_saved_objects: EncryptedSavedObjectsPlugin; -} - -interface Server extends Legacy.Server { - plugins: Plugins; -} - -export function init(server: Server) { - const config = server.config(); - const taskManager = server.plugins.task_manager; - const { callWithRequest } = server.plugins.elasticsearch.getCluster('admin'); - const spaces = createOptionalPlugin( - config, - 'xpack.spaces', - server.plugins, - 'spaces' - ); - - server.plugins.xpack_main.registerFeature({ - id: 'actions', - name: 'Actions', - app: ['actions', 'kibana'], - privileges: { - all: { - savedObject: { - all: ['action', 'action_task_params'], - read: [], - }, - ui: [], - api: ['actions-read', 'actions-all'], - }, - read: { - savedObject: { - all: ['action_task_params'], - read: ['action'], - }, - ui: [], - api: ['actions-read'], - }, - }, - }); - - // Encrypted attributes - // - `secrets` properties will be encrypted - // - `config` will be included in AAD - // - everything else excluded from AAD - server.plugins.encrypted_saved_objects.registerType({ - type: 'action', - attributesToEncrypt: new Set(['secrets']), - attributesToExcludeFromAAD: new Set(['description']), - }); - server.plugins.encrypted_saved_objects.registerType({ - type: 'action_task_params', - attributesToEncrypt: new Set(['apiKey']), - }); - - function getServices(request: any): Services { - return { - log: (...args) => server.log(...args), - callCluster: (...args) => callWithRequest(request, ...args), - savedObjectsClient: server.savedObjects.getScopedSavedObjectsClient(request), - }; - } - function getBasePath(spaceId?: string): string { - return spaces.isEnabled && spaceId - ? spaces.getBasePath(spaceId) - : ((server.config().get('server.basePath') || '') as string); - } - function spaceIdToNamespace(spaceId?: string): string | undefined { - return spaces.isEnabled && spaceId ? spaces.spaceIdToNamespace(spaceId) : undefined; - } - - const actionTypeRegistry = new ActionTypeRegistry({ - getServices, - taskManager, - encryptedSavedObjectsPlugin: server.plugins.encrypted_saved_objects, - getBasePath, - spaceIdToNamespace, - isSecurityEnabled: config.get('xpack.security.enabled'), - }); +export async function init(server: Server) { + const { initializerContext, coreSetup, coreStart, pluginsSetup, pluginsStart } = shim(server); - registerBuiltInActionTypes( - actionTypeRegistry, - getActionsConfigurationUtilities(config.get('xpack.actions') as ActionsKibanaConfig) - ); + const plugin = new Plugin(initializerContext); - // Routes - createRoute(server); - deleteRoute(server); - getRoute(server); - findRoute(server); - updateRoute(server); - listActionTypesRoute(server); - executeRoute({ - server, - actionTypeRegistry, - getServices, - }); - - const executeFn = createExecuteFunction({ - taskManager, - getScopedSavedObjectsClient: server.savedObjects.getScopedSavedObjectsClient, - getBasePath, - isSecurityEnabled: config.get('xpack.security.enabled'), - }); + const setupContract = await plugin.setup(coreSetup, pluginsSetup); + const startContract = plugin.start(coreStart, pluginsStart); - // Expose functions to server server.decorate('request', 'getActionsClient', function() { - const request = this; - const savedObjectsClient = request.getSavedObjectsClient(); - const actionsClient = new ActionsClient({ - savedObjectsClient, - actionTypeRegistry, - }); - return actionsClient; + return startContract.getActionsClientWithRequest(this); }); + const exposedFunctions: ActionsPlugin = { - execute: executeFn, - registerType: actionTypeRegistry.register.bind(actionTypeRegistry), - listTypes: actionTypeRegistry.list.bind(actionTypeRegistry), + setup: setupContract, + start: startContract, }; server.expose(exposedFunctions); } diff --git a/x-pack/legacy/plugins/actions/server/lib/action_executor.mock.ts b/x-pack/legacy/plugins/actions/server/lib/action_executor.mock.ts new file mode 100644 index 0000000000000..73e5e96ab24ed --- /dev/null +++ b/x-pack/legacy/plugins/actions/server/lib/action_executor.mock.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ActionExecutorContract } from './action_executor'; + +const createActionExecutorMock = () => { + const mocked: jest.Mocked = { + initialize: jest.fn(), + execute: jest.fn(), + }; + return mocked; +}; + +export const actionExecutorMock = { + create: createActionExecutorMock, +}; diff --git a/x-pack/legacy/plugins/actions/server/lib/execute.test.ts b/x-pack/legacy/plugins/actions/server/lib/action_executor.test.ts similarity index 82% rename from x-pack/legacy/plugins/actions/server/lib/execute.test.ts rename to x-pack/legacy/plugins/actions/server/lib/action_executor.test.ts index cc17c811d1772..42064e011b13e 100644 --- a/x-pack/legacy/plugins/actions/server/lib/execute.test.ts +++ b/x-pack/legacy/plugins/actions/server/lib/action_executor.test.ts @@ -4,12 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ +import Hapi from 'hapi'; import { schema } from '@kbn/config-schema'; -import { execute } from './execute'; +import { ActionExecutor } from './action_executor'; import { actionTypeRegistryMock } from '../action_type_registry.mock'; -import { SavedObjectsClientMock } from '../../../../../../src/core/server/mocks'; import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/plugin.mock'; +import { + SavedObjectsClientMock, + loggingServiceMock, +} from '../../../../../../src/core/server/mocks'; +const actionExecutor = new ActionExecutor(); const savedObjectsClient = SavedObjectsClientMock.create(); function getServices() { @@ -24,14 +29,36 @@ const actionTypeRegistry = actionTypeRegistryMock.create(); const executeParams = { actionId: '1', - namespace: 'some-namespace', - services: getServices(), params: { foo: true, }, + request: { + headers: {}, + getBasePath: () => '', + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, + } as Hapi.Request, +}; + +actionExecutor.initialize({ + logger: loggingServiceMock.create().get(), + spaces() { + return { + getSpaceId: () => 'some-namespace', + } as any; + }, + getServices, actionTypeRegistry, encryptedSavedObjectsPlugin, -}; +}); beforeEach(() => jest.resetAllMocks()); @@ -58,7 +85,7 @@ test('successfully executes', async () => { savedObjectsClient.get.mockResolvedValueOnce(actionSavedObject); encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject); actionTypeRegistry.get.mockReturnValueOnce(actionType); - await execute(executeParams); + await actionExecutor.execute(executeParams); expect(encryptedSavedObjectsPlugin.getDecryptedAsInternalUser).toHaveBeenCalledWith( 'action', @@ -98,7 +125,7 @@ test('provides empty config when config and / or secrets is empty', async () => savedObjectsClient.get.mockResolvedValueOnce(actionSavedObject); encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject); actionTypeRegistry.get.mockReturnValueOnce(actionType); - await execute(executeParams); + await actionExecutor.execute(executeParams); expect(actionType.executor).toHaveBeenCalledTimes(1); const executorCall = actionType.executor.mock.calls[0][0]; @@ -128,7 +155,7 @@ test('throws an error when config is invalid', async () => { encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject); actionTypeRegistry.get.mockReturnValueOnce(actionType); - const result = await execute(executeParams); + const result = await actionExecutor.execute(executeParams); expect(result).toEqual({ status: 'error', retry: false, @@ -159,7 +186,7 @@ test('throws an error when params is invalid', async () => { encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject); actionTypeRegistry.get.mockReturnValueOnce(actionType); - const result = await execute(executeParams); + const result = await actionExecutor.execute(executeParams); expect(result).toEqual({ status: 'error', retry: false, @@ -169,5 +196,7 @@ test('throws an error when params is invalid', async () => { test('throws an error when failing to load action through savedObjectsClient', async () => { savedObjectsClient.get.mockRejectedValueOnce(new Error('No access')); - await expect(execute(executeParams)).rejects.toThrowErrorMatchingInlineSnapshot(`"No access"`); + await expect(actionExecutor.execute(executeParams)).rejects.toThrowErrorMatchingInlineSnapshot( + `"No access"` + ); }); diff --git a/x-pack/legacy/plugins/actions/server/lib/action_executor.ts b/x-pack/legacy/plugins/actions/server/lib/action_executor.ts new file mode 100644 index 0000000000000..3881f1ee21817 --- /dev/null +++ b/x-pack/legacy/plugins/actions/server/lib/action_executor.ts @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Hapi from 'hapi'; +import { EncryptedSavedObjectsStartContract } from '../shim'; +import { SpacesPlugin as SpacesPluginStartContract } from '../../../spaces'; +import { Logger } from '../../../../../../src/core/server'; +import { validateParams, validateConfig, validateSecrets } from './validate_with_schema'; +import { + ActionTypeExecutorResult, + ActionTypeRegistryContract, + GetServicesFunction, + RawAction, +} from '../types'; + +export interface ActionExecutorContext { + logger: Logger; + spaces: () => SpacesPluginStartContract | undefined; + getServices: GetServicesFunction; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + actionTypeRegistry: ActionTypeRegistryContract; +} + +export interface ExecuteOptions { + actionId: string; + request: Hapi.Request; + params: Record; +} + +export type ActionExecutorContract = PublicMethodsOf; + +export class ActionExecutor { + private isInitialized = false; + private actionExecutorContext?: ActionExecutorContext; + + public initialize(actionExecutorContext: ActionExecutorContext) { + if (this.isInitialized) { + throw new Error('ActionExecutor already initialized'); + } + this.isInitialized = true; + this.actionExecutorContext = actionExecutorContext; + } + + public async execute({ + actionId, + params, + request, + }: ExecuteOptions): Promise { + if (!this.isInitialized) { + throw new Error('ActionExecutor not initialized'); + } + + const { + logger, + spaces, + getServices, + encryptedSavedObjectsPlugin, + actionTypeRegistry, + } = this.actionExecutorContext!; + + const spacesPlugin = spaces(); + const services = getServices(request); + const namespace = spacesPlugin && spacesPlugin.getSpaceId(request); + + // Ensure user can read the action before processing + const { + attributes: { actionTypeId, config, description }, + } = await services.savedObjectsClient.get('action', actionId); + // Only get encrypted attributes here, the remaining attributes can be fetched in + // the savedObjectsClient call + const { + attributes: { secrets }, + } = await encryptedSavedObjectsPlugin.getDecryptedAsInternalUser( + 'action', + actionId, + { + namespace: namespace === 'default' ? undefined : namespace, + } + ); + const actionType = actionTypeRegistry.get(actionTypeId); + + let validatedParams; + let validatedConfig; + let validatedSecrets; + + try { + validatedParams = validateParams(actionType, params); + validatedConfig = validateConfig(actionType, config); + validatedSecrets = validateSecrets(actionType, secrets); + } catch (err) { + return { status: 'error', message: err.message, retry: false }; + } + + let result: ActionTypeExecutorResult | null = null; + const actionLabel = `${actionId} - ${actionTypeId} - ${description}`; + + try { + result = await actionType.executor({ + id: actionId, + services, + params: validatedParams, + config: validatedConfig, + secrets: validatedSecrets, + }); + } catch (err) { + logger.warn(`action executed unsuccessfully: ${actionLabel} - ${err.message}`); + throw err; + } + + logger.debug(`action executed successfully: ${actionLabel}`); + + // return basic response if none provided + if (result == null) return { status: 'ok' }; + + return result; + } +} diff --git a/x-pack/legacy/plugins/actions/server/lib/execute.ts b/x-pack/legacy/plugins/actions/server/lib/execute.ts deleted file mode 100644 index d69742f6b7c33..0000000000000 --- a/x-pack/legacy/plugins/actions/server/lib/execute.ts +++ /dev/null @@ -1,83 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - ActionTypeExecutorResult, - ActionTypeRegistryContract, - RawAction, - Services, -} from '../types'; -import { validateParams, validateConfig, validateSecrets } from './validate_with_schema'; -import { EncryptedSavedObjectsPlugin } from '../../../encrypted_saved_objects'; - -interface ExecuteOptions { - actionId: string; - namespace?: string; - services: Services; - params: Record; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin; - actionTypeRegistry: ActionTypeRegistryContract; -} - -export async function execute({ - actionId, - namespace, - actionTypeRegistry, - services, - params, - encryptedSavedObjectsPlugin, -}: ExecuteOptions): Promise { - // Ensure user can read the action before processing - const { - attributes: { actionTypeId, config, description }, - } = await services.savedObjectsClient.get('action', actionId); - // Only get encrypted attributes here, the remaining attributes can be fetched in - // the savedObjectsClient call - const { - attributes: { secrets }, - } = await encryptedSavedObjectsPlugin.getDecryptedAsInternalUser('action', actionId, { - namespace, - }); - const actionType = actionTypeRegistry.get(actionTypeId); - - let validatedParams; - let validatedConfig; - let validatedSecrets; - - try { - validatedParams = validateParams(actionType, params); - validatedConfig = validateConfig(actionType, config); - validatedSecrets = validateSecrets(actionType, secrets); - } catch (err) { - return { status: 'error', message: err.message, retry: false }; - } - - let result: ActionTypeExecutorResult | null = null; - const actionLabel = `${actionId} - ${actionTypeId} - ${description}`; - - try { - result = await actionType.executor({ - id: actionId, - services, - params: validatedParams, - config: validatedConfig, - secrets: validatedSecrets, - }); - } catch (err) { - services.log( - ['warning', 'x-pack', 'actions'], - `action executed unsuccessfully: ${actionLabel} - ${err.message}` - ); - throw err; - } - - services.log(['debug', 'x-pack', 'actions'], `action executed successfully: ${actionLabel}`); - - // return basic response if none provided - if (result == null) return { status: 'ok' }; - - return result; -} diff --git a/x-pack/legacy/plugins/actions/server/lib/get_create_task_runner_function.test.ts b/x-pack/legacy/plugins/actions/server/lib/get_create_task_runner_function.test.ts deleted file mode 100644 index 79120ad51516e..0000000000000 --- a/x-pack/legacy/plugins/actions/server/lib/get_create_task_runner_function.test.ts +++ /dev/null @@ -1,233 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -jest.mock('./execute', () => ({ - execute: jest.fn(), -})); - -import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/plugin.mock'; -import { getCreateTaskRunnerFunction } from './get_create_task_runner_function'; -import { SavedObjectsClientMock } from '../../../../../../src/core/server/mocks'; -import { actionTypeRegistryMock } from '../action_type_registry.mock'; -import { ExecutorError } from './executor_error'; - -const spaceIdToNamespace = jest.fn(); -const actionTypeRegistry = actionTypeRegistryMock.create(); -const mockedEncryptedSavedObjectsPlugin = encryptedSavedObjectsMock.create(); - -const actionType = { - id: '1', - name: '1', - executor: jest.fn(), -}; -const services = { - log: jest.fn(), - callCluster: jest.fn(), - savedObjectsClient: SavedObjectsClientMock.create(), -}; - -actionTypeRegistry.get.mockReturnValue(actionType); - -const getCreateTaskRunnerFunctionParams = { - getServices: jest.fn().mockReturnValue(services), - actionTypeRegistry, - spaceIdToNamespace, - encryptedSavedObjectsPlugin: mockedEncryptedSavedObjectsPlugin, - getBasePath: jest.fn().mockReturnValue(undefined), - isSecurityEnabled: true, -}; - -const taskInstanceMock = { - runAt: new Date(), - state: {}, - params: { - spaceId: 'test', - actionTaskParamsId: '3', - }, - taskType: 'actions:1', -}; - -beforeEach(() => { - jest.resetAllMocks(); - getCreateTaskRunnerFunctionParams.getServices.mockReturnValue(services); -}); - -test('executes the task by calling the executor with proper parameters', async () => { - const { execute: mockExecute } = jest.requireMock('./execute'); - const createTaskRunner = getCreateTaskRunnerFunction(getCreateTaskRunnerFunctionParams); - const runner = createTaskRunner({ taskInstance: taskInstanceMock }); - - mockExecute.mockResolvedValueOnce({ status: 'ok' }); - spaceIdToNamespace.mockReturnValueOnce('namespace-test'); - mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: '3', - type: 'action_task_params', - attributes: { - actionId: '2', - params: { baz: true }, - apiKey: Buffer.from('123:abc').toString('base64'), - }, - references: [], - }); - - const runnerResult = await runner.run(); - - expect(runnerResult).toBeUndefined(); - expect(spaceIdToNamespace).toHaveBeenCalledWith('test'); - expect(mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser).toHaveBeenCalledWith( - 'action_task_params', - '3', - { namespace: 'namespace-test' } - ); - expect(mockExecute).toHaveBeenCalledWith({ - namespace: 'namespace-test', - actionId: '2', - actionTypeRegistry, - encryptedSavedObjectsPlugin: mockedEncryptedSavedObjectsPlugin, - services: expect.anything(), - params: { baz: true }, - }); -}); - -test('throws an error with suggested retry logic when return status is error', async () => { - const { execute: mockExecute } = jest.requireMock('./execute'); - const createTaskRunner = getCreateTaskRunnerFunction(getCreateTaskRunnerFunctionParams); - const runner = createTaskRunner({ taskInstance: taskInstanceMock }); - - mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: '3', - type: 'action_task_params', - attributes: { - actionId: '2', - params: { baz: true }, - apiKey: Buffer.from('123:abc').toString('base64'), - }, - references: [], - }); - mockExecute.mockResolvedValueOnce({ - status: 'error', - message: 'Error message', - data: { foo: true }, - retry: false, - }); - - try { - await runner.run(); - throw new Error('Should have thrown'); - } catch (e) { - expect(e instanceof ExecutorError).toEqual(true); - expect(e.data).toEqual({ foo: true }); - expect(e.retry).toEqual(false); - } -}); - -test('uses API key when provided', async () => { - const { execute: mockExecute } = jest.requireMock('./execute'); - const createTaskRunner = getCreateTaskRunnerFunction(getCreateTaskRunnerFunctionParams); - const runner = createTaskRunner({ taskInstance: taskInstanceMock }); - - mockExecute.mockResolvedValueOnce({ status: 'ok' }); - spaceIdToNamespace.mockReturnValueOnce('namespace-test'); - mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: '3', - type: 'action_task_params', - attributes: { - actionId: '2', - params: { baz: true }, - apiKey: Buffer.from('123:abc').toString('base64'), - }, - references: [], - }); - - await runner.run(); - - expect(getCreateTaskRunnerFunctionParams.getServices).toHaveBeenCalledWith({ - getBasePath: expect.anything(), - headers: { - // base64 encoded "123:abc" - authorization: 'ApiKey MTIzOmFiYw==', - }, - }); -}); - -test(`doesn't use API key when not provided`, async () => { - const { execute: mockExecute } = jest.requireMock('./execute'); - const createTaskRunner = getCreateTaskRunnerFunction({ - ...getCreateTaskRunnerFunctionParams, - isSecurityEnabled: false, - }); - const runner = createTaskRunner({ taskInstance: taskInstanceMock }); - - mockExecute.mockResolvedValueOnce({ status: 'ok' }); - spaceIdToNamespace.mockReturnValueOnce('namespace-test'); - mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: '3', - type: 'action_task_params', - attributes: { - actionId: '2', - params: { baz: true }, - }, - references: [], - }); - - await runner.run(); - - expect(getCreateTaskRunnerFunctionParams.getServices).toHaveBeenCalledWith({ - getBasePath: expect.anything(), - headers: {}, - }); -}); - -test(`doesn't use API key when provided and isSecurityEnabled is set to false`, async () => { - const { execute: mockExecute } = jest.requireMock('./execute'); - const createTaskRunner = getCreateTaskRunnerFunction({ - ...getCreateTaskRunnerFunctionParams, - isSecurityEnabled: false, - }); - const runner = createTaskRunner({ taskInstance: taskInstanceMock }); - - mockExecute.mockResolvedValueOnce({ status: 'ok' }); - spaceIdToNamespace.mockReturnValueOnce('namespace-test'); - mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: '3', - type: 'action_task_params', - attributes: { - actionId: '2', - params: { baz: true }, - apiKey: Buffer.from('123:abc').toString('base64'), - }, - references: [], - }); - - await runner.run(); - - expect(getCreateTaskRunnerFunctionParams.getServices).toHaveBeenCalledWith({ - getBasePath: expect.anything(), - headers: {}, - }); -}); - -test(`throws an error when isSecurityEnabled is true but key isn't provided`, async () => { - const { execute: mockExecute } = jest.requireMock('./execute'); - const createTaskRunner = getCreateTaskRunnerFunction(getCreateTaskRunnerFunctionParams); - const runner = createTaskRunner({ taskInstance: taskInstanceMock }); - - mockExecute.mockResolvedValueOnce({ status: 'ok' }); - spaceIdToNamespace.mockReturnValueOnce('namespace-test'); - mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: '3', - type: 'action_task_params', - attributes: { - actionId: '2', - params: { baz: true }, - }, - references: [], - }); - - await expect(runner.run()).rejects.toThrowErrorMatchingInlineSnapshot( - `"API key is required. The attribute \\"apiKey\\" is missing."` - ); -}); diff --git a/x-pack/legacy/plugins/actions/server/lib/index.ts b/x-pack/legacy/plugins/actions/server/lib/index.ts index c1cca1f68addb..0667e0548646e 100644 --- a/x-pack/legacy/plugins/actions/server/lib/index.ts +++ b/x-pack/legacy/plugins/actions/server/lib/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export { execute } from './execute'; -export { getCreateTaskRunnerFunction } from './get_create_task_runner_function'; export { ExecutorError } from './executor_error'; export { validateParams, validateConfig, validateSecrets } from './validate_with_schema'; +export { TaskRunnerFactory } from './task_runner_factory'; +export { ActionExecutor, ActionExecutorContract } from './action_executor'; diff --git a/x-pack/legacy/plugins/actions/server/lib/task_runner_factory.test.ts b/x-pack/legacy/plugins/actions/server/lib/task_runner_factory.test.ts new file mode 100644 index 0000000000000..8415adfcbc146 --- /dev/null +++ b/x-pack/legacy/plugins/actions/server/lib/task_runner_factory.test.ts @@ -0,0 +1,323 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import sinon from 'sinon'; +import { ExecutorError } from './executor_error'; +import { ActionExecutor } from './action_executor'; +import { ConcreteTaskInstance, TaskStatus } from '../../../task_manager'; +import { TaskRunnerFactory } from './task_runner_factory'; +import { actionTypeRegistryMock } from '../action_type_registry.mock'; +import { actionExecutorMock } from './action_executor.mock'; +import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/plugin.mock'; +import { + SavedObjectsClientMock, + loggingServiceMock, +} from '../../../../../../src/core/server/mocks'; + +const spaceIdToNamespace = jest.fn(); +const actionTypeRegistry = actionTypeRegistryMock.create(); +const mockedEncryptedSavedObjectsPlugin = encryptedSavedObjectsMock.create(); +const mockedActionExecutor = actionExecutorMock.create(); + +let fakeTimer: sinon.SinonFakeTimers; +let taskRunnerFactory: TaskRunnerFactory; +let mockedTaskInstance: ConcreteTaskInstance; + +beforeAll(() => { + fakeTimer = sinon.useFakeTimers(); + mockedTaskInstance = { + id: '', + runAt: new Date(), + state: {}, + attempts: 0, + ownerId: '', + status: 'running' as TaskStatus, + startedAt: new Date(), + scheduledAt: new Date(), + retryAt: new Date(Date.now() + 5 * 60 * 1000), + params: { + spaceId: 'test', + actionTaskParamsId: '3', + }, + taskType: 'actions:1', + }; + taskRunnerFactory = new TaskRunnerFactory(mockedActionExecutor); + mockedActionExecutor.initialize(actionExecutorInitializerParams); + taskRunnerFactory.initialize(taskRunnerFactoryInitializerParams); +}); + +afterAll(() => fakeTimer.restore()); + +const services = { + log: jest.fn(), + callCluster: jest.fn(), + savedObjectsClient: SavedObjectsClientMock.create(), +}; +const actionExecutorInitializerParams = { + logger: loggingServiceMock.create().get(), + getServices: jest.fn().mockReturnValue(services), + actionTypeRegistry, + spaces: () => undefined, + encryptedSavedObjectsPlugin: mockedEncryptedSavedObjectsPlugin, +}; +const taskRunnerFactoryInitializerParams = { + spaceIdToNamespace, + encryptedSavedObjectsPlugin: mockedEncryptedSavedObjectsPlugin, + getBasePath: jest.fn().mockReturnValue(undefined), + isSecurityEnabled: true, +}; + +beforeEach(() => { + jest.resetAllMocks(); + actionExecutorInitializerParams.getServices.mockReturnValue(services); +}); + +test(`throws an error if factory isn't initialized`, () => { + const factory = new TaskRunnerFactory(new ActionExecutor()); + expect(() => + factory.create({ taskInstance: mockedTaskInstance }) + ).toThrowErrorMatchingInlineSnapshot(`"TaskRunnerFactory not initialized"`); +}); + +test(`throws an error if factory is already initialized`, () => { + const factory = new TaskRunnerFactory(new ActionExecutor()); + factory.initialize(taskRunnerFactoryInitializerParams); + expect(() => + factory.initialize(taskRunnerFactoryInitializerParams) + ).toThrowErrorMatchingInlineSnapshot(`"TaskRunnerFactory already initialized"`); +}); + +test('executes the task by calling the executor with proper parameters', async () => { + const taskRunner = taskRunnerFactory.create({ + taskInstance: mockedTaskInstance, + }); + + mockedActionExecutor.execute.mockResolvedValueOnce({ status: 'ok' }); + spaceIdToNamespace.mockReturnValueOnce('namespace-test'); + mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({ + id: '3', + type: 'action_task_params', + attributes: { + actionId: '2', + params: { baz: true }, + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + + const runnerResult = await taskRunner.run(); + + expect(runnerResult).toBeUndefined(); + expect(spaceIdToNamespace).toHaveBeenCalledWith('test'); + expect(mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser).toHaveBeenCalledWith( + 'action_task_params', + '3', + { namespace: 'namespace-test' } + ); + expect(mockedActionExecutor.execute).toHaveBeenCalledWith({ + actionId: '2', + params: { baz: true }, + request: { + getBasePath: expect.any(Function), + headers: { + // base64 encoded "123:abc" + authorization: 'ApiKey MTIzOmFiYw==', + }, + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, + }, + }); +}); + +test('throws an error with suggested retry logic when return status is error', async () => { + const taskRunner = taskRunnerFactory.create({ + taskInstance: mockedTaskInstance, + }); + + mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({ + id: '3', + type: 'action_task_params', + attributes: { + actionId: '2', + params: { baz: true }, + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + mockedActionExecutor.execute.mockResolvedValueOnce({ + status: 'error', + message: 'Error message', + data: { foo: true }, + retry: false, + }); + + try { + await taskRunner.run(); + throw new Error('Should have thrown'); + } catch (e) { + expect(e instanceof ExecutorError).toEqual(true); + expect(e.data).toEqual({ foo: true }); + expect(e.retry).toEqual(false); + } +}); + +test('uses API key when provided', async () => { + const taskRunner = taskRunnerFactory.create({ + taskInstance: mockedTaskInstance, + }); + + mockedActionExecutor.execute.mockResolvedValueOnce({ status: 'ok' }); + spaceIdToNamespace.mockReturnValueOnce('namespace-test'); + mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({ + id: '3', + type: 'action_task_params', + attributes: { + actionId: '2', + params: { baz: true }, + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + + await taskRunner.run(); + + expect(mockedActionExecutor.execute).toHaveBeenCalledWith({ + actionId: '2', + params: { baz: true }, + request: { + getBasePath: expect.anything(), + headers: { + // base64 encoded "123:abc" + authorization: 'ApiKey MTIzOmFiYw==', + }, + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, + }, + }); +}); + +test(`doesn't use API key when not provided`, async () => { + const factory = new TaskRunnerFactory(mockedActionExecutor); + factory.initialize({ + ...taskRunnerFactoryInitializerParams, + isSecurityEnabled: false, + }); + const taskRunner = factory.create({ taskInstance: mockedTaskInstance }); + + mockedActionExecutor.execute.mockResolvedValueOnce({ status: 'ok' }); + spaceIdToNamespace.mockReturnValueOnce('namespace-test'); + mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({ + id: '3', + type: 'action_task_params', + attributes: { + actionId: '2', + params: { baz: true }, + }, + references: [], + }); + + await taskRunner.run(); + + expect(mockedActionExecutor.execute).toHaveBeenCalledWith({ + actionId: '2', + params: { baz: true }, + request: { + getBasePath: expect.anything(), + headers: {}, + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, + }, + }); +}); + +test(`doesn't use API key when provided and isSecurityEnabled is set to false`, async () => { + const factory = new TaskRunnerFactory(mockedActionExecutor); + factory.initialize({ + ...taskRunnerFactoryInitializerParams, + isSecurityEnabled: false, + }); + const taskRunner = factory.create({ taskInstance: mockedTaskInstance }); + + mockedActionExecutor.execute.mockResolvedValueOnce({ status: 'ok' }); + spaceIdToNamespace.mockReturnValueOnce('namespace-test'); + mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({ + id: '3', + type: 'action_task_params', + attributes: { + actionId: '2', + params: { baz: true }, + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + + await taskRunner.run(); + + expect(mockedActionExecutor.execute).toHaveBeenCalledWith({ + actionId: '2', + params: { baz: true }, + request: { + getBasePath: expect.anything(), + headers: {}, + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, + }, + }); +}); + +test(`throws an error when isSecurityEnabled is true but key isn't provided`, async () => { + const taskRunner = taskRunnerFactory.create({ + taskInstance: mockedTaskInstance, + }); + + mockedActionExecutor.execute.mockResolvedValueOnce({ status: 'ok' }); + spaceIdToNamespace.mockReturnValueOnce('namespace-test'); + mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({ + id: '3', + type: 'action_task_params', + attributes: { + actionId: '2', + params: { baz: true }, + }, + references: [], + }); + + await expect(taskRunner.run()).rejects.toThrowErrorMatchingInlineSnapshot( + `"API key is required. The attribute \\"apiKey\\" is missing."` + ); +}); diff --git a/x-pack/legacy/plugins/actions/server/lib/get_create_task_runner_function.ts b/x-pack/legacy/plugins/actions/server/lib/task_runner_factory.ts similarity index 55% rename from x-pack/legacy/plugins/actions/server/lib/get_create_task_runner_function.ts rename to x-pack/legacy/plugins/actions/server/lib/task_runner_factory.ts index 41073a194ab3c..202c4d0d62a1d 100644 --- a/x-pack/legacy/plugins/actions/server/lib/get_create_task_runner_function.ts +++ b/x-pack/legacy/plugins/actions/server/lib/task_runner_factory.ts @@ -4,42 +4,51 @@ * you may not use this file except in compliance with the Elastic License. */ -import { execute } from './execute'; +import { ActionExecutorContract } from './action_executor'; import { ExecutorError } from './executor_error'; -import { TaskInstance } from '../../../task_manager'; -import { EncryptedSavedObjectsPlugin } from '../../../encrypted_saved_objects'; -import { - ActionTaskParams, - ActionTypeRegistryContract, - GetBasePathFunction, - GetServicesFunction, - SpaceIdToNamespaceFunction, -} from '../types'; +import { RunContext } from '../../../task_manager'; +import { EncryptedSavedObjectsStartContract } from '../shim'; +import { ActionTaskParams, GetBasePathFunction, SpaceIdToNamespaceFunction } from '../types'; -interface CreateTaskRunnerFunctionOptions { - getServices: GetServicesFunction; - actionTypeRegistry: ActionTypeRegistryContract; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin; +export interface TaskRunnerContext { + encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; spaceIdToNamespace: SpaceIdToNamespaceFunction; getBasePath: GetBasePathFunction; isSecurityEnabled: boolean; } -interface TaskRunnerOptions { - taskInstance: TaskInstance; -} +export class TaskRunnerFactory { + private isInitialized = false; + private taskRunnerContext?: TaskRunnerContext; + private readonly actionExecutor: ActionExecutorContract; + + constructor(actionExecutor: ActionExecutorContract) { + this.actionExecutor = actionExecutor; + } + + public initialize(taskRunnerContext: TaskRunnerContext) { + if (this.isInitialized) { + throw new Error('TaskRunnerFactory already initialized'); + } + this.isInitialized = true; + this.taskRunnerContext = taskRunnerContext; + } + + public create({ taskInstance }: RunContext) { + if (!this.isInitialized) { + throw new Error('TaskRunnerFactory not initialized'); + } + + const { actionExecutor } = this; + const { + encryptedSavedObjectsPlugin, + spaceIdToNamespace, + getBasePath, + isSecurityEnabled, + } = this.taskRunnerContext!; -export function getCreateTaskRunnerFunction({ - getServices, - actionTypeRegistry, - encryptedSavedObjectsPlugin, - spaceIdToNamespace, - getBasePath, - isSecurityEnabled, -}: CreateTaskRunnerFunctionOptions) { - return ({ taskInstance }: TaskRunnerOptions) => { return { - run: async () => { + async run() { const { spaceId, actionTaskParamsId } = taskInstance.params; const namespace = spaceIdToNamespace(spaceId); @@ -63,15 +72,22 @@ export function getCreateTaskRunnerFunction({ const fakeRequest: any = { headers: requestHeaders, getBasePath: () => getBasePath(spaceId), + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, }; - const executorResult = await execute({ - namespace, - actionTypeRegistry, - encryptedSavedObjectsPlugin, - actionId, - services: getServices(fakeRequest), + const executorResult = await actionExecutor.execute({ params, + actionId, + request: fakeRequest, }); if (executorResult.status === 'error') { // Task manager error handler only kicks in when an error thrown (at this time) @@ -84,5 +100,5 @@ export function getCreateTaskRunnerFunction({ } }, }; - }; + } } diff --git a/x-pack/legacy/plugins/actions/server/plugin.ts b/x-pack/legacy/plugins/actions/server/plugin.ts new file mode 100644 index 0000000000000..fa2c2320504eb --- /dev/null +++ b/x-pack/legacy/plugins/actions/server/plugin.ts @@ -0,0 +1,195 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Hapi from 'hapi'; +import { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; +import { ActionsConfigType, Services } from './types'; +import { ActionExecutor, TaskRunnerFactory } from './lib'; +import { ActionsClient } from './actions_client'; +import { ActionTypeRegistry } from './action_type_registry'; +import { ExecuteOptions } from './create_execute_function'; +import { createExecuteFunction } from './create_execute_function'; +import { registerBuiltInActionTypes } from './builtin_action_types'; +import { IClusterClient, KibanaRequest, Logger } from '../../../../../src/core/server'; +import { getActionsConfigurationUtilities } from './actions_config'; +import { + ActionsPluginInitializerContext, + ActionsCoreSetup, + ActionsCoreStart, + ActionsPluginsSetup, + ActionsPluginsStart, +} from './shim'; +import { + createActionRoute, + deleteActionRoute, + findActionRoute, + getActionRoute, + updateActionRoute, + listActionTypesRoute, + getExecuteActionRoute, +} from './routes'; + +export interface PluginSetupContract { + registerType: ActionTypeRegistry['register']; +} + +export interface PluginStartContract { + listTypes: ActionTypeRegistry['list']; + execute(options: ExecuteOptions): Promise; + getActionsClientWithRequest(request: Hapi.Request): ActionsClient; +} + +export class Plugin { + private readonly config$: Observable; + private readonly logger: Logger; + private serverBasePath?: string; + private adminClient?: IClusterClient; + private taskRunnerFactory?: TaskRunnerFactory; + private actionTypeRegistry?: ActionTypeRegistry; + private actionExecutor?: ActionExecutor; + + constructor(initializerContext: ActionsPluginInitializerContext) { + this.logger = initializerContext.logger.get('plugins', 'alerting'); + this.config$ = initializerContext.config.create(); + } + + public async setup( + core: ActionsCoreSetup, + plugins: ActionsPluginsSetup + ): Promise { + const config = await this.config$.pipe(first()).toPromise(); + this.adminClient = await core.elasticsearch.adminClient$.pipe(first()).toPromise(); + + plugins.xpack_main.registerFeature({ + id: 'actions', + name: 'Actions', + app: ['actions', 'kibana'], + privileges: { + all: { + savedObject: { + all: ['action', 'action_task_params'], + read: [], + }, + ui: [], + api: ['actions-read', 'actions-all'], + }, + read: { + savedObject: { + all: ['action_task_params'], + read: ['action'], + }, + ui: [], + api: ['actions-read'], + }, + }, + }); + + // Encrypted attributes + // - `secrets` properties will be encrypted + // - `config` will be included in AAD + // - everything else excluded from AAD + plugins.encrypted_saved_objects.registerType({ + type: 'action', + attributesToEncrypt: new Set(['secrets']), + attributesToExcludeFromAAD: new Set(['description']), + }); + plugins.encrypted_saved_objects.registerType({ + type: 'action_task_params', + attributesToEncrypt: new Set(['apiKey']), + }); + + const actionExecutor = new ActionExecutor(); + const taskRunnerFactory = new TaskRunnerFactory(actionExecutor); + const actionTypeRegistry = new ActionTypeRegistry({ + taskRunnerFactory, + taskManager: plugins.task_manager, + }); + this.taskRunnerFactory = taskRunnerFactory; + this.actionTypeRegistry = actionTypeRegistry; + this.serverBasePath = core.http.basePath.serverBasePath; + this.actionExecutor = actionExecutor; + + registerBuiltInActionTypes({ + logger: this.logger, + actionTypeRegistry, + actionsConfigUtils: getActionsConfigurationUtilities(config as ActionsConfigType), + }); + + // Routes + core.http.route(createActionRoute); + core.http.route(deleteActionRoute); + core.http.route(getActionRoute); + core.http.route(findActionRoute); + core.http.route(updateActionRoute); + core.http.route(listActionTypesRoute); + core.http.route(getExecuteActionRoute(actionExecutor)); + + return { + registerType: actionTypeRegistry.register.bind(actionTypeRegistry), + }; + } + + public start(core: ActionsCoreStart, plugins: ActionsPluginsStart): PluginStartContract { + const { + logger, + actionExecutor, + actionTypeRegistry, + adminClient, + serverBasePath, + taskRunnerFactory, + } = this; + + function getServices(request: any): Services { + return { + callCluster: (...args) => + adminClient!.asScoped(KibanaRequest.from(request)).callAsCurrentUser(...args), + savedObjectsClient: core.savedObjects.getScopedSavedObjectsClient(request), + }; + } + function spaceIdToNamespace(spaceId?: string): string | undefined { + const spacesPlugin = plugins.spaces(); + return spacesPlugin && spaceId ? spacesPlugin.spaceIdToNamespace(spaceId) : undefined; + } + function getBasePath(spaceId?: string): string { + const spacesPlugin = plugins.spaces(); + return spacesPlugin && spaceId ? spacesPlugin.getBasePath(spaceId) : serverBasePath!; + } + + actionExecutor!.initialize({ + logger, + spaces: plugins.spaces, + getServices, + encryptedSavedObjectsPlugin: plugins.encrypted_saved_objects, + actionTypeRegistry: actionTypeRegistry!, + }); + taskRunnerFactory!.initialize({ + encryptedSavedObjectsPlugin: plugins.encrypted_saved_objects, + getBasePath, + spaceIdToNamespace, + isSecurityEnabled: !!plugins.security, + }); + + const executeFn = createExecuteFunction({ + taskManager: plugins.task_manager, + getScopedSavedObjectsClient: core.savedObjects.getScopedSavedObjectsClient, + getBasePath, + isSecurityEnabled: !!plugins.security, + }); + + return { + execute: executeFn, + listTypes: actionTypeRegistry!.list.bind(actionTypeRegistry!), + getActionsClientWithRequest(request: Hapi.Request) { + const savedObjectsClient = request.getSavedObjectsClient(); + return new ActionsClient({ + savedObjectsClient, + actionTypeRegistry: actionTypeRegistry!, + }); + }, + }; + } +} diff --git a/x-pack/legacy/plugins/actions/server/routes/_mock_server.ts b/x-pack/legacy/plugins/actions/server/routes/_mock_server.ts index c2bb77192c556..340d341a5ef14 100644 --- a/x-pack/legacy/plugins/actions/server/routes/_mock_server.ts +++ b/x-pack/legacy/plugins/actions/server/routes/_mock_server.ts @@ -38,8 +38,14 @@ export function createMockServer(config: Record = defaultConfig) { server.register({ name: 'actions', register(pluginServer: Hapi.Server) { - pluginServer.expose('registerType', actionTypeRegistry.register); - pluginServer.expose('listTypes', actionTypeRegistry.list); + pluginServer.expose({ + setup: { + registerType: actionTypeRegistry.register.bind(actionTypeRegistry), + }, + start: { + listTypes: actionTypeRegistry.list.bind(actionTypeRegistry), + }, + }); }, }); @@ -59,5 +65,5 @@ export function createMockServer(config: Record = defaultConfig) { server.decorate('request', 'getActionsClient', () => actionsClient); server.decorate('request', 'getBasePath', () => '/s/my-space'); - return { server, savedObjectsClient, actionsClient, actionTypeRegistry }; + return { server, savedObjectsClient, actionsClient, actionTypeRegistry, encryptedSavedObjects }; } diff --git a/x-pack/legacy/plugins/actions/server/routes/create.test.ts b/x-pack/legacy/plugins/actions/server/routes/create.test.ts index 442af6b88d16b..3b3f331fda526 100644 --- a/x-pack/legacy/plugins/actions/server/routes/create.test.ts +++ b/x-pack/legacy/plugins/actions/server/routes/create.test.ts @@ -5,10 +5,10 @@ */ import { createMockServer } from './_mock_server'; -import { createRoute } from './create'; +import { createActionRoute } from './create'; const { server, actionsClient } = createMockServer(); -createRoute(server); +server.route(createActionRoute); beforeEach(() => { jest.resetAllMocks(); diff --git a/x-pack/legacy/plugins/actions/server/routes/create.ts b/x-pack/legacy/plugins/actions/server/routes/create.ts index e41a85e813551..abe077b1f9136 100644 --- a/x-pack/legacy/plugins/actions/server/routes/create.ts +++ b/x-pack/legacy/plugins/actions/server/routes/create.ts @@ -23,31 +23,29 @@ interface CreateRequest extends WithoutQueryAndParams { }; } -export function createRoute(server: Hapi.Server) { - server.route({ - method: 'POST', - path: `/api/action`, - options: { - tags: ['access:actions-all'], - validate: { - options: { - abortEarly: false, - }, - payload: Joi.object() - .keys({ - description: Joi.string().required(), - actionTypeId: Joi.string().required(), - config: Joi.object().default({}), - secrets: Joi.object().default({}), - }) - .required(), +export const createActionRoute = { + method: 'POST', + path: `/api/action`, + config: { + tags: ['access:actions-all'], + validate: { + options: { + abortEarly: false, }, + payload: Joi.object() + .keys({ + description: Joi.string().required(), + actionTypeId: Joi.string().required(), + config: Joi.object().default({}), + secrets: Joi.object().default({}), + }) + .required(), }, - async handler(request: CreateRequest): Promise { - const actionsClient = request.getActionsClient!(); + }, + async handler(request: CreateRequest): Promise { + const actionsClient = request.getActionsClient!(); - const action = request.payload; - return await actionsClient.create({ action }); - }, - }); -} + const action = request.payload; + return await actionsClient.create({ action }); + }, +}; diff --git a/x-pack/legacy/plugins/actions/server/routes/delete.test.ts b/x-pack/legacy/plugins/actions/server/routes/delete.test.ts index a655b804f397f..66241e3f3d876 100644 --- a/x-pack/legacy/plugins/actions/server/routes/delete.test.ts +++ b/x-pack/legacy/plugins/actions/server/routes/delete.test.ts @@ -5,10 +5,10 @@ */ import { createMockServer } from './_mock_server'; -import { deleteRoute } from './delete'; +import { deleteActionRoute } from './delete'; const { server, actionsClient } = createMockServer(); -deleteRoute(server); +server.route(deleteActionRoute); beforeEach(() => { jest.resetAllMocks(); diff --git a/x-pack/legacy/plugins/actions/server/routes/delete.ts b/x-pack/legacy/plugins/actions/server/routes/delete.ts index 5c4d5bcf1cef7..9fce689c09b36 100644 --- a/x-pack/legacy/plugins/actions/server/routes/delete.ts +++ b/x-pack/legacy/plugins/actions/server/routes/delete.ts @@ -13,25 +13,23 @@ interface DeleteRequest extends Hapi.Request { }; } -export function deleteRoute(server: Hapi.Server) { - server.route({ - method: 'DELETE', - path: `/api/action/{id}`, - options: { - tags: ['access:actions-all'], - validate: { - params: Joi.object() - .keys({ - id: Joi.string().required(), - }) - .required(), - }, +export const deleteActionRoute = { + method: 'DELETE', + path: `/api/action/{id}`, + config: { + tags: ['access:actions-all'], + validate: { + params: Joi.object() + .keys({ + id: Joi.string().required(), + }) + .required(), }, - async handler(request: DeleteRequest, h: Hapi.ResponseToolkit) { - const { id } = request.params; - const actionsClient = request.getActionsClient!(); - await actionsClient.delete({ id }); - return h.response().code(204); - }, - }); -} + }, + async handler(request: DeleteRequest, h: Hapi.ResponseToolkit) { + const { id } = request.params; + const actionsClient = request.getActionsClient!(); + await actionsClient.delete({ id }); + return h.response().code(204); + }, +}; diff --git a/x-pack/legacy/plugins/actions/server/routes/execute.test.ts b/x-pack/legacy/plugins/actions/server/routes/execute.test.ts index 9d20e845f9e10..cd5b9c7c4a7e8 100644 --- a/x-pack/legacy/plugins/actions/server/routes/execute.test.ts +++ b/x-pack/legacy/plugins/actions/server/routes/execute.test.ts @@ -4,24 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('../lib/execute', () => ({ - execute: jest.fn(), -})); - import { createMockServer } from './_mock_server'; -import { executeRoute } from './execute'; +import { getExecuteActionRoute } from './execute'; +import { actionExecutorMock } from '../lib/action_executor.mock'; const getServices = jest.fn(); -const { server, actionTypeRegistry } = createMockServer(); -executeRoute({ server, actionTypeRegistry, getServices }); +const { server } = createMockServer(); +const mockedActionExecutor = actionExecutorMock.create(); +server.route(getExecuteActionRoute(mockedActionExecutor)); beforeEach(() => jest.resetAllMocks()); it('executes an action with proper parameters', async () => { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const { execute } = require('../lib/execute'); - const request = { method: 'POST', url: '/api/action/1/_execute', @@ -36,20 +31,15 @@ it('executes an action with proper parameters', async () => { callCluster: jest.fn(), savedObjectsClient: jest.fn(), }); - execute.mockResolvedValueOnce({ success: true }); + mockedActionExecutor.execute.mockResolvedValueOnce({ status: 'ok' }); const { payload, statusCode } = await server.inject(request); expect(statusCode).toBe(200); - expect(payload).toBe('{"success":true}'); + expect(payload).toBe('{"status":"ok"}'); - expect(execute).toHaveBeenCalledTimes(1); - const executeCall = execute.mock.calls[0][0]; - expect(executeCall.params).toEqual({ - foo: true, + expect(mockedActionExecutor.execute).toHaveBeenCalledWith({ + actionId: '1', + params: { foo: true }, + request: expect.anything(), }); - expect(executeCall.actionTypeRegistry).toBeTruthy(); - expect(executeCall.actionId).toBe('1'); - expect(executeCall.namespace).toBeUndefined(); - expect(executeCall.services).toBeTruthy(); - expect(executeCall.encryptedSavedObjectsPlugin).toBeTruthy(); }); diff --git a/x-pack/legacy/plugins/actions/server/routes/execute.ts b/x-pack/legacy/plugins/actions/server/routes/execute.ts index 39288c213e328..00fe7a8a24b58 100644 --- a/x-pack/legacy/plugins/actions/server/routes/execute.ts +++ b/x-pack/legacy/plugins/actions/server/routes/execute.ts @@ -6,8 +6,7 @@ import Joi from 'joi'; import Hapi from 'hapi'; -import { execute } from '../lib'; -import { ActionTypeRegistryContract, GetServicesFunction } from '../types'; +import { ActionExecutorContract } from '../lib'; interface ExecuteRequest extends Hapi.Request { params: { @@ -18,17 +17,11 @@ interface ExecuteRequest extends Hapi.Request { }; } -interface ExecuteRouteOptions { - server: Hapi.Server; - actionTypeRegistry: ActionTypeRegistryContract; - getServices: GetServicesFunction; -} - -export function executeRoute({ server, actionTypeRegistry, getServices }: ExecuteRouteOptions) { - server.route({ +export function getExecuteActionRoute(actionExecutor: ActionExecutorContract) { + return { method: 'POST', path: '/api/action/{id}/_execute', - options: { + config: { tags: ['access:actions-read'], response: { emptyStatusCode: 204, @@ -52,16 +45,11 @@ export function executeRoute({ server, actionTypeRegistry, getServices }: Execut async handler(request: ExecuteRequest, h: Hapi.ResponseToolkit) { const { id } = request.params; const { params } = request.payload; - const namespace = server.plugins.spaces && server.plugins.spaces.getSpaceId(request); - const result = await execute({ + return await actionExecutor.execute({ params, - actionTypeRegistry, + request, actionId: id, - namespace: namespace === 'default' ? undefined : namespace, - services: getServices(request), - encryptedSavedObjectsPlugin: server.plugins.encrypted_saved_objects!, }); - return result; }, - }); + }; } diff --git a/x-pack/legacy/plugins/actions/server/routes/find.test.ts b/x-pack/legacy/plugins/actions/server/routes/find.test.ts index bb02f2c5c922c..147a80bdbba5b 100644 --- a/x-pack/legacy/plugins/actions/server/routes/find.test.ts +++ b/x-pack/legacy/plugins/actions/server/routes/find.test.ts @@ -5,10 +5,10 @@ */ import { createMockServer } from './_mock_server'; -import { findRoute } from './find'; +import { findActionRoute } from './find'; const { server, actionsClient } = createMockServer(); -findRoute(server); +server.route(findActionRoute); beforeEach(() => { jest.resetAllMocks(); diff --git a/x-pack/legacy/plugins/actions/server/routes/find.ts b/x-pack/legacy/plugins/actions/server/routes/find.ts index cbe7ced4e23a2..e521ba174f287 100644 --- a/x-pack/legacy/plugins/actions/server/routes/find.ts +++ b/x-pack/legacy/plugins/actions/server/routes/find.ts @@ -26,63 +26,61 @@ interface FindRequest extends WithoutQueryAndParams { }; } -export function findRoute(server: Hapi.Server) { - server.route({ - method: 'GET', - path: `/api/action/_find`, - options: { - tags: ['access:actions-read'], - validate: { - query: Joi.object() - .keys({ - per_page: Joi.number() - .min(0) - .default(20), - page: Joi.number() - .min(1) - .default(1), - search: Joi.string() - .allow('') - .optional(), - default_search_operator: Joi.string() - .valid('OR', 'AND') - .default('OR'), - search_fields: Joi.array() - .items(Joi.string()) - .single(), - sort_field: Joi.string(), - has_reference: Joi.object() - .keys({ - type: Joi.string().required(), - id: Joi.string().required(), - }) - .optional(), - fields: Joi.array() - .items(Joi.string()) - .single(), - filter: Joi.string() - .allow('') - .optional(), - }) - .default(), - }, - }, - async handler(request: FindRequest) { - const query = request.query; - const actionsClient = request.getActionsClient!(); - return await actionsClient.find({ - options: { - perPage: query.per_page, - page: query.page, - search: query.search, - defaultSearchOperator: query.default_search_operator, - searchFields: query.search_fields, - sortField: query.sort_field, - hasReference: query.has_reference, - fields: query.fields, - filter: query.filter, - }, - }); +export const findActionRoute = { + method: 'GET', + path: `/api/action/_find`, + config: { + tags: ['access:actions-read'], + validate: { + query: Joi.object() + .keys({ + per_page: Joi.number() + .min(0) + .default(20), + page: Joi.number() + .min(1) + .default(1), + search: Joi.string() + .allow('') + .optional(), + default_search_operator: Joi.string() + .valid('OR', 'AND') + .default('OR'), + search_fields: Joi.array() + .items(Joi.string()) + .single(), + sort_field: Joi.string(), + has_reference: Joi.object() + .keys({ + type: Joi.string().required(), + id: Joi.string().required(), + }) + .optional(), + fields: Joi.array() + .items(Joi.string()) + .single(), + filter: Joi.string() + .allow('') + .optional(), + }) + .default(), }, - }); -} + }, + async handler(request: FindRequest) { + const query = request.query; + const actionsClient = request.getActionsClient!(); + return await actionsClient.find({ + options: { + perPage: query.per_page, + page: query.page, + search: query.search, + defaultSearchOperator: query.default_search_operator, + searchFields: query.search_fields, + sortField: query.sort_field, + hasReference: query.has_reference, + fields: query.fields, + filter: query.filter, + }, + }); + }, +}; diff --git a/x-pack/legacy/plugins/actions/server/routes/get.test.ts b/x-pack/legacy/plugins/actions/server/routes/get.test.ts index 8d1949774445d..5d61b3fd448e7 100644 --- a/x-pack/legacy/plugins/actions/server/routes/get.test.ts +++ b/x-pack/legacy/plugins/actions/server/routes/get.test.ts @@ -5,11 +5,11 @@ */ import { createMockServer } from './_mock_server'; -import { getRoute } from './get'; +import { getActionRoute } from './get'; import { ActionResult } from '../types'; const { server, actionsClient } = createMockServer(); -getRoute(server); +server.route(getActionRoute); beforeEach(() => { jest.resetAllMocks(); diff --git a/x-pack/legacy/plugins/actions/server/routes/get.ts b/x-pack/legacy/plugins/actions/server/routes/get.ts index ae66e42bc3f65..d6946bcb54d7f 100644 --- a/x-pack/legacy/plugins/actions/server/routes/get.ts +++ b/x-pack/legacy/plugins/actions/server/routes/get.ts @@ -13,24 +13,22 @@ interface GetRequest extends Hapi.Request { }; } -export function getRoute(server: Hapi.Server) { - server.route({ - method: 'GET', - path: `/api/action/{id}`, - options: { - tags: ['access:actions-read'], - validate: { - params: Joi.object() - .keys({ - id: Joi.string().required(), - }) - .required(), - }, +export const getActionRoute = { + method: 'GET', + path: `/api/action/{id}`, + config: { + tags: ['access:actions-read'], + validate: { + params: Joi.object() + .keys({ + id: Joi.string().required(), + }) + .required(), }, - async handler(request: GetRequest) { - const { id } = request.params; - const actionsClient = request.getActionsClient!(); - return await actionsClient.get({ id }); - }, - }); -} + }, + async handler(request: GetRequest) { + const { id } = request.params; + const actionsClient = request.getActionsClient!(); + return await actionsClient.get({ id }); + }, +}; diff --git a/x-pack/legacy/plugins/actions/server/routes/index.ts b/x-pack/legacy/plugins/actions/server/routes/index.ts index 3056fe7c78d5a..95e2d008d90ac 100644 --- a/x-pack/legacy/plugins/actions/server/routes/index.ts +++ b/x-pack/legacy/plugins/actions/server/routes/index.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -export { createRoute } from './create'; -export { deleteRoute } from './delete'; -export { findRoute } from './find'; -export { getRoute } from './get'; -export { updateRoute } from './update'; +export { createActionRoute } from './create'; +export { deleteActionRoute } from './delete'; +export { findActionRoute } from './find'; +export { getActionRoute } from './get'; +export { updateActionRoute } from './update'; export { listActionTypesRoute } from './list_action_types'; -export { executeRoute } from './execute'; +export { getExecuteActionRoute } from './execute'; diff --git a/x-pack/legacy/plugins/actions/server/routes/list_action_types.test.ts b/x-pack/legacy/plugins/actions/server/routes/list_action_types.test.ts index 3cfda61864040..3bacbe4f0911b 100644 --- a/x-pack/legacy/plugins/actions/server/routes/list_action_types.test.ts +++ b/x-pack/legacy/plugins/actions/server/routes/list_action_types.test.ts @@ -8,7 +8,7 @@ import { createMockServer } from './_mock_server'; import { listActionTypesRoute } from './list_action_types'; const { server, actionTypeRegistry } = createMockServer(); -listActionTypesRoute(server); +server.route(listActionTypesRoute); beforeEach(() => { jest.resetAllMocks(); diff --git a/x-pack/legacy/plugins/actions/server/routes/list_action_types.ts b/x-pack/legacy/plugins/actions/server/routes/list_action_types.ts index fa2be90d25c47..ff545cdf973ad 100644 --- a/x-pack/legacy/plugins/actions/server/routes/list_action_types.ts +++ b/x-pack/legacy/plugins/actions/server/routes/list_action_types.ts @@ -6,15 +6,13 @@ import Hapi from 'hapi'; -export function listActionTypesRoute(server: Hapi.Server) { - server.route({ - method: 'GET', - path: `/api/action/types`, - options: { - tags: ['access:actions-read'], - }, - async handler(request: Hapi.Request) { - return request.server.plugins.actions!.listTypes(); - }, - }); -} +export const listActionTypesRoute = { + method: 'GET', + path: `/api/action/types`, + config: { + tags: ['access:actions-read'], + }, + async handler(request: Hapi.Request) { + return request.server.plugins.actions!.start.listTypes(); + }, +}; diff --git a/x-pack/legacy/plugins/actions/server/routes/update.test.ts b/x-pack/legacy/plugins/actions/server/routes/update.test.ts index a08522ccb9815..e2e7bdb3b90d8 100644 --- a/x-pack/legacy/plugins/actions/server/routes/update.test.ts +++ b/x-pack/legacy/plugins/actions/server/routes/update.test.ts @@ -5,11 +5,11 @@ */ import { createMockServer } from './_mock_server'; -import { updateRoute } from './update'; +import { updateActionRoute } from './update'; import { ActionResult } from '../types'; const { server, actionsClient } = createMockServer(); -updateRoute(server); +server.route(updateActionRoute); beforeEach(() => { jest.resetAllMocks(); diff --git a/x-pack/legacy/plugins/actions/server/routes/update.ts b/x-pack/legacy/plugins/actions/server/routes/update.ts index 4905ae2f2246a..3d7095bf1c8d5 100644 --- a/x-pack/legacy/plugins/actions/server/routes/update.ts +++ b/x-pack/legacy/plugins/actions/server/routes/update.ts @@ -15,35 +15,33 @@ interface UpdateRequest extends Hapi.Request { }; } -export function updateRoute(server: Hapi.Server) { - server.route({ - method: 'PUT', - path: `/api/action/{id}`, - options: { - tags: ['access:actions-all'], - validate: { - options: { - abortEarly: false, - }, - params: Joi.object() - .keys({ - id: Joi.string().required(), - }) - .required(), - payload: Joi.object() - .keys({ - description: Joi.string().required(), - config: Joi.object().default({}), - secrets: Joi.object().default({}), - }) - .required(), +export const updateActionRoute = { + method: 'PUT', + path: `/api/action/{id}`, + config: { + tags: ['access:actions-all'], + validate: { + options: { + abortEarly: false, }, + params: Joi.object() + .keys({ + id: Joi.string().required(), + }) + .required(), + payload: Joi.object() + .keys({ + description: Joi.string().required(), + config: Joi.object().default({}), + secrets: Joi.object().default({}), + }) + .required(), }, - async handler(request: UpdateRequest) { - const { id } = request.params; - const { description, config, secrets } = request.payload; - const actionsClient = request.getActionsClient!(); - return await actionsClient.update({ id, action: { description, config, secrets } }); - }, - }); -} + }, + async handler(request: UpdateRequest) { + const { id } = request.params; + const { description, config, secrets } = request.payload; + const actionsClient = request.getActionsClient!(); + return await actionsClient.update({ id, action: { description, config, secrets } }); + }, +}; diff --git a/x-pack/legacy/plugins/actions/server/shim.ts b/x-pack/legacy/plugins/actions/server/shim.ts new file mode 100644 index 0000000000000..60873ffed0e8e --- /dev/null +++ b/x-pack/legacy/plugins/actions/server/shim.ts @@ -0,0 +1,148 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Hapi from 'hapi'; +import { Legacy } from 'kibana'; +import * as Rx from 'rxjs'; +import { ActionsConfigType } from './types'; +import { TaskManager } from '../../task_manager'; +import { XPackMainPlugin } from '../../xpack_main/xpack_main'; +import KbnServer from '../../../../../src/legacy/server/kbn_server'; +import { SpacesPlugin as SpacesPluginStartContract } from '../../spaces'; +import { EncryptedSavedObjectsPlugin } from '../../encrypted_saved_objects'; +import { PluginSetupContract as SecurityPlugin } from '../../../../plugins/security/server'; +import { + CoreSetup, + LoggerFactory, + SavedObjectsLegacyService, +} from '../../../../../src/core/server'; + +// Extend PluginProperties to indicate which plugins are guaranteed to exist +// due to being marked as dependencies +interface Plugins extends Hapi.PluginProperties { + task_manager: TaskManager; + encrypted_saved_objects: EncryptedSavedObjectsPlugin; +} + +export interface Server extends Legacy.Server { + plugins: Plugins; +} + +/** + * Shim what we're thinking setup and start contracts will look like + */ +export type TaskManagerStartContract = Pick; +export type XPackMainPluginSetupContract = Pick; +export type SecurityPluginSetupContract = Pick; +export type SecurityPluginStartContract = Pick; +export type EncryptedSavedObjectsSetupContract = Pick; +export type TaskManagerSetupContract = Pick< + TaskManager, + 'addMiddleware' | 'registerTaskDefinitions' +>; +export type EncryptedSavedObjectsStartContract = Pick< + EncryptedSavedObjectsPlugin, + 'isEncryptionError' | 'getDecryptedAsInternalUser' +>; + +/** + * New platform interfaces + */ +export interface ActionsPluginInitializerContext { + logger: LoggerFactory; + config: { + create(): Rx.Observable; + }; +} +export interface ActionsCoreSetup { + elasticsearch: CoreSetup['elasticsearch']; + http: { + route: (route: Hapi.ServerRoute) => void; + basePath: { + serverBasePath: string; + }; + }; +} +export interface ActionsCoreStart { + savedObjects: SavedObjectsLegacyService; +} +export interface ActionsPluginsSetup { + security?: SecurityPluginSetupContract; + task_manager: TaskManagerSetupContract; + xpack_main: XPackMainPluginSetupContract; + encrypted_saved_objects: EncryptedSavedObjectsSetupContract; +} +export interface ActionsPluginsStart { + security?: SecurityPluginStartContract; + spaces: () => SpacesPluginStartContract | undefined; + encrypted_saved_objects: EncryptedSavedObjectsStartContract; + task_manager: TaskManagerStartContract; +} + +/** + * Shim + * + * @param server Hapi server instance + */ +export function shim( + server: Server +): { + initializerContext: ActionsPluginInitializerContext; + coreSetup: ActionsCoreSetup; + coreStart: ActionsCoreStart; + pluginsSetup: ActionsPluginsSetup; + pluginsStart: ActionsPluginsStart; +} { + const newPlatform = ((server as unknown) as KbnServer).newPlatform; + + const initializerContext: ActionsPluginInitializerContext = { + logger: newPlatform.coreContext.logger, + config: { + create() { + return Rx.of({ + enabled: server.config().get('xpack.actions.enabled') as boolean, + whitelistedHosts: server.config().get('xpack.actions.whitelistedHosts') as string[], + }) as Rx.Observable; + }, + }, + }; + + const coreSetup: ActionsCoreSetup = { + elasticsearch: newPlatform.setup.core.elasticsearch, + http: { + route: server.route.bind(server), + basePath: newPlatform.setup.core.http.basePath, + }, + }; + + const coreStart: ActionsCoreStart = { + savedObjects: server.savedObjects, + }; + + const pluginsSetup: ActionsPluginsSetup = { + security: newPlatform.setup.plugins.security as SecurityPluginSetupContract | undefined, + task_manager: server.plugins.task_manager, + xpack_main: server.plugins.xpack_main, + encrypted_saved_objects: server.plugins.encrypted_saved_objects, + }; + + const pluginsStart: ActionsPluginsStart = { + security: newPlatform.setup.plugins.security as SecurityPluginStartContract | undefined, + // TODO: Currently a function because it's an optional dependency that + // initializes after this function is called + spaces: () => server.plugins.spaces, + encrypted_saved_objects: server.plugins.encrypted_saved_objects, + task_manager: server.plugins.task_manager, + }; + + return { + initializerContext, + coreSetup, + coreStart, + pluginsSetup, + pluginsStart, + }; +} diff --git a/x-pack/legacy/plugins/actions/server/types.ts b/x-pack/legacy/plugins/actions/server/types.ts index 77a5460253cd7..1ead4bece0c94 100644 --- a/x-pack/legacy/plugins/actions/server/types.ts +++ b/x-pack/legacy/plugins/actions/server/types.ts @@ -6,8 +6,7 @@ import { SavedObjectsClientContract, SavedObjectAttributes } from 'src/core/server'; import { ActionTypeRegistry } from './action_type_registry'; -import { ExecuteOptions } from './create_execute_function'; -import { ActionsKibanaConfig } from './actions_config'; +import { PluginSetupContract, PluginStartContract } from './plugin'; export type WithoutQueryAndParams = Pick>; export type GetServicesFunction = (request: any) => Services; @@ -18,13 +17,16 @@ export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefine export interface Services { callCluster(path: string, opts: any): Promise; savedObjectsClient: SavedObjectsClientContract; - log: (tags: string | string[], data?: string | object | (() => any), timestamp?: number) => void; } export interface ActionsPlugin { - registerType: ActionTypeRegistry['register']; - listTypes: ActionTypeRegistry['list']; - execute(options: ExecuteOptions): Promise; + setup: PluginSetupContract; + start: PluginStartContract; +} + +export interface ActionsConfigType { + enabled: boolean; + whitelistedHosts: string[]; } // the parameters passed to an action type executor function @@ -60,7 +62,7 @@ interface ValidatorType { validate(value: any): any; } -export type ActionTypeCreator = (config?: ActionsKibanaConfig) => ActionType; +export type ActionTypeCreator = (config?: ActionsConfigType) => ActionType; export interface ActionType { id: string; name: string; diff --git a/x-pack/legacy/plugins/alerting/server/lib/create_execution_handler.ts b/x-pack/legacy/plugins/alerting/server/lib/create_execution_handler.ts index 2b562f70c68b9..5e7c91c560a6f 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/create_execution_handler.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/create_execution_handler.ts @@ -5,9 +5,9 @@ */ import { AlertAction, State, Context, AlertType } from '../types'; -import { ActionsPluginStartContract } from '../shim'; import { Logger } from '../../../../../../src/core/server'; import { transformActionParams } from './transform_action_params'; +import { PluginStartContract as ActionsPluginStartContract } from '../../../actions'; interface CreateExecutionHandlerOptions { alertId: string; diff --git a/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.ts b/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.ts index 513f6d25d8371..d546943f9aed7 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.ts @@ -11,7 +11,8 @@ import { createAlertInstanceFactory } from './create_alert_instance_factory'; import { AlertInstance } from './alert_instance'; import { getNextRunAt } from './get_next_run_at'; import { validateAlertTypeParams } from './validate_alert_type_params'; -import { ActionsPluginStartContract, EncryptedSavedObjectsStartContract } from '../shim'; +import { EncryptedSavedObjectsStartContract } from '../shim'; +import { PluginStartContract as ActionsPluginStartContract } from '../../../actions'; import { AlertType, AlertServices, diff --git a/x-pack/legacy/plugins/alerting/server/shim.ts b/x-pack/legacy/plugins/alerting/server/shim.ts index eb21181e0b3a7..78fb0837d1031 100644 --- a/x-pack/legacy/plugins/alerting/server/shim.ts +++ b/x-pack/legacy/plugins/alerting/server/shim.ts @@ -7,7 +7,6 @@ import Hapi from 'hapi'; import { Legacy } from 'kibana'; import { SpacesPlugin as SpacesPluginStartContract } from '../../spaces'; -import { ActionsPlugin } from '../../actions'; import { TaskManager } from '../../task_manager'; import { XPackMainPlugin } from '../../xpack_main/xpack_main'; import KbnServer from '../../../../../src/legacy/server/kbn_server'; @@ -18,6 +17,11 @@ import { LoggerFactory, SavedObjectsLegacyService, } from '../../../../../src/core/server'; +import { + ActionsPlugin, + PluginSetupContract as ActionsPluginSetupContract, + PluginStartContract as ActionsPluginStartContract, +} from '../../actions'; // Extend PluginProperties to indicate which plugins are guaranteed to exist // due to being marked as dependencies @@ -34,8 +38,6 @@ export interface Server extends Legacy.Server { /** * Shim what we're thinking setup and start contracts will look like */ -export type ActionsPluginSetupContract = Pick; -export type ActionsPluginStartContract = Pick; export type TaskManagerStartContract = Pick; export type SecurityPluginSetupContract = Pick; export type SecurityPluginStartContract = Pick; @@ -118,14 +120,14 @@ export function shim( const pluginsSetup: AlertingPluginsSetup = { security: newPlatform.setup.plugins.security as SecurityPluginSetupContract | undefined, task_manager: server.plugins.task_manager, - actions: server.plugins.actions, + actions: server.plugins.actions.setup, xpack_main: server.plugins.xpack_main, encrypted_saved_objects: server.plugins.encrypted_saved_objects, }; const pluginsStart: AlertingPluginsStart = { security: newPlatform.setup.plugins.security as SecurityPluginStartContract | undefined, - actions: server.plugins.actions, + actions: server.plugins.actions.start, // TODO: Currently a function because it's an optional dependency that // initializes after this function is called spaces: () => server.plugins.spaces, diff --git a/x-pack/legacy/plugins/task_manager/index.ts b/x-pack/legacy/plugins/task_manager/index.ts index d7fa097894efb..0fda1490de714 100644 --- a/x-pack/legacy/plugins/task_manager/index.ts +++ b/x-pack/legacy/plugins/task_manager/index.ts @@ -12,7 +12,13 @@ import mappings from './mappings.json'; import { migrations } from './migrations'; export { PluginSetupContract as TaskManager }; -export { TaskInstance, ConcreteTaskInstance, TaskRunCreatorFunction, RunContext } from './task'; +export { + TaskInstance, + ConcreteTaskInstance, + TaskRunCreatorFunction, + TaskStatus, + RunContext, +} from './task'; export function taskManager(kibana: any) { return new kibana.Plugin({ diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts index f920decf708fe..9797e20357176 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts @@ -158,10 +158,10 @@ export default function(kibana: any) { }; }, }; - server.plugins.actions.registerType(indexRecordActionType); - server.plugins.actions.registerType(failingActionType); - server.plugins.actions.registerType(rateLimitedActionType); - server.plugins.actions.registerType(authorizationActionType); + server.plugins.actions.setup.registerType(indexRecordActionType); + server.plugins.actions.setup.registerType(failingActionType); + server.plugins.actions.setup.registerType(rateLimitedActionType); + server.plugins.actions.setup.registerType(authorizationActionType); // Alert types const alwaysFiringAlertType: AlertType = { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/server_log.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/server_log.ts index cefdc1fc15f70..741f006e64bfa 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/server_log.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/server_log.ts @@ -56,6 +56,7 @@ export default function serverLogTest({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .send({ params: { + level: 'info', message: 'message posted by firing an action during a test', }, })