diff --git a/x-pack/plugins/actions/server/action_type_registry.mock.ts b/x-pack/plugins/actions/server/action_type_registry.mock.ts index 5589a15932ecf..6a806d1fa531c 100644 --- a/x-pack/plugins/actions/server/action_type_registry.mock.ts +++ b/x-pack/plugins/actions/server/action_type_registry.mock.ts @@ -13,6 +13,7 @@ const createActionTypeRegistryMock = () => { get: jest.fn(), list: jest.fn(), ensureActionTypeEnabled: jest.fn(), + isActionTypeEnabled: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/actions/server/action_type_registry.test.ts b/x-pack/plugins/actions/server/action_type_registry.test.ts index 690a5ef131816..4f3fefff8392f 100644 --- a/x-pack/plugins/actions/server/action_type_registry.test.ts +++ b/x-pack/plugins/actions/server/action_type_registry.test.ts @@ -168,6 +168,48 @@ describe('has()', () => { }); }); +describe('isActionTypeEnabled', () => { + let actionTypeRegistry: ActionTypeRegistry; + const fooActionType: ActionType = { + id: 'foo', + name: 'Foo', + minimumLicenseRequired: 'basic', + executor: async () => {}, + }; + + beforeEach(() => { + actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); + actionTypeRegistry.register(fooActionType); + }); + + test('should call isActionTypeEnabled of the actions config', async () => { + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + actionTypeRegistry.isActionTypeEnabled('foo'); + expect(mockedActionsConfig.isActionTypeEnabled).toHaveBeenCalledWith('foo'); + }); + + test('should call isLicenseValidForActionType of the license state', async () => { + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + actionTypeRegistry.isActionTypeEnabled('foo'); + expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalledWith(fooActionType); + }); + + test('should return false when isActionTypeEnabled is false and isLicenseValidForActionType is true', async () => { + mockedActionsConfig.isActionTypeEnabled.mockReturnValue(false); + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + expect(actionTypeRegistry.isActionTypeEnabled('foo')).toEqual(false); + }); + + test('should return false when isActionTypeEnabled is true and isLicenseValidForActionType is false', async () => { + mockedActionsConfig.isActionTypeEnabled.mockReturnValue(true); + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ + isValid: false, + reason: 'invalid', + }); + expect(actionTypeRegistry.isActionTypeEnabled('foo')).toEqual(false); + }); +}); + describe('ensureActionTypeEnabled', () => { let actionTypeRegistry: ActionTypeRegistry; const fooActionType: ActionType = { diff --git a/x-pack/plugins/actions/server/action_type_registry.ts b/x-pack/plugins/actions/server/action_type_registry.ts index e95e7e504dc1f..884aa9ac3ad32 100644 --- a/x-pack/plugins/actions/server/action_type_registry.ts +++ b/x-pack/plugins/actions/server/action_type_registry.ts @@ -48,6 +48,16 @@ export class ActionTypeRegistry { this.licenseState.ensureLicenseForActionType(this.get(id)); } + /** + * Returns true if action type is enabled in the config and a valid license is used. + */ + public isActionTypeEnabled(id: string) { + return ( + this.actionsConfigUtils.isActionTypeEnabled(id) && + this.licenseState.isLicenseValidForActionType(this.get(id)).isValid === true + ); + } + /** * Registers an action type to the action type registry */ diff --git a/x-pack/plugins/actions/server/create_execute_function.test.ts b/x-pack/plugins/actions/server/create_execute_function.test.ts index 6d2a234639532..68c3967359ff4 100644 --- a/x-pack/plugins/actions/server/create_execute_function.test.ts +++ b/x-pack/plugins/actions/server/create_execute_function.test.ts @@ -7,6 +7,7 @@ import { taskManagerMock } from '../../task_manager/server/task_manager.mock'; import { createExecuteFunction } from './create_execute_function'; import { savedObjectsClientMock } from '../../../../src/core/server/mocks'; +import { actionTypeRegistryMock } from './action_type_registry.mock'; const mockTaskManager = taskManagerMock.start(); const savedObjectsClient = savedObjectsClientMock.create(); @@ -19,6 +20,7 @@ describe('execute()', () => { const executeFn = createExecuteFunction({ getBasePath, taskManager: mockTaskManager, + actionTypeRegistry: actionTypeRegistryMock.create(), getScopedSavedObjectsClient: jest.fn().mockReturnValueOnce(savedObjectsClient), isESOUsingEphemeralEncryptionKey: false, }); @@ -73,6 +75,7 @@ describe('execute()', () => { taskManager: mockTaskManager, getScopedSavedObjectsClient, isESOUsingEphemeralEncryptionKey: false, + actionTypeRegistry: actionTypeRegistryMock.create(), }); savedObjectsClient.get.mockResolvedValueOnce({ id: '123', @@ -121,6 +124,7 @@ describe('execute()', () => { taskManager: mockTaskManager, getScopedSavedObjectsClient, isESOUsingEphemeralEncryptionKey: false, + actionTypeRegistry: actionTypeRegistryMock.create(), }); savedObjectsClient.get.mockResolvedValueOnce({ id: '123', @@ -166,6 +170,7 @@ describe('execute()', () => { taskManager: mockTaskManager, getScopedSavedObjectsClient, isESOUsingEphemeralEncryptionKey: true, + actionTypeRegistry: actionTypeRegistryMock.create(), }); await expect( executeFn({ @@ -178,4 +183,36 @@ describe('execute()', () => { `"Unable to execute action due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml"` ); }); + + test('should ensure action type is enabled', async () => { + const mockedActionTypeRegistry = actionTypeRegistryMock.create(); + const getScopedSavedObjectsClient = jest.fn().mockReturnValueOnce(savedObjectsClient); + const executeFn = createExecuteFunction({ + getBasePath, + taskManager: mockTaskManager, + getScopedSavedObjectsClient, + isESOUsingEphemeralEncryptionKey: false, + actionTypeRegistry: mockedActionTypeRegistry, + }); + mockedActionTypeRegistry.ensureActionTypeEnabled.mockImplementation(() => { + throw new Error('Fail'); + }); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '123', + type: 'action', + attributes: { + actionTypeId: 'mock-action', + }, + references: [], + }); + + await expect( + executeFn({ + id: '123', + params: { baz: false }, + spaceId: 'default', + apiKey: null, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Fail"`); + }); }); diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts index 5316e833f33d9..4bbcda4cba7fc 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -6,13 +6,14 @@ import { SavedObjectsClientContract } from '../../../../src/core/server'; import { TaskManagerStartContract } from '../../task_manager/server'; -import { GetBasePathFunction, RawAction } from './types'; +import { GetBasePathFunction, RawAction, ActionTypeRegistryContract } from './types'; interface CreateExecuteFunctionOptions { taskManager: TaskManagerStartContract; getScopedSavedObjectsClient: (request: any) => SavedObjectsClientContract; getBasePath: GetBasePathFunction; isESOUsingEphemeralEncryptionKey: boolean; + actionTypeRegistry: ActionTypeRegistryContract; } export interface ExecuteOptions { @@ -25,6 +26,7 @@ export interface ExecuteOptions { export function createExecuteFunction({ getBasePath, taskManager, + actionTypeRegistry, getScopedSavedObjectsClient, isESOUsingEphemeralEncryptionKey, }: CreateExecuteFunctionOptions) { @@ -60,6 +62,9 @@ export function createExecuteFunction({ const savedObjectsClient = getScopedSavedObjectsClient(fakeRequest); const actionSavedObject = await savedObjectsClient.get('action', id); + + actionTypeRegistry.ensureActionTypeEnabled(actionSavedObject.attributes.actionTypeId); + const actionTaskParamsRecord = await savedObjectsClient.create('action_task_params', { actionId: id, params, diff --git a/x-pack/plugins/actions/server/lib/license_state.mock.ts b/x-pack/plugins/actions/server/lib/license_state.mock.ts index 6b9b6d0dda02a..72a21f878a150 100644 --- a/x-pack/plugins/actions/server/lib/license_state.mock.ts +++ b/x-pack/plugins/actions/server/lib/license_state.mock.ts @@ -12,6 +12,7 @@ export const createLicenseStateMock = () => { clean: jest.fn(), getLicenseInformation: jest.fn(), ensureLicenseForActionType: jest.fn(), + isLicenseValidForActionType: jest.fn(), checkLicense: jest.fn().mockResolvedValue({ state: LICENSE_CHECK_STATE.Valid, }), diff --git a/x-pack/plugins/actions/server/lib/license_state.test.ts b/x-pack/plugins/actions/server/lib/license_state.test.ts index d3522cb33e56a..ba1fbcb83464a 100644 --- a/x-pack/plugins/actions/server/lib/license_state.test.ts +++ b/x-pack/plugins/actions/server/lib/license_state.test.ts @@ -52,6 +52,67 @@ describe('checkLicense()', () => { }); }); +describe('isLicenseValidForActionType', () => { + let license: BehaviorSubject; + let licenseState: ILicenseState; + const fooActionType: ActionType = { + id: 'foo', + name: 'Foo', + minimumLicenseRequired: 'gold', + executor: async () => {}, + }; + + beforeEach(() => { + license = new BehaviorSubject(null as any); + licenseState = new LicenseState(license); + }); + + test('should return false when license not defined', () => { + expect(licenseState.isLicenseValidForActionType(fooActionType)).toEqual({ + isValid: false, + reason: 'unavailable', + }); + }); + + test('should return false when license not available', () => { + license.next({ isAvailable: false } as any); + expect(licenseState.isLicenseValidForActionType(fooActionType)).toEqual({ + isValid: false, + reason: 'unavailable', + }); + }); + + test('should return false when license is expired', () => { + const expiredLicense = licensingMock.createLicense({ license: { status: 'expired' } }); + license.next(expiredLicense); + expect(licenseState.isLicenseValidForActionType(fooActionType)).toEqual({ + isValid: false, + reason: 'expired', + }); + }); + + test('should return false when license is invalid', () => { + const basicLicense = licensingMock.createLicense({ + license: { status: 'active', type: 'basic' }, + }); + license.next(basicLicense); + expect(licenseState.isLicenseValidForActionType(fooActionType)).toEqual({ + isValid: false, + reason: 'invalid', + }); + }); + + test('should return true when license is valid', () => { + const goldLicense = licensingMock.createLicense({ + license: { status: 'active', type: 'gold' }, + }); + license.next(goldLicense); + expect(licenseState.isLicenseValidForActionType(fooActionType)).toEqual({ + isValid: true, + }); + }); +}); + describe('ensureLicenseForActionType()', () => { let license: BehaviorSubject; let licenseState: ILicenseState; diff --git a/x-pack/plugins/actions/server/lib/license_state.ts b/x-pack/plugins/actions/server/lib/license_state.ts index 1b79b5ecdbb9c..9d87818805dcf 100644 --- a/x-pack/plugins/actions/server/lib/license_state.ts +++ b/x-pack/plugins/actions/server/lib/license_state.ts @@ -42,46 +42,68 @@ export class LicenseState { return this.licenseInformation; } - public ensureLicenseForActionType(actionType: ActionType) { + public isLicenseValidForActionType( + actionType: ActionType + ): { isValid: true } | { isValid: false; reason: 'unavailable' | 'expired' | 'invalid' } { if (!this.license?.isAvailable) { - throw new ActionTypeDisabledError( - i18n.translate('xpack.actions.serverSideErrors.unavailableLicenseErrorMessage', { - defaultMessage: - 'Action type {actionTypeId} is disabled because license information is not available at this time.', - values: { - actionTypeId: actionType.id, - }, - }), - 'license_unavailable' - ); + return { isValid: false, reason: 'unavailable' }; } const check = this.license.check(actionType.id, actionType.minimumLicenseRequired); switch (check.state) { case LICENSE_CHECK_STATE.Expired: + return { isValid: false, reason: 'expired' }; + case LICENSE_CHECK_STATE.Invalid: + return { isValid: false, reason: 'invalid' }; + case LICENSE_CHECK_STATE.Unavailable: + return { isValid: false, reason: 'unavailable' }; + case LICENSE_CHECK_STATE.Valid: + return { isValid: true }; + default: + return assertNever(check.state); + } + } + + public ensureLicenseForActionType(actionType: ActionType) { + const check = this.isLicenseValidForActionType(actionType); + + if (check.isValid) { + return; + } + + switch (check.reason) { + case 'unavailable': + throw new ActionTypeDisabledError( + i18n.translate('xpack.actions.serverSideErrors.unavailableLicenseErrorMessage', { + defaultMessage: + 'Action type {actionTypeId} is disabled because license information is not available at this time.', + values: { + actionTypeId: actionType.id, + }, + }), + 'license_unavailable' + ); + case 'expired': throw new ActionTypeDisabledError( i18n.translate('xpack.actions.serverSideErrors.expirerdLicenseErrorMessage', { defaultMessage: 'Action type {actionTypeId} is disabled because your {licenseType} license has expired.', - values: { actionTypeId: actionType.id, licenseType: this.license.type }, + values: { actionTypeId: actionType.id, licenseType: this.license!.type }, }), 'license_expired' ); - case LICENSE_CHECK_STATE.Invalid: - case LICENSE_CHECK_STATE.Unavailable: + case 'invalid': throw new ActionTypeDisabledError( i18n.translate('xpack.actions.serverSideErrors.invalidLicenseErrorMessage', { defaultMessage: 'Action type {actionTypeId} is disabled because your {licenseType} license does not support it. Please upgrade your license.', - values: { actionTypeId: actionType.id, licenseType: this.license.type }, + values: { actionTypeId: actionType.id, licenseType: this.license!.type }, }), 'license_invalid' ); - case LICENSE_CHECK_STATE.Valid: - break; default: - return assertNever(check.state); + assertNever(check.reason); } } diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts index 6be5e1f79ee82..43882cef21170 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts @@ -14,6 +14,7 @@ import { actionExecutorMock } from './action_executor.mock'; import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks'; import { savedObjectsClientMock, loggingServiceMock } from 'src/core/server/mocks'; import { eventLoggerMock } from '../../../event_log/server/mocks'; +import { ActionTypeDisabledError } from './errors'; const spaceIdToNamespace = jest.fn(); const actionTypeRegistry = actionTypeRegistryMock.create(); @@ -63,6 +64,7 @@ const actionExecutorInitializerParams = { }; const taskRunnerFactoryInitializerParams = { spaceIdToNamespace, + actionTypeRegistry, logger: loggingServiceMock.create().get(), encryptedSavedObjectsPlugin: mockedEncryptedSavedObjectsPlugin, getBasePath: jest.fn().mockReturnValue(undefined), @@ -308,3 +310,32 @@ test(`doesn't use API key when not provided`, async () => { }, }); }); + +test(`throws an error when license doesn't support the action type`, 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.mockImplementation(() => { + throw new ActionTypeDisabledError('Fail', 'license_invalid'); + }); + + try { + await taskRunner.run(); + throw new Error('Should have thrown'); + } catch (e) { + expect(e instanceof ExecutorError).toEqual(true); + expect(e.data).toEqual({}); + expect(e.retry).toEqual(false); + } +}); diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.ts index c78b43f4ef3ba..e2a6128aea203 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.ts @@ -9,10 +9,18 @@ import { ExecutorError } from './executor_error'; import { Logger, CoreStart } from '../../../../../src/core/server'; import { RunContext } from '../../../task_manager/server'; import { EncryptedSavedObjectsPluginStart } from '../../../encrypted_saved_objects/server'; -import { ActionTaskParams, GetBasePathFunction, SpaceIdToNamespaceFunction } from '../types'; +import { ActionTypeDisabledError } from './errors'; +import { + ActionTaskParams, + ActionTypeRegistryContract, + GetBasePathFunction, + SpaceIdToNamespaceFunction, + ActionTypeExecutorResult, +} from '../types'; export interface TaskRunnerContext { logger: Logger; + actionTypeRegistry: ActionTypeRegistryContract; encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; spaceIdToNamespace: SpaceIdToNamespaceFunction; getBasePath: GetBasePathFunction; @@ -85,11 +93,20 @@ export class TaskRunnerFactory { }, }; - const executorResult = await actionExecutor.execute({ - params, - actionId, - request: fakeRequest, - }); + let executorResult: ActionTypeExecutorResult; + try { + executorResult = await actionExecutor.execute({ + params, + actionId, + request: fakeRequest, + }); + } catch (e) { + if (e instanceof ActionTypeDisabledError) { + // We'll stop re-trying due to action being forbidden + throw new ExecutorError(e.message, {}, false); + } + throw e; + } if (executorResult.status === 'error') { // Task manager error handler only kicks in when an error thrown (at this time) diff --git a/x-pack/plugins/actions/server/mocks.ts b/x-pack/plugins/actions/server/mocks.ts index 1f68d8d4a3a69..75396f2aad897 100644 --- a/x-pack/plugins/actions/server/mocks.ts +++ b/x-pack/plugins/actions/server/mocks.ts @@ -19,6 +19,7 @@ const createSetupMock = () => { const createStartMock = () => { const mock: jest.Mocked = { execute: jest.fn(), + isActionTypeEnabled: jest.fn(), getActionsClientWithRequest: jest.fn().mockResolvedValue(actionsClientMock.create()), }; return mock; diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 2b6cb0f13c34c..59f5b7fff8207 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -61,6 +61,7 @@ export interface PluginSetupContract { } export interface PluginStartContract { + isActionTypeEnabled(id: string): boolean; execute(options: ExecuteOptions): Promise; getActionsClientWithRequest(request: KibanaRequest): Promise>; } @@ -216,6 +217,7 @@ export class ActionsPlugin implements Plugin, Plugi taskRunnerFactory!.initialize({ logger, + actionTypeRegistry: actionTypeRegistry!, encryptedSavedObjectsPlugin: plugins.encryptedSavedObjects, getBasePath: this.getBasePath, spaceIdToNamespace: this.spaceIdToNamespace, @@ -225,10 +227,14 @@ export class ActionsPlugin implements Plugin, Plugi return { execute: createExecuteFunction({ taskManager: plugins.taskManager, + actionTypeRegistry: actionTypeRegistry!, getScopedSavedObjectsClient: core.savedObjects.getScopedClient, getBasePath: this.getBasePath, isESOUsingEphemeralEncryptionKey: isESOUsingEphemeralEncryptionKey!, }), + isActionTypeEnabled: id => { + return this.actionTypeRegistry!.isActionTypeEnabled(id); + }, // Ability to get an actions client from legacy code async getActionsClientWithRequest(request: KibanaRequest) { if (isESOUsingEphemeralEncryptionKey === true) { diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index bed163878b5ac..4db8a5b348c42 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -176,7 +176,7 @@ export class AlertingPlugin { logger, getServices: this.getServicesFactory(core.savedObjects), spaceIdToNamespace: this.spaceIdToNamespace, - executeAction: plugins.actions.execute, + actionsPlugin: plugins.actions, encryptedSavedObjectsPlugin: plugins.encryptedSavedObjects, getBasePath: this.getBasePath, }); diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts index 32299680a1f8c..78822bdace24d 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts @@ -7,6 +7,7 @@ import { AlertType } from '../types'; import { createExecutionHandler } from './create_execution_handler'; import { loggingServiceMock } from '../../../../../src/core/server/mocks'; +import { actionsMock } from '../../../actions/server/mocks'; const alertType: AlertType = { id: 'test', @@ -20,7 +21,7 @@ const alertType: AlertType = { }; const createExecutionHandlerParams = { - executeAction: jest.fn(), + actionsPlugin: actionsMock.createStart(), spaceId: 'default', alertId: '1', apiKey: 'MTIzOmFiYw==', @@ -42,9 +43,12 @@ const createExecutionHandlerParams = { ], }; -beforeEach(() => jest.resetAllMocks()); +beforeEach(() => { + jest.resetAllMocks(); + createExecutionHandlerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); +}); -test('calls executeAction per selected action', async () => { +test('calls actionsPlugin.execute per selected action', async () => { const executionHandler = createExecutionHandler(createExecutionHandlerParams); await executionHandler({ actionGroup: 'default', @@ -52,8 +56,8 @@ test('calls executeAction per selected action', async () => { context: {}, alertInstanceId: '2', }); - expect(createExecutionHandlerParams.executeAction).toHaveBeenCalledTimes(1); - expect(createExecutionHandlerParams.executeAction.mock.calls[0]).toMatchInlineSnapshot(` + expect(createExecutionHandlerParams.actionsPlugin.execute).toHaveBeenCalledTimes(1); + expect(createExecutionHandlerParams.actionsPlugin.execute.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "apiKey": "MTIzOmFiYw==", @@ -69,7 +73,46 @@ test('calls executeAction per selected action', async () => { `); }); -test('limits executeAction per action group', async () => { +test(`doesn't call actionsPlugin.execute for disabled actionTypes`, async () => { + // Mock two calls, one for check against actions[0] and the second for actions[1] + createExecutionHandlerParams.actionsPlugin.isActionTypeEnabled.mockReturnValueOnce(false); + createExecutionHandlerParams.actionsPlugin.isActionTypeEnabled.mockReturnValueOnce(true); + const executionHandler = createExecutionHandler({ + ...createExecutionHandlerParams, + actions: [ + ...createExecutionHandlerParams.actions, + { + id: '2', + group: 'default', + actionTypeId: 'test2', + params: { + foo: true, + contextVal: 'My other {{context.value}} goes here', + stateVal: 'My other {{state.value}} goes here', + }, + }, + ], + }); + await executionHandler({ + actionGroup: 'default', + state: {}, + context: {}, + alertInstanceId: '2', + }); + expect(createExecutionHandlerParams.actionsPlugin.execute).toHaveBeenCalledTimes(1); + expect(createExecutionHandlerParams.actionsPlugin.execute).toHaveBeenCalledWith({ + id: '2', + params: { + foo: true, + contextVal: 'My other goes here', + stateVal: 'My other goes here', + }, + spaceId: 'default', + apiKey: createExecutionHandlerParams.apiKey, + }); +}); + +test('limits actionsPlugin.execute per action group', async () => { const executionHandler = createExecutionHandler(createExecutionHandlerParams); await executionHandler({ actionGroup: 'other-group', @@ -77,7 +120,7 @@ test('limits executeAction per action group', async () => { context: {}, alertInstanceId: '2', }); - expect(createExecutionHandlerParams.executeAction).toMatchInlineSnapshot(`[MockFunction]`); + expect(createExecutionHandlerParams.actionsPlugin.execute).not.toHaveBeenCalled(); }); test('context attribute gets parameterized', async () => { @@ -88,8 +131,8 @@ test('context attribute gets parameterized', async () => { state: {}, alertInstanceId: '2', }); - expect(createExecutionHandlerParams.executeAction).toHaveBeenCalledTimes(1); - expect(createExecutionHandlerParams.executeAction.mock.calls[0]).toMatchInlineSnapshot(` + expect(createExecutionHandlerParams.actionsPlugin.execute).toHaveBeenCalledTimes(1); + expect(createExecutionHandlerParams.actionsPlugin.execute.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "apiKey": "MTIzOmFiYw==", @@ -113,8 +156,8 @@ test('state attribute gets parameterized', async () => { state: { value: 'state-val' }, alertInstanceId: '2', }); - expect(createExecutionHandlerParams.executeAction).toHaveBeenCalledTimes(1); - expect(createExecutionHandlerParams.executeAction.mock.calls[0]).toMatchInlineSnapshot(` + expect(createExecutionHandlerParams.actionsPlugin.execute).toHaveBeenCalledTimes(1); + expect(createExecutionHandlerParams.actionsPlugin.execute.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "apiKey": "MTIzOmFiYw==", diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts index 9b2d218114c31..1246c55866ed1 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts @@ -12,7 +12,7 @@ import { PluginStartContract as ActionsPluginStartContract } from '../../../../p interface CreateExecutionHandlerOptions { alertId: string; - executeAction: ActionsPluginStartContract['execute']; + actionsPlugin: ActionsPluginStartContract; actions: AlertAction[]; spaceId: string; apiKey: string | null; @@ -30,7 +30,7 @@ interface ExecutionHandlerOptions { export function createExecutionHandler({ logger, alertId, - executeAction, + actionsPlugin, actions: alertActions, spaceId, apiKey, @@ -57,12 +57,18 @@ export function createExecutionHandler({ }; }); for (const action of actions) { - await executeAction({ - id: action.id, - params: action.params, - spaceId, - apiKey, - }); + if (actionsPlugin.isActionTypeEnabled(action.actionTypeId)) { + await actionsPlugin.execute({ + id: action.id, + params: action.params, + spaceId, + apiKey, + }); + } else { + logger.warn( + `Alert "${alertId}" skipped scheduling action "${action.id}" because it is disabled` + ); + } } }; } diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index d1bc0de3ae0e2..5f4669f64f09d 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -12,6 +12,8 @@ import { TaskRunnerContext } from './task_runner_factory'; import { TaskRunner } from './task_runner'; import { encryptedSavedObjectsMock } from '../../../../plugins/encrypted_saved_objects/server/mocks'; import { savedObjectsClientMock, loggingServiceMock } from '../../../../../src/core/server/mocks'; +import { PluginStartContract as ActionsPluginStart } from '../../../actions/server'; +import { actionsMock } from '../../../actions/server/mocks'; const alertType = { id: 'test', @@ -55,9 +57,11 @@ describe('Task Runner', () => { savedObjectsClient, }; - const taskRunnerFactoryInitializerParams: jest.Mocked = { + const taskRunnerFactoryInitializerParams: jest.Mocked & { + actionsPlugin: jest.Mocked; + } = { getServices: jest.fn().mockReturnValue(services), - executeAction: jest.fn(), + actionsPlugin: actionsMock.createStart(), encryptedSavedObjectsPlugin, logger: loggingServiceMock.create().get(), spaceIdToNamespace: jest.fn().mockReturnValue(undefined), @@ -154,7 +158,8 @@ describe('Task Runner', () => { expect(call.services).toBeTruthy(); }); - test('executeAction is called per alert instance that is scheduled', async () => { + test('actionsPlugin.execute is called per alert instance that is scheduled', async () => { + taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); alertType.executor.mockImplementation( ({ services: executorServices }: AlertExecutorOptions) => { executorServices.alertInstanceFactory('1').scheduleActions('default'); @@ -175,8 +180,9 @@ describe('Task Runner', () => { references: [], }); await taskRunner.run(); - expect(taskRunnerFactoryInitializerParams.executeAction).toHaveBeenCalledTimes(1); - expect(taskRunnerFactoryInitializerParams.executeAction.mock.calls[0]).toMatchInlineSnapshot(` + expect(taskRunnerFactoryInitializerParams.actionsPlugin.execute).toHaveBeenCalledTimes(1); + expect(taskRunnerFactoryInitializerParams.actionsPlugin.execute.mock.calls[0]) + .toMatchInlineSnapshot(` Array [ Object { "apiKey": "MTIzOmFiYw==", diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index c2a3fbcf38069..f8429cbb23ef5 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -115,7 +115,7 @@ export class TaskRunner { return createExecutionHandler({ alertId, logger: this.logger, - executeAction: this.context.executeAction, + actionsPlugin: this.context.actionsPlugin, apiKey, actions: actionsWithIds, spaceId, diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts index f885b0bdbd046..fc34cacba2818 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts @@ -9,6 +9,7 @@ import { ConcreteTaskInstance, TaskStatus } from '../../../../plugins/task_manag import { TaskRunnerContext, TaskRunnerFactory } from './task_runner_factory'; import { encryptedSavedObjectsMock } from '../../../../plugins/encrypted_saved_objects/server/mocks'; import { savedObjectsClientMock, loggingServiceMock } from '../../../../../src/core/server/mocks'; +import { actionsMock } from '../../../actions/server/mocks'; const alertType = { id: 'test', @@ -56,7 +57,7 @@ describe('Task Runner Factory', () => { const taskRunnerFactoryInitializerParams: jest.Mocked = { getServices: jest.fn().mockReturnValue(services), - executeAction: jest.fn(), + actionsPlugin: actionsMock.createStart(), encryptedSavedObjectsPlugin, logger: loggingServiceMock.create().get(), spaceIdToNamespace: jest.fn().mockReturnValue(undefined), diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts index c598b0f52f197..3bad4e475ff49 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts @@ -18,7 +18,7 @@ import { TaskRunner } from './task_runner'; export interface TaskRunnerContext { logger: Logger; getServices: GetServicesFunction; - executeAction: ActionsPluginStartContract['execute']; + actionsPlugin: ActionsPluginStartContract; encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; spaceIdToNamespace: SpaceIdToNamespaceFunction; getBasePath: GetBasePathFunction;