From 901bcc05a3e354a25eb0a1028bcc43b063f4611d Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Mon, 25 Nov 2019 13:12:11 -0500 Subject: [PATCH 1/5] Denormalize actionTypeId for easier filtering of alerts --- x-pack/legacy/plugins/alerting/mappings.json | 3 +++ .../legacy/plugins/alerting/server/alerts_client.test.ts | 8 ++++++++ .../alerting/server/lib/create_execution_handler.test.ts | 1 + .../legacy/plugins/alerting/server/routes/create.test.ts | 4 ++++ x-pack/legacy/plugins/alerting/server/routes/create.ts | 9 +++++++-- x-pack/legacy/plugins/alerting/server/routes/get.test.ts | 1 + .../legacy/plugins/alerting/server/routes/update.test.ts | 3 +++ x-pack/legacy/plugins/alerting/server/routes/update.ts | 9 +++++++-- x-pack/legacy/plugins/alerting/server/types.ts | 2 ++ .../alerting_api_integration/common/lib/alert_utils.ts | 1 + .../security_and_spaces/tests/actions/find.ts | 1 + .../security_and_spaces/tests/alerting/alerts.ts | 4 ++++ .../spaces_only/tests/alerting/alerts.ts | 2 ++ 13 files changed, 44 insertions(+), 4 deletions(-) diff --git a/x-pack/legacy/plugins/alerting/mappings.json b/x-pack/legacy/plugins/alerting/mappings.json index f840c019d5e024..7a7446602351d5 100644 --- a/x-pack/legacy/plugins/alerting/mappings.json +++ b/x-pack/legacy/plugins/alerting/mappings.json @@ -25,6 +25,9 @@ "actionRef": { "type": "keyword" }, + "actionTypeId": { + "type": "keyword" + }, "params": { "enabled": false, "type": "object" diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts index 08607f04a52355..de4e2b316f202b 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts @@ -55,6 +55,7 @@ function getMockData(overwrites: Record = {}) { { group: 'default', id: '1', + actionTypeId: 'test', params: { foo: true, }, @@ -157,6 +158,7 @@ describe('create()', () => { "actions": Array [ Object { "actionRef": "action_0", + "actionTypeId": "test", "group": "default", "params": Object { "foo": true, @@ -506,6 +508,7 @@ describe('create()', () => { { actionRef: 'action_0', group: 'default', + actionTypeId: 'test', params: { foo: true }, }, ], @@ -1190,6 +1193,7 @@ describe('update()', () => { { group: 'default', id: '1', + actionTypeId: 'test', params: { foo: true, }, @@ -1226,6 +1230,7 @@ describe('update()', () => { "actions": Array [ Object { "actionRef": "action_0", + "actionTypeId": "test", "group": "default", "params": Object { "foo": true, @@ -1327,6 +1332,7 @@ describe('update()', () => { { group: 'default', id: '1', + actionTypeId: 'test', params: { foo: true, }, @@ -1364,6 +1370,7 @@ describe('update()', () => { "actions": Array [ Object { "actionRef": "action_0", + "actionTypeId": "test", "group": "default", "params": Object { "foo": true, @@ -1435,6 +1442,7 @@ describe('update()', () => { { group: 'default', id: '1', + actionTypeId: 'test', params: { foo: true, }, diff --git a/x-pack/legacy/plugins/alerting/server/lib/create_execution_handler.test.ts b/x-pack/legacy/plugins/alerting/server/lib/create_execution_handler.test.ts index 4f523f203f87a7..d86a06767c9d16 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/create_execution_handler.test.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/create_execution_handler.test.ts @@ -28,6 +28,7 @@ const createExecutionHandlerParams = { { id: '1', group: 'default', + actionTypeId: 'test', params: { foo: true, contextVal: 'My {{context.value}} goes here', diff --git a/x-pack/legacy/plugins/alerting/server/routes/create.test.ts b/x-pack/legacy/plugins/alerting/server/routes/create.test.ts index 318dbdf068d6a9..fe3ab94674db33 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/create.test.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/create.test.ts @@ -22,6 +22,7 @@ const mockedAlert = { { group: 'default', id: '2', + actionTypeId: 'test', params: { foo: true, }, @@ -49,6 +50,7 @@ test('creates an alert with proper parameters', async () => { Object { "actions": Array [ Object { + "actionTypeId": "test", "group": "default", "id": "2", "params": Object { @@ -75,6 +77,7 @@ test('creates an alert with proper parameters', async () => { "data": Object { "actions": Array [ Object { + "actionTypeId": "test", "group": "default", "id": "2", "params": Object { @@ -104,6 +107,7 @@ test('creates an alert with proper parameters', async () => { "data": Object { "actions": Array [ Object { + "actionTypeId": "test", "group": "default", "id": "2", "params": Object { diff --git a/x-pack/legacy/plugins/alerting/server/routes/create.ts b/x-pack/legacy/plugins/alerting/server/routes/create.ts index fb82a03f172b3b..c11065c286dae5 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/create.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/create.ts @@ -6,7 +6,6 @@ import Hapi from 'hapi'; import Joi from 'joi'; -import { AlertAction } from '../types'; import { getDurationSchema } from '../lib'; interface ScheduleRequest extends Hapi.Request { @@ -16,7 +15,12 @@ interface ScheduleRequest extends Hapi.Request { tags: string[]; alertTypeId: string; interval: string; - actions: AlertAction[]; + actions: Array<{ + group: string; + id: string; + actionTypeId: string; + params: Record; + }>; params: Record; throttle: string | null; }; @@ -47,6 +51,7 @@ export const createAlertRoute = { Joi.object().keys({ group: Joi.string().required(), id: Joi.string().required(), + actionTypeId: Joi.string().required(), params: Joi.object().required(), }) ) diff --git a/x-pack/legacy/plugins/alerting/server/routes/get.test.ts b/x-pack/legacy/plugins/alerting/server/routes/get.test.ts index 19618bc9e39feb..4d44ee9dfe6bda 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/get.test.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/get.test.ts @@ -21,6 +21,7 @@ const mockedAlert = { { group: 'default', id: '2', + actionTypeId: 'test', params: { foo: true, }, diff --git a/x-pack/legacy/plugins/alerting/server/routes/update.test.ts b/x-pack/legacy/plugins/alerting/server/routes/update.test.ts index 7fc3f459110107..f53be5a876db44 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/update.test.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/update.test.ts @@ -24,6 +24,7 @@ const mockedResponse = { { group: 'default', id: '2', + actionTypeId: 'test', params: { baz: true, }, @@ -47,6 +48,7 @@ test('calls the update function with proper parameters', async () => { { group: 'default', id: '2', + actionTypeId: 'test', params: { baz: true, }, @@ -67,6 +69,7 @@ test('calls the update function with proper parameters', async () => { "data": Object { "actions": Array [ Object { + "actionTypeId": "test", "group": "default", "id": "2", "params": Object { diff --git a/x-pack/legacy/plugins/alerting/server/routes/update.ts b/x-pack/legacy/plugins/alerting/server/routes/update.ts index 6aeedb93a10985..0a9aa1552f2c58 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/update.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/update.ts @@ -6,7 +6,6 @@ import Joi from 'joi'; import Hapi from 'hapi'; -import { AlertAction } from '../types'; import { getDurationSchema } from '../lib'; interface UpdateRequest extends Hapi.Request { @@ -18,7 +17,12 @@ interface UpdateRequest extends Hapi.Request { name: string; tags: string[]; interval: string; - actions: AlertAction[]; + actions: Array<{ + group: string; + id: string; + actionTypeId: string; + params: Record; + }>; params: Record; throttle: string | null; }; @@ -49,6 +53,7 @@ export const updateAlertRoute = { Joi.object().keys({ group: Joi.string().required(), id: Joi.string().required(), + actionTypeId: Joi.string().required(), params: Joi.object().required(), }) ) diff --git a/x-pack/legacy/plugins/alerting/server/types.ts b/x-pack/legacy/plugins/alerting/server/types.ts index e2460c549c05db..1bec2632d80822 100644 --- a/x-pack/legacy/plugins/alerting/server/types.ts +++ b/x-pack/legacy/plugins/alerting/server/types.ts @@ -49,12 +49,14 @@ export type AlertActionParams = SavedObjectAttributes; export interface AlertAction { group: string; id: string; + actionTypeId: string; params: AlertActionParams; } export interface RawAlertAction extends SavedObjectAttributes { group: string; actionRef: string; + actionTypeId: string; params: AlertActionParams; } diff --git a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts index 57b4b3b6c26c67..3c67a5cc996c59 100644 --- a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts +++ b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts @@ -191,6 +191,7 @@ export class AlertUtils { { group: 'default', id: this.indexRecordActionId, + actionTypeId: 'test.index-record', params: { index: ES_TEST_INDEX_NAME, reference, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/find.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/find.ts index 1c6d5480e400f2..191a09f947ce6d 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/find.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/find.ts @@ -170,6 +170,7 @@ export default function findActionTests({ getService }: FtrProviderContext) { { group: 'default', id: createdAction.id, + actionTypeId: 'test.index-record', params: {}, }, ], 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 09a642d1d14bbb..dca787da51617e 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 @@ -174,6 +174,7 @@ export default function alertTests({ getService }: FtrProviderContext) { { group: 'default', id: createdAction.id, + actionTypeId: 'test.rate-limit', params: { reference, index: ES_TEST_INDEX_NAME, @@ -364,6 +365,7 @@ export default function alertTests({ getService }: FtrProviderContext) { { group: 'default', id: createdAction.id, + actionTypeId: 'test.authorization', params: { callClusterAuthorizationIndex: authorizationIndex, savedObjectsClientType: 'dashboard', @@ -500,6 +502,7 @@ export default function alertTests({ getService }: FtrProviderContext) { { group: 'default', id: indexRecordActionId, + actionTypeId: 'test.index-record', params: { index: ES_TEST_INDEX_NAME, reference, @@ -509,6 +512,7 @@ export default function alertTests({ getService }: FtrProviderContext) { { group: 'other', id: indexRecordActionId, + actionTypeId: 'test.index-record', params: { index: ES_TEST_INDEX_NAME, reference, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts index 9af4848c57d7d1..49e1b96a06806e 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts @@ -133,6 +133,7 @@ export default function alertTests({ getService }: FtrProviderContext) { { group: 'default', id: createdAction.id, + actionTypeId: 'test.rate-limit', params: { reference, index: ES_TEST_INDEX_NAME, @@ -246,6 +247,7 @@ export default function alertTests({ getService }: FtrProviderContext) { { group: 'default', id: createdAction.id, + actionTypeId: 'test.authorization', params: { callClusterAuthorizationIndex: authorizationIndex, savedObjectsClientType: 'dashboard', From f88be6dff1ef6912eea8cbe63fefea1a8fc2d1b4 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Wed, 4 Dec 2019 14:00:31 -0500 Subject: [PATCH 2/5] Add tests --- .../tests/alerting/find.ts | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts index 31af7a0acffbb8..9a219cdfaa66f3 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts @@ -78,10 +78,33 @@ export default function createFindTests({ getService }: FtrProviderContext) { }); it('should handle find alert request with filter appropriately', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(space.id)}/api/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action', + actionTypeId: 'test.noop', + config: {}, + secrets: {}, + }) + .expect(200); + const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alert`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send( + getTestAlertData({ + enabled: false, + actions: [ + { + id: createdAction.id, + group: 'default', + actionTypeId: 'test.noop', + params: {}, + }, + ], + }) + ) .expect(200); objectRemover.add(space.id, createdAlert.id, 'alert'); @@ -89,7 +112,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { .get( `${getUrlPrefix( space.id - )}/api/alert/_find?filter=alert.attributes.alertTypeId:test.noop` + )}/api/alert/_find?filter=alert.attributes.actions:{ actionTypeId: test.noop }` ) .auth(user.username, user.password); @@ -117,11 +140,17 @@ export default function createFindTests({ getService }: FtrProviderContext) { tags: ['foo'], alertTypeId: 'test.noop', interval: '1m', - enabled: true, - actions: [], + enabled: false, + actions: [ + { + id: createdAction.id, + group: 'default', + actionTypeId: 'test.noop', + params: {}, + }, + ], params: {}, createdBy: 'elastic', - scheduledTaskId: match.scheduledTaskId, throttle: '1m', updatedBy: 'elastic', apiKeyOwner: 'elastic', From 005d3d9933a3e4f26e5d7222bb175c290875628c Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Fri, 6 Dec 2019 09:21:15 -0500 Subject: [PATCH 3/5] No longer pass actionTypeId for each alert action in APIs --- .../alerting/server/alerts_client.test.ts | 127 +++++++++++++++++- .../plugins/alerting/server/alerts_client.ts | 93 +++++++------ .../alerting/server/routes/create.test.ts | 38 +----- .../plugins/alerting/server/routes/create.ts | 2 - .../alerting/server/routes/update.test.ts | 2 - .../plugins/alerting/server/routes/update.ts | 2 - .../common/lib/alert_utils.ts | 1 - .../security_and_spaces/tests/actions/find.ts | 1 - .../tests/alerting/alerts.ts | 5 +- .../tests/alerting/create.ts | 32 ++++- .../tests/alerting/find.ts | 1 - .../spaces_only/tests/alerting/alerts.ts | 2 - .../spaces_only/tests/alerting/create.ts | 32 ++++- 13 files changed, 245 insertions(+), 93 deletions(-) diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts index de4e2b316f202b..42b36a44a42e76 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts @@ -75,6 +75,18 @@ describe('create()', () => { actionGroups: ['default'], async executor() {}, }); + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actionTypeId: 'test', + }, + references: [], + }, + ], + }); savedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -88,6 +100,7 @@ describe('create()', () => { { group: 'default', actionRef: 'action_0', + actionTypeId: 'test', params: { foo: true, }, @@ -134,6 +147,7 @@ describe('create()', () => { Object { "actions": Array [ Object { + "actionTypeId": "test", "group": "default", "id": "1", "params": Object { @@ -235,6 +249,18 @@ describe('create()', () => { actionGroups: ['default'], async executor() {}, }); + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actionTypeId: 'test', + }, + references: [], + }, + ], + }); savedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -249,6 +275,7 @@ describe('create()', () => { { group: 'default', actionRef: 'action_0', + actionTypeId: 'test', params: { foo: true, }, @@ -268,6 +295,7 @@ describe('create()', () => { Object { "actions": Array [ Object { + "actionTypeId": "test", "group": "default", "id": "1", "params": Object { @@ -308,6 +336,23 @@ describe('create()', () => { ); }); + test('throws error if loading actions fails', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + const data = getMockData(); + alertTypeRegistry.get.mockReturnValueOnce({ + id: '123', + name: 'Test', + actionGroups: ['default'], + async executor() {}, + }); + savedObjectsClient.bulkGet.mockRejectedValueOnce(new Error('Test Error')); + await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Test Error"` + ); + expect(savedObjectsClient.create).not.toHaveBeenCalled(); + expect(taskManager.schedule).not.toHaveBeenCalled(); + }); + test('throws error if create saved object fails', async () => { const alertsClient = new AlertsClient(alertsClientParams); const data = getMockData(); @@ -317,6 +362,18 @@ describe('create()', () => { actionGroups: ['default'], async executor() {}, }); + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actionTypeId: 'test', + }, + references: [], + }, + ], + }); savedObjectsClient.create.mockRejectedValueOnce(new Error('Test failure')); await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( `"Test failure"` @@ -333,6 +390,18 @@ describe('create()', () => { actionGroups: ['default'], async executor() {}, }); + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actionTypeId: 'test', + }, + references: [], + }, + ], + }); savedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -346,6 +415,7 @@ describe('create()', () => { { group: 'default', actionRef: 'action_0', + actionTypeId: 'test', params: { foo: true, }, @@ -383,6 +453,18 @@ describe('create()', () => { actionGroups: ['default'], async executor() {}, }); + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actionTypeId: 'test', + }, + references: [], + }, + ], + }); savedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -396,6 +478,7 @@ describe('create()', () => { { group: 'default', actionRef: 'action_0', + actionTypeId: 'test', params: { foo: true, }, @@ -444,6 +527,18 @@ describe('create()', () => { created: true, result: { id: '123', api_key: 'abc' }, }); + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actionTypeId: 'test', + }, + references: [], + }, + ], + }); savedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -457,6 +552,7 @@ describe('create()', () => { { group: 'default', actionRef: 'action_0', + actionTypeId: 'test', params: { foo: true, }, @@ -1152,6 +1248,18 @@ describe('update()', () => { references: [], version: '123', }); + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actionTypeId: 'test', + }, + references: [], + }, + ], + }); savedObjectsClient.update.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -1165,6 +1273,7 @@ describe('update()', () => { { group: 'default', actionRef: 'action_0', + actionTypeId: 'test', params: { foo: true, }, @@ -1193,7 +1302,6 @@ describe('update()', () => { { group: 'default', id: '1', - actionTypeId: 'test', params: { foo: true, }, @@ -1205,6 +1313,7 @@ describe('update()', () => { Object { "actions": Array [ Object { + "actionTypeId": "test", "group": "default", "id": "1", "params": Object { @@ -1286,6 +1395,18 @@ describe('update()', () => { references: [], version: '123', }); + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actionTypeId: 'test', + }, + references: [], + }, + ], + }); alertsClientParams.createAPIKey.mockResolvedValueOnce({ created: true, result: { id: '123', api_key: 'abc' }, @@ -1303,6 +1424,7 @@ describe('update()', () => { { group: 'default', actionRef: 'action_0', + actionTypeId: 'test', params: { foo: true, }, @@ -1332,7 +1454,6 @@ describe('update()', () => { { group: 'default', id: '1', - actionTypeId: 'test', params: { foo: true, }, @@ -1344,6 +1465,7 @@ describe('update()', () => { Object { "actions": Array [ Object { + "actionTypeId": "test", "group": "default", "id": "1", "params": Object { @@ -1442,7 +1564,6 @@ describe('update()', () => { { group: 'default', id: '1', - actionTypeId: 'test', params: { foo: true, }, diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.ts index 3916ec1d62b6c3..507cff73c4459e 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.ts @@ -21,6 +21,7 @@ interface SuccessCreateAPIKeyResult { result: SecurityPluginCreateAPIKeyResult; } export type CreateAPIKeyResult = FailedCreateAPIKeyResult | SuccessCreateAPIKeyResult; +type NormalizedAlertAction = Omit; interface ConstructorOptions { logger: Logger; @@ -62,9 +63,15 @@ interface CreateOptions { Alert, Exclude< keyof Alert, - 'createdBy' | 'updatedBy' | 'apiKey' | 'apiKeyOwner' | 'muteAll' | 'mutedInstanceIds' + | 'createdBy' + | 'updatedBy' + | 'apiKey' + | 'apiKeyOwner' + | 'muteAll' + | 'mutedInstanceIds' + | 'actions' > - >; + > & { actions: NormalizedAlertAction[] }; options?: { migrationVersion?: Record; }; @@ -76,7 +83,7 @@ interface UpdateOptions { name: string; tags: string[]; interval: string; - actions: AlertAction[]; + actions: NormalizedAlertAction[]; params: Record; }; } @@ -117,8 +124,10 @@ export class AlertsClient { this.validateActions(alertType, data.actions); - const { alert: rawAlert, references } = this.getRawAlert({ + const { references, actions } = await this.denormalizeActions(data.actions); + const rawAlert: RawAlert = { ...data, + actions, createdBy: username, updatedBy: username, apiKeyOwner: apiKey.created && username ? username : undefined, @@ -128,7 +137,7 @@ export class AlertsClient { params: validatedAlertTypeParams, muteAll: false, mutedInstanceIds: [], - }); + }; const createdAlert = await this.savedObjectsClient.create('alert', rawAlert, { ...options, references, @@ -202,7 +211,7 @@ export class AlertsClient { const validatedAlertTypeParams = validateAlertTypeParams(alertType, data.params); this.validateActions(alertType, data.actions); - const { actions, references } = this.extractReferences(data.actions); + const { actions, references } = await this.denormalizeActions(data.actions); const username = await this.getUserName(); const updatedObject = await this.savedObjectsClient.update( 'alert', @@ -371,26 +380,6 @@ export class AlertsClient { }); } - private extractReferences(actions: Alert['actions']) { - const references: SavedObjectReference[] = []; - const rawActions = actions.map((action, i) => { - const actionRef = `action_${i}`; - references.push({ - name: actionRef, - type: 'action', - id: action.id, - }); - return { - ...omit(action, 'id'), - actionRef, - }; - }) as RawAlert['actions']; - return { - actions: rawActions, - references, - }; - } - private injectReferencesIntoActions( actions: RawAlert['actions'], references: SavedObjectReference[] @@ -426,19 +415,7 @@ export class AlertsClient { }; } - private getRawAlert(alert: Alert): { alert: RawAlert; references: SavedObjectReference[] } { - const { references, actions } = this.extractReferences(alert.actions); - return { - alert: { - ...alert, - actions, - }, - references, - }; - } - - private validateActions(alertType: AlertType, actions: Alert['actions']) { - // TODO: Should also ensure user has access to each action + private validateActions(alertType: AlertType, actions: NormalizedAlertAction[]): void { const { actionGroups: alertTypeActionGroups } = alertType; const usedAlertActionGroups = actions.map(action => action.group); const invalidActionGroups = usedAlertActionGroups.filter( @@ -455,4 +432,42 @@ export class AlertsClient { ); } } + + private async denormalizeActions( + alertActions: NormalizedAlertAction[] + ): Promise<{ actions: RawAlert['actions']; references: SavedObjectReference[] }> { + // Fetch action objects in bulk + const bulkGetOpts = [ + ...new Set(alertActions.map(alertAction => ({ id: alertAction.id, type: 'action' }))), + ]; + const bulkGetResult = await this.savedObjectsClient.bulkGet(bulkGetOpts); + const actionMap = new Map(); + for (const action of bulkGetResult.saved_objects) { + if (action.error) { + throw Boom.badRequest( + `Failed to load action ${action.id} (${action.error.statusCode}): ${action.error.message}` + ); + } + actionMap.set(action.id, action); + } + // Extract references and set actionTypeId + const references: SavedObjectReference[] = []; + const actions = alertActions.map(({ id, ...alertAction }, i) => { + const actionRef = `action_${i}`; + references.push({ + id, + name: actionRef, + type: 'action', + }); + return { + ...alertAction, + actionRef, + actionTypeId: actionMap.get(id).attributes.actionTypeId, + }; + }); + return { + actions, + references, + }; + } } diff --git a/x-pack/legacy/plugins/alerting/server/routes/create.test.ts b/x-pack/legacy/plugins/alerting/server/routes/create.test.ts index fe3ab94674db33..634a797880812c 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/create.test.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/create.test.ts @@ -22,7 +22,6 @@ const mockedAlert = { { group: 'default', id: '2', - actionTypeId: 'test', params: { foo: true, }, @@ -42,6 +41,12 @@ test('creates an alert with proper parameters', async () => { alertsClient.create.mockResolvedValueOnce({ ...mockedAlert, id: '123', + actions: [ + { + ...mockedAlert.actions[0], + actionTypeId: 'test', + }, + ], }); const { payload, statusCode } = await server.inject(request); expect(statusCode).toBe(200); @@ -77,37 +82,6 @@ test('creates an alert with proper parameters', async () => { "data": Object { "actions": Array [ Object { - "actionTypeId": "test", - "group": "default", - "id": "2", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "1", - "enabled": true, - "interval": "10s", - "name": "abc", - "params": Object { - "bar": true, - }, - "tags": Array [ - "foo", - ], - "throttle": null, - }, - }, - ] - `); - expect(alertsClient.create).toHaveBeenCalledTimes(1); - expect(alertsClient.create.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "data": Object { - "actions": Array [ - Object { - "actionTypeId": "test", "group": "default", "id": "2", "params": Object { diff --git a/x-pack/legacy/plugins/alerting/server/routes/create.ts b/x-pack/legacy/plugins/alerting/server/routes/create.ts index c11065c286dae5..cb5277ae191003 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/create.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/create.ts @@ -18,7 +18,6 @@ interface ScheduleRequest extends Hapi.Request { actions: Array<{ group: string; id: string; - actionTypeId: string; params: Record; }>; params: Record; @@ -51,7 +50,6 @@ export const createAlertRoute = { Joi.object().keys({ group: Joi.string().required(), id: Joi.string().required(), - actionTypeId: Joi.string().required(), params: Joi.object().required(), }) ) diff --git a/x-pack/legacy/plugins/alerting/server/routes/update.test.ts b/x-pack/legacy/plugins/alerting/server/routes/update.test.ts index f53be5a876db44..334fb2120319de 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/update.test.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/update.test.ts @@ -48,7 +48,6 @@ test('calls the update function with proper parameters', async () => { { group: 'default', id: '2', - actionTypeId: 'test', params: { baz: true, }, @@ -69,7 +68,6 @@ test('calls the update function with proper parameters', async () => { "data": Object { "actions": Array [ Object { - "actionTypeId": "test", "group": "default", "id": "2", "params": Object { diff --git a/x-pack/legacy/plugins/alerting/server/routes/update.ts b/x-pack/legacy/plugins/alerting/server/routes/update.ts index 0a9aa1552f2c58..6e8f8557fb24ad 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/update.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/update.ts @@ -20,7 +20,6 @@ interface UpdateRequest extends Hapi.Request { actions: Array<{ group: string; id: string; - actionTypeId: string; params: Record; }>; params: Record; @@ -53,7 +52,6 @@ export const updateAlertRoute = { Joi.object().keys({ group: Joi.string().required(), id: Joi.string().required(), - actionTypeId: Joi.string().required(), params: Joi.object().required(), }) ) diff --git a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts index 3c67a5cc996c59..57b4b3b6c26c67 100644 --- a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts +++ b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts @@ -191,7 +191,6 @@ export class AlertUtils { { group: 'default', id: this.indexRecordActionId, - actionTypeId: 'test.index-record', params: { index: ES_TEST_INDEX_NAME, reference, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/find.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/find.ts index 46a325b3400c95..89c5b4f451f82b 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/find.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/find.ts @@ -170,7 +170,6 @@ export default function findActionTests({ getService }: FtrProviderContext) { { group: 'default', id: createdAction.id, - actionTypeId: 'test.index-record', params: {}, }, ], 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 f9e62d09f70c73..8f3996f958bb23 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 @@ -174,7 +174,6 @@ export default function alertTests({ getService }: FtrProviderContext) { { group: 'default', id: createdAction.id, - actionTypeId: 'test.rate-limit', params: { reference, index: ES_TEST_INDEX_NAME, @@ -365,7 +364,6 @@ export default function alertTests({ getService }: FtrProviderContext) { { group: 'default', id: createdAction.id, - actionTypeId: 'test.authorization', params: { callClusterAuthorizationIndex: authorizationIndex, savedObjectsClientType: 'dashboard', @@ -502,7 +500,6 @@ export default function alertTests({ getService }: FtrProviderContext) { { group: 'default', id: indexRecordActionId, - actionTypeId: 'test.index-record', params: { index: ES_TEST_INDEX_NAME, reference, @@ -512,7 +509,6 @@ export default function alertTests({ getService }: FtrProviderContext) { { group: 'other', id: indexRecordActionId, - actionTypeId: 'test.index-record', params: { index: ES_TEST_INDEX_NAME, reference, @@ -536,6 +532,7 @@ export default function alertTests({ getService }: FtrProviderContext) { break; case 'space_1_all at space1': case 'superuser at space1': + expect(response.statusCode).to.eql(200); // Wait for actions to execute twice before disabling the alert and waiting for tasks to finish await esTestIndexTool.waitForDocs('action:test.index-record', reference, 2); await alertUtils.disable(response.body.id); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts index bf61ee2e3f1375..c0a99ae068e714 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts @@ -31,11 +31,32 @@ export default function createAlertTests({ getService }: FtrProviderContext) { const { user, space } = scenario; describe(scenario.id, () => { it('should handle create alert request appropriately', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(space.id)}/api/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY action', + actionTypeId: 'test.noop', + config: {}, + secrets: {}, + }) + .expect(200); + const response = await supertestWithoutAuth .post(`${getUrlPrefix(space.id)}/api/alert`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) - .send(getTestAlertData()); + .send( + getTestAlertData({ + actions: [ + { + id: createdAction.id, + group: 'default', + params: {}, + }, + ], + }) + ); switch (scenario.id) { case 'no_kibana_privileges at space1': @@ -56,7 +77,14 @@ export default function createAlertTests({ getService }: FtrProviderContext) { id: response.body.id, name: 'abc', tags: ['foo'], - actions: [], + actions: [ + { + id: createdAction.id, + actionTypeId: createdAction.actionTypeId, + group: 'default', + params: {}, + }, + ], enabled: true, alertTypeId: 'test.noop', params: {}, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts index 9a219cdfaa66f3..359058f2ac23af 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts @@ -99,7 +99,6 @@ export default function createFindTests({ getService }: FtrProviderContext) { { id: createdAction.id, group: 'default', - actionTypeId: 'test.noop', params: {}, }, ], diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts index 748375e9614c61..5fafd8b0bfb610 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts @@ -133,7 +133,6 @@ export default function alertTests({ getService }: FtrProviderContext) { { group: 'default', id: createdAction.id, - actionTypeId: 'test.rate-limit', params: { reference, index: ES_TEST_INDEX_NAME, @@ -247,7 +246,6 @@ export default function alertTests({ getService }: FtrProviderContext) { { group: 'default', id: createdAction.id, - actionTypeId: 'test.authorization', params: { callClusterAuthorizationIndex: authorizationIndex, savedObjectsClientType: 'dashboard', diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts index 3018f8efffffeb..929905a958abbd 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts @@ -27,10 +27,31 @@ export default function createAlertTests({ getService }: FtrProviderContext) { } it('should handle create alert request appropriately', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY action', + actionTypeId: 'test.noop', + config: {}, + secrets: {}, + }) + .expect(200); + const response = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()); + .send( + getTestAlertData({ + actions: [ + { + id: createdAction.id, + group: 'default', + params: {}, + }, + ], + }) + ); expect(response.statusCode).to.eql(200); objectRemover.add(Spaces.space1.id, response.body.id, 'alert'); @@ -38,7 +59,14 @@ export default function createAlertTests({ getService }: FtrProviderContext) { id: response.body.id, name: 'abc', tags: ['foo'], - actions: [], + actions: [ + { + id: createdAction.id, + actionTypeId: createdAction.actionTypeId, + group: 'default', + params: {}, + }, + ], enabled: true, alertTypeId: 'test.noop', params: {}, From 1211ad146ae61fa975a2cb1c7f88cea0808180a9 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Mon, 9 Dec 2019 16:26:38 -0500 Subject: [PATCH 4/5] Add tests to ensure denormalizeActions works on multiple actions --- .../alerting/server/alerts_client.test.ts | 356 +++++++++++++++++- .../plugins/alerting/server/alerts_client.ts | 5 +- 2 files changed, 357 insertions(+), 4 deletions(-) diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts index 42b36a44a42e76..8ff54e25a0c992 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts @@ -55,7 +55,6 @@ function getMockData(overwrites: Record = {}) { { group: 'default', id: '1', - actionTypeId: 'test', params: { foo: true, }, @@ -240,6 +239,184 @@ describe('create()', () => { `); }); + test('creates an alert with multiple actions', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + const data = getMockData({ + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + { + group: 'default', + id: '2', + params: { + foo: true, + }, + }, + ], + }); + alertTypeRegistry.get.mockReturnValueOnce({ + id: '123', + name: 'Test', + actionGroups: ['default'], + async executor() {}, + }); + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actionTypeId: 'test', + }, + references: [], + }, + { + id: '2', + type: 'action', + attributes: { + actionTypeId: 'test2', + }, + references: [], + }, + ], + }); + savedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + interval: '10s', + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + { + group: 'default', + actionRef: 'action_1', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + { + group: 'default', + actionRef: 'action_2', + actionTypeId: 'test2', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + { + name: 'action_1', + type: 'action', + id: '1', + }, + { + name: 'action_2', + type: 'action', + id: '2', + }, + ], + }); + taskManager.schedule.mockResolvedValueOnce({ + id: 'task-123', + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: 'idle', + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: {}, + ownerId: null, + }); + savedObjectsClient.update.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + scheduledTaskId: 'task-123', + }, + references: [], + }); + const result = await alertsClient.create({ data }); + expect(result).toMatchInlineSnapshot(` + Object { + "actions": Array [ + Object { + "actionTypeId": "test", + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + Object { + "actionTypeId": "test", + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + Object { + "actionTypeId": "test2", + "group": "default", + "id": "2", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "id": "1", + "interval": "10s", + "params": Object { + "bar": true, + }, + "scheduledTaskId": "task-123", + } + `); + expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith([ + { + id: '1', + type: 'action', + }, + { + id: '2', + type: 'action', + }, + ]); + }); + test('creates a disabled alert', async () => { const alertsClient = new AlertsClient(alertsClientParams); const data = getMockData({ enabled: false }); @@ -1376,6 +1553,183 @@ describe('update()', () => { `); }); + it('updates with multiple actions', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + alertTypeRegistry.get.mockReturnValueOnce({ + id: '123', + name: 'Test', + actionGroups: ['default'], + async executor() {}, + }); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + enabled: true, + alertTypeId: '123', + scheduledTaskId: 'task-123', + }, + references: [], + version: '123', + }); + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actionTypeId: 'test', + }, + references: [], + }, + { + id: '2', + type: 'action', + attributes: { + actionTypeId: 'test2', + }, + references: [], + }, + ], + }); + savedObjectsClient.update.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + enabled: true, + interval: '10s', + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + { + group: 'default', + actionRef: 'action_1', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + { + group: 'default', + actionRef: 'action_2', + actionTypeId: 'test2', + params: { + foo: true, + }, + }, + ], + scheduledTaskId: 'task-123', + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + { + name: 'action_1', + type: 'action', + id: '1', + }, + { + name: 'action_2', + type: 'action', + id: '2', + }, + ], + }); + const result = await alertsClient.update({ + id: '1', + data: { + interval: '10s', + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + { + group: 'default', + id: '2', + params: { + foo: true, + }, + }, + ], + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "actions": Array [ + Object { + "actionTypeId": "test", + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + Object { + "actionTypeId": "test", + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + Object { + "actionTypeId": "test2", + "group": "default", + "id": "2", + "params": Object { + "foo": true, + }, + }, + ], + "enabled": true, + "id": "1", + "interval": "10s", + "params": Object { + "bar": true, + }, + "scheduledTaskId": "task-123", + } + `); + expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith([ + { + id: '1', + type: 'action', + }, + { + id: '2', + type: 'action', + }, + ]); + }); + it('calls the createApiKey function', async () => { const alertsClient = new AlertsClient(alertsClientParams); alertTypeRegistry.get.mockReturnValueOnce({ diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.ts index 507cff73c4459e..920f9bca666542 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.ts @@ -437,9 +437,8 @@ export class AlertsClient { alertActions: NormalizedAlertAction[] ): Promise<{ actions: RawAlert['actions']; references: SavedObjectReference[] }> { // Fetch action objects in bulk - const bulkGetOpts = [ - ...new Set(alertActions.map(alertAction => ({ id: alertAction.id, type: 'action' }))), - ]; + const actionIds = [...new Set(alertActions.map(alertAction => alertAction.id))] + const bulkGetOpts = actionIds.map(id => ({ id, type: 'action' })); const bulkGetResult = await this.savedObjectsClient.bulkGet(bulkGetOpts); const actionMap = new Map(); for (const action of bulkGetResult.saved_objects) { From ebf3678f76fd8d2a6a06e413625c7b2c4db24e72 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Mon, 9 Dec 2019 18:11:57 -0500 Subject: [PATCH 5/5] Fix ESLint errors --- x-pack/legacy/plugins/alerting/server/alerts_client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.ts index 920f9bca666542..27fda9871e6854 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.ts @@ -437,7 +437,7 @@ export class AlertsClient { alertActions: NormalizedAlertAction[] ): Promise<{ actions: RawAlert['actions']; references: SavedObjectReference[] }> { // Fetch action objects in bulk - const actionIds = [...new Set(alertActions.map(alertAction => alertAction.id))] + const actionIds = [...new Set(alertActions.map(alertAction => alertAction.id))]; const bulkGetOpts = actionIds.map(id => ({ id, type: 'action' })); const bulkGetResult = await this.savedObjectsClient.bulkGet(bulkGetOpts); const actionMap = new Map();