From f208d3cdba53a2968518904533ced9df8a153a4b Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Mon, 7 May 2018 09:46:48 -0400 Subject: [PATCH 01/41] Crude and incomplete impl of Space-Aware Saved Objects Client --- .../client/saved_objects_client.js | 40 ++++- x-pack/plugins/spaces/index.js | 4 + .../saved_objects_client_wrapper.js | 14 ++ .../spaces_saved_objects_client.js | 152 ++++++++++++++++++ 4 files changed, 203 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper.js create mode 100644 x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js diff --git a/src/server/saved_objects/client/saved_objects_client.js b/src/server/saved_objects/client/saved_objects_client.js index 79dc55540e039..141c240382929 100644 --- a/src/server/saved_objects/client/saved_objects_client.js +++ b/src/server/saved_objects/client/saved_objects_client.js @@ -107,6 +107,7 @@ export class SavedObjectsClient { async create(type, attributes = {}, options = {}) { const { id, + extraBodyProperties = {}, overwrite = false } = options; @@ -122,7 +123,8 @@ export class SavedObjectsClient { body: { type, updated_at: time, - [type]: attributes + [type]: attributes, + ...extraBodyProperties }, }); @@ -169,7 +171,8 @@ export class SavedObjectsClient { { type: object.type, updated_at: time, - [object.type]: object.attributes + [object.type]: object.attributes, + ...object.extraBodyProperties } ]; }; @@ -272,6 +275,7 @@ export class SavedObjectsClient { sortField, sortOrder, fields, + queryDecorator, } = options; if (searchFields && !Array.isArray(searchFields)) { @@ -300,6 +304,10 @@ export class SavedObjectsClient { } }; + if (esOptions.body.query && typeof queryDecorator === 'function') { + esOptions.body.query = queryDecorator(esOptions.body.query); + } + const response = await this._callCluster('search', esOptions); if (response.status === 404) { @@ -342,7 +350,7 @@ export class SavedObjectsClient { * { id: 'foo', type: 'index-pattern' } * ]) */ - async bulkGet(objects = []) { + async bulkGet(objects = [], options = {}) { if (objects.length === 0) { return { saved_objects: [] }; } @@ -357,8 +365,15 @@ export class SavedObjectsClient { } }); + const { docs } = response; + + let docsToReturn = docs; + if (typeof options.documentFilter === 'function') { + docsToReturn = docs.filter(options.documentFilter); + } + return { - saved_objects: response.docs.map((doc, i) => { + saved_objects: docsToReturn.map((doc, i) => { const { id, type } = objects[i]; if (!doc.found) { @@ -370,13 +385,19 @@ export class SavedObjectsClient { } const time = doc._source.updated_at; - return { + const savedObject = { id, type, ...time && { updated_at: time }, version: doc._version, attributes: doc._source[type] }; + + if (typeof options.resultDecorator === 'function') { + return options.resultDecorator(savedObject, doc); + } + + return savedObject; }) }; } @@ -388,7 +409,7 @@ export class SavedObjectsClient { * @param {string} id * @returns {promise} - { id, type, version, attributes } */ - async get(type, id) { + async get(type, id, options = {}) { const response = await this._callCluster('get', { id: this._generateEsId(type, id), type: this._type, @@ -396,6 +417,10 @@ export class SavedObjectsClient { ignore: [404] }); + if (typeof options.responseInterceptor === 'function') { + options.responseInterceptor(response); + } + const docNotFound = response.found === false; const indexNotFound = response.status === 404; if (docNotFound || indexNotFound) { @@ -435,7 +460,8 @@ export class SavedObjectsClient { body: { doc: { updated_at: time, - [type]: attributes + [type]: attributes, + ...options.extraBodyProperties } }, }); diff --git a/x-pack/plugins/spaces/index.js b/x-pack/plugins/spaces/index.js index 5780998378cf8..36aa5560e7e28 100644 --- a/x-pack/plugins/spaces/index.js +++ b/x-pack/plugins/spaces/index.js @@ -11,6 +11,7 @@ import { initSpacesApi } from './server/routes/api/v1/spaces'; import { initSpacesRequestInterceptors } from './server/lib/space_request_interceptors'; import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status'; import mappings from './mappings.json'; +import { spacesSavedObjectsClientWrapper } from './server/lib/saved_objects_client/saved_objects_client_wrapper'; export const spaces = (kibana) => new kibana.Plugin({ id: 'spaces', @@ -55,6 +56,9 @@ export const spaces = (kibana) => new kibana.Plugin({ const config = server.config(); validateConfig(config, message => server.log(['spaces', 'warning'], message)); + const savedObjectsClientProvider = server.getSavedObjectsClientProvider(); + savedObjectsClientProvider.addClientWrapper(spacesSavedObjectsClientWrapper); + initSpacesApi(server); initSpacesRequestInterceptors(server); diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper.js new file mode 100644 index 0000000000000..1b09b209ee944 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper.js @@ -0,0 +1,14 @@ +/* + * 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 { SpacesSavedObjectsClient } from './spaces_saved_objects_client'; + +export function spacesSavedObjectsClientWrapper(baseClient, options) { + return new SpacesSavedObjectsClient({ + baseClient, + ...options + }); +} diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js new file mode 100644 index 0000000000000..c3e8a5614cddb --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js @@ -0,0 +1,152 @@ +/* + * 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. + */ + +export class SpacesSavedObjectsClient { + constructor(options) { + const { + request, + baseClient, + spaceUrlContext, + } = options; + + this.errors = baseClient.errors; + + this._client = baseClient; + this._request = request; + + this._spaceUrlContext = spaceUrlContext; + } + + async create(type, attributes = {}, options = {}) { + + if (this._isTypeSpaceAware(type)) { + options.extraBodyProperties = { + ...options.extraBodyProperties, + spaceId: await this._getSpaceId() + }; + } + + return await this._client.create(type, attributes, options); + } + + async bulkCreate(objects, options = {}) { + options.extraBodyProperties = { + ...options.extraBodyProperties, + spaceId: await this._getSpaceId() + }; + + return await this._client.bulkCreate(objects, options); + } + + async delete(type, id) { + return await this._client.delete(type, id); + } + + async find(options = {}) { + const spaceOptions = {}; + + if (this._isTypeSpaceAware(options.type)) { + const spaceId = await this._getSpaceId(); + + spaceOptions.queryDecorator = (query) => { + const { bool = {} } = query; + + if (!Array.isArray(bool.filter)) { + bool.filter = []; + } + + bool.filter.push({ + term: { + spaceId + } + }); + + return query; + }; + } + + return await this._client.find({ ...options, ...spaceOptions }); + } + + async bulkGet(objects = []) { + // ES 'mget' does not support queries, so we have to filter results after the fact. + const thisSpaceId = await this._getSpaceId(); + + return await this._client.bulkGet(objects, { + documentFilter: (doc) => { + if (!doc.found) return true; + + const { type, spaceId } = doc._source; + + if (this._isTypeSpaceAware(type)) { + return spaceId === thisSpaceId; + } + + return true; + }, + resultDecorator(savedObject, doc) { + savedObject.attributes = { + ...savedObject.attributes, + spaceId: doc._source.spaceId + }; + return savedObject; + } + }); + } + + async get(type, id) { + // ES 'get' does not support queries, so we have to filter results after the fact. + let thisSpaceId; + + if (this._isTypeSpaceAware(type)) { + thisSpaceId = await this._getSpaceId(); + } + + return await this._client.get(type, id, { + responseInterceptor: (response) => { + if (!this._isTypeSpaceAware(type)) { + return response; + } + + if (response.found && response.status !== 404) { + const { spaceId } = response._source; + if (spaceId !== thisSpaceId) { + response.found = false; + response._source = {}; + } + } + + return response; + } + }); + } + + async update(type, id, attributes, options = {}) { + return await this._client.update(type, id, attributes, options); + } + + _isTypeSpaceAware(type) { + return type !== 'space'; + } + + async _getSpaceId() { + if (!this._spaceId) { + const { + saved_objects: spaces = [] + } = await this.find({ + type: 'space', + search: `"${this._spaceUrlContext}"`, + search_fields: ['urlContext'], + }); + + if (spaces.length > 0) { + this._spaceId = spaces[0].id; + } + } + + return this._spaceId; + } +} From 7538bf348a845265a17cd62209bb91acaad1bf53 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Tue, 8 May 2018 14:26:14 -0400 Subject: [PATCH 02/41] Code review updates --- .../client/lib/search_dsl/query_params.js | 16 ++++-- .../client/lib/search_dsl/search_dsl.js | 9 +++- .../client/saved_objects_client.js | 45 ++++++++++------- .../spaces_saved_objects_client.js | 50 ++++++------------- 4 files changed, 61 insertions(+), 59 deletions(-) diff --git a/src/server/saved_objects/client/lib/search_dsl/query_params.js b/src/server/saved_objects/client/lib/search_dsl/query_params.js index 3b29aa41b6d22..ebc743187a2a3 100644 --- a/src/server/saved_objects/client/lib/search_dsl/query_params.js +++ b/src/server/saved_objects/client/lib/search_dsl/query_params.js @@ -27,19 +27,27 @@ function getFieldsForTypes(searchFields, types) { * @param {Object} type * @param {String} search * @param {Array} searchFields + * @param {Array} extraFilters * @return {Object} */ -export function getQueryParams(mappings, type, search, searchFields) { +export function getQueryParams(mappings, type, search, searchFields, extraFilters) { if (!type && !search) { return {}; } const bool = {}; + const filters = []; if (type) { - bool.filter = [ - { term: { type } } - ]; + filters.push({ term: { type } }); + } + + if (extraFilters) { + filters.push(...extraFilters); + } + + if (filters.length > 0) { + bool.filter = filters; } if (search) { diff --git a/src/server/saved_objects/client/lib/search_dsl/search_dsl.js b/src/server/saved_objects/client/lib/search_dsl/search_dsl.js index a76f91eb11b0b..b3c9fa9572331 100644 --- a/src/server/saved_objects/client/lib/search_dsl/search_dsl.js +++ b/src/server/saved_objects/client/lib/search_dsl/search_dsl.js @@ -9,7 +9,8 @@ export function getSearchDsl(mappings, options = {}) { search, searchFields, sortField, - sortOrder + sortOrder, + extraFilters, } = options; if (!type && sortField) { @@ -20,8 +21,12 @@ export function getSearchDsl(mappings, options = {}) { throw Boom.notAcceptable('sortOrder requires a sortField'); } + if (extraFilters && !Array.isArray(extraFilters)) { + throw Boom.notAcceptable('extraFilters must be an array'); + } + return { - ...getQueryParams(mappings, type, search, searchFields), + ...getQueryParams(mappings, type, search, searchFields, extraFilters), ...getSortingParams(mappings, type, sortField, sortOrder), }; } diff --git a/src/server/saved_objects/client/saved_objects_client.js b/src/server/saved_objects/client/saved_objects_client.js index 141c240382929..b912df2fb0c19 100644 --- a/src/server/saved_objects/client/saved_objects_client.js +++ b/src/server/saved_objects/client/saved_objects_client.js @@ -121,10 +121,10 @@ export class SavedObjectsClient { index: this._index, refresh: 'wait_for', body: { + ...extraBodyProperties, type, updated_at: time, [type]: attributes, - ...extraBodyProperties }, }); @@ -275,7 +275,7 @@ export class SavedObjectsClient { sortField, sortOrder, fields, - queryDecorator, + extraFilters, } = options; if (searchFields && !Array.isArray(searchFields)) { @@ -286,6 +286,10 @@ export class SavedObjectsClient { throw new TypeError('options.searchFields must be an array'); } + if (extraFilters && !Array.isArray(extraFilters)) { + throw new TypeError('options.extraFilters must be an array'); + } + const esOptions = { index: this._index, size: perPage, @@ -299,15 +303,12 @@ export class SavedObjectsClient { searchFields, type, sortField, - sortOrder + sortOrder, + extraFilters }) } }; - if (esOptions.body.query && typeof queryDecorator === 'function') { - esOptions.body.query = queryDecorator(esOptions.body.query); - } - const response = await this._callCluster('search', esOptions); if (response.status === 404) { @@ -372,6 +373,8 @@ export class SavedObjectsClient { docsToReturn = docs.filter(options.documentFilter); } + const { extraSourceProperties = [] } = options; + return { saved_objects: docsToReturn.map((doc, i) => { const { id, type } = objects[i]; @@ -390,12 +393,14 @@ export class SavedObjectsClient { type, ...time && { updated_at: time }, version: doc._version, - attributes: doc._source[type] - }; + attributes: { + ...extraSourceProperties + .map(s => doc._source[s]) + .reduce((acc, prop) => ({ ...acc, ...prop }), {}), - if (typeof options.resultDecorator === 'function') { - return options.resultDecorator(savedObject, doc); - } + ...doc._source[type], + } + }; return savedObject; }) @@ -417,10 +422,6 @@ export class SavedObjectsClient { ignore: [404] }); - if (typeof options.responseInterceptor === 'function') { - options.responseInterceptor(response); - } - const docNotFound = response.found === false; const indexNotFound = response.status === 404; if (docNotFound || indexNotFound) { @@ -428,6 +429,8 @@ export class SavedObjectsClient { throw errors.createGenericNotFoundError(); } + const { extraSourceProperties = [] } = options; + const { updated_at: updatedAt } = response._source; return { @@ -435,7 +438,13 @@ export class SavedObjectsClient { type, ...updatedAt && { updated_at: updatedAt }, version: response._version, - attributes: response._source[type] + attributes: { + ...extraSourceProperties + .map(s => response._source[s]) + .reduce((acc, prop) => ({ ...acc, ...prop }), {}), + + ...response._source[type], + } }; } @@ -459,9 +468,9 @@ export class SavedObjectsClient { ignore: [404], body: { doc: { + ...options.extraBodyProperties, updated_at: time, [type]: attributes, - ...options.extraBodyProperties } }, }); diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js index c3e8a5614cddb..37332f84fe114 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js @@ -50,22 +50,13 @@ export class SpacesSavedObjectsClient { if (this._isTypeSpaceAware(options.type)) { const spaceId = await this._getSpaceId(); - - spaceOptions.queryDecorator = (query) => { - const { bool = {} } = query; - - if (!Array.isArray(bool.filter)) { - bool.filter = []; - } - - bool.filter.push({ + if (spaceId) { + spaceOptions.extraFilters = [{ term: { spaceId } - }); - - return query; - }; + }]; + } } return await this._client.find({ ...options, ...spaceOptions }); @@ -76,6 +67,7 @@ export class SpacesSavedObjectsClient { const thisSpaceId = await this._getSpaceId(); return await this._client.bulkGet(objects, { + extraSourceProperties: ['spaceId'], documentFilter: (doc) => { if (!doc.found) return true; @@ -86,13 +78,6 @@ export class SpacesSavedObjectsClient { } return true; - }, - resultDecorator(savedObject, doc) { - savedObject.attributes = { - ...savedObject.attributes, - spaceId: doc._source.spaceId - }; - return savedObject; } }); } @@ -105,26 +90,21 @@ export class SpacesSavedObjectsClient { thisSpaceId = await this._getSpaceId(); } - return await this._client.get(type, id, { - responseInterceptor: (response) => { - if (!this._isTypeSpaceAware(type)) { - return response; - } + const response = await this._client.get(type, id, { + extraSourceProperties: ['spaceId'] + }); - if (response.found && response.status !== 404) { - const { spaceId } = response._source; - if (spaceId !== thisSpaceId) { - response.found = false; - response._source = {}; - } - } + const { spaceId: objectSpaceId } = response.attributes; - return response; - } - }); + if (objectSpaceId !== thisSpaceId) { + throw this._client.errors.createGenericNotFoundError(); + } + + return response; } async update(type, id, attributes, options = {}) { + attributes.spaceId = await this._getSpaceId(); return await this._client.update(type, id, attributes, options); } From bb3e511759880919bb75dec2210d32cba099be2a Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Tue, 8 May 2018 15:35:17 -0400 Subject: [PATCH 03/41] Missed one - move extraBodyProperties to the top --- src/server/saved_objects/client/saved_objects_client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/saved_objects/client/saved_objects_client.js b/src/server/saved_objects/client/saved_objects_client.js index b912df2fb0c19..75899e2dc59a9 100644 --- a/src/server/saved_objects/client/saved_objects_client.js +++ b/src/server/saved_objects/client/saved_objects_client.js @@ -169,10 +169,10 @@ export class SavedObjectsClient { } }, { + ...object.extraBodyProperties, type: object.type, updated_at: time, [object.type]: object.attributes, - ...object.extraBodyProperties } ]; }; From 0e2e4e8f80d96e086e859f332940ac8cbc318c1b Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Wed, 9 May 2018 08:30:49 -0400 Subject: [PATCH 04/41] Remove documentFilter from bulkGet --- .../client/saved_objects_client.js | 8 ++------ .../spaces_saved_objects_client.js | 20 +++++++++---------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/server/saved_objects/client/saved_objects_client.js b/src/server/saved_objects/client/saved_objects_client.js index 75899e2dc59a9..bdf6e0a6c6997 100644 --- a/src/server/saved_objects/client/saved_objects_client.js +++ b/src/server/saved_objects/client/saved_objects_client.js @@ -343,6 +343,7 @@ export class SavedObjectsClient { * Returns an array of objects by id * * @param {array} objects - an array ids, or an array of objects containing id and optionally type + * @param {object} options * @returns {promise} - { saved_objects: [{ id, type, version, attributes }] } * @example * @@ -368,15 +369,10 @@ export class SavedObjectsClient { const { docs } = response; - let docsToReturn = docs; - if (typeof options.documentFilter === 'function') { - docsToReturn = docs.filter(options.documentFilter); - } - const { extraSourceProperties = [] } = options; return { - saved_objects: docsToReturn.map((doc, i) => { + saved_objects: docs.map((doc, i) => { const { id, type } = objects[i]; if (!doc.found) { diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js index 37332f84fe114..c591d3400f249 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js @@ -66,20 +66,20 @@ export class SpacesSavedObjectsClient { // ES 'mget' does not support queries, so we have to filter results after the fact. const thisSpaceId = await this._getSpaceId(); - return await this._client.bulkGet(objects, { - extraSourceProperties: ['spaceId'], - documentFilter: (doc) => { - if (!doc.found) return true; - - const { type, spaceId } = doc._source; + const result = await this._client.bulkGet(objects, { + extraSourceProperties: ['spaceId', 'type'] + }); - if (this._isTypeSpaceAware(type)) { - return spaceId === thisSpaceId; - } + result.saved_objects = result.saved_objects.filter(savedObject => { + const { type, spaceId } = savedObject.attributes; - return true; + if (this._isTypeSpaceAware(type)) { + return spaceId === thisSpaceId; } + return true; }); + + return result; } async get(type, id) { From 7db0a4a1490ef5d51d043fd1fd427e00efbc6ef0 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Mon, 21 May 2018 16:29:47 -0400 Subject: [PATCH 05/41] Make config document id independent of Kibana version --- .../kibana/ui_setting_defaults.js | 3 ++ ... => create_or_upgrade_integration.test.js} | 41 +++++++++++++------ ...=> create_or_upgrade_saved_config.test.js} | 7 +++- ... => is_config_version_upgradeable.test.js} | 3 +- .../create_or_upgrade_saved_config.js | 17 ++++++-- src/ui/ui_settings/ui_settings_mixin.js | 12 ++++++ src/ui/ui_settings/ui_settings_service.js | 4 +- .../ui_settings_service_factory.js | 7 +++- .../ui_settings_service_for_request.js | 4 +- 9 files changed, 76 insertions(+), 22 deletions(-) rename src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/{create_or_upgrade_integration.js => create_or_upgrade_integration.test.js} (85%) rename src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/{create_or_upgrade_saved_config.js => create_or_upgrade_saved_config.test.js} (96%) rename src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/{is_config_version_upgradeable.js => is_config_version_upgradeable.test.js} (96%) diff --git a/src/core_plugins/kibana/ui_setting_defaults.js b/src/core_plugins/kibana/ui_setting_defaults.js index 88ba519b65137..dc00235c55c9c 100644 --- a/src/core_plugins/kibana/ui_setting_defaults.js +++ b/src/core_plugins/kibana/ui_setting_defaults.js @@ -13,6 +13,9 @@ export function getUiSettingDefaults() { 'buildNum': { readonly: true }, + 'version': { + readonly: true + }, 'query:queryString:options': { value: '{ "analyze_wildcard": true, "default_field": "*" }', description: 'Options for the lucene query string parser', diff --git a/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_integration.js b/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_integration.test.js similarity index 85% rename from src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_integration.js rename to src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_integration.test.js index 9b5bb95c74762..2ca5b70c46de6 100644 --- a/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_integration.js +++ b/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_integration.test.js @@ -1,5 +1,4 @@ import sinon from 'sinon'; -import expect from 'expect.js'; import { createTestCluster } from '../../../../test_utils/es'; import { createServerWithCorePlugins } from '../../../../test_utils/kbn_server'; @@ -11,7 +10,7 @@ describe('createOrUpgradeSavedConfig()', () => { let kbnServer; const cleanup = []; - before(async function () { + beforeAll(async function () { const log = createToolingLog('debug'); log.pipe(process.stdout); log.indent(6); @@ -20,7 +19,6 @@ describe('createOrUpgradeSavedConfig()', () => { log.indent(4); const es = createTestCluster({ log }); - this.timeout(es.getStartTimeout()); log.indent(-4); cleanup.push(async () => await es.cleanup()); @@ -67,30 +65,33 @@ describe('createOrUpgradeSavedConfig()', () => { }, }, ]); - }); + }, 30000); - after(async () => { + afterAll(async () => { await Promise.all(cleanup.map(fn => fn())); cleanup.length = 0; }); it('upgrades the previous version on each increment', async function () { - this.timeout(30000); // ------------------------------------ // upgrade to 5.4.0 await createOrUpgradeSavedConfig({ savedObjectsClient, version: '5.4.0', + id: '5.4.0', buildNum: 54099, log: sinon.stub() }); const config540 = await savedObjectsClient.get('config', '5.4.0'); - expect(config540).to.have.property('attributes').eql({ + expect(config540).toHaveProperty('attributes'); + expect(config540.attributes).toEqual({ // should have the new build number buildNum: 54099, + version: '5.4.0', + // 5.4.0-SNAPSHOT and @@version were ignored so we only have the // attributes from 5.4.0-rc1, even though the other build nums are greater '5.4.0-rc1': true, @@ -106,15 +107,19 @@ describe('createOrUpgradeSavedConfig()', () => { await createOrUpgradeSavedConfig({ savedObjectsClient, version: '5.4.1', + id: '5.4.1', buildNum: 54199, log: sinon.stub() }); const config541 = await savedObjectsClient.get('config', '5.4.1'); - expect(config541).to.have.property('attributes').eql({ + expect(config541).toHaveProperty('attributes'); + expect(config541.attributes).toEqual({ // should have the new build number buildNum: 54199, + version: '5.4.1', + // should also include properties from 5.4.0 and 5.4.0-rc1 '5.4.0': true, '5.4.0-rc1': true, @@ -130,15 +135,19 @@ describe('createOrUpgradeSavedConfig()', () => { await createOrUpgradeSavedConfig({ savedObjectsClient, version: '7.0.0-rc1', + id: '7.0.0-rc1', buildNum: 70010, log: sinon.stub() }); const config700rc1 = await savedObjectsClient.get('config', '7.0.0-rc1'); - expect(config700rc1).to.have.property('attributes').eql({ + expect(config700rc1).toHaveProperty('attributes'); + expect(config700rc1.attributes).toEqual({ // should have the new build number buildNum: 70010, + version: '7.0.0-rc1', + // should also include properties from 5.4.1, 5.4.0 and 5.4.0-rc1 '5.4.1': true, '5.4.0': true, @@ -155,15 +164,19 @@ describe('createOrUpgradeSavedConfig()', () => { await createOrUpgradeSavedConfig({ savedObjectsClient, version: '7.0.0', + id: '7.0.0', buildNum: 70099, log: sinon.stub() }); const config700 = await savedObjectsClient.get('config', '7.0.0'); - expect(config700).to.have.property('attributes').eql({ + expect(config700).toHaveProperty('attributes'); + expect(config700.attributes).toEqual({ // should have the new build number buildNum: 70099, + version: '7.0.0', + // should also include properties from ancestors, including 7.0.0-rc1 '7.0.0-rc1': true, '5.4.1': true, @@ -181,19 +194,23 @@ describe('createOrUpgradeSavedConfig()', () => { await createOrUpgradeSavedConfig({ savedObjectsClient, version: '6.2.3-rc1', + id: '6.2.3-rc1', buildNum: 62310, log: sinon.stub() }); const config623rc1 = await savedObjectsClient.get('config', '6.2.3-rc1'); - expect(config623rc1).to.have.property('attributes').eql({ + expect(config623rc1).toHaveProperty('attributes'); + expect(config623rc1.attributes).toEqual({ // should have the new build number buildNum: 62310, + version: '6.2.3-rc1', + // should also include properties from ancestors, but not 7.0.0-rc1 or 7.0.0 '5.4.1': true, '5.4.0': true, '5.4.0-rc1': true, }); - }); + }, 30000); }); diff --git a/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_saved_config.js b/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_saved_config.test.js similarity index 96% rename from src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_saved_config.js rename to src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_saved_config.test.js index 6fd8d5fc169c6..4bcbae869beb7 100644 --- a/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_saved_config.js +++ b/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_saved_config.test.js @@ -29,6 +29,7 @@ describe('uiSettings/createOrUpgradeSavedConfig', function () { const resp = await createOrUpgradeSavedConfig({ savedObjectsClient, version, + id: `${version}:foo`, buildNum, log, }); @@ -58,8 +59,9 @@ describe('uiSettings/createOrUpgradeSavedConfig', function () { sinon.assert.calledOnce(savedObjectsClient.create); sinon.assert.calledWithExactly(savedObjectsClient.create, 'config', { buildNum, + version }, { - id: version + id: `${version}:foo` }); }); }); @@ -91,9 +93,10 @@ describe('uiSettings/createOrUpgradeSavedConfig', function () { { ...savedAttributes, buildNum, + version, }, { - id: version, + id: `${version}:foo`, } ); }); diff --git a/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/is_config_version_upgradeable.js b/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/is_config_version_upgradeable.test.js similarity index 96% rename from src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/is_config_version_upgradeable.js rename to src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/is_config_version_upgradeable.test.js index 5c05ed527a17d..7755eb1c5419a 100644 --- a/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/is_config_version_upgradeable.js +++ b/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/is_config_version_upgradeable.test.js @@ -1,4 +1,3 @@ -import expect from 'expect.js'; import { isConfigVersionUpgradeable } from '../is_config_version_upgradeable'; import { pkg } from '../../../../utils'; @@ -6,7 +5,7 @@ import { pkg } from '../../../../utils'; describe('savedObjects/health_check/isConfigVersionUpgradeable', function () { function isUpgradableTest(savedVersion, kibanaVersion, expected) { it(`should return ${expected} for config version ${savedVersion} and kibana version ${kibanaVersion}`, () => { - expect(isConfigVersionUpgradeable(savedVersion, kibanaVersion)).to.be(expected); + expect(isConfigVersionUpgradeable(savedVersion, kibanaVersion)).toBe(expected); }); } diff --git a/src/ui/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.js b/src/ui/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.js index f483d001bd3b7..8d5bda415df94 100644 --- a/src/ui/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.js +++ b/src/ui/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.js @@ -5,6 +5,7 @@ import { getUpgradeableConfig } from './get_upgradeable_config'; export async function createOrUpgradeSavedConfig(options) { const { savedObjectsClient, + id, version, buildNum, log, @@ -17,16 +18,26 @@ export async function createOrUpgradeSavedConfig(options) { }); if (upgradeableConfig) { + + let prevVersion = upgradeableConfig.version; + // TODO(legrego): update comment with real versions + // This check can be removed after Y.Z. + // Prior to version X.Y, the config document's id was also used to store the version of Kibana that config was valid for. + // From X.Y forward, the Kibana version is stored in a dedicated "version" field. + if (!prevVersion) { + prevVersion = upgradeableConfig.id; + } + log(['plugin', 'elasticsearch'], { tmpl: 'Upgrade config from <%= prevVersion %> to <%= newVersion %>', - prevVersion: upgradeableConfig.id, + prevVersion, newVersion: version }); } // default to the attributes of the upgradeableConfig if available const attributes = defaults( - { buildNum }, + { buildNum, version }, upgradeableConfig ? upgradeableConfig.attributes : {} ); @@ -34,6 +45,6 @@ export async function createOrUpgradeSavedConfig(options) { await savedObjectsClient.create( 'config', attributes, - { id: version } + { id } ); } diff --git a/src/ui/ui_settings/ui_settings_mixin.js b/src/ui/ui_settings/ui_settings_mixin.js index 1ba6ce614793e..27890005e1344 100644 --- a/src/ui/ui_settings/ui_settings_mixin.js +++ b/src/ui/ui_settings/ui_settings_mixin.js @@ -12,6 +12,17 @@ export function uiSettingsMixin(kbnServer, server) { kbnServer.uiExports.uiSettingDefaults ); + const identifierCache = new WeakMap(); + server.decorate('request', 'setUiSettingsIdentifier', function (identifier) { + const request = this; + + if (identifierCache.has(request)) { + throw new Error(`UI Settings identifier has already been set for this request`); + } + + identifierCache.set(request, identifier); + }); + server.decorate('server', 'uiSettingsServiceFactory', (options = {}) => { return uiSettingsServiceFactory(server, { getDefaults, @@ -22,6 +33,7 @@ export function uiSettingsMixin(kbnServer, server) { server.addMemoizedFactoryToRequest('getUiSettingsService', request => { return getUiSettingsServiceForRequest(server, request, { getDefaults, + idSuffix: identifierCache.get(request) }); }); diff --git a/src/ui/ui_settings/ui_settings_service.js b/src/ui/ui_settings/ui_settings_service.js index e2ffe2cd7e31e..1c4e608289204 100644 --- a/src/ui/ui_settings/ui_settings_service.js +++ b/src/ui/ui_settings/ui_settings_service.js @@ -27,6 +27,7 @@ export class UiSettingsService { const { type, id, + version, buildNum, savedObjectsClient, // we use a function for getDefaults() so that defaults can be different in @@ -38,6 +39,7 @@ export class UiSettingsService { this._type = type; this._id = id; + this._version = version; this._buildNum = buildNum; this._savedObjectsClient = savedObjectsClient; this._getDefaults = getDefaults; @@ -106,7 +108,7 @@ export class UiSettingsService { await createOrUpgradeSavedConfig({ savedObjectsClient: this._savedObjectsClient, - version: this._id, + version: this._version, buildNum: this._buildNum, log: this._log, }); diff --git a/src/ui/ui_settings/ui_settings_service_factory.js b/src/ui/ui_settings/ui_settings_service_factory.js index 5965b8d40800c..28db638e277af 100644 --- a/src/ui/ui_settings/ui_settings_service_factory.js +++ b/src/ui/ui_settings/ui_settings_service_factory.js @@ -18,11 +18,16 @@ export function uiSettingsServiceFactory(server, options) { const { savedObjectsClient, getDefaults, + idSuffix = '' } = options; + const kibanaVersion = config.get('pkg.version'); + const uiSettingsId = idSuffix ? `${kibanaVersion}:${idSuffix}` : kibanaVersion; + return new UiSettingsService({ type: 'config', - id: config.get('pkg.version'), + id: uiSettingsId, + version: kibanaVersion, buildNum: config.get('pkg.buildNum'), savedObjectsClient, getDefaults, diff --git a/src/ui/ui_settings/ui_settings_service_for_request.js b/src/ui/ui_settings/ui_settings_service_for_request.js index 484a0aecf6870..6a188b6a85682 100644 --- a/src/ui/ui_settings/ui_settings_service_for_request.js +++ b/src/ui/ui_settings/ui_settings_service_for_request.js @@ -15,11 +15,13 @@ import { uiSettingsServiceFactory } from './ui_settings_service_factory'; */ export function getUiSettingsServiceForRequest(server, request, options = {}) { const { - getDefaults + getDefaults, + idSuffix, } = options; const uiSettingsService = uiSettingsServiceFactory(server, { getDefaults, + idSuffix, savedObjectsClient: request.getSavedObjectsClient() }); From d5734570d3862c41c6b4e25214e9af8044bd1148 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Wed, 20 Jun 2018 15:33:08 -0400 Subject: [PATCH 06/41] cleanup and fixes following initial rbac phase 1 merge --- .../saved_objects/service/lib/repository.js | 16 +++---- .../saved_objects/saved_objects_client.js | 2 +- src/ui/ui_settings/ui_settings_mixin.js | 12 ----- .../ui_settings_service_factory.js | 4 +- .../ui_settings_service_for_request.js | 2 - .../secure_saved_objects_client.js | 8 ++-- x-pack/plugins/spaces/index.js | 6 ++- .../server/lib/create_spaces_service.js | 45 +++++++++++++++++++ .../saved_objects_client_wrapper.js | 9 ++-- .../spaces_saved_objects_client.js | 23 +++++++--- .../server/lib/space_request_interceptors.js | 5 +-- 11 files changed, 86 insertions(+), 46 deletions(-) create mode 100644 x-pack/plugins/spaces/server/lib/create_spaces_service.js diff --git a/src/server/saved_objects/service/lib/repository.js b/src/server/saved_objects/service/lib/repository.js index c58debf5cc1c3..147d3da7c2532 100644 --- a/src/server/saved_objects/service/lib/repository.js +++ b/src/server/saved_objects/service/lib/repository.js @@ -36,7 +36,7 @@ export class SavedObjectsRepository { index, mappings, callCluster, - onBeforeWrite = () => {}, + onBeforeWrite = () => { }, } = options; this._index = index; @@ -341,11 +341,10 @@ export class SavedObjectsRepository { type, ...time && { updated_at: time }, version: doc._version, + ...extraSourceProperties + .map(s => ({ [s]: doc._source[s] })) + .reduce((acc, prop) => ({ ...acc, ...prop }), {}), attributes: { - ...extraSourceProperties - .map(s => doc._source[s]) - .reduce((acc, prop) => ({ ...acc, ...prop }), {}), - ...doc._source[type], } }; @@ -386,11 +385,10 @@ export class SavedObjectsRepository { type, ...updatedAt && { updated_at: updatedAt }, version: response._version, + ...extraSourceProperties + .map(s => response._source[s]) + .reduce((acc, prop) => ({ ...acc, ...prop }), {}), attributes: { - ...extraSourceProperties - .map(s => response._source[s]) - .reduce((acc, prop) => ({ ...acc, ...prop }), {}), - ...response._source[type], } }; diff --git a/src/ui/public/saved_objects/saved_objects_client.js b/src/ui/public/saved_objects/saved_objects_client.js index 3326e1c0597db..bc368123feff0 100644 --- a/src/ui/public/saved_objects/saved_objects_client.js +++ b/src/ui/public/saved_objects/saved_objects_client.js @@ -40,7 +40,7 @@ export class SavedObjectsClient { $http, basePath = chrome.getBasePath(), PromiseConstructor = Promise, - onCreateFailure = () => {}, + onCreateFailure = () => { }, } = options; this._$http = $http; diff --git a/src/ui/ui_settings/ui_settings_mixin.js b/src/ui/ui_settings/ui_settings_mixin.js index d1d844a676118..6ea81df2c229b 100644 --- a/src/ui/ui_settings/ui_settings_mixin.js +++ b/src/ui/ui_settings/ui_settings_mixin.js @@ -31,17 +31,6 @@ export function uiSettingsMixin(kbnServer, server) { kbnServer.uiExports.uiSettingDefaults ); - const identifierCache = new WeakMap(); - server.decorate('request', 'setUiSettingsIdentifier', function (identifier) { - const request = this; - - if (identifierCache.has(request)) { - throw new Error(`UI Settings identifier has already been set for this request`); - } - - identifierCache.set(request, identifier); - }); - server.decorate('server', 'uiSettingsServiceFactory', (options = {}) => { return uiSettingsServiceFactory(server, { getDefaults, @@ -52,7 +41,6 @@ export function uiSettingsMixin(kbnServer, server) { server.addMemoizedFactoryToRequest('getUiSettingsService', request => { return getUiSettingsServiceForRequest(server, request, { getDefaults, - idSuffix: identifierCache.get(request) }); }); diff --git a/src/ui/ui_settings/ui_settings_service_factory.js b/src/ui/ui_settings/ui_settings_service_factory.js index 4f38bb4912ed0..ca8c674123d67 100644 --- a/src/ui/ui_settings/ui_settings_service_factory.js +++ b/src/ui/ui_settings/ui_settings_service_factory.js @@ -37,15 +37,13 @@ export function uiSettingsServiceFactory(server, options) { const { savedObjectsClient, getDefaults, - idSuffix = '' } = options; const kibanaVersion = config.get('pkg.version'); - const uiSettingsId = idSuffix ? `${kibanaVersion}:${idSuffix}` : kibanaVersion; return new UiSettingsService({ type: 'config', - id: uiSettingsId, + id: kibanaVersion, version: kibanaVersion, buildNum: config.get('pkg.buildNum'), savedObjectsClient, diff --git a/src/ui/ui_settings/ui_settings_service_for_request.js b/src/ui/ui_settings/ui_settings_service_for_request.js index c6a304f676dbb..75700d2b6daa9 100644 --- a/src/ui/ui_settings/ui_settings_service_for_request.js +++ b/src/ui/ui_settings/ui_settings_service_for_request.js @@ -35,12 +35,10 @@ import { uiSettingsServiceFactory } from './ui_settings_service_factory'; export function getUiSettingsServiceForRequest(server, request, options = {}) { const { getDefaults, - idSuffix, } = options; const uiSettingsService = uiSettingsServiceFactory(server, { getDefaults, - idSuffix, savedObjectsClient: request.getSavedObjectsClient() }); diff --git a/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.js b/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.js index 83e6123f06df5..49c58a5214930 100644 --- a/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.js +++ b/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.js @@ -70,7 +70,7 @@ export class SecureSavedObjectsClient { const typesToPrivilegesMap = new Map(types.map(type => [type, getPrivilege(type, action)])); const hasPrivilegesResult = await this._hasSavedObjectPrivileges(Array.from(typesToPrivilegesMap.values())); const authorizedTypes = Array.from(typesToPrivilegesMap.entries()) - .filter(([ , privilege]) => !hasPrivilegesResult.missing.includes(privilege)) + .filter(([, privilege]) => !hasPrivilegesResult.missing.includes(privilege)) .map(([type]) => type); if (authorizedTypes.length === 0) { @@ -91,13 +91,13 @@ export class SecureSavedObjectsClient { }); } - async bulkGet(objects = []) { + async bulkGet(objects = [], options = {}) { const types = uniq(objects.map(o => o.type)); await this._performAuthorizationCheck(types, 'bulk_get', { objects, }); - return await this._repository.bulkGet(objects); + return await this._repository.bulkGet(objects, options); } async get(type, id) { @@ -137,7 +137,7 @@ export class SecureSavedObjectsClient { async _hasSavedObjectPrivileges(privileges) { try { return await this._hasPrivileges(privileges); - } catch(error) { + } catch (error) { const { reason } = get(error, 'body.error', {}); throw this.errors.decorateGeneralError(error, reason); } diff --git a/x-pack/plugins/spaces/index.js b/x-pack/plugins/spaces/index.js index f8d1d0db99352..83809c27ae0d9 100644 --- a/x-pack/plugins/spaces/index.js +++ b/x-pack/plugins/spaces/index.js @@ -10,6 +10,7 @@ import { checkLicense } from './server/lib/check_license'; import { initSpacesApi } from './server/routes/api/v1/spaces'; import { initSpacesRequestInterceptors } from './server/lib/space_request_interceptors'; import { createDefaultSpace } from './server/lib/create_default_space'; +import { createSpacesService } from './server/lib/create_spaces_service'; import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status'; import { getActiveSpace } from './server/lib/get_active_space'; import { wrapError } from './server/lib/errors'; @@ -75,8 +76,11 @@ export const spaces = (kibana) => new kibana.Plugin({ const config = server.config(); validateConfig(config, message => server.log(['spaces', 'warning'], message)); + const spacesService = createSpacesService(server); + server.decorate('server', 'spaces', spacesService); + const { addScopedSavedObjectsClientWrapperFactory } = server.savedObjects; - addScopedSavedObjectsClientWrapperFactory(spacesSavedObjectsClientWrapper); + addScopedSavedObjectsClientWrapperFactory(spacesSavedObjectsClientWrapper(spacesService)); initSpacesApi(server); diff --git a/x-pack/plugins/spaces/server/lib/create_spaces_service.js b/x-pack/plugins/spaces/server/lib/create_spaces_service.js new file mode 100644 index 0000000000000..c352ebc4e9aca --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/create_spaces_service.js @@ -0,0 +1,45 @@ +/* + * 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 { getSpaceUrlContext } from '../../common/spaces_url_parser'; + +export function createSpacesService() { + + const contextCache = new WeakMap(); + + function getUrlContext(request) { + if (!contextCache.has(request)) { + populateCache(request); + } + + const { urlContext } = contextCache.get(request); + return urlContext; + } + + async function getSpaceId(request) { + if (!contextCache.has(request)) { + await populateCache(request); + } + + const { spaceId } = contextCache.get(request); + return spaceId; + } + + function populateCache(request) { + const urlContext = getSpaceUrlContext(request.getBasePath()); + const spaceId = 'TODO'; + + contextCache.set(request, { + urlContext, + spaceId + }); + } + + return { + getUrlContext, + getSpaceId, + }; +} diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper.js index 1b09b209ee944..e3db9905f68bc 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper.js @@ -6,9 +6,10 @@ import { SpacesSavedObjectsClient } from './spaces_saved_objects_client'; -export function spacesSavedObjectsClientWrapper(baseClient, options) { - return new SpacesSavedObjectsClient({ - baseClient, - ...options +export function spacesSavedObjectsClientWrapper(spacesService) { + return ({ client, request }) => new SpacesSavedObjectsClient({ + baseClient: client, + request, + spacesService, }); } diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js index c591d3400f249..034eea5890f3f 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js @@ -9,7 +9,7 @@ export class SpacesSavedObjectsClient { const { request, baseClient, - spaceUrlContext, + spacesService, } = options; this.errors = baseClient.errors; @@ -17,7 +17,7 @@ export class SpacesSavedObjectsClient { this._client = baseClient; this._request = request; - this._spaceUrlContext = spaceUrlContext; + this._spaceUrlContext = spacesService.getUrlContext(this._request); } async create(type, attributes = {}, options = {}) { @@ -48,7 +48,13 @@ export class SpacesSavedObjectsClient { async find(options = {}) { const spaceOptions = {}; - if (this._isTypeSpaceAware(options.type)) { + // TODO(legrego) handle multiple types + let type = options.type; + if (Array.isArray(type)) { + type = type[0]; + } + + if (this._isTypeSpaceAware(type)) { const spaceId = await this._getSpaceId(); if (spaceId) { spaceOptions.extraFilters = [{ @@ -71,7 +77,7 @@ export class SpacesSavedObjectsClient { }); result.saved_objects = result.saved_objects.filter(savedObject => { - const { type, spaceId } = savedObject.attributes; + const { type, spaceId } = savedObject; if (this._isTypeSpaceAware(type)) { return spaceId === thisSpaceId; @@ -94,7 +100,7 @@ export class SpacesSavedObjectsClient { extraSourceProperties: ['spaceId'] }); - const { spaceId: objectSpaceId } = response.attributes; + const { spaceId: objectSpaceId } = response; if (objectSpaceId !== thisSpaceId) { throw this._client.errors.createGenericNotFoundError(); @@ -104,7 +110,10 @@ export class SpacesSavedObjectsClient { } async update(type, id, attributes, options = {}) { - attributes.spaceId = await this._getSpaceId(); + options.extraBodyProperties = { + ...options.extraBodyProperties, + spaceId: await this._getSpaceId() + }; return await this._client.update(type, id, attributes, options); } @@ -116,7 +125,7 @@ export class SpacesSavedObjectsClient { if (!this._spaceId) { const { saved_objects: spaces = [] - } = await this.find({ + } = await this.find({ type: 'space', search: `"${this._spaceUrlContext}"`, search_fields: ['urlContext'], diff --git a/x-pack/plugins/spaces/server/lib/space_request_interceptors.js b/x-pack/plugins/spaces/server/lib/space_request_interceptors.js index 6f4330ba283cc..3372f55279fe3 100644 --- a/x-pack/plugins/spaces/server/lib/space_request_interceptors.js +++ b/x-pack/plugins/spaces/server/lib/space_request_interceptors.js @@ -7,7 +7,6 @@ import { wrapError } from './errors'; export function initSpacesRequestInterceptors(server) { - const contextCache = new WeakMap(); server.ext('onRequest', async function spacesOnRequestHandler(request, reply) { const path = request.path; @@ -31,7 +30,6 @@ export function initSpacesRequestInterceptors(server) { }; request.setUrl(newUrl); - contextCache.set(request, spaceUrlContext); } return reply.continue(); @@ -41,7 +39,8 @@ export function initSpacesRequestInterceptors(server) { const path = request.path; const isRequestingKibanaRoot = path === '/'; - const urlContext = contextCache.get(request); + const { spaces } = server; + const urlContext = await spaces.getUrlContext(request); // if requesting the application root, then show the Space Selector UI to allow the user to choose which space // they wish to visit. This is done "onPostAuth" to allow the Saved Objects Client to use the request's auth scope, From d99cec77b18b15e62992e3fc7272147cf71ce1ab Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Wed, 20 Jun 2018 15:36:58 -0400 Subject: [PATCH 07/41] remove unused/migrated files --- .../client/lib/search_dsl/query_params.js | 68 ------------------- .../client/lib/search_dsl/search_dsl.js | 32 --------- 2 files changed, 100 deletions(-) delete mode 100644 src/server/saved_objects/client/lib/search_dsl/query_params.js delete mode 100644 src/server/saved_objects/client/lib/search_dsl/search_dsl.js diff --git a/src/server/saved_objects/client/lib/search_dsl/query_params.js b/src/server/saved_objects/client/lib/search_dsl/query_params.js deleted file mode 100644 index ebc743187a2a3..0000000000000 --- a/src/server/saved_objects/client/lib/search_dsl/query_params.js +++ /dev/null @@ -1,68 +0,0 @@ -import { getRootProperties } from '../../../../mappings'; - -/** - * Get the field params based on the types and searchFields - * @param {Array} searchFields - * @param {Array} types - * @return {Object} - */ -function getFieldsForTypes(searchFields, types) { - if (!searchFields || !searchFields.length) { - return { - all_fields: true - }; - } - - return { - fields: searchFields.reduce((acc, field) => [ - ...acc, - ...types.map(prefix => `${prefix}.${field}`) - ], []), - }; -} - -/** - * Get the "query" related keys for the search body - * @param {EsMapping} mapping mappings from Ui - * @param {Object} type - * @param {String} search - * @param {Array} searchFields - * @param {Array} extraFilters - * @return {Object} - */ -export function getQueryParams(mappings, type, search, searchFields, extraFilters) { - if (!type && !search) { - return {}; - } - - const bool = {}; - - const filters = []; - if (type) { - filters.push({ term: { type } }); - } - - if (extraFilters) { - filters.push(...extraFilters); - } - - if (filters.length > 0) { - bool.filter = filters; - } - - if (search) { - bool.must = [ - { - simple_query_string: { - query: search, - ...getFieldsForTypes( - searchFields, - type ? [type] : Object.keys(getRootProperties(mappings)) - ) - } - } - ]; - } - - return { query: { bool } }; -} diff --git a/src/server/saved_objects/client/lib/search_dsl/search_dsl.js b/src/server/saved_objects/client/lib/search_dsl/search_dsl.js deleted file mode 100644 index b3c9fa9572331..0000000000000 --- a/src/server/saved_objects/client/lib/search_dsl/search_dsl.js +++ /dev/null @@ -1,32 +0,0 @@ -import Boom from 'boom'; - -import { getQueryParams } from './query_params'; -import { getSortingParams } from './sorting_params'; - -export function getSearchDsl(mappings, options = {}) { - const { - type, - search, - searchFields, - sortField, - sortOrder, - extraFilters, - } = options; - - if (!type && sortField) { - throw Boom.notAcceptable('Cannot sort without filtering by type'); - } - - if (sortOrder && !sortField) { - throw Boom.notAcceptable('sortOrder requires a sortField'); - } - - if (extraFilters && !Array.isArray(extraFilters)) { - throw Boom.notAcceptable('extraFilters must be an array'); - } - - return { - ...getQueryParams(mappings, type, search, searchFields, extraFilters), - ...getSortingParams(mappings, type, sortField, sortOrder), - }; -} From 7e2d1e3cde132c1871bbb5afd327ad8debe9cffe Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Wed, 20 Jun 2018 17:34:24 -0400 Subject: [PATCH 08/41] remove unused code --- .../spaces/server/lib/create_spaces_service.js | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/x-pack/plugins/spaces/server/lib/create_spaces_service.js b/x-pack/plugins/spaces/server/lib/create_spaces_service.js index c352ebc4e9aca..d9f83f58f035b 100644 --- a/x-pack/plugins/spaces/server/lib/create_spaces_service.js +++ b/x-pack/plugins/spaces/server/lib/create_spaces_service.js @@ -19,27 +19,15 @@ export function createSpacesService() { return urlContext; } - async function getSpaceId(request) { - if (!contextCache.has(request)) { - await populateCache(request); - } - - const { spaceId } = contextCache.get(request); - return spaceId; - } - function populateCache(request) { const urlContext = getSpaceUrlContext(request.getBasePath()); - const spaceId = 'TODO'; contextCache.set(request, { - urlContext, - spaceId + urlContext }); } return { getUrlContext, - getSpaceId, }; } From a63128c23b99fd7863374e933610499dda01b35c Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 22 Jun 2018 13:47:11 -0400 Subject: [PATCH 09/41] partial updates for space aware saved objects and tests --- .../saved_objects/service/lib/repository.js | 12 +- .../service/lib/search_dsl/query_params.js | 20 +- .../service/lib/search_dsl/search_dsl.js | 8 +- .../service/saved_objects_client.js | 8 +- .../secure_saved_objects_client.js | 4 +- x-pack/plugins/spaces/index.js | 4 +- .../server/lib/create_spaces_service.js | 8 +- .../saved_objects_client_wrapper.js | 3 +- .../spaces_saved_objects_client.js | 101 ++++-- x-pack/scripts/functional_tests.js | 1 + .../test/spaces_api_integration/apis/index.js | 11 + .../apis/saved_objects/bulk_get.js | 175 ++++++++++ .../apis/saved_objects/create.js | 137 ++++++++ .../apis/saved_objects/delete.js | 161 +++++++++ .../apis/saved_objects/find.js | 203 +++++++++++ .../apis/saved_objects/get.js | 110 ++++++ .../apis/saved_objects/index.js | 18 + .../apis/saved_objects/lib/authentication.js | 32 ++ .../saved_objects/lib/space_test_utils.js | 23 ++ .../apis/saved_objects/lib/spaces.js | 20 ++ .../apis/saved_objects/update.js | 186 +++++++++++ x-pack/test/spaces_api_integration/config.js | 57 ++++ .../saved_objects/spaces/data.json.gz | Bin 0 -> 2459 bytes .../saved_objects/spaces/mappings.json | 314 ++++++++++++++++++ 24 files changed, 1559 insertions(+), 57 deletions(-) create mode 100644 x-pack/test/spaces_api_integration/apis/index.js create mode 100644 x-pack/test/spaces_api_integration/apis/saved_objects/bulk_get.js create mode 100644 x-pack/test/spaces_api_integration/apis/saved_objects/create.js create mode 100644 x-pack/test/spaces_api_integration/apis/saved_objects/delete.js create mode 100644 x-pack/test/spaces_api_integration/apis/saved_objects/find.js create mode 100644 x-pack/test/spaces_api_integration/apis/saved_objects/get.js create mode 100644 x-pack/test/spaces_api_integration/apis/saved_objects/index.js create mode 100644 x-pack/test/spaces_api_integration/apis/saved_objects/lib/authentication.js create mode 100644 x-pack/test/spaces_api_integration/apis/saved_objects/lib/space_test_utils.js create mode 100644 x-pack/test/spaces_api_integration/apis/saved_objects/lib/spaces.js create mode 100644 x-pack/test/spaces_api_integration/apis/saved_objects/update.js create mode 100644 x-pack/test/spaces_api_integration/config.js create mode 100644 x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/data.json.gz create mode 100644 x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/mappings.json diff --git a/src/server/saved_objects/service/lib/repository.js b/src/server/saved_objects/service/lib/repository.js index 147d3da7c2532..be0e0598f795b 100644 --- a/src/server/saved_objects/service/lib/repository.js +++ b/src/server/saved_objects/service/lib/repository.js @@ -227,7 +227,7 @@ export class SavedObjectsRepository { sortField, sortOrder, fields, - extraFilters, + extraQueryParams, } = options; if (searchFields && !Array.isArray(searchFields)) { @@ -238,8 +238,8 @@ export class SavedObjectsRepository { throw new TypeError('options.searchFields must be an array'); } - if (extraFilters && !Array.isArray(extraFilters)) { - throw new TypeError('options.extraFilters must be an array'); + if (extraQueryParams && typeof extraQueryParams !== 'object') { + throw new TypeError('options.extraQueryParams must be an object'); } const esOptions = { @@ -256,11 +256,13 @@ export class SavedObjectsRepository { type, sortField, sortOrder, - extraFilters + extraQueryParams }) } }; + console.log('finding with query', JSON.stringify(esOptions)); + const response = await this._callCluster('search', esOptions); if (response.status === 404) { @@ -386,7 +388,7 @@ export class SavedObjectsRepository { ...updatedAt && { updated_at: updatedAt }, version: response._version, ...extraSourceProperties - .map(s => response._source[s]) + .map(s => ({ [s]: response._source[s] })) .reduce((acc, prop) => ({ ...acc, ...prop }), {}), attributes: { ...response._source[type], diff --git a/src/server/saved_objects/service/lib/search_dsl/query_params.js b/src/server/saved_objects/service/lib/search_dsl/query_params.js index 3a1c46cd8f2ca..6cd22e849f955 100644 --- a/src/server/saved_objects/service/lib/search_dsl/query_params.js +++ b/src/server/saved_objects/service/lib/search_dsl/query_params.js @@ -17,6 +17,7 @@ * under the License. */ +import { defaultsDeep } from 'lodash'; import { getRootPropertiesObjects } from '../../../../mappings'; /** @@ -67,24 +68,15 @@ function getFieldsForTypes(searchFields, types) { * @param {Array} searchFields * @return {Object} */ -export function getQueryParams(mappings, type, search, searchFields, extraFilters) { +export function getQueryParams(mappings, type, search, searchFields, extraQueryParams) { if (!type && !search) { return {}; } const bool = {}; - const filters = []; if (type) { - filters.push({ [Array.isArray(type) ? 'terms' : 'term']: { type } }); - } - - if (extraFilters) { - filters.push(...extraFilters); - } - - if (filters.length > 0) { - bool.filter = filters; + bool.filter = [{ [Array.isArray(type) ? 'terms' : 'term']: { type } }]; } if (search) { @@ -102,7 +94,11 @@ export function getQueryParams(mappings, type, search, searchFields, extraFilter ]; } - return { query: { bool } }; + const query = defaultsDeep({ + bool + }, extraQueryParams); + + return { query }; } diff --git a/src/server/saved_objects/service/lib/search_dsl/search_dsl.js b/src/server/saved_objects/service/lib/search_dsl/search_dsl.js index 3023a657e8ca2..270473ada36bd 100644 --- a/src/server/saved_objects/service/lib/search_dsl/search_dsl.js +++ b/src/server/saved_objects/service/lib/search_dsl/search_dsl.js @@ -29,7 +29,7 @@ export function getSearchDsl(mappings, options = {}) { searchFields, sortField, sortOrder, - extraFilters, + extraQueryParams, } = options; if (!type && sortField) { @@ -40,12 +40,12 @@ export function getSearchDsl(mappings, options = {}) { throw Boom.notAcceptable('sortOrder requires a sortField'); } - if (extraFilters && !Array.isArray(extraFilters)) { - throw Boom.notAcceptable('extraFilters must be an array'); + if (extraQueryParams && typeof extraQueryParams !== 'object') { + throw Boom.notAcceptable('extraQueryParams must be an object'); } return { - ...getQueryParams(mappings, type, search, searchFields, extraFilters), + ...getQueryParams(mappings, type, search, searchFields, extraQueryParams), ...getSortingParams(mappings, type, sortField, sortOrder), }; } diff --git a/src/server/saved_objects/service/saved_objects_client.js b/src/server/saved_objects/service/saved_objects_client.js index d6c3afbffc89b..4ad2f25c98cee 100644 --- a/src/server/saved_objects/service/saved_objects_client.js +++ b/src/server/saved_objects/service/saved_objects_client.js @@ -160,8 +160,8 @@ export class SavedObjectsClient { * { id: 'foo', type: 'index-pattern' } * ]) */ - async bulkGet(objects = []) { - return this._repository.bulkGet(objects); + async bulkGet(objects = [], options = {}) { + return this._repository.bulkGet(objects, options); } /** @@ -171,8 +171,8 @@ export class SavedObjectsClient { * @param {string} id * @returns {promise} - { id, type, version, attributes } */ - async get(type, id) { - return this._repository.get(type, id); + async get(type, id, options = {}) { + return this._repository.get(type, id, options); } /** diff --git a/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.js b/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.js index 49c58a5214930..f70539a97dd7e 100644 --- a/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.js +++ b/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.js @@ -100,13 +100,13 @@ export class SecureSavedObjectsClient { return await this._repository.bulkGet(objects, options); } - async get(type, id) { + async get(type, id, options = {}) { await this._performAuthorizationCheck(type, 'get', { type, id, }); - return await this._repository.get(type, id); + return await this._repository.get(type, id, options); } async update(type, id, attributes, options = {}) { diff --git a/x-pack/plugins/spaces/index.js b/x-pack/plugins/spaces/index.js index 83809c27ae0d9..c5109b9730a30 100644 --- a/x-pack/plugins/spaces/index.js +++ b/x-pack/plugins/spaces/index.js @@ -79,8 +79,8 @@ export const spaces = (kibana) => new kibana.Plugin({ const spacesService = createSpacesService(server); server.decorate('server', 'spaces', spacesService); - const { addScopedSavedObjectsClientWrapperFactory } = server.savedObjects; - addScopedSavedObjectsClientWrapperFactory(spacesSavedObjectsClientWrapper(spacesService)); + const { addScopedSavedObjectsClientWrapperFactory, types } = server.savedObjects; + addScopedSavedObjectsClientWrapperFactory(spacesSavedObjectsClientWrapper(spacesService, types)); initSpacesApi(server); diff --git a/x-pack/plugins/spaces/server/lib/create_spaces_service.js b/x-pack/plugins/spaces/server/lib/create_spaces_service.js index d9f83f58f035b..9ca1d73246058 100644 --- a/x-pack/plugins/spaces/server/lib/create_spaces_service.js +++ b/x-pack/plugins/spaces/server/lib/create_spaces_service.js @@ -10,17 +10,17 @@ export function createSpacesService() { const contextCache = new WeakMap(); - function getUrlContext(request) { + function getUrlContext(request, defaultContext = null) { if (!contextCache.has(request)) { - populateCache(request); + populateCache(request, defaultContext); } const { urlContext } = contextCache.get(request); return urlContext; } - function populateCache(request) { - const urlContext = getSpaceUrlContext(request.getBasePath()); + function populateCache(request, defaultContext) { + const urlContext = getSpaceUrlContext(request.getBasePath(), defaultContext); contextCache.set(request, { urlContext diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper.js index e3db9905f68bc..ea67c3c011f18 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper.js @@ -6,10 +6,11 @@ import { SpacesSavedObjectsClient } from './spaces_saved_objects_client'; -export function spacesSavedObjectsClientWrapper(spacesService) { +export function spacesSavedObjectsClientWrapper(spacesService, types) { return ({ client, request }) => new SpacesSavedObjectsClient({ baseClient: client, request, spacesService, + types, }); } diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js index 034eea5890f3f..8f6883382d70c 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js @@ -4,20 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ +import { DEFAULT_SPACE_ID } from '../../../common/constants'; + export class SpacesSavedObjectsClient { constructor(options) { const { request, baseClient, spacesService, + types, } = options; this.errors = baseClient.errors; this._client = baseClient; this._request = request; + this._types = types; - this._spaceUrlContext = spacesService.getUrlContext(this._request); + this._spaceUrlContext = spacesService.getUrlContext(this._request, ''); } async create(type, attributes = {}, options = {}) { @@ -48,21 +52,62 @@ export class SpacesSavedObjectsClient { async find(options = {}) { const spaceOptions = {}; - // TODO(legrego) handle multiple types - let type = options.type; - if (Array.isArray(type)) { - type = type[0]; + let types = options.type || this._types; + if (!Array.isArray(types)) { + types = [types]; } - if (this._isTypeSpaceAware(type)) { - const spaceId = await this._getSpaceId(); - if (spaceId) { - spaceOptions.extraFilters = [{ + const spaceId = await this._getSpaceId(); + console.log('got space id', spaceId); + + let minimumShouldMatch = 0; + + const typeClauses = types.map(t => { + + const shouldFilterOnSpace = this._isTypeSpaceAware(t) && spaceId; + const isDefaultSpace = spaceId === DEFAULT_SPACE_ID; + + const bool = { + must: [] + }; + + if (t) { + minimumShouldMatch = 1; + bool.must.push({ term: { - spaceId + type: t } - }]; + }); + } + + if (shouldFilterOnSpace) { + if (isDefaultSpace) { + bool.must_not = { + exists: { + field: "spaceId" + } + }; + } else { + bool.must.push({ + term: { + spaceId + } + }); + } } + + return { + bool + }; + }); + + if (typeClauses.length > 0) { + spaceOptions.extraQueryParams = { + bool: { + should: typeClauses, + minimum_should_match: minimumShouldMatch + } + }; } return await this._client.find({ ...options, ...spaceOptions }); @@ -100,7 +145,7 @@ export class SpacesSavedObjectsClient { extraSourceProperties: ['spaceId'] }); - const { spaceId: objectSpaceId } = response; + const { spaceId: objectSpaceId = DEFAULT_SPACE_ID } = response; if (objectSpaceId !== thisSpaceId) { throw this._client.errors.createGenericNotFoundError(); @@ -122,20 +167,30 @@ export class SpacesSavedObjectsClient { } async _getSpaceId() { + if (!this._spaceUrlContext) { + return DEFAULT_SPACE_ID; + } + if (!this._spaceId) { - const { - saved_objects: spaces = [] - } = await this.find({ - type: 'space', - search: `"${this._spaceUrlContext}"`, - search_fields: ['urlContext'], - }); - - if (spaces.length > 0) { - this._spaceId = spaces[0].id; - } + this._spaceId = await this._findSpace(this._spaceUrlContext); } return this._spaceId; } + + async _findSpace(urlContext) { + const { + saved_objects: spaces = [] + } = await this._client.find({ + type: 'space', + search: `${urlContext}`, + search_fields: ['urlContext'] + }); + + if (spaces.length > 0) { + return spaces[0].id; + } + + return null; + } } diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 0658878996ec2..e86b73182b943 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -15,4 +15,5 @@ require('@kbn/test').runTestsCli([ require.resolve('../test/api_integration/config.js'), require.resolve('../test/saml_api_integration/config.js'), require.resolve('../test/rbac_api_integration/config.js'), + require.resolve('../test/spaces_api_integration/config.js'), ]); diff --git a/x-pack/test/spaces_api_integration/apis/index.js b/x-pack/test/spaces_api_integration/apis/index.js new file mode 100644 index 0000000000000..3c747f6554132 --- /dev/null +++ b/x-pack/test/spaces_api_integration/apis/index.js @@ -0,0 +1,11 @@ +/* + * 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. + */ + +export default function ({ loadTestFile }) { + describe('apis spaces', () => { + loadTestFile(require.resolve('./saved_objects')); + }); +} diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/bulk_get.js b/x-pack/test/spaces_api_integration/apis/saved_objects/bulk_get.js new file mode 100644 index 0000000000000..12a25607a92ac --- /dev/null +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/bulk_get.js @@ -0,0 +1,175 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from 'expect.js'; +import { AUTHENTICATION } from './lib/authentication'; + +export default function ({ getService }) { + const supertest = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const BULK_REQUESTS = [ + { + type: 'visualization', + id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', + }, + { + type: 'dashboard', + id: 'does not exist', + }, + { + type: 'config', + id: '7.0.0-alpha1', + }, + ]; + + describe('_bulk_get', () => { + const expectResults = resp => { + expect(resp.body).to.eql({ + saved_objects: [ + { + id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', + type: 'visualization', + updated_at: '2017-09-21T18:51:23.794Z', + version: resp.body.saved_objects[0].version, + attributes: { + title: 'Count of requests', + description: '', + version: 1, + // cheat for some of the more complex attributes + visState: resp.body.saved_objects[0].attributes.visState, + uiStateJSON: resp.body.saved_objects[0].attributes.uiStateJSON, + kibanaSavedObjectMeta: + resp.body.saved_objects[0].attributes.kibanaSavedObjectMeta, + }, + }, + { + id: 'does not exist', + type: 'dashboard', + error: { + statusCode: 404, + message: 'Not found', + }, + }, + { + id: '7.0.0-alpha1', + type: 'config', + updated_at: '2017-09-21T18:49:16.302Z', + version: resp.body.saved_objects[2].version, + attributes: { + buildNum: 8467, + defaultIndex: '91200a00-9efd-11e7-acb3-3dab96693fab', + }, + }, + ], + }); + }; + + const expectForbidden = resp => { + //eslint-disable-next-line max-len + const missingActions = `action:login,action:saved_objects/config/bulk_get,action:saved_objects/dashboard/bulk_get,action:saved_objects/visualization/bulk_get`; + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: `Unable to bulk_get config,dashboard,visualization, missing ${missingActions}` + }); + }; + + const bulkGetTest = (description, { auth, tests }) => { + describe(description, () => { + before(() => esArchiver.load('saved_objects/basic')); + after(() => esArchiver.unload('saved_objects/basic')); + + it(`should return ${tests.default.statusCode}`, async () => { + await supertest + .post(`/api/saved_objects/_bulk_get`) + .auth(auth.username, auth.password) + .send(BULK_REQUESTS) + .expect(tests.default.statusCode) + .then(tests.default.response); + }); + }); + }; + + bulkGetTest(`not a kibana user`, { + auth: { + username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME, + password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD, + }, + tests: { + default: { + statusCode: 403, + response: expectForbidden, + } + } + }); + + bulkGetTest(`superuser`, { + auth: { + username: AUTHENTICATION.SUPERUSER.USERNAME, + password: AUTHENTICATION.SUPERUSER.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: expectResults, + }, + } + }); + + bulkGetTest(`kibana legacy user`, { + auth: { + username: AUTHENTICATION.KIBANA_LEGACY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_LEGACY_USER.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: expectResults, + }, + } + }); + + bulkGetTest(`kibana legacy dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: expectResults, + }, + } + }); + + bulkGetTest(`kibana rbac user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: expectResults, + }, + } + }); + + bulkGetTest(`kibana rbac dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: expectResults, + }, + } + }); + }); +} diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/create.js b/x-pack/test/spaces_api_integration/apis/saved_objects/create.js new file mode 100644 index 0000000000000..c8ca5be09b6ad --- /dev/null +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/create.js @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from 'expect.js'; +import { AUTHENTICATION } from './lib/authentication'; + +export default function ({ getService }) { + const supertest = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + describe('create', () => { + const expectResults = (resp) => { + expect(resp.body).to.have.property('id').match(/^[0-9a-f-]{36}$/); + + // loose ISO8601 UTC time with milliseconds validation + expect(resp.body).to.have.property('updated_at').match(/^[\d-]{10}T[\d:\.]{12}Z$/); + + expect(resp.body).to.eql({ + id: resp.body.id, + type: 'visualization', + updated_at: resp.body.updated_at, + version: 1, + attributes: { + title: 'My favorite vis' + } + }); + }; + + const createExpectForbidden = canLogin => resp => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: `Unable to create visualization, missing ${canLogin ? '' : 'action:login,'}action:saved_objects/visualization/create` + }); + }; + + const createTest = (description, { auth, tests }) => { + describe(description, () => { + before(() => esArchiver.load('saved_objects/basic')); + after(() => esArchiver.unload('saved_objects/basic')); + it(`should return ${tests.default.statusCode}`, async () => { + await supertest + .post(`/api/saved_objects/visualization`) + .auth(auth.username, auth.password) + .send({ + attributes: { + title: 'My favorite vis' + } + }) + .expect(tests.default.statusCode) + .then(tests.default.response); + }); + }); + }; + + createTest(`not a kibana user`, { + auth: { + username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME, + password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD, + }, + tests: { + default: { + statusCode: 403, + response: createExpectForbidden(false), + }, + } + }); + + createTest(`superuser`, { + auth: { + username: AUTHENTICATION.SUPERUSER.USERNAME, + password: AUTHENTICATION.SUPERUSER.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: expectResults, + }, + } + }); + + createTest(`kibana legacy user`, { + auth: { + username: AUTHENTICATION.KIBANA_LEGACY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_LEGACY_USER.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: expectResults, + }, + } + }); + + createTest(`kibana legacy dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + default: { + statusCode: 403, + response: createExpectForbidden(true), + }, + } + }); + + createTest(`kibana rbac user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD, + }, + tests: { + default: { + statusCode: 200, + response: expectResults, + }, + } + }); + + createTest(`kibana rbac dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + default: { + statusCode: 403, + response: createExpectForbidden(true), + }, + } + }); + }); +} diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/delete.js b/x-pack/test/spaces_api_integration/apis/saved_objects/delete.js new file mode 100644 index 0000000000000..f89073acca7fd --- /dev/null +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/delete.js @@ -0,0 +1,161 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from 'expect.js'; +import { AUTHENTICATION } from './lib/authentication'; + +export default function ({ getService }) { + const supertest = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + describe('delete', () => { + + const expectEmpty = (resp) => { + expect(resp.body).to.eql({}); + }; + + const expectNotFound = (resp) => { + expect(resp.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found' + }); + }; + + const createExpectForbidden = canLogin => resp => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: `Unable to delete dashboard, missing ${canLogin ? '' : 'action:login,'}action:saved_objects/dashboard/delete` + }); + }; + + const deleteTest = (description, { auth, tests }) => { + describe(description, () => { + before(() => esArchiver.load('saved_objects/basic')); + after(() => esArchiver.unload('saved_objects/basic')); + + it(`should return ${tests.actualId.statusCode} when deleting a doc`, async () => ( + await supertest + .delete(`/api/saved_objects/dashboard/be3733a0-9efe-11e7-acb3-3dab96693fab`) + .auth(auth.username, auth.password) + .expect(tests.actualId.statusCode) + .then(tests.actualId.response) + )); + + it(`should return ${tests.invalidId.statusCode} when deleting an unknown doc`, async () => ( + await supertest + .delete(`/api/saved_objects/dashboard/not-a-real-id`) + .auth(auth.username, auth.password) + .expect(tests.invalidId.statusCode) + .then(tests.invalidId.response) + )); + }); + }; + + deleteTest(`not a kibana user`, { + auth: { + username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME, + password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD, + }, + tests: { + actualId: { + statusCode: 403, + response: createExpectForbidden(false), + }, + invalidId: { + statusCode: 403, + response: createExpectForbidden(false), + } + } + }); + + deleteTest(`superuser`, { + auth: { + username: AUTHENTICATION.SUPERUSER.USERNAME, + password: AUTHENTICATION.SUPERUSER.PASSWORD, + }, + tests: { + actualId: { + statusCode: 200, + response: expectEmpty, + }, + invalidId: { + statusCode: 404, + response: expectNotFound, + } + } + }); + + deleteTest(`kibana legacy user`, { + auth: { + username: AUTHENTICATION.KIBANA_LEGACY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_LEGACY_USER.PASSWORD, + }, + tests: { + actualId: { + statusCode: 200, + response: expectEmpty, + }, + invalidId: { + statusCode: 404, + response: expectNotFound, + } + } + }); + + deleteTest(`kibana legacy dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + actualId: { + statusCode: 403, + response: createExpectForbidden(true), + }, + invalidId: { + statusCode: 403, + response: createExpectForbidden(true), + } + } + }); + + deleteTest(`kibana rbac user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD, + }, + tests: { + actualId: { + statusCode: 200, + response: expectEmpty, + }, + invalidId: { + statusCode: 404, + response: expectNotFound, + } + } + }); + + deleteTest(`kibana rbac dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + actualId: { + statusCode: 403, + response: createExpectForbidden(true), + }, + invalidId: { + statusCode: 403, + response: createExpectForbidden(true), + } + } + }); + }); +} diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/find.js b/x-pack/test/spaces_api_integration/apis/saved_objects/find.js new file mode 100644 index 0000000000000..2edb5a4d771d4 --- /dev/null +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/find.js @@ -0,0 +1,203 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from 'expect.js'; +import { SPACES } from './lib/spaces'; +import { getIdPrefix, getUrlPrefix, getExpectedSpaceIdProperty } from './lib/space_test_utils'; + +export default function ({ getService }) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('find', () => { + + const expectVisualizationResults = (spaceId) => (resp) => { + expect(resp.body).to.eql({ + page: 1, + per_page: 20, + total: 1, + saved_objects: [ + { + type: 'visualization', + id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, + // no space id on the saved object because it wasn't requested + version: 1, + attributes: { + 'title': 'Count of requests' + } + } + ] + }); + }; + + const expectAllResults = (spaceId) => (resp) => { + // TODO(legrego): update once config is space-aware + let objectIndex = 0; + + expect(resp.body).to.eql({ + page: 1, + per_page: 20, + total: 6, + saved_objects: [ + { + id: `${getIdPrefix(spaceId)}91200a00-9efd-11e7-acb3-3dab96693fab`, + type: 'index-pattern', + updated_at: '2017-09-21T18:49:16.270Z', + version: 1, + attributes: resp.body.saved_objects[objectIndex++].attributes + }, + // { + // id: '7.0.0-alpha1', + // type: 'config', + // updated_at: '2017-09-21T18:49:16.302Z', + // version: 1, + // ...getExpectedSpaceIdProperty(spaceId), + // attributes: resp.body.saved_objects[objectIndex++].attributes + // }, + { + id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, + type: 'visualization', + updated_at: '2017-09-21T18:51:23.794Z', + version: 1, + attributes: resp.body.saved_objects[objectIndex++].attributes + }, + + { + id: `${getIdPrefix(spaceId)}be3733a0-9efe-11e7-acb3-3dab96693fab`, + type: 'dashboard', + updated_at: '2017-09-21T18:57:40.826Z', + version: 1, + attributes: resp.body.saved_objects[objectIndex++].attributes + }, + + { + id: `default`, + type: 'space', + updated_at: '2017-09-21T18:49:16.270Z', + version: 1, + attributes: resp.body.saved_objects[objectIndex++].attributes + }, + { + id: `space_1`, + type: 'space', + updated_at: '2017-09-21T18:49:16.270Z', + version: 1, + attributes: resp.body.saved_objects[objectIndex++].attributes + }, + { + id: `space_2`, + type: 'space', + updated_at: '2017-09-21T18:49:16.270Z', + version: 1, + attributes: resp.body.saved_objects[objectIndex++].attributes + }, + ] + }); + }; + + const createExpectEmpty = (page, perPage, total) => (resp) => { + expect(resp.body).to.eql({ + page: page, + per_page: perPage, + total: total, + saved_objects: [] + }); + }; + + const findTest = (description, { urlContext, tests }) => { + describe(description, () => { + before(() => esArchiver.load('saved_objects/spaces')); + after(() => esArchiver.unload('saved_objects/spaces')); + + // it(`should return ${tests.normal.statusCode} with ${tests.normal.description}`, async () => ( + // await supertest + // .get(`${getUrlPrefix(urlContext)}/api/saved_objects/_find?type=visualization&fields=title`) + // .expect(tests.normal.statusCode) + // .then(tests.normal.response) + // )); + + // describe('page beyond total', () => { + // it(`should return ${tests.pageBeyondTotal.statusCode} with ${tests.pageBeyondTotal.description}`, async () => ( + // await supertest + // .get(`${getUrlPrefix(urlContext)}/api/saved_objects/_find?type=visualization&page=100&per_page=100`) + // .expect(tests.pageBeyondTotal.statusCode) + // .then(tests.pageBeyondTotal.response) + // )); + // }); + + describe('unknown search field', () => { + it(`should return ${tests.unknownSearchField.statusCode} with ${tests.unknownSearchField.description}`, async () => ( + await supertest + .get(`${getUrlPrefix(urlContext)}/api/saved_objects/_find?type=wigwags&search_fields=a`) + .expect(tests.unknownSearchField.statusCode) + .then(tests.unknownSearchField.response) + )); + }); + + // describe('no type', () => { + // it(`should return ${tests.noType.statusCode} with ${tests.noType.description}`, async () => ( + // await supertest + // .get(`${getUrlPrefix(urlContext)}/api/saved_objects/_find`) + // .expect(tests.noType.statusCode) + // .then(tests.noType.response) + // )); + // }); + }); + }; + + findTest(`objects only within the current space (space_1)`, { + ...SPACES.SPACE_1, + tests: { + normal: { + description: 'only the visualization', + statusCode: 200, + response: expectVisualizationResults(SPACES.SPACE_1.spaceId), + }, + pageBeyondTotal: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(100, 100, 1), + }, + unknownSearchField: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(1, 20, 0), + }, + noType: { + description: 'all objects', + statusCode: 200, + response: expectAllResults(SPACES.SPACE_1.spaceId), + }, + } + }); + + // findTest(`objects only within the current space (default)`, { + // ...SPACES.DEFAULT, + // tests: { + // normal: { + // description: 'only the visualization', + // statusCode: 200, + // response: expectVisualizationResults(SPACES.DEFAULT.spaceId), + // }, + // pageBeyondTotal: { + // description: 'empty result', + // statusCode: 200, + // response: createExpectEmpty(100, 100, 1), + // }, + // unknownSearchField: { + // description: 'empty result', + // statusCode: 200, + // response: createExpectEmpty(1, 20, 0), + // }, + // noType: { + // description: 'all objects', + // statusCode: 200, + // response: expectAllResults(SPACES.DEFAULT.spaceId), + // }, + // } + // }); + }); +} diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/get.js b/x-pack/test/spaces_api_integration/apis/saved_objects/get.js new file mode 100644 index 0000000000000..2516752cbff17 --- /dev/null +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/get.js @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from 'expect.js'; +import { getIdPrefix, getUrlPrefix } from './lib/space_test_utils'; +import { SPACES } from './lib/spaces'; + +export default function ({ getService }) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('get', () => { + + const expectResults = (spaceId) => (resp) => { + + // The default space does not assign a space id. + const expectedSpaceId = spaceId === 'default' ? undefined : spaceId; + + const expectedBody = { + id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, + type: 'visualization', + updated_at: '2017-09-21T18:51:23.794Z', + version: resp.body.version, + + attributes: { + title: 'Count of requests', + description: '', + version: 1, + // cheat for some of the more complex attributes + visState: resp.body.attributes.visState, + uiStateJSON: resp.body.attributes.uiStateJSON, + kibanaSavedObjectMeta: resp.body.attributes.kibanaSavedObjectMeta + } + }; + + if (expectedSpaceId) { + expectedBody.spaceId = expectedSpaceId; + } + + expect(resp.body).to.eql(expectedBody); + }; + + const expectNotFound = (resp) => { + expect(resp.body).to.eql({ + error: 'Not Found', + message: 'Not Found', + statusCode: 404, + }); + }; + + const getTest = (description, { spaceId, urlContext = '', tests }) => { + describe(description, () => { + before(async () => esArchiver.load(`saved_objects/spaces`)); + after(async () => esArchiver.unload(`saved_objects/spaces`)); + + it(`should return ${tests.exists.statusCode}`, async () => { + return supertest + .get(`${getUrlPrefix(urlContext)}/api/saved_objects/visualization/${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`) + .expect(tests.exists.statusCode) + .then(tests.exists.response); + }); + }); + }; + + getTest(`can access objects belonging to the current space (space_1)`, { + ...SPACES.SPACE_1, + tests: { + exists: { + statusCode: 200, + response: expectResults(SPACES.SPACE_1.spaceId), + }, + } + }); + + getTest(`cannot access objects belonging to a different space (space_1)`, { + ...SPACES.SPACE_1, + urlContext: 'space-2', + tests: { + exists: { + statusCode: 404, + response: expectNotFound + }, + } + }); + + getTest(`can access objects belonging to the current space (default)`, { + ...SPACES.DEFAULT, + tests: { + exists: { + statusCode: 200, + response: expectResults(SPACES.DEFAULT.spaceId), + }, + } + }); + + getTest(`cannot access objects belonging to a different space (default)`, { + ...SPACES.DEFAULT, + urlContext: 'space-1', + tests: { + exists: { + statusCode: 404, + response: expectNotFound + }, + } + }); + }); +} diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/index.js b/x-pack/test/spaces_api_integration/apis/saved_objects/index.js new file mode 100644 index 0000000000000..91ef79f25b075 --- /dev/null +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/index.js @@ -0,0 +1,18 @@ +/* + * 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. + */ + + +export default function ({ loadTestFile, getService }) { + + describe('saved_objects', () => { + // loadTestFile(require.resolve('./bulk_get')); + // loadTestFile(require.resolve('./create')); + // loadTestFile(require.resolve('./delete')); + loadTestFile(require.resolve('./find')); + //loadTestFile(require.resolve('./get')); + // loadTestFile(require.resolve('./update')); + }); +} diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/lib/authentication.js b/x-pack/test/spaces_api_integration/apis/saved_objects/lib/authentication.js new file mode 100644 index 0000000000000..8b140fd3b2a30 --- /dev/null +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/lib/authentication.js @@ -0,0 +1,32 @@ +/* + * 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. + */ + +export const AUTHENTICATION = { + NOT_A_KIBANA_USER: { + USERNAME: 'not_a_kibana_user', + PASSWORD: 'password' + }, + SUPERUSER: { + USERNAME: 'elastic', + PASSWORD: 'changeme' + }, + KIBANA_LEGACY_USER: { + USERNAME: 'a_kibana_legacy_user', + PASSWORD: 'password' + }, + KIBANA_LEGACY_DASHBOARD_ONLY_USER: { + USERNAME: 'a_kibana_legacy_dashboard_only_user', + PASSWORD: 'password' + }, + KIBANA_RBAC_USER: { + USERNAME: 'a_kibana_rbac_user', + PASSWORD: 'password' + }, + KIBANA_RBAC_DASHBOARD_ONLY_USER: { + USERNAME: 'a_kibana_rbac_dashboard_only_user', + PASSWORD: 'password' + } +}; diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/lib/space_test_utils.js b/x-pack/test/spaces_api_integration/apis/saved_objects/lib/space_test_utils.js new file mode 100644 index 0000000000000..186b75ccacf88 --- /dev/null +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/lib/space_test_utils.js @@ -0,0 +1,23 @@ +/* + * 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. + */ + +export function getUrlPrefix(urlContext) { + return urlContext ? `/s/${urlContext}` : ``; +} + +// Spaces do not actually prefix the ID, but this simplifies testing positive and negative flows. +export function getIdPrefix(spaceId) { + return spaceId === 'default' ? '' : `${spaceId}-`; +} + +export function getExpectedSpaceIdProperty(spaceId) { + if (spaceId === 'default') { + return {}; + } + return { + spaceId + }; +} diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/lib/spaces.js b/x-pack/test/spaces_api_integration/apis/saved_objects/lib/spaces.js new file mode 100644 index 0000000000000..f5cd9109c9a9e --- /dev/null +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/lib/spaces.js @@ -0,0 +1,20 @@ +/* + * 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. + */ + +export const SPACES = { + SPACE_1: { + spaceId: 'space_1', + urlContext: 'space-1' + }, + SPACE_2: { + spaceId: 'space_2', + urlContext: 'space-2' + }, + DEFAULT: { + spaceId: 'default', + urlContext: '' + } +}; diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/update.js b/x-pack/test/spaces_api_integration/apis/saved_objects/update.js new file mode 100644 index 0000000000000..ae299348847d6 --- /dev/null +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/update.js @@ -0,0 +1,186 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from 'expect.js'; +import { AUTHENTICATION } from './lib/authentication'; + +export default function ({ getService }) { + const supertest = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + describe('update', () => { + const expectResults = resp => { + // loose uuid validation + expect(resp.body).to.have.property('id').match(/^[0-9a-f-]{36}$/); + + // loose ISO8601 UTC time with milliseconds validation + expect(resp.body).to.have.property('updated_at').match(/^[\d-]{10}T[\d:\.]{12}Z$/); + + expect(resp.body).to.eql({ + id: resp.body.id, + type: 'visualization', + updated_at: resp.body.updated_at, + version: 2, + attributes: { + title: 'My second favorite vis' + } + }); + }; + + const expectNotFound = resp => { + expect(resp.body).eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found' + }); + }; + + const createExpectForbidden = canLogin => resp => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: `Unable to update visualization, missing ${canLogin ? '' : 'action:login,'}action:saved_objects/visualization/update` + }); + }; + + const updateTest = (description, { auth, tests }) => { + describe(description, () => { + before(() => esArchiver.load('saved_objects/basic')); + after(() => esArchiver.unload('saved_objects/basic')); + it(`should return ${tests.exists.statusCode}`, async () => { + await supertest + .put(`/api/saved_objects/visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab`) + .auth(auth.username, auth.password) + .send({ + attributes: { + title: 'My second favorite vis' + } + }) + .expect(tests.exists.statusCode) + .then(tests.exists.response); + }); + + describe('unknown id', () => { + it(`should return ${tests.doesntExist.statusCode}`, async () => { + await supertest + .put(`/api/saved_objects/visualization/not an id`) + .auth(auth.username, auth.password) + .send({ + attributes: { + title: 'My second favorite vis' + } + }) + .expect(tests.doesntExist.statusCode) + .then(tests.doesntExist.response); + }); + }); + }); + }; + + updateTest(`not a kibana user`, { + auth: { + username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME, + password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD, + }, + tests: { + exists: { + statusCode: 403, + response: createExpectForbidden(false), + }, + doesntExist: { + statusCode: 403, + response: createExpectForbidden(false), + }, + } + }); + + updateTest(`superuser`, { + auth: { + username: AUTHENTICATION.SUPERUSER.USERNAME, + password: AUTHENTICATION.SUPERUSER.PASSWORD, + }, + tests: { + exists: { + statusCode: 200, + response: expectResults, + }, + doesntExist: { + statusCode: 404, + response: expectNotFound, + }, + } + }); + + updateTest(`kibana legacy user`, { + auth: { + username: AUTHENTICATION.KIBANA_LEGACY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_LEGACY_USER.PASSWORD, + }, + tests: { + exists: { + statusCode: 200, + response: expectResults, + }, + doesntExist: { + statusCode: 404, + response: expectNotFound, + }, + } + }); + + updateTest(`kibana legacy dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + exists: { + statusCode: 403, + response: createExpectForbidden(true), + }, + doesntExist: { + statusCode: 403, + response: createExpectForbidden(true), + }, + } + }); + + updateTest(`kibana rbac user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD, + }, + tests: { + exists: { + statusCode: 200, + response: expectResults, + }, + doesntExist: { + statusCode: 404, + response: expectNotFound, + }, + } + }); + + updateTest(`kibana rbac dashboard only user`, { + auth: { + username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME, + password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD, + }, + tests: { + exists: { + statusCode: 403, + response: createExpectForbidden(true), + }, + doesntExist: { + statusCode: 403, + response: createExpectForbidden(true), + }, + } + }); + + }); +} diff --git a/x-pack/test/spaces_api_integration/config.js b/x-pack/test/spaces_api_integration/config.js new file mode 100644 index 0000000000000..d93381cbd76db --- /dev/null +++ b/x-pack/test/spaces_api_integration/config.js @@ -0,0 +1,57 @@ +/* + * 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 path from 'path'; +import { resolveKibanaPath } from '@kbn/plugin-helpers'; + +export default async function ({ readConfigFile }) { + + const config = { + kibana: { + api: await readConfigFile(resolveKibanaPath('test/api_integration/config.js')), + common: await readConfigFile(require.resolve('../../../test/common/config.js')), + functional: await readConfigFile(require.resolve('../../../test/functional/config.js')) + }, + xpack: { + api: await readConfigFile(require.resolve('../api_integration/config.js')), + functional: await readConfigFile(require.resolve('../functional/config.js')) + } + }; + + return { + testFiles: [require.resolve('./apis')], + servers: config.xpack.api.get('servers'), + services: { + es: config.kibana.common.get('services.es'), + supertest: config.kibana.api.get('services.supertest'), + supertestWithoutAuth: config.xpack.api.get('services.supertestWithoutAuth'), + esArchiver: config.kibana.functional.get('services.esArchiver'), + }, + junit: { + reportName: 'X-Pack Spaces API Integration Tests', + }, + + esArchiver: { + directory: path.join(__dirname, 'fixtures', 'es_archiver') + }, + + esTestCluster: { + ...config.xpack.api.get('esTestCluster'), + serverArgs: [ + ...config.xpack.api.get('esTestCluster.serverArgs'), + ], + }, + + kbnTestServer: { + ...config.xpack.api.get('kbnTestServer'), + serverArgs: [ + ...config.xpack.api.get('kbnTestServer.serverArgs'), + '--optimize.enabled=false', + '--server.xsrf.disableProtection=true', + ], + }, + }; +} diff --git a/x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/data.json.gz b/x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..9630865e700ebb33563399ec0911053329515cf4 GIT binary patch literal 2459 zcmYjLc{~%09~MiIC}Wi5XvVybOiIoPnYo6!D(4(CtC3tem-i+2eQb^#BU_On$16vU z$g!8q-BikvBXX51Z~684yuW{*@AEvL=lOoWVsXch%Z7d8`zLrs(Rr+m62KLRbibyM5YHRex*zOG81B5_6%@Yi52 zBIWZQjR~^O8Zm%K9ryniDD#8APWB7Y$`A8>KYO zeY4IQ8?qp6`J z6h}D7$G?hu5S~0KT0}$I5Mfp) zvISzm;;}sEZ1;6)TeVi>?drvF*1}$RdlBAqXT0g}zu2XimW>IU+JCR=)KI{9 z37I!~^CctC!R1OZg$-iq?=c_X+*)bD`ZLy`4Nxn9YiOhLFI8Ln78s~Wx0u9iyr>%I z!OQiAJyu2xkQt$S0)S!4I~w9Z<6 z_6pM_2Syo$yLkAl|J{PGT`XUWimLL4+i~mVo7rxiM77er#qFR{I(;y;_Ynx(MIBdU zvHi9iR(=!`)8={TIc>=}}SE%xbHYt6=SZd4&eUSw2AL zF&cF-uIa|y*0OFXr=AQ5%rpz?E{%`EtPAkJ;^;O=p6vuX!?XZpshr%*;>b-^3AaIm zWLbWqhb0Ek2Ts}>Y1FTD^>=QYO)DvI_@T+Z%BH%3=_wK(uuRxg1kr@G3PBLO zmXzf^9xtJ0PB30!c}_SvsD_Q`TR-{jiQ;2a`-6Tb3I1*XE-+%-vaT=0W9FD1+kI>m z=cgmyLB~bb1#S5v?8a_x@b+*x4K2Z1(>&ql*|I9@u8F2cjQ^S2^K#~^b{s2giLPW1 zIwhCr%24L&Q8dy^wylchV{775_zHKyhQDi7z#I{jBzAUOI~p+sVHv_rOI|LGTR1vy zhFG>%L*w=aThFM5rhZZidPRIuH3X5)ib@<+Yd~+VoUF4s?gqBI>hNy~DTRyfIID7ZGlM^zp)<1o!R#a~2DiVQpRVb=7mz*d(J@7q zVtpKPc&?nXtn73@!l;Dcc^0QltzDUr&b;quA2q*u+vu(&0tfomPMbhvD+-y|7ZCV; zd=7|i352sr7T>lKOS#PIEB6odsR}v$rsOc~sqs6r6tbwrh1}7L;{lDU3l36x6$gP; zd=mHi|NNt~*No)aM-Yk5;~?3MN~_SevtzE`X#d59O9z|v+j?6uMmzeT zM(NvXu6N$4gv>jB&cj*f(7)S=xE?KDGis{*f&GGwE>3#daTQNRtd{R=88!l;D{j&Q z&Ev0ggX?l#%7Bsf`3HpynQxZjwqH5csxrl=V5h+~R%JP;lyyq3OLSMx1f`d=b{*^- z*cs%K6xEkzC>&OBy1%VdhCb>zaUIkjtnb^yNJjIOJ=5m@T26oY)Y$w)Z}~0bOUi~% z{GPoc&YpWMUJ7lf&`RT%v-RF3nN$u}P66A=^OqAzXbqaS3_d~8GkxS(&V*0rdGvF6 zkg4E}H3GuZ+Cs)*cJ*4)qh`Wr(2P-gx*JAxs>?4kz>KyoU6|Mav=_FvB!}ktF|H_! zfWw>j_Nd(HuogwS)su>GjH`ZXdSY^)$;F)Vt2`{OE_8R**zJtWpoP@81FP$w!Y`r3 zsFHbD!rcTG`PM}I5c`eLXXRG|O&y-g^@olr$cpaktBmf9E@CDXB<#>*$5Px`LPz@X z16ujT7xGJdizA#!f$?U&c;6Wa^0D?5EVC!3(%Mt|z08 zvRjEe^za@^h|Hm^?G^YX0snzOoxb2h8z-QZcr6@qXxqw~|7to_#mJtT{BU}QYJet= zu}2m#^b5H=cy09rALg0g7jE=L6XAl#$8NPvasTvtuL82V4tupP`G0T6i=CvwLkO~` z7EM+59skf{di24e`cJDlX^4VY!2bhUmfsI_tQbO9h%A1icNCZbL3vJp9{U<=0X*+b z<>D)WWIp06U;>aPF9i6ICWmYZ^8IiYFgq3{BEggh3!sb&T`&`cV=sYRz9N7$d8$V| z^z2Iyz5k$~UjFAM_MD&jqcHPd`BsaVU(q9JCh6oc-lTyf>mp_cNEqPvox23$-zteT zS&DztK9*Pf>xb4^{w?d{B@hROM7>7h;fa&~JBz}(>|#;4>BcNFHV?PMOMz&-w*bm} r{eJXk_czlsz_oG6sSNhKmr*f)sYm5XCkMj(_f8rCmTmfb*x3FBdA^hP literal 0 HcmV?d00001 diff --git a/x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/mappings.json b/x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/mappings.json new file mode 100644 index 0000000000000..2e1a6d1f65fd9 --- /dev/null +++ b/x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/mappings.json @@ -0,0 +1,314 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "settings": { + "index": { + "number_of_shards": "1", + "auto_expand_replicas": "0-1", + "number_of_replicas": "0" + } + }, + "mappings": { + "doc": { + "dynamic": "strict", + "properties": { + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + }, + "defaultIndex": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "spaceId": { + "type": "keyword" + }, + "space": { + "properties": { + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + }, + "urlContext": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "color": { + "type": "keyword" + }, + "_reserved": { + "type": "boolean" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + } + } + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + } + }, + "aliases": {} + } +} From 858eff0f3927e3908552b39c8243ccd5cad259b3 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 22 Jun 2018 15:36:32 -0400 Subject: [PATCH 10/41] working get & find functional tests --- .../apis/saved_objects/find.js | 211 +++++++++--------- .../apis/saved_objects/index.js | 2 +- 2 files changed, 107 insertions(+), 106 deletions(-) diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/find.js b/x-pack/test/spaces_api_integration/apis/saved_objects/find.js index 2edb5a4d771d4..e7527df9b3563 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/find.js +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/find.js @@ -6,7 +6,7 @@ import expect from 'expect.js'; import { SPACES } from './lib/spaces'; -import { getIdPrefix, getUrlPrefix, getExpectedSpaceIdProperty } from './lib/space_test_utils'; +import { getIdPrefix, getUrlPrefix } from './lib/space_test_utils'; export default function ({ getService }) { const supertest = getService('supertest'); @@ -23,7 +23,7 @@ export default function ({ getService }) { { type: 'visualization', id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, - // no space id on the saved object because it wasn't requested + // no space id on the saved object because the field is not requested as part of a find operation version: 1, attributes: { 'title': 'Count of requests' @@ -35,66 +35,67 @@ export default function ({ getService }) { const expectAllResults = (spaceId) => (resp) => { // TODO(legrego): update once config is space-aware - let objectIndex = 0; + + const sortById = ({ id: id1 }, { id: id2 }) => id1 < id2 ? -1 : 1; + + resp.body.saved_objects.sort(sortById); + + const expectedSavedObjects = [{ + id: `default`, + type: 'space', + updated_at: '2017-09-21T18:49:16.270Z', + version: 1, + }, + { + id: `space_1`, + type: 'space', + updated_at: '2017-09-21T18:49:16.270Z', + version: 1, + }, + { + id: `space_2`, + type: 'space', + updated_at: '2017-09-21T18:49:16.270Z', + version: 1, + }, + { + id: `${getIdPrefix(spaceId)}91200a00-9efd-11e7-acb3-3dab96693fab`, + type: 'index-pattern', + updated_at: '2017-09-21T18:49:16.270Z', + version: 1, + }, + // { + // id: '7.0.0-alpha1', + // type: 'config', + // updated_at: '2017-09-21T18:49:16.302Z', + // version: 1, + // ...getExpectedSpaceIdProperty(spaceId), + // attributes: resp.body.saved_objects[objectIndex++].attributes + // }, + { + id: `${getIdPrefix(spaceId)}be3733a0-9efe-11e7-acb3-3dab96693fab`, + type: 'dashboard', + updated_at: '2017-09-21T18:57:40.826Z', + version: 1, + }, + { + id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, + type: 'visualization', + updated_at: '2017-09-21T18:51:23.794Z', + version: 1, + }] + .sort(sortById); + + expectedSavedObjects.forEach((object, index) => { + object.attributes = resp.body.saved_objects[index].attributes; + }); + expect(resp.body).to.eql({ page: 1, per_page: 20, total: 6, - saved_objects: [ - { - id: `${getIdPrefix(spaceId)}91200a00-9efd-11e7-acb3-3dab96693fab`, - type: 'index-pattern', - updated_at: '2017-09-21T18:49:16.270Z', - version: 1, - attributes: resp.body.saved_objects[objectIndex++].attributes - }, - // { - // id: '7.0.0-alpha1', - // type: 'config', - // updated_at: '2017-09-21T18:49:16.302Z', - // version: 1, - // ...getExpectedSpaceIdProperty(spaceId), - // attributes: resp.body.saved_objects[objectIndex++].attributes - // }, - { - id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, - type: 'visualization', - updated_at: '2017-09-21T18:51:23.794Z', - version: 1, - attributes: resp.body.saved_objects[objectIndex++].attributes - }, - - { - id: `${getIdPrefix(spaceId)}be3733a0-9efe-11e7-acb3-3dab96693fab`, - type: 'dashboard', - updated_at: '2017-09-21T18:57:40.826Z', - version: 1, - attributes: resp.body.saved_objects[objectIndex++].attributes - }, - - { - id: `default`, - type: 'space', - updated_at: '2017-09-21T18:49:16.270Z', - version: 1, - attributes: resp.body.saved_objects[objectIndex++].attributes - }, - { - id: `space_1`, - type: 'space', - updated_at: '2017-09-21T18:49:16.270Z', - version: 1, - attributes: resp.body.saved_objects[objectIndex++].attributes - }, - { - id: `space_2`, - type: 'space', - updated_at: '2017-09-21T18:49:16.270Z', - version: 1, - attributes: resp.body.saved_objects[objectIndex++].attributes - }, - ] + saved_objects: expectedSavedObjects, }); }; @@ -112,21 +113,21 @@ export default function ({ getService }) { before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); - // it(`should return ${tests.normal.statusCode} with ${tests.normal.description}`, async () => ( - // await supertest - // .get(`${getUrlPrefix(urlContext)}/api/saved_objects/_find?type=visualization&fields=title`) - // .expect(tests.normal.statusCode) - // .then(tests.normal.response) - // )); - - // describe('page beyond total', () => { - // it(`should return ${tests.pageBeyondTotal.statusCode} with ${tests.pageBeyondTotal.description}`, async () => ( - // await supertest - // .get(`${getUrlPrefix(urlContext)}/api/saved_objects/_find?type=visualization&page=100&per_page=100`) - // .expect(tests.pageBeyondTotal.statusCode) - // .then(tests.pageBeyondTotal.response) - // )); - // }); + it(`should return ${tests.normal.statusCode} with ${tests.normal.description}`, async () => ( + await supertest + .get(`${getUrlPrefix(urlContext)}/api/saved_objects/_find?type=visualization&fields=title`) + .expect(tests.normal.statusCode) + .then(tests.normal.response) + )); + + describe('page beyond total', () => { + it(`should return ${tests.pageBeyondTotal.statusCode} with ${tests.pageBeyondTotal.description}`, async () => ( + await supertest + .get(`${getUrlPrefix(urlContext)}/api/saved_objects/_find?type=visualization&page=100&per_page=100`) + .expect(tests.pageBeyondTotal.statusCode) + .then(tests.pageBeyondTotal.response) + )); + }); describe('unknown search field', () => { it(`should return ${tests.unknownSearchField.statusCode} with ${tests.unknownSearchField.description}`, async () => ( @@ -137,14 +138,14 @@ export default function ({ getService }) { )); }); - // describe('no type', () => { - // it(`should return ${tests.noType.statusCode} with ${tests.noType.description}`, async () => ( - // await supertest - // .get(`${getUrlPrefix(urlContext)}/api/saved_objects/_find`) - // .expect(tests.noType.statusCode) - // .then(tests.noType.response) - // )); - // }); + describe('no type', () => { + it(`should return ${tests.noType.statusCode} with ${tests.noType.description}`, async () => ( + await supertest + .get(`${getUrlPrefix(urlContext)}/api/saved_objects/_find`) + .expect(tests.noType.statusCode) + .then(tests.noType.response) + )); + }); }); }; @@ -174,30 +175,30 @@ export default function ({ getService }) { } }); - // findTest(`objects only within the current space (default)`, { - // ...SPACES.DEFAULT, - // tests: { - // normal: { - // description: 'only the visualization', - // statusCode: 200, - // response: expectVisualizationResults(SPACES.DEFAULT.spaceId), - // }, - // pageBeyondTotal: { - // description: 'empty result', - // statusCode: 200, - // response: createExpectEmpty(100, 100, 1), - // }, - // unknownSearchField: { - // description: 'empty result', - // statusCode: 200, - // response: createExpectEmpty(1, 20, 0), - // }, - // noType: { - // description: 'all objects', - // statusCode: 200, - // response: expectAllResults(SPACES.DEFAULT.spaceId), - // }, - // } - // }); + findTest(`objects only within the current space (default)`, { + ...SPACES.DEFAULT, + tests: { + normal: { + description: 'only the visualization', + statusCode: 200, + response: expectVisualizationResults(SPACES.DEFAULT.spaceId), + }, + pageBeyondTotal: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(100, 100, 1), + }, + unknownSearchField: { + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(1, 20, 0), + }, + noType: { + description: 'all objects', + statusCode: 200, + response: expectAllResults(SPACES.DEFAULT.spaceId), + }, + } + }); }); } diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/index.js b/x-pack/test/spaces_api_integration/apis/saved_objects/index.js index 91ef79f25b075..63a894816a910 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/index.js +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/index.js @@ -12,7 +12,7 @@ export default function ({ loadTestFile, getService }) { // loadTestFile(require.resolve('./create')); // loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./find')); - //loadTestFile(require.resolve('./get')); + loadTestFile(require.resolve('./get')); // loadTestFile(require.resolve('./update')); }); } From 1b95aa093ae0b3bf2e175a2d0c9873c93e364d3c Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 22 Jun 2018 16:24:29 -0400 Subject: [PATCH 11/41] added bulk_get tests --- .../saved_objects/service/lib/repository.js | 2 - .../spaces_saved_objects_client.js | 15 +- .../apis/saved_objects/bulk_get.js | 151 +++++++----------- .../apis/saved_objects/index.js | 2 +- 4 files changed, 73 insertions(+), 97 deletions(-) diff --git a/src/server/saved_objects/service/lib/repository.js b/src/server/saved_objects/service/lib/repository.js index be0e0598f795b..4cc99933f2a71 100644 --- a/src/server/saved_objects/service/lib/repository.js +++ b/src/server/saved_objects/service/lib/repository.js @@ -261,8 +261,6 @@ export class SavedObjectsRepository { } }; - console.log('finding with query', JSON.stringify(esOptions)); - const response = await this._callCluster('search', esOptions); if (response.status === 404) { diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js index 8f6883382d70c..e85aedfa5da24 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js @@ -121,13 +121,20 @@ export class SpacesSavedObjectsClient { extraSourceProperties: ['spaceId', 'type'] }); - result.saved_objects = result.saved_objects.filter(savedObject => { - const { type, spaceId } = savedObject; + result.saved_objects = result.saved_objects.map(savedObject => { + const { id, type, spaceId } = savedObject; if (this._isTypeSpaceAware(type)) { - return spaceId === thisSpaceId; + if (spaceId !== thisSpaceId) { + return { + id, + type, + error: { statusCode: 404, message: 'Not found' } + }; + } } - return true; + + return savedObject; }); return result; diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/bulk_get.js b/x-pack/test/spaces_api_integration/apis/saved_objects/bulk_get.js index 12a25607a92ac..3d747051d8c43 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/bulk_get.js +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/bulk_get.js @@ -5,10 +5,11 @@ */ import expect from 'expect.js'; -import { AUTHENTICATION } from './lib/authentication'; +import { SPACES } from './lib/spaces'; +import { getIdPrefix, getUrlPrefix, getExpectedSpaceIdProperty } from './lib/space_test_utils'; export default function ({ getService }) { - const supertest = getService('supertestWithoutAuth'); + const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); const BULK_REQUESTS = [ @@ -26,15 +27,53 @@ export default function ({ getService }) { }, ]; + const createBulkRequests = (spaceId) => BULK_REQUESTS.map(r => ({ + ...r, + id: `${getIdPrefix(spaceId)}${r.id}` + })); + describe('_bulk_get', () => { - const expectResults = resp => { + const expectNotFoundResults = (spaceId) => resp => { expect(resp.body).to.eql({ saved_objects: [ { - id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', + id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, + type: 'visualization', + error: { + statusCode: 404, + message: 'Not found', + }, + }, + { + id: `${getIdPrefix(spaceId)}does not exist`, + type: 'dashboard', + error: { + statusCode: 404, + message: 'Not found', + }, + }, + //todo(legrego) fix when config is space aware + { + id: `${getIdPrefix(spaceId)}7.0.0-alpha1`, + type: 'config', + error: { + statusCode: 404, + message: 'Not found', + }, + }, + ], + }); + }; + + const expectResults = (spaceId) => resp => { + expect(resp.body).to.eql({ + saved_objects: [ + { + id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`, type: 'visualization', updated_at: '2017-09-21T18:51:23.794Z', version: resp.body.saved_objects[0].version, + ...getExpectedSpaceIdProperty(spaceId), attributes: { title: 'Count of requests', description: '', @@ -47,129 +86,61 @@ export default function ({ getService }) { }, }, { - id: 'does not exist', + id: `${getIdPrefix(spaceId)}does not exist`, type: 'dashboard', error: { statusCode: 404, message: 'Not found', }, }, + //todo(legrego) fix when config is space aware { - id: '7.0.0-alpha1', + id: `${getIdPrefix(spaceId)}7.0.0-alpha1`, type: 'config', - updated_at: '2017-09-21T18:49:16.302Z', - version: resp.body.saved_objects[2].version, - attributes: { - buildNum: 8467, - defaultIndex: '91200a00-9efd-11e7-acb3-3dab96693fab', + error: { + statusCode: 404, + message: 'Not found', }, }, ], }); }; - const expectForbidden = resp => { - //eslint-disable-next-line max-len - const missingActions = `action:login,action:saved_objects/config/bulk_get,action:saved_objects/dashboard/bulk_get,action:saved_objects/visualization/bulk_get`; - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unable to bulk_get config,dashboard,visualization, missing ${missingActions}` - }); - }; - - const bulkGetTest = (description, { auth, tests }) => { + const bulkGetTest = (description, { spaceId, urlContext, tests }) => { describe(description, () => { - before(() => esArchiver.load('saved_objects/basic')); - after(() => esArchiver.unload('saved_objects/basic')); + before(() => esArchiver.load('saved_objects/spaces')); + after(() => esArchiver.unload('saved_objects/spaces')); it(`should return ${tests.default.statusCode}`, async () => { await supertest - .post(`/api/saved_objects/_bulk_get`) - .auth(auth.username, auth.password) - .send(BULK_REQUESTS) + .post(`${getUrlPrefix(urlContext)}/api/saved_objects/_bulk_get`) + .send(createBulkRequests(spaceId)) .expect(tests.default.statusCode) .then(tests.default.response); }); }); }; - bulkGetTest(`not a kibana user`, { - auth: { - username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME, - password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD, - }, - tests: { - default: { - statusCode: 403, - response: expectForbidden, - } - } - }); - - bulkGetTest(`superuser`, { - auth: { - username: AUTHENTICATION.SUPERUSER.USERNAME, - password: AUTHENTICATION.SUPERUSER.PASSWORD, - }, - tests: { - default: { - statusCode: 200, - response: expectResults, - }, - } - }); - - bulkGetTest(`kibana legacy user`, { - auth: { - username: AUTHENTICATION.KIBANA_LEGACY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_LEGACY_USER.PASSWORD, - }, + bulkGetTest(`objects within the current space (space_1)`, { + ...SPACES.SPACE_1, tests: { default: { statusCode: 200, - response: expectResults, + response: expectResults(SPACES.SPACE_1.spaceId), }, } }); - bulkGetTest(`kibana legacy dashboard only user`, { - auth: { - username: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.PASSWORD, - }, + bulkGetTest(`objects within another space`, { + ...SPACES.SPACE_1, + urlContext: SPACES.SPACE_2.urlContext, tests: { default: { statusCode: 200, - response: expectResults, + response: expectNotFoundResults(SPACES.SPACE_1.spaceId) }, } }); - bulkGetTest(`kibana rbac user`, { - auth: { - username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME, - password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD, - }, - tests: { - default: { - statusCode: 200, - response: expectResults, - }, - } - }); - - bulkGetTest(`kibana rbac dashboard only user`, { - auth: { - username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD, - }, - tests: { - default: { - statusCode: 200, - response: expectResults, - }, - } - }); }); } diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/index.js b/x-pack/test/spaces_api_integration/apis/saved_objects/index.js index 63a894816a910..bf0a8d2c4c47e 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/index.js +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/index.js @@ -8,7 +8,7 @@ export default function ({ loadTestFile, getService }) { describe('saved_objects', () => { - // loadTestFile(require.resolve('./bulk_get')); + loadTestFile(require.resolve('./bulk_get')); // loadTestFile(require.resolve('./create')); // loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./find')); From fc663d9d7dc7d57445e8b979d35a8fc8804a34dc Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Mon, 25 Jun 2018 07:41:11 -0400 Subject: [PATCH 12/41] refactor query params into dedicated module --- .../lib/is_type_space_aware.js | 9 +++ .../saved_objects_client/lib/query_params.js | 60 ++++++++++++++++++ .../spaces_saved_objects_client.js | 63 ++----------------- 3 files changed, 75 insertions(+), 57 deletions(-) create mode 100644 x-pack/plugins/spaces/server/lib/saved_objects_client/lib/is_type_space_aware.js create mode 100644 x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_params.js diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/is_type_space_aware.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/is_type_space_aware.js new file mode 100644 index 0000000000000..8dde8d92f7f2c --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/is_type_space_aware.js @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export function isTypeSpaceAware(type) { + return type !== 'space'; +} diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_params.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_params.js new file mode 100644 index 0000000000000..1e0acd145f952 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_params.js @@ -0,0 +1,60 @@ +/* + * 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 { DEFAULT_SPACE_ID } from "../../../../common/constants"; +import { isTypeSpaceAware } from "./is_type_space_aware"; + +function getClauseForType(spaceId, type) { + const shouldFilterOnSpace = isTypeSpaceAware(type) && spaceId; + const isDefaultSpace = spaceId === DEFAULT_SPACE_ID; + + const bool = { + must: [] + }; + + if (type) { + bool.must.push({ + term: { + type + } + }); + } + + if (shouldFilterOnSpace) { + if (isDefaultSpace) { + bool.must_not = { + exists: { + field: "spaceId" + } + }; + } else { + bool.must.push({ + term: { + spaceId + } + }); + } + } + + return { + bool + }; +} + +export function getSpacesQueryParams(spaceId, types) { + const typeClauses = types.map((type) => getClauseForType(spaceId, type)); + + if (typeClauses.length > 0) { + return { + bool: { + should: typeClauses, + minimum_should_match: 1 + } + }; + } + + return {}; +} diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js index e85aedfa5da24..fa8d1c6b17f66 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js @@ -5,6 +5,8 @@ */ import { DEFAULT_SPACE_ID } from '../../../common/constants'; +import { isTypeSpaceAware } from './lib/is_type_space_aware'; +import { getSpacesQueryParams } from './lib/query_params'; export class SpacesSavedObjectsClient { constructor(options) { @@ -26,7 +28,7 @@ export class SpacesSavedObjectsClient { async create(type, attributes = {}, options = {}) { - if (this._isTypeSpaceAware(type)) { + if (isTypeSpaceAware(type)) { options.extraBodyProperties = { ...options.extraBodyProperties, spaceId: await this._getSpaceId() @@ -58,57 +60,8 @@ export class SpacesSavedObjectsClient { } const spaceId = await this._getSpaceId(); - console.log('got space id', spaceId); - let minimumShouldMatch = 0; - - const typeClauses = types.map(t => { - - const shouldFilterOnSpace = this._isTypeSpaceAware(t) && spaceId; - const isDefaultSpace = spaceId === DEFAULT_SPACE_ID; - - const bool = { - must: [] - }; - - if (t) { - minimumShouldMatch = 1; - bool.must.push({ - term: { - type: t - } - }); - } - - if (shouldFilterOnSpace) { - if (isDefaultSpace) { - bool.must_not = { - exists: { - field: "spaceId" - } - }; - } else { - bool.must.push({ - term: { - spaceId - } - }); - } - } - - return { - bool - }; - }); - - if (typeClauses.length > 0) { - spaceOptions.extraQueryParams = { - bool: { - should: typeClauses, - minimum_should_match: minimumShouldMatch - } - }; - } + spaceOptions.extraQueryParams = getSpacesQueryParams(spaceId, types); return await this._client.find({ ...options, ...spaceOptions }); } @@ -124,7 +77,7 @@ export class SpacesSavedObjectsClient { result.saved_objects = result.saved_objects.map(savedObject => { const { id, type, spaceId } = savedObject; - if (this._isTypeSpaceAware(type)) { + if (isTypeSpaceAware(type)) { if (spaceId !== thisSpaceId) { return { id, @@ -144,7 +97,7 @@ export class SpacesSavedObjectsClient { // ES 'get' does not support queries, so we have to filter results after the fact. let thisSpaceId; - if (this._isTypeSpaceAware(type)) { + if (isTypeSpaceAware(type)) { thisSpaceId = await this._getSpaceId(); } @@ -169,10 +122,6 @@ export class SpacesSavedObjectsClient { return await this._client.update(type, id, attributes, options); } - _isTypeSpaceAware(type) { - return type !== 'space'; - } - async _getSpaceId() { if (!this._spaceUrlContext) { return DEFAULT_SPACE_ID; From fc6159491497b5626b8a4f8f42720ff48ca3c489 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Mon, 25 Jun 2018 10:47:34 -0400 Subject: [PATCH 13/41] additional tests and bugfixes for space aware saved objects --- .../lib/is_type_space_aware.js | 7 +- .../lib/is_type_space_aware.test.js | 29 +++ .../saved_objects_client/lib/query_params.js | 8 +- .../lib/query_params.test.js | 135 ++++++++++++++ .../spaces_saved_objects_client.js | 36 ++-- .../lib/space_request_interceptors.test.js | 4 + .../apis/saved_objects/create.js | 166 ++++++++--------- .../apis/saved_objects/delete.js | 144 ++++----------- .../apis/saved_objects/index.js | 8 +- .../apis/saved_objects/update.js | 170 ++++++++---------- .../saved_objects/spaces/data.json.gz | Bin 2459 -> 2459 bytes 11 files changed, 402 insertions(+), 305 deletions(-) create mode 100644 x-pack/plugins/spaces/server/lib/saved_objects_client/lib/is_type_space_aware.test.js create mode 100644 x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_params.test.js diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/is_type_space_aware.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/is_type_space_aware.js index 8dde8d92f7f2c..720fb7e28e117 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/is_type_space_aware.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/is_type_space_aware.js @@ -4,6 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +/** + * Returns if the provided Saved Object type is "space aware". + * Most types should be space-aware, and those that aren't should typically strive to become space-aware. + * Types that are not space-aware will appear in every space, and are not bound by any space-specific access controls. + */ export function isTypeSpaceAware(type) { - return type !== 'space'; + return type !== 'space' && type !== 'config'; } diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/is_type_space_aware.test.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/is_type_space_aware.test.js new file mode 100644 index 0000000000000..b2c7fbbcfd3db --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/is_type_space_aware.test.js @@ -0,0 +1,29 @@ +/* + * 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 { isTypeSpaceAware } from "./is_type_space_aware"; + +const knownSpaceAwareTypes = [ + 'dashboard', + 'visualization', + 'saved_search', + 'timelion_sheet', + 'index_pattern' +]; + +const unwareTypes = ['space']; + +knownSpaceAwareTypes.forEach(type => test(`${type} should be space-aware`, () => { + expect(isTypeSpaceAware(type)).toBe(true); +})); + +unwareTypes.forEach(type => test(`${type} should not be space-aware`, () => { + expect(isTypeSpaceAware(type)).toBe(false); +})); + +test(`unknown types should default to space-aware`, () => { + expect(isTypeSpaceAware('an-unknown-type')).toBe(true); +}); diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_params.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_params.js index 1e0acd145f952..c117423985536 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_params.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_params.js @@ -25,11 +25,13 @@ function getClauseForType(spaceId, type) { if (shouldFilterOnSpace) { if (isDefaultSpace) { - bool.must_not = { + // The default space does not add its spaceId to the objects that belong to it, in order + // to be compatible with installations that are not always space-aware. + bool.must_not = [{ exists: { field: "spaceId" } - }; + }]; } else { bool.must.push({ term: { @@ -44,7 +46,7 @@ function getClauseForType(spaceId, type) { }; } -export function getSpacesQueryParams(spaceId, types) { +export function getSpacesQueryParams(spaceId, types = []) { const typeClauses = types.map((type) => getClauseForType(spaceId, type)); if (typeClauses.length > 0) { diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_params.test.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_params.test.js new file mode 100644 index 0000000000000..520ebebc33735 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_params.test.js @@ -0,0 +1,135 @@ +/* + * 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 { getSpacesQueryParams } from './query_params'; + +test('returns no parameters when no types are provided', () => { + expect(getSpacesQueryParams('space_1', [])).toEqual({}); +}); + +test('creates a query that filters on type, but not on space, for types that are not space-aware', () => { + const spaceId = 'space_1'; + const type = 'space'; + + const expectedTypeClause = { + bool: { + must: [{ + term: { + type + } + }] + } + }; + expect(getSpacesQueryParams(spaceId, [type])).toEqual({ + bool: { + should: [expectedTypeClause], + minimum_should_match: 1 + } + }); +}); + +test('creates a query that restricts a space-aware type to the provided space (space_1)', () => { + const spaceId = 'space_1'; + const type = 'dashboard'; + + const expectedTypeClause = { + bool: { + must: [{ + term: { + type + } + }, { + term: { + spaceId + } + }] + } + }; + + expect(getSpacesQueryParams(spaceId, [type])).toEqual({ + bool: { + should: [expectedTypeClause], + minimum_should_match: 1 + } + }); +}); + +test('creates a query that restricts a space-aware type to the provided space (default)', () => { + const spaceId = 'default'; + const type = 'dashboard'; + + const expectedTypeClause = { + bool: { + must: [{ + term: { + type + } + }], + // The default space does not add its spaceId to the objects that belong to it, in order + // to be compatible with installations that are not always space-aware. + must_not: [{ + exists: { + field: 'spaceId' + } + }] + } + }; + + expect(getSpacesQueryParams(spaceId, [type])).toEqual({ + bool: { + should: [expectedTypeClause], + minimum_should_match: 1 + } + }); +}); + +test('creates a query supporting a find operation on multiple types', () => { + const spaceId = 'space_1'; + const types = [ + 'dashboard', + 'space', + 'visualization', + ]; + + const expectedSpaceClause = { + term: { + spaceId + } + }; + + const expectedTypeClauses = [{ + bool: { + must: [{ + term: { + type: 'dashboard' + } + }, expectedSpaceClause] + } + }, { + bool: { + must: [{ + term: { + type: 'space' + } + }] + } + }, { + bool: { + must: [{ + term: { + type: 'visualization' + } + }, expectedSpaceClause] + } + }]; + + expect(getSpacesQueryParams(spaceId, types)).toEqual({ + bool: { + should: expectedTypeClauses, + minimum_should_match: 1 + } + }); +}); diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js index fa8d1c6b17f66..6e8b7e2346a4a 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js @@ -28,7 +28,10 @@ export class SpacesSavedObjectsClient { async create(type, attributes = {}, options = {}) { - if (isTypeSpaceAware(type)) { + const spaceId = await this._getSpaceId(); + const shouldAssignSpaceId = spaceId !== DEFAULT_SPACE_ID && isTypeSpaceAware(type); + + if (shouldAssignSpaceId) { options.extraBodyProperties = { ...options.extraBodyProperties, spaceId: await this._getSpaceId() @@ -48,6 +51,10 @@ export class SpacesSavedObjectsClient { } async delete(type, id) { + // attempt to retrieve document before deleting. + // this ensures that the document belongs to the current space. + await this.get(type, id); + return await this._client.delete(type, id); } @@ -95,11 +102,6 @@ export class SpacesSavedObjectsClient { async get(type, id) { // ES 'get' does not support queries, so we have to filter results after the fact. - let thisSpaceId; - - if (isTypeSpaceAware(type)) { - thisSpaceId = await this._getSpaceId(); - } const response = await this._client.get(type, id, { extraSourceProperties: ['spaceId'] @@ -107,18 +109,28 @@ export class SpacesSavedObjectsClient { const { spaceId: objectSpaceId = DEFAULT_SPACE_ID } = response; - if (objectSpaceId !== thisSpaceId) { - throw this._client.errors.createGenericNotFoundError(); + if (isTypeSpaceAware(type)) { + const thisSpaceId = await this._getSpaceId(); + if (objectSpaceId !== thisSpaceId) { + throw this._client.errors.createGenericNotFoundError(); + } } return response; } async update(type, id, attributes, options = {}) { - options.extraBodyProperties = { - ...options.extraBodyProperties, - spaceId: await this._getSpaceId() - }; + // attempt to retrieve document before updating. + // this ensures that the document belongs to the current space. + if (isTypeSpaceAware(type)) { + await this.get(type, id); + + options.extraBodyProperties = { + ...options.extraBodyProperties, + spaceId: await this._getSpaceId() + }; + } + return await this._client.update(type, id, attributes, options); } diff --git a/x-pack/plugins/spaces/server/lib/space_request_interceptors.test.js b/x-pack/plugins/spaces/server/lib/space_request_interceptors.test.js index 434ec530ba6f3..2587902b2a616 100644 --- a/x-pack/plugins/spaces/server/lib/space_request_interceptors.test.js +++ b/x-pack/plugins/spaces/server/lib/space_request_interceptors.test.js @@ -6,6 +6,7 @@ import sinon from 'sinon'; import { Server } from 'hapi'; import { initSpacesRequestInterceptors } from './space_request_interceptors'; +import { createSpacesService } from './create_spaces_service'; describe('interceptors', () => { const sandbox = sinon.sandbox.create(); @@ -20,6 +21,9 @@ describe('interceptors', () => { server.connection({ port: 0 }); + const spacesService = createSpacesService(server); + server.decorate('server', 'spaces', spacesService); + initSpacesRequestInterceptors(server); server.route({ diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/create.js b/x-pack/test/spaces_api_integration/apis/saved_objects/create.js index c8ca5be09b6ad..2eee219f739f6 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/create.js +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/create.js @@ -5,14 +5,17 @@ */ import expect from 'expect.js'; -import { AUTHENTICATION } from './lib/authentication'; +import { getUrlPrefix } from './lib/space_test_utils'; +import { SPACES } from './lib/spaces'; +import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; export default function ({ getService }) { - const supertest = getService('supertestWithoutAuth'); + const supertest = getService('supertest'); + const es = getService('es'); const esArchiver = getService('esArchiver'); describe('create', () => { - const expectResults = (resp) => { + const expectSpaceAwareResults = (spaceId) => async (resp) => { expect(resp.body).to.have.property('id').match(/^[0-9a-f-]{36}$/); // loose ISO8601 UTC time with milliseconds validation @@ -27,110 +30,113 @@ export default function ({ getService }) { title: 'My favorite vis' } }); + + // query ES directory to assert on space id + const { _source } = await es.get({ + id: `visualization:${resp.body.id}`, + type: 'doc', + index: '.kibana' + }); + + const { + spaceId: actualSpaceId = '**not defined**' + } = _source; + + if (spaceId === DEFAULT_SPACE_ID) { + expect(actualSpaceId).to.eql('**not defined**'); + } else { + expect(actualSpaceId).to.eql(spaceId); + } }; - const createExpectForbidden = canLogin => resp => { + const expectNotSpaceAwareResults = () => async (resp) => { + expect(resp.body).to.have.property('id').match(/^[0-9a-f-]{36}$/); + + // loose ISO8601 UTC time with milliseconds validation + expect(resp.body).to.have.property('updated_at').match(/^[\d-]{10}T[\d:\.]{12}Z$/); + expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unable to create visualization, missing ${canLogin ? '' : 'action:login,'}action:saved_objects/visualization/create` + id: resp.body.id, + type: 'space', + updated_at: resp.body.updated_at, + version: 1, + attributes: { + name: 'My favorite space', + urlContext: 'my-favorite-space' + } + }); + + // query ES directory to assert on space id + const { _source } = await es.get({ + id: `space:${resp.body.id}`, + type: 'doc', + index: '.kibana' }); + + const { + spaceId: actualSpaceId = '**not defined**' + } = _source; + + expect(actualSpaceId).to.eql('**not defined**'); }; - const createTest = (description, { auth, tests }) => { + const createTest = (description, { urlContext, tests }) => { describe(description, () => { - before(() => esArchiver.load('saved_objects/basic')); - after(() => esArchiver.unload('saved_objects/basic')); - it(`should return ${tests.default.statusCode}`, async () => { + before(() => esArchiver.load('saved_objects/spaces')); + after(() => esArchiver.unload('saved_objects/spaces')); + it(`should return ${tests.spaceAware.statusCode} for a space-aware type`, async () => { await supertest - .post(`/api/saved_objects/visualization`) - .auth(auth.username, auth.password) + .post(`${getUrlPrefix(urlContext)}/api/saved_objects/visualization`) .send({ attributes: { title: 'My favorite vis' } }) - .expect(tests.default.statusCode) - .then(tests.default.response); + .expect(tests.spaceAware.statusCode) + .then(tests.spaceAware.response); + }); + + it(`should return ${tests.notSpaceAware.statusCode} for a non space-aware type`, async () => { + await supertest + .post(`${getUrlPrefix(urlContext)}/api/saved_objects/space`) + .send({ + attributes: { + name: 'My favorite space', + urlContext: 'my-favorite-space' + } + }) + .expect(tests.notSpaceAware.statusCode) + .then(tests.notSpaceAware.response); }); + }); }; - createTest(`not a kibana user`, { - auth: { - username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME, - password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD, - }, - tests: { - default: { - statusCode: 403, - response: createExpectForbidden(false), - }, - } - }); - - createTest(`superuser`, { - auth: { - username: AUTHENTICATION.SUPERUSER.USERNAME, - password: AUTHENTICATION.SUPERUSER.PASSWORD, - }, + createTest('in the current space (space_1)', { + ...SPACES.SPACE_1, tests: { - default: { + spaceAware: { statusCode: 200, - response: expectResults, + response: expectSpaceAwareResults(SPACES.SPACE_1.spaceId), }, - } - }); - - createTest(`kibana legacy user`, { - auth: { - username: AUTHENTICATION.KIBANA_LEGACY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_LEGACY_USER.PASSWORD, - }, - tests: { - default: { + notSpaceAware: { statusCode: 200, - response: expectResults, - }, - } - }); - - createTest(`kibana legacy dashboard only user`, { - auth: { - username: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.PASSWORD, - }, - tests: { - default: { - statusCode: 403, - response: createExpectForbidden(true), - }, + response: expectNotSpaceAwareResults(SPACES.SPACE_1.spaceId), + } } }); - createTest(`kibana rbac user`, { - auth: { - username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME, - password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD, - }, + createTest('in the default space', { + ...SPACES.DEFAULT, tests: { - default: { + spaceAware: { statusCode: 200, - response: expectResults, - }, - } - }); - - createTest(`kibana rbac dashboard only user`, { - auth: { - username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD, - }, - tests: { - default: { - statusCode: 403, - response: createExpectForbidden(true), + response: expectSpaceAwareResults(SPACES.DEFAULT.spaceId), }, + notSpaceAware: { + statusCode: 200, + response: expectNotSpaceAwareResults(SPACES.SPACE_1.spaceId), + } } }); }); diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/delete.js b/x-pack/test/spaces_api_integration/apis/saved_objects/delete.js index f89073acca7fd..dc1efb057d4e7 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/delete.js +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/delete.js @@ -5,10 +5,11 @@ */ import expect from 'expect.js'; -import { AUTHENTICATION } from './lib/authentication'; +import { SPACES } from './lib/spaces'; +import { getUrlPrefix, getIdPrefix } from './lib/space_test_utils'; export default function ({ getService }) { - const supertest = getService('supertestWithoutAuth'); + const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); describe('delete', () => { @@ -25,135 +26,66 @@ export default function ({ getService }) { }); }; - const createExpectForbidden = canLogin => resp => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unable to delete dashboard, missing ${canLogin ? '' : 'action:login,'}action:saved_objects/dashboard/delete` - }); - }; - - const deleteTest = (description, { auth, tests }) => { + const deleteTest = (description, { urlContext, spaceId, tests }) => { describe(description, () => { - before(() => esArchiver.load('saved_objects/basic')); - after(() => esArchiver.unload('saved_objects/basic')); + before(() => esArchiver.load('saved_objects/spaces')); + after(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.actualId.statusCode} when deleting a doc`, async () => ( + it(`should return ${tests.spaceAware.statusCode} when deleting a space-aware doc`, async () => ( await supertest - .delete(`/api/saved_objects/dashboard/be3733a0-9efe-11e7-acb3-3dab96693fab`) - .auth(auth.username, auth.password) - .expect(tests.actualId.statusCode) - .then(tests.actualId.response) + .delete(`${getUrlPrefix(urlContext)}/api/saved_objects/dashboard/${getIdPrefix(spaceId)}be3733a0-9efe-11e7-acb3-3dab96693fab`) + .expect(tests.spaceAware.statusCode) + .then(tests.spaceAware.response) )); - it(`should return ${tests.invalidId.statusCode} when deleting an unknown doc`, async () => ( + it(`should return ${tests.notSpaceAware.statusCode} when deleting a non-space-aware doc`, async () => ( await supertest - .delete(`/api/saved_objects/dashboard/not-a-real-id`) - .auth(auth.username, auth.password) - .expect(tests.invalidId.statusCode) - .then(tests.invalidId.response) + .delete(`${getUrlPrefix(urlContext)}/api/saved_objects/space/space_2`) + .expect(tests.notSpaceAware.statusCode) + .then(tests.notSpaceAware.response) )); + + it(`should return ${tests.inOtherSpace.statusCode} when deleting a doc belonging to another space`, async () => { + await supertest + .delete(`${getUrlPrefix(urlContext)}/api/saved_objects/dashboard/${getIdPrefix('space_2')}be3733a0-9efe-11e7-acb3-3dab96693fab`) + .expect(tests.inOtherSpace.statusCode) + .then(tests.inOtherSpace.response); + }); }); }; - deleteTest(`not a kibana user`, { - auth: { - username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME, - password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD, - }, + deleteTest(`in the default space`, { + ...SPACES.DEFAULT, tests: { - actualId: { - statusCode: 403, - response: createExpectForbidden(false), - }, - invalidId: { - statusCode: 403, - response: createExpectForbidden(false), - } - } - }); - - deleteTest(`superuser`, { - auth: { - username: AUTHENTICATION.SUPERUSER.USERNAME, - password: AUTHENTICATION.SUPERUSER.PASSWORD, - }, - tests: { - actualId: { + spaceAware: { statusCode: 200, - response: expectEmpty, + response: expectEmpty }, - invalidId: { - statusCode: 404, - response: expectNotFound, - } - } - }); - - deleteTest(`kibana legacy user`, { - auth: { - username: AUTHENTICATION.KIBANA_LEGACY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_LEGACY_USER.PASSWORD, - }, - tests: { - actualId: { + notSpaceAware: { statusCode: 200, - response: expectEmpty, + response: expectEmpty }, - invalidId: { + inOtherSpace: { statusCode: 404, - response: expectNotFound, + response: expectNotFound } } }); - deleteTest(`kibana legacy dashboard only user`, { - auth: { - username: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.PASSWORD, - }, + deleteTest(`in the current space (space_1)`, { + ...SPACES.SPACE_1, tests: { - actualId: { - statusCode: 403, - response: createExpectForbidden(true), + spaceAware: { + statusCode: 200, + response: expectEmpty }, - invalidId: { - statusCode: 403, - response: createExpectForbidden(true), - } - } - }); - - deleteTest(`kibana rbac user`, { - auth: { - username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME, - password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD, - }, - tests: { - actualId: { + notSpaceAware: { statusCode: 200, - response: expectEmpty, + response: expectEmpty }, - invalidId: { + inOtherSpace: { statusCode: 404, - response: expectNotFound, - } - } - }); - - deleteTest(`kibana rbac dashboard only user`, { - auth: { - username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD, - }, - tests: { - actualId: { - statusCode: 403, - response: createExpectForbidden(true), - }, - invalidId: { - statusCode: 403, - response: createExpectForbidden(true), + response: expectNotFound } } }); diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/index.js b/x-pack/test/spaces_api_integration/apis/saved_objects/index.js index bf0a8d2c4c47e..c74b03792ba03 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/index.js +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/index.js @@ -5,14 +5,14 @@ */ -export default function ({ loadTestFile, getService }) { +export default function ({ loadTestFile }) { describe('saved_objects', () => { loadTestFile(require.resolve('./bulk_get')); - // loadTestFile(require.resolve('./create')); - // loadTestFile(require.resolve('./delete')); + loadTestFile(require.resolve('./create')); + loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./find')); loadTestFile(require.resolve('./get')); - // loadTestFile(require.resolve('./update')); + loadTestFile(require.resolve('./update')); }); } diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/update.js b/x-pack/test/spaces_api_integration/apis/saved_objects/update.js index ae299348847d6..d87d03d9b7250 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/update.js +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/update.js @@ -5,16 +5,15 @@ */ import expect from 'expect.js'; -import { AUTHENTICATION } from './lib/authentication'; +import { SPACES } from './lib/spaces'; +import { getUrlPrefix, getIdPrefix } from './lib/space_test_utils'; export default function ({ getService }) { - const supertest = getService('supertestWithoutAuth'); + const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); describe('update', () => { - const expectResults = resp => { - // loose uuid validation - expect(resp.body).to.have.property('id').match(/^[0-9a-f-]{36}$/); + const expectSpaceAwareResults = resp => { // loose ISO8601 UTC time with milliseconds validation expect(resp.body).to.have.property('updated_at').match(/^[\d-]{10}T[\d:\.]{12}Z$/); @@ -30,6 +29,22 @@ export default function ({ getService }) { }); }; + const expectNonSpaceAwareResults = resp => { + + // loose ISO8601 UTC time with milliseconds validation + expect(resp.body).to.have.property('updated_at').match(/^[\d-]{10}T[\d:\.]{12}Z$/); + + expect(resp.body).to.eql({ + id: resp.body.id, + type: 'space', + updated_at: resp.body.updated_at, + version: 2, + attributes: { + name: 'My second favorite space' + } + }); + }; + const expectNotFound = resp => { expect(resp.body).eql({ statusCode: 404, @@ -38,36 +53,51 @@ export default function ({ getService }) { }); }; - const createExpectForbidden = canLogin => resp => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unable to update visualization, missing ${canLogin ? '' : 'action:login,'}action:saved_objects/visualization/update` - }); - }; - - const updateTest = (description, { auth, tests }) => { + const updateTest = (description, { urlContext, spaceId, tests }) => { describe(description, () => { - before(() => esArchiver.load('saved_objects/basic')); - after(() => esArchiver.unload('saved_objects/basic')); - it(`should return ${tests.exists.statusCode}`, async () => { + before(() => esArchiver.load('saved_objects/spaces')); + after(() => esArchiver.unload('saved_objects/spaces')); + it(`should return ${tests.spaceAware.statusCode} for a space-aware doc`, async () => { await supertest - .put(`/api/saved_objects/visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab`) - .auth(auth.username, auth.password) + .put(`${getUrlPrefix(urlContext)}/api/saved_objects/visualization/${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`) .send({ attributes: { title: 'My second favorite vis' } }) - .expect(tests.exists.statusCode) - .then(tests.exists.response); + .expect(tests.spaceAware.statusCode) + .then(tests.spaceAware.response); + }); + + it(`should return ${tests.notSpaceAware.statusCode} for a non space-aware doc`, async () => { + await supertest + .put(`${getUrlPrefix(urlContext)}/api/saved_objects/space/space_1`) + .send({ + attributes: { + name: 'My second favorite space' + } + }) + .expect(tests.notSpaceAware.statusCode) + .then(tests.notSpaceAware.response); + }); + + it(`should return ${tests.inOtherSpace.statusCode} for a doc in another space`, async () => { + const id = `${getIdPrefix('space_2')}dd7caf20-9efd-11e7-acb3-3dab96693fab`; + await supertest + .put(`${getUrlPrefix(urlContext)}/api/saved_objects/visualization/${id}`) + .send({ + attributes: { + title: 'My second favorite vis' + } + }) + .expect(tests.inOtherSpace.statusCode) + .then(tests.inOtherSpace.response); }); describe('unknown id', () => { it(`should return ${tests.doesntExist.statusCode}`, async () => { await supertest - .put(`/api/saved_objects/visualization/not an id`) - .auth(auth.username, auth.password) + .put(`${getUrlPrefix(urlContext)}/api/saved_objects/visualization/not an id`) .send({ attributes: { title: 'My second favorite vis' @@ -80,50 +110,21 @@ export default function ({ getService }) { }); }; - updateTest(`not a kibana user`, { - auth: { - username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME, - password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD, - }, + updateTest(`in the default space`, { + ...SPACES.DEFAULT, tests: { - exists: { - statusCode: 403, - response: createExpectForbidden(false), - }, - doesntExist: { - statusCode: 403, - response: createExpectForbidden(false), + spaceAware: { + statusCode: 200, + response: expectSpaceAwareResults, }, - } - }); - - updateTest(`superuser`, { - auth: { - username: AUTHENTICATION.SUPERUSER.USERNAME, - password: AUTHENTICATION.SUPERUSER.PASSWORD, - }, - tests: { - exists: { + notSpaceAware: { statusCode: 200, - response: expectResults, + response: expectNonSpaceAwareResults, }, - doesntExist: { + inOtherSpace: { statusCode: 404, response: expectNotFound, }, - } - }); - - updateTest(`kibana legacy user`, { - auth: { - username: AUTHENTICATION.KIBANA_LEGACY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_LEGACY_USER.PASSWORD, - }, - tests: { - exists: { - statusCode: 200, - response: expectResults, - }, doesntExist: { statusCode: 404, response: expectNotFound, @@ -131,53 +132,24 @@ export default function ({ getService }) { } }); - updateTest(`kibana legacy dashboard only user`, { - auth: { - username: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.PASSWORD, - }, + updateTest('in the current space (space_1)', { + ...SPACES.SPACE_1, tests: { - exists: { - statusCode: 403, - response: createExpectForbidden(true), - }, - doesntExist: { - statusCode: 403, - response: createExpectForbidden(true), + spaceAware: { + statusCode: 200, + response: expectSpaceAwareResults, }, - } - }); - - updateTest(`kibana rbac user`, { - auth: { - username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME, - password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD, - }, - tests: { - exists: { + notSpaceAware: { statusCode: 200, - response: expectResults, + response: expectNonSpaceAwareResults, }, - doesntExist: { + inOtherSpace: { statusCode: 404, response: expectNotFound, }, - } - }); - - updateTest(`kibana rbac dashboard only user`, { - auth: { - username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME, - password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD, - }, - tests: { - exists: { - statusCode: 403, - response: createExpectForbidden(true), - }, doesntExist: { - statusCode: 403, - response: createExpectForbidden(true), + statusCode: 404, + response: expectNotFound, }, } }); diff --git a/x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/data.json.gz b/x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/data.json.gz index 9630865e700ebb33563399ec0911053329515cf4..33586e3642245682fc862650efa6eabe1ca9bc8e 100644 GIT binary patch delta 16 XcmbO&JX@GuzMF%?;-hICxryG delta 16 XcmbO&JX@GuzMF$X=cdj^_9>hICmjT& From 776da8aa3876111a2f76481b08a65f4f64c3e43a Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Mon, 25 Jun 2018 11:32:18 -0400 Subject: [PATCH 14/41] revert changes to ui settings service --- ...st.js => create_or_upgrade_integration.js} | 16 ++++++---------- ...t.js => create_or_upgrade_saved_config.js} | 9 +++------ ...st.js => is_config_version_upgradeable.js} | 4 ++-- .../create_or_upgrade_saved_config.js | 19 ++++--------------- src/ui/ui_settings/ui_settings_service.js | 6 ++---- .../ui_settings_service_factory.js | 7 ++----- .../ui_settings_service_for_request.js | 2 +- 7 files changed, 20 insertions(+), 43 deletions(-) rename src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/{create_or_upgrade_integration.test.js => create_or_upgrade_integration.js} (97%) rename src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/{create_or_upgrade_saved_config.test.js => create_or_upgrade_saved_config.js} (96%) rename src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/{is_config_version_upgradeable.test.js => is_config_version_upgradeable.js} (98%) diff --git a/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_integration.test.js b/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_integration.js similarity index 97% rename from src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_integration.test.js rename to src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_integration.js index 5c9536277c401..e5ce9b9ea3b70 100644 --- a/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_integration.test.js +++ b/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_integration.js @@ -18,6 +18,7 @@ */ import sinon from 'sinon'; +import expect from 'expect.js'; import { createEsTestCluster } from '@kbn/test'; import { createServerWithCorePlugins } from '../../../../test_utils/kbn_server'; @@ -29,7 +30,7 @@ describe('createOrUpgradeSavedConfig()', () => { let kbnServer; const cleanup = []; - beforeAll(async function () { + before(async function () { const log = createToolingLog('debug'); log.pipe(process.stdout); log.indent(6); @@ -84,27 +85,26 @@ describe('createOrUpgradeSavedConfig()', () => { }, }, ]); - }, 30000); + }); - afterAll(async () => { + after(async () => { await Promise.all(cleanup.map(fn => fn())); cleanup.length = 0; }); it('upgrades the previous version on each increment', async function () { + this.timeout(30000); // ------------------------------------ // upgrade to 5.4.0 await createOrUpgradeSavedConfig({ savedObjectsClient, version: '5.4.0', - id: '5.4.0', buildNum: 54099, log: sinon.stub(), }); const config540 = await savedObjectsClient.get('config', '5.4.0'); - expect(config540) .to.have.property('attributes') .eql({ @@ -126,7 +126,6 @@ describe('createOrUpgradeSavedConfig()', () => { await createOrUpgradeSavedConfig({ savedObjectsClient, version: '5.4.1', - id: '5.4.1', buildNum: 54199, log: sinon.stub(), }); @@ -153,7 +152,6 @@ describe('createOrUpgradeSavedConfig()', () => { await createOrUpgradeSavedConfig({ savedObjectsClient, version: '7.0.0-rc1', - id: '7.0.0-rc1', buildNum: 70010, log: sinon.stub(), }); @@ -181,7 +179,6 @@ describe('createOrUpgradeSavedConfig()', () => { await createOrUpgradeSavedConfig({ savedObjectsClient, version: '7.0.0', - id: '7.0.0', buildNum: 70099, log: sinon.stub(), }); @@ -210,7 +207,6 @@ describe('createOrUpgradeSavedConfig()', () => { await createOrUpgradeSavedConfig({ savedObjectsClient, version: '6.2.3-rc1', - id: '6.2.3-rc1', buildNum: 62310, log: sinon.stub(), }); @@ -228,4 +224,4 @@ describe('createOrUpgradeSavedConfig()', () => { '5.4.0-rc1': true, }); }); -}); +}); \ No newline at end of file diff --git a/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_saved_config.test.js b/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_saved_config.js similarity index 96% rename from src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_saved_config.test.js rename to src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_saved_config.js index 4a987ba95a5a1..b0b823800a660 100644 --- a/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_saved_config.test.js +++ b/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_saved_config.js @@ -48,7 +48,6 @@ describe('uiSettings/createOrUpgradeSavedConfig', function () { const resp = await createOrUpgradeSavedConfig({ savedObjectsClient, version, - id: `${version}:foo`, buildNum, log, }); @@ -78,9 +77,8 @@ describe('uiSettings/createOrUpgradeSavedConfig', function () { sinon.assert.calledOnce(savedObjectsClient.create); sinon.assert.calledWithExactly(savedObjectsClient.create, 'config', { buildNum, - version }, { - id: `${version}:foo` + id: version }); }); }); @@ -112,10 +110,9 @@ describe('uiSettings/createOrUpgradeSavedConfig', function () { { ...savedAttributes, buildNum, - version, }, { - id: `${version}:foo`, + id: version, } ); }); @@ -138,4 +135,4 @@ describe('uiSettings/createOrUpgradeSavedConfig', function () { ); }); }); -}); +}); \ No newline at end of file diff --git a/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/is_config_version_upgradeable.test.js b/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/is_config_version_upgradeable.js similarity index 98% rename from src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/is_config_version_upgradeable.test.js rename to src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/is_config_version_upgradeable.js index 98f246343e76f..8351c03dee42c 100644 --- a/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/is_config_version_upgradeable.test.js +++ b/src/ui/ui_settings/create_or_upgrade_saved_config/__tests__/is_config_version_upgradeable.js @@ -25,7 +25,7 @@ import { pkg } from '../../../../utils'; describe('savedObjects/health_check/isConfigVersionUpgradeable', function () { function isUpgradableTest(savedVersion, kibanaVersion, expected) { it(`should return ${expected} for config version ${savedVersion} and kibana version ${kibanaVersion}`, () => { - expect(isConfigVersionUpgradeable(savedVersion, kibanaVersion)).toBe(expected); + expect(isConfigVersionUpgradeable(savedVersion, kibanaVersion)).to.be(expected); }); } @@ -49,4 +49,4 @@ describe('savedObjects/health_check/isConfigVersionUpgradeable', function () { isUpgradableTest('50.0.10-rc150-SNAPSHOT', '50.0.9', false); isUpgradableTest(undefined, pkg.version, false); isUpgradableTest('@@version', pkg.version, false); -}); +}); \ No newline at end of file diff --git a/src/ui/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.js b/src/ui/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.js index af188335f29a5..4bd4d8e86b73a 100644 --- a/src/ui/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.js +++ b/src/ui/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.js @@ -24,7 +24,6 @@ import { getUpgradeableConfig } from './get_upgradeable_config'; export async function createOrUpgradeSavedConfig(options) { const { savedObjectsClient, - id, version, buildNum, log, @@ -37,26 +36,16 @@ export async function createOrUpgradeSavedConfig(options) { }); if (upgradeableConfig) { - - let prevVersion = upgradeableConfig.version; - // TODO(legrego): update comment with real versions - // This check can be removed after Y.Z. - // Prior to version X.Y, the config document's id was also used to store the version of Kibana that config was valid for. - // From X.Y forward, the Kibana version is stored in a dedicated "version" field. - if (!prevVersion) { - prevVersion = upgradeableConfig.id; - } - log(['plugin', 'elasticsearch'], { tmpl: 'Upgrade config from <%= prevVersion %> to <%= newVersion %>', - prevVersion, + prevVersion: upgradeableConfig.id, newVersion: version }); } // default to the attributes of the upgradeableConfig if available const attributes = defaults( - { buildNum, version }, + { buildNum }, upgradeableConfig ? upgradeableConfig.attributes : {} ); @@ -64,6 +53,6 @@ export async function createOrUpgradeSavedConfig(options) { await savedObjectsClient.create( 'config', attributes, - { id } + { id: version } ); -} +} \ No newline at end of file diff --git a/src/ui/ui_settings/ui_settings_service.js b/src/ui/ui_settings/ui_settings_service.js index d598bb7848c87..c6a65ce2f0501 100644 --- a/src/ui/ui_settings/ui_settings_service.js +++ b/src/ui/ui_settings/ui_settings_service.js @@ -46,7 +46,6 @@ export class UiSettingsService { const { type, id, - version, buildNum, savedObjectsClient, // we use a function for getDefaults() so that defaults can be different in @@ -58,7 +57,6 @@ export class UiSettingsService { this._type = type; this._id = id; - this._version = version; this._buildNum = buildNum; this._savedObjectsClient = savedObjectsClient; this._getDefaults = getDefaults; @@ -127,7 +125,7 @@ export class UiSettingsService { await createOrUpgradeSavedConfig({ savedObjectsClient: this._savedObjectsClient, - version: this._version, + version: this._id, buildNum: this._buildNum, log: this._log, }); @@ -169,4 +167,4 @@ export class UiSettingsService { throw error; } } -} +} \ No newline at end of file diff --git a/src/ui/ui_settings/ui_settings_service_factory.js b/src/ui/ui_settings/ui_settings_service_factory.js index ca8c674123d67..1eae614650262 100644 --- a/src/ui/ui_settings/ui_settings_service_factory.js +++ b/src/ui/ui_settings/ui_settings_service_factory.js @@ -39,15 +39,12 @@ export function uiSettingsServiceFactory(server, options) { getDefaults, } = options; - const kibanaVersion = config.get('pkg.version'); - return new UiSettingsService({ type: 'config', - id: kibanaVersion, - version: kibanaVersion, + id: config.get('pkg.version'), buildNum: config.get('pkg.buildNum'), savedObjectsClient, getDefaults, log: (...args) => server.log(...args), }); -} +} \ No newline at end of file diff --git a/src/ui/ui_settings/ui_settings_service_for_request.js b/src/ui/ui_settings/ui_settings_service_for_request.js index 75700d2b6daa9..58dcbe22b4b0c 100644 --- a/src/ui/ui_settings/ui_settings_service_for_request.js +++ b/src/ui/ui_settings/ui_settings_service_for_request.js @@ -34,7 +34,7 @@ import { uiSettingsServiceFactory } from './ui_settings_service_factory'; */ export function getUiSettingsServiceForRequest(server, request, options = {}) { const { - getDefaults, + getDefaults } = options; const uiSettingsService = uiSettingsServiceFactory(server, { From d2545d4ca525d90f0a7edc859d058ad50da99ac0 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Mon, 25 Jun 2018 13:57:43 -0400 Subject: [PATCH 15/41] additional tests for space-aware saved objects --- .../service/lib/repository.test.js | 7 +- .../service/lib/search_dsl/query_params.js | 30 ++++++-- .../lib/search_dsl/query_params.test.js | 72 +++++++++++++++++++ .../service/lib/search_dsl/search_dsl.test.js | 4 +- .../service/saved_objects_client.test.js | 4 +- .../apis/saved_objects/find.js | 16 ++--- 6 files changed, 114 insertions(+), 19 deletions(-) diff --git a/src/server/saved_objects/service/lib/repository.test.js b/src/server/saved_objects/service/lib/repository.test.js index a02bfb9abb619..4000a0f19f468 100644 --- a/src/server/saved_objects/service/lib/repository.test.js +++ b/src/server/saved_objects/service/lib/repository.test.js @@ -328,7 +328,7 @@ describe('SavedObjectsRepository', () => { }); describe('#delete', () => { - it('throws notFound when ES is unable to find the document', async () => { + it('throws notFound when ES is unable to find the document', async () => { expect.assertions(1); callAdminCluster.returns(Promise.resolve({ @@ -337,7 +337,7 @@ describe('SavedObjectsRepository', () => { try { await savedObjectsRepository.delete('index-pattern', 'logstash-*'); - } catch(e) { + } catch (e) { expect(e.output.statusCode).toEqual(404); } }); @@ -388,13 +388,14 @@ describe('SavedObjectsRepository', () => { } }); - it('passes mappings, search, searchFields, type, sortField, and sortOrder to getSearchDsl', async () => { + it('passes mappings, search, searchFields, type, sortField, extraQueryParams, and sortOrder to getSearchDsl', async () => { const relevantOpts = { search: 'foo*', searchFields: ['foo'], type: 'bar', sortField: 'name', sortOrder: 'desc', + extraQueryParams: { bool: {} }, }; await savedObjectsRepository.find(relevantOpts); diff --git a/src/server/saved_objects/service/lib/search_dsl/query_params.js b/src/server/saved_objects/service/lib/search_dsl/query_params.js index 6cd22e849f955..3f80b4c6fa537 100644 --- a/src/server/saved_objects/service/lib/search_dsl/query_params.js +++ b/src/server/saved_objects/service/lib/search_dsl/query_params.js @@ -68,7 +68,7 @@ function getFieldsForTypes(searchFields, types) { * @param {Array} searchFields * @return {Object} */ -export function getQueryParams(mappings, type, search, searchFields, extraQueryParams) { +export function getQueryParams(mappings, type, search, searchFields, extraQueryParams = {}) { if (!type && !search) { return {}; } @@ -94,9 +94,31 @@ export function getQueryParams(mappings, type, search, searchFields, extraQueryP ]; } - const query = defaultsDeep({ - bool - }, extraQueryParams); + const fieldsToMerge = ['filter', 'must']; + + const extraParams = { + ...extraQueryParams.bool + }; + + fieldsToMerge.forEach(field => delete extraParams[field]); + + let query = { + bool: defaultsDeep(bool, extraParams) + }; + + if (extraQueryParams.bool) { + + const extraBoolParams = extraQueryParams.bool; + + query = fieldsToMerge.reduce((queryAcc, field) => { + const prop = queryAcc.bool[field]; + const extraProp = extraBoolParams[field]; + if (Array.isArray(prop) && Array.isArray(extraProp)) { + queryAcc.bool[field] = [...prop, ...extraProp]; + } + return queryAcc; + }, query); + } return { query }; } diff --git a/src/server/saved_objects/service/lib/search_dsl/query_params.test.js b/src/server/saved_objects/service/lib/search_dsl/query_params.test.js index 53b943ee6793b..2a1ac85181605 100644 --- a/src/server/saved_objects/service/lib/search_dsl/query_params.test.js +++ b/src/server/saved_objects/service/lib/search_dsl/query_params.test.js @@ -315,4 +315,76 @@ describe('searchDsl/queryParams', () => { }); }); }); + + describe('{extraQueryParams}', () => { + it('merges the extraQueryParams into the generated query params', () => { + const baseQueryParams = getQueryParams(MAPPINGS, 'saved', 'search'); + expect(baseQueryParams) + .toEqual({ + query: { + bool: { + filter: [ + { + term: { type: 'saved' } + } + ], + must: [{ + simple_query_string: { + all_fields: true, + query: 'search' + } + }] + } + } + }); + + const result = getQueryParams(MAPPINGS, 'saved', 'search', null, { + bool: { + filter: [{ + term: { type: 'foo' } + }], + must: [{ + term: { + someField: 'bar' + } + }], + must_not: [{ + term: { + field1: 'value' + } + }] + } + }); + + expect(result).toEqual({ + query: { + bool: { + filter: [ + { + term: { type: 'saved' } + }, + { + term: { type: 'foo' } + } + ], + must: [{ + simple_query_string: { + all_fields: true, + query: 'search' + } + }, { + term: { + someField: 'bar' + } + }], + must_not: [{ + term: { + field1: 'value' + } + }] + } + } + }); + }); + }); }); diff --git a/src/server/saved_objects/service/lib/search_dsl/search_dsl.test.js b/src/server/saved_objects/service/lib/search_dsl/search_dsl.test.js index 85302b5e25722..b21b127d51b10 100644 --- a/src/server/saved_objects/service/lib/search_dsl/search_dsl.test.js +++ b/src/server/saved_objects/service/lib/search_dsl/search_dsl.test.js @@ -46,13 +46,14 @@ describe('getSearchDsl', () => { }); describe('passes control', () => { - it('passes (mappings, type, search, searchFields) to getQueryParams', () => { + it('passes (mappings, type, search, searchFields, extraQueryParams) to getQueryParams', () => { const spy = sandbox.spy(queryParamsNS, 'getQueryParams'); const mappings = { type: { properties: {} } }; const opts = { type: 'foo', search: 'bar', searchFields: ['baz'], + extraQueryParams: { bool: {} }, }; getSearchDsl(mappings, opts); @@ -63,6 +64,7 @@ describe('getSearchDsl', () => { opts.type, opts.search, opts.searchFields, + opts.extraQueryParams, ); }); diff --git a/src/server/saved_objects/service/saved_objects_client.test.js b/src/server/saved_objects/service/saved_objects_client.test.js index a7189f5a39898..784cb3795966b 100644 --- a/src/server/saved_objects/service/saved_objects_client.test.js +++ b/src/server/saved_objects/service/saved_objects_client.test.js @@ -89,7 +89,7 @@ test(`#bulkGet`, async () => { const objects = {}; const result = await client.bulkGet(objects); - expect(mockRepository.bulkGet).toHaveBeenCalledWith(objects); + expect(mockRepository.bulkGet).toHaveBeenCalledWith(objects, {}); expect(result).toBe(returnValue); }); @@ -104,7 +104,7 @@ test(`#get`, async () => { const id = 1; const result = await client.get(type, id); - expect(mockRepository.get).toHaveBeenCalledWith(type, id); + expect(mockRepository.get).toHaveBeenCalledWith(type, id, {}); expect(result).toBe(returnValue); }); diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/find.js b/x-pack/test/spaces_api_integration/apis/saved_objects/find.js index e7527df9b3563..1daa2b2d04f4e 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/find.js +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/find.js @@ -41,6 +41,11 @@ export default function ({ getService }) { resp.body.saved_objects.sort(sortById); const expectedSavedObjects = [{ + id: '7.0.0-alpha1', + type: 'config', + updated_at: '2017-09-21T18:49:16.302Z', + version: 3, + }, { id: `default`, type: 'space', updated_at: '2017-09-21T18:49:16.270Z', @@ -64,14 +69,7 @@ export default function ({ getService }) { updated_at: '2017-09-21T18:49:16.270Z', version: 1, }, - // { - // id: '7.0.0-alpha1', - // type: 'config', - // updated_at: '2017-09-21T18:49:16.302Z', - // version: 1, - // ...getExpectedSpaceIdProperty(spaceId), - // attributes: resp.body.saved_objects[objectIndex++].attributes - // }, + { id: `${getIdPrefix(spaceId)}be3733a0-9efe-11e7-acb3-3dab96693fab`, type: 'dashboard', @@ -94,7 +92,7 @@ export default function ({ getService }) { expect(resp.body).to.eql({ page: 1, per_page: 20, - total: 6, + total: expectedSavedObjects.length, saved_objects: expectedSavedObjects, }); }; From 1fd76998466a895649cbb9691a21a31ffd5e779c Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Mon, 25 Jun 2018 14:30:29 -0400 Subject: [PATCH 16/41] Fix navigating to the default space --- .../spaces_url_parser.test.js.snap | 3 + .../spaces/common/spaces_url_parser.js | 13 +++- .../spaces/common/spaces_url_parser.test.js | 74 ++++++++++++------- .../spaces/public/lib/spaces_manager.js | 10 +++ .../public/views/components/space_cards.js | 7 +- .../views/components/space_cards.test.js | 4 +- .../views/nav_control/nav_control_modal.js | 6 +- .../views/space_selector/space_selector.js | 6 +- .../server/lib/space_request_interceptors.js | 17 ++--- .../spaces/server/routes/api/v1/spaces.js | 24 ++++++ .../server/routes/api/v1/spaces.test.js | 23 +++++- 11 files changed, 139 insertions(+), 48 deletions(-) create mode 100644 x-pack/plugins/spaces/common/__snapshots__/spaces_url_parser.test.js.snap diff --git a/x-pack/plugins/spaces/common/__snapshots__/spaces_url_parser.test.js.snap b/x-pack/plugins/spaces/common/__snapshots__/spaces_url_parser.test.js.snap new file mode 100644 index 0000000000000..a42d029097b67 --- /dev/null +++ b/x-pack/plugins/spaces/common/__snapshots__/spaces_url_parser.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`addSpaceUrlContext it throws an error when the requested path does not start with a slash 1`] = `"path must start with a /"`; diff --git a/x-pack/plugins/spaces/common/spaces_url_parser.js b/x-pack/plugins/spaces/common/spaces_url_parser.js index 075f09eea03a3..e4c41526997e2 100644 --- a/x-pack/plugins/spaces/common/spaces_url_parser.js +++ b/x-pack/plugins/spaces/common/spaces_url_parser.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export function getSpaceUrlContext(basePath = '/', defaultContext = null) { +export function getSpaceUrlContext(basePath = '/', defaultContext = '') { // Look for `/s/space-url-context` in the base path const matchResult = basePath.match(/\/s\/([a-z0-9\-]+)/); @@ -42,3 +42,14 @@ export function stripSpaceUrlContext(basePath = '/') { return basePathWithoutSpace; } + +export function addSpaceUrlContext(basePath = '/', urlContext = '', requestedPath = '') { + if (requestedPath && !requestedPath.startsWith('/')) { + throw new Error(`path must start with a /`); + } + + if (urlContext) { + return `${basePath}/s/${urlContext}${requestedPath}`; + } + return `${basePath}${requestedPath}`; +} diff --git a/x-pack/plugins/spaces/common/spaces_url_parser.test.js b/x-pack/plugins/spaces/common/spaces_url_parser.test.js index ee0813b2f70ac..2491c90243c91 100644 --- a/x-pack/plugins/spaces/common/spaces_url_parser.test.js +++ b/x-pack/plugins/spaces/common/spaces_url_parser.test.js @@ -3,37 +3,61 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { stripSpaceUrlContext, getSpaceUrlContext } from './spaces_url_parser'; +import { stripSpaceUrlContext, getSpaceUrlContext, addSpaceUrlContext } from './spaces_url_parser'; -test('it removes the space url context from the base path when the space is not at the root', () => { - const basePath = `/foo/s/my-space`; - expect(stripSpaceUrlContext(basePath)).toEqual('/foo'); -}); +describe('stripSpaceUrlContext', () => { + test('it removes the space url context from the base path when the space is not at the root', () => { + const basePath = `/foo/s/my-space`; + expect(stripSpaceUrlContext(basePath)).toEqual('/foo'); + }); -test('it removes the space url context from the base path when the space is the root', () => { - const basePath = `/s/my-space`; - expect(stripSpaceUrlContext(basePath)).toEqual(''); -}); + test('it removes the space url context from the base path when the space is the root', () => { + const basePath = `/s/my-space`; + expect(stripSpaceUrlContext(basePath)).toEqual(''); + }); -test(`it doesn't change base paths without a space url context`, () => { - const basePath = `/this/is/a-base-path/ok`; - expect(stripSpaceUrlContext(basePath)).toEqual(basePath); -}); + test(`it doesn't change base paths without a space url context`, () => { + const basePath = `/this/is/a-base-path/ok`; + expect(stripSpaceUrlContext(basePath)).toEqual(basePath); + }); -test('it accepts no parameters', () => { - expect(stripSpaceUrlContext()).toEqual(''); -}); + test('it accepts no parameters', () => { + expect(stripSpaceUrlContext()).toEqual(''); + }); -test('it remove the trailing slash', () => { - expect(stripSpaceUrlContext('/')).toEqual(''); + test('it remove the trailing slash', () => { + expect(stripSpaceUrlContext('/')).toEqual(''); + }); }); -test('it identifies the space url context', () => { - const basePath = `/this/is/a/crazy/path/s/my-awesome-space-lives-here`; - expect(getSpaceUrlContext(basePath)).toEqual('my-awesome-space-lives-here'); -}); +describe('getSpaceUrlContext', () => { + test('it identifies the space url context', () => { + const basePath = `/this/is/a/crazy/path/s/my-awesome-space-lives-here`; + expect(getSpaceUrlContext(basePath)).toEqual('my-awesome-space-lives-here'); + }); -test('it handles base url without a space url context', () => { - const basePath = `/this/is/a/crazy/path/s`; - expect(getSpaceUrlContext(basePath)).toEqual(null); + test('it handles base url without a space url context', () => { + const basePath = `/this/is/a/crazy/path/s`; + expect(getSpaceUrlContext(basePath)).toEqual(''); + }); }); + +describe('addSpaceUrlContext', () => { + test('handles no parameters', () => { + expect(addSpaceUrlContext()).toEqual(`/`); + }); + + test('it adds to the basePath correctly', () => { + expect(addSpaceUrlContext('/my/base/path', 'url-context')).toEqual('/my/base/path/s/url-context'); + }); + + test('it appends the requested path to the end of the url context', () => { + expect(addSpaceUrlContext('/base', 'context', '/final/destination')).toEqual('/base/s/context/final/destination'); + }); + + test('it throws an error when the requested path does not start with a slash', () => { + expect(() => { + addSpaceUrlContext('', '', 'foo'); + }).toThrowErrorMatchingSnapshot(); + }); +}); \ No newline at end of file diff --git a/x-pack/plugins/spaces/public/lib/spaces_manager.js b/x-pack/plugins/spaces/public/lib/spaces_manager.js index 77b1ea9b514ca..d7db211c28a3f 100644 --- a/x-pack/plugins/spaces/public/lib/spaces_manager.js +++ b/x-pack/plugins/spaces/public/lib/spaces_manager.js @@ -35,4 +35,14 @@ export class SpacesManager { return await this._httpAgent .delete(`${this._baseUrl}/space/${space.id}`); } + + async changeSelectedSpace(space) { + return await this._httpAgent + .put(`${this._baseUrl}/space/${space.id}/select`) + .then(response => { + if (response.data && response.data.location) { + window.location = response.data.location; + } + }); + } } diff --git a/x-pack/plugins/spaces/public/views/components/space_cards.js b/x-pack/plugins/spaces/public/views/components/space_cards.js index 5e96bc50cc4e2..7a96485d7c175 100644 --- a/x-pack/plugins/spaces/public/views/components/space_cards.js +++ b/x-pack/plugins/spaces/public/views/components/space_cards.js @@ -6,9 +6,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import chrome from 'ui/chrome'; import { SpaceCard } from './space_card'; -import { stripSpaceUrlContext } from '../../../common/spaces_url_parser'; import { EuiFlexGroup, EuiFlexItem, @@ -34,13 +32,12 @@ export class SpaceCards extends Component { createSpaceClickHandler = (space) => { return () => { - const baseUrlWithoutSpace = stripSpaceUrlContext(chrome.getBasePath()); - - window.location = `${baseUrlWithoutSpace}/s/${space.urlContext}`; + this.props.onSpaceSelect(space); }; }; } SpaceCards.propTypes = { spaces: PropTypes.array.isRequired, + onSpaceSelect: PropTypes.func.isRequired, }; diff --git a/x-pack/plugins/spaces/public/views/components/space_cards.test.js b/x-pack/plugins/spaces/public/views/components/space_cards.test.js index e970c37996036..0a7fdd79d2059 100644 --- a/x-pack/plugins/spaces/public/views/components/space_cards.test.js +++ b/x-pack/plugins/spaces/public/views/components/space_cards.test.js @@ -15,5 +15,5 @@ test('it renders without crashing', () => { description: 'space description' }; - shallow(); -}); \ No newline at end of file + shallow(); +}); diff --git a/x-pack/plugins/spaces/public/views/nav_control/nav_control_modal.js b/x-pack/plugins/spaces/public/views/nav_control/nav_control_modal.js index 8de5ded601092..a22803af86847 100644 --- a/x-pack/plugins/spaces/public/views/nav_control/nav_control_modal.js +++ b/x-pack/plugins/spaces/public/views/nav_control/nav_control_modal.js @@ -65,7 +65,7 @@ export class NavControlModal extends Component { Select a space - + @@ -128,6 +128,10 @@ export class NavControlModal extends Component { isOpen: false }); } + + onSelectSpace = (space) => { + this.props.spacesManager.changeSelectedSpace(space); + } } NavControlModal.propTypes = { diff --git a/x-pack/plugins/spaces/public/views/space_selector/space_selector.js b/x-pack/plugins/spaces/public/views/space_selector/space_selector.js index d0dbc65d4566a..9f84866390317 100644 --- a/x-pack/plugins/spaces/public/views/space_selector/space_selector.js +++ b/x-pack/plugins/spaces/public/views/space_selector/space_selector.js @@ -97,7 +97,7 @@ export class SpaceSelector extends Component { - + { filteredSpaces.length === 0 && @@ -134,6 +134,10 @@ export class SpaceSelector extends Component { searchTerm: searchTerm.trim().toLowerCase() }); } + + onSelectSpace = (space) => { + this.props.spacesManager.changeSelectedSpace(space); + } } SpaceSelector.propTypes = { diff --git a/x-pack/plugins/spaces/server/lib/space_request_interceptors.js b/x-pack/plugins/spaces/server/lib/space_request_interceptors.js index 3372f55279fe3..99b21e29bce14 100644 --- a/x-pack/plugins/spaces/server/lib/space_request_interceptors.js +++ b/x-pack/plugins/spaces/server/lib/space_request_interceptors.js @@ -5,6 +5,7 @@ */ import { wrapError } from './errors'; +import { addSpaceUrlContext } from '../../common/spaces_url_parser'; export function initSpacesRequestInterceptors(server) { @@ -52,6 +53,10 @@ export function initSpacesRequestInterceptors(server) { type: 'space' }); + const config = server.config(); + const basePath = config.get('server.basePath'); + const defaultRoute = config.get('server.defaultRoute'); + if (total === 1) { // If only one space is available, then send user there directly. // No need for an interstitial screen where there is only one possible outcome. @@ -60,17 +65,7 @@ export function initSpacesRequestInterceptors(server) { urlContext } = space.attributes; - const config = server.config(); - const basePath = config.get('server.basePath'); - const defaultRoute = config.get('server.defaultRoute'); - - let destination; - if (urlContext) { - destination = `${basePath}/s/${urlContext}${defaultRoute}`; - } else { - destination = `${basePath}${defaultRoute}`; - } - + const destination = addSpaceUrlContext(basePath, urlContext, defaultRoute); return reply.redirect(destination); } diff --git a/x-pack/plugins/spaces/server/routes/api/v1/spaces.js b/x-pack/plugins/spaces/server/routes/api/v1/spaces.js index 01ce98f97cf4f..1e6745444b01f 100644 --- a/x-pack/plugins/spaces/server/routes/api/v1/spaces.js +++ b/x-pack/plugins/spaces/server/routes/api/v1/spaces.js @@ -12,6 +12,7 @@ import { spaceSchema } from '../../../lib/space_schema'; import { wrapError } from '../../../lib/errors'; import { isReservedSpace } from '../../../../common/is_reserved_space'; import { createDuplicateContextQuery } from '../../../lib/check_duplicate_context'; +import { addSpaceUrlContext } from '../../../../common/spaces_url_parser'; export function initSpacesApi(server) { const routePreCheckLicenseFn = routePreCheckLicense(server); @@ -204,6 +205,29 @@ export function initSpacesApi(server) { } }); + server.route({ + method: 'PUT', + path: '/api/spaces/v1/space/{id}/select', + async handler(request, reply) { + const client = request.getSavedObjectsClient(); + + const id = request.params.id; + + try { + const existingSpace = await getSpaceById(client, id); + + const config = server.config(); + + return reply({ + location: addSpaceUrlContext(config.get('server.basePath'), existingSpace.urlContext, config.get('server.defaultRoute')) + }); + + } catch (e) { + return reply(wrapError(e)); + } + } + }); + async function getSpaceById(client, spaceId) { try { const existingSpace = await client.get('space', spaceId); diff --git a/x-pack/plugins/spaces/server/routes/api/v1/spaces.test.js b/x-pack/plugins/spaces/server/routes/api/v1/spaces.test.js index 2318b4cd24319..9e60f66d42238 100644 --- a/x-pack/plugins/spaces/server/routes/api/v1/spaces.test.js +++ b/x-pack/plugins/spaces/server/routes/api/v1/spaces.test.js @@ -17,7 +17,7 @@ jest.mock('../../../../../../server/lib/get_client_shield', () => { return { getClient: () => { return { - callWithInternalUser: jest.fn(() => {}) + callWithInternalUser: jest.fn(() => { }) }; } }; @@ -48,6 +48,11 @@ describe('Spaces API', () => { const teardowns = []; let request; + const config = { + 'server.ssl.enabled': true, + 'server.basePath': '' + }; + beforeEach(() => { request = async (method, path, setupFn = () => { }) => { @@ -59,7 +64,7 @@ describe('Spaces API', () => { server.decorate('server', 'config', jest.fn(() => { return { - get: () => '' + get: (key) => config[key] }; })); @@ -148,4 +153,18 @@ describe('Spaces API', () => { message: "This Space cannot be deleted because it is reserved." }); }); + + test('PUT space/{id}/select should respond with the new space location', async () => { + const response = await request('PUT', '/api/spaces/v1/space/a-space/select'); + + const { + statusCode, + payload + } = response; + + expect(statusCode).toEqual(200); + + const result = JSON.parse(payload); + expect(result.location).toEqual('/s/a-space'); + }); }); From 9469742bdd74946f3ccdd55b55e66903c2539b3a Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Mon, 25 Jun 2018 14:53:48 -0400 Subject: [PATCH 17/41] additional unit tests --- .../__snapshots__/space_avatar.test.js.snap | 11 ++++++++ .../views/components/space_avatar.test.js | 14 ++++++++++ .../server/lib/create_spaces_service.test.js | 27 +++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 x-pack/plugins/spaces/public/views/components/__snapshots__/space_avatar.test.js.snap create mode 100644 x-pack/plugins/spaces/public/views/components/space_avatar.test.js create mode 100644 x-pack/plugins/spaces/server/lib/create_spaces_service.test.js diff --git a/x-pack/plugins/spaces/public/views/components/__snapshots__/space_avatar.test.js.snap b/x-pack/plugins/spaces/public/views/components/__snapshots__/space_avatar.test.js.snap new file mode 100644 index 0000000000000..14a898d7e66d6 --- /dev/null +++ b/x-pack/plugins/spaces/public/views/components/__snapshots__/space_avatar.test.js.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders without crashing 1`] = ` + +`; diff --git a/x-pack/plugins/spaces/public/views/components/space_avatar.test.js b/x-pack/plugins/spaces/public/views/components/space_avatar.test.js new file mode 100644 index 0000000000000..d63d3b393940d --- /dev/null +++ b/x-pack/plugins/spaces/public/views/components/space_avatar.test.js @@ -0,0 +1,14 @@ +/* + * 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 React from 'react'; +import { shallow } from 'enzyme'; +import { SpaceAvatar } from './space_avatar'; + +test('renders without crashing', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/x-pack/plugins/spaces/server/lib/create_spaces_service.test.js b/x-pack/plugins/spaces/server/lib/create_spaces_service.test.js new file mode 100644 index 0000000000000..55003267d1990 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/create_spaces_service.test.js @@ -0,0 +1,27 @@ +/* + * 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 { createSpacesService } from "./create_spaces_service"; + +const createRequest = (urlContext) => ({ + getBasePath: () => urlContext ? `/s/${urlContext}` : '' +}); + +test('returns null for the default space', () => { + const service = createSpacesService(); + expect(service.getUrlContext(createRequest())).toEqual(null); +}); + +test('uses the provided default context when supplied for the default space', () => { + const service = createSpacesService(); + expect(service.getUrlContext(createRequest(), 'default-context')).toEqual('default-context'); +}); + +test('returns the urlContext for the current space', () => { + const request = createRequest('my-space-context'); + const service = createSpacesService(); + expect(service.getUrlContext(request)).toEqual('my-space-context'); +}); From e24578fc0cc5a6a5cdefd667ad3eb7d9bef0ebae Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Mon, 25 Jun 2018 15:14:49 -0400 Subject: [PATCH 18/41] Create default space on startup, *after* ES has gone green --- x-pack/plugins/spaces/index.js | 9 +- .../lib/mirror_status_and_initialize.js | 61 ++++++++ .../lib/mirror_status_and_initialize.test.js | 136 ++++++++++++++++++ 3 files changed, 202 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/spaces/server/lib/mirror_status_and_initialize.js create mode 100644 x-pack/plugins/spaces/server/lib/mirror_status_and_initialize.test.js diff --git a/x-pack/plugins/spaces/index.js b/x-pack/plugins/spaces/index.js index c5109b9730a30..f1ce0cf419f86 100644 --- a/x-pack/plugins/spaces/index.js +++ b/x-pack/plugins/spaces/index.js @@ -11,11 +11,11 @@ import { initSpacesApi } from './server/routes/api/v1/spaces'; import { initSpacesRequestInterceptors } from './server/lib/space_request_interceptors'; import { createDefaultSpace } from './server/lib/create_default_space'; import { createSpacesService } from './server/lib/create_spaces_service'; -import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status'; import { getActiveSpace } from './server/lib/get_active_space'; import { wrapError } from './server/lib/errors'; import mappings from './mappings.json'; import { spacesSavedObjectsClientWrapper } from './server/lib/saved_objects_client/saved_objects_client_wrapper'; +import { mirrorStatusAndInitialize } from './server/lib/mirror_status_and_initialize'; export const spaces = (kibana) => new kibana.Plugin({ id: 'spaces', @@ -67,7 +67,10 @@ export const spaces = (kibana) => new kibana.Plugin({ async init(server) { const thisPlugin = this; const xpackMainPlugin = server.plugins.xpack_main; - mirrorPluginStatus(xpackMainPlugin, thisPlugin); + + mirrorStatusAndInitialize(xpackMainPlugin.status, thisPlugin.status, async () => { + await createDefaultSpace(server); + }); // Register a function that is called whenever the xpack info changes, // to re-compute the license check results for this plugin @@ -85,7 +88,5 @@ export const spaces = (kibana) => new kibana.Plugin({ initSpacesApi(server); initSpacesRequestInterceptors(server); - - await createDefaultSpace(server); } }); diff --git a/x-pack/plugins/spaces/server/lib/mirror_status_and_initialize.js b/x-pack/plugins/spaces/server/lib/mirror_status_and_initialize.js new file mode 100644 index 0000000000000..5a4035504d948 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/mirror_status_and_initialize.js @@ -0,0 +1,61 @@ +/* + * 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 { Observable } from 'rxjs'; + +export function mirrorStatusAndInitialize(upstreamStatus, downstreamStatus, onGreen) { + const currentState$ = Observable + .of({ + state: upstreamStatus.state, + message: upstreamStatus.message, + }); + + const newState$ = Observable + .fromEvent(upstreamStatus, 'change', null, (previousState, previousMsg, state, message) => { + return { + state, + message, + }; + }); + + const state$ = Observable.merge(currentState$, newState$); + + let onGreenPromise; + const onGreen$ = Observable.create(observer => { + if (!onGreenPromise) { + onGreenPromise = onGreen(); + } + + onGreenPromise + .then(() => { + observer.next({ + state: 'green', + message: 'Ready', + }); + }) + .catch((err) => { + onGreenPromise = null; + observer.next({ + state: 'red', + message: err.message + }); + }); + }); + + + state$ + .switchMap(({ state, message }) => { + if (state !== 'green') { + return Observable.of({ state, message }); + } + + return onGreen$; + }) + .do(({ state, message }) => { + downstreamStatus[state](message); + }) + .subscribe(); +} diff --git a/x-pack/plugins/spaces/server/lib/mirror_status_and_initialize.test.js b/x-pack/plugins/spaces/server/lib/mirror_status_and_initialize.test.js new file mode 100644 index 0000000000000..494b029be30b5 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/mirror_status_and_initialize.test.js @@ -0,0 +1,136 @@ +/* + * 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 { EventEmitter } from 'events'; +import { once } from 'lodash'; +import { mirrorStatusAndInitialize } from './mirror_status_and_initialize'; + +['red', 'yellow', 'disabled'].forEach(state => { + test(`mirrors ${state} immediately`, () => { + const message = `${state} is the status`; + const upstreamStatus = new EventEmitter(); + upstreamStatus.state = state; + upstreamStatus.message = message; + + const downstreamStatus = { + [state]: jest.fn() + }; + + mirrorStatusAndInitialize(upstreamStatus, downstreamStatus); + expect(downstreamStatus[state]).toHaveBeenCalledTimes(1); + expect(downstreamStatus[state]).toHaveBeenCalledWith(message); + }); +}); + +test(`calls onGreen and doesn't immediately set downstream status when the initial status is green`, () => { + const upstreamStatus = new EventEmitter(); + upstreamStatus.state = 'green'; + upstreamStatus.message = ''; + + const downstreamStatus = { + green: jest.fn() + }; + + const onGreenMock = jest.fn().mockImplementation(() => new Promise(() => { })); + mirrorStatusAndInitialize(upstreamStatus, downstreamStatus, onGreenMock); + expect(onGreenMock).toHaveBeenCalledTimes(1); + expect(downstreamStatus.green).toHaveBeenCalledTimes(0); +}); + +test(`only calls onGreen once if it resolves immediately`, () => { + const upstreamStatus = new EventEmitter(); + upstreamStatus.state = 'green'; + upstreamStatus.message = ''; + + const downstreamStatus = { + green: () => { } + }; + + const onGreenMock = jest.fn().mockImplementation(() => Promise.resolve()); + + mirrorStatusAndInitialize(upstreamStatus, downstreamStatus, onGreenMock); + upstreamStatus.emit('change', '', '', 'green', ''); + expect(onGreenMock).toHaveBeenCalledTimes(1); +}); + +test(`calls onGreen twice if it rejects`, (done) => { + const upstreamStatus = new EventEmitter(); + upstreamStatus.state = 'green'; + upstreamStatus.message = ''; + + const downstreamStatus = { + red: once(() => { + // once we see this red, we immediately trigger the upstream status again + // to have it retrigger the onGreen function + upstreamStatus.emit('change', '', '', 'green', ''); + }), + }; + + let count = 0; + const onGreenMock = jest.fn().mockImplementation(() => { + if (++count === 2) { + done(); + } + + return Promise.reject(new Error()); + }); + + mirrorStatusAndInitialize(upstreamStatus, downstreamStatus, onGreenMock); +}); + +test(`sets downstream status to green when onGreen promise resolves`, (done) => { + const state = 'green'; + const message = `${state} is the status`; + const upstreamStatus = new EventEmitter(); + upstreamStatus.state = state; + upstreamStatus.message = message; + + const downstreamStatus = { + green: () => { + done(); + } + }; + + const onGreenMock = jest.fn().mockImplementation(() => Promise.resolve()); + mirrorStatusAndInitialize(upstreamStatus, downstreamStatus, onGreenMock); +}); + +test(`sets downstream status to red when onGreen promise rejects`, (done) => { + const upstreamStatus = new EventEmitter(); + upstreamStatus.state = 'green'; + upstreamStatus.message = ''; + + const errorMessage = 'something went real wrong'; + + const downstreamStatus = { + red: (msg) => { + expect(msg).toBe(errorMessage); + done(); + } + }; + + const onGreenMock = jest.fn().mockImplementation(() => Promise.reject(new Error(errorMessage))); + mirrorStatusAndInitialize(upstreamStatus, downstreamStatus, onGreenMock); +}); + +['red', 'yellow', 'disabled'].forEach(state => { + test(`switches from uninitialized to ${state} on event`, () => { + const message = `${state} is the status`; + const upstreamStatus = new EventEmitter(); + upstreamStatus.state = 'uninitialized'; + upstreamStatus.message = 'uninitialized'; + + const downstreamStatus = { + uninitialized: jest.fn(), + [state]: jest.fn(), + }; + + mirrorStatusAndInitialize(upstreamStatus, downstreamStatus); + upstreamStatus.emit('change', '', '', state, message); + expect(downstreamStatus[state]).toHaveBeenCalledTimes(1); + expect(downstreamStatus[state]).toHaveBeenCalledWith(message); + }); +}); From 5fe4bfd0b309caadecc0830e761e267071fb0fd4 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Tue, 26 Jun 2018 07:50:01 -0400 Subject: [PATCH 19/41] support & testing for bulk_create for space-enabled installations --- .../saved_objects/service/lib/repository.js | 2 +- .../service/lib/repository.test.js | 61 ++- .../secure_saved_objects_client.test.js | 8 +- .../spaces_saved_objects_client.test.js.snap | 7 + .../spaces_saved_objects_client.js | 19 +- .../spaces_saved_objects_client.test.js | 444 ++++++++++++++++++ 6 files changed, 526 insertions(+), 15 deletions(-) create mode 100644 x-pack/plugins/spaces/server/lib/saved_objects_client/__snapshots__/spaces_saved_objects_client.test.js.snap create mode 100644 x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js diff --git a/src/server/saved_objects/service/lib/repository.js b/src/server/saved_objects/service/lib/repository.js index 4cc99933f2a71..c0c2313f4af17 100644 --- a/src/server/saved_objects/service/lib/repository.js +++ b/src/server/saved_objects/service/lib/repository.js @@ -100,7 +100,7 @@ export class SavedObjectsRepository { /** * Creates multiple documents at once * - * @param {array} objects - [{ type, id, attributes }] + * @param {array} objects - [{ type, id, attributes, extraBodyProperties }] * @param {object} [options={}] * @property {boolean} [options.overwrite=false] - overwrites existing documents * @returns {promise} - [{ id, type, version, attributes, error: { message } }] diff --git a/src/server/saved_objects/service/lib/repository.test.js b/src/server/saved_objects/service/lib/repository.test.js index 4000a0f19f468..b5938e26a83c0 100644 --- a/src/server/saved_objects/service/lib/repository.test.js +++ b/src/server/saved_objects/service/lib/repository.test.js @@ -155,11 +155,12 @@ describe('SavedObjectsRepository', () => { }); it('should use create action if ID defined and overwrite=false', async () => { - await savedObjectsRepository.create('index-pattern', { - title: 'Logstash' - }, { - id: 'logstash-*', - }); + await savedObjectsRepository.create('index-pattern', + { + title: 'Logstash' + }, { + id: 'logstash-*', + }); sinon.assert.calledOnce(callAdminCluster); sinon.assert.calledWith(callAdminCluster, 'create'); @@ -191,6 +192,34 @@ describe('SavedObjectsRepository', () => { sinon.assert.calledOnce(onBeforeWrite); }); + + it('appends extraBodyProperties to the document', async () => { + await savedObjectsRepository.create('index-pattern', + { + title: 'Logstash' + }, + { + extraBodyProperties: { + myExtraProp: 'myExtraValue', + myOtherExtraProp: true, + } + } + ); + + sinon.assert.calledOnce(callAdminCluster); + sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({ + id: sinon.match(/index-pattern:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/), + body: { + [`index-pattern`]: { title: 'Logstash' }, + myExtraProp: 'myExtraValue', + myOtherExtraProp: true, + type: 'index-pattern', + updated_at: '2017-08-14T15:49:14.886Z' + } + })); + + sinon.assert.calledOnce(onBeforeWrite); + }); }); describe('#bulkCreate', () => { @@ -325,6 +354,28 @@ describe('SavedObjectsRepository', () => { } ]); }); + + it('appends extraBodyProperties to each created object', async () => { + callAdminCluster.returns({ items: [] }); + + await savedObjectsRepository.bulkCreate( + [ + { type: 'config', id: 'one', attributes: { title: 'Test One' }, extraBodyProperties: { extraConfigValue: true } }, + { type: 'index-pattern', id: 'two', attributes: { title: 'Test Two' }, extraBodyProperties: { extraIndexValue: true } } + ]); + + sinon.assert.calledOnce(callAdminCluster); + sinon.assert.calledWithExactly(callAdminCluster, 'bulk', sinon.match({ + body: [ + { create: { _type: 'doc', _id: 'config:one' } }, + { type: 'config', ...mockTimestampFields, config: { title: 'Test One' }, extraConfigValue: true }, + { create: { _type: 'doc', _id: 'index-pattern:two' } }, + { type: 'index-pattern', ...mockTimestampFields, 'index-pattern': { title: 'Test Two' }, extraIndexValue: true } + ] + })); + + sinon.assert.calledOnce(onBeforeWrite); + }); }); describe('#delete', () => { diff --git a/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.test.js b/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.test.js index 0f9fa6610f683..c7a48b32c4b6c 100644 --- a/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.test.js +++ b/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.test.js @@ -378,7 +378,7 @@ describe('#find', () => { hasPrivileges: mockHasPrivileges, auditLogger: mockAuditLogger, }); - const options = { type: [ type1, type2 ] }; + const options = { type: [type1, type2] }; await expect(client.find(options)).rejects.toThrowError(mockErrors.forbiddenError); @@ -417,7 +417,7 @@ describe('#find', () => { hasPrivileges: mockHasPrivileges, auditLogger: mockAuditLogger, }); - const options = { type: [ type1, type2 ] }; + const options = { type: [type1, type2] }; await expect(client.find(options)).rejects.toThrowError(mockErrors.forbiddenError); @@ -701,7 +701,7 @@ describe('#bulkGet', () => { const result = await client.bulkGet(objects); expect(result).toBe(returnValue); - expect(mockRepository.bulkGet).toHaveBeenCalledWith(objects); + expect(mockRepository.bulkGet).toHaveBeenCalledWith(objects, {}); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'bulk_get', [type1, type2], { objects, @@ -789,7 +789,7 @@ describe('#get', () => { const result = await client.get(type, id); expect(result).toBe(returnValue); - expect(mockRepository.get).toHaveBeenCalledWith(type, id); + expect(mockRepository.get).toHaveBeenCalledWith(type, id, {}); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'get', [type], { type, diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/__snapshots__/spaces_saved_objects_client.test.js.snap b/x-pack/plugins/spaces/server/lib/saved_objects_client/__snapshots__/spaces_saved_objects_client.test.js.snap new file mode 100644 index 0000000000000..1acec87914e90 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/__snapshots__/spaces_saved_objects_client.test.js.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`#delete does not allow an object to be deleted via a different space 1`] = `"not found"`; + +exports[`#get returns error when the object belongs to a different space 1`] = `"not found"`; + +exports[`#update does not allow an object to be updated via a different space 1`] = `"not found"`; diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js index 6e8b7e2346a4a..96c00c710debc 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js @@ -42,12 +42,21 @@ export class SpacesSavedObjectsClient { } async bulkCreate(objects, options = {}) { - options.extraBodyProperties = { - ...options.extraBodyProperties, - spaceId: await this._getSpaceId() - }; + const spaceId = await this._getSpaceId(); + const objectsToCreate = objects.map(o => { + if (isTypeSpaceAware(o.type)) { + return { + ...o, + extraBodyProperties: { + ...o.extraBodyProperties, + spaceId + } + }; + } + return o; + }); - return await this._client.bulkCreate(objects, options); + return await this._client.bulkCreate(objectsToCreate, options); } async delete(type, id) { diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js new file mode 100644 index 0000000000000..11fcc8f96004e --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js @@ -0,0 +1,444 @@ +/* + * 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 { SpacesSavedObjectsClient } from './spaces_saved_objects_client'; +import { createSpacesService } from '../create_spaces_service'; + +const createObjectEntry = (type, id, spaceId) => ({ + [id]: { + id, + type, + spaceId + } +}); + +const SAVED_OBJECTS = { + ...createObjectEntry('foo', 'object_0'), + ...createObjectEntry('foo', 'object_1', 'space_1'), + ...createObjectEntry('foo', 'object_2', 'space_2'), +}; + +const createMockRequest = (space) => ({ + getBasePath: () => space.urlContext ? `/s/${space.urlContext}` : '', +}); + +const createMockClient = (space) => { + return { + get: jest.fn((type, id) => { + return SAVED_OBJECTS[id]; + }), + bulkGet: jest.fn((objects) => { + return { + saved_objects: objects.map(o => SAVED_OBJECTS[o.id]) + }; + }), + find: jest.fn(({ type }) => { + // used to locate spaces when type is `space` within these tests + if (type === 'space') { + return { + saved_objects: [space] + }; + } + throw new Error(`not implemented`); + }), + create: jest.fn(), + bulkCreate: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + errors: { + createGenericNotFoundError: jest.fn(() => { + return new Error('not found'); + }) + } + }; +}; + +describe('#get', () => { + test(`returns the object when it belongs to the current space`, async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + const id = 'object_1'; + + const result = await client.get(type, id); + + expect(result).toBe(SAVED_OBJECTS[id]); + }); + + test(`returns error when the object belongs to a different space`, async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + const id = 'object_2'; + + await expect(client.get(type, id)).rejects.toThrowErrorMatchingSnapshot(); + }); +}); + +describe('#bulk_get', () => { + test(`only returns objects belonging to the current space`, async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + + const result = await client.bulkGet([{ + type, + id: 'object_1' + }, { + type, + id: 'object_2' + }]); + + expect(result).toEqual({ + saved_objects: [{ + id: 'object_1', + spaceId: 'space_1', + type: 'foo', + }, { + id: 'object_2', + type: 'foo', + error: { + message: 'Not found', + statusCode: 404 + } + }] + }); + }); +}); + +describe('#create', () => { + test('automatically assigns the object to the current space via extraBodyProperties', async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + + await client.create(type, attributes); + + expect(baseClient.create).toHaveBeenCalledWith(type, attributes, { + extraBodyProperties: { + spaceId: 'space_1' + } + }); + }); + + test('does not assign a space-unaware object to a space', async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'space'; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + + await client.create(type, attributes); + + expect(baseClient.create).toHaveBeenCalledWith(type, attributes, {}); + }); +}); + +describe('#bulk_create', () => { + test('allows for bulk creation when all types are space-aware', async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + const objects = [{ + type: 'foo', + attributes + }, { + type: 'bar', + attributes + }]; + + await client.bulkCreate(objects); + + const expectedCalledWithObjects = objects.map(o => ({ + ...o, + extraBodyProperties: { + spaceId: 'space_1' + } + })); + + expect(baseClient.bulkCreate).toHaveBeenCalledWith(expectedCalledWithObjects, {}); + }); + + test('allows for bulk creation when all types are not space-aware', async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + + const objects = [{ + type: 'space', + attributes + }, { + type: 'space', + attributes + }]; + + await client.bulkCreate(objects); + + expect(baseClient.bulkCreate).toHaveBeenCalledWith(objects, {}); + }); + + test('allows space-aware and non-space-aware objects to be created at the same time', async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + + const objects = [{ + type: 'space', + attributes + }, { + type: 'foo', + attributes + }]; + + await client.bulkCreate(objects); + + const expectedCalledWithObjects = [...objects]; + expectedCalledWithObjects[1] = { + ...expectedCalledWithObjects[1], + extraBodyProperties: { + spaceId: 'space_1' + } + }; + + expect(baseClient.bulkCreate).toHaveBeenCalledWith(expectedCalledWithObjects, {}); + }); +}); + +describe('#update', () => { + test('allows an object to be updated if it exists in the same space', async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const id = 'object_1'; + const type = 'foo'; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + + await client.update(type, id, attributes); + + expect(baseClient.update).toHaveBeenCalledWith(type, id, attributes, { extraBodyProperties: { spaceId: 'space_1' } }); + }); + + test('does not allow an object to be updated via a different space', async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const id = 'object_2'; + const type = 'foo'; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + + await expect(client.update(type, id, attributes)).rejects.toThrowErrorMatchingSnapshot(); + }); +}); + +describe('#delete', () => { + test('allows an object to be deleted if it exists in the same space', async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const id = 'object_1'; + const type = 'foo'; + + await client.delete(type, id); + + expect(baseClient.delete).toHaveBeenCalledWith(type, id); + }); + + test('does not allow an object to be deleted via a different space', async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const id = 'object_2'; + const type = 'foo'; + + await expect(client.delete(type, id)).rejects.toThrowErrorMatchingSnapshot(); + }); +}); From c6e892584252a6c5acfffcaafc09c3e57bf9d776 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Tue, 26 Jun 2018 08:47:03 -0400 Subject: [PATCH 20/41] cleanup and docs --- .../kibana/ui_setting_defaults.js | 55 +++++++++---------- .../saved_objects/service/lib/repository.js | 8 ++- .../service/lib/search_dsl/query_params.js | 3 + 3 files changed, 36 insertions(+), 30 deletions(-) diff --git a/src/core_plugins/kibana/ui_setting_defaults.js b/src/core_plugins/kibana/ui_setting_defaults.js index 122d28ba07fae..d0ed94c99c6a4 100644 --- a/src/core_plugins/kibana/ui_setting_defaults.js +++ b/src/core_plugins/kibana/ui_setting_defaults.js @@ -32,9 +32,6 @@ export function getUiSettingDefaults() { 'buildNum': { readonly: true }, - 'version': { - readonly: true - }, 'query:queryString:options': { name: 'Query string options', value: '{ "analyze_wildcard": true }', @@ -78,7 +75,7 @@ export function getUiSettingDefaults() { name: 'Scaled date format', type: 'json', value: -`[ + `[ ["", "HH:mm:ss.SSS"], ["PT1S", "HH:mm:ss"], ["PT1M", "HH:mm"], @@ -307,7 +304,7 @@ export function getUiSettingDefaults() { 'format:defaultTypeMap': { name: 'Field type format name', value: -`{ + `{ "ip": { "id": "ip", "params": {} }, "date": { "id": "date", "params": {} }, "number": { "id": "number", "params": {} }, @@ -365,7 +362,7 @@ export function getUiSettingDefaults() { 'timepicker:timeDefaults': { name: 'Time picker defaults', value: -`{ + `{ "from": "now-15m", "to": "now", "mode": "quick" @@ -376,7 +373,7 @@ export function getUiSettingDefaults() { 'timepicker:refreshIntervalDefaults': { name: 'Time picker refresh interval', value: -`{ + `{ "display": "Off", "pause": false, "value": 0 @@ -387,30 +384,30 @@ export function getUiSettingDefaults() { 'timepicker:quickRanges': { name: 'Time picker quick ranges', value: JSON.stringify([ - { from: 'now/d', to: 'now/d', display: 'Today', section: 0 }, - { from: 'now/w', to: 'now/w', display: 'This week', section: 0 }, - { from: 'now/M', to: 'now/M', display: 'This month', section: 0 }, - { from: 'now/y', to: 'now/y', display: 'This year', section: 0 }, - { from: 'now/d', to: 'now', display: 'Today so far', section: 0 }, - { from: 'now/w', to: 'now', display: 'Week to date', section: 0 }, - { from: 'now/M', to: 'now', display: 'Month to date', section: 0 }, - { from: 'now/y', to: 'now', display: 'Year to date', section: 0 }, + { from: 'now/d', to: 'now/d', display: 'Today', section: 0 }, + { from: 'now/w', to: 'now/w', display: 'This week', section: 0 }, + { from: 'now/M', to: 'now/M', display: 'This month', section: 0 }, + { from: 'now/y', to: 'now/y', display: 'This year', section: 0 }, + { from: 'now/d', to: 'now', display: 'Today so far', section: 0 }, + { from: 'now/w', to: 'now', display: 'Week to date', section: 0 }, + { from: 'now/M', to: 'now', display: 'Month to date', section: 0 }, + { from: 'now/y', to: 'now', display: 'Year to date', section: 0 }, - { from: 'now-15m', to: 'now', display: 'Last 15 minutes', section: 1 }, - { from: 'now-30m', to: 'now', display: 'Last 30 minutes', section: 1 }, - { from: 'now-1h', to: 'now', display: 'Last 1 hour', section: 1 }, - { from: 'now-4h', to: 'now', display: 'Last 4 hours', section: 1 }, - { from: 'now-12h', to: 'now', display: 'Last 12 hours', section: 1 }, - { from: 'now-24h', to: 'now', display: 'Last 24 hours', section: 1 }, - { from: 'now-7d', to: 'now', display: 'Last 7 days', section: 1 }, + { from: 'now-15m', to: 'now', display: 'Last 15 minutes', section: 1 }, + { from: 'now-30m', to: 'now', display: 'Last 30 minutes', section: 1 }, + { from: 'now-1h', to: 'now', display: 'Last 1 hour', section: 1 }, + { from: 'now-4h', to: 'now', display: 'Last 4 hours', section: 1 }, + { from: 'now-12h', to: 'now', display: 'Last 12 hours', section: 1 }, + { from: 'now-24h', to: 'now', display: 'Last 24 hours', section: 1 }, + { from: 'now-7d', to: 'now', display: 'Last 7 days', section: 1 }, - { from: 'now-30d', to: 'now', display: 'Last 30 days', section: 2 }, - { from: 'now-60d', to: 'now', display: 'Last 60 days', section: 2 }, - { from: 'now-90d', to: 'now', display: 'Last 90 days', section: 2 }, - { from: 'now-6M', to: 'now', display: 'Last 6 months', section: 2 }, - { from: 'now-1y', to: 'now', display: 'Last 1 year', section: 2 }, - { from: 'now-2y', to: 'now', display: 'Last 2 years', section: 2 }, - { from: 'now-5y', to: 'now', display: 'Last 5 years', section: 2 }, + { from: 'now-30d', to: 'now', display: 'Last 30 days', section: 2 }, + { from: 'now-60d', to: 'now', display: 'Last 60 days', section: 2 }, + { from: 'now-90d', to: 'now', display: 'Last 90 days', section: 2 }, + { from: 'now-6M', to: 'now', display: 'Last 6 months', section: 2 }, + { from: 'now-1y', to: 'now', display: 'Last 1 year', section: 2 }, + { from: 'now-2y', to: 'now', display: 'Last 2 years', section: 2 }, + { from: 'now-5y', to: 'now', display: 'Last 5 years', section: 2 }, ], null, 2), type: 'json', diff --git a/src/server/saved_objects/service/lib/repository.js b/src/server/saved_objects/service/lib/repository.js index c0c2313f4af17..7e400cdcd5ecf 100644 --- a/src/server/saved_objects/service/lib/repository.js +++ b/src/server/saved_objects/service/lib/repository.js @@ -54,6 +54,7 @@ export class SavedObjectsRepository { * @param {object} [options={}] * @property {string} [options.id] - force id on creation, not recommended * @property {boolean} [options.overwrite=false] + * @property {object} [options.extraBodyProperties={}] - extra properties to append to the document body, outside of the object's type property * @returns {promise} - { id, type, version, attributes } */ async create(type, attributes = {}, options = {}) { @@ -210,6 +211,7 @@ export class SavedObjectsRepository { * @property {string} [options.search] * @property {Array} [options.searchFields] - see Elasticsearch Simple Query String * Query field argument for more information + * @property {object} [options.extraQueryParams] - ES Query parameters to merge/append into the generated query * @property {integer} [options.page=1] * @property {integer} [options.perPage=20] * @property {string} [options.sortField] @@ -295,7 +297,8 @@ export class SavedObjectsRepository { * Returns an array of objects by id * * @param {array} objects - an array ids, or an array of objects containing id and optionally type - * @param {object} options + * @param {object} [options = {}] + * @param {array} [options.extraSourceProperties = []] - an array of extra properties to return from the underlying document * @returns {promise} - { saved_objects: [{ id, type, version, attributes }] } * @example * @@ -359,6 +362,8 @@ export class SavedObjectsRepository { * * @param {string} type * @param {string} id + * @param {object} [options = {}] + * @param {array} [options.extraSourceProperties = []] - an array of extra properties to return from the underlying document * @returns {promise} - { id, type, version, attributes } */ async get(type, id, options = {}) { @@ -401,6 +406,7 @@ export class SavedObjectsRepository { * @param {string} id * @param {object} [options={}] * @property {integer} options.version - ensures version matches that of persisted object + * @param {array} [options.extraBodyProperties = []] - an array of extra properties to write into the underlying document * @returns {promise} */ async update(type, id, attributes, options = {}) { diff --git a/src/server/saved_objects/service/lib/search_dsl/query_params.js b/src/server/saved_objects/service/lib/search_dsl/query_params.js index 3f80b4c6fa537..f5986de2bb8da 100644 --- a/src/server/saved_objects/service/lib/search_dsl/query_params.js +++ b/src/server/saved_objects/service/lib/search_dsl/query_params.js @@ -66,6 +66,7 @@ function getFieldsForTypes(searchFields, types) { * @param {(string|Array)} type * @param {String} search * @param {Array} searchFields + * @param {Object} extraQueryParams query parameters to merge into the result * @return {Object} */ export function getQueryParams(mappings, type, search, searchFields, extraQueryParams = {}) { @@ -94,12 +95,14 @@ export function getQueryParams(mappings, type, search, searchFields, extraQueryP ]; } + // a list of fields to manually merge together const fieldsToMerge = ['filter', 'must']; const extraParams = { ...extraQueryParams.bool }; + // Remove the manual merge fields from the collection of properties we will automatically combine. fieldsToMerge.forEach(field => delete extraParams[field]); let query = { From f4a19ab2da58bd6c3854686a2e70ec4e8ae18f21 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Wed, 27 Jun 2018 09:41:40 -0400 Subject: [PATCH 21/41] undo formatting changes --- .../kibana/ui_setting_defaults.js | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/core_plugins/kibana/ui_setting_defaults.js b/src/core_plugins/kibana/ui_setting_defaults.js index d0ed94c99c6a4..2511a25fba04f 100644 --- a/src/core_plugins/kibana/ui_setting_defaults.js +++ b/src/core_plugins/kibana/ui_setting_defaults.js @@ -75,7 +75,7 @@ export function getUiSettingDefaults() { name: 'Scaled date format', type: 'json', value: - `[ +`[ ["", "HH:mm:ss.SSS"], ["PT1S", "HH:mm:ss"], ["PT1M", "HH:mm"], @@ -304,7 +304,7 @@ export function getUiSettingDefaults() { 'format:defaultTypeMap': { name: 'Field type format name', value: - `{ +`{ "ip": { "id": "ip", "params": {} }, "date": { "id": "date", "params": {} }, "number": { "id": "number", "params": {} }, @@ -362,7 +362,7 @@ export function getUiSettingDefaults() { 'timepicker:timeDefaults': { name: 'Time picker defaults', value: - `{ +`{ "from": "now-15m", "to": "now", "mode": "quick" @@ -373,7 +373,7 @@ export function getUiSettingDefaults() { 'timepicker:refreshIntervalDefaults': { name: 'Time picker refresh interval', value: - `{ +`{ "display": "Off", "pause": false, "value": 0 @@ -384,30 +384,30 @@ export function getUiSettingDefaults() { 'timepicker:quickRanges': { name: 'Time picker quick ranges', value: JSON.stringify([ - { from: 'now/d', to: 'now/d', display: 'Today', section: 0 }, - { from: 'now/w', to: 'now/w', display: 'This week', section: 0 }, - { from: 'now/M', to: 'now/M', display: 'This month', section: 0 }, - { from: 'now/y', to: 'now/y', display: 'This year', section: 0 }, - { from: 'now/d', to: 'now', display: 'Today so far', section: 0 }, - { from: 'now/w', to: 'now', display: 'Week to date', section: 0 }, - { from: 'now/M', to: 'now', display: 'Month to date', section: 0 }, - { from: 'now/y', to: 'now', display: 'Year to date', section: 0 }, + { from: 'now/d', to: 'now/d', display: 'Today', section: 0 }, + { from: 'now/w', to: 'now/w', display: 'This week', section: 0 }, + { from: 'now/M', to: 'now/M', display: 'This month', section: 0 }, + { from: 'now/y', to: 'now/y', display: 'This year', section: 0 }, + { from: 'now/d', to: 'now', display: 'Today so far', section: 0 }, + { from: 'now/w', to: 'now', display: 'Week to date', section: 0 }, + { from: 'now/M', to: 'now', display: 'Month to date', section: 0 }, + { from: 'now/y', to: 'now', display: 'Year to date', section: 0 }, - { from: 'now-15m', to: 'now', display: 'Last 15 minutes', section: 1 }, - { from: 'now-30m', to: 'now', display: 'Last 30 minutes', section: 1 }, - { from: 'now-1h', to: 'now', display: 'Last 1 hour', section: 1 }, - { from: 'now-4h', to: 'now', display: 'Last 4 hours', section: 1 }, - { from: 'now-12h', to: 'now', display: 'Last 12 hours', section: 1 }, - { from: 'now-24h', to: 'now', display: 'Last 24 hours', section: 1 }, - { from: 'now-7d', to: 'now', display: 'Last 7 days', section: 1 }, + { from: 'now-15m', to: 'now', display: 'Last 15 minutes', section: 1 }, + { from: 'now-30m', to: 'now', display: 'Last 30 minutes', section: 1 }, + { from: 'now-1h', to: 'now', display: 'Last 1 hour', section: 1 }, + { from: 'now-4h', to: 'now', display: 'Last 4 hours', section: 1 }, + { from: 'now-12h', to: 'now', display: 'Last 12 hours', section: 1 }, + { from: 'now-24h', to: 'now', display: 'Last 24 hours', section: 1 }, + { from: 'now-7d', to: 'now', display: 'Last 7 days', section: 1 }, - { from: 'now-30d', to: 'now', display: 'Last 30 days', section: 2 }, - { from: 'now-60d', to: 'now', display: 'Last 60 days', section: 2 }, - { from: 'now-90d', to: 'now', display: 'Last 90 days', section: 2 }, - { from: 'now-6M', to: 'now', display: 'Last 6 months', section: 2 }, - { from: 'now-1y', to: 'now', display: 'Last 1 year', section: 2 }, - { from: 'now-2y', to: 'now', display: 'Last 2 years', section: 2 }, - { from: 'now-5y', to: 'now', display: 'Last 5 years', section: 2 }, + { from: 'now-30d', to: 'now', display: 'Last 30 days', section: 2 }, + { from: 'now-60d', to: 'now', display: 'Last 60 days', section: 2 }, + { from: 'now-90d', to: 'now', display: 'Last 90 days', section: 2 }, + { from: 'now-6M', to: 'now', display: 'Last 6 months', section: 2 }, + { from: 'now-1y', to: 'now', display: 'Last 1 year', section: 2 }, + { from: 'now-2y', to: 'now', display: 'Last 2 years', section: 2 }, + { from: 'now-5y', to: 'now', display: 'Last 5 years', section: 2 }, ], null, 2), type: 'json', @@ -512,4 +512,4 @@ export function getUiSettingDefaults() { category: ['discover'], }, }; -} +} \ No newline at end of file From 7827e023940546adcfc587b9a559699012ea0fec Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 29 Jun 2018 11:37:13 -0400 Subject: [PATCH 22/41] append space id to document id within spaces saved objects client --- x-pack/package.json | 1 + .../spaces_saved_objects_client.test.js.snap | 12 +- .../spaces_saved_objects_client.js | 95 +- .../spaces_saved_objects_client.test.js | 1034 ++++++++++++----- .../apis/saved_objects/create.js | 4 +- .../saved_objects/lib/space_test_utils.js | 1 - .../saved_objects/spaces/data.json.gz | Bin 2459 -> 2467 bytes 7 files changed, 805 insertions(+), 342 deletions(-) diff --git a/x-pack/package.json b/x-pack/package.json index 0bfae3833bd9f..89f3c5896b71b 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -70,6 +70,7 @@ "tmp": "0.0.31", "tree-kill": "^1.1.0", "typescript": "^2.8.3", + "uuid": "3.0.1", "vinyl-fs": "^3.0.2", "xml-crypto": "^0.10.1", "xml2js": "^0.4.19", diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/__snapshots__/spaces_saved_objects_client.test.js.snap b/x-pack/plugins/spaces/server/lib/saved_objects_client/__snapshots__/spaces_saved_objects_client.test.js.snap index 1acec87914e90..3279b87ff3cad 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/__snapshots__/spaces_saved_objects_client.test.js.snap +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/__snapshots__/spaces_saved_objects_client.test.js.snap @@ -1,7 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#delete does not allow an object to be deleted via a different space 1`] = `"not found"`; +exports[`within a space #delete does not allow an object to be deleted via a different space 1`] = `"object not found: space_1:object_2"`; -exports[`#get returns error when the object belongs to a different space 1`] = `"not found"`; +exports[`within a space #get returns error when the object belongs to a different space 1`] = `"object not found: space_1:object_2"`; -exports[`#update does not allow an object to be updated via a different space 1`] = `"not found"`; +exports[`within a space #update does not allow an object to be updated via a different space 1`] = `"object not found: space_1:object_2"`; + +exports[`within the default space #delete does not allow an object to be deleted via a different space 1`] = `"object not found: object_2"`; + +exports[`within the default space #get returns error when the object belongs to a different space 1`] = `"object not found: object_2"`; + +exports[`within the default space #update does not allow an object to be updated via a different space 1`] = `"object not found: object_2"`; diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js index 96c00c710debc..8337ea7b672dd 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js @@ -7,6 +7,7 @@ import { DEFAULT_SPACE_ID } from '../../../common/constants'; import { isTypeSpaceAware } from './lib/is_type_space_aware'; import { getSpacesQueryParams } from './lib/query_params'; +import uuid from 'uuid'; export class SpacesSavedObjectsClient { constructor(options) { @@ -29,24 +30,26 @@ export class SpacesSavedObjectsClient { async create(type, attributes = {}, options = {}) { const spaceId = await this._getSpaceId(); - const shouldAssignSpaceId = spaceId !== DEFAULT_SPACE_ID && isTypeSpaceAware(type); - if (shouldAssignSpaceId) { + if (this._shouldAssignSpaceId(type, spaceId)) { + options.id = this._generateDocumentId(spaceId, options.id); options.extraBodyProperties = { ...options.extraBodyProperties, - spaceId: await this._getSpaceId() + spaceId }; } - return await this._client.create(type, attributes, options); + const result = await this._client.create(type, attributes, options); + return this._trimSpaceId(spaceId, result); } async bulkCreate(objects, options = {}) { const spaceId = await this._getSpaceId(); const objectsToCreate = objects.map(o => { - if (isTypeSpaceAware(o.type)) { + if (this._shouldAssignSpaceId(o.type, spaceId)) { return { ...o, + id: this._generateDocumentId(spaceId, o.id), extraBodyProperties: { ...o.extraBodyProperties, spaceId @@ -56,7 +59,8 @@ export class SpacesSavedObjectsClient { return o; }); - return await this._client.bulkCreate(objectsToCreate, options); + const result = await this._client.bulkCreate(objectsToCreate, options); + return result.map(object => this._trimSpaceId(spaceId, object)); } async delete(type, id) { @@ -64,7 +68,14 @@ export class SpacesSavedObjectsClient { // this ensures that the document belongs to the current space. await this.get(type, id); - return await this._client.delete(type, id); + let documentId = id; + + if (this._shouldAssignSpaceId(type, id)) { + const spaceId = await this._getSpaceId(); + documentId = this._generateDocumentId(spaceId, id); + } + + return await this._client.delete(type, documentId); } async find(options = {}) { @@ -79,31 +90,39 @@ export class SpacesSavedObjectsClient { spaceOptions.extraQueryParams = getSpacesQueryParams(spaceId, types); - return await this._client.find({ ...options, ...spaceOptions }); + const result = await this._client.find({ ...options, ...spaceOptions }); + result.saved_objects.map(object => this._trimSpaceId(spaceId, object)); + + return result; } async bulkGet(objects = []) { // ES 'mget' does not support queries, so we have to filter results after the fact. const thisSpaceId = await this._getSpaceId(); - const result = await this._client.bulkGet(objects, { + const objectsToQuery = objects.map(o => ({ + ...o, + id: this._generateDocumentId(thisSpaceId, o.id) + })); + + const result = await this._client.bulkGet(objectsToQuery, { extraSourceProperties: ['spaceId', 'type'] }); result.saved_objects = result.saved_objects.map(savedObject => { - const { id, type, spaceId } = savedObject; + const { id, type, spaceId = DEFAULT_SPACE_ID } = savedObject; if (isTypeSpaceAware(type)) { if (spaceId !== thisSpaceId) { - return { + return this._trimSpaceId(thisSpaceId, { id, type, error: { statusCode: 404, message: 'Not found' } - }; + }); } } - return savedObject; + return this._trimSpaceId(thisSpaceId, savedObject); }); return result; @@ -112,7 +131,14 @@ export class SpacesSavedObjectsClient { async get(type, id) { // ES 'get' does not support queries, so we have to filter results after the fact. - const response = await this._client.get(type, id, { + let documentId = id; + + const spaceId = await this._getSpaceId(); + if (isTypeSpaceAware(type)) { + documentId = this._generateDocumentId(spaceId, id); + } + + const response = await this._client.get(type, documentId, { extraSourceProperties: ['spaceId'] }); @@ -125,22 +151,30 @@ export class SpacesSavedObjectsClient { } } - return response; + return this._trimSpaceId(spaceId, response); } async update(type, id, attributes, options = {}) { // attempt to retrieve document before updating. // this ensures that the document belongs to the current space. + let documentId = id; + const spaceId = await this._getSpaceId(); + if (isTypeSpaceAware(type)) { await this.get(type, id); - options.extraBodyProperties = { - ...options.extraBodyProperties, - spaceId: await this._getSpaceId() - }; + documentId = this._generateDocumentId(spaceId, id); + + if (this._shouldAssignSpaceId(type, spaceId)) { + options.extraBodyProperties = { + ...options.extraBodyProperties, + spaceId + }; + } } - return await this._client.update(type, id, attributes, options); + const result = await this._client.update(type, documentId, attributes, options); + return this._trimSpaceId(spaceId, result); } async _getSpaceId() { @@ -170,4 +204,25 @@ export class SpacesSavedObjectsClient { return null; } + + _shouldAssignSpaceId(type, spaceId) { + return isTypeSpaceAware(type) && spaceId !== DEFAULT_SPACE_ID; + } + + _generateDocumentId(spaceId, id = uuid.v1()) { + if (!spaceId || spaceId === DEFAULT_SPACE_ID) { + return id; + } + return `${spaceId}:${id}`; + } + + _trimSpaceId(spaceId, savedObject) { + const prefix = `${spaceId}:`; + + if (savedObject.id.startsWith(prefix)) { + savedObject.id = savedObject.id.slice(prefix.length); + } + + return savedObject; + } } diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js index 11fcc8f96004e..98fcc39ff7916 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js @@ -7,6 +7,10 @@ import { SpacesSavedObjectsClient } from './spaces_saved_objects_client'; import { createSpacesService } from '../create_spaces_service'; +jest.mock('uuid', () => ({ + v1: jest.fn(() => `mock-id`) +})); + const createObjectEntry = (type, id, spaceId) => ({ [id]: { id, @@ -17,8 +21,8 @@ const createObjectEntry = (type, id, spaceId) => ({ const SAVED_OBJECTS = { ...createObjectEntry('foo', 'object_0'), - ...createObjectEntry('foo', 'object_1', 'space_1'), - ...createObjectEntry('foo', 'object_2', 'space_2'), + ...createObjectEntry('foo', 'space_1:object_1', 'space_1'), + ...createObjectEntry('foo', 'space_2:object_2', 'space_2'), }; const createMockRequest = (space) => ({ @@ -28,11 +32,22 @@ const createMockRequest = (space) => ({ const createMockClient = (space) => { return { get: jest.fn((type, id) => { - return SAVED_OBJECTS[id]; + const object = SAVED_OBJECTS[id]; + if (!object) { + throw new Error(`object not found: ${id}`); + } + return object; }), bulkGet: jest.fn((objects) => { return { - saved_objects: objects.map(o => SAVED_OBJECTS[o.id]) + saved_objects: objects.map(o => SAVED_OBJECTS[o.id] || { + id: o.id, + type: o.type, + error: { + statusCode: 404, + message: 'Not found' + } + }) }; }), find: jest.fn(({ type }) => { @@ -44,9 +59,20 @@ const createMockClient = (space) => { } throw new Error(`not implemented`); }), - create: jest.fn(), - bulkCreate: jest.fn(), - update: jest.fn(), + create: jest.fn((type, attributes, options) => ({ + id: options.id || 'some-new-id', + type, + attributes + })), + bulkCreate: jest.fn(((objects) => objects.map((o, i) => ({ + ...o, + id: o.id || `abc-${i}` + })))), + update: jest.fn((type, id, attributes) => ({ + id, + type, + attributes + })), delete: jest.fn(), errors: { createGenericNotFoundError: jest.fn(() => { @@ -55,390 +81,764 @@ const createMockClient = (space) => { } }; }; +describe('within the default space', () => { + describe('#get', () => { + test(`returns the object when it belongs to the default space`, async () => { + const currentSpace = { + id: 'default', + urlContext: '' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + const id = 'object_0'; -describe('#get', () => { - test(`returns the object when it belongs to the current space`, async () => { - const currentSpace = { - id: 'space_1', - urlContext: 'space-1' - }; - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], + const result = await client.get(type, id); + + expect(result).toBe(SAVED_OBJECTS[id]); }); - const type = 'foo'; - const id = 'object_1'; + test(`returns error when the object belongs to a different space`, async () => { + const currentSpace = { + id: 'default', + urlContext: '' + }; - const result = await client.get(type, id); + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); - expect(result).toBe(SAVED_OBJECTS[id]); - }); + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); - test(`returns error when the object belongs to a different space`, async () => { - const currentSpace = { - id: 'space_1', - urlContext: 'space-1' - }; - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], + const type = 'foo'; + const id = 'object_2'; + + await expect(client.get(type, id)).rejects.toThrowErrorMatchingSnapshot(); }); + }); - const type = 'foo'; - const id = 'object_2'; + describe('#bulk_get', () => { + test(`only returns objects belonging to the default space`, async () => { + const currentSpace = { + id: 'default', + urlContext: '' + }; - await expect(client.get(type, id)).rejects.toThrowErrorMatchingSnapshot(); + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + + const result = await client.bulkGet([{ + type, + id: 'object_0' + }, { + type, + id: 'object_2' + }]); + + expect(result).toEqual({ + saved_objects: [{ + id: 'object_0', + type: 'foo', + }, { + id: 'object_2', + type: 'foo', + error: { + message: 'Not found', + statusCode: 404 + } + }] + }); + }); }); -}); -describe('#bulk_get', () => { - test(`only returns objects belonging to the current space`, async () => { - const currentSpace = { - id: 'space_1', - urlContext: 'space-1' - }; - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], + describe('#create', () => { + test('automatically assigns the object to the default space by not using extraBodyProperties', async () => { + const currentSpace = { + id: 'default', + urlContext: '' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + + await client.create(type, attributes); + + expect(baseClient.create).toHaveBeenCalledWith(type, attributes, {}); }); - const type = 'foo'; + test('does not assign a space-unaware object to a space', async () => { + const currentSpace = { + id: 'default', + urlContext: '' + }; - const result = await client.bulkGet([{ - type, - id: 'object_1' - }, { - type, - id: 'object_2' - }]); + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'space'; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + + await client.create(type, attributes); - expect(result).toEqual({ - saved_objects: [{ - id: 'object_1', - spaceId: 'space_1', + expect(baseClient.create).toHaveBeenCalledWith(type, attributes, {}); + }); + }); + + describe('#bulk_create', () => { + test('allows for bulk creation when all types are space-aware', async () => { + const currentSpace = { + id: 'default', + urlContext: '' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + const objects = [{ type: 'foo', + attributes + }, { + type: 'bar', + attributes + }]; + + await client.bulkCreate(objects); + + expect(baseClient.bulkCreate).toHaveBeenCalledWith(objects, {}); + }); + + test('allows for bulk creation when all types are not space-aware', async () => { + const currentSpace = { + id: 'default', + urlContext: '' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + + const objects = [{ + type: 'space', + attributes + }, { + type: 'space', + attributes + }]; + + await client.bulkCreate(objects); + + expect(baseClient.bulkCreate).toHaveBeenCalledWith(objects, {}); + }); + + test('allows space-aware and non-space-aware objects to be created at the same time', async () => { + const currentSpace = { + id: 'default', + urlContext: '' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + + const objects = [{ + type: 'space', + attributes }, { - id: 'object_2', type: 'foo', - error: { - message: 'Not found', - statusCode: 404 - } - }] + attributes + }]; + + await client.bulkCreate(objects); + + expect(baseClient.bulkCreate).toHaveBeenCalledWith(objects, {}); }); }); -}); -describe('#create', () => { - test('automatically assigns the object to the current space via extraBodyProperties', async () => { - const currentSpace = { - id: 'space_1', - urlContext: 'space-1' - }; - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], + describe('#update', () => { + test('allows an object to be updated if it exists in the same space', async () => { + const currentSpace = { + id: 'default', + urlContext: '' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const id = 'object_0'; + const type = 'foo'; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + + await client.update(type, id, attributes); + + expect(baseClient.update).toHaveBeenCalledWith(type, id, attributes, {}); }); - const type = 'foo'; - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; + test('does not allow an object to be updated via a different space', async () => { + const currentSpace = { + id: 'default', + urlContext: '' + }; - await client.create(type, attributes); + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const id = 'object_2'; + const type = 'foo'; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; - expect(baseClient.create).toHaveBeenCalledWith(type, attributes, { - extraBodyProperties: { - spaceId: 'space_1' - } + await expect(client.update(type, id, attributes)).rejects.toThrowErrorMatchingSnapshot(); }); }); - test('does not assign a space-unaware object to a space', async () => { - const currentSpace = { - id: 'space_1', - urlContext: 'space-1' - }; - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], + describe('#delete', () => { + test('allows an object to be deleted if it exists in the same space', async () => { + const currentSpace = { + id: 'default', + urlContext: '' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const id = 'object_0'; + const type = 'foo'; + + await client.delete(type, id); + + expect(baseClient.delete).toHaveBeenCalledWith(type, id); }); - const type = 'space'; - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; + test('does not allow an object to be deleted via a different space', async () => { + const currentSpace = { + id: 'default', + urlContext: '' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); - await client.create(type, attributes); + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); - expect(baseClient.create).toHaveBeenCalledWith(type, attributes, {}); + const id = 'object_2'; + const type = 'foo'; + + await expect(client.delete(type, id)).rejects.toThrowErrorMatchingSnapshot(); + }); }); }); -describe('#bulk_create', () => { - test('allows for bulk creation when all types are space-aware', async () => { - const currentSpace = { - id: 'space_1', - urlContext: 'space-1' - }; - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], +describe('within a space', () => { + describe('#get', () => { + test(`returns the object when it belongs to the current space`, async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + const id = 'object_1'; + + const result = await client.get(type, id); + + expect(result).toBe(SAVED_OBJECTS['space_1:' + id]); }); + test(`returns error when the object belongs to a different space`, async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; - const objects = [{ - type: 'foo', - attributes - }, { - type: 'bar', - attributes - }]; + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); - await client.bulkCreate(objects); + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); - const expectedCalledWithObjects = objects.map(o => ({ - ...o, - extraBodyProperties: { - spaceId: 'space_1' - } - })); + const type = 'foo'; + const id = 'object_2'; - expect(baseClient.bulkCreate).toHaveBeenCalledWith(expectedCalledWithObjects, {}); + await expect(client.get(type, id)).rejects.toThrowErrorMatchingSnapshot(); + }); }); - test('allows for bulk creation when all types are not space-aware', async () => { - const currentSpace = { - id: 'space_1', - urlContext: 'space-1' - }; - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); + describe('#bulk_get', () => { + test(`only returns objects belonging to the current space`, async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); - const objects = [{ - type: 'space', - attributes - }, { - type: 'space', - attributes - }]; + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); - await client.bulkCreate(objects); + const type = 'foo'; - expect(baseClient.bulkCreate).toHaveBeenCalledWith(objects, {}); + const result = await client.bulkGet([{ + type, + id: 'object_1' + }, { + type, + id: 'object_2' + }]); + + expect(result).toEqual({ + saved_objects: [{ + id: 'object_1', + spaceId: 'space_1', + type: 'foo', + }, { + id: 'object_2', + type: 'foo', + error: { + message: 'Not found', + statusCode: 404 + } + }] + }); + }); }); - test('allows space-aware and non-space-aware objects to be created at the same time', async () => { - const currentSpace = { - id: 'space_1', - urlContext: 'space-1' - }; - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); + describe('#create', () => { + test('automatically assigns the object to the current space via extraBodyProperties', async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; - const objects = [{ - type: 'space', - attributes - }, { - type: 'foo', - attributes - }]; + await client.create(type, attributes); - await client.bulkCreate(objects); + expect(baseClient.create).toHaveBeenCalledWith(type, attributes, { + id: 'space_1:mock-id', + extraBodyProperties: { + spaceId: 'space_1' + } + }); + }); - const expectedCalledWithObjects = [...objects]; - expectedCalledWithObjects[1] = { - ...expectedCalledWithObjects[1], - extraBodyProperties: { - spaceId: 'space_1' - } - }; + test('does not assign a space-unaware object to a space', async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; - expect(baseClient.bulkCreate).toHaveBeenCalledWith(expectedCalledWithObjects, {}); + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'space'; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + + await client.create(type, attributes); + + expect(baseClient.create).toHaveBeenCalledWith(type, attributes, {}); + }); }); -}); -describe('#update', () => { - test('allows an object to be updated if it exists in the same space', async () => { - const currentSpace = { - id: 'space_1', - urlContext: 'space-1' - }; - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], + describe('#bulk_create', () => { + test('allows for bulk creation when all types are space-aware', async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + const objects = [{ + type: 'foo', + attributes + }, { + type: 'bar', + attributes + }]; + + await client.bulkCreate(objects); + + const expectedCalledWithObjects = objects.map(o => ({ + ...o, + id: `space_1:mock-id`, + extraBodyProperties: { + spaceId: 'space_1' + } + })); + + expect(baseClient.bulkCreate).toHaveBeenCalledWith(expectedCalledWithObjects, {}); }); - const id = 'object_1'; - const type = 'foo'; - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; + test('allows for bulk creation when all types are not space-aware', async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; - await client.update(type, id, attributes); + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); - expect(baseClient.update).toHaveBeenCalledWith(type, id, attributes, { extraBodyProperties: { spaceId: 'space_1' } }); - }); + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); - test('does not allow an object to be updated via a different space', async () => { - const currentSpace = { - id: 'space_1', - urlContext: 'space-1' - }; - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + + const objects = [{ + type: 'space', + attributes + }, { + type: 'space', + attributes + }]; + + await client.bulkCreate(objects); + + expect(baseClient.bulkCreate).toHaveBeenCalledWith(objects, {}); }); - const id = 'object_2'; - const type = 'foo'; - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; + test('allows space-aware and non-space-aware objects to be created at the same time', async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); - await expect(client.update(type, id, attributes)).rejects.toThrowErrorMatchingSnapshot(); + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + + const objects = [{ + type: 'space', + attributes + }, { + type: 'foo', + attributes + }]; + + await client.bulkCreate(objects); + + const expectedCalledWithObjects = [...objects]; + expectedCalledWithObjects[1] = { + ...expectedCalledWithObjects[1], + id: `space_1:mock-id`, + extraBodyProperties: { + spaceId: 'space_1' + } + }; + + expect(baseClient.bulkCreate).toHaveBeenCalledWith(expectedCalledWithObjects, {}); + }); }); -}); -describe('#delete', () => { - test('allows an object to be deleted if it exists in the same space', async () => { - const currentSpace = { - id: 'space_1', - urlContext: 'space-1' - }; - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], + describe('#update', () => { + test('allows an object to be updated if it exists in the same space', async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const id = 'object_1'; + const type = 'foo'; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + + await client.update(type, id, attributes); + + expect(baseClient.update).toHaveBeenCalledWith(type, `space_1:${id}`, attributes, { extraBodyProperties: { spaceId: 'space_1' } }); }); - const id = 'object_1'; - const type = 'foo'; + test('does not allow an object to be updated via a different space', async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; - await client.delete(type, id); + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const id = 'object_2'; + const type = 'foo'; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; - expect(baseClient.delete).toHaveBeenCalledWith(type, id); + await expect(client.update(type, id, attributes)).rejects.toThrowErrorMatchingSnapshot(); + }); }); - test('does not allow an object to be deleted via a different space', async () => { - const currentSpace = { - id: 'space_1', - urlContext: 'space-1' - }; - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], + describe('#delete', () => { + test('allows an object to be deleted if it exists in the same space', async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const id = 'object_1'; + const type = 'foo'; + + await client.delete(type, id); + + expect(baseClient.delete).toHaveBeenCalledWith(type, `space_1:${id}`); }); - const id = 'object_2'; - const type = 'foo'; + test('does not allow an object to be deleted via a different space', async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const id = 'object_2'; + const type = 'foo'; - await expect(client.delete(type, id)).rejects.toThrowErrorMatchingSnapshot(); + await expect(client.delete(type, id)).rejects.toThrowErrorMatchingSnapshot(); + }); }); }); + diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/create.js b/x-pack/test/spaces_api_integration/apis/saved_objects/create.js index 2eee219f739f6..790a99e78a5d3 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/create.js +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/create.js @@ -32,8 +32,10 @@ export default function ({ getService }) { }); // query ES directory to assert on space id + const idPrefix = spaceId === DEFAULT_SPACE_ID ? '' : `${spaceId}:`; + const { _source } = await es.get({ - id: `visualization:${resp.body.id}`, + id: `visualization:${idPrefix}${resp.body.id}`, type: 'doc', index: '.kibana' }); diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/lib/space_test_utils.js b/x-pack/test/spaces_api_integration/apis/saved_objects/lib/space_test_utils.js index 186b75ccacf88..f82dbba5ee885 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/lib/space_test_utils.js +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/lib/space_test_utils.js @@ -8,7 +8,6 @@ export function getUrlPrefix(urlContext) { return urlContext ? `/s/${urlContext}` : ``; } -// Spaces do not actually prefix the ID, but this simplifies testing positive and negative flows. export function getIdPrefix(spaceId) { return spaceId === 'default' ? '' : `${spaceId}-`; } diff --git a/x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/data.json.gz b/x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/data.json.gz index 33586e3642245682fc862650efa6eabe1ca9bc8e..f9d440c701b4d470efd7815c4fdd17b22b8d8c04 100644 GIT binary patch delta 2062 zcmYjMc{~&TAI@!+2%TRcUm7!Gs}^(45nGg42fjtDn3tqvbTCI)C^HM2YgsCDWim%i z%#}H6Bpa1Nt`W*vu`hnTe!t&8&*ypGpZDu|o_D4SLq$#_QC^Js*O!w>^|N)jGHj^ zSq)aX9zUL84SgW+&@m$?ysi>Y!}}BR=k_sBO9cd&Q1P+t5yv0+@&cTe(~5>-@B5d% zEvxghiZun=DYeGc>|j>D3n0yo4gPHR`1(Tz!LS2u6KpS%p6etxBpf>QH`(_WGjUX3?(u_=VW*o11}eCa-=RS$ZOWsuo|LO zPLNxO@@?m?IG1zJ`n;(4QkNpJYfHbmeivZJUkwFYJlPp7`B$d2{C9nmaJef*Y-xD>bsxqeEd1t~N@vXdl zXJ=|&`LRU|QCu7=$jWWAZPtmi3ksx$vLH=2Jso5_4DmVA!QTjpMUzw$wxl*ndP~@) z0;S9(MXIS6r4AD0e(X4_80jXkpWDWh^6Jil)urOB)HkI+OSaxei4@*Zy+o=S;kL^4N@b^q3YL;PB6*qAdA+uKAeuRNKh>yfqt*GTjYnjMT2R@>#GSM-@ z^8DIb{|MK*a=pag{JK5H;pAcHjic7*g}BuA{D(souyg`*HJg%;swdC5rc8AG=q^7A zJktKysJ8UULXmb+@9<1qYqa*Hq4z3ct|3U3RItcZE9=a7L=vG7h*x>V~>!BGv zCf-*%$lqphr3mV4{I(4B&o1L0w78qOn3hluFzseYO^XI7nEPYNK&*cuGxW=pm;tZtRrU5FH{6z<164n(f3Nr269>?ggE1zz$4t3vWG8 zs$LyxwYc;4uu6%lq2)y~^wnkfq}uEDz;G7^Et%>!Jfg4Rno9yWG2KGr`IfA@o9+W4 zYTfLSxeM@LOv5Sve{woXFV5Zu1t-r(9q(c45&EvCji!%{7*exKj^8+&cJZG_+Bf{0 zqYJOqHv1lT%WLo5AN;H2i;WmPjVbR-YI@}k)ZhKUb0T)$KG@z=EgY-UdEuB{^p&2< z6E{s$zMscSPGx4dSBJLg#UYFYUzatQfqEUa8CQN^OteuLG*CfWd&JfNc9@*t>EXj( z7B~@V0=!I5D94@20>Tet)g8GrQuq9J!tL&IR=eOVzfbx!u&IrUIlXq(p|P@Awj5?_ z0Is|S<+n>0zvs{t4vjjE290xxvh8jXFpKy$_w5%yV{LrioQD0_a@d=s&cDU)_GI1iEgt)@ zwG06hv$Ql@9I2Fm(HkNiAa#LPQC<85OW>D5mxQR03t;$jQ*>pxr-59h3cBwJk?nK4 z)9ncY4(y?G5&ma1f<26?WeA2IZLoJ16X+|ym$yeuMP!{450id7AFf=a9Q zOk{g;j?orB{K#PsWafyLKNVP-O|L8tof_C<(&?hkv1rJ=|BC-?GVC;d&P;r7^abO= zKAG&zprJk)gTNkAtJ(g=OM*AuxI#vS-~NuRyd_~t!4YEjWU0Y2(TPyGL{27#n4ujV zfASF=p6E;*Q#>O~^&A74Ve+)+-&p82$!(bkXDmPi@1UF&BzrXBbon054ThHk{9(=t zT>HcFm>7Y)G80MP+jvZf+>p{T8xl{oUYAz?g-F)rlL~`71h$03L<7=Yf2F6g5^3m- zjf{Z2y^Z*=J2@*veIi<6?I`Z)opU%C$yBA)P7BME^h7=7R`jyF2QiIu<1kKeakp zZ!5DILl5hVf0-v8dqyk`L4J8C-Sj42M`MlkcTI$uV@c9Gouegn zblF^Gv@>10X(Y}XknlkFKeIN~^_k57fO0ueS+%t80NObq%5lwv0NDHtlefN6hgDfK zY504Na*RS3NXF2~Eazx%IdGnIQ>U(ZQn$&5NdV-6Myzx6;8(Ti#35Z|l(K5S7eMDv zTC%K;ZtX2V!s*l9p_)kkFs(j~?&l`+=X;~k1|XDGoest8Bl!u#J9Uw3XK$R+75`g- MCDC$Vg|}(bzh8dutpET3 delta 2054 zcmYjMdpy&98!m@5i5Mdip)rr+q?}EW*&Ifrt(uiXW+qfZ(vLhZ$vKB?4vmd$A)B0% z5K<0HEJjVGlThUJErJa~oPN+H_w%@K7Zt*r2Nj9?cLuJULwr6#U_-ysh(h;>1 z2~x~DD{2C$%q!qzV)25L3)0=9rw6CutyzJHy<2Wo*B@5Rjve{Cpu6qkciUv))oG3~ z0kTUk&5vgS>NZ5%XW+PrGFzHu489|LNWhy`1K+I*8gZQ+hs9&Zph72vYiZl`hzF6l zc;#4Y4J>)>MeA;}n42Grqn^Y)Ln!Fy!R)J+;ZELL(&Py24 zOqrQtSx@a63OXOz?q?^rq-YGNuADKGe9Di0x0}Pud+xiEIu+gCHZgv`^I}Ba%YcqC z+J52tLGN0V%&#V=FUO)wLjsk_wye6DQSIEz;e>>V#bES#4I~-zPdj@QnP;fzLZFAp zg#lsfasH_x%IO~eETxr!+#0HXt~;;{^7~wAS8c~e&V6FiR6R5chK)udnr0`x_uE&k zM{;HMUF`e&ug)Gfs$Xx2N@AfX6+f(P%3jV(I(qKWobe)v+RvngM=1RyLIlXErZnHt zp#q!Uv&6+xLTXRVa;;vGoBv*pO+V{|Dd1qqn_H6;uI;ZGdKYq|B|LOprI z9lbDg_OgZFxi_X)CWv1O$zJ)KAIDYv#2Itw=IURhZ=^&8z0>ftf zwfi59JS&KSJ(Ot^1zk`IyXkW`R#)SNlO?Bz3tgqURKKhq1Gm#A4yIADR%}}xN~&RC_K-yWXjtc7 zY^^@TRpG>32r|&iL&y8m?BR6a_M?#DsE_FOY=2+Xv99pk2sie;c5zxGn4si!j22TE z&O2nHq6Iu!TVqLP!&?kFo_DK8eEl45W~XKJx)|kGno9{mUD&5wL2yoP)a1?A{j>8Q zfCCscmPR2d)SMOTAz8HaluIf(*)8UojDPQ1~!9F}zAdz`i zsJOL{(r;75pf#V(l}<|0F1X%*yZ;@@OzTp~f?B}Da^&+~p5{7#cR_SO<@9SQj)loO zS7Tq7GgWhhH$Q}Sg{$TSU3i6j_N&C#ceW;L@-dD=vPRX%5cMVMvrwZ=*DsyE!t=f} zdQkf%{qZ*P@sJBAjT!-Q!;?L5zVRf=^4FCeEkke4x~*OZNwtvRjr5%H&ch!S4jpL* zS423?7e$W^E=3Psuzq8?nE1JQ{p!O>OU*94h2y<{X3O4(9MH2Y%2Bm`4!mFfM)G&G zyK}9(LE0|4N*PYo{xBxB8f6h2ejlRGq}ow!Uzyg%o23H5u%i@UavW`}F|@2>Z2rYj zWr1Bz+|B;g9^e*Me0LU;baTNDW{tzlsN|%FX(F%b?kX4IY;`CXiQ8nB$UkgD+OQFH z$&RBL7;zC#b?PO)zjQ0-CeKV z$*!;*v2g;CcskFaSh7PJXRmO*(EYMf(*{i!iudYq*&v=U3 zPB!97i0-Z_SJR)wzwG$|y$F~?Ma?!gl72(%{{gEj7Y;xq7*LKuJY&FK9GnBi1de|Z ze81rV-W$pS$&^BKZ_Cj6Vknn~@-irwO*RevvH8;Z9aH<_wV1IUU>#GALN10ZQVKnI zNFL=HX%E-m>Mt2o%xt From dee335b5d1b1ef28f8504539f3cef20ca812bc88 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 29 Jun 2018 15:21:46 -0400 Subject: [PATCH 23/41] only allow filters to be passed to getQueryParams --- .../saved_objects/service/lib/repository.js | 10 +- .../service/lib/repository.test.js | 15 +- .../service/lib/search_dsl/query_params.js | 46 +--- .../lib/search_dsl/query_params.test.js | 228 ++++++++++++------ .../service/lib/search_dsl/search_dsl.js | 8 +- .../service/lib/search_dsl/search_dsl.test.js | 6 +- .../saved_objects_client/lib/query_params.js | 10 +- .../lib/query_params.test.js | 20 +- .../spaces_saved_objects_client.js | 6 +- 9 files changed, 211 insertions(+), 138 deletions(-) diff --git a/src/server/saved_objects/service/lib/repository.js b/src/server/saved_objects/service/lib/repository.js index 7e400cdcd5ecf..93cef292b79b8 100644 --- a/src/server/saved_objects/service/lib/repository.js +++ b/src/server/saved_objects/service/lib/repository.js @@ -211,7 +211,7 @@ export class SavedObjectsRepository { * @property {string} [options.search] * @property {Array} [options.searchFields] - see Elasticsearch Simple Query String * Query field argument for more information - * @property {object} [options.extraQueryParams] - ES Query parameters to merge/append into the generated query + * @property {object} [options.filters] - ES Query filters to append * @property {integer} [options.page=1] * @property {integer} [options.perPage=20] * @property {string} [options.sortField] @@ -229,7 +229,7 @@ export class SavedObjectsRepository { sortField, sortOrder, fields, - extraQueryParams, + filters, } = options; if (searchFields && !Array.isArray(searchFields)) { @@ -240,8 +240,8 @@ export class SavedObjectsRepository { throw new TypeError('options.searchFields must be an array'); } - if (extraQueryParams && typeof extraQueryParams !== 'object') { - throw new TypeError('options.extraQueryParams must be an object'); + if (filters && !Array.isArray(filters)) { + throw new TypeError('options.filters must be an array'); } const esOptions = { @@ -258,7 +258,7 @@ export class SavedObjectsRepository { type, sortField, sortOrder, - extraQueryParams + filters }) } }; diff --git a/src/server/saved_objects/service/lib/repository.test.js b/src/server/saved_objects/service/lib/repository.test.js index b5938e26a83c0..0301968674a49 100644 --- a/src/server/saved_objects/service/lib/repository.test.js +++ b/src/server/saved_objects/service/lib/repository.test.js @@ -439,14 +439,25 @@ describe('SavedObjectsRepository', () => { } }); - it('passes mappings, search, searchFields, type, sortField, extraQueryParams, and sortOrder to getSearchDsl', async () => { + it('requires filters to be an array if defined', async () => { + try { + await savedObjectsRepository.find({ filters: 'string' }); + throw new Error('expected find() to reject'); + } catch (error) { + sinon.assert.notCalled(callAdminCluster); + sinon.assert.notCalled(onBeforeWrite); + expect(error.message).toMatch('must be an array'); + } + }); + + it('passes mappings, search, searchFields, type, sortField, filters, and sortOrder to getSearchDsl', async () => { const relevantOpts = { search: 'foo*', searchFields: ['foo'], type: 'bar', sortField: 'name', sortOrder: 'desc', - extraQueryParams: { bool: {} }, + filters: [{ bool: {} }], }; await savedObjectsRepository.find(relevantOpts); diff --git a/src/server/saved_objects/service/lib/search_dsl/query_params.js b/src/server/saved_objects/service/lib/search_dsl/query_params.js index f5986de2bb8da..c5ca55f985bdd 100644 --- a/src/server/saved_objects/service/lib/search_dsl/query_params.js +++ b/src/server/saved_objects/service/lib/search_dsl/query_params.js @@ -17,7 +17,6 @@ * under the License. */ -import { defaultsDeep } from 'lodash'; import { getRootPropertiesObjects } from '../../../../mappings'; /** @@ -66,23 +65,21 @@ function getFieldsForTypes(searchFields, types) { * @param {(string|Array)} type * @param {String} search * @param {Array} searchFields - * @param {Object} extraQueryParams query parameters to merge into the result + * @param {Array} filters additional query filters * @return {Object} */ -export function getQueryParams(mappings, type, search, searchFields, extraQueryParams = {}) { - if (!type && !search) { - return {}; - } +export function getQueryParams(mappings, type, search, searchFields, filters = []) { - const bool = {}; + const bool = { + filter: [...filters], + }; if (type) { - bool.filter = [{ [Array.isArray(type) ? 'terms' : 'term']: { type } }]; + bool.filter.push({ [Array.isArray(type) ? 'terms' : 'term']: { type } }); } if (search) { bool.must = [ - ...bool.must || [], { simple_query_string: { query: search, @@ -95,35 +92,12 @@ export function getQueryParams(mappings, type, search, searchFields, extraQueryP ]; } - // a list of fields to manually merge together - const fieldsToMerge = ['filter', 'must']; - - const extraParams = { - ...extraQueryParams.bool - }; - - // Remove the manual merge fields from the collection of properties we will automatically combine. - fieldsToMerge.forEach(field => delete extraParams[field]); - - let query = { - bool: defaultsDeep(bool, extraParams) - }; - - if (extraQueryParams.bool) { - - const extraBoolParams = extraQueryParams.bool; - - query = fieldsToMerge.reduce((queryAcc, field) => { - const prop = queryAcc.bool[field]; - const extraProp = extraBoolParams[field]; - if (Array.isArray(prop) && Array.isArray(extraProp)) { - queryAcc.bool[field] = [...prop, ...extraProp]; - } - return queryAcc; - }, query); + // Don't construct a query if there is nothing to search on. + if (bool.filter.length === 0 && !search) { + return {}; } - return { query }; + return { query: { bool } }; } diff --git a/src/server/saved_objects/service/lib/search_dsl/query_params.test.js b/src/server/saved_objects/service/lib/search_dsl/query_params.test.js index 2a1ac85181605..e3e261db1e77e 100644 --- a/src/server/saved_objects/service/lib/search_dsl/query_params.test.js +++ b/src/server/saved_objects/service/lib/search_dsl/query_params.test.js @@ -64,7 +64,7 @@ describe('searchDsl/queryParams', () => { }); describe('{type}', () => { - it('adds a term filter when a string', () => { + it('includes just a terms filter', () => { expect(getQueryParams(MAPPINGS, 'saved')) .toEqual({ query: { @@ -78,15 +78,18 @@ describe('searchDsl/queryParams', () => { } }); }); + }); - it('adds a terms filter when an array', () => { - expect(getQueryParams(MAPPINGS, ['saved', 'vis'])) + describe('{type,filters}', () => { + it('includes filters and a term filter for type', () => { + expect(getQueryParams(MAPPINGS, 'saved', null, null, [{ terms: { foo: ['bar', 'baz'] } }])) .toEqual({ query: { bool: { filter: [ + { terms: { foo: ['bar', 'baz'] } }, { - terms: { type: ['saved', 'vis'] } + term: { type: 'saved' } } ] } @@ -101,6 +104,30 @@ describe('searchDsl/queryParams', () => { .toEqual({ query: { bool: { + filter: [], + must: [ + { + simple_query_string: { + query: 'us*', + all_fields: true + } + } + ] + } + } + }); + }); + }); + + describe('{search,filters}', () => { + it('includes filters and a sqs query', () => { + expect(getQueryParams(MAPPINGS, null, 'us*', null, [{ terms: { foo: ['bar', 'baz'] } }])) + .toEqual({ + query: { + bool: { + filter: [ + { terms: { foo: ['bar', 'baz'] } } + ], must: [ { simple_query_string: { @@ -136,13 +163,17 @@ describe('searchDsl/queryParams', () => { } }); }); - it('includes bool with sqs query and terms filter for type', () => { - expect(getQueryParams(MAPPINGS, ['saved', 'vis'], 'y*')) + }); + + describe('{type,search,filters}', () => { + it('includes bool with sqs query, filters and term filter for type', () => { + expect(getQueryParams(MAPPINGS, 'saved', 'y*', null, [{ terms: { foo: ['bar', 'baz'] } }])) .toEqual({ query: { bool: { filter: [ - { terms: { type: ['saved', 'vis'] } } + { terms: { foo: ['bar', 'baz'] } }, + { term: { type: 'saved' } } ], must: [ { @@ -164,6 +195,7 @@ describe('searchDsl/queryParams', () => { .toEqual({ query: { bool: { + filter: [], must: [ { simple_query_string: { @@ -184,6 +216,7 @@ describe('searchDsl/queryParams', () => { .toEqual({ query: { bool: { + filter: [], must: [ { simple_query_string: { @@ -204,6 +237,7 @@ describe('searchDsl/queryParams', () => { .toEqual({ query: { bool: { + filter: [], must: [ { simple_query_string: { @@ -223,20 +257,21 @@ describe('searchDsl/queryParams', () => { }); }); - describe('{type,search,searchFields}', () => { - it('includes bool, with term filter and sqs with field list', () => { - expect(getQueryParams(MAPPINGS, 'saved', 'y*', ['title'])) + describe('{search,searchFields,filters}', () => { + it('specifies filters and includes all types for field', () => { + expect(getQueryParams(MAPPINGS, null, 'y*', ['title'], [{ terms: { foo: ['bar', 'baz'] } }])) .toEqual({ query: { bool: { filter: [ - { term: { type: 'saved' } } + { terms: { foo: ['bar', 'baz'] } }, ], must: [ { simple_query_string: { query: 'y*', fields: [ + 'pending.title', 'saved.title' ] } @@ -246,21 +281,71 @@ describe('searchDsl/queryParams', () => { } }); }); - it('includes bool, with terms filter and sqs with field list', () => { - expect(getQueryParams(MAPPINGS, ['saved', 'vis'], 'y*', ['title'])) + it('specifies filters and supports field boosting', () => { + expect(getQueryParams(MAPPINGS, null, 'y*', ['title^3'], [{ terms: { foo: ['bar', 'baz'] } }])) .toEqual({ query: { bool: { filter: [ - { terms: { type: ['saved', 'vis'] } } + { terms: { foo: ['bar', 'baz'] } }, ], must: [ { simple_query_string: { query: 'y*', fields: [ + 'pending.title^3', + 'saved.title^3' + ] + } + } + ] + } + } + }); + }); + it('specifies filters and supports field and multi-field', () => { + expect(getQueryParams(MAPPINGS, null, 'y*', ['title', 'title.raw'], [{ terms: { foo: ['bar', 'baz'] } }])) + .toEqual({ + query: { + bool: { + filter: [ + { terms: { foo: ['bar', 'baz'] } }, + ], + must: [ + { + simple_query_string: { + query: 'y*', + fields: [ + 'pending.title', 'saved.title', - 'vis.title' + 'pending.title.raw', + 'saved.title.raw', + ] + } + } + ] + } + } + }); + }); + }); + + describe('{type,search,searchFields}', () => { + it('includes bool, and sqs with field list', () => { + expect(getQueryParams(MAPPINGS, 'saved', 'y*', ['title'])) + .toEqual({ + query: { + bool: { + filter: [ + { term: { type: 'saved' } } + ], + must: [ + { + simple_query_string: { + query: 'y*', + fields: [ + 'saved.title' ] } } @@ -316,75 +401,76 @@ describe('searchDsl/queryParams', () => { }); }); - describe('{extraQueryParams}', () => { - it('merges the extraQueryParams into the generated query params', () => { - const baseQueryParams = getQueryParams(MAPPINGS, 'saved', 'search'); - expect(baseQueryParams) + describe('{type,search,searchFields,filters}', () => { + it('includes specified filters, type filter and sqs with field list', () => { + expect(getQueryParams(MAPPINGS, 'saved', 'y*', ['title'], [{ terms: { foo: ['bar', 'baz'] } }])) .toEqual({ query: { bool: { filter: [ + { terms: { foo: ['bar', 'baz'] } }, + { term: { type: 'saved' } } + ], + must: [ { - term: { type: 'saved' } + simple_query_string: { + query: 'y*', + fields: [ + 'saved.title' + ] + } } + ] + } + } + }); + }); + it('supports fields pointing to multi-fields', () => { + expect(getQueryParams(MAPPINGS, 'saved', 'y*', ['title.raw'], [{ terms: { foo: ['bar', 'baz'] } }])) + .toEqual({ + query: { + bool: { + filter: [ + { terms: { foo: ['bar', 'baz'] } }, + { term: { type: 'saved' } } ], - must: [{ - simple_query_string: { - all_fields: true, - query: 'search' + must: [ + { + simple_query_string: { + query: 'y*', + fields: [ + 'saved.title.raw' + ] + } } - }] + ] } } }); - - const result = getQueryParams(MAPPINGS, 'saved', 'search', null, { - bool: { - filter: [{ - term: { type: 'foo' } - }], - must: [{ - term: { - someField: 'bar' - } - }], - must_not: [{ - term: { - field1: 'value' + }); + it('supports multiple search fields', () => { + expect(getQueryParams(MAPPINGS, 'saved', 'y*', ['title', 'title.raw'], [{ terms: { foo: ['bar', 'baz'] } }])) + .toEqual({ + query: { + bool: { + filter: [ + { terms: { foo: ['bar', 'baz'] } }, + { term: { type: 'saved' } } + ], + must: [ + { + simple_query_string: { + query: 'y*', + fields: [ + 'saved.title', + 'saved.title.raw' + ] + } + } + ] } - }] - } - }); - - expect(result).toEqual({ - query: { - bool: { - filter: [ - { - term: { type: 'saved' } - }, - { - term: { type: 'foo' } - } - ], - must: [{ - simple_query_string: { - all_fields: true, - query: 'search' - } - }, { - term: { - someField: 'bar' - } - }], - must_not: [{ - term: { - field1: 'value' - } - }] } - } - }); + }); }); }); }); diff --git a/src/server/saved_objects/service/lib/search_dsl/search_dsl.js b/src/server/saved_objects/service/lib/search_dsl/search_dsl.js index 270473ada36bd..94f938c921ed0 100644 --- a/src/server/saved_objects/service/lib/search_dsl/search_dsl.js +++ b/src/server/saved_objects/service/lib/search_dsl/search_dsl.js @@ -29,7 +29,7 @@ export function getSearchDsl(mappings, options = {}) { searchFields, sortField, sortOrder, - extraQueryParams, + filters, } = options; if (!type && sortField) { @@ -40,12 +40,12 @@ export function getSearchDsl(mappings, options = {}) { throw Boom.notAcceptable('sortOrder requires a sortField'); } - if (extraQueryParams && typeof extraQueryParams !== 'object') { - throw Boom.notAcceptable('extraQueryParams must be an object'); + if (filters && !Array.isArray(filters)) { + throw Boom.notAcceptable('filters must be an array'); } return { - ...getQueryParams(mappings, type, search, searchFields, extraQueryParams), + ...getQueryParams(mappings, type, search, searchFields, filters), ...getSortingParams(mappings, type, sortField, sortOrder), }; } diff --git a/src/server/saved_objects/service/lib/search_dsl/search_dsl.test.js b/src/server/saved_objects/service/lib/search_dsl/search_dsl.test.js index b21b127d51b10..6ed40a7929dcf 100644 --- a/src/server/saved_objects/service/lib/search_dsl/search_dsl.test.js +++ b/src/server/saved_objects/service/lib/search_dsl/search_dsl.test.js @@ -46,14 +46,14 @@ describe('getSearchDsl', () => { }); describe('passes control', () => { - it('passes (mappings, type, search, searchFields, extraQueryParams) to getQueryParams', () => { + it('passes (mappings, type, search, searchFields, filters) to getQueryParams', () => { const spy = sandbox.spy(queryParamsNS, 'getQueryParams'); const mappings = { type: { properties: {} } }; const opts = { type: 'foo', search: 'bar', searchFields: ['baz'], - extraQueryParams: { bool: {} }, + filters: [{ bool: {} }], }; getSearchDsl(mappings, opts); @@ -64,7 +64,7 @@ describe('getSearchDsl', () => { opts.type, opts.search, opts.searchFields, - opts.extraQueryParams, + opts.filters, ); }); diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_params.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_params.js index c117423985536..e6d3ec387d8a6 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_params.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_params.js @@ -46,17 +46,19 @@ function getClauseForType(spaceId, type) { }; } -export function getSpacesQueryParams(spaceId, types = []) { +export function getSpacesQueryFilters(spaceId, types = []) { + const filters = []; + const typeClauses = types.map((type) => getClauseForType(spaceId, type)); if (typeClauses.length > 0) { - return { + filters.push({ bool: { should: typeClauses, minimum_should_match: 1 } - }; + }); } - return {}; + return filters; } diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_params.test.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_params.test.js index 520ebebc33735..0463a2aaac533 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_params.test.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_params.test.js @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getSpacesQueryParams } from './query_params'; +import { getSpacesQueryFilters } from './query_params'; test('returns no parameters when no types are provided', () => { - expect(getSpacesQueryParams('space_1', [])).toEqual({}); + expect(getSpacesQueryFilters('space_1', [])).toEqual([]); }); test('creates a query that filters on type, but not on space, for types that are not space-aware', () => { @@ -23,12 +23,12 @@ test('creates a query that filters on type, but not on space, for types that are }] } }; - expect(getSpacesQueryParams(spaceId, [type])).toEqual({ + expect(getSpacesQueryFilters(spaceId, [type])).toEqual([{ bool: { should: [expectedTypeClause], minimum_should_match: 1 } - }); + }]); }); test('creates a query that restricts a space-aware type to the provided space (space_1)', () => { @@ -49,12 +49,12 @@ test('creates a query that restricts a space-aware type to the provided space (s } }; - expect(getSpacesQueryParams(spaceId, [type])).toEqual({ + expect(getSpacesQueryFilters(spaceId, [type])).toEqual([{ bool: { should: [expectedTypeClause], minimum_should_match: 1 } - }); + }]); }); test('creates a query that restricts a space-aware type to the provided space (default)', () => { @@ -78,12 +78,12 @@ test('creates a query that restricts a space-aware type to the provided space (d } }; - expect(getSpacesQueryParams(spaceId, [type])).toEqual({ + expect(getSpacesQueryFilters(spaceId, [type])).toEqual([{ bool: { should: [expectedTypeClause], minimum_should_match: 1 } - }); + }]); }); test('creates a query supporting a find operation on multiple types', () => { @@ -126,10 +126,10 @@ test('creates a query supporting a find operation on multiple types', () => { } }]; - expect(getSpacesQueryParams(spaceId, types)).toEqual({ + expect(getSpacesQueryFilters(spaceId, types)).toEqual([{ bool: { should: expectedTypeClauses, minimum_should_match: 1 } - }); + }]); }); diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js index 96c00c710debc..7b4ead075db12 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js @@ -6,7 +6,7 @@ import { DEFAULT_SPACE_ID } from '../../../common/constants'; import { isTypeSpaceAware } from './lib/is_type_space_aware'; -import { getSpacesQueryParams } from './lib/query_params'; +import { getSpacesQueryFilters } from './lib/query_params'; export class SpacesSavedObjectsClient { constructor(options) { @@ -77,7 +77,7 @@ export class SpacesSavedObjectsClient { const spaceId = await this._getSpaceId(); - spaceOptions.extraQueryParams = getSpacesQueryParams(spaceId, types); + spaceOptions.filters = getSpacesQueryFilters(spaceId, types); return await this._client.find({ ...options, ...spaceOptions }); } @@ -91,7 +91,7 @@ export class SpacesSavedObjectsClient { }); result.saved_objects = result.saved_objects.map(savedObject => { - const { id, type, spaceId } = savedObject; + const { id, type, spaceId = DEFAULT_SPACE_ID } = savedObject; if (isTypeSpaceAware(type)) { if (spaceId !== thisSpaceId) { From 452de107a41dc896059a133e030a9e6886f9d3a4 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 29 Jun 2018 15:31:26 -0400 Subject: [PATCH 24/41] don't add space id when updating within the default space --- .../spaces_saved_objects_client.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js index 7b4ead075db12..7cddc93a769f0 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js @@ -134,10 +134,13 @@ export class SpacesSavedObjectsClient { if (isTypeSpaceAware(type)) { await this.get(type, id); - options.extraBodyProperties = { - ...options.extraBodyProperties, - spaceId: await this._getSpaceId() - }; + const spaceId = await this._getSpaceId(); + if (spaceId !== DEFAULT_SPACE_ID) { + options.extraBodyProperties = { + ...options.extraBodyProperties, + spaceId + }; + } } return await this._client.update(type, id, attributes, options); From 6bf3515209ae318c7b36082d59ad9b7060f9d1a6 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 29 Jun 2018 15:37:43 -0400 Subject: [PATCH 25/41] renaming files --- .../lib/{query_params.js => query_filters.js} | 0 .../lib/{query_params.test.js => query_filters.test.js} | 2 +- .../lib/saved_objects_client/spaces_saved_objects_client.js | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename x-pack/plugins/spaces/server/lib/saved_objects_client/lib/{query_params.js => query_filters.js} (100%) rename x-pack/plugins/spaces/server/lib/saved_objects_client/lib/{query_params.test.js => query_filters.test.js} (97%) diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_params.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_filters.js similarity index 100% rename from x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_params.js rename to x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_filters.js diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_params.test.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_filters.test.js similarity index 97% rename from x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_params.test.js rename to x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_filters.test.js index 0463a2aaac533..a332065e76273 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_params.test.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/query_filters.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getSpacesQueryFilters } from './query_params'; +import { getSpacesQueryFilters } from './query_filters'; test('returns no parameters when no types are provided', () => { expect(getSpacesQueryFilters('space_1', [])).toEqual([]); diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js index 7cddc93a769f0..96b0baef9360d 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js @@ -6,7 +6,7 @@ import { DEFAULT_SPACE_ID } from '../../../common/constants'; import { isTypeSpaceAware } from './lib/is_type_space_aware'; -import { getSpacesQueryFilters } from './lib/query_params'; +import { getSpacesQueryFilters } from './lib/query_filters'; export class SpacesSavedObjectsClient { constructor(options) { From 8cb871a0696eb27609d2a50368d970a2906871f2 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 5 Jul 2018 09:39:19 -0400 Subject: [PATCH 26/41] additional SOC and repository tests --- .../saved_objects/service/lib/repository.js | 2 +- .../service/lib/repository.test.js | 215 +++++++++++++++++- .../service/saved_objects_client.js | 9 +- .../service/saved_objects_client.test.js | 10 +- .../secure_saved_objects_client.test.js | 10 +- 5 files changed, 235 insertions(+), 11 deletions(-) diff --git a/src/server/saved_objects/service/lib/repository.js b/src/server/saved_objects/service/lib/repository.js index 93cef292b79b8..bca1a748fdae9 100644 --- a/src/server/saved_objects/service/lib/repository.js +++ b/src/server/saved_objects/service/lib/repository.js @@ -406,7 +406,7 @@ export class SavedObjectsRepository { * @param {string} id * @param {object} [options={}] * @property {integer} options.version - ensures version matches that of persisted object - * @param {array} [options.extraBodyProperties = []] - an array of extra properties to write into the underlying document + * @param {array} [options.extraBodyProperties = {}] - an object of extra properties to write into the underlying document * @returns {promise} */ async update(type, id, attributes, options = {}) { diff --git a/src/server/saved_objects/service/lib/repository.test.js b/src/server/saved_objects/service/lib/repository.test.js index 0301968674a49..5f870ae12d859 100644 --- a/src/server/saved_objects/service/lib/repository.test.js +++ b/src/server/saved_objects/service/lib/repository.test.js @@ -208,7 +208,37 @@ describe('SavedObjectsRepository', () => { sinon.assert.calledOnce(callAdminCluster); sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({ - id: sinon.match(/index-pattern:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/), + body: { + [`index-pattern`]: { title: 'Logstash' }, + myExtraProp: 'myExtraValue', + myOtherExtraProp: true, + type: 'index-pattern', + updated_at: '2017-08-14T15:49:14.886Z' + } + })); + + sinon.assert.calledOnce(onBeforeWrite); + }); + + it('does not allow extraBodyProperties to overwrite existing properties', async () => { + await savedObjectsRepository.create('index-pattern', + { + title: 'Logstash' + }, + { + extraBodyProperties: { + myExtraProp: 'myExtraValue', + myOtherExtraProp: true, + updated_at: 'should_not_be_used', + 'index-pattern': { + title: 'should_not_be_used' + } + } + } + ); + + sinon.assert.calledOnce(callAdminCluster); + sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({ body: { [`index-pattern`]: { title: 'Logstash' }, myExtraProp: 'myExtraValue', @@ -376,6 +406,42 @@ describe('SavedObjectsRepository', () => { sinon.assert.calledOnce(onBeforeWrite); }); + + it('does not allow extraBodyProperties to overwrite existing properties', async () => { + + callAdminCluster.returns({ items: [] }); + + const extraBodyProperties = { + extraProp: 'extraVal', + updated_at: 'should_not_be_used', + }; + const configExtraBodyProperties = { + ...extraBodyProperties, + 'config': { newIgnoredProp: 'should_not_be_used' } + }; + const indexPatternExtraBodyProperties = { + ...extraBodyProperties, + 'index-pattern': { title: 'should_not_be_used', newIgnoredProp: 'should_not_be_used' } + }; + + await savedObjectsRepository.bulkCreate( + [ + { type: 'config', id: 'one', attributes: { title: 'Test One' }, extraBodyProperties: configExtraBodyProperties }, + { type: 'index-pattern', id: 'two', attributes: { title: 'Test Two' }, extraBodyProperties: indexPatternExtraBodyProperties } + ]); + + sinon.assert.calledOnce(callAdminCluster); + sinon.assert.calledWithExactly(callAdminCluster, 'bulk', sinon.match({ + body: [ + { create: { _type: 'doc', _id: 'config:one' } }, + { type: 'config', ...mockTimestampFields, config: { title: 'Test One' }, extraProp: 'extraVal' }, + { create: { _type: 'doc', _id: 'index-pattern:two' } }, + { type: 'index-pattern', ...mockTimestampFields, 'index-pattern': { title: 'Test Two' }, extraProp: 'extraVal' } + ] + })); + + sinon.assert.calledOnce(onBeforeWrite); + }); }); describe('#delete', () => { @@ -531,6 +597,7 @@ describe('SavedObjectsRepository', () => { _version: 2, _source: { type: 'index-pattern', + specialProperty: 'specialValue', ...mockTimestampFields, 'index-pattern': { title: 'Testing' @@ -563,6 +630,24 @@ describe('SavedObjectsRepository', () => { type: 'doc' })); }); + + it('includes the requested extraSourceProperties in the response for the requested object', async () => { + const response = await savedObjectsRepository.get('index-pattern', 'logstash-*', { + extraSourceProperties: ['specialProperty', 'undefinedProperty'] + }); + + expect(response).toEqual({ + id: 'logstash-*', + type: 'index-pattern', + updated_at: mockTimestamp, + version: 2, + specialProperty: 'specialValue', + undefinedProperty: undefined, + attributes: { + title: 'Testing' + } + }); + }); }); describe('#bulkGet', () => { @@ -633,6 +718,80 @@ describe('SavedObjectsRepository', () => { error: { statusCode: 404, message: 'Not found' } }); }); + + it('includes the requested extraSourceProperties in the response for each requested object', async () => { + callAdminCluster.returns(Promise.resolve({ + docs: [{ + _type: 'doc', + _id: 'config:good', + found: true, + _version: 2, + _source: { + ...mockTimestampFields, + type: 'config', + specialProperty: 'specialValue', + config: { title: 'Test' } + } + }, { + _type: 'doc', + _id: 'config:bad', + found: false + }, { + _id: 'index-pattern:logstash-*', + _type: 'doc', + found: true, + _version: 2, + _source: { + type: 'index-pattern', + specialProperty: 'anotherSpecialValue', + ...mockTimestampFields, + 'index-pattern': { + title: 'Testing' + } + } + }] + })); + + const { saved_objects: savedObjects } = await savedObjectsRepository.bulkGet( + [ + { id: 'good', type: 'config' }, + { id: 'bad', type: 'config' }, + { id: 'logstash-*', type: 'index-pattern' } + ], { + extraSourceProperties: ['specialProperty', 'undefinedProperty'] + } + ); + + expect(savedObjects).toHaveLength(3); + + expect(savedObjects[0]).toEqual({ + id: 'good', + type: 'config', + ...mockTimestampFields, + version: 2, + specialProperty: 'specialValue', + undefinedProperty: undefined, + attributes: { title: 'Test' } + }); + + expect(savedObjects[1]).toEqual({ + id: 'bad', + type: 'config', + error: { statusCode: 404, message: 'Not found' } + }); + + expect(savedObjects[2]).toEqual({ + id: 'logstash-*', + type: 'index-pattern', + ...mockTimestampFields, + version: 2, + specialProperty: 'anotherSpecialValue', + undefinedProperty: undefined, + attributes: { + title: 'Testing' + } + }); + }); }); describe('#update', () => { @@ -693,6 +852,60 @@ describe('SavedObjectsRepository', () => { sinon.assert.calledOnce(onBeforeWrite); }); + + it('updates the document including all provided extraBodyProperties', async () => { + await savedObjectsRepository.update( + 'index-pattern', + 'logstash-*', + { title: 'Testing' }, + { extraBodyProperties: { extraProp: 'extraVal' } } + ); + + sinon.assert.calledOnce(callAdminCluster); + sinon.assert.calledWithExactly(callAdminCluster, 'update', { + type: 'doc', + id: 'index-pattern:logstash-*', + version: undefined, + body: { + doc: { updated_at: mockTimestamp, extraProp: 'extraVal', 'index-pattern': { title: 'Testing' } } + }, + ignore: [404], + refresh: 'wait_for', + index: '.kibana-test' + }); + + sinon.assert.calledOnce(onBeforeWrite); + }); + + it('does not allow extraBodyProperties to overwrite existing properties', async () => { + await savedObjectsRepository.update( + 'index-pattern', + 'logstash-*', + { title: 'Testing' }, + { + extraBodyProperties: { + extraProp: 'extraVal', + updated_at: 'should_not_be_used', + 'index-pattern': { title: 'should_not_be_used', newIgnoredProp: 'should_not_be_used' } + } + } + ); + + sinon.assert.calledOnce(callAdminCluster); + sinon.assert.calledWithExactly(callAdminCluster, 'update', { + type: 'doc', + id: 'index-pattern:logstash-*', + version: undefined, + body: { + doc: { updated_at: mockTimestamp, extraProp: 'extraVal', 'index-pattern': { title: 'Testing' } } + }, + ignore: [404], + refresh: 'wait_for', + index: '.kibana-test' + }); + + sinon.assert.calledOnce(onBeforeWrite); + }); }); describe('onBeforeWrite', () => { diff --git a/src/server/saved_objects/service/saved_objects_client.js b/src/server/saved_objects/service/saved_objects_client.js index 4ad2f25c98cee..d45db6177398d 100644 --- a/src/server/saved_objects/service/saved_objects_client.js +++ b/src/server/saved_objects/service/saved_objects_client.js @@ -102,6 +102,7 @@ export class SavedObjectsClient { * @param {object} [options={}] * @property {string} [options.id] - force id on creation, not recommended * @property {boolean} [options.overwrite=false] + * @property {object} [options.extraBodyProperties={}] - extra properties to append to the document body, outside of the object's type property * @returns {promise} - { id, type, version, attributes } */ async create(type, attributes = {}, options = {}) { @@ -111,7 +112,7 @@ export class SavedObjectsClient { /** * Creates multiple documents at once * - * @param {array} objects - [{ type, id, attributes }] + * @param {array} objects - [{ type, id, attributes, extraBodyProperties }] * @param {object} [options={}] * @property {boolean} [options.overwrite=false] - overwrites existing documents * @returns {promise} - [{ id, type, version, attributes, error: { message } }] @@ -137,6 +138,7 @@ export class SavedObjectsClient { * @property {string} [options.search] * @property {Array} [options.searchFields] - see Elasticsearch Simple Query String * Query field argument for more information + * @property {object} [options.filters] - ES Query filters to append * @property {integer} [options.page=1] * @property {integer} [options.perPage=20] * @property {string} [options.sortField] @@ -152,6 +154,8 @@ export class SavedObjectsClient { * Returns an array of objects by id * * @param {array} objects - an array ids, or an array of objects containing id and optionally type + * @param {object} [options = {}] + * @param {array} [options.extraSourceProperties = []] - an array of extra properties to return from the underlying document * @returns {promise} - { saved_objects: [{ id, type, version, attributes }] } * @example * @@ -169,6 +173,8 @@ export class SavedObjectsClient { * * @param {string} type * @param {string} id + * @param {object} [options = {}] + * @param {array} [options.extraSourceProperties = []] - an array of extra properties to return from the underlying document * @returns {promise} - { id, type, version, attributes } */ async get(type, id, options = {}) { @@ -182,6 +188,7 @@ export class SavedObjectsClient { * @param {string} id * @param {object} [options={}] * @property {integer} options.version - ensures version matches that of persisted object + * @param {array} [options.extraBodyProperties = {}] - an object of extra properties to write into the underlying document * @returns {promise} */ async update(type, id, attributes, options = {}) { diff --git a/src/server/saved_objects/service/saved_objects_client.test.js b/src/server/saved_objects/service/saved_objects_client.test.js index 784cb3795966b..56127023dd1fe 100644 --- a/src/server/saved_objects/service/saved_objects_client.test.js +++ b/src/server/saved_objects/service/saved_objects_client.test.js @@ -87,9 +87,10 @@ test(`#bulkGet`, async () => { const client = new SavedObjectsClient(mockRepository); const objects = {}; - const result = await client.bulkGet(objects); + const options = Symbol(); + const result = await client.bulkGet(objects, options); - expect(mockRepository.bulkGet).toHaveBeenCalledWith(objects, {}); + expect(mockRepository.bulkGet).toHaveBeenCalledWith(objects, options); expect(result).toBe(returnValue); }); @@ -102,9 +103,10 @@ test(`#get`, async () => { const type = 'foo'; const id = 1; - const result = await client.get(type, id); + const options = Symbol(); + const result = await client.get(type, id, options); - expect(mockRepository.get).toHaveBeenCalledWith(type, id, {}); + expect(mockRepository.get).toHaveBeenCalledWith(type, id, options); expect(result).toBe(returnValue); }); diff --git a/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.test.js b/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.test.js index c7a48b32c4b6c..0ab89ae714d62 100644 --- a/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.test.js +++ b/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.test.js @@ -697,11 +697,12 @@ describe('#bulkGet', () => { { type: type1, id: 'foo-id' }, { type: type2, id: 'bar-id' }, ]; + const options = Symbol(); - const result = await client.bulkGet(objects); + const result = await client.bulkGet(objects, options); expect(result).toBe(returnValue); - expect(mockRepository.bulkGet).toHaveBeenCalledWith(objects, {}); + expect(mockRepository.bulkGet).toHaveBeenCalledWith(objects, options); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'bulk_get', [type1, type2], { objects, @@ -785,11 +786,12 @@ describe('#get', () => { auditLogger: mockAuditLogger, }); const id = Symbol(); + const options = Symbol(); - const result = await client.get(type, id); + const result = await client.get(type, id, options); expect(result).toBe(returnValue); - expect(mockRepository.get).toHaveBeenCalledWith(type, id, {}); + expect(mockRepository.get).toHaveBeenCalledWith(type, id, options); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'get', [type], { type, From 53bb020c7153a565f55f2fca759a49d24d76ab3a Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 5 Jul 2018 11:51:37 -0400 Subject: [PATCH 27/41] remove default context from utility functions --- x-pack/plugins/spaces/common/spaces_url_parser.js | 6 +++--- .../plugins/spaces/server/lib/create_spaces_service.js | 8 ++++---- .../spaces/server/lib/create_spaces_service.test.js | 9 ++------- .../saved_objects_client/spaces_saved_objects_client.js | 2 +- 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/spaces/common/spaces_url_parser.js b/x-pack/plugins/spaces/common/spaces_url_parser.js index e4c41526997e2..26acd98bfe703 100644 --- a/x-pack/plugins/spaces/common/spaces_url_parser.js +++ b/x-pack/plugins/spaces/common/spaces_url_parser.js @@ -4,16 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -export function getSpaceUrlContext(basePath = '/', defaultContext = '') { +export function getSpaceUrlContext(basePath = '/') { // Look for `/s/space-url-context` in the base path const matchResult = basePath.match(/\/s\/([a-z0-9\-]+)/); if (!matchResult || matchResult.length === 0) { - return defaultContext; + return ''; } // Ignoring first result, we only want the capture group result at index 1 - const [, urlContext = defaultContext] = matchResult; + const [, urlContext = ''] = matchResult; return urlContext; } diff --git a/x-pack/plugins/spaces/server/lib/create_spaces_service.js b/x-pack/plugins/spaces/server/lib/create_spaces_service.js index 9ca1d73246058..d9f83f58f035b 100644 --- a/x-pack/plugins/spaces/server/lib/create_spaces_service.js +++ b/x-pack/plugins/spaces/server/lib/create_spaces_service.js @@ -10,17 +10,17 @@ export function createSpacesService() { const contextCache = new WeakMap(); - function getUrlContext(request, defaultContext = null) { + function getUrlContext(request) { if (!contextCache.has(request)) { - populateCache(request, defaultContext); + populateCache(request); } const { urlContext } = contextCache.get(request); return urlContext; } - function populateCache(request, defaultContext) { - const urlContext = getSpaceUrlContext(request.getBasePath(), defaultContext); + function populateCache(request) { + const urlContext = getSpaceUrlContext(request.getBasePath()); contextCache.set(request, { urlContext diff --git a/x-pack/plugins/spaces/server/lib/create_spaces_service.test.js b/x-pack/plugins/spaces/server/lib/create_spaces_service.test.js index 55003267d1990..40b52dac94a6c 100644 --- a/x-pack/plugins/spaces/server/lib/create_spaces_service.test.js +++ b/x-pack/plugins/spaces/server/lib/create_spaces_service.test.js @@ -10,14 +10,9 @@ const createRequest = (urlContext) => ({ getBasePath: () => urlContext ? `/s/${urlContext}` : '' }); -test('returns null for the default space', () => { +test('returns empty string for the default space', () => { const service = createSpacesService(); - expect(service.getUrlContext(createRequest())).toEqual(null); -}); - -test('uses the provided default context when supplied for the default space', () => { - const service = createSpacesService(); - expect(service.getUrlContext(createRequest(), 'default-context')).toEqual('default-context'); + expect(service.getUrlContext(createRequest())).toEqual(''); }); test('returns the urlContext for the current space', () => { diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js index 96b0baef9360d..9102d31908f84 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js @@ -23,7 +23,7 @@ export class SpacesSavedObjectsClient { this._request = request; this._types = types; - this._spaceUrlContext = spacesService.getUrlContext(this._request, ''); + this._spaceUrlContext = spacesService.getUrlContext(this._request); } async create(type, attributes = {}, options = {}) { From 90892cac9150f888db9dac92e6e28642e5528257 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 5 Jul 2018 12:05:17 -0400 Subject: [PATCH 28/41] rename spacesSavedObjectsClientWrapper => spacesSavedObjectsClientWrapperFactory --- x-pack/plugins/spaces/index.js | 6 ++++-- ...t_wrapper.js => saved_objects_client_wrapper_factory.js} | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) rename x-pack/plugins/spaces/server/lib/saved_objects_client/{saved_objects_client_wrapper.js => saved_objects_client_wrapper_factory.js} (85%) diff --git a/x-pack/plugins/spaces/index.js b/x-pack/plugins/spaces/index.js index f1ce0cf419f86..3c9b55fc58fe6 100644 --- a/x-pack/plugins/spaces/index.js +++ b/x-pack/plugins/spaces/index.js @@ -14,7 +14,7 @@ import { createSpacesService } from './server/lib/create_spaces_service'; import { getActiveSpace } from './server/lib/get_active_space'; import { wrapError } from './server/lib/errors'; import mappings from './mappings.json'; -import { spacesSavedObjectsClientWrapper } from './server/lib/saved_objects_client/saved_objects_client_wrapper'; +import { spacesSavedObjectsClientWrapperFactory } from './server/lib/saved_objects_client/saved_objects_client_wrapper_factory'; import { mirrorStatusAndInitialize } from './server/lib/mirror_status_and_initialize'; export const spaces = (kibana) => new kibana.Plugin({ @@ -83,7 +83,9 @@ export const spaces = (kibana) => new kibana.Plugin({ server.decorate('server', 'spaces', spacesService); const { addScopedSavedObjectsClientWrapperFactory, types } = server.savedObjects; - addScopedSavedObjectsClientWrapperFactory(spacesSavedObjectsClientWrapper(spacesService, types)); + addScopedSavedObjectsClientWrapperFactory( + spacesSavedObjectsClientWrapperFactory(spacesService, types) + ); initSpacesApi(server); diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper_factory.js similarity index 85% rename from x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper.js rename to x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper_factory.js index ea67c3c011f18..da7a448d86d2d 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper_factory.js @@ -6,7 +6,7 @@ import { SpacesSavedObjectsClient } from './spaces_saved_objects_client'; -export function spacesSavedObjectsClientWrapper(spacesService, types) { +export function spacesSavedObjectsClientWrapperFactory(spacesService, types) { return ({ client, request }) => new SpacesSavedObjectsClient({ baseClient: client, request, From 4181c9e0f38e4641dee325a1e3cbb06818154580 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 5 Jul 2018 12:07:16 -0400 Subject: [PATCH 29/41] don't mutate passed options for SOC create method --- .../spaces_saved_objects_client.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js index 9102d31908f84..20a766f11b305 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js @@ -31,14 +31,18 @@ export class SpacesSavedObjectsClient { const spaceId = await this._getSpaceId(); const shouldAssignSpaceId = spaceId !== DEFAULT_SPACE_ID && isTypeSpaceAware(type); + const createOptions = { + ...options + }; + if (shouldAssignSpaceId) { - options.extraBodyProperties = { + createOptions.extraBodyProperties = { ...options.extraBodyProperties, - spaceId: await this._getSpaceId() + spaceId }; } - return await this._client.create(type, attributes, options); + return await this._client.create(type, attributes, createOptions); } async bulkCreate(objects, options = {}) { From a35d15fec4b7e49d3357053d2b121fa538d14a06 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 5 Jul 2018 12:52:02 -0400 Subject: [PATCH 30/41] allow options to be passed for get and bulkGet --- .../spaces_saved_objects_client.js | 19 ++++- .../spaces_saved_objects_client.test.js | 77 ++++++++++++++++++- 2 files changed, 89 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js index 20a766f11b305..b8cad1d24072a 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js @@ -7,6 +7,7 @@ import { DEFAULT_SPACE_ID } from '../../../common/constants'; import { isTypeSpaceAware } from './lib/is_type_space_aware'; import { getSpacesQueryFilters } from './lib/query_filters'; +import uniq from 'lodash'; export class SpacesSavedObjectsClient { constructor(options) { @@ -86,12 +87,15 @@ export class SpacesSavedObjectsClient { return await this._client.find({ ...options, ...spaceOptions }); } - async bulkGet(objects = []) { + async bulkGet(objects = [], options = {}) { // ES 'mget' does not support queries, so we have to filter results after the fact. const thisSpaceId = await this._getSpaceId(); + const extraSourceProperties = this._collectExtraSourceProperties(['spaceId', 'type'], options.extraSourceProperties); + const result = await this._client.bulkGet(objects, { - extraSourceProperties: ['spaceId', 'type'] + ...options, + extraSourceProperties }); result.saved_objects = result.saved_objects.map(savedObject => { @@ -113,11 +117,14 @@ export class SpacesSavedObjectsClient { return result; } - async get(type, id) { + async get(type, id, options = {}) { // ES 'get' does not support queries, so we have to filter results after the fact. + const extraSourceProperties = this._collectExtraSourceProperties(['spaceId'], options.extraSourceProperties); + const response = await this._client.get(type, id, { - extraSourceProperties: ['spaceId'] + ...options, + extraSourceProperties }); const { spaceId: objectSpaceId = DEFAULT_SPACE_ID } = response; @@ -177,4 +184,8 @@ export class SpacesSavedObjectsClient { return null; } + + _collectExtraSourceProperties(thisClientProperties, optionalProperties = []) { + return uniq([...thisClientProperties, ...optionalProperties]).value(); + } } diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js index 11fcc8f96004e..265c4f777dae3 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js @@ -76,12 +76,43 @@ describe('#get', () => { const type = 'foo'; const id = 'object_1'; + const options = {}; - const result = await client.get(type, id); + const result = await client.get(type, id, options); expect(result).toBe(SAVED_OBJECTS[id]); }); + test(`merges options.extraSourceProperties`, async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + const id = 'object_1'; + const options = { + extraSourceProperties: ['otherSourceProp'] + }; + + await client.get(type, id, options); + + expect(baseClient.get).toHaveBeenCalledWith(type, id, { + extraSourceProperties: ['spaceId', 'otherSourceProp'] + }); + }); + test(`returns error when the object belongs to a different space`, async () => { const currentSpace = { id: 'space_1', @@ -101,8 +132,9 @@ describe('#get', () => { const type = 'foo'; const id = 'object_2'; + const options = {}; - await expect(client.get(type, id)).rejects.toThrowErrorMatchingSnapshot(); + await expect(client.get(type, id, options)).rejects.toThrowErrorMatchingSnapshot(); }); }); @@ -125,6 +157,7 @@ describe('#bulk_get', () => { }); const type = 'foo'; + const options = {}; const result = await client.bulkGet([{ type, @@ -132,7 +165,7 @@ describe('#bulk_get', () => { }, { type, id: 'object_2' - }]); + }], options); expect(result).toEqual({ saved_objects: [{ @@ -149,6 +182,44 @@ describe('#bulk_get', () => { }] }); }); + + test(`merges options.extraSourceProperties`, async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + + const objects = [{ + type, + id: 'object_1' + }, { + type, + id: 'object_2' + }]; + + const options = { + extraSourceProperties: ['otherSourceProp'] + }; + + await client.bulkGet(objects, options); + + expect(baseClient.bulkGet).toHaveBeenCalledWith(objects, { + extraSourceProperties: ['spaceId', 'type', 'otherSourceProp'] + }); + }); }); describe('#create', () => { From 093dd47b7c027c31e1e43a7064393a1424deb497 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 5 Jul 2018 13:18:35 -0400 Subject: [PATCH 31/41] additional review updates --- .../spaces/public/lib/spaces_manager.js | 2 +- .../spaces_saved_objects_client.js | 3 +- .../spaces_saved_objects_client.test.js | 127 +++++++++++++++++- .../spaces/server/routes/api/v1/spaces.js | 2 +- .../server/routes/api/v1/spaces.test.js | 30 ++++- 5 files changed, 153 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/spaces/public/lib/spaces_manager.js b/x-pack/plugins/spaces/public/lib/spaces_manager.js index d7db211c28a3f..f0a06a2881592 100644 --- a/x-pack/plugins/spaces/public/lib/spaces_manager.js +++ b/x-pack/plugins/spaces/public/lib/spaces_manager.js @@ -38,7 +38,7 @@ export class SpacesManager { async changeSelectedSpace(space) { return await this._httpAgent - .put(`${this._baseUrl}/space/${space.id}/select`) + .post(`${this._baseUrl}/space/${space.id}/select`) .then(response => { if (response.data && response.data.location) { window.location = response.data.location; diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js index b8cad1d24072a..14681eb835904 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js @@ -49,7 +49,8 @@ export class SpacesSavedObjectsClient { async bulkCreate(objects, options = {}) { const spaceId = await this._getSpaceId(); const objectsToCreate = objects.map(o => { - if (isTypeSpaceAware(o.type)) { + const shouldAssignSpaceId = spaceId !== DEFAULT_SPACE_ID && isTypeSpaceAware(o.type); + if (shouldAssignSpaceId) { return { ...o, extraBodyProperties: { diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js index 265c4f777dae3..886c692b345f2 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js @@ -6,6 +6,7 @@ import { SpacesSavedObjectsClient } from './spaces_saved_objects_client'; import { createSpacesService } from '../create_spaces_service'; +import { DEFAULT_SPACE_ID } from '../../../common/constants'; const createObjectEntry = (type, id, spaceId) => ({ [id]: { @@ -282,6 +283,35 @@ describe('#create', () => { expect(baseClient.create).toHaveBeenCalledWith(type, attributes, {}); }); + + test('does not assign a spaceId to space-aware objects belonging to the default space', async () => { + const currentSpace = { + id: DEFAULT_SPACE_ID, + urlContext: '' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + + await client.create(type, attributes); + + // called without extraBodyProperties + expect(baseClient.create).toHaveBeenCalledWith(type, attributes, {}); + }); }); describe('#bulk_create', () => { @@ -315,7 +345,7 @@ describe('#bulk_create', () => { attributes }]; - await client.bulkCreate(objects); + await client.bulkCreate(objects, {}); const expectedCalledWithObjects = objects.map(o => ({ ...o, @@ -357,7 +387,7 @@ describe('#bulk_create', () => { attributes }]; - await client.bulkCreate(objects); + await client.bulkCreate(objects, {}); expect(baseClient.bulkCreate).toHaveBeenCalledWith(objects, {}); }); @@ -392,7 +422,7 @@ describe('#bulk_create', () => { attributes }]; - await client.bulkCreate(objects); + await client.bulkCreate(objects, {}); const expectedCalledWithObjects = [...objects]; expectedCalledWithObjects[1] = { @@ -404,6 +434,42 @@ describe('#bulk_create', () => { expect(baseClient.bulkCreate).toHaveBeenCalledWith(expectedCalledWithObjects, {}); }); + + test('does not assign a spaceId to space-aware objects that belong to the default space', async () => { + const currentSpace = { + id: DEFAULT_SPACE_ID, + urlContext: '' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + const objects = [{ + type: 'foo', + attributes + }, { + type: 'bar', + attributes + }]; + + await client.bulkCreate(objects, {}); + + // called without extraBodyProperties + expect(baseClient.bulkCreate).toHaveBeenCalledWith(objects, {}); + }); }); describe('#update', () => { @@ -462,6 +528,36 @@ describe('#update', () => { await expect(client.update(type, id, attributes)).rejects.toThrowErrorMatchingSnapshot(); }); + + test('allows an object to be updated within the default space', async () => { + const currentSpace = { + id: DEFAULT_SPACE_ID, + urlContext: '' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const id = 'object_0'; + const type = 'foo'; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + const options = {}; + + await client.update(type, id, attributes, options); + + expect(baseClient.update).toHaveBeenCalledWith(type, id, attributes, {}); + }); }); describe('#delete', () => { @@ -490,6 +586,31 @@ describe('#delete', () => { expect(baseClient.delete).toHaveBeenCalledWith(type, id); }); + test('allows an object to be deleted from the default space', async () => { + const currentSpace = { + id: DEFAULT_SPACE_ID, + urlContext: '' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const id = 'object_0'; + const type = 'foo'; + + await client.delete(type, id); + + expect(baseClient.delete).toHaveBeenCalledWith(type, id); + }); + test('does not allow an object to be deleted via a different space', async () => { const currentSpace = { id: 'space_1', diff --git a/x-pack/plugins/spaces/server/routes/api/v1/spaces.js b/x-pack/plugins/spaces/server/routes/api/v1/spaces.js index 1e6745444b01f..6a5912824fe31 100644 --- a/x-pack/plugins/spaces/server/routes/api/v1/spaces.js +++ b/x-pack/plugins/spaces/server/routes/api/v1/spaces.js @@ -206,7 +206,7 @@ export function initSpacesApi(server) { }); server.route({ - method: 'PUT', + method: 'POST', path: '/api/spaces/v1/space/{id}/select', async handler(request, reply) { const client = request.getSavedObjectsClient(); diff --git a/x-pack/plugins/spaces/server/routes/api/v1/spaces.test.js b/x-pack/plugins/spaces/server/routes/api/v1/spaces.test.js index 9e60f66d42238..cd3910e539873 100644 --- a/x-pack/plugins/spaces/server/routes/api/v1/spaces.test.js +++ b/x-pack/plugins/spaces/server/routes/api/v1/spaces.test.js @@ -48,16 +48,20 @@ describe('Spaces API', () => { const teardowns = []; let request; - const config = { - 'server.ssl.enabled': true, + const baseConfig = { 'server.basePath': '' }; beforeEach(() => { - request = async (method, path, setupFn = () => { }) => { + request = async (method, path, setupFn = () => { }, testConfig = {}) => { const server = new Server(); + const config = { + ...baseConfig, + ...testConfig + }; + server.connection({ port: 0 }); await setupFn(server); @@ -154,8 +158,8 @@ describe('Spaces API', () => { }); }); - test('PUT space/{id}/select should respond with the new space location', async () => { - const response = await request('PUT', '/api/spaces/v1/space/a-space/select'); + test('POST space/{id}/select should respond with the new space location', async () => { + const response = await request('POST', '/api/spaces/v1/space/a-space/select'); const { statusCode, @@ -167,4 +171,20 @@ describe('Spaces API', () => { const result = JSON.parse(payload); expect(result.location).toEqual('/s/a-space'); }); + + test('POST space/{id}/select should respond with the new space location when a baseUrl is provided', async () => { + const response = await request('POST', '/api/spaces/v1/space/a-space/select', () => { }, { + 'server.basePath': '/my/base/path' + }); + + const { + statusCode, + payload + } = response; + + expect(statusCode).toEqual(200); + + const result = JSON.parse(payload); + expect(result.location).toEqual('/my/base/path/s/a-space'); + }); }); From 3bb2aa798860eb6abfccd9f1a739d7c49b26d18b Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 5 Jul 2018 14:21:39 -0400 Subject: [PATCH 32/41] manually re-add tests from space-aware-saved-objects --- .../spaces_saved_objects_client.test.js | 1125 ++++++++++++----- 1 file changed, 809 insertions(+), 316 deletions(-) diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js index 11fcc8f96004e..840895ed572b1 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js @@ -7,6 +7,10 @@ import { SpacesSavedObjectsClient } from './spaces_saved_objects_client'; import { createSpacesService } from '../create_spaces_service'; +jest.mock('uuid', () => ({ + v1: jest.fn(() => `mock-id`) +})); + const createObjectEntry = (type, id, spaceId) => ({ [id]: { id, @@ -17,8 +21,8 @@ const createObjectEntry = (type, id, spaceId) => ({ const SAVED_OBJECTS = { ...createObjectEntry('foo', 'object_0'), - ...createObjectEntry('foo', 'object_1', 'space_1'), - ...createObjectEntry('foo', 'object_2', 'space_2'), + ...createObjectEntry('foo', 'space_1:object_1', 'space_1'), + ...createObjectEntry('foo', 'space_2:object_2', 'space_2'), }; const createMockRequest = (space) => ({ @@ -28,11 +32,22 @@ const createMockRequest = (space) => ({ const createMockClient = (space) => { return { get: jest.fn((type, id) => { - return SAVED_OBJECTS[id]; + const object = SAVED_OBJECTS[id]; + if (!object) { + throw new Error(`object not found: ${id}`); + } + return object; }), bulkGet: jest.fn((objects) => { return { - saved_objects: objects.map(o => SAVED_OBJECTS[o.id]) + saved_objects: objects.map(o => SAVED_OBJECTS[o.id] || { + id: o.id, + type: o.type, + error: { + statusCode: 404, + message: 'Not found' + } + }) }; }), find: jest.fn(({ type }) => { @@ -44,9 +59,20 @@ const createMockClient = (space) => { } throw new Error(`not implemented`); }), - create: jest.fn(), - bulkCreate: jest.fn(), - update: jest.fn(), + create: jest.fn((type, attributes, options) => ({ + id: options.id || 'some-new-id', + type, + attributes + })), + bulkCreate: jest.fn(((objects) => objects.map((o, i) => ({ + ...o, + id: o.id || `abc-${i}` + })))), + update: jest.fn((type, id, attributes) => ({ + id, + type, + attributes + })), delete: jest.fn(), errors: { createGenericNotFoundError: jest.fn(() => { @@ -55,390 +81,857 @@ const createMockClient = (space) => { } }; }; +describe('within the default space', () => { + describe('#get', () => { + test(`returns the object when it belongs to the default space`, async () => { + const currentSpace = { + id: 'default', + urlContext: '' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + const id = 'object_0'; + const options = {}; + + const result = await client.get(type, id, options); -describe('#get', () => { - test(`returns the object when it belongs to the current space`, async () => { - const currentSpace = { - id: 'space_1', - urlContext: 'space-1' - }; - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], + expect(result).toBe(SAVED_OBJECTS[id]); }); - const type = 'foo'; - const id = 'object_1'; + test(`returns error when the object belongs to a different space`, async () => { + const currentSpace = { + id: 'default', + urlContext: '' + }; - const result = await client.get(type, id); + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); - expect(result).toBe(SAVED_OBJECTS[id]); - }); + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + const id = 'object_2'; + const options = {}; - test(`returns error when the object belongs to a different space`, async () => { - const currentSpace = { - id: 'space_1', - urlContext: 'space-1' - }; - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], + await expect(client.get(type, id, options)).rejects.toThrowErrorMatchingSnapshot(); }); + }); + + describe('#bulk_get', () => { + test(`only returns objects belonging to the default space`, async () => { + const currentSpace = { + id: 'default', + urlContext: '' + }; - const type = 'foo'; - const id = 'object_2'; + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); - await expect(client.get(type, id)).rejects.toThrowErrorMatchingSnapshot(); + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + const options = {}; + + const result = await client.bulkGet([{ + type, + id: 'object_0' + }, { + type, + id: 'object_2' + }], options); + + expect(result).toEqual({ + saved_objects: [{ + id: 'object_0', + type: 'foo', + }, { + id: 'object_2', + type: 'foo', + error: { + message: 'Not found', + statusCode: 404 + } + }] + }); + }); }); -}); -describe('#bulk_get', () => { - test(`only returns objects belonging to the current space`, async () => { - const currentSpace = { - id: 'space_1', - urlContext: 'space-1' - }; - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], + describe('#create', () => { + test('automatically assigns the object to the default space by not using extraBodyProperties', async () => { + const currentSpace = { + id: 'default', + urlContext: '' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + const options = {}; + + await client.create(type, attributes, options); + + expect(baseClient.create).toHaveBeenCalledWith(type, attributes, options); }); - const type = 'foo'; + test('does not assign a space-unaware object to a space', async () => { + const currentSpace = { + id: 'default', + urlContext: '' + }; - const result = await client.bulkGet([{ - type, - id: 'object_1' - }, { - type, - id: 'object_2' - }]); + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'space'; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + const options = {}; + + await client.create(type, attributes, options); + + expect(baseClient.create).toHaveBeenCalledWith(type, attributes, options); + }); + }); - expect(result).toEqual({ - saved_objects: [{ - id: 'object_1', - spaceId: 'space_1', + describe('#bulk_create', () => { + test('allows for bulk creation when all types are space-aware', async () => { + const currentSpace = { + id: 'default', + urlContext: '' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + const objects = [{ type: 'foo', + attributes + }, { + type: 'bar', + attributes + }]; + const options = {}; + + await client.bulkCreate(objects, options); + + expect(baseClient.bulkCreate).toHaveBeenCalledWith(objects, options); + }); + + test('allows for bulk creation when all types are not space-aware', async () => { + const currentSpace = { + id: 'default', + urlContext: '' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + + const objects = [{ + type: 'space', + attributes + }, { + type: 'space', + attributes + }]; + const options = {}; + + await client.bulkCreate(objects, options); + + expect(baseClient.bulkCreate).toHaveBeenCalledWith(objects, options); + }); + + test('allows space-aware and non-space-aware objects to be created at the same time', async () => { + const currentSpace = { + id: 'default', + urlContext: '' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + + const objects = [{ + type: 'space', + attributes }, { - id: 'object_2', type: 'foo', - error: { - message: 'Not found', - statusCode: 404 - } - }] + attributes + }]; + const options = {}; + + await client.bulkCreate(objects, options); + + expect(baseClient.bulkCreate).toHaveBeenCalledWith(objects, options); }); }); -}); -describe('#create', () => { - test('automatically assigns the object to the current space via extraBodyProperties', async () => { - const currentSpace = { - id: 'space_1', - urlContext: 'space-1' - }; - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], + describe('#update', () => { + test('allows an object to be updated if it exists in the same space', async () => { + const currentSpace = { + id: 'default', + urlContext: '' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const id = 'object_0'; + const type = 'foo'; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + const options = {}; + + await client.update(type, id, attributes, options); + + expect(baseClient.update).toHaveBeenCalledWith(type, id, attributes, options); }); - const type = 'foo'; - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; + test('does not allow an object to be updated via a different space', async () => { + const currentSpace = { + id: 'default', + urlContext: '' + }; - await client.create(type, attributes); + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const id = 'object_2'; + const type = 'foo'; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + const options = {}; - expect(baseClient.create).toHaveBeenCalledWith(type, attributes, { - extraBodyProperties: { - spaceId: 'space_1' - } + await expect(client.update(type, id, attributes, options)).rejects.toThrowErrorMatchingSnapshot(); }); }); - test('does not assign a space-unaware object to a space', async () => { - const currentSpace = { - id: 'space_1', - urlContext: 'space-1' - }; - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], + describe('#delete', () => { + test('allows an object to be deleted if it exists in the same space', async () => { + const currentSpace = { + id: 'default', + urlContext: '' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const id = 'object_0'; + const type = 'foo'; + + await client.delete(type, id); + + expect(baseClient.delete).toHaveBeenCalledWith(type, id); }); - const type = 'space'; - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; + test('does not allow an object to be deleted via a different space', async () => { + const currentSpace = { + id: 'default', + urlContext: '' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); - await client.create(type, attributes); + const id = 'object_2'; + const type = 'foo'; - expect(baseClient.create).toHaveBeenCalledWith(type, attributes, {}); + await expect(client.delete(type, id)).rejects.toThrowErrorMatchingSnapshot(); + }); }); }); -describe('#bulk_create', () => { - test('allows for bulk creation when all types are space-aware', async () => { - const currentSpace = { - id: 'space_1', - urlContext: 'space-1' - }; - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); +describe('within a space', () => { + describe('#get', () => { + test(`returns the object when it belongs to the current space`, async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; - const objects = [{ - type: 'foo', - attributes - }, { - type: 'bar', - attributes - }]; + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); - await client.bulkCreate(objects); + const type = 'foo'; + const id = 'object_1'; + const options = {}; - const expectedCalledWithObjects = objects.map(o => ({ - ...o, - extraBodyProperties: { - spaceId: 'space_1' - } - })); + const result = await client.get(type, id, options); - expect(baseClient.bulkCreate).toHaveBeenCalledWith(expectedCalledWithObjects, {}); - }); + expect(result).toBe(SAVED_OBJECTS['space_1:' + id]); + }); + + test(`returns error when the object belongs to a different space`, async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + const id = 'object_2'; + const options = {}; - test('allows for bulk creation when all types are not space-aware', async () => { - const currentSpace = { - id: 'space_1', - urlContext: 'space-1' - }; - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], + await expect(client.get(type, id, options)).rejects.toThrowErrorMatchingSnapshot(); }); - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; + test(`merges options.extraSourceProperties`, async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; - const objects = [{ - type: 'space', - attributes - }, { - type: 'space', - attributes - }]; + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + const id = 'object_1'; + const options = { + extraSourceProperties: ['otherSourceProp'] + }; - await client.bulkCreate(objects); + await client.get(type, id, options); - expect(baseClient.bulkCreate).toHaveBeenCalledWith(objects, {}); + expect(baseClient.get).toHaveBeenCalledWith(type, `space_1:${id}`, { + extraSourceProperties: ['spaceId', 'otherSourceProp'] + }); + }); }); - test('allows space-aware and non-space-aware objects to be created at the same time', async () => { - const currentSpace = { - id: 'space_1', - urlContext: 'space-1' - }; - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], + describe('#bulk_get', () => { + test(`only returns objects belonging to the current space`, async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + const options = {}; + + const result = await client.bulkGet([{ + type, + id: 'object_1' + }, { + type, + id: 'object_2' + }], options); + + expect(result).toEqual({ + saved_objects: [{ + id: 'object_1', + spaceId: 'space_1', + type: 'foo', + }, { + id: 'object_2', + type: 'foo', + error: { + message: 'Not found', + statusCode: 404 + } + }] + }); }); - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; + test(`merges options.extraSourceProperties`, async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; - const objects = [{ - type: 'space', - attributes - }, { - type: 'foo', - attributes - }]; + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); - await client.bulkCreate(objects); + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); - const expectedCalledWithObjects = [...objects]; - expectedCalledWithObjects[1] = { - ...expectedCalledWithObjects[1], - extraBodyProperties: { - spaceId: 'space_1' - } - }; + const type = 'foo'; + + const objects = [{ + type, + id: 'object_1' + }, { + type, + id: 'object_2' + }]; - expect(baseClient.bulkCreate).toHaveBeenCalledWith(expectedCalledWithObjects, {}); + const options = { + extraSourceProperties: ['otherSourceProp'] + }; + + await client.bulkGet(objects, options); + + const expectedCalledWithObjects = objects.map(obj => ({ + ...obj, + id: `space_1:${obj.id}` + })); + + expect(baseClient.bulkGet).toHaveBeenCalledWith(expectedCalledWithObjects, { + extraSourceProperties: ['spaceId', 'type', 'otherSourceProp'] + }); + }); }); -}); -describe('#update', () => { - test('allows an object to be updated if it exists in the same space', async () => { - const currentSpace = { - id: 'space_1', - urlContext: 'space-1' - }; - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], + describe('#create', () => { + test('automatically assigns the object to the current space via extraBodyProperties', async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + const options = {}; + + await client.create(type, attributes, options); + + expect(baseClient.create).toHaveBeenCalledWith(type, attributes, { + id: 'space_1:mock-id', + extraBodyProperties: { + spaceId: 'space_1' + } + }); }); - const id = 'object_1'; - const type = 'foo'; - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; + test('does not assign a space-unaware object to a space', async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'space'; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + const options = {}; - await client.update(type, id, attributes); + await client.create(type, attributes, options); - expect(baseClient.update).toHaveBeenCalledWith(type, id, attributes, { extraBodyProperties: { spaceId: 'space_1' } }); + expect(baseClient.create).toHaveBeenCalledWith(type, attributes, {}); + }); }); - test('does not allow an object to be updated via a different space', async () => { - const currentSpace = { - id: 'space_1', - urlContext: 'space-1' - }; - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], + describe('#bulk_create', () => { + test('allows for bulk creation when all types are space-aware', async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + const objects = [{ + type: 'foo', + attributes + }, { + type: 'bar', + attributes + }]; + const options = {}; + + await client.bulkCreate(objects, options); + + const expectedCalledWithObjects = objects.map(o => ({ + ...o, + id: `space_1:mock-id`, + extraBodyProperties: { + spaceId: 'space_1' + } + })); + + expect(baseClient.bulkCreate).toHaveBeenCalledWith(expectedCalledWithObjects, {}); }); - const id = 'object_2'; - const type = 'foo'; - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; + test('allows for bulk creation when all types are not space-aware', async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + + const objects = [{ + type: 'space', + attributes + }, { + type: 'space', + attributes + }]; + + const options = {}; + + await client.bulkCreate(objects, options); + + expect(baseClient.bulkCreate).toHaveBeenCalledWith(objects, options); + }); + + test('allows space-aware and non-space-aware objects to be created at the same time', async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + + const objects = [{ + type: 'space', + attributes + }, { + type: 'foo', + attributes + }]; + const options = {}; + + await client.bulkCreate(objects, options); + + const expectedCalledWithObjects = [...objects]; + expectedCalledWithObjects[1] = { + ...expectedCalledWithObjects[1], + id: `space_1:mock-id`, + extraBodyProperties: { + spaceId: 'space_1' + } + }; - await expect(client.update(type, id, attributes)).rejects.toThrowErrorMatchingSnapshot(); + expect(baseClient.bulkCreate).toHaveBeenCalledWith(expectedCalledWithObjects, options); + }); }); -}); -describe('#delete', () => { - test('allows an object to be deleted if it exists in the same space', async () => { - const currentSpace = { - id: 'space_1', - urlContext: 'space-1' - }; - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], + describe('#update', () => { + test('allows an object to be updated if it exists in the same space', async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const id = 'object_1'; + const type = 'foo'; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + const options = {}; + + await client.update(type, id, attributes, options); + + expect(baseClient.update).toHaveBeenCalledWith(type, `space_1:${id}`, attributes, { extraBodyProperties: { spaceId: 'space_1' } }); }); - const id = 'object_1'; - const type = 'foo'; + test('does not allow an object to be updated via a different space', async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; - await client.delete(type, id); + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const id = 'object_2'; + const type = 'foo'; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + const options = {}; - expect(baseClient.delete).toHaveBeenCalledWith(type, id); + await expect(client.update(type, id, attributes, options)).rejects.toThrowErrorMatchingSnapshot(); + }); }); - test('does not allow an object to be deleted via a different space', async () => { - const currentSpace = { - id: 'space_1', - urlContext: 'space-1' - }; - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], + describe('#delete', () => { + test('allows an object to be deleted if it exists in the same space', async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const id = 'object_1'; + const type = 'foo'; + + await client.delete(type, id); + + expect(baseClient.delete).toHaveBeenCalledWith(type, `space_1:${id}`); }); - const id = 'object_2'; - const type = 'foo'; + test('does not allow an object to be deleted via a different space', async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); - await expect(client.delete(type, id)).rejects.toThrowErrorMatchingSnapshot(); + const id = 'object_2'; + const type = 'foo'; + + await expect(client.delete(type, id)).rejects.toThrowErrorMatchingSnapshot(); + }); }); }); From 2195ee0bd2d5387639eba347334b343ee0d8882f Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 5 Jul 2018 17:04:03 -0400 Subject: [PATCH 33/41] consolidate init logic --- x-pack/plugins/security/index.js | 2 +- x-pack/plugins/spaces/index.js | 4 +- .../lib/mirror_status_and_initialize.js | 61 -------- .../lib/mirror_status_and_initialize.test.js | 136 ------------------ .../watch_status_and_license_to_initialize.js | 0 ...h_status_and_license_to_initialize.test.js | 0 6 files changed, 3 insertions(+), 200 deletions(-) delete mode 100644 x-pack/plugins/spaces/server/lib/mirror_status_and_initialize.js delete mode 100644 x-pack/plugins/spaces/server/lib/mirror_status_and_initialize.test.js rename x-pack/{plugins/security => }/server/lib/watch_status_and_license_to_initialize.js (100%) rename x-pack/{plugins/security => }/server/lib/watch_status_and_license_to_initialize.test.js (100%) diff --git a/x-pack/plugins/security/index.js b/x-pack/plugins/security/index.js index 6bcaf4e7aec46..d80e162ae466c 100644 --- a/x-pack/plugins/security/index.js +++ b/x-pack/plugins/security/index.js @@ -22,7 +22,7 @@ import { SecurityAuditLogger } from './server/lib/audit_logger'; import { AuditLogger } from '../../server/lib/audit_logger'; import { SecureSavedObjectsClient } from './server/lib/saved_objects_client/secure_saved_objects_client'; import { registerPrivilegesWithCluster } from './server/lib/privileges'; -import { watchStatusAndLicenseToInitialize } from './server/lib/watch_status_and_license_to_initialize'; +import { watchStatusAndLicenseToInitialize } from '../../server/lib/watch_status_and_license_to_initialize'; export const security = (kibana) => new kibana.Plugin({ id: 'security', diff --git a/x-pack/plugins/spaces/index.js b/x-pack/plugins/spaces/index.js index 3c9b55fc58fe6..e029400ef63c8 100644 --- a/x-pack/plugins/spaces/index.js +++ b/x-pack/plugins/spaces/index.js @@ -15,7 +15,7 @@ import { getActiveSpace } from './server/lib/get_active_space'; import { wrapError } from './server/lib/errors'; import mappings from './mappings.json'; import { spacesSavedObjectsClientWrapperFactory } from './server/lib/saved_objects_client/saved_objects_client_wrapper_factory'; -import { mirrorStatusAndInitialize } from './server/lib/mirror_status_and_initialize'; +import { watchStatusAndLicenseToInitialize } from '../../server/lib/watch_status_and_license_to_initialize'; export const spaces = (kibana) => new kibana.Plugin({ id: 'spaces', @@ -68,7 +68,7 @@ export const spaces = (kibana) => new kibana.Plugin({ const thisPlugin = this; const xpackMainPlugin = server.plugins.xpack_main; - mirrorStatusAndInitialize(xpackMainPlugin.status, thisPlugin.status, async () => { + watchStatusAndLicenseToInitialize(xpackMainPlugin, thisPlugin, async () => { await createDefaultSpace(server); }); diff --git a/x-pack/plugins/spaces/server/lib/mirror_status_and_initialize.js b/x-pack/plugins/spaces/server/lib/mirror_status_and_initialize.js deleted file mode 100644 index 5a4035504d948..0000000000000 --- a/x-pack/plugins/spaces/server/lib/mirror_status_and_initialize.js +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 { Observable } from 'rxjs'; - -export function mirrorStatusAndInitialize(upstreamStatus, downstreamStatus, onGreen) { - const currentState$ = Observable - .of({ - state: upstreamStatus.state, - message: upstreamStatus.message, - }); - - const newState$ = Observable - .fromEvent(upstreamStatus, 'change', null, (previousState, previousMsg, state, message) => { - return { - state, - message, - }; - }); - - const state$ = Observable.merge(currentState$, newState$); - - let onGreenPromise; - const onGreen$ = Observable.create(observer => { - if (!onGreenPromise) { - onGreenPromise = onGreen(); - } - - onGreenPromise - .then(() => { - observer.next({ - state: 'green', - message: 'Ready', - }); - }) - .catch((err) => { - onGreenPromise = null; - observer.next({ - state: 'red', - message: err.message - }); - }); - }); - - - state$ - .switchMap(({ state, message }) => { - if (state !== 'green') { - return Observable.of({ state, message }); - } - - return onGreen$; - }) - .do(({ state, message }) => { - downstreamStatus[state](message); - }) - .subscribe(); -} diff --git a/x-pack/plugins/spaces/server/lib/mirror_status_and_initialize.test.js b/x-pack/plugins/spaces/server/lib/mirror_status_and_initialize.test.js deleted file mode 100644 index 494b029be30b5..0000000000000 --- a/x-pack/plugins/spaces/server/lib/mirror_status_and_initialize.test.js +++ /dev/null @@ -1,136 +0,0 @@ -/* - * 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 { EventEmitter } from 'events'; -import { once } from 'lodash'; -import { mirrorStatusAndInitialize } from './mirror_status_and_initialize'; - -['red', 'yellow', 'disabled'].forEach(state => { - test(`mirrors ${state} immediately`, () => { - const message = `${state} is the status`; - const upstreamStatus = new EventEmitter(); - upstreamStatus.state = state; - upstreamStatus.message = message; - - const downstreamStatus = { - [state]: jest.fn() - }; - - mirrorStatusAndInitialize(upstreamStatus, downstreamStatus); - expect(downstreamStatus[state]).toHaveBeenCalledTimes(1); - expect(downstreamStatus[state]).toHaveBeenCalledWith(message); - }); -}); - -test(`calls onGreen and doesn't immediately set downstream status when the initial status is green`, () => { - const upstreamStatus = new EventEmitter(); - upstreamStatus.state = 'green'; - upstreamStatus.message = ''; - - const downstreamStatus = { - green: jest.fn() - }; - - const onGreenMock = jest.fn().mockImplementation(() => new Promise(() => { })); - mirrorStatusAndInitialize(upstreamStatus, downstreamStatus, onGreenMock); - expect(onGreenMock).toHaveBeenCalledTimes(1); - expect(downstreamStatus.green).toHaveBeenCalledTimes(0); -}); - -test(`only calls onGreen once if it resolves immediately`, () => { - const upstreamStatus = new EventEmitter(); - upstreamStatus.state = 'green'; - upstreamStatus.message = ''; - - const downstreamStatus = { - green: () => { } - }; - - const onGreenMock = jest.fn().mockImplementation(() => Promise.resolve()); - - mirrorStatusAndInitialize(upstreamStatus, downstreamStatus, onGreenMock); - upstreamStatus.emit('change', '', '', 'green', ''); - expect(onGreenMock).toHaveBeenCalledTimes(1); -}); - -test(`calls onGreen twice if it rejects`, (done) => { - const upstreamStatus = new EventEmitter(); - upstreamStatus.state = 'green'; - upstreamStatus.message = ''; - - const downstreamStatus = { - red: once(() => { - // once we see this red, we immediately trigger the upstream status again - // to have it retrigger the onGreen function - upstreamStatus.emit('change', '', '', 'green', ''); - }), - }; - - let count = 0; - const onGreenMock = jest.fn().mockImplementation(() => { - if (++count === 2) { - done(); - } - - return Promise.reject(new Error()); - }); - - mirrorStatusAndInitialize(upstreamStatus, downstreamStatus, onGreenMock); -}); - -test(`sets downstream status to green when onGreen promise resolves`, (done) => { - const state = 'green'; - const message = `${state} is the status`; - const upstreamStatus = new EventEmitter(); - upstreamStatus.state = state; - upstreamStatus.message = message; - - const downstreamStatus = { - green: () => { - done(); - } - }; - - const onGreenMock = jest.fn().mockImplementation(() => Promise.resolve()); - mirrorStatusAndInitialize(upstreamStatus, downstreamStatus, onGreenMock); -}); - -test(`sets downstream status to red when onGreen promise rejects`, (done) => { - const upstreamStatus = new EventEmitter(); - upstreamStatus.state = 'green'; - upstreamStatus.message = ''; - - const errorMessage = 'something went real wrong'; - - const downstreamStatus = { - red: (msg) => { - expect(msg).toBe(errorMessage); - done(); - } - }; - - const onGreenMock = jest.fn().mockImplementation(() => Promise.reject(new Error(errorMessage))); - mirrorStatusAndInitialize(upstreamStatus, downstreamStatus, onGreenMock); -}); - -['red', 'yellow', 'disabled'].forEach(state => { - test(`switches from uninitialized to ${state} on event`, () => { - const message = `${state} is the status`; - const upstreamStatus = new EventEmitter(); - upstreamStatus.state = 'uninitialized'; - upstreamStatus.message = 'uninitialized'; - - const downstreamStatus = { - uninitialized: jest.fn(), - [state]: jest.fn(), - }; - - mirrorStatusAndInitialize(upstreamStatus, downstreamStatus); - upstreamStatus.emit('change', '', '', state, message); - expect(downstreamStatus[state]).toHaveBeenCalledTimes(1); - expect(downstreamStatus[state]).toHaveBeenCalledWith(message); - }); -}); diff --git a/x-pack/plugins/security/server/lib/watch_status_and_license_to_initialize.js b/x-pack/server/lib/watch_status_and_license_to_initialize.js similarity index 100% rename from x-pack/plugins/security/server/lib/watch_status_and_license_to_initialize.js rename to x-pack/server/lib/watch_status_and_license_to_initialize.js diff --git a/x-pack/plugins/security/server/lib/watch_status_and_license_to_initialize.test.js b/x-pack/server/lib/watch_status_and_license_to_initialize.test.js similarity index 100% rename from x-pack/plugins/security/server/lib/watch_status_and_license_to_initialize.test.js rename to x-pack/server/lib/watch_status_and_license_to_initialize.test.js From 48c5f234c98741aebf316a9951898ca2cddac963 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 6 Jul 2018 09:13:19 -0400 Subject: [PATCH 34/41] Add error handling when switching spaces --- x-pack/plugins/spaces/public/lib/spaces_manager.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/spaces/public/lib/spaces_manager.js b/x-pack/plugins/spaces/public/lib/spaces_manager.js index f0a06a2881592..1c0c162eed6e1 100644 --- a/x-pack/plugins/spaces/public/lib/spaces_manager.js +++ b/x-pack/plugins/spaces/public/lib/spaces_manager.js @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { toastNotifications } from 'ui/notify'; export class SpacesManager { constructor(httpAgent, chrome) { @@ -42,7 +43,17 @@ export class SpacesManager { .then(response => { if (response.data && response.data.location) { window.location = response.data.location; + } else { + this._displayError(); } - }); + }) + .catch(() => this._displayError()); + } + + _displayError() { + toastNotifications.addDanger({ + title: 'Unable to change your Space', + text: 'please try again later' + }); } } From 3a832e990180afce547c7ade77d2bacc12945b8b Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 6 Jul 2018 09:22:11 -0400 Subject: [PATCH 35/41] rename single character variables --- .../views/space_selector/space_selector.js | 2 +- .../spaces/server/lib/get_active_space.js | 4 +-- .../spaces_saved_objects_client.js | 10 +++--- .../spaces_saved_objects_client.test.js | 6 ++-- .../server/lib/space_request_interceptors.js | 4 +-- .../spaces/server/routes/api/v1/spaces.js | 36 +++++++++---------- 6 files changed, 31 insertions(+), 31 deletions(-) diff --git a/x-pack/plugins/spaces/public/views/space_selector/space_selector.js b/x-pack/plugins/spaces/public/views/space_selector/space_selector.js index 9f84866390317..d5c1559d955b9 100644 --- a/x-pack/plugins/spaces/public/views/space_selector/space_selector.js +++ b/x-pack/plugins/spaces/public/views/space_selector/space_selector.js @@ -66,7 +66,7 @@ export class SpaceSelector extends Component { let filteredSpaces = spaces; if (searchTerm) { filteredSpaces = spaces - .filter(s => s.name.toLowerCase().indexOf(searchTerm) >= 0 || s.description.toLowerCase().indexOf(searchTerm) >= 0); + .filter(space => space.name.toLowerCase().indexOf(searchTerm) >= 0 || space.description.toLowerCase().indexOf(searchTerm) >= 0); } return ( diff --git a/x-pack/plugins/spaces/server/lib/get_active_space.js b/x-pack/plugins/spaces/server/lib/get_active_space.js index 7fb7821e75274..d46f622880af6 100644 --- a/x-pack/plugins/spaces/server/lib/get_active_space.js +++ b/x-pack/plugins/spaces/server/lib/get_active_space.js @@ -26,7 +26,7 @@ export async function getActiveSpace(savedObjectsClient, basePath) { }); spaces = savedObjects || []; - } catch(e) { + } catch (e) { throw wrapError(e); } @@ -37,7 +37,7 @@ export async function getActiveSpace(savedObjectsClient, basePath) { } if (spaces.length > 1) { - const spaceNames = spaces.map(s => s.attributes.name).join(', '); + const spaceNames = spaces.map(space => space.attributes.name).join(', '); throw Boom.badRequest( `Multiple Spaces share this URL Context: (${spaceNames}). Please correct this in the Management Section.` diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js index 14681eb835904..84d4c6416e790 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js @@ -48,18 +48,18 @@ export class SpacesSavedObjectsClient { async bulkCreate(objects, options = {}) { const spaceId = await this._getSpaceId(); - const objectsToCreate = objects.map(o => { - const shouldAssignSpaceId = spaceId !== DEFAULT_SPACE_ID && isTypeSpaceAware(o.type); + const objectsToCreate = objects.map(object => { + const shouldAssignSpaceId = spaceId !== DEFAULT_SPACE_ID && isTypeSpaceAware(object.type); if (shouldAssignSpaceId) { return { - ...o, + ...object, extraBodyProperties: { - ...o.extraBodyProperties, + ...object.extraBodyProperties, spaceId } }; } - return o; + return object; }); return await this._client.bulkCreate(objectsToCreate, options); diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js index 886c692b345f2..80988b73696a6 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js @@ -33,7 +33,7 @@ const createMockClient = (space) => { }), bulkGet: jest.fn((objects) => { return { - saved_objects: objects.map(o => SAVED_OBJECTS[o.id]) + saved_objects: objects.map(object => SAVED_OBJECTS[object.id]) }; }), find: jest.fn(({ type }) => { @@ -347,8 +347,8 @@ describe('#bulk_create', () => { await client.bulkCreate(objects, {}); - const expectedCalledWithObjects = objects.map(o => ({ - ...o, + const expectedCalledWithObjects = objects.map(object => ({ + ...object, extraBodyProperties: { spaceId: 'space_1' } diff --git a/x-pack/plugins/spaces/server/lib/space_request_interceptors.js b/x-pack/plugins/spaces/server/lib/space_request_interceptors.js index 99b21e29bce14..b664b7a36dc80 100644 --- a/x-pack/plugins/spaces/server/lib/space_request_interceptors.js +++ b/x-pack/plugins/spaces/server/lib/space_request_interceptors.js @@ -77,8 +77,8 @@ export function initSpacesRequestInterceptors(server) { }); } - } catch (e) { - return reply(wrapError(e)); + } catch (error) { + return reply(wrapError(error)); } } diff --git a/x-pack/plugins/spaces/server/routes/api/v1/spaces.js b/x-pack/plugins/spaces/server/routes/api/v1/spaces.js index 6a5912824fe31..613fb41e416f0 100644 --- a/x-pack/plugins/spaces/server/routes/api/v1/spaces.js +++ b/x-pack/plugins/spaces/server/routes/api/v1/spaces.js @@ -64,8 +64,8 @@ export function initSpacesApi(server) { }); spaces = result.saved_objects.map(convertSavedObjectToSpace); - } catch (e) { - return reply(wrapError(e)); + } catch (error) { + return reply(wrapError(error)); } return reply(spaces); @@ -87,8 +87,8 @@ export function initSpacesApi(server) { const response = await client.get('space', spaceId); return reply(convertSavedObjectToSpace(response)); - } catch (e) { - return reply(wrapError(e)); + } catch (error) { + return reply(wrapError(error)); } }, config: { @@ -108,10 +108,10 @@ export function initSpacesApi(server) { const space = omit(request.payload, ['id', '_reserved']); - const { error } = await checkForDuplicateContext(space); + const { error: contextError } = await checkForDuplicateContext(space); - if (error) { - return reply(wrapError(error)); + if (contextError) { + return reply(wrapError(contextError)); } const id = request.params.id; @@ -127,8 +127,8 @@ export function initSpacesApi(server) { } result = await client.create('space', { ...space }, { id, overwrite }); - } catch (e) { - return reply(wrapError(e)); + } catch (error) { + return reply(wrapError(error)); } return reply(convertSavedObjectToSpace(result)); @@ -163,8 +163,8 @@ export function initSpacesApi(server) { let result; try { result = await client.create('space', { ...space }, { id, overwrite }); - } catch (e) { - return reply(wrapError(e)); + } catch (error) { + return reply(wrapError(error)); } return reply(convertSavedObjectToSpace(result)); @@ -194,8 +194,8 @@ export function initSpacesApi(server) { } result = await client.delete('space', id); - } catch (e) { - return reply(wrapError(e)); + } catch (error) { + return reply(wrapError(error)); } return reply(result).code(204); @@ -222,8 +222,8 @@ export function initSpacesApi(server) { location: addSpaceUrlContext(config.get('server.basePath'), existingSpace.urlContext, config.get('server.defaultRoute')) }); - } catch (e) { - return reply(wrapError(e)); + } catch (error) { + return reply(wrapError(error)); } } }); @@ -235,11 +235,11 @@ export function initSpacesApi(server) { id: existingSpace.id, ...existingSpace.attributes }; - } catch (e) { - if (client.errors.isNotFoundError(e)) { + } catch (error) { + if (client.errors.isNotFoundError(error)) { return null; } - throw e; + throw error; } } } From 00bd94cf95499032122a699716487059d23e5107 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Mon, 16 Jul 2018 10:42:07 -0400 Subject: [PATCH 36/41] fix merge --- .../lib/saved_objects_client/secure_saved_objects_client.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.js b/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.js index eeb8a6837d5b5..2b76386dd92ee 100644 --- a/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.js +++ b/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.js @@ -72,7 +72,7 @@ export class SecureSavedObjectsClient { types, 'bulk_get', { objects, options }, - repository => repository.bulkGet(objects) + repository => repository.bulkGet(objects, options) ); } @@ -81,7 +81,7 @@ export class SecureSavedObjectsClient { type, 'get', { type, id, options }, - repository => repository.get(type, id) + repository => repository.get(type, id, options) ); } From 8b1ff6b53f5fad71aca0db4f6321dc0e7417b0b0 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Mon, 23 Jul 2018 13:07:40 -0400 Subject: [PATCH 37/41] address PR feedback --- .../saved_objects/service/lib/repository.js | 26 ++-- .../service/lib/repository.test.js | 59 ++++---- .../spaces/common/spaces_url_parser.js | 25 ---- .../spaces/common/spaces_url_parser.test.js | 29 +--- .../__snapshots__/space_selector.test.js.snap | 96 ++++++------ .../lib/is_type_space_aware.test.js | 4 +- .../spaces_saved_objects_client.js | 29 ++-- .../spaces_saved_objects_client.test.js | 137 ++++++++++++++++-- 8 files changed, 237 insertions(+), 168 deletions(-) diff --git a/src/server/saved_objects/service/lib/repository.js b/src/server/saved_objects/service/lib/repository.js index ae27624a0bcb4..6ede707c1819a 100644 --- a/src/server/saved_objects/service/lib/repository.js +++ b/src/server/saved_objects/service/lib/repository.js @@ -54,13 +54,13 @@ export class SavedObjectsRepository { * @param {object} [options={}] * @property {string} [options.id] - force id on creation, not recommended * @property {boolean} [options.overwrite=false] - * @property {object} [options.extraBodyProperties={}] - extra properties to append to the document body, outside of the object's type property + * @property {object} [options.extraDocumentProperties={}] - extra properties to append to the document body, outside of the object's type property * @returns {promise} - { id, type, version, attributes } */ async create(type, attributes = {}, options = {}) { const { id, - extraBodyProperties = {}, + extraDocumentProperties = {}, overwrite = false } = options; @@ -74,7 +74,7 @@ export class SavedObjectsRepository { index: this._index, refresh: 'wait_for', body: { - ...extraBodyProperties, + ...extraDocumentProperties, type, updated_at: time, [type]: attributes, @@ -101,7 +101,7 @@ export class SavedObjectsRepository { /** * Creates multiple documents at once * - * @param {array} objects - [{ type, id, attributes, extraBodyProperties }] + * @param {array} objects - [{ type, id, attributes, extraDocumentProperties }] * @param {object} [options={}] * @property {boolean} [options.overwrite=false] - overwrites existing documents * @returns {promise} - {saved_objects: [[{ id, type, version, attributes, error: { message } }]} @@ -122,7 +122,7 @@ export class SavedObjectsRepository { } }, { - ...object.extraBodyProperties, + ...object.extraDocumentProperties, type: object.type, updated_at: time, [object.type]: object.attributes, @@ -307,7 +307,7 @@ export class SavedObjectsRepository { * * @param {array} objects - an array ids, or an array of objects containing id and optionally type * @param {object} [options = {}] - * @param {array} [options.extraSourceProperties = []] - an array of extra properties to return from the underlying document + * @param {array} [options.extraDocumentProperties = []] - an array of extra properties to return from the underlying document * @returns {promise} - { saved_objects: [{ id, type, version, attributes }] } * @example * @@ -333,7 +333,7 @@ export class SavedObjectsRepository { const { docs } = response; - const { extraSourceProperties = [] } = options; + const { extraDocumentProperties = [] } = options; return { saved_objects: docs.map((doc, i) => { @@ -353,7 +353,7 @@ export class SavedObjectsRepository { type, ...time && { updated_at: time }, version: doc._version, - ...extraSourceProperties + ...extraDocumentProperties .map(s => ({ [s]: doc._source[s] })) .reduce((acc, prop) => ({ ...acc, ...prop }), {}), attributes: { @@ -372,7 +372,7 @@ export class SavedObjectsRepository { * @param {string} type * @param {string} id * @param {object} [options = {}] - * @param {array} [options.extraSourceProperties = []] - an array of extra properties to return from the underlying document + * @param {array} [options.extraDocumentProperties = []] - an array of extra properties to return from the underlying document * @returns {promise} - { id, type, version, attributes } */ async get(type, id, options = {}) { @@ -390,7 +390,7 @@ export class SavedObjectsRepository { throw errors.createGenericNotFoundError(type, id); } - const { extraSourceProperties = [] } = options; + const { extraDocumentProperties = [] } = options; const { updated_at: updatedAt } = response._source; @@ -399,7 +399,7 @@ export class SavedObjectsRepository { type, ...updatedAt && { updated_at: updatedAt }, version: response._version, - ...extraSourceProperties + ...extraDocumentProperties .map(s => ({ [s]: response._source[s] })) .reduce((acc, prop) => ({ ...acc, ...prop }), {}), attributes: { @@ -415,7 +415,7 @@ export class SavedObjectsRepository { * @param {string} id * @param {object} [options={}] * @property {integer} options.version - ensures version matches that of persisted object - * @param {array} [options.extraBodyProperties = {}] - an object of extra properties to write into the underlying document + * @param {array} [options.extraDocumentProperties = {}] - an object of extra properties to write into the underlying document * @returns {promise} */ async update(type, id, attributes, options = {}) { @@ -429,7 +429,7 @@ export class SavedObjectsRepository { ignore: [404], body: { doc: { - ...options.extraBodyProperties, + ...options.extraDocumentProperties, updated_at: time, [type]: attributes, } diff --git a/src/server/saved_objects/service/lib/repository.test.js b/src/server/saved_objects/service/lib/repository.test.js index f73121c211fd9..2b84e773e0276 100644 --- a/src/server/saved_objects/service/lib/repository.test.js +++ b/src/server/saved_objects/service/lib/repository.test.js @@ -193,13 +193,13 @@ describe('SavedObjectsRepository', () => { sinon.assert.calledOnce(onBeforeWrite); }); - it('appends extraBodyProperties to the document', async () => { + it('appends extraDocumentProperties to the document', async () => { await savedObjectsRepository.create('index-pattern', { title: 'Logstash' }, { - extraBodyProperties: { + extraDocumentProperties: { myExtraProp: 'myExtraValue', myOtherExtraProp: true, } @@ -220,13 +220,13 @@ describe('SavedObjectsRepository', () => { sinon.assert.calledOnce(onBeforeWrite); }); - it('does not allow extraBodyProperties to overwrite existing properties', async () => { + it('does not allow extraDocumentProperties to overwrite existing properties', async () => { await savedObjectsRepository.create('index-pattern', { title: 'Logstash' }, { - extraBodyProperties: { + extraDocumentProperties: { myExtraProp: 'myExtraValue', myOtherExtraProp: true, updated_at: 'should_not_be_used', @@ -389,13 +389,13 @@ describe('SavedObjectsRepository', () => { }); }); - it('appends extraBodyProperties to each created object', async () => { + it('appends extraDocumentProperties to each created object', async () => { callAdminCluster.returns({ items: [] }); await savedObjectsRepository.bulkCreate( [ - { type: 'config', id: 'one', attributes: { title: 'Test One' }, extraBodyProperties: { extraConfigValue: true } }, - { type: 'index-pattern', id: 'two', attributes: { title: 'Test Two' }, extraBodyProperties: { extraIndexValue: true } } + { type: 'config', id: 'one', attributes: { title: 'Test One' }, extraDocumentProperties: { extraConfigValue: true } }, + { type: 'index-pattern', id: 'two', attributes: { title: 'Test Two' }, extraDocumentProperties: { extraIndexValue: true } } ]); sinon.assert.calledOnce(callAdminCluster); @@ -411,28 +411,37 @@ describe('SavedObjectsRepository', () => { sinon.assert.calledOnce(onBeforeWrite); }); - it('does not allow extraBodyProperties to overwrite existing properties', async () => { + it('does not allow extraDocumentProperties to overwrite existing properties', async () => { callAdminCluster.returns({ items: [] }); - const extraBodyProperties = { + const extraDocumentProperties = { extraProp: 'extraVal', updated_at: 'should_not_be_used', }; - const configExtraBodyProperties = { - ...extraBodyProperties, + const configExtraDocumentProperties = { + ...extraDocumentProperties, 'config': { newIgnoredProp: 'should_not_be_used' } }; - const indexPatternExtraBodyProperties = { - ...extraBodyProperties, + const indexPatternExtraDocumentProperties = { + ...extraDocumentProperties, 'index-pattern': { title: 'should_not_be_used', newIgnoredProp: 'should_not_be_used' } }; await savedObjectsRepository.bulkCreate( - [ - { type: 'config', id: 'one', attributes: { title: 'Test One' }, extraBodyProperties: configExtraBodyProperties }, - { type: 'index-pattern', id: 'two', attributes: { title: 'Test Two' }, extraBodyProperties: indexPatternExtraBodyProperties } - ]); + [{ + type: 'config', + id: 'one', + attributes: { title: 'Test One' }, + extraDocumentProperties: configExtraDocumentProperties + }, + { + type: 'index-pattern', + id: 'two', + attributes: { title: 'Test Two' }, + extraDocumentProperties: indexPatternExtraDocumentProperties + }] + ); sinon.assert.calledOnce(callAdminCluster); sinon.assert.calledWithExactly(callAdminCluster, 'bulk', sinon.match({ @@ -635,9 +644,9 @@ describe('SavedObjectsRepository', () => { })); }); - it('includes the requested extraSourceProperties in the response for the requested object', async () => { + it('includes the requested extraDocumentProperties in the response for the requested object', async () => { const response = await savedObjectsRepository.get('index-pattern', 'logstash-*', { - extraSourceProperties: ['specialProperty', 'undefinedProperty'] + extraDocumentProperties: ['specialProperty', 'undefinedProperty'] }); expect(response).toEqual({ @@ -723,7 +732,7 @@ describe('SavedObjectsRepository', () => { }); }); - it('includes the requested extraSourceProperties in the response for each requested object', async () => { + it('includes the requested extraDocumentProperties in the response for each requested object', async () => { callAdminCluster.returns(Promise.resolve({ docs: [{ _type: 'doc', @@ -762,7 +771,7 @@ describe('SavedObjectsRepository', () => { { id: 'bad', type: 'config' }, { id: 'logstash-*', type: 'index-pattern' } ], { - extraSourceProperties: ['specialProperty', 'undefinedProperty'] + extraDocumentProperties: ['specialProperty', 'undefinedProperty'] } ); @@ -857,12 +866,12 @@ describe('SavedObjectsRepository', () => { sinon.assert.calledOnce(onBeforeWrite); }); - it('updates the document including all provided extraBodyProperties', async () => { + it('updates the document including all provided extraDocumentProperties', async () => { await savedObjectsRepository.update( 'index-pattern', 'logstash-*', { title: 'Testing' }, - { extraBodyProperties: { extraProp: 'extraVal' } } + { extraDocumentProperties: { extraProp: 'extraVal' } } ); sinon.assert.calledOnce(callAdminCluster); @@ -881,13 +890,13 @@ describe('SavedObjectsRepository', () => { sinon.assert.calledOnce(onBeforeWrite); }); - it('does not allow extraBodyProperties to overwrite existing properties', async () => { + it('does not allow extraDocumentProperties to overwrite existing properties', async () => { await savedObjectsRepository.update( 'index-pattern', 'logstash-*', { title: 'Testing' }, { - extraBodyProperties: { + extraDocumentProperties: { extraProp: 'extraVal', updated_at: 'should_not_be_used', 'index-pattern': { title: 'should_not_be_used', newIgnoredProp: 'should_not_be_used' } diff --git a/x-pack/plugins/spaces/common/spaces_url_parser.js b/x-pack/plugins/spaces/common/spaces_url_parser.js index 26acd98bfe703..d8d00975e4fde 100644 --- a/x-pack/plugins/spaces/common/spaces_url_parser.js +++ b/x-pack/plugins/spaces/common/spaces_url_parser.js @@ -18,31 +18,6 @@ export function getSpaceUrlContext(basePath = '/') { return urlContext; } -export function stripSpaceUrlContext(basePath = '/') { - const currentSpaceUrlContext = getSpaceUrlContext(basePath); - - let basePathWithoutSpace; - if (currentSpaceUrlContext) { - const indexOfSpaceContext = basePath.indexOf(`/s/${currentSpaceUrlContext}`); - - const startsWithSpace = indexOfSpaceContext === 0; - - if (startsWithSpace) { - basePathWithoutSpace = '/'; - } else { - basePathWithoutSpace = basePath.substring(0, indexOfSpaceContext); - } - } else { - basePathWithoutSpace = basePath; - } - - if (basePathWithoutSpace.endsWith('/')) { - return basePathWithoutSpace.substr(0, -1); - } - - return basePathWithoutSpace; -} - export function addSpaceUrlContext(basePath = '/', urlContext = '', requestedPath = '') { if (requestedPath && !requestedPath.startsWith('/')) { throw new Error(`path must start with a /`); diff --git a/x-pack/plugins/spaces/common/spaces_url_parser.test.js b/x-pack/plugins/spaces/common/spaces_url_parser.test.js index 2491c90243c91..0b83baadc44a6 100644 --- a/x-pack/plugins/spaces/common/spaces_url_parser.test.js +++ b/x-pack/plugins/spaces/common/spaces_url_parser.test.js @@ -3,32 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { stripSpaceUrlContext, getSpaceUrlContext, addSpaceUrlContext } from './spaces_url_parser'; - -describe('stripSpaceUrlContext', () => { - test('it removes the space url context from the base path when the space is not at the root', () => { - const basePath = `/foo/s/my-space`; - expect(stripSpaceUrlContext(basePath)).toEqual('/foo'); - }); - - test('it removes the space url context from the base path when the space is the root', () => { - const basePath = `/s/my-space`; - expect(stripSpaceUrlContext(basePath)).toEqual(''); - }); - - test(`it doesn't change base paths without a space url context`, () => { - const basePath = `/this/is/a-base-path/ok`; - expect(stripSpaceUrlContext(basePath)).toEqual(basePath); - }); - - test('it accepts no parameters', () => { - expect(stripSpaceUrlContext()).toEqual(''); - }); - - test('it remove the trailing slash', () => { - expect(stripSpaceUrlContext('/')).toEqual(''); - }); -}); +import { getSpaceUrlContext, addSpaceUrlContext } from './spaces_url_parser'; describe('getSpaceUrlContext', () => { test('it identifies the space url context', () => { @@ -60,4 +35,4 @@ describe('addSpaceUrlContext', () => { addSpaceUrlContext('', '', 'foo'); }).toThrowErrorMatchingSnapshot(); }); -}); \ No newline at end of file +}); diff --git a/x-pack/plugins/spaces/public/views/space_selector/__snapshots__/space_selector.test.js.snap b/x-pack/plugins/spaces/public/views/space_selector/__snapshots__/space_selector.test.js.snap index a9a69385339a2..debc9de08283d 100644 --- a/x-pack/plugins/spaces/public/views/space_selector/__snapshots__/space_selector.test.js.snap +++ b/x-pack/plugins/spaces/public/views/space_selector/__snapshots__/space_selector.test.js.snap @@ -6,61 +6,61 @@ exports[`it renders without crashing 1`] = ` style={undefined} >
- + + + + + + +
+
+ - - - - - +

+ Select your space +

+
-
- -

- Select your space -

-
-
-
@@ -98,11 +98,11 @@ exports[`it renders without crashing 1`] = `
- No spaces match search criteria - +
diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/is_type_space_aware.test.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/is_type_space_aware.test.js index b2c7fbbcfd3db..a88d4c35125f7 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/is_type_space_aware.test.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/lib/is_type_space_aware.test.js @@ -14,13 +14,13 @@ const knownSpaceAwareTypes = [ 'index_pattern' ]; -const unwareTypes = ['space']; +const unawareTypes = ['space']; knownSpaceAwareTypes.forEach(type => test(`${type} should be space-aware`, () => { expect(isTypeSpaceAware(type)).toBe(true); })); -unwareTypes.forEach(type => test(`${type} should not be space-aware`, () => { +unawareTypes.forEach(type => test(`${type} should not be space-aware`, () => { expect(isTypeSpaceAware(type)).toBe(false); })); diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js index 84d4c6416e790..3f7fee513de3a 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js @@ -21,10 +21,9 @@ export class SpacesSavedObjectsClient { this.errors = baseClient.errors; this._client = baseClient; - this._request = request; this._types = types; - this._spaceUrlContext = spacesService.getUrlContext(this._request); + this._spaceUrlContext = spacesService.getUrlContext(request); } async create(type, attributes = {}, options = {}) { @@ -37,8 +36,8 @@ export class SpacesSavedObjectsClient { }; if (shouldAssignSpaceId) { - createOptions.extraBodyProperties = { - ...options.extraBodyProperties, + createOptions.extraDocumentProperties = { + ...options.extraDocumentProperties, spaceId }; } @@ -53,8 +52,8 @@ export class SpacesSavedObjectsClient { if (shouldAssignSpaceId) { return { ...object, - extraBodyProperties: { - ...object.extraBodyProperties, + extraDocumentProperties: { + ...object.extraDocumentProperties, spaceId } }; @@ -81,9 +80,11 @@ export class SpacesSavedObjectsClient { types = [types]; } + const filters = options.filters || []; + const spaceId = await this._getSpaceId(); - spaceOptions.filters = getSpacesQueryFilters(spaceId, types); + spaceOptions.filters = [...filters, ...getSpacesQueryFilters(spaceId, types)]; return await this._client.find({ ...options, ...spaceOptions }); } @@ -92,11 +93,11 @@ export class SpacesSavedObjectsClient { // ES 'mget' does not support queries, so we have to filter results after the fact. const thisSpaceId = await this._getSpaceId(); - const extraSourceProperties = this._collectExtraSourceProperties(['spaceId', 'type'], options.extraSourceProperties); + const extraDocumentProperties = this._collectExtraDocumentProperties(['spaceId', 'type'], options.extraDocumentProperties); const result = await this._client.bulkGet(objects, { ...options, - extraSourceProperties + extraDocumentProperties }); result.saved_objects = result.saved_objects.map(savedObject => { @@ -121,11 +122,11 @@ export class SpacesSavedObjectsClient { async get(type, id, options = {}) { // ES 'get' does not support queries, so we have to filter results after the fact. - const extraSourceProperties = this._collectExtraSourceProperties(['spaceId'], options.extraSourceProperties); + const extraDocumentProperties = this._collectExtraDocumentProperties(['spaceId'], options.extraDocumentProperties); const response = await this._client.get(type, id, { ...options, - extraSourceProperties + extraDocumentProperties }); const { spaceId: objectSpaceId = DEFAULT_SPACE_ID } = response; @@ -148,8 +149,8 @@ export class SpacesSavedObjectsClient { const spaceId = await this._getSpaceId(); if (spaceId !== DEFAULT_SPACE_ID) { - options.extraBodyProperties = { - ...options.extraBodyProperties, + options.extraDocumentProperties = { + ...options.extraDocumentProperties, spaceId }; } @@ -186,7 +187,7 @@ export class SpacesSavedObjectsClient { return null; } - _collectExtraSourceProperties(thisClientProperties, optionalProperties = []) { + _collectExtraDocumentProperties(thisClientProperties, optionalProperties = []) { return uniq([...thisClientProperties, ...optionalProperties]).value(); } } diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js index 80988b73696a6..5cebbc5d56e26 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js @@ -43,7 +43,9 @@ const createMockClient = (space) => { saved_objects: [space] }; } - throw new Error(`not implemented`); + return { + saved_objects: [] + }; }), create: jest.fn(), bulkCreate: jest.fn(), @@ -84,7 +86,7 @@ describe('#get', () => { expect(result).toBe(SAVED_OBJECTS[id]); }); - test(`merges options.extraSourceProperties`, async () => { + test(`merges options.extraDocumentProperties`, async () => { const currentSpace = { id: 'space_1', urlContext: 'space-1' @@ -104,13 +106,13 @@ describe('#get', () => { const type = 'foo'; const id = 'object_1'; const options = { - extraSourceProperties: ['otherSourceProp'] + extraDocumentProperties: ['otherSourceProp'] }; await client.get(type, id, options); expect(baseClient.get).toHaveBeenCalledWith(type, id, { - extraSourceProperties: ['spaceId', 'otherSourceProp'] + extraDocumentProperties: ['spaceId', 'otherSourceProp'] }); }); @@ -184,7 +186,7 @@ describe('#bulk_get', () => { }); }); - test(`merges options.extraSourceProperties`, async () => { + test(`merges options.extraDocumentProperties`, async () => { const currentSpace = { id: 'space_1', urlContext: 'space-1' @@ -212,19 +214,126 @@ describe('#bulk_get', () => { }]; const options = { - extraSourceProperties: ['otherSourceProp'] + extraDocumentProperties: ['otherSourceProp'] }; await client.bulkGet(objects, options); expect(baseClient.bulkGet).toHaveBeenCalledWith(objects, { - extraSourceProperties: ['spaceId', 'type', 'otherSourceProp'] + extraDocumentProperties: ['spaceId', 'type', 'otherSourceProp'] + }); + }); +}); + +describe('#find', () => { + test(`creates ES query filters restricting objects to the current space`, async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + const options = { + type + }; + + await client.find(options); + + expect(baseClient.find).toHaveBeenCalledWith({ + type, + filters: [{ + bool: { + minimum_should_match: 1, + should: [{ + bool: { + must: [{ + term: { + type + }, + }, { + term: { + spaceId: 'space_1' + } + }] + } + }] + } + }] + }); + }); + + test(`merges incoming filters with filters generated by Spaces Saved Objects Client`, async () => { + const currentSpace = { + id: 'space_1', + urlContext: 'space-1' + }; + + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + const otherFilters = [{ + bool: { + must: [{ + term: { + foo: 'bar' + } + }] + } + }]; + + const options = { + type, + filters: otherFilters + }; + + await client.find(options); + + expect(baseClient.find).toHaveBeenCalledWith({ + type, + filters: [...otherFilters, { + bool: { + minimum_should_match: 1, + should: [{ + bool: { + must: [{ + term: { + type + }, + }, { + term: { + spaceId: 'space_1' + } + }] + } + }] + } + }] }); }); }); describe('#create', () => { - test('automatically assigns the object to the current space via extraBodyProperties', async () => { + test('automatically assigns the object to the current space via extraDocumentProperties', async () => { const currentSpace = { id: 'space_1', urlContext: 'space-1' @@ -250,7 +359,7 @@ describe('#create', () => { await client.create(type, attributes); expect(baseClient.create).toHaveBeenCalledWith(type, attributes, { - extraBodyProperties: { + extraDocumentProperties: { spaceId: 'space_1' } }); @@ -309,7 +418,7 @@ describe('#create', () => { await client.create(type, attributes); - // called without extraBodyProperties + // called without extraDocumentProperties expect(baseClient.create).toHaveBeenCalledWith(type, attributes, {}); }); }); @@ -349,7 +458,7 @@ describe('#bulk_create', () => { const expectedCalledWithObjects = objects.map(object => ({ ...object, - extraBodyProperties: { + extraDocumentProperties: { spaceId: 'space_1' } })); @@ -427,7 +536,7 @@ describe('#bulk_create', () => { const expectedCalledWithObjects = [...objects]; expectedCalledWithObjects[1] = { ...expectedCalledWithObjects[1], - extraBodyProperties: { + extraDocumentProperties: { spaceId: 'space_1' } }; @@ -467,7 +576,7 @@ describe('#bulk_create', () => { await client.bulkCreate(objects, {}); - // called without extraBodyProperties + // called without extraDocumentProperties expect(baseClient.bulkCreate).toHaveBeenCalledWith(objects, {}); }); }); @@ -499,7 +608,7 @@ describe('#update', () => { await client.update(type, id, attributes); - expect(baseClient.update).toHaveBeenCalledWith(type, id, attributes, { extraBodyProperties: { spaceId: 'space_1' } }); + expect(baseClient.update).toHaveBeenCalledWith(type, id, attributes, { extraDocumentProperties: { spaceId: 'space_1' } }); }); test('does not allow an object to be updated via a different space', async () => { From 7e851d048929f53a953f25d637cb3465347fdd94 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 27 Jul 2018 15:57:18 -0400 Subject: [PATCH 38/41] append space id to document id --- .../spaces_url_parser.test.js.snap | 3 - .../spaces_saved_objects_client.test.js.snap | 12 +- .../spaces_saved_objects_client.js | 52 +- .../spaces_saved_objects_client.test.js | 488 ++++++++++-------- 4 files changed, 311 insertions(+), 244 deletions(-) delete mode 100644 x-pack/plugins/spaces/common/__snapshots__/spaces_url_parser.test.js.snap diff --git a/x-pack/plugins/spaces/common/__snapshots__/spaces_url_parser.test.js.snap b/x-pack/plugins/spaces/common/__snapshots__/spaces_url_parser.test.js.snap deleted file mode 100644 index a42d029097b67..0000000000000 --- a/x-pack/plugins/spaces/common/__snapshots__/spaces_url_parser.test.js.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`addSpaceUrlContext it throws an error when the requested path does not start with a slash 1`] = `"path must start with a /"`; diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/__snapshots__/spaces_saved_objects_client.test.js.snap b/x-pack/plugins/spaces/server/lib/saved_objects_client/__snapshots__/spaces_saved_objects_client.test.js.snap index 861b170685f99..fcc01b543263b 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/__snapshots__/spaces_saved_objects_client.test.js.snap +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/__snapshots__/spaces_saved_objects_client.test.js.snap @@ -1,13 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`current space (space_1) #create #delete does not allow an object to be deleted via a different space 1`] = `"not found"`; +exports[`current space (space_1) #delete does not allow an object to be deleted via a different space 1`] = `"not found: foo space_1:object_2"`; -exports[`current space (space_1) #create #update does not allow an object to be updated via a different space 1`] = `"not found"`; +exports[`current space (space_1) #get returns error when the object belongs to a different space 1`] = `"not found: foo space_1:object_2"`; -exports[`current space (space_1) #get returns error when the object belongs to a different space 1`] = `"not found"`; +exports[`current space (space_1) #update does not allow an object to be updated via a different space 1`] = `"not found: foo space_1:object_2"`; -exports[`default space #delete does not allow an object to be deleted via a different space 1`] = `"not found"`; +exports[`default space #delete does not allow an object to be deleted via a different space 1`] = `"not found: foo object_2"`; -exports[`default space #get returns error when the object belongs to a different space 1`] = `"not found"`; +exports[`default space #get returns error when the object belongs to a different space 1`] = `"not found: foo object_2"`; -exports[`default space #update does not allow an object to be updated via a different space 1`] = `"not found"`; +exports[`default space #update does not allow an object to be updated via a different space 1`] = `"not found: foo object_2"`; diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js index cbc45d9e3bcad..c13d9cd6e36df 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js @@ -46,7 +46,8 @@ export class SpacesSavedObjectsClient { ...options, extraDocumentProperties: { ...options.extraDocumentProperties - } + }, + id: this._generateDocumentId(type, options.id) }; if (this._shouldAssignSpaceId(type, spaceId)) { @@ -55,7 +56,8 @@ export class SpacesSavedObjectsClient { delete createOptions.extraDocumentProperties.spaceId; } - return await this._client.create(type, attributes, createOptions); + const result = await this._client.create(type, attributes, createOptions); + return this._trimSpaceId(result); } /** @@ -74,7 +76,8 @@ export class SpacesSavedObjectsClient { ...object, extraDocumentProperties: { ...object.extraDocumentProperties - } + }, + id: this._generateDocumentId(object.type, object.id) }; if (this._shouldAssignSpaceId(object.type, spaceId)) { @@ -86,7 +89,10 @@ export class SpacesSavedObjectsClient { return objectToCreate; }); - return await this._client.bulkCreate(objectsToCreate, options); + const result = await this._client.bulkCreate(objectsToCreate, options); + result.saved_objects.forEach(this._trimSpaceId.bind(this)); + + return result; } /** @@ -97,11 +103,13 @@ export class SpacesSavedObjectsClient { * @returns {promise} */ async delete(type, id) { + const documentId = this._generateDocumentId(type, id); + // attempt to retrieve document before deleting. // this ensures that the document belongs to the current space. await this.get(type, id); - return await this._client.delete(type, id); + return await this._client.delete(type, documentId); } /** @@ -132,7 +140,9 @@ export class SpacesSavedObjectsClient { spaceOptions.filters = [...filters, ...getSpacesQueryFilters(spaceId, types)]; - return await this._client.find({ ...options, ...spaceOptions }); + const result = await this._client.find({ ...options, ...spaceOptions }); + result.saved_objects.forEach(this._trimSpaceId.bind(this)); + return result; } /** @@ -155,13 +165,18 @@ export class SpacesSavedObjectsClient { const extraDocumentProperties = this._collectExtraDocumentProperties(['spaceId', 'type'], options.extraDocumentProperties); - const result = await this._client.bulkGet(objects, { + const objectsToRetrieve = objects.map(object => ({ + ...object, + id: this._generateDocumentId(object.type, object.id) + })); + + const result = await this._client.bulkGet(objectsToRetrieve, { ...options, extraDocumentProperties }); result.saved_objects = result.saved_objects.map(savedObject => { - const { id, type, spaceId = DEFAULT_SPACE_ID } = savedObject; + const { id, type, spaceId = DEFAULT_SPACE_ID } = this._trimSpaceId(savedObject); if (isTypeSpaceAware(type)) { if (spaceId !== thisSpaceId) { @@ -191,9 +206,11 @@ export class SpacesSavedObjectsClient { async get(type, id, options = {}) { // ES 'get' does not support queries, so we have to filter results after the fact. + const documentId = this._generateDocumentId(type, id); + const extraDocumentProperties = this._collectExtraDocumentProperties(['spaceId'], options.extraDocumentProperties); - const response = await this._client.get(type, id, { + const response = await this._client.get(type, documentId, { ...options, extraDocumentProperties }); @@ -207,7 +224,7 @@ export class SpacesSavedObjectsClient { } } - return response; + return this._trimSpaceId(response); } /** @@ -228,6 +245,8 @@ export class SpacesSavedObjectsClient { } }; + const documentId = this._generateDocumentId(type, id); + // attempt to retrieve document before updating. // this ensures that the document belongs to the current space. if (isTypeSpaceAware(type)) { @@ -242,7 +261,8 @@ export class SpacesSavedObjectsClient { } } - return await this._client.update(type, id, attributes, updateOptions); + const result = await this._client.update(type, documentId, attributes, updateOptions); + return this._trimSpaceId(result); } _collectExtraDocumentProperties(thisClientProperties, optionalProperties = []) { @@ -253,15 +273,15 @@ export class SpacesSavedObjectsClient { return spaceId !== DEFAULT_SPACE_ID && isTypeSpaceAware(type); } - _generateDocumentId(spaceId, id = uuid.v1()) { - if (!spaceId || spaceId === DEFAULT_SPACE_ID) { + _generateDocumentId(type, id = uuid.v1()) { + if (!this._spaceId || this._spaceId === DEFAULT_SPACE_ID || !isTypeSpaceAware(type)) { return id; } - return `${spaceId}:${id}`; + return `${this._spaceId}:${id}`; } - _trimSpaceId(spaceId, savedObject) { - const prefix = `${spaceId}:`; + _trimSpaceId(savedObject) { + const prefix = `${this._spaceId}:`; if (savedObject.id.startsWith(prefix)) { savedObject.id = savedObject.id.slice(prefix.length); diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js index ec7180572e8bc..4ab1152a91072 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js @@ -11,6 +11,7 @@ jest.mock('uuid', () => ({ v1: jest.fn(() => `mock-id`) })); import { DEFAULT_SPACE_ID } from '../../../common/constants'; +import { cloneDeep } from 'lodash'; const createObjectEntry = (type, id, spaceId) => ({ [id]: { @@ -22,8 +23,8 @@ const createObjectEntry = (type, id, spaceId) => ({ const SAVED_OBJECTS = { ...createObjectEntry('foo', 'object_0'), - ...createObjectEntry('foo', 'object_1', 'space_1'), - ...createObjectEntry('foo', 'object_2', 'space_2'), + ...createObjectEntry('foo', 'space_1:object_1', 'space_1'), + ...createObjectEntry('foo', 'space_2:object_2', 'space_2'), ...createObjectEntry('space', 'space_1'), }; @@ -44,13 +45,33 @@ const createMockRequest = (space) => ({ }); const createMockClient = (space) => { + const errors = { + createGenericNotFoundError: jest.fn((type, id) => { + return new Error(`not found: ${type} ${id}`); + }) + }; + return { get: jest.fn((type, id) => { - return SAVED_OBJECTS[id]; + const result = SAVED_OBJECTS[id]; + if (!result) { + throw errors.createGenericNotFoundError(type, id); + } + return result; }), bulkGet: jest.fn((objects) => { return { - saved_objects: objects.map(object => SAVED_OBJECTS[object.id]) + saved_objects: objects.map(object => { + const result = SAVED_OBJECTS[object.id]; + if (!result) { + return { + id: object.id, + type: object.type, + error: { statusCode: 404, message: 'Not found' } + }; + } + return result; + }) }; }), find: jest.fn(({ type }) => { @@ -64,15 +85,27 @@ const createMockClient = (space) => { saved_objects: [] }; }), - create: jest.fn(), - bulkCreate: jest.fn(), - update: jest.fn(), + create: jest.fn((type, attributes, options) => { + return { + id: options.id || 'foo-id', + type, + attributes + }; + }), + bulkCreate: jest.fn((objects) => { + return { + saved_objects: cloneDeep(objects) + }; + }), + update: jest.fn((type, id, attributes) => { + return { + id, + type, + attributes + }; + }), delete: jest.fn(), - errors: { - createGenericNotFoundError: jest.fn(() => { - return new Error('not found'); - }) - } + errors, }; }; @@ -411,7 +444,7 @@ describe('default space', () => { await client.create(type, attributes); - expect(baseClient.create).toHaveBeenCalledWith(type, attributes, { extraDocumentProperties: {} }); + expect(baseClient.create).toHaveBeenCalledWith(type, attributes, { extraDocumentProperties: {}, id: 'mock-id' }); }); test('does not assign a spaceId to space-aware objects belonging to the default space', async () => { @@ -436,7 +469,7 @@ describe('default space', () => { await client.create(type, attributes); // called without extraDocumentProperties - expect(baseClient.create).toHaveBeenCalledWith(type, attributes, { extraDocumentProperties: {} }); + expect(baseClient.create).toHaveBeenCalledWith(type, attributes, { extraDocumentProperties: {}, id: 'mock-id' }); }); }); @@ -471,7 +504,8 @@ describe('default space', () => { const expectedCalledWithObjects = objects.map(object => ({ ...object, - extraDocumentProperties: {} + extraDocumentProperties: {}, + id: 'mock-id' })); expect(baseClient.bulkCreate).toHaveBeenCalledWith(expectedCalledWithObjects, {}); @@ -506,7 +540,7 @@ describe('default space', () => { await client.bulkCreate(objects, {}); expect(baseClient.bulkCreate).toHaveBeenCalledWith(objects.map(o => { - return { ...o, extraDocumentProperties: {} }; + return { ...o, extraDocumentProperties: {}, id: 'mock-id' }; }), {}); }); @@ -541,11 +575,13 @@ describe('default space', () => { const expectedCalledWithObjects = [...objects]; expectedCalledWithObjects[0] = { ...expectedCalledWithObjects[0], - extraDocumentProperties: {} + extraDocumentProperties: {}, + id: 'mock-id' }; expectedCalledWithObjects[1] = { ...expectedCalledWithObjects[1], - extraDocumentProperties: {} + extraDocumentProperties: {}, + id: 'mock-id' }; expect(baseClient.bulkCreate).toHaveBeenCalledWith(expectedCalledWithObjects, {}); @@ -582,7 +618,8 @@ describe('default space', () => { // called with empty extraDocumentProperties expect(baseClient.bulkCreate).toHaveBeenCalledWith(objects.map(o => ({ ...o, - extraDocumentProperties: {} + extraDocumentProperties: {}, + id: 'mock-id' })), {}); }); }); @@ -749,7 +786,7 @@ describe('current space (space_1)', () => { const result = await client.get(type, id, options); - expect(result).toBe(SAVED_OBJECTS[id]); + expect(result).toBe(SAVED_OBJECTS[`${currentSpace.id}:${id}`]); }); test(`returns global objects that don't belong to a specific space`, async () => { @@ -794,7 +831,7 @@ describe('current space (space_1)', () => { await client.get(type, id, options); - expect(baseClient.get).toHaveBeenCalledWith(type, id, { + expect(baseClient.get).toHaveBeenCalledWith(type, `${currentSpace.id}:${id}`, { extraDocumentProperties: ['spaceId', 'otherSourceProp'] }); }); @@ -880,7 +917,7 @@ describe('current space (space_1)', () => { type, id: 'object_1' }, { - type, + type: 'space', id: 'space_1' }], options); @@ -924,7 +961,15 @@ describe('current space (space_1)', () => { await client.bulkGet(objects, options); - expect(baseClient.bulkGet).toHaveBeenCalledWith(objects, { + const expectedCalledWithObjects = objects.map(object => { + const id = `${currentSpace.id}:${object.id}`; + return { + ...object, + id + }; + }); + + expect(baseClient.bulkGet).toHaveBeenCalledWith(expectedCalledWithObjects, { extraDocumentProperties: ['spaceId', 'type', 'otherSourceProp'] }); }); @@ -1059,6 +1104,7 @@ describe('current space (space_1)', () => { await client.create(type, attributes); expect(baseClient.create).toHaveBeenCalledWith(type, attributes, { + id: `${currentSpace.id}:mock-id`, extraDocumentProperties: { spaceId: 'space_1' } @@ -1086,258 +1132,262 @@ describe('current space (space_1)', () => { await client.create(type, attributes); - expect(baseClient.create).toHaveBeenCalledWith(type, attributes, { extraDocumentProperties: {} }); + expect(baseClient.create).toHaveBeenCalledWith(type, attributes, { extraDocumentProperties: {}, id: 'mock-id' }); }); + }); - describe('#bulk_create', () => { - test('allows for bulk creation when all types are space-aware', async () => { + describe('#bulk_create', () => { + test('allows for bulk creation when all types are space-aware', async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(server); - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; - const objects = [{ - type: 'foo', - attributes - }, { - type: 'bar', - attributes - }]; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + const objects = [{ + type: 'foo', + attributes + }, { + type: 'bar', + attributes + }]; - await client.bulkCreate(objects, {}); + await client.bulkCreate(objects, {}); - const expectedCalledWithObjects = objects.map(object => ({ - ...object, - extraDocumentProperties: { - spaceId: 'space_1' - } - })); + const expectedCalledWithObjects = objects.map(object => ({ + ...object, + extraDocumentProperties: { + spaceId: 'space_1' + }, + id: `${currentSpace.id}:mock-id` + })); - expect(baseClient.bulkCreate).toHaveBeenCalledWith(expectedCalledWithObjects, {}); - }); + expect(baseClient.bulkCreate).toHaveBeenCalledWith(expectedCalledWithObjects, {}); + }); - test('allows for bulk creation when all types are not space-aware', async () => { + test('allows for bulk creation when all types are not space-aware', async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(server); - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; - const objects = [{ - type: 'space', - attributes - }, { - type: 'space', - attributes - }]; + const objects = [{ + type: 'space', + attributes + }, { + type: 'space', + attributes + }]; - await client.bulkCreate(objects, {}); + await client.bulkCreate(objects, {}); - expect(baseClient.bulkCreate).toHaveBeenCalledWith(objects.map(o => { - return { ...o, extraDocumentProperties: {} }; - }), {}); - }); + expect(baseClient.bulkCreate).toHaveBeenCalledWith(objects.map(o => { + return { ...o, extraDocumentProperties: {}, id: 'mock-id' }; + }), {}); + }); - test('allows space-aware and non-space-aware (global) objects to be created at the same time', async () => { + test('allows space-aware and non-space-aware (global) objects to be created at the same time', async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(server); - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; - const objects = [{ - type: 'space', - attributes - }, { - type: 'foo', - attributes - }]; + const objects = [{ + type: 'space', + attributes + }, { + type: 'foo', + attributes + }]; - await client.bulkCreate(objects, {}); + await client.bulkCreate(objects, {}); - const expectedCalledWithObjects = [...objects]; - expectedCalledWithObjects[0] = { - ...expectedCalledWithObjects[0], - extraDocumentProperties: {} - }; - expectedCalledWithObjects[1] = { - ...expectedCalledWithObjects[1], - extraDocumentProperties: { - spaceId: 'space_1' - } - }; + const expectedCalledWithObjects = [...objects]; + expectedCalledWithObjects[0] = { + ...expectedCalledWithObjects[0], + extraDocumentProperties: {}, + id: 'mock-id' + }; + expectedCalledWithObjects[1] = { + ...expectedCalledWithObjects[1], + extraDocumentProperties: { + spaceId: 'space_1' + }, + id: `${currentSpace.id}:mock-id` + }; - expect(baseClient.bulkCreate).toHaveBeenCalledWith(expectedCalledWithObjects, {}); - }); + expect(baseClient.bulkCreate).toHaveBeenCalledWith(expectedCalledWithObjects, {}); }); + }); - describe('#update', () => { - test('allows an object to be updated if it exists in the same space', async () => { - - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const id = 'object_1'; - const type = 'foo'; - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; + describe('#update', () => { + test('allows an object to be updated if it exists in the same space', async () => { - await client.update(type, id, attributes); + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(server); - expect(baseClient.update).toHaveBeenCalledWith(type, id, attributes, { extraDocumentProperties: { spaceId: 'space_1' } }); + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], }); - test('allows a global object to be updated', async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); - - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); - - const id = 'space_1'; - const type = 'space'; - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; + const id = 'object_1'; + const type = 'foo'; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; - await client.update(type, id, attributes); + await client.update(type, id, attributes); - expect(baseClient.update).toHaveBeenCalledWith(type, id, attributes, { extraDocumentProperties: {} }); - }); + expect(baseClient.update) + .toHaveBeenCalledWith(type, `${currentSpace.id}:${id}`, attributes, { extraDocumentProperties: { spaceId: 'space_1' } }); + }); - test('does not allow an object to be updated via a different space', async () => { + test('allows a global object to be updated', async () => { + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(server); - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); + const id = 'space_1'; + const type = 'space'; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; - const id = 'object_2'; - const type = 'foo'; - const attributes = { - prop1: 'value 1', - prop2: 'value 2' - }; + await client.update(type, id, attributes); - await expect(client.update(type, id, attributes)).rejects.toThrowErrorMatchingSnapshot(); - }); + expect(baseClient.update).toHaveBeenCalledWith(type, id, attributes, { extraDocumentProperties: {} }); }); - describe('#delete', () => { - test('allows an object to be deleted if it exists in the same space', async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); + test('does not allow an object to be updated via a different space', async () => { - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(server); - const id = 'object_1'; - const type = 'foo'; + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const id = 'object_2'; + const type = 'foo'; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + + await expect(client.update(type, id, attributes)).rejects.toThrowErrorMatchingSnapshot(); + }); + }); - await client.delete(type, id); + describe('#delete', () => { + test('allows an object to be deleted if it exists in the same space', async () => { + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(server); - expect(baseClient.delete).toHaveBeenCalledWith(type, id); + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], }); - test('allows a global object to be deleted', async () => { - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); + const id = 'object_1'; + const type = 'foo'; - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); + await client.delete(type, id); - const id = 'space_1'; - const type = 'space'; + expect(baseClient.delete).toHaveBeenCalledWith(type, `${currentSpace.id}:${id}`); + }); - await client.delete(type, id); + test('allows a global object to be deleted', async () => { + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(server); - expect(baseClient.delete).toHaveBeenCalledWith(type, id); + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], }); - test('does not allow an object to be deleted via a different space', async () => { + const id = 'space_1'; + const type = 'space'; - const request = createMockRequest(currentSpace); - const baseClient = createMockClient(currentSpace); - const spacesService = createSpacesService(server); + await client.delete(type, id); - const client = new SpacesSavedObjectsClient({ - request, - baseClient, - spacesService, - types: [], - }); + expect(baseClient.delete).toHaveBeenCalledWith(type, id); + }); - const id = 'object_2'; - const type = 'foo'; + test('does not allow an object to be deleted via a different space', async () => { - await expect(client.delete(type, id)).rejects.toThrowErrorMatchingSnapshot(); + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(server); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], }); + + const id = 'object_2'; + const type = 'foo'; + + await expect(client.delete(type, id)).rejects.toThrowErrorMatchingSnapshot(); }); }); }); From 7042889901b37f375885e1fc8937d206dd5dba3f Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 27 Jul 2018 17:03:46 -0400 Subject: [PATCH 39/41] update functional tests --- .../apis/saved_objects/create.js | 4 ++- .../apis/saved_objects/delete.js | 23 ++++++++++++------ .../apis/saved_objects/get.js | 17 ++++++++++--- .../saved_objects/lib/space_test_utils.js | 1 - .../apis/saved_objects/update.js | 17 +++++++------ .../saved_objects/spaces/data.json.gz | Bin 2467 -> 2448 bytes 6 files changed, 41 insertions(+), 21 deletions(-) diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/create.js b/x-pack/test/spaces_api_integration/apis/saved_objects/create.js index 59f18c131b1ec..14d6ba54de2e8 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/create.js +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/create.js @@ -31,9 +31,11 @@ export default function ({ getService }) { } }); + const expectedDocumentId = spaceId === DEFAULT_SPACE_ID ? resp.body.id : `${spaceId}:${resp.body.id}`; + // query ES directory to assert on space id const { _source } = await es.get({ - id: `visualization:${resp.body.id}`, + id: `visualization:${expectedDocumentId}`, type: 'doc', index: '.kibana' }); diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/delete.js b/x-pack/test/spaces_api_integration/apis/saved_objects/delete.js index 0e7b72a5553f2..fe3cb7bb2be06 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/delete.js +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/delete.js @@ -7,6 +7,7 @@ import expect from 'expect.js'; import { SPACES } from './lib/spaces'; import { getUrlPrefix, getIdPrefix } from './lib/space_test_utils'; +import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; export default function ({ getService }) { const supertest = getService('supertest'); @@ -14,15 +15,15 @@ export default function ({ getService }) { describe('delete', () => { - const expectEmpty = (resp) => { + const expectEmpty = () => (resp) => { expect(resp.body).to.eql({}); }; - const expectNotFound = (resp) => { + const expectNotFound = (type, id) => (resp) => { expect(resp.body).to.eql({ statusCode: 404, error: 'Not Found', - message: 'Not Found' + message: `Saved object [${type}/${id}] not found` }); }; @@ -35,21 +36,29 @@ export default function ({ getService }) { await supertest .delete(`${getUrlPrefix(spaceId)}/api/saved_objects/dashboard/${getIdPrefix(spaceId)}be3733a0-9efe-11e7-acb3-3dab96693fab`) .expect(tests.spaceAware.statusCode) - .then(tests.spaceAware.response) + .then(tests.spaceAware.response()) )); it(`should return ${tests.notSpaceAware.statusCode} when deleting a non-space-aware doc`, async () => ( await supertest .delete(`${getUrlPrefix(spaceId)}/api/saved_objects/space/space_2`) .expect(tests.notSpaceAware.statusCode) - .then(tests.notSpaceAware.response) + .then(tests.notSpaceAware.response()) )); it(`should return ${tests.inOtherSpace.statusCode} when deleting a doc belonging to another space`, async () => { + const documentId = `${getIdPrefix('space_2')}be3733a0-9efe-11e7-acb3-3dab96693fab`; + + let expectedObjectId = documentId; + + if (spaceId !== DEFAULT_SPACE_ID) { + expectedObjectId = `${spaceId}:${expectedObjectId}`; + } + await supertest - .delete(`${getUrlPrefix(spaceId)}/api/saved_objects/dashboard/${getIdPrefix('space_2')}be3733a0-9efe-11e7-acb3-3dab96693fab`) + .delete(`${getUrlPrefix(spaceId)}/api/saved_objects/dashboard/${documentId}`) .expect(tests.inOtherSpace.statusCode) - .then(tests.inOtherSpace.response); + .then(tests.inOtherSpace.response('dashboard', expectedObjectId)); }); }); }; diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/get.js b/x-pack/test/spaces_api_integration/apis/saved_objects/get.js index 092b9ae499b5c..6a8e21e9b5c99 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/get.js +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/get.js @@ -7,6 +7,7 @@ import expect from 'expect.js'; import { getIdPrefix, getUrlPrefix } from './lib/space_test_utils'; import { SPACES } from './lib/spaces'; +import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; export default function ({ getService }) { const supertest = getService('supertest'); @@ -14,7 +15,7 @@ export default function ({ getService }) { describe('get', () => { - const expectResults = (spaceId) => (resp) => { + const expectResults = (spaceId) => () => (resp) => { // The default space does not assign a space id. const expectedSpaceId = spaceId === 'default' ? undefined : spaceId; @@ -43,10 +44,10 @@ export default function ({ getService }) { expect(resp.body).to.eql(expectedBody); }; - const expectNotFound = (resp) => { + const expectNotFound = (type, id) => (resp) => { expect(resp.body).to.eql({ error: 'Not Found', - message: 'Not Found', + message: `Saved object [${type}/${id}] not found`, statusCode: 404, }); }; @@ -58,10 +59,18 @@ export default function ({ getService }) { it(`should return ${tests.exists.statusCode}`, async () => { const objectId = `${getIdPrefix(otherSpaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`; + + let expectedObjectId = objectId; + const testingMismatchedSpaces = spaceId !== otherSpaceId; + + if (testingMismatchedSpaces && spaceId !== DEFAULT_SPACE_ID) { + expectedObjectId = `${spaceId}:${expectedObjectId}`; + } + return supertest .get(`${getUrlPrefix(spaceId)}/api/saved_objects/visualization/${objectId}`) .expect(tests.exists.statusCode) - .then(tests.exists.response); + .then(tests.exists.response('visualization', expectedObjectId)); }); }); }; diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/lib/space_test_utils.js b/x-pack/test/spaces_api_integration/apis/saved_objects/lib/space_test_utils.js index 73df273c6e6e9..fab201a5b3c00 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/lib/space_test_utils.js +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/lib/space_test_utils.js @@ -10,7 +10,6 @@ export function getUrlPrefix(spaceId) { return spaceId && spaceId !== DEFAULT_SPACE_ID ? `/s/${spaceId}` : ``; } -// Spaces do not actually prefix the ID, but this simplifies testing positive and negative flows. export function getIdPrefix(spaceId) { return spaceId === DEFAULT_SPACE_ID ? '' : `${spaceId}-`; } diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/update.js b/x-pack/test/spaces_api_integration/apis/saved_objects/update.js index 409e4b9db549e..3aea0aadde82e 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/update.js +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/update.js @@ -7,13 +7,14 @@ import expect from 'expect.js'; import { SPACES } from './lib/spaces'; import { getUrlPrefix, getIdPrefix } from './lib/space_test_utils'; +import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants'; export default function ({ getService }) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); describe('update', () => { - const expectSpaceAwareResults = resp => { + const expectSpaceAwareResults = () => resp => { // loose ISO8601 UTC time with milliseconds validation expect(resp.body).to.have.property('updated_at').match(/^[\d-]{10}T[\d:\.]{12}Z$/); @@ -29,7 +30,7 @@ export default function ({ getService }) { }); }; - const expectNonSpaceAwareResults = resp => { + const expectNonSpaceAwareResults = () => resp => { // loose ISO8601 UTC time with milliseconds validation expect(resp.body).to.have.property('updated_at').match(/^[\d-]{10}T[\d:\.]{12}Z$/); @@ -45,11 +46,11 @@ export default function ({ getService }) { }); }; - const expectNotFound = resp => { + const expectNotFound = (type, id) => resp => { expect(resp.body).eql({ statusCode: 404, error: 'Not Found', - message: 'Not Found' + message: `Saved object [${type}/${id}] not found` }); }; @@ -66,7 +67,7 @@ export default function ({ getService }) { } }) .expect(tests.spaceAware.statusCode) - .then(tests.spaceAware.response); + .then(tests.spaceAware.response()); }); it(`should return ${tests.notSpaceAware.statusCode} for a non space-aware doc`, async () => { @@ -78,7 +79,7 @@ export default function ({ getService }) { } }) .expect(tests.notSpaceAware.statusCode) - .then(tests.notSpaceAware.response); + .then(tests.notSpaceAware.response()); }); it(`should return ${tests.inOtherSpace.statusCode} for a doc in another space`, async () => { @@ -91,7 +92,7 @@ export default function ({ getService }) { } }) .expect(tests.inOtherSpace.statusCode) - .then(tests.inOtherSpace.response); + .then(tests.inOtherSpace.response(`visualization`, `${spaceId === DEFAULT_SPACE_ID ? '' : (spaceId + ':')}${id}`)); }); describe('unknown id', () => { @@ -104,7 +105,7 @@ export default function ({ getService }) { } }) .expect(tests.doesntExist.statusCode) - .then(tests.doesntExist.response); + .then(tests.doesntExist.response(`visualization`, `${spaceId === DEFAULT_SPACE_ID ? '' : (spaceId + ':')}not an id`)); }); }); }); diff --git a/x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/data.json.gz b/x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/data.json.gz index f9d440c701b4d470efd7815c4fdd17b22b8d8c04..0be0d4c12010a325540585af33a8a1f8cc877883 100644 GIT binary patch delta 2410 zcmYjLdpr{g8+K<{V7Oi8_K04<&xWR z$(+pBp~%|6=B_j%c)4#2Ref#v4j;ANY)c^};;CPt>zsitKKx!3Q#e zn|&R*Pg}iF6_C}+Zp?Z4W7Q**_(zdUmGGFQO$UrhVR2sKsSLTwdHkhC+w7F}PK4r3 zUq}W;mU9(*W)guAJ+3LZfiZ(&%a^nVq+}5lzi)BgyPoj&npbl|Kp{kJi$3LzB3Ao{ z*H3eH`B}>srCZb2N-E>nZAxwmMD-`f_Rd)@dPjaPJ)GmdE`_(XMQh zBs=PVRg%)Kep=yAwUKLL<`qrtjXL;iYC2M4{ib%$kU;}g#D)(c{R?zJbqsYde*_cu zUb*-L2{WP&j5?@7MR3YWAdIU1?>f`r(6`7P9W&r|oh>d7Y>Mf)ztKdImSY;JTTNGA*OH72I^b}2W4 z+?Kyiby)1+PiCo4C)%*kUTBygTynr4t%jFdi5TUnS5|~U zpWU@eFO_w2y*BGY0JlTTcT&VV@>1v#v)o|$@&wh*SS#18Iw2zSpdlQLN69H6AAhTE z_EJc%Pr&@G)VN|eRpVY$Z+smYO7VNqo7fV7H7mOkgp7Fe(q!%Fdabt=uS*!E9VXym z#FHA{DMcQU63*xlNYALqd1r z@MSe{zer$IJl}b9=w`rzxRpT6)dsP7+0bZd@ilpOTD->$ZvDXEvCET%$6sV#L#YVn zJf>MH)8pC6(812$)jj#`of+StXS%BH(1F4LFx@*lRkhwP6C}_E(qBbgSqzUey^v?? zy5}%B@!&H4TeWq>#|3_GRpw!Th|JuyGe-;X$=I~jw?P%Kos`>gV8;+D>bKNph^smA z-ruexERKX!^e$@7{d}O&49BfyPgtDhxoSe#BZ8B{UOI8MlLQn8A8~Wqq<)GOblhs0 zIsfO`uA41amxSUnQ}6VFbc2E?-RUt+kqG;!t`y$hNt+0EF!|N`Hor5o zN#q!XBuzfx=Wky!RrTIi<4_BKuy0D!&oVRc)vV7tP4{u}t4QU(rdrdu{NeT`+vkUK z0@ge|%u5FU@{KEXeiBF}7cTH&&-k|f4_VGC*+eA;IKH@`{%>aCqt!g;Y^}A3{+n^r zD`gZ0h{?Y0-`jCPQ%5oD($c++*l^q_=*|emp|CrfDl*3;l{A^Af4N8&;uRDPw?w{# z-!?Ox;qgJt&+y^)T+(;q9SrcT9t3Uaneq%EZ|XP*Cr7oia&AN?@XYawuS1JxrtU`@ z0=hwna*r7~*w-!F*mUgL#c@BjQpS?t#s={vxve_E-9B2H7xABm;VzBS zcPG@+SCb*KTM#oS@Yfy(`tE{+#Sv2XqWPE6qE1HSICW%LhIMT1k&e(Wlp#8PfNa7s zFdb-etIOfU*nAaNE)eZNvh;_jz*_9b-K;pmt3KdBdn# zG}CjX;fWrGR}s?mW|X!P#Jm2r^`2ti3QHqX>cU4?U6iNoKnRw~2js8Y({UN#=9PwOp-57lP8X{?&08jK*5B zdA`pX$A`7Qk+D+O{0V=IlI^dnI{02X^+lGG|1E=-8ewi@XSc;RYa9IJCgr;KCS0{+ z%r&Y?9lKN#N1%cZPHmqZ^8IV^x6(r*Omr|>mWkeHTo9N^`7bR!luQehsS>M~S9eK5 z8K>qnB<)L}$MZGJ{2a)Zkc^@Kzv7w@D7 z%AC3^<8n)miRLC?U6N)FGy1~JaK;iYN%6xlZiNgJ-CCGQkN{8=bjyBzR(h!C^Y{;- zRf^T?vp()!fL}41mZtyG^UM|P3UJ9n{R`4;T(21ACpqQ0D-1X#lqaT1%2lCOTS3uS zfaIfHfLCG{20&f-Af|;*0LvNlCV}Pl*>>3J{i%gsjptcPjw3%_#=@F#P9n$r-P%(~>{@m!={LeNKr&FfDQ?*oN@q1G^t>6`&2_ z`wCdDwx;q3DzSXPI7tsZenEb}`EOXXCLqS4bs|`uj0zE~C&Sgu$}euY_%JwnM)k&z?B%);iHo4Io2s);c% zM=j(k%AHv#XFtbNo?g%M{PX_4-=FX2^?tuUGV%QUL@P~a4sMj&JvT5qz|WV1{rA88 zrZpvYvt8U!uhozy__KHNY}q!Dc%`|hGhSFt?#NWHC%X6&JVIJ%XY!Ig9yFRLb^-H{ z8h~vBPF;$b$8SRM-$Jw&*As_AZIX5;?sP>?i#hn4;AsTCg~OrUHMj{PZ=|1(F%)^K zV!@LXU1T%|B!^iP7g!)}fI{RVJO-~m8h?c@ijUU=#znj5SryJV=C=eeH$SRuel(=6 ze5%D>xEtm-mi8&*PT=K>_PXC~L60YH%^#VQmJjJUYqq?kKXzX38P>H4sdx5@BP2zC z*Op%AXkDUk-2BvG<)!cu$Ko>%JEtb}!~@x-sP0h%-nJAJ^5mzpObPSGaaM-*T%IIx1Xb zdco=krnb~u-g;L?c=*Gs;WvBhtHRBtz~mOKruu-ILVJN!vwGFfW<~dM^B<}A!A(N7 zK~D;bVnvU3pz!I5o$CuwQL~7PrdJ9B9blGQHY;li(mQBDC06rjx3VcW%r_{OqI`pg zflza`2jasOq5iI`4(d%{`C7gWtYRI`$=;#`MBd0^AnYPz9gmCTE{<2O=AXqO{=X1CP zS_6iHANJ+avH4BB}uCnv<6{yV43m9jo81sMPCs zQwxF>BkVNdsn_Q>*d|51riv24LEDh7qJ(JoAL62&++F6lIXch{Y5*v~^Bx+^k6hQ1 zGh>?LltrGf+A^< zD{;oh0OW8_kR{&23-kfA=Ou9=bsX4$#_`QE^X zNTa2yc%vLxK0LD$-5mj3Qb+FTthM`#<4QMaQq}$GZ{2@BWj$zbTbQb&-wR3~(nk9A z;lHhVSLqPDFh|m*KihwGCPm-RLA(i~Dx}^IY!GC!##3iZaf7PgVy?VM)^GunnvEub zQ&W4~JtV3(hS1y)bj~`M7RBCIhX;=OVBW9&v)Ye2OGINVp8DTxM@_P6We=W-H0@1y z8$5n@UaV4F-S8G3^7^*sl2li(Z>W7fDT(McGp8cskb@JjrZ@$~Fb!*3?_Zw`lp3g? zTXWO=hoa6#|DD}eb!+vZazN5L)@F#JGn4A{ ziQ!^rWc4-$^(aN_XpGEm%TRok`dZTcc5cO+2u-Pp!Hk`wngxz&+cK0e!LLfKn|)k$ zZAGIUGZi$?qol29E8I`LBs9&QQ1{+z)_6^*kW_U|?DfeldlnrHoxHVBQ#I9wyAWnC zpyC6SBy?`xf)`@l9k@;;xVF*ib4EFwg;@HK@>SpIbW3{v&q&&bE&N6AtdYSD70t^- z_1C(T>t-f#@5>A9UL|~qk~R#Tto*@9i&pCC9HS<&6qmCR+X}fGO=34BuR4%IvjV?o z5CidsE@68JZ8&i`VI1^GMZ^?Ly>$TR%sIZLw*66N`a$2f~_h8Ekdg|PR5wJ3Q*X;c8&taDPI9SdJF~XPZ@##2c zU-^JRc0P&fK{)axK_N5tUCz5NURpp*-kOa0!glc6g#N$w2Tx=^@T^$)C|Ion-LH|C zePcx=_{@i}KnlbS#*3zkV$>sMHDqhlGSE#`vrZS@5bC1F*B}NTD~_#qf7tI-tfQ$g zM5gJW9c2QXHJW(ToqM2z28(dNo$aGx?63@ds(FI@mRU+SPldR;gmP8miN!2e>Lt?V z#~<1ClNs5p?TJ!Dz2)7_=_`{bwEOL)sRr#CiLaT@mx8Zi*7WwD&cDomcA6*akn;2x zkDBigu3JxT(_`fAfOlE`3op4oLB`f-W$A_3fu$<7?O5xEEqqdJpdQ@H>L*i&%3wK0 zQ}$OzQ(P95^$>Z$^|uC!biTujq4^sS-^Z4ALR>?#nq>YV*#m~*6ZpgIglOjH`4N#L z{Gt>X`DDkXQP_b5@Ek}C(fGGC`Y(jn+`cR{b!^1+e5@Mo30kCtmopW9u#oPPcd`Q$ z{5X3z7A@lJDuM_Tr@)rN&3<{GD2%zIa}X2W2U^M(p}<~cn)u(5`rQF}N;UT+;2>;+ z`K!QY5pOlP?Cbt@B_w26kZ`v{r;JPiP~iU$riIGYl6`F@R^{`N|y{yl)3KsFMHOPhiT96Dg1v}At2zeKP;3I~v|i{Jt&*3(4cc4Pp3 zF4|Zi?wR6$Wug;-?j1SW)qDR{_wUuq0lPV#@=8xT{wO zf@ERL8B$v+*~^LN&-X#01JDr_w>}r60%OL_D8haneQ?P8m>r=S&NpXv85|t{2A+Mf AyZ`_I From 560acfa352cbb62a97a6bf7e294262151319ca09 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Mon, 30 Jul 2018 07:58:17 -0400 Subject: [PATCH 40/41] additional get/bulkget tests --- .../spaces_saved_objects_client.test.js | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js index 4ab1152a91072..f864891c4b1a6 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js @@ -137,6 +137,27 @@ describe('default space', () => { expect(result).toBe(SAVED_OBJECTS[id]); }); + test(`does not append the space id to the document id`, async () => { + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(server); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + const id = 'object_0'; + const options = {}; + + await client.get(type, id, options); + + expect(baseClient.get).toHaveBeenCalledWith(type, id, { extraDocumentProperties: ['spaceId'] }); + }); + test(`returns global objects that don't belong to a specific space`, async () => { const request = createMockRequest(currentSpace); const baseClient = createMockClient(currentSpace); @@ -245,6 +266,34 @@ describe('default space', () => { }); }); + test(`does not append the space id to the document id`, async () => { + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(server); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + const options = {}; + + const objects = [{ + type, + id: 'object_0' + }, { + type, + id: 'object_2' + }]; + + await client.bulkGet(objects, options); + + expect(baseClient.bulkGet).toHaveBeenCalledWith(objects, { ...options, extraDocumentProperties: ["spaceId", "type"] }); + }); + test(`returns global objects that don't belong to a specific space`, async () => { const request = createMockRequest(currentSpace); const baseClient = createMockClient(currentSpace); @@ -789,6 +838,27 @@ describe('current space (space_1)', () => { expect(result).toBe(SAVED_OBJECTS[`${currentSpace.id}:${id}`]); }); + test('appends the space id to the document id', async () => { + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(server); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + const id = 'object_1'; + const options = {}; + + await client.get(type, id, options); + + expect(baseClient.get).toHaveBeenCalledWith(type, `${currentSpace.id}:${id}`, { ...options, extraDocumentProperties: ['spaceId'] }); + }); + test(`returns global objects that don't belong to a specific space`, async () => { const request = createMockRequest(currentSpace); const baseClient = createMockClient(currentSpace); @@ -898,6 +968,35 @@ describe('current space (space_1)', () => { }); }); + test('appends the space id to the document id', async () => { + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace); + const spacesService = createSpacesService(server); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + const options = {}; + const objects = [{ + type, + id: 'object_1' + }, { + type, + id: 'object_2' + }]; + + await client.bulkGet(objects, options); + + const expectedObjects = objects.map(o => ({ ...o, id: `${currentSpace.id}:${o.id}` })); + expect(baseClient.bulkGet) + .toHaveBeenCalledWith(expectedObjects, { ...options, extraDocumentProperties: ["spaceId", "type"] }); + }); + test(`returns global objects that don't belong to a specific space`, async () => { const request = createMockRequest(currentSpace); const baseClient = createMockClient(currentSpace); From a5161bfc5d6777410661690527c6c2dad2e2efa1 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Wed, 1 Aug 2018 15:04:35 -0400 Subject: [PATCH 41/41] address PR feedback --- .../spaces_saved_objects_client.test.js.snap | 24 ++ .../spaces_saved_objects_client.js | 34 +- .../spaces_saved_objects_client.test.js | 306 +++++++++++++++++- 3 files changed, 336 insertions(+), 28 deletions(-) diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/__snapshots__/spaces_saved_objects_client.test.js.snap b/x-pack/plugins/spaces/server/lib/saved_objects_client/__snapshots__/spaces_saved_objects_client.test.js.snap index fcc01b543263b..9bd6165f8d057 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/__snapshots__/spaces_saved_objects_client.test.js.snap +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/__snapshots__/spaces_saved_objects_client.test.js.snap @@ -1,13 +1,37 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`current space (space_1) #bulk_create throws when the base client returns a malformed document id 1`] = `"Saved object [foo/mock-id] is missing its expected space identifier."`; + +exports[`current space (space_1) #bulk_get throws when base client returns documents with malformed ids 1`] = `"Saved object [foo/object_1] is missing its expected space identifier."`; + +exports[`current space (space_1) #create throws when the base client returns a malformed document id 1`] = `"Saved object [foo/mock-id] is missing its expected space identifier."`; + exports[`current space (space_1) #delete does not allow an object to be deleted via a different space 1`] = `"not found: foo space_1:object_2"`; +exports[`current space (space_1) #find throws when base client returns documents with malformed ids 1`] = `"Saved object [foo/object_1] is missing its expected space identifier."`; + exports[`current space (space_1) #get returns error when the object belongs to a different space 1`] = `"not found: foo space_1:object_2"`; +exports[`current space (space_1) #get returns error when the object has a malformed identifier 1`] = `"Saved object [foo/object_1] is missing its expected space identifier."`; + exports[`current space (space_1) #update does not allow an object to be updated via a different space 1`] = `"not found: foo space_1:object_2"`; +exports[`current space (space_1) #update throws when the base client returns a malformed document id 1`] = `"Saved object [foo/object_1] is missing its expected space identifier."`; + +exports[`default space #bulk_create throws when the base client returns a malformed document id 1`] = `"Saved object [foo/default:default] has an unexpected space identifier [default]."`; + +exports[`default space #bulk_get throws when the base client returns a malformed document id 1`] = `"Saved object [foo/default:default] has an unexpected space identifier [default]."`; + +exports[`default space #create throws when the base client returns a malformed document id 1`] = `"Saved object [foo/default:default] has an unexpected space identifier [default]."`; + exports[`default space #delete does not allow an object to be deleted via a different space 1`] = `"not found: foo object_2"`; +exports[`default space #find throws when the base client returns a malformed document id 1`] = `"Saved object [foo/default:default] has an unexpected space identifier [default]."`; + exports[`default space #get returns error when the object belongs to a different space 1`] = `"not found: foo object_2"`; +exports[`default space #get throws when the base client returns a malformed document id 1`] = `"Saved object [foo/default:default] has an unexpected space identifier [default]."`; + exports[`default space #update does not allow an object to be updated via a different space 1`] = `"not found: foo object_2"`; + +exports[`default space #update throws when the base client returns a malformed document id 1`] = `"Saved object [space/default:default] has an unexpected space identifier [default]."`; diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js index c13d9cd6e36df..c9fd34a60b6d8 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js @@ -47,7 +47,7 @@ export class SpacesSavedObjectsClient { extraDocumentProperties: { ...options.extraDocumentProperties }, - id: this._generateDocumentId(type, options.id) + id: this._prependSpaceId(type, options.id) }; if (this._shouldAssignSpaceId(type, spaceId)) { @@ -77,7 +77,7 @@ export class SpacesSavedObjectsClient { extraDocumentProperties: { ...object.extraDocumentProperties }, - id: this._generateDocumentId(object.type, object.id) + id: this._prependSpaceId(object.type, object.id) }; if (this._shouldAssignSpaceId(object.type, spaceId)) { @@ -103,13 +103,13 @@ export class SpacesSavedObjectsClient { * @returns {promise} */ async delete(type, id) { - const documentId = this._generateDocumentId(type, id); + const objectId = this._prependSpaceId(type, id); // attempt to retrieve document before deleting. // this ensures that the document belongs to the current space. await this.get(type, id); - return await this._client.delete(type, documentId); + return await this._client.delete(type, objectId); } /** @@ -167,7 +167,7 @@ export class SpacesSavedObjectsClient { const objectsToRetrieve = objects.map(object => ({ ...object, - id: this._generateDocumentId(object.type, object.id) + id: this._prependSpaceId(object.type, object.id) })); const result = await this._client.bulkGet(objectsToRetrieve, { @@ -206,11 +206,11 @@ export class SpacesSavedObjectsClient { async get(type, id, options = {}) { // ES 'get' does not support queries, so we have to filter results after the fact. - const documentId = this._generateDocumentId(type, id); + const objectId = this._prependSpaceId(type, id); const extraDocumentProperties = this._collectExtraDocumentProperties(['spaceId'], options.extraDocumentProperties); - const response = await this._client.get(type, documentId, { + const response = await this._client.get(type, objectId, { ...options, extraDocumentProperties }); @@ -245,7 +245,7 @@ export class SpacesSavedObjectsClient { } }; - const documentId = this._generateDocumentId(type, id); + const objectId = this._prependSpaceId(type, id); // attempt to retrieve document before updating. // this ensures that the document belongs to the current space. @@ -261,7 +261,7 @@ export class SpacesSavedObjectsClient { } } - const result = await this._client.update(type, documentId, attributes, updateOptions); + const result = await this._client.update(type, objectId, attributes, updateOptions); return this._trimSpaceId(result); } @@ -273,8 +273,8 @@ export class SpacesSavedObjectsClient { return spaceId !== DEFAULT_SPACE_ID && isTypeSpaceAware(type); } - _generateDocumentId(type, id = uuid.v1()) { - if (!this._spaceId || this._spaceId === DEFAULT_SPACE_ID || !isTypeSpaceAware(type)) { + _prependSpaceId(type, id = uuid.v1()) { + if (this._spaceId === DEFAULT_SPACE_ID || !isTypeSpaceAware(type)) { return id; } return `${this._spaceId}:${id}`; @@ -283,8 +283,16 @@ export class SpacesSavedObjectsClient { _trimSpaceId(savedObject) { const prefix = `${this._spaceId}:`; - if (savedObject.id.startsWith(prefix)) { - savedObject.id = savedObject.id.slice(prefix.length); + const idHasPrefix = savedObject.id.startsWith(prefix); + + if (this._shouldAssignSpaceId(savedObject.type, this._spaceId)) { + if (idHasPrefix) { + savedObject.id = savedObject.id.slice(prefix.length); + } else { + throw new Error(`Saved object [${savedObject.type}/${savedObject.id}] is missing its expected space identifier.`); + } + } else if (idHasPrefix) { + throw new Error(`Saved object [${savedObject.type}/${savedObject.id}] has an unexpected space identifier [${this._spaceId}].`); } return savedObject; diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js index f864891c4b1a6..e7dbffa413c9e 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js @@ -28,6 +28,8 @@ const SAVED_OBJECTS = { ...createObjectEntry('space', 'space_1'), }; +const createSavedObjects = () => cloneDeep(SAVED_OBJECTS); + const config = { 'server.basePath': '/' }; @@ -44,25 +46,39 @@ const createMockRequest = (space) => ({ getBasePath: () => space.id !== DEFAULT_SPACE_ID ? `/s/${space.id}` : '', }); -const createMockClient = (space) => { +const createMockClient = (space, { mangleSpaceIdentifier = false } = {}) => { const errors = { createGenericNotFoundError: jest.fn((type, id) => { return new Error(`not found: ${type} ${id}`); }) }; + const maybeTransformSavedObject = (savedObject) => { + if (!mangleSpaceIdentifier) { + return savedObject; + } + if (space.id === DEFAULT_SPACE_ID) { + savedObject.id = `default:${space.id}`; + } else { + savedObject.id = savedObject.id.split(':')[1]; + } + + return savedObject; + }; + return { get: jest.fn((type, id) => { - const result = SAVED_OBJECTS[id]; + const result = createSavedObjects()[id]; if (!result) { throw errors.createGenericNotFoundError(type, id); } - return result; + + return maybeTransformSavedObject(result); }), bulkGet: jest.fn((objects) => { return { saved_objects: objects.map(object => { - const result = SAVED_OBJECTS[object.id]; + const result = createSavedObjects()[object.id]; if (!result) { return { id: object.id, @@ -70,7 +86,7 @@ const createMockClient = (space) => { error: { statusCode: 404, message: 'Not found' } }; } - return result; + return maybeTransformSavedObject(result); }) }; }), @@ -81,28 +97,33 @@ const createMockClient = (space) => { saved_objects: [space] }; } + const objects = createSavedObjects(); + const result = Object.keys(objects) + .filter(key => objects[key].spaceId === space.id || (space.id === DEFAULT_SPACE_ID && !objects[key].spaceId)) + .map(key => maybeTransformSavedObject(objects[key])); + return { - saved_objects: [] + saved_objects: result }; }), create: jest.fn((type, attributes, options) => { - return { + return maybeTransformSavedObject({ id: options.id || 'foo-id', type, attributes - }; + }); }), bulkCreate: jest.fn((objects) => { return { - saved_objects: cloneDeep(objects) + saved_objects: cloneDeep(objects).map(maybeTransformSavedObject) }; }), update: jest.fn((type, id, attributes) => { - return { + return maybeTransformSavedObject({ id, type, attributes - }; + }); }), delete: jest.fn(), errors, @@ -134,7 +155,7 @@ describe('default space', () => { const result = await client.get(type, id, options); - expect(result).toBe(SAVED_OBJECTS[id]); + expect(result).toEqual(SAVED_OBJECTS[id]); }); test(`does not append the space id to the document id`, async () => { @@ -176,7 +197,7 @@ describe('default space', () => { const result = await client.get(type, id, options); - expect(result).toBe(SAVED_OBJECTS[id]); + expect(result).toEqual(SAVED_OBJECTS[id]); }); test(`merges options.extraDocumentProperties`, async () => { @@ -224,6 +245,25 @@ describe('default space', () => { await expect(client.get(type, id, options)).rejects.toThrowErrorMatchingSnapshot(); }); + + test(`throws when the base client returns a malformed document id`, async () => { + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace, { mangleSpaceIdentifier: true }); + const spacesService = createSpacesService(server); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + const id = 'object_0'; + const options = {}; + + await expect(client.get(type, id, options)).rejects.toThrowErrorMatchingSnapshot(); + }); }); describe('#bulk_get', () => { @@ -360,6 +400,25 @@ describe('default space', () => { extraDocumentProperties: ['spaceId', 'type', 'otherSourceProp'] }); }); + + test(`throws when the base client returns a malformed document id`, async () => { + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace, { mangleSpaceIdentifier: true }); + const spacesService = createSpacesService(server); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + const id = 'object_0'; + const options = {}; + + await expect(client.bulkGet([{ type, id }], options)).rejects.toThrowErrorMatchingSnapshot(); + }); }); describe('#find', () => { @@ -468,6 +527,24 @@ describe('default space', () => { }] }); }); + + test(`throws when the base client returns a malformed document id`, async () => { + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace, { mangleSpaceIdentifier: true }); + const spacesService = createSpacesService(server); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + const options = { type }; + + await expect(client.find(options)).rejects.toThrowErrorMatchingSnapshot(); + }); }); describe('#create', () => { @@ -520,6 +597,27 @@ describe('default space', () => { // called without extraDocumentProperties expect(baseClient.create).toHaveBeenCalledWith(type, attributes, { extraDocumentProperties: {}, id: 'mock-id' }); }); + + test(`throws when the base client returns a malformed document id`, async () => { + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace, { mangleSpaceIdentifier: true }); + const spacesService = createSpacesService(server); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + + await expect(client.create(type, attributes)).rejects.toThrowErrorMatchingSnapshot(); + }); }); describe('#bulk_create', () => { @@ -671,6 +769,33 @@ describe('default space', () => { id: 'mock-id' })), {}); }); + + test(`throws when the base client returns a malformed document id`, async () => { + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace, { mangleSpaceIdentifier: true }); + const spacesService = createSpacesService(server); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + const objects = [{ + type: 'foo', + attributes + }, { + type: 'bar', + attributes + }]; + + await expect(client.bulkCreate(objects, {})).rejects.toThrowErrorMatchingSnapshot(); + }); }); describe('#update', () => { @@ -745,6 +870,28 @@ describe('default space', () => { await expect(client.update(type, id, attributes)).rejects.toThrowErrorMatchingSnapshot(); }); + + test(`throws when the base client returns a malformed document id`, async () => { + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace, { mangleSpaceIdentifier: true }); + const spacesService = createSpacesService(server); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const id = 'space_1'; + const type = 'space'; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + + await expect(client.update(type, id, attributes)).rejects.toThrowErrorMatchingSnapshot(); + }); }); describe('#delete', () => { @@ -835,7 +982,11 @@ describe('current space (space_1)', () => { const result = await client.get(type, id, options); - expect(result).toBe(SAVED_OBJECTS[`${currentSpace.id}:${id}`]); + expect(result).toEqual({ + id, + type, + spaceId: currentSpace.id + }); }); test('appends the space id to the document id', async () => { @@ -877,7 +1028,7 @@ describe('current space (space_1)', () => { const result = await client.get(type, id, options); - expect(result).toBe(SAVED_OBJECTS[id]); + expect(result).toEqual(SAVED_OBJECTS[id]); }); test(`merges options.extraDocumentProperties`, async () => { @@ -925,6 +1076,25 @@ describe('current space (space_1)', () => { await expect(client.get(type, id, options)).rejects.toThrowErrorMatchingSnapshot(); }); + + test(`returns error when the object has a malformed identifier`, async () => { + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace, { mangleSpaceIdentifier: true }); + const spacesService = createSpacesService(server); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + const id = 'object_1'; + const options = {}; + + await expect(client.get(type, id, options)).rejects.toThrowErrorMatchingSnapshot(); + }); }); describe('#bulk_get', () => { @@ -1072,6 +1242,28 @@ describe('current space (space_1)', () => { extraDocumentProperties: ['spaceId', 'type', 'otherSourceProp'] }); }); + + test(`throws when base client returns documents with malformed ids`, async () => { + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace, { mangleSpaceIdentifier: true }); + const spacesService = createSpacesService(server); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + const objects = [{ + type, + id: 'object_1' + }]; + const options = {}; + + await expect(client.bulkGet(objects, options)).rejects.toThrowErrorMatchingSnapshot(); + }); }); describe('#find', () => { @@ -1178,6 +1370,26 @@ describe('current space (space_1)', () => { }] }); }); + + test(`throws when base client returns documents with malformed ids`, async () => { + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace, { mangleSpaceIdentifier: true }); + const spacesService = createSpacesService(server); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + const options = { + type, + }; + + await expect(client.find(options)).rejects.toThrowErrorMatchingSnapshot(); + }); }); describe('#create', () => { @@ -1233,6 +1445,27 @@ describe('current space (space_1)', () => { expect(baseClient.create).toHaveBeenCalledWith(type, attributes, { extraDocumentProperties: {}, id: 'mock-id' }); }); + + test('throws when the base client returns a malformed document id', async () => { + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace, { mangleSpaceIdentifier: true }); + const spacesService = createSpacesService(server); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + + await expect(client.create(type, attributes)).rejects.toThrowErrorMatchingSnapshot(); + }); }); describe('#bulk_create', () => { @@ -1352,6 +1585,27 @@ describe('current space (space_1)', () => { expect(baseClient.bulkCreate).toHaveBeenCalledWith(expectedCalledWithObjects, {}); }); + + test('throws when the base client returns a malformed document id', async () => { + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace, { mangleSpaceIdentifier: true }); + const spacesService = createSpacesService(server); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const type = 'foo'; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + + await expect(client.bulkCreate([{ type, attributes }])).rejects.toThrowErrorMatchingSnapshot(); + }); }); describe('#update', () => { @@ -1427,6 +1681,28 @@ describe('current space (space_1)', () => { await expect(client.update(type, id, attributes)).rejects.toThrowErrorMatchingSnapshot(); }); + + test('throws when the base client returns a malformed document id', async () => { + const request = createMockRequest(currentSpace); + const baseClient = createMockClient(currentSpace, { mangleSpaceIdentifier: true }); + const spacesService = createSpacesService(server); + + const client = new SpacesSavedObjectsClient({ + request, + baseClient, + spacesService, + types: [], + }); + + const id = 'object_1'; + const type = 'foo'; + const attributes = { + prop1: 'value 1', + prop2: 'value 2' + }; + + await expect(client.update(type, id, attributes)).rejects.toThrowErrorMatchingSnapshot(); + }); }); describe('#delete', () => {