From bfae08713e868c7c4e1100ce6ad335662127d3f1 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 11 Aug 2020 15:00:02 +0100 Subject: [PATCH] [Event log] Use Alerts client & Actions client when fetching these types of SOs (#73257) Introduces a pluggable API to Event Log which allows custom Providers for Saved Objects which is used to ensure a user is authorised to get the Saved Object referenced in the Event Log whenever the find api is called. --- x-pack/.i18nrc.json | 1 + x-pack/plugins/actions/server/plugin.ts | 7 ++ x-pack/plugins/alerts/server/plugin.ts | 7 ++ .../event_log/server/event_log_client.test.ts | 55 ++++++----- .../event_log/server/event_log_client.ts | 18 ++-- .../server/event_log_service.mock.ts | 1 + .../server/event_log_service.test.ts | 25 +++++ .../event_log/server/event_log_service.ts | 16 ++- .../server/event_log_start_service.test.ts | 11 +-- .../server/event_log_start_service.ts | 29 +++--- .../event_log/server/event_logger.test.ts | 2 + x-pack/plugins/event_log/server/plugin.ts | 11 ++- .../saved_object_provider_registry.mock.ts | 19 ++++ .../saved_object_provider_registry.test.ts | 98 +++++++++++++++++++ .../server/saved_object_provider_registry.ts | 68 +++++++++++++ x-pack/plugins/event_log/server/types.ts | 3 +- .../plugins/alerts/server/alert_types.ts | 4 +- .../fixtures/plugins/alerts/server/plugin.ts | 6 ++ 18 files changed, 319 insertions(+), 62 deletions(-) create mode 100644 x-pack/plugins/event_log/server/saved_object_provider_registry.mock.ts create mode 100644 x-pack/plugins/event_log/server/saved_object_provider_registry.test.ts create mode 100644 x-pack/plugins/event_log/server/saved_object_provider_registry.ts diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index d0055008eb9bf..69ad9ad33bf72 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -4,6 +4,7 @@ "xpack.actions": "plugins/actions", "xpack.uiActionsEnhanced": ["plugins/ui_actions_enhanced", "examples/ui_actions_enhanced_examples"], "xpack.alerts": "plugins/alerts", + "xpack.eventLog": "plugins/event_log", "xpack.alertingBuiltins": "plugins/alerting_builtins", "xpack.apm": ["legacy/plugins/apm", "plugins/apm"], "xpack.beatsManagement": ["legacy/plugins/beats_management", "plugins/beats_management"], diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 54d137cc0f617..ee50ee81d507c 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -123,6 +123,7 @@ export class ActionsPlugin implements Plugin, Plugi private licenseState: ILicenseState | null = null; private spaces?: SpacesServiceSetup; private security?: SecurityPluginSetup; + private eventLogService?: IEventLogService; private eventLogger?: IEventLogger; private isESOUsingEphemeralEncryptionKey?: boolean; private readonly telemetryLogger: Logger; @@ -160,6 +161,7 @@ export class ActionsPlugin implements Plugin, Plugi plugins.features.registerFeature(ACTIONS_FEATURE); setupSavedObjects(core.savedObjects, plugins.encryptedSavedObjects); + this.eventLogService = plugins.eventLog; plugins.eventLog.registerProviderActions(EVENT_LOG_PROVIDER, Object.values(EVENT_LOG_ACTIONS)); this.eventLogger = plugins.eventLog.getLogger({ event: { provider: EVENT_LOG_PROVIDER }, @@ -295,6 +297,11 @@ export class ActionsPlugin implements Plugin, Plugi }); }; + this.eventLogService!.registerSavedObjectProvider('action', (request) => { + const client = getActionsClientWithRequest(request); + return async (type: string, id: string) => (await client).get({ id }); + }); + const getScopedSavedObjectsClientWithoutAccessToActions = (request: KibanaRequest) => core.savedObjects.getScopedClient(request); diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index 2f0df44197553..5d69887bd5bf0 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -106,6 +106,7 @@ export class AlertingPlugin { private readonly alertsClientFactory: AlertsClientFactory; private readonly telemetryLogger: Logger; private readonly kibanaIndex: Promise; + private eventLogService?: IEventLogService; private eventLogger?: IEventLogger; constructor(initializerContext: PluginInitializerContext) { @@ -150,6 +151,7 @@ export class AlertingPlugin { setupSavedObjects(core.savedObjects, plugins.encryptedSavedObjects); + this.eventLogService = plugins.eventLog; plugins.eventLog.registerProviderActions(EVENT_LOG_PROVIDER, Object.values(EVENT_LOG_ACTIONS)); this.eventLogger = plugins.eventLog.getLogger({ event: { provider: EVENT_LOG_PROVIDER }, @@ -255,6 +257,11 @@ export class AlertingPlugin { eventLogger: this.eventLogger!, }); + this.eventLogService!.registerSavedObjectProvider('alert', (request) => { + const client = getAlertsClientWithRequest(request); + return (type: string, id: string) => client.get({ id }); + }); + scheduleAlertingTelemetry(this.telemetryLogger, plugins.taskManager); return { 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 917d517a6e27d..3273fe847080f 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 @@ -7,7 +7,6 @@ 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'; import { merge } from 'lodash'; import moment from 'moment'; @@ -15,14 +14,15 @@ describe('EventLogStart', () => { describe('findEventsBySavedObject', () => { test('verifies that the user can access the specified saved object', async () => { const esContext = contextMock.create(); - const savedObjectsClient = savedObjectsClientMock.create(); + const savedObjectGetter = jest.fn(); + const eventLogClient = new EventLogClient({ esContext, - savedObjectsClient, + savedObjectGetter, request: FakeRequest(), }); - savedObjectsClient.get.mockResolvedValueOnce({ + savedObjectGetter.mockResolvedValueOnce({ id: 'saved-object-id', type: 'saved-object-type', attributes: {}, @@ -31,19 +31,21 @@ describe('EventLogStart', () => { await eventLogClient.findEventsBySavedObject('saved-object-type', 'saved-object-id'); - expect(savedObjectsClient.get).toHaveBeenCalledWith('saved-object-type', 'saved-object-id'); + expect(savedObjectGetter).toHaveBeenCalledWith('saved-object-type', 'saved-object-id'); }); test('throws when the user doesnt have permission to access the specified saved object', async () => { const esContext = contextMock.create(); - const savedObjectsClient = savedObjectsClientMock.create(); + + const savedObjectGetter = jest.fn(); + const eventLogClient = new EventLogClient({ esContext, - savedObjectsClient, + savedObjectGetter, request: FakeRequest(), }); - savedObjectsClient.get.mockRejectedValue(new Error('Fail')); + savedObjectGetter.mockRejectedValue(new Error('Fail')); expect( eventLogClient.findEventsBySavedObject('saved-object-type', 'saved-object-id') @@ -52,14 +54,16 @@ describe('EventLogStart', () => { test('fetches all event that reference the saved object', async () => { const esContext = contextMock.create(); - const savedObjectsClient = savedObjectsClientMock.create(); + + const savedObjectGetter = jest.fn(); + const eventLogClient = new EventLogClient({ esContext, - savedObjectsClient, + savedObjectGetter, request: FakeRequest(), }); - savedObjectsClient.get.mockResolvedValueOnce({ + savedObjectGetter.mockResolvedValueOnce({ id: 'saved-object-id', type: 'saved-object-type', attributes: {}, @@ -125,14 +129,16 @@ describe('EventLogStart', () => { test('fetches all events in time frame that reference the saved object', async () => { const esContext = contextMock.create(); - const savedObjectsClient = savedObjectsClientMock.create(); + + const savedObjectGetter = jest.fn(); + const eventLogClient = new EventLogClient({ esContext, - savedObjectsClient, + savedObjectGetter, request: FakeRequest(), }); - savedObjectsClient.get.mockResolvedValueOnce({ + savedObjectGetter.mockResolvedValueOnce({ id: 'saved-object-id', type: 'saved-object-type', attributes: {}, @@ -206,14 +212,16 @@ describe('EventLogStart', () => { test('validates that the start date is valid', async () => { const esContext = contextMock.create(); - const savedObjectsClient = savedObjectsClientMock.create(); + + const savedObjectGetter = jest.fn(); + const eventLogClient = new EventLogClient({ esContext, - savedObjectsClient, + savedObjectGetter, request: FakeRequest(), }); - savedObjectsClient.get.mockResolvedValueOnce({ + savedObjectGetter.mockResolvedValueOnce({ id: 'saved-object-id', type: 'saved-object-type', attributes: {}, @@ -236,14 +244,16 @@ describe('EventLogStart', () => { test('validates that the end date is valid', async () => { const esContext = contextMock.create(); - const savedObjectsClient = savedObjectsClientMock.create(); + + const savedObjectGetter = jest.fn(); + const eventLogClient = new EventLogClient({ esContext, - savedObjectsClient, + savedObjectGetter, request: FakeRequest(), }); - savedObjectsClient.get.mockResolvedValueOnce({ + savedObjectGetter.mockResolvedValueOnce({ id: 'saved-object-id', type: 'saved-object-type', attributes: {}, @@ -297,7 +307,8 @@ function fakeEvent(overrides = {}) { } function FakeRequest(): KibanaRequest { - const savedObjectsClient = savedObjectsClientMock.create(); + const savedObjectGetter = jest.fn(); + return ({ headers: {}, getBasePath: () => '', @@ -311,6 +322,6 @@ function FakeRequest(): KibanaRequest { url: '/', }, }, - getSavedObjectsClient: () => savedObjectsClient, + getSavedObjectsClient: () => savedObjectGetter, } 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 f4115e06160d7..32fd99d170026 100644 --- a/x-pack/plugins/event_log/server/event_log_client.ts +++ b/x-pack/plugins/event_log/server/event_log_client.ts @@ -6,12 +6,13 @@ import { Observable } from 'rxjs'; import { schema, TypeOf } from '@kbn/config-schema'; -import { LegacyClusterClient, SavedObjectsClientContract, KibanaRequest } from 'src/core/server'; +import { LegacyClusterClient, 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'; +import { SavedObjectGetter } from './saved_object_provider_registry'; export type PluginClusterClient = Pick; export type AdminClusterClient$ = Observable; @@ -58,7 +59,7 @@ export type FindOptionsType = Pick< interface EventLogServiceCtorParams { esContext: EsContext; - savedObjectsClient: SavedObjectsClientContract; + savedObjectGetter: SavedObjectGetter; spacesService?: SpacesServiceSetup; request: KibanaRequest; } @@ -66,18 +67,13 @@ interface EventLogServiceCtorParams { // 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 savedObjectGetter: SavedObjectGetter; private spacesService?: SpacesServiceSetup; private request: KibanaRequest; - constructor({ - esContext, - savedObjectsClient, - spacesService, - request, - }: EventLogServiceCtorParams) { + constructor({ esContext, savedObjectGetter, spacesService, request }: EventLogServiceCtorParams) { this.esContext = esContext; - this.savedObjectsClient = savedObjectsClient; + this.savedObjectGetter = savedObjectGetter; this.spacesService = spacesService; this.request = request; } @@ -93,7 +89,7 @@ export class EventLogClient implements IEventLogClient { 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); + await this.savedObjectGetter(type, id); return await this.esContext.esAdapter.queryEventsBySavedObject( this.esContext.esNames.alias, diff --git a/x-pack/plugins/event_log/server/event_log_service.mock.ts b/x-pack/plugins/event_log/server/event_log_service.mock.ts index 805c241414a2e..877e5d59a1831 100644 --- a/x-pack/plugins/event_log/server/event_log_service.mock.ts +++ b/x-pack/plugins/event_log/server/event_log_service.mock.ts @@ -15,6 +15,7 @@ const createEventLogServiceMock = () => { registerProviderActions: jest.fn(), isProviderActionRegistered: jest.fn(), getProviderActions: jest.fn(), + registerSavedObjectProvider: jest.fn(), getLogger: jest.fn().mockReturnValue(eventLoggerMock.create()), }; return mock; diff --git a/x-pack/plugins/event_log/server/event_log_service.test.ts b/x-pack/plugins/event_log/server/event_log_service.test.ts index 2cf68592f2fa1..2b92443569f4f 100644 --- a/x-pack/plugins/event_log/server/event_log_service.test.ts +++ b/x-pack/plugins/event_log/server/event_log_service.test.ts @@ -8,9 +8,11 @@ import { IEventLogConfig } from './types'; import { EventLogService } from './event_log_service'; import { contextMock } from './es/context.mock'; import { loggingSystemMock } from 'src/core/server/mocks'; +import { savedObjectProviderRegistryMock } from './saved_object_provider_registry.mock'; const loggingService = loggingSystemMock.create(); const systemLogger = loggingService.get(); +const savedObjectProviderRegistry = savedObjectProviderRegistryMock.create(); describe('EventLogService', () => { const esContext = contextMock.create(); @@ -21,6 +23,7 @@ describe('EventLogService', () => { esContext, systemLogger, kibanaUUID: '42', + savedObjectProviderRegistry, config: { enabled, logEntries, @@ -65,6 +68,7 @@ describe('EventLogService', () => { esContext, systemLogger, kibanaUUID: '42', + savedObjectProviderRegistry, config: { enabled: true, logEntries: true, @@ -102,6 +106,7 @@ describe('EventLogService', () => { esContext, systemLogger, kibanaUUID: '42', + savedObjectProviderRegistry, config: { enabled: true, logEntries: true, @@ -112,4 +117,24 @@ describe('EventLogService', () => { const eventLogger = service.getLogger({}); expect(eventLogger).toBeTruthy(); }); + + describe('registerSavedObjectProvider', () => { + test('register SavedObject Providers in the registry', () => { + const params = { + esContext, + systemLogger, + kibanaUUID: '42', + savedObjectProviderRegistry, + config: { + enabled: true, + logEntries: true, + indexEntries: true, + }, + }; + const service = new EventLogService(params); + const provider = jest.fn(); + service.registerSavedObjectProvider('myType', provider); + expect(savedObjectProviderRegistry.registerProvider).toHaveBeenCalledWith('myType', provider); + }); + }); }); diff --git a/x-pack/plugins/event_log/server/event_log_service.ts b/x-pack/plugins/event_log/server/event_log_service.ts index f7f915f1cf0ef..9249288d33939 100644 --- a/x-pack/plugins/event_log/server/event_log_service.ts +++ b/x-pack/plugins/event_log/server/event_log_service.ts @@ -11,6 +11,7 @@ import { Plugin } from './plugin'; import { EsContext } from './es'; import { IEvent, IEventLogger, IEventLogService, IEventLogConfig } from './types'; import { EventLogger } from './event_logger'; +import { SavedObjectProvider, SavedObjectProviderRegistry } from './saved_object_provider_registry'; export type PluginClusterClient = Pick; export type AdminClusterClient$ = Observable; @@ -21,6 +22,7 @@ interface EventLogServiceCtorParams { esContext: EsContext; kibanaUUID: string; systemLogger: SystemLogger; + savedObjectProviderRegistry: SavedObjectProviderRegistry; } // note that clusterClient may be null, indicating we can't write to ES @@ -29,15 +31,23 @@ export class EventLogService implements IEventLogService { private esContext: EsContext; private systemLogger: SystemLogger; private registeredProviderActions: Map>; + private savedObjectProviderRegistry: SavedObjectProviderRegistry; public readonly kibanaUUID: string; - constructor({ config, esContext, kibanaUUID, systemLogger }: EventLogServiceCtorParams) { + constructor({ + config, + esContext, + kibanaUUID, + systemLogger, + savedObjectProviderRegistry, + }: EventLogServiceCtorParams) { this.config = config; this.esContext = esContext; this.kibanaUUID = kibanaUUID; this.systemLogger = systemLogger; this.registeredProviderActions = new Map>(); + this.savedObjectProviderRegistry = savedObjectProviderRegistry; } public isEnabled(): boolean { @@ -77,6 +87,10 @@ export class EventLogService implements IEventLogService { return new Map(this.registeredProviderActions.entries()); } + registerSavedObjectProvider(type: string, provider: SavedObjectProvider) { + return this.savedObjectProviderRegistry.registerProvider(type, provider); + } + getLogger(initialProperties: IEvent): IEventLogger { return new EventLogger({ esContext: this.esContext, 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 3bd5ef7c0b3ba..cbdc168a8ffde 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 @@ -5,10 +5,11 @@ */ import { KibanaRequest } from 'src/core/server'; -import { savedObjectsClientMock, savedObjectsServiceMock } from 'src/core/server/mocks'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; import { EventLogClientService } from './event_log_start_service'; import { contextMock } from './es/context.mock'; +import { savedObjectProviderRegistryMock } from './saved_object_provider_registry.mock'; jest.mock('./event_log_client'); @@ -17,19 +18,17 @@ describe('EventLogClientService', () => { describe('getClient', () => { test('creates a client with a scoped SavedObjects client', () => { - const savedObjectsService = savedObjectsServiceMock.createStartContract(); + const savedObjectProviderRegistry = savedObjectProviderRegistryMock.create(); const request = fakeRequest(); const eventLogStartService = new EventLogClientService({ esContext, - savedObjectsService, + savedObjectProviderRegistry, }); eventLogStartService.getClient(request); - expect(savedObjectsService.getScopedClient).toHaveBeenCalledWith(request, { - includedHiddenTypes: ['action', 'alert'], - }); + expect(savedObjectProviderRegistry.getProvidersClient).toHaveBeenCalledWith(request); }); }); }); 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 8b752684c1cc3..5cadab4df3ed7 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 @@ -5,49 +5,42 @@ */ import { Observable } from 'rxjs'; -import { - LegacyClusterClient, - KibanaRequest, - SavedObjectsServiceStart, - SavedObjectsClientContract, -} from 'src/core/server'; +import { LegacyClusterClient, KibanaRequest } from 'src/core/server'; import { SpacesServiceSetup } from '../../spaces/server'; import { EsContext } from './es'; import { IEventLogClientService } from './types'; import { EventLogClient } from './event_log_client'; +import { SavedObjectProviderRegistry } from './saved_object_provider_registry'; export type PluginClusterClient = Pick; export type AdminClusterClient$ = Observable; -const includedHiddenTypes = ['action', 'alert']; - interface EventLogServiceCtorParams { esContext: EsContext; - savedObjectsService: SavedObjectsServiceStart; + savedObjectProviderRegistry: SavedObjectProviderRegistry; 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 savedObjectProviderRegistry: SavedObjectProviderRegistry; private spacesService?: SpacesServiceSetup; - constructor({ esContext, savedObjectsService, spacesService }: EventLogServiceCtorParams) { + constructor({ + esContext, + savedObjectProviderRegistry, + spacesService, + }: EventLogServiceCtorParams) { this.esContext = esContext; - this.savedObjectsService = savedObjectsService; + this.savedObjectProviderRegistry = savedObjectProviderRegistry; this.spacesService = spacesService; } getClient(request: KibanaRequest) { - const savedObjectsClient: SavedObjectsClientContract = this.savedObjectsService.getScopedClient( - request, - { includedHiddenTypes } - ); - return new EventLogClient({ esContext: this.esContext, - savedObjectsClient, + savedObjectGetter: this.savedObjectProviderRegistry.getProvidersClient(request), spacesService: this.spacesService, request, }); diff --git a/x-pack/plugins/event_log/server/event_logger.test.ts b/x-pack/plugins/event_log/server/event_logger.test.ts index fde3b2de8dd36..0ab3071f70efa 100644 --- a/x-pack/plugins/event_log/server/event_logger.test.ts +++ b/x-pack/plugins/event_log/server/event_logger.test.ts @@ -12,6 +12,7 @@ import { contextMock } from './es/context.mock'; import { loggingSystemMock } from 'src/core/server/mocks'; import { delay } from './lib/delay'; import { EVENT_LOGGED_PREFIX } from './event_logger'; +import { savedObjectProviderRegistryMock } from './saved_object_provider_registry.mock'; const KIBANA_SERVER_UUID = '424-24-2424'; const WRITE_LOG_WAIT_MILLIS = 3000; @@ -31,6 +32,7 @@ describe('EventLogger', () => { systemLogger, config: { enabled: true, logEntries: true, indexEntries: true }, kibanaUUID: KIBANA_SERVER_UUID, + savedObjectProviderRegistry: savedObjectProviderRegistryMock.create(), }); eventLogger = service.getLogger({}); }); diff --git a/x-pack/plugins/event_log/server/plugin.ts b/x-pack/plugins/event_log/server/plugin.ts index 9e36ca10b71f2..1353877fa4629 100644 --- a/x-pack/plugins/event_log/server/plugin.ts +++ b/x-pack/plugins/event_log/server/plugin.ts @@ -30,6 +30,7 @@ import { findRoute } from './routes'; import { EventLogService } from './event_log_service'; import { createEsContext, EsContext } from './es'; import { EventLogClientService } from './event_log_start_service'; +import { SavedObjectProviderRegistry } from './saved_object_provider_registry'; export type PluginClusterClient = Pick; @@ -53,11 +54,13 @@ export class Plugin implements CorePlugin; private eventLogClientService?: EventLogClientService; private spacesService?: SpacesServiceSetup; + private savedObjectProviderRegistry: SavedObjectProviderRegistry; constructor(private readonly context: PluginInitializerContext) { this.systemLogger = this.context.logger.get(); this.config$ = this.context.config.create(); this.globalConfig$ = this.context.config.legacy.globalConfig$; + this.savedObjectProviderRegistry = new SavedObjectProviderRegistry(); } async setup(core: CoreSetup, { spaces }: PluginSetupDeps): Promise { @@ -83,6 +86,7 @@ export class Plugin implements CorePlugin { + const client = core.savedObjects.getScopedClient(request); + return client.get.bind(client); + }); + this.eventLogClientService = new EventLogClientService({ esContext: this.esContext, - savedObjectsService: core.savedObjects, + savedObjectProviderRegistry: this.savedObjectProviderRegistry, spacesService: this.spacesService, }); return this.eventLogClientService; diff --git a/x-pack/plugins/event_log/server/saved_object_provider_registry.mock.ts b/x-pack/plugins/event_log/server/saved_object_provider_registry.mock.ts new file mode 100644 index 0000000000000..433deaf7bff72 --- /dev/null +++ b/x-pack/plugins/event_log/server/saved_object_provider_registry.mock.ts @@ -0,0 +1,19 @@ +/* + * 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 { SavedObjectProviderRegistry } from './saved_object_provider_registry'; + +const createSavedObjectProviderRegistryMock = () => { + return ({ + registerProvider: jest.fn(), + registerDefaultProvider: jest.fn(), + getProvidersClient: jest.fn(), + } as unknown) as jest.Mocked; +}; + +export const savedObjectProviderRegistryMock = { + create: createSavedObjectProviderRegistryMock, +}; diff --git a/x-pack/plugins/event_log/server/saved_object_provider_registry.test.ts b/x-pack/plugins/event_log/server/saved_object_provider_registry.test.ts new file mode 100644 index 0000000000000..6a02d54c87514 --- /dev/null +++ b/x-pack/plugins/event_log/server/saved_object_provider_registry.test.ts @@ -0,0 +1,98 @@ +/* + * 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 { SavedObjectProviderRegistry } from './saved_object_provider_registry'; +import uuid from 'uuid'; +import { KibanaRequest } from 'src/core/server'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; + +describe('SavedObjectProviderRegistry', () => { + beforeEach(() => jest.resetAllMocks()); + + describe('registerProvider()', () => { + test('should register providers', () => { + const registry = new SavedObjectProviderRegistry(); + registry.registerProvider('alert', jest.fn()); + }); + + test('should throw an error if type is already registered', () => { + const registry = new SavedObjectProviderRegistry(); + registry.registerProvider('alert', jest.fn()); + expect(() => + registry.registerProvider('alert', jest.fn()) + ).toThrowErrorMatchingInlineSnapshot( + `"The Event Log has already registered a Provider for the Save Object type \\"alert\\"."` + ); + }); + }); + + describe('getProvidersClient()', () => { + test('should get SavedObject using the registered provider by type', async () => { + const registry = new SavedObjectProviderRegistry(); + registry.registerDefaultProvider(jest.fn()); + + const getter = jest.fn(); + const provider = jest.fn().mockReturnValue(getter); + registry.registerProvider('alert', provider); + + const request = fakeRequest(); + const alert = { + id: uuid.v4(), + }; + + getter.mockResolvedValue(alert); + + expect(await registry.getProvidersClient(request)('alert', alert.id)).toMatchObject(alert); + + expect(provider).toHaveBeenCalledWith(request); + expect(getter).toHaveBeenCalledWith('alert', alert.id); + }); + + test('should get SavedObject using the default provider for unregistered types', async () => { + const registry = new SavedObjectProviderRegistry(); + const defaultProvider = jest.fn(); + registry.registerDefaultProvider(defaultProvider); + + registry.registerProvider('alert', jest.fn().mockReturnValue(jest.fn())); + + const request = fakeRequest(); + const action = { + id: uuid.v4(), + type: 'action', + attributes: {}, + references: [], + }; + + const getter = jest.fn(); + defaultProvider.mockReturnValue(getter); + getter.mockResolvedValue(action); + + expect(await registry.getProvidersClient(request)('action', action.id)).toMatchObject(action); + + expect(getter).toHaveBeenCalledWith('action', action.id); + expect(defaultProvider).toHaveBeenCalledWith(request); + }); + }); +}); + +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/saved_object_provider_registry.ts b/x-pack/plugins/event_log/server/saved_object_provider_registry.ts new file mode 100644 index 0000000000000..87a1da5dd6f4a --- /dev/null +++ b/x-pack/plugins/event_log/server/saved_object_provider_registry.ts @@ -0,0 +1,68 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { KibanaRequest, SavedObjectsClientContract } from 'src/core/server'; + +import { fromNullable, getOrElse } from 'fp-ts/lib/Option'; +import { pipe } from 'fp-ts/lib/pipeable'; + +export type SavedObjectGetter = ( + ...params: Parameters +) => Promise; +export type SavedObjectProvider = (request: KibanaRequest) => SavedObjectGetter; + +export class SavedObjectProviderRegistry { + private providers = new Map(); + private defaultProvider?: SavedObjectProvider; + + constructor() {} + + public registerDefaultProvider(provider: SavedObjectProvider) { + this.defaultProvider = provider; + } + + public registerProvider(type: string, provider: SavedObjectProvider) { + if (this.providers.has(type)) { + throw new Error( + `The Event Log has already registered a Provider for the Save Object type "${type}".` + ); + } + this.providers.set(type, provider); + } + + public getProvidersClient(request: KibanaRequest): SavedObjectGetter { + if (!this.defaultProvider) { + throw new Error( + i18n.translate( + 'xpack.eventLog.savedObjectProviderRegistry.getProvidersClient.noDefaultProvider', + { + defaultMessage: 'The Event Log requires a default Provider.', + } + ) + ); + } + + // `scopedProviders` is a cache of providers which are scoped t othe current request. + // The client will only instantiate a provider on-demand and it will cache each + // one to enable the request to reuse each provider. + const scopedProviders = new Map(); + const defaultGetter = this.defaultProvider(request); + return (type: string, id: string) => { + const getter = pipe( + fromNullable(scopedProviders.get(type)), + getOrElse(() => { + const client = this.providers.has(type) + ? this.providers.get(type)!(request) + : defaultGetter; + scopedProviders.set(type, client); + return client; + }) + ); + return getter(type, id); + }; + } +} diff --git a/x-pack/plugins/event_log/server/types.ts b/x-pack/plugins/event_log/server/types.ts index 1a37c4e58d079..cda9579220623 100644 --- a/x-pack/plugins/event_log/server/types.ts +++ b/x-pack/plugins/event_log/server/types.ts @@ -12,6 +12,7 @@ export { IEvent, IValidatedEvent, EventSchema, ECS_VERSION } from '../generated/ import { IEvent } from '../generated/schemas'; import { FindOptionsType } from './event_log_client'; import { QueryEventsBySavedObjectResult } from './es/cluster_client_adapter'; +import { SavedObjectProvider } from './saved_object_provider_registry'; export const SAVED_OBJECT_REL_PRIMARY = 'primary'; @@ -40,7 +41,7 @@ export interface IEventLogService { registerProviderActions(provider: string, actions: string[]): void; isProviderActionRegistered(provider: string, action: string): boolean; getProviderActions(): Map>; - + registerSavedObjectProvider(type: string, provider: SavedObjectProvider): void; getLogger(properties: IEvent): IEventLogger; } 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 ebf639067518f..269a9d3a504a2 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 @@ -296,7 +296,7 @@ export function defineAlertTypes( name: 'Default', }, ], - producer: 'alerting', + producer: 'alertsFixture', defaultActionGroupId: 'default', async executor({ services, params, state }: AlertExecutorOptions) { throw new Error('this alert is intended to fail'); @@ -306,7 +306,7 @@ export function defineAlertTypes( id: 'test.patternFiring', name: 'Test: Firing on a Pattern', actionGroups: [{ id: 'default', name: 'Default' }], - producer: 'alerting', + producer: 'alertsFixture', defaultActionGroupId: 'default', async executor(alertExecutorOptions: AlertExecutorOptions) { const { services, state, params } = alertExecutorOptions; diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts index 5881201a82e09..1b8a380eaaeb2 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts @@ -40,6 +40,8 @@ export class FixturePlugin implements Plugin