diff --git a/src/core/server/saved_objects/service/lib/priority_collection.test.ts b/src/core/server/saved_objects/service/lib/priority_collection.test.ts index 9256b2e913931..13180b912e7d7 100644 --- a/src/core/server/saved_objects/service/lib/priority_collection.test.ts +++ b/src/core/server/saved_objects/service/lib/priority_collection.test.ts @@ -55,3 +55,15 @@ test(`1, 1 throws Error`, () => { priorityCollection.add(1, 1); expect(() => priorityCollection.add(1, 1)).toThrowErrorMatchingSnapshot(); }); + +test(`#has when empty returns false`, () => { + const priorityCollection = new PriorityCollection(); + expect(priorityCollection.has(() => true)).toEqual(false); +}); + +test(`#has returns result of predicate`, () => { + const priorityCollection = new PriorityCollection(); + priorityCollection.add(1, 'foo'); + expect(priorityCollection.has(val => val === 'foo')).toEqual(true); + expect(priorityCollection.has(val => val === 'bar')).toEqual(false); +}); diff --git a/src/core/server/saved_objects/service/lib/priority_collection.ts b/src/core/server/saved_objects/service/lib/priority_collection.ts index 3c918f0c1e1fc..a2fe13b933347 100644 --- a/src/core/server/saved_objects/service/lib/priority_collection.ts +++ b/src/core/server/saved_objects/service/lib/priority_collection.ts @@ -38,6 +38,10 @@ export class PriorityCollection { this.array.splice(spliceIndex, 0, { priority, value }); } + public has(predicate: (value: T) => boolean): boolean { + return this.array.some(entry => predicate(entry.value)); + } + public toPrioritizedArray(): T[] { return this.array.map(entry => entry.value); } diff --git a/src/core/server/saved_objects/service/lib/scoped_client_provider.test.js b/src/core/server/saved_objects/service/lib/scoped_client_provider.test.js index d0cc2e6d97285..bce76bc7841e6 100644 --- a/src/core/server/saved_objects/service/lib/scoped_client_provider.test.js +++ b/src/core/server/saved_objects/service/lib/scoped_client_provider.test.js @@ -64,6 +64,20 @@ test(`throws error when more than one scoped saved objects client factory is set }).toThrowErrorMatchingSnapshot(); }); +test(`throws error when registering a wrapper with a duplicate id`, () => { + const defaultClientFactoryMock = jest.fn(); + const clientProvider = new ScopedSavedObjectsClientProvider({ + defaultClientFactory: defaultClientFactoryMock, + }); + const firstClientWrapperFactoryMock = jest.fn(); + const secondClientWrapperFactoryMock = jest.fn(); + + clientProvider.addClientWrapperFactory(1, 'foo', secondClientWrapperFactoryMock); + expect(() => + clientProvider.addClientWrapperFactory(0, 'foo', firstClientWrapperFactoryMock) + ).toThrowErrorMatchingInlineSnapshot(`"wrapper factory with id foo is already defined"`); +}); + test(`invokes and uses wrappers in specified order`, () => { const defaultClient = Symbol(); const defaultClientFactoryMock = jest.fn().mockReturnValue(defaultClient); @@ -76,8 +90,8 @@ test(`invokes and uses wrappers in specified order`, () => { const secondClientWrapperFactoryMock = jest.fn().mockReturnValue(secondWrapperClient); const request = Symbol(); - clientProvider.addClientWrapperFactory(1, secondClientWrapperFactoryMock); - clientProvider.addClientWrapperFactory(0, firstClientWrapperFactoryMock); + clientProvider.addClientWrapperFactory(1, 'foo', secondClientWrapperFactoryMock); + clientProvider.addClientWrapperFactory(0, 'bar', firstClientWrapperFactoryMock); const actualClient = clientProvider.getClient(request); expect(actualClient).toBe(firstWrappedClient); @@ -90,3 +104,54 @@ test(`invokes and uses wrappers in specified order`, () => { client: defaultClient, }); }); + +test(`does not invoke or use excluded wrappers`, () => { + const defaultClient = Symbol(); + const defaultClientFactoryMock = jest.fn().mockReturnValue(defaultClient); + const clientProvider = new ScopedSavedObjectsClientProvider({ + defaultClientFactory: defaultClientFactoryMock, + }); + const firstWrappedClient = Symbol('first client'); + const firstClientWrapperFactoryMock = jest.fn().mockReturnValue(firstWrappedClient); + const secondWrapperClient = Symbol('second client'); + const secondClientWrapperFactoryMock = jest.fn().mockReturnValue(secondWrapperClient); + const request = Symbol(); + + clientProvider.addClientWrapperFactory(1, 'foo', secondClientWrapperFactoryMock); + clientProvider.addClientWrapperFactory(0, 'bar', firstClientWrapperFactoryMock); + + const actualClient = clientProvider.getClient(request, { + excludedWrappers: ['foo'], + }); + + expect(actualClient).toBe(firstWrappedClient); + expect(firstClientWrapperFactoryMock).toHaveBeenCalledWith({ + request, + client: defaultClient, + }); + expect(secondClientWrapperFactoryMock).not.toHaveBeenCalled(); +}); + +test(`allows all wrappers to be excluded`, () => { + const defaultClient = Symbol(); + const defaultClientFactoryMock = jest.fn().mockReturnValue(defaultClient); + const clientProvider = new ScopedSavedObjectsClientProvider({ + defaultClientFactory: defaultClientFactoryMock, + }); + const firstWrappedClient = Symbol('first client'); + const firstClientWrapperFactoryMock = jest.fn().mockReturnValue(firstWrappedClient); + const secondWrapperClient = Symbol('second client'); + const secondClientWrapperFactoryMock = jest.fn().mockReturnValue(secondWrapperClient); + const request = Symbol(); + + clientProvider.addClientWrapperFactory(1, 'foo', secondClientWrapperFactoryMock); + clientProvider.addClientWrapperFactory(0, 'bar', firstClientWrapperFactoryMock); + + const actualClient = clientProvider.getClient(request, { + excludedWrappers: ['foo', 'bar'], + }); + + expect(actualClient).toBe(defaultClient); + expect(firstClientWrapperFactoryMock).not.toHaveBeenCalled(); + expect(secondClientWrapperFactoryMock).not.toHaveBeenCalled(); +}); diff --git a/src/core/server/saved_objects/service/lib/scoped_client_provider.ts b/src/core/server/saved_objects/service/lib/scoped_client_provider.ts index 16924faf6cf77..fc0a3ea64c7a4 100644 --- a/src/core/server/saved_objects/service/lib/scoped_client_provider.ts +++ b/src/core/server/saved_objects/service/lib/scoped_client_provider.ts @@ -34,13 +34,18 @@ export type SavedObjectsClientFactory = ({ request: Request; }) => SavedObjectsClientContract; +export interface SavedObjectsClientProviderOptions { + excludedWrappers?: string[]; +} + /** * Provider for the Scoped Saved Object Client. */ export class ScopedSavedObjectsClientProvider { - private readonly _wrapperFactories = new PriorityCollection< - SavedObjectsClientWrapperFactory - >(); + private readonly _wrapperFactories = new PriorityCollection<{ + id: string; + factory: SavedObjectsClientWrapperFactory; + }>(); private _clientFactory: SavedObjectsClientFactory; private readonly _originalClientFactory: SavedObjectsClientFactory; @@ -54,9 +59,14 @@ export class ScopedSavedObjectsClientProvider { addClientWrapperFactory( priority: number, - wrapperFactory: SavedObjectsClientWrapperFactory + id: string, + factory: SavedObjectsClientWrapperFactory ): void { - this._wrapperFactories.add(priority, wrapperFactory); + if (this._wrapperFactories.has(entry => entry.id === id)) { + throw new Error(`wrapper factory with id ${id} is already defined`); + } + + this._wrapperFactories.add(priority, { id, factory }); } setClientFactory(customClientFactory: SavedObjectsClientFactory) { @@ -67,15 +77,24 @@ export class ScopedSavedObjectsClientProvider { this._clientFactory = customClientFactory; } - getClient(request: Request): SavedObjectsClientContract { + getClient( + request: Request, + options: SavedObjectsClientProviderOptions = {} + ): SavedObjectsClientContract { const client = this._clientFactory({ request, }); + const excludedWrappers = options.excludedWrappers || []; + return this._wrapperFactories .toPrioritizedArray() - .reduceRight((clientToWrap, wrapperFactory) => { - return wrapperFactory({ + .reduceRight((clientToWrap, { id, factory }) => { + if (excludedWrappers.includes(id)) { + return clientToWrap; + } + + return factory({ request, client: clientToWrap, }); diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.js b/src/legacy/server/saved_objects/saved_objects_mixin.js index 52845abbf08c2..29e2cb356da8e 100644 --- a/src/legacy/server/saved_objects/saved_objects_mixin.js +++ b/src/legacy/server/saved_objects/saved_objects_mixin.js @@ -148,14 +148,14 @@ export function savedObjectsMixin(kbnServer, server) { server.decorate('server', 'savedObjects', service); const savedObjectsClientCache = new WeakMap(); - server.decorate('request', 'getSavedObjectsClient', function() { + server.decorate('request', 'getSavedObjectsClient', function(options) { const request = this; if (savedObjectsClientCache.has(request)) { return savedObjectsClientCache.get(request); } - const savedObjectsClient = server.savedObjects.getScopedSavedObjectsClient(request); + const savedObjectsClient = server.savedObjects.getScopedSavedObjectsClient(request, options); savedObjectsClientCache.set(request, savedObjectsClient); return savedObjectsClient; diff --git a/x-pack/legacy/plugins/encrypted_saved_objects/server/plugin.ts b/x-pack/legacy/plugins/encrypted_saved_objects/server/plugin.ts index 02b20798afc59..0117e0a878848 100644 --- a/x-pack/legacy/plugins/encrypted_saved_objects/server/plugin.ts +++ b/x-pack/legacy/plugins/encrypted_saved_objects/server/plugin.ts @@ -60,6 +60,7 @@ export class Plugin { // `namespace` is included into AAD. core.savedObjects.addScopedSavedObjectsClientWrapperFactory( Number.MAX_SAFE_INTEGER, + 'encrypted_saved_objects', ({ client: baseClient }) => new EncryptedSavedObjectsClientWrapper({ baseClient, service }) ); diff --git a/x-pack/legacy/plugins/security/index.js b/x-pack/legacy/plugins/security/index.js index 196cedfa8dc36..77025ad80b89b 100644 --- a/x-pack/legacy/plugins/security/index.js +++ b/x-pack/legacy/plugins/security/index.js @@ -187,7 +187,7 @@ export const security = (kibana) => new kibana.Plugin({ return new savedObjects.SavedObjectsClient(callWithRequestRepository); }); - savedObjects.addScopedSavedObjectsClientWrapperFactory(Number.MIN_SAFE_INTEGER, ({ client, request }) => { + savedObjects.addScopedSavedObjectsClientWrapperFactory(Number.MIN_SAFE_INTEGER, 'security', ({ client, request }) => { if (authorization.mode.useRbacForRequest(request)) { return new SecureSavedObjectsClientWrapper({ actions: authorization.actions, diff --git a/x-pack/legacy/plugins/spaces/server/new_platform/plugin.ts b/x-pack/legacy/plugins/spaces/server/new_platform/plugin.ts index 5689804c125bd..6d6b8c83470be 100644 --- a/x-pack/legacy/plugins/spaces/server/new_platform/plugin.ts +++ b/x-pack/legacy/plugins/spaces/server/new_platform/plugin.ts @@ -120,6 +120,7 @@ export class Plugin { const { addScopedSavedObjectsClientWrapperFactory, types } = core.savedObjects; addScopedSavedObjectsClientWrapperFactory( Number.MAX_SAFE_INTEGER - 1, + 'spaces', spacesSavedObjectsClientWrapperFactory(spacesService, types) );