Skip to content

Commit

Permalink
[Spaces] Space-Aware Saved Objects (#18862)
Browse files Browse the repository at this point in the history
**This is an updated description that incorperates some of the discussions below**

This PR introduces changes which allow the Spaces plugin to make saved objects "space aware". Effectively, this means wrapping the Saved Objects Client to alter or filter requests/responses.

**Note** Advanced UI Settings (i.e., saved objects of type `config`) are not in scope for this PR, and will be addressed separately.


### Terminology:
- SOC: Saved Objects Client

## Saved Objects Client
### `get`
The response from the base SOC is checked to see if the object belongs to the current space. If not, a 404 is thrown to indicate the object does not exist.

### `bulk_get`
The response from the base SOC is checked to see if each object belongs to the current space. For each object that does not belong, its contents are replaced with a 404 response, which looks identical to the base SOC's 404 response for a missing object.

### `create`
The `spaceId` is appended to the create request, so the base SOC will write the new object into the correct space.

### `bulk_create`
The `spaceId` is appended to each space-aware object in the request, so the base SOC will write the new objects into the correct space.

### `update`
Before allowing an update to be processed by the base SOC, we check to ensure that it belongs to the current space. If not, a 404 is thrown. We also ensure that the `spaceId` is not changed as a result of an update.

### `delete`
Before allowing a delete to be processed by the base SOC, we check to ensure that it belongs to the current space. If not, a 404 is thrown.

### `find`
Searching is arguably the most complex case for this PR, and is responsible for a bulk of the LOC (other than tests). When performing a find, we augment the ES query to ensure that each object belongs to the current space.


## * Belonging to the current space
To figure out if an object belongs to the current space, the following check is performed:
### 1. Is the object's type space-aware?
Most saved object types are space-aware. There are a couple of exceptions as of this PR: space and config.
If the type is not space-aware, then ✅ this object belongs to the current space. This implies that objects that are not space aware belong to every space.

If the type is space-aware, then processing continues to step 2
### 2. Check the object's `spaceId`
Each saved object may have a `spaceId` assigned. This `spaceId` is compared against the `spaceId` that the user's request is executed within. If they match, then the object belongs to the current space.

**caveat** The Default Space is a special-case space that does not assign a `spaceId` to its underlying objects. This is done to maintain backwards compatibility, and makes bootstrapping Spaces much easier for upgrading installations. Given this, there is logic in place which accounts for this special-case. The most interesting example is when we build the query for the SOC's `find` operation. Rather than checking that the object has a particular `spaceId`, we have to check that the object does not have a `spaceId` assigned.
  • Loading branch information
legrego authored Jul 26, 2018
1 parent 3d8b9b9 commit 252e407
Show file tree
Hide file tree
Showing 63 changed files with 4,221 additions and 239 deletions.
2 changes: 1 addition & 1 deletion src/core_plugins/kibana/ui_setting_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -511,4 +511,4 @@ export function getUiSettingDefaults() {
category: ['discover'],
},
};
}
}
59 changes: 47 additions & 12 deletions src/server/saved_objects/service/lib/repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class SavedObjectsRepository {
index,
mappings,
callCluster,
onBeforeWrite = () => {},
onBeforeWrite = () => { },
} = options;

this._index = index;
Expand All @@ -54,11 +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.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,
extraDocumentProperties = {},
overwrite = false
} = options;

Expand All @@ -72,9 +74,10 @@ export class SavedObjectsRepository {
index: this._index,
refresh: 'wait_for',
body: {
...extraDocumentProperties,
type,
updated_at: time,
[type]: attributes
[type]: attributes,
},
});

Expand All @@ -98,7 +101,7 @@ export class SavedObjectsRepository {
/**
* Creates multiple documents at once
*
* @param {array} objects - [{ type, id, attributes }]
* @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 } }]}
Expand All @@ -119,9 +122,10 @@ export class SavedObjectsRepository {
}
},
{
...object.extraDocumentProperties,
type: object.type,
updated_at: time,
[object.type]: object.attributes
[object.type]: object.attributes,
}
];
};
Expand Down Expand Up @@ -216,6 +220,7 @@ export class SavedObjectsRepository {
* @property {string} [options.search]
* @property {Array<string>} [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]
Expand All @@ -233,6 +238,7 @@ export class SavedObjectsRepository {
sortField,
sortOrder,
fields,
filters,
} = options;

if (searchFields && !Array.isArray(searchFields)) {
Expand All @@ -243,6 +249,10 @@ export class SavedObjectsRepository {
throw new TypeError('options.searchFields must be an array');
}

if (filters && !Array.isArray(filters)) {
throw new TypeError('options.filters must be an array');
}

const esOptions = {
index: this._index,
size: perPage,
Expand All @@ -256,7 +266,8 @@ export class SavedObjectsRepository {
searchFields,
type,
sortField,
sortOrder
sortOrder,
filters
})
}
};
Expand Down Expand Up @@ -295,6 +306,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 {array} [options.extraDocumentProperties = []] - an array of extra properties to return from the underlying document
* @returns {promise} - { saved_objects: [{ id, type, version, attributes }] }
* @example
*
Expand All @@ -303,7 +316,7 @@ export class SavedObjectsRepository {
* { id: 'foo', type: 'index-pattern' }
* ])
*/
async bulkGet(objects = []) {
async bulkGet(objects = [], options = {}) {
if (objects.length === 0) {
return { saved_objects: [] };
}
Expand All @@ -318,8 +331,12 @@ export class SavedObjectsRepository {
}
});

const { docs } = response;

const { extraDocumentProperties = [] } = options;

return {
saved_objects: response.docs.map((doc, i) => {
saved_objects: docs.map((doc, i) => {
const { id, type } = objects[i];

if (!doc.found) {
Expand All @@ -331,13 +348,20 @@ export class SavedObjectsRepository {
}

const time = doc._source.updated_at;
return {
const savedObject = {
id,
type,
...time && { updated_at: time },
version: doc._version,
attributes: doc._source[type]
...extraDocumentProperties
.map(s => ({ [s]: doc._source[s] }))
.reduce((acc, prop) => ({ ...acc, ...prop }), {}),
attributes: {
...doc._source[type],
}
};

return savedObject;
})
};
}
Expand All @@ -347,9 +371,11 @@ export class SavedObjectsRepository {
*
* @param {string} type
* @param {string} id
* @param {object} [options = {}]
* @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) {
async get(type, id, options = {}) {
const response = await this._callCluster('get', {
id: this._generateEsId(type, id),
type: this._type,
Expand All @@ -364,14 +390,21 @@ export class SavedObjectsRepository {
throw errors.createGenericNotFoundError(type, id);
}

const { extraDocumentProperties = [] } = options;

const { updated_at: updatedAt } = response._source;

return {
id,
type,
...updatedAt && { updated_at: updatedAt },
version: response._version,
attributes: response._source[type]
...extraDocumentProperties
.map(s => ({ [s]: response._source[s] }))
.reduce((acc, prop) => ({ ...acc, ...prop }), {}),
attributes: {
...response._source[type],
}
};
}

Expand All @@ -382,6 +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.extraDocumentProperties = {}] - an object of extra properties to write into the underlying document
* @returns {promise}
*/
async update(type, id, attributes, options = {}) {
Expand All @@ -395,8 +429,9 @@ export class SavedObjectsRepository {
ignore: [404],
body: {
doc: {
...options.extraDocumentProperties,
updated_at: time,
[type]: attributes
[type]: attributes,
}
},
});
Expand Down
Loading

0 comments on commit 252e407

Please sign in to comment.