Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ export class PriorityCollection<T> {
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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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();
});
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,18 @@ export type SavedObjectsClientFactory<Request = unknown> = ({
request: Request;
}) => SavedObjectsClientContract;

export interface SavedObjectsClientProviderOptions {
excludedWrappers?: string[];
}

/**
* Provider for the Scoped Saved Object Client.
*/
export class ScopedSavedObjectsClientProvider<Request = unknown> {
private readonly _wrapperFactories = new PriorityCollection<
SavedObjectsClientWrapperFactory<Request>
>();
private readonly _wrapperFactories = new PriorityCollection<{
id: string;
factory: SavedObjectsClientWrapperFactory<Request>;
}>();
private _clientFactory: SavedObjectsClientFactory<Request>;
private readonly _originalClientFactory: SavedObjectsClientFactory<Request>;

Expand All @@ -54,9 +59,14 @@ export class ScopedSavedObjectsClientProvider<Request = unknown> {

addClientWrapperFactory(
priority: number,
wrapperFactory: SavedObjectsClientWrapperFactory<Request>
id: string,
factory: SavedObjectsClientWrapperFactory<Request>
): 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) {
Expand All @@ -67,15 +77,24 @@ export class ScopedSavedObjectsClientProvider<Request = unknown> {
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,
});
Expand Down
4 changes: 2 additions & 2 deletions src/legacy/server/saved_objects/saved_objects_mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
);

Expand Down
2 changes: 1 addition & 1 deletion x-pack/legacy/plugins/security/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,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,
Expand Down
1 change: 1 addition & 0 deletions x-pack/legacy/plugins/spaces/server/new_platform/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export class Plugin {
const { addScopedSavedObjectsClientWrapperFactory, types } = core.savedObjects;
addScopedSavedObjectsClientWrapperFactory(
Number.MAX_SAFE_INTEGER - 1,
'spaces',
spacesSavedObjectsClientWrapperFactory(spacesService, types)
);

Expand Down