Sharing saved-objects phase 1 (#54605)

Co-authored-by: kobelb <brandon.kobel@elastic.co>
This commit is contained in:
Joe Portner 2020-04-09 23:18:18 -04:00 committed by GitHub
parent 330956ec1a
commit 97d1685c3d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
185 changed files with 12670 additions and 15958 deletions

View file

@ -104,7 +104,7 @@ The API returns the following:
"type": "dashboard",
"error": {
"statusCode": 409,
"message": "version conflict, document already exists"
"message": "Saved object [dashboard/be3733a0-9efe-11e7-acb3-3dab96693fab] conflict"
}
}
]

View file

@ -18,6 +18,7 @@ export interface SavedObject<T = unknown>
| [error](./kibana-plugin-core-public.savedobject.error.md) | <code>{</code><br/><code> message: string;</code><br/><code> statusCode: number;</code><br/><code> }</code> | |
| [id](./kibana-plugin-core-public.savedobject.id.md) | <code>string</code> | The ID of this Saved Object, guaranteed to be unique for all objects of the same <code>type</code> |
| [migrationVersion](./kibana-plugin-core-public.savedobject.migrationversion.md) | <code>SavedObjectsMigrationVersion</code> | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. |
| [namespaces](./kibana-plugin-core-public.savedobject.namespaces.md) | <code>string[]</code> | Namespace(s) that this saved object exists in. This attribute is only used for multi-namespace saved object types. |
| [references](./kibana-plugin-core-public.savedobject.references.md) | <code>SavedObjectReference[]</code> | A reference to another saved object. |
| [type](./kibana-plugin-core-public.savedobject.type.md) | <code>string</code> | The type of Saved Object. Each plugin can define it's own custom Saved Object types. |
| [updated\_at](./kibana-plugin-core-public.savedobject.updated_at.md) | <code>string</code> | Timestamp of the last time this document had been updated. |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [SavedObject](./kibana-plugin-core-public.savedobject.md) &gt; [namespaces](./kibana-plugin-core-public.savedobject.namespaces.md)
## SavedObject.namespaces property
Namespace(s) that this saved object exists in. This attribute is only used for multi-namespace saved object types.
<b>Signature:</b>
```typescript
namespaces?: string[];
```

View file

@ -9,5 +9,5 @@ See [SavedObjectTypeRegistry](./kibana-plugin-core-server.savedobjecttyperegistr
<b>Signature:</b>
```typescript
export declare type ISavedObjectTypeRegistry = Pick<SavedObjectTypeRegistry, 'getType' | 'getAllTypes' | 'getIndex' | 'isNamespaceAgnostic' | 'isHidden' | 'getImportableAndExportableTypes' | 'isImportableAndExportable'>;
export declare type ISavedObjectTypeRegistry = Omit<SavedObjectTypeRegistry, 'registerType'>;
```

View file

@ -130,6 +130,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [SavedObjectMigrationContext](./kibana-plugin-core-server.savedobjectmigrationcontext.md) | Migration context provided when invoking a [migration handler](./kibana-plugin-core-server.savedobjectmigrationfn.md) |
| [SavedObjectMigrationMap](./kibana-plugin-core-server.savedobjectmigrationmap.md) | A map of [migration functions](./kibana-plugin-core-server.savedobjectmigrationfn.md) to be used for a given type. The map's keys must be valid semver versions.<!-- -->For a given document, only migrations with a higher version number than that of the document will be applied. Migrations are executed in order, starting from the lowest version and ending with the highest one. |
| [SavedObjectReference](./kibana-plugin-core-server.savedobjectreference.md) | A reference to another saved object. |
| [SavedObjectsAddToNamespacesOptions](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md) | |
| [SavedObjectsBaseOptions](./kibana-plugin-core-server.savedobjectsbaseoptions.md) | |
| [SavedObjectsBulkCreateObject](./kibana-plugin-core-server.savedobjectsbulkcreateobject.md) | |
| [SavedObjectsBulkGetObject](./kibana-plugin-core-server.savedobjectsbulkgetobject.md) | |
@ -143,6 +144,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [SavedObjectsCoreFieldMapping](./kibana-plugin-core-server.savedobjectscorefieldmapping.md) | See [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) for documentation. |
| [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md) | |
| [SavedObjectsDeleteByNamespaceOptions](./kibana-plugin-core-server.savedobjectsdeletebynamespaceoptions.md) | |
| [SavedObjectsDeleteFromNamespacesOptions](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md) | |
| [SavedObjectsDeleteOptions](./kibana-plugin-core-server.savedobjectsdeleteoptions.md) | |
| [SavedObjectsExportOptions](./kibana-plugin-core-server.savedobjectsexportoptions.md) | Options controlling the export operation. |
| [SavedObjectsExportResultDetails](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) | Structure of the export result details entry |
@ -262,6 +264,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [SavedObjectsClientFactoryProvider](./kibana-plugin-core-server.savedobjectsclientfactoryprovider.md) | Provider to invoke to retrieve a [SavedObjectsClientFactory](./kibana-plugin-core-server.savedobjectsclientfactory.md)<!-- -->. |
| [SavedObjectsClientWrapperFactory](./kibana-plugin-core-server.savedobjectsclientwrapperfactory.md) | Describes the factory used to create instances of Saved Objects Client Wrappers. |
| [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) | Describe a [saved object type mapping](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) field.<!-- -->Please refer to [elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html) For the mapping documentation |
| [SavedObjectsNamespaceType](./kibana-plugin-core-server.savedobjectsnamespacetype.md) | The namespace type dictates how a saved object can be interacted in relation to namespaces. Each type is mutually exclusive: \* single (default): this type of saved object is namespace-isolated, e.g., it exists in only one namespace. \* multiple: this type of saved object is shareable, e.g., it can exist in one or more namespaces. \* agnostic: this type of saved object is global.<!-- -->Note: do not write logic that uses this value directly; instead, use the appropriate accessors in the [type registry](./kibana-plugin-core-server.savedobjecttyperegistry.md)<!-- -->. |
| [ScopeableRequest](./kibana-plugin-core-server.scopeablerequest.md) | A user credentials container. It accommodates the necessary auth credentials to impersonate the current user.<!-- -->See [KibanaRequest](./kibana-plugin-core-server.kibanarequest.md)<!-- -->. |
| [ServiceStatusLevel](./kibana-plugin-core-server.servicestatuslevel.md) | A convenience type that represents the union of each value in [ServiceStatusLevels](./kibana-plugin-core-server.servicestatuslevels.md)<!-- -->. |
| [SharedGlobalConfig](./kibana-plugin-core-server.sharedglobalconfig.md) | |

View file

@ -18,6 +18,7 @@ export interface SavedObject<T = unknown>
| [error](./kibana-plugin-core-server.savedobject.error.md) | <code>{</code><br/><code> message: string;</code><br/><code> statusCode: number;</code><br/><code> }</code> | |
| [id](./kibana-plugin-core-server.savedobject.id.md) | <code>string</code> | The ID of this Saved Object, guaranteed to be unique for all objects of the same <code>type</code> |
| [migrationVersion](./kibana-plugin-core-server.savedobject.migrationversion.md) | <code>SavedObjectsMigrationVersion</code> | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. |
| [namespaces](./kibana-plugin-core-server.savedobject.namespaces.md) | <code>string[]</code> | Namespace(s) that this saved object exists in. This attribute is only used for multi-namespace saved object types. |
| [references](./kibana-plugin-core-server.savedobject.references.md) | <code>SavedObjectReference[]</code> | A reference to another saved object. |
| [type](./kibana-plugin-core-server.savedobject.type.md) | <code>string</code> | The type of Saved Object. Each plugin can define it's own custom Saved Object types. |
| [updated\_at](./kibana-plugin-core-server.savedobject.updated_at.md) | <code>string</code> | Timestamp of the last time this document had been updated. |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObject](./kibana-plugin-core-server.savedobject.md) &gt; [namespaces](./kibana-plugin-core-server.savedobject.namespaces.md)
## SavedObject.namespaces property
Namespace(s) that this saved object exists in. This attribute is only used for multi-namespace saved object types.
<b>Signature:</b>
```typescript
namespaces?: string[];
```

View file

@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsAddToNamespacesOptions](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md)
## SavedObjectsAddToNamespacesOptions interface
<b>Signature:</b>
```typescript
export interface SavedObjectsAddToNamespacesOptions extends SavedObjectsBaseOptions
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [refresh](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md) | <code>MutatingOperationRefreshSetting</code> | The Elasticsearch Refresh setting for this operation |
| [version](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.version.md) | <code>string</code> | An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsAddToNamespacesOptions](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md) &gt; [refresh](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md)
## SavedObjectsAddToNamespacesOptions.refresh property
The Elasticsearch Refresh setting for this operation
<b>Signature:</b>
```typescript
refresh?: MutatingOperationRefreshSetting;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsAddToNamespacesOptions](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md) &gt; [version](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.version.md)
## SavedObjectsAddToNamespacesOptions.version property
An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control.
<b>Signature:</b>
```typescript
version?: string;
```

View file

@ -0,0 +1,27 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) &gt; [addToNamespaces](./kibana-plugin-core-server.savedobjectsclient.addtonamespaces.md)
## SavedObjectsClient.addToNamespaces() method
Adds namespaces to a SavedObject
<b>Signature:</b>
```typescript
addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise<{}>;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| type | <code>string</code> | |
| id | <code>string</code> | |
| namespaces | <code>string[]</code> | |
| options | <code>SavedObjectsAddToNamespacesOptions</code> | |
<b>Returns:</b>
`Promise<{}>`

View file

@ -0,0 +1,27 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) &gt; [deleteFromNamespaces](./kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md)
## SavedObjectsClient.deleteFromNamespaces() method
Removes namespaces from a SavedObject
<b>Signature:</b>
```typescript
deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<{}>;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| type | <code>string</code> | |
| id | <code>string</code> | |
| namespaces | <code>string[]</code> | |
| options | <code>SavedObjectsDeleteFromNamespacesOptions</code> | |
<b>Returns:</b>
`Promise<{}>`

View file

@ -25,11 +25,13 @@ The constructor for this class is marked as internal. Third-party code should no
| Method | Modifiers | Description |
| --- | --- | --- |
| [addToNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsclient.addtonamespaces.md) | | Adds namespaces to a SavedObject |
| [bulkCreate(objects, options)](./kibana-plugin-core-server.savedobjectsclient.bulkcreate.md) | | Persists multiple documents batched together as a single request |
| [bulkGet(objects, options)](./kibana-plugin-core-server.savedobjectsclient.bulkget.md) | | Returns an array of objects by id |
| [bulkUpdate(objects, options)](./kibana-plugin-core-server.savedobjectsclient.bulkupdate.md) | | Bulk Updates multiple SavedObject at once |
| [create(type, attributes, options)](./kibana-plugin-core-server.savedobjectsclient.create.md) | | Persists a SavedObject |
| [delete(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.delete.md) | | Deletes a SavedObject |
| [deleteFromNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md) | | Removes namespaces from a SavedObject |
| [find(options)](./kibana-plugin-core-server.savedobjectsclient.find.md) | | Find all SavedObjects matching the search query |
| [get(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.get.md) | | Retrieves a single object |
| [update(type, id, attributes, options)](./kibana-plugin-core-server.savedobjectsclient.update.md) | | Updates an SavedObject |

View file

@ -0,0 +1,19 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsDeleteFromNamespacesOptions](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md)
## SavedObjectsDeleteFromNamespacesOptions interface
<b>Signature:</b>
```typescript
export interface SavedObjectsDeleteFromNamespacesOptions extends SavedObjectsBaseOptions
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [refresh](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md) | <code>MutatingOperationRefreshSetting</code> | The Elasticsearch Refresh setting for this operation |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsDeleteFromNamespacesOptions](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md) &gt; [refresh](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md)
## SavedObjectsDeleteFromNamespacesOptions.refresh property
The Elasticsearch Refresh setting for this operation
<b>Signature:</b>
```typescript
refresh?: MutatingOperationRefreshSetting;
```

View file

@ -0,0 +1,23 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) &gt; [createConflictError](./kibana-plugin-core-server.savedobjectserrorhelpers.createconflicterror.md)
## SavedObjectsErrorHelpers.createConflictError() method
<b>Signature:</b>
```typescript
static createConflictError(type: string, id: string): DecoratedError;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| type | <code>string</code> | |
| id | <code>string</code> | |
<b>Returns:</b>
`DecoratedError`

View file

@ -0,0 +1,23 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) &gt; [decorateEsCannotExecuteScriptError](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateescannotexecutescripterror.md)
## SavedObjectsErrorHelpers.decorateEsCannotExecuteScriptError() method
<b>Signature:</b>
```typescript
static decorateEsCannotExecuteScriptError(error: Error, reason?: string): DecoratedError;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| error | <code>Error</code> | |
| reason | <code>string</code> | |
<b>Returns:</b>
`DecoratedError`

View file

@ -0,0 +1,22 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) &gt; [isEsCannotExecuteScriptError](./kibana-plugin-core-server.savedobjectserrorhelpers.isescannotexecutescripterror.md)
## SavedObjectsErrorHelpers.isEsCannotExecuteScriptError() method
<b>Signature:</b>
```typescript
static isEsCannotExecuteScriptError(error: Error | DecoratedError): boolean;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| error | <code>Error &#124; DecoratedError</code> | |
<b>Returns:</b>
`boolean`

View file

@ -16,12 +16,14 @@ export declare class SavedObjectsErrorHelpers
| Method | Modifiers | Description |
| --- | --- | --- |
| [createBadRequestError(reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.createbadrequesterror.md) | <code>static</code> | |
| [createConflictError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.createconflicterror.md) | <code>static</code> | |
| [createEsAutoCreateIndexError()](./kibana-plugin-core-server.savedobjectserrorhelpers.createesautocreateindexerror.md) | <code>static</code> | |
| [createGenericNotFoundError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfounderror.md) | <code>static</code> | |
| [createInvalidVersionError(versionInput)](./kibana-plugin-core-server.savedobjectserrorhelpers.createinvalidversionerror.md) | <code>static</code> | |
| [createUnsupportedTypeError(type)](./kibana-plugin-core-server.savedobjectserrorhelpers.createunsupportedtypeerror.md) | <code>static</code> | |
| [decorateBadRequestError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decoratebadrequesterror.md) | <code>static</code> | |
| [decorateConflictError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateconflicterror.md) | <code>static</code> | |
| [decorateEsCannotExecuteScriptError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateescannotexecutescripterror.md) | <code>static</code> | |
| [decorateEsUnavailableError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateesunavailableerror.md) | <code>static</code> | |
| [decorateForbiddenError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateforbiddenerror.md) | <code>static</code> | |
| [decorateGeneralError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorategeneralerror.md) | <code>static</code> | |
@ -30,6 +32,7 @@ export declare class SavedObjectsErrorHelpers
| [isBadRequestError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isbadrequesterror.md) | <code>static</code> | |
| [isConflictError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isconflicterror.md) | <code>static</code> | |
| [isEsAutoCreateIndexError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isesautocreateindexerror.md) | <code>static</code> | |
| [isEsCannotExecuteScriptError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isescannotexecutescripterror.md) | <code>static</code> | |
| [isEsUnavailableError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isesunavailableerror.md) | <code>static</code> | |
| [isForbiddenError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isforbiddenerror.md) | <code>static</code> | |
| [isInvalidVersionError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isinvalidversionerror.md) | <code>static</code> | |

View file

@ -0,0 +1,15 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsNamespaceType](./kibana-plugin-core-server.savedobjectsnamespacetype.md)
## SavedObjectsNamespaceType type
The namespace type dictates how a saved object can be interacted in relation to namespaces. Each type is mutually exclusive: \* single (default): this type of saved object is namespace-isolated, e.g., it exists in only one namespace. \* multiple: this type of saved object is shareable, e.g., it can exist in one or more namespaces. \* agnostic: this type of saved object is global.
Note: do not write logic that uses this value directly; instead, use the appropriate accessors in the [type registry](./kibana-plugin-core-server.savedobjecttyperegistry.md)<!-- -->.
<b>Signature:</b>
```typescript
export declare type SavedObjectsNamespaceType = 'single' | 'multiple' | 'agnostic';
```

View file

