From 2b3af4c99f6d0b11515baf9dfc1c8218eabdf05c Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Fri, 26 Apr 2019 15:47:52 -0400 Subject: [PATCH 01/51] Basic alerting plugin with actions --- x-pack/index.js | 2 + x-pack/plugins/alerting/index.ts | 26 +++ x-pack/plugins/alerting/mappings.json | 20 ++ .../alerting/server/alerting_service.test.ts | 185 ++++++++++++++++++ .../alerting/server/alerting_service.ts | 66 +++++++ .../server/default_connectors/console.ts | 14 ++ .../server/default_connectors/index.ts | 7 + x-pack/plugins/alerting/server/index.ts | 8 + .../alerting/server/routes/create_action.ts | 46 +++++ .../plugins/alerting/server/routes/index.ts | 7 + .../apis/alerting/create_action.ts | 120 ++++++++++++ .../api_integration/apis/alerting/index.ts | 14 ++ x-pack/test/api_integration/apis/index.js | 1 + 13 files changed, 516 insertions(+) create mode 100644 x-pack/plugins/alerting/index.ts create mode 100644 x-pack/plugins/alerting/mappings.json create mode 100644 x-pack/plugins/alerting/server/alerting_service.test.ts create mode 100644 x-pack/plugins/alerting/server/alerting_service.ts create mode 100644 x-pack/plugins/alerting/server/default_connectors/console.ts create mode 100644 x-pack/plugins/alerting/server/default_connectors/index.ts create mode 100644 x-pack/plugins/alerting/server/index.ts create mode 100644 x-pack/plugins/alerting/server/routes/create_action.ts create mode 100644 x-pack/plugins/alerting/server/routes/index.ts create mode 100644 x-pack/test/api_integration/apis/alerting/create_action.ts create mode 100644 x-pack/test/api_integration/apis/alerting/index.ts diff --git a/x-pack/index.js b/x-pack/index.js index 86bed4bcc8851..a20ff0bc28e2d 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -38,6 +38,7 @@ import { translations } from './plugins/translations'; import { upgradeAssistant } from './plugins/upgrade_assistant'; import { uptime } from './plugins/uptime'; import { ossTelemetry } from './plugins/oss_telemetry'; +import { alerting } from './plugins/alerting'; module.exports = function (kibana) { return [ @@ -75,5 +76,6 @@ module.exports = function (kibana) { upgradeAssistant(kibana), uptime(kibana), ossTelemetry(kibana), + alerting(kibana), ]; }; diff --git a/x-pack/plugins/alerting/index.ts b/x-pack/plugins/alerting/index.ts new file mode 100644 index 0000000000000..7a832164461ca --- /dev/null +++ b/x-pack/plugins/alerting/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Hapi from 'hapi'; +import mappings from './mappings.json'; +import { AlertingService, createActionRoute } from './server'; + +export function alerting(kibana: any) { + return new kibana.Plugin({ + id: 'alerting', + require: ['kibana', 'elasticsearch'], + init(server: Hapi.Server) { + const alertingService = new AlertingService(); + // Routes + createActionRoute(server); + // Register service to server + server.decorate('server', 'alerting', () => alertingService); + }, + uiExports: { + mappings, + }, + }); +} diff --git a/x-pack/plugins/alerting/mappings.json b/x-pack/plugins/alerting/mappings.json new file mode 100644 index 0000000000000..29bd3aea5b2a4 --- /dev/null +++ b/x-pack/plugins/alerting/mappings.json @@ -0,0 +1,20 @@ +{ + "action": { + "properties": { + "description": { + "type": "text" + }, + "connectorId": { + "type": "keyword" + }, + "connectorOptions": { + "dynamic": "true", + "type": "object" + }, + "connectorOptionsSecrets": { + "dynamic": "true", + "type": "object" + } + } + } +} diff --git a/x-pack/plugins/alerting/server/alerting_service.test.ts b/x-pack/plugins/alerting/server/alerting_service.test.ts new file mode 100644 index 0000000000000..f1f05f00d55ea --- /dev/null +++ b/x-pack/plugins/alerting/server/alerting_service.test.ts @@ -0,0 +1,185 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AlertingService } from './alerting_service'; + +describe('#AlertingService', () => { + const savedObjectsClient = { + errors: {} as any, + bulkCreate: jest.fn(), + bulkGet: jest.fn(), + create: jest.fn(), + delete: jest.fn(), + find: jest.fn(), + get: jest.fn(), + update: jest.fn(), + }; + + beforeEach(() => jest.resetAllMocks()); + + test('automatically registers default connectors', () => { + const alerting = new AlertingService(); + expect(alerting.hasConnector('console')).toEqual(true); + }); + + describe('registerConnector()', () => { + test('able to register connectors', () => { + const executor = jest.fn(); + const alerting = new AlertingService(); + alerting.registerConnector('my-connector', executor); + }); + + test('throws error if connector already registered', () => { + const executor = jest.fn(); + const alerting = new AlertingService(); + alerting.registerConnector('my-connector', executor); + expect(() => + alerting.registerConnector('my-connector', executor) + ).toThrowErrorMatchingInlineSnapshot(`"Connector \\"my-connector\\" is already registered"`); + }); + }); + + describe('hasConnector()', () => { + test('returns true for default connectors', () => { + const alerting = new AlertingService(); + expect(alerting.hasConnector('console')).toEqual(true); + }); + + test('returns true after registering a connector', () => { + const executor = jest.fn(); + const alerting = new AlertingService(); + alerting.registerConnector('my-connector', executor); + expect(alerting.hasConnector('my-connector')).toEqual(true); + }); + + test('returns false for unregistered connectors', () => { + const alerting = new AlertingService(); + expect(alerting.hasConnector('my-connector')).toEqual(false); + }); + }); + + describe('createAction()', () => { + test('creates an action with all given properties', async () => { + const expectedResult = Symbol(); + const alerting = new AlertingService(); + savedObjectsClient.create.mockResolvedValueOnce(expectedResult); + const result = await alerting.createAction(savedObjectsClient, { + id: 'my-alert', + description: 'my description', + connectorId: 'console', + connectorOptions: {}, + connectorOptionsSecrets: {}, + }); + expect(result).toEqual(expectedResult); + expect(savedObjectsClient.create).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + "action", + Object { + "connectorId": "console", + "connectorOptions": Object {}, + "connectorOptionsSecrets": Object {}, + "description": "my description", + }, + Object { + "id": "my-alert", + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], +} +`); + }); + + test(`throws an error when connector doesn't exist`, async () => { + const alerting = new AlertingService(); + await expect( + alerting.createAction(savedObjectsClient, { + id: 'my-alert', + description: 'my description', + connectorId: 'unregistered-connector', + connectorOptions: {}, + connectorOptionsSecrets: {}, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Connector \\"unregistered-connector\\" is not registered"` + ); + }); + }); + + describe('fireAction()', () => { + test('fires an action with all given parameters', async () => { + const alerting = new AlertingService(); + const mockConnector = jest.fn().mockReturnValueOnce({ success: true }); + alerting.registerConnector('mock', mockConnector); + savedObjectsClient.get.mockResolvedValueOnce({ + id: 'mock-action', + attributes: { + connectorId: 'mock', + connectorOptions: { + foo: true, + }, + connectorOptionsSecrets: { + bar: false, + }, + }, + }); + const result = await alerting.fireAction(savedObjectsClient, 'mock-action', { baz: false }); + expect(result).toEqual({ success: true }); + expect(mockConnector).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + Object { + "bar": false, + "foo": true, + }, + Object { + "baz": false, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Object { + "success": true, + }, + }, + ], +} +`); + expect(savedObjectsClient.get.mock.calls).toEqual([['action', 'mock-action']]); + }); + + test(`throws an error when the connector isn't registered`, async () => { + const alerting = new AlertingService(); + savedObjectsClient.get.mockResolvedValueOnce({ + id: 'mock-action', + attributes: { + connectorId: 'non-registered-connector', + connectorOptions: { + foo: true, + }, + connectorOptionsSecrets: { + bar: false, + }, + }, + }); + await expect( + alerting.fireAction(savedObjectsClient, 'mock-action', { baz: false }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Connector \\"non-registered-connector\\" is not registered"` + ); + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/alerting_service.ts b/x-pack/plugins/alerting/server/alerting_service.ts new file mode 100644 index 0000000000000..8416cccdac1de --- /dev/null +++ b/x-pack/plugins/alerting/server/alerting_service.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { consoleConnector } from './default_connectors'; +import { SavedObjectsClient } from '../../../../src/legacy/server/saved_objects'; + +type ConnectorExecutor = (connectorOptions: any, params: any) => Promise; + +interface Action { + id: string; + description: string; + connectorId: string; + connectorOptions: { [key: string]: any }; + connectorOptionsSecrets: { [key: string]: any }; +} + +export class AlertingService { + private connectors: { + [id: string]: ConnectorExecutor; + } = {}; + + constructor() { + // Register default connectors + this.registerConnector('console', consoleConnector); + } + + public registerConnector(connectorId: string, executor: ConnectorExecutor) { + if (this.hasConnector(connectorId)) { + throw Boom.badRequest(`Connector "${connectorId}" is already registered`); + } + this.connectors[connectorId] = executor; + } + + public hasConnector(connectorId: string) { + return !!this.connectors[connectorId]; + } + + public async createAction(savedObjectsClient: SavedObjectsClient, { id, ...data }: Action) { + const { connectorId } = data; + if (!this.hasConnector(connectorId)) { + throw Boom.badRequest(`Connector "${connectorId}" is not registered`); + } + return await savedObjectsClient.create('action', data, { id }); + } + + public async fireAction( + savedObjectsClient: SavedObjectsClient, + id: string, + params: { [key: string]: any } + ) { + const action = await savedObjectsClient.get('action', id); + const executor = this.connectors[action.attributes.connectorId]; + if (!executor) { + throw Boom.badRequest(`Connector "${action.attributes.connectorId}" is not registered`); + } + const connectorOptions = { + ...(action.attributes.connectorOptions || {}), + ...(action.attributes.connectorOptionsSecrets || {}), + }; + return await executor(connectorOptions, params); + } +} diff --git a/x-pack/plugins/alerting/server/default_connectors/console.ts b/x-pack/plugins/alerting/server/default_connectors/console.ts new file mode 100644 index 0000000000000..01561c5751c16 --- /dev/null +++ b/x-pack/plugins/alerting/server/default_connectors/console.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +interface ConsoleParams { + message: string; +} + +export async function consoleConnector(connectorOptions: any, { message }: ConsoleParams) { + // eslint-disable-next-line no-console + console.log(message); +} diff --git a/x-pack/plugins/alerting/server/default_connectors/index.ts b/x-pack/plugins/alerting/server/default_connectors/index.ts new file mode 100644 index 0000000000000..0593c060e9f8c --- /dev/null +++ b/x-pack/plugins/alerting/server/default_connectors/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { consoleConnector } from './console'; diff --git a/x-pack/plugins/alerting/server/index.ts b/x-pack/plugins/alerting/server/index.ts new file mode 100644 index 0000000000000..2c7b5320f2095 --- /dev/null +++ b/x-pack/plugins/alerting/server/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { AlertingService } from './alerting_service'; +export { createActionRoute } from './routes'; diff --git a/x-pack/plugins/alerting/server/routes/create_action.ts b/x-pack/plugins/alerting/server/routes/create_action.ts new file mode 100644 index 0000000000000..13684635fac94 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/create_action.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Joi from 'joi'; +import Hapi from 'hapi'; +import { AlertingService } from '../alerting_service'; + +interface Server extends Hapi.Server { + alerting: AlertingService; +} + +interface CreateActionRequest extends Hapi.Request { + payload: { + id: string; + description: string; + connectorId: string; + connectorOptions: { [key: string]: any }; + connectorOptionsSecrets: { [key: string]: any }; + }; + server: Server; +} + +export function createActionRoute(server: any) { + server.route({ + method: 'POST', + path: '/api/alerting/action', + options: { + validate: { + payload: Joi.object().keys({ + id: Joi.string().required(), + description: Joi.string().required(), + connectorId: Joi.string().required(), + connectorOptions: Joi.object(), + connectorOptionsSecrets: Joi.object(), + }), + }, + }, + async handler(request: CreateActionRequest) { + const savedObjectsClient = request.getSavedObjectsClient(); + return await request.server.alerting.createAction(savedObjectsClient, request.payload); + }, + }); +} diff --git a/x-pack/plugins/alerting/server/routes/index.ts b/x-pack/plugins/alerting/server/routes/index.ts new file mode 100644 index 0000000000000..8598dbdf962bf --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { createActionRoute } from './create_action'; diff --git a/x-pack/test/api_integration/apis/alerting/create_action.ts b/x-pack/test/api_integration/apis/alerting/create_action.ts new file mode 100644 index 0000000000000..983897c5b9ea4 --- /dev/null +++ b/x-pack/test/api_integration/apis/alerting/create_action.ts @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function createActionTests({ getService }: KibanaFunctionalTestDefaultProviders) { + const supertest = getService('supertest'); + + async function deleteObject(type: string, id: string) { + await supertest + .delete(`/api/saved_objects/${type}/${id}`) + .set('kbn-xsrf', 'foo') + .expect(200); + } + + describe('create_action', () => { + after(async () => { + await Promise.all([ + deleteObject('action', 'my-action'), + deleteObject('action', 'my-action-to-duplicate'), + ]); + }); + + it('should return 200 when creating an action', async () => { + await supertest + .post('/api/alerting/action') + .set('kbn-xsrf', 'foo') + .send({ + id: 'my-action', + description: 'My action', + connectorId: 'console', + connectorOptions: { + username: 'username', + }, + connectorOptionsSecrets: { + password: 'password', + }, + }) + .expect(200) + .then((resp: any) => { + expect(resp.body).to.eql({ + type: 'action', + id: 'my-action', + attributes: { + description: 'My action', + connectorId: 'console', + connectorOptions: { username: 'username' }, + connectorOptionsSecrets: { password: 'password' }, + }, + references: [], + updated_at: resp.body.updated_at, + version: resp.body.version, + }); + }); + }); + + it('should return 409 when action already exists', async () => { + await supertest + .post('/api/alerting/action') + .set('kbn-xsrf', 'foo') + .send({ + id: 'my-action-to-duplicate', + description: 'My action to duplicate', + connectorId: 'console', + connectorOptions: { + username: 'username', + }, + connectorOptionsSecrets: { + password: 'password', + }, + }) + .expect(200); + await supertest + .post('/api/alerting/action') + .set('kbn-xsrf', 'foo') + .send({ + id: 'my-action-to-duplicate', + description: 'My action to duplicate', + connectorId: 'console', + connectorOptions: { + username: 'username', + }, + connectorOptionsSecrets: { + password: 'password', + }, + }) + .expect(409); + }); + + it(`should return 400 when connector isn't registered`, async () => { + await supertest + .post('/api/alerting/action') + .set('kbn-xsrf', 'foo') + .send({ + id: 'my-action-without-connector', + description: 'My action', + connectorId: 'unregistered-connector', + connectorOptions: { + username: 'username', + }, + connectorOptionsSecrets: { + password: 'password', + }, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: 'Connector unregistered-connector not registered', + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/alerting/index.ts b/x-pack/test/api_integration/apis/alerting/index.ts new file mode 100644 index 0000000000000..9a17dbd9b8ba1 --- /dev/null +++ b/x-pack/test/api_integration/apis/alerting/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function alertingTests({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('Alerting', () => { + loadTestFile(require.resolve('./create_action')); + }); +} diff --git a/x-pack/test/api_integration/apis/index.js b/x-pack/test/api_integration/apis/index.js index 210402904c084..783d9dce3fc6b 100644 --- a/x-pack/test/api_integration/apis/index.js +++ b/x-pack/test/api_integration/apis/index.js @@ -8,6 +8,7 @@ export default function ({ loadTestFile }) { describe('apis', function () { this.tags('ciGroup6'); + loadTestFile(require.resolve('./alerting')); loadTestFile(require.resolve('./es')); loadTestFile(require.resolve('./security')); loadTestFile(require.resolve('./monitoring')); From bc0140bc7f80ce9f8ec85adb20d95eeaf8cdc285 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Fri, 26 Apr 2019 16:38:02 -0400 Subject: [PATCH 02/51] Remove relative imports --- x-pack/plugins/alerting/server/alerting_service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/alerting/server/alerting_service.ts b/x-pack/plugins/alerting/server/alerting_service.ts index 8416cccdac1de..67023f0a163e2 100644 --- a/x-pack/plugins/alerting/server/alerting_service.ts +++ b/x-pack/plugins/alerting/server/alerting_service.ts @@ -5,8 +5,8 @@ */ import Boom from 'boom'; +import { SavedObjectsClient } from 'src/legacy/server/saved_objects'; import { consoleConnector } from './default_connectors'; -import { SavedObjectsClient } from '../../../../src/legacy/server/saved_objects'; type ConnectorExecutor = (connectorOptions: any, params: any) => Promise; From 682743eb7ebde6737dc551f8db1e890165cfec58 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Mon, 29 Apr 2019 09:26:48 -0400 Subject: [PATCH 03/51] Code cleanup --- x-pack/plugins/alerting/common/constants.ts | 7 +++++++ x-pack/plugins/alerting/index.ts | 4 +++- .../alerting/server/alerting_service.test.ts | 8 +++++-- .../alerting/server/alerting_service.ts | 21 +++++++++++-------- .../alerting/server/routes/create_action.ts | 5 +++-- 5 files changed, 31 insertions(+), 14 deletions(-) create mode 100644 x-pack/plugins/alerting/common/constants.ts diff --git a/x-pack/plugins/alerting/common/constants.ts b/x-pack/plugins/alerting/common/constants.ts new file mode 100644 index 0000000000000..d8bf357e8781b --- /dev/null +++ b/x-pack/plugins/alerting/common/constants.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const APP_ID = 'alerting'; diff --git a/x-pack/plugins/alerting/index.ts b/x-pack/plugins/alerting/index.ts index 7a832164461ca..28ee545f5cfb7 100644 --- a/x-pack/plugins/alerting/index.ts +++ b/x-pack/plugins/alerting/index.ts @@ -8,9 +8,11 @@ import Hapi from 'hapi'; import mappings from './mappings.json'; import { AlertingService, createActionRoute } from './server'; +import { APP_ID } from './common/constants'; + export function alerting(kibana: any) { return new kibana.Plugin({ - id: 'alerting', + id: APP_ID, require: ['kibana', 'elasticsearch'], init(server: Hapi.Server) { const alertingService = new AlertingService(); diff --git a/x-pack/plugins/alerting/server/alerting_service.test.ts b/x-pack/plugins/alerting/server/alerting_service.test.ts index f1f05f00d55ea..761bece57eea3 100644 --- a/x-pack/plugins/alerting/server/alerting_service.test.ts +++ b/x-pack/plugins/alerting/server/alerting_service.test.ts @@ -133,7 +133,11 @@ describe('#AlertingService', () => { }, }, }); - const result = await alerting.fireAction(savedObjectsClient, 'mock-action', { baz: false }); + const result = await alerting.fireAction({ + id: 'mock-action', + params: { baz: false }, + savedObjectsClient, + }); expect(result).toEqual({ success: true }); expect(mockConnector).toMatchInlineSnapshot(` [MockFunction] { @@ -176,7 +180,7 @@ describe('#AlertingService', () => { }, }); await expect( - alerting.fireAction(savedObjectsClient, 'mock-action', { baz: false }) + alerting.fireAction({ savedObjectsClient, id: 'mock-action', params: { baz: false } }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Connector \\"non-registered-connector\\" is not registered"` ); diff --git a/x-pack/plugins/alerting/server/alerting_service.ts b/x-pack/plugins/alerting/server/alerting_service.ts index 67023f0a163e2..ce37746dbd578 100644 --- a/x-pack/plugins/alerting/server/alerting_service.ts +++ b/x-pack/plugins/alerting/server/alerting_service.ts @@ -18,6 +18,12 @@ interface Action { connectorOptionsSecrets: { [key: string]: any }; } +interface FireActionOptions { + id: string; + params: { [key: string]: any }; + savedObjectsClient: SavedObjectsClient; +} + export class AlertingService { private connectors: { [id: string]: ConnectorExecutor; @@ -47,20 +53,17 @@ export class AlertingService { return await savedObjectsClient.create('action', data, { id }); } - public async fireAction( - savedObjectsClient: SavedObjectsClient, - id: string, - params: { [key: string]: any } - ) { + public async fireAction({ id, params, savedObjectsClient }: FireActionOptions) { const action = await savedObjectsClient.get('action', id); const executor = this.connectors[action.attributes.connectorId]; if (!executor) { throw Boom.badRequest(`Connector "${action.attributes.connectorId}" is not registered`); } - const connectorOptions = { - ...(action.attributes.connectorOptions || {}), - ...(action.attributes.connectorOptionsSecrets || {}), - }; + const connectorOptions = Object.assign( + {}, + action.attributes.connectorOptions, + action.attributes.connectorOptionsSecrets + ); return await executor(connectorOptions, params); } } diff --git a/x-pack/plugins/alerting/server/routes/create_action.ts b/x-pack/plugins/alerting/server/routes/create_action.ts index 13684635fac94..56e82316a809c 100644 --- a/x-pack/plugins/alerting/server/routes/create_action.ts +++ b/x-pack/plugins/alerting/server/routes/create_action.ts @@ -7,12 +7,14 @@ import Joi from 'joi'; import Hapi from 'hapi'; import { AlertingService } from '../alerting_service'; +import { APP_ID } from '../../common/constants'; interface Server extends Hapi.Server { alerting: AlertingService; } interface CreateActionRequest extends Hapi.Request { + server: Server; payload: { id: string; description: string; @@ -20,13 +22,12 @@ interface CreateActionRequest extends Hapi.Request { connectorOptions: { [key: string]: any }; connectorOptionsSecrets: { [key: string]: any }; }; - server: Server; } export function createActionRoute(server: any) { server.route({ method: 'POST', - path: '/api/alerting/action', + path: `/api/${APP_ID}/action`, options: { validate: { payload: Joi.object().keys({ From 0311afffad055f884c56f759e36c86b67a920f01 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Tue, 30 Apr 2019 09:25:23 -0400 Subject: [PATCH 04/51] Split service into 3 parts, change connector structure --- x-pack/plugins/alerting/index.ts | 12 +- .../alerting/server/action_service.test.ts | 149 ++++++++++++++ .../plugins/alerting/server/action_service.ts | 53 +++++ .../plugins/alerting/server/alert_service.ts | 7 + .../alerting/server/alerting_service.test.ts | 189 ------------------ .../alerting/server/alerting_service.ts | 69 ------- .../alerting/server/connector_service.test.ts | 48 +++++ .../alerting/server/connector_service.ts | 40 ++++ .../server/default_connectors/console.ts | 11 +- x-pack/plugins/alerting/server/index.ts | 4 +- .../alerting/server/routes/create_action.ts | 12 +- .../apis/alerting/create_action.ts | 2 +- 12 files changed, 326 insertions(+), 270 deletions(-) create mode 100644 x-pack/plugins/alerting/server/action_service.test.ts create mode 100644 x-pack/plugins/alerting/server/action_service.ts create mode 100644 x-pack/plugins/alerting/server/alert_service.ts delete mode 100644 x-pack/plugins/alerting/server/alerting_service.test.ts delete mode 100644 x-pack/plugins/alerting/server/alerting_service.ts create mode 100644 x-pack/plugins/alerting/server/connector_service.test.ts create mode 100644 x-pack/plugins/alerting/server/connector_service.ts diff --git a/x-pack/plugins/alerting/index.ts b/x-pack/plugins/alerting/index.ts index 28ee545f5cfb7..28c4178fea355 100644 --- a/x-pack/plugins/alerting/index.ts +++ b/x-pack/plugins/alerting/index.ts @@ -6,7 +6,7 @@ import Hapi from 'hapi'; import mappings from './mappings.json'; -import { AlertingService, createActionRoute } from './server'; +import { createActionRoute, AlertService, ActionService, ConnectorService } from './server'; import { APP_ID } from './common/constants'; @@ -15,11 +15,17 @@ export function alerting(kibana: any) { id: APP_ID, require: ['kibana', 'elasticsearch'], init(server: Hapi.Server) { - const alertingService = new AlertingService(); + const connectorService = new ConnectorService(); + const actionService = new ActionService(connectorService); + const alertService = new AlertService(); // Routes createActionRoute(server); // Register service to server - server.decorate('server', 'alerting', () => alertingService); + server.decorate('server', 'alerting', () => ({ + alerts: alertService, + actions: actionService, + connectors: connectorService, + })); }, uiExports: { mappings, diff --git a/x-pack/plugins/alerting/server/action_service.test.ts b/x-pack/plugins/alerting/server/action_service.test.ts new file mode 100644 index 0000000000000..fa5451ea3568f --- /dev/null +++ b/x-pack/plugins/alerting/server/action_service.test.ts @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ConnectorService } from './connector_service'; +import { ActionService } from './action_service'; + +const savedObjectsClient = { + errors: {} as any, + bulkCreate: jest.fn(), + bulkGet: jest.fn(), + create: jest.fn(), + delete: jest.fn(), + find: jest.fn(), + get: jest.fn(), + update: jest.fn(), +}; + +beforeEach(() => jest.resetAllMocks()); + +describe('create()', () => { + test('creates an action with all given properties', async () => { + const expectedResult = Symbol(); + const connectorService = new ConnectorService(); + const actionService = new ActionService(connectorService); + savedObjectsClient.create.mockResolvedValueOnce(expectedResult); + const result = await actionService.create(savedObjectsClient, { + id: 'my-alert', + description: 'my description', + connectorId: 'console', + connectorOptions: {}, + connectorOptionsSecrets: {}, + }); + expect(result).toEqual(expectedResult); + expect(savedObjectsClient.create).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + "action", + Object { + "connectorId": "console", + "connectorOptions": Object {}, + "connectorOptionsSecrets": Object {}, + "description": "my description", + }, + Object { + "id": "my-alert", + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], +} +`); + }); + + test(`throws an error when connector doesn't exist`, async () => { + const connectorService = new ConnectorService(); + const actionService = new ActionService(connectorService); + await expect( + actionService.create(savedObjectsClient, { + id: 'my-alert', + description: 'my description', + connectorId: 'unregistered-connector', + connectorOptions: {}, + connectorOptionsSecrets: {}, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Connector \\"unregistered-connector\\" is not registered."` + ); + }); +}); + +describe('fire()', () => { + test('fires an action with all given parameters', async () => { + const connectorService = new ConnectorService(); + const actionService = new ActionService(connectorService); + const mockConnector = jest.fn().mockResolvedValueOnce({ success: true }); + connectorService.register({ id: 'mock', executor: mockConnector }); + savedObjectsClient.get.mockResolvedValueOnce({ + id: 'mock-action', + attributes: { + connectorId: 'mock', + connectorOptions: { + foo: true, + }, + connectorOptionsSecrets: { + bar: false, + }, + }, + }); + const result = await actionService.fire({ + id: 'mock-action', + params: { baz: false }, + savedObjectsClient, + }); + expect(result).toEqual({ success: true }); + expect(mockConnector).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + Object { + "bar": false, + "foo": true, + }, + Object { + "baz": false, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], +} +`); + expect(savedObjectsClient.get.mock.calls).toEqual([['action', 'mock-action']]); + }); + + test(`throws an error when the connector isn't registered`, async () => { + const connectorService = new ConnectorService(); + const actionService = new ActionService(connectorService); + savedObjectsClient.get.mockResolvedValueOnce({ + id: 'mock-action', + attributes: { + connectorId: 'non-registered-connector', + connectorOptions: { + foo: true, + }, + connectorOptionsSecrets: { + bar: false, + }, + }, + }); + await expect( + actionService.fire({ savedObjectsClient, id: 'mock-action', params: { baz: false } }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Connector \\"non-registered-connector\\" is not registered."` + ); + }); +}); diff --git a/x-pack/plugins/alerting/server/action_service.ts b/x-pack/plugins/alerting/server/action_service.ts new file mode 100644 index 0000000000000..34da25273101a --- /dev/null +++ b/x-pack/plugins/alerting/server/action_service.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { SavedObjectsClient } from 'src/legacy/server/saved_objects'; +import { ConnectorService } from './connector_service'; + +interface Action { + id: string; + description: string; + connectorId: string; + connectorOptions: { [key: string]: any }; + connectorOptionsSecrets: { [key: string]: any }; +} + +interface FireActionOptions { + id: string; + params: { [key: string]: any }; + savedObjectsClient: SavedObjectsClient; +} + +export class ActionService { + private connectorService: ConnectorService; + + constructor(connectorService: ConnectorService) { + this.connectorService = connectorService; + } + + public async create(savedObjectsClient: SavedObjectsClient, { id, ...data }: Action) { + const { connectorId } = data; + if (!this.connectorService.has(connectorId)) { + throw Boom.badRequest(`Connector "${connectorId}" is not registered.`); + } + return await savedObjectsClient.create('action', data, { id }); + } + + public async fire({ id, params, savedObjectsClient }: FireActionOptions) { + const action = await savedObjectsClient.get('action', id); + const connectorOptions = Object.assign( + {}, + action.attributes.connectorOptions, + action.attributes.connectorOptionsSecrets + ); + return await this.connectorService.execute( + action.attributes.connectorId, + connectorOptions, + params + ); + } +} diff --git a/x-pack/plugins/alerting/server/alert_service.ts b/x-pack/plugins/alerting/server/alert_service.ts new file mode 100644 index 0000000000000..f6b2e509480d2 --- /dev/null +++ b/x-pack/plugins/alerting/server/alert_service.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export class AlertService {} diff --git a/x-pack/plugins/alerting/server/alerting_service.test.ts b/x-pack/plugins/alerting/server/alerting_service.test.ts deleted file mode 100644 index 761bece57eea3..0000000000000 --- a/x-pack/plugins/alerting/server/alerting_service.test.ts +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { AlertingService } from './alerting_service'; - -describe('#AlertingService', () => { - const savedObjectsClient = { - errors: {} as any, - bulkCreate: jest.fn(), - bulkGet: jest.fn(), - create: jest.fn(), - delete: jest.fn(), - find: jest.fn(), - get: jest.fn(), - update: jest.fn(), - }; - - beforeEach(() => jest.resetAllMocks()); - - test('automatically registers default connectors', () => { - const alerting = new AlertingService(); - expect(alerting.hasConnector('console')).toEqual(true); - }); - - describe('registerConnector()', () => { - test('able to register connectors', () => { - const executor = jest.fn(); - const alerting = new AlertingService(); - alerting.registerConnector('my-connector', executor); - }); - - test('throws error if connector already registered', () => { - const executor = jest.fn(); - const alerting = new AlertingService(); - alerting.registerConnector('my-connector', executor); - expect(() => - alerting.registerConnector('my-connector', executor) - ).toThrowErrorMatchingInlineSnapshot(`"Connector \\"my-connector\\" is already registered"`); - }); - }); - - describe('hasConnector()', () => { - test('returns true for default connectors', () => { - const alerting = new AlertingService(); - expect(alerting.hasConnector('console')).toEqual(true); - }); - - test('returns true after registering a connector', () => { - const executor = jest.fn(); - const alerting = new AlertingService(); - alerting.registerConnector('my-connector', executor); - expect(alerting.hasConnector('my-connector')).toEqual(true); - }); - - test('returns false for unregistered connectors', () => { - const alerting = new AlertingService(); - expect(alerting.hasConnector('my-connector')).toEqual(false); - }); - }); - - describe('createAction()', () => { - test('creates an action with all given properties', async () => { - const expectedResult = Symbol(); - const alerting = new AlertingService(); - savedObjectsClient.create.mockResolvedValueOnce(expectedResult); - const result = await alerting.createAction(savedObjectsClient, { - id: 'my-alert', - description: 'my description', - connectorId: 'console', - connectorOptions: {}, - connectorOptionsSecrets: {}, - }); - expect(result).toEqual(expectedResult); - expect(savedObjectsClient.create).toMatchInlineSnapshot(` -[MockFunction] { - "calls": Array [ - Array [ - "action", - Object { - "connectorId": "console", - "connectorOptions": Object {}, - "connectorOptionsSecrets": Object {}, - "description": "my description", - }, - Object { - "id": "my-alert", - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], -} -`); - }); - - test(`throws an error when connector doesn't exist`, async () => { - const alerting = new AlertingService(); - await expect( - alerting.createAction(savedObjectsClient, { - id: 'my-alert', - description: 'my description', - connectorId: 'unregistered-connector', - connectorOptions: {}, - connectorOptionsSecrets: {}, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Connector \\"unregistered-connector\\" is not registered"` - ); - }); - }); - - describe('fireAction()', () => { - test('fires an action with all given parameters', async () => { - const alerting = new AlertingService(); - const mockConnector = jest.fn().mockReturnValueOnce({ success: true }); - alerting.registerConnector('mock', mockConnector); - savedObjectsClient.get.mockResolvedValueOnce({ - id: 'mock-action', - attributes: { - connectorId: 'mock', - connectorOptions: { - foo: true, - }, - connectorOptionsSecrets: { - bar: false, - }, - }, - }); - const result = await alerting.fireAction({ - id: 'mock-action', - params: { baz: false }, - savedObjectsClient, - }); - expect(result).toEqual({ success: true }); - expect(mockConnector).toMatchInlineSnapshot(` -[MockFunction] { - "calls": Array [ - Array [ - Object { - "bar": false, - "foo": true, - }, - Object { - "baz": false, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Object { - "success": true, - }, - }, - ], -} -`); - expect(savedObjectsClient.get.mock.calls).toEqual([['action', 'mock-action']]); - }); - - test(`throws an error when the connector isn't registered`, async () => { - const alerting = new AlertingService(); - savedObjectsClient.get.mockResolvedValueOnce({ - id: 'mock-action', - attributes: { - connectorId: 'non-registered-connector', - connectorOptions: { - foo: true, - }, - connectorOptionsSecrets: { - bar: false, - }, - }, - }); - await expect( - alerting.fireAction({ savedObjectsClient, id: 'mock-action', params: { baz: false } }) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Connector \\"non-registered-connector\\" is not registered"` - ); - }); - }); -}); diff --git a/x-pack/plugins/alerting/server/alerting_service.ts b/x-pack/plugins/alerting/server/alerting_service.ts deleted file mode 100644 index ce37746dbd578..0000000000000 --- a/x-pack/plugins/alerting/server/alerting_service.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; -import { SavedObjectsClient } from 'src/legacy/server/saved_objects'; -import { consoleConnector } from './default_connectors'; - -type ConnectorExecutor = (connectorOptions: any, params: any) => Promise; - -interface Action { - id: string; - description: string; - connectorId: string; - connectorOptions: { [key: string]: any }; - connectorOptionsSecrets: { [key: string]: any }; -} - -interface FireActionOptions { - id: string; - params: { [key: string]: any }; - savedObjectsClient: SavedObjectsClient; -} - -export class AlertingService { - private connectors: { - [id: string]: ConnectorExecutor; - } = {}; - - constructor() { - // Register default connectors - this.registerConnector('console', consoleConnector); - } - - public registerConnector(connectorId: string, executor: ConnectorExecutor) { - if (this.hasConnector(connectorId)) { - throw Boom.badRequest(`Connector "${connectorId}" is already registered`); - } - this.connectors[connectorId] = executor; - } - - public hasConnector(connectorId: string) { - return !!this.connectors[connectorId]; - } - - public async createAction(savedObjectsClient: SavedObjectsClient, { id, ...data }: Action) { - const { connectorId } = data; - if (!this.hasConnector(connectorId)) { - throw Boom.badRequest(`Connector "${connectorId}" is not registered`); - } - return await savedObjectsClient.create('action', data, { id }); - } - - public async fireAction({ id, params, savedObjectsClient }: FireActionOptions) { - const action = await savedObjectsClient.get('action', id); - const executor = this.connectors[action.attributes.connectorId]; - if (!executor) { - throw Boom.badRequest(`Connector "${action.attributes.connectorId}" is not registered`); - } - const connectorOptions = Object.assign( - {}, - action.attributes.connectorOptions, - action.attributes.connectorOptionsSecrets - ); - return await executor(connectorOptions, params); - } -} diff --git a/x-pack/plugins/alerting/server/connector_service.test.ts b/x-pack/plugins/alerting/server/connector_service.test.ts new file mode 100644 index 0000000000000..08bdc8dad4894 --- /dev/null +++ b/x-pack/plugins/alerting/server/connector_service.test.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ConnectorService } from './connector_service'; + +test('automatically registers default connectors', () => { + const connectorService = new ConnectorService(); + expect(connectorService.has('console')).toEqual(true); +}); + +describe('register()', () => { + test('able to register connectors', () => { + const executor = jest.fn(); + const connectorService = new ConnectorService(); + connectorService.register({ id: 'my-connector', executor }); + }); + + test('throws error if connector already registered', () => { + const executor = jest.fn(); + const connectorService = new ConnectorService(); + connectorService.register({ id: 'my-connector', executor }); + expect(() => + connectorService.register({ id: 'my-connector', executor }) + ).toThrowErrorMatchingInlineSnapshot(`"Connector \\"my-connector\\" is already registered."`); + }); +}); + +describe('has()', () => { + test('returns false for unregistered connectors', () => { + const connectorService = new ConnectorService(); + expect(connectorService.has('my-connector')).toEqual(false); + }); + + test('returns true for default connectors', () => { + const connectorService = new ConnectorService(); + expect(connectorService.has('console')).toEqual(true); + }); + + test('returns true after registering a connector', () => { + const executor = jest.fn(); + const connectorService = new ConnectorService(); + connectorService.register({ id: 'my-connector', executor }); + expect(connectorService.has('my-connector')); + }); +}); diff --git a/x-pack/plugins/alerting/server/connector_service.ts b/x-pack/plugins/alerting/server/connector_service.ts new file mode 100644 index 0000000000000..f03bbbe278be0 --- /dev/null +++ b/x-pack/plugins/alerting/server/connector_service.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { consoleConnector } from './default_connectors'; + +interface Connector { + id: string; + executor(connectorOptions: any, params: any): Promise; +} + +export class ConnectorService { + private connectors: { [id: string]: Connector } = {}; + + constructor() { + this.register(consoleConnector); + } + + public has(connectorId: string) { + return !!this.connectors[connectorId]; + } + + public register(connector: Connector) { + if (this.has(connector.id)) { + throw Boom.badRequest(`Connector "${connector.id}" is already registered.`); + } + this.connectors[connector.id] = connector; + } + + public async execute(connectorId: string, connectorOptions: any, params: any) { + const connector = this.connectors[connectorId]; + if (!connector) { + throw Boom.badRequest(`Connector "${connectorId}" is not registered.`); + } + return await connector.executor(connectorOptions, params); + } +} diff --git a/x-pack/plugins/alerting/server/default_connectors/console.ts b/x-pack/plugins/alerting/server/default_connectors/console.ts index 01561c5751c16..a62b9dc9f8fc9 100644 --- a/x-pack/plugins/alerting/server/default_connectors/console.ts +++ b/x-pack/plugins/alerting/server/default_connectors/console.ts @@ -8,7 +8,10 @@ interface ConsoleParams { message: string; } -export async function consoleConnector(connectorOptions: any, { message }: ConsoleParams) { - // eslint-disable-next-line no-console - console.log(message); -} +export const consoleConnector = { + id: 'console', + async executor(connectorOptions: any, { message }: ConsoleParams) { + // eslint-disable-next-line no-console + console.log(message); + }, +}; diff --git a/x-pack/plugins/alerting/server/index.ts b/x-pack/plugins/alerting/server/index.ts index 2c7b5320f2095..0151f998ed34c 100644 --- a/x-pack/plugins/alerting/server/index.ts +++ b/x-pack/plugins/alerting/server/index.ts @@ -4,5 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export { AlertingService } from './alerting_service'; export { createActionRoute } from './routes'; +export { ActionService } from './action_service'; +export { AlertService } from './alert_service'; +export { ConnectorService } from './connector_service'; diff --git a/x-pack/plugins/alerting/server/routes/create_action.ts b/x-pack/plugins/alerting/server/routes/create_action.ts index 56e82316a809c..8a298f840381e 100644 --- a/x-pack/plugins/alerting/server/routes/create_action.ts +++ b/x-pack/plugins/alerting/server/routes/create_action.ts @@ -6,11 +6,17 @@ import Joi from 'joi'; import Hapi from 'hapi'; -import { AlertingService } from '../alerting_service'; +import { ActionService } from '../action_service'; +import { AlertService } from '../alert_service'; +import { ConnectorService } from '../connector_service'; import { APP_ID } from '../../common/constants'; interface Server extends Hapi.Server { - alerting: AlertingService; + alerting: () => { + actions: ActionService; + alerts: AlertService; + connectors: ConnectorService; + }; } interface CreateActionRequest extends Hapi.Request { @@ -41,7 +47,7 @@ export function createActionRoute(server: any) { }, async handler(request: CreateActionRequest) { const savedObjectsClient = request.getSavedObjectsClient(); - return await request.server.alerting.createAction(savedObjectsClient, request.payload); + return await request.server.alerting().actions.create(savedObjectsClient, request.payload); }, }); } diff --git a/x-pack/test/api_integration/apis/alerting/create_action.ts b/x-pack/test/api_integration/apis/alerting/create_action.ts index 983897c5b9ea4..bbe7505f7e330 100644 --- a/x-pack/test/api_integration/apis/alerting/create_action.ts +++ b/x-pack/test/api_integration/apis/alerting/create_action.ts @@ -112,7 +112,7 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe expect(resp.body).to.eql({ statusCode: 400, error: 'Bad Request', - message: 'Connector unregistered-connector not registered', + message: 'Connector "unregistered-connector" is not registered.', }); }); }); From d2d9250b7bd94a43298b93cc071a94d62ae38854 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Tue, 30 Apr 2019 09:45:44 -0400 Subject: [PATCH 05/51] Ability to disable plugin, ability to get actions --- x-pack/plugins/alerting/index.ts | 15 +++++++++++ .../alerting/server/action_service.test.ts | 27 +++++++++++++++++++ .../plugins/alerting/server/action_service.ts | 6 ++++- 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/alerting/index.ts b/x-pack/plugins/alerting/index.ts index 28c4178fea355..3109ada84fa27 100644 --- a/x-pack/plugins/alerting/index.ts +++ b/x-pack/plugins/alerting/index.ts @@ -13,13 +13,28 @@ import { APP_ID } from './common/constants'; export function alerting(kibana: any) { return new kibana.Plugin({ id: APP_ID, + configPrefix: 'xpack.alerting', require: ['kibana', 'elasticsearch'], + config(Joi: any) { + return Joi.object({ + enabled: Joi.boolean().default(true), + }); + }, init(server: Hapi.Server) { + const alertingEnabled = server.config().get('xpack.alerting.enabled'); + + if (!alertingEnabled) { + server.log(['info', 'alerting'], 'Alerting app disabled by configuration'); + return; + } + const connectorService = new ConnectorService(); const actionService = new ActionService(connectorService); const alertService = new AlertService(); + // Routes createActionRoute(server); + // Register service to server server.decorate('server', 'alerting', () => ({ alerts: alertService, diff --git a/x-pack/plugins/alerting/server/action_service.test.ts b/x-pack/plugins/alerting/server/action_service.test.ts index fa5451ea3568f..8bb3ebf74346b 100644 --- a/x-pack/plugins/alerting/server/action_service.test.ts +++ b/x-pack/plugins/alerting/server/action_service.test.ts @@ -77,6 +77,33 @@ describe('create()', () => { }); }); +describe('get()', () => { + test('calls savedObjectsClient with id', async () => { + const expectedResult = Symbol(); + const connectorService = new ConnectorService(); + const actionService = new ActionService(connectorService); + savedObjectsClient.get.mockResolvedValueOnce(expectedResult); + const result = await actionService.get(savedObjectsClient, '1'); + expect(result).toEqual(expectedResult); + expect(savedObjectsClient.get).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + "action", + "1", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], +} +`); + }); +}); + describe('fire()', () => { test('fires an action with all given parameters', async () => { const connectorService = new ConnectorService(); diff --git a/x-pack/plugins/alerting/server/action_service.ts b/x-pack/plugins/alerting/server/action_service.ts index 34da25273101a..705435befe18f 100644 --- a/x-pack/plugins/alerting/server/action_service.ts +++ b/x-pack/plugins/alerting/server/action_service.ts @@ -37,8 +37,12 @@ export class ActionService { return await savedObjectsClient.create('action', data, { id }); } + public async get(savedObjectsClient: SavedObjectsClient, id: string) { + return await savedObjectsClient.get('action', id); + } + public async fire({ id, params, savedObjectsClient }: FireActionOptions) { - const action = await savedObjectsClient.get('action', id); + const action = await this.get(savedObjectsClient, id); const connectorOptions = Object.assign( {}, action.attributes.connectorOptions, From 06669abcf093a7f4698d57589031f74a844c299f Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Tue, 30 Apr 2019 10:00:08 -0400 Subject: [PATCH 06/51] Add slack connector --- x-pack/package.json | 7 ++-- .../alerting/server/connector_service.test.ts | 1 + .../alerting/server/connector_service.ts | 3 +- .../server/default_connectors/index.ts | 1 + .../server/default_connectors/slack.ts | 40 +++++++++++++++++++ yarn.lock | 24 ++++++++++- 6 files changed, 71 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugins/alerting/server/default_connectors/slack.ts diff --git a/x-pack/package.json b/x-pack/package.json index 3dd4ea2d6e580..47debbd3cf8e0 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -42,8 +42,8 @@ "@storybook/react": "^5.0.5", "@storybook/theming": "^5.0.5", "@types/angular": "1.6.50", - "@types/boom": "^7.2.0", "@types/base64-js": "^1.2.5", + "@types/boom": "^7.2.0", "@types/cheerio": "^0.22.10", "@types/chroma-js": "^1.4.1", "@types/color": "^3.0.0", @@ -53,9 +53,9 @@ "@types/d3-time": "^1.0.7", "@types/d3-time-format": "^2.1.0", "@types/elasticsearch": "^5.0.30", + "@types/file-saver": "^2.0.0", "@types/git-url-parse": "^9.0.0", "@types/glob": "^7.1.1", - "@types/file-saver": "^2.0.0", "@types/graphql": "^0.13.1", "@types/hapi-auth-cookie": "^9.1.0", "@types/history": "^4.6.2", @@ -65,8 +65,8 @@ "@types/json-stable-stringify": "^1.0.32", "@types/jsonwebtoken": "^7.2.7", "@types/lodash": "^3.10.1", - "@types/mkdirp": "^0.5.2", "@types/mime": "^2.0.1", + "@types/mkdirp": "^0.5.2", "@types/mocha": "^5.2.6", "@types/nock": "^9.3.0", "@types/node": "^10.12.27", @@ -314,6 +314,7 @@ "rison-node": "0.3.1", "rxjs": "^6.2.1", "semver": "5.1.0", + "slack": "^11.0.2", "squel": "^5.12.2", "stats-lite": "^2.2.0", "style-it": "2.1.2", diff --git a/x-pack/plugins/alerting/server/connector_service.test.ts b/x-pack/plugins/alerting/server/connector_service.test.ts index 08bdc8dad4894..524a8ea1ffaac 100644 --- a/x-pack/plugins/alerting/server/connector_service.test.ts +++ b/x-pack/plugins/alerting/server/connector_service.test.ts @@ -37,6 +37,7 @@ describe('has()', () => { test('returns true for default connectors', () => { const connectorService = new ConnectorService(); expect(connectorService.has('console')).toEqual(true); + expect(connectorService.has('slack')).toEqual(true); }); test('returns true after registering a connector', () => { diff --git a/x-pack/plugins/alerting/server/connector_service.ts b/x-pack/plugins/alerting/server/connector_service.ts index f03bbbe278be0..2913f09a556fe 100644 --- a/x-pack/plugins/alerting/server/connector_service.ts +++ b/x-pack/plugins/alerting/server/connector_service.ts @@ -5,7 +5,7 @@ */ import Boom from 'boom'; -import { consoleConnector } from './default_connectors'; +import { consoleConnector, slackConnector } from './default_connectors'; interface Connector { id: string; @@ -17,6 +17,7 @@ export class ConnectorService { constructor() { this.register(consoleConnector); + this.register(slackConnector); } public has(connectorId: string) { diff --git a/x-pack/plugins/alerting/server/default_connectors/index.ts b/x-pack/plugins/alerting/server/default_connectors/index.ts index 0593c060e9f8c..4cb9207560a61 100644 --- a/x-pack/plugins/alerting/server/default_connectors/index.ts +++ b/x-pack/plugins/alerting/server/default_connectors/index.ts @@ -5,3 +5,4 @@ */ export { consoleConnector } from './console'; +export { slackConnector } from './slack'; diff --git a/x-pack/plugins/alerting/server/default_connectors/slack.ts b/x-pack/plugins/alerting/server/default_connectors/slack.ts new file mode 100644 index 0000000000000..099cf3466c5c8 --- /dev/null +++ b/x-pack/plugins/alerting/server/default_connectors/slack.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import slack from 'slack'; + +interface SlackConnectorOptions { + token: string; +} + +interface DefaulParams { + command: string; +} + +interface PostMessageParams extends DefaulParams { + command: 'post-message'; + message: string; + channel: string; +} + +type SlackParams = PostMessageParams; + +export const slackConnector = { + id: 'slack', + async executor(connectorOptions: SlackConnectorOptions, params: SlackParams) { + switch (params.command) { + case 'post-message': + await slack.chat.postMessage({ + token: connectorOptions.token, + text: params.message, + channel: params.channel, + }); + break; + default: + throw new Error(`Unsupported command "${params.command}".`); + } + }, +}; diff --git a/yarn.lock b/yarn.lock index 53a8114a3dd1a..f6fc622dc56ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23333,6 +23333,13 @@ sisteransi@^1.0.0: resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.0.tgz#77d9622ff909080f1c19e5f4a1df0c1b0a27b88c" integrity sha512-N+z4pHB4AmUv0SjveWRd6q1Nj5w62m5jodv+GD8lvmbY/83T/rpbJGZOnK5T149OldDj4Db07BSv9xY4K6NTPQ== +slack@^11.0.2: + version "11.0.2" + resolved "https://registry.yarnpkg.com/slack/-/slack-11.0.2.tgz#30f68527c5d1712b7faa3141db7716f89ac6e911" + integrity sha512-rv842+S+AGyZCmMMd8xPtW5DvJ9LzWTAKfxi8Gw57oYlXgcKtFuHd4nqk6lTPpRKdUGn3tx/Drd0rjQR3dQPqw== + dependencies: + tiny-json-http "^7.0.2" + slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" @@ -24873,6 +24880,11 @@ tiny-invariant@^1.0.3, tiny-invariant@^1.0.4: resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.4.tgz#346b5415fd93cb696b0c4e8a96697ff590f92463" integrity sha512-lMhRd/djQJ3MoaHEBrw8e2/uM4rs9YMNk0iOr8rHQ0QdbM7D4l0gFl3szKdeixrlyfm9Zqi4dxHCM2qVG8ND5g== +tiny-json-http@^7.0.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/tiny-json-http/-/tiny-json-http-7.1.2.tgz#620e189849bab08992ec23fada7b48c7c61637b4" + integrity sha512-XB9Bu+ohdQso6ziPFNVqK+pcTt0l8BSRkW/CCBq0pUVlLxcYDsorpo7ae5yPhu2CF1xYgJuKVLF7cfOGeLCTlA== + tiny-lr@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/tiny-lr/-/tiny-lr-1.1.1.tgz#9fa547412f238fedb068ee295af8b682c98b2aab" @@ -25729,11 +25741,21 @@ typescript-fsa@^2.0.0, typescript-fsa@^2.5.0: resolved "https://registry.yarnpkg.com/typescript-fsa/-/typescript-fsa-2.5.0.tgz#1baec01b5e8f5f34c322679d1327016e9e294faf" integrity sha1-G67AG16PXzTDImedEycBbp4pT68= -typescript@^3.3.3333, typescript@~3.0.3, typescript@~3.3.3333, typescript@~3.4.3: +typescript@^3.3.3333, typescript@~3.3.3333: version "3.3.3333" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.3.3333.tgz#171b2c5af66c59e9431199117a3bcadc66fdcfd6" integrity sha512-JjSKsAfuHBE/fB2oZ8NxtRTk5iGcg6hkYXMnZ3Wc+b2RSqejEqTaem11mHASMnFilHrax3sLK0GDzcJrekZYLw== +typescript@~3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.0.3.tgz#4853b3e275ecdaa27f78fda46dc273a7eb7fc1c8" + integrity sha512-kk80vLW9iGtjMnIv11qyxLqZm20UklzuR2tL0QAnDIygIUIemcZMxlMWudl9OOt76H3ntVzcTiddQ1/pAAJMYg== + +typescript@~3.4.3: + version "3.4.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.5.tgz#2d2618d10bb566572b8d7aad5180d84257d70a99" + integrity sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw== + typings-tester@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/typings-tester/-/typings-tester-0.3.2.tgz#04cc499d15ab1d8b2d14dd48415a13d01333bc5b" From d84f9d6d8427bf018002e794c38e404e876d3e86 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Tue, 30 Apr 2019 10:38:46 -0400 Subject: [PATCH 07/51] Add email connector --- x-pack/package.json | 1 + .../alerting/server/connector_service.test.ts | 39 +++++++++++++++++++ .../alerting/server/connector_service.ts | 3 +- .../server/default_connectors/email.ts | 15 +++++++ .../server/default_connectors/index.ts | 1 + yarn.lock | 7 ++++ 6 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/alerting/server/default_connectors/email.ts diff --git a/x-pack/package.json b/x-pack/package.json index 47debbd3cf8e0..0f4eaca53fb87 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -182,6 +182,7 @@ "@scant/router": "^0.1.0", "@slack/client": "^4.8.0", "@turf/boolean-contains": "6.0.1", + "@types/nodemailer": "^4.6.8", "angular-resource": "1.4.9", "angular-sanitize": "1.6.5", "angular-ui-ace": "0.2.3", diff --git a/x-pack/plugins/alerting/server/connector_service.test.ts b/x-pack/plugins/alerting/server/connector_service.test.ts index 524a8ea1ffaac..a896f9eb9d10b 100644 --- a/x-pack/plugins/alerting/server/connector_service.test.ts +++ b/x-pack/plugins/alerting/server/connector_service.test.ts @@ -38,6 +38,7 @@ describe('has()', () => { const connectorService = new ConnectorService(); expect(connectorService.has('console')).toEqual(true); expect(connectorService.has('slack')).toEqual(true); + expect(connectorService.has('email')).toEqual(true); }); test('returns true after registering a connector', () => { @@ -47,3 +48,41 @@ describe('has()', () => { expect(connectorService.has('my-connector')); }); }); + +describe('execute()', () => { + test('calls the executor with proper params', async () => { + const executor = jest.fn().mockResolvedValueOnce({ success: true }); + const connectorService = new ConnectorService(); + connectorService.register({ id: 'my-connector', executor }); + await connectorService.execute('my-connector', { foo: true }, { bar: false }); + expect(executor).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + Object { + "foo": true, + }, + Object { + "bar": false, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], +} +`); + }); + + test('throws error if connector not registered', async () => { + const connectorService = new ConnectorService(); + await expect( + connectorService.execute('my-connector', { foo: true }, { bar: false }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Connector \\"my-connector\\" is not registered."` + ); + }); +}); diff --git a/x-pack/plugins/alerting/server/connector_service.ts b/x-pack/plugins/alerting/server/connector_service.ts index 2913f09a556fe..d2af68bd9de3d 100644 --- a/x-pack/plugins/alerting/server/connector_service.ts +++ b/x-pack/plugins/alerting/server/connector_service.ts @@ -5,7 +5,7 @@ */ import Boom from 'boom'; -import { consoleConnector, slackConnector } from './default_connectors'; +import { consoleConnector, emailConnector, slackConnector } from './default_connectors'; interface Connector { id: string; @@ -18,6 +18,7 @@ export class ConnectorService { constructor() { this.register(consoleConnector); this.register(slackConnector); + this.register(emailConnector); } public has(connectorId: string) { diff --git a/x-pack/plugins/alerting/server/default_connectors/email.ts b/x-pack/plugins/alerting/server/default_connectors/email.ts new file mode 100644 index 0000000000000..d5197bba73d78 --- /dev/null +++ b/x-pack/plugins/alerting/server/default_connectors/email.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import nodemailer, { SendMailOptions } from 'nodemailer'; + +export const emailConnector = { + id: 'email', + async executor(connectorOptions: any, params: SendMailOptions) { + const transporter = nodemailer.createTransport(connectorOptions); + await transporter.sendMail(params); + }, +}; diff --git a/x-pack/plugins/alerting/server/default_connectors/index.ts b/x-pack/plugins/alerting/server/default_connectors/index.ts index 4cb9207560a61..4fdab4a380b7c 100644 --- a/x-pack/plugins/alerting/server/default_connectors/index.ts +++ b/x-pack/plugins/alerting/server/default_connectors/index.ts @@ -6,3 +6,4 @@ export { consoleConnector } from './console'; export { slackConnector } from './slack'; +export { emailConnector } from './email'; diff --git a/yarn.lock b/yarn.lock index f6fc622dc56ba..e86ddda219d16 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3276,6 +3276,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.27.tgz#eb3843f15d0ba0986cc7e4d734d2ee8b50709ef8" integrity sha512-e9wgeY6gaY21on3ve0xAjgBVjGDWq/xUteK0ujsE53bUoxycMkqfnkUgMt6ffZtykZ5X12Mg3T7Pw4TRCObDKg== +"@types/nodemailer@^4.6.8": + version "4.6.8" + resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-4.6.8.tgz#c14356e799fe1d4ee566126f901bc6031cc7b1b5" + integrity sha512-IX1P3bxDP1VIdZf6/kIWYNmSejkYm9MOyMEtoDFi4DVzKjJ3kY4GhOcOAKs6lZRjqVVmF9UjPOZXuQczlpZThw== + dependencies: + "@types/node" "*" + "@types/normalize-package-data@*": version "2.4.0" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" From dfd0f36ea9be6e2cc5d95a7cb433a0c5568cea54 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Tue, 30 Apr 2019 12:55:55 -0400 Subject: [PATCH 08/51] Ability to validate params and connector options --- .../alerting/server/action_service.test.ts | 28 +++ .../plugins/alerting/server/action_service.ts | 4 + .../alerting/server/connector_service.test.ts | 161 ++++++++++++++++++ .../alerting/server/connector_service.ts | 43 ++++- .../server/default_connectors/console.ts | 9 + .../server/default_connectors/slack.ts | 19 +++ 6 files changed, 259 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/alerting/server/action_service.test.ts b/x-pack/plugins/alerting/server/action_service.test.ts index 8bb3ebf74346b..06bee119355eb 100644 --- a/x-pack/plugins/alerting/server/action_service.test.ts +++ b/x-pack/plugins/alerting/server/action_service.test.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import Joi from 'joi'; import { ConnectorService } from './connector_service'; import { ActionService } from './action_service'; @@ -60,6 +61,33 @@ describe('create()', () => { `); }); + test('validates connectorOptions', async () => { + const connectorService = new ConnectorService(); + const actionService = new ActionService(connectorService); + connectorService.register({ + id: 'my-connector', + validate: { + connectorOptions: Joi.object() + .keys({ + param1: Joi.string().required(), + }) + .required(), + }, + async executor() {}, + }); + await expect( + actionService.create(savedObjectsClient, { + id: 'my-alert', + description: 'my description', + connectorId: 'my-connector', + connectorOptions: {}, + connectorOptionsSecrets: {}, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"child \\"param1\\" fails because [\\"param1\\" is required]"` + ); + }); + test(`throws an error when connector doesn't exist`, async () => { const connectorService = new ConnectorService(); const actionService = new ActionService(connectorService); diff --git a/x-pack/plugins/alerting/server/action_service.ts b/x-pack/plugins/alerting/server/action_service.ts index 705435befe18f..c495b281c114a 100644 --- a/x-pack/plugins/alerting/server/action_service.ts +++ b/x-pack/plugins/alerting/server/action_service.ts @@ -34,6 +34,10 @@ export class ActionService { if (!this.connectorService.has(connectorId)) { throw Boom.badRequest(`Connector "${connectorId}" is not registered.`); } + this.connectorService.validateConnectorOptions(connectorId, { + ...data.connectorOptions, + ...data.connectorOptionsSecrets, + }); return await savedObjectsClient.create('action', data, { id }); } diff --git a/x-pack/plugins/alerting/server/connector_service.test.ts b/x-pack/plugins/alerting/server/connector_service.test.ts index a896f9eb9d10b..282157a54e919 100644 --- a/x-pack/plugins/alerting/server/connector_service.test.ts +++ b/x-pack/plugins/alerting/server/connector_service.test.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import Joi from 'joi'; import { ConnectorService } from './connector_service'; test('automatically registers default connectors', () => { @@ -28,6 +29,124 @@ describe('register()', () => { }); }); +describe('get()', () => { + test('returns connector', () => { + const connectorService = new ConnectorService(); + connectorService.register({ + id: 'my-connector', + async executor() {}, + }); + const connector = connectorService.get('my-connector'); + expect(connector).toMatchInlineSnapshot(` +Object { + "executor": [Function], + "id": "my-connector", +} +`); + }); + + test(`throws an error when connector doesn't exist`, () => { + const connectorService = new ConnectorService(); + expect(() => connectorService.get('my-connector')).toThrowErrorMatchingInlineSnapshot( + `"Connector \\"my-connector\\" is not registered."` + ); + }); +}); + +describe('validateParams()', () => { + test('should pass when validation not defined', () => { + const connectorService = new ConnectorService(); + connectorService.register({ + id: 'my-connector', + async executor() {}, + }); + connectorService.validateParams('my-connector', {}); + }); + + test('should validate and pass when params is valid', () => { + const connectorService = new ConnectorService(); + connectorService.register({ + id: 'my-connector', + validate: { + params: Joi.object() + .keys({ + param1: Joi.string().required(), + }) + .required(), + }, + async executor() {}, + }); + connectorService.validateParams('my-connector', { param1: 'value' }); + }); + + test('should validate and throw error when params is invalid', () => { + const connectorService = new ConnectorService(); + connectorService.register({ + id: 'my-connector', + validate: { + params: Joi.object() + .keys({ + param1: Joi.string().required(), + }) + .required(), + }, + async executor() {}, + }); + expect(() => + connectorService.validateParams('my-connector', {}) + ).toThrowErrorMatchingInlineSnapshot( + `"child \\"param1\\" fails because [\\"param1\\" is required]"` + ); + }); +}); + +describe('validateConnectorOptions()', () => { + test('should pass when validation not defined', () => { + const connectorService = new ConnectorService(); + connectorService.register({ + id: 'my-connector', + async executor() {}, + }); + connectorService.validateConnectorOptions('my-connector', {}); + }); + + test('should validate and pass when connectorOptions is valid', () => { + const connectorService = new ConnectorService(); + connectorService.register({ + id: 'my-connector', + validate: { + connectorOptions: Joi.object() + .keys({ + param1: Joi.string().required(), + }) + .required(), + }, + async executor() {}, + }); + connectorService.validateConnectorOptions('my-connector', { param1: 'value' }); + }); + + test('should validate and throw error when connectorOptions is invalid', () => { + const connectorService = new ConnectorService(); + connectorService.register({ + id: 'my-connector', + validate: { + connectorOptions: Joi.object() + .keys({ + param1: Joi.string().required(), + }) + .required(), + }, + async executor() {}, + }); + expect(() => + connectorService.validateConnectorOptions('my-connector', {}) + ).toThrowErrorMatchingInlineSnapshot( + `"child \\"param1\\" fails because [\\"param1\\" is required]"` + ); + }); +}); + describe('has()', () => { test('returns false for unregistered connectors', () => { const connectorService = new ConnectorService(); @@ -77,6 +196,48 @@ describe('execute()', () => { `); }); + test('validates params', async () => { + const executor = jest.fn().mockResolvedValueOnce({ success: true }); + const connectorService = new ConnectorService(); + connectorService.register({ + id: 'my-connector', + executor, + validate: { + params: Joi.object() + .keys({ + param1: Joi.string().required(), + }) + .required(), + }, + }); + await expect( + connectorService.execute('my-connector', {}, {}) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"child \\"param1\\" fails because [\\"param1\\" is required]"` + ); + }); + + test('validates connectorOptions', async () => { + const executor = jest.fn().mockResolvedValueOnce({ success: true }); + const connectorService = new ConnectorService(); + connectorService.register({ + id: 'my-connector', + executor, + validate: { + connectorOptions: Joi.object() + .keys({ + param1: Joi.string().required(), + }) + .required(), + }, + }); + await expect( + connectorService.execute('my-connector', {}, {}) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"child \\"param1\\" fails because [\\"param1\\" is required]"` + ); + }); + test('throws error if connector not registered', async () => { const connectorService = new ConnectorService(); await expect( diff --git a/x-pack/plugins/alerting/server/connector_service.ts b/x-pack/plugins/alerting/server/connector_service.ts index d2af68bd9de3d..14d68f52a4bf9 100644 --- a/x-pack/plugins/alerting/server/connector_service.ts +++ b/x-pack/plugins/alerting/server/connector_service.ts @@ -9,6 +9,10 @@ import { consoleConnector, emailConnector, slackConnector } from './default_conn interface Connector { id: string; + validate?: { + params?: any; + connectorOptions?: any; + }; executor(connectorOptions: any, params: any): Promise; } @@ -21,8 +25,8 @@ export class ConnectorService { this.register(emailConnector); } - public has(connectorId: string) { - return !!this.connectors[connectorId]; + public has(id: string) { + return !!this.connectors[id]; } public register(connector: Connector) { @@ -32,11 +36,40 @@ export class ConnectorService { this.connectors[connector.id] = connector; } - public async execute(connectorId: string, connectorOptions: any, params: any) { - const connector = this.connectors[connectorId]; + public get(id: string) { + const connector = this.connectors[id]; if (!connector) { - throw Boom.badRequest(`Connector "${connectorId}" is not registered.`); + throw Boom.badRequest(`Connector "${id}" is not registered.`); } + return connector; + } + + public validateParams(id: string, params: any) { + const connector = this.get(id); + const validator = connector.validate && connector.validate.params; + if (validator) { + const { error } = validator.validate(params); + if (error) { + throw error; + } + } + } + + public validateConnectorOptions(id: string, connectorOptions: any) { + const connector = this.get(id); + const validator = connector.validate && connector.validate.connectorOptions; + if (validator) { + const { error } = validator.validate(connectorOptions); + if (error) { + throw error; + } + } + } + + public async execute(id: string, connectorOptions: any, params: any) { + const connector = this.get(id); + this.validateConnectorOptions(id, connectorOptions); + this.validateParams(id, params); return await connector.executor(connectorOptions, params); } } diff --git a/x-pack/plugins/alerting/server/default_connectors/console.ts b/x-pack/plugins/alerting/server/default_connectors/console.ts index a62b9dc9f8fc9..842f62a622811 100644 --- a/x-pack/plugins/alerting/server/default_connectors/console.ts +++ b/x-pack/plugins/alerting/server/default_connectors/console.ts @@ -4,12 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ +import Joi from 'joi'; + interface ConsoleParams { message: string; } export const consoleConnector = { id: 'console', + validate: { + params: Joi.object() + .keys({ + message: Joi.string().required(), + }) + .required(), + }, async executor(connectorOptions: any, { message }: ConsoleParams) { // eslint-disable-next-line no-console console.log(message); diff --git a/x-pack/plugins/alerting/server/default_connectors/slack.ts b/x-pack/plugins/alerting/server/default_connectors/slack.ts index 099cf3466c5c8..2f6f3d7137df8 100644 --- a/x-pack/plugins/alerting/server/default_connectors/slack.ts +++ b/x-pack/plugins/alerting/server/default_connectors/slack.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import Joi from 'joi'; import slack from 'slack'; interface SlackConnectorOptions { @@ -24,6 +25,24 @@ type SlackParams = PostMessageParams; export const slackConnector = { id: 'slack', + validate: { + params: Joi.alternatives() + .try( + Joi.object() + .keys({ + command: Joi.string().valid('post-message'), + message: Joi.string().required(), + channel: Joi.string().required(), + }) + .required() + ) + .required(), + connectorOptions: Joi.object() + .keys({ + token: Joi.string().required(), + }) + .required(), + }, async executor(connectorOptions: SlackConnectorOptions, params: SlackParams) { switch (params.command) { case 'post-message': From 632a2d95a963845318dc4e8c00351950372cf183 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Tue, 30 Apr 2019 13:05:55 -0400 Subject: [PATCH 09/51] Remove connectorOptionsSecrets for now --- x-pack/plugins/alerting/mappings.json | 4 ---- .../plugins/alerting/server/action_service.test.ts | 11 ----------- x-pack/plugins/alerting/server/action_service.ts | 13 ++----------- .../plugins/alerting/server/routes/create_action.ts | 2 -- .../api_integration/apis/alerting/create_action.ts | 13 ------------- yarn.lock | 12 +----------- 6 files changed, 3 insertions(+), 52 deletions(-) diff --git a/x-pack/plugins/alerting/mappings.json b/x-pack/plugins/alerting/mappings.json index 29bd3aea5b2a4..2ecaf42bce2ac 100644 --- a/x-pack/plugins/alerting/mappings.json +++ b/x-pack/plugins/alerting/mappings.json @@ -10,10 +10,6 @@ "connectorOptions": { "dynamic": "true", "type": "object" - }, - "connectorOptionsSecrets": { - "dynamic": "true", - "type": "object" } } } diff --git a/x-pack/plugins/alerting/server/action_service.test.ts b/x-pack/plugins/alerting/server/action_service.test.ts index 06bee119355eb..aa2604a6382ca 100644 --- a/x-pack/plugins/alerting/server/action_service.test.ts +++ b/x-pack/plugins/alerting/server/action_service.test.ts @@ -32,7 +32,6 @@ describe('create()', () => { description: 'my description', connectorId: 'console', connectorOptions: {}, - connectorOptionsSecrets: {}, }); expect(result).toEqual(expectedResult); expect(savedObjectsClient.create).toMatchInlineSnapshot(` @@ -43,7 +42,6 @@ describe('create()', () => { Object { "connectorId": "console", "connectorOptions": Object {}, - "connectorOptionsSecrets": Object {}, "description": "my description", }, Object { @@ -81,7 +79,6 @@ describe('create()', () => { description: 'my description', connectorId: 'my-connector', connectorOptions: {}, - connectorOptionsSecrets: {}, }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"child \\"param1\\" fails because [\\"param1\\" is required]"` @@ -97,7 +94,6 @@ describe('create()', () => { description: 'my description', connectorId: 'unregistered-connector', connectorOptions: {}, - connectorOptionsSecrets: {}, }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Connector \\"unregistered-connector\\" is not registered."` @@ -145,9 +141,6 @@ describe('fire()', () => { connectorOptions: { foo: true, }, - connectorOptionsSecrets: { - bar: false, - }, }, }); const result = await actionService.fire({ @@ -161,7 +154,6 @@ describe('fire()', () => { "calls": Array [ Array [ Object { - "bar": false, "foo": true, }, Object { @@ -190,9 +182,6 @@ describe('fire()', () => { connectorOptions: { foo: true, }, - connectorOptionsSecrets: { - bar: false, - }, }, }); await expect( diff --git a/x-pack/plugins/alerting/server/action_service.ts b/x-pack/plugins/alerting/server/action_service.ts index c495b281c114a..036d881ff59f5 100644 --- a/x-pack/plugins/alerting/server/action_service.ts +++ b/x-pack/plugins/alerting/server/action_service.ts @@ -13,7 +13,6 @@ interface Action { description: string; connectorId: string; connectorOptions: { [key: string]: any }; - connectorOptionsSecrets: { [key: string]: any }; } interface FireActionOptions { @@ -34,10 +33,7 @@ export class ActionService { if (!this.connectorService.has(connectorId)) { throw Boom.badRequest(`Connector "${connectorId}" is not registered.`); } - this.connectorService.validateConnectorOptions(connectorId, { - ...data.connectorOptions, - ...data.connectorOptionsSecrets, - }); + this.connectorService.validateConnectorOptions(connectorId, data.connectorOptions); return await savedObjectsClient.create('action', data, { id }); } @@ -47,14 +43,9 @@ export class ActionService { public async fire({ id, params, savedObjectsClient }: FireActionOptions) { const action = await this.get(savedObjectsClient, id); - const connectorOptions = Object.assign( - {}, - action.attributes.connectorOptions, - action.attributes.connectorOptionsSecrets - ); return await this.connectorService.execute( action.attributes.connectorId, - connectorOptions, + action.attributes.connectorOptions, params ); } diff --git a/x-pack/plugins/alerting/server/routes/create_action.ts b/x-pack/plugins/alerting/server/routes/create_action.ts index 8a298f840381e..500da21408185 100644 --- a/x-pack/plugins/alerting/server/routes/create_action.ts +++ b/x-pack/plugins/alerting/server/routes/create_action.ts @@ -26,7 +26,6 @@ interface CreateActionRequest extends Hapi.Request { description: string; connectorId: string; connectorOptions: { [key: string]: any }; - connectorOptionsSecrets: { [key: string]: any }; }; } @@ -41,7 +40,6 @@ export function createActionRoute(server: any) { description: Joi.string().required(), connectorId: Joi.string().required(), connectorOptions: Joi.object(), - connectorOptionsSecrets: Joi.object(), }), }, }, diff --git a/x-pack/test/api_integration/apis/alerting/create_action.ts b/x-pack/test/api_integration/apis/alerting/create_action.ts index bbe7505f7e330..05c93570aa65b 100644 --- a/x-pack/test/api_integration/apis/alerting/create_action.ts +++ b/x-pack/test/api_integration/apis/alerting/create_action.ts @@ -37,9 +37,6 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe connectorOptions: { username: 'username', }, - connectorOptionsSecrets: { - password: 'password', - }, }) .expect(200) .then((resp: any) => { @@ -50,7 +47,6 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe description: 'My action', connectorId: 'console', connectorOptions: { username: 'username' }, - connectorOptionsSecrets: { password: 'password' }, }, references: [], updated_at: resp.body.updated_at, @@ -70,9 +66,6 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe connectorOptions: { username: 'username', }, - connectorOptionsSecrets: { - password: 'password', - }, }) .expect(200); await supertest @@ -85,9 +78,6 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe connectorOptions: { username: 'username', }, - connectorOptionsSecrets: { - password: 'password', - }, }) .expect(409); }); @@ -103,9 +93,6 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe connectorOptions: { username: 'username', }, - connectorOptionsSecrets: { - password: 'password', - }, }) .expect(400) .then((resp: any) => { diff --git a/yarn.lock b/yarn.lock index e86ddda219d16..704994bbe0ce3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25748,21 +25748,11 @@ typescript-fsa@^2.0.0, typescript-fsa@^2.5.0: resolved "https://registry.yarnpkg.com/typescript-fsa/-/typescript-fsa-2.5.0.tgz#1baec01b5e8f5f34c322679d1327016e9e294faf" integrity sha1-G67AG16PXzTDImedEycBbp4pT68= -typescript@^3.3.3333, typescript@~3.3.3333: +typescript@^3.3.3333, typescript@~3.0.3, typescript@~3.3.3333, typescript@~3.4.3: version "3.3.3333" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.3.3333.tgz#171b2c5af66c59e9431199117a3bcadc66fdcfd6" integrity sha512-JjSKsAfuHBE/fB2oZ8NxtRTk5iGcg6hkYXMnZ3Wc+b2RSqejEqTaem11mHASMnFilHrax3sLK0GDzcJrekZYLw== -typescript@~3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.0.3.tgz#4853b3e275ecdaa27f78fda46dc273a7eb7fc1c8" - integrity sha512-kk80vLW9iGtjMnIv11qyxLqZm20UklzuR2tL0QAnDIygIUIemcZMxlMWudl9OOt76H3ntVzcTiddQ1/pAAJMYg== - -typescript@~3.4.3: - version "3.4.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.5.tgz#2d2618d10bb566572b8d7aad5180d84257d70a99" - integrity sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw== - typings-tester@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/typings-tester/-/typings-tester-0.3.2.tgz#04cc499d15ab1d8b2d14dd48415a13d01333bc5b" From c8580ef87a70465a43ededc6eda43e3f82b5b3ec Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Tue, 30 Apr 2019 13:27:22 -0400 Subject: [PATCH 10/51] Fix plugin config validation --- x-pack/plugins/alerting/index.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/alerting/index.ts b/x-pack/plugins/alerting/index.ts index 3109ada84fa27..67e9edd8866fc 100644 --- a/x-pack/plugins/alerting/index.ts +++ b/x-pack/plugins/alerting/index.ts @@ -16,9 +16,11 @@ export function alerting(kibana: any) { configPrefix: 'xpack.alerting', require: ['kibana', 'elasticsearch'], config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }); + return Joi.object() + .keys({ + enabled: Joi.boolean().default(true), + }) + .default(); }, init(server: Hapi.Server) { const alertingEnabled = server.config().get('xpack.alerting.enabled'); From d7fad2288ce94c2254552e00a75cdc84371fe136 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Tue, 30 Apr 2019 15:47:05 -0400 Subject: [PATCH 11/51] Add tests for slack connector --- .../server/default_connectors/slack.test.ts | 48 +++++++++++++++++++ .../server/default_connectors/slack.ts | 6 +-- 2 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/alerting/server/default_connectors/slack.test.ts diff --git a/x-pack/plugins/alerting/server/default_connectors/slack.test.ts b/x-pack/plugins/alerting/server/default_connectors/slack.test.ts new file mode 100644 index 0000000000000..0be0406115692 --- /dev/null +++ b/x-pack/plugins/alerting/server/default_connectors/slack.test.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as slackMock from 'slack'; +import { slackConnector } from './slack'; + +jest.mock('slack', () => ({ + chat: { + postMessage: jest.fn(), + }, +})); + +test(`executor throws error when command isn't valid`, async () => { + await expect( + slackConnector.executor({ token: '123' }, { command: 'invalid' }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Unsupported command \\"invalid\\"."`); +}); + +describe('post-message', () => { + test('Calls the slack API with the proper arguments', async () => { + await slackConnector.executor( + { token: '123' }, + { command: 'post-message', message: 'hello', channel: 'general' } + ); + expect(slackMock.chat.postMessage).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + Object { + "channel": "general", + "text": "hello", + "token": "123", + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], +} +`); + }); +}); diff --git a/x-pack/plugins/alerting/server/default_connectors/slack.ts b/x-pack/plugins/alerting/server/default_connectors/slack.ts index 2f6f3d7137df8..484861999d028 100644 --- a/x-pack/plugins/alerting/server/default_connectors/slack.ts +++ b/x-pack/plugins/alerting/server/default_connectors/slack.ts @@ -21,7 +21,7 @@ interface PostMessageParams extends DefaulParams { channel: string; } -type SlackParams = PostMessageParams; +type SlackParams = PostMessageParams | DefaulParams; export const slackConnector = { id: 'slack', @@ -48,8 +48,8 @@ export const slackConnector = { case 'post-message': await slack.chat.postMessage({ token: connectorOptions.token, - text: params.message, - channel: params.channel, + text: (params as PostMessageParams).message, + channel: (params as PostMessageParams).channel, }); break; default: From e4c88978e490a27604a5789f18c9f77096cd9730 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Thu, 2 May 2019 15:56:00 -0400 Subject: [PATCH 12/51] Default connectors register on plugin init, console renamed to log, slack to message_slack --- x-pack/plugins/alerting/index.ts | 6 ++ .../alerting/server/connector_service.ts | 7 --- .../server/default_connectors/index.ts | 4 +- .../default_connectors/{console.ts => log.ts} | 8 +-- .../{slack.test.ts => message_slack.test.ts} | 19 ++---- .../default_connectors/message_slack.ts | 41 +++++++++++++ .../server/default_connectors/slack.ts | 59 ------------------- 7 files changed, 57 insertions(+), 87 deletions(-) rename x-pack/plugins/alerting/server/default_connectors/{console.ts => log.ts} (77%) rename x-pack/plugins/alerting/server/default_connectors/{slack.test.ts => message_slack.test.ts} (51%) create mode 100644 x-pack/plugins/alerting/server/default_connectors/message_slack.ts delete mode 100644 x-pack/plugins/alerting/server/default_connectors/slack.ts diff --git a/x-pack/plugins/alerting/index.ts b/x-pack/plugins/alerting/index.ts index 67e9edd8866fc..97c62d4348de2 100644 --- a/x-pack/plugins/alerting/index.ts +++ b/x-pack/plugins/alerting/index.ts @@ -6,6 +6,7 @@ import Hapi from 'hapi'; import mappings from './mappings.json'; +import { logConnector, messageSlackConnector, emailConnector } from './server/default_connectors'; import { createActionRoute, AlertService, ActionService, ConnectorService } from './server'; import { APP_ID } from './common/constants'; @@ -34,6 +35,11 @@ export function alerting(kibana: any) { const actionService = new ActionService(connectorService); const alertService = new AlertService(); + // Register default connectors + connectorService.register(logConnector); + connectorService.register(messageSlackConnector); + connectorService.register(emailConnector); + // Routes createActionRoute(server); diff --git a/x-pack/plugins/alerting/server/connector_service.ts b/x-pack/plugins/alerting/server/connector_service.ts index 14d68f52a4bf9..12b6720defa0c 100644 --- a/x-pack/plugins/alerting/server/connector_service.ts +++ b/x-pack/plugins/alerting/server/connector_service.ts @@ -5,7 +5,6 @@ */ import Boom from 'boom'; -import { consoleConnector, emailConnector, slackConnector } from './default_connectors'; interface Connector { id: string; @@ -19,12 +18,6 @@ interface Connector { export class ConnectorService { private connectors: { [id: string]: Connector } = {}; - constructor() { - this.register(consoleConnector); - this.register(slackConnector); - this.register(emailConnector); - } - public has(id: string) { return !!this.connectors[id]; } diff --git a/x-pack/plugins/alerting/server/default_connectors/index.ts b/x-pack/plugins/alerting/server/default_connectors/index.ts index 4fdab4a380b7c..ec6a79e90dbae 100644 --- a/x-pack/plugins/alerting/server/default_connectors/index.ts +++ b/x-pack/plugins/alerting/server/default_connectors/index.ts @@ -4,6 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export { consoleConnector } from './console'; -export { slackConnector } from './slack'; +export { logConnector } from './log'; +export { messageSlackConnector } from './message_slack'; export { emailConnector } from './email'; diff --git a/x-pack/plugins/alerting/server/default_connectors/console.ts b/x-pack/plugins/alerting/server/default_connectors/log.ts similarity index 77% rename from x-pack/plugins/alerting/server/default_connectors/console.ts rename to x-pack/plugins/alerting/server/default_connectors/log.ts index 842f62a622811..a5de43be4ca15 100644 --- a/x-pack/plugins/alerting/server/default_connectors/console.ts +++ b/x-pack/plugins/alerting/server/default_connectors/log.ts @@ -6,12 +6,12 @@ import Joi from 'joi'; -interface ConsoleParams { +interface LogParams { message: string; } -export const consoleConnector = { - id: 'console', +export const logConnector = { + id: 'log', validate: { params: Joi.object() .keys({ @@ -19,7 +19,7 @@ export const consoleConnector = { }) .required(), }, - async executor(connectorOptions: any, { message }: ConsoleParams) { + async executor(connectorOptions: any, { message }: LogParams) { // eslint-disable-next-line no-console console.log(message); }, diff --git a/x-pack/plugins/alerting/server/default_connectors/slack.test.ts b/x-pack/plugins/alerting/server/default_connectors/message_slack.test.ts similarity index 51% rename from x-pack/plugins/alerting/server/default_connectors/slack.test.ts rename to x-pack/plugins/alerting/server/default_connectors/message_slack.test.ts index 0be0406115692..92badd7cfe0fa 100644 --- a/x-pack/plugins/alerting/server/default_connectors/slack.test.ts +++ b/x-pack/plugins/alerting/server/default_connectors/message_slack.test.ts @@ -5,7 +5,7 @@ */ import * as slackMock from 'slack'; -import { slackConnector } from './slack'; +import { messageSlackConnector } from './message_slack'; jest.mock('slack', () => ({ chat: { @@ -13,19 +13,9 @@ jest.mock('slack', () => ({ }, })); -test(`executor throws error when command isn't valid`, async () => { - await expect( - slackConnector.executor({ token: '123' }, { command: 'invalid' }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"Unsupported command \\"invalid\\"."`); -}); - -describe('post-message', () => { - test('Calls the slack API with the proper arguments', async () => { - await slackConnector.executor( - { token: '123' }, - { command: 'post-message', message: 'hello', channel: 'general' } - ); - expect(slackMock.chat.postMessage).toMatchInlineSnapshot(` +test('Calls the slack API with the proper arguments', async () => { + await messageSlackConnector.executor({ token: '123' }, { message: 'hello', channel: 'general' }); + expect(slackMock.chat.postMessage).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ Array [ @@ -44,5 +34,4 @@ describe('post-message', () => { ], } `); - }); }); diff --git a/x-pack/plugins/alerting/server/default_connectors/message_slack.ts b/x-pack/plugins/alerting/server/default_connectors/message_slack.ts new file mode 100644 index 0000000000000..51d9598a4335d --- /dev/null +++ b/x-pack/plugins/alerting/server/default_connectors/message_slack.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Joi from 'joi'; +import slack from 'slack'; + +interface SlackConnectorOptions { + token: string; +} + +interface MessageSlackParams { + message: string; + channel: string; +} + +export const messageSlackConnector = { + id: 'message-slack', + validate: { + params: Joi.object() + .keys({ + message: Joi.string().required(), + channel: Joi.string().required(), + }) + .required(), + connectorOptions: Joi.object() + .keys({ + token: Joi.string().required(), + }) + .required(), + }, + async executor(connectorOptions: SlackConnectorOptions, params: MessageSlackParams) { + await slack.chat.postMessage({ + token: connectorOptions.token, + text: params.message, + channel: params.channel, + }); + }, +}; diff --git a/x-pack/plugins/alerting/server/default_connectors/slack.ts b/x-pack/plugins/alerting/server/default_connectors/slack.ts deleted file mode 100644 index 484861999d028..0000000000000 --- a/x-pack/plugins/alerting/server/default_connectors/slack.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Joi from 'joi'; -import slack from 'slack'; - -interface SlackConnectorOptions { - token: string; -} - -interface DefaulParams { - command: string; -} - -interface PostMessageParams extends DefaulParams { - command: 'post-message'; - message: string; - channel: string; -} - -type SlackParams = PostMessageParams | DefaulParams; - -export const slackConnector = { - id: 'slack', - validate: { - params: Joi.alternatives() - .try( - Joi.object() - .keys({ - command: Joi.string().valid('post-message'), - message: Joi.string().required(), - channel: Joi.string().required(), - }) - .required() - ) - .required(), - connectorOptions: Joi.object() - .keys({ - token: Joi.string().required(), - }) - .required(), - }, - async executor(connectorOptions: SlackConnectorOptions, params: SlackParams) { - switch (params.command) { - case 'post-message': - await slack.chat.postMessage({ - token: connectorOptions.token, - text: (params as PostMessageParams).message, - channel: (params as PostMessageParams).channel, - }); - break; - default: - throw new Error(`Unsupported command "${params.command}".`); - } - }, -}; From 661e73167527fbb2d0a15879b0a272db94044c9c Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Fri, 3 May 2019 09:54:49 -0400 Subject: [PATCH 13/51] Add remaining API endpoints for action CRUD --- x-pack/plugins/alerting/index.ts | 15 +- .../alerting/server/action_service.test.ts | 185 ++++++++++++++++-- .../plugins/alerting/server/action_service.ts | 27 ++- .../alerting/server/connector_service.test.ts | 12 -- x-pack/plugins/alerting/server/index.ts | 8 +- .../alerting/server/routes/create_action.ts | 16 +- .../alerting/server/routes/delete_action.ts | 38 ++++ .../alerting/server/routes/find_action.ts | 36 ++++ .../alerting/server/routes/get_action.ts | 38 ++++ .../plugins/alerting/server/routes/index.ts | 4 + .../plugins/alerting/server/routes/types.ts | 7 + .../alerting/server/routes/update_action.ts | 54 +++++ .../apis/alerting/create_action.ts | 51 +++-- .../apis/alerting/delete_action.ts | 44 +++++ .../apis/alerting/find_action.ts | 48 +++++ .../apis/alerting/get_action.ts | 54 +++++ .../api_integration/apis/alerting/index.ts | 4 + .../apis/alerting/update_action.ts | 73 +++++++ .../es_archives/alerting/basic/data.json | 18 ++ .../es_archives/alerting/basic/mappings.json | 97 +++++++++ 20 files changed, 775 insertions(+), 54 deletions(-) create mode 100644 x-pack/plugins/alerting/server/routes/delete_action.ts create mode 100644 x-pack/plugins/alerting/server/routes/find_action.ts create mode 100644 x-pack/plugins/alerting/server/routes/get_action.ts create mode 100644 x-pack/plugins/alerting/server/routes/types.ts create mode 100644 x-pack/plugins/alerting/server/routes/update_action.ts create mode 100644 x-pack/test/api_integration/apis/alerting/delete_action.ts create mode 100644 x-pack/test/api_integration/apis/alerting/find_action.ts create mode 100644 x-pack/test/api_integration/apis/alerting/get_action.ts create mode 100644 x-pack/test/api_integration/apis/alerting/update_action.ts create mode 100644 x-pack/test/functional/es_archives/alerting/basic/data.json create mode 100644 x-pack/test/functional/es_archives/alerting/basic/mappings.json diff --git a/x-pack/plugins/alerting/index.ts b/x-pack/plugins/alerting/index.ts index 97c62d4348de2..4e8d05e719e0a 100644 --- a/x-pack/plugins/alerting/index.ts +++ b/x-pack/plugins/alerting/index.ts @@ -7,7 +7,16 @@ import Hapi from 'hapi'; import mappings from './mappings.json'; import { logConnector, messageSlackConnector, emailConnector } from './server/default_connectors'; -import { createActionRoute, AlertService, ActionService, ConnectorService } from './server'; +import { + createActionRoute, + deleteActionRoute, + findActionRoute, + getActionRoute, + updateActionRoute, + AlertService, + ActionService, + ConnectorService, +} from './server'; import { APP_ID } from './common/constants'; @@ -42,6 +51,10 @@ export function alerting(kibana: any) { // Routes createActionRoute(server); + deleteActionRoute(server); + getActionRoute(server); + findActionRoute(server); + updateActionRoute(server); // Register service to server server.decorate('server', 'alerting', () => ({ diff --git a/x-pack/plugins/alerting/server/action_service.test.ts b/x-pack/plugins/alerting/server/action_service.test.ts index aa2604a6382ca..f4c88108c093e 100644 --- a/x-pack/plugins/alerting/server/action_service.test.ts +++ b/x-pack/plugins/alerting/server/action_service.test.ts @@ -25,14 +25,21 @@ describe('create()', () => { test('creates an action with all given properties', async () => { const expectedResult = Symbol(); const connectorService = new ConnectorService(); + connectorService.register({ + id: 'my-connector', + async executor() {}, + }); const actionService = new ActionService(connectorService); savedObjectsClient.create.mockResolvedValueOnce(expectedResult); - const result = await actionService.create(savedObjectsClient, { - id: 'my-alert', - description: 'my description', - connectorId: 'console', - connectorOptions: {}, - }); + const result = await actionService.create( + savedObjectsClient, + { + description: 'my description', + connectorId: 'my-connector', + connectorOptions: {}, + }, + { id: 'my-alert' } + ); expect(result).toEqual(expectedResult); expect(savedObjectsClient.create).toMatchInlineSnapshot(` [MockFunction] { @@ -40,7 +47,7 @@ describe('create()', () => { Array [ "action", Object { - "connectorId": "console", + "connectorId": "my-connector", "connectorOptions": Object {}, "description": "my description", }, @@ -74,12 +81,15 @@ describe('create()', () => { async executor() {}, }); await expect( - actionService.create(savedObjectsClient, { - id: 'my-alert', - description: 'my description', - connectorId: 'my-connector', - connectorOptions: {}, - }) + actionService.create( + savedObjectsClient, + { + description: 'my description', + connectorId: 'my-connector', + connectorOptions: {}, + }, + { id: 'my-alert' } + ) ).rejects.toThrowErrorMatchingInlineSnapshot( `"child \\"param1\\" fails because [\\"param1\\" is required]"` ); @@ -89,12 +99,15 @@ describe('create()', () => { const connectorService = new ConnectorService(); const actionService = new ActionService(connectorService); await expect( - actionService.create(savedObjectsClient, { - id: 'my-alert', - description: 'my description', - connectorId: 'unregistered-connector', - connectorOptions: {}, - }) + actionService.create( + savedObjectsClient, + { + description: 'my description', + connectorId: 'unregistered-connector', + connectorOptions: {}, + }, + { id: 'my-alert' } + ) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Connector \\"unregistered-connector\\" is not registered."` ); @@ -128,6 +141,140 @@ describe('get()', () => { }); }); +describe('find()', () => { + test('calls savedObjectsClient with parameters', async () => { + const expectedResult = Symbol(); + const connectorService = new ConnectorService(); + const actionService = new ActionService(connectorService); + savedObjectsClient.find.mockResolvedValueOnce(expectedResult); + const result = await actionService.find(savedObjectsClient, {}); + expect(result).toEqual(expectedResult); + expect(savedObjectsClient.find).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + Object { + "type": "action", + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], +} +`); + }); +}); + +describe('delete()', () => { + test('calls savedObjectsClient with id', async () => { + const expectedResult = Symbol(); + const connectorService = new ConnectorService(); + const actionService = new ActionService(connectorService); + savedObjectsClient.delete.mockResolvedValueOnce(expectedResult); + const result = await actionService.delete(savedObjectsClient, '1'); + expect(result).toEqual(expectedResult); + expect(savedObjectsClient.delete).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + "action", + "1", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], +} +`); + }); +}); + +describe('update()', () => { + test('updates an action with all given properties', async () => { + const expectedResult = Symbol(); + const connectorService = new ConnectorService(); + connectorService.register({ + id: 'my-connector', + async executor() {}, + }); + const actionService = new ActionService(connectorService); + savedObjectsClient.update.mockResolvedValueOnce(expectedResult); + const result = await actionService.update(savedObjectsClient, 'my-alert', { + description: 'my description', + connectorId: 'my-connector', + connectorOptions: {}, + }); + expect(result).toEqual(expectedResult); + expect(savedObjectsClient.update).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + "action", + "my-alert", + Object { + "connectorId": "my-connector", + "connectorOptions": Object {}, + "description": "my description", + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], +} +`); + }); + + test('validates connectorOptions', async () => { + const connectorService = new ConnectorService(); + const actionService = new ActionService(connectorService); + connectorService.register({ + id: 'my-connector', + validate: { + connectorOptions: Joi.object() + .keys({ + param1: Joi.string().required(), + }) + .required(), + }, + async executor() {}, + }); + await expect( + actionService.update(savedObjectsClient, 'my-alert', { + description: 'my description', + connectorId: 'my-connector', + connectorOptions: {}, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"child \\"param1\\" fails because [\\"param1\\" is required]"` + ); + }); + + test(`throws an error when connector doesn't exist`, async () => { + const connectorService = new ConnectorService(); + const actionService = new ActionService(connectorService); + await expect( + actionService.update(savedObjectsClient, 'my-alert', { + description: 'my description', + connectorId: 'unregistered-connector', + connectorOptions: {}, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Connector \\"unregistered-connector\\" is not registered."` + ); + }); +}); + describe('fire()', () => { test('fires an action with all given parameters', async () => { const connectorService = new ConnectorService(); diff --git a/x-pack/plugins/alerting/server/action_service.ts b/x-pack/plugins/alerting/server/action_service.ts index 036d881ff59f5..e8d3f32c08520 100644 --- a/x-pack/plugins/alerting/server/action_service.ts +++ b/x-pack/plugins/alerting/server/action_service.ts @@ -9,7 +9,6 @@ import { SavedObjectsClient } from 'src/legacy/server/saved_objects'; import { ConnectorService } from './connector_service'; interface Action { - id: string; description: string; connectorId: string; connectorOptions: { [key: string]: any }; @@ -28,7 +27,11 @@ export class ActionService { this.connectorService = connectorService; } - public async create(savedObjectsClient: SavedObjectsClient, { id, ...data }: Action) { + public async create( + savedObjectsClient: SavedObjectsClient, + data: Action, + { id }: { id?: string } = {} + ) { const { connectorId } = data; if (!this.connectorService.has(connectorId)) { throw Boom.badRequest(`Connector "${connectorId}" is not registered.`); @@ -41,6 +44,26 @@ export class ActionService { return await savedObjectsClient.get('action', id); } + public async find(savedObjectsClient: SavedObjectsClient, params: {}) { + return await savedObjectsClient.find({ + ...params, + type: 'action', + }); + } + + public async delete(savedObjectsClient: SavedObjectsClient, id: string) { + return await savedObjectsClient.delete('action', id); + } + + public async update(savedObjectsClient: SavedObjectsClient, id: string, data: Action) { + const { connectorId } = data; + if (!this.connectorService.has(connectorId)) { + throw Boom.badRequest(`Connector "${connectorId}" is not registered.`); + } + this.connectorService.validateConnectorOptions(connectorId, data.connectorOptions); + return await savedObjectsClient.update('action', id, data); + } + public async fire({ id, params, savedObjectsClient }: FireActionOptions) { const action = await this.get(savedObjectsClient, id); return await this.connectorService.execute( diff --git a/x-pack/plugins/alerting/server/connector_service.test.ts b/x-pack/plugins/alerting/server/connector_service.test.ts index 282157a54e919..7a0ebca00a74f 100644 --- a/x-pack/plugins/alerting/server/connector_service.test.ts +++ b/x-pack/plugins/alerting/server/connector_service.test.ts @@ -7,11 +7,6 @@ import Joi from 'joi'; import { ConnectorService } from './connector_service'; -test('automatically registers default connectors', () => { - const connectorService = new ConnectorService(); - expect(connectorService.has('console')).toEqual(true); -}); - describe('register()', () => { test('able to register connectors', () => { const executor = jest.fn(); @@ -153,13 +148,6 @@ describe('has()', () => { expect(connectorService.has('my-connector')).toEqual(false); }); - test('returns true for default connectors', () => { - const connectorService = new ConnectorService(); - expect(connectorService.has('console')).toEqual(true); - expect(connectorService.has('slack')).toEqual(true); - expect(connectorService.has('email')).toEqual(true); - }); - test('returns true after registering a connector', () => { const executor = jest.fn(); const connectorService = new ConnectorService(); diff --git a/x-pack/plugins/alerting/server/index.ts b/x-pack/plugins/alerting/server/index.ts index 0151f998ed34c..1e65476dc9d8e 100644 --- a/x-pack/plugins/alerting/server/index.ts +++ b/x-pack/plugins/alerting/server/index.ts @@ -4,7 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -export { createActionRoute } from './routes'; +export { + createActionRoute, + deleteActionRoute, + findActionRoute, + getActionRoute, + updateActionRoute, +} from './routes'; export { ActionService } from './action_service'; export { AlertService } from './alert_service'; export { ConnectorService } from './connector_service'; diff --git a/x-pack/plugins/alerting/server/routes/create_action.ts b/x-pack/plugins/alerting/server/routes/create_action.ts index 500da21408185..1858fa1daf954 100644 --- a/x-pack/plugins/alerting/server/routes/create_action.ts +++ b/x-pack/plugins/alerting/server/routes/create_action.ts @@ -10,6 +10,7 @@ import { ActionService } from '../action_service'; import { AlertService } from '../alert_service'; import { ConnectorService } from '../connector_service'; import { APP_ID } from '../../common/constants'; +import { WithoutQueryAndParams } from './types'; interface Server extends Hapi.Server { alerting: () => { @@ -19,24 +20,25 @@ interface Server extends Hapi.Server { }; } -interface CreateActionRequest extends Hapi.Request { +interface CreateActionRequest extends WithoutQueryAndParams { server: Server; payload: { - id: string; description: string; connectorId: string; connectorOptions: { [key: string]: any }; }; + params: { + id?: string; + }; } -export function createActionRoute(server: any) { +export function createActionRoute(server: Hapi.Server) { server.route({ method: 'POST', - path: `/api/${APP_ID}/action`, + path: `/api/${APP_ID}/action/{id?}`, options: { validate: { payload: Joi.object().keys({ - id: Joi.string().required(), description: Joi.string().required(), connectorId: Joi.string().required(), connectorOptions: Joi.object(), @@ -45,7 +47,9 @@ export function createActionRoute(server: any) { }, async handler(request: CreateActionRequest) { const savedObjectsClient = request.getSavedObjectsClient(); - return await request.server.alerting().actions.create(savedObjectsClient, request.payload); + return await request.server + .alerting() + .actions.create(savedObjectsClient, request.payload, { id: request.params.id }); }, }); } diff --git a/x-pack/plugins/alerting/server/routes/delete_action.ts b/x-pack/plugins/alerting/server/routes/delete_action.ts new file mode 100644 index 0000000000000..6e86784530e72 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/delete_action.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Hapi from 'hapi'; + +import { APP_ID } from '../../common/constants'; +import { ActionService } from '../action_service'; +import { AlertService } from '../alert_service'; +import { ConnectorService } from '../connector_service'; + +interface Server extends Hapi.Server { + alerting: () => { + actions: ActionService; + alerts: AlertService; + connectors: ConnectorService; + }; +} + +interface DeleteActionRequest extends Hapi.Request { + server: Server; + params: { + id: string; + }; +} + +export function deleteActionRoute(server: Hapi.Server) { + server.route({ + method: 'DELETE', + path: `/api/${APP_ID}/action/{id}`, + async handler(request: DeleteActionRequest) { + const savedObjectsClient = request.getSavedObjectsClient(); + return await request.server.alerting().actions.delete(savedObjectsClient, request.params.id); + }, + }); +} diff --git a/x-pack/plugins/alerting/server/routes/find_action.ts b/x-pack/plugins/alerting/server/routes/find_action.ts new file mode 100644 index 0000000000000..dabc596d9836c --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/find_action.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Hapi from 'hapi'; + +import { APP_ID } from '../../common/constants'; +import { ActionService } from '../action_service'; +import { AlertService } from '../alert_service'; +import { ConnectorService } from '../connector_service'; + +interface Server extends Hapi.Server { + alerting: () => { + actions: ActionService; + alerts: AlertService; + connectors: ConnectorService; + }; +} + +interface FindActionRequest extends Hapi.Request { + server: Server; + params: {}; +} + +export function findActionRoute(server: any) { + server.route({ + method: 'GET', + path: `/api/${APP_ID}/action`, + async handler(request: FindActionRequest) { + const savedObjectsClient = request.getSavedObjectsClient(); + return await request.server.alerting().actions.find(savedObjectsClient, request.params); + }, + }); +} diff --git a/x-pack/plugins/alerting/server/routes/get_action.ts b/x-pack/plugins/alerting/server/routes/get_action.ts new file mode 100644 index 0000000000000..93b47aa1cac11 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/get_action.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Hapi from 'hapi'; + +import { APP_ID } from '../../common/constants'; +import { ActionService } from '../action_service'; +import { AlertService } from '../alert_service'; +import { ConnectorService } from '../connector_service'; + +interface Server extends Hapi.Server { + alerting: () => { + actions: ActionService; + alerts: AlertService; + connectors: ConnectorService; + }; +} + +interface GetActionRequest extends Hapi.Request { + server: Server; + params: { + id: string; + }; +} + +export function getActionRoute(server: Hapi.Server) { + server.route({ + method: 'GET', + path: `/api/${APP_ID}/action/{id}`, + async handler(request: GetActionRequest) { + const savedObjectsClient = request.getSavedObjectsClient(); + return await request.server.alerting().actions.get(savedObjectsClient, request.params.id); + }, + }); +} diff --git a/x-pack/plugins/alerting/server/routes/index.ts b/x-pack/plugins/alerting/server/routes/index.ts index 8598dbdf962bf..78105877016f1 100644 --- a/x-pack/plugins/alerting/server/routes/index.ts +++ b/x-pack/plugins/alerting/server/routes/index.ts @@ -5,3 +5,7 @@ */ export { createActionRoute } from './create_action'; +export { deleteActionRoute } from './delete_action'; +export { findActionRoute } from './find_action'; +export { getActionRoute } from './get_action'; +export { updateActionRoute } from './update_action'; diff --git a/x-pack/plugins/alerting/server/routes/types.ts b/x-pack/plugins/alerting/server/routes/types.ts new file mode 100644 index 0000000000000..062044b907f2b --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/types.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export type WithoutQueryAndParams = Pick>; diff --git a/x-pack/plugins/alerting/server/routes/update_action.ts b/x-pack/plugins/alerting/server/routes/update_action.ts new file mode 100644 index 0000000000000..7a7b0b68be606 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/update_action.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Joi from 'joi'; +import Hapi from 'hapi'; + +import { APP_ID } from '../../common/constants'; +import { ActionService } from '../action_service'; +import { AlertService } from '../alert_service'; +import { ConnectorService } from '../connector_service'; + +interface Server extends Hapi.Server { + alerting: () => { + actions: ActionService; + alerts: AlertService; + connectors: ConnectorService; + }; +} + +interface UpdateActionRequest extends Hapi.Request { + server: Server; + payload: { + description: string; + connectorId: string; + connectorOptions: { [key: string]: any }; + }; +} + +export function updateActionRoute(server: Hapi.Server) { + server.route({ + method: 'PUT', + path: `/api/${APP_ID}/action/{id}`, + options: { + validate: { + payload: Joi.object() + .keys({ + description: Joi.string().required(), + connectorId: Joi.string().required(), + connectorOptions: Joi.object(), + }) + .required(), + }, + }, + async handler(request: UpdateActionRequest) { + const savedObjectsClient = request.getSavedObjectsClient(); + return await request.server + .alerting() + .actions.update(savedObjectsClient, request.params.id, request.payload); + }, + }); +} diff --git a/x-pack/test/api_integration/apis/alerting/create_action.ts b/x-pack/test/api_integration/apis/alerting/create_action.ts index 05c93570aa65b..567e803da5da4 100644 --- a/x-pack/test/api_integration/apis/alerting/create_action.ts +++ b/x-pack/test/api_integration/apis/alerting/create_action.ts @@ -26,14 +26,13 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe ]); }); - it('should return 200 when creating an action', async () => { + it('should return 200 when creating an action with provided id', async () => { await supertest - .post('/api/alerting/action') + .post('/api/alerting/action/my-action') .set('kbn-xsrf', 'foo') .send({ - id: 'my-action', description: 'My action', - connectorId: 'console', + connectorId: 'log', connectorOptions: { username: 'username', }, @@ -45,7 +44,7 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe id: 'my-action', attributes: { description: 'My action', - connectorId: 'console', + connectorId: 'log', connectorOptions: { username: 'username' }, }, references: [], @@ -55,26 +54,53 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe }); }); - it('should return 409 when action already exists', async () => { + it('should return 200 when creating an action without id provided', async () => { await supertest .post('/api/alerting/action') .set('kbn-xsrf', 'foo') .send({ - id: 'my-action-to-duplicate', + description: 'My action', + connectorId: 'log', + connectorOptions: { + username: 'username', + }, + }) + .expect(200) + .then((resp: any) => { + expect(resp.body).to.eql({ + type: 'action', + id: resp.body.id, + attributes: { + description: 'My action', + connectorId: 'log', + connectorOptions: { username: 'username' }, + }, + references: [], + updated_at: resp.body.updated_at, + version: resp.body.version, + }); + expect(typeof resp.body.id).to.be('string'); + }); + }); + + it('should return 409 when action already exists', async () => { + await supertest + .post('/api/alerting/action/my-action-to-duplicate') + .set('kbn-xsrf', 'foo') + .send({ description: 'My action to duplicate', - connectorId: 'console', + connectorId: 'log', connectorOptions: { username: 'username', }, }) .expect(200); await supertest - .post('/api/alerting/action') + .post('/api/alerting/action/my-action-to-duplicate') .set('kbn-xsrf', 'foo') .send({ - id: 'my-action-to-duplicate', description: 'My action to duplicate', - connectorId: 'console', + connectorId: 'log', connectorOptions: { username: 'username', }, @@ -84,10 +110,9 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe it(`should return 400 when connector isn't registered`, async () => { await supertest - .post('/api/alerting/action') + .post('/api/alerting/action/my-action-without-connector') .set('kbn-xsrf', 'foo') .send({ - id: 'my-action-without-connector', description: 'My action', connectorId: 'unregistered-connector', connectorOptions: { diff --git a/x-pack/test/api_integration/apis/alerting/delete_action.ts b/x-pack/test/api_integration/apis/alerting/delete_action.ts new file mode 100644 index 0000000000000..59552f1472c32 --- /dev/null +++ b/x-pack/test/api_integration/apis/alerting/delete_action.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function deleteActionTests({ getService }: KibanaFunctionalTestDefaultProviders) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('delete_action', () => { + beforeEach(() => esArchiver.load('alerting/basic')); + afterEach(() => esArchiver.unload('alerting/basic')); + + it('should return 200 when deleting an action', async () => { + await supertest + .delete('/api/alerting/action/1') + .set('kbn-xsrf', 'foo') + .expect(200) + .then((resp: any) => { + expect(resp.body).to.eql({}); + }); + }); + + it(`should return 404 when action doesn't exist`, async () => { + await supertest + .delete('/api/alerting/action/2') + .set('kbn-xsrf', 'foo') + .expect(404) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Saved object [action/2] not found', + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/alerting/find_action.ts b/x-pack/test/api_integration/apis/alerting/find_action.ts new file mode 100644 index 0000000000000..04cae01e8d13b --- /dev/null +++ b/x-pack/test/api_integration/apis/alerting/find_action.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function findActionTests({ getService }: KibanaFunctionalTestDefaultProviders) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('find_action', () => { + before(() => esArchiver.load('alerting/basic')); + after(() => esArchiver.unload('alerting/basic')); + + it('should return results', async () => { + await supertest + .get('/api/alerting/action') + .expect(200) + .then((resp: any) => { + expect(resp.body).to.eql({ + page: 1, + per_page: 20, + total: 1, + saved_objects: [ + { + id: '1', + type: 'action', + version: resp.body.saved_objects[0].version, + references: [], + attributes: { + connectorId: 'log', + description: 'My description', + connectorOptions: { + bar: false, + foo: true, + }, + }, + }, + ], + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/alerting/get_action.ts b/x-pack/test/api_integration/apis/alerting/get_action.ts new file mode 100644 index 0000000000000..8ce262b44c3a8 --- /dev/null +++ b/x-pack/test/api_integration/apis/alerting/get_action.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function getActionTests({ getService }: KibanaFunctionalTestDefaultProviders) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('get_action', () => { + before(() => esArchiver.load('alerting/basic')); + after(() => esArchiver.unload('alerting/basic')); + + it('should return 200 when finding a record', async () => { + await supertest + .get('/api/alerting/action/1') + .expect(200) + .then((resp: any) => { + expect(resp.body).to.eql({ + id: '1', + type: 'action', + references: [], + version: resp.body.version, + attributes: { + connectorId: 'log', + description: 'My description', + connectorOptions: { + bar: false, + foo: true, + }, + }, + }); + }); + }); + + it('should return 404 when not finding a record', async () => { + await supertest + .get('/api/alerting/action/2') + .expect(404) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Saved object [action/2] not found', + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/alerting/index.ts b/x-pack/test/api_integration/apis/alerting/index.ts index 9a17dbd9b8ba1..ced4722dc3874 100644 --- a/x-pack/test/api_integration/apis/alerting/index.ts +++ b/x-pack/test/api_integration/apis/alerting/index.ts @@ -10,5 +10,9 @@ import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; export default function alertingTests({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { describe('Alerting', () => { loadTestFile(require.resolve('./create_action')); + loadTestFile(require.resolve('./delete_action')); + loadTestFile(require.resolve('./find_action')); + loadTestFile(require.resolve('./get_action')); + loadTestFile(require.resolve('./update_action')); }); } diff --git a/x-pack/test/api_integration/apis/alerting/update_action.ts b/x-pack/test/api_integration/apis/alerting/update_action.ts new file mode 100644 index 0000000000000..2f587fb6f5d4e --- /dev/null +++ b/x-pack/test/api_integration/apis/alerting/update_action.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function updateActionTests({ getService }: KibanaFunctionalTestDefaultProviders) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('update_action', () => { + beforeEach(() => esArchiver.load('alerting/basic')); + afterEach(() => esArchiver.unload('alerting/basic')); + + it('should return 200 when updating a document', async () => { + await supertest + .put('/api/alerting/action/1') + .set('kbn-xsrf', 'foo') + .send({ + connectorId: 'log', + description: 'My description updated', + connectorOptions: { + bar: true, + foo: false, + }, + }) + .expect(200) + .then((resp: any) => { + expect(resp.body).to.eql({ + id: '1', + type: 'action', + references: [], + version: resp.body.version, + updated_at: resp.body.updated_at, + attributes: { + connectorId: 'log', + description: 'My description updated', + connectorOptions: { + bar: true, + foo: false, + }, + }, + }); + }); + }); + + it('should return 404 when updating a non existing document', async () => { + await supertest + .put('/api/alerting/action/2') + .set('kbn-xsrf', 'foo') + .send({ + connectorId: 'log', + description: 'My description updated', + connectorOptions: { + bar: true, + foo: false, + }, + }) + .expect(404) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Saved object [action/2] not found', + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/es_archives/alerting/basic/data.json b/x-pack/test/functional/es_archives/alerting/basic/data.json new file mode 100644 index 0000000000000..e609c2005a6c1 --- /dev/null +++ b/x-pack/test/functional/es_archives/alerting/basic/data.json @@ -0,0 +1,18 @@ +{ + "value": { + "id": "action:1", + "index": ".kibana", + "source": { + "action": { + "description": "My description", + "connectorId": "log", + "connectorOptions": { + "foo": true, + "bar": false + } + }, + "type": "action", + "migrationVersion": {} + } + } +} diff --git a/x-pack/test/functional/es_archives/alerting/basic/mappings.json b/x-pack/test/functional/es_archives/alerting/basic/mappings.json new file mode 100644 index 0000000000000..e8c6d3f32ced9 --- /dev/null +++ b/x-pack/test/functional/es_archives/alerting/basic/mappings.json @@ -0,0 +1,97 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "settings": { + "index": { + "number_of_shards": "1", + "auto_expand_replicas": "0-1", + "number_of_replicas": "0" + } + }, + "mappings": { + "doc": { + "dynamic": "strict", + "properties": { + "spaceId": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "index-pattern": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "space": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "action": { + "properties": { + "description": { + "type": "text" + }, + "connectorId": { + "type": "keyword" + }, + "connectorOptions": { + "dynamic": "true", + "type": "object" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "initials": { + "type": "keyword" + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + } + } + } + } + } +} From d393e46f779e5e5e14d4a96c2a9676fd96ee6c66 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Fri, 3 May 2019 10:20:59 -0400 Subject: [PATCH 14/51] Add list connectors API --- x-pack/plugins/alerting/index.ts | 2 + .../alerting/server/action_service.test.ts | 10 +++- .../alerting/server/connector_service.test.ts | 58 +++++++++++++++++-- .../alerting/server/connector_service.ts | 9 +++ .../server/default_connectors/email.ts | 1 + .../alerting/server/default_connectors/log.ts | 1 + .../default_connectors/message_slack.ts | 1 + x-pack/plugins/alerting/server/index.ts | 1 + .../plugins/alerting/server/routes/index.ts | 1 + .../alerting/server/routes/list_connectors.ts | 34 +++++++++++ .../api_integration/apis/alerting/index.ts | 1 + .../apis/alerting/list_connectors.ts | 35 +++++++++++ 12 files changed, 148 insertions(+), 6 deletions(-) create mode 100644 x-pack/plugins/alerting/server/routes/list_connectors.ts create mode 100644 x-pack/test/api_integration/apis/alerting/list_connectors.ts diff --git a/x-pack/plugins/alerting/index.ts b/x-pack/plugins/alerting/index.ts index 4e8d05e719e0a..bb2f76137bd66 100644 --- a/x-pack/plugins/alerting/index.ts +++ b/x-pack/plugins/alerting/index.ts @@ -13,6 +13,7 @@ import { findActionRoute, getActionRoute, updateActionRoute, + listconnectorsRoute, AlertService, ActionService, ConnectorService, @@ -55,6 +56,7 @@ export function alerting(kibana: any) { getActionRoute(server); findActionRoute(server); updateActionRoute(server); + listconnectorsRoute(server); // Register service to server server.decorate('server', 'alerting', () => ({ diff --git a/x-pack/plugins/alerting/server/action_service.test.ts b/x-pack/plugins/alerting/server/action_service.test.ts index f4c88108c093e..03ea7ae705c55 100644 --- a/x-pack/plugins/alerting/server/action_service.test.ts +++ b/x-pack/plugins/alerting/server/action_service.test.ts @@ -27,6 +27,7 @@ describe('create()', () => { const connectorService = new ConnectorService(); connectorService.register({ id: 'my-connector', + name: 'My connector', async executor() {}, }); const actionService = new ActionService(connectorService); @@ -71,6 +72,7 @@ describe('create()', () => { const actionService = new ActionService(connectorService); connectorService.register({ id: 'my-connector', + name: 'My connector', validate: { connectorOptions: Joi.object() .keys({ @@ -202,6 +204,7 @@ describe('update()', () => { const connectorService = new ConnectorService(); connectorService.register({ id: 'my-connector', + name: 'My connector', async executor() {}, }); const actionService = new ActionService(connectorService); @@ -240,6 +243,7 @@ describe('update()', () => { const actionService = new ActionService(connectorService); connectorService.register({ id: 'my-connector', + name: 'My connector', validate: { connectorOptions: Joi.object() .keys({ @@ -280,7 +284,11 @@ describe('fire()', () => { const connectorService = new ConnectorService(); const actionService = new ActionService(connectorService); const mockConnector = jest.fn().mockResolvedValueOnce({ success: true }); - connectorService.register({ id: 'mock', executor: mockConnector }); + connectorService.register({ + id: 'mock', + name: 'Mock', + executor: mockConnector, + }); savedObjectsClient.get.mockResolvedValueOnce({ id: 'mock-action', attributes: { diff --git a/x-pack/plugins/alerting/server/connector_service.test.ts b/x-pack/plugins/alerting/server/connector_service.test.ts index 7a0ebca00a74f..f58ae0241e9e3 100644 --- a/x-pack/plugins/alerting/server/connector_service.test.ts +++ b/x-pack/plugins/alerting/server/connector_service.test.ts @@ -11,15 +11,27 @@ describe('register()', () => { test('able to register connectors', () => { const executor = jest.fn(); const connectorService = new ConnectorService(); - connectorService.register({ id: 'my-connector', executor }); + connectorService.register({ + id: 'my-connector', + name: 'My connector', + executor, + }); }); test('throws error if connector already registered', () => { const executor = jest.fn(); const connectorService = new ConnectorService(); - connectorService.register({ id: 'my-connector', executor }); + connectorService.register({ + id: 'my-connector', + name: 'My connector', + executor, + }); expect(() => - connectorService.register({ id: 'my-connector', executor }) + connectorService.register({ + id: 'my-connector', + name: 'My connector', + executor, + }) ).toThrowErrorMatchingInlineSnapshot(`"Connector \\"my-connector\\" is already registered."`); }); }); @@ -29,6 +41,7 @@ describe('get()', () => { const connectorService = new ConnectorService(); connectorService.register({ id: 'my-connector', + name: 'My connector', async executor() {}, }); const connector = connectorService.get('my-connector'); @@ -36,6 +49,7 @@ describe('get()', () => { Object { "executor": [Function], "id": "my-connector", + "name": "My connector", } `); }); @@ -48,11 +62,30 @@ Object { }); }); +describe('list()', () => { + test('returns list of connectors', () => { + const connectorService = new ConnectorService(); + connectorService.register({ + id: 'my-connector', + name: 'My connector', + async executor() {}, + }); + const connectors = connectorService.list(); + expect(connectors).toEqual([ + { + id: 'my-connector', + name: 'My connector', + }, + ]); + }); +}); + describe('validateParams()', () => { test('should pass when validation not defined', () => { const connectorService = new ConnectorService(); connectorService.register({ id: 'my-connector', + name: 'My connector', async executor() {}, }); connectorService.validateParams('my-connector', {}); @@ -62,6 +95,7 @@ describe('validateParams()', () => { const connectorService = new ConnectorService(); connectorService.register({ id: 'my-connector', + name: 'My connector', validate: { params: Joi.object() .keys({ @@ -78,6 +112,7 @@ describe('validateParams()', () => { const connectorService = new ConnectorService(); connectorService.register({ id: 'my-connector', + name: 'My connector', validate: { params: Joi.object() .keys({ @@ -100,6 +135,7 @@ describe('validateConnectorOptions()', () => { const connectorService = new ConnectorService(); connectorService.register({ id: 'my-connector', + name: 'My connector', async executor() {}, }); connectorService.validateConnectorOptions('my-connector', {}); @@ -109,6 +145,7 @@ describe('validateConnectorOptions()', () => { const connectorService = new ConnectorService(); connectorService.register({ id: 'my-connector', + name: 'My connector', validate: { connectorOptions: Joi.object() .keys({ @@ -125,6 +162,7 @@ describe('validateConnectorOptions()', () => { const connectorService = new ConnectorService(); connectorService.register({ id: 'my-connector', + name: 'My connector', validate: { connectorOptions: Joi.object() .keys({ @@ -151,7 +189,11 @@ describe('has()', () => { test('returns true after registering a connector', () => { const executor = jest.fn(); const connectorService = new ConnectorService(); - connectorService.register({ id: 'my-connector', executor }); + connectorService.register({ + id: 'my-connector', + name: 'My connector', + executor, + }); expect(connectorService.has('my-connector')); }); }); @@ -160,7 +202,11 @@ describe('execute()', () => { test('calls the executor with proper params', async () => { const executor = jest.fn().mockResolvedValueOnce({ success: true }); const connectorService = new ConnectorService(); - connectorService.register({ id: 'my-connector', executor }); + connectorService.register({ + id: 'my-connector', + name: 'My connector', + executor, + }); await connectorService.execute('my-connector', { foo: true }, { bar: false }); expect(executor).toMatchInlineSnapshot(` [MockFunction] { @@ -189,6 +235,7 @@ describe('execute()', () => { const connectorService = new ConnectorService(); connectorService.register({ id: 'my-connector', + name: 'My connector', executor, validate: { params: Joi.object() @@ -210,6 +257,7 @@ describe('execute()', () => { const connectorService = new ConnectorService(); connectorService.register({ id: 'my-connector', + name: 'My connector', executor, validate: { connectorOptions: Joi.object() diff --git a/x-pack/plugins/alerting/server/connector_service.ts b/x-pack/plugins/alerting/server/connector_service.ts index 12b6720defa0c..78aa07fe68700 100644 --- a/x-pack/plugins/alerting/server/connector_service.ts +++ b/x-pack/plugins/alerting/server/connector_service.ts @@ -8,6 +8,7 @@ import Boom from 'boom'; interface Connector { id: string; + name: string; validate?: { params?: any; connectorOptions?: any; @@ -37,6 +38,14 @@ export class ConnectorService { return connector; } + public list() { + const connectorIds = Object.keys(this.connectors); + return connectorIds.map(id => ({ + id, + name: this.connectors[id].name, + })); + } + public validateParams(id: string, params: any) { const connector = this.get(id); const validator = connector.validate && connector.validate.params; diff --git a/x-pack/plugins/alerting/server/default_connectors/email.ts b/x-pack/plugins/alerting/server/default_connectors/email.ts index d5197bba73d78..4a35d1a16fb91 100644 --- a/x-pack/plugins/alerting/server/default_connectors/email.ts +++ b/x-pack/plugins/alerting/server/default_connectors/email.ts @@ -8,6 +8,7 @@ import nodemailer, { SendMailOptions } from 'nodemailer'; export const emailConnector = { id: 'email', + name: 'E-mail', async executor(connectorOptions: any, params: SendMailOptions) { const transporter = nodemailer.createTransport(connectorOptions); await transporter.sendMail(params); diff --git a/x-pack/plugins/alerting/server/default_connectors/log.ts b/x-pack/plugins/alerting/server/default_connectors/log.ts index a5de43be4ca15..19cdc7e6799cf 100644 --- a/x-pack/plugins/alerting/server/default_connectors/log.ts +++ b/x-pack/plugins/alerting/server/default_connectors/log.ts @@ -12,6 +12,7 @@ interface LogParams { export const logConnector = { id: 'log', + name: 'Log', validate: { params: Joi.object() .keys({ diff --git a/x-pack/plugins/alerting/server/default_connectors/message_slack.ts b/x-pack/plugins/alerting/server/default_connectors/message_slack.ts index 51d9598a4335d..fc907d8e4fd2d 100644 --- a/x-pack/plugins/alerting/server/default_connectors/message_slack.ts +++ b/x-pack/plugins/alerting/server/default_connectors/message_slack.ts @@ -18,6 +18,7 @@ interface MessageSlackParams { export const messageSlackConnector = { id: 'message-slack', + name: 'Message slack', validate: { params: Joi.object() .keys({ diff --git a/x-pack/plugins/alerting/server/index.ts b/x-pack/plugins/alerting/server/index.ts index 1e65476dc9d8e..e4235e0d02c07 100644 --- a/x-pack/plugins/alerting/server/index.ts +++ b/x-pack/plugins/alerting/server/index.ts @@ -10,6 +10,7 @@ export { findActionRoute, getActionRoute, updateActionRoute, + listconnectorsRoute, } from './routes'; export { ActionService } from './action_service'; export { AlertService } from './alert_service'; diff --git a/x-pack/plugins/alerting/server/routes/index.ts b/x-pack/plugins/alerting/server/routes/index.ts index 78105877016f1..fccbfceb848e8 100644 --- a/x-pack/plugins/alerting/server/routes/index.ts +++ b/x-pack/plugins/alerting/server/routes/index.ts @@ -9,3 +9,4 @@ export { deleteActionRoute } from './delete_action'; export { findActionRoute } from './find_action'; export { getActionRoute } from './get_action'; export { updateActionRoute } from './update_action'; +export { listconnectorsRoute } from './list_connectors'; diff --git a/x-pack/plugins/alerting/server/routes/list_connectors.ts b/x-pack/plugins/alerting/server/routes/list_connectors.ts new file mode 100644 index 0000000000000..f1246586e1fe4 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/list_connectors.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Hapi from 'hapi'; + +import { APP_ID } from '../../common/constants'; +import { ActionService } from '../action_service'; +import { AlertService } from '../alert_service'; +import { ConnectorService } from '../connector_service'; + +interface Server extends Hapi.Server { + alerting: () => { + actions: ActionService; + alerts: AlertService; + connectors: ConnectorService; + }; +} + +interface ListConnectorRequest extends Hapi.Request { + server: Server; +} + +export function listconnectorsRoute(server: any) { + server.route({ + method: 'GET', + path: `/api/${APP_ID}/connectors`, + async handler(request: ListConnectorRequest) { + return request.server.alerting().connectors.list(); + }, + }); +} diff --git a/x-pack/test/api_integration/apis/alerting/index.ts b/x-pack/test/api_integration/apis/alerting/index.ts index ced4722dc3874..93909e824bc20 100644 --- a/x-pack/test/api_integration/apis/alerting/index.ts +++ b/x-pack/test/api_integration/apis/alerting/index.ts @@ -13,6 +13,7 @@ export default function alertingTests({ loadTestFile }: KibanaFunctionalTestDefa loadTestFile(require.resolve('./delete_action')); loadTestFile(require.resolve('./find_action')); loadTestFile(require.resolve('./get_action')); + loadTestFile(require.resolve('./list_connectors')); loadTestFile(require.resolve('./update_action')); }); } diff --git a/x-pack/test/api_integration/apis/alerting/list_connectors.ts b/x-pack/test/api_integration/apis/alerting/list_connectors.ts new file mode 100644 index 0000000000000..7d7cabfefeebe --- /dev/null +++ b/x-pack/test/api_integration/apis/alerting/list_connectors.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function listConnectorTests({ getService }: KibanaFunctionalTestDefaultProviders) { + const supertest = getService('supertest'); + + describe('list_connectors', () => { + it('should return 200 with list of connectors containing defaults', async () => { + await supertest + .get('/api/alerting/connectors') + .expect(200) + .then((resp: any) => { + function createConnectorMatcher(id: string, name: string) { + return (connector: { id: string; name: string }) => { + return connector.id === id && connector.name === name; + }; + } + // Check for values explicitly in order to avoid this test failing each time plugins register + // a new connector + expect(resp.body.some(createConnectorMatcher('log', 'Log'))).to.be(true); + expect(resp.body.some(createConnectorMatcher('message-slack', 'Message slack'))).to.be( + true + ); + expect(resp.body.some(createConnectorMatcher('email', 'E-mail'))); + }); + }); + }); +} From 488f7ddf49aec003d4f9df6992cd72deb6558f65 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Fri, 3 May 2019 11:26:38 -0400 Subject: [PATCH 15/51] Change actions CRUD APIs to be closer with saved objects structure --- .../alerting/server/action_service.test.ts | 52 ++++++++---- .../plugins/alerting/server/action_service.ts | 37 +++++++-- .../alerting/server/routes/create_action.ts | 52 +++++++++--- .../alerting/server/routes/delete_action.ts | 13 ++- .../alerting/server/routes/find_action.ts | 65 ++++++++++++++- .../alerting/server/routes/get_action.ts | 13 ++- .../plugins/alerting/server/routes/types.ts | 6 ++ .../alerting/server/routes/update_action.ts | 41 ++++++++-- .../apis/alerting/create_action.ts | 80 ++++++++++++++----- .../apis/alerting/find_action.ts | 9 +-- .../apis/alerting/update_action.ts | 24 +++--- 11 files changed, 311 insertions(+), 81 deletions(-) diff --git a/x-pack/plugins/alerting/server/action_service.test.ts b/x-pack/plugins/alerting/server/action_service.test.ts index 03ea7ae705c55..8db1db221ffd1 100644 --- a/x-pack/plugins/alerting/server/action_service.test.ts +++ b/x-pack/plugins/alerting/server/action_service.test.ts @@ -39,7 +39,10 @@ describe('create()', () => { connectorId: 'my-connector', connectorOptions: {}, }, - { id: 'my-alert' } + { + id: 'my-alert', + overwrite: true, + } ); expect(result).toEqual(expectedResult); expect(savedObjectsClient.create).toMatchInlineSnapshot(` @@ -54,6 +57,7 @@ describe('create()', () => { }, Object { "id": "my-alert", + "overwrite": true, }, ], ], @@ -209,11 +213,16 @@ describe('update()', () => { }); const actionService = new ActionService(connectorService); savedObjectsClient.update.mockResolvedValueOnce(expectedResult); - const result = await actionService.update(savedObjectsClient, 'my-alert', { - description: 'my description', - connectorId: 'my-connector', - connectorOptions: {}, - }); + const result = await actionService.update( + savedObjectsClient, + 'my-alert', + { + description: 'my description', + connectorId: 'my-connector', + connectorOptions: {}, + }, + {} + ); expect(result).toEqual(expectedResult); expect(savedObjectsClient.update).toMatchInlineSnapshot(` [MockFunction] { @@ -226,6 +235,7 @@ describe('update()', () => { "connectorOptions": Object {}, "description": "my description", }, + Object {}, ], ], "results": Array [ @@ -254,11 +264,16 @@ describe('update()', () => { async executor() {}, }); await expect( - actionService.update(savedObjectsClient, 'my-alert', { - description: 'my description', - connectorId: 'my-connector', - connectorOptions: {}, - }) + actionService.update( + savedObjectsClient, + 'my-alert', + { + description: 'my description', + connectorId: 'my-connector', + connectorOptions: {}, + }, + {} + ) ).rejects.toThrowErrorMatchingInlineSnapshot( `"child \\"param1\\" fails because [\\"param1\\" is required]"` ); @@ -268,11 +283,16 @@ describe('update()', () => { const connectorService = new ConnectorService(); const actionService = new ActionService(connectorService); await expect( - actionService.update(savedObjectsClient, 'my-alert', { - description: 'my description', - connectorId: 'unregistered-connector', - connectorOptions: {}, - }) + actionService.update( + savedObjectsClient, + 'my-alert', + { + description: 'my description', + connectorId: 'unregistered-connector', + connectorOptions: {}, + }, + {} + ) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Connector \\"unregistered-connector\\" is not registered."` ); diff --git a/x-pack/plugins/alerting/server/action_service.ts b/x-pack/plugins/alerting/server/action_service.ts index e8d3f32c08520..b5459db0fd276 100644 --- a/x-pack/plugins/alerting/server/action_service.ts +++ b/x-pack/plugins/alerting/server/action_service.ts @@ -20,6 +20,26 @@ interface FireActionOptions { savedObjectsClient: SavedObjectsClient; } +export interface SavedObjectReference { + name: string; + type: string; + id: string; +} + +interface FindOptions { + perPage?: number; + page?: number; + search?: string; + defaultSearchOperator?: 'AND' | 'OR'; + searchFields?: string[]; + sortField?: string; + hasReference?: { + type: string; + id: string; + }; + fields?: string[]; +} + export class ActionService { private connectorService: ConnectorService; @@ -30,23 +50,23 @@ export class ActionService { public async create( savedObjectsClient: SavedObjectsClient, data: Action, - { id }: { id?: string } = {} + { id, overwrite }: { id?: string; overwrite?: boolean } = {} ) { const { connectorId } = data; if (!this.connectorService.has(connectorId)) { throw Boom.badRequest(`Connector "${connectorId}" is not registered.`); } this.connectorService.validateConnectorOptions(connectorId, data.connectorOptions); - return await savedObjectsClient.create('action', data, { id }); + return await savedObjectsClient.create('action', data, { id, overwrite }); } public async get(savedObjectsClient: SavedObjectsClient, id: string) { return await savedObjectsClient.get('action', id); } - public async find(savedObjectsClient: SavedObjectsClient, params: {}) { + public async find(savedObjectsClient: SavedObjectsClient, options: FindOptions) { return await savedObjectsClient.find({ - ...params, + ...options, type: 'action', }); } @@ -55,13 +75,18 @@ export class ActionService { return await savedObjectsClient.delete('action', id); } - public async update(savedObjectsClient: SavedObjectsClient, id: string, data: Action) { + public async update( + savedObjectsClient: SavedObjectsClient, + id: string, + data: Action, + options: { version?: string; references?: SavedObjectReference[] } + ) { const { connectorId } = data; if (!this.connectorService.has(connectorId)) { throw Boom.badRequest(`Connector "${connectorId}" is not registered.`); } this.connectorService.validateConnectorOptions(connectorId, data.connectorOptions); - return await savedObjectsClient.update('action', id, data); + return await savedObjectsClient.update('action', id, data, options); } public async fire({ id, params, savedObjectsClient }: FireActionOptions) { diff --git a/x-pack/plugins/alerting/server/routes/create_action.ts b/x-pack/plugins/alerting/server/routes/create_action.ts index 1858fa1daf954..4417ff693f073 100644 --- a/x-pack/plugins/alerting/server/routes/create_action.ts +++ b/x-pack/plugins/alerting/server/routes/create_action.ts @@ -10,7 +10,7 @@ import { ActionService } from '../action_service'; import { AlertService } from '../alert_service'; import { ConnectorService } from '../connector_service'; import { APP_ID } from '../../common/constants'; -import { WithoutQueryAndParams } from './types'; +import { WithoutQueryAndParams, SavedObjectReference } from './types'; interface Server extends Hapi.Server { alerting: () => { @@ -22,14 +22,21 @@ interface Server extends Hapi.Server { interface CreateActionRequest extends WithoutQueryAndParams { server: Server; - payload: { - description: string; - connectorId: string; - connectorOptions: { [key: string]: any }; + query: { + overwrite: boolean; }; params: { id?: string; }; + payload: { + attributes: { + description: string; + connectorId: string; + connectorOptions: { [key: string]: any }; + }; + migrationVersion?: { [key: string]: string }; + references: SavedObjectReference[]; + }; } export function createActionRoute(server: Hapi.Server) { @@ -38,10 +45,34 @@ export function createActionRoute(server: Hapi.Server) { path: `/api/${APP_ID}/action/{id?}`, options: { validate: { + query: Joi.object() + .keys({ + overwrite: Joi.boolean().default(false), + }) + .default(), + params: Joi.object() + .keys({ + id: Joi.string(), + }) + .required(), payload: Joi.object().keys({ - description: Joi.string().required(), - connectorId: Joi.string().required(), - connectorOptions: Joi.object(), + attributes: Joi.object() + .keys({ + description: Joi.string().required(), + connectorId: Joi.string().required(), + connectorOptions: Joi.object(), + }) + .required(), + migrationVersion: Joi.object().optional(), + references: Joi.array() + .items( + Joi.object().keys({ + name: Joi.string().required(), + type: Joi.string().required(), + id: Joi.string().required(), + }) + ) + .default([]), }), }, }, @@ -49,7 +80,10 @@ export function createActionRoute(server: Hapi.Server) { const savedObjectsClient = request.getSavedObjectsClient(); return await request.server .alerting() - .actions.create(savedObjectsClient, request.payload, { id: request.params.id }); + .actions.create(savedObjectsClient, request.payload.attributes, { + id: request.params.id, + overwrite: request.query.overwrite, + }); }, }); } diff --git a/x-pack/plugins/alerting/server/routes/delete_action.ts b/x-pack/plugins/alerting/server/routes/delete_action.ts index 6e86784530e72..dc08b0fb20487 100644 --- a/x-pack/plugins/alerting/server/routes/delete_action.ts +++ b/x-pack/plugins/alerting/server/routes/delete_action.ts @@ -5,6 +5,7 @@ */ import Hapi from 'hapi'; +import Joi from 'joi'; import { APP_ID } from '../../common/constants'; import { ActionService } from '../action_service'; @@ -30,9 +31,19 @@ export function deleteActionRoute(server: Hapi.Server) { server.route({ method: 'DELETE', path: `/api/${APP_ID}/action/{id}`, + options: { + validate: { + params: Joi.object() + .keys({ + id: Joi.string().required(), + }) + .required(), + }, + }, async handler(request: DeleteActionRequest) { + const { id } = request.params; const savedObjectsClient = request.getSavedObjectsClient(); - return await request.server.alerting().actions.delete(savedObjectsClient, request.params.id); + return await request.server.alerting().actions.delete(savedObjectsClient, id); }, }); } diff --git a/x-pack/plugins/alerting/server/routes/find_action.ts b/x-pack/plugins/alerting/server/routes/find_action.ts index dabc596d9836c..99f5bf0eb16ba 100644 --- a/x-pack/plugins/alerting/server/routes/find_action.ts +++ b/x-pack/plugins/alerting/server/routes/find_action.ts @@ -4,12 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import Joi from 'joi'; import Hapi from 'hapi'; import { APP_ID } from '../../common/constants'; import { ActionService } from '../action_service'; import { AlertService } from '../alert_service'; import { ConnectorService } from '../connector_service'; +import { WithoutQueryAndParams } from './types'; interface Server extends Hapi.Server { alerting: () => { @@ -19,18 +21,73 @@ interface Server extends Hapi.Server { }; } -interface FindActionRequest extends Hapi.Request { +interface FindActionRequest extends WithoutQueryAndParams { server: Server; - params: {}; + query: { + per_page: number; + page: number; + search?: string; + default_search_operator: 'AND' | 'OR'; + search_fields?: string[]; + sort_field?: string; + has_reference?: { + type: string; + id: string; + }; + fields?: string[]; + }; } export function findActionRoute(server: any) { server.route({ method: 'GET', - path: `/api/${APP_ID}/action`, + path: `/api/${APP_ID}/action/_find`, + options: { + validate: { + query: Joi.object() + .keys({ + per_page: Joi.number() + .min(0) + .default(20), + page: Joi.number() + .min(0) + .default(1), + search: Joi.string() + .allow('') + .optional(), + default_search_operator: Joi.string() + .valid('OR', 'AND') + .default('OR'), + search_fields: Joi.array() + .items(Joi.string()) + .single(), + sort_field: Joi.string(), + has_reference: Joi.object() + .keys({ + type: Joi.string().required(), + id: Joi.string().required(), + }) + .optional(), + fields: Joi.array() + .items(Joi.string()) + .single(), + }) + .default(), + }, + }, async handler(request: FindActionRequest) { + const query = request.query; const savedObjectsClient = request.getSavedObjectsClient(); - return await request.server.alerting().actions.find(savedObjectsClient, request.params); + return await request.server.alerting().actions.find(savedObjectsClient, { + perPage: query.per_page, + page: query.page, + search: query.search, + defaultSearchOperator: query.default_search_operator, + searchFields: query.search_fields, + sortField: query.sort_field, + hasReference: query.has_reference, + fields: query.fields, + }); }, }); } diff --git a/x-pack/plugins/alerting/server/routes/get_action.ts b/x-pack/plugins/alerting/server/routes/get_action.ts index 93b47aa1cac11..0f366435a0416 100644 --- a/x-pack/plugins/alerting/server/routes/get_action.ts +++ b/x-pack/plugins/alerting/server/routes/get_action.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import Joi from 'joi'; import Hapi from 'hapi'; import { APP_ID } from '../../common/constants'; @@ -30,9 +31,19 @@ export function getActionRoute(server: Hapi.Server) { server.route({ method: 'GET', path: `/api/${APP_ID}/action/{id}`, + options: { + validate: { + params: Joi.object() + .keys({ + id: Joi.string().required(), + }) + .required(), + }, + }, async handler(request: GetActionRequest) { + const { id } = request.params; const savedObjectsClient = request.getSavedObjectsClient(); - return await request.server.alerting().actions.get(savedObjectsClient, request.params.id); + return await request.server.alerting().actions.get(savedObjectsClient, id); }, }); } diff --git a/x-pack/plugins/alerting/server/routes/types.ts b/x-pack/plugins/alerting/server/routes/types.ts index 062044b907f2b..f2933fe82e237 100644 --- a/x-pack/plugins/alerting/server/routes/types.ts +++ b/x-pack/plugins/alerting/server/routes/types.ts @@ -5,3 +5,9 @@ */ export type WithoutQueryAndParams = Pick>; + +export interface SavedObjectReference { + name: string; + type: string; + id: string; +} diff --git a/x-pack/plugins/alerting/server/routes/update_action.ts b/x-pack/plugins/alerting/server/routes/update_action.ts index 7a7b0b68be606..4e0e0a871edb4 100644 --- a/x-pack/plugins/alerting/server/routes/update_action.ts +++ b/x-pack/plugins/alerting/server/routes/update_action.ts @@ -11,6 +11,7 @@ import { APP_ID } from '../../common/constants'; import { ActionService } from '../action_service'; import { AlertService } from '../alert_service'; import { ConnectorService } from '../connector_service'; +import { SavedObjectReference } from './types'; interface Server extends Hapi.Server { alerting: () => { @@ -23,9 +24,13 @@ interface Server extends Hapi.Server { interface UpdateActionRequest extends Hapi.Request { server: Server; payload: { - description: string; - connectorId: string; - connectorOptions: { [key: string]: any }; + attributes: { + description: string; + connectorId: string; + connectorOptions: { [key: string]: any }; + }; + version?: string; + references: SavedObjectReference[]; }; } @@ -35,20 +40,42 @@ export function updateActionRoute(server: Hapi.Server) { path: `/api/${APP_ID}/action/{id}`, options: { validate: { + params: Joi.object() + .keys({ + id: Joi.string().required(), + }) + .required(), payload: Joi.object() .keys({ - description: Joi.string().required(), - connectorId: Joi.string().required(), - connectorOptions: Joi.object(), + attributes: Joi.object() + .keys({ + description: Joi.string().required(), + connectorId: Joi.string().required(), + connectorOptions: Joi.object(), + }) + .required(), + version: Joi.string(), + references: Joi.array() + .items( + Joi.object().keys({ + name: Joi.string().required(), + type: Joi.string().required(), + id: Joi.string().required(), + }) + ) + .default([]), }) .required(), }, }, async handler(request: UpdateActionRequest) { + const { id } = request.params; + const { attributes, version, references } = request.payload; + const options = { version, references }; const savedObjectsClient = request.getSavedObjectsClient(); return await request.server .alerting() - .actions.update(savedObjectsClient, request.params.id, request.payload); + .actions.update(savedObjectsClient, id, attributes, options); }, }); } diff --git a/x-pack/test/api_integration/apis/alerting/create_action.ts b/x-pack/test/api_integration/apis/alerting/create_action.ts index 567e803da5da4..2826d67eff742 100644 --- a/x-pack/test/api_integration/apis/alerting/create_action.ts +++ b/x-pack/test/api_integration/apis/alerting/create_action.ts @@ -23,6 +23,7 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe await Promise.all([ deleteObject('action', 'my-action'), deleteObject('action', 'my-action-to-duplicate'), + deleteObject('action', 'my-action-to-overwrite'), ]); }); @@ -31,10 +32,12 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe .post('/api/alerting/action/my-action') .set('kbn-xsrf', 'foo') .send({ - description: 'My action', - connectorId: 'log', - connectorOptions: { - username: 'username', + attributes: { + description: 'My action', + connectorId: 'log', + connectorOptions: { + username: 'username', + }, }, }) .expect(200) @@ -59,10 +62,12 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe .post('/api/alerting/action') .set('kbn-xsrf', 'foo') .send({ - description: 'My action', - connectorId: 'log', - connectorOptions: { - username: 'username', + attributes: { + description: 'My action', + connectorId: 'log', + connectorOptions: { + username: 'username', + }, }, }) .expect(200) @@ -88,10 +93,12 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe .post('/api/alerting/action/my-action-to-duplicate') .set('kbn-xsrf', 'foo') .send({ - description: 'My action to duplicate', - connectorId: 'log', - connectorOptions: { - username: 'username', + attributes: { + description: 'My action to duplicate', + connectorId: 'log', + connectorOptions: { + username: 'username', + }, }, }) .expect(200); @@ -99,24 +106,57 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe .post('/api/alerting/action/my-action-to-duplicate') .set('kbn-xsrf', 'foo') .send({ - description: 'My action to duplicate', - connectorId: 'log', - connectorOptions: { - username: 'username', + attributes: { + description: 'My action to duplicate', + connectorId: 'log', + connectorOptions: { + username: 'username', + }, }, }) .expect(409); }); + it('should return 200 when overwriting an action', async () => { + await supertest + .post('/api/alerting/action/my-action-to-overwrite') + .set('kbn-xsrf', 'foo') + .send({ + attributes: { + description: 'My action to duplicate', + connectorId: 'log', + connectorOptions: { + username: 'username', + }, + }, + }) + .expect(200); + await supertest + .post('/api/alerting/action/my-action-to-overwrite?overwrite=true') + .set('kbn-xsrf', 'foo') + .send({ + attributes: { + description: 'My action to overwrite', + connectorId: 'log', + connectorOptions: { + username: 'username', + }, + }, + }) + .expect(200); + }); + it(`should return 400 when connector isn't registered`, async () => { await supertest .post('/api/alerting/action/my-action-without-connector') .set('kbn-xsrf', 'foo') .send({ - description: 'My action', - connectorId: 'unregistered-connector', - connectorOptions: { - username: 'username', + attributes: { + description: 'My action', + connectorId: 'unregistered-connector', + connectorOptions: { + username: 'username', + }, }, }) .expect(400) diff --git a/x-pack/test/api_integration/apis/alerting/find_action.ts b/x-pack/test/api_integration/apis/alerting/find_action.ts index 04cae01e8d13b..578d37868b35c 100644 --- a/x-pack/test/api_integration/apis/alerting/find_action.ts +++ b/x-pack/test/api_integration/apis/alerting/find_action.ts @@ -16,9 +16,9 @@ export default function findActionTests({ getService }: KibanaFunctionalTestDefa before(() => esArchiver.load('alerting/basic')); after(() => esArchiver.unload('alerting/basic')); - it('should return results', async () => { + it('should return 200 with individual responses', async () => { await supertest - .get('/api/alerting/action') + .get('/api/alerting/action/_find?fields=description') .expect(200) .then((resp: any) => { expect(resp.body).to.eql({ @@ -32,12 +32,7 @@ export default function findActionTests({ getService }: KibanaFunctionalTestDefa version: resp.body.saved_objects[0].version, references: [], attributes: { - connectorId: 'log', description: 'My description', - connectorOptions: { - bar: false, - foo: true, - }, }, }, ], diff --git a/x-pack/test/api_integration/apis/alerting/update_action.ts b/x-pack/test/api_integration/apis/alerting/update_action.ts index 2f587fb6f5d4e..0115c472eff2e 100644 --- a/x-pack/test/api_integration/apis/alerting/update_action.ts +++ b/x-pack/test/api_integration/apis/alerting/update_action.ts @@ -21,11 +21,13 @@ export default function updateActionTests({ getService }: KibanaFunctionalTestDe .put('/api/alerting/action/1') .set('kbn-xsrf', 'foo') .send({ - connectorId: 'log', - description: 'My description updated', - connectorOptions: { - bar: true, - foo: false, + attributes: { + connectorId: 'log', + description: 'My description updated', + connectorOptions: { + bar: true, + foo: false, + }, }, }) .expect(200) @@ -53,11 +55,13 @@ export default function updateActionTests({ getService }: KibanaFunctionalTestDe .put('/api/alerting/action/2') .set('kbn-xsrf', 'foo') .send({ - connectorId: 'log', - description: 'My description updated', - connectorOptions: { - bar: true, - foo: false, + attributes: { + connectorId: 'log', + description: 'My description updated', + connectorOptions: { + bar: true, + foo: false, + }, }, }) .expect(404) From c5f48ef50b9cf9bd1f094b10b6c4d9bc933b0a27 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Fri, 3 May 2019 13:50:28 -0400 Subject: [PATCH 16/51] WIP --- x-pack/plugins/alerting/index.ts | 9 ++++++++- x-pack/plugins/alerting/mappings.json | 3 +++ x-pack/plugins/alerting/server/connector_service.ts | 1 + .../plugins/alerting/server/default_connectors/email.ts | 4 ++-- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/alerting/index.ts b/x-pack/plugins/alerting/index.ts index bb2f76137bd66..ea1af8909c747 100644 --- a/x-pack/plugins/alerting/index.ts +++ b/x-pack/plugins/alerting/index.ts @@ -25,7 +25,7 @@ export function alerting(kibana: any) { return new kibana.Plugin({ id: APP_ID, configPrefix: 'xpack.alerting', - require: ['kibana', 'elasticsearch'], + require: ['kibana', 'elasticsearch', 'encrypted_saved_objects'], config(Joi: any) { return Joi.object() .keys({ @@ -41,6 +41,13 @@ export function alerting(kibana: any) { return; } + // Encrypted attributes + // @ts-ignore + server.plugins.encrypted_saved_objects.registerType({ + type: 'action', + attributesToEncrypt: new Set(['connectorOptionsSecrets']), + }); + const connectorService = new ConnectorService(); const actionService = new ActionService(connectorService); const alertService = new AlertService(); diff --git a/x-pack/plugins/alerting/mappings.json b/x-pack/plugins/alerting/mappings.json index 2ecaf42bce2ac..2773ed053af07 100644 --- a/x-pack/plugins/alerting/mappings.json +++ b/x-pack/plugins/alerting/mappings.json @@ -10,6 +10,9 @@ "connectorOptions": { "dynamic": "true", "type": "object" + }, + "connectorOptionsSecrets": { + "type": "binary" } } } diff --git a/x-pack/plugins/alerting/server/connector_service.ts b/x-pack/plugins/alerting/server/connector_service.ts index 78aa07fe68700..f565a7548a78e 100644 --- a/x-pack/plugins/alerting/server/connector_service.ts +++ b/x-pack/plugins/alerting/server/connector_service.ts @@ -9,6 +9,7 @@ import Boom from 'boom'; interface Connector { id: string; name: string; + unencryptedAttributes: string[]; validate?: { params?: any; connectorOptions?: any; diff --git a/x-pack/plugins/alerting/server/default_connectors/email.ts b/x-pack/plugins/alerting/server/default_connectors/email.ts index 4a35d1a16fb91..edbba2787b39c 100644 --- a/x-pack/plugins/alerting/server/default_connectors/email.ts +++ b/x-pack/plugins/alerting/server/default_connectors/email.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import nodemailer, { SendMailOptions } from 'nodemailer'; +import nodemailer from 'nodemailer'; export const emailConnector = { id: 'email', name: 'E-mail', - async executor(connectorOptions: any, params: SendMailOptions) { + async executor(connectorOptions: any, params: any) { const transporter = nodemailer.createTransport(connectorOptions); await transporter.sendMail(params); }, From 09173393834c4936498dc2bee19c821932c136f1 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Mon, 6 May 2019 09:07:42 -0400 Subject: [PATCH 17/51] Fix broken tests --- x-pack/plugins/alerting/server/connector_service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/alerting/server/connector_service.ts b/x-pack/plugins/alerting/server/connector_service.ts index f565a7548a78e..8b05f4ed8f7a1 100644 --- a/x-pack/plugins/alerting/server/connector_service.ts +++ b/x-pack/plugins/alerting/server/connector_service.ts @@ -9,7 +9,7 @@ import Boom from 'boom'; interface Connector { id: string; name: string; - unencryptedAttributes: string[]; + unencryptedAttributes?: string[]; validate?: { params?: any; connectorOptions?: any; From b1faff5a8a0eccce07d836b5cbb9080703557284 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Mon, 6 May 2019 09:35:08 -0400 Subject: [PATCH 18/51] Add encrypted attribute support --- x-pack/plugins/alerting/index.ts | 6 +- .../alerting/server/action_service.test.ts | 209 ++++++++++++++++-- .../plugins/alerting/server/action_service.ts | 51 ++++- .../alerting/server/connector_service.test.ts | 25 +++ .../alerting/server/connector_service.ts | 5 + 5 files changed, 274 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/alerting/index.ts b/x-pack/plugins/alerting/index.ts index ea1af8909c747..ecb2836406a45 100644 --- a/x-pack/plugins/alerting/index.ts +++ b/x-pack/plugins/alerting/index.ts @@ -49,7 +49,11 @@ export function alerting(kibana: any) { }); const connectorService = new ConnectorService(); - const actionService = new ActionService(connectorService); + const actionService = new ActionService( + connectorService, + // @ts-ignore + server.plugins.encrypted_saved_objects + ); const alertService = new AlertService(); // Register default connectors diff --git a/x-pack/plugins/alerting/server/action_service.test.ts b/x-pack/plugins/alerting/server/action_service.test.ts index 8db1db221ffd1..9f2c6521fd2c6 100644 --- a/x-pack/plugins/alerting/server/action_service.test.ts +++ b/x-pack/plugins/alerting/server/action_service.test.ts @@ -21,6 +21,10 @@ const savedObjectsClient = { beforeEach(() => jest.resetAllMocks()); +const mockEncryptedSavedObjects = { + getDecryptedAsInternalUser: jest.fn(), +}; + describe('create()', () => { test('creates an action with all given properties', async () => { const expectedResult = Symbol(); @@ -30,7 +34,7 @@ describe('create()', () => { name: 'My connector', async executor() {}, }); - const actionService = new ActionService(connectorService); + const actionService = new ActionService(connectorService, mockEncryptedSavedObjects); savedObjectsClient.create.mockResolvedValueOnce(expectedResult); const result = await actionService.create( savedObjectsClient, @@ -53,6 +57,7 @@ describe('create()', () => { Object { "connectorId": "my-connector", "connectorOptions": Object {}, + "connectorOptionsSecrets": Object {}, "description": "my description", }, Object { @@ -73,7 +78,7 @@ describe('create()', () => { test('validates connectorOptions', async () => { const connectorService = new ConnectorService(); - const actionService = new ActionService(connectorService); + const actionService = new ActionService(connectorService, mockEncryptedSavedObjects); connectorService.register({ id: 'my-connector', name: 'My connector', @@ -103,7 +108,7 @@ describe('create()', () => { test(`throws an error when connector doesn't exist`, async () => { const connectorService = new ConnectorService(); - const actionService = new ActionService(connectorService); + const actionService = new ActionService(connectorService, mockEncryptedSavedObjects); await expect( actionService.create( savedObjectsClient, @@ -118,13 +123,73 @@ describe('create()', () => { `"Connector \\"unregistered-connector\\" is not registered."` ); }); + + test('encrypts connector options unless specified not to', async () => { + const expectedResult = Symbol(); + const connectorService = new ConnectorService(); + connectorService.register({ + id: 'my-connector', + name: 'My connector', + unencryptedAttributes: ['a', 'c'], + async executor() {}, + }); + const actionService = new ActionService(connectorService, mockEncryptedSavedObjects); + savedObjectsClient.create.mockResolvedValueOnce(expectedResult); + const result = await actionService.create( + savedObjectsClient, + { + description: 'my description', + connectorId: 'my-connector', + connectorOptions: { + a: true, + b: true, + c: true, + }, + }, + { + id: 'my-alert', + overwrite: true, + } + ); + expect(result).toEqual(expectedResult); + expect(savedObjectsClient.create).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + "action", + Object { + "connectorId": "my-connector", + "connectorOptions": Object { + "a": true, + "c": true, + }, + "connectorOptionsSecrets": Object { + "b": true, + }, + "description": "my description", + }, + Object { + "id": "my-alert", + "overwrite": true, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], +} +`); + }); }); describe('get()', () => { test('calls savedObjectsClient with id', async () => { const expectedResult = Symbol(); const connectorService = new ConnectorService(); - const actionService = new ActionService(connectorService); + const actionService = new ActionService(connectorService, mockEncryptedSavedObjects); savedObjectsClient.get.mockResolvedValueOnce(expectedResult); const result = await actionService.get(savedObjectsClient, '1'); expect(result).toEqual(expectedResult); @@ -151,7 +216,7 @@ describe('find()', () => { test('calls savedObjectsClient with parameters', async () => { const expectedResult = Symbol(); const connectorService = new ConnectorService(); - const actionService = new ActionService(connectorService); + const actionService = new ActionService(connectorService, mockEncryptedSavedObjects); savedObjectsClient.find.mockResolvedValueOnce(expectedResult); const result = await actionService.find(savedObjectsClient, {}); expect(result).toEqual(expectedResult); @@ -179,7 +244,7 @@ describe('delete()', () => { test('calls savedObjectsClient with id', async () => { const expectedResult = Symbol(); const connectorService = new ConnectorService(); - const actionService = new ActionService(connectorService); + const actionService = new ActionService(connectorService, mockEncryptedSavedObjects); savedObjectsClient.delete.mockResolvedValueOnce(expectedResult); const result = await actionService.delete(savedObjectsClient, '1'); expect(result).toEqual(expectedResult); @@ -211,7 +276,7 @@ describe('update()', () => { name: 'My connector', async executor() {}, }); - const actionService = new ActionService(connectorService); + const actionService = new ActionService(connectorService, mockEncryptedSavedObjects); savedObjectsClient.update.mockResolvedValueOnce(expectedResult); const result = await actionService.update( savedObjectsClient, @@ -233,6 +298,7 @@ describe('update()', () => { Object { "connectorId": "my-connector", "connectorOptions": Object {}, + "connectorOptionsSecrets": Object {}, "description": "my description", }, Object {}, @@ -250,7 +316,7 @@ describe('update()', () => { test('validates connectorOptions', async () => { const connectorService = new ConnectorService(); - const actionService = new ActionService(connectorService); + const actionService = new ActionService(connectorService, mockEncryptedSavedObjects); connectorService.register({ id: 'my-connector', name: 'My connector', @@ -281,7 +347,7 @@ describe('update()', () => { test(`throws an error when connector doesn't exist`, async () => { const connectorService = new ConnectorService(); - const actionService = new ActionService(connectorService); + const actionService = new ActionService(connectorService, mockEncryptedSavedObjects); await expect( actionService.update( savedObjectsClient, @@ -297,23 +363,79 @@ describe('update()', () => { `"Connector \\"unregistered-connector\\" is not registered."` ); }); + + test('encrypts connector options unless specified not to', async () => { + const expectedResult = Symbol(); + const connectorService = new ConnectorService(); + connectorService.register({ + id: 'my-connector', + name: 'My connector', + unencryptedAttributes: ['a', 'c'], + async executor() {}, + }); + const actionService = new ActionService(connectorService, mockEncryptedSavedObjects); + savedObjectsClient.update.mockResolvedValueOnce(expectedResult); + const result = await actionService.update( + savedObjectsClient, + 'my-alert', + { + description: 'my description', + connectorId: 'my-connector', + connectorOptions: { + a: true, + b: true, + c: true, + }, + }, + {} + ); + expect(result).toEqual(expectedResult); + expect(savedObjectsClient.update).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + "action", + "my-alert", + Object { + "connectorId": "my-connector", + "connectorOptions": Object { + "a": true, + "c": true, + }, + "connectorOptionsSecrets": Object { + "b": true, + }, + "description": "my description", + }, + Object {}, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], +} +`); + }); }); describe('fire()', () => { test('fires an action with all given parameters', async () => { const connectorService = new ConnectorService(); - const actionService = new ActionService(connectorService); + const actionService = new ActionService(connectorService, mockEncryptedSavedObjects); const mockConnector = jest.fn().mockResolvedValueOnce({ success: true }); connectorService.register({ id: 'mock', name: 'Mock', executor: mockConnector, }); - savedObjectsClient.get.mockResolvedValueOnce({ + mockEncryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ id: 'mock-action', attributes: { connectorId: 'mock', - connectorOptions: { + connectorOptionsSecrets: { foo: true, }, }, @@ -344,17 +466,19 @@ describe('fire()', () => { ], } `); - expect(savedObjectsClient.get.mock.calls).toEqual([['action', 'mock-action']]); + expect(mockEncryptedSavedObjects.getDecryptedAsInternalUser.mock.calls).toEqual([ + ['action', 'mock-action'], + ]); }); test(`throws an error when the connector isn't registered`, async () => { const connectorService = new ConnectorService(); - const actionService = new ActionService(connectorService); - savedObjectsClient.get.mockResolvedValueOnce({ + const actionService = new ActionService(connectorService, mockEncryptedSavedObjects); + mockEncryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ id: 'mock-action', attributes: { connectorId: 'non-registered-connector', - connectorOptions: { + connectorOptionsSecrets: { foo: true, }, }, @@ -365,4 +489,57 @@ describe('fire()', () => { `"Connector \\"non-registered-connector\\" is not registered."` ); }); + + test('merges encrypted and unencrypted attributes', async () => { + const connectorService = new ConnectorService(); + const actionService = new ActionService(connectorService, mockEncryptedSavedObjects); + const mockConnector = jest.fn().mockResolvedValueOnce({ success: true }); + connectorService.register({ + id: 'mock', + name: 'Mock', + unencryptedAttributes: ['a', 'c'], + executor: mockConnector, + }); + mockEncryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ + id: 'mock-action', + attributes: { + connectorId: 'mock', + connectorOptions: { + a: true, + c: true, + }, + connectorOptionsSecrets: { + b: true, + }, + }, + }); + const result = await actionService.fire({ + id: 'mock-action', + params: { baz: false }, + savedObjectsClient, + }); + expect(result).toEqual({ success: true }); + expect(mockConnector).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + Object { + "a": true, + "b": true, + "c": true, + }, + Object { + "baz": false, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], +} +`); + }); }); diff --git a/x-pack/plugins/alerting/server/action_service.ts b/x-pack/plugins/alerting/server/action_service.ts index b5459db0fd276..654cc23ee9fca 100644 --- a/x-pack/plugins/alerting/server/action_service.ts +++ b/x-pack/plugins/alerting/server/action_service.ts @@ -14,6 +14,13 @@ interface Action { connectorOptions: { [key: string]: any }; } +interface EncryptedAction extends Action { + description: string; + connectorId: string; + connectorOptions: { [key: string]: any }; + connectorOptionsSecrets: { [key: string]: any }; +} + interface FireActionOptions { id: string; params: { [key: string]: any }; @@ -42,9 +49,11 @@ interface FindOptions { export class ActionService { private connectorService: ConnectorService; + private encryptedSavedObjects: any; - constructor(connectorService: ConnectorService) { + constructor(connectorService: ConnectorService, encryptedSavedObjects: any) { this.connectorService = connectorService; + this.encryptedSavedObjects = encryptedSavedObjects; } public async create( @@ -57,7 +66,11 @@ export class ActionService { throw Boom.badRequest(`Connector "${connectorId}" is not registered.`); } this.connectorService.validateConnectorOptions(connectorId, data.connectorOptions); - return await savedObjectsClient.create('action', data, { id, overwrite }); + const actionWithSplitConnectorOptions = this.applyEncryptedAttributes(data); + return await savedObjectsClient.create('action', actionWithSplitConnectorOptions, { + id, + overwrite, + }); } public async get(savedObjectsClient: SavedObjectsClient, id: string) { @@ -86,15 +99,43 @@ export class ActionService { throw Boom.badRequest(`Connector "${connectorId}" is not registered.`); } this.connectorService.validateConnectorOptions(connectorId, data.connectorOptions); - return await savedObjectsClient.update('action', id, data, options); + const actionWithSplitConnectorOptions = this.applyEncryptedAttributes(data); + return await savedObjectsClient.update( + 'action', + id, + actionWithSplitConnectorOptions, + options + ); } public async fire({ id, params, savedObjectsClient }: FireActionOptions) { - const action = await this.get(savedObjectsClient, id); + const action = await this.encryptedSavedObjects.getDecryptedAsInternalUser('action', id); return await this.connectorService.execute( action.attributes.connectorId, - action.attributes.connectorOptions, + { + ...action.attributes.connectorOptions, + ...action.attributes.connectorOptionsSecrets, + }, params ); } + + private applyEncryptedAttributes(action: Action): EncryptedAction { + const unencryptedAttributes = this.connectorService.getEncryptedAttributes(action.connectorId); + const encryptedConnectorOptions: { [key: string]: any } = {}; + const unencryptedConnectorOptions: { [key: string]: any } = {}; + for (const key of Object.keys(action.connectorOptions)) { + if (unencryptedAttributes.includes(key)) { + unencryptedConnectorOptions[key] = action.connectorOptions[key]; + continue; + } + encryptedConnectorOptions[key] = action.connectorOptions[key]; + } + return { + ...action, + // Important these overwrite attributes in data for encryption purposes + connectorOptions: unencryptedConnectorOptions, + connectorOptionsSecrets: encryptedConnectorOptions, + }; + } } diff --git a/x-pack/plugins/alerting/server/connector_service.test.ts b/x-pack/plugins/alerting/server/connector_service.test.ts index f58ae0241e9e3..1cb080b185613 100644 --- a/x-pack/plugins/alerting/server/connector_service.test.ts +++ b/x-pack/plugins/alerting/server/connector_service.test.ts @@ -62,6 +62,31 @@ Object { }); }); +describe('getEncryptedAttributes()', () => { + test('returns empty array when unencryptedAttributes is undefined', () => { + const connectorService = new ConnectorService(); + connectorService.register({ + id: 'my-connector', + name: 'My connector', + async executor() {}, + }); + const result = connectorService.getEncryptedAttributes('my-connector'); + expect(result).toEqual([]); + }); + + test('returns values inside unencryptedAttributes array when it exists', () => { + const connectorService = new ConnectorService(); + connectorService.register({ + id: 'my-connector', + name: 'My connector', + unencryptedAttributes: ['a', 'b', 'c'], + async executor() {}, + }); + const result = connectorService.getEncryptedAttributes('my-connector'); + expect(result).toEqual(['a', 'b', 'c']); + }); +}); + describe('list()', () => { test('returns list of connectors', () => { const connectorService = new ConnectorService(); diff --git a/x-pack/plugins/alerting/server/connector_service.ts b/x-pack/plugins/alerting/server/connector_service.ts index 8b05f4ed8f7a1..ebb0469a70f3f 100644 --- a/x-pack/plugins/alerting/server/connector_service.ts +++ b/x-pack/plugins/alerting/server/connector_service.ts @@ -39,6 +39,11 @@ export class ConnectorService { return connector; } + public getEncryptedAttributes(id: string) { + const connector = this.get(id); + return connector.unencryptedAttributes || []; + } + public list() { const connectorIds = Object.keys(this.connectors); return connectorIds.map(id => ({ From 9ff7836dadcdd746fb158e6c680c9b3e158d098c Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Mon, 6 May 2019 10:45:02 -0400 Subject: [PATCH 19/51] Add params and connectorOptions for email --- .../server/default_connectors/email.test.ts | 94 +++++++++++++++++++ .../server/default_connectors/email.ts | 61 +++++++++++- 2 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/alerting/server/default_connectors/email.test.ts diff --git a/x-pack/plugins/alerting/server/default_connectors/email.test.ts b/x-pack/plugins/alerting/server/default_connectors/email.test.ts new file mode 100644 index 0000000000000..34505e22ad0ec --- /dev/null +++ b/x-pack/plugins/alerting/server/default_connectors/email.test.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('nodemailer', () => ({ + createTransport: jest.fn().mockImplementation(() => { + return { + sendMail: jest.fn(), + }; + }), +})); + +import { emailConnector } from './email'; + +test('calls the nodemailer API with proper arguments', async () => { + // eslint-disable-next-line + const nodemailer = require('nodemailer'); + await emailConnector.executor( + { + port: 123, + host: 'localhost', + auth: { + type: 'PLAIN', + username: 'admin', + password: 'admin', + }, + }, + { + from: 'me@localhost', + to: ['you@localhost'], + cc: ['cc@localhost'], + bcc: ['bcc@localhost'], + subject: 'My subject', + text: 'My text', + html: 'My html', + } + ); + expect(nodemailer.createTransport).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + Object { + "auth": Object { + "password": "admin", + "type": "PLAIN", + "username": "admin", + }, + "host": "localhost", + "port": 123, + }, + Object { + "secure": true, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Object { + "sendMail": [MockFunction] { + "calls": Array [ + Array [ + Object { + "bcc": Array [ + "bcc@localhost", + ], + "cc": Array [ + "cc@localhost", + ], + "from": "me@localhost", + "html": "My html", + "subject": "My subject", + "text": "My text", + "to": Array [ + "you@localhost", + ], + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], + }, + }, + }, + ], +} +`); +}); diff --git a/x-pack/plugins/alerting/server/default_connectors/email.ts b/x-pack/plugins/alerting/server/default_connectors/email.ts index edbba2787b39c..e7391b0454c15 100644 --- a/x-pack/plugins/alerting/server/default_connectors/email.ts +++ b/x-pack/plugins/alerting/server/default_connectors/email.ts @@ -4,13 +4,70 @@ * you may not use this file except in compliance with the Elastic License. */ +import Joi from 'joi'; import nodemailer from 'nodemailer'; +interface EmailConnectorOptions { + port?: number; + host: string; + auth: { + type?: string; + username: string; + password: string; + }; +} + +interface EmailParams { + from: string; + to: string[]; + cc?: string[]; + bcc?: string[]; + subject: string; + text: string; + html?: string; +} + export const emailConnector = { id: 'email', name: 'E-mail', - async executor(connectorOptions: any, params: any) { - const transporter = nodemailer.createTransport(connectorOptions); + validate: { + params: Joi.object() + .keys({ + from: Joi.string().required(), + to: Joi.array() + .items(Joi.string()) + .min(1) + .required(), + cc: Joi.array() + .items(Joi.string()) + .optional(), + bcc: Joi.array() + .items(Joi.string()) + .optional(), + subject: Joi.string().required(), + text: Joi.string().required(), + html: Joi.strict().optional(), + }) + .required(), + connectorOptions: Joi.object() + .keys({ + port: Joi.number().optional(), + host: Joi.string().required(), + auth: Joi.object() + .keys({ + type: Joi.string().optional(), + username: Joi.string().required(), + password: Joi.string().required(), + }) + .required(), + }) + .required(), + }, + async executor(connectorOptions: EmailConnectorOptions, params: EmailParams) { + // @ts-ignore + const transporter = nodemailer.createTransport(connectorOptions, { + secure: true, + }); await transporter.sendMail(params); }, }; From 40d59a20f20e153750f3ccf60d4f08bdc63dead8 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Mon, 6 May 2019 12:13:34 -0400 Subject: [PATCH 20/51] WIP --- .../apis/alerting/create_action.ts | 32 ++++++------------- .../api_integration/apis/alerting/index.ts | 2 +- .../apis/alerting/update_action.ts | 15 ++------- 3 files changed, 13 insertions(+), 36 deletions(-) diff --git a/x-pack/test/api_integration/apis/alerting/create_action.ts b/x-pack/test/api_integration/apis/alerting/create_action.ts index 2826d67eff742..323fa38e75a76 100644 --- a/x-pack/test/api_integration/apis/alerting/create_action.ts +++ b/x-pack/test/api_integration/apis/alerting/create_action.ts @@ -35,9 +35,7 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe attributes: { description: 'My action', connectorId: 'log', - connectorOptions: { - username: 'username', - }, + connectorOptions: {}, }, }) .expect(200) @@ -48,7 +46,7 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe attributes: { description: 'My action', connectorId: 'log', - connectorOptions: { username: 'username' }, + connectorOptions: {}, }, references: [], updated_at: resp.body.updated_at, @@ -65,9 +63,7 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe attributes: { description: 'My action', connectorId: 'log', - connectorOptions: { - username: 'username', - }, + connectorOptions: {}, }, }) .expect(200) @@ -78,7 +74,7 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe attributes: { description: 'My action', connectorId: 'log', - connectorOptions: { username: 'username' }, + connectorOptions: {}, }, references: [], updated_at: resp.body.updated_at, @@ -96,9 +92,7 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe attributes: { description: 'My action to duplicate', connectorId: 'log', - connectorOptions: { - username: 'username', - }, + connectorOptions: {}, }, }) .expect(200); @@ -109,9 +103,7 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe attributes: { description: 'My action to duplicate', connectorId: 'log', - connectorOptions: { - username: 'username', - }, + connectorOptions: {}, }, }) .expect(409); @@ -125,9 +117,7 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe attributes: { description: 'My action to duplicate', connectorId: 'log', - connectorOptions: { - username: 'username', - }, + connectorOptions: {}, }, }) .expect(200); @@ -138,9 +128,7 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe attributes: { description: 'My action to overwrite', connectorId: 'log', - connectorOptions: { - username: 'username', - }, + connectorOptions: {}, }, }) .expect(200); @@ -154,9 +142,7 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe attributes: { description: 'My action', connectorId: 'unregistered-connector', - connectorOptions: { - username: 'username', - }, + connectorOptions: {}, }, }) .expect(400) diff --git a/x-pack/test/api_integration/apis/alerting/index.ts b/x-pack/test/api_integration/apis/alerting/index.ts index 93909e824bc20..053a89d36fd81 100644 --- a/x-pack/test/api_integration/apis/alerting/index.ts +++ b/x-pack/test/api_integration/apis/alerting/index.ts @@ -8,7 +8,7 @@ import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; // eslint-disable-next-line import/no-default-export export default function alertingTests({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { - describe('Alerting', () => { + describe.only('Alerting', () => { loadTestFile(require.resolve('./create_action')); loadTestFile(require.resolve('./delete_action')); loadTestFile(require.resolve('./find_action')); diff --git a/x-pack/test/api_integration/apis/alerting/update_action.ts b/x-pack/test/api_integration/apis/alerting/update_action.ts index 0115c472eff2e..07bd6ce3ee722 100644 --- a/x-pack/test/api_integration/apis/alerting/update_action.ts +++ b/x-pack/test/api_integration/apis/alerting/update_action.ts @@ -24,10 +24,7 @@ export default function updateActionTests({ getService }: KibanaFunctionalTestDe attributes: { connectorId: 'log', description: 'My description updated', - connectorOptions: { - bar: true, - foo: false, - }, + connectorOptions: {}, }, }) .expect(200) @@ -41,10 +38,7 @@ export default function updateActionTests({ getService }: KibanaFunctionalTestDe attributes: { connectorId: 'log', description: 'My description updated', - connectorOptions: { - bar: true, - foo: false, - }, + connectorOptions: {}, }, }); }); @@ -58,10 +52,7 @@ export default function updateActionTests({ getService }: KibanaFunctionalTestDe attributes: { connectorId: 'log', description: 'My description updated', - connectorOptions: { - bar: true, - foo: false, - }, + connectorOptions: {}, }, }) .expect(404) From ff69d43e78cc1947910a5a1ac58774db1c541dba Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Tue, 7 May 2019 08:09:47 -0400 Subject: [PATCH 21/51] Remove action's ability to have custom ids --- .../alerting/server/action_service.test.ts | 76 +++++---------- .../plugins/alerting/server/action_service.ts | 11 +-- .../alerting/server/routes/create_action.ts | 17 +--- .../apis/alerting/create_action.ts | 97 +------------------ .../api_integration/apis/alerting/index.ts | 2 +- 5 files changed, 30 insertions(+), 173 deletions(-) diff --git a/x-pack/plugins/alerting/server/action_service.test.ts b/x-pack/plugins/alerting/server/action_service.test.ts index 9f2c6521fd2c6..f443b9b62b257 100644 --- a/x-pack/plugins/alerting/server/action_service.test.ts +++ b/x-pack/plugins/alerting/server/action_service.test.ts @@ -36,18 +36,11 @@ describe('create()', () => { }); const actionService = new ActionService(connectorService, mockEncryptedSavedObjects); savedObjectsClient.create.mockResolvedValueOnce(expectedResult); - const result = await actionService.create( - savedObjectsClient, - { - description: 'my description', - connectorId: 'my-connector', - connectorOptions: {}, - }, - { - id: 'my-alert', - overwrite: true, - } - ); + const result = await actionService.create(savedObjectsClient, { + description: 'my description', + connectorId: 'my-connector', + connectorOptions: {}, + }); expect(result).toEqual(expectedResult); expect(savedObjectsClient.create).toMatchInlineSnapshot(` [MockFunction] { @@ -60,10 +53,6 @@ describe('create()', () => { "connectorOptionsSecrets": Object {}, "description": "my description", }, - Object { - "id": "my-alert", - "overwrite": true, - }, ], ], "results": Array [ @@ -92,15 +81,11 @@ describe('create()', () => { async executor() {}, }); await expect( - actionService.create( - savedObjectsClient, - { - description: 'my description', - connectorId: 'my-connector', - connectorOptions: {}, - }, - { id: 'my-alert' } - ) + actionService.create(savedObjectsClient, { + description: 'my description', + connectorId: 'my-connector', + connectorOptions: {}, + }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"child \\"param1\\" fails because [\\"param1\\" is required]"` ); @@ -110,15 +95,11 @@ describe('create()', () => { const connectorService = new ConnectorService(); const actionService = new ActionService(connectorService, mockEncryptedSavedObjects); await expect( - actionService.create( - savedObjectsClient, - { - description: 'my description', - connectorId: 'unregistered-connector', - connectorOptions: {}, - }, - { id: 'my-alert' } - ) + actionService.create(savedObjectsClient, { + description: 'my description', + connectorId: 'unregistered-connector', + connectorOptions: {}, + }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Connector \\"unregistered-connector\\" is not registered."` ); @@ -135,22 +116,15 @@ describe('create()', () => { }); const actionService = new ActionService(connectorService, mockEncryptedSavedObjects); savedObjectsClient.create.mockResolvedValueOnce(expectedResult); - const result = await actionService.create( - savedObjectsClient, - { - description: 'my description', - connectorId: 'my-connector', - connectorOptions: { - a: true, - b: true, - c: true, - }, + const result = await actionService.create(savedObjectsClient, { + description: 'my description', + connectorId: 'my-connector', + connectorOptions: { + a: true, + b: true, + c: true, }, - { - id: 'my-alert', - overwrite: true, - } - ); + }); expect(result).toEqual(expectedResult); expect(savedObjectsClient.create).toMatchInlineSnapshot(` [MockFunction] { @@ -168,10 +142,6 @@ describe('create()', () => { }, "description": "my description", }, - Object { - "id": "my-alert", - "overwrite": true, - }, ], ], "results": Array [ diff --git a/x-pack/plugins/alerting/server/action_service.ts b/x-pack/plugins/alerting/server/action_service.ts index 654cc23ee9fca..54771ce98e0e6 100644 --- a/x-pack/plugins/alerting/server/action_service.ts +++ b/x-pack/plugins/alerting/server/action_service.ts @@ -56,21 +56,14 @@ export class ActionService { this.encryptedSavedObjects = encryptedSavedObjects; } - public async create( - savedObjectsClient: SavedObjectsClient, - data: Action, - { id, overwrite }: { id?: string; overwrite?: boolean } = {} - ) { + public async create(savedObjectsClient: SavedObjectsClient, data: Action) { const { connectorId } = data; if (!this.connectorService.has(connectorId)) { throw Boom.badRequest(`Connector "${connectorId}" is not registered.`); } this.connectorService.validateConnectorOptions(connectorId, data.connectorOptions); const actionWithSplitConnectorOptions = this.applyEncryptedAttributes(data); - return await savedObjectsClient.create('action', actionWithSplitConnectorOptions, { - id, - overwrite, - }); + return await savedObjectsClient.create('action', actionWithSplitConnectorOptions); } public async get(savedObjectsClient: SavedObjectsClient, id: string) { diff --git a/x-pack/plugins/alerting/server/routes/create_action.ts b/x-pack/plugins/alerting/server/routes/create_action.ts index 4417ff693f073..2ef8164f4ea25 100644 --- a/x-pack/plugins/alerting/server/routes/create_action.ts +++ b/x-pack/plugins/alerting/server/routes/create_action.ts @@ -42,19 +42,9 @@ interface CreateActionRequest extends WithoutQueryAndParams { export function createActionRoute(server: Hapi.Server) { server.route({ method: 'POST', - path: `/api/${APP_ID}/action/{id?}`, + path: `/api/${APP_ID}/action`, options: { validate: { - query: Joi.object() - .keys({ - overwrite: Joi.boolean().default(false), - }) - .default(), - params: Joi.object() - .keys({ - id: Joi.string(), - }) - .required(), payload: Joi.object().keys({ attributes: Joi.object() .keys({ @@ -80,10 +70,7 @@ export function createActionRoute(server: Hapi.Server) { const savedObjectsClient = request.getSavedObjectsClient(); return await request.server .alerting() - .actions.create(savedObjectsClient, request.payload.attributes, { - id: request.params.id, - overwrite: request.query.overwrite, - }); + .actions.create(savedObjectsClient, request.payload.attributes); }, }); } diff --git a/x-pack/test/api_integration/apis/alerting/create_action.ts b/x-pack/test/api_integration/apis/alerting/create_action.ts index 323fa38e75a76..c9f6f3e2752e7 100644 --- a/x-pack/test/api_integration/apis/alerting/create_action.ts +++ b/x-pack/test/api_integration/apis/alerting/create_action.ts @@ -11,51 +11,8 @@ import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; export default function createActionTests({ getService }: KibanaFunctionalTestDefaultProviders) { const supertest = getService('supertest'); - async function deleteObject(type: string, id: string) { - await supertest - .delete(`/api/saved_objects/${type}/${id}`) - .set('kbn-xsrf', 'foo') - .expect(200); - } - describe('create_action', () => { - after(async () => { - await Promise.all([ - deleteObject('action', 'my-action'), - deleteObject('action', 'my-action-to-duplicate'), - deleteObject('action', 'my-action-to-overwrite'), - ]); - }); - - it('should return 200 when creating an action with provided id', async () => { - await supertest - .post('/api/alerting/action/my-action') - .set('kbn-xsrf', 'foo') - .send({ - attributes: { - description: 'My action', - connectorId: 'log', - connectorOptions: {}, - }, - }) - .expect(200) - .then((resp: any) => { - expect(resp.body).to.eql({ - type: 'action', - id: 'my-action', - attributes: { - description: 'My action', - connectorId: 'log', - connectorOptions: {}, - }, - references: [], - updated_at: resp.body.updated_at, - version: resp.body.version, - }); - }); - }); - - it('should return 200 when creating an action without id provided', async () => { + it('should return 200 when creating an action', async () => { await supertest .post('/api/alerting/action') .set('kbn-xsrf', 'foo') @@ -84,59 +41,9 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe }); }); - it('should return 409 when action already exists', async () => { - await supertest - .post('/api/alerting/action/my-action-to-duplicate') - .set('kbn-xsrf', 'foo') - .send({ - attributes: { - description: 'My action to duplicate', - connectorId: 'log', - connectorOptions: {}, - }, - }) - .expect(200); - await supertest - .post('/api/alerting/action/my-action-to-duplicate') - .set('kbn-xsrf', 'foo') - .send({ - attributes: { - description: 'My action to duplicate', - connectorId: 'log', - connectorOptions: {}, - }, - }) - .expect(409); - }); - - it('should return 200 when overwriting an action', async () => { - await supertest - .post('/api/alerting/action/my-action-to-overwrite') - .set('kbn-xsrf', 'foo') - .send({ - attributes: { - description: 'My action to duplicate', - connectorId: 'log', - connectorOptions: {}, - }, - }) - .expect(200); - await supertest - .post('/api/alerting/action/my-action-to-overwrite?overwrite=true') - .set('kbn-xsrf', 'foo') - .send({ - attributes: { - description: 'My action to overwrite', - connectorId: 'log', - connectorOptions: {}, - }, - }) - .expect(200); - }); - it(`should return 400 when connector isn't registered`, async () => { await supertest - .post('/api/alerting/action/my-action-without-connector') + .post('/api/alerting/action') .set('kbn-xsrf', 'foo') .send({ attributes: { diff --git a/x-pack/test/api_integration/apis/alerting/index.ts b/x-pack/test/api_integration/apis/alerting/index.ts index 053a89d36fd81..93909e824bc20 100644 --- a/x-pack/test/api_integration/apis/alerting/index.ts +++ b/x-pack/test/api_integration/apis/alerting/index.ts @@ -8,7 +8,7 @@ import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; // eslint-disable-next-line import/no-default-export export default function alertingTests({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { - describe.only('Alerting', () => { + describe('Alerting', () => { loadTestFile(require.resolve('./create_action')); loadTestFile(require.resolve('./delete_action')); loadTestFile(require.resolve('./find_action')); From ef1fb55338a9239713457ce9a5e69961a85ed383 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Tue, 7 May 2019 09:57:47 -0400 Subject: [PATCH 22/51] Remove ts-ignore --- x-pack/plugins/alerting/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/x-pack/plugins/alerting/index.ts b/x-pack/plugins/alerting/index.ts index ecb2836406a45..989e708864a27 100644 --- a/x-pack/plugins/alerting/index.ts +++ b/x-pack/plugins/alerting/index.ts @@ -42,8 +42,7 @@ export function alerting(kibana: any) { } // Encrypted attributes - // @ts-ignore - server.plugins.encrypted_saved_objects.registerType({ + server.plugins.encrypted_saved_objects!.registerType({ type: 'action', attributesToEncrypt: new Set(['connectorOptionsSecrets']), }); @@ -51,7 +50,6 @@ export function alerting(kibana: any) { const connectorService = new ConnectorService(); const actionService = new ActionService( connectorService, - // @ts-ignore server.plugins.encrypted_saved_objects ); const alertService = new AlertService(); From 25c1bfeabadd17242841da004898de0498c83a47 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Tue, 7 May 2019 12:53:35 -0400 Subject: [PATCH 23/51] Fix broken test --- .../ui_capabilities/common/saved_objects_management_builder.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/test/ui_capabilities/common/saved_objects_management_builder.ts b/x-pack/test/ui_capabilities/common/saved_objects_management_builder.ts index c2cfb00a9490c..879608217a4a6 100644 --- a/x-pack/test/ui_capabilities/common/saved_objects_management_builder.ts +++ b/x-pack/test/ui_capabilities/common/saved_objects_management_builder.ts @@ -52,6 +52,7 @@ export class SavedObjectsManagementBuilder { 'timelion-sheet', 'ui-metric', 'sample-data-telemetry', + 'action', ]; } From 3bd30f87cda719ce4b6da6fbf7ed9eb6b62cc871 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Tue, 7 May 2019 13:06:31 -0400 Subject: [PATCH 24/51] Remove default connectors from this branch --- x-pack/package.json | 2 - x-pack/plugins/alerting/index.ts | 6 -- .../server/default_connectors/email.test.ts | 94 ------------------- .../server/default_connectors/email.ts | 73 -------------- .../server/default_connectors/index.ts | 9 -- .../alerting/server/default_connectors/log.ts | 27 ------ .../default_connectors/message_slack.test.ts | 37 -------- .../default_connectors/message_slack.ts | 42 --------- yarn.lock | 19 ---- 9 files changed, 309 deletions(-) delete mode 100644 x-pack/plugins/alerting/server/default_connectors/email.test.ts delete mode 100644 x-pack/plugins/alerting/server/default_connectors/email.ts delete mode 100644 x-pack/plugins/alerting/server/default_connectors/index.ts delete mode 100644 x-pack/plugins/alerting/server/default_connectors/log.ts delete mode 100644 x-pack/plugins/alerting/server/default_connectors/message_slack.test.ts delete mode 100644 x-pack/plugins/alerting/server/default_connectors/message_slack.ts diff --git a/x-pack/package.json b/x-pack/package.json index f5d6bf9f8beea..773e642dd4ed1 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -182,7 +182,6 @@ "@scant/router": "^0.1.0", "@slack/client": "^4.8.0", "@turf/boolean-contains": "6.0.1", - "@types/nodemailer": "^4.6.8", "angular-resource": "1.4.9", "angular-sanitize": "1.6.5", "angular-ui-ace": "0.2.3", @@ -315,7 +314,6 @@ "rison-node": "0.3.1", "rxjs": "^6.2.1", "semver": "5.1.0", - "slack": "^11.0.2", "squel": "^5.12.2", "stats-lite": "^2.2.0", "style-it": "^2.1.3", diff --git a/x-pack/plugins/alerting/index.ts b/x-pack/plugins/alerting/index.ts index 989e708864a27..1f84623b07586 100644 --- a/x-pack/plugins/alerting/index.ts +++ b/x-pack/plugins/alerting/index.ts @@ -6,7 +6,6 @@ import Hapi from 'hapi'; import mappings from './mappings.json'; -import { logConnector, messageSlackConnector, emailConnector } from './server/default_connectors'; import { createActionRoute, deleteActionRoute, @@ -54,11 +53,6 @@ export function alerting(kibana: any) { ); const alertService = new AlertService(); - // Register default connectors - connectorService.register(logConnector); - connectorService.register(messageSlackConnector); - connectorService.register(emailConnector); - // Routes createActionRoute(server); deleteActionRoute(server); diff --git a/x-pack/plugins/alerting/server/default_connectors/email.test.ts b/x-pack/plugins/alerting/server/default_connectors/email.test.ts deleted file mode 100644 index 34505e22ad0ec..0000000000000 --- a/x-pack/plugins/alerting/server/default_connectors/email.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -jest.mock('nodemailer', () => ({ - createTransport: jest.fn().mockImplementation(() => { - return { - sendMail: jest.fn(), - }; - }), -})); - -import { emailConnector } from './email'; - -test('calls the nodemailer API with proper arguments', async () => { - // eslint-disable-next-line - const nodemailer = require('nodemailer'); - await emailConnector.executor( - { - port: 123, - host: 'localhost', - auth: { - type: 'PLAIN', - username: 'admin', - password: 'admin', - }, - }, - { - from: 'me@localhost', - to: ['you@localhost'], - cc: ['cc@localhost'], - bcc: ['bcc@localhost'], - subject: 'My subject', - text: 'My text', - html: 'My html', - } - ); - expect(nodemailer.createTransport).toMatchInlineSnapshot(` -[MockFunction] { - "calls": Array [ - Array [ - Object { - "auth": Object { - "password": "admin", - "type": "PLAIN", - "username": "admin", - }, - "host": "localhost", - "port": 123, - }, - Object { - "secure": true, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Object { - "sendMail": [MockFunction] { - "calls": Array [ - Array [ - Object { - "bcc": Array [ - "bcc@localhost", - ], - "cc": Array [ - "cc@localhost", - ], - "from": "me@localhost", - "html": "My html", - "subject": "My subject", - "text": "My text", - "to": Array [ - "you@localhost", - ], - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - }, - }, - ], -} -`); -}); diff --git a/x-pack/plugins/alerting/server/default_connectors/email.ts b/x-pack/plugins/alerting/server/default_connectors/email.ts deleted file mode 100644 index e7391b0454c15..0000000000000 --- a/x-pack/plugins/alerting/server/default_connectors/email.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Joi from 'joi'; -import nodemailer from 'nodemailer'; - -interface EmailConnectorOptions { - port?: number; - host: string; - auth: { - type?: string; - username: string; - password: string; - }; -} - -interface EmailParams { - from: string; - to: string[]; - cc?: string[]; - bcc?: string[]; - subject: string; - text: string; - html?: string; -} - -export const emailConnector = { - id: 'email', - name: 'E-mail', - validate: { - params: Joi.object() - .keys({ - from: Joi.string().required(), - to: Joi.array() - .items(Joi.string()) - .min(1) - .required(), - cc: Joi.array() - .items(Joi.string()) - .optional(), - bcc: Joi.array() - .items(Joi.string()) - .optional(), - subject: Joi.string().required(), - text: Joi.string().required(), - html: Joi.strict().optional(), - }) - .required(), - connectorOptions: Joi.object() - .keys({ - port: Joi.number().optional(), - host: Joi.string().required(), - auth: Joi.object() - .keys({ - type: Joi.string().optional(), - username: Joi.string().required(), - password: Joi.string().required(), - }) - .required(), - }) - .required(), - }, - async executor(connectorOptions: EmailConnectorOptions, params: EmailParams) { - // @ts-ignore - const transporter = nodemailer.createTransport(connectorOptions, { - secure: true, - }); - await transporter.sendMail(params); - }, -}; diff --git a/x-pack/plugins/alerting/server/default_connectors/index.ts b/x-pack/plugins/alerting/server/default_connectors/index.ts deleted file mode 100644 index ec6a79e90dbae..0000000000000 --- a/x-pack/plugins/alerting/server/default_connectors/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { logConnector } from './log'; -export { messageSlackConnector } from './message_slack'; -export { emailConnector } from './email'; diff --git a/x-pack/plugins/alerting/server/default_connectors/log.ts b/x-pack/plugins/alerting/server/default_connectors/log.ts deleted file mode 100644 index 19cdc7e6799cf..0000000000000 --- a/x-pack/plugins/alerting/server/default_connectors/log.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Joi from 'joi'; - -interface LogParams { - message: string; -} - -export const logConnector = { - id: 'log', - name: 'Log', - validate: { - params: Joi.object() - .keys({ - message: Joi.string().required(), - }) - .required(), - }, - async executor(connectorOptions: any, { message }: LogParams) { - // eslint-disable-next-line no-console - console.log(message); - }, -}; diff --git a/x-pack/plugins/alerting/server/default_connectors/message_slack.test.ts b/x-pack/plugins/alerting/server/default_connectors/message_slack.test.ts deleted file mode 100644 index 92badd7cfe0fa..0000000000000 --- a/x-pack/plugins/alerting/server/default_connectors/message_slack.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import * as slackMock from 'slack'; -import { messageSlackConnector } from './message_slack'; - -jest.mock('slack', () => ({ - chat: { - postMessage: jest.fn(), - }, -})); - -test('Calls the slack API with the proper arguments', async () => { - await messageSlackConnector.executor({ token: '123' }, { message: 'hello', channel: 'general' }); - expect(slackMock.chat.postMessage).toMatchInlineSnapshot(` -[MockFunction] { - "calls": Array [ - Array [ - Object { - "channel": "general", - "text": "hello", - "token": "123", - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], -} -`); -}); diff --git a/x-pack/plugins/alerting/server/default_connectors/message_slack.ts b/x-pack/plugins/alerting/server/default_connectors/message_slack.ts deleted file mode 100644 index fc907d8e4fd2d..0000000000000 --- a/x-pack/plugins/alerting/server/default_connectors/message_slack.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Joi from 'joi'; -import slack from 'slack'; - -interface SlackConnectorOptions { - token: string; -} - -interface MessageSlackParams { - message: string; - channel: string; -} - -export const messageSlackConnector = { - id: 'message-slack', - name: 'Message slack', - validate: { - params: Joi.object() - .keys({ - message: Joi.string().required(), - channel: Joi.string().required(), - }) - .required(), - connectorOptions: Joi.object() - .keys({ - token: Joi.string().required(), - }) - .required(), - }, - async executor(connectorOptions: SlackConnectorOptions, params: MessageSlackParams) { - await slack.chat.postMessage({ - token: connectorOptions.token, - text: params.message, - channel: params.channel, - }); - }, -}; diff --git a/yarn.lock b/yarn.lock index 2d2efe66430d3..9fe13192de7ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3378,13 +3378,6 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.27.tgz#eb3843f15d0ba0986cc7e4d734d2ee8b50709ef8" integrity sha512-e9wgeY6gaY21on3ve0xAjgBVjGDWq/xUteK0ujsE53bUoxycMkqfnkUgMt6ffZtykZ5X12Mg3T7Pw4TRCObDKg== -"@types/nodemailer@^4.6.8": - version "4.6.8" - resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-4.6.8.tgz#c14356e799fe1d4ee566126f901bc6031cc7b1b5" - integrity sha512-IX1P3bxDP1VIdZf6/kIWYNmSejkYm9MOyMEtoDFi4DVzKjJ3kY4GhOcOAKs6lZRjqVVmF9UjPOZXuQczlpZThw== - dependencies: - "@types/node" "*" - "@types/normalize-package-data@*": version "2.4.0" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" @@ -23648,13 +23641,6 @@ sisteransi@^1.0.0: resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.0.tgz#77d9622ff909080f1c19e5f4a1df0c1b0a27b88c" integrity sha512-N+z4pHB4AmUv0SjveWRd6q1Nj5w62m5jodv+GD8lvmbY/83T/rpbJGZOnK5T149OldDj4Db07BSv9xY4K6NTPQ== -slack@^11.0.2: - version "11.0.2" - resolved "https://registry.yarnpkg.com/slack/-/slack-11.0.2.tgz#30f68527c5d1712b7faa3141db7716f89ac6e911" - integrity sha512-rv842+S+AGyZCmMMd8xPtW5DvJ9LzWTAKfxi8Gw57oYlXgcKtFuHd4nqk6lTPpRKdUGn3tx/Drd0rjQR3dQPqw== - dependencies: - tiny-json-http "^7.0.2" - slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" @@ -25193,11 +25179,6 @@ tiny-invariant@^1.0.3, tiny-invariant@^1.0.4: resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.4.tgz#346b5415fd93cb696b0c4e8a96697ff590f92463" integrity sha512-lMhRd/djQJ3MoaHEBrw8e2/uM4rs9YMNk0iOr8rHQ0QdbM7D4l0gFl3szKdeixrlyfm9Zqi4dxHCM2qVG8ND5g== -tiny-json-http@^7.0.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/tiny-json-http/-/tiny-json-http-7.1.2.tgz#620e189849bab08992ec23fada7b48c7c61637b4" - integrity sha512-XB9Bu+ohdQso6ziPFNVqK+pcTt0l8BSRkW/CCBq0pUVlLxcYDsorpo7ae5yPhu2CF1xYgJuKVLF7cfOGeLCTlA== - tiny-lr@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/tiny-lr/-/tiny-lr-1.1.1.tgz#9fa547412f238fedb068ee295af8b682c98b2aab" From bebf71798217917eb037de9829524cfc640d2de0 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Tue, 7 May 2019 13:53:28 -0400 Subject: [PATCH 25/51] Fix API integration tests to use fixture connector --- .../apis/alerting/create_action.ts | 4 ++-- .../apis/alerting/get_action.ts | 2 +- .../apis/alerting/list_connectors.ts | 6 +----- .../apis/alerting/update_action.ts | 6 +++--- x-pack/test/api_integration/config.js | 2 ++ .../fixtures/plugins/alerts/index.ts | 20 +++++++++++++++++++ .../fixtures/plugins/alerts/package.json | 7 +++++++ .../es_archives/alerting/basic/data.json | 2 +- 8 files changed, 37 insertions(+), 12 deletions(-) create mode 100644 x-pack/test/api_integration/fixtures/plugins/alerts/index.ts create mode 100644 x-pack/test/api_integration/fixtures/plugins/alerts/package.json diff --git a/x-pack/test/api_integration/apis/alerting/create_action.ts b/x-pack/test/api_integration/apis/alerting/create_action.ts index c9f6f3e2752e7..72931426638a8 100644 --- a/x-pack/test/api_integration/apis/alerting/create_action.ts +++ b/x-pack/test/api_integration/apis/alerting/create_action.ts @@ -19,7 +19,7 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe .send({ attributes: { description: 'My action', - connectorId: 'log', + connectorId: 'test', connectorOptions: {}, }, }) @@ -30,7 +30,7 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe id: resp.body.id, attributes: { description: 'My action', - connectorId: 'log', + connectorId: 'test', connectorOptions: {}, }, references: [], diff --git a/x-pack/test/api_integration/apis/alerting/get_action.ts b/x-pack/test/api_integration/apis/alerting/get_action.ts index 8ce262b44c3a8..08b248d635475 100644 --- a/x-pack/test/api_integration/apis/alerting/get_action.ts +++ b/x-pack/test/api_integration/apis/alerting/get_action.ts @@ -27,7 +27,7 @@ export default function getActionTests({ getService }: KibanaFunctionalTestDefau references: [], version: resp.body.version, attributes: { - connectorId: 'log', + connectorId: 'test', description: 'My description', connectorOptions: { bar: false, diff --git a/x-pack/test/api_integration/apis/alerting/list_connectors.ts b/x-pack/test/api_integration/apis/alerting/list_connectors.ts index 7d7cabfefeebe..23076547cd77c 100644 --- a/x-pack/test/api_integration/apis/alerting/list_connectors.ts +++ b/x-pack/test/api_integration/apis/alerting/list_connectors.ts @@ -24,11 +24,7 @@ export default function listConnectorTests({ getService }: KibanaFunctionalTestD } // Check for values explicitly in order to avoid this test failing each time plugins register // a new connector - expect(resp.body.some(createConnectorMatcher('log', 'Log'))).to.be(true); - expect(resp.body.some(createConnectorMatcher('message-slack', 'Message slack'))).to.be( - true - ); - expect(resp.body.some(createConnectorMatcher('email', 'E-mail'))); + expect(resp.body.some(createConnectorMatcher('test', 'Test'))).to.be(true); }); }); }); diff --git a/x-pack/test/api_integration/apis/alerting/update_action.ts b/x-pack/test/api_integration/apis/alerting/update_action.ts index 07bd6ce3ee722..ce181458548ca 100644 --- a/x-pack/test/api_integration/apis/alerting/update_action.ts +++ b/x-pack/test/api_integration/apis/alerting/update_action.ts @@ -22,7 +22,7 @@ export default function updateActionTests({ getService }: KibanaFunctionalTestDe .set('kbn-xsrf', 'foo') .send({ attributes: { - connectorId: 'log', + connectorId: 'test', description: 'My description updated', connectorOptions: {}, }, @@ -36,7 +36,7 @@ export default function updateActionTests({ getService }: KibanaFunctionalTestDe version: resp.body.version, updated_at: resp.body.updated_at, attributes: { - connectorId: 'log', + connectorId: 'test', description: 'My description updated', connectorOptions: {}, }, @@ -50,7 +50,7 @@ export default function updateActionTests({ getService }: KibanaFunctionalTestDe .set('kbn-xsrf', 'foo') .send({ attributes: { - connectorId: 'log', + connectorId: 'test', description: 'My description updated', connectorOptions: {}, }, diff --git a/x-pack/test/api_integration/config.js b/x-pack/test/api_integration/config.js index f4d7f462276e8..42804064617be 100644 --- a/x-pack/test/api_integration/config.js +++ b/x-pack/test/api_integration/config.js @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import path from 'path'; import { EsProvider, EsSupertestWithoutAuthProvider, @@ -60,6 +61,7 @@ export default async function ({ readConfigFile }) { serverArgs: [ ...xPackFunctionalTestsConfig.get('kbnTestServer.serverArgs'), '--optimize.enabled=false', + `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'alerts')}`, ], }, esTestCluster: xPackFunctionalTestsConfig.get('esTestCluster'), diff --git a/x-pack/test/api_integration/fixtures/plugins/alerts/index.ts b/x-pack/test/api_integration/fixtures/plugins/alerts/index.ts new file mode 100644 index 0000000000000..99300fa5eaf3a --- /dev/null +++ b/x-pack/test/api_integration/fixtures/plugins/alerts/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// eslint-disable-next-line import/no-default-export +export default function(kibana: any) { + return new kibana.Plugin({ + require: ['alerting'], + name: 'alerts', + init(server: any) { + server.alerting().connectors.register({ + id: 'test', + name: 'Test', + async executor(connectorOptions: any, params: any) {}, + }); + }, + }); +} diff --git a/x-pack/test/api_integration/fixtures/plugins/alerts/package.json b/x-pack/test/api_integration/fixtures/plugins/alerts/package.json new file mode 100644 index 0000000000000..836fa09855d8f --- /dev/null +++ b/x-pack/test/api_integration/fixtures/plugins/alerts/package.json @@ -0,0 +1,7 @@ +{ + "name": "alerts", + "version": "0.0.0", + "kibana": { + "version": "kibana" + } +} diff --git a/x-pack/test/functional/es_archives/alerting/basic/data.json b/x-pack/test/functional/es_archives/alerting/basic/data.json index e609c2005a6c1..372dcbb22c960 100644 --- a/x-pack/test/functional/es_archives/alerting/basic/data.json +++ b/x-pack/test/functional/es_archives/alerting/basic/data.json @@ -5,7 +5,7 @@ "source": { "action": { "description": "My description", - "connectorId": "log", + "connectorId": "test", "connectorOptions": { "foo": true, "bar": false From 3641255fa46fc79d87bc36ffcaefe4aabfb8a5f5 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Tue, 7 May 2019 20:21:55 -0400 Subject: [PATCH 26/51] Rename connector terminology to action type --- x-pack/plugins/alerting/index.ts | 17 +- x-pack/plugins/alerting/mappings.json | 6 +- .../alerting/server/action_service.test.ts | 204 ++++++------ .../plugins/alerting/server/action_service.ts | 68 ++-- .../server/action_type_service.test.ts | 312 ++++++++++++++++++ .../alerting/server/action_type_service.ts | 83 +++++ .../plugins/alerting/server/alert_service.ts | 7 - .../alerting/server/connector_service.test.ts | 310 ----------------- .../alerting/server/connector_service.ts | 83 ----- x-pack/plugins/alerting/server/index.ts | 5 +- .../alerting/server/routes/create_action.ts | 21 +- .../alerting/server/routes/delete_action.ts | 12 +- .../alerting/server/routes/find_action.ts | 13 +- .../alerting/server/routes/get_action.ts | 12 +- .../plugins/alerting/server/routes/index.ts | 2 +- .../server/routes/list_action_types.ts | 24 ++ .../alerting/server/routes/list_connectors.ts | 34 -- .../plugins/alerting/server/routes/types.ts | 11 + .../alerting/server/routes/update_action.ts | 21 +- .../apis/alerting/create_action.ts | 16 +- .../apis/alerting/get_action.ts | 4 +- .../api_integration/apis/alerting/index.ts | 2 +- ...ist_connectors.ts => list_action_types.ts} | 18 +- .../apis/alerting/update_action.ts | 12 +- .../fixtures/plugins/alerts/index.ts | 4 +- .../es_archives/alerting/basic/data.json | 4 +- .../es_archives/alerting/basic/mappings.json | 7 +- 27 files changed, 628 insertions(+), 684 deletions(-) create mode 100644 x-pack/plugins/alerting/server/action_type_service.test.ts create mode 100644 x-pack/plugins/alerting/server/action_type_service.ts delete mode 100644 x-pack/plugins/alerting/server/alert_service.ts delete mode 100644 x-pack/plugins/alerting/server/connector_service.test.ts delete mode 100644 x-pack/plugins/alerting/server/connector_service.ts create mode 100644 x-pack/plugins/alerting/server/routes/list_action_types.ts delete mode 100644 x-pack/plugins/alerting/server/routes/list_connectors.ts rename x-pack/test/api_integration/apis/alerting/{list_connectors.ts => list_action_types.ts} (53%) diff --git a/x-pack/plugins/alerting/index.ts b/x-pack/plugins/alerting/index.ts index 1f84623b07586..815360af8c1b7 100644 --- a/x-pack/plugins/alerting/index.ts +++ b/x-pack/plugins/alerting/index.ts @@ -12,10 +12,9 @@ import { findActionRoute, getActionRoute, updateActionRoute, - listconnectorsRoute, - AlertService, + listActionTypesRoute, ActionService, - ConnectorService, + ActionTypeService, } from './server'; import { APP_ID } from './common/constants'; @@ -43,15 +42,14 @@ export function alerting(kibana: any) { // Encrypted attributes server.plugins.encrypted_saved_objects!.registerType({ type: 'action', - attributesToEncrypt: new Set(['connectorOptionsSecrets']), + attributesToEncrypt: new Set(['actionTypeOptionsSecrets']), }); - const connectorService = new ConnectorService(); + const actionTypeService = new ActionTypeService(); const actionService = new ActionService( - connectorService, + actionTypeService, server.plugins.encrypted_saved_objects ); - const alertService = new AlertService(); // Routes createActionRoute(server); @@ -59,13 +57,12 @@ export function alerting(kibana: any) { getActionRoute(server); findActionRoute(server); updateActionRoute(server); - listconnectorsRoute(server); + listActionTypesRoute(server); // Register service to server server.decorate('server', 'alerting', () => ({ - alerts: alertService, actions: actionService, - connectors: connectorService, + actionTypes: actionTypeService, })); }, uiExports: { diff --git a/x-pack/plugins/alerting/mappings.json b/x-pack/plugins/alerting/mappings.json index 2773ed053af07..4fa46c0a537b7 100644 --- a/x-pack/plugins/alerting/mappings.json +++ b/x-pack/plugins/alerting/mappings.json @@ -4,14 +4,14 @@ "description": { "type": "text" }, - "connectorId": { + "actionTypeId": { "type": "keyword" }, - "connectorOptions": { + "actionTypeOptions": { "dynamic": "true", "type": "object" }, - "connectorOptionsSecrets": { + "actionTypeOptionsSecrets": { "type": "binary" } } diff --git a/x-pack/plugins/alerting/server/action_service.test.ts b/x-pack/plugins/alerting/server/action_service.test.ts index f443b9b62b257..52418e535a893 100644 --- a/x-pack/plugins/alerting/server/action_service.test.ts +++ b/x-pack/plugins/alerting/server/action_service.test.ts @@ -5,7 +5,7 @@ */ import Joi from 'joi'; -import { ConnectorService } from './connector_service'; +import { ActionTypeService } from './action_type_service'; import { ActionService } from './action_service'; const savedObjectsClient = { @@ -28,18 +28,18 @@ const mockEncryptedSavedObjects = { describe('create()', () => { test('creates an action with all given properties', async () => { const expectedResult = Symbol(); - const connectorService = new ConnectorService(); - connectorService.register({ - id: 'my-connector', - name: 'My connector', + const actionTypeService = new ActionTypeService(); + actionTypeService.register({ + id: 'my-action-type', + name: 'My action type', async executor() {}, }); - const actionService = new ActionService(connectorService, mockEncryptedSavedObjects); + const actionService = new ActionService(actionTypeService, mockEncryptedSavedObjects); savedObjectsClient.create.mockResolvedValueOnce(expectedResult); const result = await actionService.create(savedObjectsClient, { description: 'my description', - connectorId: 'my-connector', - connectorOptions: {}, + actionTypeId: 'my-action-type', + actionTypeOptions: {}, }); expect(result).toEqual(expectedResult); expect(savedObjectsClient.create).toMatchInlineSnapshot(` @@ -48,9 +48,9 @@ describe('create()', () => { Array [ "action", Object { - "connectorId": "my-connector", - "connectorOptions": Object {}, - "connectorOptionsSecrets": Object {}, + "actionTypeId": "my-action-type", + "actionTypeOptions": Object {}, + "actionTypeOptionsSecrets": Object {}, "description": "my description", }, ], @@ -65,14 +65,14 @@ describe('create()', () => { `); }); - test('validates connectorOptions', async () => { - const connectorService = new ConnectorService(); - const actionService = new ActionService(connectorService, mockEncryptedSavedObjects); - connectorService.register({ - id: 'my-connector', - name: 'My connector', + test('validates actionTypeOptions', async () => { + const actionTypeService = new ActionTypeService(); + const actionService = new ActionService(actionTypeService, mockEncryptedSavedObjects); + actionTypeService.register({ + id: 'my-action-type', + name: 'My action type', validate: { - connectorOptions: Joi.object() + actionTypeOptions: Joi.object() .keys({ param1: Joi.string().required(), }) @@ -83,43 +83,43 @@ describe('create()', () => { await expect( actionService.create(savedObjectsClient, { description: 'my description', - connectorId: 'my-connector', - connectorOptions: {}, + actionTypeId: 'my-action-type', + actionTypeOptions: {}, }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"child \\"param1\\" fails because [\\"param1\\" is required]"` ); }); - test(`throws an error when connector doesn't exist`, async () => { - const connectorService = new ConnectorService(); - const actionService = new ActionService(connectorService, mockEncryptedSavedObjects); + test(`throws an error when an action type doesn't exist`, async () => { + const actionTypeService = new ActionTypeService(); + const actionService = new ActionService(actionTypeService, mockEncryptedSavedObjects); await expect( actionService.create(savedObjectsClient, { description: 'my description', - connectorId: 'unregistered-connector', - connectorOptions: {}, + actionTypeId: 'unregistered-action-type', + actionTypeOptions: {}, }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Connector \\"unregistered-connector\\" is not registered."` + `"Action type \\"unregistered-action-type\\" is not registered."` ); }); - test('encrypts connector options unless specified not to', async () => { + test('encrypts action type options unless specified not to', async () => { const expectedResult = Symbol(); - const connectorService = new ConnectorService(); - connectorService.register({ - id: 'my-connector', - name: 'My connector', + const actionTypeService = new ActionTypeService(); + actionTypeService.register({ + id: 'my-action-type', + name: 'My action type', unencryptedAttributes: ['a', 'c'], async executor() {}, }); - const actionService = new ActionService(connectorService, mockEncryptedSavedObjects); + const actionService = new ActionService(actionTypeService, mockEncryptedSavedObjects); savedObjectsClient.create.mockResolvedValueOnce(expectedResult); const result = await actionService.create(savedObjectsClient, { description: 'my description', - connectorId: 'my-connector', - connectorOptions: { + actionTypeId: 'my-action-type', + actionTypeOptions: { a: true, b: true, c: true, @@ -132,12 +132,12 @@ describe('create()', () => { Array [ "action", Object { - "connectorId": "my-connector", - "connectorOptions": Object { + "actionTypeId": "my-action-type", + "actionTypeOptions": Object { "a": true, "c": true, }, - "connectorOptionsSecrets": Object { + "actionTypeOptionsSecrets": Object { "b": true, }, "description": "my description", @@ -158,8 +158,8 @@ describe('create()', () => { describe('get()', () => { test('calls savedObjectsClient with id', async () => { const expectedResult = Symbol(); - const connectorService = new ConnectorService(); - const actionService = new ActionService(connectorService, mockEncryptedSavedObjects); + const actionTypeService = new ActionTypeService(); + const actionService = new ActionService(actionTypeService, mockEncryptedSavedObjects); savedObjectsClient.get.mockResolvedValueOnce(expectedResult); const result = await actionService.get(savedObjectsClient, '1'); expect(result).toEqual(expectedResult); @@ -185,8 +185,8 @@ describe('get()', () => { describe('find()', () => { test('calls savedObjectsClient with parameters', async () => { const expectedResult = Symbol(); - const connectorService = new ConnectorService(); - const actionService = new ActionService(connectorService, mockEncryptedSavedObjects); + const actionTypeService = new ActionTypeService(); + const actionService = new ActionService(actionTypeService, mockEncryptedSavedObjects); savedObjectsClient.find.mockResolvedValueOnce(expectedResult); const result = await actionService.find(savedObjectsClient, {}); expect(result).toEqual(expectedResult); @@ -213,8 +213,8 @@ describe('find()', () => { describe('delete()', () => { test('calls savedObjectsClient with id', async () => { const expectedResult = Symbol(); - const connectorService = new ConnectorService(); - const actionService = new ActionService(connectorService, mockEncryptedSavedObjects); + const actionTypeService = new ActionTypeService(); + const actionService = new ActionService(actionTypeService, mockEncryptedSavedObjects); savedObjectsClient.delete.mockResolvedValueOnce(expectedResult); const result = await actionService.delete(savedObjectsClient, '1'); expect(result).toEqual(expectedResult); @@ -240,21 +240,21 @@ describe('delete()', () => { describe('update()', () => { test('updates an action with all given properties', async () => { const expectedResult = Symbol(); - const connectorService = new ConnectorService(); - connectorService.register({ - id: 'my-connector', - name: 'My connector', + const actionTypeService = new ActionTypeService(); + actionTypeService.register({ + id: 'my-action-type', + name: 'My action type', async executor() {}, }); - const actionService = new ActionService(connectorService, mockEncryptedSavedObjects); + const actionService = new ActionService(actionTypeService, mockEncryptedSavedObjects); savedObjectsClient.update.mockResolvedValueOnce(expectedResult); const result = await actionService.update( savedObjectsClient, 'my-alert', { description: 'my description', - connectorId: 'my-connector', - connectorOptions: {}, + actionTypeId: 'my-action-type', + actionTypeOptions: {}, }, {} ); @@ -266,9 +266,9 @@ describe('update()', () => { "action", "my-alert", Object { - "connectorId": "my-connector", - "connectorOptions": Object {}, - "connectorOptionsSecrets": Object {}, + "actionTypeId": "my-action-type", + "actionTypeOptions": Object {}, + "actionTypeOptionsSecrets": Object {}, "description": "my description", }, Object {}, @@ -284,14 +284,14 @@ describe('update()', () => { `); }); - test('validates connectorOptions', async () => { - const connectorService = new ConnectorService(); - const actionService = new ActionService(connectorService, mockEncryptedSavedObjects); - connectorService.register({ - id: 'my-connector', - name: 'My connector', + test('validates actionTypeOptions', async () => { + const actionTypeService = new ActionTypeService(); + const actionService = new ActionService(actionTypeService, mockEncryptedSavedObjects); + actionTypeService.register({ + id: 'my-action-type', + name: 'My action type', validate: { - connectorOptions: Joi.object() + actionTypeOptions: Joi.object() .keys({ param1: Joi.string().required(), }) @@ -305,8 +305,8 @@ describe('update()', () => { 'my-alert', { description: 'my description', - connectorId: 'my-connector', - connectorOptions: {}, + actionTypeId: 'my-action-type', + actionTypeOptions: {}, }, {} ) @@ -315,43 +315,43 @@ describe('update()', () => { ); }); - test(`throws an error when connector doesn't exist`, async () => { - const connectorService = new ConnectorService(); - const actionService = new ActionService(connectorService, mockEncryptedSavedObjects); + test(`throws an error when action type doesn't exist`, async () => { + const actionTypeService = new ActionTypeService(); + const actionService = new ActionService(actionTypeService, mockEncryptedSavedObjects); await expect( actionService.update( savedObjectsClient, 'my-alert', { description: 'my description', - connectorId: 'unregistered-connector', - connectorOptions: {}, + actionTypeId: 'unregistered-action-type', + actionTypeOptions: {}, }, {} ) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Connector \\"unregistered-connector\\" is not registered."` + `"Action type \\"unregistered-action-type\\" is not registered."` ); }); - test('encrypts connector options unless specified not to', async () => { + test('encrypts action type options unless specified not to', async () => { const expectedResult = Symbol(); - const connectorService = new ConnectorService(); - connectorService.register({ - id: 'my-connector', - name: 'My connector', + const actionTypeService = new ActionTypeService(); + actionTypeService.register({ + id: 'my-action-type', + name: 'My action type', unencryptedAttributes: ['a', 'c'], async executor() {}, }); - const actionService = new ActionService(connectorService, mockEncryptedSavedObjects); + const actionService = new ActionService(actionTypeService, mockEncryptedSavedObjects); savedObjectsClient.update.mockResolvedValueOnce(expectedResult); const result = await actionService.update( savedObjectsClient, 'my-alert', { description: 'my description', - connectorId: 'my-connector', - connectorOptions: { + actionTypeId: 'my-action-type', + actionTypeOptions: { a: true, b: true, c: true, @@ -367,12 +367,12 @@ describe('update()', () => { "action", "my-alert", Object { - "connectorId": "my-connector", - "connectorOptions": Object { + "actionTypeId": "my-action-type", + "actionTypeOptions": Object { "a": true, "c": true, }, - "connectorOptionsSecrets": Object { + "actionTypeOptionsSecrets": Object { "b": true, }, "description": "my description", @@ -393,19 +393,19 @@ describe('update()', () => { describe('fire()', () => { test('fires an action with all given parameters', async () => { - const connectorService = new ConnectorService(); - const actionService = new ActionService(connectorService, mockEncryptedSavedObjects); - const mockConnector = jest.fn().mockResolvedValueOnce({ success: true }); - connectorService.register({ + const actionTypeService = new ActionTypeService(); + const actionService = new ActionService(actionTypeService, mockEncryptedSavedObjects); + const mockActionType = jest.fn().mockResolvedValueOnce({ success: true }); + actionTypeService.register({ id: 'mock', name: 'Mock', - executor: mockConnector, + executor: mockActionType, }); mockEncryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ id: 'mock-action', attributes: { - connectorId: 'mock', - connectorOptionsSecrets: { + actionTypeId: 'mock', + actionTypeOptionsSecrets: { foo: true, }, }, @@ -416,7 +416,7 @@ describe('fire()', () => { savedObjectsClient, }); expect(result).toEqual({ success: true }); - expect(mockConnector).toMatchInlineSnapshot(` + expect(mockActionType).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ Array [ @@ -441,14 +441,14 @@ describe('fire()', () => { ]); }); - test(`throws an error when the connector isn't registered`, async () => { - const connectorService = new ConnectorService(); - const actionService = new ActionService(connectorService, mockEncryptedSavedObjects); + test(`throws an error when the action type isn't registered`, async () => { + const actionTypeService = new ActionTypeService(); + const actionService = new ActionService(actionTypeService, mockEncryptedSavedObjects); mockEncryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ id: 'mock-action', attributes: { - connectorId: 'non-registered-connector', - connectorOptionsSecrets: { + actionTypeId: 'non-registered-action-type', + actionTypeOptionsSecrets: { foo: true, }, }, @@ -456,29 +456,29 @@ describe('fire()', () => { await expect( actionService.fire({ savedObjectsClient, id: 'mock-action', params: { baz: false } }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Connector \\"non-registered-connector\\" is not registered."` + `"Action type \\"non-registered-action-type\\" is not registered."` ); }); test('merges encrypted and unencrypted attributes', async () => { - const connectorService = new ConnectorService(); - const actionService = new ActionService(connectorService, mockEncryptedSavedObjects); - const mockConnector = jest.fn().mockResolvedValueOnce({ success: true }); - connectorService.register({ + const actionTypeService = new ActionTypeService(); + const actionService = new ActionService(actionTypeService, mockEncryptedSavedObjects); + const mockActionType = jest.fn().mockResolvedValueOnce({ success: true }); + actionTypeService.register({ id: 'mock', name: 'Mock', unencryptedAttributes: ['a', 'c'], - executor: mockConnector, + executor: mockActionType, }); mockEncryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ id: 'mock-action', attributes: { - connectorId: 'mock', - connectorOptions: { + actionTypeId: 'mock', + actionTypeOptions: { a: true, c: true, }, - connectorOptionsSecrets: { + actionTypeOptionsSecrets: { b: true, }, }, @@ -489,7 +489,7 @@ describe('fire()', () => { savedObjectsClient, }); expect(result).toEqual({ success: true }); - expect(mockConnector).toMatchInlineSnapshot(` + expect(mockActionType).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ Array [ diff --git a/x-pack/plugins/alerting/server/action_service.ts b/x-pack/plugins/alerting/server/action_service.ts index 54771ce98e0e6..c46e24c3360f6 100644 --- a/x-pack/plugins/alerting/server/action_service.ts +++ b/x-pack/plugins/alerting/server/action_service.ts @@ -6,19 +6,19 @@ import Boom from 'boom'; import { SavedObjectsClient } from 'src/legacy/server/saved_objects'; -import { ConnectorService } from './connector_service'; +import { ActionTypeService } from './action_type_service'; interface Action { description: string; - connectorId: string; - connectorOptions: { [key: string]: any }; + actionTypeId: string; + actionTypeOptions: { [key: string]: any }; } interface EncryptedAction extends Action { description: string; - connectorId: string; - connectorOptions: { [key: string]: any }; - connectorOptionsSecrets: { [key: string]: any }; + actionTypeId: string; + actionTypeOptions: { [key: string]: any }; + actionTypeOptionsSecrets: { [key: string]: any }; } interface FireActionOptions { @@ -48,22 +48,22 @@ interface FindOptions { } export class ActionService { - private connectorService: ConnectorService; + private actionTypeService: ActionTypeService; private encryptedSavedObjects: any; - constructor(connectorService: ConnectorService, encryptedSavedObjects: any) { - this.connectorService = connectorService; + constructor(actionTypeService: ActionTypeService, encryptedSavedObjects: any) { + this.actionTypeService = actionTypeService; this.encryptedSavedObjects = encryptedSavedObjects; } public async create(savedObjectsClient: SavedObjectsClient, data: Action) { - const { connectorId } = data; - if (!this.connectorService.has(connectorId)) { - throw Boom.badRequest(`Connector "${connectorId}" is not registered.`); + const { actionTypeId } = data; + if (!this.actionTypeService.has(actionTypeId)) { + throw Boom.badRequest(`Action type "${actionTypeId}" is not registered.`); } - this.connectorService.validateConnectorOptions(connectorId, data.connectorOptions); - const actionWithSplitConnectorOptions = this.applyEncryptedAttributes(data); - return await savedObjectsClient.create('action', actionWithSplitConnectorOptions); + this.actionTypeService.validateActionTypeOptions(actionTypeId, data.actionTypeOptions); + const actionWithSplitActionTypeOptions = this.applyEncryptedAttributes(data); + return await savedObjectsClient.create('action', actionWithSplitActionTypeOptions); } public async get(savedObjectsClient: SavedObjectsClient, id: string) { @@ -87,48 +87,50 @@ export class ActionService { data: Action, options: { version?: string; references?: SavedObjectReference[] } ) { - const { connectorId } = data; - if (!this.connectorService.has(connectorId)) { - throw Boom.badRequest(`Connector "${connectorId}" is not registered.`); + const { actionTypeId } = data; + if (!this.actionTypeService.has(actionTypeId)) { + throw Boom.badRequest(`Action type "${actionTypeId}" is not registered.`); } - this.connectorService.validateConnectorOptions(connectorId, data.connectorOptions); - const actionWithSplitConnectorOptions = this.applyEncryptedAttributes(data); + this.actionTypeService.validateActionTypeOptions(actionTypeId, data.actionTypeOptions); + const actionWithSplitActionTypeOptions = this.applyEncryptedAttributes(data); return await savedObjectsClient.update( 'action', id, - actionWithSplitConnectorOptions, + actionWithSplitActionTypeOptions, options ); } public async fire({ id, params, savedObjectsClient }: FireActionOptions) { const action = await this.encryptedSavedObjects.getDecryptedAsInternalUser('action', id); - return await this.connectorService.execute( - action.attributes.connectorId, + return await this.actionTypeService.execute( + action.attributes.actionTypeId, { - ...action.attributes.connectorOptions, - ...action.attributes.connectorOptionsSecrets, + ...action.attributes.actionTypeOptions, + ...action.attributes.actionTypeOptionsSecrets, }, params ); } private applyEncryptedAttributes(action: Action): EncryptedAction { - const unencryptedAttributes = this.connectorService.getEncryptedAttributes(action.connectorId); - const encryptedConnectorOptions: { [key: string]: any } = {}; - const unencryptedConnectorOptions: { [key: string]: any } = {}; - for (const key of Object.keys(action.connectorOptions)) { + const unencryptedAttributes = this.actionTypeService.getUnencryptedAttributes( + action.actionTypeId + ); + const encryptedActionTypeOptions: { [key: string]: any } = {}; + const unencryptedActionTypeOptions: { [key: string]: any } = {}; + for (const key of Object.keys(action.actionTypeOptions)) { if (unencryptedAttributes.includes(key)) { - unencryptedConnectorOptions[key] = action.connectorOptions[key]; + unencryptedActionTypeOptions[key] = action.actionTypeOptions[key]; continue; } - encryptedConnectorOptions[key] = action.connectorOptions[key]; + encryptedActionTypeOptions[key] = action.actionTypeOptions[key]; } return { ...action, // Important these overwrite attributes in data for encryption purposes - connectorOptions: unencryptedConnectorOptions, - connectorOptionsSecrets: encryptedConnectorOptions, + actionTypeOptions: unencryptedActionTypeOptions, + actionTypeOptionsSecrets: encryptedActionTypeOptions, }; } } diff --git a/x-pack/plugins/alerting/server/action_type_service.test.ts b/x-pack/plugins/alerting/server/action_type_service.test.ts new file mode 100644 index 0000000000000..f4eeaacf36f30 --- /dev/null +++ b/x-pack/plugins/alerting/server/action_type_service.test.ts @@ -0,0 +1,312 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Joi from 'joi'; +import { ActionTypeService } from './action_type_service'; + +describe('register()', () => { + test('able to register action types', () => { + const executor = jest.fn(); + const actionTypeService = new ActionTypeService(); + actionTypeService.register({ + id: 'my-action-type', + name: 'My action type', + executor, + }); + }); + + test('throws error if action type already registered', () => { + const executor = jest.fn(); + const actionTypeService = new ActionTypeService(); + actionTypeService.register({ + id: 'my-action-type', + name: 'My action type', + executor, + }); + expect(() => + actionTypeService.register({ + id: 'my-action-type', + name: 'My action type', + executor, + }) + ).toThrowErrorMatchingInlineSnapshot( + `"Action type \\"my-action-type\\" is already registered."` + ); + }); +}); + +describe('get()', () => { + test('returns action type', () => { + const actionTypeService = new ActionTypeService(); + actionTypeService.register({ + id: 'my-action-type', + name: 'My action type', + async executor() {}, + }); + const actionType = actionTypeService.get('my-action-type'); + expect(actionType).toMatchInlineSnapshot(` +Object { + "executor": [Function], + "id": "my-action-type", + "name": "My action type", +} +`); + }); + + test(`throws an error when action type doesn't exist`, () => { + const actionTypeService = new ActionTypeService(); + expect(() => actionTypeService.get('my-action-type')).toThrowErrorMatchingInlineSnapshot( + `"Action type \\"my-action-type\\" is not registered."` + ); + }); +}); + +describe('getUnencryptedAttributes()', () => { + test('returns empty array when unencryptedAttributes is undefined', () => { + const actionTypeService = new ActionTypeService(); + actionTypeService.register({ + id: 'my-action-type', + name: 'My action type', + async executor() {}, + }); + const result = actionTypeService.getUnencryptedAttributes('my-action-type'); + expect(result).toEqual([]); + }); + + test('returns values inside unencryptedAttributes array when it exists', () => { + const actionTypeService = new ActionTypeService(); + actionTypeService.register({ + id: 'my-action-type', + name: 'My action type', + unencryptedAttributes: ['a', 'b', 'c'], + async executor() {}, + }); + const result = actionTypeService.getUnencryptedAttributes('my-action-type'); + expect(result).toEqual(['a', 'b', 'c']); + }); +}); + +describe('list()', () => { + test('returns list of action types', () => { + const actionTypeService = new ActionTypeService(); + actionTypeService.register({ + id: 'my-action-type', + name: 'My action type', + async executor() {}, + }); + const actionTypes = actionTypeService.list(); + expect(actionTypes).toEqual([ + { + id: 'my-action-type', + name: 'My action type', + }, + ]); + }); +}); + +describe('validateParams()', () => { + test('should pass when validation not defined', () => { + const actionTypeService = new ActionTypeService(); + actionTypeService.register({ + id: 'my-action-type', + name: 'My action type', + async executor() {}, + }); + actionTypeService.validateParams('my-action-type', {}); + }); + + test('should validate and pass when params is valid', () => { + const actionTypeService = new ActionTypeService(); + actionTypeService.register({ + id: 'my-action-type', + name: 'My action type', + validate: { + params: Joi.object() + .keys({ + param1: Joi.string().required(), + }) + .required(), + }, + async executor() {}, + }); + actionTypeService.validateParams('my-action-type', { param1: 'value' }); + }); + + test('should validate and throw error when params is invalid', () => { + const actionTypeService = new ActionTypeService(); + actionTypeService.register({ + id: 'my-action-type', + name: 'My action type', + validate: { + params: Joi.object() + .keys({ + param1: Joi.string().required(), + }) + .required(), + }, + async executor() {}, + }); + expect(() => + actionTypeService.validateParams('my-action-type', {}) + ).toThrowErrorMatchingInlineSnapshot( + `"child \\"param1\\" fails because [\\"param1\\" is required]"` + ); + }); +}); + +describe('validateActionTypeOptions()', () => { + test('should pass when validation not defined', () => { + const actionTypeService = new ActionTypeService(); + actionTypeService.register({ + id: 'my-action-type', + name: 'My action type', + async executor() {}, + }); + actionTypeService.validateActionTypeOptions('my-action-type', {}); + }); + + test('should validate and pass when actionTypeOptions is valid', () => { + const actionTypeService = new ActionTypeService(); + actionTypeService.register({ + id: 'my-action-type', + name: 'My action type', + validate: { + actionTypeOptions: Joi.object() + .keys({ + param1: Joi.string().required(), + }) + .required(), + }, + async executor() {}, + }); + actionTypeService.validateActionTypeOptions('my-action-type', { param1: 'value' }); + }); + + test('should validate and throw error when actionTypeOptions is invalid', () => { + const actionTypeService = new ActionTypeService(); + actionTypeService.register({ + id: 'my-action-type', + name: 'My action type', + validate: { + actionTypeOptions: Joi.object() + .keys({ + param1: Joi.string().required(), + }) + .required(), + }, + async executor() {}, + }); + expect(() => + actionTypeService.validateActionTypeOptions('my-action-type', {}) + ).toThrowErrorMatchingInlineSnapshot( + `"child \\"param1\\" fails because [\\"param1\\" is required]"` + ); + }); +}); + +describe('has()', () => { + test('returns false for unregistered action types', () => { + const actionTypeService = new ActionTypeService(); + expect(actionTypeService.has('my-action-type')).toEqual(false); + }); + + test('returns true after registering an action type', () => { + const executor = jest.fn(); + const actionTypeService = new ActionTypeService(); + actionTypeService.register({ + id: 'my-action-type', + name: 'My action type', + executor, + }); + expect(actionTypeService.has('my-action-type')); + }); +}); + +describe('execute()', () => { + test('calls the executor with proper params', async () => { + const executor = jest.fn().mockResolvedValueOnce({ success: true }); + const actionTypeService = new ActionTypeService(); + actionTypeService.register({ + id: 'my-action-type', + name: 'My action type', + executor, + }); + await actionTypeService.execute('my-action-type', { foo: true }, { bar: false }); + expect(executor).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + Object { + "foo": true, + }, + Object { + "bar": false, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], +} +`); + }); + + test('validates params', async () => { + const executor = jest.fn().mockResolvedValueOnce({ success: true }); + const actionTypeService = new ActionTypeService(); + actionTypeService.register({ + id: 'my-action-type', + name: 'My action type', + executor, + validate: { + params: Joi.object() + .keys({ + param1: Joi.string().required(), + }) + .required(), + }, + }); + await expect( + actionTypeService.execute('my-action-type', {}, {}) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"child \\"param1\\" fails because [\\"param1\\" is required]"` + ); + }); + + test('validates actionTypeOptions', async () => { + const executor = jest.fn().mockResolvedValueOnce({ success: true }); + const actionTypeService = new ActionTypeService(); + actionTypeService.register({ + id: 'my-action-type', + name: 'My action type', + executor, + validate: { + actionTypeOptions: Joi.object() + .keys({ + param1: Joi.string().required(), + }) + .required(), + }, + }); + await expect( + actionTypeService.execute('my-action-type', {}, {}) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"child \\"param1\\" fails because [\\"param1\\" is required]"` + ); + }); + + test('throws error if action type not registered', async () => { + const actionTypeService = new ActionTypeService(); + await expect( + actionTypeService.execute('my-action-type', { foo: true }, { bar: false }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Action type \\"my-action-type\\" is not registered."` + ); + }); +}); diff --git a/x-pack/plugins/alerting/server/action_type_service.ts b/x-pack/plugins/alerting/server/action_type_service.ts new file mode 100644 index 0000000000000..689abe51006b0 --- /dev/null +++ b/x-pack/plugins/alerting/server/action_type_service.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; + +interface ActionType { + id: string; + name: string; + unencryptedAttributes?: string[]; + validate?: { + params?: any; + actionTypeOptions?: any; + }; + executor(actionTypeOptions: any, params: any): Promise; +} + +export class ActionTypeService { + private actionTypes: { [id: string]: ActionType } = {}; + + public has(id: string) { + return !!this.actionTypes[id]; + } + + public register(actionType: ActionType) { + if (this.has(actionType.id)) { + throw Boom.badRequest(`Action type "${actionType.id}" is already registered.`); + } + this.actionTypes[actionType.id] = actionType; + } + + public get(id: string) { + const actionType = this.actionTypes[id]; + if (!actionType) { + throw Boom.badRequest(`Action type "${id}" is not registered.`); + } + return actionType; + } + + public getUnencryptedAttributes(id: string) { + const actionType = this.get(id); + return actionType.unencryptedAttributes || []; + } + + public list() { + const actionTypeIds = Object.keys(this.actionTypes); + return actionTypeIds.map(actionTypeId => ({ + id: actionTypeId, + name: this.actionTypes[actionTypeId].name, + })); + } + + public validateParams(id: string, params: any) { + const actionType = this.get(id); + const validator = actionType.validate && actionType.validate.params; + if (validator) { + const { error } = validator.validate(params); + if (error) { + throw error; + } + } + } + + public validateActionTypeOptions(id: string, actionTypeOptions: any) { + const actionType = this.get(id); + const validator = actionType.validate && actionType.validate.actionTypeOptions; + if (validator) { + const { error } = validator.validate(actionTypeOptions); + if (error) { + throw error; + } + } + } + + public async execute(id: string, actionTypeOptions: any, params: any) { + const actionType = this.get(id); + this.validateActionTypeOptions(id, actionTypeOptions); + this.validateParams(id, params); + return await actionType.executor(actionTypeOptions, params); + } +} diff --git a/x-pack/plugins/alerting/server/alert_service.ts b/x-pack/plugins/alerting/server/alert_service.ts deleted file mode 100644 index f6b2e509480d2..0000000000000 --- a/x-pack/plugins/alerting/server/alert_service.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export class AlertService {} diff --git a/x-pack/plugins/alerting/server/connector_service.test.ts b/x-pack/plugins/alerting/server/connector_service.test.ts deleted file mode 100644 index 1cb080b185613..0000000000000 --- a/x-pack/plugins/alerting/server/connector_service.test.ts +++ /dev/null @@ -1,310 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Joi from 'joi'; -import { ConnectorService } from './connector_service'; - -describe('register()', () => { - test('able to register connectors', () => { - const executor = jest.fn(); - const connectorService = new ConnectorService(); - connectorService.register({ - id: 'my-connector', - name: 'My connector', - executor, - }); - }); - - test('throws error if connector already registered', () => { - const executor = jest.fn(); - const connectorService = new ConnectorService(); - connectorService.register({ - id: 'my-connector', - name: 'My connector', - executor, - }); - expect(() => - connectorService.register({ - id: 'my-connector', - name: 'My connector', - executor, - }) - ).toThrowErrorMatchingInlineSnapshot(`"Connector \\"my-connector\\" is already registered."`); - }); -}); - -describe('get()', () => { - test('returns connector', () => { - const connectorService = new ConnectorService(); - connectorService.register({ - id: 'my-connector', - name: 'My connector', - async executor() {}, - }); - const connector = connectorService.get('my-connector'); - expect(connector).toMatchInlineSnapshot(` -Object { - "executor": [Function], - "id": "my-connector", - "name": "My connector", -} -`); - }); - - test(`throws an error when connector doesn't exist`, () => { - const connectorService = new ConnectorService(); - expect(() => connectorService.get('my-connector')).toThrowErrorMatchingInlineSnapshot( - `"Connector \\"my-connector\\" is not registered."` - ); - }); -}); - -describe('getEncryptedAttributes()', () => { - test('returns empty array when unencryptedAttributes is undefined', () => { - const connectorService = new ConnectorService(); - connectorService.register({ - id: 'my-connector', - name: 'My connector', - async executor() {}, - }); - const result = connectorService.getEncryptedAttributes('my-connector'); - expect(result).toEqual([]); - }); - - test('returns values inside unencryptedAttributes array when it exists', () => { - const connectorService = new ConnectorService(); - connectorService.register({ - id: 'my-connector', - name: 'My connector', - unencryptedAttributes: ['a', 'b', 'c'], - async executor() {}, - }); - const result = connectorService.getEncryptedAttributes('my-connector'); - expect(result).toEqual(['a', 'b', 'c']); - }); -}); - -describe('list()', () => { - test('returns list of connectors', () => { - const connectorService = new ConnectorService(); - connectorService.register({ - id: 'my-connector', - name: 'My connector', - async executor() {}, - }); - const connectors = connectorService.list(); - expect(connectors).toEqual([ - { - id: 'my-connector', - name: 'My connector', - }, - ]); - }); -}); - -describe('validateParams()', () => { - test('should pass when validation not defined', () => { - const connectorService = new ConnectorService(); - connectorService.register({ - id: 'my-connector', - name: 'My connector', - async executor() {}, - }); - connectorService.validateParams('my-connector', {}); - }); - - test('should validate and pass when params is valid', () => { - const connectorService = new ConnectorService(); - connectorService.register({ - id: 'my-connector', - name: 'My connector', - validate: { - params: Joi.object() - .keys({ - param1: Joi.string().required(), - }) - .required(), - }, - async executor() {}, - }); - connectorService.validateParams('my-connector', { param1: 'value' }); - }); - - test('should validate and throw error when params is invalid', () => { - const connectorService = new ConnectorService(); - connectorService.register({ - id: 'my-connector', - name: 'My connector', - validate: { - params: Joi.object() - .keys({ - param1: Joi.string().required(), - }) - .required(), - }, - async executor() {}, - }); - expect(() => - connectorService.validateParams('my-connector', {}) - ).toThrowErrorMatchingInlineSnapshot( - `"child \\"param1\\" fails because [\\"param1\\" is required]"` - ); - }); -}); - -describe('validateConnectorOptions()', () => { - test('should pass when validation not defined', () => { - const connectorService = new ConnectorService(); - connectorService.register({ - id: 'my-connector', - name: 'My connector', - async executor() {}, - }); - connectorService.validateConnectorOptions('my-connector', {}); - }); - - test('should validate and pass when connectorOptions is valid', () => { - const connectorService = new ConnectorService(); - connectorService.register({ - id: 'my-connector', - name: 'My connector', - validate: { - connectorOptions: Joi.object() - .keys({ - param1: Joi.string().required(), - }) - .required(), - }, - async executor() {}, - }); - connectorService.validateConnectorOptions('my-connector', { param1: 'value' }); - }); - - test('should validate and throw error when connectorOptions is invalid', () => { - const connectorService = new ConnectorService(); - connectorService.register({ - id: 'my-connector', - name: 'My connector', - validate: { - connectorOptions: Joi.object() - .keys({ - param1: Joi.string().required(), - }) - .required(), - }, - async executor() {}, - }); - expect(() => - connectorService.validateConnectorOptions('my-connector', {}) - ).toThrowErrorMatchingInlineSnapshot( - `"child \\"param1\\" fails because [\\"param1\\" is required]"` - ); - }); -}); - -describe('has()', () => { - test('returns false for unregistered connectors', () => { - const connectorService = new ConnectorService(); - expect(connectorService.has('my-connector')).toEqual(false); - }); - - test('returns true after registering a connector', () => { - const executor = jest.fn(); - const connectorService = new ConnectorService(); - connectorService.register({ - id: 'my-connector', - name: 'My connector', - executor, - }); - expect(connectorService.has('my-connector')); - }); -}); - -describe('execute()', () => { - test('calls the executor with proper params', async () => { - const executor = jest.fn().mockResolvedValueOnce({ success: true }); - const connectorService = new ConnectorService(); - connectorService.register({ - id: 'my-connector', - name: 'My connector', - executor, - }); - await connectorService.execute('my-connector', { foo: true }, { bar: false }); - expect(executor).toMatchInlineSnapshot(` -[MockFunction] { - "calls": Array [ - Array [ - Object { - "foo": true, - }, - Object { - "bar": false, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], -} -`); - }); - - test('validates params', async () => { - const executor = jest.fn().mockResolvedValueOnce({ success: true }); - const connectorService = new ConnectorService(); - connectorService.register({ - id: 'my-connector', - name: 'My connector', - executor, - validate: { - params: Joi.object() - .keys({ - param1: Joi.string().required(), - }) - .required(), - }, - }); - await expect( - connectorService.execute('my-connector', {}, {}) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"child \\"param1\\" fails because [\\"param1\\" is required]"` - ); - }); - - test('validates connectorOptions', async () => { - const executor = jest.fn().mockResolvedValueOnce({ success: true }); - const connectorService = new ConnectorService(); - connectorService.register({ - id: 'my-connector', - name: 'My connector', - executor, - validate: { - connectorOptions: Joi.object() - .keys({ - param1: Joi.string().required(), - }) - .required(), - }, - }); - await expect( - connectorService.execute('my-connector', {}, {}) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"child \\"param1\\" fails because [\\"param1\\" is required]"` - ); - }); - - test('throws error if connector not registered', async () => { - const connectorService = new ConnectorService(); - await expect( - connectorService.execute('my-connector', { foo: true }, { bar: false }) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Connector \\"my-connector\\" is not registered."` - ); - }); -}); diff --git a/x-pack/plugins/alerting/server/connector_service.ts b/x-pack/plugins/alerting/server/connector_service.ts deleted file mode 100644 index ebb0469a70f3f..0000000000000 --- a/x-pack/plugins/alerting/server/connector_service.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; - -interface Connector { - id: string; - name: string; - unencryptedAttributes?: string[]; - validate?: { - params?: any; - connectorOptions?: any; - }; - executor(connectorOptions: any, params: any): Promise; -} - -export class ConnectorService { - private connectors: { [id: string]: Connector } = {}; - - public has(id: string) { - return !!this.connectors[id]; - } - - public register(connector: Connector) { - if (this.has(connector.id)) { - throw Boom.badRequest(`Connector "${connector.id}" is already registered.`); - } - this.connectors[connector.id] = connector; - } - - public get(id: string) { - const connector = this.connectors[id]; - if (!connector) { - throw Boom.badRequest(`Connector "${id}" is not registered.`); - } - return connector; - } - - public getEncryptedAttributes(id: string) { - const connector = this.get(id); - return connector.unencryptedAttributes || []; - } - - public list() { - const connectorIds = Object.keys(this.connectors); - return connectorIds.map(id => ({ - id, - name: this.connectors[id].name, - })); - } - - public validateParams(id: string, params: any) { - const connector = this.get(id); - const validator = connector.validate && connector.validate.params; - if (validator) { - const { error } = validator.validate(params); - if (error) { - throw error; - } - } - } - - public validateConnectorOptions(id: string, connectorOptions: any) { - const connector = this.get(id); - const validator = connector.validate && connector.validate.connectorOptions; - if (validator) { - const { error } = validator.validate(connectorOptions); - if (error) { - throw error; - } - } - } - - public async execute(id: string, connectorOptions: any, params: any) { - const connector = this.get(id); - this.validateConnectorOptions(id, connectorOptions); - this.validateParams(id, params); - return await connector.executor(connectorOptions, params); - } -} diff --git a/x-pack/plugins/alerting/server/index.ts b/x-pack/plugins/alerting/server/index.ts index e4235e0d02c07..e50ec0762a3fb 100644 --- a/x-pack/plugins/alerting/server/index.ts +++ b/x-pack/plugins/alerting/server/index.ts @@ -10,8 +10,7 @@ export { findActionRoute, getActionRoute, updateActionRoute, - listconnectorsRoute, + listActionTypesRoute, } from './routes'; export { ActionService } from './action_service'; -export { AlertService } from './alert_service'; -export { ConnectorService } from './connector_service'; +export { ActionTypeService } from './action_type_service'; diff --git a/x-pack/plugins/alerting/server/routes/create_action.ts b/x-pack/plugins/alerting/server/routes/create_action.ts index 2ef8164f4ea25..1adb89e39cb5e 100644 --- a/x-pack/plugins/alerting/server/routes/create_action.ts +++ b/x-pack/plugins/alerting/server/routes/create_action.ts @@ -6,19 +6,8 @@ import Joi from 'joi'; import Hapi from 'hapi'; -import { ActionService } from '../action_service'; -import { AlertService } from '../alert_service'; -import { ConnectorService } from '../connector_service'; import { APP_ID } from '../../common/constants'; -import { WithoutQueryAndParams, SavedObjectReference } from './types'; - -interface Server extends Hapi.Server { - alerting: () => { - actions: ActionService; - alerts: AlertService; - connectors: ConnectorService; - }; -} +import { WithoutQueryAndParams, SavedObjectReference, Server } from './types'; interface CreateActionRequest extends WithoutQueryAndParams { server: Server; @@ -31,8 +20,8 @@ interface CreateActionRequest extends WithoutQueryAndParams { payload: { attributes: { description: string; - connectorId: string; - connectorOptions: { [key: string]: any }; + actionTypeId: string; + actionTypeOptions: { [key: string]: any }; }; migrationVersion?: { [key: string]: string }; references: SavedObjectReference[]; @@ -49,8 +38,8 @@ export function createActionRoute(server: Hapi.Server) { attributes: Joi.object() .keys({ description: Joi.string().required(), - connectorId: Joi.string().required(), - connectorOptions: Joi.object(), + actionTypeId: Joi.string().required(), + actionTypeOptions: Joi.object(), }) .required(), migrationVersion: Joi.object().optional(), diff --git a/x-pack/plugins/alerting/server/routes/delete_action.ts b/x-pack/plugins/alerting/server/routes/delete_action.ts index dc08b0fb20487..279c59fab1cc4 100644 --- a/x-pack/plugins/alerting/server/routes/delete_action.ts +++ b/x-pack/plugins/alerting/server/routes/delete_action.ts @@ -8,17 +8,7 @@ import Hapi from 'hapi'; import Joi from 'joi'; import { APP_ID } from '../../common/constants'; -import { ActionService } from '../action_service'; -import { AlertService } from '../alert_service'; -import { ConnectorService } from '../connector_service'; - -interface Server extends Hapi.Server { - alerting: () => { - actions: ActionService; - alerts: AlertService; - connectors: ConnectorService; - }; -} +import { Server } from './types'; interface DeleteActionRequest extends Hapi.Request { server: Server; diff --git a/x-pack/plugins/alerting/server/routes/find_action.ts b/x-pack/plugins/alerting/server/routes/find_action.ts index 99f5bf0eb16ba..22fd08f29657b 100644 --- a/x-pack/plugins/alerting/server/routes/find_action.ts +++ b/x-pack/plugins/alerting/server/routes/find_action.ts @@ -8,18 +8,7 @@ import Joi from 'joi'; import Hapi from 'hapi'; import { APP_ID } from '../../common/constants'; -import { ActionService } from '../action_service'; -import { AlertService } from '../alert_service'; -import { ConnectorService } from '../connector_service'; -import { WithoutQueryAndParams } from './types'; - -interface Server extends Hapi.Server { - alerting: () => { - actions: ActionService; - alerts: AlertService; - connectors: ConnectorService; - }; -} +import { WithoutQueryAndParams, Server } from './types'; interface FindActionRequest extends WithoutQueryAndParams { server: Server; diff --git a/x-pack/plugins/alerting/server/routes/get_action.ts b/x-pack/plugins/alerting/server/routes/get_action.ts index 0f366435a0416..9fbe508ca287d 100644 --- a/x-pack/plugins/alerting/server/routes/get_action.ts +++ b/x-pack/plugins/alerting/server/routes/get_action.ts @@ -8,17 +8,7 @@ import Joi from 'joi'; import Hapi from 'hapi'; import { APP_ID } from '../../common/constants'; -import { ActionService } from '../action_service'; -import { AlertService } from '../alert_service'; -import { ConnectorService } from '../connector_service'; - -interface Server extends Hapi.Server { - alerting: () => { - actions: ActionService; - alerts: AlertService; - connectors: ConnectorService; - }; -} +import { Server } from './types'; interface GetActionRequest extends Hapi.Request { server: Server; diff --git a/x-pack/plugins/alerting/server/routes/index.ts b/x-pack/plugins/alerting/server/routes/index.ts index fccbfceb848e8..945774b706c69 100644 --- a/x-pack/plugins/alerting/server/routes/index.ts +++ b/x-pack/plugins/alerting/server/routes/index.ts @@ -9,4 +9,4 @@ export { deleteActionRoute } from './delete_action'; export { findActionRoute } from './find_action'; export { getActionRoute } from './get_action'; export { updateActionRoute } from './update_action'; -export { listconnectorsRoute } from './list_connectors'; +export { listActionTypesRoute } from './list_action_types'; diff --git a/x-pack/plugins/alerting/server/routes/list_action_types.ts b/x-pack/plugins/alerting/server/routes/list_action_types.ts new file mode 100644 index 0000000000000..acc4674f6dea5 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/list_action_types.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Hapi from 'hapi'; + +import { APP_ID } from '../../common/constants'; +import { Server } from './types'; + +interface ListActionTypesRequest extends Hapi.Request { + server: Server; +} + +export function listActionTypesRoute(server: any) { + server.route({ + method: 'GET', + path: `/api/${APP_ID}/action_types`, + async handler(request: ListActionTypesRequest) { + return request.server.alerting().actionTypes.list(); + }, + }); +} diff --git a/x-pack/plugins/alerting/server/routes/list_connectors.ts b/x-pack/plugins/alerting/server/routes/list_connectors.ts deleted file mode 100644 index f1246586e1fe4..0000000000000 --- a/x-pack/plugins/alerting/server/routes/list_connectors.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Hapi from 'hapi'; - -import { APP_ID } from '../../common/constants'; -import { ActionService } from '../action_service'; -import { AlertService } from '../alert_service'; -import { ConnectorService } from '../connector_service'; - -interface Server extends Hapi.Server { - alerting: () => { - actions: ActionService; - alerts: AlertService; - connectors: ConnectorService; - }; -} - -interface ListConnectorRequest extends Hapi.Request { - server: Server; -} - -export function listconnectorsRoute(server: any) { - server.route({ - method: 'GET', - path: `/api/${APP_ID}/connectors`, - async handler(request: ListConnectorRequest) { - return request.server.alerting().connectors.list(); - }, - }); -} diff --git a/x-pack/plugins/alerting/server/routes/types.ts b/x-pack/plugins/alerting/server/routes/types.ts index f2933fe82e237..5a8cb48669978 100644 --- a/x-pack/plugins/alerting/server/routes/types.ts +++ b/x-pack/plugins/alerting/server/routes/types.ts @@ -4,6 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import Hapi from 'hapi'; +import { ActionService } from '../action_service'; +import { ActionTypeService } from '../action_type_service'; + export type WithoutQueryAndParams = Pick>; export interface SavedObjectReference { @@ -11,3 +15,10 @@ export interface SavedObjectReference { type: string; id: string; } + +export interface Server extends Hapi.Server { + alerting: () => { + actions: ActionService; + actionTypes: ActionTypeService; + }; +} diff --git a/x-pack/plugins/alerting/server/routes/update_action.ts b/x-pack/plugins/alerting/server/routes/update_action.ts index 4e0e0a871edb4..cd3d4344186d5 100644 --- a/x-pack/plugins/alerting/server/routes/update_action.ts +++ b/x-pack/plugins/alerting/server/routes/update_action.ts @@ -8,26 +8,15 @@ import Joi from 'joi'; import Hapi from 'hapi'; import { APP_ID } from '../../common/constants'; -import { ActionService } from '../action_service'; -import { AlertService } from '../alert_service'; -import { ConnectorService } from '../connector_service'; -import { SavedObjectReference } from './types'; - -interface Server extends Hapi.Server { - alerting: () => { - actions: ActionService; - alerts: AlertService; - connectors: ConnectorService; - }; -} +import { SavedObjectReference, Server } from './types'; interface UpdateActionRequest extends Hapi.Request { server: Server; payload: { attributes: { description: string; - connectorId: string; - connectorOptions: { [key: string]: any }; + actionTypeId: string; + actionTypeOptions: { [key: string]: any }; }; version?: string; references: SavedObjectReference[]; @@ -50,8 +39,8 @@ export function updateActionRoute(server: Hapi.Server) { attributes: Joi.object() .keys({ description: Joi.string().required(), - connectorId: Joi.string().required(), - connectorOptions: Joi.object(), + actionTypeId: Joi.string().required(), + actionTypeOptions: Joi.object(), }) .required(), version: Joi.string(), diff --git a/x-pack/test/api_integration/apis/alerting/create_action.ts b/x-pack/test/api_integration/apis/alerting/create_action.ts index 72931426638a8..4b25f10489462 100644 --- a/x-pack/test/api_integration/apis/alerting/create_action.ts +++ b/x-pack/test/api_integration/apis/alerting/create_action.ts @@ -19,8 +19,8 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe .send({ attributes: { description: 'My action', - connectorId: 'test', - connectorOptions: {}, + actionTypeId: 'test', + actionTypeOptions: {}, }, }) .expect(200) @@ -30,8 +30,8 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe id: resp.body.id, attributes: { description: 'My action', - connectorId: 'test', - connectorOptions: {}, + actionTypeId: 'test', + actionTypeOptions: {}, }, references: [], updated_at: resp.body.updated_at, @@ -41,15 +41,15 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe }); }); - it(`should return 400 when connector isn't registered`, async () => { + it(`should return 400 when action type isn't registered`, async () => { await supertest .post('/api/alerting/action') .set('kbn-xsrf', 'foo') .send({ attributes: { description: 'My action', - connectorId: 'unregistered-connector', - connectorOptions: {}, + actionTypeId: 'unregistered-action-type', + actionTypeOptions: {}, }, }) .expect(400) @@ -57,7 +57,7 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe expect(resp.body).to.eql({ statusCode: 400, error: 'Bad Request', - message: 'Connector "unregistered-connector" is not registered.', + message: 'Action type "unregistered-action-type" is not registered.', }); }); }); diff --git a/x-pack/test/api_integration/apis/alerting/get_action.ts b/x-pack/test/api_integration/apis/alerting/get_action.ts index 08b248d635475..2e8f8adf2fece 100644 --- a/x-pack/test/api_integration/apis/alerting/get_action.ts +++ b/x-pack/test/api_integration/apis/alerting/get_action.ts @@ -27,9 +27,9 @@ export default function getActionTests({ getService }: KibanaFunctionalTestDefau references: [], version: resp.body.version, attributes: { - connectorId: 'test', + actionTypeId: 'test', description: 'My description', - connectorOptions: { + actionTypeOptions: { bar: false, foo: true, }, diff --git a/x-pack/test/api_integration/apis/alerting/index.ts b/x-pack/test/api_integration/apis/alerting/index.ts index 93909e824bc20..45c959aca71bc 100644 --- a/x-pack/test/api_integration/apis/alerting/index.ts +++ b/x-pack/test/api_integration/apis/alerting/index.ts @@ -13,7 +13,7 @@ export default function alertingTests({ loadTestFile }: KibanaFunctionalTestDefa loadTestFile(require.resolve('./delete_action')); loadTestFile(require.resolve('./find_action')); loadTestFile(require.resolve('./get_action')); - loadTestFile(require.resolve('./list_connectors')); + loadTestFile(require.resolve('./list_action_types')); loadTestFile(require.resolve('./update_action')); }); } diff --git a/x-pack/test/api_integration/apis/alerting/list_connectors.ts b/x-pack/test/api_integration/apis/alerting/list_action_types.ts similarity index 53% rename from x-pack/test/api_integration/apis/alerting/list_connectors.ts rename to x-pack/test/api_integration/apis/alerting/list_action_types.ts index 23076547cd77c..ce9827927128d 100644 --- a/x-pack/test/api_integration/apis/alerting/list_connectors.ts +++ b/x-pack/test/api_integration/apis/alerting/list_action_types.ts @@ -8,23 +8,23 @@ import expect from '@kbn/expect'; import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; // eslint-disable-next-line import/no-default-export -export default function listConnectorTests({ getService }: KibanaFunctionalTestDefaultProviders) { +export default function listActionTypesTests({ getService }: KibanaFunctionalTestDefaultProviders) { const supertest = getService('supertest'); - describe('list_connectors', () => { - it('should return 200 with list of connectors containing defaults', async () => { + describe('list_action_types', () => { + it('should return 200 with list of action types containing defaults', async () => { await supertest - .get('/api/alerting/connectors') + .get('/api/alerting/action_types') .expect(200) .then((resp: any) => { - function createConnectorMatcher(id: string, name: string) { - return (connector: { id: string; name: string }) => { - return connector.id === id && connector.name === name; + function createActionTypeMatcher(id: string, name: string) { + return (actionType: { id: string; name: string }) => { + return actionType.id === id && actionType.name === name; }; } // Check for values explicitly in order to avoid this test failing each time plugins register - // a new connector - expect(resp.body.some(createConnectorMatcher('test', 'Test'))).to.be(true); + // a new action type + expect(resp.body.some(createActionTypeMatcher('test', 'Test'))).to.be(true); }); }); }); diff --git a/x-pack/test/api_integration/apis/alerting/update_action.ts b/x-pack/test/api_integration/apis/alerting/update_action.ts index ce181458548ca..edf2f5d6ab3fb 100644 --- a/x-pack/test/api_integration/apis/alerting/update_action.ts +++ b/x-pack/test/api_integration/apis/alerting/update_action.ts @@ -22,9 +22,9 @@ export default function updateActionTests({ getService }: KibanaFunctionalTestDe .set('kbn-xsrf', 'foo') .send({ attributes: { - connectorId: 'test', + actionTypeId: 'test', description: 'My description updated', - connectorOptions: {}, + actionTypeOptions: {}, }, }) .expect(200) @@ -36,9 +36,9 @@ export default function updateActionTests({ getService }: KibanaFunctionalTestDe version: resp.body.version, updated_at: resp.body.updated_at, attributes: { - connectorId: 'test', + actionTypeId: 'test', description: 'My description updated', - connectorOptions: {}, + actionTypeOptions: {}, }, }); }); @@ -50,9 +50,9 @@ export default function updateActionTests({ getService }: KibanaFunctionalTestDe .set('kbn-xsrf', 'foo') .send({ attributes: { - connectorId: 'test', + actionTypeId: 'test', description: 'My description updated', - connectorOptions: {}, + actionTypeOptions: {}, }, }) .expect(404) diff --git a/x-pack/test/api_integration/fixtures/plugins/alerts/index.ts b/x-pack/test/api_integration/fixtures/plugins/alerts/index.ts index 99300fa5eaf3a..6d8153ef7a74e 100644 --- a/x-pack/test/api_integration/fixtures/plugins/alerts/index.ts +++ b/x-pack/test/api_integration/fixtures/plugins/alerts/index.ts @@ -10,10 +10,10 @@ export default function(kibana: any) { require: ['alerting'], name: 'alerts', init(server: any) { - server.alerting().connectors.register({ + server.alerting().actionTypes.register({ id: 'test', name: 'Test', - async executor(connectorOptions: any, params: any) {}, + async executor(actionTypeOptions: any, params: any) {}, }); }, }); diff --git a/x-pack/test/functional/es_archives/alerting/basic/data.json b/x-pack/test/functional/es_archives/alerting/basic/data.json index 372dcbb22c960..c0896655bcc05 100644 --- a/x-pack/test/functional/es_archives/alerting/basic/data.json +++ b/x-pack/test/functional/es_archives/alerting/basic/data.json @@ -5,8 +5,8 @@ "source": { "action": { "description": "My description", - "connectorId": "test", - "connectorOptions": { + "actionTypeId": "test", + "actionTypeOptions": { "foo": true, "bar": false } diff --git a/x-pack/test/functional/es_archives/alerting/basic/mappings.json b/x-pack/test/functional/es_archives/alerting/basic/mappings.json index e8c6d3f32ced9..694deaef5cac4 100644 --- a/x-pack/test/functional/es_archives/alerting/basic/mappings.json +++ b/x-pack/test/functional/es_archives/alerting/basic/mappings.json @@ -53,12 +53,15 @@ "description": { "type": "text" }, - "connectorId": { + "actionTypeId": { "type": "keyword" }, - "connectorOptions": { + "actionTypeOptions": { "dynamic": "true", "type": "object" + }, + "actionTypeOptionsSecrets": { + "type": "binary" } } }, From 9915fb4fc9fce93204b3f6280fe21f861b3661cf Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Tue, 7 May 2019 20:44:37 -0400 Subject: [PATCH 27/51] Rename actionTypeOptions to actionTypeConfig --- x-pack/plugins/alerting/index.ts | 47 +---------------- x-pack/plugins/alerting/init.ts | 52 +++++++++++++++++++ x-pack/plugins/alerting/mappings.json | 4 +- .../alerting/server/action_service.test.ts | 48 ++++++++--------- .../plugins/alerting/server/action_service.ts | 36 ++++++------- .../server/action_type_service.test.ts | 30 ++++++----- .../alerting/server/action_type_service.ts | 21 +++++--- .../alerting/server/routes/create_action.ts | 4 +- .../alerting/server/routes/update_action.ts | 4 +- .../apis/alerting/create_action.ts | 6 +-- .../apis/alerting/get_action.ts | 2 +- .../apis/alerting/update_action.ts | 6 +-- .../fixtures/plugins/alerts/index.ts | 2 +- .../es_archives/alerting/basic/data.json | 2 +- .../es_archives/alerting/basic/mappings.json | 4 +- 15 files changed, 142 insertions(+), 126 deletions(-) create mode 100644 x-pack/plugins/alerting/init.ts diff --git a/x-pack/plugins/alerting/index.ts b/x-pack/plugins/alerting/index.ts index 815360af8c1b7..3527cf63ef87b 100644 --- a/x-pack/plugins/alerting/index.ts +++ b/x-pack/plugins/alerting/index.ts @@ -4,18 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import Hapi from 'hapi'; import mappings from './mappings.json'; -import { - createActionRoute, - deleteActionRoute, - findActionRoute, - getActionRoute, - updateActionRoute, - listActionTypesRoute, - ActionService, - ActionTypeService, -} from './server'; +import { init } from './init'; import { APP_ID } from './common/constants'; @@ -31,40 +21,7 @@ export function alerting(kibana: any) { }) .default(); }, - init(server: Hapi.Server) { - const alertingEnabled = server.config().get('xpack.alerting.enabled'); - - if (!alertingEnabled) { - server.log(['info', 'alerting'], 'Alerting app disabled by configuration'); - return; - } - - // Encrypted attributes - server.plugins.encrypted_saved_objects!.registerType({ - type: 'action', - attributesToEncrypt: new Set(['actionTypeOptionsSecrets']), - }); - - const actionTypeService = new ActionTypeService(); - const actionService = new ActionService( - actionTypeService, - server.plugins.encrypted_saved_objects - ); - - // Routes - createActionRoute(server); - deleteActionRoute(server); - getActionRoute(server); - findActionRoute(server); - updateActionRoute(server); - listActionTypesRoute(server); - - // Register service to server - server.decorate('server', 'alerting', () => ({ - actions: actionService, - actionTypes: actionTypeService, - })); - }, + init, uiExports: { mappings, }, diff --git a/x-pack/plugins/alerting/init.ts b/x-pack/plugins/alerting/init.ts new file mode 100644 index 0000000000000..a1431770e4744 --- /dev/null +++ b/x-pack/plugins/alerting/init.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Hapi from 'hapi'; +import { + createActionRoute, + deleteActionRoute, + findActionRoute, + getActionRoute, + updateActionRoute, + listActionTypesRoute, + ActionService, + ActionTypeService, +} from './server'; + +export function init(server: Hapi.Server) { + const alertingEnabled = server.config().get('xpack.alerting.enabled'); + + if (!alertingEnabled) { + server.log(['info', 'alerting'], 'Alerting app disabled by configuration'); + return; + } + + // Encrypted attributes + server.plugins.encrypted_saved_objects!.registerType({ + type: 'action', + attributesToEncrypt: new Set(['actionTypeConfigSecrets']), + }); + + const actionTypeService = new ActionTypeService(); + const actionService = new ActionService( + actionTypeService, + server.plugins.encrypted_saved_objects + ); + + // Routes + createActionRoute(server); + deleteActionRoute(server); + getActionRoute(server); + findActionRoute(server); + updateActionRoute(server); + listActionTypesRoute(server); + + // Register service to server + server.decorate('server', 'alerting', () => ({ + actions: actionService, + actionTypes: actionTypeService, + })); +} diff --git a/x-pack/plugins/alerting/mappings.json b/x-pack/plugins/alerting/mappings.json index 4fa46c0a537b7..d538220dae452 100644 --- a/x-pack/plugins/alerting/mappings.json +++ b/x-pack/plugins/alerting/mappings.json @@ -7,11 +7,11 @@ "actionTypeId": { "type": "keyword" }, - "actionTypeOptions": { + "actionTypeConfig": { "dynamic": "true", "type": "object" }, - "actionTypeOptionsSecrets": { + "actionTypeConfigSecrets": { "type": "binary" } } diff --git a/x-pack/plugins/alerting/server/action_service.test.ts b/x-pack/plugins/alerting/server/action_service.test.ts index 52418e535a893..a5bff39b9181c 100644 --- a/x-pack/plugins/alerting/server/action_service.test.ts +++ b/x-pack/plugins/alerting/server/action_service.test.ts @@ -39,7 +39,7 @@ describe('create()', () => { const result = await actionService.create(savedObjectsClient, { description: 'my description', actionTypeId: 'my-action-type', - actionTypeOptions: {}, + actionTypeConfig: {}, }); expect(result).toEqual(expectedResult); expect(savedObjectsClient.create).toMatchInlineSnapshot(` @@ -49,8 +49,8 @@ describe('create()', () => { "action", Object { "actionTypeId": "my-action-type", - "actionTypeOptions": Object {}, - "actionTypeOptionsSecrets": Object {}, + "actionTypeConfig": Object {}, + "actionTypeConfigSecrets": Object {}, "description": "my description", }, ], @@ -65,14 +65,14 @@ describe('create()', () => { `); }); - test('validates actionTypeOptions', async () => { + test('validates actionTypeConfig', async () => { const actionTypeService = new ActionTypeService(); const actionService = new ActionService(actionTypeService, mockEncryptedSavedObjects); actionTypeService.register({ id: 'my-action-type', name: 'My action type', validate: { - actionTypeOptions: Joi.object() + actionTypeConfig: Joi.object() .keys({ param1: Joi.string().required(), }) @@ -84,7 +84,7 @@ describe('create()', () => { actionService.create(savedObjectsClient, { description: 'my description', actionTypeId: 'my-action-type', - actionTypeOptions: {}, + actionTypeConfig: {}, }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"child \\"param1\\" fails because [\\"param1\\" is required]"` @@ -98,7 +98,7 @@ describe('create()', () => { actionService.create(savedObjectsClient, { description: 'my description', actionTypeId: 'unregistered-action-type', - actionTypeOptions: {}, + actionTypeConfig: {}, }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Action type \\"unregistered-action-type\\" is not registered."` @@ -119,7 +119,7 @@ describe('create()', () => { const result = await actionService.create(savedObjectsClient, { description: 'my description', actionTypeId: 'my-action-type', - actionTypeOptions: { + actionTypeConfig: { a: true, b: true, c: true, @@ -133,11 +133,11 @@ describe('create()', () => { "action", Object { "actionTypeId": "my-action-type", - "actionTypeOptions": Object { + "actionTypeConfig": Object { "a": true, "c": true, }, - "actionTypeOptionsSecrets": Object { + "actionTypeConfigSecrets": Object { "b": true, }, "description": "my description", @@ -254,7 +254,7 @@ describe('update()', () => { { description: 'my description', actionTypeId: 'my-action-type', - actionTypeOptions: {}, + actionTypeConfig: {}, }, {} ); @@ -267,8 +267,8 @@ describe('update()', () => { "my-alert", Object { "actionTypeId": "my-action-type", - "actionTypeOptions": Object {}, - "actionTypeOptionsSecrets": Object {}, + "actionTypeConfig": Object {}, + "actionTypeConfigSecrets": Object {}, "description": "my description", }, Object {}, @@ -284,14 +284,14 @@ describe('update()', () => { `); }); - test('validates actionTypeOptions', async () => { + test('validates actionTypeConfig', async () => { const actionTypeService = new ActionTypeService(); const actionService = new ActionService(actionTypeService, mockEncryptedSavedObjects); actionTypeService.register({ id: 'my-action-type', name: 'My action type', validate: { - actionTypeOptions: Joi.object() + actionTypeConfig: Joi.object() .keys({ param1: Joi.string().required(), }) @@ -306,7 +306,7 @@ describe('update()', () => { { description: 'my description', actionTypeId: 'my-action-type', - actionTypeOptions: {}, + actionTypeConfig: {}, }, {} ) @@ -325,7 +325,7 @@ describe('update()', () => { { description: 'my description', actionTypeId: 'unregistered-action-type', - actionTypeOptions: {}, + actionTypeConfig: {}, }, {} ) @@ -351,7 +351,7 @@ describe('update()', () => { { description: 'my description', actionTypeId: 'my-action-type', - actionTypeOptions: { + actionTypeConfig: { a: true, b: true, c: true, @@ -368,11 +368,11 @@ describe('update()', () => { "my-alert", Object { "actionTypeId": "my-action-type", - "actionTypeOptions": Object { + "actionTypeConfig": Object { "a": true, "c": true, }, - "actionTypeOptionsSecrets": Object { + "actionTypeConfigSecrets": Object { "b": true, }, "description": "my description", @@ -405,7 +405,7 @@ describe('fire()', () => { id: 'mock-action', attributes: { actionTypeId: 'mock', - actionTypeOptionsSecrets: { + actionTypeConfigSecrets: { foo: true, }, }, @@ -448,7 +448,7 @@ describe('fire()', () => { id: 'mock-action', attributes: { actionTypeId: 'non-registered-action-type', - actionTypeOptionsSecrets: { + actionTypeConfigSecrets: { foo: true, }, }, @@ -474,11 +474,11 @@ describe('fire()', () => { id: 'mock-action', attributes: { actionTypeId: 'mock', - actionTypeOptions: { + actionTypeConfig: { a: true, c: true, }, - actionTypeOptionsSecrets: { + actionTypeConfigSecrets: { b: true, }, }, diff --git a/x-pack/plugins/alerting/server/action_service.ts b/x-pack/plugins/alerting/server/action_service.ts index c46e24c3360f6..0e24cb9e40857 100644 --- a/x-pack/plugins/alerting/server/action_service.ts +++ b/x-pack/plugins/alerting/server/action_service.ts @@ -11,14 +11,14 @@ import { ActionTypeService } from './action_type_service'; interface Action { description: string; actionTypeId: string; - actionTypeOptions: { [key: string]: any }; + actionTypeConfig: { [key: string]: any }; } interface EncryptedAction extends Action { description: string; actionTypeId: string; - actionTypeOptions: { [key: string]: any }; - actionTypeOptionsSecrets: { [key: string]: any }; + actionTypeConfig: { [key: string]: any }; + actionTypeConfigSecrets: { [key: string]: any }; } interface FireActionOptions { @@ -61,9 +61,9 @@ export class ActionService { if (!this.actionTypeService.has(actionTypeId)) { throw Boom.badRequest(`Action type "${actionTypeId}" is not registered.`); } - this.actionTypeService.validateActionTypeOptions(actionTypeId, data.actionTypeOptions); - const actionWithSplitActionTypeOptions = this.applyEncryptedAttributes(data); - return await savedObjectsClient.create('action', actionWithSplitActionTypeOptions); + this.actionTypeService.validateActionTypeConfig(actionTypeId, data.actionTypeConfig); + const actionWithSplitActionTypeConfig = this.applyEncryptedAttributes(data); + return await savedObjectsClient.create('action', actionWithSplitActionTypeConfig); } public async get(savedObjectsClient: SavedObjectsClient, id: string) { @@ -91,12 +91,12 @@ export class ActionService { if (!this.actionTypeService.has(actionTypeId)) { throw Boom.badRequest(`Action type "${actionTypeId}" is not registered.`); } - this.actionTypeService.validateActionTypeOptions(actionTypeId, data.actionTypeOptions); - const actionWithSplitActionTypeOptions = this.applyEncryptedAttributes(data); + this.actionTypeService.validateActionTypeConfig(actionTypeId, data.actionTypeConfig); + const actionWithSplitActionTypeConfig = this.applyEncryptedAttributes(data); return await savedObjectsClient.update( 'action', id, - actionWithSplitActionTypeOptions, + actionWithSplitActionTypeConfig, options ); } @@ -106,8 +106,8 @@ export class ActionService { return await this.actionTypeService.execute( action.attributes.actionTypeId, { - ...action.attributes.actionTypeOptions, - ...action.attributes.actionTypeOptionsSecrets, + ...action.attributes.actionTypeConfig, + ...action.attributes.actionTypeConfigSecrets, }, params ); @@ -117,20 +117,20 @@ export class ActionService { const unencryptedAttributes = this.actionTypeService.getUnencryptedAttributes( action.actionTypeId ); - const encryptedActionTypeOptions: { [key: string]: any } = {}; - const unencryptedActionTypeOptions: { [key: string]: any } = {}; - for (const key of Object.keys(action.actionTypeOptions)) { + const encryptedActionTypeConfig: { [key: string]: any } = {}; + const unencryptedActionTypeConfig: { [key: string]: any } = {}; + for (const key of Object.keys(action.actionTypeConfig)) { if (unencryptedAttributes.includes(key)) { - unencryptedActionTypeOptions[key] = action.actionTypeOptions[key]; + unencryptedActionTypeConfig[key] = action.actionTypeConfig[key]; continue; } - encryptedActionTypeOptions[key] = action.actionTypeOptions[key]; + encryptedActionTypeConfig[key] = action.actionTypeConfig[key]; } return { ...action, // Important these overwrite attributes in data for encryption purposes - actionTypeOptions: unencryptedActionTypeOptions, - actionTypeOptionsSecrets: encryptedActionTypeOptions, + actionTypeConfig: unencryptedActionTypeConfig, + actionTypeConfigSecrets: encryptedActionTypeConfig, }; } } diff --git a/x-pack/plugins/alerting/server/action_type_service.test.ts b/x-pack/plugins/alerting/server/action_type_service.test.ts index f4eeaacf36f30..3e27d49df7804 100644 --- a/x-pack/plugins/alerting/server/action_type_service.test.ts +++ b/x-pack/plugins/alerting/server/action_type_service.test.ts @@ -157,7 +157,7 @@ describe('validateParams()', () => { }); }); -describe('validateActionTypeOptions()', () => { +describe('validateActionTypeConfig()', () => { test('should pass when validation not defined', () => { const actionTypeService = new ActionTypeService(); actionTypeService.register({ @@ -165,16 +165,16 @@ describe('validateActionTypeOptions()', () => { name: 'My action type', async executor() {}, }); - actionTypeService.validateActionTypeOptions('my-action-type', {}); + actionTypeService.validateActionTypeConfig('my-action-type', {}); }); - test('should validate and pass when actionTypeOptions is valid', () => { + test('should validate and pass when actionTypeConfig is valid', () => { const actionTypeService = new ActionTypeService(); actionTypeService.register({ id: 'my-action-type', name: 'My action type', validate: { - actionTypeOptions: Joi.object() + actionTypeConfig: Joi.object() .keys({ param1: Joi.string().required(), }) @@ -182,16 +182,16 @@ describe('validateActionTypeOptions()', () => { }, async executor() {}, }); - actionTypeService.validateActionTypeOptions('my-action-type', { param1: 'value' }); + actionTypeService.validateActionTypeConfig('my-action-type', { param1: 'value' }); }); - test('should validate and throw error when actionTypeOptions is invalid', () => { + test('should validate and throw error when actionTypeConfig is invalid', () => { const actionTypeService = new ActionTypeService(); actionTypeService.register({ id: 'my-action-type', name: 'My action type', validate: { - actionTypeOptions: Joi.object() + actionTypeConfig: Joi.object() .keys({ param1: Joi.string().required(), }) @@ -200,7 +200,7 @@ describe('validateActionTypeOptions()', () => { async executor() {}, }); expect(() => - actionTypeService.validateActionTypeOptions('my-action-type', {}) + actionTypeService.validateActionTypeConfig('my-action-type', {}) ).toThrowErrorMatchingInlineSnapshot( `"child \\"param1\\" fails because [\\"param1\\" is required]"` ); @@ -240,10 +240,12 @@ describe('execute()', () => { "calls": Array [ Array [ Object { - "foo": true, - }, - Object { - "bar": false, + "actionTypeConfig": Object { + "foo": true, + }, + "params": Object { + "bar": false, + }, }, ], ], @@ -279,7 +281,7 @@ describe('execute()', () => { ); }); - test('validates actionTypeOptions', async () => { + test('validates actionTypeConfig', async () => { const executor = jest.fn().mockResolvedValueOnce({ success: true }); const actionTypeService = new ActionTypeService(); actionTypeService.register({ @@ -287,7 +289,7 @@ describe('execute()', () => { name: 'My action type', executor, validate: { - actionTypeOptions: Joi.object() + actionTypeConfig: Joi.object() .keys({ param1: Joi.string().required(), }) diff --git a/x-pack/plugins/alerting/server/action_type_service.ts b/x-pack/plugins/alerting/server/action_type_service.ts index 689abe51006b0..0a0df78395320 100644 --- a/x-pack/plugins/alerting/server/action_type_service.ts +++ b/x-pack/plugins/alerting/server/action_type_service.ts @@ -6,15 +6,20 @@ import Boom from 'boom'; +interface ExecutorOptions { + actionTypeConfig: any; + params: any; +} + interface ActionType { id: string; name: string; unencryptedAttributes?: string[]; validate?: { params?: any; - actionTypeOptions?: any; + actionTypeConfig?: any; }; - executor(actionTypeOptions: any, params: any): Promise; + executor({ actionTypeConfig, params }: ExecutorOptions): Promise; } export class ActionTypeService { @@ -63,21 +68,21 @@ export class ActionTypeService { } } - public validateActionTypeOptions(id: string, actionTypeOptions: any) { + public validateActionTypeConfig(id: string, actionTypeConfig: any) { const actionType = this.get(id); - const validator = actionType.validate && actionType.validate.actionTypeOptions; + const validator = actionType.validate && actionType.validate.actionTypeConfig; if (validator) { - const { error } = validator.validate(actionTypeOptions); + const { error } = validator.validate(actionTypeConfig); if (error) { throw error; } } } - public async execute(id: string, actionTypeOptions: any, params: any) { + public async execute(id: string, actionTypeConfig: any, params: any) { const actionType = this.get(id); - this.validateActionTypeOptions(id, actionTypeOptions); + this.validateActionTypeConfig(id, actionTypeConfig); this.validateParams(id, params); - return await actionType.executor(actionTypeOptions, params); + return await actionType.executor({ actionTypeConfig, params }); } } diff --git a/x-pack/plugins/alerting/server/routes/create_action.ts b/x-pack/plugins/alerting/server/routes/create_action.ts index 1adb89e39cb5e..420e178de59c5 100644 --- a/x-pack/plugins/alerting/server/routes/create_action.ts +++ b/x-pack/plugins/alerting/server/routes/create_action.ts @@ -21,7 +21,7 @@ interface CreateActionRequest extends WithoutQueryAndParams { attributes: { description: string; actionTypeId: string; - actionTypeOptions: { [key: string]: any }; + actionTypeConfig: { [key: string]: any }; }; migrationVersion?: { [key: string]: string }; references: SavedObjectReference[]; @@ -39,7 +39,7 @@ export function createActionRoute(server: Hapi.Server) { .keys({ description: Joi.string().required(), actionTypeId: Joi.string().required(), - actionTypeOptions: Joi.object(), + actionTypeConfig: Joi.object(), }) .required(), migrationVersion: Joi.object().optional(), diff --git a/x-pack/plugins/alerting/server/routes/update_action.ts b/x-pack/plugins/alerting/server/routes/update_action.ts index cd3d4344186d5..b5a03d40a0d4a 100644 --- a/x-pack/plugins/alerting/server/routes/update_action.ts +++ b/x-pack/plugins/alerting/server/routes/update_action.ts @@ -16,7 +16,7 @@ interface UpdateActionRequest extends Hapi.Request { attributes: { description: string; actionTypeId: string; - actionTypeOptions: { [key: string]: any }; + actionTypeConfig: { [key: string]: any }; }; version?: string; references: SavedObjectReference[]; @@ -40,7 +40,7 @@ export function updateActionRoute(server: Hapi.Server) { .keys({ description: Joi.string().required(), actionTypeId: Joi.string().required(), - actionTypeOptions: Joi.object(), + actionTypeConfig: Joi.object(), }) .required(), version: Joi.string(), diff --git a/x-pack/test/api_integration/apis/alerting/create_action.ts b/x-pack/test/api_integration/apis/alerting/create_action.ts index 4b25f10489462..050eb36cc3c93 100644 --- a/x-pack/test/api_integration/apis/alerting/create_action.ts +++ b/x-pack/test/api_integration/apis/alerting/create_action.ts @@ -20,7 +20,7 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe attributes: { description: 'My action', actionTypeId: 'test', - actionTypeOptions: {}, + actionTypeConfig: {}, }, }) .expect(200) @@ -31,7 +31,7 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe attributes: { description: 'My action', actionTypeId: 'test', - actionTypeOptions: {}, + actionTypeConfig: {}, }, references: [], updated_at: resp.body.updated_at, @@ -49,7 +49,7 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe attributes: { description: 'My action', actionTypeId: 'unregistered-action-type', - actionTypeOptions: {}, + actionTypeConfig: {}, }, }) .expect(400) diff --git a/x-pack/test/api_integration/apis/alerting/get_action.ts b/x-pack/test/api_integration/apis/alerting/get_action.ts index 2e8f8adf2fece..a15870fa1f031 100644 --- a/x-pack/test/api_integration/apis/alerting/get_action.ts +++ b/x-pack/test/api_integration/apis/alerting/get_action.ts @@ -29,7 +29,7 @@ export default function getActionTests({ getService }: KibanaFunctionalTestDefau attributes: { actionTypeId: 'test', description: 'My description', - actionTypeOptions: { + actionTypeConfig: { bar: false, foo: true, }, diff --git a/x-pack/test/api_integration/apis/alerting/update_action.ts b/x-pack/test/api_integration/apis/alerting/update_action.ts index edf2f5d6ab3fb..e8cf3bf8fdbfa 100644 --- a/x-pack/test/api_integration/apis/alerting/update_action.ts +++ b/x-pack/test/api_integration/apis/alerting/update_action.ts @@ -24,7 +24,7 @@ export default function updateActionTests({ getService }: KibanaFunctionalTestDe attributes: { actionTypeId: 'test', description: 'My description updated', - actionTypeOptions: {}, + actionTypeConfig: {}, }, }) .expect(200) @@ -38,7 +38,7 @@ export default function updateActionTests({ getService }: KibanaFunctionalTestDe attributes: { actionTypeId: 'test', description: 'My description updated', - actionTypeOptions: {}, + actionTypeConfig: {}, }, }); }); @@ -52,7 +52,7 @@ export default function updateActionTests({ getService }: KibanaFunctionalTestDe attributes: { actionTypeId: 'test', description: 'My description updated', - actionTypeOptions: {}, + actionTypeConfig: {}, }, }) .expect(404) diff --git a/x-pack/test/api_integration/fixtures/plugins/alerts/index.ts b/x-pack/test/api_integration/fixtures/plugins/alerts/index.ts index 6d8153ef7a74e..c863be491239b 100644 --- a/x-pack/test/api_integration/fixtures/plugins/alerts/index.ts +++ b/x-pack/test/api_integration/fixtures/plugins/alerts/index.ts @@ -13,7 +13,7 @@ export default function(kibana: any) { server.alerting().actionTypes.register({ id: 'test', name: 'Test', - async executor(actionTypeOptions: any, params: any) {}, + async executor(actionTypeConfig: any, params: any) {}, }); }, }); diff --git a/x-pack/test/functional/es_archives/alerting/basic/data.json b/x-pack/test/functional/es_archives/alerting/basic/data.json index c0896655bcc05..d041055f2131b 100644 --- a/x-pack/test/functional/es_archives/alerting/basic/data.json +++ b/x-pack/test/functional/es_archives/alerting/basic/data.json @@ -6,7 +6,7 @@ "action": { "description": "My description", "actionTypeId": "test", - "actionTypeOptions": { + "actionTypeConfig": { "foo": true, "bar": false } diff --git a/x-pack/test/functional/es_archives/alerting/basic/mappings.json b/x-pack/test/functional/es_archives/alerting/basic/mappings.json index 694deaef5cac4..0113e064db3ab 100644 --- a/x-pack/test/functional/es_archives/alerting/basic/mappings.json +++ b/x-pack/test/functional/es_archives/alerting/basic/mappings.json @@ -56,11 +56,11 @@ "actionTypeId": { "type": "keyword" }, - "actionTypeOptions": { + "actionTypeConfig": { "dynamic": "true", "type": "object" }, - "actionTypeOptionsSecrets": { + "actionTypeConfigSecrets": { "type": "binary" } } From 4e2b1df7edcd190cd6d037fece06e5a0b486abf3 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Tue, 7 May 2019 21:28:59 -0400 Subject: [PATCH 28/51] Code cleanup --- x-pack/plugins/alerting/index.ts | 2 +- .../{ => __jest__}/action_service.test.ts | 4 +- .../action_type_service.test.ts | 2 +- .../plugins/alerting/server/action_service.ts | 66 +++++++++++-------- .../alerting/server/action_type_service.ts | 36 ++++++++-- x-pack/plugins/alerting/server/index.ts | 11 +--- x-pack/plugins/alerting/{ => server}/init.ts | 6 +- .../alerting/server/routes/create_action.ts | 6 +- .../alerting/server/routes/delete_action.ts | 2 +- .../alerting/server/routes/find_action.ts | 2 +- .../alerting/server/routes/get_action.ts | 2 +- .../server/routes/list_action_types.ts | 2 +- .../alerting/server/routes/update_action.ts | 4 +- .../alerting/server/{routes => }/types.ts | 4 +- 14 files changed, 88 insertions(+), 61 deletions(-) rename x-pack/plugins/alerting/server/{ => __jest__}/action_service.test.ts (99%) rename x-pack/plugins/alerting/server/{ => __jest__}/action_type_service.test.ts (99%) rename x-pack/plugins/alerting/{ => server}/init.ts (91%) rename x-pack/plugins/alerting/server/{routes => }/types.ts (83%) diff --git a/x-pack/plugins/alerting/index.ts b/x-pack/plugins/alerting/index.ts index 3527cf63ef87b..7ebb298f00b9b 100644 --- a/x-pack/plugins/alerting/index.ts +++ b/x-pack/plugins/alerting/index.ts @@ -5,7 +5,7 @@ */ import mappings from './mappings.json'; -import { init } from './init'; +import { init } from './server'; import { APP_ID } from './common/constants'; diff --git a/x-pack/plugins/alerting/server/action_service.test.ts b/x-pack/plugins/alerting/server/__jest__/action_service.test.ts similarity index 99% rename from x-pack/plugins/alerting/server/action_service.test.ts rename to x-pack/plugins/alerting/server/__jest__/action_service.test.ts index a5bff39b9181c..c02ce0c4f3e70 100644 --- a/x-pack/plugins/alerting/server/action_service.test.ts +++ b/x-pack/plugins/alerting/server/__jest__/action_service.test.ts @@ -5,8 +5,8 @@ */ import Joi from 'joi'; -import { ActionTypeService } from './action_type_service'; -import { ActionService } from './action_service'; +import { ActionTypeService } from '../action_type_service'; +import { ActionService } from '../action_service'; const savedObjectsClient = { errors: {} as any, diff --git a/x-pack/plugins/alerting/server/action_type_service.test.ts b/x-pack/plugins/alerting/server/__jest__/action_type_service.test.ts similarity index 99% rename from x-pack/plugins/alerting/server/action_type_service.test.ts rename to x-pack/plugins/alerting/server/__jest__/action_type_service.test.ts index 3e27d49df7804..25c2c739aa9a9 100644 --- a/x-pack/plugins/alerting/server/action_type_service.test.ts +++ b/x-pack/plugins/alerting/server/__jest__/action_type_service.test.ts @@ -5,7 +5,7 @@ */ import Joi from 'joi'; -import { ActionTypeService } from './action_type_service'; +import { ActionTypeService } from '../action_type_service'; describe('register()', () => { test('able to register action types', () => { diff --git a/x-pack/plugins/alerting/server/action_service.ts b/x-pack/plugins/alerting/server/action_service.ts index 0e24cb9e40857..ebf37e8f1d7a0 100644 --- a/x-pack/plugins/alerting/server/action_service.ts +++ b/x-pack/plugins/alerting/server/action_service.ts @@ -7,32 +7,24 @@ import Boom from 'boom'; import { SavedObjectsClient } from 'src/legacy/server/saved_objects'; import { ActionTypeService } from './action_type_service'; +import { SavedObjectReference } from './types'; interface Action { description: string; actionTypeId: string; - actionTypeConfig: { [key: string]: any }; + actionTypeConfig: Record; } interface EncryptedAction extends Action { - description: string; - actionTypeId: string; - actionTypeConfig: { [key: string]: any }; - actionTypeConfigSecrets: { [key: string]: any }; + actionTypeConfigSecrets: Record; } interface FireActionOptions { id: string; - params: { [key: string]: any }; + params: Record; savedObjectsClient: SavedObjectsClient; } -export interface SavedObjectReference { - name: string; - type: string; - id: string; -} - interface FindOptions { perPage?: number; page?: number; @@ -56,20 +48,29 @@ export class ActionService { this.encryptedSavedObjects = encryptedSavedObjects; } + /** + * Create an action + */ public async create(savedObjectsClient: SavedObjectsClient, data: Action) { const { actionTypeId } = data; if (!this.actionTypeService.has(actionTypeId)) { throw Boom.badRequest(`Action type "${actionTypeId}" is not registered.`); } this.actionTypeService.validateActionTypeConfig(actionTypeId, data.actionTypeConfig); - const actionWithSplitActionTypeConfig = this.applyEncryptedAttributes(data); + const actionWithSplitActionTypeConfig = this.moveEncryptedAttributesToSecrets(data); return await savedObjectsClient.create('action', actionWithSplitActionTypeConfig); } + /** + * Get an action + */ public async get(savedObjectsClient: SavedObjectsClient, id: string) { return await savedObjectsClient.get('action', id); } + /** + * Find actions + */ public async find(savedObjectsClient: SavedObjectsClient, options: FindOptions) { return await savedObjectsClient.find({ ...options, @@ -77,10 +78,16 @@ export class ActionService { }); } + /** + * Delete action + */ public async delete(savedObjectsClient: SavedObjectsClient, id: string) { return await savedObjectsClient.delete('action', id); } + /** + * Update action + */ public async update( savedObjectsClient: SavedObjectsClient, id: string, @@ -92,7 +99,7 @@ export class ActionService { throw Boom.badRequest(`Action type "${actionTypeId}" is not registered.`); } this.actionTypeService.validateActionTypeConfig(actionTypeId, data.actionTypeConfig); - const actionWithSplitActionTypeConfig = this.applyEncryptedAttributes(data); + const actionWithSplitActionTypeConfig = this.moveEncryptedAttributesToSecrets(data); return await savedObjectsClient.update( 'action', id, @@ -101,36 +108,43 @@ export class ActionService { ); } + /** + * Fire an action + */ public async fire({ id, params, savedObjectsClient }: FireActionOptions) { const action = await this.encryptedSavedObjects.getDecryptedAsInternalUser('action', id); + const mergedActionTypeConfig = { + ...action.attributes.actionTypeConfig, + ...action.attributes.actionTypeConfigSecrets, + }; return await this.actionTypeService.execute( action.attributes.actionTypeId, - { - ...action.attributes.actionTypeConfig, - ...action.attributes.actionTypeConfigSecrets, - }, + mergedActionTypeConfig, params ); } - private applyEncryptedAttributes(action: Action): EncryptedAction { + /** + * Set actionTypeConfigSecrets values on a given action + */ + private moveEncryptedAttributesToSecrets(action: Action): EncryptedAction { const unencryptedAttributes = this.actionTypeService.getUnencryptedAttributes( action.actionTypeId ); - const encryptedActionTypeConfig: { [key: string]: any } = {}; - const unencryptedActionTypeConfig: { [key: string]: any } = {}; - for (const key of Object.keys(action.actionTypeConfig)) { + const config = { ...action.actionTypeConfig }; + const configSecrets: Record = {}; + for (const key of Object.keys(config)) { if (unencryptedAttributes.includes(key)) { - unencryptedActionTypeConfig[key] = action.actionTypeConfig[key]; continue; } - encryptedActionTypeConfig[key] = action.actionTypeConfig[key]; + configSecrets[key] = config[key]; + delete config[key]; } return { ...action, // Important these overwrite attributes in data for encryption purposes - actionTypeConfig: unencryptedActionTypeConfig, - actionTypeConfigSecrets: encryptedActionTypeConfig, + actionTypeConfig: config, + actionTypeConfigSecrets: configSecrets, }; } } diff --git a/x-pack/plugins/alerting/server/action_type_service.ts b/x-pack/plugins/alerting/server/action_type_service.ts index 0a0df78395320..9ea4eef08c2e9 100644 --- a/x-pack/plugins/alerting/server/action_type_service.ts +++ b/x-pack/plugins/alerting/server/action_type_service.ts @@ -23,12 +23,18 @@ interface ActionType { } export class ActionTypeService { - private actionTypes: { [id: string]: ActionType } = {}; + private actionTypes: Record = {}; + /** + * Returns if the action type service has the given action type registered + */ public has(id: string) { return !!this.actionTypes[id]; } + /** + * Registers an action type to the action type service + */ public register(actionType: ActionType) { if (this.has(actionType.id)) { throw Boom.badRequest(`Action type "${actionType.id}" is already registered.`); @@ -36,27 +42,37 @@ export class ActionTypeService { this.actionTypes[actionType.id] = actionType; } + /** + * Returns an action type, throws if not registered + */ public get(id: string) { - const actionType = this.actionTypes[id]; - if (!actionType) { + if (!this.actionTypes[id]) { throw Boom.badRequest(`Action type "${id}" is not registered.`); } - return actionType; + return this.actionTypes[id]; } + /** + * Returns attributes to be treated as unencrypted + */ public getUnencryptedAttributes(id: string) { const actionType = this.get(id); return actionType.unencryptedAttributes || []; } + /** + * Returns a list of registered action types [{ id, name }] + */ public list() { - const actionTypeIds = Object.keys(this.actionTypes); - return actionTypeIds.map(actionTypeId => ({ + return Object.entries(this.actionTypes).map(([actionTypeId, actionType]) => ({ id: actionTypeId, - name: this.actionTypes[actionTypeId].name, + name: actionType.name, })); } + /** + * Throws an error if params are invalid for given action type + */ public validateParams(id: string, params: any) { const actionType = this.get(id); const validator = actionType.validate && actionType.validate.params; @@ -68,6 +84,9 @@ export class ActionTypeService { } } + /** + * Throws an error if actionTypeConfig is invalid for given action type + */ public validateActionTypeConfig(id: string, actionTypeConfig: any) { const actionType = this.get(id); const validator = actionType.validate && actionType.validate.actionTypeConfig; @@ -79,6 +98,9 @@ export class ActionTypeService { } } + /** + * Executes an action type based on given parameters + */ public async execute(id: string, actionTypeConfig: any, params: any) { const actionType = this.get(id); this.validateActionTypeConfig(id, actionTypeConfig); diff --git a/x-pack/plugins/alerting/server/index.ts b/x-pack/plugins/alerting/server/index.ts index e50ec0762a3fb..04fefad4de395 100644 --- a/x-pack/plugins/alerting/server/index.ts +++ b/x-pack/plugins/alerting/server/index.ts @@ -4,13 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { - createActionRoute, - deleteActionRoute, - findActionRoute, - getActionRoute, - updateActionRoute, - listActionTypesRoute, -} from './routes'; -export { ActionService } from './action_service'; -export { ActionTypeService } from './action_type_service'; +export { init } from './init'; diff --git a/x-pack/plugins/alerting/init.ts b/x-pack/plugins/alerting/server/init.ts similarity index 91% rename from x-pack/plugins/alerting/init.ts rename to x-pack/plugins/alerting/server/init.ts index a1431770e4744..fe669b9213ded 100644 --- a/x-pack/plugins/alerting/init.ts +++ b/x-pack/plugins/alerting/server/init.ts @@ -5,6 +5,8 @@ */ import Hapi from 'hapi'; +import { ActionService } from './action_service'; +import { ActionTypeService } from './action_type_service'; import { createActionRoute, deleteActionRoute, @@ -12,9 +14,7 @@ import { getActionRoute, updateActionRoute, listActionTypesRoute, - ActionService, - ActionTypeService, -} from './server'; +} from './routes'; export function init(server: Hapi.Server) { const alertingEnabled = server.config().get('xpack.alerting.enabled'); diff --git a/x-pack/plugins/alerting/server/routes/create_action.ts b/x-pack/plugins/alerting/server/routes/create_action.ts index 420e178de59c5..4df5a2e2a1c02 100644 --- a/x-pack/plugins/alerting/server/routes/create_action.ts +++ b/x-pack/plugins/alerting/server/routes/create_action.ts @@ -7,7 +7,7 @@ import Joi from 'joi'; import Hapi from 'hapi'; import { APP_ID } from '../../common/constants'; -import { WithoutQueryAndParams, SavedObjectReference, Server } from './types'; +import { WithoutQueryAndParams, SavedObjectReference, Server } from '../types'; interface CreateActionRequest extends WithoutQueryAndParams { server: Server; @@ -21,9 +21,9 @@ interface CreateActionRequest extends WithoutQueryAndParams { attributes: { description: string; actionTypeId: string; - actionTypeConfig: { [key: string]: any }; + actionTypeConfig: Record; }; - migrationVersion?: { [key: string]: string }; + migrationVersion?: Record; references: SavedObjectReference[]; }; } diff --git a/x-pack/plugins/alerting/server/routes/delete_action.ts b/x-pack/plugins/alerting/server/routes/delete_action.ts index 279c59fab1cc4..a34c26b0ce305 100644 --- a/x-pack/plugins/alerting/server/routes/delete_action.ts +++ b/x-pack/plugins/alerting/server/routes/delete_action.ts @@ -8,7 +8,7 @@ import Hapi from 'hapi'; import Joi from 'joi'; import { APP_ID } from '../../common/constants'; -import { Server } from './types'; +import { Server } from '../types'; interface DeleteActionRequest extends Hapi.Request { server: Server; diff --git a/x-pack/plugins/alerting/server/routes/find_action.ts b/x-pack/plugins/alerting/server/routes/find_action.ts index 22fd08f29657b..422f138227dce 100644 --- a/x-pack/plugins/alerting/server/routes/find_action.ts +++ b/x-pack/plugins/alerting/server/routes/find_action.ts @@ -8,7 +8,7 @@ import Joi from 'joi'; import Hapi from 'hapi'; import { APP_ID } from '../../common/constants'; -import { WithoutQueryAndParams, Server } from './types'; +import { WithoutQueryAndParams, Server } from '../types'; interface FindActionRequest extends WithoutQueryAndParams { server: Server; diff --git a/x-pack/plugins/alerting/server/routes/get_action.ts b/x-pack/plugins/alerting/server/routes/get_action.ts index 9fbe508ca287d..3d654f9d3e6b5 100644 --- a/x-pack/plugins/alerting/server/routes/get_action.ts +++ b/x-pack/plugins/alerting/server/routes/get_action.ts @@ -8,7 +8,7 @@ import Joi from 'joi'; import Hapi from 'hapi'; import { APP_ID } from '../../common/constants'; -import { Server } from './types'; +import { Server } from '../types'; interface GetActionRequest extends Hapi.Request { server: Server; diff --git a/x-pack/plugins/alerting/server/routes/list_action_types.ts b/x-pack/plugins/alerting/server/routes/list_action_types.ts index acc4674f6dea5..7a5eb6fbf640e 100644 --- a/x-pack/plugins/alerting/server/routes/list_action_types.ts +++ b/x-pack/plugins/alerting/server/routes/list_action_types.ts @@ -7,7 +7,7 @@ import Hapi from 'hapi'; import { APP_ID } from '../../common/constants'; -import { Server } from './types'; +import { Server } from '../types'; interface ListActionTypesRequest extends Hapi.Request { server: Server; diff --git a/x-pack/plugins/alerting/server/routes/update_action.ts b/x-pack/plugins/alerting/server/routes/update_action.ts index b5a03d40a0d4a..0c5281cb34231 100644 --- a/x-pack/plugins/alerting/server/routes/update_action.ts +++ b/x-pack/plugins/alerting/server/routes/update_action.ts @@ -8,7 +8,7 @@ import Joi from 'joi'; import Hapi from 'hapi'; import { APP_ID } from '../../common/constants'; -import { SavedObjectReference, Server } from './types'; +import { SavedObjectReference, Server } from '../types'; interface UpdateActionRequest extends Hapi.Request { server: Server; @@ -16,7 +16,7 @@ interface UpdateActionRequest extends Hapi.Request { attributes: { description: string; actionTypeId: string; - actionTypeConfig: { [key: string]: any }; + actionTypeConfig: Record; }; version?: string; references: SavedObjectReference[]; diff --git a/x-pack/plugins/alerting/server/routes/types.ts b/x-pack/plugins/alerting/server/types.ts similarity index 83% rename from x-pack/plugins/alerting/server/routes/types.ts rename to x-pack/plugins/alerting/server/types.ts index 5a8cb48669978..a30df21ed927f 100644 --- a/x-pack/plugins/alerting/server/routes/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -5,8 +5,8 @@ */ import Hapi from 'hapi'; -import { ActionService } from '../action_service'; -import { ActionTypeService } from '../action_type_service'; +import { ActionService } from './action_service'; +import { ActionTypeService } from './action_type_service'; export type WithoutQueryAndParams = Pick>; From 4d7d6f2b97e418ec9194ad8eb9dc51aa840984f2 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Wed, 8 May 2019 09:30:06 -0400 Subject: [PATCH 29/51] Fix broken tests --- .../server/__jest__/action_service.test.ts | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/alerting/server/__jest__/action_service.test.ts b/x-pack/plugins/alerting/server/__jest__/action_service.test.ts index c02ce0c4f3e70..55218b12eae47 100644 --- a/x-pack/plugins/alerting/server/__jest__/action_service.test.ts +++ b/x-pack/plugins/alerting/server/__jest__/action_service.test.ts @@ -48,9 +48,9 @@ describe('create()', () => { Array [ "action", Object { - "actionTypeId": "my-action-type", "actionTypeConfig": Object {}, "actionTypeConfigSecrets": Object {}, + "actionTypeId": "my-action-type", "description": "my description", }, ], @@ -132,7 +132,6 @@ describe('create()', () => { Array [ "action", Object { - "actionTypeId": "my-action-type", "actionTypeConfig": Object { "a": true, "c": true, @@ -140,6 +139,7 @@ describe('create()', () => { "actionTypeConfigSecrets": Object { "b": true, }, + "actionTypeId": "my-action-type", "description": "my description", }, ], @@ -266,9 +266,9 @@ describe('update()', () => { "action", "my-alert", Object { - "actionTypeId": "my-action-type", "actionTypeConfig": Object {}, "actionTypeConfigSecrets": Object {}, + "actionTypeId": "my-action-type", "description": "my description", }, Object {}, @@ -367,7 +367,6 @@ describe('update()', () => { "action", "my-alert", Object { - "actionTypeId": "my-action-type", "actionTypeConfig": Object { "a": true, "c": true, @@ -375,6 +374,7 @@ describe('update()', () => { "actionTypeConfigSecrets": Object { "b": true, }, + "actionTypeId": "my-action-type", "description": "my description", }, Object {}, @@ -421,10 +421,12 @@ describe('fire()', () => { "calls": Array [ Array [ Object { - "foo": true, - }, - Object { - "baz": false, + "actionTypeConfig": Object { + "foo": true, + }, + "params": Object { + "baz": false, + }, }, ], ], @@ -494,12 +496,14 @@ describe('fire()', () => { "calls": Array [ Array [ Object { - "a": true, - "b": true, - "c": true, - }, - Object { - "baz": false, + "actionTypeConfig": Object { + "a": true, + "b": true, + "c": true, + }, + "params": Object { + "baz": false, + }, }, ], ], From fcd9306da752cc1fe4c9bc143a969d4e6c0a13d3 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Wed, 8 May 2019 16:48:47 -0400 Subject: [PATCH 30/51] Rename alerting plugin to actions --- x-pack/index.js | 4 +- .../{alerting => actions}/common/constants.ts | 2 +- x-pack/plugins/{alerting => actions}/index.ts | 17 +++++- .../{alerting => actions}/mappings.json | 0 .../server/__jest__/action_service.test.ts | 12 ++-- .../__jest__/action_type_service.test.ts | 0 .../server/action_service.ts | 18 +++--- .../server/action_type_service.ts | 0 .../{alerting => actions}/server/index.ts | 2 + x-pack/plugins/actions/server/init.ts | 56 +++++++++++++++++++ .../server/routes/create.ts} | 19 +++---- .../server/routes/delete.ts} | 14 ++--- .../server/routes/find.ts} | 14 ++--- .../server/routes/get.ts} | 14 ++--- .../server/routes/index.ts | 10 ++-- .../server/routes/list_action_types.ts | 13 +---- .../server/routes/update.ts} | 21 +++---- .../{alerting => actions}/server/types.ts | 11 ---- x-pack/plugins/alerting/server/init.ts | 52 ----------------- .../create_action.ts => actions/create.ts} | 6 +- .../delete_action.ts => actions/delete.ts} | 6 +- .../find_action.ts => actions/find.ts} | 4 +- .../get_action.ts => actions/get.ts} | 6 +- .../apis/{alerting => actions}/index.ts | 12 ++-- .../list_action_types.ts | 2 +- .../update_action.ts => actions/update.ts} | 6 +- x-pack/test/api_integration/apis/index.js | 2 +- .../fixtures/plugins/alerts/index.ts | 4 +- x-pack/typings/hapi.d.ts | 2 + 29 files changed, 160 insertions(+), 169 deletions(-) rename x-pack/plugins/{alerting => actions}/common/constants.ts (87%) rename x-pack/plugins/{alerting => actions}/index.ts (60%) rename x-pack/plugins/{alerting => actions}/mappings.json (100%) rename x-pack/plugins/{alerting => actions}/server/__jest__/action_service.test.ts (99%) rename x-pack/plugins/{alerting => actions}/server/__jest__/action_type_service.test.ts (100%) rename x-pack/plugins/{alerting => actions}/server/action_service.ts (87%) rename x-pack/plugins/{alerting => actions}/server/action_type_service.ts (100%) rename x-pack/plugins/{alerting => actions}/server/index.ts (71%) create mode 100644 x-pack/plugins/actions/server/init.ts rename x-pack/plugins/{alerting/server/routes/create_action.ts => actions/server/routes/create.ts} (73%) rename x-pack/plugins/{alerting/server/routes/delete_action.ts => actions/server/routes/delete.ts} (62%) rename x-pack/plugins/{alerting/server/routes/find_action.ts => actions/server/routes/find.ts} (82%) rename x-pack/plugins/{alerting/server/routes/get_action.ts => actions/server/routes/get.ts} (63%) rename x-pack/plugins/{alerting => actions}/server/routes/index.ts (54%) rename x-pack/plugins/{alerting => actions}/server/routes/list_action_types.ts (54%) rename x-pack/plugins/{alerting/server/routes/update_action.ts => actions/server/routes/update.ts} (78%) rename x-pack/plugins/{alerting => actions}/server/types.ts (60%) delete mode 100644 x-pack/plugins/alerting/server/init.ts rename x-pack/test/api_integration/apis/{alerting/create_action.ts => actions/create.ts} (94%) rename x-pack/test/api_integration/apis/{alerting/delete_action.ts => actions/delete.ts} (91%) rename x-pack/test/api_integration/apis/{alerting/find_action.ts => actions/find.ts} (93%) rename x-pack/test/api_integration/apis/{alerting/get_action.ts => actions/get.ts} (93%) rename x-pack/test/api_integration/apis/{alerting => actions}/index.ts (64%) rename x-pack/test/api_integration/apis/{alerting => actions}/list_action_types.ts (96%) rename x-pack/test/api_integration/apis/{alerting/update_action.ts => actions/update.ts} (94%) diff --git a/x-pack/index.js b/x-pack/index.js index 5f6cec9456e5e..613d907a2ab63 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -40,7 +40,7 @@ import { upgradeAssistant } from './plugins/upgrade_assistant'; import { uptime } from './plugins/uptime'; import { ossTelemetry } from './plugins/oss_telemetry'; import { encryptedSavedObjects } from './plugins/encrypted_saved_objects'; -import { alerting } from './plugins/alerting'; +import { actions } from './plugins/actions'; module.exports = function (kibana) { return [ @@ -80,6 +80,6 @@ module.exports = function (kibana) { uptime(kibana), ossTelemetry(kibana), encryptedSavedObjects(kibana), - alerting(kibana), + actions(kibana), ]; }; diff --git a/x-pack/plugins/alerting/common/constants.ts b/x-pack/plugins/actions/common/constants.ts similarity index 87% rename from x-pack/plugins/alerting/common/constants.ts rename to x-pack/plugins/actions/common/constants.ts index d8bf357e8781b..1016086c68d03 100644 --- a/x-pack/plugins/alerting/common/constants.ts +++ b/x-pack/plugins/actions/common/constants.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export const APP_ID = 'alerting'; +export const APP_ID = 'actions'; diff --git a/x-pack/plugins/alerting/index.ts b/x-pack/plugins/actions/index.ts similarity index 60% rename from x-pack/plugins/alerting/index.ts rename to x-pack/plugins/actions/index.ts index 7ebb298f00b9b..5c5c749367245 100644 --- a/x-pack/plugins/alerting/index.ts +++ b/x-pack/plugins/actions/index.ts @@ -9,10 +9,23 @@ import { init } from './server'; import { APP_ID } from './common/constants'; -export function alerting(kibana: any) { +import { ActionService, ActionTypeService } from './server'; + +export interface ActionsPlugin { + create: ActionService['create']; + get: ActionService['get']; + find: ActionService['find']; + delete: ActionService['delete']; + update: ActionService['update']; + fire: ActionService['fire']; + registerType: ActionTypeService['register']; + listTypes: ActionTypeService['list']; +} + +export function actions(kibana: any) { return new kibana.Plugin({ id: APP_ID, - configPrefix: 'xpack.alerting', + configPrefix: 'xpack.actions', require: ['kibana', 'elasticsearch', 'encrypted_saved_objects'], config(Joi: any) { return Joi.object() diff --git a/x-pack/plugins/alerting/mappings.json b/x-pack/plugins/actions/mappings.json similarity index 100% rename from x-pack/plugins/alerting/mappings.json rename to x-pack/plugins/actions/mappings.json diff --git a/x-pack/plugins/alerting/server/__jest__/action_service.test.ts b/x-pack/plugins/actions/server/__jest__/action_service.test.ts similarity index 99% rename from x-pack/plugins/alerting/server/__jest__/action_service.test.ts rename to x-pack/plugins/actions/server/__jest__/action_service.test.ts index 55218b12eae47..ff9c44b8c992a 100644 --- a/x-pack/plugins/alerting/server/__jest__/action_service.test.ts +++ b/x-pack/plugins/actions/server/__jest__/action_service.test.ts @@ -250,7 +250,7 @@ describe('update()', () => { savedObjectsClient.update.mockResolvedValueOnce(expectedResult); const result = await actionService.update( savedObjectsClient, - 'my-alert', + 'my-action', { description: 'my description', actionTypeId: 'my-action-type', @@ -264,7 +264,7 @@ describe('update()', () => { "calls": Array [ Array [ "action", - "my-alert", + "my-action", Object { "actionTypeConfig": Object {}, "actionTypeConfigSecrets": Object {}, @@ -302,7 +302,7 @@ describe('update()', () => { await expect( actionService.update( savedObjectsClient, - 'my-alert', + 'my-action', { description: 'my description', actionTypeId: 'my-action-type', @@ -321,7 +321,7 @@ describe('update()', () => { await expect( actionService.update( savedObjectsClient, - 'my-alert', + 'my-action', { description: 'my description', actionTypeId: 'unregistered-action-type', @@ -347,7 +347,7 @@ describe('update()', () => { savedObjectsClient.update.mockResolvedValueOnce(expectedResult); const result = await actionService.update( savedObjectsClient, - 'my-alert', + 'my-action', { description: 'my description', actionTypeId: 'my-action-type', @@ -365,7 +365,7 @@ describe('update()', () => { "calls": Array [ Array [ "action", - "my-alert", + "my-action", Object { "actionTypeConfig": Object { "a": true, diff --git a/x-pack/plugins/alerting/server/__jest__/action_type_service.test.ts b/x-pack/plugins/actions/server/__jest__/action_type_service.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/__jest__/action_type_service.test.ts rename to x-pack/plugins/actions/server/__jest__/action_type_service.test.ts diff --git a/x-pack/plugins/alerting/server/action_service.ts b/x-pack/plugins/actions/server/action_service.ts similarity index 87% rename from x-pack/plugins/alerting/server/action_service.ts rename to x-pack/plugins/actions/server/action_service.ts index ebf37e8f1d7a0..3d25151a7e230 100644 --- a/x-pack/plugins/alerting/server/action_service.ts +++ b/x-pack/plugins/actions/server/action_service.ts @@ -40,11 +40,11 @@ interface FindOptions { } export class ActionService { - private actionTypeService: ActionTypeService; + public actionTypes: ActionTypeService; private encryptedSavedObjects: any; constructor(actionTypeService: ActionTypeService, encryptedSavedObjects: any) { - this.actionTypeService = actionTypeService; + this.actionTypes = actionTypeService; this.encryptedSavedObjects = encryptedSavedObjects; } @@ -53,10 +53,10 @@ export class ActionService { */ public async create(savedObjectsClient: SavedObjectsClient, data: Action) { const { actionTypeId } = data; - if (!this.actionTypeService.has(actionTypeId)) { + if (!this.actionTypes.has(actionTypeId)) { throw Boom.badRequest(`Action type "${actionTypeId}" is not registered.`); } - this.actionTypeService.validateActionTypeConfig(actionTypeId, data.actionTypeConfig); + this.actionTypes.validateActionTypeConfig(actionTypeId, data.actionTypeConfig); const actionWithSplitActionTypeConfig = this.moveEncryptedAttributesToSecrets(data); return await savedObjectsClient.create('action', actionWithSplitActionTypeConfig); } @@ -95,10 +95,10 @@ export class ActionService { options: { version?: string; references?: SavedObjectReference[] } ) { const { actionTypeId } = data; - if (!this.actionTypeService.has(actionTypeId)) { + if (!this.actionTypes.has(actionTypeId)) { throw Boom.badRequest(`Action type "${actionTypeId}" is not registered.`); } - this.actionTypeService.validateActionTypeConfig(actionTypeId, data.actionTypeConfig); + this.actionTypes.validateActionTypeConfig(actionTypeId, data.actionTypeConfig); const actionWithSplitActionTypeConfig = this.moveEncryptedAttributesToSecrets(data); return await savedObjectsClient.update( 'action', @@ -117,7 +117,7 @@ export class ActionService { ...action.attributes.actionTypeConfig, ...action.attributes.actionTypeConfigSecrets, }; - return await this.actionTypeService.execute( + return await this.actionTypes.execute( action.attributes.actionTypeId, mergedActionTypeConfig, params @@ -128,9 +128,7 @@ export class ActionService { * Set actionTypeConfigSecrets values on a given action */ private moveEncryptedAttributesToSecrets(action: Action): EncryptedAction { - const unencryptedAttributes = this.actionTypeService.getUnencryptedAttributes( - action.actionTypeId - ); + const unencryptedAttributes = this.actionTypes.getUnencryptedAttributes(action.actionTypeId); const config = { ...action.actionTypeConfig }; const configSecrets: Record = {}; for (const key of Object.keys(config)) { diff --git a/x-pack/plugins/alerting/server/action_type_service.ts b/x-pack/plugins/actions/server/action_type_service.ts similarity index 100% rename from x-pack/plugins/alerting/server/action_type_service.ts rename to x-pack/plugins/actions/server/action_type_service.ts diff --git a/x-pack/plugins/alerting/server/index.ts b/x-pack/plugins/actions/server/index.ts similarity index 71% rename from x-pack/plugins/alerting/server/index.ts rename to x-pack/plugins/actions/server/index.ts index 04fefad4de395..217bc43ba778e 100644 --- a/x-pack/plugins/alerting/server/index.ts +++ b/x-pack/plugins/actions/server/index.ts @@ -5,3 +5,5 @@ */ export { init } from './init'; +export { ActionService } from './action_service'; +export { ActionTypeService } from './action_type_service'; diff --git a/x-pack/plugins/actions/server/init.ts b/x-pack/plugins/actions/server/init.ts new file mode 100644 index 0000000000000..8f1972b273d67 --- /dev/null +++ b/x-pack/plugins/actions/server/init.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Hapi from 'hapi'; +import { ActionService } from './action_service'; +import { ActionTypeService } from './action_type_service'; +import { + createRoute, + deleteRoute, + findRoute, + getRoute, + updateRoute, + listActionTypesRoute, +} from './routes'; + +export function init(server: Hapi.Server) { + const actionsEnabled = server.config().get('xpack.actions.enabled'); + + if (!actionsEnabled) { + server.log(['info', 'actions'], 'Actions app disabled by configuration'); + return; + } + + // Encrypted attributes + server.plugins.encrypted_saved_objects!.registerType({ + type: 'action', + attributesToEncrypt: new Set(['actionTypeConfigSecrets']), + }); + + const actionTypeService = new ActionTypeService(); + const actionService = new ActionService( + actionTypeService, + server.plugins.encrypted_saved_objects + ); + + // Routes + createRoute(server); + deleteRoute(server); + getRoute(server); + findRoute(server); + updateRoute(server); + listActionTypesRoute(server); + + // Expose service to server + server.expose('create', actionService.create.bind(actionService)); + server.expose('get', actionService.get.bind(actionService)); + server.expose('find', actionService.find.bind(actionService)); + server.expose('delete', actionService.delete.bind(actionService)); + server.expose('update', actionService.update.bind(actionService)); + server.expose('fire', actionService.fire.bind(actionService)); + server.expose('registerType', actionTypeService.register.bind(actionTypeService)); + server.expose('listTypes', actionTypeService.list.bind(actionTypeService)); +} diff --git a/x-pack/plugins/alerting/server/routes/create_action.ts b/x-pack/plugins/actions/server/routes/create.ts similarity index 73% rename from x-pack/plugins/alerting/server/routes/create_action.ts rename to x-pack/plugins/actions/server/routes/create.ts index 4df5a2e2a1c02..7487bc8307770 100644 --- a/x-pack/plugins/alerting/server/routes/create_action.ts +++ b/x-pack/plugins/actions/server/routes/create.ts @@ -6,11 +6,9 @@ import Joi from 'joi'; import Hapi from 'hapi'; -import { APP_ID } from '../../common/constants'; -import { WithoutQueryAndParams, SavedObjectReference, Server } from '../types'; +import { WithoutQueryAndParams, SavedObjectReference } from '../types'; -interface CreateActionRequest extends WithoutQueryAndParams { - server: Server; +interface CreateRequest extends WithoutQueryAndParams { query: { overwrite: boolean; }; @@ -28,10 +26,10 @@ interface CreateActionRequest extends WithoutQueryAndParams { }; } -export function createActionRoute(server: Hapi.Server) { +export function createRoute(server: Hapi.Server) { server.route({ method: 'POST', - path: `/api/${APP_ID}/action`, + path: `/api/action`, options: { validate: { payload: Joi.object().keys({ @@ -55,11 +53,12 @@ export function createActionRoute(server: Hapi.Server) { }), }, }, - async handler(request: CreateActionRequest) { + async handler(request: CreateRequest) { const savedObjectsClient = request.getSavedObjectsClient(); - return await request.server - .alerting() - .actions.create(savedObjectsClient, request.payload.attributes); + return await request.server.plugins.actions.create( + savedObjectsClient, + request.payload.attributes + ); }, }); } diff --git a/x-pack/plugins/alerting/server/routes/delete_action.ts b/x-pack/plugins/actions/server/routes/delete.ts similarity index 62% rename from x-pack/plugins/alerting/server/routes/delete_action.ts rename to x-pack/plugins/actions/server/routes/delete.ts index a34c26b0ce305..1d75871617000 100644 --- a/x-pack/plugins/alerting/server/routes/delete_action.ts +++ b/x-pack/plugins/actions/server/routes/delete.ts @@ -7,20 +7,16 @@ import Hapi from 'hapi'; import Joi from 'joi'; -import { APP_ID } from '../../common/constants'; -import { Server } from '../types'; - -interface DeleteActionRequest extends Hapi.Request { - server: Server; +interface DeleteRequest extends Hapi.Request { params: { id: string; }; } -export function deleteActionRoute(server: Hapi.Server) { +export function deleteRoute(server: Hapi.Server) { server.route({ method: 'DELETE', - path: `/api/${APP_ID}/action/{id}`, + path: `/api/action/{id}`, options: { validate: { params: Joi.object() @@ -30,10 +26,10 @@ export function deleteActionRoute(server: Hapi.Server) { .required(), }, }, - async handler(request: DeleteActionRequest) { + async handler(request: DeleteRequest) { const { id } = request.params; const savedObjectsClient = request.getSavedObjectsClient(); - return await request.server.alerting().actions.delete(savedObjectsClient, id); + return await request.server.plugins.actions.delete(savedObjectsClient, id); }, }); } diff --git a/x-pack/plugins/alerting/server/routes/find_action.ts b/x-pack/plugins/actions/server/routes/find.ts similarity index 82% rename from x-pack/plugins/alerting/server/routes/find_action.ts rename to x-pack/plugins/actions/server/routes/find.ts index 422f138227dce..dfcbc1f08562f 100644 --- a/x-pack/plugins/alerting/server/routes/find_action.ts +++ b/x-pack/plugins/actions/server/routes/find.ts @@ -7,11 +7,9 @@ import Joi from 'joi'; import Hapi from 'hapi'; -import { APP_ID } from '../../common/constants'; -import { WithoutQueryAndParams, Server } from '../types'; +import { WithoutQueryAndParams } from '../types'; -interface FindActionRequest extends WithoutQueryAndParams { - server: Server; +interface FindRequest extends WithoutQueryAndParams { query: { per_page: number; page: number; @@ -27,10 +25,10 @@ interface FindActionRequest extends WithoutQueryAndParams { }; } -export function findActionRoute(server: any) { +export function findRoute(server: any) { server.route({ method: 'GET', - path: `/api/${APP_ID}/action/_find`, + path: `/api/action/_find`, options: { validate: { query: Joi.object() @@ -64,10 +62,10 @@ export function findActionRoute(server: any) { .default(), }, }, - async handler(request: FindActionRequest) { + async handler(request: FindRequest) { const query = request.query; const savedObjectsClient = request.getSavedObjectsClient(); - return await request.server.alerting().actions.find(savedObjectsClient, { + return await request.server.plugins.actions.find(savedObjectsClient, { perPage: query.per_page, page: query.page, search: query.search, diff --git a/x-pack/plugins/alerting/server/routes/get_action.ts b/x-pack/plugins/actions/server/routes/get.ts similarity index 63% rename from x-pack/plugins/alerting/server/routes/get_action.ts rename to x-pack/plugins/actions/server/routes/get.ts index 3d654f9d3e6b5..b0008d40a518e 100644 --- a/x-pack/plugins/alerting/server/routes/get_action.ts +++ b/x-pack/plugins/actions/server/routes/get.ts @@ -7,20 +7,16 @@ import Joi from 'joi'; import Hapi from 'hapi'; -import { APP_ID } from '../../common/constants'; -import { Server } from '../types'; - -interface GetActionRequest extends Hapi.Request { - server: Server; +interface GetRequest extends Hapi.Request { params: { id: string; }; } -export function getActionRoute(server: Hapi.Server) { +export function getRoute(server: Hapi.Server) { server.route({ method: 'GET', - path: `/api/${APP_ID}/action/{id}`, + path: `/api/action/{id}`, options: { validate: { params: Joi.object() @@ -30,10 +26,10 @@ export function getActionRoute(server: Hapi.Server) { .required(), }, }, - async handler(request: GetActionRequest) { + async handler(request: GetRequest) { const { id } = request.params; const savedObjectsClient = request.getSavedObjectsClient(); - return await request.server.alerting().actions.get(savedObjectsClient, id); + return await request.server.plugins.actions.get(savedObjectsClient, id); }, }); } diff --git a/x-pack/plugins/alerting/server/routes/index.ts b/x-pack/plugins/actions/server/routes/index.ts similarity index 54% rename from x-pack/plugins/alerting/server/routes/index.ts rename to x-pack/plugins/actions/server/routes/index.ts index 945774b706c69..7ed6dd222fe00 100644 --- a/x-pack/plugins/alerting/server/routes/index.ts +++ b/x-pack/plugins/actions/server/routes/index.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -export { createActionRoute } from './create_action'; -export { deleteActionRoute } from './delete_action'; -export { findActionRoute } from './find_action'; -export { getActionRoute } from './get_action'; -export { updateActionRoute } from './update_action'; +export { createRoute } from './create'; +export { deleteRoute } from './delete'; +export { findRoute } from './find'; +export { getRoute } from './get'; +export { updateRoute } from './update'; export { listActionTypesRoute } from './list_action_types'; diff --git a/x-pack/plugins/alerting/server/routes/list_action_types.ts b/x-pack/plugins/actions/server/routes/list_action_types.ts similarity index 54% rename from x-pack/plugins/alerting/server/routes/list_action_types.ts rename to x-pack/plugins/actions/server/routes/list_action_types.ts index 7a5eb6fbf640e..1dc49d45ff4f9 100644 --- a/x-pack/plugins/alerting/server/routes/list_action_types.ts +++ b/x-pack/plugins/actions/server/routes/list_action_types.ts @@ -6,19 +6,12 @@ import Hapi from 'hapi'; -import { APP_ID } from '../../common/constants'; -import { Server } from '../types'; - -interface ListActionTypesRequest extends Hapi.Request { - server: Server; -} - export function listActionTypesRoute(server: any) { server.route({ method: 'GET', - path: `/api/${APP_ID}/action_types`, - async handler(request: ListActionTypesRequest) { - return request.server.alerting().actionTypes.list(); + path: `/api/action/types`, + async handler(request: Hapi.Request) { + return request.server.plugins.actions.listTypes(); }, }); } diff --git a/x-pack/plugins/alerting/server/routes/update_action.ts b/x-pack/plugins/actions/server/routes/update.ts similarity index 78% rename from x-pack/plugins/alerting/server/routes/update_action.ts rename to x-pack/plugins/actions/server/routes/update.ts index 0c5281cb34231..49138dd3f6e2e 100644 --- a/x-pack/plugins/alerting/server/routes/update_action.ts +++ b/x-pack/plugins/actions/server/routes/update.ts @@ -7,11 +7,9 @@ import Joi from 'joi'; import Hapi from 'hapi'; -import { APP_ID } from '../../common/constants'; -import { SavedObjectReference, Server } from '../types'; +import { SavedObjectReference } from '../types'; -interface UpdateActionRequest extends Hapi.Request { - server: Server; +interface UpdateRequest extends Hapi.Request { payload: { attributes: { description: string; @@ -23,10 +21,10 @@ interface UpdateActionRequest extends Hapi.Request { }; } -export function updateActionRoute(server: Hapi.Server) { +export function updateRoute(server: Hapi.Server) { server.route({ method: 'PUT', - path: `/api/${APP_ID}/action/{id}`, + path: `/api/action/{id}`, options: { validate: { params: Joi.object() @@ -57,14 +55,17 @@ export function updateActionRoute(server: Hapi.Server) { .required(), }, }, - async handler(request: UpdateActionRequest) { + async handler(request: UpdateRequest) { const { id } = request.params; const { attributes, version, references } = request.payload; const options = { version, references }; const savedObjectsClient = request.getSavedObjectsClient(); - return await request.server - .alerting() - .actions.update(savedObjectsClient, id, attributes, options); + return await request.server.plugins.actions.update( + savedObjectsClient, + id, + attributes, + options + ); }, }); } diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/actions/server/types.ts similarity index 60% rename from x-pack/plugins/alerting/server/types.ts rename to x-pack/plugins/actions/server/types.ts index a30df21ed927f..f2933fe82e237 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -4,10 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import Hapi from 'hapi'; -import { ActionService } from './action_service'; -import { ActionTypeService } from './action_type_service'; - export type WithoutQueryAndParams = Pick>; export interface SavedObjectReference { @@ -15,10 +11,3 @@ export interface SavedObjectReference { type: string; id: string; } - -export interface Server extends Hapi.Server { - alerting: () => { - actions: ActionService; - actionTypes: ActionTypeService; - }; -} diff --git a/x-pack/plugins/alerting/server/init.ts b/x-pack/plugins/alerting/server/init.ts deleted file mode 100644 index fe669b9213ded..0000000000000 --- a/x-pack/plugins/alerting/server/init.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Hapi from 'hapi'; -import { ActionService } from './action_service'; -import { ActionTypeService } from './action_type_service'; -import { - createActionRoute, - deleteActionRoute, - findActionRoute, - getActionRoute, - updateActionRoute, - listActionTypesRoute, -} from './routes'; - -export function init(server: Hapi.Server) { - const alertingEnabled = server.config().get('xpack.alerting.enabled'); - - if (!alertingEnabled) { - server.log(['info', 'alerting'], 'Alerting app disabled by configuration'); - return; - } - - // Encrypted attributes - server.plugins.encrypted_saved_objects!.registerType({ - type: 'action', - attributesToEncrypt: new Set(['actionTypeConfigSecrets']), - }); - - const actionTypeService = new ActionTypeService(); - const actionService = new ActionService( - actionTypeService, - server.plugins.encrypted_saved_objects - ); - - // Routes - createActionRoute(server); - deleteActionRoute(server); - getActionRoute(server); - findActionRoute(server); - updateActionRoute(server); - listActionTypesRoute(server); - - // Register service to server - server.decorate('server', 'alerting', () => ({ - actions: actionService, - actionTypes: actionTypeService, - })); -} diff --git a/x-pack/test/api_integration/apis/alerting/create_action.ts b/x-pack/test/api_integration/apis/actions/create.ts similarity index 94% rename from x-pack/test/api_integration/apis/alerting/create_action.ts rename to x-pack/test/api_integration/apis/actions/create.ts index 050eb36cc3c93..3c6fef025d6c6 100644 --- a/x-pack/test/api_integration/apis/alerting/create_action.ts +++ b/x-pack/test/api_integration/apis/actions/create.ts @@ -11,10 +11,10 @@ import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; export default function createActionTests({ getService }: KibanaFunctionalTestDefaultProviders) { const supertest = getService('supertest'); - describe('create_action', () => { + describe('create', () => { it('should return 200 when creating an action', async () => { await supertest - .post('/api/alerting/action') + .post('/api/action') .set('kbn-xsrf', 'foo') .send({ attributes: { @@ -43,7 +43,7 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe it(`should return 400 when action type isn't registered`, async () => { await supertest - .post('/api/alerting/action') + .post('/api/action') .set('kbn-xsrf', 'foo') .send({ attributes: { diff --git a/x-pack/test/api_integration/apis/alerting/delete_action.ts b/x-pack/test/api_integration/apis/actions/delete.ts similarity index 91% rename from x-pack/test/api_integration/apis/alerting/delete_action.ts rename to x-pack/test/api_integration/apis/actions/delete.ts index 59552f1472c32..d88fbdb36e7e8 100644 --- a/x-pack/test/api_integration/apis/alerting/delete_action.ts +++ b/x-pack/test/api_integration/apis/actions/delete.ts @@ -13,13 +13,13 @@ export default function deleteActionTests({ getService }: KibanaFunctionalTestDe const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - describe('delete_action', () => { + describe('delete', () => { beforeEach(() => esArchiver.load('alerting/basic')); afterEach(() => esArchiver.unload('alerting/basic')); it('should return 200 when deleting an action', async () => { await supertest - .delete('/api/alerting/action/1') + .delete('/api/action/1') .set('kbn-xsrf', 'foo') .expect(200) .then((resp: any) => { @@ -29,7 +29,7 @@ export default function deleteActionTests({ getService }: KibanaFunctionalTestDe it(`should return 404 when action doesn't exist`, async () => { await supertest - .delete('/api/alerting/action/2') + .delete('/api/action/2') .set('kbn-xsrf', 'foo') .expect(404) .then((resp: any) => { diff --git a/x-pack/test/api_integration/apis/alerting/find_action.ts b/x-pack/test/api_integration/apis/actions/find.ts similarity index 93% rename from x-pack/test/api_integration/apis/alerting/find_action.ts rename to x-pack/test/api_integration/apis/actions/find.ts index 578d37868b35c..80d7b0fafa5ac 100644 --- a/x-pack/test/api_integration/apis/alerting/find_action.ts +++ b/x-pack/test/api_integration/apis/actions/find.ts @@ -12,13 +12,13 @@ export default function findActionTests({ getService }: KibanaFunctionalTestDefa const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - describe('find_action', () => { + describe('find', () => { before(() => esArchiver.load('alerting/basic')); after(() => esArchiver.unload('alerting/basic')); it('should return 200 with individual responses', async () => { await supertest - .get('/api/alerting/action/_find?fields=description') + .get('/api/action/_find?fields=description') .expect(200) .then((resp: any) => { expect(resp.body).to.eql({ diff --git a/x-pack/test/api_integration/apis/alerting/get_action.ts b/x-pack/test/api_integration/apis/actions/get.ts similarity index 93% rename from x-pack/test/api_integration/apis/alerting/get_action.ts rename to x-pack/test/api_integration/apis/actions/get.ts index a15870fa1f031..a400ae7728ff5 100644 --- a/x-pack/test/api_integration/apis/alerting/get_action.ts +++ b/x-pack/test/api_integration/apis/actions/get.ts @@ -12,13 +12,13 @@ export default function getActionTests({ getService }: KibanaFunctionalTestDefau const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - describe('get_action', () => { + describe('get', () => { before(() => esArchiver.load('alerting/basic')); after(() => esArchiver.unload('alerting/basic')); it('should return 200 when finding a record', async () => { await supertest - .get('/api/alerting/action/1') + .get('/api/action/1') .expect(200) .then((resp: any) => { expect(resp.body).to.eql({ @@ -40,7 +40,7 @@ export default function getActionTests({ getService }: KibanaFunctionalTestDefau it('should return 404 when not finding a record', async () => { await supertest - .get('/api/alerting/action/2') + .get('/api/action/2') .expect(404) .then((resp: any) => { expect(resp.body).to.eql({ diff --git a/x-pack/test/api_integration/apis/alerting/index.ts b/x-pack/test/api_integration/apis/actions/index.ts similarity index 64% rename from x-pack/test/api_integration/apis/alerting/index.ts rename to x-pack/test/api_integration/apis/actions/index.ts index 45c959aca71bc..41b8ea8d8793c 100644 --- a/x-pack/test/api_integration/apis/alerting/index.ts +++ b/x-pack/test/api_integration/apis/actions/index.ts @@ -8,12 +8,12 @@ import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; // eslint-disable-next-line import/no-default-export export default function alertingTests({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { - describe('Alerting', () => { - loadTestFile(require.resolve('./create_action')); - loadTestFile(require.resolve('./delete_action')); - loadTestFile(require.resolve('./find_action')); - loadTestFile(require.resolve('./get_action')); + describe('Actions', () => { + loadTestFile(require.resolve('./create')); + loadTestFile(require.resolve('./delete')); + loadTestFile(require.resolve('./find')); + loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./list_action_types')); - loadTestFile(require.resolve('./update_action')); + loadTestFile(require.resolve('./update')); }); } diff --git a/x-pack/test/api_integration/apis/alerting/list_action_types.ts b/x-pack/test/api_integration/apis/actions/list_action_types.ts similarity index 96% rename from x-pack/test/api_integration/apis/alerting/list_action_types.ts rename to x-pack/test/api_integration/apis/actions/list_action_types.ts index ce9827927128d..29e5a3e4e4b33 100644 --- a/x-pack/test/api_integration/apis/alerting/list_action_types.ts +++ b/x-pack/test/api_integration/apis/actions/list_action_types.ts @@ -14,7 +14,7 @@ export default function listActionTypesTests({ getService }: KibanaFunctionalTes describe('list_action_types', () => { it('should return 200 with list of action types containing defaults', async () => { await supertest - .get('/api/alerting/action_types') + .get('/api/action/types') .expect(200) .then((resp: any) => { function createActionTypeMatcher(id: string, name: string) { diff --git a/x-pack/test/api_integration/apis/alerting/update_action.ts b/x-pack/test/api_integration/apis/actions/update.ts similarity index 94% rename from x-pack/test/api_integration/apis/alerting/update_action.ts rename to x-pack/test/api_integration/apis/actions/update.ts index e8cf3bf8fdbfa..db2785e246b13 100644 --- a/x-pack/test/api_integration/apis/alerting/update_action.ts +++ b/x-pack/test/api_integration/apis/actions/update.ts @@ -12,13 +12,13 @@ export default function updateActionTests({ getService }: KibanaFunctionalTestDe const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - describe('update_action', () => { + describe('update', () => { beforeEach(() => esArchiver.load('alerting/basic')); afterEach(() => esArchiver.unload('alerting/basic')); it('should return 200 when updating a document', async () => { await supertest - .put('/api/alerting/action/1') + .put('/api/action/1') .set('kbn-xsrf', 'foo') .send({ attributes: { @@ -46,7 +46,7 @@ export default function updateActionTests({ getService }: KibanaFunctionalTestDe it('should return 404 when updating a non existing document', async () => { await supertest - .put('/api/alerting/action/2') + .put('/api/action/2') .set('kbn-xsrf', 'foo') .send({ attributes: { diff --git a/x-pack/test/api_integration/apis/index.js b/x-pack/test/api_integration/apis/index.js index 1dc48304919fd..950c8277a5e0c 100644 --- a/x-pack/test/api_integration/apis/index.js +++ b/x-pack/test/api_integration/apis/index.js @@ -8,7 +8,7 @@ export default function ({ loadTestFile }) { describe('apis', function () { this.tags('ciGroup6'); - loadTestFile(require.resolve('./alerting')); + loadTestFile(require.resolve('./actions')); loadTestFile(require.resolve('./es')); loadTestFile(require.resolve('./security')); loadTestFile(require.resolve('./monitoring')); diff --git a/x-pack/test/api_integration/fixtures/plugins/alerts/index.ts b/x-pack/test/api_integration/fixtures/plugins/alerts/index.ts index c863be491239b..467ee3c4afc5d 100644 --- a/x-pack/test/api_integration/fixtures/plugins/alerts/index.ts +++ b/x-pack/test/api_integration/fixtures/plugins/alerts/index.ts @@ -7,10 +7,10 @@ // eslint-disable-next-line import/no-default-export export default function(kibana: any) { return new kibana.Plugin({ - require: ['alerting'], + require: ['actions'], name: 'alerts', init(server: any) { - server.alerting().actionTypes.register({ + server.plugins.actions.registerType({ id: 'test', name: 'Test', async executor(actionTypeConfig: any, params: any) {}, diff --git a/x-pack/typings/hapi.d.ts b/x-pack/typings/hapi.d.ts index 9fedea3e541ad..f25f29868dd06 100644 --- a/x-pack/typings/hapi.d.ts +++ b/x-pack/typings/hapi.d.ts @@ -10,6 +10,7 @@ import { CloudPlugin } from '../plugins/cloud'; import { EncryptedSavedObjectsPlugin } from '../plugins/encrypted_saved_objects'; import { XPackMainPlugin } from '../plugins/xpack_main/xpack_main'; import { SecurityPlugin } from '../plugins/security'; +import { ActionsPlugin } from '../plugins/actions'; declare module 'hapi' { interface PluginProperties { @@ -17,5 +18,6 @@ declare module 'hapi' { xpack_main: XPackMainPlugin; security?: SecurityPlugin; encrypted_saved_objects?: EncryptedSavedObjectsPlugin; + actions: ActionsPlugin; } } From 0ccd660387db99b6645fce66ea8dd8f210c6d65e Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Thu, 9 May 2019 11:53:39 -0400 Subject: [PATCH 31/51] Some code cleanup and add API unit tests --- x-pack/plugins/actions/common/constants.ts | 7 -- x-pack/plugins/actions/index.ts | 17 +--- .../server/__jest__/action_service.test.ts | 22 +++-- .../plugins/actions/server/action_service.ts | 13 ++- x-pack/plugins/actions/server/index.ts | 3 +- .../server/routes/__jest__/_mock_server.ts | 77 ++++++++++++++++ .../server/routes/__jest__/create.test.ts | 88 +++++++++++++++++++ .../server/routes/__jest__/delete.test.ts | 53 +++++++++++ .../server/routes/__jest__/find.test.ts | 74 ++++++++++++++++ .../server/routes/__jest__/get.test.ts | 53 +++++++++++ .../routes/__jest__/list_action_types.test.ts | 41 +++++++++ .../server/routes/__jest__/update.test.ts | 85 ++++++++++++++++++ .../plugins/actions/server/routes/create.ts | 6 +- x-pack/plugins/actions/server/types.ts | 14 +++ 14 files changed, 521 insertions(+), 32 deletions(-) delete mode 100644 x-pack/plugins/actions/common/constants.ts create mode 100644 x-pack/plugins/actions/server/routes/__jest__/_mock_server.ts create mode 100644 x-pack/plugins/actions/server/routes/__jest__/create.test.ts create mode 100644 x-pack/plugins/actions/server/routes/__jest__/delete.test.ts create mode 100644 x-pack/plugins/actions/server/routes/__jest__/find.test.ts create mode 100644 x-pack/plugins/actions/server/routes/__jest__/get.test.ts create mode 100644 x-pack/plugins/actions/server/routes/__jest__/list_action_types.test.ts create mode 100644 x-pack/plugins/actions/server/routes/__jest__/update.test.ts diff --git a/x-pack/plugins/actions/common/constants.ts b/x-pack/plugins/actions/common/constants.ts deleted file mode 100644 index 1016086c68d03..0000000000000 --- a/x-pack/plugins/actions/common/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const APP_ID = 'actions'; diff --git a/x-pack/plugins/actions/index.ts b/x-pack/plugins/actions/index.ts index 5c5c749367245..c9fc35c8cb841 100644 --- a/x-pack/plugins/actions/index.ts +++ b/x-pack/plugins/actions/index.ts @@ -7,24 +7,11 @@ import mappings from './mappings.json'; import { init } from './server'; -import { APP_ID } from './common/constants'; - -import { ActionService, ActionTypeService } from './server'; - -export interface ActionsPlugin { - create: ActionService['create']; - get: ActionService['get']; - find: ActionService['find']; - delete: ActionService['delete']; - update: ActionService['update']; - fire: ActionService['fire']; - registerType: ActionTypeService['register']; - listTypes: ActionTypeService['list']; -} +export { ActionsPlugin } from './server'; export function actions(kibana: any) { return new kibana.Plugin({ - id: APP_ID, + id: 'actions', configPrefix: 'xpack.actions', require: ['kibana', 'elasticsearch', 'encrypted_saved_objects'], config(Joi: any) { diff --git a/x-pack/plugins/actions/server/__jest__/action_service.test.ts b/x-pack/plugins/actions/server/__jest__/action_service.test.ts index ff9c44b8c992a..4a6b21e6b90f7 100644 --- a/x-pack/plugins/actions/server/__jest__/action_service.test.ts +++ b/x-pack/plugins/actions/server/__jest__/action_service.test.ts @@ -36,11 +36,18 @@ describe('create()', () => { }); const actionService = new ActionService(actionTypeService, mockEncryptedSavedObjects); savedObjectsClient.create.mockResolvedValueOnce(expectedResult); - const result = await actionService.create(savedObjectsClient, { - description: 'my description', - actionTypeId: 'my-action-type', - actionTypeConfig: {}, - }); + const result = await actionService.create( + savedObjectsClient, + { + description: 'my description', + actionTypeId: 'my-action-type', + actionTypeConfig: {}, + }, + { + migrationVersion: {}, + references: [], + } + ); expect(result).toEqual(expectedResult); expect(savedObjectsClient.create).toMatchInlineSnapshot(` [MockFunction] { @@ -53,6 +60,10 @@ describe('create()', () => { "actionTypeId": "my-action-type", "description": "my description", }, + Object { + "migrationVersion": Object {}, + "references": Array [], + }, ], ], "results": Array [ @@ -142,6 +153,7 @@ describe('create()', () => { "actionTypeId": "my-action-type", "description": "my description", }, + undefined, ], ], "results": Array [ diff --git a/x-pack/plugins/actions/server/action_service.ts b/x-pack/plugins/actions/server/action_service.ts index 3d25151a7e230..ce36ef95072ff 100644 --- a/x-pack/plugins/actions/server/action_service.ts +++ b/x-pack/plugins/actions/server/action_service.ts @@ -39,6 +39,11 @@ interface FindOptions { fields?: string[]; } +interface CreateOptions { + migrationVersion?: Record; + references?: SavedObjectReference[]; +} + export class ActionService { public actionTypes: ActionTypeService; private encryptedSavedObjects: any; @@ -51,14 +56,18 @@ export class ActionService { /** * Create an action */ - public async create(savedObjectsClient: SavedObjectsClient, data: Action) { + public async create( + savedObjectsClient: SavedObjectsClient, + data: Action, + options?: CreateOptions + ) { const { actionTypeId } = data; if (!this.actionTypes.has(actionTypeId)) { throw Boom.badRequest(`Action type "${actionTypeId}" is not registered.`); } this.actionTypes.validateActionTypeConfig(actionTypeId, data.actionTypeConfig); const actionWithSplitActionTypeConfig = this.moveEncryptedAttributesToSecrets(data); - return await savedObjectsClient.create('action', actionWithSplitActionTypeConfig); + return await savedObjectsClient.create('action', actionWithSplitActionTypeConfig, options); } /** diff --git a/x-pack/plugins/actions/server/index.ts b/x-pack/plugins/actions/server/index.ts index 217bc43ba778e..678b3b46a6663 100644 --- a/x-pack/plugins/actions/server/index.ts +++ b/x-pack/plugins/actions/server/index.ts @@ -5,5 +5,4 @@ */ export { init } from './init'; -export { ActionService } from './action_service'; -export { ActionTypeService } from './action_type_service'; +export { ActionsPlugin } from './types'; diff --git a/x-pack/plugins/actions/server/routes/__jest__/_mock_server.ts b/x-pack/plugins/actions/server/routes/__jest__/_mock_server.ts new file mode 100644 index 0000000000000..f7dc49b92f6a3 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/__jest__/_mock_server.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Hapi from 'hapi'; + +const defaultConfig = { + 'kibana.index': '.kibana', +}; + +interface PluginProperties extends Hapi.PluginProperties { + actions: { + create: jest.Mock; + get: jest.Mock; + find: jest.Mock; + delete: jest.Mock; + update: jest.Mock; + fire: jest.Mock; + registerType: jest.Mock; + listTypes: jest.Mock; + }; +} + +interface MockServer extends Hapi.Server { + plugins: PluginProperties; +} + +export function createMockServer(config: Record = defaultConfig): MockServer { + const server = new Hapi.Server({ + port: 0, + }); + + server.config = () => { + return { + get(key: string) { + return config[key]; + }, + has(key: string) { + return config.hasOwnProperty(key); + }, + }; + }; + + server.register({ + name: 'actions', + register(pluginServer: Hapi.Server) { + pluginServer.expose('create', jest.fn()); + pluginServer.expose('get', jest.fn()); + pluginServer.expose('find', jest.fn()); + pluginServer.expose('delete', jest.fn()); + pluginServer.expose('update', jest.fn()); + pluginServer.expose('fire', jest.fn()); + pluginServer.expose('registerType', jest.fn()); + pluginServer.expose('listTypes', jest.fn()); + }, + }); + + server.ext('onRequest', (request, h) => { + const client = { + errors: {} as any, + bulkCreate: jest.fn(), + bulkGet: jest.fn(), + create: jest.fn(), + delete: jest.fn(), + find: jest.fn(), + get: jest.fn(), + update: jest.fn(), + }; + request.getSavedObjectsClient = () => client; + return h.continue; + }); + + // @ts-ignore + return server; +} diff --git a/x-pack/plugins/actions/server/routes/__jest__/create.test.ts b/x-pack/plugins/actions/server/routes/__jest__/create.test.ts new file mode 100644 index 0000000000000..cd03d1fbf3b7c --- /dev/null +++ b/x-pack/plugins/actions/server/routes/__jest__/create.test.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createMockServer } from './_mock_server'; +import { createRoute } from '../create'; + +const server = createMockServer(); +createRoute(server); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +it('creates an action with proper parameters', async () => { + const request = { + method: 'POST', + url: '/api/action', + payload: { + attributes: { + description: 'My description', + actionTypeId: 'abc', + actionTypeConfig: { foo: true }, + }, + migrationVersion: { + abc: '1.2.3', + }, + references: [ + { + name: 'ref_0', + type: 'bcd', + id: '234', + }, + ], + }, + }; + + server.plugins.actions.create.mockResolvedValueOnce({ success: true }); + const { payload, statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + const response = JSON.parse(payload); + expect(response).toEqual({ success: true }); + expect(server.plugins.actions.create).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + Object { + "bulkCreate": [MockFunction], + "bulkGet": [MockFunction], + "create": [MockFunction], + "delete": [MockFunction], + "errors": Object {}, + "find": [MockFunction], + "get": [MockFunction], + "update": [MockFunction], + }, + Object { + "actionTypeConfig": Object { + "foo": true, + }, + "actionTypeId": "abc", + "description": "My description", + }, + Object { + "migrationVersion": Object { + "abc": "1.2.3", + }, + "references": Array [ + Object { + "id": "234", + "name": "ref_0", + "type": "bcd", + }, + ], + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], +} +`); +}); diff --git a/x-pack/plugins/actions/server/routes/__jest__/delete.test.ts b/x-pack/plugins/actions/server/routes/__jest__/delete.test.ts new file mode 100644 index 0000000000000..fca8dd41f701e --- /dev/null +++ b/x-pack/plugins/actions/server/routes/__jest__/delete.test.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createMockServer } from './_mock_server'; +import { deleteRoute } from '../delete'; + +const server = createMockServer(); +deleteRoute(server); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +it('deletes an action with proper parameters', async () => { + const request = { + method: 'DELETE', + url: '/api/action/1', + }; + + server.plugins.actions.delete.mockResolvedValueOnce({ success: true }); + const { payload, statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + const response = JSON.parse(payload); + expect(response).toEqual({ success: true }); + expect(server.plugins.actions.delete).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + Object { + "bulkCreate": [MockFunction], + "bulkGet": [MockFunction], + "create": [MockFunction], + "delete": [MockFunction], + "errors": Object {}, + "find": [MockFunction], + "get": [MockFunction], + "update": [MockFunction], + }, + "1", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], +} +`); +}); diff --git a/x-pack/plugins/actions/server/routes/__jest__/find.test.ts b/x-pack/plugins/actions/server/routes/__jest__/find.test.ts new file mode 100644 index 0000000000000..bcffd092784fd --- /dev/null +++ b/x-pack/plugins/actions/server/routes/__jest__/find.test.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createMockServer } from './_mock_server'; +import { findRoute } from '../find'; + +const server = createMockServer(); +findRoute(server); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +it('sends proper arguments to action find function', async () => { + const request = { + method: 'GET', + url: + '/api/action/_find?' + + 'per_page=1&' + + 'page=1&' + + 'search=text*&' + + 'default_search_operator=AND&' + + 'search_fields=description&' + + 'sort_field=description&' + + 'fields=description', + }; + + server.plugins.actions.find.mockResolvedValueOnce({ success: true }); + const { payload, statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + const response = JSON.parse(payload); + expect(response).toEqual({ success: true }); + expect(server.plugins.actions.find).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + Object { + "bulkCreate": [MockFunction], + "bulkGet": [MockFunction], + "create": [MockFunction], + "delete": [MockFunction], + "errors": Object {}, + "find": [MockFunction], + "get": [MockFunction], + "update": [MockFunction], + }, + Object { + "defaultSearchOperator": "AND", + "fields": Array [ + "description", + ], + "hasReference": undefined, + "page": 1, + "perPage": 1, + "search": "text*", + "searchFields": Array [ + "description", + ], + "sortField": "description", + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], +} +`); +}); diff --git a/x-pack/plugins/actions/server/routes/__jest__/get.test.ts b/x-pack/plugins/actions/server/routes/__jest__/get.test.ts new file mode 100644 index 0000000000000..8b603e248b727 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/__jest__/get.test.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createMockServer } from './_mock_server'; +import { getRoute } from '../get'; + +const server = createMockServer(); +getRoute(server); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +it('calls get with proper parameters', async () => { + const request = { + method: 'GET', + url: '/api/action/1', + }; + + server.plugins.actions.get.mockResolvedValueOnce({ success: true }); + const { payload, statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + const response = JSON.parse(payload); + expect(response).toEqual({ success: true }); + expect(server.plugins.actions.get).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + Object { + "bulkCreate": [MockFunction], + "bulkGet": [MockFunction], + "create": [MockFunction], + "delete": [MockFunction], + "errors": Object {}, + "find": [MockFunction], + "get": [MockFunction], + "update": [MockFunction], + }, + "1", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], +} +`); +}); diff --git a/x-pack/plugins/actions/server/routes/__jest__/list_action_types.test.ts b/x-pack/plugins/actions/server/routes/__jest__/list_action_types.test.ts new file mode 100644 index 0000000000000..f7dac793399cd --- /dev/null +++ b/x-pack/plugins/actions/server/routes/__jest__/list_action_types.test.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createMockServer } from './_mock_server'; +import { listActionTypesRoute } from '../list_action_types'; + +const server = createMockServer(); +listActionTypesRoute(server); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +it('calls the list function', async () => { + const request = { + method: 'GET', + url: '/api/action/types', + }; + + server.plugins.actions.listTypes.mockResolvedValueOnce({ success: true }); + const { payload, statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + const response = JSON.parse(payload); + expect(response).toEqual({ success: true }); + expect(server.plugins.actions.listTypes).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], +} +`); +}); diff --git a/x-pack/plugins/actions/server/routes/__jest__/update.test.ts b/x-pack/plugins/actions/server/routes/__jest__/update.test.ts new file mode 100644 index 0000000000000..c0cc064458bf1 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/__jest__/update.test.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createMockServer } from './_mock_server'; +import { updateRoute } from '../update'; + +const server = createMockServer(); +updateRoute(server); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +it('calls the update function with proper parameters', async () => { + const request = { + method: 'PUT', + url: '/api/action/1', + payload: { + attributes: { + description: 'My description', + actionTypeId: 'abc', + actionTypeConfig: { foo: true }, + }, + version: '2', + references: [ + { + name: 'ref_0', + type: 'bcd', + id: '234', + }, + ], + }, + }; + + server.plugins.actions.update.mockResolvedValueOnce({ success: true }); + const { payload, statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + const response = JSON.parse(payload); + expect(response).toEqual({ success: true }); + expect(server.plugins.actions.update).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + Object { + "bulkCreate": [MockFunction], + "bulkGet": [MockFunction], + "create": [MockFunction], + "delete": [MockFunction], + "errors": Object {}, + "find": [MockFunction], + "get": [MockFunction], + "update": [MockFunction], + }, + "1", + Object { + "actionTypeConfig": Object { + "foo": true, + }, + "actionTypeId": "abc", + "description": "My description", + }, + Object { + "references": Array [ + Object { + "id": "234", + "name": "ref_0", + "type": "bcd", + }, + ], + "version": "2", + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], +} +`); +}); diff --git a/x-pack/plugins/actions/server/routes/create.ts b/x-pack/plugins/actions/server/routes/create.ts index 7487bc8307770..602410fcc1afe 100644 --- a/x-pack/plugins/actions/server/routes/create.ts +++ b/x-pack/plugins/actions/server/routes/create.ts @@ -57,7 +57,11 @@ export function createRoute(server: Hapi.Server) { const savedObjectsClient = request.getSavedObjectsClient(); return await request.server.plugins.actions.create( savedObjectsClient, - request.payload.attributes + request.payload.attributes, + { + migrationVersion: request.payload.migrationVersion, + references: request.payload.references, + } ); }, }); diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index f2933fe82e237..3b9f595b821a3 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -4,6 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ActionService } from './action_service'; +import { ActionTypeService } from './action_type_service'; + export type WithoutQueryAndParams = Pick>; export interface SavedObjectReference { @@ -11,3 +14,14 @@ export interface SavedObjectReference { type: string; id: string; } + +export interface ActionsPlugin { + create: ActionService['create']; + get: ActionService['get']; + find: ActionService['find']; + delete: ActionService['delete']; + update: ActionService['update']; + fire: ActionService['fire']; + registerType: ActionTypeService['register']; + listTypes: ActionTypeService['list']; +} From da985f88f809b76e527e117c5448fa984452b98b Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Thu, 9 May 2019 13:05:00 -0400 Subject: [PATCH 32/51] Change signature of action type service execute function --- .../__jest__/action_type_service.test.ts | 24 +++++++++++++++---- .../plugins/actions/server/action_service.ts | 10 ++++---- .../actions/server/action_type_service.ts | 8 ++++++- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/actions/server/__jest__/action_type_service.test.ts b/x-pack/plugins/actions/server/__jest__/action_type_service.test.ts index 25c2c739aa9a9..0f1ccdd23db14 100644 --- a/x-pack/plugins/actions/server/__jest__/action_type_service.test.ts +++ b/x-pack/plugins/actions/server/__jest__/action_type_service.test.ts @@ -234,7 +234,11 @@ describe('execute()', () => { name: 'My action type', executor, }); - await actionTypeService.execute('my-action-type', { foo: true }, { bar: false }); + await actionTypeService.execute({ + id: 'my-action-type', + actionTypeConfig: { foo: true }, + params: { bar: false }, + }); expect(executor).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ @@ -275,7 +279,11 @@ describe('execute()', () => { }, }); await expect( - actionTypeService.execute('my-action-type', {}, {}) + actionTypeService.execute({ + id: 'my-action-type', + actionTypeConfig: {}, + params: {}, + }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"child \\"param1\\" fails because [\\"param1\\" is required]"` ); @@ -297,7 +305,11 @@ describe('execute()', () => { }, }); await expect( - actionTypeService.execute('my-action-type', {}, {}) + actionTypeService.execute({ + id: 'my-action-type', + actionTypeConfig: {}, + params: {}, + }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"child \\"param1\\" fails because [\\"param1\\" is required]"` ); @@ -306,7 +318,11 @@ describe('execute()', () => { test('throws error if action type not registered', async () => { const actionTypeService = new ActionTypeService(); await expect( - actionTypeService.execute('my-action-type', { foo: true }, { bar: false }) + actionTypeService.execute({ + id: 'my-action-type', + actionTypeConfig: { foo: true }, + params: { bar: false }, + }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Action type \\"my-action-type\\" is not registered."` ); diff --git a/x-pack/plugins/actions/server/action_service.ts b/x-pack/plugins/actions/server/action_service.ts index ce36ef95072ff..a5cee15463424 100644 --- a/x-pack/plugins/actions/server/action_service.ts +++ b/x-pack/plugins/actions/server/action_service.ts @@ -126,11 +126,11 @@ export class ActionService { ...action.attributes.actionTypeConfig, ...action.attributes.actionTypeConfigSecrets, }; - return await this.actionTypes.execute( - action.attributes.actionTypeId, - mergedActionTypeConfig, - params - ); + return await this.actionTypes.execute({ + id: action.attributes.actionTypeId, + actionTypeConfig: mergedActionTypeConfig, + params, + }); } /** diff --git a/x-pack/plugins/actions/server/action_type_service.ts b/x-pack/plugins/actions/server/action_type_service.ts index 9ea4eef08c2e9..21ebbccbe1301 100644 --- a/x-pack/plugins/actions/server/action_type_service.ts +++ b/x-pack/plugins/actions/server/action_type_service.ts @@ -22,6 +22,12 @@ interface ActionType { executor({ actionTypeConfig, params }: ExecutorOptions): Promise; } +interface ExecuteOptions { + id: string; + actionTypeConfig: any; + params: any; +} + export class ActionTypeService { private actionTypes: Record = {}; @@ -101,7 +107,7 @@ export class ActionTypeService { /** * Executes an action type based on given parameters */ - public async execute(id: string, actionTypeConfig: any, params: any) { + public async execute({ id, actionTypeConfig, params }: ExecuteOptions) { const actionType = this.get(id); this.validateActionTypeConfig(id, actionTypeConfig); this.validateParams(id, params); From e2bec4fde47173fc6c1250edb9641fc4a7749a9c Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Thu, 9 May 2019 20:44:59 -0400 Subject: [PATCH 33/51] Add some plugin api integration tests --- .../fixtures/plugins/alerts/index.ts | 5 +- x-pack/test/plugin_api_integration/config.js | 1 + .../plugins/actions/index.ts | 61 ++++ .../plugins/actions/package.json | 4 + .../test_suites/actions/actions.ts | 273 ++++++++++++++++++ .../test_suites/actions/index.ts | 15 + 6 files changed, 358 insertions(+), 1 deletion(-) create mode 100644 x-pack/test/plugin_api_integration/plugins/actions/index.ts create mode 100644 x-pack/test/plugin_api_integration/plugins/actions/package.json create mode 100644 x-pack/test/plugin_api_integration/test_suites/actions/actions.ts create mode 100644 x-pack/test/plugin_api_integration/test_suites/actions/index.ts diff --git a/x-pack/test/api_integration/fixtures/plugins/alerts/index.ts b/x-pack/test/api_integration/fixtures/plugins/alerts/index.ts index 467ee3c4afc5d..115c91f3acc62 100644 --- a/x-pack/test/api_integration/fixtures/plugins/alerts/index.ts +++ b/x-pack/test/api_integration/fixtures/plugins/alerts/index.ts @@ -13,7 +13,10 @@ export default function(kibana: any) { server.plugins.actions.registerType({ id: 'test', name: 'Test', - async executor(actionTypeConfig: any, params: any) {}, + unencryptedAttributes: ['unencrypted'], + async executor({ actionTypeConfig, params }: { actionTypeConfig: any; params: any }) { + return { success: true, actionTypeConfig, params }; + }, }); }, }); diff --git a/x-pack/test/plugin_api_integration/config.js b/x-pack/test/plugin_api_integration/config.js index 1af76e5d134d2..cfb056bf685a3 100644 --- a/x-pack/test/plugin_api_integration/config.js +++ b/x-pack/test/plugin_api_integration/config.js @@ -19,6 +19,7 @@ export default async function ({ readConfigFile }) { testFiles: [ require.resolve('./test_suites/task_manager'), require.resolve('./test_suites/encrypted_saved_objects'), + require.resolve('./test_suites/actions'), ], services: { retry: kibanaFunctionalConfig.get('services.retry'), diff --git a/x-pack/test/plugin_api_integration/plugins/actions/index.ts b/x-pack/test/plugin_api_integration/plugins/actions/index.ts new file mode 100644 index 0000000000000..01c82d212621e --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/actions/index.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Joi from 'joi'; +import { Legacy } from 'kibana'; +import { Request } from 'hapi'; +import { ActionsPlugin } from '../../../../plugins/actions'; + +// @ts-ignore +interface FireRequest extends Request { + params: { + id: string; + }; + payload: { + params: Record; + }; + server: { + plugins: { + actions: ActionsPlugin; + }; + }; +} + +// eslint-disable-next-line import/no-default-export +export default function actionsPlugin(kibana: any) { + return new kibana.Plugin({ + id: 'actions-test', + require: ['actions'], + init(server: Legacy.Server) { + server.route({ + method: 'POST', + path: '/api/action/{id}/fire', + options: { + validate: { + params: Joi.object() + .keys({ + id: Joi.string().required(), + }) + .required(), + payload: Joi.object() + .keys({ + params: Joi.object(), + }) + .required(), + }, + }, + async handler(request: FireRequest) { + const savedObjectsClient = request.getSavedObjectsClient(); + return await request.server.plugins.actions.fire({ + id: request.params.id, + params: request.payload.params, + savedObjectsClient, + }); + }, + }); + }, + }); +} diff --git a/x-pack/test/plugin_api_integration/plugins/actions/package.json b/x-pack/test/plugin_api_integration/plugins/actions/package.json new file mode 100644 index 0000000000000..d6dbd425a8026 --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/actions/package.json @@ -0,0 +1,4 @@ +{ + "name": "actions-test", + "version": "kibana" + } \ No newline at end of file diff --git a/x-pack/test/plugin_api_integration/test_suites/actions/actions.ts b/x-pack/test/plugin_api_integration/test_suites/actions/actions.ts new file mode 100644 index 0000000000000..9f9b76943a441 --- /dev/null +++ b/x-pack/test/plugin_api_integration/test_suites/actions/actions.ts @@ -0,0 +1,273 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getService }: KibanaFunctionalTestDefaultProviders) { + const supertest = getService('supertest'); + + describe('encrypted attributes', () => { + after(async () => { + const { body: findResult } = await supertest.get('/api/action/_find').expect(200); + await Promise.all( + findResult.saved_objects.map(({ id }: { id: string }) => { + return supertest + .delete(`/api/action/${id}`) + .set('kbn-xsrf', 'foo') + .expect(200); + }) + ); + }); + + it('decrypts attributes and joins on actionTypeConfig when firing', async () => { + // Create an action + const { body: createdAction } = await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + attributes: { + description: 'My description', + actionTypeId: 'test', + actionTypeConfig: { + unencrypted: 'not encrypted value', + encrypted: 'encrypted by default value', + }, + }, + }) + .expect(200); + + await supertest + .post(`/api/action/${createdAction.id}/fire`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + foo: true, + bar: false, + }, + }) + .expect(200) + .then((resp: any) => { + expect(resp.body).to.eql({ + success: true, + actionTypeConfig: { + unencrypted: 'not encrypted value', + encrypted: 'encrypted by default value', + }, + params: { + foo: true, + bar: false, + }, + }); + }); + }); + + it(`doesn't return encrypted attributes on create`, async () => { + const { body: createdAction } = await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + attributes: { + description: 'My description', + actionTypeId: 'test', + actionTypeConfig: { + unencrypted: 'not encrypted value', + encrypted: 'encrypted by default value', + }, + }, + }) + .expect(200); + + expect(createdAction).to.eql({ + id: createdAction.id, + type: 'action', + references: [], + updated_at: createdAction.updated_at, + version: createdAction.version, + attributes: { + description: 'My description', + actionTypeId: 'test', + actionTypeConfig: { + unencrypted: 'not encrypted value', + }, + }, + }); + }); + + it(`doesn't return encrypted attributes on get`, async () => { + const { body: createdAction } = await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + attributes: { + description: 'My description', + actionTypeId: 'test', + actionTypeConfig: { + unencrypted: 'not encrypted value', + encrypted: 'encrypted by default value', + }, + }, + }) + .expect(200); + + const { body: result } = await supertest.get(`/api/action/${createdAction.id}`).expect(200); + expect(result).to.eql({ + id: createdAction.id, + type: 'action', + references: [], + updated_at: createdAction.updated_at, + version: createdAction.version, + attributes: { + description: 'My description', + actionTypeId: 'test', + actionTypeConfig: { + unencrypted: 'not encrypted value', + }, + }, + }); + }); + + it(`doesn't return encrypted attributes on update`, async () => { + const { body: createdAction } = await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + attributes: { + description: 'My description', + actionTypeId: 'test', + actionTypeConfig: { + unencrypted: 'not encrypted value', + encrypted: 'encrypted by default value', + }, + }, + }) + .expect(200); + const { body: result } = await supertest + .put(`/api/action/${createdAction.id}`) + .set('kbn-xsrf', 'foo') + .send({ + attributes: { + description: 'My description 2', + actionTypeId: 'test', + actionTypeConfig: { + unencrypted: 'not encrypted value', + encrypted: 'encrypted by default value', + }, + }, + }) + .expect(200); + expect(result).to.eql({ + id: createdAction.id, + type: 'action', + references: [], + updated_at: result.updated_at, + version: result.version, + attributes: { + description: 'My description 2', + actionTypeId: 'test', + actionTypeConfig: { + unencrypted: 'not encrypted value', + }, + }, + }); + }); + + it(`update without re-providing encrypted attributes erases them`, async () => { + const { body: createdAction } = await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + attributes: { + description: 'My description', + actionTypeId: 'test', + actionTypeConfig: { + unencrypted: 'not encrypted value', + encrypted: 'encrypted by default value', + }, + }, + }) + .expect(200); + await supertest + .put(`/api/action/${createdAction.id}`) + .set('kbn-xsrf', 'foo') + .send({ + attributes: { + description: 'My description 2', + actionTypeId: 'test', + actionTypeConfig: { + unencrypted: 'not encrypted value', + }, + }, + }) + .expect(200); + await supertest + .post(`/api/action/${createdAction.id}/fire`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + foo: true, + bar: false, + }, + }) + .expect(200) + .then((resp: any) => { + expect(resp.body).to.eql({ + success: true, + actionTypeConfig: { + unencrypted: 'not encrypted value', + }, + params: { + foo: true, + bar: false, + }, + }); + }); + }); + + it(`doesn't return encrypted attributes on find`, async () => { + const { body: createdAction } = await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + attributes: { + description: 'sometexttofind', + actionTypeId: 'test', + actionTypeConfig: { + unencrypted: 'not encrypted value', + encrypted: 'encrypted by default value', + }, + }, + }) + .expect(200); + + const { body: result } = await supertest + .get('/api/action/_find?search=sometexttofind') + .expect(200); + expect(result).to.eql({ + page: 1, + per_page: 20, + total: 1, + saved_objects: [ + { + id: createdAction.id, + type: 'action', + references: [], + updated_at: createdAction.updated_at, + version: createdAction.version, + attributes: { + description: 'sometexttofind', + actionTypeId: 'test', + actionTypeConfig: { + unencrypted: 'not encrypted value', + }, + }, + }, + ], + }); + }); + }); +} diff --git a/x-pack/test/plugin_api_integration/test_suites/actions/index.ts b/x-pack/test/plugin_api_integration/test_suites/actions/index.ts new file mode 100644 index 0000000000000..f123cb366cec5 --- /dev/null +++ b/x-pack/test/plugin_api_integration/test_suites/actions/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('actions', function actionsSuite() { + this.tags('ciGroup2'); + loadTestFile(require.resolve('./actions')); + }); +} From 95730538728d79b050f9aeb1dccb0000a3778f16 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Fri, 10 May 2019 08:50:27 -0400 Subject: [PATCH 34/51] Fix type check failure --- .../plugins/actions/index.ts | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/x-pack/test/plugin_api_integration/plugins/actions/index.ts b/x-pack/test/plugin_api_integration/plugins/actions/index.ts index 01c82d212621e..279356b71125c 100644 --- a/x-pack/test/plugin_api_integration/plugins/actions/index.ts +++ b/x-pack/test/plugin_api_integration/plugins/actions/index.ts @@ -6,23 +6,6 @@ import Joi from 'joi'; import { Legacy } from 'kibana'; -import { Request } from 'hapi'; -import { ActionsPlugin } from '../../../../plugins/actions'; - -// @ts-ignore -interface FireRequest extends Request { - params: { - id: string; - }; - payload: { - params: Record; - }; - server: { - plugins: { - actions: ActionsPlugin; - }; - }; -} // eslint-disable-next-line import/no-default-export export default function actionsPlugin(kibana: any) { @@ -47,7 +30,7 @@ export default function actionsPlugin(kibana: any) { .required(), }, }, - async handler(request: FireRequest) { + async handler(request: any) { const savedObjectsClient = request.getSavedObjectsClient(); return await request.server.plugins.actions.fire({ id: request.params.id, From 1aa660c491def106dfc7391206682601dbe00b08 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Fri, 10 May 2019 11:29:44 -0400 Subject: [PATCH 35/51] Code cleanup --- x-pack/plugins/actions/index.ts | 3 +- .../server/__jest__/action_service.test.ts | 2 + .../plugins/actions/server/action_service.ts | 53 +++++++++---------- .../actions/server/action_type_service.ts | 16 +++--- x-pack/plugins/actions/server/init.ts | 6 +-- x-pack/plugins/actions/server/routes/find.ts | 2 +- .../server/routes/list_action_types.ts | 2 +- 7 files changed, 42 insertions(+), 42 deletions(-) diff --git a/x-pack/plugins/actions/index.ts b/x-pack/plugins/actions/index.ts index c9fc35c8cb841..3c9b987376638 100644 --- a/x-pack/plugins/actions/index.ts +++ b/x-pack/plugins/actions/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Root } from 'joi'; import mappings from './mappings.json'; import { init } from './server'; @@ -14,7 +15,7 @@ export function actions(kibana: any) { id: 'actions', configPrefix: 'xpack.actions', require: ['kibana', 'elasticsearch', 'encrypted_saved_objects'], - config(Joi: any) { + config(Joi: Root) { return Joi.object() .keys({ enabled: Joi.boolean().default(true), diff --git a/x-pack/plugins/actions/server/__jest__/action_service.test.ts b/x-pack/plugins/actions/server/__jest__/action_service.test.ts index 4a6b21e6b90f7..d4cf9a66916d8 100644 --- a/x-pack/plugins/actions/server/__jest__/action_service.test.ts +++ b/x-pack/plugins/actions/server/__jest__/action_service.test.ts @@ -22,6 +22,8 @@ const savedObjectsClient = { beforeEach(() => jest.resetAllMocks()); const mockEncryptedSavedObjects = { + isEncryptionError: jest.fn(), + registerType: jest.fn(), getDecryptedAsInternalUser: jest.fn(), }; diff --git a/x-pack/plugins/actions/server/action_service.ts b/x-pack/plugins/actions/server/action_service.ts index a5cee15463424..f0f3b9ecd6178 100644 --- a/x-pack/plugins/actions/server/action_service.ts +++ b/x-pack/plugins/actions/server/action_service.ts @@ -6,6 +6,7 @@ import Boom from 'boom'; import { SavedObjectsClient } from 'src/legacy/server/saved_objects'; +import { EncryptedSavedObjectsPlugin } from '../../encrypted_saved_objects'; import { ActionTypeService } from './action_type_service'; import { SavedObjectReference } from './types'; @@ -15,11 +16,7 @@ interface Action { actionTypeConfig: Record; } -interface EncryptedAction extends Action { - actionTypeConfigSecrets: Record; -} - -interface FireActionOptions { +interface FireOptions { id: string; params: Record; savedObjectsClient: SavedObjectsClient; @@ -45,12 +42,15 @@ interface CreateOptions { } export class ActionService { - public actionTypes: ActionTypeService; - private encryptedSavedObjects: any; + private actionTypeService: ActionTypeService; + private encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin; - constructor(actionTypeService: ActionTypeService, encryptedSavedObjects: any) { - this.actionTypes = actionTypeService; - this.encryptedSavedObjects = encryptedSavedObjects; + constructor( + actionTypeService: ActionTypeService, + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin + ) { + this.actionTypeService = actionTypeService; + this.encryptedSavedObjectsPlugin = encryptedSavedObjectsPlugin; } /** @@ -62,12 +62,12 @@ export class ActionService { options?: CreateOptions ) { const { actionTypeId } = data; - if (!this.actionTypes.has(actionTypeId)) { + if (!this.actionTypeService.has(actionTypeId)) { throw Boom.badRequest(`Action type "${actionTypeId}" is not registered.`); } - this.actionTypes.validateActionTypeConfig(actionTypeId, data.actionTypeConfig); + this.actionTypeService.validateActionTypeConfig(actionTypeId, data.actionTypeConfig); const actionWithSplitActionTypeConfig = this.moveEncryptedAttributesToSecrets(data); - return await savedObjectsClient.create('action', actionWithSplitActionTypeConfig, options); + return await savedObjectsClient.create('action', actionWithSplitActionTypeConfig, options); } /** @@ -80,7 +80,7 @@ export class ActionService { /** * Find actions */ - public async find(savedObjectsClient: SavedObjectsClient, options: FindOptions) { + public async find(savedObjectsClient: SavedObjectsClient, options: FindOptions = {}) { return await savedObjectsClient.find({ ...options, type: 'action', @@ -101,32 +101,27 @@ export class ActionService { savedObjectsClient: SavedObjectsClient, id: string, data: Action, - options: { version?: string; references?: SavedObjectReference[] } + options: { version?: string; references?: SavedObjectReference[] } = {} ) { const { actionTypeId } = data; - if (!this.actionTypes.has(actionTypeId)) { + if (!this.actionTypeService.has(actionTypeId)) { throw Boom.badRequest(`Action type "${actionTypeId}" is not registered.`); } - this.actionTypes.validateActionTypeConfig(actionTypeId, data.actionTypeConfig); + this.actionTypeService.validateActionTypeConfig(actionTypeId, data.actionTypeConfig); const actionWithSplitActionTypeConfig = this.moveEncryptedAttributesToSecrets(data); - return await savedObjectsClient.update( - 'action', - id, - actionWithSplitActionTypeConfig, - options - ); + return await savedObjectsClient.update('action', id, actionWithSplitActionTypeConfig, options); } /** * Fire an action */ - public async fire({ id, params, savedObjectsClient }: FireActionOptions) { - const action = await this.encryptedSavedObjects.getDecryptedAsInternalUser('action', id); + public async fire({ id, params, savedObjectsClient }: FireOptions) { + const action = await this.encryptedSavedObjectsPlugin.getDecryptedAsInternalUser('action', id); const mergedActionTypeConfig = { ...action.attributes.actionTypeConfig, ...action.attributes.actionTypeConfigSecrets, }; - return await this.actionTypes.execute({ + return await this.actionTypeService.execute({ id: action.attributes.actionTypeId, actionTypeConfig: mergedActionTypeConfig, params, @@ -136,8 +131,10 @@ export class ActionService { /** * Set actionTypeConfigSecrets values on a given action */ - private moveEncryptedAttributesToSecrets(action: Action): EncryptedAction { - const unencryptedAttributes = this.actionTypes.getUnencryptedAttributes(action.actionTypeId); + private moveEncryptedAttributesToSecrets(action: Action) { + const unencryptedAttributes = this.actionTypeService.getUnencryptedAttributes( + action.actionTypeId + ); const config = { ...action.actionTypeConfig }; const configSecrets: Record = {}; for (const key of Object.keys(config)) { diff --git a/x-pack/plugins/actions/server/action_type_service.ts b/x-pack/plugins/actions/server/action_type_service.ts index 21ebbccbe1301..932d60bf1adfc 100644 --- a/x-pack/plugins/actions/server/action_type_service.ts +++ b/x-pack/plugins/actions/server/action_type_service.ts @@ -7,8 +7,8 @@ import Boom from 'boom'; interface ExecutorOptions { - actionTypeConfig: any; - params: any; + actionTypeConfig: Record; + params: Record; } interface ActionType { @@ -16,16 +16,16 @@ interface ActionType { name: string; unencryptedAttributes?: string[]; validate?: { - params?: any; - actionTypeConfig?: any; + params?: Record; + actionTypeConfig?: Record; }; executor({ actionTypeConfig, params }: ExecutorOptions): Promise; } interface ExecuteOptions { id: string; - actionTypeConfig: any; - params: any; + actionTypeConfig: Record; + params: Record; } export class ActionTypeService { @@ -79,7 +79,7 @@ export class ActionTypeService { /** * Throws an error if params are invalid for given action type */ - public validateParams(id: string, params: any) { + public validateParams(id: string, params: Record) { const actionType = this.get(id); const validator = actionType.validate && actionType.validate.params; if (validator) { @@ -93,7 +93,7 @@ export class ActionTypeService { /** * Throws an error if actionTypeConfig is invalid for given action type */ - public validateActionTypeConfig(id: string, actionTypeConfig: any) { + public validateActionTypeConfig(id: string, actionTypeConfig: Record) { const actionType = this.get(id); const validator = actionType.validate && actionType.validate.actionTypeConfig; if (validator) { diff --git a/x-pack/plugins/actions/server/init.ts b/x-pack/plugins/actions/server/init.ts index 8f1972b273d67..25c9463d11ad7 100644 --- a/x-pack/plugins/actions/server/init.ts +++ b/x-pack/plugins/actions/server/init.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import Hapi from 'hapi'; +import { Legacy } from 'kibana'; import { ActionService } from './action_service'; import { ActionTypeService } from './action_type_service'; import { @@ -16,7 +16,7 @@ import { listActionTypesRoute, } from './routes'; -export function init(server: Hapi.Server) { +export function init(server: Legacy.Server) { const actionsEnabled = server.config().get('xpack.actions.enabled'); if (!actionsEnabled) { @@ -33,7 +33,7 @@ export function init(server: Hapi.Server) { const actionTypeService = new ActionTypeService(); const actionService = new ActionService( actionTypeService, - server.plugins.encrypted_saved_objects + server.plugins.encrypted_saved_objects! ); // Routes diff --git a/x-pack/plugins/actions/server/routes/find.ts b/x-pack/plugins/actions/server/routes/find.ts index dfcbc1f08562f..9c85800693cf0 100644 --- a/x-pack/plugins/actions/server/routes/find.ts +++ b/x-pack/plugins/actions/server/routes/find.ts @@ -25,7 +25,7 @@ interface FindRequest extends WithoutQueryAndParams { }; } -export function findRoute(server: any) { +export function findRoute(server: Hapi.Server) { server.route({ method: 'GET', path: `/api/action/_find`, diff --git a/x-pack/plugins/actions/server/routes/list_action_types.ts b/x-pack/plugins/actions/server/routes/list_action_types.ts index 1dc49d45ff4f9..d5ed49e77084d 100644 --- a/x-pack/plugins/actions/server/routes/list_action_types.ts +++ b/x-pack/plugins/actions/server/routes/list_action_types.ts @@ -6,7 +6,7 @@ import Hapi from 'hapi'; -export function listActionTypesRoute(server: any) { +export function listActionTypesRoute(server: Hapi.Server) { server.route({ method: 'GET', path: `/api/action/types`, From e57191a0ac8fdfb0fba4e6a0e18b10d81e0b79a5 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Fri, 10 May 2019 15:53:52 -0400 Subject: [PATCH 36/51] Create an actions client instead of an action service --- x-pack/plugins/actions/index.ts | 2 +- x-pack/plugins/actions/mappings.json | 2 +- ...service.test.ts => actions_client.test.ts} | 187 +++++++++++------- .../{action_service.ts => actions_client.ts} | 71 ++++--- x-pack/plugins/actions/server/index.ts | 1 + x-pack/plugins/actions/server/init.ts | 22 +-- .../server/routes/__jest__/_mock_server.ts | 73 +++---- .../server/routes/__jest__/create.test.ts | 48 ++--- .../server/routes/__jest__/delete.test.ts | 16 +- .../server/routes/__jest__/find.test.ts | 16 +- .../server/routes/__jest__/get.test.ts | 16 +- .../routes/__jest__/list_action_types.test.ts | 6 +- .../server/routes/__jest__/update.test.ts | 48 ++--- .../plugins/actions/server/routes/create.ts | 17 +- .../plugins/actions/server/routes/delete.ts | 4 +- x-pack/plugins/actions/server/routes/find.ts | 4 +- x-pack/plugins/actions/server/routes/get.ts | 4 +- .../plugins/actions/server/routes/update.ts | 11 +- x-pack/plugins/actions/server/types.ts | 14 +- x-pack/typings/hapi.d.ts | 5 +- 20 files changed, 294 insertions(+), 273 deletions(-) rename x-pack/plugins/actions/server/__jest__/{action_service.test.ts => actions_client.test.ts} (77%) rename x-pack/plugins/actions/server/{action_service.ts => actions_client.ts} (71%) diff --git a/x-pack/plugins/actions/index.ts b/x-pack/plugins/actions/index.ts index 3c9b987376638..a2e0a387d63e1 100644 --- a/x-pack/plugins/actions/index.ts +++ b/x-pack/plugins/actions/index.ts @@ -8,7 +8,7 @@ import { Root } from 'joi'; import mappings from './mappings.json'; import { init } from './server'; -export { ActionsPlugin } from './server'; +export { ActionsPlugin, ActionsClient } from './server'; export function actions(kibana: any) { return new kibana.Plugin({ diff --git a/x-pack/plugins/actions/mappings.json b/x-pack/plugins/actions/mappings.json index d538220dae452..e76612ca56c48 100644 --- a/x-pack/plugins/actions/mappings.json +++ b/x-pack/plugins/actions/mappings.json @@ -8,7 +8,7 @@ "type": "keyword" }, "actionTypeConfig": { - "dynamic": "true", + "enabled": false, "type": "object" }, "actionTypeConfigSecrets": { diff --git a/x-pack/plugins/actions/server/__jest__/action_service.test.ts b/x-pack/plugins/actions/server/__jest__/actions_client.test.ts similarity index 77% rename from x-pack/plugins/actions/server/__jest__/action_service.test.ts rename to x-pack/plugins/actions/server/__jest__/actions_client.test.ts index d4cf9a66916d8..4da15778e63f4 100644 --- a/x-pack/plugins/actions/server/__jest__/action_service.test.ts +++ b/x-pack/plugins/actions/server/__jest__/actions_client.test.ts @@ -6,7 +6,7 @@ import Joi from 'joi'; import { ActionTypeService } from '../action_type_service'; -import { ActionService } from '../action_service'; +import { ActionsClient } from '../actions_client'; const savedObjectsClient = { errors: {} as any, @@ -36,20 +36,23 @@ describe('create()', () => { name: 'My action type', async executor() {}, }); - const actionService = new ActionService(actionTypeService, mockEncryptedSavedObjects); - savedObjectsClient.create.mockResolvedValueOnce(expectedResult); - const result = await actionService.create( + const actionService = new ActionsClient({ + actionTypeService, + encryptedSavedObjectsPlugin: mockEncryptedSavedObjects, savedObjectsClient, - { + }); + savedObjectsClient.create.mockResolvedValueOnce(expectedResult); + const result = await actionService.create({ + data: { description: 'my description', actionTypeId: 'my-action-type', actionTypeConfig: {}, }, - { + options: { migrationVersion: {}, references: [], - } - ); + }, + }); expect(result).toEqual(expectedResult); expect(savedObjectsClient.create).toMatchInlineSnapshot(` [MockFunction] { @@ -80,7 +83,11 @@ describe('create()', () => { test('validates actionTypeConfig', async () => { const actionTypeService = new ActionTypeService(); - const actionService = new ActionService(actionTypeService, mockEncryptedSavedObjects); + const actionService = new ActionsClient({ + actionTypeService, + encryptedSavedObjectsPlugin: mockEncryptedSavedObjects, + savedObjectsClient, + }); actionTypeService.register({ id: 'my-action-type', name: 'My action type', @@ -94,10 +101,12 @@ describe('create()', () => { async executor() {}, }); await expect( - actionService.create(savedObjectsClient, { - description: 'my description', - actionTypeId: 'my-action-type', - actionTypeConfig: {}, + actionService.create({ + data: { + description: 'my description', + actionTypeId: 'my-action-type', + actionTypeConfig: {}, + }, }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"child \\"param1\\" fails because [\\"param1\\" is required]"` @@ -106,12 +115,18 @@ describe('create()', () => { test(`throws an error when an action type doesn't exist`, async () => { const actionTypeService = new ActionTypeService(); - const actionService = new ActionService(actionTypeService, mockEncryptedSavedObjects); + const actionService = new ActionsClient({ + actionTypeService, + encryptedSavedObjectsPlugin: mockEncryptedSavedObjects, + savedObjectsClient, + }); await expect( - actionService.create(savedObjectsClient, { - description: 'my description', - actionTypeId: 'unregistered-action-type', - actionTypeConfig: {}, + actionService.create({ + data: { + description: 'my description', + actionTypeId: 'unregistered-action-type', + actionTypeConfig: {}, + }, }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Action type \\"unregistered-action-type\\" is not registered."` @@ -127,15 +142,21 @@ describe('create()', () => { unencryptedAttributes: ['a', 'c'], async executor() {}, }); - const actionService = new ActionService(actionTypeService, mockEncryptedSavedObjects); + const actionService = new ActionsClient({ + actionTypeService, + encryptedSavedObjectsPlugin: mockEncryptedSavedObjects, + savedObjectsClient, + }); savedObjectsClient.create.mockResolvedValueOnce(expectedResult); - const result = await actionService.create(savedObjectsClient, { - description: 'my description', - actionTypeId: 'my-action-type', - actionTypeConfig: { - a: true, - b: true, - c: true, + const result = await actionService.create({ + data: { + description: 'my description', + actionTypeId: 'my-action-type', + actionTypeConfig: { + a: true, + b: true, + c: true, + }, }, }); expect(result).toEqual(expectedResult); @@ -173,9 +194,13 @@ describe('get()', () => { test('calls savedObjectsClient with id', async () => { const expectedResult = Symbol(); const actionTypeService = new ActionTypeService(); - const actionService = new ActionService(actionTypeService, mockEncryptedSavedObjects); + const actionService = new ActionsClient({ + actionTypeService, + encryptedSavedObjectsPlugin: mockEncryptedSavedObjects, + savedObjectsClient, + }); savedObjectsClient.get.mockResolvedValueOnce(expectedResult); - const result = await actionService.get(savedObjectsClient, '1'); + const result = await actionService.get({ id: '1' }); expect(result).toEqual(expectedResult); expect(savedObjectsClient.get).toMatchInlineSnapshot(` [MockFunction] { @@ -200,9 +225,13 @@ describe('find()', () => { test('calls savedObjectsClient with parameters', async () => { const expectedResult = Symbol(); const actionTypeService = new ActionTypeService(); - const actionService = new ActionService(actionTypeService, mockEncryptedSavedObjects); + const actionService = new ActionsClient({ + actionTypeService, + encryptedSavedObjectsPlugin: mockEncryptedSavedObjects, + savedObjectsClient, + }); savedObjectsClient.find.mockResolvedValueOnce(expectedResult); - const result = await actionService.find(savedObjectsClient, {}); + const result = await actionService.find({}); expect(result).toEqual(expectedResult); expect(savedObjectsClient.find).toMatchInlineSnapshot(` [MockFunction] { @@ -228,9 +257,13 @@ describe('delete()', () => { test('calls savedObjectsClient with id', async () => { const expectedResult = Symbol(); const actionTypeService = new ActionTypeService(); - const actionService = new ActionService(actionTypeService, mockEncryptedSavedObjects); + const actionService = new ActionsClient({ + actionTypeService, + encryptedSavedObjectsPlugin: mockEncryptedSavedObjects, + savedObjectsClient, + }); savedObjectsClient.delete.mockResolvedValueOnce(expectedResult); - const result = await actionService.delete(savedObjectsClient, '1'); + const result = await actionService.delete({ id: '1' }); expect(result).toEqual(expectedResult); expect(savedObjectsClient.delete).toMatchInlineSnapshot(` [MockFunction] { @@ -260,18 +293,21 @@ describe('update()', () => { name: 'My action type', async executor() {}, }); - const actionService = new ActionService(actionTypeService, mockEncryptedSavedObjects); - savedObjectsClient.update.mockResolvedValueOnce(expectedResult); - const result = await actionService.update( + const actionService = new ActionsClient({ + actionTypeService, + encryptedSavedObjectsPlugin: mockEncryptedSavedObjects, savedObjectsClient, - 'my-action', - { + }); + savedObjectsClient.update.mockResolvedValueOnce(expectedResult); + const result = await actionService.update({ + id: 'my-action', + data: { description: 'my description', actionTypeId: 'my-action-type', actionTypeConfig: {}, }, - {} - ); + options: {}, + }); expect(result).toEqual(expectedResult); expect(savedObjectsClient.update).toMatchInlineSnapshot(` [MockFunction] { @@ -300,7 +336,11 @@ describe('update()', () => { test('validates actionTypeConfig', async () => { const actionTypeService = new ActionTypeService(); - const actionService = new ActionService(actionTypeService, mockEncryptedSavedObjects); + const actionService = new ActionsClient({ + actionTypeService, + encryptedSavedObjectsPlugin: mockEncryptedSavedObjects, + savedObjectsClient, + }); actionTypeService.register({ id: 'my-action-type', name: 'My action type', @@ -314,16 +354,15 @@ describe('update()', () => { async executor() {}, }); await expect( - actionService.update( - savedObjectsClient, - 'my-action', - { + actionService.update({ + id: 'my-action', + data: { description: 'my description', actionTypeId: 'my-action-type', actionTypeConfig: {}, }, - {} - ) + options: {}, + }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"child \\"param1\\" fails because [\\"param1\\" is required]"` ); @@ -331,18 +370,21 @@ describe('update()', () => { test(`throws an error when action type doesn't exist`, async () => { const actionTypeService = new ActionTypeService(); - const actionService = new ActionService(actionTypeService, mockEncryptedSavedObjects); + const actionService = new ActionsClient({ + actionTypeService, + encryptedSavedObjectsPlugin: mockEncryptedSavedObjects, + savedObjectsClient, + }); await expect( - actionService.update( - savedObjectsClient, - 'my-action', - { + actionService.update({ + id: 'my-action', + data: { description: 'my description', actionTypeId: 'unregistered-action-type', actionTypeConfig: {}, }, - {} - ) + options: {}, + }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Action type \\"unregistered-action-type\\" is not registered."` ); @@ -357,12 +399,15 @@ describe('update()', () => { unencryptedAttributes: ['a', 'c'], async executor() {}, }); - const actionService = new ActionService(actionTypeService, mockEncryptedSavedObjects); - savedObjectsClient.update.mockResolvedValueOnce(expectedResult); - const result = await actionService.update( + const actionService = new ActionsClient({ + actionTypeService, + encryptedSavedObjectsPlugin: mockEncryptedSavedObjects, savedObjectsClient, - 'my-action', - { + }); + savedObjectsClient.update.mockResolvedValueOnce(expectedResult); + const result = await actionService.update({ + id: 'my-action', + data: { description: 'my description', actionTypeId: 'my-action-type', actionTypeConfig: { @@ -371,8 +416,8 @@ describe('update()', () => { c: true, }, }, - {} - ); + options: {}, + }); expect(result).toEqual(expectedResult); expect(savedObjectsClient.update).toMatchInlineSnapshot(` [MockFunction] { @@ -408,7 +453,11 @@ describe('update()', () => { describe('fire()', () => { test('fires an action with all given parameters', async () => { const actionTypeService = new ActionTypeService(); - const actionService = new ActionService(actionTypeService, mockEncryptedSavedObjects); + const actionService = new ActionsClient({ + actionTypeService, + encryptedSavedObjectsPlugin: mockEncryptedSavedObjects, + savedObjectsClient, + }); const mockActionType = jest.fn().mockResolvedValueOnce({ success: true }); actionTypeService.register({ id: 'mock', @@ -427,7 +476,6 @@ describe('fire()', () => { const result = await actionService.fire({ id: 'mock-action', params: { baz: false }, - savedObjectsClient, }); expect(result).toEqual({ success: true }); expect(mockActionType).toMatchInlineSnapshot(` @@ -459,7 +507,11 @@ describe('fire()', () => { test(`throws an error when the action type isn't registered`, async () => { const actionTypeService = new ActionTypeService(); - const actionService = new ActionService(actionTypeService, mockEncryptedSavedObjects); + const actionService = new ActionsClient({ + actionTypeService, + encryptedSavedObjectsPlugin: mockEncryptedSavedObjects, + savedObjectsClient, + }); mockEncryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ id: 'mock-action', attributes: { @@ -470,7 +522,7 @@ describe('fire()', () => { }, }); await expect( - actionService.fire({ savedObjectsClient, id: 'mock-action', params: { baz: false } }) + actionService.fire({ id: 'mock-action', params: { baz: false } }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Action type \\"non-registered-action-type\\" is not registered."` ); @@ -478,7 +530,11 @@ describe('fire()', () => { test('merges encrypted and unencrypted attributes', async () => { const actionTypeService = new ActionTypeService(); - const actionService = new ActionService(actionTypeService, mockEncryptedSavedObjects); + const actionService = new ActionsClient({ + actionTypeService, + encryptedSavedObjectsPlugin: mockEncryptedSavedObjects, + savedObjectsClient, + }); const mockActionType = jest.fn().mockResolvedValueOnce({ success: true }); actionTypeService.register({ id: 'mock', @@ -502,7 +558,6 @@ describe('fire()', () => { const result = await actionService.fire({ id: 'mock-action', params: { baz: false }, - savedObjectsClient, }); expect(result).toEqual({ success: true }); expect(mockActionType).toMatchInlineSnapshot(` diff --git a/x-pack/plugins/actions/server/action_service.ts b/x-pack/plugins/actions/server/actions_client.ts similarity index 71% rename from x-pack/plugins/actions/server/action_service.ts rename to x-pack/plugins/actions/server/actions_client.ts index f0f3b9ecd6178..63e2bb416ac12 100644 --- a/x-pack/plugins/actions/server/action_service.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -19,7 +19,14 @@ interface Action { interface FireOptions { id: string; params: Record; - savedObjectsClient: SavedObjectsClient; +} + +interface CreateOptions { + data: Action; + options?: { + migrationVersion?: Record; + references?: SavedObjectReference[]; + }; } interface FindOptions { @@ -36,52 +43,58 @@ interface FindOptions { fields?: string[]; } -interface CreateOptions { - migrationVersion?: Record; - references?: SavedObjectReference[]; +interface ConstructorOptions { + actionTypeService: ActionTypeService; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin; + savedObjectsClient: SavedObjectsClient; } -export class ActionService { +interface UpdateOptions { + id: string; + data: Action; + options: { version?: string; references?: SavedObjectReference[] }; +} + +export class ActionsClient { + private savedObjectsClient: SavedObjectsClient; private actionTypeService: ActionTypeService; private encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin; - constructor( - actionTypeService: ActionTypeService, - encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin - ) { + constructor({ + actionTypeService, + encryptedSavedObjectsPlugin, + savedObjectsClient, + }: ConstructorOptions) { this.actionTypeService = actionTypeService; this.encryptedSavedObjectsPlugin = encryptedSavedObjectsPlugin; + this.savedObjectsClient = savedObjectsClient; } /** * Create an action */ - public async create( - savedObjectsClient: SavedObjectsClient, - data: Action, - options?: CreateOptions - ) { + public async create({ data, options }: CreateOptions) { const { actionTypeId } = data; if (!this.actionTypeService.has(actionTypeId)) { throw Boom.badRequest(`Action type "${actionTypeId}" is not registered.`); } this.actionTypeService.validateActionTypeConfig(actionTypeId, data.actionTypeConfig); const actionWithSplitActionTypeConfig = this.moveEncryptedAttributesToSecrets(data); - return await savedObjectsClient.create('action', actionWithSplitActionTypeConfig, options); + return await this.savedObjectsClient.create('action', actionWithSplitActionTypeConfig, options); } /** * Get an action */ - public async get(savedObjectsClient: SavedObjectsClient, id: string) { - return await savedObjectsClient.get('action', id); + public async get({ id }: { id: string }) { + return await this.savedObjectsClient.get('action', id); } /** * Find actions */ - public async find(savedObjectsClient: SavedObjectsClient, options: FindOptions = {}) { - return await savedObjectsClient.find({ + public async find(options: FindOptions = {}) { + return await this.savedObjectsClient.find({ ...options, type: 'action', }); @@ -90,32 +103,32 @@ export class ActionService { /** * Delete action */ - public async delete(savedObjectsClient: SavedObjectsClient, id: string) { - return await savedObjectsClient.delete('action', id); + public async delete({ id }: { id: string }) { + return await this.savedObjectsClient.delete('action', id); } /** * Update action */ - public async update( - savedObjectsClient: SavedObjectsClient, - id: string, - data: Action, - options: { version?: string; references?: SavedObjectReference[] } = {} - ) { + public async update({ id, data, options = {} }: UpdateOptions) { const { actionTypeId } = data; if (!this.actionTypeService.has(actionTypeId)) { throw Boom.badRequest(`Action type "${actionTypeId}" is not registered.`); } this.actionTypeService.validateActionTypeConfig(actionTypeId, data.actionTypeConfig); const actionWithSplitActionTypeConfig = this.moveEncryptedAttributesToSecrets(data); - return await savedObjectsClient.update('action', id, actionWithSplitActionTypeConfig, options); + return await this.savedObjectsClient.update( + 'action', + id, + actionWithSplitActionTypeConfig, + options + ); } /** * Fire an action */ - public async fire({ id, params, savedObjectsClient }: FireOptions) { + public async fire({ id, params }: FireOptions) { const action = await this.encryptedSavedObjectsPlugin.getDecryptedAsInternalUser('action', id); const mergedActionTypeConfig = { ...action.attributes.actionTypeConfig, diff --git a/x-pack/plugins/actions/server/index.ts b/x-pack/plugins/actions/server/index.ts index 678b3b46a6663..bb5f94b843514 100644 --- a/x-pack/plugins/actions/server/index.ts +++ b/x-pack/plugins/actions/server/index.ts @@ -6,3 +6,4 @@ export { init } from './init'; export { ActionsPlugin } from './types'; +export { ActionsClient } from './actions_client'; diff --git a/x-pack/plugins/actions/server/init.ts b/x-pack/plugins/actions/server/init.ts index 25c9463d11ad7..63bb895bdded7 100644 --- a/x-pack/plugins/actions/server/init.ts +++ b/x-pack/plugins/actions/server/init.ts @@ -5,7 +5,7 @@ */ import { Legacy } from 'kibana'; -import { ActionService } from './action_service'; +import { ActionsClient } from './actions_client'; import { ActionTypeService } from './action_type_service'; import { createRoute, @@ -31,10 +31,6 @@ export function init(server: Legacy.Server) { }); const actionTypeService = new ActionTypeService(); - const actionService = new ActionService( - actionTypeService, - server.plugins.encrypted_saved_objects! - ); // Routes createRoute(server); @@ -45,12 +41,16 @@ export function init(server: Legacy.Server) { listActionTypesRoute(server); // Expose service to server - server.expose('create', actionService.create.bind(actionService)); - server.expose('get', actionService.get.bind(actionService)); - server.expose('find', actionService.find.bind(actionService)); - server.expose('delete', actionService.delete.bind(actionService)); - server.expose('update', actionService.update.bind(actionService)); - server.expose('fire', actionService.fire.bind(actionService)); + server.decorate('request', 'getActionsClient', function() { + const request = this; + const savedObjectsClient = request.getSavedObjectsClient(); + const actionsClient = new ActionsClient({ + savedObjectsClient, + actionTypeService, + encryptedSavedObjectsPlugin: server.plugins.encrypted_saved_objects!, + }); + return actionsClient; + }); server.expose('registerType', actionTypeService.register.bind(actionTypeService)); server.expose('listTypes', actionTypeService.list.bind(actionTypeService)); } diff --git a/x-pack/plugins/actions/server/routes/__jest__/_mock_server.ts b/x-pack/plugins/actions/server/routes/__jest__/_mock_server.ts index f7dc49b92f6a3..3a10c49203e25 100644 --- a/x-pack/plugins/actions/server/routes/__jest__/_mock_server.ts +++ b/x-pack/plugins/actions/server/routes/__jest__/_mock_server.ts @@ -10,28 +10,36 @@ const defaultConfig = { 'kibana.index': '.kibana', }; -interface PluginProperties extends Hapi.PluginProperties { - actions: { - create: jest.Mock; - get: jest.Mock; - find: jest.Mock; - delete: jest.Mock; - update: jest.Mock; - fire: jest.Mock; - registerType: jest.Mock; - listTypes: jest.Mock; - }; -} - -interface MockServer extends Hapi.Server { - plugins: PluginProperties; -} - -export function createMockServer(config: Record = defaultConfig): MockServer { +export function createMockServer(config: Record = defaultConfig) { const server = new Hapi.Server({ port: 0, }); + const savedObjectsClient = { + errors: {} as any, + bulkCreate: jest.fn(), + bulkGet: jest.fn(), + create: jest.fn(), + delete: jest.fn(), + find: jest.fn(), + get: jest.fn(), + update: jest.fn(), + }; + + const actionsClient = { + create: jest.fn(), + get: jest.fn(), + find: jest.fn(), + delete: jest.fn(), + update: jest.fn(), + fire: jest.fn(), + }; + + const actionTypeService = { + registerType: jest.fn(), + listTypes: jest.fn(), + }; + server.config = () => { return { get(key: string) { @@ -46,32 +54,13 @@ export function createMockServer(config: Record = defaultConfig): M server.register({ name: 'actions', register(pluginServer: Hapi.Server) { - pluginServer.expose('create', jest.fn()); - pluginServer.expose('get', jest.fn()); - pluginServer.expose('find', jest.fn()); - pluginServer.expose('delete', jest.fn()); - pluginServer.expose('update', jest.fn()); - pluginServer.expose('fire', jest.fn()); - pluginServer.expose('registerType', jest.fn()); - pluginServer.expose('listTypes', jest.fn()); + pluginServer.expose('registerType', actionTypeService.registerType); + pluginServer.expose('listTypes', actionTypeService.listTypes); }, }); - server.ext('onRequest', (request, h) => { - const client = { - errors: {} as any, - bulkCreate: jest.fn(), - bulkGet: jest.fn(), - create: jest.fn(), - delete: jest.fn(), - find: jest.fn(), - get: jest.fn(), - update: jest.fn(), - }; - request.getSavedObjectsClient = () => client; - return h.continue; - }); + server.decorate('request', 'getSavedObjectsClient', () => savedObjectsClient); + server.decorate('request', 'getActionsClient', () => actionsClient); - // @ts-ignore - return server; + return { server, savedObjectsClient, actionsClient, actionTypeService }; } diff --git a/x-pack/plugins/actions/server/routes/__jest__/create.test.ts b/x-pack/plugins/actions/server/routes/__jest__/create.test.ts index cd03d1fbf3b7c..a1e0321918d91 100644 --- a/x-pack/plugins/actions/server/routes/__jest__/create.test.ts +++ b/x-pack/plugins/actions/server/routes/__jest__/create.test.ts @@ -7,7 +7,7 @@ import { createMockServer } from './_mock_server'; import { createRoute } from '../create'; -const server = createMockServer(); +const { server, actionsClient } = createMockServer(); createRoute(server); beforeEach(() => { @@ -37,43 +37,35 @@ it('creates an action with proper parameters', async () => { }, }; - server.plugins.actions.create.mockResolvedValueOnce({ success: true }); + actionsClient.create.mockResolvedValueOnce({ success: true }); const { payload, statusCode } = await server.inject(request); expect(statusCode).toBe(200); const response = JSON.parse(payload); expect(response).toEqual({ success: true }); - expect(server.plugins.actions.create).toMatchInlineSnapshot(` + expect(actionsClient.create).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ Array [ Object { - "bulkCreate": [MockFunction], - "bulkGet": [MockFunction], - "create": [MockFunction], - "delete": [MockFunction], - "errors": Object {}, - "find": [MockFunction], - "get": [MockFunction], - "update": [MockFunction], - }, - Object { - "actionTypeConfig": Object { - "foo": true, - }, - "actionTypeId": "abc", - "description": "My description", - }, - Object { - "migrationVersion": Object { - "abc": "1.2.3", + "data": Object { + "actionTypeConfig": Object { + "foo": true, + }, + "actionTypeId": "abc", + "description": "My description", }, - "references": Array [ - Object { - "id": "234", - "name": "ref_0", - "type": "bcd", + "options": Object { + "migrationVersion": Object { + "abc": "1.2.3", }, - ], + "references": Array [ + Object { + "id": "234", + "name": "ref_0", + "type": "bcd", + }, + ], + }, }, ], ], diff --git a/x-pack/plugins/actions/server/routes/__jest__/delete.test.ts b/x-pack/plugins/actions/server/routes/__jest__/delete.test.ts index fca8dd41f701e..29269680cc2ef 100644 --- a/x-pack/plugins/actions/server/routes/__jest__/delete.test.ts +++ b/x-pack/plugins/actions/server/routes/__jest__/delete.test.ts @@ -7,7 +7,7 @@ import { createMockServer } from './_mock_server'; import { deleteRoute } from '../delete'; -const server = createMockServer(); +const { server, actionsClient } = createMockServer(); deleteRoute(server); beforeEach(() => { @@ -20,26 +20,18 @@ it('deletes an action with proper parameters', async () => { url: '/api/action/1', }; - server.plugins.actions.delete.mockResolvedValueOnce({ success: true }); + actionsClient.delete.mockResolvedValueOnce({ success: true }); const { payload, statusCode } = await server.inject(request); expect(statusCode).toBe(200); const response = JSON.parse(payload); expect(response).toEqual({ success: true }); - expect(server.plugins.actions.delete).toMatchInlineSnapshot(` + expect(actionsClient.delete).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ Array [ Object { - "bulkCreate": [MockFunction], - "bulkGet": [MockFunction], - "create": [MockFunction], - "delete": [MockFunction], - "errors": Object {}, - "find": [MockFunction], - "get": [MockFunction], - "update": [MockFunction], + "id": "1", }, - "1", ], ], "results": Array [ diff --git a/x-pack/plugins/actions/server/routes/__jest__/find.test.ts b/x-pack/plugins/actions/server/routes/__jest__/find.test.ts index bcffd092784fd..8a74941f0d5f4 100644 --- a/x-pack/plugins/actions/server/routes/__jest__/find.test.ts +++ b/x-pack/plugins/actions/server/routes/__jest__/find.test.ts @@ -7,7 +7,7 @@ import { createMockServer } from './_mock_server'; import { findRoute } from '../find'; -const server = createMockServer(); +const { server, actionsClient } = createMockServer(); findRoute(server); beforeEach(() => { @@ -28,25 +28,15 @@ it('sends proper arguments to action find function', async () => { 'fields=description', }; - server.plugins.actions.find.mockResolvedValueOnce({ success: true }); + actionsClient.find.mockResolvedValueOnce({ success: true }); const { payload, statusCode } = await server.inject(request); expect(statusCode).toBe(200); const response = JSON.parse(payload); expect(response).toEqual({ success: true }); - expect(server.plugins.actions.find).toMatchInlineSnapshot(` + expect(actionsClient.find).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ Array [ - Object { - "bulkCreate": [MockFunction], - "bulkGet": [MockFunction], - "create": [MockFunction], - "delete": [MockFunction], - "errors": Object {}, - "find": [MockFunction], - "get": [MockFunction], - "update": [MockFunction], - }, Object { "defaultSearchOperator": "AND", "fields": Array [ diff --git a/x-pack/plugins/actions/server/routes/__jest__/get.test.ts b/x-pack/plugins/actions/server/routes/__jest__/get.test.ts index 8b603e248b727..36e37b1d3688e 100644 --- a/x-pack/plugins/actions/server/routes/__jest__/get.test.ts +++ b/x-pack/plugins/actions/server/routes/__jest__/get.test.ts @@ -7,7 +7,7 @@ import { createMockServer } from './_mock_server'; import { getRoute } from '../get'; -const server = createMockServer(); +const { server, actionsClient } = createMockServer(); getRoute(server); beforeEach(() => { @@ -20,26 +20,18 @@ it('calls get with proper parameters', async () => { url: '/api/action/1', }; - server.plugins.actions.get.mockResolvedValueOnce({ success: true }); + actionsClient.get.mockResolvedValueOnce({ success: true }); const { payload, statusCode } = await server.inject(request); expect(statusCode).toBe(200); const response = JSON.parse(payload); expect(response).toEqual({ success: true }); - expect(server.plugins.actions.get).toMatchInlineSnapshot(` + expect(actionsClient.get).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ Array [ Object { - "bulkCreate": [MockFunction], - "bulkGet": [MockFunction], - "create": [MockFunction], - "delete": [MockFunction], - "errors": Object {}, - "find": [MockFunction], - "get": [MockFunction], - "update": [MockFunction], + "id": "1", }, - "1", ], ], "results": Array [ diff --git a/x-pack/plugins/actions/server/routes/__jest__/list_action_types.test.ts b/x-pack/plugins/actions/server/routes/__jest__/list_action_types.test.ts index f7dac793399cd..3f3baf5d43ec6 100644 --- a/x-pack/plugins/actions/server/routes/__jest__/list_action_types.test.ts +++ b/x-pack/plugins/actions/server/routes/__jest__/list_action_types.test.ts @@ -7,7 +7,7 @@ import { createMockServer } from './_mock_server'; import { listActionTypesRoute } from '../list_action_types'; -const server = createMockServer(); +const { server, actionTypeService } = createMockServer(); listActionTypesRoute(server); beforeEach(() => { @@ -20,12 +20,12 @@ it('calls the list function', async () => { url: '/api/action/types', }; - server.plugins.actions.listTypes.mockResolvedValueOnce({ success: true }); + actionTypeService.listTypes.mockResolvedValueOnce({ success: true }); const { payload, statusCode } = await server.inject(request); expect(statusCode).toBe(200); const response = JSON.parse(payload); expect(response).toEqual({ success: true }); - expect(server.plugins.actions.listTypes).toMatchInlineSnapshot(` + expect(actionTypeService.listTypes).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ Array [], diff --git a/x-pack/plugins/actions/server/routes/__jest__/update.test.ts b/x-pack/plugins/actions/server/routes/__jest__/update.test.ts index c0cc064458bf1..59e75b240ec2c 100644 --- a/x-pack/plugins/actions/server/routes/__jest__/update.test.ts +++ b/x-pack/plugins/actions/server/routes/__jest__/update.test.ts @@ -7,7 +7,7 @@ import { createMockServer } from './_mock_server'; import { updateRoute } from '../update'; -const server = createMockServer(); +const { server, actionsClient } = createMockServer(); updateRoute(server); beforeEach(() => { @@ -35,42 +35,34 @@ it('calls the update function with proper parameters', async () => { }, }; - server.plugins.actions.update.mockResolvedValueOnce({ success: true }); + actionsClient.update.mockResolvedValueOnce({ success: true }); const { payload, statusCode } = await server.inject(request); expect(statusCode).toBe(200); const response = JSON.parse(payload); expect(response).toEqual({ success: true }); - expect(server.plugins.actions.update).toMatchInlineSnapshot(` + expect(actionsClient.update).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ Array [ Object { - "bulkCreate": [MockFunction], - "bulkGet": [MockFunction], - "create": [MockFunction], - "delete": [MockFunction], - "errors": Object {}, - "find": [MockFunction], - "get": [MockFunction], - "update": [MockFunction], - }, - "1", - Object { - "actionTypeConfig": Object { - "foo": true, - }, - "actionTypeId": "abc", - "description": "My description", - }, - Object { - "references": Array [ - Object { - "id": "234", - "name": "ref_0", - "type": "bcd", + "data": Object { + "actionTypeConfig": Object { + "foo": true, }, - ], - "version": "2", + "actionTypeId": "abc", + "description": "My description", + }, + "id": "1", + "options": Object { + "references": Array [ + Object { + "id": "234", + "name": "ref_0", + "type": "bcd", + }, + ], + "version": "2", + }, }, ], ], diff --git a/x-pack/plugins/actions/server/routes/create.ts b/x-pack/plugins/actions/server/routes/create.ts index 602410fcc1afe..011c929c16b0e 100644 --- a/x-pack/plugins/actions/server/routes/create.ts +++ b/x-pack/plugins/actions/server/routes/create.ts @@ -32,6 +32,9 @@ export function createRoute(server: Hapi.Server) { path: `/api/action`, options: { validate: { + options: { + abortEarly: false, + }, payload: Joi.object().keys({ attributes: Joi.object() .keys({ @@ -54,15 +57,15 @@ export function createRoute(server: Hapi.Server) { }, }, async handler(request: CreateRequest) { - const savedObjectsClient = request.getSavedObjectsClient(); - return await request.server.plugins.actions.create( - savedObjectsClient, - request.payload.attributes, - { + const actionsClient = request.getActionsClient(); + + return await actionsClient.create({ + data: request.payload.attributes, + options: { migrationVersion: request.payload.migrationVersion, references: request.payload.references, - } - ); + }, + }); }, }); } diff --git a/x-pack/plugins/actions/server/routes/delete.ts b/x-pack/plugins/actions/server/routes/delete.ts index 1d75871617000..4b32386f75b4d 100644 --- a/x-pack/plugins/actions/server/routes/delete.ts +++ b/x-pack/plugins/actions/server/routes/delete.ts @@ -28,8 +28,8 @@ export function deleteRoute(server: Hapi.Server) { }, async handler(request: DeleteRequest) { const { id } = request.params; - const savedObjectsClient = request.getSavedObjectsClient(); - return await request.server.plugins.actions.delete(savedObjectsClient, id); + const actionsClient = request.getActionsClient(); + return await actionsClient.delete({ id }); }, }); } diff --git a/x-pack/plugins/actions/server/routes/find.ts b/x-pack/plugins/actions/server/routes/find.ts index 9c85800693cf0..7196c1b0ccd5c 100644 --- a/x-pack/plugins/actions/server/routes/find.ts +++ b/x-pack/plugins/actions/server/routes/find.ts @@ -64,8 +64,8 @@ export function findRoute(server: Hapi.Server) { }, async handler(request: FindRequest) { const query = request.query; - const savedObjectsClient = request.getSavedObjectsClient(); - return await request.server.plugins.actions.find(savedObjectsClient, { + const actionsClient = request.getActionsClient(); + return await actionsClient.find({ perPage: query.per_page, page: query.page, search: query.search, diff --git a/x-pack/plugins/actions/server/routes/get.ts b/x-pack/plugins/actions/server/routes/get.ts index b0008d40a518e..5a8e617d36331 100644 --- a/x-pack/plugins/actions/server/routes/get.ts +++ b/x-pack/plugins/actions/server/routes/get.ts @@ -28,8 +28,8 @@ export function getRoute(server: Hapi.Server) { }, async handler(request: GetRequest) { const { id } = request.params; - const savedObjectsClient = request.getSavedObjectsClient(); - return await request.server.plugins.actions.get(savedObjectsClient, id); + const actionsClient = request.getActionsClient(); + return await actionsClient.get({ id }); }, }); } diff --git a/x-pack/plugins/actions/server/routes/update.ts b/x-pack/plugins/actions/server/routes/update.ts index 49138dd3f6e2e..e99609b691c79 100644 --- a/x-pack/plugins/actions/server/routes/update.ts +++ b/x-pack/plugins/actions/server/routes/update.ts @@ -59,13 +59,12 @@ export function updateRoute(server: Hapi.Server) { const { id } = request.params; const { attributes, version, references } = request.payload; const options = { version, references }; - const savedObjectsClient = request.getSavedObjectsClient(); - return await request.server.plugins.actions.update( - savedObjectsClient, + const actionsClient = request.getActionsClient(); + return await actionsClient.update({ id, - attributes, - options - ); + data: attributes, + options, + }); }, }); } diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index 3b9f595b821a3..2b800ede03092 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ActionService } from './action_service'; +// import { ActionService } from './action_service'; import { ActionTypeService } from './action_type_service'; export type WithoutQueryAndParams = Pick>; @@ -16,12 +16,12 @@ export interface SavedObjectReference { } export interface ActionsPlugin { - create: ActionService['create']; - get: ActionService['get']; - find: ActionService['find']; - delete: ActionService['delete']; - update: ActionService['update']; - fire: ActionService['fire']; + // create: ActionService['create']; + // get: ActionService['get']; + // find: ActionService['find']; + // delete: ActionService['delete']; + // update: ActionService['update']; + // fire: ActionService['fire']; registerType: ActionTypeService['register']; listTypes: ActionTypeService['list']; } diff --git a/x-pack/typings/hapi.d.ts b/x-pack/typings/hapi.d.ts index f25f29868dd06..2b6fe84763517 100644 --- a/x-pack/typings/hapi.d.ts +++ b/x-pack/typings/hapi.d.ts @@ -10,9 +10,12 @@ import { CloudPlugin } from '../plugins/cloud'; import { EncryptedSavedObjectsPlugin } from '../plugins/encrypted_saved_objects'; import { XPackMainPlugin } from '../plugins/xpack_main/xpack_main'; import { SecurityPlugin } from '../plugins/security'; -import { ActionsPlugin } from '../plugins/actions'; +import { ActionsPlugin, ActionsClient } from '../plugins/actions'; declare module 'hapi' { + interface Request { + getActionsClient: () => ActionsClient; + } interface PluginProperties { cloud?: CloudPlugin; xpack_main: XPackMainPlugin; From 6d9363da199caeba53677edd745ae601400b10a4 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Sun, 19 May 2019 04:36:01 -0400 Subject: [PATCH 37/51] Apply Bill's PR feedback --- .../__jest__/action_type_service.test.ts | 1 + .../actions/server/action_type_service.ts | 19 +++++++++++++++++-- .../plugins/actions/server/actions_client.ts | 10 +++++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/actions/server/__jest__/action_type_service.test.ts b/x-pack/plugins/actions/server/__jest__/action_type_service.test.ts index 0f1ccdd23db14..413193202a10b 100644 --- a/x-pack/plugins/actions/server/__jest__/action_type_service.test.ts +++ b/x-pack/plugins/actions/server/__jest__/action_type_service.test.ts @@ -16,6 +16,7 @@ describe('register()', () => { name: 'My action type', executor, }); + expect(actionTypeService.has('my-action-type')).toEqual(true); }); test('throws error if action type already registered', () => { diff --git a/x-pack/plugins/actions/server/action_type_service.ts b/x-pack/plugins/actions/server/action_type_service.ts index 932d60bf1adfc..17a711e11016b 100644 --- a/x-pack/plugins/actions/server/action_type_service.ts +++ b/x-pack/plugins/actions/server/action_type_service.ts @@ -5,6 +5,7 @@ */ import Boom from 'boom'; +import { i18n } from '@kbn/i18n'; interface ExecutorOptions { actionTypeConfig: Record; @@ -43,7 +44,14 @@ export class ActionTypeService { */ public register(actionType: ActionType) { if (this.has(actionType.id)) { - throw Boom.badRequest(`Action type "${actionType.id}" is already registered.`); + throw Boom.badRequest( + i18n.translate('xpack.actions.actionTypeService.register.duplicateActionTypeError', { + defaultMessage: 'Action type "{id}" is already registered.', + values: { + id: actionType.id, + }, + }) + ); } this.actionTypes[actionType.id] = actionType; } @@ -53,7 +61,14 @@ export class ActionTypeService { */ public get(id: string) { if (!this.actionTypes[id]) { - throw Boom.badRequest(`Action type "${id}" is not registered.`); + throw Boom.badRequest( + i18n.translate('xpack.actions.actionTypeService.get.missingActionTypeError', { + defaultMessage: 'Action type "{id}" is not registered.', + values: { + id, + }, + }) + ); } return this.actionTypes[id]; } diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index 63e2bb416ac12..072c919fb938f 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -5,6 +5,7 @@ */ import Boom from 'boom'; +import { i18n } from '@kbn/i18n'; import { SavedObjectsClient } from 'src/legacy/server/saved_objects'; import { EncryptedSavedObjectsPlugin } from '../../encrypted_saved_objects'; import { ActionTypeService } from './action_type_service'; @@ -76,7 +77,14 @@ export class ActionsClient { public async create({ data, options }: CreateOptions) { const { actionTypeId } = data; if (!this.actionTypeService.has(actionTypeId)) { - throw Boom.badRequest(`Action type "${actionTypeId}" is not registered.`); + throw Boom.badRequest( + i18n.translate('xpack.actions.actionsClient.create.missingActionTypeError', { + defaultMessage: 'Action type "{actionTypeId}" is not registered.', + values: { + actionTypeId, + }, + }) + ); } this.actionTypeService.validateActionTypeConfig(actionTypeId, data.actionTypeConfig); const actionWithSplitActionTypeConfig = this.moveEncryptedAttributesToSecrets(data); From c26eaf6ddc9151a91e09d73c7d5c2faca08253ad Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Sun, 19 May 2019 04:43:25 -0400 Subject: [PATCH 38/51] Fix broken test --- x-pack/test/plugin_api_integration/plugins/actions/index.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/x-pack/test/plugin_api_integration/plugins/actions/index.ts b/x-pack/test/plugin_api_integration/plugins/actions/index.ts index 279356b71125c..caccbf30360a2 100644 --- a/x-pack/test/plugin_api_integration/plugins/actions/index.ts +++ b/x-pack/test/plugin_api_integration/plugins/actions/index.ts @@ -31,11 +31,10 @@ export default function actionsPlugin(kibana: any) { }, }, async handler(request: any) { - const savedObjectsClient = request.getSavedObjectsClient(); - return await request.server.plugins.actions.fire({ + const actionsClient = request.getActionsClient(); + return await actionsClient.fire({ id: request.params.id, params: request.payload.params, - savedObjectsClient, }); }, }); From 9225e4480364006fffc64ffd93fbe80c68ebfd1b Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Tue, 21 May 2019 09:09:51 -0400 Subject: [PATCH 39/51] Find function to have destructured params --- .../plugins/actions/server/actions_client.ts | 24 ++++++++++--------- x-pack/plugins/actions/server/routes/find.ts | 18 +++++++------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index 072c919fb938f..cfefda071b8ae 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -31,17 +31,19 @@ interface CreateOptions { } interface FindOptions { - perPage?: number; - page?: number; - search?: string; - defaultSearchOperator?: 'AND' | 'OR'; - searchFields?: string[]; - sortField?: string; - hasReference?: { - type: string; - id: string; + options?: { + perPage?: number; + page?: number; + search?: string; + defaultSearchOperator?: 'AND' | 'OR'; + searchFields?: string[]; + sortField?: string; + hasReference?: { + type: string; + id: string; + }; + fields?: string[]; }; - fields?: string[]; } interface ConstructorOptions { @@ -101,7 +103,7 @@ export class ActionsClient { /** * Find actions */ - public async find(options: FindOptions = {}) { + public async find({ options = {} }: FindOptions) { return await this.savedObjectsClient.find({ ...options, type: 'action', diff --git a/x-pack/plugins/actions/server/routes/find.ts b/x-pack/plugins/actions/server/routes/find.ts index 7196c1b0ccd5c..cdd50725692e3 100644 --- a/x-pack/plugins/actions/server/routes/find.ts +++ b/x-pack/plugins/actions/server/routes/find.ts @@ -66,14 +66,16 @@ export function findRoute(server: Hapi.Server) { const query = request.query; const actionsClient = request.getActionsClient(); return await actionsClient.find({ - perPage: query.per_page, - page: query.page, - search: query.search, - defaultSearchOperator: query.default_search_operator, - searchFields: query.search_fields, - sortField: query.sort_field, - hasReference: query.has_reference, - fields: query.fields, + options: { + perPage: query.per_page, + page: query.page, + search: query.search, + defaultSearchOperator: query.default_search_operator, + searchFields: query.search_fields, + sortField: query.sort_field, + hasReference: query.has_reference, + fields: query.fields, + }, }); }, }); From 7e4dc577d76f9e308954497825b32b22163fd66d Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Tue, 21 May 2019 09:43:55 -0400 Subject: [PATCH 40/51] Add tests to ensure encrypted attributes are not returned --- .../api_integration/apis/actions/create.ts | 34 +++++++++++++++++++ .../test/api_integration/apis/actions/find.ts | 28 +++++++++++++++ .../test/api_integration/apis/actions/get.ts | 5 ++- .../api_integration/apis/actions/update.ts | 33 ++++++++++++++++++ .../es_archives/alerting/basic/data.json | 6 ++-- 5 files changed, 100 insertions(+), 6 deletions(-) diff --git a/x-pack/test/api_integration/apis/actions/create.ts b/x-pack/test/api_integration/apis/actions/create.ts index 3c6fef025d6c6..4db1f30b4eb58 100644 --- a/x-pack/test/api_integration/apis/actions/create.ts +++ b/x-pack/test/api_integration/apis/actions/create.ts @@ -41,6 +41,40 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe }); }); + it('should not return encrypted attributes', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + attributes: { + description: 'My action', + actionTypeId: 'test', + actionTypeConfig: { + unencrypted: 'unencrypted text', + encrypted: 'something encrypted', + }, + }, + }) + .expect(200) + .then((resp: any) => { + expect(resp.body).to.eql({ + type: 'action', + id: resp.body.id, + attributes: { + description: 'My action', + actionTypeId: 'test', + actionTypeConfig: { + unencrypted: 'unencrypted text', + }, + }, + references: [], + updated_at: resp.body.updated_at, + version: resp.body.version, + }); + expect(typeof resp.body.id).to.be('string'); + }); + }); + it(`should return 400 when action type isn't registered`, async () => { await supertest .post('/api/action') diff --git a/x-pack/test/api_integration/apis/actions/find.ts b/x-pack/test/api_integration/apis/actions/find.ts index 80d7b0fafa5ac..5ba3bd09c8c40 100644 --- a/x-pack/test/api_integration/apis/actions/find.ts +++ b/x-pack/test/api_integration/apis/actions/find.ts @@ -39,5 +39,33 @@ export default function findActionTests({ getService }: KibanaFunctionalTestDefa }); }); }); + + it('should not return encrypted attributes', async () => { + await supertest + .get('/api/action/_find') + .expect(200) + .then((resp: any) => { + expect(resp.body).to.eql({ + page: 1, + per_page: 20, + total: 1, + saved_objects: [ + { + id: '1', + type: 'action', + version: resp.body.saved_objects[0].version, + references: [], + attributes: { + description: 'My description', + actionTypeId: 'test', + actionTypeConfig: { + unencrypted: 'unencrypted text', + }, + }, + }, + ], + }); + }); + }); }); } diff --git a/x-pack/test/api_integration/apis/actions/get.ts b/x-pack/test/api_integration/apis/actions/get.ts index a400ae7728ff5..ebfb8686fb33f 100644 --- a/x-pack/test/api_integration/apis/actions/get.ts +++ b/x-pack/test/api_integration/apis/actions/get.ts @@ -16,7 +16,7 @@ export default function getActionTests({ getService }: KibanaFunctionalTestDefau before(() => esArchiver.load('alerting/basic')); after(() => esArchiver.unload('alerting/basic')); - it('should return 200 when finding a record', async () => { + it('should return 200 when finding a record and not return encrypted attributes', async () => { await supertest .get('/api/action/1') .expect(200) @@ -30,8 +30,7 @@ export default function getActionTests({ getService }: KibanaFunctionalTestDefau actionTypeId: 'test', description: 'My description', actionTypeConfig: { - bar: false, - foo: true, + unencrypted: 'unencrypted text', }, }, }); diff --git a/x-pack/test/api_integration/apis/actions/update.ts b/x-pack/test/api_integration/apis/actions/update.ts index db2785e246b13..b26297a694191 100644 --- a/x-pack/test/api_integration/apis/actions/update.ts +++ b/x-pack/test/api_integration/apis/actions/update.ts @@ -44,6 +44,39 @@ export default function updateActionTests({ getService }: KibanaFunctionalTestDe }); }); + it('should not return encrypted attributes', async () => { + await supertest + .put('/api/action/1') + .set('kbn-xsrf', 'foo') + .send({ + attributes: { + actionTypeId: 'test', + description: 'My description updated', + actionTypeConfig: { + unencrypted: 'unencrypted text', + encrypted: 'something encrypted', + }, + }, + }) + .expect(200) + .then((resp: any) => { + expect(resp.body).to.eql({ + id: '1', + type: 'action', + references: [], + version: resp.body.version, + updated_at: resp.body.updated_at, + attributes: { + actionTypeId: 'test', + description: 'My description updated', + actionTypeConfig: { + unencrypted: 'unencrypted text', + }, + }, + }); + }); + }); + it('should return 404 when updating a non existing document', async () => { await supertest .put('/api/action/2') diff --git a/x-pack/test/functional/es_archives/alerting/basic/data.json b/x-pack/test/functional/es_archives/alerting/basic/data.json index d041055f2131b..e1f889a551164 100644 --- a/x-pack/test/functional/es_archives/alerting/basic/data.json +++ b/x-pack/test/functional/es_archives/alerting/basic/data.json @@ -7,9 +7,9 @@ "description": "My description", "actionTypeId": "test", "actionTypeConfig": { - "foo": true, - "bar": false - } + "unencrypted" : "unencrypted text" + }, + "actionTypeConfigSecrets": "eCd4Z7Y3L6dVGNgpxHIOQR5rmP2UlxdUk7dFNAPLPmM96UqnyQqW0oFUWjox9SsvJtcN1Rdpst7HVxXRUPYs6XP0Dk7xcdNszXSYYnq5DcjEi+bhGPe3Ce00srY7NCMc0rZiTBn1KfW2xuaWl8qy1kYCiu+hq7eKgnJ/YIU9Fg==" }, "type": "action", "migrationVersion": {} From e1dd5253378c6eb0451c2859ddb58fd6663293fa Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Tue, 21 May 2019 09:48:57 -0400 Subject: [PATCH 41/51] Fix broken test --- .../server/routes/__jest__/find.test.ts | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/actions/server/routes/__jest__/find.test.ts b/x-pack/plugins/actions/server/routes/__jest__/find.test.ts index 8a74941f0d5f4..00b8e5c8c33bd 100644 --- a/x-pack/plugins/actions/server/routes/__jest__/find.test.ts +++ b/x-pack/plugins/actions/server/routes/__jest__/find.test.ts @@ -38,18 +38,20 @@ it('sends proper arguments to action find function', async () => { "calls": Array [ Array [ Object { - "defaultSearchOperator": "AND", - "fields": Array [ - "description", - ], - "hasReference": undefined, - "page": 1, - "perPage": 1, - "search": "text*", - "searchFields": Array [ - "description", - ], - "sortField": "description", + "options": Object { + "defaultSearchOperator": "AND", + "fields": Array [ + "description", + ], + "hasReference": undefined, + "page": 1, + "perPage": 1, + "search": "text*", + "searchFields": Array [ + "description", + ], + "sortField": "description", + }, }, ], ], From c4ef04ac321ccfb82d16a9b5607e7696e05f2362 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Tue, 21 May 2019 11:07:02 -0400 Subject: [PATCH 42/51] Add tests for validation --- .../plugins/actions/server/routes/update.ts | 3 ++ .../api_integration/apis/actions/create.ts | 41 +++++++++++++++++++ .../api_integration/apis/actions/update.ts | 38 +++++++++++++++++ 3 files changed, 82 insertions(+) diff --git a/x-pack/plugins/actions/server/routes/update.ts b/x-pack/plugins/actions/server/routes/update.ts index e99609b691c79..6948f77ea2137 100644 --- a/x-pack/plugins/actions/server/routes/update.ts +++ b/x-pack/plugins/actions/server/routes/update.ts @@ -27,6 +27,9 @@ export function updateRoute(server: Hapi.Server) { path: `/api/action/{id}`, options: { validate: { + options: { + abortEarly: false, + }, params: Joi.object() .keys({ id: Joi.string().required(), diff --git a/x-pack/test/api_integration/apis/actions/create.ts b/x-pack/test/api_integration/apis/actions/create.ts index 4db1f30b4eb58..41b72860d1ca4 100644 --- a/x-pack/test/api_integration/apis/actions/create.ts +++ b/x-pack/test/api_integration/apis/actions/create.ts @@ -95,5 +95,46 @@ export default function createActionTests({ getService }: KibanaFunctionalTestDe }); }); }); + + it('should return 400 when payload is empty and invalid', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({}) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: 'child "attributes" fails because ["attributes" is required]', + validation: { + source: 'payload', + keys: ['attributes'], + }, + }); + }); + }); + + it('should return 400 when payload attributes are empty and invalid', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + attributes: {}, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'child "attributes" fails because [child "description" fails because ["description" is required], child "actionTypeId" fails because ["actionTypeId" is required]]', + validation: { + source: 'payload', + keys: ['attributes.description', 'attributes.actionTypeId'], + }, + }); + }); + }); }); } diff --git a/x-pack/test/api_integration/apis/actions/update.ts b/x-pack/test/api_integration/apis/actions/update.ts index b26297a694191..a4737ce40ee1c 100644 --- a/x-pack/test/api_integration/apis/actions/update.ts +++ b/x-pack/test/api_integration/apis/actions/update.ts @@ -97,5 +97,43 @@ export default function updateActionTests({ getService }: KibanaFunctionalTestDe }); }); }); + + it('should return 400 when payload is empty and invalid', async () => { + await supertest + .put('/api/action/1') + .set('kbn-xsrf', 'foo') + .send({}) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: 'child "attributes" fails because ["attributes" is required]', + validation: { source: 'payload', keys: ['attributes'] }, + }); + }); + }); + + it('should return 400 when payload attributes are empty and invalid', async () => { + await supertest + .put('/api/action/1') + .set('kbn-xsrf', 'foo') + .send({ + attributes: {}, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'child "attributes" fails because [child "description" fails because ["description" is required], child "actionTypeId" fails because ["actionTypeId" is required]]', + validation: { + source: 'payload', + keys: ['attributes.description', 'attributes.actionTypeId'], + }, + }); + }); + }); }); } From fe23aec6347d547b1ffb1db9a70cd5c1dbb8c30d Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Tue, 21 May 2019 11:33:20 -0400 Subject: [PATCH 43/51] Ensure actions can be updated without re-passing the config --- .../plugins/actions/server/actions_client.ts | 13 ++--- .../api_integration/apis/actions/update.ts | 57 +++++++++++++++++++ 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index cfefda071b8ae..c2c8d9817b93b 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -125,14 +125,11 @@ export class ActionsClient { if (!this.actionTypeService.has(actionTypeId)) { throw Boom.badRequest(`Action type "${actionTypeId}" is not registered.`); } - this.actionTypeService.validateActionTypeConfig(actionTypeId, data.actionTypeConfig); - const actionWithSplitActionTypeConfig = this.moveEncryptedAttributesToSecrets(data); - return await this.savedObjectsClient.update( - 'action', - id, - actionWithSplitActionTypeConfig, - options - ); + if (data.actionTypeConfig) { + this.actionTypeService.validateActionTypeConfig(actionTypeId, data.actionTypeConfig); + data = this.moveEncryptedAttributesToSecrets(data); + } + return await this.savedObjectsClient.update('action', id, data, options); } /** diff --git a/x-pack/test/api_integration/apis/actions/update.ts b/x-pack/test/api_integration/apis/actions/update.ts index a4737ce40ee1c..9cbb7212c5e55 100644 --- a/x-pack/test/api_integration/apis/actions/update.ts +++ b/x-pack/test/api_integration/apis/actions/update.ts @@ -44,6 +44,63 @@ export default function updateActionTests({ getService }: KibanaFunctionalTestDe }); }); + it('should support partial updates', async () => { + await supertest + .put('/api/action/1') + .set('kbn-xsrf', 'foo') + .send({ + attributes: { + actionTypeId: 'test', + description: 'My description updated again', + }, + }) + .expect(200); + await supertest + .get('/api/action/1') + .expect(200) + .then((resp: any) => { + expect(resp.body).to.eql({ + id: '1', + type: 'action', + references: [], + version: resp.body.version, + updated_at: resp.body.updated_at, + attributes: { + actionTypeId: 'test', + description: 'My description updated again', + actionTypeConfig: { + unencrypted: 'unencrypted text', + }, + }, + }); + }); + }); + + it('should not be able to pass null to actionTypeConfig', async () => { + await supertest + .put('/api/action/1') + .set('kbn-xsrf', 'foo') + .send({ + attributes: { + actionTypeId: 'test', + description: 'My description updated', + actionTypeConfig: null, + }, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: 'child "attributes" fails because [child "actionTypeConfig" fails because ["actionTypeConfig" must be an object]]', + validation: { + source: 'payload', + keys: ['attributes.actionTypeConfig'], + }, + }); + }); + }); + it('should not return encrypted attributes', async () => { await supertest .put('/api/action/1') From 9fc83603ba88d0bf9e680e8911e9960858851ac3 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Tue, 21 May 2019 22:32:49 -0400 Subject: [PATCH 44/51] Remove dead code --- x-pack/plugins/actions/server/types.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index 2b800ede03092..dd4b197de3232 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -16,12 +16,6 @@ export interface SavedObjectReference { } export interface ActionsPlugin { - // create: ActionService['create']; - // get: ActionService['get']; - // find: ActionService['find']; - // delete: ActionService['delete']; - // update: ActionService['update']; - // fire: ActionService['fire']; registerType: ActionTypeService['register']; listTypes: ActionTypeService['list']; } From 8af6507e73cf58d8e535f632f7d9d1268e9f7ea1 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Tue, 21 May 2019 22:41:39 -0400 Subject: [PATCH 45/51] Test cleanup --- .../test_suites/actions/actions.ts | 204 ------------------ 1 file changed, 204 deletions(-) diff --git a/x-pack/test/plugin_api_integration/test_suites/actions/actions.ts b/x-pack/test/plugin_api_integration/test_suites/actions/actions.ts index 9f9b76943a441..5e0a253a5f1a4 100644 --- a/x-pack/test/plugin_api_integration/test_suites/actions/actions.ts +++ b/x-pack/test/plugin_api_integration/test_suites/actions/actions.ts @@ -65,209 +65,5 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) { }); }); }); - - it(`doesn't return encrypted attributes on create`, async () => { - const { body: createdAction } = await supertest - .post('/api/action') - .set('kbn-xsrf', 'foo') - .send({ - attributes: { - description: 'My description', - actionTypeId: 'test', - actionTypeConfig: { - unencrypted: 'not encrypted value', - encrypted: 'encrypted by default value', - }, - }, - }) - .expect(200); - - expect(createdAction).to.eql({ - id: createdAction.id, - type: 'action', - references: [], - updated_at: createdAction.updated_at, - version: createdAction.version, - attributes: { - description: 'My description', - actionTypeId: 'test', - actionTypeConfig: { - unencrypted: 'not encrypted value', - }, - }, - }); - }); - - it(`doesn't return encrypted attributes on get`, async () => { - const { body: createdAction } = await supertest - .post('/api/action') - .set('kbn-xsrf', 'foo') - .send({ - attributes: { - description: 'My description', - actionTypeId: 'test', - actionTypeConfig: { - unencrypted: 'not encrypted value', - encrypted: 'encrypted by default value', - }, - }, - }) - .expect(200); - - const { body: result } = await supertest.get(`/api/action/${createdAction.id}`).expect(200); - expect(result).to.eql({ - id: createdAction.id, - type: 'action', - references: [], - updated_at: createdAction.updated_at, - version: createdAction.version, - attributes: { - description: 'My description', - actionTypeId: 'test', - actionTypeConfig: { - unencrypted: 'not encrypted value', - }, - }, - }); - }); - - it(`doesn't return encrypted attributes on update`, async () => { - const { body: createdAction } = await supertest - .post('/api/action') - .set('kbn-xsrf', 'foo') - .send({ - attributes: { - description: 'My description', - actionTypeId: 'test', - actionTypeConfig: { - unencrypted: 'not encrypted value', - encrypted: 'encrypted by default value', - }, - }, - }) - .expect(200); - const { body: result } = await supertest - .put(`/api/action/${createdAction.id}`) - .set('kbn-xsrf', 'foo') - .send({ - attributes: { - description: 'My description 2', - actionTypeId: 'test', - actionTypeConfig: { - unencrypted: 'not encrypted value', - encrypted: 'encrypted by default value', - }, - }, - }) - .expect(200); - expect(result).to.eql({ - id: createdAction.id, - type: 'action', - references: [], - updated_at: result.updated_at, - version: result.version, - attributes: { - description: 'My description 2', - actionTypeId: 'test', - actionTypeConfig: { - unencrypted: 'not encrypted value', - }, - }, - }); - }); - - it(`update without re-providing encrypted attributes erases them`, async () => { - const { body: createdAction } = await supertest - .post('/api/action') - .set('kbn-xsrf', 'foo') - .send({ - attributes: { - description: 'My description', - actionTypeId: 'test', - actionTypeConfig: { - unencrypted: 'not encrypted value', - encrypted: 'encrypted by default value', - }, - }, - }) - .expect(200); - await supertest - .put(`/api/action/${createdAction.id}`) - .set('kbn-xsrf', 'foo') - .send({ - attributes: { - description: 'My description 2', - actionTypeId: 'test', - actionTypeConfig: { - unencrypted: 'not encrypted value', - }, - }, - }) - .expect(200); - await supertest - .post(`/api/action/${createdAction.id}/fire`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - foo: true, - bar: false, - }, - }) - .expect(200) - .then((resp: any) => { - expect(resp.body).to.eql({ - success: true, - actionTypeConfig: { - unencrypted: 'not encrypted value', - }, - params: { - foo: true, - bar: false, - }, - }); - }); - }); - - it(`doesn't return encrypted attributes on find`, async () => { - const { body: createdAction } = await supertest - .post('/api/action') - .set('kbn-xsrf', 'foo') - .send({ - attributes: { - description: 'sometexttofind', - actionTypeId: 'test', - actionTypeConfig: { - unencrypted: 'not encrypted value', - encrypted: 'encrypted by default value', - }, - }, - }) - .expect(200); - - const { body: result } = await supertest - .get('/api/action/_find?search=sometexttofind') - .expect(200); - expect(result).to.eql({ - page: 1, - per_page: 20, - total: 1, - saved_objects: [ - { - id: createdAction.id, - type: 'action', - references: [], - updated_at: createdAction.updated_at, - version: createdAction.version, - attributes: { - description: 'sometexttofind', - actionTypeId: 'test', - actionTypeConfig: { - unencrypted: 'not encrypted value', - }, - }, - }, - ], - }); - }); }); } From 0fab9c8842b88288c0a3a3833632bd15ef4b04d1 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Wed, 22 May 2019 08:34:07 -0400 Subject: [PATCH 46/51] Fix eslint issue --- x-pack/test/api_integration/apis/actions/update.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/actions/update.ts b/x-pack/test/api_integration/apis/actions/update.ts index 9cbb7212c5e55..e7a880c128f11 100644 --- a/x-pack/test/api_integration/apis/actions/update.ts +++ b/x-pack/test/api_integration/apis/actions/update.ts @@ -92,7 +92,8 @@ export default function updateActionTests({ getService }: KibanaFunctionalTestDe expect(resp.body).to.eql({ statusCode: 400, error: 'Bad Request', - message: 'child "attributes" fails because [child "actionTypeConfig" fails because ["actionTypeConfig" must be an object]]', + message: + 'child "attributes" fails because [child "actionTypeConfig" fails because ["actionTypeConfig" must be an object]]', validation: { source: 'payload', keys: ['attributes.actionTypeConfig'], From 30bcd8af349ff273894eac59f02db741b8165a5f Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Wed, 22 May 2019 15:56:46 -0400 Subject: [PATCH 47/51] Apply Peter's PR feedback --- .../plugins/actions/server/action_type_service.ts | 2 +- x-pack/plugins/actions/server/actions_client.ts | 15 ++------------- x-pack/plugins/actions/server/routes/find.ts | 2 +- 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/actions/server/action_type_service.ts b/x-pack/plugins/actions/server/action_type_service.ts index 17a711e11016b..bdc3b3310e72b 100644 --- a/x-pack/plugins/actions/server/action_type_service.ts +++ b/x-pack/plugins/actions/server/action_type_service.ts @@ -60,7 +60,7 @@ export class ActionTypeService { * Returns an action type, throws if not registered */ public get(id: string) { - if (!this.actionTypes[id]) { + if (!this.has(id)) { throw Boom.badRequest( i18n.translate('xpack.actions.actionTypeService.get.missingActionTypeError', { defaultMessage: 'Action type "{id}" is not registered.', diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index c2c8d9817b93b..e4dec97b35281 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -78,16 +78,6 @@ export class ActionsClient { */ public async create({ data, options }: CreateOptions) { const { actionTypeId } = data; - if (!this.actionTypeService.has(actionTypeId)) { - throw Boom.badRequest( - i18n.translate('xpack.actions.actionsClient.create.missingActionTypeError', { - defaultMessage: 'Action type "{actionTypeId}" is not registered.', - values: { - actionTypeId, - }, - }) - ); - } this.actionTypeService.validateActionTypeConfig(actionTypeId, data.actionTypeConfig); const actionWithSplitActionTypeConfig = this.moveEncryptedAttributesToSecrets(data); return await this.savedObjectsClient.create('action', actionWithSplitActionTypeConfig, options); @@ -122,9 +112,8 @@ export class ActionsClient { */ public async update({ id, data, options = {} }: UpdateOptions) { const { actionTypeId } = data; - if (!this.actionTypeService.has(actionTypeId)) { - throw Boom.badRequest(`Action type "${actionTypeId}" is not registered.`); - } + // Throws an error if action type is invalid + this.actionTypeService.get(actionTypeId); if (data.actionTypeConfig) { this.actionTypeService.validateActionTypeConfig(actionTypeId, data.actionTypeConfig); data = this.moveEncryptedAttributesToSecrets(data); diff --git a/x-pack/plugins/actions/server/routes/find.ts b/x-pack/plugins/actions/server/routes/find.ts index cdd50725692e3..39f09d1007f0c 100644 --- a/x-pack/plugins/actions/server/routes/find.ts +++ b/x-pack/plugins/actions/server/routes/find.ts @@ -37,7 +37,7 @@ export function findRoute(server: Hapi.Server) { .min(0) .default(20), page: Joi.number() - .min(0) + .min(1) .default(1), search: Joi.string() .allow('') From 910fc7b7ee14a0c94a37870c4f675d685c649c8a Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Wed, 22 May 2019 17:10:02 -0400 Subject: [PATCH 48/51] Code cleanup and fix broken tests --- x-pack/plugins/actions/server/actions_client.ts | 2 -- x-pack/plugins/actions/server/init.ts | 13 +++++++++---- .../actions/server/routes/list_action_types.ts | 2 +- x-pack/plugins/actions/server/types.ts | 4 +++- x-pack/typings/hapi.d.ts | 2 +- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index e4dec97b35281..5e3490c598e4a 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; -import { i18n } from '@kbn/i18n'; import { SavedObjectsClient } from 'src/legacy/server/saved_objects'; import { EncryptedSavedObjectsPlugin } from '../../encrypted_saved_objects'; import { ActionTypeService } from './action_type_service'; diff --git a/x-pack/plugins/actions/server/init.ts b/x-pack/plugins/actions/server/init.ts index 63bb895bdded7..280a12da8959b 100644 --- a/x-pack/plugins/actions/server/init.ts +++ b/x-pack/plugins/actions/server/init.ts @@ -5,6 +5,7 @@ */ import { Legacy } from 'kibana'; +import { SavedObjectsClient } from 'src/legacy/server/saved_objects'; import { ActionsClient } from './actions_client'; import { ActionTypeService } from './action_type_service'; import { @@ -40,17 +41,21 @@ export function init(server: Legacy.Server) { updateRoute(server); listActionTypesRoute(server); - // Expose service to server - server.decorate('request', 'getActionsClient', function() { - const request = this; - const savedObjectsClient = request.getSavedObjectsClient(); + function createActionsClient(savedObjectsClient: SavedObjectsClient) { const actionsClient = new ActionsClient({ savedObjectsClient, actionTypeService, encryptedSavedObjectsPlugin: server.plugins.encrypted_saved_objects!, }); return actionsClient; + } + + // Expose service to server + server.decorate('request', 'getActionsClient', function() { + const request = this; + return createActionsClient(request.getSavedObjectsClient()); }); + server.expose('createActionsClient', createActionsClient); server.expose('registerType', actionTypeService.register.bind(actionTypeService)); server.expose('listTypes', actionTypeService.list.bind(actionTypeService)); } diff --git a/x-pack/plugins/actions/server/routes/list_action_types.ts b/x-pack/plugins/actions/server/routes/list_action_types.ts index d5ed49e77084d..9ff04af72beaa 100644 --- a/x-pack/plugins/actions/server/routes/list_action_types.ts +++ b/x-pack/plugins/actions/server/routes/list_action_types.ts @@ -11,7 +11,7 @@ export function listActionTypesRoute(server: Hapi.Server) { method: 'GET', path: `/api/action/types`, async handler(request: Hapi.Request) { - return request.server.plugins.actions.listTypes(); + return request.server.plugins.actions!.listTypes(); }, }); } diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index dd4b197de3232..f5a2300c7961b 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -// import { ActionService } from './action_service'; +import { SavedObjectsClient } from 'src/legacy/server/saved_objects'; +import { ActionsClient } from './actions_client'; import { ActionTypeService } from './action_type_service'; export type WithoutQueryAndParams = Pick>; @@ -18,4 +19,5 @@ export interface SavedObjectReference { export interface ActionsPlugin { registerType: ActionTypeService['register']; listTypes: ActionTypeService['list']; + createActionsClient: (savedObjectsClient: SavedObjectsClient) => ActionsClient; } diff --git a/x-pack/typings/hapi.d.ts b/x-pack/typings/hapi.d.ts index 2b6fe84763517..16a4b640292ee 100644 --- a/x-pack/typings/hapi.d.ts +++ b/x-pack/typings/hapi.d.ts @@ -21,6 +21,6 @@ declare module 'hapi' { xpack_main: XPackMainPlugin; security?: SecurityPlugin; encrypted_saved_objects?: EncryptedSavedObjectsPlugin; - actions: ActionsPlugin; + actions?: ActionsPlugin; } } From e0df91dd4cfcc16ba5d815a6d219cf46d6b3395a Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Thu, 23 May 2019 17:00:06 -0400 Subject: [PATCH 49/51] Apply Brandon's PR feedback --- .../server/__jest__/actions_client.test.ts | 154 ------------------ .../__jest__/create_fire_function.test.ts | 148 +++++++++++++++++ .../actions/server/action_type_service.ts | 2 +- .../plugins/actions/server/actions_client.ts | 31 +--- .../actions/server/create_fire_function.ts | 36 ++++ x-pack/plugins/actions/server/init.ts | 21 +-- x-pack/plugins/actions/server/types.ts | 4 +- .../plugins/actions/index.ts | 3 +- 8 files changed, 199 insertions(+), 200 deletions(-) create mode 100644 x-pack/plugins/actions/server/__jest__/create_fire_function.test.ts create mode 100644 x-pack/plugins/actions/server/create_fire_function.ts diff --git a/x-pack/plugins/actions/server/__jest__/actions_client.test.ts b/x-pack/plugins/actions/server/__jest__/actions_client.test.ts index 4da15778e63f4..6e9fb8fa842b7 100644 --- a/x-pack/plugins/actions/server/__jest__/actions_client.test.ts +++ b/x-pack/plugins/actions/server/__jest__/actions_client.test.ts @@ -21,12 +21,6 @@ const savedObjectsClient = { beforeEach(() => jest.resetAllMocks()); -const mockEncryptedSavedObjects = { - isEncryptionError: jest.fn(), - registerType: jest.fn(), - getDecryptedAsInternalUser: jest.fn(), -}; - describe('create()', () => { test('creates an action with all given properties', async () => { const expectedResult = Symbol(); @@ -38,7 +32,6 @@ describe('create()', () => { }); const actionService = new ActionsClient({ actionTypeService, - encryptedSavedObjectsPlugin: mockEncryptedSavedObjects, savedObjectsClient, }); savedObjectsClient.create.mockResolvedValueOnce(expectedResult); @@ -85,7 +78,6 @@ describe('create()', () => { const actionTypeService = new ActionTypeService(); const actionService = new ActionsClient({ actionTypeService, - encryptedSavedObjectsPlugin: mockEncryptedSavedObjects, savedObjectsClient, }); actionTypeService.register({ @@ -117,7 +109,6 @@ describe('create()', () => { const actionTypeService = new ActionTypeService(); const actionService = new ActionsClient({ actionTypeService, - encryptedSavedObjectsPlugin: mockEncryptedSavedObjects, savedObjectsClient, }); await expect( @@ -144,7 +135,6 @@ describe('create()', () => { }); const actionService = new ActionsClient({ actionTypeService, - encryptedSavedObjectsPlugin: mockEncryptedSavedObjects, savedObjectsClient, }); savedObjectsClient.create.mockResolvedValueOnce(expectedResult); @@ -196,7 +186,6 @@ describe('get()', () => { const actionTypeService = new ActionTypeService(); const actionService = new ActionsClient({ actionTypeService, - encryptedSavedObjectsPlugin: mockEncryptedSavedObjects, savedObjectsClient, }); savedObjectsClient.get.mockResolvedValueOnce(expectedResult); @@ -227,7 +216,6 @@ describe('find()', () => { const actionTypeService = new ActionTypeService(); const actionService = new ActionsClient({ actionTypeService, - encryptedSavedObjectsPlugin: mockEncryptedSavedObjects, savedObjectsClient, }); savedObjectsClient.find.mockResolvedValueOnce(expectedResult); @@ -259,7 +247,6 @@ describe('delete()', () => { const actionTypeService = new ActionTypeService(); const actionService = new ActionsClient({ actionTypeService, - encryptedSavedObjectsPlugin: mockEncryptedSavedObjects, savedObjectsClient, }); savedObjectsClient.delete.mockResolvedValueOnce(expectedResult); @@ -295,7 +282,6 @@ describe('update()', () => { }); const actionService = new ActionsClient({ actionTypeService, - encryptedSavedObjectsPlugin: mockEncryptedSavedObjects, savedObjectsClient, }); savedObjectsClient.update.mockResolvedValueOnce(expectedResult); @@ -338,7 +324,6 @@ describe('update()', () => { const actionTypeService = new ActionTypeService(); const actionService = new ActionsClient({ actionTypeService, - encryptedSavedObjectsPlugin: mockEncryptedSavedObjects, savedObjectsClient, }); actionTypeService.register({ @@ -372,7 +357,6 @@ describe('update()', () => { const actionTypeService = new ActionTypeService(); const actionService = new ActionsClient({ actionTypeService, - encryptedSavedObjectsPlugin: mockEncryptedSavedObjects, savedObjectsClient, }); await expect( @@ -401,7 +385,6 @@ describe('update()', () => { }); const actionService = new ActionsClient({ actionTypeService, - encryptedSavedObjectsPlugin: mockEncryptedSavedObjects, savedObjectsClient, }); savedObjectsClient.update.mockResolvedValueOnce(expectedResult); @@ -449,140 +432,3 @@ describe('update()', () => { `); }); }); - -describe('fire()', () => { - test('fires an action with all given parameters', async () => { - const actionTypeService = new ActionTypeService(); - const actionService = new ActionsClient({ - actionTypeService, - encryptedSavedObjectsPlugin: mockEncryptedSavedObjects, - savedObjectsClient, - }); - const mockActionType = jest.fn().mockResolvedValueOnce({ success: true }); - actionTypeService.register({ - id: 'mock', - name: 'Mock', - executor: mockActionType, - }); - mockEncryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: 'mock-action', - attributes: { - actionTypeId: 'mock', - actionTypeConfigSecrets: { - foo: true, - }, - }, - }); - const result = await actionService.fire({ - id: 'mock-action', - params: { baz: false }, - }); - expect(result).toEqual({ success: true }); - expect(mockActionType).toMatchInlineSnapshot(` -[MockFunction] { - "calls": Array [ - Array [ - Object { - "actionTypeConfig": Object { - "foo": true, - }, - "params": Object { - "baz": false, - }, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], -} -`); - expect(mockEncryptedSavedObjects.getDecryptedAsInternalUser.mock.calls).toEqual([ - ['action', 'mock-action'], - ]); - }); - - test(`throws an error when the action type isn't registered`, async () => { - const actionTypeService = new ActionTypeService(); - const actionService = new ActionsClient({ - actionTypeService, - encryptedSavedObjectsPlugin: mockEncryptedSavedObjects, - savedObjectsClient, - }); - mockEncryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: 'mock-action', - attributes: { - actionTypeId: 'non-registered-action-type', - actionTypeConfigSecrets: { - foo: true, - }, - }, - }); - await expect( - actionService.fire({ id: 'mock-action', params: { baz: false } }) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Action type \\"non-registered-action-type\\" is not registered."` - ); - }); - - test('merges encrypted and unencrypted attributes', async () => { - const actionTypeService = new ActionTypeService(); - const actionService = new ActionsClient({ - actionTypeService, - encryptedSavedObjectsPlugin: mockEncryptedSavedObjects, - savedObjectsClient, - }); - const mockActionType = jest.fn().mockResolvedValueOnce({ success: true }); - actionTypeService.register({ - id: 'mock', - name: 'Mock', - unencryptedAttributes: ['a', 'c'], - executor: mockActionType, - }); - mockEncryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: 'mock-action', - attributes: { - actionTypeId: 'mock', - actionTypeConfig: { - a: true, - c: true, - }, - actionTypeConfigSecrets: { - b: true, - }, - }, - }); - const result = await actionService.fire({ - id: 'mock-action', - params: { baz: false }, - }); - expect(result).toEqual({ success: true }); - expect(mockActionType).toMatchInlineSnapshot(` -[MockFunction] { - "calls": Array [ - Array [ - Object { - "actionTypeConfig": Object { - "a": true, - "b": true, - "c": true, - }, - "params": Object { - "baz": false, - }, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], -} -`); - }); -}); diff --git a/x-pack/plugins/actions/server/__jest__/create_fire_function.test.ts b/x-pack/plugins/actions/server/__jest__/create_fire_function.test.ts new file mode 100644 index 0000000000000..7040b64c67e76 --- /dev/null +++ b/x-pack/plugins/actions/server/__jest__/create_fire_function.test.ts @@ -0,0 +1,148 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ActionTypeService } from '../action_type_service'; +import { createFireFunction } from '../create_fire_function'; + +const mockEncryptedSavedObjects = { + isEncryptionError: jest.fn(), + registerType: jest.fn(), + getDecryptedAsInternalUser: jest.fn(), +}; + +describe('fire()', () => { + test('fires an action with all given parameters', async () => { + const actionTypeService = new ActionTypeService(); + const fireFn = createFireFunction({ + actionTypeService, + encryptedSavedObjectsPlugin: mockEncryptedSavedObjects, + }); + const mockActionType = jest.fn().mockResolvedValueOnce({ success: true }); + actionTypeService.register({ + id: 'mock', + name: 'Mock', + executor: mockActionType, + }); + mockEncryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ + id: 'mock-action', + attributes: { + actionTypeId: 'mock', + actionTypeConfigSecrets: { + foo: true, + }, + }, + }); + const result = await fireFn({ + id: 'mock-action', + params: { baz: false }, + }); + expect(result).toEqual({ success: true }); + expect(mockActionType).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + Object { + "actionTypeConfig": Object { + "foo": true, + }, + "params": Object { + "baz": false, + }, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], +} +`); + expect(mockEncryptedSavedObjects.getDecryptedAsInternalUser.mock.calls).toEqual([ + ['action', 'mock-action'], + ]); + }); + + test(`throws an error when the action type isn't registered`, async () => { + const actionTypeService = new ActionTypeService(); + const fireFn = createFireFunction({ + actionTypeService, + encryptedSavedObjectsPlugin: mockEncryptedSavedObjects, + }); + mockEncryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ + id: 'mock-action', + attributes: { + actionTypeId: 'non-registered-action-type', + actionTypeConfigSecrets: { + foo: true, + }, + }, + }); + await expect( + fireFn({ id: 'mock-action', params: { baz: false } }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Action type \\"non-registered-action-type\\" is not registered."` + ); + }); + + test('merges encrypted and unencrypted attributes', async () => { + const actionTypeService = new ActionTypeService(); + const fireFn = createFireFunction({ + actionTypeService, + encryptedSavedObjectsPlugin: mockEncryptedSavedObjects, + }); + const mockActionType = jest.fn().mockResolvedValueOnce({ success: true }); + actionTypeService.register({ + id: 'mock', + name: 'Mock', + unencryptedAttributes: ['a', 'c'], + executor: mockActionType, + }); + mockEncryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ + id: 'mock-action', + attributes: { + actionTypeId: 'mock', + actionTypeConfig: { + a: true, + c: true, + }, + actionTypeConfigSecrets: { + b: true, + }, + }, + }); + const result = await fireFn({ + id: 'mock-action', + params: { baz: false }, + }); + expect(result).toEqual({ success: true }); + expect(mockActionType).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + Object { + "actionTypeConfig": Object { + "a": true, + "b": true, + "c": true, + }, + "params": Object { + "baz": false, + }, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], +} +`); + }); +}); diff --git a/x-pack/plugins/actions/server/action_type_service.ts b/x-pack/plugins/actions/server/action_type_service.ts index bdc3b3310e72b..a83edcf98b7a9 100644 --- a/x-pack/plugins/actions/server/action_type_service.ts +++ b/x-pack/plugins/actions/server/action_type_service.ts @@ -44,7 +44,7 @@ export class ActionTypeService { */ public register(actionType: ActionType) { if (this.has(actionType.id)) { - throw Boom.badRequest( + throw new Error( i18n.translate('xpack.actions.actionTypeService.register.duplicateActionTypeError', { defaultMessage: 'Action type "{id}" is already registered.', values: { diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index 5e3490c598e4a..0bbaf858bfce9 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -5,7 +5,6 @@ */ import { SavedObjectsClient } from 'src/legacy/server/saved_objects'; -import { EncryptedSavedObjectsPlugin } from '../../encrypted_saved_objects'; import { ActionTypeService } from './action_type_service'; import { SavedObjectReference } from './types'; @@ -15,11 +14,6 @@ interface Action { actionTypeConfig: Record; } -interface FireOptions { - id: string; - params: Record; -} - interface CreateOptions { data: Action; options?: { @@ -46,7 +40,6 @@ interface FindOptions { interface ConstructorOptions { actionTypeService: ActionTypeService; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin; savedObjectsClient: SavedObjectsClient; } @@ -59,15 +52,9 @@ interface UpdateOptions { export class ActionsClient { private savedObjectsClient: SavedObjectsClient; private actionTypeService: ActionTypeService; - private encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin; - constructor({ - actionTypeService, - encryptedSavedObjectsPlugin, - savedObjectsClient, - }: ConstructorOptions) { + constructor({ actionTypeService, savedObjectsClient }: ConstructorOptions) { this.actionTypeService = actionTypeService; - this.encryptedSavedObjectsPlugin = encryptedSavedObjectsPlugin; this.savedObjectsClient = savedObjectsClient; } @@ -119,22 +106,6 @@ export class ActionsClient { return await this.savedObjectsClient.update('action', id, data, options); } - /** - * Fire an action - */ - public async fire({ id, params }: FireOptions) { - const action = await this.encryptedSavedObjectsPlugin.getDecryptedAsInternalUser('action', id); - const mergedActionTypeConfig = { - ...action.attributes.actionTypeConfig, - ...action.attributes.actionTypeConfigSecrets, - }; - return await this.actionTypeService.execute({ - id: action.attributes.actionTypeId, - actionTypeConfig: mergedActionTypeConfig, - params, - }); - } - /** * Set actionTypeConfigSecrets values on a given action */ diff --git a/x-pack/plugins/actions/server/create_fire_function.ts b/x-pack/plugins/actions/server/create_fire_function.ts new file mode 100644 index 0000000000000..dcab8ce3d1932 --- /dev/null +++ b/x-pack/plugins/actions/server/create_fire_function.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ActionTypeService } from './action_type_service'; +import { EncryptedSavedObjectsPlugin } from '../../encrypted_saved_objects'; + +interface CreateFireFunctionOptions { + actionTypeService: ActionTypeService; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin; +} + +interface FireOptions { + id: string; + params: Record; +} + +export function createFireFunction({ + actionTypeService, + encryptedSavedObjectsPlugin, +}: CreateFireFunctionOptions) { + return async function fire({ id, params }: FireOptions) { + const action = await encryptedSavedObjectsPlugin.getDecryptedAsInternalUser('action', id); + const mergedActionTypeConfig = { + ...action.attributes.actionTypeConfig, + ...action.attributes.actionTypeConfigSecrets, + }; + return await actionTypeService.execute({ + id: action.attributes.actionTypeId, + actionTypeConfig: mergedActionTypeConfig, + params, + }); + }; +} diff --git a/x-pack/plugins/actions/server/init.ts b/x-pack/plugins/actions/server/init.ts index 280a12da8959b..74b92059c36fc 100644 --- a/x-pack/plugins/actions/server/init.ts +++ b/x-pack/plugins/actions/server/init.ts @@ -5,9 +5,9 @@ */ import { Legacy } from 'kibana'; -import { SavedObjectsClient } from 'src/legacy/server/saved_objects'; import { ActionsClient } from './actions_client'; import { ActionTypeService } from './action_type_service'; +import { createFireFunction } from './create_fire_function'; import { createRoute, deleteRoute, @@ -41,21 +41,22 @@ export function init(server: Legacy.Server) { updateRoute(server); listActionTypesRoute(server); - function createActionsClient(savedObjectsClient: SavedObjectsClient) { + const fireFn = createFireFunction({ + actionTypeService, + encryptedSavedObjectsPlugin: server.plugins.encrypted_saved_objects!, + }); + + // Expose service to server + server.decorate('request', 'getActionsClient', function() { + const request = this; + const savedObjectsClient = request.getSavedObjectsClient(); const actionsClient = new ActionsClient({ savedObjectsClient, actionTypeService, - encryptedSavedObjectsPlugin: server.plugins.encrypted_saved_objects!, }); return actionsClient; - } - - // Expose service to server - server.decorate('request', 'getActionsClient', function() { - const request = this; - return createActionsClient(request.getSavedObjectsClient()); }); - server.expose('createActionsClient', createActionsClient); + server.expose('fire', fireFn); server.expose('registerType', actionTypeService.register.bind(actionTypeService)); server.expose('listTypes', actionTypeService.list.bind(actionTypeService)); } diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index f5a2300c7961b..02282d25a9e56 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClient } from 'src/legacy/server/saved_objects'; -import { ActionsClient } from './actions_client'; import { ActionTypeService } from './action_type_service'; export type WithoutQueryAndParams = Pick>; @@ -19,5 +17,5 @@ export interface SavedObjectReference { export interface ActionsPlugin { registerType: ActionTypeService['register']; listTypes: ActionTypeService['list']; - createActionsClient: (savedObjectsClient: SavedObjectsClient) => ActionsClient; + fire: ({ id, params }: { id: string; params: Record }) => Promise; } diff --git a/x-pack/test/plugin_api_integration/plugins/actions/index.ts b/x-pack/test/plugin_api_integration/plugins/actions/index.ts index caccbf30360a2..68296b06ed1d0 100644 --- a/x-pack/test/plugin_api_integration/plugins/actions/index.ts +++ b/x-pack/test/plugin_api_integration/plugins/actions/index.ts @@ -31,8 +31,7 @@ export default function actionsPlugin(kibana: any) { }, }, async handler(request: any) { - const actionsClient = request.getActionsClient(); - return await actionsClient.fire({ + return await request.server.plugins.actions.fire({ id: request.params.id, params: request.payload.params, }); From 42098d42cae1735ef4f47650a8b199dfa9ab1e42 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Thu, 23 May 2019 17:34:14 -0400 Subject: [PATCH 50/51] Add namespace support --- x-pack/plugins/actions/server/create_fire_function.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/actions/server/create_fire_function.ts b/x-pack/plugins/actions/server/create_fire_function.ts index dcab8ce3d1932..8ca138dbead13 100644 --- a/x-pack/plugins/actions/server/create_fire_function.ts +++ b/x-pack/plugins/actions/server/create_fire_function.ts @@ -15,14 +15,17 @@ interface CreateFireFunctionOptions { interface FireOptions { id: string; params: Record; + namespace?: string; } export function createFireFunction({ actionTypeService, encryptedSavedObjectsPlugin, }: CreateFireFunctionOptions) { - return async function fire({ id, params }: FireOptions) { - const action = await encryptedSavedObjectsPlugin.getDecryptedAsInternalUser('action', id); + return async function fire({ id, params, namespace }: FireOptions) { + const action = await encryptedSavedObjectsPlugin.getDecryptedAsInternalUser('action', id, { + namespace, + }); const mergedActionTypeConfig = { ...action.attributes.actionTypeConfig, ...action.attributes.actionTypeConfigSecrets, From 94107eb6842cc3655b9b834f6baa6fc70fbd0322 Mon Sep 17 00:00:00 2001 From: Mike Cote Date: Thu, 23 May 2019 18:20:58 -0400 Subject: [PATCH 51/51] Fix broken test --- .../actions/server/__jest__/create_fire_function.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/actions/server/__jest__/create_fire_function.test.ts b/x-pack/plugins/actions/server/__jest__/create_fire_function.test.ts index 7040b64c67e76..7cc73796ea2e9 100644 --- a/x-pack/plugins/actions/server/__jest__/create_fire_function.test.ts +++ b/x-pack/plugins/actions/server/__jest__/create_fire_function.test.ts @@ -63,7 +63,7 @@ describe('fire()', () => { } `); expect(mockEncryptedSavedObjects.getDecryptedAsInternalUser.mock.calls).toEqual([ - ['action', 'mock-action'], + ['action', 'mock-action', { namespace: undefined }], ]); });