From 01b5cf0a88a0ddd905a8802326b521916c85c452 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Mon, 26 Mar 2018 10:40:36 -0400 Subject: [PATCH 01/75] partial implementation for OLS Phase 1 --- .../client/lib/client_provider.js | 41 ++++++++++++++ src/server/saved_objects/client/lib/index.js | 2 + .../client/lib/interceptor_registry.js | 37 +++++++++++++ .../client/saved_objects_client.js | 53 +++++++++++++++++-- .../saved_objects/saved_objects_mixin.js | 9 ++-- 5 files changed, 133 insertions(+), 9 deletions(-) create mode 100644 src/server/saved_objects/client/lib/client_provider.js create mode 100644 src/server/saved_objects/client/lib/interceptor_registry.js diff --git a/src/server/saved_objects/client/lib/client_provider.js b/src/server/saved_objects/client/lib/client_provider.js new file mode 100644 index 0000000000000..2f26731b91b9c --- /dev/null +++ b/src/server/saved_objects/client/lib/client_provider.js @@ -0,0 +1,41 @@ + +/** + * The default Saved Object Client provider. + * A custom implementation may be substituted by calling `SavedObjectClientProvider.setClientProvider` + * + * @param {*} server + * @param {*} request + */ +const DEFAULT_PROVIDER = function savedObjectsClientProvider(server, request) { + + const cluster = server.plugins.elasticsearch.getCluster('admin'); + + const callCluster = (...args) => cluster.callWithRequest(request, ...args); + + return server.savedObjectsClientFactory({ callCluster, request }); +}; + +let activeProvider = DEFAULT_PROVIDER; + +/** + * Provider for the Saved Object Client. + */ +class ClientProvider { + constructor() { + this._activeProvider = DEFAULT_PROVIDER; + } + + setClientProvider(provider) { + if (activeProvider !== DEFAULT_PROVIDER) { + throw new Error(`A Saved Objects Client Provider has already been registered. Registering multiple providers is not supported.`); + } + + activeProvider = provider; + } + + getClientProvider() { + return activeProvider; + } +} + +export const SavedObjectClientProvider = new ClientProvider(); diff --git a/src/server/saved_objects/client/lib/index.js b/src/server/saved_objects/client/lib/index.js index 3e90b4820f9c7..724046af2713a 100644 --- a/src/server/saved_objects/client/lib/index.js +++ b/src/server/saved_objects/client/lib/index.js @@ -2,6 +2,8 @@ export { getSearchDsl } from './search_dsl'; export { trimIdPrefix } from './trim_id_prefix'; export { includedFields } from './included_fields'; export { decorateEsError } from './decorate_es_error'; +export { SavedObjectClientProvider } from './client_provider'; +export { SavedObjectClientInterceptorRegistry } from './interceptor_registry'; import * as errors from './errors'; export { errors }; diff --git a/src/server/saved_objects/client/lib/interceptor_registry.js b/src/server/saved_objects/client/lib/interceptor_registry.js new file mode 100644 index 0000000000000..ac6205eb9d1ad --- /dev/null +++ b/src/server/saved_objects/client/lib/interceptor_registry.js @@ -0,0 +1,37 @@ +/** + * Registry for Saved Objects Client Request Interceptors. + * + * Interceptors will be invoked by the Saved Object Client prior to calling the ElasticSearch cluster, + * which allows interceptors to modify or interrupt the process. + */ +class SavedObjectClientRequestInterceptorRegistry { + constructor() { + this._interceptorFactories = []; + } + + registerInterceptorFactory(interceptorFactory) { + if (typeof interceptorFactory !== 'function') { + throw new Error(`Invalid interceptor factory - must be a function`); + } + + this._interceptorFactories.push(interceptorFactory); + } + + createInterceptorsForRequest(request) { + const interceptors = this._interceptorFactories.map(factory => factory(request)); + + const anyInvalid = interceptors.some( + interceptor => typeof interceptor.method !== 'string' || typeof interceptor.intercept !== 'function' + ); + + if (anyInvalid) { + throw new Error( + `One or more interceptors are invalid: each interceptor must include a 'method' property, and an 'intercept' function` + ); + } + + return interceptors; + } +} + +export const SavedObjectClientInterceptorRegistry = new SavedObjectClientRequestInterceptorRegistry(); diff --git a/src/server/saved_objects/client/saved_objects_client.js b/src/server/saved_objects/client/saved_objects_client.js index 79dc55540e039..b6049dcebd0a5 100644 --- a/src/server/saved_objects/client/saved_objects_client.js +++ b/src/server/saved_objects/client/saved_objects_client.js @@ -17,12 +17,14 @@ export class SavedObjectsClient { mappings, callCluster, onBeforeWrite = () => {}, + interceptors = [] } = options; this._index = index; this._mappings = mappings; this._type = getRootType(this._mappings); this._onBeforeWrite = onBeforeWrite; + this._interceptors = interceptors; this._unwrappedCallCluster = callCluster; } @@ -114,6 +116,8 @@ export class SavedObjectsClient { const time = this._getCurrentTime(); try { + await this._invokeRequestInterceptors(method, type, attributes, options); + const response = await this._writeToCluster(method, { id: this._generateEsId(type, id), type: this._type, @@ -156,9 +160,12 @@ export class SavedObjectsClient { overwrite = false } = options; const time = this._getCurrentTime(); - const objectToBulkRequest = (object) => { + + const objectToBulkRequest = async (object) => { const method = object.id && !overwrite ? 'create' : 'index'; + await this._invokeRequestInterceptors(method, object.type, object.attributes, options); + return [ { [method]: { @@ -174,13 +181,18 @@ export class SavedObjectsClient { ]; }; + const bulkRequestBody = await objects.reduce(async (acc, object) => { + const collection = await acc; + + const objectRequestBody = await objectToBulkRequest(object); + + return [...collection, ...objectRequestBody]; + }, Promise.resolve([])); + const { items } = await this._writeToCluster('bulk', { index: this._index, refresh: 'wait_for', - body: objects.reduce((acc, object) => ([ - ...acc, - ...objectToBulkRequest(object) - ]), []), + body: bulkRequestBody }); return items.map((response, i) => { @@ -224,6 +236,8 @@ export class SavedObjectsClient { * @returns {promise} */ async delete(type, id) { + await this._invokeRequestInterceptors('delete', type, id); + const response = await this._writeToCluster('delete', { id: this._generateEsId(type, id), type: this._type, @@ -282,6 +296,10 @@ export class SavedObjectsClient { throw new TypeError('options.searchFields must be an array'); } + const method = 'search'; + + await this._invokeRequestInterceptors(method, type); + const esOptions = { index: this._index, size: perPage, @@ -347,6 +365,10 @@ export class SavedObjectsClient { return { saved_objects: [] }; } + const method = 'mget'; + + await this._invokeRequestInterceptors(method, null); + const response = await this._callCluster('mget', { index: this._index, body: { @@ -389,6 +411,10 @@ export class SavedObjectsClient { * @returns {promise} - { id, type, version, attributes } */ async get(type, id) { + const method = 'get'; + + await this._invokeRequestInterceptors(method, type, id); + const response = await this._callCluster('get', { id: this._generateEsId(type, id), type: this._type, @@ -424,6 +450,8 @@ export class SavedObjectsClient { * @returns {promise} */ async update(type, id, attributes, options = {}) { + await this._invokeRequestInterceptors('update', type, attributes, options); + const time = this._getCurrentTime(); const response = await this._writeToCluster('update', { id: this._generateEsId(type, id), @@ -478,4 +506,19 @@ export class SavedObjectsClient { _getCurrentTime() { return new Date().toISOString(); } + + _collectRequestInterceptors(method) { + return this._interceptors.filter(interceptor => interceptor.method === method || interceptor.method === 'all'); + } + + async _invokeRequestInterceptors(method, type, ...args) { + const interceptors = this._collectRequestInterceptors(method); + + for (const interceptor of interceptors) { + if (typeof interceptor.intercept !== 'function') { + throw new Error(`Request interceptor missing 'intercept' function`); + } + await interceptor.intercept(this, method, type, ...args); + } + } } diff --git a/src/server/saved_objects/saved_objects_mixin.js b/src/server/saved_objects/saved_objects_mixin.js index 9a8ceb97704ee..826f87c511f01 100644 --- a/src/server/saved_objects/saved_objects_mixin.js +++ b/src/server/saved_objects/saved_objects_mixin.js @@ -1,4 +1,5 @@ import { SavedObjectsClient } from './client'; +import { SavedObjectClientInterceptorRegistry, SavedObjectClientProvider } from './client/lib'; import { createBulkGetRoute, @@ -62,12 +63,13 @@ export function savedObjectsMixin(kbnServer, server) { } } - server.decorate('server', 'savedObjectsClientFactory', ({ callCluster }) => { + server.decorate('server', 'savedObjectsClientFactory', ({ callCluster, request }) => { return new SavedObjectsClient({ index: server.config().get('kibana.index'), mappings: server.getKibanaIndexMappingsDsl(), callCluster, onBeforeWrite, + interceptors: request ? SavedObjectClientInterceptorRegistry.createInterceptorsForRequest(request) : [] }); }); @@ -79,9 +81,8 @@ export function savedObjectsMixin(kbnServer, server) { return savedObjectsClientCache.get(request); } - const { callWithRequest } = server.plugins.elasticsearch.getCluster('admin'); - const callCluster = (...args) => callWithRequest(request, ...args); - const savedObjectsClient = server.savedObjectsClientFactory({ callCluster }); + const clientProvider = SavedObjectClientProvider.getClientProvider(); + const savedObjectsClient = clientProvider(server, request); savedObjectsClientCache.set(request, savedObjectsClient); return savedObjectsClient; From 7cee640d3b2191ccac507a11e1e30770832745e6 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Tue, 17 Apr 2018 08:19:47 -0400 Subject: [PATCH 02/75] Allow Saved Objects Client to be wrapped --- .../client/lib/client_provider.js | 56 +++++++++++------- src/server/saved_objects/client/lib/index.js | 3 +- .../client/lib/interceptor_registry.js | 37 ------------ .../client/lib/prioritized_collection.js | 40 +++++++++++++ .../client/lib/prioritized_collection.test.js | 57 +++++++++++++++++++ .../client/saved_objects_client.js | 34 ----------- .../saved_objects/saved_objects_mixin.js | 17 +++--- 7 files changed, 142 insertions(+), 102 deletions(-) delete mode 100644 src/server/saved_objects/client/lib/interceptor_registry.js create mode 100644 src/server/saved_objects/client/lib/prioritized_collection.js create mode 100644 src/server/saved_objects/client/lib/prioritized_collection.test.js diff --git a/src/server/saved_objects/client/lib/client_provider.js b/src/server/saved_objects/client/lib/client_provider.js index 2f26731b91b9c..e561274b963a1 100644 --- a/src/server/saved_objects/client/lib/client_provider.js +++ b/src/server/saved_objects/client/lib/client_provider.js @@ -1,41 +1,55 @@ +import { SavedObjectsClient } from '../saved_objects_client'; +import { PrioritizedCollection } from './prioritized_collection'; /** - * The default Saved Object Client provider. - * A custom implementation may be substituted by calling `SavedObjectClientProvider.setClientProvider` + * The base Saved Objects Client. * * @param {*} server * @param {*} request */ -const DEFAULT_PROVIDER = function savedObjectsClientProvider(server, request) { - - const cluster = server.plugins.elasticsearch.getCluster('admin'); - - const callCluster = (...args) => cluster.callWithRequest(request, ...args); - - return server.savedObjectsClientFactory({ callCluster, request }); -}; - -let activeProvider = DEFAULT_PROVIDER; +function createBaseSavedObjectsClient(options) { + + const { + server, + mappings, + callCluster, + onBeforeWrite, + } = options; + + return new SavedObjectsClient({ + index: server.config().get('kibana.index'), + mappings, + callCluster, + onBeforeWrite + }); +} /** * Provider for the Saved Object Client. */ class ClientProvider { constructor() { - this._activeProvider = DEFAULT_PROVIDER; + this._optionBuilders = new PrioritizedCollection('optionBuilders'); + this._wrappers = new PrioritizedCollection('savedObjectClientWrappers'); } - setClientProvider(provider) { - if (activeProvider !== DEFAULT_PROVIDER) { - throw new Error(`A Saved Objects Client Provider has already been registered. Registering multiple providers is not supported.`); - } + addClientOptionBuilder(builder, priority) { + this._optionBuilders.add(builder, priority); + } - activeProvider = provider; + addClientWrapper(wrapper, priority) { + this._wrappers.add(wrapper, priority); } - getClientProvider() { - return activeProvider; + createWrappedSavedObjectsClient(options) { + const orderedBuilders = this._optionBuilders.toArray(); + const clientOptions = orderedBuilders.reduce((acc, builder) => builder(acc), options); + + const baseClient = createBaseSavedObjectsClient(clientOptions); + + const orderedWrappers = this._wrappers.toArray(); + return orderedWrappers.reduce((client, wrapper) => wrapper(client, clientOptions), baseClient); } } -export const SavedObjectClientProvider = new ClientProvider(); +export const SavedObjectsClientProvider = new ClientProvider(); diff --git a/src/server/saved_objects/client/lib/index.js b/src/server/saved_objects/client/lib/index.js index 724046af2713a..ac10009b59118 100644 --- a/src/server/saved_objects/client/lib/index.js +++ b/src/server/saved_objects/client/lib/index.js @@ -2,8 +2,7 @@ export { getSearchDsl } from './search_dsl'; export { trimIdPrefix } from './trim_id_prefix'; export { includedFields } from './included_fields'; export { decorateEsError } from './decorate_es_error'; -export { SavedObjectClientProvider } from './client_provider'; -export { SavedObjectClientInterceptorRegistry } from './interceptor_registry'; +export { SavedObjectsClientProvider } from './client_provider'; import * as errors from './errors'; export { errors }; diff --git a/src/server/saved_objects/client/lib/interceptor_registry.js b/src/server/saved_objects/client/lib/interceptor_registry.js deleted file mode 100644 index ac6205eb9d1ad..0000000000000 --- a/src/server/saved_objects/client/lib/interceptor_registry.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Registry for Saved Objects Client Request Interceptors. - * - * Interceptors will be invoked by the Saved Object Client prior to calling the ElasticSearch cluster, - * which allows interceptors to modify or interrupt the process. - */ -class SavedObjectClientRequestInterceptorRegistry { - constructor() { - this._interceptorFactories = []; - } - - registerInterceptorFactory(interceptorFactory) { - if (typeof interceptorFactory !== 'function') { - throw new Error(`Invalid interceptor factory - must be a function`); - } - - this._interceptorFactories.push(interceptorFactory); - } - - createInterceptorsForRequest(request) { - const interceptors = this._interceptorFactories.map(factory => factory(request)); - - const anyInvalid = interceptors.some( - interceptor => typeof interceptor.method !== 'string' || typeof interceptor.intercept !== 'function' - ); - - if (anyInvalid) { - throw new Error( - `One or more interceptors are invalid: each interceptor must include a 'method' property, and an 'intercept' function` - ); - } - - return interceptors; - } -} - -export const SavedObjectClientInterceptorRegistry = new SavedObjectClientRequestInterceptorRegistry(); diff --git a/src/server/saved_objects/client/lib/prioritized_collection.js b/src/server/saved_objects/client/lib/prioritized_collection.js new file mode 100644 index 0000000000000..e958ac678a120 --- /dev/null +++ b/src/server/saved_objects/client/lib/prioritized_collection.js @@ -0,0 +1,40 @@ +/** + * A simple collection of entities that can be prioritized. + */ +export class PrioritizedCollection { + constructor(name) { + this._name = name; + this._entities = {}; + } + + /** + * Add an entity to this collection. + * + * @param {*} entity the entity to store + * @param {number} priority optionally specify a priority. Omit to use the next available priority. + */ + add(entity, priority = this._getNextPriority()) { + if (this._entities.hasOwnProperty(priority)) { + throw new Error(`${this._name} already has an entry with priority ${priority}. Please choose a different priority.`); + } + if (typeof priority !== 'number') { + throw new Error(`Priority for ${this._name} must be a number.`); + } + + this._entities[priority] = entity; + } + + /** + * Returns an array of all entities, in priority order. + */ + toArray() { + return Object + .keys(this._entities) + .sort((priority1, priority2) => priority1 - priority2) + .map(key => this._entities[key]); + } + + _getNextPriority() { + return Math.max(0, ...Object.keys(this._entities)) + 1; + } +} diff --git a/src/server/saved_objects/client/lib/prioritized_collection.test.js b/src/server/saved_objects/client/lib/prioritized_collection.test.js new file mode 100644 index 0000000000000..56583403f34c2 --- /dev/null +++ b/src/server/saved_objects/client/lib/prioritized_collection.test.js @@ -0,0 +1,57 @@ +import { PrioritizedCollection } from './prioritized_collection'; + +describe('PrioritizedCollection', () => { + it('should add a single entry with a default priority', () => { + const collection = new PrioritizedCollection('unit test'); + const entity = {}; + collection.add(entity); + + const result = collection.toArray(); + expect(result).to.equal([entity]); + }); + + it('should prioritize entities in the order in which they are added', () => { + const collection = new PrioritizedCollection('unit test'); + const entity1 = {}; + const entity2 = {}; + + collection.add(entity1); + collection.add(entity2); + + const result = collection.toArray(); + expect(result).to.equal([entity1, entity2]); + }); + + it('should honor the provided priority', () => { + const collection = new PrioritizedCollection('unit test'); + const entity1 = {}; + const entity2 = {}; + + collection.add(entity1, 10); + collection.add(entity2, 1); + + const result = collection.toArray(); + expect(result).to.equal([entity2, entity1]); + }); + + it('should throw an error when a duplicate priority is specified', () => { + const collection = new PrioritizedCollection('unit test'); + const entity1 = {}; + const entity2 = {}; + + collection.add(entity1, 10); + + expect(() => { + collection.add(entity2, 10); + }).to.throwError(`unit test already has an entry with priority 10. Please choose a different priority.`); + }); + + it('should throw an error when an invalid priority is specified', () => { + const collection = new PrioritizedCollection('unit test'); + const entity1 = {}; + + expect(() => { + collection.add(entity1, 'not a number'); + }).to.throwError(`Priority for unit test must be a number.`); + }); +}); diff --git a/src/server/saved_objects/client/saved_objects_client.js b/src/server/saved_objects/client/saved_objects_client.js index b6049dcebd0a5..092dd0e4b45b2 100644 --- a/src/server/saved_objects/client/saved_objects_client.js +++ b/src/server/saved_objects/client/saved_objects_client.js @@ -17,14 +17,12 @@ export class SavedObjectsClient { mappings, callCluster, onBeforeWrite = () => {}, - interceptors = [] } = options; this._index = index; this._mappings = mappings; this._type = getRootType(this._mappings); this._onBeforeWrite = onBeforeWrite; - this._interceptors = interceptors; this._unwrappedCallCluster = callCluster; } @@ -116,8 +114,6 @@ export class SavedObjectsClient { const time = this._getCurrentTime(); try { - await this._invokeRequestInterceptors(method, type, attributes, options); - const response = await this._writeToCluster(method, { id: this._generateEsId(type, id), type: this._type, @@ -164,8 +160,6 @@ export class SavedObjectsClient { const objectToBulkRequest = async (object) => { const method = object.id && !overwrite ? 'create' : 'index'; - await this._invokeRequestInterceptors(method, object.type, object.attributes, options); - return [ { [method]: { @@ -236,7 +230,6 @@ export class SavedObjectsClient { * @returns {promise} */ async delete(type, id) { - await this._invokeRequestInterceptors('delete', type, id); const response = await this._writeToCluster('delete', { id: this._generateEsId(type, id), @@ -296,10 +289,6 @@ export class SavedObjectsClient { throw new TypeError('options.searchFields must be an array'); } - const method = 'search'; - - await this._invokeRequestInterceptors(method, type); - const esOptions = { index: this._index, size: perPage, @@ -365,10 +354,6 @@ export class SavedObjectsClient { return { saved_objects: [] }; } - const method = 'mget'; - - await this._invokeRequestInterceptors(method, null); - const response = await this._callCluster('mget', { index: this._index, body: { @@ -411,9 +396,6 @@ export class SavedObjectsClient { * @returns {promise} - { id, type, version, attributes } */ async get(type, id) { - const method = 'get'; - - await this._invokeRequestInterceptors(method, type, id); const response = await this._callCluster('get', { id: this._generateEsId(type, id), @@ -450,7 +432,6 @@ export class SavedObjectsClient { * @returns {promise} */ async update(type, id, attributes, options = {}) { - await this._invokeRequestInterceptors('update', type, attributes, options); const time = this._getCurrentTime(); const response = await this._writeToCluster('update', { @@ -506,19 +487,4 @@ export class SavedObjectsClient { _getCurrentTime() { return new Date().toISOString(); } - - _collectRequestInterceptors(method) { - return this._interceptors.filter(interceptor => interceptor.method === method || interceptor.method === 'all'); - } - - async _invokeRequestInterceptors(method, type, ...args) { - const interceptors = this._collectRequestInterceptors(method); - - for (const interceptor of interceptors) { - if (typeof interceptor.intercept !== 'function') { - throw new Error(`Request interceptor missing 'intercept' function`); - } - await interceptor.intercept(this, method, type, ...args); - } - } } diff --git a/src/server/saved_objects/saved_objects_mixin.js b/src/server/saved_objects/saved_objects_mixin.js index 826f87c511f01..4169dedc77aab 100644 --- a/src/server/saved_objects/saved_objects_mixin.js +++ b/src/server/saved_objects/saved_objects_mixin.js @@ -1,5 +1,4 @@ -import { SavedObjectsClient } from './client'; -import { SavedObjectClientInterceptorRegistry, SavedObjectClientProvider } from './client/lib'; +import { SavedObjectsClientProvider } from './client/lib'; import { createBulkGetRoute, @@ -64,12 +63,12 @@ export function savedObjectsMixin(kbnServer, server) { } server.decorate('server', 'savedObjectsClientFactory', ({ callCluster, request }) => { - return new SavedObjectsClient({ - index: server.config().get('kibana.index'), + return SavedObjectsClientProvider.createWrappedSavedObjectsClient({ + server, + request, mappings: server.getKibanaIndexMappingsDsl(), callCluster, - onBeforeWrite, - interceptors: request ? SavedObjectClientInterceptorRegistry.createInterceptorsForRequest(request) : [] + onBeforeWrite }); }); @@ -81,8 +80,10 @@ export function savedObjectsMixin(kbnServer, server) { return savedObjectsClientCache.get(request); } - const clientProvider = SavedObjectClientProvider.getClientProvider(); - const savedObjectsClient = clientProvider(server, request); + const { callWithRequest } = server.plugins.elasticsearch.getCluster('admin'); + const callCluster = (...args) => callWithRequest(request, ...args); + + const savedObjectsClient = server.savedObjectsClientFactory({ callCluster, request }); savedObjectsClientCache.set(request, savedObjectsClient); return savedObjectsClient; From 0afd1c1c0093c4a5521c9ecc7efd1f6caeb09619 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Tue, 17 Apr 2018 17:02:02 -0400 Subject: [PATCH 03/75] Add placeholder "kibana.namespace" configuration property --- src/core_plugins/kibana/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core_plugins/kibana/index.js b/src/core_plugins/kibana/index.js index 189009f037ea9..afabf0c0e8b8c 100644 --- a/src/core_plugins/kibana/index.js +++ b/src/core_plugins/kibana/index.js @@ -30,7 +30,8 @@ export default function (kibana) { return Joi.object({ enabled: Joi.boolean().default(true), defaultAppId: Joi.string().default('home'), - index: Joi.string().default('.kibana') + index: Joi.string().default('.kibana'), + namespace: Joi.string().default('kibana') }).default(); }, From 30e86d12684e648fa0f94721e65a34b44b639a66 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Wed, 18 Apr 2018 14:12:01 -0400 Subject: [PATCH 04/75] revert changes to saved objects client --- .../client/saved_objects_client.js | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/src/server/saved_objects/client/saved_objects_client.js b/src/server/saved_objects/client/saved_objects_client.js index 092dd0e4b45b2..79dc55540e039 100644 --- a/src/server/saved_objects/client/saved_objects_client.js +++ b/src/server/saved_objects/client/saved_objects_client.js @@ -156,8 +156,7 @@ export class SavedObjectsClient { overwrite = false } = options; const time = this._getCurrentTime(); - - const objectToBulkRequest = async (object) => { + const objectToBulkRequest = (object) => { const method = object.id && !overwrite ? 'create' : 'index'; return [ @@ -175,18 +174,13 @@ export class SavedObjectsClient { ]; }; - const bulkRequestBody = await objects.reduce(async (acc, object) => { - const collection = await acc; - - const objectRequestBody = await objectToBulkRequest(object); - - return [...collection, ...objectRequestBody]; - }, Promise.resolve([])); - const { items } = await this._writeToCluster('bulk', { index: this._index, refresh: 'wait_for', - body: bulkRequestBody + body: objects.reduce((acc, object) => ([ + ...acc, + ...objectToBulkRequest(object) + ]), []), }); return items.map((response, i) => { @@ -230,7 +224,6 @@ export class SavedObjectsClient { * @returns {promise} */ async delete(type, id) { - const response = await this._writeToCluster('delete', { id: this._generateEsId(type, id), type: this._type, @@ -396,7 +389,6 @@ export class SavedObjectsClient { * @returns {promise} - { id, type, version, attributes } */ async get(type, id) { - const response = await this._callCluster('get', { id: this._generateEsId(type, id), type: this._type, @@ -432,7 +424,6 @@ export class SavedObjectsClient { * @returns {promise} */ async update(type, id, attributes, options = {}) { - const time = this._getCurrentTime(); const response = await this._writeToCluster('update', { id: this._generateEsId(type, id), From 936180b82c6d9fdd3625f583e564b0f6bc2ef682 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 19 Apr 2018 13:15:38 -0400 Subject: [PATCH 05/75] Remove circular dependency --- .../client/lib/client_provider.js | 28 ++----------------- .../saved_objects/saved_objects_mixin.js | 20 +++++++++++-- 2 files changed, 20 insertions(+), 28 deletions(-) diff --git a/src/server/saved_objects/client/lib/client_provider.js b/src/server/saved_objects/client/lib/client_provider.js index e561274b963a1..a0e510a75fb75 100644 --- a/src/server/saved_objects/client/lib/client_provider.js +++ b/src/server/saved_objects/client/lib/client_provider.js @@ -1,29 +1,5 @@ -import { SavedObjectsClient } from '../saved_objects_client'; import { PrioritizedCollection } from './prioritized_collection'; -/** - * The base Saved Objects Client. - * - * @param {*} server - * @param {*} request - */ -function createBaseSavedObjectsClient(options) { - - const { - server, - mappings, - callCluster, - onBeforeWrite, - } = options; - - return new SavedObjectsClient({ - index: server.config().get('kibana.index'), - mappings, - callCluster, - onBeforeWrite - }); -} - /** * Provider for the Saved Object Client. */ @@ -41,11 +17,11 @@ class ClientProvider { this._wrappers.add(wrapper, priority); } - createWrappedSavedObjectsClient(options) { + createSavedObjectsClient(baseClientFactory, options) { const orderedBuilders = this._optionBuilders.toArray(); const clientOptions = orderedBuilders.reduce((acc, builder) => builder(acc), options); - const baseClient = createBaseSavedObjectsClient(clientOptions); + const baseClient = baseClientFactory(clientOptions); const orderedWrappers = this._wrappers.toArray(); return orderedWrappers.reduce((client, wrapper) => wrapper(client, clientOptions), baseClient); diff --git a/src/server/saved_objects/saved_objects_mixin.js b/src/server/saved_objects/saved_objects_mixin.js index 4169dedc77aab..d5d7b2c4c31cf 100644 --- a/src/server/saved_objects/saved_objects_mixin.js +++ b/src/server/saved_objects/saved_objects_mixin.js @@ -1,5 +1,5 @@ import { SavedObjectsClientProvider } from './client/lib'; - +import { SavedObjectsClient } from './client'; import { createBulkGetRoute, createCreateRoute, @@ -63,7 +63,23 @@ export function savedObjectsMixin(kbnServer, server) { } server.decorate('server', 'savedObjectsClientFactory', ({ callCluster, request }) => { - return SavedObjectsClientProvider.createWrappedSavedObjectsClient({ + const createBaseClient = (options) => { + const { + server, + mappings, + callCluster, + onBeforeWrite, + } = options; + + return new SavedObjectsClient({ + index: server.config().get('kibana.index'), + mappings, + callCluster, + onBeforeWrite + }); + }; + + return SavedObjectsClientProvider.createSavedObjectsClient(createBaseClient, { server, request, mappings: server.getKibanaIndexMappingsDsl(), From 4d4f9462f896f2394531d071fcb293c9164edf01 Mon Sep 17 00:00:00 2001 From: kobelb Date: Tue, 24 Apr 2018 10:12:25 -0400 Subject: [PATCH 06/75] Removing namespace setting, we're using xpack.security.rbac.application --- src/core_plugins/kibana/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core_plugins/kibana/index.js b/src/core_plugins/kibana/index.js index afabf0c0e8b8c..a8e759424a842 100644 --- a/src/core_plugins/kibana/index.js +++ b/src/core_plugins/kibana/index.js @@ -31,7 +31,6 @@ export default function (kibana) { enabled: Joi.boolean().default(true), defaultAppId: Joi.string().default('home'), index: Joi.string().default('.kibana'), - namespace: Joi.string().default('kibana') }).default(); }, From df569df451728867264bf248046367b1711373f4 Mon Sep 17 00:00:00 2001 From: kobelb Date: Tue, 24 Apr 2018 12:21:25 -0400 Subject: [PATCH 07/75] Adding config.getDefault --- src/server/config/config.js | 15 +++++++++++++++ src/server/config/config.test.js | 25 +++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/server/config/config.js b/src/server/config/config.js index 61276a645f7de..e415ef04e39bb 100644 --- a/src/server/config/config.js +++ b/src/server/config/config.js @@ -123,11 +123,24 @@ export class Config { return clone(value); } + getDefault(key) { + const schemaDescription = Joi.describe(this.getSchema()); + const parts = key.split('.'); + const path = `children.${parts.join('.children.')}`; + const description = _.get(schemaDescription, path); + if (!description) { + throw new Error('Unknown config key: ' + key); + } + + return _.get(description, 'flags.default'); + } + has(key) { function has(key, schema, path) { path = path || []; // Catch the partial paths if (path.join('.') === key) return true; + // Only go deep on inner objects with children if (_.size(schema._inner.children)) { for (let i = 0; i < schema._inner.children.length; i++) { @@ -174,4 +187,6 @@ export class Config { return this[schema]; } + + } diff --git a/src/server/config/config.test.js b/src/server/config/config.test.js index ac45557e22cb5..14b7ffd2226d9 100644 --- a/src/server/config/config.test.js +++ b/src/server/config/config.test.js @@ -213,6 +213,31 @@ describe('lib/config/config', function () { }); + describe('#getDefault(key)', function () { + let config; + + beforeEach(function () { + config = new Config(schema); + config.set(data); + }); + + it('should return undefined if there is no default', function () { + const hostDefault = config.getDefault('test.client.host'); + expect(hostDefault).toBeUndefined(); + }); + + it('should return default if specified', function () { + const typeDefault = config.getDefault('test.client.type'); + expect(typeDefault).toBe('datastore'); + }); + + it('should throw exception for unknown key', function () { + expect(() => { + config.getDefault('foo.bar'); + }).toThrow(); + }); + }); + describe('#extendSchema(key, schema)', function () { let config; beforeEach(function () { From 9979fb9c2654c3aef289ed5c9e895a7cf213c9fd Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Wed, 25 Apr 2018 09:25:36 -0400 Subject: [PATCH 08/75] Expose SavedObjectsClientProvider on the server for easy plugin consumption --- src/server/saved_objects/saved_objects_mixin.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/server/saved_objects/saved_objects_mixin.js b/src/server/saved_objects/saved_objects_mixin.js index d5d7b2c4c31cf..2cf83946de63b 100644 --- a/src/server/saved_objects/saved_objects_mixin.js +++ b/src/server/saved_objects/saved_objects_mixin.js @@ -62,6 +62,8 @@ export function savedObjectsMixin(kbnServer, server) { } } + server.decorate('server', 'getSavedObjectsClientProvider', () => SavedObjectsClientProvider); + server.decorate('server', 'savedObjectsClientFactory', ({ callCluster, request }) => { const createBaseClient = (options) => { const { From 646a80a1f1bdaa0b005300d76113f90799d7263c Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Wed, 25 Apr 2018 10:46:40 -0400 Subject: [PATCH 09/75] migrate x-pack changes into kibana --- x-pack/plugins/security/index.js | 37 ++++- .../public/services/application_privilege.js | 19 +++ .../public/services/shield_privileges.js | 3 +- .../public/views/management/edit_role.html | 24 +++ .../public/views/management/edit_role.js | 28 +++- .../server/lib/__tests__/validate_config.js | 19 ++- .../lib/authorization/create_default_roles.js | 47 ++++++ .../server/lib/check_user_permission.js | 14 ++ .../lib/mirror_status_and_initialize.js | 63 ++++++++ .../lib/mirror_status_and_initialize.test.js | 138 ++++++++++++++++++ .../privileges/privilege_action_registry.js | 32 ++++ .../server/lib/privileges/privileges.js | 56 +++++++ .../security/server/lib/role_schema.js | 3 +- .../lib/saved_object_client_interceptor.js | 42 ++++++ .../saved_objects_client_wrapper.js | 17 +++ .../secure_options_builder.js | 19 +++ .../secure_saved_objects_client.js | 96 ++++++++++++ .../security/server/lib/validate_config.js | 11 +- .../server/routes/api/v1/privileges.js | 28 ++++ .../v1/roles/contains_other_applications.js | 15 ++ .../roles/contains_other_applications.test.js | 66 +++++++++ .../api/v1/{roles.js => roles/index.js} | 24 ++- x-pack/server/lib/esjs_shield_plugin.js | 31 ++++ 23 files changed, 814 insertions(+), 18 deletions(-) create mode 100644 x-pack/plugins/security/public/services/application_privilege.js create mode 100644 x-pack/plugins/security/server/lib/authorization/create_default_roles.js create mode 100644 x-pack/plugins/security/server/lib/check_user_permission.js create mode 100644 x-pack/plugins/security/server/lib/mirror_status_and_initialize.js create mode 100644 x-pack/plugins/security/server/lib/mirror_status_and_initialize.test.js create mode 100644 x-pack/plugins/security/server/lib/privileges/privilege_action_registry.js create mode 100644 x-pack/plugins/security/server/lib/privileges/privileges.js create mode 100644 x-pack/plugins/security/server/lib/saved_object_client_interceptor.js create mode 100644 x-pack/plugins/security/server/lib/saved_objects_client/saved_objects_client_wrapper.js create mode 100644 x-pack/plugins/security/server/lib/saved_objects_client/secure_options_builder.js create mode 100644 x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.js create mode 100644 x-pack/plugins/security/server/routes/api/v1/privileges.js create mode 100644 x-pack/plugins/security/server/routes/api/v1/roles/contains_other_applications.js create mode 100644 x-pack/plugins/security/server/routes/api/v1/roles/contains_other_applications.test.js rename x-pack/plugins/security/server/routes/api/v1/{roles.js => roles/index.js} (69%) diff --git a/x-pack/plugins/security/index.js b/x-pack/plugins/security/index.js index 5e4602d448084..2810f7ae3cb7f 100644 --- a/x-pack/plugins/security/index.js +++ b/x-pack/plugins/security/index.js @@ -16,7 +16,12 @@ import { validateConfig } from './server/lib/validate_config'; import { authenticateFactory } from './server/lib/auth_redirect'; import { checkLicense } from './server/lib/check_license'; import { initAuthenticator } from './server/lib/authentication/authenticator'; -import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status'; +import { mirrorStatusAndInitialize } from './server/lib/mirror_status_and_initialize'; +import { secureSavedObjectsClientWrapper } from './server/lib/saved_objects_client/saved_objects_client_wrapper'; +import { secureSavedObjectsClientOptionsBuilder } from './server/lib/saved_objects_client/secure_options_builder'; +import { registerPrivilegesWithCluster } from './server/lib/privileges/privilege_action_registry'; +import { createDefaultRoles } from './server/lib/authorization/create_default_roles'; +import { initPrivilegesApi } from './server/routes/api/v1/privileges'; export const security = (kibana) => new kibana.Plugin({ id: 'security', @@ -37,6 +42,11 @@ export const security = (kibana) => new kibana.Plugin({ hostname: Joi.string().hostname(), port: Joi.number().integer().min(0).max(65535) }).default(), + rbac: Joi.object({ + enabled: Joi.boolean().default(false), + createDefaultRoles: Joi.boolean().default(true), + application: Joi.string().default('kibana'), + }).default(), }).default(); }, @@ -64,21 +74,29 @@ export const security = (kibana) => new kibana.Plugin({ return { secureCookies: config.get('xpack.security.secureCookies'), - sessionTimeout: config.get('xpack.security.sessionTimeout') + sessionTimeout: config.get('xpack.security.sessionTimeout'), + rbacEnabled: config.get('xpack.security.rbac.enabled') }; } }, async init(server) { - const thisPlugin = this; + const config = server.config(); const xpackMainPlugin = server.plugins.xpack_main; - mirrorPluginStatus(xpackMainPlugin, thisPlugin); + + mirrorStatusAndInitialize(xpackMainPlugin.status, this.status, async () => { + if (!config.get('xpack.security.rbac.enabled')) { + return; + } + + await registerPrivilegesWithCluster(server); + await createDefaultRoles(server); + }); // Register a function that is called whenever the xpack info changes, // to re-compute the license check results for this plugin - xpackMainPlugin.info.feature(thisPlugin.id).registerLicenseCheckResultsGenerator(checkLicense); + xpackMainPlugin.info.feature(this.id).registerLicenseCheckResultsGenerator(checkLicense); - const config = server.config(); validateConfig(config, message => server.log(['security', 'warning'], message)); // Create a Hapi auth scheme that should be applied to each request. @@ -88,6 +106,12 @@ export const security = (kibana) => new kibana.Plugin({ // automatically assigned to all routes that don't contain an auth config. server.auth.strategy('session', 'login', 'required'); + if (config.get('xpack.security.rbac.enabled')) { + const savedObjectsClientProvider = server.getSavedObjectsClientProvider(); + savedObjectsClientProvider.addClientOptionBuilder((options) => secureSavedObjectsClientOptionsBuilder(server, options)); + savedObjectsClientProvider.addClientWrapper(secureSavedObjectsClientWrapper); + } + getUserProvider(server); await initAuthenticator(server); @@ -95,6 +119,7 @@ export const security = (kibana) => new kibana.Plugin({ initUsersApi(server); initRolesApi(server); initIndicesApi(server); + initPrivilegesApi(server); initLoginView(server, xpackMainPlugin); initLogoutView(server); diff --git a/x-pack/plugins/security/public/services/application_privilege.js b/x-pack/plugins/security/public/services/application_privilege.js new file mode 100644 index 0000000000000..2b21eb7e63e4b --- /dev/null +++ b/x-pack/plugins/security/public/services/application_privilege.js @@ -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. + */ + +/*! 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 'angular-resource'; +import { uiModules } from 'ui/modules'; + +const module = uiModules.get('security', ['ngResource']); +module.service('ApplicationPrivilege', ($resource, chrome) => { + const baseUrl = chrome.addBasePath('/api/security/v1/privileges'); + const ShieldPrivilege = $resource(baseUrl); + + return ShieldPrivilege; +}); diff --git a/x-pack/plugins/security/public/services/shield_privileges.js b/x-pack/plugins/security/public/services/shield_privileges.js index aabd0a95b26ca..a00b326ab82da 100644 --- a/x-pack/plugins/security/public/services/shield_privileges.js +++ b/x-pack/plugins/security/public/services/shield_privileges.js @@ -35,5 +35,6 @@ module.constant('shieldPrivileges', { 'create_index', 'view_index_metadata', 'read_cross_cluster', - ] + ], + applications: [] }); diff --git a/x-pack/plugins/security/public/views/management/edit_role.html b/x-pack/plugins/security/public/views/management/edit_role.html index f547788606cc3..768f50a210758 100644 --- a/x-pack/plugins/security/public/views/management/edit_role.html +++ b/x-pack/plugins/security/public/views/management/edit_role.html @@ -96,6 +96,30 @@

+ +
+ + +
+ Changes to this section are not supported: this role contains application privileges that do not belong to this instance of Kibana. +
+ +
+ +
+
+