@ -0,0 +1,27 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) &gt; [addToNamespaces](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md)
## SavedObjectsRepository.addToNamespaces() method
Adds one or more namespaces to a given multi-namespace saved object. This method and \[`deleteFromNamespaces`<!-- -->\][SavedObjectsRepository.deleteFromNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to.
<b>Signature:</b>
```typescript
addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise<{}>;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| type | <code>string</code> | |
| id | <code>string</code> | |
| namespaces | <code>string[]</code> | |
| options | <code>SavedObjectsAddToNamespacesOptions</code> | |
<b>Returns:</b>
`Promise<{}>`

View file

@ -0,0 +1,27 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) &gt; [deleteFromNamespaces](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md)
## SavedObjectsRepository.deleteFromNamespaces() method
Removes one or more namespaces from a given multi-namespace saved object. If no namespaces remain, the saved object is deleted entirely. This method and \[`addToNamespaces`<!-- -->\][SavedObjectsRepository.addToNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to.
<b>Signature:</b>
```typescript
deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<{}>;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| type | <code>string</code> | |
| id | <code>string</code> | |
| namespaces | <code>string[]</code> | |
| options | <code>SavedObjectsDeleteFromNamespacesOptions</code> | |
<b>Returns:</b>
`Promise<{}>`

View file

@ -15,12 +15,14 @@ export declare class SavedObjectsRepository
| Method | Modifiers | Description |
| --- | --- | --- |
| [addToNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) | | Adds one or more namespaces to a given multi-namespace saved object. This method and \[<code>deleteFromNamespaces</code>\][SavedObjectsRepository.deleteFromNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. |
| [bulkCreate(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.bulkcreate.md) | | Creates multiple documents at once |
| [bulkGet(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.bulkget.md) | | Returns an array of objects by id |
| [bulkUpdate(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.bulkupdate.md) | | Updates multiple objects in bulk |
| [create(type, attributes, options)](./kibana-plugin-core-server.savedobjectsrepository.create.md) | | Persists an object |
| [delete(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.delete.md) | | Deletes an object |
| [deleteByNamespace(namespace, options)](./kibana-plugin-core-server.savedobjectsrepository.deletebynamespace.md) | | Deletes all objects from the provided namespace. |
| [deleteFromNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) | | Removes one or more namespaces from a given multi-namespace saved object. If no namespaces remain, the saved object is deleted entirely. This method and \[<code>addToNamespaces</code>\][SavedObjectsRepository.addToNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. |
| [find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, })](./kibana-plugin-core-server.savedobjectsrepository.find.md) | | |
| [get(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.get.md) | | Gets a single object |
| [incrementCounter(type, id, counterFieldName, options)](./kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md) | | Increases a counter field by one. Creates the document if one doesn't exist for the given id. |

View file

@ -29,7 +29,7 @@ import * as migrations from './migrations';
export const myType: SavedObjectsType = {
name: 'MyType',
hidden: false,
namespaceAgnostic: true,
namespaceType: 'multiple',
mappings: {
properties: {
textField: {

View file

@ -25,5 +25,6 @@ This is only internal for now, and will only be public when we expose the regist
| [mappings](./kibana-plugin-core-server.savedobjectstype.mappings.md) | <code>SavedObjectsTypeMappingDefinition</code> | The [mapping definition](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) for the type. |
| [migrations](./kibana-plugin-core-server.savedobjectstype.migrations.md) | <code>SavedObjectMigrationMap</code> | An optional map of [migrations](./kibana-plugin-core-server.savedobjectmigrationfn.md) to be used to migrate the type. |
| [name](./kibana-plugin-core-server.savedobjectstype.name.md) | <code>string</code> | The name of the type, which is also used as the internal id. |
| [namespaceAgnostic](./kibana-plugin-core-server.savedobjectstype.namespaceagnostic.md) | <code>boolean</code> | Is the type global (true), or namespaced (false). |
| [namespaceAgnostic](./kibana-plugin-core-server.savedobjectstype.namespaceagnostic.md) | <code>boolean</code> | Is the type global (true), or not (false). |
| [namespaceType](./kibana-plugin-core-server.savedobjectstype.namespacetype.md) | <code>SavedObjectsNamespaceType</code> | The [namespace type](./kibana-plugin-core-server.savedobjectsnamespacetype.md) for the type. |

View file

@ -4,10 +4,15 @@
## SavedObjectsType.namespaceAgnostic property
Is the type global (true), or namespaced (false).
> Warning: This API is now obsolete.
>
> Use `namespaceType` instead.
>
Is the type global (true), or not (false).
<b>Signature:</b>
```typescript
namespaceAgnostic: boolean;
namespaceAgnostic?: boolean;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsType](./kibana-plugin-core-server.savedobjectstype.md) &gt; [namespaceType](./kibana-plugin-core-server.savedobjectstype.namespacetype.md)
## SavedObjectsType.namespaceType property
The [namespace type](./kibana-plugin-core-server.savedobjectsnamespacetype.md) for the type.
<b>Signature:</b>
```typescript
namespaceType?: SavedObjectsNamespaceType;
```

View file

@ -0,0 +1,24 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectTypeRegistry](./kibana-plugin-core-server.savedobjecttyperegistry.md) &gt; [isMultiNamespace](./kibana-plugin-core-server.savedobjecttyperegistry.ismultinamespace.md)
## SavedObjectTypeRegistry.isMultiNamespace() method
Returns whether the type is multi-namespace (shareable); resolves to `false` if the type is not registered
<b>Signature:</b>
```typescript
isMultiNamespace(type: string): boolean;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| type | <code>string</code> | |
<b>Returns:</b>
`boolean`

View file

@ -4,7 +4,7 @@
## SavedObjectTypeRegistry.isNamespaceAgnostic() method
Returns the `namespaceAgnostic` property for given type, or `false` if the type is not registered.
Returns whether the type is namespace-agnostic (global); resolves to `false` if the type is not registered
<b>Signature:</b>

View file

@ -0,0 +1,24 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectTypeRegistry](./kibana-plugin-core-server.savedobjecttyperegistry.md) &gt; [isSingleNamespace](./kibana-plugin-core-server.savedobjecttyperegistry.issinglenamespace.md)
## SavedObjectTypeRegistry.isSingleNamespace() method
Returns whether the type is single-namespace (isolated); resolves to `true` if the type is not registered
<b>Signature:</b>
```typescript
isSingleNamespace(type: string): boolean;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| type | <code>string</code> | |
<b>Returns:</b>
`boolean`

View file

@ -22,6 +22,8 @@ export declare class SavedObjectTypeRegistry
| [getType(type)](./kibana-plugin-core-server.savedobjecttyperegistry.gettype.md) | | Return the [type](./kibana-plugin-core-server.savedobjectstype.md) definition for given type name. |
| [isHidden(type)](./kibana-plugin-core-server.savedobjecttyperegistry.ishidden.md) | | Returns the <code>hidden</code> property for given type, or <code>false</code> if the type is not registered. |
| [isImportableAndExportable(type)](./kibana-plugin-core-server.savedobjecttyperegistry.isimportableandexportable.md) | | Returns the <code>management.importableAndExportable</code> property for given type, or <code>false</code> if the type is not registered or does not define a management section. |
| [isNamespaceAgnostic(type)](./kibana-plugin-core-server.savedobjecttyperegistry.isnamespaceagnostic.md) | | Returns the <code>namespaceAgnostic</code> property for given type, or <code>false</code> if the type is not registered. |
| [isMultiNamespace(type)](./kibana-plugin-core-server.savedobjecttyperegistry.ismultinamespace.md) | | Returns whether the type is multi-namespace (shareable); resolves to <code>false</code> if the type is not registered |
| [isNamespaceAgnostic(type)](./kibana-plugin-core-server.savedobjecttyperegistry.isnamespaceagnostic.md) | | Returns whether the type is namespace-agnostic (global); resolves to <code>false</code> if the type is not registered |
| [isSingleNamespace(type)](./kibana-plugin-core-server.savedobjecttyperegistry.issinglenamespace.md) | | Returns whether the type is single-namespace (isolated); resolves to <code>true</code> if the type is not registered |
| [registerType(type)](./kibana-plugin-core-server.savedobjecttyperegistry.registertype.md) | | Register a [type](./kibana-plugin-core-server.savedobjectstype.md) inside the registry. A type can only be registered once. subsequent calls with the same type name will throw an error. |

View file

@ -956,6 +956,7 @@ export interface SavedObject<T = unknown> {
};
id: string;
migrationVersion?: SavedObjectsMigrationVersion;
namespaces?: string[];
references: SavedObjectReference[];
type: string;
updated_at?: string;

View file

@ -227,6 +227,8 @@ export {
SavedObjectsLegacyService,
SavedObjectsUpdateOptions,
SavedObjectsUpdateResponse,
SavedObjectsAddToNamespacesOptions,
SavedObjectsDeleteFromNamespacesOptions,
SavedObjectsServiceStart,
SavedObjectsServiceSetup,
SavedObjectStatusMeta,
@ -242,6 +244,7 @@ export {
SavedObjectsMappingProperties,
SavedObjectTypeRegistry,
ISavedObjectTypeRegistry,
SavedObjectsNamespaceType,
SavedObjectsType,
SavedObjectsTypeManagementDefinition,
SavedObjectMigrationMap,

View file

@ -16,7 +16,7 @@ Array [
},
"migrations": Object {},
"name": "typeA",
"namespaceAgnostic": false,
"namespaceType": "single",
},
Object {
"convertToAliasScript": undefined,
@ -32,7 +32,7 @@ Array [
},
"migrations": Object {},
"name": "typeB",
"namespaceAgnostic": false,
"namespaceType": "single",
},
Object {
"convertToAliasScript": undefined,
@ -48,7 +48,7 @@ Array [
},
"migrations": Object {},
"name": "typeC",
"namespaceAgnostic": false,
"namespaceType": "single",
},
]
`;
@ -72,7 +72,7 @@ Array [
"2.0.4": [Function],
},
"name": "typeA",
"namespaceAgnostic": true,
"namespaceType": "agnostic",
},
Object {
"convertToAliasScript": "some alias script",
@ -91,7 +91,7 @@ Array [
},
"migrations": Object {},
"name": "typeB",
"namespaceAgnostic": false,
"namespaceType": "single",
},
Object {
"convertToAliasScript": undefined,
@ -109,7 +109,7 @@ Array [
"1.5.3": [Function],
},
"name": "typeC",
"namespaceAgnostic": false,
"namespaceType": "single",
},
]
`;
@ -130,7 +130,23 @@ Array [
},
"migrations": Object {},
"name": "typeA",
"namespaceAgnostic": true,
"namespaceType": "agnostic",
},
Object {
"convertToAliasScript": undefined,
"hidden": false,
"indexPattern": "barBaz",
"management": undefined,
"mappings": Object {
"properties": Object {
"fieldB": Object {
"type": "text",
},
},
},
"migrations": Object {},
"name": "typeB",
"namespaceType": "multiple",
},
Object {
"convertToAliasScript": undefined,
@ -146,7 +162,23 @@ Array [
},
"migrations": Object {},
"name": "typeC",
"namespaceAgnostic": false,
"namespaceType": "single",
},
Object {
"convertToAliasScript": undefined,
"hidden": false,
"indexPattern": "bazQux",
"management": undefined,
"mappings": Object {
"properties": Object {
"fieldD": Object {
"type": "text",
},
},
},
"migrations": Object {},
"name": "typeD",
"namespaceType": "agnostic",
},
]
`;

View file

@ -69,6 +69,7 @@ export {
} from './migrations';
export {
SavedObjectsNamespaceType,
SavedObjectStatusMeta,
SavedObjectsType,
SavedObjectsTypeManagementDefinition,

View file

@ -8,6 +8,7 @@ Object {
"bbb": "18c78c995965207ed3f6e7fc5c6e55fe",
"migrationVersion": "4a1746014a75ade3a714e1db5763276f",
"namespace": "2f4316de49999235636386fe51dc06c1",
"namespaces": "2f4316de49999235636386fe51dc06c1",
"references": "7997cf5a56cc02bdc9c93361bde732b0",
"type": "2f4316de49999235636386fe51dc06c1",
"updated_at": "00da57df13e94e9d98437d13ace4bfe0",
@ -28,6 +29,9 @@ Object {
"namespace": Object {
"type": "keyword",
},
"namespaces": Object {
"type": "keyword",
},
"references": Object {
"properties": Object {
"id": Object {
@ -59,6 +63,7 @@ Object {
"firstType": "635418ab953d81d93f1190b70a8d3f57",
"migrationVersion": "4a1746014a75ade3a714e1db5763276f",
"namespace": "2f4316de49999235636386fe51dc06c1",
"namespaces": "2f4316de49999235636386fe51dc06c1",
"references": "7997cf5a56cc02bdc9c93361bde732b0",
"secondType": "72d57924f415fbadb3ee293b67d233ab",
"thirdType": "510f1f0adb69830cf8a1c5ce2923ed82",
@ -83,6 +88,9 @@ Object {
"namespace": Object {
"type": "keyword",
},
"namespaces": Object {
"type": "keyword",
},
"references": Object {
"properties": Object {
"id": Object {

View file

@ -142,6 +142,9 @@ function defaultMapping(): IndexMapping {
namespace: {
type: 'keyword',
},
namespaces: {
type: 'keyword',
},
updated_at: {
type: 'date',
},

View file

@ -61,6 +61,7 @@ describe('IndexMigrator', () => {
foo: '18c78c995965207ed3f6e7fc5c6e55fe',
migrationVersion: '4a1746014a75ade3a714e1db5763276f',
namespace: '2f4316de49999235636386fe51dc06c1',
namespaces: '2f4316de49999235636386fe51dc06c1',
references: '7997cf5a56cc02bdc9c93361bde732b0',
type: '2f4316de49999235636386fe51dc06c1',
updated_at: '00da57df13e94e9d98437d13ace4bfe0',
@ -70,6 +71,7 @@ describe('IndexMigrator', () => {
foo: { type: 'long' },
migrationVersion: { dynamic: 'true', type: 'object' },
namespace: { type: 'keyword' },
namespaces: { type: 'keyword' },
type: { type: 'keyword' },
updated_at: { type: 'date' },
references: {
@ -178,6 +180,7 @@ describe('IndexMigrator', () => {
foo: '625b32086eb1d1203564cf85062dd22e',
migrationVersion: '4a1746014a75ade3a714e1db5763276f',
namespace: '2f4316de49999235636386fe51dc06c1',
namespaces: '2f4316de49999235636386fe51dc06c1',
references: '7997cf5a56cc02bdc9c93361bde732b0',
type: '2f4316de49999235636386fe51dc06c1',
updated_at: '00da57df13e94e9d98437d13ace4bfe0',
@ -188,6 +191,7 @@ describe('IndexMigrator', () => {
foo: { type: 'text' },
migrationVersion: { dynamic: 'true', type: 'object' },
namespace: { type: 'keyword' },
namespaces: { type: 'keyword' },
type: { type: 'keyword' },
updated_at: { type: 'date' },
references: {

View file

@ -8,6 +8,7 @@ Object {
"bmap": "510f1f0adb69830cf8a1c5ce2923ed82",
"migrationVersion": "4a1746014a75ade3a714e1db5763276f",
"namespace": "2f4316de49999235636386fe51dc06c1",
"namespaces": "2f4316de49999235636386fe51dc06c1",
"references": "7997cf5a56cc02bdc9c93361bde732b0",
"type": "2f4316de49999235636386fe51dc06c1",
"updated_at": "00da57df13e94e9d98437d13ace4bfe0",
@ -36,6 +37,9 @@ Object {
"namespace": Object {
"type": "keyword",
},
"namespaces": Object {
"type": "keyword",
},
"references": Object {
"properties": Object {
"id": Object {

View file

@ -187,7 +187,7 @@ describe('POST /internal/saved_objects/_import', () => {
references: [],
error: {
statusCode: 409,
message: 'version conflict, document already exists',
message: 'Saved object [index-pattern/my-pattern] conflict',
},
},
{

View file

@ -138,7 +138,7 @@ describe('SavedObjectsService', () => {
const type = {
name: 'someType',
hidden: false,
namespaceAgnostic: false,
namespaceType: 'single' as 'single',
mappings: { properties: {} },
};
setup.registerType(type);
@ -251,7 +251,7 @@ describe('SavedObjectsService', () => {
setup.registerType({
name: 'someType',
hidden: false,
namespaceAgnostic: false,
namespaceType: 'single' as 'single',
mappings: { properties: {} },
});
}).toThrowErrorMatchingInlineSnapshot(

View file

@ -124,7 +124,7 @@ export interface SavedObjectsServiceSetup {
* export const myType: SavedObjectsType = {
* name: 'MyType',
* hidden: false,
* namespaceAgnostic: true,
* namespaceType: 'multiple',
* mappings: {
* properties: {
* textField: {

View file

@ -27,6 +27,8 @@ const createRegistryMock = (): jest.Mocked<ISavedObjectTypeRegistry &
getAllTypes: jest.fn(),
getImportableAndExportableTypes: jest.fn(),
isNamespaceAgnostic: jest.fn(),
isSingleNamespace: jest.fn(),
isMultiNamespace: jest.fn(),
isHidden: jest.fn(),
getIndex: jest.fn(),
isImportableAndExportable: jest.fn(),
@ -38,6 +40,10 @@ const createRegistryMock = (): jest.Mocked<ISavedObjectTypeRegistry &
mock.getIndex.mockReturnValue('.kibana-test');
mock.isHidden.mockReturnValue(false);
mock.isNamespaceAgnostic.mockImplementation((type: string) => type === 'global');
mock.isSingleNamespace.mockImplementation(
(type: string) => type !== 'global' && type !== 'shared'
);
mock.isMultiNamespace.mockImplementation((type: string) => type === 'shared');
mock.isImportableAndExportable.mockReturnValue(true);
return mock;

View file

@ -23,7 +23,7 @@ import { SavedObjectsType } from './types';
const createType = (type: Partial<SavedObjectsType>): SavedObjectsType => ({
name: 'unknown',
hidden: false,
namespaceAgnostic: false,
namespaceType: 'single' as 'single',
mappings: { properties: {} },
migrations: {},
...type,
@ -164,19 +164,93 @@ describe('SavedObjectTypeRegistry', () => {
});
describe('#isNamespaceAgnostic', () => {
it('returns correct value for the type', () => {
registry.registerType(createType({ name: 'typeA', namespaceAgnostic: true }));
registry.registerType(createType({ name: 'typeB', namespaceAgnostic: false }));
expect(registry.isNamespaceAgnostic('typeA')).toEqual(true);
expect(registry.isNamespaceAgnostic('typeB')).toEqual(false);
});
it('returns false when the type is not registered', () => {
registry.registerType(createType({ name: 'typeA', namespaceAgnostic: true }));
registry.registerType(createType({ name: 'typeB', namespaceAgnostic: false }));
const expectResult = (expected: boolean, schemaDefinition?: Partial<SavedObjectsType>) => {
registry = new SavedObjectTypeRegistry();
registry.registerType(createType({ name: 'foo', ...schemaDefinition }));
expect(registry.isNamespaceAgnostic('foo')).toBe(expected);
};
it(`returns false when the type is not registered`, () => {
expect(registry.isNamespaceAgnostic('unknownType')).toEqual(false);
});
it(`returns true for namespaceType 'agnostic'`, () => {
expectResult(true, { namespaceType: 'agnostic' });
});
it(`returns false for other namespaceType`, () => {
expectResult(false, { namespaceType: 'multiple' });
expectResult(false, { namespaceType: 'single' });
expectResult(false, { namespaceType: undefined });
});
// deprecated test cases
it(`returns true when namespaceAgnostic is true`, () => {
expectResult(true, { namespaceAgnostic: true, namespaceType: 'agnostic' });
expectResult(true, { namespaceAgnostic: true, namespaceType: 'multiple' });
expectResult(true, { namespaceAgnostic: true, namespaceType: 'single' });
expectResult(true, { namespaceAgnostic: true, namespaceType: undefined });
});
});
describe('#isSingleNamespace', () => {
const expectResult = (expected: boolean, schemaDefinition?: Partial<SavedObjectsType>) => {
registry = new SavedObjectTypeRegistry();
registry.registerType(createType({ name: 'foo', ...schemaDefinition }));
expect(registry.isSingleNamespace('foo')).toBe(expected);
};
it(`returns true when the type is not registered`, () => {
expect(registry.isSingleNamespace('unknownType')).toEqual(true);
});
it(`returns true for namespaceType 'single'`, () => {
expectResult(true, { namespaceType: 'single' });
expectResult(true, { namespaceType: undefined });
});
it(`returns false for other namespaceType`, () => {
expectResult(false, { namespaceType: 'agnostic' });
expectResult(false, { namespaceType: 'multiple' });
});
// deprecated test cases
it(`returns false when namespaceAgnostic is true`, () => {
expectResult(false, { namespaceAgnostic: true, namespaceType: 'agnostic' });
expectResult(false, { namespaceAgnostic: true, namespaceType: 'multiple' });
expectResult(false, { namespaceAgnostic: true, namespaceType: 'single' });
expectResult(false, { namespaceAgnostic: true, namespaceType: undefined });
});
});
describe('#isMultiNamespace', () => {
const expectResult = (expected: boolean, schemaDefinition?: Partial<SavedObjectsType>) => {
registry = new SavedObjectTypeRegistry();
registry.registerType(createType({ name: 'foo', ...schemaDefinition }));
expect(registry.isMultiNamespace('foo')).toBe(expected);
};
it(`returns false when the type is not registered`, () => {
expect(registry.isMultiNamespace('unknownType')).toEqual(false);
});
it(`returns true for namespaceType 'multiple'`, () => {
expectResult(true, { namespaceType: 'multiple' });
});
it(`returns false for other namespaceType`, () => {
expectResult(false, { namespaceType: 'agnostic' });
expectResult(false, { namespaceType: 'single' });
expectResult(false, { namespaceType: undefined });
});
// deprecated test cases
it(`returns false when namespaceAgnostic is true`, () => {
expectResult(false, { namespaceAgnostic: true, namespaceType: 'agnostic' });
expectResult(false, { namespaceAgnostic: true, namespaceType: 'multiple' });
expectResult(false, { namespaceAgnostic: true, namespaceType: 'single' });
expectResult(false, { namespaceAgnostic: true, namespaceType: undefined });
});
});
describe('#isHidden', () => {
@ -206,8 +280,8 @@ describe('SavedObjectTypeRegistry', () => {
expect(registry.getIndex('typeWithNoIndex')).toBeUndefined();
});
it('returns undefined when the type is not registered', () => {
registry.registerType(createType({ name: 'typeA', namespaceAgnostic: true }));
registry.registerType(createType({ name: 'typeB', namespaceAgnostic: false }));
registry.registerType(createType({ name: 'typeA', namespaceType: 'agnostic' }));
registry.registerType(createType({ name: 'typeB', namespaceType: 'single' }));
expect(registry.getIndex('unknownType')).toBeUndefined();
});

View file

@ -25,16 +25,7 @@ import { SavedObjectsType } from './types';
*
* @public
*/
export type ISavedObjectTypeRegistry = Pick<
SavedObjectTypeRegistry,
| 'getType'
| 'getAllTypes'
| 'getIndex'
| 'isNamespaceAgnostic'
| 'isHidden'
| 'getImportableAndExportableTypes'
| 'isImportableAndExportable'
>;
export type ISavedObjectTypeRegistry = Omit<SavedObjectTypeRegistry, 'registerType'>;
/**
* Registry holding information about all the registered {@link SavedObjectsType | saved object types}.
@ -77,11 +68,31 @@ export class SavedObjectTypeRegistry {
}
/**
* Returns the `namespaceAgnostic` property for given type, or `false` if
* the type is not registered.
* Returns whether the type is namespace-agnostic (global);
* resolves to `false` if the type is not registered
*/
public isNamespaceAgnostic(type: string) {
return this.types.get(type)?.namespaceAgnostic ?? false;
return (
this.types.get(type)?.namespaceType === 'agnostic' ||
this.types.get(type)?.namespaceAgnostic ||
false
);
}
/**
* Returns whether the type is single-namespace (isolated);
* resolves to `true` if the type is not registered
*/
public isSingleNamespace(type: string) {
return !this.isNamespaceAgnostic(type) && !this.isMultiNamespace(type);
}
/**
* Returns whether the type is multi-namespace (shareable);
* resolves to `false` if the type is not registered
*/
public isMultiNamespace(type: string) {
return !this.isNamespaceAgnostic(type) && this.types.get(type)?.namespaceType === 'multiple';
}
/**

View file

@ -17,32 +17,90 @@
* under the License.
*/
import { SavedObjectsSchema } from './schema';
import { SavedObjectsSchema, SavedObjectsSchemaDefinition } from './schema';
describe('#isNamespaceAgnostic', () => {
const expectResult = (expected: boolean, schemaDefinition?: SavedObjectsSchemaDefinition) => {
const schema = new SavedObjectsSchema(schemaDefinition);
const result = schema.isNamespaceAgnostic('foo');
expect(result).toBe(expected);
};
it(`returns false when no schema is defined`, () => {
expectResult(false);
});
it(`returns false for unknown types`, () => {
const schema = new SavedObjectsSchema();
const result = schema.isNamespaceAgnostic('bar');
expect(result).toBe(false);
expectResult(false, { bar: {} });
});
it(`returns true for explicitly namespace agnostic type`, () => {
const schema = new SavedObjectsSchema({
foo: {
isNamespaceAgnostic: true,
},
});
const result = schema.isNamespaceAgnostic('foo');
expect(result).toBe(true);
it(`returns false for non-namespace-agnostic type`, () => {
expectResult(false, { foo: { isNamespaceAgnostic: false } });
expectResult(false, { foo: { isNamespaceAgnostic: undefined } });
});
it(`returns false for explicitly namespaced type`, () => {
const schema = new SavedObjectsSchema({
foo: {
isNamespaceAgnostic: false,
},
});
const result = schema.isNamespaceAgnostic('foo');
expect(result).toBe(false);
it(`returns true for explicitly namespace-agnostic type`, () => {
expectResult(true, { foo: { isNamespaceAgnostic: true } });
});
});
describe('#isSingleNamespace', () => {
const expectResult = (expected: boolean, schemaDefinition?: SavedObjectsSchemaDefinition) => {
const schema = new SavedObjectsSchema(schemaDefinition);
const result = schema.isSingleNamespace('foo');
expect(result).toBe(expected);
};
it(`returns true when no schema is defined`, () => {
expectResult(true);
});
it(`returns true for unknown types`, () => {
expectResult(true, { bar: {} });
});
it(`returns false for explicitly namespace-agnostic type`, () => {
expectResult(false, { foo: { isNamespaceAgnostic: true } });
});
it(`returns false for explicitly multi-namespace type`, () => {
expectResult(false, { foo: { multiNamespace: true } });
});
it(`returns true for non-namespace-agnostic and non-multi-namespace type`, () => {
expectResult(true, { foo: { isNamespaceAgnostic: false, multiNamespace: false } });
expectResult(true, { foo: { isNamespaceAgnostic: false, multiNamespace: undefined } });
expectResult(true, { foo: { isNamespaceAgnostic: undefined, multiNamespace: false } });
expectResult(true, { foo: { isNamespaceAgnostic: undefined, multiNamespace: undefined } });
});
});
describe('#isMultiNamespace', () => {
const expectResult = (expected: boolean, schemaDefinition?: SavedObjectsSchemaDefinition) => {
const schema = new SavedObjectsSchema(schemaDefinition);
const result = schema.isMultiNamespace('foo');
expect(result).toBe(expected);
};
it(`returns false when no schema is defined`, () => {
expectResult(false);
});
it(`returns false for unknown types`, () => {
expectResult(false, { bar: {} });
});
it(`returns false for explicitly namespace-agnostic type`, () => {
expectResult(false, { foo: { isNamespaceAgnostic: true } });
});
it(`returns false for non-multi-namespace type`, () => {
expectResult(false, { foo: { multiNamespace: false } });
expectResult(false, { foo: { multiNamespace: undefined } });
});
it(`returns true for non-namespace-agnostic and explicitly multi-namespace type`, () => {
expectResult(true, { foo: { isNamespaceAgnostic: false, multiNamespace: true } });
expectResult(true, { foo: { isNamespaceAgnostic: undefined, multiNamespace: true } });
});
});

View file

@ -24,7 +24,8 @@ import { LegacyConfig } from '../../legacy';
* @internal
**/
interface SavedObjectsSchemaTypeDefinition {
isNamespaceAgnostic: boolean;
isNamespaceAgnostic?: boolean;
multiNamespace?: boolean;
hidden?: boolean;
indexPattern?: ((config: LegacyConfig) => string) | string;
convertToAliasScript?: string;
@ -72,7 +73,7 @@ export class SavedObjectsSchema {
}
public isNamespaceAgnostic(type: string) {
// if no plugins have registered a uiExports.savedObjectSchemas,
// if no plugins have registered a Saved Objects Schema,
// this.schema will be undefined, and no types are namespace agnostic
if (!this.definition) {
return false;
@ -84,4 +85,32 @@ export class SavedObjectsSchema {
}
return Boolean(typeSchema.isNamespaceAgnostic);
}
public isSingleNamespace(type: string) {
// if no plugins have registered a Saved Objects Schema,
// this.schema will be undefined, and all types are namespace isolated
if (!this.definition) {
return true;
}
const typeSchema = this.definition[type];
if (!typeSchema) {
return true;
}
return !Boolean(typeSchema.isNamespaceAgnostic) && !Boolean(typeSchema.multiNamespace);
}
public isMultiNamespace(type: string) {
// if no plugins have registered a Saved Objects Schema,
// this.schema will be undefined, and no types are multi-namespace
if (!this.definition) {
return false;
}
const typeSchema = this.definition[type];
if (!typeSchema) {
return false;
}
return !Boolean(typeSchema.isNamespaceAgnostic) && Boolean(typeSchema.multiNamespace);
}
}

File diff suppressed because it is too large Load diff

View file

@ -49,7 +49,7 @@ export class SavedObjectsSerializer {
public isRawSavedObject(rawDoc: SavedObjectsRawDoc) {
const { type, namespace } = rawDoc._source;
const namespacePrefix =
namespace && !this.registry.isNamespaceAgnostic(type) ? `${namespace}:` : '';
namespace && this.registry.isSingleNamespace(type) ? `${namespace}:` : '';
return Boolean(
type &&
rawDoc._id.startsWith(`${namespacePrefix}${type}:`) &&
@ -64,7 +64,7 @@ export class SavedObjectsSerializer {
*/
public rawToSavedObject(doc: SavedObjectsRawDoc): SavedObjectSanitizedDoc {
const { _id, _source, _seq_no, _primary_term } = doc;
const { type, namespace } = _source;
const { type, namespace, namespaces } = _source;
const version =
_seq_no != null || _primary_term != null
@ -74,7 +74,8 @@ export class SavedObjectsSerializer {
return {
type,
id: this.trimIdPrefix(namespace, type, _id),
...(namespace && !this.registry.isNamespaceAgnostic(type) && { namespace }),
...(namespace && this.registry.isSingleNamespace(type) && { namespace }),
...(namespaces && this.registry.isMultiNamespace(type) && { namespaces }),
attributes: _source[type],
references: _source.references || [],
...(_source.migrationVersion && { migrationVersion: _source.migrationVersion }),
@ -93,6 +94,7 @@ export class SavedObjectsSerializer {
id,
type,
namespace,
namespaces,
attributes,
migrationVersion,
updated_at,
@ -103,7 +105,8 @@ export class SavedObjectsSerializer {
[type]: attributes,
type,
references,
...(namespace && !this.registry.isNamespaceAgnostic(type) && { namespace }),
...(namespace && this.registry.isSingleNamespace(type) && { namespace }),
...(namespaces && this.registry.isMultiNamespace(type) && { namespaces }),
...(migrationVersion && { migrationVersion }),
...(updated_at && { updated_at }),
};
@ -124,7 +127,7 @@ export class SavedObjectsSerializer {
*/
public generateRawId(namespace: string | undefined, type: string, id?: string) {
const namespacePrefix =
namespace && !this.registry.isNamespaceAgnostic(type) ? `${namespace}:` : '';
namespace && this.registry.isSingleNamespace(type) ? `${namespace}:` : '';
return `${namespacePrefix}${type}:${id || uuid.v1()}`;
}
@ -133,7 +136,7 @@ export class SavedObjectsSerializer {
assertNonEmptyString(type, 'saved object type');
const namespacePrefix =
namespace && !this.registry.isNamespaceAgnostic(type) ? `${namespace}:` : '';
namespace && this.registry.isSingleNamespace(type) ? `${namespace}:` : '';
const prefix = `${namespacePrefix}${type}:`;
if (!id.startsWith(prefix)) {

View file

@ -36,6 +36,7 @@ export interface SavedObjectsRawDoc {
export interface SavedObjectsRawDocSource {
type: string;
namespace?: string;
namespaces?: string[];
migrationVersion?: SavedObjectsMigrationVersion;
updated_at?: string;
references?: SavedObjectReference[];
@ -54,6 +55,7 @@ interface SavedObjectDoc {
id?: string; // NOTE: SavedObjectDoc is used for uncreated objects where `id` is optional
type: string;
namespace?: string;
namespaces?: string[];
migrationVersion?: SavedObjectsMigrationVersion;
version?: string;
updated_at?: string;

View file

@ -1,5 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SavedObjectsRepository #deleteByNamespace requires namespace to be a string 1`] = `"namespace is required, and must be a string"`;
exports[`SavedObjectsRepository #deleteByNamespace requires namespace to be defined 1`] = `"namespace is required, and must be a string"`;

View file

@ -100,6 +100,38 @@ describe('savedObjectsClient/decorateEsError', () => {
expect(SavedObjectsErrorHelpers.isBadRequestError(error)).toBe(true);
});
describe('when es.BadRequest has a reason', () => {
it('makes a SavedObjectsClient/esCannotExecuteScriptError error when script context is disabled', () => {
const error = new esErrors.BadRequest();
(error as Record<string, any>).body = {
error: { reason: 'cannot execute scripts using [update] context' },
};
expect(SavedObjectsErrorHelpers.isEsCannotExecuteScriptError(error)).toBe(false);
expect(decorateEsError(error)).toBe(error);
expect(SavedObjectsErrorHelpers.isEsCannotExecuteScriptError(error)).toBe(true);
expect(SavedObjectsErrorHelpers.isBadRequestError(error)).toBe(false);
});
it('makes a SavedObjectsClient/esCannotExecuteScriptError error when inline scripts are disabled', () => {
const error = new esErrors.BadRequest();
(error as Record<string, any>).body = {
error: { reason: 'cannot execute [inline] scripts' },
};
expect(SavedObjectsErrorHelpers.isEsCannotExecuteScriptError(error)).toBe(false);
expect(decorateEsError(error)).toBe(error);
expect(SavedObjectsErrorHelpers.isEsCannotExecuteScriptError(error)).toBe(true);
expect(SavedObjectsErrorHelpers.isBadRequestError(error)).toBe(false);
});
it('makes a SavedObjectsClient/BadRequest error for any other reason', () => {
const error = new esErrors.BadRequest();
(error as Record<string, any>).body = { error: { reason: 'some other reason' } };
expect(SavedObjectsErrorHelpers.isBadRequestError(error)).toBe(false);
expect(decorateEsError(error)).toBe(error);
expect(SavedObjectsErrorHelpers.isBadRequestError(error)).toBe(true);
});
});
it('returns other errors as Boom errors', () => {
const error = new Error();
expect(error).not.toHaveProperty('isBoom');

View file

@ -35,6 +35,8 @@ const {
NotFound,
BadRequest,
} = legacyElasticsearch.errors;
const SCRIPT_CONTEXT_DISABLED_REGEX = /(?:cannot execute scripts using \[)([a-z]*)(?:\] context)/;
const INLINE_SCRIPTS_DISABLED_MESSAGE = 'cannot execute [inline] scripts';
import { SavedObjectsErrorHelpers } from './errors';
@ -43,7 +45,7 @@ export function decorateEsError(error: Error) {
throw new Error('Expected an instance of Error');
}
const { reason } = get(error, 'body.error', { reason: undefined });
const { reason } = get(error, 'body.error', { reason: undefined }) as { reason?: string };
if (
error instanceof ConnectionFault ||
error instanceof ServiceUnavailable ||
@ -74,6 +76,12 @@ export function decorateEsError(error: Error) {
}
if (error instanceof BadRequest) {
if (
SCRIPT_CONTEXT_DISABLED_REGEX.test(reason || '') ||
reason === INLINE_SCRIPTS_DISABLED_MESSAGE
) {
return SavedObjectsErrorHelpers.decorateEsCannotExecuteScriptError(error, reason);
}
return SavedObjectsErrorHelpers.decorateBadRequestError(error, reason);
}

View file

@ -34,6 +34,7 @@ describe('savedObjectsClient/errorTypes', () => {
});
it('has boom properties', () => {
expect(errorObj).toHaveProperty('isBoom', true);
expect(errorObj.output.payload).toMatchObject({
statusCode: 400,
message: "Unsupported saved object type: 'someType': Bad Request",
@ -57,6 +58,7 @@ describe('savedObjectsClient/errorTypes', () => {
});
it('has boom properties', () => {
expect(errorObj).toHaveProperty('isBoom', true);
expect(errorObj.output.payload).toMatchObject({
statusCode: 400,
message: 'test reason message: Bad Request',
@ -80,14 +82,7 @@ describe('savedObjectsClient/errorTypes', () => {
it('adds boom properties', () => {
const error = SavedObjectsErrorHelpers.decorateBadRequestError(new Error());
expect(typeof error.output).toBe('object');
expect(error.output.statusCode).toBe(400);
});
it('preserves boom properties of input', () => {
const error = Boom.notFound();
SavedObjectsErrorHelpers.decorateBadRequestError(error);
expect(error.output.statusCode).toBe(404);
expect(error).toHaveProperty('isBoom', true);
});
describe('error.output', () => {
@ -95,6 +90,7 @@ describe('savedObjectsClient/errorTypes', () => {
const error = SavedObjectsErrorHelpers.decorateBadRequestError(new Error('foobar'));
expect(error.output.payload).toHaveProperty('message', 'foobar');
});
it('prefixes message with passed reason', () => {
const error = SavedObjectsErrorHelpers.decorateBadRequestError(
new Error('foobar'),
@ -102,13 +98,21 @@ describe('savedObjectsClient/errorTypes', () => {
);
expect(error.output.payload).toHaveProperty('message', 'biz: foobar');
});
it('sets statusCode to 400', () => {
const error = SavedObjectsErrorHelpers.decorateBadRequestError(new Error('foo'));
expect(error.output).toHaveProperty('statusCode', 400);
});
it('preserves boom properties of input', () => {
const error = Boom.notFound();
SavedObjectsErrorHelpers.decorateBadRequestError(error);
expect(error.output).toHaveProperty('statusCode', 404);
});
});
});
});
describe('NotAuthorized error', () => {
describe('decorateNotAuthorizedError', () => {
it('returns original object', () => {
@ -125,14 +129,7 @@ describe('savedObjectsClient/errorTypes', () => {
it('adds boom properties', () => {
const error = SavedObjectsErrorHelpers.decorateNotAuthorizedError(new Error());
expect(typeof error.output).toBe('object');
expect(error.output.statusCode).toBe(401);
});
it('preserves boom properties of input', () => {
const error = Boom.notFound();
SavedObjectsErrorHelpers.decorateNotAuthorizedError(error);
expect(error.output.statusCode).toBe(404);
expect(error).toHaveProperty('isBoom', true);
});
describe('error.output', () => {
@ -140,6 +137,7 @@ describe('savedObjectsClient/errorTypes', () => {
const error = SavedObjectsErrorHelpers.decorateNotAuthorizedError(new Error('foobar'));
expect(error.output.payload).toHaveProperty('message', 'foobar');
});
it('prefixes message with passed reason', () => {
const error = SavedObjectsErrorHelpers.decorateNotAuthorizedError(
new Error('foobar'),
@ -147,13 +145,21 @@ describe('savedObjectsClient/errorTypes', () => {
);
expect(error.output.payload).toHaveProperty('message', 'biz: foobar');
});
it('sets statusCode to 401', () => {
const error = SavedObjectsErrorHelpers.decorateNotAuthorizedError(new Error('foo'));
expect(error.output).toHaveProperty('statusCode', 401);
});
it('preserves boom properties of input', () => {
const error = Boom.notFound();
SavedObjectsErrorHelpers.decorateNotAuthorizedError(error);
expect(error.output).toHaveProperty('statusCode', 404);
});
});
});
});
describe('Forbidden error', () => {
describe('decorateForbiddenError', () => {
it('returns original object', () => {
@ -170,14 +176,7 @@ describe('savedObjectsClient/errorTypes', () => {
it('adds boom properties', () => {
const error = SavedObjectsErrorHelpers.decorateForbiddenError(new Error());
expect(typeof error.output).toBe('object');
expect(error.output.statusCode).toBe(403);
});
it('preserves boom properties of input', () => {
const error = Boom.notFound();
SavedObjectsErrorHelpers.decorateForbiddenError(error);
expect(error.output.statusCode).toBe(404);
expect(error).toHaveProperty('isBoom', true);
});
describe('error.output', () => {
@ -185,17 +184,26 @@ describe('savedObjectsClient/errorTypes', () => {
const error = SavedObjectsErrorHelpers.decorateForbiddenError(new Error('foobar'));
expect(error.output.payload).toHaveProperty('message', 'foobar');
});
it('prefixes message with passed reason', () => {
const error = SavedObjectsErrorHelpers.decorateForbiddenError(new Error('foobar'), 'biz');
expect(error.output.payload).toHaveProperty('message', 'biz: foobar');
});
it('sets statusCode to 403', () => {
const error = SavedObjectsErrorHelpers.decorateForbiddenError(new Error('foo'));
expect(error.output).toHaveProperty('statusCode', 403);
});
it('preserves boom properties of input', () => {
const error = Boom.notFound();
SavedObjectsErrorHelpers.decorateForbiddenError(error);
expect(error.output).toHaveProperty('statusCode', 404);
});
});
});
});
describe('NotFound error', () => {
describe('createGenericNotFoundError', () => {
it('makes an error identifiable as a NotFound error', () => {
@ -203,11 +211,9 @@ describe('savedObjectsClient/errorTypes', () => {
expect(SavedObjectsErrorHelpers.isNotFoundError(error)).toBe(true);
});
it('is a boom error, has boom properties', () => {
it('returns a boom error', () => {
const error = SavedObjectsErrorHelpers.createGenericNotFoundError();
expect(error).toHaveProperty('isBoom');
expect(typeof error.output).toBe('object');
expect(error.output.statusCode).toBe(404);
expect(error).toHaveProperty('isBoom', true);
});
describe('error.output', () => {
@ -215,6 +221,7 @@ describe('savedObjectsClient/errorTypes', () => {
const error = SavedObjectsErrorHelpers.createGenericNotFoundError();
expect(error.output.payload).toHaveProperty('message', 'Not Found');
});
it('sets statusCode to 404', () => {
const error = SavedObjectsErrorHelpers.createGenericNotFoundError();
expect(error.output).toHaveProperty('statusCode', 404);
@ -222,6 +229,7 @@ describe('savedObjectsClient/errorTypes', () => {
});
});
});
describe('Conflict error', () => {
describe('decorateConflictError', () => {
it('returns original object', () => {
@ -238,14 +246,7 @@ describe('savedObjectsClient/errorTypes', () => {
it('adds boom properties', () => {
const error = SavedObjectsErrorHelpers.decorateConflictError(new Error());
expect(typeof error.output).toBe('object');
expect(error.output.statusCode).toBe(409);
});
it('preserves boom properties of input', () => {
const error = Boom.notFound();
SavedObjectsErrorHelpers.decorateConflictError(error);
expect(error.output.statusCode).toBe(404);
expect(error).toHaveProperty('isBoom', true);
});
describe('error.output', () => {
@ -253,17 +254,77 @@ describe('savedObjectsClient/errorTypes', () => {
const error = SavedObjectsErrorHelpers.decorateConflictError(new Error('foobar'));
expect(error.output.payload).toHaveProperty('message', 'foobar');
});
it('prefixes message with passed reason', () => {
const error = SavedObjectsErrorHelpers.decorateConflictError(new Error('foobar'), 'biz');
expect(error.output.payload).toHaveProperty('message', 'biz: foobar');
});
it('sets statusCode to 409', () => {
const error = SavedObjectsErrorHelpers.decorateConflictError(new Error('foo'));
expect(error.output).toHaveProperty('statusCode', 409);
});
it('preserves boom properties of input', () => {
const error = Boom.notFound();
SavedObjectsErrorHelpers.decorateConflictError(error);
expect(error.output).toHaveProperty('statusCode', 404);
});
});
});
});
describe('EsCannotExecuteScript error', () => {
describe('decorateEsCannotExecuteScriptError', () => {
it('returns original object', () => {
const error = new Error();
expect(SavedObjectsErrorHelpers.decorateEsCannotExecuteScriptError(error)).toBe(error);
});
it('makes the error identifiable as a EsCannotExecuteScript error', () => {
const error = new Error();
expect(SavedObjectsErrorHelpers.isEsCannotExecuteScriptError(error)).toBe(false);
SavedObjectsErrorHelpers.decorateEsCannotExecuteScriptError(error);
expect(SavedObjectsErrorHelpers.isEsCannotExecuteScriptError(error)).toBe(true);
});
it('adds boom properties', () => {
const error = SavedObjectsErrorHelpers.decorateEsCannotExecuteScriptError(new Error());
expect(error).toHaveProperty('isBoom', true);
});
describe('error.output', () => {
it('defaults to message of error', () => {
const error = SavedObjectsErrorHelpers.decorateEsCannotExecuteScriptError(
new Error('foobar')
);
expect(error.output.payload).toHaveProperty('message', 'foobar');
});
it('prefixes message with passed reason', () => {
const error = SavedObjectsErrorHelpers.decorateEsCannotExecuteScriptError(
new Error('foobar'),
'biz'
);
expect(error.output.payload).toHaveProperty('message', 'biz: foobar');
});
it('sets statusCode to 501', () => {
const error = SavedObjectsErrorHelpers.decorateEsCannotExecuteScriptError(
new Error('foo')
);
expect(error.output).toHaveProperty('statusCode', 400);
});
it('preserves boom properties of input', () => {
const error = Boom.notFound();
SavedObjectsErrorHelpers.decorateEsCannotExecuteScriptError(error);
expect(error.output).toHaveProperty('statusCode', 404);
});
});
});
});
describe('EsUnavailable error', () => {
describe('decorateEsUnavailableError', () => {
it('returns original object', () => {
@ -280,14 +341,7 @@ describe('savedObjectsClient/errorTypes', () => {
it('adds boom properties', () => {
const error = SavedObjectsErrorHelpers.decorateEsUnavailableError(new Error());
expect(typeof error.output).toBe('object');
expect(error.output.statusCode).toBe(503);
});
it('preserves boom properties of input', () => {
const error = Boom.notFound();
SavedObjectsErrorHelpers.decorateEsUnavailableError(error);
expect(error.output.statusCode).toBe(404);
expect(error).toHaveProperty('isBoom', true);
});
describe('error.output', () => {
@ -295,6 +349,7 @@ describe('savedObjectsClient/errorTypes', () => {
const error = SavedObjectsErrorHelpers.decorateEsUnavailableError(new Error('foobar'));
expect(error.output.payload).toHaveProperty('message', 'foobar');
});
it('prefixes message with passed reason', () => {
const error = SavedObjectsErrorHelpers.decorateEsUnavailableError(
new Error('foobar'),
@ -302,13 +357,21 @@ describe('savedObjectsClient/errorTypes', () => {
);
expect(error.output.payload).toHaveProperty('message', 'biz: foobar');
});
it('sets statusCode to 503', () => {
const error = SavedObjectsErrorHelpers.decorateEsUnavailableError(new Error('foo'));
expect(error.output).toHaveProperty('statusCode', 503);
});
it('preserves boom properties of input', () => {
const error = Boom.notFound();
SavedObjectsErrorHelpers.decorateEsUnavailableError(error);
expect(error.output).toHaveProperty('statusCode', 404);
});
});
});
});
describe('General error', () => {
describe('decorateGeneralError', () => {
it('returns original object', () => {
@ -318,14 +381,7 @@ describe('savedObjectsClient/errorTypes', () => {
it('adds boom properties', () => {
const error = SavedObjectsErrorHelpers.decorateGeneralError(new Error());
expect(typeof error.output).toBe('object');
expect(error.output.statusCode).toBe(500);
});
it('preserves boom properties of input', () => {
const error = Boom.notFound();
SavedObjectsErrorHelpers.decorateGeneralError(error);
expect(error.output.statusCode).toBe(404);
expect(error).toHaveProperty('isBoom', true);
});
describe('error.output', () => {
@ -333,10 +389,17 @@ describe('savedObjectsClient/errorTypes', () => {
const error = SavedObjectsErrorHelpers.decorateGeneralError(new Error('foobar'));
expect(error.output.payload.message).toMatch(/internal server error/i);
});
it('sets statusCode to 500', () => {
const error = SavedObjectsErrorHelpers.decorateGeneralError(new Error('foo'));
expect(error.output).toHaveProperty('statusCode', 500);
});
it('preserves boom properties of input', () => {
const error = Boom.notFound();
SavedObjectsErrorHelpers.decorateGeneralError(error);
expect(error.output).toHaveProperty('statusCode', 404);
});
});
});
});
@ -363,9 +426,7 @@ describe('savedObjectsClient/errorTypes', () => {
it('returns a boom error', () => {
const error = SavedObjectsErrorHelpers.createEsAutoCreateIndexError();
expect(error).toHaveProperty('isBoom');
expect(typeof error.output).toBe('object');
expect(error.output.statusCode).toBe(503);
expect(error).toHaveProperty('isBoom', true);
});
describe('error.output', () => {
@ -373,6 +434,7 @@ describe('savedObjectsClient/errorTypes', () => {
const error = SavedObjectsErrorHelpers.createEsAutoCreateIndexError();
expect(error.output.payload).toHaveProperty('message', 'Automatic index creation failed');
});
it('sets statusCode to 503', () => {
const error = SavedObjectsErrorHelpers.createEsAutoCreateIndexError();
expect(error.output).toHaveProperty('statusCode', 503);

View file

@ -33,6 +33,8 @@ const CODE_REQUEST_ENTITY_TOO_LARGE = 'SavedObjectsClient/requestEntityTooLarge'
const CODE_NOT_FOUND = 'SavedObjectsClient/notFound';
// 409 - Conflict
const CODE_CONFLICT = 'SavedObjectsClient/conflict';
// 400 - Es Cannot Execute Script
const CODE_ES_CANNOT_EXECUTE_SCRIPT = 'SavedObjectsClient/esCannotExecuteScript';
// 503 - Es Unavailable
const CODE_ES_UNAVAILABLE = 'SavedObjectsClient/esUnavailable';
// 503 - Unable to automatically create index because of action.auto_create_index setting
@ -152,10 +154,24 @@ export class SavedObjectsErrorHelpers {
return decorate(error, CODE_CONFLICT, 409, reason);
}
public static createConflictError(type: string, id: string) {
return SavedObjectsErrorHelpers.decorateConflictError(
Boom.conflict(`Saved object [${type}/${id}] conflict`)
);
}
public static isConflictError(error: Error | DecoratedError) {
return isSavedObjectsClientError(error) && error[code] === CODE_CONFLICT;
}
public static decorateEsCannotExecuteScriptError(error: Error, reason?: string) {
return decorate(error, CODE_ES_CANNOT_EXECUTE_SCRIPT, 400, reason);
}
public static isEsCannotExecuteScriptError(error: Error | DecoratedError) {
return isSavedObjectsClientError(error) && error[code] === CODE_ES_CANNOT_EXECUTE_SCRIPT;
}
public static decorateEsUnavailableError(error: Error, reason?: string) {
return decorate(error, CODE_ES_UNAVAILABLE, 503, reason);
}

View file

@ -26,7 +26,7 @@ describe('includedFields', () => {
it('accepts type string', () => {
const fields = includedFields('config', 'foo');
expect(fields).toHaveLength(7);
expect(fields).toHaveLength(8);
expect(fields).toContain('type');
});
@ -37,6 +37,7 @@ Array [
"config.foo",
"secret.foo",
"namespace",
"namespaces",
"type",
"references",
"migrationVersion",
@ -48,14 +49,14 @@ Array [
it('accepts field as string', () => {
const fields = includedFields('config', 'foo');
expect(fields).toHaveLength(7);
expect(fields).toHaveLength(8);
expect(fields).toContain('config.foo');
});
it('accepts fields as an array', () => {
const fields = includedFields('config', ['foo', 'bar']);
expect(fields).toHaveLength(9);
expect(fields).toHaveLength(10);
expect(fields).toContain('config.foo');
expect(fields).toContain('config.bar');
});
@ -69,6 +70,7 @@ Array [
"secret.foo",
"secret.bar",
"namespace",
"namespaces",
"type",
"references",
"migrationVersion",
@ -81,31 +83,37 @@ Array [
it('includes namespace', () => {
const fields = includedFields('config', 'foo');
expect(fields).toHaveLength(7);
expect(fields).toHaveLength(8);
expect(fields).toContain('namespace');
});
it('includes namespaces', () => {
const fields = includedFields('config', 'foo');
expect(fields).toHaveLength(8);
expect(fields).toContain('namespaces');
});
it('includes references', () => {
const fields = includedFields('config', 'foo');
expect(fields).toHaveLength(7);
expect(fields).toHaveLength(8);
expect(fields).toContain('references');
});
it('includes migrationVersion', () => {
const fields = includedFields('config', 'foo');
expect(fields).toHaveLength(7);
expect(fields).toHaveLength(8);
expect(fields).toContain('migrationVersion');
});
it('includes updated_at', () => {
const fields = includedFields('config', 'foo');
expect(fields).toHaveLength(7);
expect(fields).toHaveLength(8);
expect(fields).toContain('updated_at');
});
it('uses wildcard when type is not provided', () => {
const fields = includedFields(undefined, 'foo');
expect(fields).toHaveLength(7);
expect(fields).toHaveLength(8);
expect(fields).toContain('*.foo');
});
@ -113,7 +121,7 @@ Array [
it('includes legacy field path', () => {
const fields = includedFields('config', ['foo', 'bar']);
expect(fields).toHaveLength(9);
expect(fields).toHaveLength(10);
expect(fields).toContain('foo');
expect(fields).toContain('bar');
});

View file

@ -37,6 +37,7 @@ export function includedFields(type: string | string[] = '*', fields?: string[]
return [...acc, ...sourceFields.map(f => `${t}.${f}`)];
}, [])
.concat('namespace')
.concat('namespaces')
.concat('type')
.concat('references')
.concat('migrationVersion')

View file

@ -28,6 +28,8 @@ const create = (): jest.Mocked<ISavedObjectsRepository> => ({
find: jest.fn(),
get: jest.fn(),
update: jest.fn(),
addToNamespaces: jest.fn(),
deleteFromNamespaces: jest.fn(),
deleteByNamespace: jest.fn(),
incrementCounter: jest.fn(),
});

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -66,18 +66,26 @@ function getClauseForType(
namespace: string | undefined,
type: string
) {
if (namespace && !registry.isNamespaceAgnostic(type)) {
if (registry.isMultiNamespace(type)) {
return {
bool: {
must: [{ term: { type } }, { term: { namespaces: namespace ?? 'default' } }],
must_not: [{ exists: { field: 'namespace' } }],
},
};
} else if (namespace && registry.isSingleNamespace(type)) {
return {
bool: {
must: [{ term: { type } }, { term: { namespace } }],
must_not: [{ exists: { field: 'namespaces' } }],
},
};
}
// isSingleNamespace in the default namespace, or isNamespaceAgnostic
return {
bool: {
must: [{ term: { type } }],
must_not: [{ exists: { field: 'namespace' } }],
must_not: [{ exists: { field: 'namespace' } }, { exists: { field: 'namespaces' } }],
},
};
}

View file

@ -31,6 +31,8 @@ const create = () =>
find: jest.fn(),
get: jest.fn(),
update: jest.fn(),
addToNamespaces: jest.fn(),
deleteFromNamespaces: jest.fn(),
} as unknown) as jest.Mocked<SavedObjectsClientContract>);
export const savedObjectsClientMock = { create };

View file

@ -147,3 +147,37 @@ test(`#bulkUpdate`, async () => {
});
expect(result).toBe(returnValue);
});
test(`#addToNamespaces`, async () => {
const returnValue = Symbol();
const mockRepository = {
addToNamespaces: jest.fn().mockResolvedValue(returnValue),
};
const client = new SavedObjectsClient(mockRepository);
const type = Symbol();
const id = Symbol();
const namespaces = Symbol();
const options = Symbol();
const result = await client.addToNamespaces(type, id, namespaces, options);
expect(mockRepository.addToNamespaces).toHaveBeenCalledWith(type, id, namespaces, options);
expect(result).toBe(returnValue);
});
test(`#deleteFromNamespaces`, async () => {
const returnValue = Symbol();
const mockRepository = {
deleteFromNamespaces: jest.fn().mockResolvedValue(returnValue),
};
const client = new SavedObjectsClient(mockRepository);
const type = Symbol();
const id = Symbol();
const namespaces = Symbol();
const options = Symbol();
const result = await client.deleteFromNamespaces(type, id, namespaces, options);
expect(mockRepository.deleteFromNamespaces).toHaveBeenCalledWith(type, id, namespaces, options);
expect(result).toBe(returnValue);
});

View file

@ -107,6 +107,26 @@ export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions {
refresh?: MutatingOperationRefreshSetting;
}
/**
*
* @public
*/
export interface SavedObjectsAddToNamespacesOptions extends SavedObjectsBaseOptions {
/** An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. */
version?: string;
/** The Elasticsearch Refresh setting for this operation */
refresh?: MutatingOperationRefreshSetting;
}
/**
*
* @public
*/
export interface SavedObjectsDeleteFromNamespacesOptions extends SavedObjectsBaseOptions {
/** The Elasticsearch Refresh setting for this operation */
refresh?: MutatingOperationRefreshSetting;
}
/**
*
* @public
@ -270,6 +290,40 @@ export class SavedObjectsClient {
return await this._repository.update(type, id, attributes, options);
}
/**
* Adds namespaces to a SavedObject
*
* @param type
* @param id
* @param namespaces
* @param options
*/
async addToNamespaces(
type: string,
id: string,
namespaces: string[],
options: SavedObjectsAddToNamespacesOptions = {}
): Promise<{}> {
return await this._repository.addToNamespaces(type, id, namespaces, options);
}
/**
* Removes namespaces from a SavedObject
*
* @param type
* @param id
* @param namespaces
* @param options
*/
async deleteFromNamespaces(
type: string,
id: string,
namespaces: string[],
options: SavedObjectsDeleteFromNamespacesOptions = {}
): Promise<{}> {
return await this._repository.deleteFromNamespaces(type, id, namespaces, options);
}
/**
* Bulk Updates multiple SavedObject at once
*

View file

@ -172,6 +172,19 @@ export type MutatingOperationRefreshSetting = boolean | 'wait_for';
*/
export type SavedObjectsClientContract = Pick<SavedObjectsClient, keyof SavedObjectsClient>;
/**
* The namespace type dictates how a saved object can be interacted in relation to namespaces. Each type is mutually exclusive:
* * single (default): this type of saved object is namespace-isolated, e.g., it exists in only one namespace.
* * multiple: this type of saved object is shareable, e.g., it can exist in one or more namespaces.
* * agnostic: this type of saved object is global.
*
* Note: do not write logic that uses this value directly; instead, use the appropriate accessors in the
* {@link SavedObjectTypeRegistry | type registry}.
*
* @public
*/
export type SavedObjectsNamespaceType = 'single' | 'multiple' | 'agnostic';
/**
* @remarks This is only internal for now, and will only be public when we expose the registerType API
*
@ -190,9 +203,14 @@ export interface SavedObjectsType {
*/
hidden: boolean;
/**
* Is the type global (true), or namespaced (false).
* Is the type global (true), or not (false).
* @deprecated Use `namespaceType` instead.
*/
namespaceAgnostic: boolean;
namespaceAgnostic?: boolean;
/**
* The {@link SavedObjectsNamespaceType | namespace type} for the type.
*/
namespaceType?: SavedObjectsNamespaceType;
/**
* If defined, the type instances will be stored in the given index instead of the default one.
*/
@ -329,6 +347,8 @@ export type SavedObjectLegacyMigrationFn = (
*/
interface SavedObjectsLegacyTypeSchema {
isNamespaceAgnostic?: boolean;
/** Cannot be used in conjunction with `isNamespaceAgnostic` */
multiNamespace?: boolean;
hidden?: boolean;
indexPattern?: ((config: LegacyConfig) => string) | string;
convertToAliasScript?: string;

View file

@ -84,6 +84,16 @@ describe('convertLegacyTypes', () => {
},
{
pluginId: 'pluginB',
properties: {
typeB: {
properties: {
fieldB: { type: 'text' },
},
},
},
},
{
pluginId: 'pluginC',
properties: {
typeC: {
properties: {
@ -92,6 +102,16 @@ describe('convertLegacyTypes', () => {
},
},
},
{
pluginId: 'pluginD',
properties: {
typeD: {
properties: {
fieldD: { type: 'text' },
},
},
},
},
],
savedObjectMigrations: {},
savedObjectSchemas: {
@ -100,6 +120,18 @@ describe('convertLegacyTypes', () => {
hidden: true,
isNamespaceAgnostic: true,
},
typeB: {
indexPattern: 'barBaz',
hidden: false,
multiNamespace: true,
},
typeD: {
indexPattern: 'bazQux',
hidden: false,
// if both isNamespaceAgnostic and multiNamespace are true, the resulting namespaceType is 'agnostic'
isNamespaceAgnostic: true,
multiNamespace: true,
},
},
savedObjectValidations: {},
savedObjectsManagement: {},
@ -372,29 +404,56 @@ describe('convertTypesToLegacySchema', () => {
{
name: 'typeA',
hidden: false,
namespaceAgnostic: true,
namespaceType: 'agnostic',
mappings: { properties: {} },
convertToAliasScript: 'some script',
},
{
name: 'typeB',
hidden: true,
namespaceAgnostic: false,
namespaceType: 'single',
indexPattern: 'myIndex',
mappings: { properties: {} },
},
{
name: 'typeC',
hidden: false,
namespaceType: 'multiple',
mappings: { properties: {} },
},
// deprecated test case
{
name: 'typeD',
hidden: false,
namespaceAgnostic: true,
namespaceType: 'multiple', // if namespaceAgnostic and namespaceType are both set, namespaceAgnostic takes precedence
mappings: { properties: {} },
},
];
expect(convertTypesToLegacySchema(types)).toEqual({
typeA: {
hidden: false,
isNamespaceAgnostic: true,
multiNamespace: false,
convertToAliasScript: 'some script',
},
typeB: {
hidden: true,
isNamespaceAgnostic: false,
multiNamespace: false,
indexPattern: 'myIndex',
},
typeC: {
hidden: false,
isNamespaceAgnostic: false,
multiNamespace: true,
},
// deprecated test case
typeD: {
hidden: false,
isNamespaceAgnostic: true,
multiNamespace: false,
},
});
});
});

View file

@ -20,6 +20,7 @@
import { LegacyConfig } from '../legacy';
import { SavedObjectMigrationMap } from './migrations';
import {
SavedObjectsNamespaceType,
SavedObjectsType,
SavedObjectsLegacyUiExports,
SavedObjectLegacyMigrationMap,
@ -48,10 +49,15 @@ export const convertLegacyTypes = (
const schema = savedObjectSchemas[type];
const migrations = savedObjectMigrations[type];
const management = savedObjectsManagement[type];
const namespaceType = (schema?.isNamespaceAgnostic
? 'agnostic'
: schema?.multiNamespace
? 'multiple'
: 'single') as SavedObjectsNamespaceType;
return {
name: type,
hidden: schema?.hidden ?? false,
namespaceAgnostic: schema?.isNamespaceAgnostic ?? false,
namespaceType,
mappings,
indexPattern:
typeof schema?.indexPattern === 'function'
@ -76,7 +82,8 @@ export const convertTypesToLegacySchema = (
return {
...schema,
[type.name]: {
isNamespaceAgnostic: type.namespaceAgnostic,
isNamespaceAgnostic: type.namespaceAgnostic || type.namespaceType === 'agnostic',
multiNamespace: !type.namespaceAgnostic && type.namespaceType === 'multiple',
hidden: type.hidden,
indexPattern: type.indexPattern,
convertToAliasScript: type.convertToAliasScript,

View file

@ -1003,7 +1003,7 @@ export type IsAuthenticated = (request: KibanaRequest | LegacyRequest) => boolea
export type ISavedObjectsRepository = Pick<SavedObjectsRepository, keyof SavedObjectsRepository>;
// @public
export type ISavedObjectTypeRegistry = Pick<SavedObjectTypeRegistry, 'getType' | 'getAllTypes' | 'getIndex' | 'isNamespaceAgnostic' | 'isHidden' | 'getImportableAndExportableTypes' | 'isImportableAndExportable'>;
export type ISavedObjectTypeRegistry = Omit<SavedObjectTypeRegistry, 'registerType'>;
// @public
export type IScopedClusterClient = Pick<ScopedClusterClient, 'callAsCurrentUser' | 'callAsInternalUser'>;
@ -1643,6 +1643,7 @@ export interface SavedObject<T = unknown> {
};
id: string;
migrationVersion?: SavedObjectsMigrationVersion;
namespaces?: string[];
references: SavedObjectReference[];
type: string;
updated_at?: string;
@ -1687,6 +1688,12 @@ export interface SavedObjectReference {
type: string;
}
// @public (undocumented)
export interface SavedObjectsAddToNamespacesOptions extends SavedObjectsBaseOptions {
refresh?: MutatingOperationRefreshSetting;
version?: string;
}
// Warning: (ae-forgotten-export) The symbol "SavedObjectDoc" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "Referencable" needs to be exported by the entry point index.d.ts
//
@ -1754,11 +1761,13 @@ export interface SavedObjectsBulkUpdateResponse<T = unknown> {
export class SavedObjectsClient {
// @internal
constructor(repository: ISavedObjectsRepository);
addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise<{}>;
bulkCreate<T = unknown>(objects: Array<SavedObjectsBulkCreateObject<T>>, options?: SavedObjectsCreateOptions): Promise<SavedObjectsBulkResponse<T>>;
bulkGet<T = unknown>(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise<SavedObjectsBulkResponse<T>>;
bulkUpdate<T = unknown>(objects: Array<SavedObjectsBulkUpdateObject<T>>, options?: SavedObjectsBulkUpdateOptions): Promise<SavedObjectsBulkUpdateResponse<T>>;
create<T = unknown>(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise<SavedObject<T>>;
delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>;
deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<{}>;
// (undocumented)
static errors: typeof SavedObjectsErrorHelpers;
// (undocumented)
@ -1839,6 +1848,11 @@ export interface SavedObjectsDeleteByNamespaceOptions extends SavedObjectsBaseOp
refresh?: MutatingOperationRefreshSetting;
}
// @public (undocumented)
export interface SavedObjectsDeleteFromNamespacesOptions extends SavedObjectsBaseOptions {
refresh?: MutatingOperationRefreshSetting;
}
// @public (undocumented)
export interface SavedObjectsDeleteOptions extends SavedObjectsBaseOptions {
refresh?: MutatingOperationRefreshSetting;
@ -1849,6 +1863,8 @@ export class SavedObjectsErrorHelpers {
// (undocumented)
static createBadRequestError(reason?: string): DecoratedError;
// (undocumented)
static createConflictError(type: string, id: string): DecoratedError;
// (undocumented)
static createEsAutoCreateIndexError(): DecoratedError;
// (undocumented)
static createGenericNotFoundError(type?: string | null, id?: string | null): DecoratedError;
@ -1861,6 +1877,8 @@ export class SavedObjectsErrorHelpers {
// (undocumented)
static decorateConflictError(error: Error, reason?: string): DecoratedError;
// (undocumented)
static decorateEsCannotExecuteScriptError(error: Error, reason?: string): DecoratedError;
// (undocumented)
static decorateEsUnavailableError(error: Error, reason?: string): DecoratedError;
// (undocumented)
static decorateForbiddenError(error: Error, reason?: string): DecoratedError;
@ -1877,6 +1895,8 @@ export class SavedObjectsErrorHelpers {
// (undocumented)
static isEsAutoCreateIndexError(error: Error | DecoratedError): boolean;
// (undocumented)
static isEsCannotExecuteScriptError(error: Error | DecoratedError): boolean;
// (undocumented)
static isEsUnavailableError(error: Error | DecoratedError): boolean;
// (undocumented)
static isForbiddenError(error: Error | DecoratedError): boolean;
@ -2106,6 +2126,9 @@ export interface SavedObjectsMigrationVersion {
[pluginName: string]: string;
}
// @public
export type SavedObjectsNamespaceType = 'single' | 'multiple' | 'agnostic';
// @public
export interface SavedObjectsRawDoc {
// (undocumented)
@ -2124,6 +2147,7 @@ export interface SavedObjectsRawDoc {
// @public (undocumented)
export class SavedObjectsRepository {
addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise<{}>;
bulkCreate<T = unknown>(objects: Array<SavedObjectsBulkCreateObject<T>>, options?: SavedObjectsCreateOptions): Promise<SavedObjectsBulkResponse<T>>;
bulkGet<T = unknown>(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise<SavedObjectsBulkResponse<T>>;
bulkUpdate<T = unknown>(objects: Array<SavedObjectsBulkUpdateObject<T>>, options?: SavedObjectsBulkUpdateOptions): Promise<SavedObjectsBulkUpdateResponse<T>>;
@ -2134,6 +2158,7 @@ export class SavedObjectsRepository {
static createRepository(migrator: KibanaMigrator, typeRegistry: SavedObjectTypeRegistry, indexName: string, callCluster: APICaller, extraTypes?: string[], injectedConstructor?: any): ISavedObjectsRepository;
delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>;
deleteByNamespace(namespace: string, options?: SavedObjectsDeleteByNamespaceOptions): Promise<any>;
deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<{}>;
// (undocumented)
find<T = unknown>({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, }: SavedObjectsFindOptions): Promise<SavedObjectsFindResponse<T>>;
get<T = unknown>(type: string, id: string, options?: SavedObjectsBaseOptions): Promise<SavedObject<T>>;
@ -2175,7 +2200,11 @@ export class SavedObjectsSchema {
// (undocumented)
isHiddenType(type: string): boolean;
// (undocumented)
isMultiNamespace(type: string): boolean;
// (undocumented)
isNamespaceAgnostic(type: string): boolean;
// (undocumented)
isSingleNamespace(type: string): boolean;
}
// @public
@ -2224,7 +2253,9 @@ export interface SavedObjectsType {
mappings: SavedObjectsTypeMappingDefinition;
migrations?: SavedObjectMigrationMap;
name: string;
namespaceAgnostic: boolean;
// @deprecated
namespaceAgnostic?: boolean;
namespaceType?: SavedObjectsNamespaceType;
}
// @public
@ -2269,7 +2300,9 @@ export class SavedObjectTypeRegistry {
getType(type: string): SavedObjectsType | undefined;
isHidden(type: string): boolean;
isImportableAndExportable(type: string): boolean;
isMultiNamespace(type: string): boolean;
isNamespaceAgnostic(type: string): boolean;
isSingleNamespace(type: string): boolean;
registerType(type: SavedObjectsType): void;
}

View file

@ -96,4 +96,6 @@ export interface SavedObject<T = unknown> {
references: SavedObjectReference[];
/** {@inheritdoc SavedObjectsMigrationVersion} */
migrationVersion?: SavedObjectsMigrationVersion;
/** Namespace(s) that this saved object exists in. This attribute is only used for multi-namespace saved object types. */
namespaces?: string[];
}

View file

@ -58,7 +58,9 @@ export default function({ getService }) {
type: 'visualization',
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
error: {
message: 'version conflict, document already exists',
error: 'Conflict',
message:
'Saved object [visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab] conflict',
statusCode: 409,
},
},

View file

@ -80,8 +80,9 @@ export default function({ getService }) {
id: 'does not exist',
type: 'dashboard',
error: {
error: 'Not Found',
message: 'Saved object [dashboard/does not exist] not found',
statusCode: 404,
message: 'Not found',
},
},
{
@ -123,24 +124,28 @@ export default function({ getService }) {
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
type: 'visualization',
error: {
error: 'Not Found',
message:
'Saved object [visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab] not found',
statusCode: 404,
message: 'Not found',
},
},
{
id: 'does not exist',
type: 'dashboard',
error: {
error: 'Not Found',
message: 'Saved object [dashboard/does not exist] not found',
statusCode: 404,
message: 'Not found',
},
},
{
id: '7.0.0-alpha1',
type: 'config',
error: {
error: 'Not Found',
message: 'Saved object [config/7.0.0-alpha1] not found',
statusCode: 404,
message: 'Not found',
},
},
],

View file

@ -170,8 +170,9 @@ export default function({ getService }) {
id: '1',
type: 'dashboard',
error: {
error: 'Not Found',
message: 'Saved object [dashboard/1] not found',
statusCode: 404,
message: 'Not found',
},
},
],

View file

@ -10,14 +10,16 @@ import { SavedObjectsClientContract } from 'src/core/server';
import { EncryptedSavedObjectsService } from '../crypto';
import { EncryptedSavedObjectsClientWrapper } from './encrypted_saved_objects_client_wrapper';
import { savedObjectsClientMock } from 'src/core/server/mocks';
import { savedObjectsClientMock, savedObjectsTypeRegistryMock } from 'src/core/server/mocks';
import { encryptedSavedObjectsServiceMock } from '../crypto/index.mock';
let wrapper: EncryptedSavedObjectsClientWrapper;
let mockBaseClient: jest.Mocked<SavedObjectsClientContract>;
let mockBaseTypeRegistry: ReturnType<typeof savedObjectsTypeRegistryMock.create>;
let encryptedSavedObjectsServiceMockInstance: jest.Mocked<EncryptedSavedObjectsService>;
beforeEach(() => {
mockBaseClient = savedObjectsClientMock.create();
mockBaseTypeRegistry = savedObjectsTypeRegistryMock.create();
encryptedSavedObjectsServiceMockInstance = encryptedSavedObjectsServiceMock.create([
{
type: 'known-type',
@ -28,6 +30,7 @@ beforeEach(() => {
wrapper = new EncryptedSavedObjectsClientWrapper({
service: encryptedSavedObjectsServiceMockInstance,
baseClient: mockBaseClient,
baseTypeRegistry: mockBaseTypeRegistry,
} as any);
});
@ -91,35 +94,50 @@ describe('#create', () => {
);
});
it('uses `namespace` to encrypt attributes if it is specified', async () => {
const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' };
const options = { overwrite: true, namespace: 'some-namespace' };
const mockedResponse = {
id: 'uuid-v4-id',
type: 'known-type',
attributes: { attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' },
references: [],
describe('namespace', () => {
const doTest = async (namespace: string, expectNamespaceInDescriptor: boolean) => {
const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' };
const options = { overwrite: true, namespace };
const mockedResponse = {
id: 'uuid-v4-id',
type: 'known-type',
attributes: { attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' },
references: [],
};
mockBaseClient.create.mockResolvedValue(mockedResponse);
expect(await wrapper.create('known-type', attributes, options)).toEqual({
...mockedResponse,
attributes: { attrOne: 'one', attrThree: 'three' },
});
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1);
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith(
{
type: 'known-type',
id: 'uuid-v4-id',
namespace: expectNamespaceInDescriptor ? namespace : undefined,
},
{ attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }
);
expect(mockBaseClient.create).toHaveBeenCalledTimes(1);
expect(mockBaseClient.create).toHaveBeenCalledWith(
'known-type',
{ attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' },
{ id: 'uuid-v4-id', overwrite: true, namespace }
);
};
mockBaseClient.create.mockResolvedValue(mockedResponse);
expect(await wrapper.create('known-type', attributes, options)).toEqual({
...mockedResponse,
attributes: { attrOne: 'one', attrThree: 'three' },
it('uses `namespace` to encrypt attributes if it is specified when type is single-namespace', async () => {
await doTest('some-namespace', true);
});
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1);
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith(
{ type: 'known-type', id: 'uuid-v4-id', namespace: 'some-namespace' },
{ attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }
);
expect(mockBaseClient.create).toHaveBeenCalledTimes(1);
expect(mockBaseClient.create).toHaveBeenCalledWith(
'known-type',
{ attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' },
{ id: 'uuid-v4-id', overwrite: true, namespace: 'some-namespace' }
);
it('does not use `namespace` to encrypt attributes if it is specified when type is not single-namespace', async () => {
mockBaseTypeRegistry.isSingleNamespace.mockReturnValue(false);
await doTest('some-namespace', false);
});
});
it('fails if base client fails', async () => {
@ -190,14 +208,13 @@ describe('#bulkCreate', () => {
it('fails if ID is specified for registered type', async () => {
const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' };
const options = { namespace: 'some-namespace' };
const bulkCreateParams = [
{ id: 'some-id', type: 'known-type', attributes },
{ type: 'unknown-type', attributes },
];
await expect(wrapper.bulkCreate(bulkCreateParams, options)).rejects.toThrowError(
await expect(wrapper.bulkCreate(bulkCreateParams)).rejects.toThrowError(
'Predefined IDs are not allowed for saved objects with encrypted attributes.'
);
@ -257,39 +274,57 @@ describe('#bulkCreate', () => {
);
});
it('uses `namespace` to encrypt attributes if it is specified', async () => {
const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' };
const options = { namespace: 'some-namespace' };
const mockedResponse = {
saved_objects: [{ id: 'uuid-v4-id', type: 'known-type', attributes, references: [] }],
describe('namespace', () => {
const doTest = async (namespace: string, expectNamespaceInDescriptor: boolean) => {
const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' };
const options = { namespace };
const mockedResponse = {
saved_objects: [{ id: 'uuid-v4-id', type: 'known-type', attributes, references: [] }],
};
mockBaseClient.bulkCreate.mockResolvedValue(mockedResponse);
const bulkCreateParams = [{ type: 'known-type', attributes }];
await expect(wrapper.bulkCreate(bulkCreateParams, options)).resolves.toEqual({
saved_objects: [
{
...mockedResponse.saved_objects[0],
attributes: { attrOne: 'one', attrThree: 'three' },
},
],
});
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1);
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith(
{
type: 'known-type',
id: 'uuid-v4-id',
namespace: expectNamespaceInDescriptor ? namespace : undefined,
},
{ attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }
);
expect(mockBaseClient.bulkCreate).toHaveBeenCalledTimes(1);
expect(mockBaseClient.bulkCreate).toHaveBeenCalledWith(
[
{
...bulkCreateParams[0],
id: 'uuid-v4-id',
attributes: { attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' },
},
],
options
);
};
mockBaseClient.bulkCreate.mockResolvedValue(mockedResponse);
const bulkCreateParams = [{ type: 'known-type', attributes }];
await expect(wrapper.bulkCreate(bulkCreateParams, options)).resolves.toEqual({
saved_objects: [
{ ...mockedResponse.saved_objects[0], attributes: { attrOne: 'one', attrThree: 'three' } },
],
it('uses `namespace` to encrypt attributes if it is specified when type is single-namespace', async () => {
await doTest('some-namespace', true);
});
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1);
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith(
{ type: 'known-type', id: 'uuid-v4-id', namespace: 'some-namespace' },
{ attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }
);
expect(mockBaseClient.bulkCreate).toHaveBeenCalledTimes(1);
expect(mockBaseClient.bulkCreate).toHaveBeenCalledWith(
[
{
...bulkCreateParams[0],
id: 'uuid-v4-id',
attributes: { attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' },
},
],
options
);
it('does not use `namespace` to encrypt attributes if it is specified when type is not single-namespace', async () => {
mockBaseTypeRegistry.isSingleNamespace.mockReturnValue(false);
await doTest('some-namespace', false);
});
});
it('fails if base client fails', async () => {
@ -432,63 +467,79 @@ describe('#bulkUpdate', () => {
);
});
it('uses `namespace` to encrypt attributes if it is specified', async () => {
const docs = [
{
id: 'some-id',
type: 'known-type',
attributes: {
attrOne: 'one',
attrSecret: 'secret',
attrThree: 'three',
},
version: 'some-version',
},
];
mockBaseClient.bulkUpdate.mockResolvedValue({
saved_objects: docs.map(doc => ({ ...doc, references: undefined })),
});
await expect(wrapper.bulkUpdate(docs, { namespace: 'some-namespace' })).resolves.toEqual({
saved_objects: [
describe('namespace', () => {
const doTest = async (namespace: string, expectNamespaceInDescriptor: boolean) => {
const docs = [
{
id: 'some-id',
type: 'known-type',
attributes: {
attrOne: 'one',
attrSecret: 'secret',
attrThree: 'three',
},
version: 'some-version',
references: undefined,
},
],
];
const options = { namespace };
mockBaseClient.bulkUpdate.mockResolvedValue({
saved_objects: docs.map(doc => ({ ...doc, references: undefined })),
});
await expect(wrapper.bulkUpdate(docs, options)).resolves.toEqual({
saved_objects: [
{
id: 'some-id',
type: 'known-type',
attributes: {
attrOne: 'one',
attrThree: 'three',
},
version: 'some-version',
references: undefined,
},
],
});
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1);
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith(
{
type: 'known-type',
id: 'some-id',
namespace: expectNamespaceInDescriptor ? namespace : undefined,
},
{ attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }
);
expect(mockBaseClient.bulkUpdate).toHaveBeenCalledTimes(1);
expect(mockBaseClient.bulkUpdate).toHaveBeenCalledWith(
[
{
id: 'some-id',
type: 'known-type',
attributes: {
attrOne: 'one',
attrSecret: '*secret*',
attrThree: 'three',
},
version: 'some-version',
references: undefined,
},
],
options
);
};
it('uses `namespace` to encrypt attributes if it is specified when type is single-namespace', async () => {
await doTest('some-namespace', true);
});
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1);
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith(
{ type: 'known-type', id: 'some-id', namespace: 'some-namespace' },
{ attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }
);
expect(mockBaseClient.bulkUpdate).toHaveBeenCalledTimes(1);
expect(mockBaseClient.bulkUpdate).toHaveBeenCalledWith(
[
{
id: 'some-id',
type: 'known-type',
attributes: {
attrOne: 'one',
attrSecret: '*secret*',
attrThree: 'three',
},
version: 'some-version',
references: undefined,
},
],
{ namespace: 'some-namespace' }
);
it('does not use `namespace` to encrypt attributes if it is specified when type is not single-namespace', async () => {
mockBaseTypeRegistry.isSingleNamespace.mockReturnValue(false);
await doTest('some-namespace', false);
});
});
it('fails if base client fails', async () => {
@ -871,31 +922,46 @@ describe('#update', () => {
);
});
it('uses `namespace` to encrypt attributes if it is specified', async () => {
const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' };
const options = { version: 'some-version', namespace: 'some-namespace' };
const mockedResponse = { id: 'some-id', type: 'known-type', attributes, references: [] };
describe('namespace', () => {
const doTest = async (namespace: string, expectNamespaceInDescriptor: boolean) => {
const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' };
const options = { version: 'some-version', namespace };
const mockedResponse = { id: 'some-id', type: 'known-type', attributes, references: [] };
mockBaseClient.update.mockResolvedValue(mockedResponse);
mockBaseClient.update.mockResolvedValue(mockedResponse);
await expect(wrapper.update('known-type', 'some-id', attributes, options)).resolves.toEqual({
...mockedResponse,
attributes: { attrOne: 'one', attrThree: 'three' },
await expect(wrapper.update('known-type', 'some-id', attributes, options)).resolves.toEqual({
...mockedResponse,
attributes: { attrOne: 'one', attrThree: 'three' },
});
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1);
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith(
{
type: 'known-type',
id: 'some-id',
namespace: expectNamespaceInDescriptor ? namespace : undefined,
},
{ attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }
);
expect(mockBaseClient.update).toHaveBeenCalledTimes(1);
expect(mockBaseClient.update).toHaveBeenCalledWith(
'known-type',
'some-id',
{ attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' },
options
);
};
it('uses `namespace` to encrypt attributes if it is specified when type is single-namespace', async () => {
await doTest('some-namespace', true);
});
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1);
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith(
{ type: 'known-type', id: 'some-id', namespace: 'some-namespace' },
{ attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }
);
expect(mockBaseClient.update).toHaveBeenCalledTimes(1);
expect(mockBaseClient.update).toHaveBeenCalledWith(
'known-type',
'some-id',
{ attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' },
options
);
it('does not use `namespace` to encrypt attributes if it is specified when type is not single-namespace', async () => {
mockBaseTypeRegistry.isSingleNamespace.mockReturnValue(false);
await doTest('some-namespace', false);
});
});
it('fails if base client fails', async () => {

View file

@ -19,11 +19,15 @@ import {
SavedObjectsFindResponse,
SavedObjectsUpdateOptions,
SavedObjectsUpdateResponse,
SavedObjectsAddToNamespacesOptions,
SavedObjectsDeleteFromNamespacesOptions,
ISavedObjectTypeRegistry,
} from 'src/core/server';
import { EncryptedSavedObjectsService } from '../crypto';
interface EncryptedSavedObjectsClientOptions {
baseClient: SavedObjectsClientContract;
baseTypeRegistry: ISavedObjectTypeRegistry;
service: Readonly<EncryptedSavedObjectsService>;
}
@ -41,6 +45,10 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon
public readonly errors = options.baseClient.errors
) {}
// only include namespace in AAD descriptor if the specified type is single-namespace
private getDescriptorNamespace = (type: string, namespace?: string) =>
this.options.baseTypeRegistry.isSingleNamespace(type) ? namespace : undefined;
public async create<T = unknown>(
type: string,
attributes: T = {} as T,
@ -60,11 +68,12 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon
}
const id = generateID();
const namespace = this.getDescriptorNamespace(type, options.namespace);
return this.stripEncryptedAttributesFromResponse(
await this.options.baseClient.create(
type,
await this.options.service.encryptAttributes(
{ type, id, namespace: options.namespace },
{ type, id, namespace },
attributes as Record<string, unknown>
),
{ ...options, id }
@ -95,11 +104,12 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon
}
const id = generateID();
const namespace = this.getDescriptorNamespace(object.type, options?.namespace);
return {
...object,
id,
attributes: await this.options.service.encryptAttributes(
{ type: object.type, id, namespace: options && options.namespace },
{ type: object.type, id, namespace },
object.attributes as Record<string, unknown>
),
} as SavedObjectsBulkCreateObject<T>;
@ -124,10 +134,11 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon
if (!this.options.service.isRegistered(type)) {
return object;
}
const namespace = this.getDescriptorNamespace(type, options?.namespace);
return {
...object,
attributes: await this.options.service.encryptAttributes(
{ type, id, namespace: options && options.namespace },
{ type, id, namespace },
attributes
),
};
@ -173,20 +184,35 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon
if (!this.options.service.isRegistered(type)) {
return await this.options.baseClient.update(type, id, attributes, options);
}
const namespace = this.getDescriptorNamespace(type, options?.namespace);
return this.stripEncryptedAttributesFromResponse(
await this.options.baseClient.update(
type,
id,
await this.options.service.encryptAttributes(
{ type, id, namespace: options && options.namespace },
attributes
),
await this.options.service.encryptAttributes({ type, id, namespace }, attributes),
options
)
);
}
public async addToNamespaces(
type: string,
id: string,
namespaces: string[],
options?: SavedObjectsAddToNamespacesOptions
) {
return await this.options.baseClient.addToNamespaces(type, id, namespaces, options);
}
public async deleteFromNamespaces(
type: string,
id: string,
namespaces: string[],
options?: SavedObjectsDeleteFromNamespacesOptions
) {
return await this.options.baseClient.deleteFromNamespaces(type, id, namespaces, options);
}
/**
* Strips encrypted attributes from any non-bulk Saved Objects API response. If type isn't
* registered, response is returned as is.

View file

@ -40,7 +40,8 @@ export function setupSavedObjects({
savedObjects.addClientWrapper(
Number.MAX_SAFE_INTEGER,
'encryptedSavedObjects',
({ client: baseClient }) => new EncryptedSavedObjectsClientWrapper({ baseClient, service })
({ client: baseClient, typeRegistry: baseTypeRegistry }) =>
new EncryptedSavedObjectsClientWrapper({ baseClient, baseTypeRegistry, service })
);
const internalRepositoryPromise = getStartServices().then(([core]) =>

View file

@ -18,25 +18,46 @@ describe(`#savedObjectsAuthorizationFailure`, () => {
const username = 'foo-user';
const action = 'foo-action';
const types = ['foo-type-1', 'foo-type-2'];
const missing = [`saved_object:${types[0]}/foo-action`, `saved_object:${types[1]}/foo-action`];
const spaceIds = ['foo-space', 'bar-space'];
const missing = [
{
spaceId: 'foo-space',
privilege: `saved_object:${types[0]}/${action}`,
},
{
spaceId: 'foo-space',
privilege: `saved_object:${types[1]}/${action}`,
},
];
const args = {
foo: 'bar',
baz: 'quz',
};
securityAuditLogger.savedObjectsAuthorizationFailure(username, action, types, missing, args);
securityAuditLogger.savedObjectsAuthorizationFailure(
username,
action,
types,
spaceIds,
missing,
args
);
expect(auditLogger.log).toHaveBeenCalledWith(
'saved_objects_authorization_failure',
expect.stringContaining(`${username} unauthorized to ${action}`),
expect.any(String),
{
username,
action,
types,
spaceIds,
missing,
args,
}
);
expect(auditLogger.log.mock.calls[0][1]).toMatchInlineSnapshot(
`"foo-user unauthorized to [foo-action] [foo-type-1,foo-type-2] in [foo-space,bar-space]: missing [(foo-space)saved_object:foo-type-1/foo-action,(foo-space)saved_object:foo-type-2/foo-action]"`
);
});
});
@ -47,22 +68,27 @@ describe(`#savedObjectsAuthorizationSuccess`, () => {
const username = 'foo-user';
const action = 'foo-action';
const types = ['foo-type-1', 'foo-type-2'];
const spaceIds = ['foo-space', 'bar-space'];
const args = {
foo: 'bar',
baz: 'quz',
};
securityAuditLogger.savedObjectsAuthorizationSuccess(username, action, types, args);
securityAuditLogger.savedObjectsAuthorizationSuccess(username, action, types, spaceIds, args);
expect(auditLogger.log).toHaveBeenCalledWith(
'saved_objects_authorization_success',
expect.stringContaining(`${username} authorized to ${action}`),
expect.any(String),
{
username,
action,
types,
spaceIds,
args,
}
);
expect(auditLogger.log.mock.calls[0][1]).toMatchInlineSnapshot(
`"foo-user authorized to [foo-action] [foo-type-1,foo-type-2] in [foo-space,bar-space]"`
);
});
});

View file

@ -13,16 +13,23 @@ export class SecurityAuditLogger {
username: string,
action: string,
types: string[],
missing: string[],
spaceIds: string[],
missing: Array<{ spaceId?: string; privilege: string }>,
args?: Record<string, unknown>
) {
const typesString = types.join(',');
const spacesString = spaceIds.length ? ` in [${spaceIds.join(',')}]` : '';
const missingString = missing
.map(({ spaceId, privilege }) => `${spaceId ? `(${spaceId})` : ''}${privilege}`)
.join(',');
this.getAuditLogger().log(
'saved_objects_authorization_failure',
`${username} unauthorized to ${action} ${types.join(',')}, missing ${missing.join(',')}`,
`${username} unauthorized to [${action}] [${typesString}]${spacesString}: missing [${missingString}]`,
{
username,
action,
types,
spaceIds,
missing,
args,
}
@ -33,15 +40,19 @@ export class SecurityAuditLogger {
username: string,
action: string,
types: string[],
spaceIds: string[],
args?: Record<string, unknown>
) {
const typesString = types.join(',');
const spacesString = spaceIds.length ? ` in [${spaceIds.join(',')}]` : '';
this.getAuditLogger().log(
'saved_objects_authorization_success',
`${username} authorized to ${action} ${types.join(',')}`,
`${username} authorized to [${action}] [${typesString}]${spacesString}`,
{
username,
action,
types,
spaceIds,
args,
}
);

View file

@ -1,27 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`#atSpace throws error when checking for login and user has login but doesn't have version 1`] = `[Error: Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.]`;
exports[`#atSpace with a malformed Elasticsearch response throws a validation error when an extra privilege is present in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_1" fails because ["saved_object:bar-type/get" is not allowed]]]]`;
exports[`#atSpace with a malformed Elasticsearch response throws a validation error when privileges are missing in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_1" fails because [child "saved_object:foo-type/get" fails because ["saved_object:foo-type/get" is required]]]]]`;
exports[`#atSpaces throws error when Elasticsearch returns malformed response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_1" fails because [child "mock-action:version" fails because ["mock-action:version" is required]]]]]`;
exports[`#atSpaces throws error when checking for login and user has login but doesn't have version 1`] = `[Error: Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.]`;
exports[`#atSpaces with a malformed Elasticsearch response throws a validation error when an a space is missing in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_2" fails because ["space:space_2" is required]]]]`;
exports[`#atSpaces with a malformed Elasticsearch response throws a validation error when an extra privilege is present in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_2" fails because ["space:space_2" is required]]]]`;
exports[`#atSpaces with a malformed Elasticsearch response throws a validation error when an extra space is present in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because ["space:space_3" is not allowed]]]`;
exports[`#atSpaces with a malformed Elasticsearch response throws a validation error when privileges are missing in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_2" fails because ["space:space_2" is required]]]]`;
exports[`#globally throws error when Elasticsearch returns malformed response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "*" fails because [child "mock-action:version" fails because ["mock-action:version" is required]]]]]`;
exports[`#globally throws error when checking for login and user has login but doesn't have version 1`] = `[Error: Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.]`;
exports[`#globally with a malformed Elasticsearch response throws a validation error when an extra privilege is present in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "*" fails because ["saved_object:bar-type/get" is not allowed]]]]`;
exports[`#globally with a malformed Elasticsearch response throws a validation error when privileges are missing in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "*" fails because [child "saved_object:foo-type/get" fails because ["saved_object:foo-type/get" is required]]]]]`;

View file

@ -16,32 +16,17 @@ interface CheckPrivilegesActions {
version: string;
}
interface CheckPrivilegesAtResourcesResponse {
export interface CheckPrivilegesResponse {
hasAllRequested: boolean;
username: string;
resourcePrivileges: {
[resource: string]: {
[privilege: string]: boolean;
};
};
}
export interface CheckPrivilegesAtResourceResponse {
hasAllRequested: boolean;
username: string;
privileges: {
[privilege: string]: boolean;
};
}
export interface CheckPrivilegesAtSpacesResponse {
hasAllRequested: boolean;
username: string;
spacePrivileges: {
[spaceId: string]: {
[privilege: string]: boolean;
};
};
privileges: Array<{
/**
* If this attribute is undefined, this element is a privilege for the global resource.
*/
resource?: string;
privilege: string;
authorized: boolean;
}>;
}
export type CheckPrivilegesWithRequest = (request: KibanaRequest) => CheckPrivileges;
@ -50,12 +35,12 @@ export interface CheckPrivileges {
atSpace(
spaceId: string,
privilegeOrPrivileges: string | string[]
): Promise<CheckPrivilegesAtResourceResponse>;
): Promise<CheckPrivilegesResponse>;
atSpaces(
spaceIds: string[],
privilegeOrPrivileges: string | string[]
): Promise<CheckPrivilegesAtSpacesResponse>;
globally(privilegeOrPrivileges: string | string[]): Promise<CheckPrivilegesAtResourceResponse>;
): Promise<CheckPrivilegesResponse>;
globally(privilegeOrPrivileges: string | string[]): Promise<CheckPrivilegesResponse>;
}
export function checkPrivilegesWithRequestFactory(
@ -75,7 +60,7 @@ export function checkPrivilegesWithRequestFactory(
const checkPrivilegesAtResources = async (
resources: string[],
privilegeOrPrivileges: string | string[]
): Promise<CheckPrivilegesAtResourcesResponse> => {
): Promise<CheckPrivilegesResponse> => {
const privileges = Array.isArray(privilegeOrPrivileges)
? privilegeOrPrivileges
: [privilegeOrPrivileges];
@ -106,55 +91,43 @@ export function checkPrivilegesWithRequestFactory(
);
}
// we need to filter out the non requested privileges from the response
const resourcePrivileges = transform(applicationPrivilegesResponse, (result, value, key) => {
result[key!] = pick(value, privileges);
}) as HasPrivilegesResponseApplication;
const privilegeArray = Object.entries(resourcePrivileges)
.map(([key, val]) => {
// we need to turn the resource responses back into the space ids
const resource =
key !== GLOBAL_RESOURCE ? ResourceSerializer.deserializeSpaceResource(key!) : undefined;
return Object.entries(val).map(([privilege, authorized]) => ({
resource,
privilege,
authorized,
}));
})
.flat();
return {
hasAllRequested: hasPrivilegesResponse.has_all_requested,
username: hasPrivilegesResponse.username,
// we need to filter out the non requested privileges from the response
resourcePrivileges: transform(applicationPrivilegesResponse, (result, value, key) => {
result[key!] = pick(value, privileges);
}),
};
};
const checkPrivilegesAtResource = async (
resource: string,
privilegeOrPrivileges: string | string[]
) => {
const { hasAllRequested, username, resourcePrivileges } = await checkPrivilegesAtResources(
[resource],
privilegeOrPrivileges
);
return {
hasAllRequested,
username,
privileges: resourcePrivileges[resource],
privileges: privilegeArray,
};
};
return {
async atSpace(spaceId: string, privilegeOrPrivileges: string | string[]) {
const spaceResource = ResourceSerializer.serializeSpaceResource(spaceId);
return await checkPrivilegesAtResource(spaceResource, privilegeOrPrivileges);
return await checkPrivilegesAtResources([spaceResource], privilegeOrPrivileges);
},
async atSpaces(spaceIds: string[], privilegeOrPrivileges: string | string[]) {
const spaceResources = spaceIds.map(spaceId =>
ResourceSerializer.serializeSpaceResource(spaceId)
);
const { hasAllRequested, username, resourcePrivileges } = await checkPrivilegesAtResources(
spaceResources,
privilegeOrPrivileges
);
return {
hasAllRequested,
username,
// we need to turn the resource responses back into the space ids
spacePrivileges: transform(resourcePrivileges, (result, value, key) => {
result[ResourceSerializer.deserializeSpaceResource(key!)] = value;
}),
};
return await checkPrivilegesAtResources(spaceResources, privilegeOrPrivileges);
},
async globally(privilegeOrPrivileges: string | string[]) {
return await checkPrivilegesAtResource(GLOBAL_RESOURCE, privilegeOrPrivileges);
return await checkPrivilegesAtResources([GLOBAL_RESOURCE], privilegeOrPrivileges);
},
};
};

View file

@ -6,11 +6,11 @@
import { KibanaRequest } from '../../../../../src/core/server';
import { SpacesService } from '../plugin';
import { CheckPrivilegesAtResourceResponse, CheckPrivilegesWithRequest } from './check_privileges';
import { CheckPrivilegesResponse, CheckPrivilegesWithRequest } from './check_privileges';
export type CheckPrivilegesDynamically = (
privilegeOrPrivileges: string | string[]
) => Promise<CheckPrivilegesAtResourceResponse>;
) => Promise<CheckPrivilegesResponse>;
export type CheckPrivilegesDynamicallyWithRequest = (
request: KibanaRequest

View file

@ -7,57 +7,113 @@
import { checkSavedObjectsPrivilegesWithRequestFactory } from './check_saved_objects_privileges';
import { httpServerMock } from '../../../../../src/core/server/mocks';
import { CheckPrivileges, CheckPrivilegesWithRequest } from './check_privileges';
import { SpacesService } from '../plugin';
test(`checkPrivileges.atSpace when spaces is enabled`, async () => {
const expectedResult = Symbol();
const spaceId = 'foo-space';
const mockCheckPrivileges = {
atSpace: jest.fn().mockReturnValue(expectedResult),
};
const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges);
const request = httpServerMock.createKibanaRequest();
const privilegeOrPrivileges = ['foo', 'bar'];
const mockSpacesService = {
getSpaceId: jest.fn(),
namespaceToSpaceId: jest.fn().mockReturnValue(spaceId),
};
let mockCheckPrivileges: jest.Mocked<CheckPrivileges>;
let mockCheckPrivilegesWithRequest: jest.Mocked<CheckPrivilegesWithRequest>;
let mockSpacesService: jest.Mocked<SpacesService> | undefined;
const request = httpServerMock.createKibanaRequest();
const checkSavedObjectsPrivileges = checkSavedObjectsPrivilegesWithRequestFactory(
const createFactory = () =>
checkSavedObjectsPrivilegesWithRequestFactory(
mockCheckPrivilegesWithRequest,
() => mockSpacesService
)(request);
const namespace = 'foo';
const result = await checkSavedObjectsPrivileges(privilegeOrPrivileges, namespace);
expect(result).toBe(expectedResult);
expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request);
expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, privilegeOrPrivileges);
expect(mockSpacesService.namespaceToSpaceId).toBeCalledWith(namespace);
});
test(`checkPrivileges.globally when spaces is disabled`, async () => {
const expectedResult = Symbol();
const mockCheckPrivileges = {
globally: jest.fn().mockReturnValue(expectedResult),
beforeEach(() => {
mockCheckPrivileges = {
atSpace: jest.fn(),
atSpaces: jest.fn(),
globally: jest.fn(),
};
const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges);
mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges);
const request = httpServerMock.createKibanaRequest();
const privilegeOrPrivileges = ['foo', 'bar'];
const checkSavedObjectsPrivileges = checkSavedObjectsPrivilegesWithRequestFactory(
mockCheckPrivilegesWithRequest,
() => undefined
)(request);
const namespace = 'foo';
const result = await checkSavedObjectsPrivileges(privilegeOrPrivileges, namespace);
expect(result).toBe(expectedResult);
expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request);
expect(mockCheckPrivileges.globally).toHaveBeenCalledWith(privilegeOrPrivileges);
mockSpacesService = {
getSpaceId: jest.fn(),
namespaceToSpaceId: jest.fn().mockImplementation((namespace: string) => `${namespace}-id`),
};
});
describe('#checkSavedObjectsPrivileges', () => {
const actions = ['foo', 'bar'];
const namespace1 = 'baz';
const namespace2 = 'qux';
describe('when checking multiple namespaces', () => {
const namespaces = [namespace1, namespace2];
test(`throws an error when Spaces is disabled`, async () => {
mockSpacesService = undefined;
const checkSavedObjectsPrivileges = createFactory();
await expect(
checkSavedObjectsPrivileges(actions, namespaces)
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Can't check saved object privileges for multiple namespaces if Spaces is disabled"`
);
});
test(`throws an error when using an empty namespaces array`, async () => {
const checkSavedObjectsPrivileges = createFactory();
await expect(
checkSavedObjectsPrivileges(actions, [])
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Can't check saved object privileges for 0 namespaces"`
);
});
test(`uses checkPrivileges.atSpaces when spaces is enabled`, async () => {
const expectedResult = Symbol();
mockCheckPrivileges.atSpaces.mockReturnValue(expectedResult as any);
const checkSavedObjectsPrivileges = createFactory();
const result = await checkSavedObjectsPrivileges(actions, namespaces);
expect(result).toBe(expectedResult);
expect(mockSpacesService!.namespaceToSpaceId).toHaveBeenCalledTimes(2);
expect(mockSpacesService!.namespaceToSpaceId).toHaveBeenNthCalledWith(1, namespace1);
expect(mockSpacesService!.namespaceToSpaceId).toHaveBeenNthCalledWith(2, namespace2);
expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledTimes(1);
expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request);
expect(mockCheckPrivileges.atSpaces).toHaveBeenCalledTimes(1);
const spaceIds = mockSpacesService!.namespaceToSpaceId.mock.results.map(x => x.value);
expect(mockCheckPrivileges.atSpaces).toHaveBeenCalledWith(spaceIds, actions);
});
});
describe('when checking a single namespace', () => {
test(`uses checkPrivileges.atSpace when Spaces is enabled`, async () => {
const expectedResult = Symbol();
mockCheckPrivileges.atSpace.mockReturnValue(expectedResult as any);
const checkSavedObjectsPrivileges = createFactory();
const result = await checkSavedObjectsPrivileges(actions, namespace1);
expect(result).toBe(expectedResult);
expect(mockSpacesService!.namespaceToSpaceId).toHaveBeenCalledTimes(1);
expect(mockSpacesService!.namespaceToSpaceId).toHaveBeenCalledWith(namespace1);
expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledTimes(1);
expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request);
expect(mockCheckPrivileges.atSpace).toHaveBeenCalledTimes(1);
const spaceId = mockSpacesService!.namespaceToSpaceId.mock.results[0].value;
expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, actions);
});
test(`uses checkPrivileges.globally when Spaces is disabled`, async () => {
const expectedResult = Symbol();
mockCheckPrivileges.globally.mockReturnValue(expectedResult as any);
mockSpacesService = undefined;
const checkSavedObjectsPrivileges = createFactory();
const result = await checkSavedObjectsPrivileges(actions, namespace1);
expect(result).toBe(expectedResult);
expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledTimes(1);
expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request);
expect(mockCheckPrivileges.globally).toHaveBeenCalledTimes(1);
expect(mockCheckPrivileges.globally).toHaveBeenCalledWith(actions);
});
});
});

View file

@ -6,32 +6,44 @@
import { KibanaRequest } from '../../../../../src/core/server';
import { SpacesService } from '../plugin';
import { CheckPrivilegesAtResourceResponse, CheckPrivilegesWithRequest } from './check_privileges';
import { CheckPrivilegesWithRequest, CheckPrivilegesResponse } from './check_privileges';
export type CheckSavedObjectsPrivilegesWithRequest = (
request: KibanaRequest
) => CheckSavedObjectsPrivileges;
export type CheckSavedObjectsPrivileges = (
actions: string | string[],
namespace?: string
) => Promise<CheckPrivilegesAtResourceResponse>;
namespaceOrNamespaces?: string | string[]
) => Promise<CheckPrivilegesResponse>;
export const checkSavedObjectsPrivilegesWithRequestFactory = (
checkPrivilegesWithRequest: CheckPrivilegesWithRequest,
getSpacesService: () => SpacesService | undefined
): CheckSavedObjectsPrivilegesWithRequest => {
return function checkSavedObjectsPrivilegesWithRequest(request: KibanaRequest) {
return function checkSavedObjectsPrivilegesWithRequest(
request: KibanaRequest
): CheckSavedObjectsPrivileges {
return async function checkSavedObjectsPrivileges(
actions: string | string[],
namespace?: string
namespaceOrNamespaces?: string | string[]
) {
const spacesService = getSpacesService();
return spacesService
? await checkPrivilegesWithRequest(request).atSpace(
spacesService.namespaceToSpaceId(namespace),
actions
)
: await checkPrivilegesWithRequest(request).globally(actions);
if (Array.isArray(namespaceOrNamespaces)) {
if (spacesService === undefined) {
throw new Error(
`Can't check saved object privileges for multiple namespaces if Spaces is disabled`
);
} else if (!namespaceOrNamespaces.length) {
throw new Error(`Can't check saved object privileges for 0 namespaces`);
}
const spaceIds = namespaceOrNamespaces.map(x => spacesService.namespaceToSpaceId(x));
return await checkPrivilegesWithRequest(request).atSpaces(spaceIds, actions);
} else if (spacesService) {
const spaceId = spacesService.namespaceToSpaceId(namespaceOrNamespaces);
return await checkPrivilegesWithRequest(request).atSpace(spaceId, actions);
}
return await checkPrivilegesWithRequest(request).globally(actions);
};
};
};

View file

@ -11,7 +11,9 @@ import { httpServerMock, loggingServiceMock } from '../../../../../src/core/serv
import { authorizationMock } from './index.mock';
import { Feature } from '../../../features/server';
type MockAuthzOptions = { rejectCheckPrivileges: any } | { resolveCheckPrivileges: any };
type MockAuthzOptions =
| { rejectCheckPrivileges: any }
| { resolveCheckPrivileges: { privileges: Array<{ privilege: string; authorized: boolean }> } };
const actions = new Actions('1.0.0-zeta1');
const mockRequest = httpServerMock.createKibanaRequest();
@ -26,7 +28,8 @@ const createMockAuthz = (options: MockAuthzOptions) => {
throw options.rejectCheckPrivileges;
}
expect(checkActions).toEqual(Object.keys(options.resolveCheckPrivileges.privileges));
const expected = options.resolveCheckPrivileges.privileges.map(x => x.privilege);
expect(checkActions).toEqual(expected);
return options.resolveCheckPrivileges;
});
});
@ -226,17 +229,17 @@ describe('usingPrivileges', () => {
test(`disables ui capabilities when they don't have privileges`, async () => {
const mockAuthz = createMockAuthz({
resolveCheckPrivileges: {
privileges: {
[actions.ui.get('navLinks', 'foo')]: true,
[actions.ui.get('navLinks', 'bar')]: false,
[actions.ui.get('navLinks', 'quz')]: false,
[actions.ui.get('management', 'kibana', 'indices')]: true,
[actions.ui.get('management', 'kibana', 'settings')]: false,
[actions.ui.get('fooFeature', 'foo')]: true,
[actions.ui.get('fooFeature', 'bar')]: false,
[actions.ui.get('barFeature', 'foo')]: true,
[actions.ui.get('barFeature', 'bar')]: false,
},
privileges: [
{ privilege: actions.ui.get('navLinks', 'foo'), authorized: true },
{ privilege: actions.ui.get('navLinks', 'bar'), authorized: false },
{ privilege: actions.ui.get('navLinks', 'quz'), authorized: false },
{ privilege: actions.ui.get('management', 'kibana', 'indices'), authorized: true },
{ privilege: actions.ui.get('management', 'kibana', 'settings'), authorized: false },
{ privilege: actions.ui.get('fooFeature', 'foo'), authorized: true },
{ privilege: actions.ui.get('fooFeature', 'bar'), authorized: false },
{ privilege: actions.ui.get('barFeature', 'foo'), authorized: true },
{ privilege: actions.ui.get('barFeature', 'bar'), authorized: false },
],
},
});
@ -314,15 +317,15 @@ describe('usingPrivileges', () => {
test(`doesn't re-enable disabled uiCapabilities`, async () => {
const mockAuthz = createMockAuthz({
resolveCheckPrivileges: {
privileges: {
[actions.ui.get('navLinks', 'foo')]: true,
[actions.ui.get('navLinks', 'bar')]: true,
[actions.ui.get('management', 'kibana', 'indices')]: true,
[actions.ui.get('fooFeature', 'foo')]: true,
[actions.ui.get('fooFeature', 'bar')]: true,
[actions.ui.get('barFeature', 'foo')]: true,
[actions.ui.get('barFeature', 'bar')]: true,
},
privileges: [
{ privilege: actions.ui.get('navLinks', 'foo'), authorized: true },
{ privilege: actions.ui.get('navLinks', 'bar'), authorized: true },
{ privilege: actions.ui.get('management', 'kibana', 'indices'), authorized: true },
{ privilege: actions.ui.get('fooFeature', 'foo'), authorized: true },
{ privilege: actions.ui.get('fooFeature', 'bar'), authorized: true },
{ privilege: actions.ui.get('barFeature', 'foo'), authorized: true },
{ privilege: actions.ui.get('barFeature', 'bar'), authorized: true },
],
},
});

View file

@ -9,7 +9,7 @@ import { UICapabilities } from 'ui/capabilities';
import { KibanaRequest, Logger } from '../../../../../src/core/server';
import { Feature } from '../../../features/server';
import { CheckPrivilegesAtResourceResponse } from './check_privileges';
import { CheckPrivilegesResponse } from './check_privileges';
import { Authorization } from './index';
export function disableUICapabilitiesFactory(
@ -77,7 +77,7 @@ export function disableUICapabilitiesFactory(
[]
);
let checkPrivilegesResponse: CheckPrivilegesAtResourceResponse;
let checkPrivilegesResponse: CheckPrivilegesResponse;
try {
const checkPrivilegesDynamically = authz.checkPrivilegesDynamicallyWithRequest(request);
checkPrivilegesResponse = await checkPrivilegesDynamically(uiActions);
@ -105,7 +105,9 @@ export function disableUICapabilitiesFactory(
}
const action = authz.actions.ui.get(featureId, ...uiCapabilityParts);
return checkPrivilegesResponse.privileges[action] === true;
return checkPrivilegesResponse.privileges.some(
x => x.privilege === action && x.authorized === true
);
};
return mapValues(uiCapabilities, (featureUICapabilities, featureId) => {

View file

@ -149,6 +149,7 @@ export class Plugin {
auditLogger: new SecurityAuditLogger(() => this.getLegacyAPI().auditLogger),
authz,
savedObjects: core.savedObjects,
getSpacesService: this.getSpacesService,
});
core.capabilities.registerSwitcher(authz.disableUnauthorizedCapabilities);

View file

@ -13,14 +13,21 @@ import {
import { SecureSavedObjectsClientWrapper } from './secure_saved_objects_client_wrapper';
import { Authorization } from '../authorization';
import { SecurityAuditLogger } from '../audit';
import { SpacesService } from '../plugin';
interface SetupSavedObjectsParams {
auditLogger: SecurityAuditLogger;
authz: Pick<Authorization, 'mode' | 'actions' | 'checkSavedObjectsPrivilegesWithRequest'>;
savedObjects: CoreSetup['savedObjects'];
getSpacesService(): SpacesService | undefined;
}
export function setupSavedObjects({ auditLogger, authz, savedObjects }: SetupSavedObjectsParams) {
export function setupSavedObjects({
auditLogger,
authz,
savedObjects,
getSpacesService,
}: SetupSavedObjectsParams) {
const getKibanaRequest = (request: KibanaRequest | LegacyRequest) =>
request instanceof KibanaRequest ? request : KibanaRequest.from(request);
@ -44,6 +51,7 @@ export function setupSavedObjects({ auditLogger, authz, savedObjects }: SetupSav
kibanaRequest
),
errors: SavedObjectsClient.errors,
getSpacesService,
})
: client;
});

View file

@ -13,9 +13,13 @@ import {
SavedObjectsCreateOptions,
SavedObjectsFindOptions,
SavedObjectsUpdateOptions,
SavedObjectsAddToNamespacesOptions,
SavedObjectsDeleteFromNamespacesOptions,
} from '../../../../../src/core/server';
import { SecurityAuditLogger } from '../audit';
import { Actions, CheckSavedObjectsPrivileges } from '../authorization';
import { CheckPrivilegesResponse } from '../authorization/check_privileges';
import { SpacesService } from '../plugin';
interface SecureSavedObjectsClientWrapperOptions {
actions: Actions;
@ -23,6 +27,19 @@ interface SecureSavedObjectsClientWrapperOptions {
baseClient: SavedObjectsClientContract;
errors: SavedObjectsClientContract['errors'];
checkSavedObjectsPrivilegesAsCurrentUser: CheckSavedObjectsPrivileges;
getSpacesService(): SpacesService | undefined;
}
interface SavedObjectNamespaces {
namespaces?: string[];
}
interface SavedObjectsNamespaces {
saved_objects: SavedObjectNamespaces[];
}
function uniq<T>(arr: T[]): T[] {
return Array.from(new Set<T>(arr));
}
export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContract {
@ -30,19 +47,23 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
private readonly auditLogger: PublicMethodsOf<SecurityAuditLogger>;
private readonly baseClient: SavedObjectsClientContract;
private readonly checkSavedObjectsPrivilegesAsCurrentUser: CheckSavedObjectsPrivileges;
private getSpacesService: () => SpacesService | undefined;
public readonly errors: SavedObjectsClientContract['errors'];
constructor({
actions,
auditLogger,
baseClient,
checkSavedObjectsPrivilegesAsCurrentUser,
errors,
getSpacesService,
}: SecureSavedObjectsClientWrapperOptions) {
this.errors = errors;
this.actions = actions;
this.auditLogger = auditLogger;
this.baseClient = baseClient;
this.checkSavedObjectsPrivilegesAsCurrentUser = checkSavedObjectsPrivilegesAsCurrentUser;
this.getSpacesService = getSpacesService;
}
public async create<T = unknown>(
@ -52,7 +73,8 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
) {
await this.ensureAuthorized(type, 'create', options.namespace, { type, attributes, options });
return await this.baseClient.create(type, attributes, options);
const savedObject = await this.baseClient.create(type, attributes, options);
return await this.redactSavedObjectNamespaces(savedObject);
}
public async bulkCreate<T = unknown>(
@ -66,7 +88,8 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
{ objects, options }
);
return await this.baseClient.bulkCreate(objects, options);
const response = await this.baseClient.bulkCreate(objects, options);
return await this.redactSavedObjectsNamespaces(response);
}
public async delete(type: string, id: string, options: SavedObjectsBaseOptions = {}) {
@ -78,7 +101,8 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
public async find<T = unknown>(options: SavedObjectsFindOptions) {
await this.ensureAuthorized(options.type, 'find', options.namespace, { options });
return this.baseClient.find<T>(options);
const response = await this.baseClient.find<T>(options);
return await this.redactSavedObjectsNamespaces(response);
}
public async bulkGet<T = unknown>(
@ -90,13 +114,15 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
options,
});
return await this.baseClient.bulkGet<T>(objects, options);
const response = await this.baseClient.bulkGet<T>(objects, options);
return await this.redactSavedObjectsNamespaces(response);
}
public async get<T = unknown>(type: string, id: string, options: SavedObjectsBaseOptions = {}) {
await this.ensureAuthorized(type, 'get', options.namespace, { type, id, options });
return await this.baseClient.get<T>(type, id, options);
const savedObject = await this.baseClient.get<T>(type, id, options);
return await this.redactSavedObjectNamespaces(savedObject);
}
public async update<T = unknown>(
@ -105,14 +131,44 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
attributes: Partial<T>,
options: SavedObjectsUpdateOptions = {}
) {
await this.ensureAuthorized(type, 'update', options.namespace, {
type,
id,
attributes,
options,
});
const args = { type, id, attributes, options };
await this.ensureAuthorized(type, 'update', options.namespace, args);
return await this.baseClient.update(type, id, attributes, options);
const savedObject = await this.baseClient.update(type, id, attributes, options);
return await this.redactSavedObjectNamespaces(savedObject);
}
public async addToNamespaces(
type: string,
id: string,
namespaces: string[],
options: SavedObjectsAddToNamespacesOptions = {}
) {
const args = { type, id, namespaces, options };
const { namespace } = options;
// To share an object, the user must have the "create" permission in each of the destination namespaces.
await this.ensureAuthorized(type, 'create', namespaces, args, 'addToNamespacesCreate');
// To share an object, the user must also have the "update" permission in one or more of the source namespaces. Because the
// `addToNamespaces` operation is scoped to the current namespace, we can just check if the user has the "update" permission in the
// current namespace. If the user has permission, but the saved object doesn't exist in this namespace, the base client operation will
// result in a 404 error.
await this.ensureAuthorized(type, 'update', namespace, args, 'addToNamespacesUpdate');
return await this.baseClient.addToNamespaces(type, id, namespaces, options);
}
public async deleteFromNamespaces(
type: string,
id: string,
namespaces: string[],
options: SavedObjectsDeleteFromNamespacesOptions = {}
) {
const args = { type, id, namespaces, options };
// To un-share an object, the user must have the "delete" permission in each of the target namespaces.
await this.ensureAuthorized(type, 'delete', namespaces, args, 'deleteFromNamespaces');
return await this.baseClient.deleteFromNamespaces(type, id, namespaces, options);
}
public async bulkUpdate<T = unknown>(
@ -126,12 +182,16 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
{ objects, options }
);
return await this.baseClient.bulkUpdate<T>(objects, options);
const response = await this.baseClient.bulkUpdate<T>(objects, options);
return await this.redactSavedObjectsNamespaces(response);
}
private async checkPrivileges(actions: string | string[], namespace?: string) {
private async checkPrivileges(
actions: string | string[],
namespaceOrNamespaces?: string | string[]
) {
try {
return await this.checkSavedObjectsPrivilegesAsCurrentUser(actions, namespace);
return await this.checkSavedObjectsPrivilegesAsCurrentUser(actions, namespaceOrNamespaces);
} catch (error) {
throw this.errors.decorateGeneralError(error, error.body && error.body.reason);
}
@ -140,43 +200,133 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
private async ensureAuthorized(
typeOrTypes: string | string[],
action: string,
namespace?: string,
args?: Record<string, unknown>
namespaceOrNamespaces?: string | string[],
args?: Record<string, unknown>,
auditAction: string = action,
requiresAll = true
) {
const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes];
const actionsToTypesMap = new Map(
types.map(type => [this.actions.savedObject.get(type, action), type])
);
const actions = Array.from(actionsToTypesMap.keys());
const { hasAllRequested, username, privileges } = await this.checkPrivileges(
actions,
namespace
);
const result = await this.checkPrivileges(actions, namespaceOrNamespaces);
if (hasAllRequested) {
this.auditLogger.savedObjectsAuthorizationSuccess(username, action, types, args);
const { hasAllRequested, username, privileges } = result;
const spaceIds = uniq(
privileges.map(({ resource }) => resource).filter(x => x !== undefined)
).sort() as string[];
const isAuthorized =
(requiresAll && hasAllRequested) ||
(!requiresAll && privileges.some(({ authorized }) => authorized));
if (isAuthorized) {
this.auditLogger.savedObjectsAuthorizationSuccess(
username,
auditAction,
types,
spaceIds,
args
);
} else {
const missingPrivileges = this.getMissingPrivileges(privileges);
this.auditLogger.savedObjectsAuthorizationFailure(
username,
action,
auditAction,
types,
spaceIds,
missingPrivileges,
args
);
const msg = `Unable to ${action} ${missingPrivileges
.map(privilege => actionsToTypesMap.get(privilege))
.sort()
.join(',')}`;
const targetTypes = uniq(
missingPrivileges.map(({ privilege }) => actionsToTypesMap.get(privilege)).sort()
).join(',');
const msg = `Unable to ${action} ${targetTypes}`;
throw this.errors.decorateForbiddenError(new Error(msg));
}
}
private getMissingPrivileges(privileges: Record<string, boolean>) {
return Object.keys(privileges).filter(privilege => !privileges[privilege]);
private getMissingPrivileges(privileges: CheckPrivilegesResponse['privileges']) {
return privileges
.filter(({ authorized }) => !authorized)
.map(({ resource, privilege }) => ({ spaceId: resource, privilege }));
}
private getUniqueObjectTypes(objects: Array<{ type: string }>) {
return [...new Set(objects.map(o => o.type))];
return uniq(objects.map(o => o.type));
}
private async getNamespacesPrivilegeMap(namespaces: string[]) {
const action = this.actions.login;
const checkPrivilegesResult = await this.checkPrivileges(action, namespaces);
// check if the user can log into each namespace
const map = checkPrivilegesResult.privileges.reduce(
(acc: Record<string, boolean>, { resource, authorized }) => {
// there should never be a case where more than one privilege is returned for a given space
// if there is, fail-safe (authorized + unauthorized = unauthorized)
if (resource && (!authorized || !acc.hasOwnProperty(resource))) {
acc[resource] = authorized;
}
return acc;
},
{}
);
return map;
}
private redactAndSortNamespaces(spaceIds: string[], privilegeMap: Record<string, boolean>) {
const comparator = (a: string, b: string) => {
const _a = a.toLowerCase();
const _b = b.toLowerCase();
if (_a === '?') {
return 1;
} else if (_a < _b) {
return -1;
} else if (_a > _b) {
return 1;
}
return 0;
};
return spaceIds.map(spaceId => (privilegeMap[spaceId] ? spaceId : '?')).sort(comparator);
}
private async redactSavedObjectNamespaces<T extends SavedObjectNamespaces>(
savedObject: T
): Promise<T> {
if (this.getSpacesService() === undefined || savedObject.namespaces == null) {
return savedObject;
}
const privilegeMap = await this.getNamespacesPrivilegeMap(savedObject.namespaces);
return {
...savedObject,
namespaces: this.redactAndSortNamespaces(savedObject.namespaces, privilegeMap),
};
}
private async redactSavedObjectsNamespaces<T extends SavedObjectsNamespaces>(
response: T
): Promise<T> {
if (this.getSpacesService() === undefined) {
return response;
}
const { saved_objects: savedObjects } = response;
const namespaces = uniq(savedObjects.flatMap(savedObject => savedObject.namespaces || []));
if (namespaces.length === 0) {
return response;
}
const privilegeMap = await this.getNamespacesPrivilegeMap(namespaces);
return {
...response,
saved_objects: savedObjects.map(savedObject => ({
...savedObject,
namespaces:
savedObject.namespaces &&
this.redactAndSortNamespaces(savedObject.namespaces, privilegeMap),
})),
};
}
}

View file

@ -99,10 +99,14 @@ export class DeleteSpacesButton extends Component<Props, State> {
public deleteSpaces = async () => {
const { spacesManager, space } = this.props;
this.setState({
showConfirmDeleteModal: false,
});
try {
await spacesManager.deleteSpace(space);
} catch (error) {
const { message: errorMessage = '' } = error.data || {};
const { message: errorMessage = '' } = error.data || error.body || {};
this.props.notifications.toasts.addDanger(
i18n.translate('xpack.spaces.management.deleteSpacesButton.deleteSpaceErrorTitle', {
@ -110,12 +114,9 @@ export class DeleteSpacesButton extends Component<Props, State> {
values: { errorMessage },
})
);
return;
}
this.setState({
showConfirmDeleteModal: false,
});
const message = i18n.translate(
'xpack.spaces.management.deleteSpacesButton.spaceSuccessfullyDeletedNotificationMessage',
{

View file

@ -176,10 +176,14 @@ export class SpacesGridPage extends Component<Props, State> {
return;
}
this.setState({
showConfirmDeleteModal: false,
});
try {
await spacesManager.deleteSpace(space);
} catch (error) {
const { message: errorMessage = '' } = error.data || {};
const { message: errorMessage = '' } = error.data || error.body || {};
this.props.notifications.toasts.addDanger(
i18n.translate('xpack.spaces.management.spacesGridPage.errorDeletingSpaceErrorMessage', {
@ -189,12 +193,9 @@ export class SpacesGridPage extends Component<Props, State> {
},
})
);
return;
}
this.setState({
showConfirmDeleteModal: false,
});
this.loadGrid();
const message = i18n.translate(

View file

@ -188,11 +188,13 @@ describe('copySavedObjectsToSpaces', () => {
},
],
"savedObjectsClient": Object {
"addToNamespaces": [MockFunction],
"bulkCreate": [MockFunction],
"bulkGet": [MockFunction],
"bulkUpdate": [MockFunction],
"create": [MockFunction],
"delete": [MockFunction],
"deleteFromNamespaces": [MockFunction],
"errors": [Function],
"find": [MockFunction],
"get": [MockFunction],
@ -252,11 +254,13 @@ describe('copySavedObjectsToSpaces', () => {
"readable": false,
},
"savedObjectsClient": Object {
"addToNamespaces": [MockFunction],
"bulkCreate": [MockFunction],
"bulkGet": [MockFunction],
"bulkUpdate": [MockFunction],
"create": [MockFunction],
"delete": [MockFunction],
"deleteFromNamespaces": [MockFunction],
"errors": [Function],
"find": [MockFunction],
"get": [MockFunction],
@ -315,11 +319,13 @@ describe('copySavedObjectsToSpaces', () => {
"readable": false,
},
"savedObjectsClient": Object {
"addToNamespaces": [MockFunction],
"bulkCreate": [MockFunction],
"bulkGet": [MockFunction],
"bulkUpdate": [MockFunction],
"create": [MockFunction],
"delete": [MockFunction],
"deleteFromNamespaces": [MockFunction],
"errors": [Function],
"find": [MockFunction],
"get": [MockFunction],

View file

@ -204,11 +204,13 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => {
},
],
"savedObjectsClient": Object {
"addToNamespaces": [MockFunction],
"bulkCreate": [MockFunction],
"bulkGet": [MockFunction],
"bulkUpdate": [MockFunction],
"create": [MockFunction],
"delete": [MockFunction],
"deleteFromNamespaces": [MockFunction],
"errors": [Function],
"find": [MockFunction],
"get": [MockFunction],
@ -275,11 +277,13 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => {
},
],
"savedObjectsClient": Object {
"addToNamespaces": [MockFunction],
"bulkCreate": [MockFunction],
"bulkGet": [MockFunction],
"bulkUpdate": [MockFunction],
"create": [MockFunction],
"delete": [MockFunction],
"deleteFromNamespaces": [MockFunction],
"errors": [Function],
"find": [MockFunction],
"get": [MockFunction],
@ -345,11 +349,13 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => {
},
],
"savedObjectsClient": Object {
"addToNamespaces": [MockFunction],
"bulkCreate": [MockFunction],
"bulkGet": [MockFunction],
"bulkUpdate": [MockFunction],
"create": [MockFunction],
"delete": [MockFunction],
"deleteFromNamespaces": [MockFunction],
"errors": [Function],
"find": [MockFunction],
"get": [MockFunction],

View file

@ -250,14 +250,10 @@ describe('#getAll', () => {
mockAuthorization.mode.useRbacForRequest.mockReturnValue(true);
mockCheckPrivilegesAtSpaces.mockReturnValue({
username,
spacePrivileges: {
[savedObjects[0].id]: {
[privilege]: false,
},
[savedObjects[1].id]: {
[privilege]: false,
},
},
privileges: [
{ resource: savedObjects[0].id, privilege, authorized: false },
{ resource: savedObjects[1].id, privilege, authorized: false },
],
});
const maxSpaces = 1234;
const mockConfig = createMockConfig({
@ -314,14 +310,10 @@ describe('#getAll', () => {
mockAuthorization.mode.useRbacForRequest.mockReturnValue(true);
mockCheckPrivilegesAtSpaces.mockReturnValue({
username,
spacePrivileges: {
[savedObjects[0].id]: {
[privilege]: true,
},
[savedObjects[1].id]: {
[privilege]: false,
},
},
privileges: [
{ resource: savedObjects[0].id, privilege, authorized: true },
{ resource: savedObjects[1].id, privilege, authorized: false },
],
});
const mockInternalRepository = {
find: jest.fn().mockReturnValue({

View file

@ -74,16 +74,14 @@ export class SpacesClient {
const privilege = privilegeFactory(this.authorization!);
const { username, spacePrivileges } = await checkPrivileges.atSpaces(spaceIds, privilege);
const { username, privileges } = await checkPrivileges.atSpaces(spaceIds, privilege);
const authorized = Object.keys(spacePrivileges).filter(spaceId => {
return spacePrivileges[spaceId][privilege];
});
const authorized = privileges.filter(x => x.authorized).map(x => x.resource);
this.debugLogger(
`SpacesClient.getAll(), authorized for ${
authorized.length
} spaces, derived from ES privilege check: ${JSON.stringify(spacePrivileges)}`
} spaces, derived from ES privilege check: ${JSON.stringify(privileges)}`
);
if (authorized.length === 0) {
@ -94,7 +92,7 @@ export class SpacesClient {
throw Boom.forbidden();
}
this.auditLogger.spacesAuthorizationSuccess(username, 'getAll', authorized);
this.auditLogger.spacesAuthorizationSuccess(username, 'getAll', authorized as string[]);
const filteredSpaces: Space[] = spaces.filter((space: any) => authorized.includes(space.id));
this.debugLogger(
`SpacesClient.getAll(), using RBAC. returning spaces: ${filteredSpaces
@ -211,9 +209,9 @@ export class SpacesClient {
throw Boom.badRequest('This Space cannot be deleted because it is reserved.');
}
await repository.delete('space', id);
await repository.deleteByNamespace(id);
await repository.delete('space', id);
}
private useRbac(): boolean {

View file

@ -11,7 +11,13 @@ import {
mockRouteContext,
mockRouteContextWithInvalidLicense,
} from '../__fixtures__';
import { CoreSetup, IRouter, kibanaResponseFactory, RouteValidatorConfig } from 'src/core/server';
import {
CoreSetup,
IRouter,
kibanaResponseFactory,
RouteValidatorConfig,
SavedObjectsErrorHelpers,
} from 'src/core/server';
import {
loggingServiceMock,
httpServiceMock,
@ -75,6 +81,7 @@ describe('Spaces Public API', () => {
return {
routeValidation: routeDefinition.validate as RouteValidatorConfig<{}, {}, {}>,
routeHandler,
savedObjectsRepositoryMock,
};
};
@ -143,6 +150,27 @@ describe('Spaces Public API', () => {
expect(status).toEqual(404);
});
it(`returns http/400 when scripts cannot be executed in Elasticsearch`, async () => {
const { routeHandler, savedObjectsRepositoryMock } = await setup();
const request = httpServerMock.createKibanaRequest({
params: {
id: 'a-space',
},
method: 'delete',
});
// @ts-ignore
savedObjectsRepositoryMock.deleteByNamespace.mockRejectedValue(
SavedObjectsErrorHelpers.decorateEsCannotExecuteScriptError(new Error())
);
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory);
const { status, payload } = response;
expect(status).toEqual(400);
expect(payload.message).toEqual('Cannot execute script in Elasticsearch query');
});
it(`DELETE spaces/{id}' cannot delete reserved spaces`, async () => {
const { routeHandler } = await setup();

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import Boom from 'boom';
import { schema } from '@kbn/config-schema';
import { SavedObjectsErrorHelpers } from '../../../../../../../src/core/server';
import { wrapError } from '../../../lib/errors';
@ -12,7 +13,7 @@ import { ExternalRouteDeps } from '.';
import { createLicensedRouteHandler } from '../../lib';
export function initDeleteSpacesApi(deps: ExternalRouteDeps) {
const { externalRouter, spacesService } = deps;
const { externalRouter, log, spacesService } = deps;
externalRouter.delete(
{
@ -33,6 +34,13 @@ export function initDeleteSpacesApi(deps: ExternalRouteDeps) {
} catch (error) {
if (SavedObjectsErrorHelpers.isNotFoundError(error)) {
return response.notFound();
} else if (SavedObjectsErrorHelpers.isEsCannotExecuteScriptError(error)) {
log.error(
`Failed to delete space '${id}', cannot execute script in Elasticsearch query: ${error.message}`
);
return response.customError(
wrapError(Boom.badRequest('Cannot execute script in Elasticsearch query'))
);
}
return response.customError(wrapError(error));
}

View file

@ -12,6 +12,8 @@ import { initPostSpacesApi } from './post';
import { initPutSpacesApi } from './put';
import { SpacesServiceSetup } from '../../../spaces_service/spaces_service';
import { initCopyToSpacesApi } from './copy_to_space';
import { initShareAddSpacesApi } from './share_add_spaces';
import { initShareRemoveSpacesApi } from './share_remove_spaces';
export interface ExternalRouteDeps {
externalRouter: IRouter;
@ -28,4 +30,6 @@ export function initExternalSpacesApi(deps: ExternalRouteDeps) {
initPostSpacesApi(deps);
initPutSpacesApi(deps);
initCopyToSpacesApi(deps);
initShareAddSpacesApi(deps);
initShareRemoveSpacesApi(deps);
}

View file

@ -0,0 +1,62 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { wrapError } from '../../../lib/errors';
import { ExternalRouteDeps } from '.';
import { SPACE_ID_REGEX } from '../../../lib/space_schema';
import { createLicensedRouteHandler } from '../../lib';
const uniq = <T>(arr: T[]): T[] => Array.from(new Set<T>(arr));
export function initShareAddSpacesApi(deps: ExternalRouteDeps) {
const { externalRouter, getStartServices } = deps;
externalRouter.post(
{
path: '/api/spaces/_share_saved_object_add',
validate: {
body: schema.object({
spaces: schema.arrayOf(
schema.string({
validate: value => {
if (!SPACE_ID_REGEX.test(value)) {
return `lower case, a-z, 0-9, "_", and "-" are allowed`;
}
},
}),
{
validate: spaceIds => {
if (!spaceIds.length) {
return 'must specify one or more space ids';
} else if (uniq(spaceIds).length !== spaceIds.length) {
return 'duplicate space ids are not allowed';
}
},
}
),
object: schema.object({
type: schema.string(),
id: schema.string(),
}),
}),
},
},
createLicensedRouteHandler(async (_context, request, response) => {
const [startServices] = await getStartServices();
const scopedClient = startServices.savedObjects.getScopedClient(request);
const spaces = request.body.spaces;
const { type, id } = request.body.object;
try {
await scopedClient.addToNamespaces(type, id, spaces);
} catch (error) {
return response.customError(wrapError(error));
}
return response.noContent();
})
);
}

Some files were not shown because too many files have changed in this diff Show more