From b21d4bd6adf22a8ce38d1011b6235ac3cd35e248 Mon Sep 17 00:00:00 2001 From: Mike Cote <mikecote@users.noreply.github.com> Date: Fri, 24 Apr 2020 10:26:33 -0400 Subject: [PATCH 1/6] Initial work --- .../server/builtin_action_types/email.test.ts | 9 ++------- .../builtin_action_types/es_index.test.ts | 9 ++------- .../builtin_action_types/pagerduty.test.ts | 7 ++----- .../builtin_action_types/server_log.test.ts | 7 ++----- .../servicenow/index.test.ts | 7 ++----- .../server/builtin_action_types/slack.test.ts | 7 ++----- .../builtin_action_types/webhook.test.ts | 7 ++----- .../actions/server/lib/action_executor.test.ts | 18 ++++++------------ x-pack/plugins/actions/server/mocks.ts | 14 ++++++++++++++ x-pack/plugins/actions/server/plugin.ts | 8 ++++++++ x-pack/plugins/actions/server/types.ts | 8 +++++++- x-pack/plugins/alerting/server/mocks.ts | 1 + x-pack/plugins/alerting/server/plugin.ts | 8 ++++++++ .../server/task_runner/task_runner.test.ts | 11 ++++------- .../task_runner/task_runner_factory.test.ts | 10 +++------- x-pack/plugins/alerting/server/types.ts | 8 +++++++- 16 files changed, 72 insertions(+), 67 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts index 658f8f3fd8cf9..967c302d24ee2 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts @@ -9,13 +9,13 @@ jest.mock('./lib/send_email', () => ({ })); import { Logger } from '../../../../../src/core/server'; -import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; import { ActionType, ActionTypeExecutorOptions } from '../types'; import { actionsConfigMock } from '../actions_config.mock'; import { validateConfig, validateSecrets, validateParams } from '../lib'; import { createActionTypeRegistry } from './index.test'; import { sendEmail } from './lib/send_email'; +import { actionsMock } from '../mocks'; import { ActionParamsType, ActionTypeConfigType, @@ -26,13 +26,8 @@ import { const sendEmailMock = sendEmail as jest.Mock; const ACTION_TYPE_ID = '.email'; -const NO_OP_FN = () => {}; -const services = { - log: NO_OP_FN, - callCluster: async (path: string, opts: any) => {}, - savedObjectsClient: savedObjectsClientMock.create(), -}; +const services = actionsMock.createServices(); let actionType: ActionType; let mockedLogger: jest.Mocked<Logger>; diff --git a/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts index ec495aed7675a..6abf426dc348f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts @@ -10,18 +10,13 @@ jest.mock('./lib/send_email', () => ({ import { ActionType, ActionTypeExecutorOptions } from '../types'; import { validateConfig, validateParams } from '../lib'; -import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; import { createActionTypeRegistry } from './index.test'; import { ActionParamsType, ActionTypeConfigType } from './es_index'; +import { actionsMock } from '../mocks'; const ACTION_TYPE_ID = '.index'; -const NO_OP_FN = () => {}; -const services = { - log: NO_OP_FN, - callCluster: jest.fn(), - savedObjectsClient: savedObjectsClientMock.create(), -}; +const services = actionsMock.createServices(); let actionType: ActionType; diff --git a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts index e5521558bc2da..1bca7c18e4e1b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts @@ -11,20 +11,17 @@ jest.mock('./lib/post_pagerduty', () => ({ import { getActionType } from './pagerduty'; import { ActionType, Services, ActionTypeExecutorOptions } from '../types'; import { validateConfig, validateSecrets, validateParams } from '../lib'; -import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; import { postPagerduty } from './lib/post_pagerduty'; import { createActionTypeRegistry } from './index.test'; import { Logger } from '../../../../../src/core/server'; import { actionsConfigMock } from '../actions_config.mock'; +import { actionsMock } from '../mocks'; const postPagerdutyMock = postPagerduty as jest.Mock; const ACTION_TYPE_ID = '.pagerduty'; -const services: Services = { - callCluster: async (path: string, opts: any) => {}, - savedObjectsClient: savedObjectsClientMock.create(), -}; +const services: Services = actionsMock.createServices(); let actionType: ActionType; let mockedLogger: jest.Mocked<Logger>; diff --git a/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts b/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts index bb806f8ae36fc..d5a9c0cc1ccd2 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts @@ -7,8 +7,8 @@ import { ActionType } from '../types'; import { validateParams } from '../lib'; import { Logger } from '../../../../../src/core/server'; -import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; import { createActionTypeRegistry } from './index.test'; +import { actionsMock } from '../mocks'; const ACTION_TYPE_ID = '.server-log'; @@ -90,10 +90,7 @@ describe('execute()', () => { const actionId = 'some-id'; await actionType.executor({ actionId, - services: { - callCluster: async (path: string, opts: any) => {}, - savedObjectsClient: savedObjectsClientMock.create(), - }, + services: actionsMock.createServices(), params: { message: 'message text here', level: 'info' }, config: {}, secrets: {}, diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts index 1a23354e6490d..a6c3ae88765ac 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts @@ -7,9 +7,9 @@ import { getActionType } from '.'; import { ActionType, Services, ActionTypeExecutorOptions } from '../../types'; import { validateConfig, validateSecrets, validateParams } from '../../lib'; -import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; import { createActionTypeRegistry } from '../index.test'; import { actionsConfigMock } from '../../actions_config.mock'; +import { actionsMock } from '../../mocks'; import { ACTION_TYPE_ID } from './constants'; import * as i18n from './translations'; @@ -21,10 +21,7 @@ jest.mock('./action_handlers'); const handleIncidentMock = handleIncident as jest.Mock; -const services: Services = { - callCluster: async (path: string, opts: any) => {}, - savedObjectsClient: savedObjectsClientMock.create(), -}; +const services: Services = actionsMock.createServices(); let actionType: ActionType; diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts index 49b0b84e9dbb5..782d30ba02f1d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts @@ -5,17 +5,14 @@ */ import { ActionType, Services, ActionTypeExecutorOptions } from '../types'; -import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; import { validateParams, validateSecrets } from '../lib'; import { getActionType } from './slack'; import { actionsConfigMock } from '../actions_config.mock'; +import { actionsMock } from '../mocks'; const ACTION_TYPE_ID = '.slack'; -const services: Services = { - callCluster: async (path: string, opts: any) => {}, - savedObjectsClient: savedObjectsClientMock.create(), -}; +const services: Services = actionsMock.createServices(); let actionType: ActionType; diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts index 03658b3b1dd85..60b26f345f6a3 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts @@ -11,20 +11,17 @@ jest.mock('axios', () => ({ import { getActionType } from './webhook'; import { ActionType, Services } from '../types'; import { validateConfig, validateSecrets, validateParams } from '../lib'; -import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; import { actionsConfigMock } from '../actions_config.mock'; import { createActionTypeRegistry } from './index.test'; import { Logger } from '../../../../../src/core/server'; +import { actionsMock } from '../mocks'; import axios from 'axios'; const axiosRequestMock = axios.request as jest.Mock; const ACTION_TYPE_ID = '.webhook'; -const services: Services = { - callCluster: async (path: string, opts: any) => {}, - savedObjectsClient: savedObjectsClientMock.create(), -}; +const services: Services = actionsMock.createServices(); let actionType: ActionType; let mockedLogger: jest.Mocked<Logger>; diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts index 124e5951c714b..1b47bfeaf19af 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.test.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts @@ -9,21 +9,15 @@ import { schema } from '@kbn/config-schema'; import { ActionExecutor } from './action_executor'; import { actionTypeRegistryMock } from '../action_type_registry.mock'; import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks'; -import { savedObjectsClientMock, loggingServiceMock } from '../../../../../src/core/server/mocks'; +import { loggingServiceMock } from '../../../../../src/core/server/mocks'; import { eventLoggerMock } from '../../../event_log/server/mocks'; import { spacesServiceMock } from '../../../spaces/server/spaces_service/spaces_service.mock'; import { ActionType } from '../types'; +import { actionsMock } from '../mocks'; const actionExecutor = new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }); -const savedObjectsClient = savedObjectsClientMock.create(); - -function getServices() { - return { - savedObjectsClient, - log: jest.fn(), - callCluster: jest.fn(), - }; -} +const services = actionsMock.createServices(); +const savedObjectsClient = services.savedObjectsClient; const encryptedSavedObjectsPlugin = encryptedSavedObjectsMock.createStart(); const actionTypeRegistry = actionTypeRegistryMock.create(); @@ -39,7 +33,7 @@ const spacesMock = spacesServiceMock.createSetupContract(); actionExecutor.initialize({ logger: loggingServiceMock.create().get(), spaces: spacesMock, - getServices, + getServices: () => services, actionTypeRegistry, encryptedSavedObjectsPlugin, eventLogger: eventLoggerMock.create(), @@ -229,7 +223,7 @@ test('throws an error when passing isESOUsingEphemeralEncryptionKey with value o customActionExecutor.initialize({ logger: loggingServiceMock.create().get(), spaces: spacesMock, - getServices, + getServices: () => services, actionTypeRegistry, encryptedSavedObjectsPlugin, eventLogger: eventLoggerMock.create(), diff --git a/x-pack/plugins/actions/server/mocks.ts b/x-pack/plugins/actions/server/mocks.ts index bc4268bb69872..27e8cc35b1ddc 100644 --- a/x-pack/plugins/actions/server/mocks.ts +++ b/x-pack/plugins/actions/server/mocks.ts @@ -6,6 +6,8 @@ import { actionsClientMock } from './actions_client.mock'; import { PluginSetupContract, PluginStartContract } from './plugin'; +import { Services } from './types'; +import { savedObjectsClientMock } from '../../../../src/core/server/mocks'; export { actionsClientMock }; @@ -26,7 +28,19 @@ const createStartMock = () => { return mock; }; +const createServicesMock = () => { + const mock: jest.Mocked<Services & { + savedObjectsClient: ReturnType<typeof savedObjectsClientMock.create>; + }> = { + callCluster: jest.fn(), + getScopedClusterClient: jest.fn(), + savedObjectsClient: savedObjectsClientMock.create(), + }; + return mock; +}; + export const actionsMock = { + createServices: createServicesMock, createSetup: createSetupMock, createStart: createStartMock, }; diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index a8ab3bbb2fad2..539845221e485 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -18,6 +18,8 @@ import { IContextProvider, SavedObjectsServiceStart, ElasticsearchServiceStart, + IClusterClient, + ClusterClient, } from '../../../../src/core/server'; import { @@ -294,6 +296,12 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi return request => ({ callCluster: elasticsearch.legacy.client.asScoped(request).callAsCurrentUser, savedObjectsClient: savedObjects.getScopedClient(request), + getScopedClusterClient(clusterClient: IClusterClient) { + if (!(clusterClient instanceof ClusterClient)) { + throw new Error('given clusterClient is not an instance of ClusterClient'); + } + return clusterClient.asScoped(request); + }, }); } diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index 088398b40830e..6804d89e949e2 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -4,11 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract, SavedObjectAttributes } from '../../../../src/core/server'; import { ActionTypeRegistry } from './action_type_registry'; import { PluginSetupContract, PluginStartContract } from './plugin'; import { ActionsClient } from './actions_client'; import { LicenseType } from '../../licensing/common/types'; +import { + IClusterClient, + IScopedClusterClient, + SavedObjectsClientContract, + SavedObjectAttributes, +} from '../../../../src/core/server'; export type WithoutQueryAndParams<T> = Pick<T, Exclude<keyof T, 'query' | 'params'>>; export type GetServicesFunction = (request: any) => Services; @@ -19,6 +24,7 @@ export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefine export interface Services { callCluster(path: string, opts: any): Promise<any>; savedObjectsClient: SavedObjectsClientContract; + getScopedClusterClient(clusterClient: IClusterClient): IScopedClusterClient; } declare module 'src/core/server' { diff --git a/x-pack/plugins/alerting/server/mocks.ts b/x-pack/plugins/alerting/server/mocks.ts index a9e224142a632..e69423183b708 100644 --- a/x-pack/plugins/alerting/server/mocks.ts +++ b/x-pack/plugins/alerting/server/mocks.ts @@ -56,6 +56,7 @@ const createAlertServicesMock = () => { .fn<jest.Mocked<AlertInstance>, [string]>() .mockReturnValue(alertInstanceFactoryMock), callCluster: jest.fn(), + getScopedClusterClient: jest.fn(), savedObjectsClient: savedObjectsClientMock.create(), }; }; diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index ad39d09bd6d3d..99ba2e4b6cea1 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -29,6 +29,8 @@ import { RequestHandler, SharedGlobalConfig, ElasticsearchServiceStart, + IClusterClient, + ClusterClient, } from '../../../../src/core/server'; import { @@ -267,6 +269,12 @@ export class AlertingPlugin { return request => ({ callCluster: elasticsearch.legacy.client.asScoped(request).callAsCurrentUser, savedObjectsClient: savedObjects.getScopedClient(request), + getScopedClusterClient(clusterClient: IClusterClient) { + if (!(clusterClient instanceof ClusterClient)) { + throw new Error('given clusterClient is not an instance of ClusterClient'); + } + return clusterClient.asScoped(request); + }, }); } 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 31cc893f785cb..1d1f00bf32900 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 @@ -11,9 +11,10 @@ import { ConcreteTaskInstance, TaskStatus } from '../../../../plugins/task_manag 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 { loggingServiceMock } from '../../../../../src/core/server/mocks'; import { PluginStartContract as ActionsPluginStart } from '../../../actions/server'; import { actionsMock } from '../../../actions/server/mocks'; +import { alertsMock } from '../mocks'; import { eventLoggerMock } from '../../../event_log/server/event_logger.mock'; import { IEventLogger } from '../../../event_log/server'; import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; @@ -52,13 +53,9 @@ describe('Task Runner', () => { afterAll(() => fakeTimer.restore()); - const savedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjectsPlugin = encryptedSavedObjectsMock.createStart(); - const services = { - log: jest.fn(), - callCluster: jest.fn(), - savedObjectsClient, - }; + const services = alertsMock.createAlertServices(); + const savedObjectsClient = services.savedObjectsClient; const taskRunnerFactoryInitializerParams: jest.Mocked<TaskRunnerContext> & { actionsPlugin: jest.Mocked<ActionsPluginStart>; 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 1d220f97f127a..563664d3544ac 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 @@ -8,8 +8,9 @@ import sinon from 'sinon'; import { ConcreteTaskInstance, TaskStatus } from '../../../../plugins/task_manager/server'; 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 { loggingServiceMock } from '../../../../../src/core/server/mocks'; import { actionsMock } from '../../../actions/server/mocks'; +import { alertsMock } from '../mocks'; import { eventLoggerMock } from '../../../event_log/server/event_logger.mock'; const alertType = { @@ -48,13 +49,8 @@ describe('Task Runner Factory', () => { afterAll(() => fakeTimer.restore()); - const savedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjectsPlugin = encryptedSavedObjectsMock.createStart(); - const services = { - log: jest.fn(), - callCluster: jest.fn(), - savedObjectsClient, - }; + const services = alertsMock.createAlertServices(); const taskRunnerFactoryInitializerParams: jest.Mocked<TaskRunnerContext> = { getServices: jest.fn().mockReturnValue(services), diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index 739a0d0aece24..0eff6c3a66072 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -7,10 +7,15 @@ import { AlertInstance } from './alert_instance'; import { AlertTypeRegistry as OrigAlertTypeRegistry } from './alert_type_registry'; import { PluginSetupContract, PluginStartContract } from './plugin'; -import { SavedObjectAttributes, SavedObjectsClientContract } from '../../../../src/core/server'; import { Alert, AlertActionParams, ActionGroup } from '../common'; import { AlertsClient } from './alerts_client'; export * from '../common'; +import { + IClusterClient, + IScopedClusterClient, + SavedObjectAttributes, + SavedObjectsClientContract, +} from '../../../../src/core/server'; export type State = Record<string, any>; export type Context = Record<string, any>; @@ -31,6 +36,7 @@ declare module 'src/core/server' { export interface Services { callCluster(path: string, opts: any): Promise<any>; savedObjectsClient: SavedObjectsClientContract; + getScopedClusterClient(clusterClient: IClusterClient): IScopedClusterClient; } export interface AlertServices extends Services { From d4d3bd8ee920fb12b49206e5d8a351afa6084d80 Mon Sep 17 00:00:00 2001 From: Mike Cote <mikecote@users.noreply.github.com> Date: Fri, 24 Apr 2020 13:17:35 -0400 Subject: [PATCH 2/6] Rename to getScopedCallCluster --- x-pack/plugins/actions/server/plugin.ts | 4 ++-- x-pack/plugins/actions/server/types.ts | 2 +- x-pack/plugins/alerting/server/plugin.ts | 4 ++-- x-pack/plugins/alerting/server/types.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 539845221e485..ea69eff769a4e 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -296,11 +296,11 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi return request => ({ callCluster: elasticsearch.legacy.client.asScoped(request).callAsCurrentUser, savedObjectsClient: savedObjects.getScopedClient(request), - getScopedClusterClient(clusterClient: IClusterClient) { + getScopedCallCluster(clusterClient: IClusterClient) { if (!(clusterClient instanceof ClusterClient)) { throw new Error('given clusterClient is not an instance of ClusterClient'); } - return clusterClient.asScoped(request); + return clusterClient.asScoped(request).callAsCurrentUser; }, }); } diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index 6804d89e949e2..3ccaf04916dfb 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -24,7 +24,7 @@ export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefine export interface Services { callCluster(path: string, opts: any): Promise<any>; savedObjectsClient: SavedObjectsClientContract; - getScopedClusterClient(clusterClient: IClusterClient): IScopedClusterClient; + getScopedCallCluster(clusterClient: IClusterClient): IScopedClusterClient['callAsCurrentUser']; } declare module 'src/core/server' { diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 99ba2e4b6cea1..1cadfde15f676 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -269,11 +269,11 @@ export class AlertingPlugin { return request => ({ callCluster: elasticsearch.legacy.client.asScoped(request).callAsCurrentUser, savedObjectsClient: savedObjects.getScopedClient(request), - getScopedClusterClient(clusterClient: IClusterClient) { + getScopedCallCluster(clusterClient: IClusterClient) { if (!(clusterClient instanceof ClusterClient)) { throw new Error('given clusterClient is not an instance of ClusterClient'); } - return clusterClient.asScoped(request); + return clusterClient.asScoped(request).callAsCurrentUser; }, }); } diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index 0eff6c3a66072..f209a5b606028 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -36,7 +36,7 @@ declare module 'src/core/server' { export interface Services { callCluster(path: string, opts: any): Promise<any>; savedObjectsClient: SavedObjectsClientContract; - getScopedClusterClient(clusterClient: IClusterClient): IScopedClusterClient; + getScopedCallCluster(clusterClient: IClusterClient): IScopedClusterClient['callAsCurrentUser']; } export interface AlertServices extends Services { From a48a5c7b7c581396fd6d05582c0102adfa4d8156 Mon Sep 17 00:00:00 2001 From: Mike Cote <mikecote@users.noreply.github.com> Date: Fri, 24 Apr 2020 15:19:34 -0400 Subject: [PATCH 3/6] Fix typecheck --- x-pack/plugins/actions/server/mocks.ts | 2 +- x-pack/plugins/alerting/server/mocks.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/actions/server/mocks.ts b/x-pack/plugins/actions/server/mocks.ts index 27e8cc35b1ddc..09ae91bd6876c 100644 --- a/x-pack/plugins/actions/server/mocks.ts +++ b/x-pack/plugins/actions/server/mocks.ts @@ -33,7 +33,7 @@ const createServicesMock = () => { savedObjectsClient: ReturnType<typeof savedObjectsClientMock.create>; }> = { callCluster: jest.fn(), - getScopedClusterClient: jest.fn(), + getScopedCallCluster: jest.fn(), savedObjectsClient: savedObjectsClientMock.create(), }; return mock; diff --git a/x-pack/plugins/alerting/server/mocks.ts b/x-pack/plugins/alerting/server/mocks.ts index e69423183b708..275d625882f90 100644 --- a/x-pack/plugins/alerting/server/mocks.ts +++ b/x-pack/plugins/alerting/server/mocks.ts @@ -56,7 +56,7 @@ const createAlertServicesMock = () => { .fn<jest.Mocked<AlertInstance>, [string]>() .mockReturnValue(alertInstanceFactoryMock), callCluster: jest.fn(), - getScopedClusterClient: jest.fn(), + getScopedCallCluster: jest.fn(), savedObjectsClient: savedObjectsClientMock.create(), }; }; From f562a327721e092790afe9cfe6ffbf3099ef1e81 Mon Sep 17 00:00:00 2001 From: Mike Cote <mikecote@users.noreply.github.com> Date: Fri, 24 Apr 2020 15:46:03 -0400 Subject: [PATCH 4/6] Fix more type check issues --- .../actions/server/builtin_action_types/es_index.test.ts | 4 ++-- .../actions/server/builtin_action_types/es_index.ts | 2 +- .../actions/server/builtin_action_types/slack.test.ts | 1 - x-pack/plugins/actions/server/mocks.ts | 7 +++++-- x-pack/plugins/actions/server/types.ts | 4 ++-- x-pack/plugins/alerting/server/mocks.ts | 7 +++++-- x-pack/plugins/alerting/server/types.ts | 6 ++---- .../metric_threshold/metric_threshold_executor.test.ts | 2 +- 8 files changed, 18 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts index 455264e495c84..be60f4c2f28af 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts @@ -191,9 +191,9 @@ describe('execute()', () => { await actionType.executor(executorOptions); const calls = services.callCluster.mock.calls; - const timeValue = calls[0][1].body[1].field_to_use_for_time; + const timeValue = calls[0][1]?.body[1].field_to_use_for_time; expect(timeValue).toBeInstanceOf(Date); - delete calls[0][1].body[1].field_to_use_for_time; + delete calls[0][1]?.body[1].field_to_use_for_time; expect(calls).toMatchInlineSnapshot(` Array [ Array [ diff --git a/x-pack/plugins/actions/server/builtin_action_types/es_index.ts b/x-pack/plugins/actions/server/builtin_action_types/es_index.ts index 32f5e23015700..899684367d52d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/es_index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/es_index.ts @@ -72,7 +72,7 @@ async function executor( bulkBody.push(document); } - const bulkParams: unknown = { + const bulkParams = { index, body: bulkBody, refresh: config.refresh, diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts index 8de3664d874bb..cbcd4b2954518 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts @@ -10,7 +10,6 @@ import { ActionTypeExecutorOptions, ActionTypeExecutorResult, } from '../types'; -import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; import { validateParams, validateSecrets } from '../lib'; import { getActionType } from './slack'; import { actionsConfigMock } from '../actions_config.mock'; diff --git a/x-pack/plugins/actions/server/mocks.ts b/x-pack/plugins/actions/server/mocks.ts index 09ae91bd6876c..81be75d8f5daf 100644 --- a/x-pack/plugins/actions/server/mocks.ts +++ b/x-pack/plugins/actions/server/mocks.ts @@ -7,7 +7,10 @@ import { actionsClientMock } from './actions_client.mock'; import { PluginSetupContract, PluginStartContract } from './plugin'; import { Services } from './types'; -import { savedObjectsClientMock } from '../../../../src/core/server/mocks'; +import { + elasticsearchServiceMock, + savedObjectsClientMock, +} from '../../../../src/core/server/mocks'; export { actionsClientMock }; @@ -32,7 +35,7 @@ const createServicesMock = () => { const mock: jest.Mocked<Services & { savedObjectsClient: ReturnType<typeof savedObjectsClientMock.create>; }> = { - callCluster: jest.fn(), + callCluster: elasticsearchServiceMock.createScopedClusterClient().callAsCurrentUser, getScopedCallCluster: jest.fn(), savedObjectsClient: savedObjectsClientMock.create(), }; diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index ad3a2dd6d1340..093d22c2c1a71 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -11,7 +11,7 @@ import { LicenseType } from '../../licensing/common/types'; import { IClusterClient, IScopedClusterClient, - KibanaRequest, + KibanaRequest, SavedObjectsClientContract, SavedObjectAttributes, } from '../../../../src/core/server'; @@ -23,7 +23,7 @@ export type GetBasePathFunction = (spaceId?: string) => string; export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined; export interface Services { - callCluster(path: string, opts: unknown): Promise<unknown>; + callCluster: IScopedClusterClient['callAsCurrentUser']; savedObjectsClient: SavedObjectsClientContract; getScopedCallCluster(clusterClient: IClusterClient): IScopedClusterClient['callAsCurrentUser']; } diff --git a/x-pack/plugins/alerting/server/mocks.ts b/x-pack/plugins/alerting/server/mocks.ts index 275d625882f90..c94a7aba46cfa 100644 --- a/x-pack/plugins/alerting/server/mocks.ts +++ b/x-pack/plugins/alerting/server/mocks.ts @@ -6,8 +6,11 @@ import { alertsClientMock } from './alerts_client.mock'; import { PluginSetupContract, PluginStartContract } from './plugin'; -import { savedObjectsClientMock } from '../../../../src/core/server/mocks'; import { AlertInstance } from './alert_instance'; +import { + elasticsearchServiceMock, + savedObjectsClientMock, +} from '../../../../src/core/server/mocks'; export { alertsClientMock }; @@ -55,7 +58,7 @@ const createAlertServicesMock = () => { alertInstanceFactory: jest .fn<jest.Mocked<AlertInstance>, [string]>() .mockReturnValue(alertInstanceFactoryMock), - callCluster: jest.fn(), + callCluster: elasticsearchServiceMock.createScopedClusterClient().callAsCurrentUser, getScopedCallCluster: jest.fn(), savedObjectsClient: savedObjectsClientMock.create(), }; diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index c7533028f7ac4..b733b23dd71e6 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -13,7 +13,7 @@ export * from '../common'; import { IClusterClient, IScopedClusterClient, - KibanaRequest, + KibanaRequest, SavedObjectAttributes, SavedObjectsClientContract, } from '../../../../src/core/server'; @@ -38,9 +38,7 @@ declare module 'src/core/server' { } export interface Services { - // This will have to remain `any` until we can extend Alert Services with generics - // eslint-disable-next-line @typescript-eslint/no-explicit-any - callCluster(path: string, opts: any): Promise<any>; + callCluster: IScopedClusterClient['callAsCurrentUser']; savedObjectsClient: SavedObjectsClientContract; getScopedCallCluster(clusterClient: IClusterClient): IScopedClusterClient['callAsCurrentUser']; } diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index a52659dae01f1..24b6ba2ec378b 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -20,7 +20,7 @@ const executor = createMetricThresholdExecutor('test') as (opts: { }) => Promise<void>; const services: AlertServicesMock = alertsMock.createAlertServices(); -services.callCluster.mockImplementation((_: string, { body, index }: any) => { +services.callCluster.mockImplementation(async (_: string, { body, index }: any) => { if (index === 'alternatebeat-*') return mocks.changedSourceIdResponse; const metric = body.query.bool.filter[1]?.exists.field; if (body.aggs.groupings) { From 521a46ca22c2945dc43295e6e4b56300c2e6457b Mon Sep 17 00:00:00 2001 From: Mike Cote <mikecote@users.noreply.github.com> Date: Fri, 24 Apr 2020 16:21:55 -0400 Subject: [PATCH 5/6] Add tests --- x-pack/plugins/actions/server/plugin.ts | 4 -- x-pack/plugins/alerting/server/plugin.ts | 4 -- .../common/fixtures/plugins/alerts/index.ts | 37 +++++++++++++++++++ .../tests/actions/execute.ts | 6 +++ .../tests/alerting/alerts.ts | 12 ++++++ .../spaces_only/tests/actions/execute.ts | 1 + .../spaces_only/tests/alerting/alerts_base.ts | 2 + 7 files changed, 58 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 69ccd33729f81..5c50417172c0a 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -19,7 +19,6 @@ import { SavedObjectsServiceStart, ElasticsearchServiceStart, IClusterClient, - ClusterClient, } from '../../../../src/core/server'; import { @@ -300,9 +299,6 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi callCluster: elasticsearch.legacy.client.asScoped(request).callAsCurrentUser, savedObjectsClient: savedObjects.getScopedClient(request), getScopedCallCluster(clusterClient: IClusterClient) { - if (!(clusterClient instanceof ClusterClient)) { - throw new Error('given clusterClient is not an instance of ClusterClient'); - } return clusterClient.asScoped(request).callAsCurrentUser; }, }); diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 9fb6bd41e9ca1..c03d3506a051d 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -30,7 +30,6 @@ import { SharedGlobalConfig, ElasticsearchServiceStart, IClusterClient, - ClusterClient, } from '../../../../src/core/server'; import { @@ -273,9 +272,6 @@ export class AlertingPlugin { callCluster: elasticsearch.legacy.client.asScoped(request).callAsCurrentUser, savedObjectsClient: savedObjects.getScopedClient(request), getScopedCallCluster(clusterClient: IClusterClient) { - if (!(clusterClient instanceof ClusterClient)) { - throw new Error('given clusterClient is not an instance of ClusterClient'); - } return clusterClient.asScoped(request).callAsCurrentUser; }, }); 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 fe0f630830a56..43d533ad3ae14 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 @@ -14,6 +14,7 @@ export default function(kibana: any) { require: ['xpack_main', 'actions', 'alerting', 'elasticsearch'], name: 'alerts', init(server: any) { + const clusterClient = server.newPlatform.start.core.elasticsearch.legacy.client; server.plugins.xpack_main.registerFeature({ id: 'alerting', name: 'Alerting', @@ -165,6 +166,22 @@ export default function(kibana: any) { } catch (e) { callClusterError = e; } + // Call scoped cluster + const callScopedCluster = services.getScopedCallCluster(clusterClient); + let callScopedClusterSuccess = false; + let callScopedClusterError; + try { + await callScopedCluster('index', { + index: params.callClusterAuthorizationIndex, + refresh: 'wait_for', + body: { + param1: 'test', + }, + }); + callScopedClusterSuccess = true; + } catch (e) { + callScopedClusterError = e; + } // Saved objects client let savedObjectsClientSuccess = false; let savedObjectsClientError; @@ -185,6 +202,8 @@ export default function(kibana: any) { state: { callClusterSuccess, callClusterError, + callScopedClusterSuccess, + callScopedClusterError, savedObjectsClientSuccess, savedObjectsClientError, }, @@ -376,6 +395,22 @@ export default function(kibana: any) { } catch (e) { callClusterError = e; } + // Call scoped cluster + const callScopedCluster = services.getScopedCallCluster(clusterClient); + let callScopedClusterSuccess = false; + let callScopedClusterError; + try { + await callScopedCluster('index', { + index: params.callClusterAuthorizationIndex, + refresh: 'wait_for', + body: { + param1: 'test', + }, + }); + callScopedClusterSuccess = true; + } catch (e) { + callScopedClusterError = e; + } // Saved objects client let savedObjectsClientSuccess = false; let savedObjectsClientError; @@ -396,6 +431,8 @@ export default function(kibana: any) { state: { callClusterSuccess, callClusterError, + callScopedClusterSuccess, + callScopedClusterError, savedObjectsClientSuccess, savedObjectsClientError, }, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts index a58e14dd563ef..af8af72d458fd 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts @@ -436,11 +436,16 @@ export default function({ getService }: FtrProviderContext) { indexedRecord = searchResult.hits.hits[0]; expect(indexedRecord._source.state).to.eql({ callClusterSuccess: false, + callScopedClusterSuccess: false, savedObjectsClientSuccess: false, callClusterError: { ...indexedRecord._source.state.callClusterError, statusCode: 403, }, + callScopedClusterError: { + ...indexedRecord._source.state.callScopedClusterError, + statusCode: 403, + }, savedObjectsClientError: { ...indexedRecord._source.state.savedObjectsClientError, output: { @@ -457,6 +462,7 @@ export default function({ getService }: FtrProviderContext) { indexedRecord = searchResult.hits.hits[0]; expect(indexedRecord._source.state).to.eql({ callClusterSuccess: true, + callScopedClusterSuccess: true, savedObjectsClientSuccess: false, savedObjectsClientError: { ...indexedRecord._source.state.savedObjectsClientError, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts index d8e4f808f5cd2..59cf22b52920c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts @@ -469,11 +469,16 @@ instanceStateValue: true expect(searchResult.hits.total.value).to.eql(1); expect(searchResult.hits.hits[0]._source.state).to.eql({ callClusterSuccess: false, + callScopedClusterSuccess: false, savedObjectsClientSuccess: false, callClusterError: { ...searchResult.hits.hits[0]._source.state.callClusterError, statusCode: 403, }, + callScopedClusterError: { + ...searchResult.hits.hits[0]._source.state.callScopedClusterError, + statusCode: 403, + }, savedObjectsClientError: { ...searchResult.hits.hits[0]._source.state.savedObjectsClientError, output: { @@ -497,6 +502,7 @@ instanceStateValue: true expect(searchResult.hits.total.value).to.eql(1); expect(searchResult.hits.hits[0]._source.state).to.eql({ callClusterSuccess: true, + callScopedClusterSuccess: true, savedObjectsClientSuccess: false, savedObjectsClientError: { ...searchResult.hits.hits[0]._source.state.savedObjectsClientError, @@ -577,11 +583,16 @@ instanceStateValue: true expect(searchResult.hits.total.value).to.eql(1); expect(searchResult.hits.hits[0]._source.state).to.eql({ callClusterSuccess: false, + callScopedClusterSuccess: false, savedObjectsClientSuccess: false, callClusterError: { ...searchResult.hits.hits[0]._source.state.callClusterError, statusCode: 403, }, + callScopedClusterError: { + ...searchResult.hits.hits[0]._source.state.callScopedClusterError, + statusCode: 403, + }, savedObjectsClientError: { ...searchResult.hits.hits[0]._source.state.savedObjectsClientError, output: { @@ -605,6 +616,7 @@ instanceStateValue: true expect(searchResult.hits.total.value).to.eql(1); expect(searchResult.hits.hits[0]._source.state).to.eql({ callClusterSuccess: true, + callScopedClusterSuccess: true, savedObjectsClientSuccess: false, savedObjectsClientError: { ...searchResult.hits.hits[0]._source.state.savedObjectsClientError, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts index 3faa54ee0b219..715573ef1237e 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts @@ -186,6 +186,7 @@ export default function({ getService }: FtrProviderContext) { const indexedRecord = searchResult.hits.hits[0]; expect(indexedRecord._source.state).to.eql({ callClusterSuccess: true, + callScopedClusterSuccess: true, savedObjectsClientSuccess: false, savedObjectsClientError: { ...indexedRecord._source.state.savedObjectsClientError, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts index 0d1596a95bfbb..95ccfb897cf54 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts @@ -277,6 +277,7 @@ instanceStateValue: true )[0]; expect(alertTestRecord._source.state).to.eql({ callClusterSuccess: true, + callScopedClusterSuccess: true, savedObjectsClientSuccess: false, savedObjectsClientError: { ...alertTestRecord._source.state.savedObjectsClientError, @@ -332,6 +333,7 @@ instanceStateValue: true )[0]; expect(actionTestRecord._source.state).to.eql({ callClusterSuccess: true, + callScopedClusterSuccess: true, savedObjectsClientSuccess: false, savedObjectsClientError: { ...actionTestRecord._source.state.savedObjectsClientError, From 8c1b93df8a36cd5ca22934e20910edc19af3905f Mon Sep 17 00:00:00 2001 From: Mike Cote <mikecote@users.noreply.github.com> Date: Mon, 27 Apr 2020 10:02:45 -0400 Subject: [PATCH 6/6] Add docs --- x-pack/plugins/actions/README.md | 3 ++- x-pack/plugins/alerting/README.md | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index d6c85606edc2c..decd170ca5dd6 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -143,7 +143,8 @@ This is the primary function for an action type. Whenever the action needs to ex | actionId | The action saved object id that the action type is executing for. | | config | The decrypted configuration given to an action. This comes from the action saved object that is partially or fully encrypted within the data store. If you would like to validate the config before being passed to the executor, define `validate.config` within the action type. | | params | Parameters for the execution. These will be given at execution time by either an alert or manually provided when calling the plugin provided execute function. | -| services.callCluster(path, opts) | Use this to do Elasticsearch queries on the cluster Kibana connects to. This function is the same as any other `callCluster` in Kibana.<br><br>**NOTE**: This currently authenticates as the Kibana internal user, but will change in a future PR. | +| services.callCluster(path, opts) | Use this to do Elasticsearch queries on the cluster Kibana connects to. This function is the same as any other `callCluster` in Kibana but runs in the context of the user who is calling the action when security is enabled.| +| services.getScopedCallCluster | This function scopes an instance of CallCluster by returning a `callCluster(path, opts)` function that runs in the context of the user who is calling the action when security is enabled. This must only be called with instances of CallCluster provided by core.| | services.savedObjectsClient | This is an instance of the saved objects client. This provides the ability to do CRUD on any saved objects within the same space the alert lives in.<br><br>The scope of the saved objects client is tied to the user in context calling the execute API or the API key provided to the execute plugin function (only when security isenabled). | | services.log(tags, [data], [timestamp]) | Use this to create server logs. (This is the same function as server.log) | diff --git a/x-pack/plugins/alerting/README.md b/x-pack/plugins/alerting/README.md index 177e42de5a95b..62c2caed669af 100644 --- a/x-pack/plugins/alerting/README.md +++ b/x-pack/plugins/alerting/README.md @@ -101,6 +101,7 @@ This is the primary function for an alert type. Whenever the alert needs to exec |---|---| |services.callCluster(path, opts)|Use this to do Elasticsearch queries on the cluster Kibana connects to. This function is the same as any other `callCluster` in Kibana but in the context of the user who created the alert when security is enabled.| |services.savedObjectsClient|This is an instance of the saved objects client. This provides the ability to do CRUD on any saved objects within the same space the alert lives in.<br><br>The scope of the saved objects client is tied to the user who created the alert (only when security isenabled).| +|services.getScopedCallCluster|This function scopes an instance of CallCluster by returning a `callCluster(path, opts)` function that runs in the context of the user who created the alert when security is enabled. This must only be called with instances of CallCluster provided by core.| |services.log(tags, [data], [timestamp])|Use this to create server logs. (This is the same function as server.log)| |startedAt|The date and time the alert type started execution.| |previousStartedAt|The previous date and time the alert type started a successful execution.|