From 8957be70866da7e70bd4d7ad938331604ab43201 Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Tue, 30 Jun 2020 23:58:25 -0400 Subject: [PATCH 1/6] [eventLog] search for actions/alerts as hidden saved objects resolves https://github.com/elastic/kibana/issues/70086 Configures the saved object client for the event log to access the recently hidden action and alert saved objects. We didn't have tests for action/alert event log activity, so added some now. Also found a buglet that was preventing access to event log data from actions and alerts in non-default spaces. --- x-pack/plugins/event_log/kibana.json | 1 + .../server/es/cluster_client_adapter.test.ts | 370 ++++++++++++------ .../server/es/cluster_client_adapter.ts | 149 ++++--- .../event_log/server/es/context.mock.ts | 3 +- .../event_log/server/es/context.test.ts | 5 +- .../event_log/server/event_log_client.test.ts | 28 ++ .../event_log/server/event_log_client.ts | 37 +- .../server/event_log_start_service.test.ts | 14 +- .../server/event_log_start_service.ts | 22 +- x-pack/plugins/event_log/server/index.ts | 1 + x-pack/plugins/event_log/server/plugin.ts | 15 +- .../server/routes/_mock_handler_arguments.ts | 5 +- .../plugins/event_log/server/routes/find.ts | 16 +- x-pack/plugins/event_log/server/types.ts | 2 +- .../builtin_action_types/server_log.ts | 3 - .../alerting_api_integration/common/config.ts | 1 + .../plugins/alerts/server/action_types.ts | 9 + .../plugins/alerts/server/alert_types.ts | 46 +++ .../common/lib/get_event_log.ts | 50 +++ .../common/lib/index.ts | 1 + .../actions/builtin_action_types/email.ts | 3 - .../actions/builtin_action_types/es_index.ts | 2 - .../es_index_preconfigured.ts | 2 - .../actions/builtin_action_types/jira.ts | 3 - .../actions/builtin_action_types/pagerduty.ts | 3 - .../builtin_action_types/server_log.ts | 3 - .../builtin_action_types/servicenow.ts | 3 - .../actions/builtin_action_types/slack.ts | 3 - .../actions/builtin_action_types/webhook.ts | 3 - .../actions/builtin_action_types/es_index.ts | 2 - .../actions/builtin_action_types/webhook.ts | 3 - .../spaces_only/tests/actions/execute.ts | 81 ++++ .../spaces_only/tests/alerting/event_log.ts | 266 +++++++++++++ .../spaces_only/tests/alerting/index.ts | 3 + .../spaces_only/tests/index.ts | 2 +- .../plugins/event_log/server/plugin.ts | 2 +- .../event_log/public_api_integration.ts | 254 +++++++----- 37 files changed, 1052 insertions(+), 364 deletions(-) create mode 100644 x-pack/test/alerting_api_integration/common/lib/get_event_log.ts create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts diff --git a/x-pack/plugins/event_log/kibana.json b/x-pack/plugins/event_log/kibana.json index 7231d967b4c8d..0231bb6234471 100644 --- a/x-pack/plugins/event_log/kibana.json +++ b/x-pack/plugins/event_log/kibana.json @@ -3,6 +3,7 @@ "version": "0.0.1", "kibanaVersion": "kibana", "configPath": ["xpack", "eventLog"], + "optionalPlugins": ["spaces"], "server": true, "ui": false } diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts index ee6f0a301e9f8..6e787c905d400 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts @@ -4,10 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacyClusterClient, Logger } from '../../../../../src/core/server'; -import { elasticsearchServiceMock, loggingSystemMock } from '../../../../../src/core/server/mocks'; +import { LegacyClusterClient, Logger } from 'src/core/server'; +import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; import { ClusterClientAdapter, IClusterClientAdapter } from './cluster_client_adapter'; -import moment from 'moment'; import { findOptionsSchema } from '../event_log_client'; type EsClusterClient = Pick, 'callAsInternalUser' | 'asScoped'>; @@ -205,7 +204,7 @@ describe('createIndex', () => { describe('queryEventsBySavedObject', () => { const DEFAULT_OPTIONS = findOptionsSchema.validate({}); - test('should call cluster with proper arguments', async () => { + test('should call cluster with proper arguments with non-default namespace', async () => { clusterClient.callAsInternalUser.mockResolvedValue({ hits: { hits: [], @@ -214,6 +213,7 @@ describe('queryEventsBySavedObject', () => { }); await clusterClientAdapter.queryEventsBySavedObject( 'index-name', + 'namespace', 'saved-object-type', 'saved-object-id', DEFAULT_OPTIONS @@ -221,52 +221,147 @@ describe('queryEventsBySavedObject', () => { const [method, query] = clusterClient.callAsInternalUser.mock.calls[0]; expect(method).toEqual('search'); - expect(query).toMatchObject({ - index: 'index-name', - body: { - from: 0, - size: 10, - sort: { '@timestamp': { order: 'asc' } }, - query: { - bool: { - must: [ - { - nested: { - path: 'kibana.saved_objects', - query: { - bool: { - must: [ - { - term: { - 'kibana.saved_objects.rel': { - value: 'primary', + expect(query).toMatchInlineSnapshot(` + Object { + "body": Object { + "from": 0, + "query": Object { + "bool": Object { + "must": Array [ + Object { + "nested": Object { + "path": "kibana.saved_objects", + "query": Object { + "bool": Object { + "must": Array [ + Object { + "term": Object { + "kibana.saved_objects.rel": Object { + "value": "primary", + }, }, }, - }, - { - term: { - 'kibana.saved_objects.type': { - value: 'saved-object-type', + Object { + "term": Object { + "kibana.saved_objects.type": Object { + "value": "saved-object-type", + }, }, }, - }, - { - term: { - 'kibana.saved_objects.id': { - value: 'saved-object-id', + Object { + "term": Object { + "kibana.saved_objects.id": Object { + "value": "saved-object-id", + }, }, }, - }, - ], + Object { + "term": Object { + "kibana.saved_objects.namespace": Object { + "value": "namespace", + }, + }, + }, + ], + }, }, }, }, - }, - ], + ], + }, + }, + "size": 10, + "sort": Object { + "@timestamp": Object { + "order": "asc", + }, }, }, + "index": "index-name", + "rest_total_hits_as_int": true, + } + `); + }); + + test('should call cluster with proper arguments with default namespace', async () => { + clusterClient.callAsInternalUser.mockResolvedValue({ + hits: { + hits: [], + total: { value: 0 }, }, }); + await clusterClientAdapter.queryEventsBySavedObject( + 'index-name', + undefined, + 'saved-object-type', + 'saved-object-id', + DEFAULT_OPTIONS + ); + + const [method, query] = clusterClient.callAsInternalUser.mock.calls[0]; + expect(method).toEqual('search'); + expect(query).toMatchInlineSnapshot(` + Object { + "body": Object { + "from": 0, + "query": Object { + "bool": Object { + "must": Array [ + Object { + "nested": Object { + "path": "kibana.saved_objects", + "query": Object { + "bool": Object { + "must": Array [ + Object { + "term": Object { + "kibana.saved_objects.rel": Object { + "value": "primary", + }, + }, + }, + Object { + "term": Object { + "kibana.saved_objects.type": Object { + "value": "saved-object-type", + }, + }, + }, + Object { + "term": Object { + "kibana.saved_objects.id": Object { + "value": "saved-object-id", + }, + }, + }, + Object { + "bool": Object { + "must_not": Object { + "exists": Object { + "field": "kibana.saved_objects.namespace", + }, + }, + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + "size": 10, + "sort": Object { + "@timestamp": Object { + "order": "asc", + }, + }, + }, + "index": "index-name", + "rest_total_hits_as_int": true, + } + `); }); test('should call cluster with sort', async () => { @@ -278,6 +373,7 @@ describe('queryEventsBySavedObject', () => { }); await clusterClientAdapter.queryEventsBySavedObject( 'index-name', + 'namespace', 'saved-object-type', 'saved-object-id', { ...DEFAULT_OPTIONS, sort_field: 'event.end', sort_order: 'desc' } @@ -301,10 +397,11 @@ describe('queryEventsBySavedObject', () => { }, }); - const start = moment().subtract(1, 'days').toISOString(); + const start = '2020-07-08T00:52:28.350Z'; await clusterClientAdapter.queryEventsBySavedObject( 'index-name', + 'namespace', 'saved-object-type', 'saved-object-id', { ...DEFAULT_OPTIONS, start } @@ -312,56 +409,73 @@ describe('queryEventsBySavedObject', () => { const [method, query] = clusterClient.callAsInternalUser.mock.calls[0]; expect(method).toEqual('search'); - expect(query).toMatchObject({ - index: 'index-name', - body: { - query: { - bool: { - must: [ - { - nested: { - path: 'kibana.saved_objects', - query: { - bool: { - must: [ - { - term: { - 'kibana.saved_objects.rel': { - value: 'primary', + expect(query).toMatchInlineSnapshot(` + Object { + "body": Object { + "from": 0, + "query": Object { + "bool": Object { + "must": Array [ + Object { + "nested": Object { + "path": "kibana.saved_objects", + "query": Object { + "bool": Object { + "must": Array [ + Object { + "term": Object { + "kibana.saved_objects.rel": Object { + "value": "primary", + }, }, }, - }, - { - term: { - 'kibana.saved_objects.type': { - value: 'saved-object-type', + Object { + "term": Object { + "kibana.saved_objects.type": Object { + "value": "saved-object-type", + }, }, }, - }, - { - term: { - 'kibana.saved_objects.id': { - value: 'saved-object-id', + Object { + "term": Object { + "kibana.saved_objects.id": Object { + "value": "saved-object-id", + }, }, }, - }, - ], + Object { + "term": Object { + "kibana.saved_objects.namespace": Object { + "value": "namespace", + }, + }, + }, + ], + }, }, }, }, - }, - { - range: { - '@timestamp': { - gte: start, + Object { + "range": Object { + "@timestamp": Object { + "gte": "2020-07-08T00:52:28.350Z", + }, }, }, - }, - ], + ], + }, + }, + "size": 10, + "sort": Object { + "@timestamp": Object { + "order": "asc", + }, }, }, - }, - }); + "index": "index-name", + "rest_total_hits_as_int": true, + } + `); }); test('supports optional date range', async () => { @@ -372,11 +486,12 @@ describe('queryEventsBySavedObject', () => { }, }); - const start = moment().subtract(1, 'days').toISOString(); - const end = moment().add(1, 'days').toISOString(); + const start = '2020-07-08T00:52:28.350Z'; + const end = '2020-07-08T00:00:00.000Z'; await clusterClientAdapter.queryEventsBySavedObject( 'index-name', + 'namespace', 'saved-object-type', 'saved-object-id', { ...DEFAULT_OPTIONS, start, end } @@ -384,62 +499,79 @@ describe('queryEventsBySavedObject', () => { const [method, query] = clusterClient.callAsInternalUser.mock.calls[0]; expect(method).toEqual('search'); - expect(query).toMatchObject({ - index: 'index-name', - body: { - query: { - bool: { - must: [ - { - nested: { - path: 'kibana.saved_objects', - query: { - bool: { - must: [ - { - term: { - 'kibana.saved_objects.rel': { - value: 'primary', + expect(query).toMatchInlineSnapshot(` + Object { + "body": Object { + "from": 0, + "query": Object { + "bool": Object { + "must": Array [ + Object { + "nested": Object { + "path": "kibana.saved_objects", + "query": Object { + "bool": Object { + "must": Array [ + Object { + "term": Object { + "kibana.saved_objects.rel": Object { + "value": "primary", + }, }, }, - }, - { - term: { - 'kibana.saved_objects.type': { - value: 'saved-object-type', + Object { + "term": Object { + "kibana.saved_objects.type": Object { + "value": "saved-object-type", + }, }, }, - }, - { - term: { - 'kibana.saved_objects.id': { - value: 'saved-object-id', + Object { + "term": Object { + "kibana.saved_objects.id": Object { + "value": "saved-object-id", + }, }, }, - }, - ], + Object { + "term": Object { + "kibana.saved_objects.namespace": Object { + "value": "namespace", + }, + }, + }, + ], + }, }, }, }, - }, - { - range: { - '@timestamp': { - gte: start, + Object { + "range": Object { + "@timestamp": Object { + "gte": "2020-07-08T00:52:28.350Z", + }, }, }, - }, - { - range: { - '@timestamp': { - lte: end, + Object { + "range": Object { + "@timestamp": Object { + "lte": "2020-07-08T00:00:00.000Z", + }, }, }, - }, - ], + ], + }, + }, + "size": 10, + "sort": Object { + "@timestamp": Object { + "order": "asc", + }, }, }, - }, - }); + "index": "index-name", + "rest_total_hits_as_int": true, + } + `); }); }); diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts index 3478358f88a54..ab2f320cae34c 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts @@ -6,8 +6,9 @@ import { reject, isUndefined } from 'lodash'; import { SearchResponse, Client } from 'elasticsearch'; -import { Logger, LegacyClusterClient } from '../../../../../src/core/server'; -import { IEvent, SAVED_OBJECT_REL_PRIMARY } from '../types'; +import { Logger, LegacyClusterClient } from 'src/core/server'; + +import { IValidatedEvent, SAVED_OBJECT_REL_PRIMARY } from '../types'; import { FindOptionsType } from '../event_log_client'; export type EsClusterClient = Pick; @@ -22,7 +23,7 @@ export interface QueryEventsBySavedObjectResult { page: number; per_page: number; total: number; - data: IEvent[]; + data: IValidatedEvent[]; } export class ClusterClientAdapter { @@ -129,84 +130,106 @@ export class ClusterClientAdapter { public async queryEventsBySavedObject( index: string, + namespace: string | undefined, // TODO - add term clause for namespace type: string, id: string, { page, per_page: perPage, start, end, sort_field, sort_order }: FindOptionsType ): Promise { - try { - const { - hits: { hits, total }, - }: SearchResponse = await this.callEs('search', { - index, - // The SearchResponse type only supports total as an int, - // so we're forced to explicitly request that it return as an int - rest_total_hits_as_int: true, - body: { - size: perPage, - from: (page - 1) * perPage, - sort: { [sort_field]: { order: sort_order } }, - query: { - bool: { - must: reject( - [ - { - nested: { - path: 'kibana.saved_objects', - query: { - bool: { - must: [ - { - term: { - 'kibana.saved_objects.rel': { - value: SAVED_OBJECT_REL_PRIMARY, - }, - }, + const defaultNamespaceQuery = { + bool: { + must_not: { + exists: { + field: 'kibana.saved_objects.namespace', + }, + }, + }, + }; + const namedNamespaceQuery = { + term: { + 'kibana.saved_objects.namespace': { + value: namespace, + }, + }, + }; + const namespaceQuery = namespace === undefined ? defaultNamespaceQuery : namedNamespaceQuery; + + const body = { + size: perPage, + from: (page - 1) * perPage, + sort: { [sort_field]: { order: sort_order } }, + query: { + bool: { + must: reject( + [ + { + nested: { + path: 'kibana.saved_objects', + query: { + bool: { + must: [ + { + term: { + 'kibana.saved_objects.rel': { + value: SAVED_OBJECT_REL_PRIMARY, }, - { - term: { - 'kibana.saved_objects.type': { - value: type, - }, - }, + }, + }, + { + term: { + 'kibana.saved_objects.type': { + value: type, }, - { - term: { - 'kibana.saved_objects.id': { - value: id, - }, - }, + }, + }, + { + term: { + 'kibana.saved_objects.id': { + value: id, }, - ], + }, }, - }, + namespaceQuery, + ], }, }, - start && { - range: { - '@timestamp': { - gte: start, - }, - }, + }, + }, + start && { + range: { + '@timestamp': { + gte: start, }, - end && { - range: { - '@timestamp': { - lte: end, - }, - }, + }, + }, + end && { + range: { + '@timestamp': { + lte: end, }, - ], - isUndefined - ), - }, - }, + }, + }, + ], + isUndefined + ), }, + }, + }; + + try { + const { + hits: { hits, total }, + }: SearchResponse = await this.callEs('search', { + index, + // The SearchResponse type only supports total as an int, + // so we're forced to explicitly request that it return as an int + rest_total_hits_as_int: true, + body, }); return { page, per_page: perPage, total, - data: hits.map((hit) => hit._source) as IEvent[], + data: hits.map((hit) => hit._source) as IValidatedEvent[], }; } catch (err) { throw new Error( diff --git a/x-pack/plugins/event_log/server/es/context.mock.ts b/x-pack/plugins/event_log/server/es/context.mock.ts index 0c9f7b29b6411..ec44307fa402e 100644 --- a/x-pack/plugins/event_log/server/es/context.mock.ts +++ b/x-pack/plugins/event_log/server/es/context.mock.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { loggingSystemMock } from 'src/core/server/mocks'; + import { EsContext } from './context'; import { namesMock } from './names.mock'; import { IClusterClientAdapter } from './cluster_client_adapter'; -import { loggingSystemMock } from '../../../../../src/core/server/mocks'; import { clusterClientAdapterMock } from './cluster_client_adapter.mock'; const createContextMock = () => { diff --git a/x-pack/plugins/event_log/server/es/context.test.ts b/x-pack/plugins/event_log/server/es/context.test.ts index a78e47446fef8..af48e2c60e429 100644 --- a/x-pack/plugins/event_log/server/es/context.test.ts +++ b/x-pack/plugins/event_log/server/es/context.test.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { LegacyClusterClient, Logger } from 'src/core/server'; +import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; + import { createEsContext } from './context'; -import { LegacyClusterClient, Logger } from '../../../../../src/core/server'; -import { elasticsearchServiceMock, loggingSystemMock } from '../../../../../src/core/server/mocks'; jest.mock('../lib/../../../../package.json', () => ({ version: '1.2.3', })); diff --git a/x-pack/plugins/event_log/server/event_log_client.test.ts b/x-pack/plugins/event_log/server/event_log_client.test.ts index 16e5fa69d36f6..917d517a6e27d 100644 --- a/x-pack/plugins/event_log/server/event_log_client.test.ts +++ b/x-pack/plugins/event_log/server/event_log_client.test.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { KibanaRequest } from 'src/core/server'; import { EventLogClient } from './event_log_client'; import { contextMock } from './es/context.mock'; import { savedObjectsClientMock } from 'src/core/server/mocks'; @@ -18,6 +19,7 @@ describe('EventLogStart', () => { const eventLogClient = new EventLogClient({ esContext, savedObjectsClient, + request: FakeRequest(), }); savedObjectsClient.get.mockResolvedValueOnce({ @@ -38,6 +40,7 @@ describe('EventLogStart', () => { const eventLogClient = new EventLogClient({ esContext, savedObjectsClient, + request: FakeRequest(), }); savedObjectsClient.get.mockRejectedValue(new Error('Fail')); @@ -53,6 +56,7 @@ describe('EventLogStart', () => { const eventLogClient = new EventLogClient({ esContext, savedObjectsClient, + request: FakeRequest(), }); savedObjectsClient.get.mockResolvedValueOnce({ @@ -107,6 +111,7 @@ describe('EventLogStart', () => { expect(esContext.esAdapter.queryEventsBySavedObject).toHaveBeenCalledWith( esContext.esNames.alias, + undefined, 'saved-object-type', 'saved-object-id', { @@ -124,6 +129,7 @@ describe('EventLogStart', () => { const eventLogClient = new EventLogClient({ esContext, savedObjectsClient, + request: FakeRequest(), }); savedObjectsClient.get.mockResolvedValueOnce({ @@ -184,6 +190,7 @@ describe('EventLogStart', () => { expect(esContext.esAdapter.queryEventsBySavedObject).toHaveBeenCalledWith( esContext.esNames.alias, + undefined, 'saved-object-type', 'saved-object-id', { @@ -203,6 +210,7 @@ describe('EventLogStart', () => { const eventLogClient = new EventLogClient({ esContext, savedObjectsClient, + request: FakeRequest(), }); savedObjectsClient.get.mockResolvedValueOnce({ @@ -232,6 +240,7 @@ describe('EventLogStart', () => { const eventLogClient = new EventLogClient({ esContext, savedObjectsClient, + request: FakeRequest(), }); savedObjectsClient.get.mockResolvedValueOnce({ @@ -286,3 +295,22 @@ function fakeEvent(overrides = {}) { overrides ); } + +function FakeRequest(): KibanaRequest { + const savedObjectsClient = savedObjectsClientMock.create(); + return ({ + headers: {}, + getBasePath: () => '', + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, + getSavedObjectsClient: () => savedObjectsClient, + } as unknown) as KibanaRequest; +} diff --git a/x-pack/plugins/event_log/server/event_log_client.ts b/x-pack/plugins/event_log/server/event_log_client.ts index e7ba598de2ac6..fdb9daa638369 100644 --- a/x-pack/plugins/event_log/server/event_log_client.ts +++ b/x-pack/plugins/event_log/server/event_log_client.ts @@ -5,20 +5,16 @@ */ import { Observable } from 'rxjs'; -import { LegacyClusterClient, SavedObjectsClientContract } from 'src/core/server'; - import { schema, TypeOf } from '@kbn/config-schema'; +import { LegacyClusterClient, SavedObjectsClientContract, KibanaRequest } from 'src/core/server'; +import { SpacesServiceSetup } from '../../spaces/server'; + import { EsContext } from './es'; import { IEventLogClient } from './types'; import { QueryEventsBySavedObjectResult } from './es/cluster_client_adapter'; export type PluginClusterClient = Pick; export type AdminClusterClient$ = Observable; -interface EventLogServiceCtorParams { - esContext: EsContext; - savedObjectsClient: SavedObjectsClientContract; -} - const optionalDateFieldSchema = schema.maybe( schema.string({ validate(value) { @@ -60,14 +56,30 @@ export type FindOptionsType = Pick< > & Partial>; +interface EventLogServiceCtorParams { + esContext: EsContext; + savedObjectsClient: SavedObjectsClientContract; + spacesService?: SpacesServiceSetup; + request: KibanaRequest; +} + // note that clusterClient may be null, indicating we can't write to ES export class EventLogClient implements IEventLogClient { private esContext: EsContext; private savedObjectsClient: SavedObjectsClientContract; + private spacesService?: SpacesServiceSetup; + private request: KibanaRequest; - constructor({ esContext, savedObjectsClient }: EventLogServiceCtorParams) { + constructor({ + esContext, + savedObjectsClient, + spacesService, + request, + }: EventLogServiceCtorParams) { this.esContext = esContext; this.savedObjectsClient = savedObjectsClient; + this.spacesService = spacesService; + this.request = request; } async findEventsBySavedObject( @@ -75,13 +87,20 @@ export class EventLogClient implements IEventLogClient { id: string, options?: Partial ): Promise { + const findOptions = findOptionsSchema.validate(options ?? {}); + + const space = await this.spacesService?.getActiveSpace(this.request); + const namespace = space?.id === 'default' ? undefined : space?.id; + // verify the user has the required permissions to view this saved object await this.savedObjectsClient.get(type, id); + return await this.esContext.esAdapter.queryEventsBySavedObject( this.esContext.esNames.alias, + namespace, type, id, - findOptionsSchema.validate(options ?? {}) + findOptions ); } } diff --git a/x-pack/plugins/event_log/server/event_log_start_service.test.ts b/x-pack/plugins/event_log/server/event_log_start_service.test.ts index 58dd3ae6eb514..3bd5ef7c0b3ba 100644 --- a/x-pack/plugins/event_log/server/event_log_start_service.test.ts +++ b/x-pack/plugins/event_log/server/event_log_start_service.test.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { KibanaRequest } from 'src/core/server'; +import { savedObjectsClientMock, savedObjectsServiceMock } from 'src/core/server/mocks'; + import { EventLogClientService } from './event_log_start_service'; import { contextMock } from './es/context.mock'; -import { KibanaRequest } from 'kibana/server'; -import { savedObjectsClientMock, savedObjectsServiceMock } from 'src/core/server/mocks'; jest.mock('./event_log_client'); @@ -26,13 +27,8 @@ describe('EventLogClientService', () => { eventLogStartService.getClient(request); - expect(savedObjectsService.getScopedClient).toHaveBeenCalledWith(request); - - const [{ value: savedObjectsClient }] = savedObjectsService.getScopedClient.mock.results; - - expect(jest.requireMock('./event_log_client').EventLogClient).toHaveBeenCalledWith({ - esContext, - savedObjectsClient, + expect(savedObjectsService.getScopedClient).toHaveBeenCalledWith(request, { + includedHiddenTypes: ['action', 'alert'], }); }); }); diff --git a/x-pack/plugins/event_log/server/event_log_start_service.ts b/x-pack/plugins/event_log/server/event_log_start_service.ts index 0339d0883dc46..8b752684c1cc3 100644 --- a/x-pack/plugins/event_log/server/event_log_start_service.ts +++ b/x-pack/plugins/event_log/server/event_log_start_service.ts @@ -11,6 +11,7 @@ import { SavedObjectsServiceStart, SavedObjectsClientContract, } from 'src/core/server'; +import { SpacesServiceSetup } from '../../spaces/server'; import { EsContext } from './es'; import { IEventLogClientService } from './types'; @@ -18,30 +19,37 @@ import { EventLogClient } from './event_log_client'; export type PluginClusterClient = Pick; export type AdminClusterClient$ = Observable; +const includedHiddenTypes = ['action', 'alert']; + interface EventLogServiceCtorParams { esContext: EsContext; savedObjectsService: SavedObjectsServiceStart; + spacesService?: SpacesServiceSetup; } // note that clusterClient may be null, indicating we can't write to ES export class EventLogClientService implements IEventLogClientService { private esContext: EsContext; private savedObjectsService: SavedObjectsServiceStart; + private spacesService?: SpacesServiceSetup; - constructor({ esContext, savedObjectsService }: EventLogServiceCtorParams) { + constructor({ esContext, savedObjectsService, spacesService }: EventLogServiceCtorParams) { this.esContext = esContext; this.savedObjectsService = savedObjectsService; + this.spacesService = spacesService; } - getClient( - request: KibanaRequest, - savedObjectsClient: SavedObjectsClientContract = this.savedObjectsService.getScopedClient( - request - ) - ) { + getClient(request: KibanaRequest) { + const savedObjectsClient: SavedObjectsClientContract = this.savedObjectsService.getScopedClient( + request, + { includedHiddenTypes } + ); + return new EventLogClient({ esContext: this.esContext, savedObjectsClient, + spacesService: this.spacesService, + request, }); } } diff --git a/x-pack/plugins/event_log/server/index.ts b/x-pack/plugins/event_log/server/index.ts index 0612b5319c15b..25b1b95831b8a 100644 --- a/x-pack/plugins/event_log/server/index.ts +++ b/x-pack/plugins/event_log/server/index.ts @@ -13,6 +13,7 @@ export { IEventLogger, IEventLogClientService, IEvent, + IValidatedEvent, SAVED_OBJECT_REL_PRIMARY, } from './types'; export const config = { schema: ConfigSchema }; diff --git a/x-pack/plugins/event_log/server/plugin.ts b/x-pack/plugins/event_log/server/plugin.ts index ed530607aabba..66ca86a3537ef 100644 --- a/x-pack/plugins/event_log/server/plugin.ts +++ b/x-pack/plugins/event_log/server/plugin.ts @@ -17,6 +17,7 @@ import { IContextProvider, RequestHandler, } from 'src/core/server'; +import { SpacesPluginSetup, SpacesServiceSetup } from '../../spaces/server'; import { IEventLogConfig, @@ -39,14 +40,19 @@ const ACTIONS = { stopping: 'stopping', }; +interface PluginSetupDeps { + spaces: SpacesPluginSetup; +} + export class Plugin implements CorePlugin { private readonly config$: IEventLogConfig$; private systemLogger: Logger; - private eventLogService?: IEventLogService; + private eventLogService?: EventLogService; private esContext?: EsContext; private eventLogger?: IEventLogger; private globalConfig$: Observable; private eventLogClientService?: EventLogClientService; + private spacesService?: SpacesServiceSetup; constructor(private readonly context: PluginInitializerContext) { this.systemLogger = this.context.logger.get(); @@ -54,13 +60,14 @@ export class Plugin implements CorePlugin { + async setup(core: CoreSetup, { spaces }: PluginSetupDeps): Promise { const globalConfig = await this.globalConfig$.pipe(first()).toPromise(); const kibanaIndex = globalConfig.kibana.index; this.systemLogger.debug('setting up plugin'); const config = await this.config$.pipe(first()).toPromise(); + this.spacesService = spaces?.spacesService; this.esContext = createEsContext({ logger: this.systemLogger, @@ -115,6 +122,7 @@ export class Plugin implements CorePlugin => { return async (context, request) => { return { - getEventLogClient: () => - this.eventLogClientService!.getClient(request, context.core.savedObjects.client), + getEventLogClient: () => this.eventLogClientService!.getClient(request), }; }; }; diff --git a/x-pack/plugins/event_log/server/routes/_mock_handler_arguments.ts b/x-pack/plugins/event_log/server/routes/_mock_handler_arguments.ts index 2d5e37e870b28..b0ce5605d0e51 100644 --- a/x-pack/plugins/event_log/server/routes/_mock_handler_arguments.ts +++ b/x-pack/plugins/event_log/server/routes/_mock_handler_arguments.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandlerContext, KibanaRequest, KibanaResponseFactory } from 'kibana/server'; import { identity, merge } from 'lodash'; -import { httpServerMock } from '../../../../../src/core/server/mocks'; +import { RequestHandlerContext, KibanaRequest, KibanaResponseFactory } from 'src/core/server'; + +import { httpServerMock } from 'src/core/server/mocks'; import { IEventLogClient } from '../types'; export function mockHandlerArguments( diff --git a/x-pack/plugins/event_log/server/routes/find.ts b/x-pack/plugins/event_log/server/routes/find.ts index e03aef7c757f6..d67eed1fcde9a 100644 --- a/x-pack/plugins/event_log/server/routes/find.ts +++ b/x-pack/plugins/event_log/server/routes/find.ts @@ -11,9 +11,11 @@ import { KibanaRequest, IKibanaResponse, KibanaResponseFactory, -} from 'kibana/server'; +} from 'src/core/server'; + import { BASE_EVENT_LOG_API_PATH } from '../../common'; import { findOptionsSchema, FindOptionsType } from '../event_log_client'; +import { QueryEventsBySavedObjectResult } from '../es/cluster_client_adapter'; const paramSchema = schema.object({ type: schema.string(), @@ -42,9 +44,15 @@ export const findRoute = (router: IRouter) => { params: { id, type }, query, } = req; - return res.ok({ - body: await eventLogClient.findEventsBySavedObject(type, id, query), - }); + + let result: QueryEventsBySavedObjectResult; + try { + result = await eventLogClient.findEventsBySavedObject(type, id, query); + } catch (err) { + return res.notFound(); + } + + return res.ok({ body: result }); }) ); }; diff --git a/x-pack/plugins/event_log/server/types.ts b/x-pack/plugins/event_log/server/types.ts index 58be6707b0373..1a37c4e58d079 100644 --- a/x-pack/plugins/event_log/server/types.ts +++ b/x-pack/plugins/event_log/server/types.ts @@ -6,9 +6,9 @@ import { Observable } from 'rxjs'; import { schema, TypeOf } from '@kbn/config-schema'; +import { KibanaRequest } from 'src/core/server'; export { IEvent, IValidatedEvent, EventSchema, ECS_VERSION } from '../generated/schemas'; -import { KibanaRequest } from 'kibana/server'; import { IEvent } from '../generated/schemas'; import { FindOptionsType } from './event_log_client'; import { QueryEventsBySavedObjectResult } from './es/cluster_client_adapter'; diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/server_log.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/server_log.ts index ea5f523b396b4..7204581d036e0 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/server_log.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/server_log.ts @@ -9,11 +9,8 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function serverLogTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); describe('server-log action', () => { - after(() => esArchiver.unload('empty_kibana')); - it('should return 200 when creating a server-log action', async () => { await supertest .post('/api/actions/action') diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 0877fdc949dc4..9a2362c550b1a 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -32,6 +32,7 @@ const enabledActionTypes = [ 'test.index-record', 'test.noop', 'test.rate-limit', + 'test.throw', ]; // eslint-disable-next-line import/no-default-export diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/action_types.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/action_types.ts index 3b6befb3fe807..e89c6b7a909a1 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/action_types.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/action_types.ts @@ -24,6 +24,14 @@ export function defineActionTypes( return { status: 'ok', actionId: '' }; }, }; + const throwActionType: ActionType = { + id: 'test.throw', + name: 'Test: Throw', + minimumLicenseRequired: 'gold', + async executor() { + throw new Error('this action is intended to fail'); + }, + }; const indexRecordActionType: ActionType = { id: 'test.index-record', name: 'Test: Index Record', @@ -193,6 +201,7 @@ export function defineActionTypes( }, }; actions.registerType(noopActionType); + actions.registerType(throwActionType); actions.registerType(indexRecordActionType); actions.registerType(failingActionType); actions.registerType(rateLimitedActionType); diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts index 8e3d6b6909a14..99e823dbf5b23 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts @@ -286,6 +286,50 @@ export function defineAlertTypes( }, async executor(opts: AlertExecutorOptions) {}, }; + const throwAlertType: AlertType = { + id: 'test.throw', + name: 'Test: Throw', + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + producer: 'alerting', + defaultActionGroupId: 'default', + async executor({ services, params, state }: AlertExecutorOptions) { + throw new Error('this alert is intended to fail'); + }, + }; + const patternFiringAlertType: AlertType = { + id: 'test.patternFiring', + name: 'Test: Firing on a Pattern', + actionGroups: [{ id: 'default', name: 'Default' }], + producer: 'alerting', + defaultActionGroupId: 'default', + async executor(alertExecutorOptions: AlertExecutorOptions) { + const { services, state, params } = alertExecutorOptions; + const pattern = params.pattern; + if (!Array.isArray(pattern)) throw new Error('pattern is not an array'); + if (pattern.length === 0) throw new Error('pattern is empty'); + + // get the pattern index, return if past it + const patternIndex = state.patternIndex ?? 0; + if (patternIndex > pattern.length) { + return { patternIndex }; + } + + // fire if pattern says to + if (pattern[patternIndex]) { + services.alertInstanceFactory('instance').scheduleActions('default'); + } + + return { + patternIndex: (patternIndex + 1) % pattern.length, + }; + }, + }; + alerts.registerType(alwaysFiringAlertType); alerts.registerType(cumulativeFiringAlertType); alerts.registerType(neverFiringAlertType); @@ -295,4 +339,6 @@ export function defineAlertTypes( alerts.registerType(noopAlertType); alerts.registerType(onlyContextVariablesAlertType); alerts.registerType(onlyStateVariablesAlertType); + alerts.registerType(patternFiringAlertType); + alerts.registerType(throwAlertType); } diff --git a/x-pack/test/alerting_api_integration/common/lib/get_event_log.ts b/x-pack/test/alerting_api_integration/common/lib/get_event_log.ts new file mode 100644 index 0000000000000..090b3ee0a4854 --- /dev/null +++ b/x-pack/test/alerting_api_integration/common/lib/get_event_log.ts @@ -0,0 +1,50 @@ +/* + * 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 { IEvent } from '../../../../../../plugins/event_log/server'; + +import { IValidatedEvent } from '../../../../plugins/event_log/server'; +import { getUrlPrefix } from '.'; +import { FtrProviderContext } from '../ftr_provider_context'; + +interface GetEventLogParams { + getService: FtrProviderContext['getService']; + spaceId: string; + type: string; + id: string; + provider: string; + actions: string[]; +} + +// Return event log entries given the specified parameters; for the `actions` +// parameter, at least one event of each action must be in the returned entries. +export async function getEventLog(params: GetEventLogParams): Promise { + const { getService, spaceId, type, id, provider, actions } = params; + const supertest = getService('supertest'); + + const spacePrefix = getUrlPrefix(spaceId); + const url = `${spacePrefix}/api/event_log/${type}/${id}/_find`; + + const { body: result } = await supertest.get(url).expect(200); + if (!result.total) { + throw new Error('no events found yet'); + } + + const events: IValidatedEvent[] = (result.data as IValidatedEvent[]).filter( + (event) => event?.event?.provider === provider + ); + const foundActions = new Set( + events.map((event) => event?.event?.action).filter((event) => !!event) + ); + + for (const action of actions) { + if (!foundActions.has(action)) { + throw new Error(`no event found with action "${action}"`); + } + } + + return events; +} diff --git a/x-pack/test/alerting_api_integration/common/lib/index.ts b/x-pack/test/alerting_api_integration/common/lib/index.ts index c1e59664f9ce2..eae679cd38c11 100644 --- a/x-pack/test/alerting_api_integration/common/lib/index.ts +++ b/x-pack/test/alerting_api_integration/common/lib/index.ts @@ -12,3 +12,4 @@ export { AlertUtils } from './alert_utils'; export { TaskManagerUtils } from './task_manager_utils'; export * from './test_assertions'; export { checkAAD } from './check_aad'; +export { getEventLog } from './get_event_log'; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts index ffbb29b431d8d..1c3d3e3d713e2 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts @@ -11,11 +11,8 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function emailTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); describe('create email action', () => { - after(() => esArchiver.unload('empty_kibana')); - let createdActionId = ''; it('should return 200 when creating an email action successfully', async () => { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts index c1dc155c17238..61903c2902317 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts @@ -14,10 +14,8 @@ const ES_TEST_INDEX_NAME = 'functional-test-actions-index'; export default function indexTest({ getService }: FtrProviderContext) { const es = getService('legacyEs'); const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); describe('index action', () => { - after(() => esArchiver.unload('empty_kibana')); beforeEach(() => clearTestIndex(es)); let createdActionID: string; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index_preconfigured.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index_preconfigured.ts index 93daa16e71bcf..09b4b433d4847 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index_preconfigured.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index_preconfigured.ts @@ -16,10 +16,8 @@ const ES_TEST_INDEX_NAME = 'functional-test-actions-index-preconfigured'; export default function indexTest({ getService }: FtrProviderContext) { const es = getService('legacyEs'); const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); describe('preconfigured index action', () => { - after(() => esArchiver.unload('empty_kibana')); beforeEach(() => clearTestIndex(es)); it('should execute successfully when expected for a single body', async () => { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts index 19206ce681000..24931f11d4999 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts @@ -34,7 +34,6 @@ const mapping = [ // eslint-disable-next-line import/no-default-export export default function jiraTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const mockJira = { @@ -82,8 +81,6 @@ export default function jiraTest({ getService }: FtrProviderContext) { ); }); - after(() => esArchiver.unload('empty_kibana')); - describe('Jira - Action Creation', () => { it('should return 200 when creating a jira action successfully', async () => { const { body: createdAction } = await supertest diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts index 71ceb731da9f0..f4fcbb65ab5a3 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts @@ -16,7 +16,6 @@ import { // eslint-disable-next-line import/no-default-export export default function pagerdutyTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); describe('pagerduty action', () => { @@ -30,8 +29,6 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { ); }); - after(() => esArchiver.unload('empty_kibana')); - it('should return successfully when passed valid create parameters', async () => { const { body: createdAction } = await supertest .post('/api/actions/action') diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/server_log.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/server_log.ts index a915987ce5feb..e8b088abff3c8 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/server_log.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/server_log.ts @@ -11,11 +11,8 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function serverLogTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); describe('server-log action', () => { - after(() => esArchiver.unload('empty_kibana')); - let serverLogActionId: string; it('should return 200 when creating a builtin server-log action', async () => { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts index 8205b75cabed5..d3b72d01216d0 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts @@ -34,7 +34,6 @@ const mapping = [ // eslint-disable-next-line import/no-default-export export default function servicenowTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const mockServiceNow = { @@ -81,8 +80,6 @@ export default function servicenowTest({ getService }: FtrProviderContext) { ); }); - after(() => esArchiver.unload('empty_kibana')); - describe('ServiceNow - Action Creation', () => { it('should return 200 when creating a servicenow action successfully', async () => { const { body: createdAction } = await supertest diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts index 537205360f4aa..f21bc8edeef1e 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts @@ -16,7 +16,6 @@ import { // eslint-disable-next-line import/no-default-export export default function slackTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); describe('slack action', () => { @@ -30,8 +29,6 @@ export default function slackTest({ getService }: FtrProviderContext) { ); }); - after(() => esArchiver.unload('empty_kibana')); - it('should return 200 when creating a slack action successfully', async () => { const { body: createdAction } = await supertest .post('/api/actions/action') diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts index 10adf12baf652..7eba753d7e98b 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts @@ -27,7 +27,6 @@ function parsePort(url: Record): Record esArchiver.unload('empty_kibana')); - it('should return 200 when creating a webhook action successfully', async () => { const { body: createdAction } = await supertest .post('/api/actions/action') diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/es_index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/es_index.ts index 0822e614464cb..0609e2f3f444f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/es_index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/es_index.ts @@ -14,10 +14,8 @@ const ES_TEST_INDEX_NAME = 'functional-test-actions-index'; export default function indexTest({ getService }: FtrProviderContext) { const es = getService('legacyEs'); const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); describe('index action', () => { - after(() => esArchiver.unload('empty_kibana')); beforeEach(() => clearTestIndex(es)); let createdActionID: string; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/webhook.ts index fb8460068cbcf..b3572978cee70 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/webhook.ts @@ -15,7 +15,6 @@ import { // eslint-disable-next-line import/no-default-export export default function webhookTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); async function createWebhookAction( @@ -55,8 +54,6 @@ export default function webhookTest({ getService }: FtrProviderContext) { ); }); - after(() => esArchiver.unload('empty_kibana')); - it('webhook can be executed without username and password', async () => { const webhookActionId = await createWebhookAction(webhookSimulatorURL); const { body: result } = await supertest diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts index 7bbeab7cc8726..f74c6eaa3298a 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts @@ -11,8 +11,12 @@ import { ES_TEST_INDEX_NAME, getUrlPrefix, ObjectRemover, + getEventLog, } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { IValidatedEvent } from '../../../../../plugins/event_log/server'; + +const NANOS_IN_MILLIS = 1000 * 1000; // eslint-disable-next-line import/no-default-export export default function ({ getService }: FtrProviderContext) { @@ -86,6 +90,13 @@ export default function ({ getService }: FtrProviderContext) { reference, source: 'action:test.index-record', }); + + await validateEventLog({ + spaceId: Spaces.space1.id, + actionId: createdAction.id, + outcome: 'success', + message: `action executed: test.index-record:${createdAction.id}: My action`, + }); }); it('should handle failed executions', async () => { @@ -118,6 +129,14 @@ export default function ({ getService }: FtrProviderContext) { serviceMessage: `expected failure for ${ES_TEST_INDEX_NAME} ${reference}`, retry: false, }); + + await validateEventLog({ + spaceId: Spaces.space1.id, + actionId: createdAction.id, + outcome: 'failure', + message: `action execution failure: test.failing:${createdAction.id}: failing action`, + errorMessage: `an error occurred while running the action executor: expected failure for .kibana-alerting-test-data actions-failure-1:space1`, + }); }); it(`shouldn't execute an action from another space`, async () => { @@ -198,4 +217,66 @@ export default function ({ getService }: FtrProviderContext) { }); }); }); + + interface ValidateEventLogParams { + spaceId: string; + actionId: string; + outcome: string; + message: string; + errorMessage?: string; + } + + async function validateEventLog(params: ValidateEventLogParams): Promise { + const { spaceId, actionId, outcome, message, errorMessage } = params; + + const events: IValidatedEvent[] = await retry.try(async () => { + return await getEventLog({ + getService, + spaceId, + type: 'action', + id: actionId, + provider: 'actions', + actions: ['execute'], + }); + }); + + expect(events.length).to.equal(1); + + const event = events[0]; + + const duration = event?.event?.duration; + const eventStart = Date.parse(event?.event?.start || 'undefined'); + const eventEnd = Date.parse(event?.event?.end || 'undefined'); + const dateNow = Date.now(); + + expect(typeof duration).to.be('number'); + expect(eventStart).to.be.ok(); + expect(eventEnd).to.be.ok(); + + const durationDiff = Math.abs( + Math.round(duration! / NANOS_IN_MILLIS) - (eventEnd - eventStart) + ); + + // account for rounding errors + expect(durationDiff < 1).to.equal(true); + expect(eventStart <= eventEnd).to.equal(true); + expect(eventEnd <= dateNow).to.equal(true); + + expect(event?.event?.outcome).to.equal(outcome); + + expect(event?.kibana?.saved_objects).to.eql([ + { + rel: 'primary', + type: 'action', + id: actionId, + namespace: 'space1', + }, + ]); + + expect(event?.message).to.eql(message); + + if (errorMessage) { + expect(event?.error?.message).to.eql(errorMessage); + } + } } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts new file mode 100644 index 0000000000000..9f34b69745adb --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts @@ -0,0 +1,266 @@ +/* + * 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 { Spaces } from '../../scenarios'; +import { getUrlPrefix, getTestAlertData, ObjectRemover, getEventLog } from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { IValidatedEvent } from '../../../../../plugins/event_log/server'; + +const NANOS_IN_MILLIS = 1000 * 1000; + +// eslint-disable-next-line import/no-default-export +export default function eventLogTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const retry = getService('retry'); + + describe('eventLog', () => { + const objectRemover = new ObjectRemover(supertest); + + after(() => objectRemover.removeAll()); + + it('should generate expected events for normal operation', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY action', + actionTypeId: 'test.noop', + config: {}, + secrets: {}, + }) + .expect(200); + + // pattern of when the alert should fire + const pattern = [false, true, true]; + + const response = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + alertTypeId: 'test.patternFiring', + schedule: { interval: '1s' }, + throttle: null, + params: { + pattern, + }, + actions: [ + { + id: createdAction.id, + group: 'default', + params: {}, + }, + ], + }) + ); + + expect(response.status).to.eql(200); + const alertId = response.body.id; + objectRemover.add(Spaces.space1.id, alertId, 'alert', 'alerts'); + + // get the events we're expecting + const events = await retry.try(async () => { + return await getEventLog({ + getService, + spaceId: Spaces.space1.id, + type: 'alert', + id: alertId, + provider: 'alerting', + actions: ['execute', 'execute-action', 'new-instance', 'resolved-instance'], + }); + }); + // console.log(JSON.stringify(events, null, 4)); + + // make sure the counts of the # of events per type are as expected + const executeEvents = getEventsByAction(events, 'execute'); + const executeActionEvents = getEventsByAction(events, 'execute-action'); + const newInstanceEvents = getEventsByAction(events, 'new-instance'); + const resolvedInstanceEvents = getEventsByAction(events, 'resolved-instance'); + + expect(executeEvents.length >= 4).to.be(true); + expect(executeActionEvents.length).to.be(2); + expect(newInstanceEvents.length).to.be(1); + expect(resolvedInstanceEvents.length).to.be(1); + + // make sure the events are in the right temporal order + const executeTimes = getTimestamps(executeEvents); + const executeActionTimes = getTimestamps(executeActionEvents); + const newInstanceTimes = getTimestamps(newInstanceEvents); + const resolvedInstanceTimes = getTimestamps(resolvedInstanceEvents); + + expect(executeTimes[0] < newInstanceTimes[0]).to.be(true); + expect(executeTimes[1] <= newInstanceTimes[0]).to.be(true); + expect(executeTimes[2] > newInstanceTimes[0]).to.be(true); + expect(executeTimes[1] <= executeActionTimes[0]).to.be(true); + expect(executeTimes[2] > executeActionTimes[0]).to.be(true); + expect(resolvedInstanceTimes[0] > newInstanceTimes[0]).to.be(true); + + // validate each event + for (const event of events) { + switch (event?.event?.action) { + case 'execute': + validateEvent(event, { + spaceId: Spaces.space1.id, + savedObjects: [{ type: 'alert', id: alertId, rel: 'primary' }], + outcome: 'success', + message: `alert executed: test.patternFiring:${alertId}: 'abc'`, + }); + break; + case 'execute-action': + validateEvent(event, { + spaceId: Spaces.space1.id, + savedObjects: [ + { type: 'alert', id: alertId, rel: 'primary' }, + { type: 'action', id: createdAction.id }, + ], + message: `alert: test.patternFiring:${alertId}: 'abc' instanceId: 'instance' scheduled actionGroup: 'default' action: test.noop:${createdAction.id}`, + }); + break; + case 'new-instance': + validateEvent(event, { + spaceId: Spaces.space1.id, + savedObjects: [{ type: 'alert', id: alertId, rel: 'primary' }], + message: `test.patternFiring:${alertId}: 'abc' created new instance: 'instance'`, + }); + break; + case 'resolved-instance': + validateEvent(event, { + spaceId: Spaces.space1.id, + savedObjects: [{ type: 'alert', id: alertId, rel: 'primary' }], + message: `test.patternFiring:${alertId}: 'abc' resolved instance: 'instance'`, + }); + break; + // this will get triggered as we add new event actions + default: + throw new Error(`unexpected event action "${event?.event?.action}"`); + } + } + }); + + it('should generate events for execution errors', async () => { + const response = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + alertTypeId: 'test.throw', + schedule: { interval: '1s' }, + throttle: null, + }) + ); + + expect(response.status).to.eql(200); + const alertId = response.body.id; + objectRemover.add(Spaces.space1.id, alertId, 'alert', 'alerts'); + + const events = await retry.try(async () => { + return await getEventLog({ + getService, + spaceId: Spaces.space1.id, + type: 'alert', + id: alertId, + provider: 'alerting', + actions: ['execute'], + }); + }); + + const event = events[0]; + expect(event).to.be.ok(); + + validateEvent(event, { + spaceId: Spaces.space1.id, + savedObjects: [{ type: 'alert', id: alertId, rel: 'primary' }], + outcome: 'failure', + message: `alert execution failure: test.throw:${alertId}: 'abc'`, + errorMessage: 'this alert is intended to fail', + }); + }); + }); + + interface SavedObject { + type: string; + id: string; + rel?: string; + } + + interface ValidateEventLogParams { + spaceId: string; + savedObjects: SavedObject[]; + outcome?: string; + message: string; + errorMessage?: string; + } + + function validateEvent(event: IValidatedEvent, params: ValidateEventLogParams): void { + const { spaceId, savedObjects, outcome, message, errorMessage } = params; + + const duration = event?.event?.duration; + const eventStart = Date.parse(event?.event?.start || 'undefined'); + const eventEnd = Date.parse(event?.event?.end || 'undefined'); + const dateNow = Date.now(); + + if (duration !== undefined) { + expect(typeof duration).to.be('number'); + expect(eventStart).to.be.ok(); + expect(eventEnd).to.be.ok(); + + const durationDiff = Math.abs( + Math.round(duration! / NANOS_IN_MILLIS) - (eventEnd - eventStart) + ); + + // account for rounding errors + expect(durationDiff < 1).to.equal(true); + expect(eventStart <= eventEnd).to.equal(true); + expect(eventEnd <= dateNow).to.equal(true); + } + + expect(event?.event?.outcome).to.equal(outcome); + + for (const savedObject of savedObjects) { + expect( + isSavedObjectInEvent(event, spaceId, savedObject.type, savedObject.id, savedObject.rel) + ).to.be(true); + } + + expect(event?.message).to.eql(message); + + if (errorMessage) { + expect(event?.error?.message).to.eql(errorMessage); + } + } +} + +function getEventsByAction(events: IValidatedEvent[], action: string) { + return events.filter((event) => event?.event?.action === action); +} + +function getTimestamps(events: IValidatedEvent[]) { + return events.map((event) => event?.['@timestamp'] ?? 'missing timestamp'); +} + +function isSavedObjectInEvent( + event: IValidatedEvent, + namespace: string, + type: string, + id: string, + rel?: string +): boolean { + const savedObjects = event?.kibana?.saved_objects ?? []; + + for (const savedObject of savedObjects) { + if ( + savedObject.namespace === namespace && + savedObject.type === type && + savedObject.id === id && + savedObject.rel === rel + ) { + return true; + } + } + + return false; +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts index 2fc35ddaa3c61..0970738b630c4 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts @@ -17,6 +17,7 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./get_alert_state')); loadTestFile(require.resolve('./list_alert_types')); + loadTestFile(require.resolve('./event_log')); loadTestFile(require.resolve('./mute_all')); loadTestFile(require.resolve('./mute_instance')); loadTestFile(require.resolve('./unmute_all')); @@ -26,6 +27,8 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./alerts_space1')); loadTestFile(require.resolve('./alerts_default_space')); loadTestFile(require.resolve('./builtin_alert_types')); + + // note that this test will destroy existing spaces loadTestFile(require.resolve('./migrations')); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/index.ts index 17f602d3d94f7..281096f8a3592 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/index.ts @@ -27,7 +27,7 @@ export default function alertingApiIntegrationTests({ } }); - after(() => esArchiver.unload('empty_kibana')); + after(async () => await esArchiver.unload('empty_kibana')); loadTestFile(require.resolve('./actions')); loadTestFile(require.resolve('./alerting')); diff --git a/x-pack/test/plugin_api_integration/plugins/event_log/server/plugin.ts b/x-pack/test/plugin_api_integration/plugins/event_log/server/plugin.ts index 2ef932d19e9ee..4fb0511db2194 100644 --- a/x-pack/test/plugin_api_integration/plugins/event_log/server/plugin.ts +++ b/x-pack/test/plugin_api_integration/plugins/event_log/server/plugin.ts @@ -44,7 +44,7 @@ export class EventLogFixturePlugin core.savedObjects.registerType({ name: 'event_log_test', hidden: false, - namespaceType: 'agnostic', + namespaceType: 'single', mappings: { properties: {}, }, diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts index b6eacf2427643..eea18863e3be8 100644 --- a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts +++ b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts @@ -18,137 +18,162 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const log = getService('log'); const retry = getService('retry'); + const spacesService = getService('spaces'); + const esArchiver = getService('esArchiver'); describe('Event Log public API', () => { - it('should allow querying for events by Saved Object', async () => { - const id = uuid.v4(); + before(async () => { + await spacesService.create({ + id: 'namespace-a', + name: 'Space A', + disabledFeatures: [], + }); + }); - const expectedEvents = [fakeEvent(id), fakeEvent(id)]; + after(async () => { + await esArchiver.unload('empty_kibana'); + }); - await logTestEvent(id, expectedEvents[0]); - await logTestEvent(id, expectedEvents[1]); + for (const namespace of [undefined, 'namespace-a']) { + const namespaceName = namespace === undefined ? 'default' : namespace; - await retry.try(async () => { - const { - body: { data, total }, - } = await findEvents(id, {}); + describe(`namespace: ${namespaceName}`, () => { + it('should allow querying for events by Saved Object', async () => { + const id = uuid.v4(); - expect(data.length).to.be(2); - expect(total).to.be(2); + const expectedEvents = [fakeEvent(namespace, id), fakeEvent(namespace, id)]; - assertEventsFromApiMatchCreatedEvents(data, expectedEvents); - }); - }); + await logTestEvent(namespace, id, expectedEvents[0]); + await logTestEvent(namespace, id, expectedEvents[1]); - it('should support pagination for events', async () => { - const id = uuid.v4(); + await retry.try(async () => { + const { + body: { data, total }, + } = await findEvents(namespace, id, {}); - const expectedEvents = await logFakeEvents(id, 6); + expect(data.length).to.be(2); + expect(total).to.be(2); - await retry.try(async () => { - const { - body: { data: foundEvents }, - } = await findEvents(id, {}); + assertEventsFromApiMatchCreatedEvents(data, expectedEvents); + }); + }); - expect(foundEvents.length).to.be(6); - }); + it('should support pagination for events', async () => { + const id = uuid.v4(); - const [expectedFirstPage, expectedSecondPage] = chunk(expectedEvents, 3); + const expectedEvents = await logFakeEvents(namespace, id, 6); - const { - body: { data: firstPage }, - } = await findEvents(id, { per_page: 3 }); + await retry.try(async () => { + const { + body: { data: foundEvents }, + } = await findEvents(namespace, id, {}); - expect(firstPage.length).to.be(3); - assertEventsFromApiMatchCreatedEvents(firstPage, expectedFirstPage); + expect(foundEvents.length).to.be(6); + }); - const { - body: { data: secondPage }, - } = await findEvents(id, { per_page: 3, page: 2 }); + const [expectedFirstPage, expectedSecondPage] = chunk(expectedEvents, 3); - expect(secondPage.length).to.be(3); - assertEventsFromApiMatchCreatedEvents(secondPage, expectedSecondPage); - }); + const { + body: { data: firstPage }, + } = await findEvents(namespace, id, { per_page: 3 }); - it('should support sorting by event end', async () => { - const id = uuid.v4(); + expect(firstPage.length).to.be(3); + assertEventsFromApiMatchCreatedEvents(firstPage, expectedFirstPage); - const expectedEvents = await logFakeEvents(id, 6); + const { + body: { data: secondPage }, + } = await findEvents(namespace, id, { per_page: 3, page: 2 }); - await retry.try(async () => { - const { - body: { data: foundEvents }, - } = await findEvents(id, { sort_field: 'event.end', sort_order: 'desc' }); + expect(secondPage.length).to.be(3); + assertEventsFromApiMatchCreatedEvents(secondPage, expectedSecondPage); + }); - expect(foundEvents.length).to.be(expectedEvents.length); - assertEventsFromApiMatchCreatedEvents(foundEvents, expectedEvents.reverse()); - }); - }); + it('should support sorting by event end', async () => { + const id = uuid.v4(); - it('should support date ranges for events', async () => { - const id = uuid.v4(); + const expectedEvents = await logFakeEvents(namespace, id, 6); - // write a document that shouldn't be found in the inclusive date range search - const firstEvent = fakeEvent(id); - await logTestEvent(id, firstEvent); + await retry.try(async () => { + const { + body: { data: foundEvents }, + } = await findEvents(namespace, id, { sort_field: 'event.end', sort_order: 'desc' }); - // wait a second, get the start time for the date range search - await delay(1000); - const start = new Date().toISOString(); + expect(foundEvents.length).to.be(expectedEvents.length); + assertEventsFromApiMatchCreatedEvents(foundEvents, expectedEvents.reverse()); + }); + }); - // write the documents that we should be found in the date range searches - const expectedEvents = await logFakeEvents(id, 6); + it('should support date ranges for events', async () => { + const id = uuid.v4(); - // get the end time for the date range search - const end = new Date().toISOString(); + // write a document that shouldn't be found in the inclusive date range search + const firstEvent = fakeEvent(namespace, id); + await logTestEvent(namespace, id, firstEvent); - // write a document that shouldn't be found in the inclusive date range search - await delay(1000); - const lastEvent = fakeEvent(id); - await logTestEvent(id, lastEvent); + // wait a second, get the start time for the date range search + await delay(1000); + const start = new Date().toISOString(); - await retry.try(async () => { - const { - body: { data: foundEvents, total }, - } = await findEvents(id, {}); + // write the documents that we should be found in the date range searches + const expectedEvents = await logFakeEvents(namespace, id, 6); - expect(foundEvents.length).to.be(8); - expect(total).to.be(8); - }); + // get the end time for the date range search + const end = new Date().toISOString(); - const { - body: { data: eventsWithinRange }, - } = await findEvents(id, { start, end }); + // write a document that shouldn't be found in the inclusive date range search + await delay(1000); + const lastEvent = fakeEvent(namespace, id); + await logTestEvent(namespace, id, lastEvent); - expect(eventsWithinRange.length).to.be(expectedEvents.length); - assertEventsFromApiMatchCreatedEvents(eventsWithinRange, expectedEvents); + await retry.try(async () => { + const { + body: { data: foundEvents, total }, + } = await findEvents(namespace, id, {}); - const { - body: { data: eventsFrom }, - } = await findEvents(id, { start }); + expect(foundEvents.length).to.be(8); + expect(total).to.be(8); + }); - expect(eventsFrom.length).to.be(expectedEvents.length + 1); - assertEventsFromApiMatchCreatedEvents(eventsFrom, [...expectedEvents, lastEvent]); + const { + body: { data: eventsWithinRange }, + } = await findEvents(namespace, id, { start, end }); - const { - body: { data: eventsUntil }, - } = await findEvents(id, { end }); + expect(eventsWithinRange.length).to.be(expectedEvents.length); + assertEventsFromApiMatchCreatedEvents(eventsWithinRange, expectedEvents); - expect(eventsUntil.length).to.be(expectedEvents.length + 1); - assertEventsFromApiMatchCreatedEvents(eventsUntil, [firstEvent, ...expectedEvents]); - }); + const { + body: { data: eventsFrom }, + } = await findEvents(namespace, id, { start }); + + expect(eventsFrom.length).to.be(expectedEvents.length + 1); + assertEventsFromApiMatchCreatedEvents(eventsFrom, [...expectedEvents, lastEvent]); + + const { + body: { data: eventsUntil }, + } = await findEvents(namespace, id, { end }); + + expect(eventsUntil.length).to.be(expectedEvents.length + 1); + assertEventsFromApiMatchCreatedEvents(eventsUntil, [firstEvent, ...expectedEvents]); + }); + }); + } }); - async function findEvents(id: string, query: Record = {}) { - const uri = `/api/event_log/event_log_test/${id}/_find${ + async function findEvents( + namespace: string | undefined, + id: string, + query: Record = {} + ) { + const urlPrefix = urlPrefixFromNamespace(namespace); + const url = `${urlPrefix}/api/event_log/event_log_test/${id}/_find${ isEmpty(query) ? '' : `?${Object.entries(query) .map(([key, val]) => `${key}=${val}`) .join('&')}` }`; - log.debug(`calling ${uri}`); - return await supertest.get(uri).set('kbn-xsrf', 'foo').expect(200); + log.debug(`Finding Events for Saved Object with ${url}`); + return await supertest.get(url).set('kbn-xsrf', 'foo').expect(200); } function assertEventsFromApiMatchCreatedEvents( @@ -169,16 +194,27 @@ export default function ({ getService }: FtrProviderContext) { } } - async function logTestEvent(id: string, event: IEvent) { - log.debug(`Logging Event for Saved Object ${id}`); - return await supertest - .post(`/api/log_event_fixture/${id}/_log`) - .set('kbn-xsrf', 'foo') - .send(event) - .expect(200); + async function logTestEvent(namespace: string | undefined, id: string, event: IEvent) { + const urlPrefix = urlPrefixFromNamespace(namespace); + const url = `${urlPrefix}/api/log_event_fixture/${id}/_log`; + log.debug(`Logging Event for Saved Object with ${url} - ${JSON.stringify(event)}`); + return await supertest.post(url).set('kbn-xsrf', 'foo').send(event).expect(200); } - function fakeEvent(id: string, overrides: Partial = {}): IEvent { + function fakeEvent( + namespace: string | undefined, + id: string, + overrides: Partial = {} + ): IEvent { + const savedObject: any = { + rel: 'primary', + type: 'event_log_test', + id, + }; + if (namespace !== undefined) { + savedObject.namespace = namespace; + } + return merge( { event: { @@ -186,14 +222,7 @@ export default function ({ getService }: FtrProviderContext) { action: 'test', }, kibana: { - saved_objects: [ - { - rel: 'primary', - namespace: 'default', - type: 'event_log_test', - id, - }, - ], + saved_objects: [savedObject], }, message: `test ${moment().toISOString()}`, }, @@ -201,13 +230,22 @@ export default function ({ getService }: FtrProviderContext) { ); } - async function logFakeEvents(savedObjectId: string, eventsToLog: number): Promise { + async function logFakeEvents( + namespace: string | undefined, + savedObjectId: string, + eventsToLog: number + ): Promise { const expectedEvents: IEvent[] = []; for (let index = 0; index < eventsToLog; index++) { - const event = fakeEvent(savedObjectId); - await logTestEvent(savedObjectId, event); + const event = fakeEvent(namespace, savedObjectId); + await logTestEvent(namespace, savedObjectId, event); expectedEvents.push(event); } return expectedEvents; } } + +function urlPrefixFromNamespace(namespace: string | undefined): string { + if (namespace === undefined) return ''; + return `/s/${namespace}`; +} From b47704b81c0afc4eff6d41a4f0e3a4884ef91b41 Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Tue, 14 Jul 2020 19:38:59 -0400 Subject: [PATCH 2/6] address PR review comments --- .../plugins/event_log/server/es/cluster_client_adapter.ts | 2 +- x-pack/plugins/event_log/server/es/context.test.ts | 7 ++----- x-pack/plugins/event_log/server/event_log_client.ts | 2 +- x-pack/plugins/event_log/server/plugin.ts | 2 +- .../spaces_only/tests/alerting/event_log.ts | 1 - 5 files changed, 5 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts index ab2f320cae34c..f86e5d9ca0e32 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts @@ -130,7 +130,7 @@ export class ClusterClientAdapter { public async queryEventsBySavedObject( index: string, - namespace: string | undefined, // TODO - add term clause for namespace + namespace: string | undefined, type: string, id: string, { page, per_page: perPage, start, end, sort_field, sort_order }: FindOptionsType diff --git a/x-pack/plugins/event_log/server/es/context.test.ts b/x-pack/plugins/event_log/server/es/context.test.ts index af48e2c60e429..74c860c18d101 100644 --- a/x-pack/plugins/event_log/server/es/context.test.ts +++ b/x-pack/plugins/event_log/server/es/context.test.ts @@ -4,13 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { createEsContext } from './context'; import { LegacyClusterClient, Logger } from 'src/core/server'; import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; - -import { createEsContext } from './context'; -jest.mock('../lib/../../../../package.json', () => ({ - version: '1.2.3', -})); +jest.mock('../lib/../../../../package.json', () => ({ version: '1.2.3' })); type EsClusterClient = Pick, 'callAsInternalUser' | 'asScoped'>; let logger: Logger; diff --git a/x-pack/plugins/event_log/server/event_log_client.ts b/x-pack/plugins/event_log/server/event_log_client.ts index fdb9daa638369..f4115e06160d7 100644 --- a/x-pack/plugins/event_log/server/event_log_client.ts +++ b/x-pack/plugins/event_log/server/event_log_client.ts @@ -90,7 +90,7 @@ export class EventLogClient implements IEventLogClient { const findOptions = findOptionsSchema.validate(options ?? {}); const space = await this.spacesService?.getActiveSpace(this.request); - const namespace = space?.id === 'default' ? undefined : space?.id; + const namespace = space && this.spacesService?.spaceIdToNamespace(space.id); // verify the user has the required permissions to view this saved object await this.savedObjectsClient.get(type, id); diff --git a/x-pack/plugins/event_log/server/plugin.ts b/x-pack/plugins/event_log/server/plugin.ts index 66ca86a3537ef..584d715bb2389 100644 --- a/x-pack/plugins/event_log/server/plugin.ts +++ b/x-pack/plugins/event_log/server/plugin.ts @@ -41,7 +41,7 @@ const ACTIONS = { }; interface PluginSetupDeps { - spaces: SpacesPluginSetup; + spaces?: SpacesPluginSetup; } export class Plugin implements CorePlugin { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts index 9f34b69745adb..79d25d8d10436 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts @@ -73,7 +73,6 @@ export default function eventLogTests({ getService }: FtrProviderContext) { actions: ['execute', 'execute-action', 'new-instance', 'resolved-instance'], }); }); - // console.log(JSON.stringify(events, null, 4)); // make sure the counts of the # of events per type are as expected const executeEvents = getEventsByAction(events, 'execute'); From 03f746bba0ac8df4ac1098f26ab5e1a3b2f96216 Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Wed, 15 Jul 2020 00:54:24 -0400 Subject: [PATCH 3/6] fix merge conflict --- .../plugins/event_log/server/es/context.test.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/event_log/server/es/context.test.ts b/x-pack/plugins/event_log/server/es/context.test.ts index 74c860c18d101..f30b71c99a043 100644 --- a/x-pack/plugins/event_log/server/es/context.test.ts +++ b/x-pack/plugins/event_log/server/es/context.test.ts @@ -5,9 +5,10 @@ */ import { createEsContext } from './context'; -import { LegacyClusterClient, Logger } from 'src/core/server'; -import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; +import { LegacyClusterClient, Logger } from '../../../../../src/core/server'; +import { elasticsearchServiceMock, loggingSystemMock } from '../../../../../src/core/server/mocks'; jest.mock('../lib/../../../../package.json', () => ({ version: '1.2.3' })); +jest.mock('./init'); type EsClusterClient = Pick, 'callAsInternalUser' | 'asScoped'>; let logger: Logger; @@ -90,4 +91,16 @@ describe('createEsContext', () => { ); expect(doesIndexTemplateExist).toBeTruthy(); }); + + test('should handled failed initialization', async () => { + jest.requireMock('./init').initializeEs.mockResolvedValue(false); + const context = createEsContext({ + logger, + clusterClientPromise: Promise.resolve(clusterClient), + indexNameRoot: 'test2', + }); + context.initialize(); + const success = await context.waitTillReady(); + expect(success).toBe(false); + }); }); From d1ab30e7e05a10c3bbd1ebd2610ab9b93eec4773 Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Wed, 15 Jul 2020 12:33:53 -0400 Subject: [PATCH 4/6] apply fixes from Gidi's PR review --- x-pack/plugins/event_log/server/plugin.ts | 2 +- .../event_log/server/routes/find.test.ts | 31 +++++++++++++++++-- .../plugins/event_log/server/routes/find.ts | 13 ++++---- .../common/lib/get_event_log.ts | 2 -- 4 files changed, 37 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/event_log/server/plugin.ts b/x-pack/plugins/event_log/server/plugin.ts index 584d715bb2389..9e36ca10b71f2 100644 --- a/x-pack/plugins/event_log/server/plugin.ts +++ b/x-pack/plugins/event_log/server/plugin.ts @@ -96,7 +96,7 @@ export class Plugin implements CorePlugin { jest.resetAllMocks(); @@ -19,7 +21,7 @@ describe('find', () => { it('finds events with proper parameters', async () => { const router = httpServiceMock.createRouter(); - findRoute(router); + findRoute(router, systemLogger); const [config, handler] = router.get.mock.calls[0]; @@ -58,7 +60,7 @@ describe('find', () => { it('supports optional pagination parameters', async () => { const router = httpServiceMock.createRouter(); - findRoute(router); + findRoute(router, systemLogger); const [, handler] = router.get.mock.calls[0]; eventLogClient.findEventsBySavedObject.mockResolvedValueOnce({ @@ -95,4 +97,29 @@ describe('find', () => { }, }); }); + + it('logs a warning when the query throws an error', async () => { + const router = httpServiceMock.createRouter(); + + findRoute(router, systemLogger); + + const [, handler] = router.get.mock.calls[0]; + eventLogClient.findEventsBySavedObject.mockRejectedValueOnce(new Error('oof!')); + + const [context, req, res] = mockHandlerArguments( + eventLogClient, + { + params: { id: '1', type: 'action' }, + query: { page: 3, per_page: 10 }, + }, + ['ok'] + ); + + await handler(context, req, res); + + expect(systemLogger.debug).toHaveBeenCalledTimes(1); + expect(systemLogger.debug).toHaveBeenCalledWith( + 'error calling eventLog findEventsBySavedObject(action, 1, {"page":3,"per_page":10}): oof!' + ); + }); }); diff --git a/x-pack/plugins/event_log/server/routes/find.ts b/x-pack/plugins/event_log/server/routes/find.ts index d67eed1fcde9a..3880ac2c10129 100644 --- a/x-pack/plugins/event_log/server/routes/find.ts +++ b/x-pack/plugins/event_log/server/routes/find.ts @@ -11,18 +11,18 @@ import { KibanaRequest, IKibanaResponse, KibanaResponseFactory, + Logger, } from 'src/core/server'; import { BASE_EVENT_LOG_API_PATH } from '../../common'; import { findOptionsSchema, FindOptionsType } from '../event_log_client'; -import { QueryEventsBySavedObjectResult } from '../es/cluster_client_adapter'; const paramSchema = schema.object({ type: schema.string(), id: schema.string(), }); -export const findRoute = (router: IRouter) => { +export const findRoute = (router: IRouter, systemLogger: Logger) => { router.get( { path: `${BASE_EVENT_LOG_API_PATH}/{type}/{id}/_find`, @@ -45,14 +45,15 @@ export const findRoute = (router: IRouter) => { query, } = req; - let result: QueryEventsBySavedObjectResult; try { - result = await eventLogClient.findEventsBySavedObject(type, id, query); + return res.ok({ + body: await eventLogClient.findEventsBySavedObject(type, id, query), + }); } catch (err) { + const call = `findEventsBySavedObject(${type}, ${id}, ${JSON.stringify(query)})`; + systemLogger.debug(`error calling eventLog ${call}: ${err.message}`); return res.notFound(); } - - return res.ok({ body: result }); }) ); }; diff --git a/x-pack/test/alerting_api_integration/common/lib/get_event_log.ts b/x-pack/test/alerting_api_integration/common/lib/get_event_log.ts index 090b3ee0a4854..69eeaafbf64fa 100644 --- a/x-pack/test/alerting_api_integration/common/lib/get_event_log.ts +++ b/x-pack/test/alerting_api_integration/common/lib/get_event_log.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -// import { IEvent } from '../../../../../../plugins/event_log/server'; - import { IValidatedEvent } from '../../../../plugins/event_log/server'; import { getUrlPrefix } from '.'; import { FtrProviderContext } from '../ftr_provider_context'; From 8df8d44d716d2cf2db2060c28cd4c6ca57d4c8c9 Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Wed, 15 Jul 2020 14:00:40 -0400 Subject: [PATCH 5/6] add some FT for security_and_spaces per PR review --- .../tests/actions/execute.ts | 73 +++++++++++++++++++ .../tests/alerting/alerts.ts | 73 +++++++++++++++++++ 2 files changed, 146 insertions(+) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts index 70a3663c1c798..5d609d001ee5d 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts @@ -11,8 +11,12 @@ import { ES_TEST_INDEX_NAME, getUrlPrefix, ObjectRemover, + getEventLog, } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { IValidatedEvent } from '../../../../../plugins/event_log/server'; + +const NANOS_IN_MILLIS = 1000 * 1000; // eslint-disable-next-line import/no-default-export export default function ({ getService }: FtrProviderContext) { @@ -107,6 +111,13 @@ export default function ({ getService }: FtrProviderContext) { reference, source: 'action:test.index-record', }); + + await validateEventLog({ + spaceId: space.id, + actionId: createdAction.id, + outcome: 'success', + message: `action executed: test.index-record:${createdAction.id}: My action`, + }); break; default: throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); @@ -480,4 +491,66 @@ export default function ({ getService }: FtrProviderContext) { }); } }); + + interface ValidateEventLogParams { + spaceId: string; + actionId: string; + outcome: string; + message: string; + errorMessage?: string; + } + + async function validateEventLog(params: ValidateEventLogParams): Promise { + const { spaceId, actionId, outcome, message, errorMessage } = params; + + const events: IValidatedEvent[] = await retry.try(async () => { + return await getEventLog({ + getService, + spaceId, + type: 'action', + id: actionId, + provider: 'actions', + actions: ['execute'], + }); + }); + + expect(events.length).to.equal(1); + + const event = events[0]; + + const duration = event?.event?.duration; + const eventStart = Date.parse(event?.event?.start || 'undefined'); + const eventEnd = Date.parse(event?.event?.end || 'undefined'); + const dateNow = Date.now(); + + expect(typeof duration).to.be('number'); + expect(eventStart).to.be.ok(); + expect(eventEnd).to.be.ok(); + + const durationDiff = Math.abs( + Math.round(duration! / NANOS_IN_MILLIS) - (eventEnd - eventStart) + ); + + // account for rounding errors + expect(durationDiff < 1).to.equal(true); + expect(eventStart <= eventEnd).to.equal(true); + expect(eventEnd <= dateNow).to.equal(true); + + expect(event?.event?.outcome).to.equal(outcome); + + expect(event?.kibana?.saved_objects).to.eql([ + { + rel: 'primary', + type: 'action', + id: actionId, + namespace: spaceId, + }, + ]); + + expect(event?.message).to.eql(message); + + if (errorMessage) { + expect(event?.error?.message).to.eql(errorMessage); + } + } } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts index dce809f0b7be9..dd1f7ea526921 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts @@ -15,7 +15,11 @@ import { ObjectRemover, AlertUtils, TaskManagerUtils, + getEventLog, } from '../../../common/lib'; +import { IValidatedEvent } from '../../../../../plugins/event_log/server'; + +const NANOS_IN_MILLIS = 1000 * 1000; // eslint-disable-next-line import/no-default-export export default function alertTests({ getService }: FtrProviderContext) { @@ -160,6 +164,13 @@ instanceStateValue: true }); await taskManagerUtils.waitForActionTaskParamsToBeCleanedUp(testStart); + + await validateEventLog({ + spaceId: space.id, + alertId, + outcome: 'success', + message: `alert executed: test.always-firing:${alertId}: 'abc'`, + }); break; default: throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); @@ -928,4 +939,66 @@ instanceStateValue: true }); } }); + + interface ValidateEventLogParams { + spaceId: string; + alertId: string; + outcome: string; + message: string; + errorMessage?: string; + } + + async function validateEventLog(params: ValidateEventLogParams): Promise { + const { spaceId, alertId, outcome, message, errorMessage } = params; + + const events: IValidatedEvent[] = await retry.try(async () => { + return await getEventLog({ + getService, + spaceId, + type: 'alert', + id: alertId, + provider: 'alerting', + actions: ['execute'], + }); + }); + + expect(events.length).to.be.greaterThan(0); + + const event = events[0]; + + const duration = event?.event?.duration; + const eventStart = Date.parse(event?.event?.start || 'undefined'); + const eventEnd = Date.parse(event?.event?.end || 'undefined'); + const dateNow = Date.now(); + + expect(typeof duration).to.be('number'); + expect(eventStart).to.be.ok(); + expect(eventEnd).to.be.ok(); + + const durationDiff = Math.abs( + Math.round(duration! / NANOS_IN_MILLIS) - (eventEnd - eventStart) + ); + + // account for rounding errors + expect(durationDiff < 1).to.equal(true); + expect(eventStart <= eventEnd).to.equal(true); + expect(eventEnd <= dateNow).to.equal(true); + + expect(event?.event?.outcome).to.equal(outcome); + + expect(event?.kibana?.saved_objects).to.eql([ + { + rel: 'primary', + type: 'alert', + id: alertId, + namespace: spaceId, + }, + ]); + + expect(event?.message).to.eql(message); + + if (errorMessage) { + expect(event?.error?.message).to.eql(errorMessage); + } + } } From 47301c9aad16557f99b3369125dd193034cb333e Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Wed, 15 Jul 2020 16:47:30 -0400 Subject: [PATCH 6/6] remove another esArchiver.unload('empty_kibana') that crept in a master merge --- .../tests/actions/builtin_action_types/resilient.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts index a77e0414a19d4..94feabb556a51 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts @@ -34,7 +34,6 @@ const mapping = [ // eslint-disable-next-line import/no-default-export export default function resilientTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const mockResilient = { @@ -82,8 +81,6 @@ export default function resilientTest({ getService }: FtrProviderContext) { ); }); - after(() => esArchiver.unload('empty_kibana')); - describe('IBM Resilient - Action Creation', () => { it('should return 200 when creating a ibm resilient action successfully', async () => { const { body: createdAction } = await supertest