mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Sharing saved-objects phase 1 (#54605)
Co-authored-by: kobelb <brandon.kobel@elastic.co>
This commit is contained in:
parent
330956ec1a
commit
97d1685c3d
185 changed files with 12670 additions and 15958 deletions
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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. |
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObject](./kibana-plugin-core-public.savedobject.md) > [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[];
|
||||
```
|
|
@ -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'>;
|
||||
```
|
||||
|
|
|
@ -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) | |
|
||||
|
|
|
@ -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. |
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObject](./kibana-plugin-core-server.savedobject.md) > [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[];
|
||||
```
|
|
@ -0,0 +1,20 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [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. |
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsAddToNamespacesOptions](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md) > [refresh](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md)
|
||||
|
||||
## SavedObjectsAddToNamespacesOptions.refresh property
|
||||
|
||||
The Elasticsearch Refresh setting for this operation
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
refresh?: MutatingOperationRefreshSetting;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsAddToNamespacesOptions](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md) > [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;
|
||||
```
|
|
@ -0,0 +1,27 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) > [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<{}>`
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) > [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<{}>`
|
||||
|
|
@ -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 |
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [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 |
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsDeleteFromNamespacesOptions](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md) > [refresh](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md)
|
||||
|
||||
## SavedObjectsDeleteFromNamespacesOptions.refresh property
|
||||
|
||||
The Elasticsearch Refresh setting for this operation
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
refresh?: MutatingOperationRefreshSetting;
|
||||
```
|
|
@ -0,0 +1,23 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) > [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`
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) > [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`
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) > [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 | DecoratedError</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`boolean`
|
||||
|
|
@ -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> | |
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [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';
|
||||
```
|
|
@ -0,0 +1,27 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) > [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<{}>`
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) > [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<{}>`
|
||||
|
|
@ -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. |
|
||||
|
|
|
@ -29,7 +29,7 @@ import * as migrations from './migrations';
|
|||
export const myType: SavedObjectsType = {
|
||||
name: 'MyType',
|
||||
hidden: false,
|
||||
namespaceAgnostic: true,
|
||||
namespaceType: 'multiple',
|
||||
mappings: {
|
||||
properties: {
|
||||
textField: {
|
||||
|
|
|
@ -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. |
|
||||
|
||||
|
|
|
@ -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;
|
||||
```
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsType](./kibana-plugin-core-server.savedobjectstype.md) > [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;
|
||||
```
|
|
@ -0,0 +1,24 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectTypeRegistry](./kibana-plugin-core-server.savedobjecttyperegistry.md) > [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`
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectTypeRegistry](./kibana-plugin-core-server.savedobjecttyperegistry.md) > [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`
|
||||
|
|
@ -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. |
|
||||
|
||||
|
|
|
@ -956,6 +956,7 @@ export interface SavedObject<T = unknown> {
|
|||
};
|
||||
id: string;
|
||||
migrationVersion?: SavedObjectsMigrationVersion;
|
||||
namespaces?: string[];
|
||||
references: SavedObjectReference[];
|
||||
type: string;
|
||||
updated_at?: string;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
|
|
@ -69,6 +69,7 @@ export {
|
|||
} from './migrations';
|
||||
|
||||
export {
|
||||
SavedObjectsNamespaceType,
|
||||
SavedObjectStatusMeta,
|
||||
SavedObjectsType,
|
||||
SavedObjectsTypeManagementDefinition,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -142,6 +142,9 @@ function defaultMapping(): IndexMapping {
|
|||
namespace: {
|
||||
type: 'keyword',
|
||||
},
|
||||
namespaces: {
|
||||
type: 'keyword',
|
||||
},
|
||||
updated_at: {
|
||||
type: 'date',
|
||||
},
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -124,7 +124,7 @@ export interface SavedObjectsServiceSetup {
|
|||
* export const myType: SavedObjectsType = {
|
||||
* name: 'MyType',
|
||||
* hidden: false,
|
||||
* namespaceAgnostic: true,
|
||||
* namespaceType: 'multiple',
|
||||
* mappings: {
|
||||
* properties: {
|
||||
* textField: {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 } });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
@ -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)) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"`;
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
File diff suppressed because it is too large
Load diff
|
@ -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' } }],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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]) =>
|
||||
|
|
|
@ -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]"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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]]]]]`;
|
File diff suppressed because it is too large
Load diff
|
@ -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);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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 },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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),
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
{
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
62
x-pack/plugins/spaces/server/routes/api/external/share_add_spaces.ts
vendored
Normal file
62
x-pack/plugins/spaces/server/routes/api/external/share_add_spaces.ts
vendored
Normal 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
Loading…
Add table
Add a link
Reference in a new issue