mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Copy Saved Objects to Spaces API (#38014)
This commit is contained in:
parent
5fe9d0e780
commit
608e2391d0
44 changed files with 5015 additions and 139 deletions
|
@ -12,8 +12,12 @@ NOTE: You cannot access these endpoints via the Console in Kibana.
|
|||
* <<spaces-api-put>>
|
||||
* <<spaces-api-get>>
|
||||
* <<spaces-api-delete>>
|
||||
* <<spaces-api-copy-saved-objects>>
|
||||
* <<spaces-api-resolve-copy-saved-objects-conflicts>>
|
||||
|
||||
include::spaces-management/post.asciidoc[]
|
||||
include::spaces-management/put.asciidoc[]
|
||||
include::spaces-management/get.asciidoc[]
|
||||
include::spaces-management/delete.asciidoc[]
|
||||
include::spaces-management/copy_saved_objects.asciidoc[]
|
||||
include::spaces-management/resolve_copy_saved_objects_conflicts.asciidoc[]
|
||||
|
|
288
docs/api/spaces-management/copy_saved_objects.asciidoc
Normal file
288
docs/api/spaces-management/copy_saved_objects.asciidoc
Normal file
|
@ -0,0 +1,288 @@
|
|||
[role="xpack"]
|
||||
[[spaces-api-copy-saved-objects]]
|
||||
=== Copy Saved Objects to Space
|
||||
++++
|
||||
<titleabbrev>Copy Saved Objects to Space</titleabbrev>
|
||||
++++
|
||||
|
||||
experimental[This functionality is *experimental* and may be changed or removed completely in a future release.]
|
||||
|
||||
Copies saved objects from one space to other spaces.
|
||||
|
||||
////
|
||||
Use the appropriate heading levels for your book.
|
||||
Add anchors for each section.
|
||||
FYI: The section titles use attributes in case those terms change.
|
||||
////
|
||||
|
||||
[[spaces-api-copy-saved-objects-request]]
|
||||
==== {api-request-title}
|
||||
////
|
||||
This section show the basic endpoint, without the body or optional parameters.
|
||||
Variables should use <...> syntax.
|
||||
If an API supports both PUT and POST, include both here.
|
||||
////
|
||||
|
||||
`POST /api/spaces/_copy_saved_objects`
|
||||
|
||||
`POST /s/<space_id>/api/spaces/_copy_saved_objects`
|
||||
|
||||
|
||||
////
|
||||
[[spaces-api-copy-saved-objects-prereqs]]
|
||||
==== {api-prereq-title}
|
||||
////
|
||||
////
|
||||
Optional list of prerequisites.
|
||||
|
||||
For example:
|
||||
|
||||
* A snapshot of an index created in 5.x can be restored to 6.x. You must...
|
||||
* If the {es} {security-features} are enabled, you must have `write`, `monitor`,
|
||||
and `manage_follow_index` index privileges...
|
||||
////
|
||||
|
||||
|
||||
[[spaces-api-copy-saved-objects-desc]]
|
||||
==== {api-description-title}
|
||||
|
||||
Copy saved objects between spaces.
|
||||
|
||||
It also allows you to automatically copy related objects, so when you copy a `dashboard`, this can automatically copy over the
|
||||
associated visualizations, index patterns, and saved searches, as required.
|
||||
|
||||
You can request to overwrite any objects that already exist in the target space if they share an ID, or you can use the
|
||||
<<spaces-api-resolve-copy-saved-objects-conflicts, Resolve copy saved objects conflicts API>> to do this on a per-object basis.
|
||||
|
||||
////
|
||||
Add a more detailed description the context.
|
||||
Link to related APIs if appropriate.
|
||||
|
||||
Guidelines for parameter documentation
|
||||
***************************************
|
||||
* Use a definition list.
|
||||
* End each definition with a period.
|
||||
* Include whether the parameter is Optional or Required and the data type.
|
||||
* Include default values as the last sentence of the first paragraph.
|
||||
* Include a range of valid values, if applicable.
|
||||
* If the parameter requires a specific delimiter for multiple values, say so.
|
||||
* If the parameter supports wildcards, ditto.
|
||||
* For large or nested objects, consider linking to a separate definition list.
|
||||
***************************************
|
||||
////
|
||||
|
||||
|
||||
[[spaces-api-copy-saved-objects-path-params]]
|
||||
==== {api-path-parms-title}
|
||||
////
|
||||
A list of all the parameters within the path of the endpoint (before the query string (?)).
|
||||
|
||||
For example:
|
||||
`<follower_index>`::
|
||||
(Required, string) Name of the follower index
|
||||
////
|
||||
`space_id`::
|
||||
(Optional, string) Identifies the source space from which saved objects will be copied. If `space_id` is not specified in the URL, the default space is used.
|
||||
|
||||
////
|
||||
[[spaces-api-copy-saved-objects-params]]
|
||||
==== {api-query-parms-title}
|
||||
////
|
||||
////
|
||||
A list of the parameters in the query string of the endpoint (after the ?).
|
||||
|
||||
For example:
|
||||
`wait_for_active_shards`::
|
||||
(Optional, integer) Specifies the number of shards to wait on being active before
|
||||
responding. A shard must be restored from the leader index being active.
|
||||
Restoring a follower shard requires transferring all the remote Lucene segment
|
||||
files to the follower index. The default is `0`, which means waiting on none of
|
||||
the shards to be active.
|
||||
////
|
||||
|
||||
[[spaces-api-copy-saved-objects-request-body]]
|
||||
==== {api-request-body-title}
|
||||
////
|
||||
A list of the properties you can specify in the body of the request.
|
||||
|
||||
For example:
|
||||
`remote_cluster`::
|
||||
(Required, string) The <<modules-remote-clusters,remote cluster>> that contains
|
||||
the leader index.
|
||||
|
||||
`leader_index`::
|
||||
(Required, string) The name of the index in the leader cluster to follow.
|
||||
////
|
||||
`spaces` ::
|
||||
(Required, string array) The ids of the spaces the specified object(s) will be copied into.
|
||||
|
||||
`objects` ::
|
||||
(Required, object array) The saved objects to copy.
|
||||
`type` :::
|
||||
(Required, string) The saved object type.
|
||||
`id` :::
|
||||
(Required, string) The saved object id.
|
||||
|
||||
`includeReferences` ::
|
||||
(Optional, boolean) When set to `true`, all saved objects related to the specified saved objects will also be copied into the target spaces. The default value is `false`.
|
||||
|
||||
`overwrite` ::
|
||||
(Optional, boolean) When set to `true`, all conflicts will be automatically overidden. If a saved object with a matching `type` and `id` exists in the target space, then that version will be replaced with the version from the source space. The default value is `false`.
|
||||
|
||||
|
||||
[[spaces-api-copy-saved-objects-response-body]]
|
||||
==== {api-response-body-title}
|
||||
////
|
||||
Response body is only required for detailed responses.
|
||||
|
||||
For example:
|
||||
`auto_follow_stats`::
|
||||
(object) An object representing stats for the auto-follow coordinator. This
|
||||
object consists of the following fields:
|
||||
|
||||
`auto_follow_stats.number_of_successful_follow_indices`:::
|
||||
(long) the number of indices that the auto-follow coordinator successfully
|
||||
followed
|
||||
...
|
||||
|
||||
////
|
||||
|
||||
`<space_id>`::
|
||||
(object) Specifies the dynamic keys that are included in the response. An object describing the result of the copy operation for this particular space.
|
||||
`success`:::
|
||||
(boolean) Indicates if the copy operation was successful. Note that some objects may have been copied even if this is set to `false`. Consult the `successCount` and `errors` properties of the response for additional information.
|
||||
`successCount`:::
|
||||
(number) The number of objects that were successfully copied.
|
||||
`errors`:::
|
||||
(Optional, array) Collection of any errors that were encountered during the copy operation. If any errors are reported, then the `success` flag will be set to `false`.
|
||||
`id`::::
|
||||
(string) The saved object id which failed to copy.
|
||||
`type`::::
|
||||
(string) The type of saved object which failed to copy.
|
||||
`error`::::
|
||||
(object) The error which caused the copy operation to fail.
|
||||
`type`:::::
|
||||
(string) Indicates the type of error. May be one of: `conflict`, `unsupported_type`, `missing_references`, `unknown`. Errors marked as `conflict` may be resolved by using the <<spaces-api-resolve-copy-saved-objects-conflicts, Resolve copy saved objects conflicts API>>.
|
||||
|
||||
////
|
||||
[[spaces-api-copy-saved-objects-response-codes]]
|
||||
==== {api-response-codes-title}
|
||||
////
|
||||
////
|
||||
Response codes are only required when needed to understand the response body.
|
||||
|
||||
For example:
|
||||
`200`::
|
||||
Indicates all listed indices or index aliases exist.
|
||||
|
||||
`404`::
|
||||
Indicates one or more listed indices or index aliases **do not** exist.
|
||||
////
|
||||
|
||||
|
||||
[[spaces-api-copy-saved-objects-example]]
|
||||
==== {api-examples-title}
|
||||
////
|
||||
Optional brief example.
|
||||
Use an 'Examples' heading if you include multiple examples.
|
||||
|
||||
|
||||
[source,js]
|
||||
----
|
||||
PUT /follower_index/_ccr/follow?wait_for_active_shards=1
|
||||
{
|
||||
"remote_cluster" : "remote_cluster",
|
||||
"leader_index" : "leader_index",
|
||||
"max_read_request_operation_count" : 1024,
|
||||
"max_outstanding_read_requests" : 16,
|
||||
"max_read_request_size" : "1024k",
|
||||
"max_write_request_operation_count" : 32768,
|
||||
"max_write_request_size" : "16k",
|
||||
"max_outstanding_write_requests" : 8,
|
||||
"max_write_buffer_count" : 512,
|
||||
"max_write_buffer_size" : "512k",
|
||||
"max_retry_delay" : "10s",
|
||||
"read_poll_timeout" : "30s"
|
||||
}
|
||||
----
|
||||
// CONSOLE
|
||||
// TEST[setup:remote_cluster_and_leader_index]
|
||||
|
||||
The API returns the following result:
|
||||
|
||||
[source,js]
|
||||
----
|
||||
{
|
||||
"follow_index_created" : true,
|
||||
"follow_index_shards_acked" : true,
|
||||
"index_following_started" : true
|
||||
}
|
||||
----
|
||||
// TESTRESPONSE
|
||||
////
|
||||
|
||||
The following example attempts to copy a dashboard with id `my-dashboard`, including all references from the `default` space to the `marketing` and `sales` spaces. The `marketing` space succeeds, while the `sales` space fails due to a conflict on the underlying index pattern:
|
||||
|
||||
[source,js]
|
||||
----
|
||||
POST /api/spaces/_copy_saved_objects
|
||||
{
|
||||
"objects": [{
|
||||
"type": "dashboard",
|
||||
"id": "my-dashboard"
|
||||
}],
|
||||
"spaces": ["marketing", "sales"],
|
||||
"includeReferences": true
|
||||
}
|
||||
----
|
||||
// KIBANA
|
||||
|
||||
The API returns the following result:
|
||||
|
||||
[source,js]
|
||||
----
|
||||
{
|
||||
"marketing": {
|
||||
"success": true,
|
||||
"successCount": 5
|
||||
},
|
||||
"sales": {
|
||||
"success": false,
|
||||
"successCount": 4,
|
||||
"errors": [{
|
||||
"id": "my-index-pattern",
|
||||
"type": "index-pattern",
|
||||
"error": {
|
||||
"type": "conflict"
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
The following example successfully copies a visualization with id `my-viz` from the `marketing` space to the `default` space:
|
||||
|
||||
[source,js]
|
||||
----
|
||||
POST /s/marketing/api/spaces/_copy_saved_objects
|
||||
{
|
||||
"objects": [{
|
||||
"type": "visualization",
|
||||
"id": "my-viz"
|
||||
}],
|
||||
"spaces": ["default"]
|
||||
}
|
||||
----
|
||||
// KIBANA
|
||||
|
||||
The API returns the following result:
|
||||
|
||||
[source,js]
|
||||
----
|
||||
{
|
||||
"default": {
|
||||
"success": true,
|
||||
"successCount": 1
|
||||
}
|
||||
}
|
||||
----
|
|
@ -17,14 +17,23 @@ To retrieve all spaces, issue a GET request to the
|
|||
[source,js]
|
||||
--------------------------------------------------
|
||||
GET /api/spaces/space
|
||||
GET /api/spaces/space?purpose= copySavedObjectsIntoSpace
|
||||
--------------------------------------------------
|
||||
// KIBANA
|
||||
|
||||
===== Request Query Parameters
|
||||
purpose (optional) :: Retrieve the available spaces for a specific purpose. This parameter only has an effect when security is enabled.
|
||||
`any` (default) ::: Retrieves all spaces the user is authorized to access.
|
||||
`copySavedObjectsIntoSpace` ::: Retrieves all spaces the user is authorized to copy saved objects into via Saved Objects Management.
|
||||
|
||||
|
||||
===== Response
|
||||
|
||||
A successful call returns a response code of `200` and a response body containing a JSON
|
||||
representation of the spaces.
|
||||
|
||||
If you are not authorized for any spaces for the provided `purpose`, then a response code of `403` will be returned.
|
||||
|
||||
[source,js]
|
||||
--------------------------------------------------
|
||||
[
|
||||
|
|
|
@ -0,0 +1,265 @@
|
|||
[role="xpack"]
|
||||
[[spaces-api-resolve-copy-saved-objects-conflicts]]
|
||||
=== Resolve Copy Saved Objects to Space Conflicts
|
||||
++++
|
||||
<titleabbrev>Resolve copy to space conflicts</titleabbrev>
|
||||
++++
|
||||
|
||||
experimental[This functionality is *experimental* and may be changed or removed completely in a future release.]
|
||||
|
||||
Overwrites specific saved objects that were returned as errors from the <<spaces-api-copy-saved-objects, Copy Saved Objects to Space API>>.
|
||||
|
||||
////
|
||||
Use the appropriate heading levels for your book.
|
||||
Add anchors for each section.
|
||||
FYI: The section titles use attributes in case those terms change.
|
||||
////
|
||||
|
||||
[[spaces-api-resolve-copy-saved-objects-conflicts-request]]
|
||||
==== {api-request-title}
|
||||
////
|
||||
This section show the basic endpoint, without the body or optional parameters.
|
||||
Variables should use <...> syntax.
|
||||
If an API supports both PUT and POST, include both here.
|
||||
////
|
||||
|
||||
`POST /api/spaces/_resolve_copy_saved_objects_errors`
|
||||
|
||||
`POST /s/<space_id>/api/spaces/_resolve_copy_saved_objects_errors`
|
||||
|
||||
|
||||
|
||||
[[spaces-api-resolve-copy-saved-objects-conflicts-prereqs]]
|
||||
==== {api-prereq-title}
|
||||
////
|
||||
Optional list of prerequisites.
|
||||
|
||||
For example:
|
||||
|
||||
* A snapshot of an index created in 5.x can be restored to 6.x. You must...
|
||||
* If the {es} {security-features} are enabled, you must have `write`, `monitor`,
|
||||
and `manage_follow_index` index privileges...
|
||||
////
|
||||
* Executed the <<spaces-api-copy-saved-objects, Copy Saved Objects to Space API>>, which returned one or more `conflict` errors that you wish to resolve.
|
||||
|
||||
////
|
||||
[[spaces-api-resolve-copy-saved-objects-conflicts-desc]]
|
||||
==== {api-description-title}
|
||||
|
||||
Allows saved objects to be selectively overridden in the target spaces.
|
||||
////
|
||||
|
||||
////
|
||||
Add a more detailed description the context.
|
||||
Link to related APIs if appropriate.
|
||||
|
||||
Guidelines for parameter documentation
|
||||
***************************************
|
||||
* Use a definition list.
|
||||
* End each definition with a period.
|
||||
* Include whether the parameter is Optional or Required and the data type.
|
||||
* Include default values as the last sentence of the first paragraph.
|
||||
* Include a range of valid values, if applicable.
|
||||
* If the parameter requires a specific delimiter for multiple values, say so.
|
||||
* If the parameter supports wildcards, ditto.
|
||||
* For large or nested objects, consider linking to a separate definition list.
|
||||
***************************************
|
||||
////
|
||||
|
||||
|
||||
[[spaces-api-resolve-copy-saved-objects-conflicts-path-params]]
|
||||
==== {api-path-parms-title}
|
||||
////
|
||||
A list of all the parameters within the path of the endpoint (before the query string (?)).
|
||||
|
||||
For example:
|
||||
`<follower_index>`::
|
||||
(Required, string) Name of the follower index
|
||||
////
|
||||
`space_id`::
|
||||
(Optional, string) Identifies the source space from which saved objects will be copied. If `space_id` is not specified in the URL, the default space is used. Must be the same value that was used during the failed <<spaces-api-copy-saved-objects, Copy Saved Objects to Space>> operation.
|
||||
|
||||
////
|
||||
[[spaces-api-resolve-copy-saved-objects-conflicts-request-params]]
|
||||
==== {api-query-parms-title}
|
||||
////
|
||||
////
|
||||
A list of the parameters in the query string of the endpoint (after the ?).
|
||||
|
||||
For example:
|
||||
`wait_for_active_shards`::
|
||||
(Optional, integer) Specifies the number of shards to wait on being active before
|
||||
responding. A shard must be restored from the leader index being active.
|
||||
Restoring a follower shard requires transferring all the remote Lucene segment
|
||||
files to the follower index. The default is `0`, which means waiting on none of
|
||||
the shards to be active.
|
||||
////
|
||||
|
||||
[[spaces-api-resolve-copy-saved-objects-conflicts-request-body]]
|
||||
==== {api-request-body-title}
|
||||
////
|
||||
A list of the properties you can specify in the body of the request.
|
||||
|
||||
For example:
|
||||
`remote_cluster`::
|
||||
(Required, string) The <<modules-remote-clusters,remote cluster>> that contains
|
||||
the leader index.
|
||||
|
||||
`leader_index`::
|
||||
(Required, string) The name of the index in the leader cluster to follow.
|
||||
////
|
||||
`objects` ::
|
||||
(Required, object array) The saved objects to copy. Must be the same value that was used during the failed <<spaces-api-copy-saved-objects, Copy Saved Objects to Space>> operation.
|
||||
`type` :::
|
||||
(Required, string) The saved object type.
|
||||
`id` :::
|
||||
(Required, string) The saved object id.
|
||||
|
||||
`includeReferences` ::
|
||||
(Optional, boolean) When set to `true`, all saved objects related to the specified saved objects will also be copied into the target spaces. You must set this to the same value that you used when executing the <<spaces-api-copy-saved-objects, Copy Saved Objects to Space API>>. The default value is `false`.
|
||||
|
||||
`retries`::
|
||||
(Required, object) The retry operations to attempt. Object keys represent the target space ids.
|
||||
`<space_id>` :::
|
||||
(Required, array) The the conflicts to resolve for the indicated `<space_id>`.
|
||||
`type` ::::
|
||||
(Required, string) The saved object type.
|
||||
`id` ::::
|
||||
(Required, string) The saved object id.
|
||||
`overwrite` ::::
|
||||
(Required, boolean) when set to `true`, the saved object from the source space (desigated by the <<spaces-api-resolve-copy-saved-objects-conflicts-path-params, `space_id` path parameter>>) will overwrite the the conflicting object in the destination space. When `false`, this does nothing.
|
||||
|
||||
|
||||
[[spaces-api-resolve-copy-saved-objects-conflicts-response-body]]
|
||||
==== {api-response-body-title}
|
||||
////
|
||||
Response body is only required for detailed responses.
|
||||
|
||||
For example:
|
||||
`auto_follow_stats`::
|
||||
(object) An object representing stats for the auto-follow coordinator. This
|
||||
object consists of the following fields:
|
||||
|
||||
`auto_follow_stats.number_of_successful_follow_indices`:::
|
||||
(long) the number of indices that the auto-follow coordinator successfully
|
||||
followed
|
||||
...
|
||||
|
||||
////
|
||||
|
||||
`<space_id>`::
|
||||
(object) Specifies the dynamic keys that are included in the response. An object describing the result of the copy operation for this particular space.
|
||||
`success`:::
|
||||
(boolean) Indicates if the copy operation was successful. Note that some objects may have been copied even if this is set to `false`. Consult the `successCount` and `errors` properties of the response for additional information.
|
||||
`successCount`:::
|
||||
(number) The number of objects that were successfully copied.
|
||||
`errors`:::
|
||||
(Optional, array) Collection of any errors that were encountered during the copy operation. If any errors are reported, then the `success` flag will be set to `false`.
|
||||
`id`::::
|
||||
(string) The saved object id which failed to copy.
|
||||
`type`::::
|
||||
(string) The type of saved object which failed to copy.
|
||||
`error`::::
|
||||
(object) The error which caused the copy operation to fail.
|
||||
`type`:::::
|
||||
(string) Indicates the type of error. May be one of: `unsupported_type`, `missing_references`, `unknown`.
|
||||
|
||||
////
|
||||
[[spaces-api-resolve-copy-saved-objects-conflicts-response-codes]]
|
||||
==== {api-response-codes-title}
|
||||
////
|
||||
////
|
||||
Response codes are only required when needed to understand the response body.
|
||||
|
||||
For example:
|
||||
`200`::
|
||||
Indicates all listed indices or index aliases exist.
|
||||
|
||||
`404`::
|
||||
Indicates one or more listed indices or index aliases **do not** exist.
|
||||
////
|
||||
|
||||
|
||||
[[spaces-api-resolve-copy-saved-objects-conflicts-example]]
|
||||
==== {api-examples-title}
|
||||
////
|
||||
Optional brief example.
|
||||
Use an 'Examples' heading if you include multiple examples.
|
||||
|
||||
|
||||
[source,js]
|
||||
----
|
||||
PUT /follower_index/_ccr/follow?wait_for_active_shards=1
|
||||
{
|
||||
"remote_cluster" : "remote_cluster",
|
||||
"leader_index" : "leader_index",
|
||||
"max_read_request_operation_count" : 1024,
|
||||
"max_outstanding_read_requests" : 16,
|
||||
"max_read_request_size" : "1024k",
|
||||
"max_write_request_operation_count" : 32768,
|
||||
"max_write_request_size" : "16k",
|
||||
"max_outstanding_write_requests" : 8,
|
||||
"max_write_buffer_count" : 512,
|
||||
"max_write_buffer_size" : "512k",
|
||||
"max_retry_delay" : "10s",
|
||||
"read_poll_timeout" : "30s"
|
||||
}
|
||||
----
|
||||
// CONSOLE
|
||||
// TEST[setup:remote_cluster_and_leader_index]
|
||||
|
||||
The API returns the following result:
|
||||
|
||||
[source,js]
|
||||
----
|
||||
{
|
||||
"follow_index_created" : true,
|
||||
"follow_index_shards_acked" : true,
|
||||
"index_following_started" : true
|
||||
}
|
||||
----
|
||||
// TESTRESPONSE
|
||||
////
|
||||
|
||||
The following example overwrites an index pattern in the marketing space, and a visualization in the sales space.
|
||||
|
||||
[source,js]
|
||||
----
|
||||
POST api/spaces/_resolve_copy_saved_objects_errors
|
||||
{
|
||||
"objects": [{
|
||||
"type": "dashboard",
|
||||
"id": "my-dashboard"
|
||||
}],
|
||||
"includeReferences": true,
|
||||
"retries": {
|
||||
"marketing": [{
|
||||
"type": "index-pattern",
|
||||
"id": "my-pattern",
|
||||
"overwrite": true
|
||||
}],
|
||||
"sales": [{
|
||||
"type": "visualization",
|
||||
"id": "my-viz",
|
||||
"overwrite": true
|
||||
}]
|
||||
}
|
||||
}
|
||||
----
|
||||
// KIBANA
|
||||
|
||||
The API returns the following result:
|
||||
|
||||
[source,js]
|
||||
----
|
||||
{
|
||||
"marketing": {
|
||||
"success": true,
|
||||
"successCount": 1
|
||||
},
|
||||
"sales": {
|
||||
"success": true,
|
||||
"successCount": 1
|
||||
}
|
||||
}
|
||||
----
|
|
@ -19,6 +19,7 @@ export function createJestConfig({ kibanaDirectory, xPackKibanaDirectory }) {
|
|||
'^ui/(.*)': `${kibanaDirectory}/src/legacy/ui/public/$1`,
|
||||
'uiExports/(.*)': fileMockPath,
|
||||
'^src/core/(.*)': `${kibanaDirectory}/src/core/$1`,
|
||||
'^src/legacy/(.*)': `${kibanaDirectory}/src/legacy/$1`,
|
||||
'^plugins/watcher/models/(.*)': `${xPackKibanaDirectory}/legacy/plugins/watcher/public/models/$1`,
|
||||
'^plugins/([^/.]*)(.*)': `${kibanaDirectory}/src/legacy/core_plugins/$1/public$2`,
|
||||
'^legacy/plugins/xpack_main/(.*);': `${xPackKibanaDirectory}/legacy/plugins/xpack_main/public/$1`,
|
||||
|
|
|
@ -0,0 +1,401 @@
|
|||
/*
|
||||
* 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 {
|
||||
SavedObjectsSchema,
|
||||
SavedObjectsService,
|
||||
SavedObjectsClientContract,
|
||||
SavedObjectsImportResponse,
|
||||
SavedObjectsImportOptions,
|
||||
SavedObjectsExportOptions,
|
||||
} from 'src/core/server';
|
||||
import { copySavedObjectsToSpacesFactory } from './copy_to_spaces';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
interface SetupOpts {
|
||||
objects: Array<{ type: string; id: string; attributes: Record<string, any> }>;
|
||||
getSortedObjectsForExportImpl?: (opts: SavedObjectsExportOptions) => Promise<Readable>;
|
||||
importSavedObjectsImpl?: (opts: SavedObjectsImportOptions) => Promise<SavedObjectsImportResponse>;
|
||||
}
|
||||
|
||||
const expectStreamToContainObjects = async (
|
||||
stream: Readable,
|
||||
expectedObjects: SetupOpts['objects']
|
||||
) => {
|
||||
const objectsToResolve: unknown[] = await new Promise((resolve, reject) => {
|
||||
const objects: SetupOpts['objects'] = [];
|
||||
stream.on('data', chunk => {
|
||||
objects.push(chunk);
|
||||
});
|
||||
stream.on('end', () => resolve(objects));
|
||||
stream.on('error', err => reject(err));
|
||||
});
|
||||
|
||||
// Ensure the Readable stream passed to `resolveImportErrors` contains all of the expected objects.
|
||||
// Verifies functionality for `readStreamToCompletion` and `createReadableStreamFromArray`
|
||||
expect(objectsToResolve).toEqual(expectedObjects);
|
||||
};
|
||||
|
||||
describe('copySavedObjectsToSpaces', () => {
|
||||
const setup = (setupOpts: SetupOpts) => {
|
||||
const savedObjectsClient = (null as unknown) as SavedObjectsClientContract;
|
||||
|
||||
const savedObjectsService: SavedObjectsService = ({
|
||||
importExport: {
|
||||
objectLimit: 1000,
|
||||
getSortedObjectsForExport:
|
||||
setupOpts.getSortedObjectsForExportImpl ||
|
||||
jest.fn().mockResolvedValue(
|
||||
new Readable({
|
||||
objectMode: true,
|
||||
read() {
|
||||
setupOpts.objects.forEach(o => this.push(o));
|
||||
|
||||
this.push(null);
|
||||
},
|
||||
})
|
||||
),
|
||||
importSavedObjects:
|
||||
setupOpts.importSavedObjectsImpl ||
|
||||
jest.fn().mockImplementation(async (importOpts: SavedObjectsImportOptions) => {
|
||||
await expectStreamToContainObjects(importOpts.readStream, setupOpts.objects);
|
||||
const response: SavedObjectsImportResponse = {
|
||||
success: true,
|
||||
successCount: setupOpts.objects.length,
|
||||
};
|
||||
|
||||
return Promise.resolve(response);
|
||||
}),
|
||||
},
|
||||
types: ['dashboard', 'visualization', 'globalType'],
|
||||
schema: new SavedObjectsSchema({
|
||||
globalType: { isNamespaceAgnostic: true },
|
||||
}),
|
||||
} as unknown) as SavedObjectsService;
|
||||
|
||||
return {
|
||||
savedObjectsClient,
|
||||
savedObjectsService,
|
||||
};
|
||||
};
|
||||
|
||||
it('uses the Saved Objects Service to perform an export followed by a series of imports', async () => {
|
||||
const { savedObjectsClient, savedObjectsService } = setup({
|
||||
objects: [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'my-dashboard',
|
||||
attributes: {},
|
||||
},
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'my-viz',
|
||||
attributes: {},
|
||||
},
|
||||
{
|
||||
type: 'index-pattern',
|
||||
id: 'my-index-pattern',
|
||||
attributes: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory(
|
||||
savedObjectsClient,
|
||||
savedObjectsService
|
||||
);
|
||||
|
||||
const result = await copySavedObjectsToSpaces('sourceSpace', ['destination1', 'destination2'], {
|
||||
includeReferences: true,
|
||||
overwrite: true,
|
||||
objects: [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'my-dashboard',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"destination1": Object {
|
||||
"errors": undefined,
|
||||
"success": true,
|
||||
"successCount": 3,
|
||||
},
|
||||
"destination2": Object {
|
||||
"errors": undefined,
|
||||
"success": true,
|
||||
"successCount": 3,
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
expect((savedObjectsService.importExport.getSortedObjectsForExport as jest.Mock).mock.calls)
|
||||
.toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"exportSizeLimit": 1000,
|
||||
"includeReferencesDeep": true,
|
||||
"namespace": "sourceSpace",
|
||||
"objects": Array [
|
||||
Object {
|
||||
"id": "my-dashboard",
|
||||
"type": "dashboard",
|
||||
},
|
||||
],
|
||||
"savedObjectsClient": null,
|
||||
"types": Array [
|
||||
"dashboard",
|
||||
"visualization",
|
||||
],
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
|
||||
expect((savedObjectsService.importExport.importSavedObjects as jest.Mock).mock.calls)
|
||||
.toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"namespace": "destination1",
|
||||
"objectLimit": 1000,
|
||||
"overwrite": true,
|
||||
"readStream": Readable {
|
||||
"_events": Object {
|
||||
"data": [Function],
|
||||
"end": [Function],
|
||||
"error": [Function],
|
||||
},
|
||||
"_eventsCount": 3,
|
||||
"_maxListeners": undefined,
|
||||
"_read": [Function],
|
||||
"_readableState": ReadableState {
|
||||
"awaitDrain": 0,
|
||||
"buffer": BufferList {
|
||||
"head": null,
|
||||
"length": 0,
|
||||
"tail": null,
|
||||
},
|
||||
"decoder": null,
|
||||
"defaultEncoding": "utf8",
|
||||
"destroyed": false,
|
||||
"emitClose": true,
|
||||
"emittedReadable": false,
|
||||
"encoding": null,
|
||||
"endEmitted": true,
|
||||
"ended": true,
|
||||
"flowing": true,
|
||||
"highWaterMark": 16,
|
||||
"length": 0,
|
||||
"needReadable": false,
|
||||
"objectMode": true,
|
||||
"paused": false,
|
||||
"pipes": null,
|
||||
"pipesCount": 0,
|
||||
"readableListening": false,
|
||||
"reading": false,
|
||||
"readingMore": false,
|
||||
"resumeScheduled": false,
|
||||
"sync": false,
|
||||
},
|
||||
"readable": false,
|
||||
},
|
||||
"savedObjectsClient": null,
|
||||
"supportedTypes": Array [
|
||||
"dashboard",
|
||||
"visualization",
|
||||
],
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Object {
|
||||
"namespace": "destination2",
|
||||
"objectLimit": 1000,
|
||||
"overwrite": true,
|
||||
"readStream": Readable {
|
||||
"_events": Object {
|
||||
"data": [Function],
|
||||
"end": [Function],
|
||||
"error": [Function],
|
||||
},
|
||||
"_eventsCount": 3,
|
||||
"_maxListeners": undefined,
|
||||
"_read": [Function],
|
||||
"_readableState": ReadableState {
|
||||
"awaitDrain": 0,
|
||||
"buffer": BufferList {
|
||||
"head": null,
|
||||
"length": 0,
|
||||
"tail": null,
|
||||
},
|
||||
"decoder": null,
|
||||
"defaultEncoding": "utf8",
|
||||
"destroyed": false,
|
||||
"emitClose": true,
|
||||
"emittedReadable": false,
|
||||
"encoding": null,
|
||||
"endEmitted": true,
|
||||
"ended": true,
|
||||
"flowing": true,
|
||||
"highWaterMark": 16,
|
||||
"length": 0,
|
||||
"needReadable": false,
|
||||
"objectMode": true,
|
||||
"paused": false,
|
||||
"pipes": null,
|
||||
"pipesCount": 0,
|
||||
"readableListening": false,
|
||||
"reading": false,
|
||||
"readingMore": false,
|
||||
"resumeScheduled": false,
|
||||
"sync": false,
|
||||
},
|
||||
"readable": false,
|
||||
},
|
||||
"savedObjectsClient": null,
|
||||
"supportedTypes": Array [
|
||||
"dashboard",
|
||||
"visualization",
|
||||
],
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it(`doesn't stop copy if some spaces fail`, async () => {
|
||||
const objects = [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'my-dashboard',
|
||||
attributes: {},
|
||||
},
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'my-viz',
|
||||
attributes: {},
|
||||
},
|
||||
{
|
||||
type: 'index-pattern',
|
||||
id: 'my-index-pattern',
|
||||
attributes: {},
|
||||
},
|
||||
];
|
||||
const { savedObjectsClient, savedObjectsService } = setup({
|
||||
objects,
|
||||
importSavedObjectsImpl: async opts => {
|
||||
if (opts.namespace === 'failure-space') {
|
||||
throw new Error(`Some error occurred!`);
|
||||
}
|
||||
await expectStreamToContainObjects(opts.readStream, objects);
|
||||
return Promise.resolve({
|
||||
success: true,
|
||||
successCount: 3,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory(
|
||||
savedObjectsClient,
|
||||
savedObjectsService
|
||||
);
|
||||
|
||||
const result = await copySavedObjectsToSpaces(
|
||||
'sourceSpace',
|
||||
['failure-space', 'non-existent-space', 'marketing'],
|
||||
{
|
||||
includeReferences: true,
|
||||
overwrite: true,
|
||||
objects: [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'my-dashboard',
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"failure-space": Object {
|
||||
"errors": Array [
|
||||
[Error: Some error occurred!],
|
||||
],
|
||||
"success": false,
|
||||
"successCount": 0,
|
||||
},
|
||||
"marketing": Object {
|
||||
"errors": undefined,
|
||||
"success": true,
|
||||
"successCount": 3,
|
||||
},
|
||||
"non-existent-space": Object {
|
||||
"errors": undefined,
|
||||
"success": true,
|
||||
"successCount": 3,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it(`handles stream read errors`, async () => {
|
||||
const { savedObjectsClient, savedObjectsService } = setup({
|
||||
objects: [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'my-dashboard',
|
||||
attributes: {},
|
||||
},
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'my-viz',
|
||||
attributes: {},
|
||||
},
|
||||
{
|
||||
type: 'index-pattern',
|
||||
id: 'my-index-pattern',
|
||||
attributes: {},
|
||||
},
|
||||
],
|
||||
getSortedObjectsForExportImpl: opts => {
|
||||
return Promise.resolve(
|
||||
new Readable({
|
||||
objectMode: true,
|
||||
read() {
|
||||
this.emit('error', new Error('Something went wrong while reading this stream'));
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory(
|
||||
savedObjectsClient,
|
||||
savedObjectsService
|
||||
);
|
||||
|
||||
await expect(
|
||||
copySavedObjectsToSpaces(
|
||||
'sourceSpace',
|
||||
['failure-space', 'non-existent-space', 'marketing'],
|
||||
{
|
||||
includeReferences: true,
|
||||
overwrite: true,
|
||||
objects: [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'my-dashboard',
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Something went wrong while reading this stream"`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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 { SavedObjectsClientContract, SavedObjectsService, SavedObject } from 'src/core/server';
|
||||
import { Readable } from 'stream';
|
||||
import { SavedObjectsClientProviderOptions } from 'src/core/server';
|
||||
import { spaceIdToNamespace } from '../utils/namespace';
|
||||
import { CopyOptions, CopyResponse } from './types';
|
||||
import { getEligibleTypes } from './lib/get_eligible_types';
|
||||
import { createReadableStreamFromArray } from './lib/readable_stream_from_array';
|
||||
import { createEmptyFailureResponse } from './lib/create_empty_failure_response';
|
||||
import { readStreamToCompletion } from './lib/read_stream_to_completion';
|
||||
|
||||
export const COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS: SavedObjectsClientProviderOptions = {
|
||||
excludedWrappers: ['spaces'],
|
||||
};
|
||||
|
||||
export function copySavedObjectsToSpacesFactory(
|
||||
savedObjectsClient: SavedObjectsClientContract,
|
||||
savedObjectsService: SavedObjectsService
|
||||
) {
|
||||
const { importExport, types, schema } = savedObjectsService;
|
||||
const eligibleTypes = getEligibleTypes({ types, schema });
|
||||
|
||||
const exportRequestedObjects = async (
|
||||
sourceSpaceId: string,
|
||||
options: Pick<CopyOptions, 'includeReferences' | 'objects'>
|
||||
) => {
|
||||
const objectStream = await importExport.getSortedObjectsForExport({
|
||||
namespace: spaceIdToNamespace(sourceSpaceId),
|
||||
includeReferencesDeep: options.includeReferences,
|
||||
objects: options.objects,
|
||||
savedObjectsClient,
|
||||
types: eligibleTypes,
|
||||
exportSizeLimit: importExport.objectLimit,
|
||||
});
|
||||
|
||||
return readStreamToCompletion<SavedObject>(objectStream);
|
||||
};
|
||||
|
||||
const importObjectsToSpace = async (
|
||||
spaceId: string,
|
||||
objectsStream: Readable,
|
||||
options: CopyOptions
|
||||
) => {
|
||||
try {
|
||||
const importResponse = await importExport.importSavedObjects({
|
||||
namespace: spaceIdToNamespace(spaceId),
|
||||
objectLimit: importExport.objectLimit,
|
||||
overwrite: options.overwrite,
|
||||
savedObjectsClient,
|
||||
supportedTypes: eligibleTypes,
|
||||
readStream: objectsStream,
|
||||
});
|
||||
|
||||
return {
|
||||
success: importResponse.success,
|
||||
successCount: importResponse.successCount,
|
||||
errors: importResponse.errors,
|
||||
};
|
||||
} catch (error) {
|
||||
return createEmptyFailureResponse([error]);
|
||||
}
|
||||
};
|
||||
|
||||
const copySavedObjectsToSpaces = async (
|
||||
sourceSpaceId: string,
|
||||
destinationSpaceIds: string[],
|
||||
options: CopyOptions
|
||||
): Promise<CopyResponse> => {
|
||||
const response: CopyResponse = {};
|
||||
|
||||
const exportedSavedObjects = await exportRequestedObjects(sourceSpaceId, options);
|
||||
|
||||
for (const spaceId of destinationSpaceIds) {
|
||||
response[spaceId] = await importObjectsToSpace(
|
||||
spaceId,
|
||||
createReadableStreamFromArray(exportedSavedObjects),
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
return copySavedObjectsToSpaces;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { copySavedObjectsToSpacesFactory } from './copy_to_spaces';
|
||||
export { resolveCopySavedObjectsToSpacesConflictsFactory } from './resolve_copy_conflicts';
|
||||
export { CopyResponse } from './types';
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 Boom, { Payload } from 'boom';
|
||||
import { SavedObjectsImportError } from 'src/core/server';
|
||||
|
||||
export const createEmptyFailureResponse = (errors?: Array<SavedObjectsImportError | Boom>) => {
|
||||
const errorMessages: Array<SavedObjectsImportError | Payload> = (errors || []).map(error => {
|
||||
if (Boom.isBoom(error as any)) {
|
||||
return (error as Boom).output.payload as Payload;
|
||||
}
|
||||
return error as SavedObjectsImportError;
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
successCount: 0,
|
||||
errors: errorMessages,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { SavedObjectsService } from 'src/core/server';
|
||||
|
||||
export function getEligibleTypes({ types, schema }: Pick<SavedObjectsService, 'schema' | 'types'>) {
|
||||
return types.filter(type => !schema.isNamespaceAgnostic(type));
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 { Readable, pipeline, Writable } from 'stream';
|
||||
|
||||
export const readStreamToCompletion = <T = any>(stream: Readable) => {
|
||||
return new Promise<T[]>((resolve, reject) => {
|
||||
const chunks: T[] = [];
|
||||
pipeline(
|
||||
stream,
|
||||
new Writable({
|
||||
objectMode: true,
|
||||
write(chunk, enc, done) {
|
||||
chunks.push(chunk);
|
||||
done();
|
||||
},
|
||||
}),
|
||||
err => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(chunks);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Readable } from 'stream';
|
||||
|
||||
// TODO: Remove in favor of Readable.from once we upgrade to Node 12.x
|
||||
export const createReadableStreamFromArray = (array: unknown[]) => {
|
||||
return new Readable({
|
||||
objectMode: true,
|
||||
read() {
|
||||
array.forEach(entry => this.push(entry));
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
};
|
|
@ -0,0 +1,429 @@
|
|||
/*
|
||||
* 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 {
|
||||
SavedObjectsSchema,
|
||||
SavedObjectsService,
|
||||
SavedObjectsClientContract,
|
||||
SavedObjectsImportResponse,
|
||||
SavedObjectsResolveImportErrorsOptions,
|
||||
SavedObjectsExportOptions,
|
||||
} from 'src/core/server';
|
||||
import { Readable } from 'stream';
|
||||
import { resolveCopySavedObjectsToSpacesConflictsFactory } from './resolve_copy_conflicts';
|
||||
|
||||
interface SetupOpts {
|
||||
objects: Array<{ type: string; id: string; attributes: Record<string, any> }>;
|
||||
getSortedObjectsForExportImpl?: (opts: SavedObjectsExportOptions) => Promise<Readable>;
|
||||
resolveImportErrorsImpl?: (
|
||||
opts: SavedObjectsResolveImportErrorsOptions
|
||||
) => Promise<SavedObjectsImportResponse>;
|
||||
}
|
||||
|
||||
const expectStreamToContainObjects = async (
|
||||
stream: Readable,
|
||||
expectedObjects: SetupOpts['objects']
|
||||
) => {
|
||||
const objectsToResolve: unknown[] = await new Promise((resolve, reject) => {
|
||||
const objects: SetupOpts['objects'] = [];
|
||||
stream.on('data', chunk => {
|
||||
objects.push(chunk);
|
||||
});
|
||||
stream.on('end', () => resolve(objects));
|
||||
stream.on('error', err => reject(err));
|
||||
});
|
||||
|
||||
// Ensure the Readable stream passed to `resolveImportErrors` contains all of the expected objects.
|
||||
// Verifies functionality for `readStreamToCompletion` and `createReadableStreamFromArray`
|
||||
expect(objectsToResolve).toEqual(expectedObjects);
|
||||
};
|
||||
|
||||
describe('resolveCopySavedObjectsToSpacesConflicts', () => {
|
||||
const setup = (setupOpts: SetupOpts) => {
|
||||
const savedObjectsService: SavedObjectsService = ({
|
||||
importExport: {
|
||||
objectLimit: 1000,
|
||||
getSortedObjectsForExport:
|
||||
setupOpts.getSortedObjectsForExportImpl ||
|
||||
jest.fn().mockResolvedValue(
|
||||
new Readable({
|
||||
objectMode: true,
|
||||
read() {
|
||||
setupOpts.objects.forEach(o => this.push(o));
|
||||
|
||||
this.push(null);
|
||||
},
|
||||
})
|
||||
),
|
||||
resolveImportErrors:
|
||||
setupOpts.resolveImportErrorsImpl ||
|
||||
jest
|
||||
.fn()
|
||||
.mockImplementation(async (resolveOpts: SavedObjectsResolveImportErrorsOptions) => {
|
||||
await expectStreamToContainObjects(resolveOpts.readStream, setupOpts.objects);
|
||||
|
||||
const response: SavedObjectsImportResponse = {
|
||||
success: true,
|
||||
successCount: setupOpts.objects.length,
|
||||
};
|
||||
|
||||
return response;
|
||||
}),
|
||||
},
|
||||
types: ['dashboard', 'visualization', 'globalType'],
|
||||
schema: new SavedObjectsSchema({
|
||||
globalType: { isNamespaceAgnostic: true },
|
||||
}),
|
||||
} as unknown) as SavedObjectsService;
|
||||
|
||||
const savedObjectsClient = (null as unknown) as SavedObjectsClientContract;
|
||||
|
||||
return {
|
||||
savedObjectsClient,
|
||||
savedObjectsService,
|
||||
};
|
||||
};
|
||||
|
||||
it('uses the Saved Objects Service to perform an export followed by a series of conflict resolution calls', async () => {
|
||||
const { savedObjectsClient, savedObjectsService } = setup({
|
||||
objects: [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'my-dashboard',
|
||||
attributes: {},
|
||||
},
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'my-viz',
|
||||
attributes: {},
|
||||
},
|
||||
{
|
||||
type: 'index-pattern',
|
||||
id: 'my-index-pattern',
|
||||
attributes: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const resolveCopySavedObjectsToSpacesConflicts = resolveCopySavedObjectsToSpacesConflictsFactory(
|
||||
savedObjectsClient,
|
||||
savedObjectsService
|
||||
);
|
||||
|
||||
const result = await resolveCopySavedObjectsToSpacesConflicts('sourceSpace', {
|
||||
includeReferences: true,
|
||||
objects: [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'my-dashboard',
|
||||
},
|
||||
],
|
||||
retries: {
|
||||
destination1: [
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'my-visualization',
|
||||
overwrite: true,
|
||||
},
|
||||
],
|
||||
destination2: [
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'my-visualization',
|
||||
overwrite: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"destination1": Object {
|
||||
"errors": undefined,
|
||||
"success": true,
|
||||
"successCount": 3,
|
||||
},
|
||||
"destination2": Object {
|
||||
"errors": undefined,
|
||||
"success": true,
|
||||
"successCount": 3,
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
expect((savedObjectsService.importExport.getSortedObjectsForExport as jest.Mock).mock.calls)
|
||||
.toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"exportSizeLimit": 1000,
|
||||
"includeReferencesDeep": true,
|
||||
"namespace": "sourceSpace",
|
||||
"objects": Array [
|
||||
Object {
|
||||
"id": "my-dashboard",
|
||||
"type": "dashboard",
|
||||
},
|
||||
],
|
||||
"savedObjectsClient": null,
|
||||
"types": Array [
|
||||
"dashboard",
|
||||
"visualization",
|
||||
],
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
|
||||
expect((savedObjectsService.importExport.resolveImportErrors as jest.Mock).mock.calls)
|
||||
.toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"namespace": "destination1",
|
||||
"objectLimit": 1000,
|
||||
"readStream": Readable {
|
||||
"_events": Object {
|
||||
"data": [Function],
|
||||
"end": [Function],
|
||||
"error": [Function],
|
||||
},
|
||||
"_eventsCount": 3,
|
||||
"_maxListeners": undefined,
|
||||
"_read": [Function],
|
||||
"_readableState": ReadableState {
|
||||
"awaitDrain": 0,
|
||||
"buffer": BufferList {
|
||||
"head": null,
|
||||
"length": 0,
|
||||
"tail": null,
|
||||
},
|
||||
"decoder": null,
|
||||
"defaultEncoding": "utf8",
|
||||
"destroyed": false,
|
||||
"emitClose": true,
|
||||
"emittedReadable": false,
|
||||
"encoding": null,
|
||||
"endEmitted": true,
|
||||
"ended": true,
|
||||
"flowing": true,
|
||||
"highWaterMark": 16,
|
||||
"length": 0,
|
||||
"needReadable": false,
|
||||
"objectMode": true,
|
||||
"paused": false,
|
||||
"pipes": null,
|
||||
"pipesCount": 0,
|
||||
"readableListening": false,
|
||||
"reading": false,
|
||||
"readingMore": false,
|
||||
"resumeScheduled": false,
|
||||
"sync": false,
|
||||
},
|
||||
"readable": false,
|
||||
},
|
||||
"retries": Array [
|
||||
Object {
|
||||
"id": "my-visualization",
|
||||
"overwrite": true,
|
||||
"replaceReferences": Array [],
|
||||
"type": "visualization",
|
||||
},
|
||||
],
|
||||
"savedObjectsClient": null,
|
||||
"supportedTypes": Array [
|
||||
"dashboard",
|
||||
"visualization",
|
||||
],
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Object {
|
||||
"namespace": "destination2",
|
||||
"objectLimit": 1000,
|
||||
"readStream": Readable {
|
||||
"_events": Object {
|
||||
"data": [Function],
|
||||
"end": [Function],
|
||||
"error": [Function],
|
||||
},
|
||||
"_eventsCount": 3,
|
||||
"_maxListeners": undefined,
|
||||
"_read": [Function],
|
||||
"_readableState": ReadableState {
|
||||
"awaitDrain": 0,
|
||||
"buffer": BufferList {
|
||||
"head": null,
|
||||
"length": 0,
|
||||
"tail": null,
|
||||
},
|
||||
"decoder": null,
|
||||
"defaultEncoding": "utf8",
|
||||
"destroyed": false,
|
||||
"emitClose": true,
|
||||
"emittedReadable": false,
|
||||
"encoding": null,
|
||||
"endEmitted": true,
|
||||
"ended": true,
|
||||
"flowing": true,
|
||||
"highWaterMark": 16,
|
||||
"length": 0,
|
||||
"needReadable": false,
|
||||
"objectMode": true,
|
||||
"paused": false,
|
||||
"pipes": null,
|
||||
"pipesCount": 0,
|
||||
"readableListening": false,
|
||||
"reading": false,
|
||||
"readingMore": false,
|
||||
"resumeScheduled": false,
|
||||
"sync": false,
|
||||
},
|
||||
"readable": false,
|
||||
},
|
||||
"retries": Array [
|
||||
Object {
|
||||
"id": "my-visualization",
|
||||
"overwrite": false,
|
||||
"replaceReferences": Array [],
|
||||
"type": "visualization",
|
||||
},
|
||||
],
|
||||
"savedObjectsClient": null,
|
||||
"supportedTypes": Array [
|
||||
"dashboard",
|
||||
"visualization",
|
||||
],
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it(`doesn't stop resolution if some spaces fail`, async () => {
|
||||
const objects = [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'my-dashboard',
|
||||
attributes: {},
|
||||
},
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'my-viz',
|
||||
attributes: {},
|
||||
},
|
||||
{
|
||||
type: 'index-pattern',
|
||||
id: 'my-index-pattern',
|
||||
attributes: {},
|
||||
},
|
||||
];
|
||||
|
||||
const { savedObjectsClient, savedObjectsService } = setup({
|
||||
objects,
|
||||
resolveImportErrorsImpl: async opts => {
|
||||
if (opts.namespace === 'failure-space') {
|
||||
throw new Error(`Some error occurred!`);
|
||||
}
|
||||
await expectStreamToContainObjects(opts.readStream, objects);
|
||||
return Promise.resolve({
|
||||
success: true,
|
||||
successCount: 3,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const resolveCopySavedObjectsToSpacesConflicts = resolveCopySavedObjectsToSpacesConflictsFactory(
|
||||
savedObjectsClient,
|
||||
savedObjectsService
|
||||
);
|
||||
|
||||
const result = await resolveCopySavedObjectsToSpacesConflicts('sourceSpace', {
|
||||
includeReferences: true,
|
||||
objects: [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'my-dashboard',
|
||||
},
|
||||
],
|
||||
retries: {
|
||||
['failure-space']: [
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'my-visualization',
|
||||
overwrite: true,
|
||||
},
|
||||
],
|
||||
['non-existent-space']: [
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'my-visualization',
|
||||
overwrite: false,
|
||||
},
|
||||
],
|
||||
['marketing']: [
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'my-visualization',
|
||||
overwrite: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"failure-space": Object {
|
||||
"errors": Array [
|
||||
[Error: Some error occurred!],
|
||||
],
|
||||
"success": false,
|
||||
"successCount": 0,
|
||||
},
|
||||
"marketing": Object {
|
||||
"errors": undefined,
|
||||
"success": true,
|
||||
"successCount": 3,
|
||||
},
|
||||
"non-existent-space": Object {
|
||||
"errors": undefined,
|
||||
"success": true,
|
||||
"successCount": 3,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it(`handles stream read errors`, async () => {
|
||||
const { savedObjectsClient, savedObjectsService } = setup({
|
||||
objects: [],
|
||||
getSortedObjectsForExportImpl: opts => {
|
||||
return Promise.resolve(
|
||||
new Readable({
|
||||
objectMode: true,
|
||||
read() {
|
||||
this.emit('error', new Error('Something went wrong while reading this stream'));
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const resolveCopySavedObjectsToSpacesConflicts = resolveCopySavedObjectsToSpacesConflictsFactory(
|
||||
savedObjectsClient,
|
||||
savedObjectsService
|
||||
);
|
||||
|
||||
await expect(
|
||||
resolveCopySavedObjectsToSpacesConflicts('sourceSpace', {
|
||||
includeReferences: true,
|
||||
objects: [],
|
||||
retries: {},
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Something went wrong while reading this stream"`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* 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 { SavedObjectsClientContract, SavedObjectsService, SavedObject } from 'src/core/server';
|
||||
import { Readable } from 'stream';
|
||||
import { spaceIdToNamespace } from '../utils/namespace';
|
||||
import { CopyOptions, ResolveConflictsOptions, CopyResponse } from './types';
|
||||
import { getEligibleTypes } from './lib/get_eligible_types';
|
||||
import { createEmptyFailureResponse } from './lib/create_empty_failure_response';
|
||||
import { readStreamToCompletion } from './lib/read_stream_to_completion';
|
||||
import { createReadableStreamFromArray } from './lib/readable_stream_from_array';
|
||||
|
||||
export function resolveCopySavedObjectsToSpacesConflictsFactory(
|
||||
savedObjectsClient: SavedObjectsClientContract,
|
||||
savedObjectsService: SavedObjectsService
|
||||
) {
|
||||
const { importExport, types, schema } = savedObjectsService;
|
||||
const eligibleTypes = getEligibleTypes({ types, schema });
|
||||
|
||||
const exportRequestedObjects = async (
|
||||
sourceSpaceId: string,
|
||||
options: Pick<CopyOptions, 'includeReferences' | 'objects'>
|
||||
) => {
|
||||
const objectStream = await importExport.getSortedObjectsForExport({
|
||||
namespace: spaceIdToNamespace(sourceSpaceId),
|
||||
includeReferencesDeep: options.includeReferences,
|
||||
objects: options.objects,
|
||||
savedObjectsClient,
|
||||
types: eligibleTypes,
|
||||
exportSizeLimit: importExport.objectLimit,
|
||||
});
|
||||
return readStreamToCompletion<SavedObject>(objectStream);
|
||||
};
|
||||
|
||||
const resolveConflictsForSpace = async (
|
||||
spaceId: string,
|
||||
objectsStream: Readable,
|
||||
retries: Array<{
|
||||
type: string;
|
||||
id: string;
|
||||
overwrite: boolean;
|
||||
replaceReferences: Array<{ type: string; from: string; to: string }>;
|
||||
}>
|
||||
) => {
|
||||
try {
|
||||
const importResponse = await importExport.resolveImportErrors({
|
||||
namespace: spaceIdToNamespace(spaceId),
|
||||
objectLimit: importExport.objectLimit,
|
||||
savedObjectsClient,
|
||||
supportedTypes: eligibleTypes,
|
||||
readStream: objectsStream,
|
||||
retries,
|
||||
});
|
||||
|
||||
return {
|
||||
success: importResponse.success,
|
||||
successCount: importResponse.successCount,
|
||||
errors: importResponse.errors,
|
||||
};
|
||||
} catch (error) {
|
||||
return createEmptyFailureResponse([error]);
|
||||
}
|
||||
};
|
||||
|
||||
const resolveCopySavedObjectsToSpacesConflicts = async (
|
||||
sourceSpaceId: string,
|
||||
options: ResolveConflictsOptions
|
||||
): Promise<CopyResponse> => {
|
||||
const response: CopyResponse = {};
|
||||
|
||||
const exportedSavedObjects = await exportRequestedObjects(sourceSpaceId, {
|
||||
includeReferences: options.includeReferences,
|
||||
objects: options.objects,
|
||||
});
|
||||
|
||||
for (const entry of Object.entries(options.retries)) {
|
||||
const [spaceId, entryRetries] = entry;
|
||||
|
||||
const retries = entryRetries.map(retry => ({ ...retry, replaceReferences: [] }));
|
||||
|
||||
response[spaceId] = await resolveConflictsForSpace(
|
||||
spaceId,
|
||||
createReadableStreamFromArray(exportedSavedObjects),
|
||||
retries
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
return resolveCopySavedObjectsToSpacesConflicts;
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 { Payload } from 'boom';
|
||||
import { SavedObjectsImportError } from 'src/core/server';
|
||||
|
||||
export interface CopyOptions {
|
||||
objects: Array<{ type: string; id: string }>;
|
||||
overwrite: boolean;
|
||||
includeReferences: boolean;
|
||||
}
|
||||
|
||||
export interface ResolveConflictsOptions {
|
||||
objects: Array<{ type: string; id: string }>;
|
||||
includeReferences: boolean;
|
||||
retries: {
|
||||
[spaceId: string]: Array<{ type: string; id: string; overwrite: boolean }>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CopyResponse {
|
||||
[spaceId: string]: {
|
||||
success: boolean;
|
||||
successCount: number;
|
||||
errors?: Array<SavedObjectsImportError | Payload>;
|
||||
};
|
||||
}
|
|
@ -7,8 +7,10 @@
|
|||
import Joi from 'joi';
|
||||
import { MAX_SPACE_INITIALS } from '../../common/constants';
|
||||
|
||||
export const SPACE_ID_REGEX = /^[a-z0-9_\-]+$/;
|
||||
|
||||
export const spaceSchema = Joi.object({
|
||||
id: Joi.string().regex(/^[a-z0-9_\-]+$/, `lower case, a-z, 0-9, "_", and "-" are allowed`),
|
||||
id: Joi.string().regex(SPACE_ID_REGEX, `lower case, a-z, 0-9, "_", and "-" are allowed`),
|
||||
name: Joi.string().required(),
|
||||
description: Joi.string().allow(''),
|
||||
initials: Joi.string().max(MAX_SPACE_INITIALS),
|
||||
|
|
|
@ -18,6 +18,14 @@ exports[`#delete authorization.mode.useRbacForRequest returns true throws bad re
|
|||
|
||||
exports[`#get useRbacForRequest is true throws Boom.forbidden if the user isn't authorized at space 1`] = `"Unauthorized to get foo-space space"`;
|
||||
|
||||
exports[`#getAll useRbacForRequest is true throws Boom.forbidden when user isn't authorized for any spaces 1`] = `"Forbidden"`;
|
||||
exports[`#getAll authorization.mode.useRbacForRequest returns false throws Boom.badRequest when an invalid purpose is provided' 1`] = `"unsupported space purpose: invalid_purpose"`;
|
||||
|
||||
exports[`#getAll useRbacForRequest is true throws Boom.badRequest when an invalid purpose is provided 1`] = `"unsupported space purpose: invalid_purpose"`;
|
||||
|
||||
exports[`#getAll useRbacForRequest is true with purpose='any' throws Boom.forbidden when user isn't authorized for any spaces 1`] = `"Forbidden"`;
|
||||
|
||||
exports[`#getAll useRbacForRequest is true with purpose='copySavedObjectsIntoSpace' throws Boom.forbidden when user isn't authorized for any spaces 1`] = `"Forbidden"`;
|
||||
|
||||
exports[`#getAll useRbacForRequest is true with purpose='undefined' throws Boom.forbidden when user isn't authorized for any spaces 1`] = `"Forbidden"`;
|
||||
|
||||
exports[`#update useRbacForRequest is true throws Boom.forbidden when user isn't authorized at space 1`] = `"Unauthorized to update spaces"`;
|
||||
|
|
|
@ -4,4 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { SpacesClient } from './spaces_client';
|
||||
export { SpacesClient, GetSpacePurpose } from './spaces_client';
|
||||
|
|
|
@ -6,33 +6,30 @@
|
|||
|
||||
import { DEFAULT_SPACE_ID } from '../../../common/constants';
|
||||
import { Space } from '../../../common/model/space';
|
||||
import { SpacesClient } from './spaces_client';
|
||||
|
||||
const createSpacesClientMock = () => ({
|
||||
canEnumerateSpaces: jest.fn().mockResolvedValue(true),
|
||||
|
||||
getAll: jest.fn().mockResolvedValue([
|
||||
{
|
||||
id: DEFAULT_SPACE_ID,
|
||||
name: 'mock default space',
|
||||
disabledFeatures: [],
|
||||
_reserved: true,
|
||||
},
|
||||
]),
|
||||
|
||||
get: jest.fn().mockImplementation((spaceId: string) => {
|
||||
return Promise.resolve({
|
||||
id: spaceId,
|
||||
name: `mock space for ${spaceId}`,
|
||||
disabledFeatures: [],
|
||||
});
|
||||
}),
|
||||
|
||||
create: jest.fn().mockImplementation((space: Space) => Promise.resolve(space)),
|
||||
|
||||
update: jest.fn().mockImplementation((space: Space) => Promise.resolve(space)),
|
||||
|
||||
delete: jest.fn(),
|
||||
});
|
||||
const createSpacesClientMock = () =>
|
||||
(({
|
||||
canEnumerateSpaces: jest.fn().mockResolvedValue(true),
|
||||
getAll: jest.fn().mockResolvedValue([
|
||||
{
|
||||
id: DEFAULT_SPACE_ID,
|
||||
name: 'mock default space',
|
||||
disabledFeatures: [],
|
||||
_reserved: true,
|
||||
},
|
||||
]),
|
||||
get: jest.fn().mockImplementation((spaceId: string) => {
|
||||
return Promise.resolve({
|
||||
id: spaceId,
|
||||
name: `mock space for ${spaceId}`,
|
||||
disabledFeatures: [],
|
||||
});
|
||||
}),
|
||||
create: jest.fn().mockImplementation((space: Space) => Promise.resolve(space)),
|
||||
update: jest.fn().mockImplementation((space: Space) => Promise.resolve(space)),
|
||||
delete: jest.fn(),
|
||||
} as unknown) as SpacesClient);
|
||||
|
||||
export const spacesClientMock = {
|
||||
create: createSpacesClientMock,
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { SpacesClient } from './spaces_client';
|
||||
import { SpacesClient, GetSpacePurpose } from './spaces_client';
|
||||
import { AuthorizationService } from '../../../../security/server/lib/authorization/service';
|
||||
import { actionsFactory } from '../../../../security/server/lib/authorization/actions';
|
||||
import { SpacesConfigType, config } from '../../new_platform/config';
|
||||
|
@ -186,30 +186,37 @@ describe('#getAll', () => {
|
|||
expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0);
|
||||
expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test(`throws Boom.badRequest when an invalid purpose is provided'`, async () => {
|
||||
const { mockAuthorization } = createMockAuthorization();
|
||||
mockAuthorization.mode.useRbacForRequest.mockReturnValue(false);
|
||||
|
||||
const request = Symbol() as any;
|
||||
|
||||
const client = new SpacesClient(
|
||||
null as any,
|
||||
null as any,
|
||||
mockAuthorization,
|
||||
null as any,
|
||||
null as any,
|
||||
null,
|
||||
request
|
||||
);
|
||||
await expect(
|
||||
client.getAll('invalid_purpose' as GetSpacePurpose)
|
||||
).rejects.toThrowErrorMatchingSnapshot();
|
||||
|
||||
expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useRbacForRequest is true', () => {
|
||||
test(`throws Boom.forbidden when user isn't authorized for any spaces`, async () => {
|
||||
const username = Symbol();
|
||||
it('throws Boom.badRequest when an invalid purpose is provided', async () => {
|
||||
const mockAuditLogger = createMockAuditLogger();
|
||||
const mockDebugLogger = createMockDebugLogger();
|
||||
const { mockAuthorization, mockCheckPrivilegesAtSpaces } = createMockAuthorization();
|
||||
|
||||
mockAuthorization.mode.useRbacForRequest.mockReturnValue(true);
|
||||
mockCheckPrivilegesAtSpaces.mockReturnValue({
|
||||
username,
|
||||
spacePrivileges: {
|
||||
[savedObjects[0].id]: {
|
||||
[mockAuthorization.actions.login]: false,
|
||||
},
|
||||
[savedObjects[1].id]: {
|
||||
[mockAuthorization.actions.login]: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
const maxSpaces = 1234;
|
||||
const mockConfig = createMockConfig({
|
||||
maxSpaces: 1234,
|
||||
});
|
||||
|
||||
const mockInternalRepository = {
|
||||
find: jest.fn().mockReturnValue({
|
||||
saved_objects: savedObjects,
|
||||
|
@ -219,87 +226,169 @@ describe('#getAll', () => {
|
|||
|
||||
const client = new SpacesClient(
|
||||
mockAuditLogger as any,
|
||||
mockDebugLogger,
|
||||
null as any,
|
||||
mockAuthorization,
|
||||
null,
|
||||
mockConfig,
|
||||
null as any,
|
||||
mockInternalRepository,
|
||||
request
|
||||
);
|
||||
await expect(client.getAll()).rejects.toThrowErrorMatchingSnapshot();
|
||||
await expect(
|
||||
client.getAll('invalid_purpose' as GetSpacePurpose)
|
||||
).rejects.toThrowErrorMatchingSnapshot();
|
||||
|
||||
expect(mockInternalRepository.find).toHaveBeenCalledWith({
|
||||
type: 'space',
|
||||
page: 1,
|
||||
perPage: maxSpaces,
|
||||
sortField: 'name.keyword',
|
||||
});
|
||||
expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request);
|
||||
expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request);
|
||||
expect(mockCheckPrivilegesAtSpaces).toHaveBeenCalledWith(
|
||||
savedObjects.map(savedObject => savedObject.id),
|
||||
mockAuthorization.actions.login
|
||||
);
|
||||
expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith(username, 'getAll');
|
||||
expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0);
|
||||
expect(mockInternalRepository.find).not.toHaveBeenCalled();
|
||||
expect(mockAuthorization.mode.useRbacForRequest).not.toHaveBeenCalled();
|
||||
expect(mockAuthorization.checkPrivilegesWithRequest).not.toHaveBeenCalled();
|
||||
expect(mockCheckPrivilegesAtSpaces).not.toHaveBeenCalled();
|
||||
expect(mockAuditLogger.spacesAuthorizationFailure).not.toHaveBeenCalled();
|
||||
expect(mockAuditLogger.spacesAuthorizationSuccess).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test(`returns spaces that the user is authorized for`, async () => {
|
||||
const username = Symbol();
|
||||
const mockAuditLogger = createMockAuditLogger();
|
||||
const mockDebugLogger = createMockDebugLogger();
|
||||
const { mockAuthorization, mockCheckPrivilegesAtSpaces } = createMockAuthorization();
|
||||
mockAuthorization.mode.useRbacForRequest.mockReturnValue(true);
|
||||
mockCheckPrivilegesAtSpaces.mockReturnValue({
|
||||
username,
|
||||
spacePrivileges: {
|
||||
[savedObjects[0].id]: {
|
||||
[mockAuthorization.actions.login]: true,
|
||||
},
|
||||
[savedObjects[1].id]: {
|
||||
[mockAuthorization.actions.login]: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
const mockInternalRepository = {
|
||||
find: jest.fn().mockReturnValue({
|
||||
saved_objects: savedObjects,
|
||||
}),
|
||||
};
|
||||
const maxSpaces = 1234;
|
||||
const mockConfig = createMockConfig({
|
||||
maxSpaces: 1234,
|
||||
});
|
||||
const request = Symbol() as any;
|
||||
[
|
||||
{
|
||||
purpose: undefined,
|
||||
expectedPrivilege: (mockAuthorization: MockedAuthorization) =>
|
||||
mockAuthorization.actions.login,
|
||||
},
|
||||
{
|
||||
purpose: 'any',
|
||||
expectedPrivilege: (mockAuthorization: MockedAuthorization) =>
|
||||
mockAuthorization.actions.login,
|
||||
},
|
||||
{
|
||||
purpose: 'copySavedObjectsIntoSpace',
|
||||
expectedPrivilege: (mockAuthorization: MockedAuthorization) =>
|
||||
mockAuthorization.actions.ui.get('savedObjectsManagement', 'copyIntoSpace'),
|
||||
},
|
||||
].forEach(scenario => {
|
||||
describe(`with purpose='${scenario.purpose}'`, () => {
|
||||
test(`throws Boom.forbidden when user isn't authorized for any spaces`, async () => {
|
||||
const username = Symbol();
|
||||
const mockAuditLogger = createMockAuditLogger();
|
||||
const mockDebugLogger = createMockDebugLogger();
|
||||
const { mockAuthorization, mockCheckPrivilegesAtSpaces } = createMockAuthorization();
|
||||
|
||||
const client = new SpacesClient(
|
||||
mockAuditLogger as any,
|
||||
mockDebugLogger,
|
||||
mockAuthorization,
|
||||
null,
|
||||
mockConfig,
|
||||
mockInternalRepository,
|
||||
request
|
||||
);
|
||||
const actualSpaces = await client.getAll();
|
||||
const privilege = scenario.expectedPrivilege(mockAuthorization);
|
||||
|
||||
expect(actualSpaces).toEqual([expectedSpaces[0]]);
|
||||
expect(mockInternalRepository.find).toHaveBeenCalledWith({
|
||||
type: 'space',
|
||||
page: 1,
|
||||
perPage: maxSpaces,
|
||||
sortField: 'name.keyword',
|
||||
mockAuthorization.mode.useRbacForRequest.mockReturnValue(true);
|
||||
mockCheckPrivilegesAtSpaces.mockReturnValue({
|
||||
username,
|
||||
spacePrivileges: {
|
||||
[savedObjects[0].id]: {
|
||||
[privilege]: false,
|
||||
},
|
||||
[savedObjects[1].id]: {
|
||||
[privilege]: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
const maxSpaces = 1234;
|
||||
const mockConfig = createMockConfig({
|
||||
maxSpaces: 1234,
|
||||
});
|
||||
const mockInternalRepository = {
|
||||
find: jest.fn().mockReturnValue({
|
||||
saved_objects: savedObjects,
|
||||
}),
|
||||
};
|
||||
const request = Symbol() as any;
|
||||
|
||||
const client = new SpacesClient(
|
||||
mockAuditLogger as any,
|
||||
mockDebugLogger,
|
||||
mockAuthorization,
|
||||
null,
|
||||
mockConfig,
|
||||
mockInternalRepository,
|
||||
request
|
||||
);
|
||||
await expect(
|
||||
client.getAll(scenario.purpose as GetSpacePurpose)
|
||||
).rejects.toThrowErrorMatchingSnapshot();
|
||||
|
||||
expect(mockInternalRepository.find).toHaveBeenCalledWith({
|
||||
type: 'space',
|
||||
page: 1,
|
||||
perPage: maxSpaces,
|
||||
sortField: 'name.keyword',
|
||||
});
|
||||
expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request);
|
||||
expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request);
|
||||
expect(mockCheckPrivilegesAtSpaces).toHaveBeenCalledWith(
|
||||
savedObjects.map(savedObject => savedObject.id),
|
||||
privilege
|
||||
);
|
||||
expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith(
|
||||
username,
|
||||
'getAll'
|
||||
);
|
||||
expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test(`returns spaces that the user is authorized for`, async () => {
|
||||
const username = Symbol();
|
||||
const mockAuditLogger = createMockAuditLogger();
|
||||
const mockDebugLogger = createMockDebugLogger();
|
||||
const { mockAuthorization, mockCheckPrivilegesAtSpaces } = createMockAuthorization();
|
||||
|
||||
const privilege = scenario.expectedPrivilege(mockAuthorization);
|
||||
|
||||
mockAuthorization.mode.useRbacForRequest.mockReturnValue(true);
|
||||
mockCheckPrivilegesAtSpaces.mockReturnValue({
|
||||
username,
|
||||
spacePrivileges: {
|
||||
[savedObjects[0].id]: {
|
||||
[privilege]: true,
|
||||
},
|
||||
[savedObjects[1].id]: {
|
||||
[privilege]: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
const mockInternalRepository = {
|
||||
find: jest.fn().mockReturnValue({
|
||||
saved_objects: savedObjects,
|
||||
}),
|
||||
};
|
||||
const maxSpaces = 1234;
|
||||
const mockConfig = createMockConfig({
|
||||
maxSpaces: 1234,
|
||||
});
|
||||
const request = Symbol() as any;
|
||||
|
||||
const client = new SpacesClient(
|
||||
mockAuditLogger as any,
|
||||
mockDebugLogger,
|
||||
mockAuthorization,
|
||||
null,
|
||||
mockConfig,
|
||||
mockInternalRepository,
|
||||
request
|
||||
);
|
||||
const actualSpaces = await client.getAll(scenario.purpose as GetSpacePurpose);
|
||||
|
||||
expect(actualSpaces).toEqual([expectedSpaces[0]]);
|
||||
expect(mockInternalRepository.find).toHaveBeenCalledWith({
|
||||
type: 'space',
|
||||
page: 1,
|
||||
perPage: maxSpaces,
|
||||
sortField: 'name.keyword',
|
||||
});
|
||||
expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request);
|
||||
expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request);
|
||||
expect(mockCheckPrivilegesAtSpaces).toHaveBeenCalledWith(
|
||||
savedObjects.map(savedObject => savedObject.id),
|
||||
privilege
|
||||
);
|
||||
expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0);
|
||||
expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith(
|
||||
username,
|
||||
'getAll',
|
||||
[savedObjects[0].id]
|
||||
);
|
||||
});
|
||||
});
|
||||
expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request);
|
||||
expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request);
|
||||
expect(mockCheckPrivilegesAtSpaces).toHaveBeenCalledWith(
|
||||
savedObjects.map(savedObject => savedObject.id),
|
||||
mockAuthorization.actions.login
|
||||
);
|
||||
expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0);
|
||||
expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith(username, 'getAll', [
|
||||
savedObjects[0].id,
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,6 +14,19 @@ import { SpacesAuditLogger } from '../audit_logger';
|
|||
import { SpacesConfigType } from '../../new_platform/config';
|
||||
|
||||
type SpacesClientRequestFacade = Legacy.Request | KibanaRequest;
|
||||
|
||||
export type GetSpacePurpose = 'any' | 'copySavedObjectsIntoSpace';
|
||||
const SUPPORTED_GET_SPACE_PURPOSES: GetSpacePurpose[] = ['any', 'copySavedObjectsIntoSpace'];
|
||||
|
||||
const PURPOSE_PRIVILEGE_MAP: Record<
|
||||
GetSpacePurpose,
|
||||
(authorization: AuthorizationService) => string
|
||||
> = {
|
||||
any: authorization => authorization.actions.login,
|
||||
copySavedObjectsIntoSpace: authorization =>
|
||||
authorization.actions.ui.get('savedObjectsManagement', 'copyIntoSpace'),
|
||||
};
|
||||
|
||||
export class SpacesClient {
|
||||
constructor(
|
||||
private readonly auditLogger: SpacesAuditLogger,
|
||||
|
@ -40,8 +53,14 @@ export class SpacesClient {
|
|||
return true;
|
||||
}
|
||||
|
||||
public async getAll(): Promise<Space[]> {
|
||||
public async getAll(purpose: GetSpacePurpose = 'any'): Promise<Space[]> {
|
||||
if (!SUPPORTED_GET_SPACE_PURPOSES.includes(purpose)) {
|
||||
throw Boom.badRequest(`unsupported space purpose: ${purpose}`);
|
||||
}
|
||||
|
||||
if (this.useRbac()) {
|
||||
const privilegeFactory = PURPOSE_PRIVILEGE_MAP[purpose];
|
||||
|
||||
const { saved_objects } = await this.internalSavedObjectRepository.find({
|
||||
type: 'space',
|
||||
page: 1,
|
||||
|
@ -55,13 +74,13 @@ export class SpacesClient {
|
|||
|
||||
const spaceIds = spaces.map((space: Space) => space.id);
|
||||
const checkPrivileges = this.authorization!.checkPrivilegesWithRequest(this.request);
|
||||
const { username, spacePrivileges } = await checkPrivileges.atSpaces(
|
||||
spaceIds,
|
||||
this.authorization!.actions.login
|
||||
);
|
||||
|
||||
const privilege = privilegeFactory(this.authorization!);
|
||||
|
||||
const { username, spacePrivileges } = await checkPrivileges.atSpaces(spaceIds, privilege);
|
||||
|
||||
const authorized = Object.keys(spacePrivileges).filter(spaceId => {
|
||||
return spacePrivileges[spaceId][this.authorization!.actions.login];
|
||||
return spacePrivileges[spaceId][privilege];
|
||||
});
|
||||
|
||||
this.debugLogger(
|
||||
|
|
|
@ -10,6 +10,9 @@ import { Legacy } from 'kibana';
|
|||
import { KibanaConfig } from 'src/legacy/server/kbn_server';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { httpServiceMock, elasticsearchServiceMock } from 'src/core/server/mocks';
|
||||
import { SavedObjectsSchema, SavedObjectsService } from 'src/core/server';
|
||||
import { Readable } from 'stream';
|
||||
import { createPromiseFromStreams, createConcatStream } from 'src/legacy/utils/streams';
|
||||
import { createOptionalPlugin } from '../../../../../../server/lib/optional_plugin';
|
||||
import { SpacesClient } from '../../../lib/spaces_client';
|
||||
import { createSpaces } from './create_spaces';
|
||||
|
@ -33,6 +36,7 @@ export interface TestOptions {
|
|||
payload?: any;
|
||||
preCheckLicenseImpl?: (req: any, h: any) => any;
|
||||
expectSpacesClientCall?: boolean;
|
||||
expectPreCheckLicenseCall?: boolean;
|
||||
}
|
||||
|
||||
export type TeardownFn = () => void;
|
||||
|
@ -40,6 +44,17 @@ export type TeardownFn = () => void;
|
|||
export interface RequestRunnerResult {
|
||||
server: any;
|
||||
mockSavedObjectsRepository: any;
|
||||
mockSavedObjectsService: {
|
||||
getScopedSavedObjectsClient: jest.Mock<SavedObjectsService['getScopedSavedObjectsClient']>;
|
||||
importExport: {
|
||||
getSortedObjectsForExport: jest.Mock<
|
||||
SavedObjectsService['importExport']['getSortedObjectsForExport']
|
||||
>;
|
||||
importSavedObjects: jest.Mock<SavedObjectsService['importExport']['importSavedObjects']>;
|
||||
resolveImportErrors: jest.Mock<SavedObjectsService['importExport']['resolveImportErrors']>;
|
||||
};
|
||||
};
|
||||
headers: Record<string, unknown>;
|
||||
response: any;
|
||||
}
|
||||
|
||||
|
@ -55,6 +70,10 @@ const baseConfig: TestConfig = {
|
|||
'server.basePath': '',
|
||||
};
|
||||
|
||||
async function readStreamToCompletion(stream: Readable) {
|
||||
return (createPromiseFromStreams([stream, createConcatStream([])]) as unknown) as any[];
|
||||
}
|
||||
|
||||
export function createTestHandler(
|
||||
initApiFn: (deps: ExternalRouteDeps & InternalRouteDeps) => void
|
||||
) {
|
||||
|
@ -74,6 +93,7 @@ export function createTestHandler(
|
|||
testConfig = {},
|
||||
payload,
|
||||
preCheckLicenseImpl = defaultPreCheckLicenseImpl,
|
||||
expectPreCheckLicenseCall = true,
|
||||
expectSpacesClientCall = true,
|
||||
} = options;
|
||||
|
||||
|
@ -97,7 +117,7 @@ export function createTestHandler(
|
|||
|
||||
server.decorate('server', 'config', jest.fn<any, any>(() => mockConfig));
|
||||
|
||||
const mockSavedObjectsRepository = {
|
||||
const mockSavedObjectsClientContract = {
|
||||
get: jest.fn((type, id) => {
|
||||
const result = spaces.filter(s => s.id === id);
|
||||
if (!result.length) {
|
||||
|
@ -130,6 +150,44 @@ export function createTestHandler(
|
|||
};
|
||||
|
||||
server.savedObjects = {
|
||||
types: ['visualization', 'dashboard', 'index-pattern', 'globalType'],
|
||||
schema: new SavedObjectsSchema({
|
||||
space: {
|
||||
isNamespaceAgnostic: true,
|
||||
hidden: true,
|
||||
},
|
||||
globalType: {
|
||||
isNamespaceAgnostic: true,
|
||||
},
|
||||
}),
|
||||
getScopedSavedObjectsClient: jest.fn().mockResolvedValue(mockSavedObjectsClientContract),
|
||||
importExport: {
|
||||
getSortedObjectsForExport: jest.fn().mockResolvedValue(
|
||||
new Readable({
|
||||
objectMode: true,
|
||||
read() {
|
||||
if (Array.isArray(payload.objects)) {
|
||||
payload.objects.forEach((o: any) => this.push(o));
|
||||
}
|
||||
this.push(null);
|
||||
},
|
||||
})
|
||||
),
|
||||
importSavedObjects: jest.fn().mockImplementation(async (opts: Record<string, any>) => {
|
||||
const objectsToImport: any[] = await readStreamToCompletion(opts.readStream);
|
||||
return {
|
||||
success: true,
|
||||
successCount: objectsToImport.length,
|
||||
};
|
||||
}),
|
||||
resolveImportErrors: jest.fn().mockImplementation(async (opts: Record<string, any>) => {
|
||||
const objectsToImport: any[] = await readStreamToCompletion(opts.readStream);
|
||||
return {
|
||||
success: true,
|
||||
successCount: objectsToImport.length,
|
||||
};
|
||||
}),
|
||||
},
|
||||
SavedObjectsClient: {
|
||||
errors: {
|
||||
isNotFoundError: jest.fn((e: any) => e.message.startsWith('not found:')),
|
||||
|
@ -173,9 +231,9 @@ export function createTestHandler(
|
|||
null as any,
|
||||
() => null,
|
||||
null,
|
||||
mockSavedObjectsRepository,
|
||||
mockSavedObjectsClientContract,
|
||||
{ maxSpaces: 1000 },
|
||||
mockSavedObjectsRepository,
|
||||
mockSavedObjectsClientContract,
|
||||
req
|
||||
)
|
||||
);
|
||||
|
@ -207,7 +265,7 @@ export function createTestHandler(
|
|||
payload,
|
||||
});
|
||||
|
||||
if (preCheckLicenseImpl) {
|
||||
if (preCheckLicenseImpl && expectPreCheckLicenseCall) {
|
||||
expect(pre).toHaveBeenCalled();
|
||||
} else {
|
||||
expect(pre).not.toHaveBeenCalled();
|
||||
|
@ -231,7 +289,8 @@ export function createTestHandler(
|
|||
return {
|
||||
server,
|
||||
headers,
|
||||
mockSavedObjectsRepository,
|
||||
mockSavedObjectsRepository: mockSavedObjectsClientContract,
|
||||
mockSavedObjectsService: server.savedObjects,
|
||||
response: await testRun(),
|
||||
};
|
||||
};
|
||||
|
|
443
x-pack/legacy/plugins/spaces/server/routes/api/external/copy_to_space.test.ts
vendored
Normal file
443
x-pack/legacy/plugins/spaces/server/routes/api/external/copy_to_space.test.ts
vendored
Normal file
|
@ -0,0 +1,443 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
jest.mock('../../../lib/route_pre_check_license', () => {
|
||||
return {
|
||||
routePreCheckLicense: () => (request: any, h: any) => h.continue,
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../../../../server/lib/get_client_shield', () => {
|
||||
return {
|
||||
getClient: () => {
|
||||
return {
|
||||
callWithInternalUser: jest.fn(() => {
|
||||
return;
|
||||
}),
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
import Boom from 'boom';
|
||||
import { createTestHandler, RequestRunner, TeardownFn } from '../__fixtures__';
|
||||
import { initCopyToSpacesApi } from './copy_to_space';
|
||||
|
||||
describe('POST /api/spaces/_copy_saved_objects', () => {
|
||||
let request: RequestRunner;
|
||||
let teardowns: TeardownFn[];
|
||||
|
||||
beforeEach(() => {
|
||||
const setup = createTestHandler(initCopyToSpacesApi);
|
||||
|
||||
request = setup.request;
|
||||
teardowns = setup.teardowns;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(teardowns.splice(0).map(fn => fn()));
|
||||
});
|
||||
|
||||
test(`returns result of routePreCheckLicense`, async () => {
|
||||
const payload = {
|
||||
spaces: ['a-space'],
|
||||
objects: [],
|
||||
};
|
||||
|
||||
const { response } = await request('POST', '/api/spaces/_copy_saved_objects', {
|
||||
preCheckLicenseImpl: () => Boom.forbidden('test forbidden message'),
|
||||
expectSpacesClientCall: false,
|
||||
payload,
|
||||
});
|
||||
|
||||
const { statusCode, payload: responsePayload } = response;
|
||||
|
||||
expect(statusCode).toEqual(403);
|
||||
expect(JSON.parse(responsePayload)).toMatchObject({
|
||||
message: 'test forbidden message',
|
||||
});
|
||||
});
|
||||
|
||||
test(`uses a Saved Objects Client instance without the spaces wrapper`, async () => {
|
||||
const payload = {
|
||||
spaces: ['a-space'],
|
||||
objects: [],
|
||||
};
|
||||
|
||||
const { mockSavedObjectsService } = await request('POST', '/api/spaces/_copy_saved_objects', {
|
||||
expectSpacesClientCall: false,
|
||||
payload,
|
||||
});
|
||||
|
||||
expect(mockSavedObjectsService.getScopedSavedObjectsClient).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
{
|
||||
excludedWrappers: ['spaces'],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test(`requires space IDs to be unique`, async () => {
|
||||
const payload = {
|
||||
spaces: ['a-space', 'a-space'],
|
||||
objects: [],
|
||||
};
|
||||
|
||||
const { response } = await request('POST', '/api/spaces/_copy_saved_objects', {
|
||||
expectSpacesClientCall: false,
|
||||
expectPreCheckLicenseCall: false,
|
||||
payload,
|
||||
});
|
||||
|
||||
const { statusCode, payload: responsePayload } = response;
|
||||
|
||||
expect(statusCode).toEqual(400);
|
||||
expect(JSON.parse(responsePayload)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": "Bad Request",
|
||||
"message": "Invalid request payload input",
|
||||
"statusCode": 400,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test(`requires well-formed space IDS`, async () => {
|
||||
const payload = {
|
||||
spaces: ['a-space', 'a-space-invalid-!@#$%^&*()'],
|
||||
objects: [],
|
||||
};
|
||||
|
||||
const { response } = await request('POST', '/api/spaces/_copy_saved_objects', {
|
||||
expectSpacesClientCall: false,
|
||||
expectPreCheckLicenseCall: false,
|
||||
payload,
|
||||
});
|
||||
|
||||
const { statusCode, payload: responsePayload } = response;
|
||||
|
||||
expect(statusCode).toEqual(400);
|
||||
expect(JSON.parse(responsePayload)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": "Bad Request",
|
||||
"message": "Invalid request payload input",
|
||||
"statusCode": 400,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test(`requires objects to be unique`, async () => {
|
||||
const payload = {
|
||||
spaces: ['a-space'],
|
||||
objects: [{ type: 'foo', id: 'bar' }, { type: 'foo', id: 'bar' }],
|
||||
};
|
||||
|
||||
const { response } = await request('POST', '/api/spaces/_copy_saved_objects', {
|
||||
expectSpacesClientCall: false,
|
||||
expectPreCheckLicenseCall: false,
|
||||
payload,
|
||||
});
|
||||
|
||||
const { statusCode, payload: responsePayload } = response;
|
||||
|
||||
expect(statusCode).toEqual(400);
|
||||
expect(JSON.parse(responsePayload)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": "Bad Request",
|
||||
"message": "Invalid request payload input",
|
||||
"statusCode": 400,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('does not allow namespace agnostic types to be copied (via "supportedTypes" property)', async () => {
|
||||
const payload = {
|
||||
spaces: ['a-space'],
|
||||
objects: [{ type: 'globalType', id: 'bar' }, { type: 'visualization', id: 'bar' }],
|
||||
};
|
||||
|
||||
const { response, mockSavedObjectsService } = await request(
|
||||
'POST',
|
||||
'/api/spaces/_copy_saved_objects',
|
||||
{
|
||||
expectSpacesClientCall: false,
|
||||
payload,
|
||||
}
|
||||
);
|
||||
|
||||
const { statusCode } = response;
|
||||
|
||||
expect(statusCode).toEqual(200);
|
||||
expect(mockSavedObjectsService.importExport.importSavedObjects).toHaveBeenCalledTimes(1);
|
||||
const [
|
||||
importCallOptions,
|
||||
] = mockSavedObjectsService.importExport.importSavedObjects.mock.calls[0];
|
||||
|
||||
expect(importCallOptions).toMatchObject({
|
||||
namespace: 'a-space',
|
||||
supportedTypes: ['visualization', 'dashboard', 'index-pattern'],
|
||||
});
|
||||
});
|
||||
|
||||
test('copies to multiple spaces', async () => {
|
||||
const payload = {
|
||||
spaces: ['a-space', 'b-space'],
|
||||
objects: [{ type: 'visualization', id: 'bar' }],
|
||||
};
|
||||
|
||||
const { response, mockSavedObjectsService } = await request(
|
||||
'POST',
|
||||
'/api/spaces/_copy_saved_objects',
|
||||
{
|
||||
expectSpacesClientCall: false,
|
||||
payload,
|
||||
}
|
||||
);
|
||||
|
||||
const { statusCode } = response;
|
||||
|
||||
expect(statusCode).toEqual(200);
|
||||
expect(mockSavedObjectsService.importExport.importSavedObjects).toHaveBeenCalledTimes(2);
|
||||
const [
|
||||
firstImportCallOptions,
|
||||
] = mockSavedObjectsService.importExport.importSavedObjects.mock.calls[0];
|
||||
|
||||
expect(firstImportCallOptions).toMatchObject({
|
||||
namespace: 'a-space',
|
||||
});
|
||||
|
||||
const [
|
||||
secondImportCallOptions,
|
||||
] = mockSavedObjectsService.importExport.importSavedObjects.mock.calls[1];
|
||||
|
||||
expect(secondImportCallOptions).toMatchObject({
|
||||
namespace: 'b-space',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/spaces/_resolve_copy_saved_objects_errors', () => {
|
||||
let request: RequestRunner;
|
||||
let teardowns: TeardownFn[];
|
||||
|
||||
beforeEach(() => {
|
||||
const setup = createTestHandler(initCopyToSpacesApi);
|
||||
|
||||
request = setup.request;
|
||||
teardowns = setup.teardowns;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(teardowns.splice(0).map(fn => fn()));
|
||||
});
|
||||
|
||||
test(`returns result of routePreCheckLicense`, async () => {
|
||||
const payload = {
|
||||
retries: {},
|
||||
objects: [],
|
||||
};
|
||||
|
||||
const { response } = await request('POST', '/api/spaces/_resolve_copy_saved_objects_errors', {
|
||||
preCheckLicenseImpl: () => Boom.forbidden('test forbidden message'),
|
||||
expectSpacesClientCall: false,
|
||||
payload,
|
||||
});
|
||||
|
||||
const { statusCode, payload: responsePayload } = response;
|
||||
|
||||
expect(statusCode).toEqual(403);
|
||||
expect(JSON.parse(responsePayload)).toMatchObject({
|
||||
message: 'test forbidden message',
|
||||
});
|
||||
});
|
||||
|
||||
test(`uses a Saved Objects Client instance without the spaces wrapper`, async () => {
|
||||
const payload = {
|
||||
retries: {
|
||||
['a-space']: [
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'bar',
|
||||
overwrite: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
objects: [{ type: 'visualization', id: 'bar' }],
|
||||
};
|
||||
|
||||
const { mockSavedObjectsService } = await request(
|
||||
'POST',
|
||||
'/api/spaces/_resolve_copy_saved_objects_errors',
|
||||
{
|
||||
expectSpacesClientCall: false,
|
||||
payload,
|
||||
}
|
||||
);
|
||||
|
||||
expect(mockSavedObjectsService.getScopedSavedObjectsClient).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
{
|
||||
excludedWrappers: ['spaces'],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test(`requires objects to be unique`, async () => {
|
||||
const payload = {
|
||||
retries: {},
|
||||
objects: [{ type: 'foo', id: 'bar' }, { type: 'foo', id: 'bar' }],
|
||||
};
|
||||
|
||||
const { response } = await request('POST', '/api/spaces/_resolve_copy_saved_objects_errors', {
|
||||
expectSpacesClientCall: false,
|
||||
expectPreCheckLicenseCall: false,
|
||||
payload,
|
||||
});
|
||||
|
||||
const { statusCode, payload: responsePayload } = response;
|
||||
|
||||
expect(statusCode).toEqual(400);
|
||||
expect(JSON.parse(responsePayload)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": "Bad Request",
|
||||
"message": "Invalid request payload input",
|
||||
"statusCode": 400,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test(`requires well-formed space ids`, async () => {
|
||||
const payload = {
|
||||
retries: {
|
||||
['invalid-space-id!@#$%^&*()']: [
|
||||
{
|
||||
type: 'foo',
|
||||
id: 'bar',
|
||||
overwrite: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
objects: [{ type: 'foo', id: 'bar' }],
|
||||
};
|
||||
|
||||
const { response } = await request('POST', '/api/spaces/_resolve_copy_saved_objects_errors', {
|
||||
expectSpacesClientCall: false,
|
||||
expectPreCheckLicenseCall: false,
|
||||
payload,
|
||||
});
|
||||
|
||||
const { statusCode, payload: responsePayload } = response;
|
||||
|
||||
expect(statusCode).toEqual(400);
|
||||
expect(JSON.parse(responsePayload)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": "Bad Request",
|
||||
"message": "Invalid request payload input",
|
||||
"statusCode": 400,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('does not allow namespace agnostic types to be copied (via "supportedTypes" property)', async () => {
|
||||
const payload = {
|
||||
retries: {
|
||||
['a-space']: [
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'bar',
|
||||
overwrite: true,
|
||||
},
|
||||
{
|
||||
type: 'globalType',
|
||||
id: 'bar',
|
||||
overwrite: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
objects: [
|
||||
{
|
||||
type: 'globalType',
|
||||
id: 'bar',
|
||||
},
|
||||
{ type: 'visualization', id: 'bar' },
|
||||
],
|
||||
};
|
||||
|
||||
const { response, mockSavedObjectsService } = await request(
|
||||
'POST',
|
||||
'/api/spaces/_resolve_copy_saved_objects_errors',
|
||||
{
|
||||
expectSpacesClientCall: false,
|
||||
payload,
|
||||
}
|
||||
);
|
||||
|
||||
const { statusCode } = response;
|
||||
|
||||
expect(statusCode).toEqual(200);
|
||||
expect(mockSavedObjectsService.importExport.resolveImportErrors).toHaveBeenCalledTimes(1);
|
||||
const [
|
||||
resolveImportErrorsCallOptions,
|
||||
] = mockSavedObjectsService.importExport.resolveImportErrors.mock.calls[0];
|
||||
|
||||
expect(resolveImportErrorsCallOptions).toMatchObject({
|
||||
namespace: 'a-space',
|
||||
supportedTypes: ['visualization', 'dashboard', 'index-pattern'],
|
||||
});
|
||||
});
|
||||
|
||||
test('resolves conflicts for multiple spaces', async () => {
|
||||
const payload = {
|
||||
objects: [{ type: 'visualization', id: 'bar' }],
|
||||
retries: {
|
||||
['a-space']: [
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'bar',
|
||||
overwrite: true,
|
||||
},
|
||||
],
|
||||
['b-space']: [
|
||||
{
|
||||
type: 'globalType',
|
||||
id: 'bar',
|
||||
overwrite: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const { response, mockSavedObjectsService } = await request(
|
||||
'POST',
|
||||
'/api/spaces/_resolve_copy_saved_objects_errors',
|
||||
{
|
||||
expectSpacesClientCall: false,
|
||||
payload,
|
||||
}
|
||||
);
|
||||
|
||||
const { statusCode } = response;
|
||||
|
||||
expect(statusCode).toEqual(200);
|
||||
expect(mockSavedObjectsService.importExport.resolveImportErrors).toHaveBeenCalledTimes(2);
|
||||
const [
|
||||
resolveImportErrorsFirstCallOptions,
|
||||
] = mockSavedObjectsService.importExport.resolveImportErrors.mock.calls[0];
|
||||
|
||||
expect(resolveImportErrorsFirstCallOptions).toMatchObject({
|
||||
namespace: 'a-space',
|
||||
supportedTypes: ['visualization', 'dashboard', 'index-pattern'],
|
||||
});
|
||||
|
||||
const [
|
||||
resolveImportErrorsSecondCallOptions,
|
||||
] = mockSavedObjectsService.importExport.resolveImportErrors.mock.calls[1];
|
||||
|
||||
expect(resolveImportErrorsSecondCallOptions).toMatchObject({
|
||||
namespace: 'b-space',
|
||||
supportedTypes: ['visualization', 'dashboard', 'index-pattern'],
|
||||
});
|
||||
});
|
||||
});
|
145
x-pack/legacy/plugins/spaces/server/routes/api/external/copy_to_space.ts
vendored
Normal file
145
x-pack/legacy/plugins/spaces/server/routes/api/external/copy_to_space.ts
vendored
Normal file
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* 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 Joi from 'joi';
|
||||
import { Legacy } from 'kibana';
|
||||
import {
|
||||
copySavedObjectsToSpacesFactory,
|
||||
resolveCopySavedObjectsToSpacesConflictsFactory,
|
||||
} from '../../../lib/copy_to_spaces';
|
||||
import { ExternalRouteDeps } from '.';
|
||||
import { COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS } from '../../../lib/copy_to_spaces/copy_to_spaces';
|
||||
import { SPACE_ID_REGEX } from '../../../lib/space_schema';
|
||||
|
||||
interface CopyPayload {
|
||||
spaces: string[];
|
||||
objects: Array<{ type: string; id: string }>;
|
||||
includeReferences: boolean;
|
||||
overwrite: boolean;
|
||||
}
|
||||
|
||||
interface ResolveConflictsPayload {
|
||||
objects: Array<{ type: string; id: string }>;
|
||||
includeReferences: boolean;
|
||||
retries: {
|
||||
[spaceId: string]: Array<{
|
||||
type: string;
|
||||
id: string;
|
||||
overwrite: boolean;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
export function initCopyToSpacesApi(deps: ExternalRouteDeps) {
|
||||
const { http, spacesService, savedObjects, routePreCheckLicenseFn } = deps;
|
||||
|
||||
http.route({
|
||||
method: 'POST',
|
||||
path: '/api/spaces/_copy_saved_objects',
|
||||
async handler(request: Legacy.Request, h: Legacy.ResponseToolkit) {
|
||||
const savedObjectsClient = savedObjects.getScopedSavedObjectsClient(
|
||||
request,
|
||||
COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS
|
||||
);
|
||||
|
||||
const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory(
|
||||
savedObjectsClient,
|
||||
savedObjects
|
||||
);
|
||||
|
||||
const {
|
||||
spaces: destinationSpaceIds,
|
||||
objects,
|
||||
includeReferences,
|
||||
overwrite,
|
||||
} = request.payload as CopyPayload;
|
||||
|
||||
const sourceSpaceId = spacesService.getSpaceId(request);
|
||||
|
||||
const copyResponse = await copySavedObjectsToSpaces(sourceSpaceId, destinationSpaceIds, {
|
||||
objects,
|
||||
includeReferences,
|
||||
overwrite,
|
||||
});
|
||||
|
||||
return h.response(copyResponse);
|
||||
},
|
||||
options: {
|
||||
tags: ['access:copySavedObjectsToSpaces'],
|
||||
validate: {
|
||||
payload: {
|
||||
spaces: Joi.array()
|
||||
.items(
|
||||
Joi.string().regex(SPACE_ID_REGEX, `lower case, a-z, 0-9, "_", and "-" are allowed`)
|
||||
)
|
||||
.unique(),
|
||||
objects: Joi.array()
|
||||
.items(Joi.object({ type: Joi.string(), id: Joi.string() }))
|
||||
.unique(),
|
||||
includeReferences: Joi.bool().default(false),
|
||||
overwrite: Joi.bool().default(false),
|
||||
},
|
||||
},
|
||||
pre: [routePreCheckLicenseFn],
|
||||
},
|
||||
});
|
||||
|
||||
http.route({
|
||||
method: 'POST',
|
||||
path: '/api/spaces/_resolve_copy_saved_objects_errors',
|
||||
async handler(request: Legacy.Request, h: Legacy.ResponseToolkit) {
|
||||
const savedObjectsClient = savedObjects.getScopedSavedObjectsClient(
|
||||
request,
|
||||
COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS
|
||||
);
|
||||
|
||||
const resolveCopySavedObjectsToSpacesConflicts = resolveCopySavedObjectsToSpacesConflictsFactory(
|
||||
savedObjectsClient,
|
||||
savedObjects
|
||||
);
|
||||
|
||||
const { objects, includeReferences, retries } = request.payload as ResolveConflictsPayload;
|
||||
|
||||
const sourceSpaceId = spacesService.getSpaceId(request);
|
||||
|
||||
const resolveConflictsResponse = await resolveCopySavedObjectsToSpacesConflicts(
|
||||
sourceSpaceId,
|
||||
{
|
||||
objects,
|
||||
includeReferences,
|
||||
retries,
|
||||
}
|
||||
);
|
||||
|
||||
return h.response(resolveConflictsResponse);
|
||||
},
|
||||
options: {
|
||||
tags: ['access:copySavedObjectsToSpaces'],
|
||||
validate: {
|
||||
payload: Joi.object({
|
||||
objects: Joi.array()
|
||||
.items(Joi.object({ type: Joi.string(), id: Joi.string() }))
|
||||
.required()
|
||||
.unique(),
|
||||
includeReferences: Joi.bool().default(false),
|
||||
retries: Joi.object()
|
||||
.pattern(
|
||||
SPACE_ID_REGEX,
|
||||
Joi.array().items(
|
||||
Joi.object({
|
||||
type: Joi.string().required(),
|
||||
id: Joi.string().required(),
|
||||
overwrite: Joi.boolean().default(false),
|
||||
})
|
||||
)
|
||||
)
|
||||
.required(),
|
||||
}).default(),
|
||||
},
|
||||
pre: [routePreCheckLicenseFn],
|
||||
},
|
||||
});
|
||||
}
|
|
@ -52,6 +52,29 @@ describe('GET spaces', () => {
|
|||
expect(resultSpaces.map(s => s.id)).toEqual(spaces.map(s => s.id));
|
||||
});
|
||||
|
||||
test(`'GET spaces' returns all available spaces with the 'any' purpose`, async () => {
|
||||
const { response } = await request('GET', '/api/spaces/space?purpose=any');
|
||||
|
||||
const { statusCode, payload } = response;
|
||||
|
||||
expect(statusCode).toEqual(200);
|
||||
const resultSpaces: Space[] = JSON.parse(payload);
|
||||
expect(resultSpaces.map(s => s.id)).toEqual(spaces.map(s => s.id));
|
||||
});
|
||||
|
||||
test(`'GET spaces' returns all available spaces with the 'copySavedObjectsIntoSpace' purpose`, async () => {
|
||||
const { response } = await request(
|
||||
'GET',
|
||||
'/api/spaces/space?purpose=copySavedObjectsIntoSpace'
|
||||
);
|
||||
|
||||
const { statusCode, payload } = response;
|
||||
|
||||
expect(statusCode).toEqual(200);
|
||||
const resultSpaces: Space[] = JSON.parse(payload);
|
||||
expect(resultSpaces.map(s => s.id)).toEqual(spaces.map(s => s.id));
|
||||
});
|
||||
|
||||
test(`returns result of routePreCheckLicense`, async () => {
|
||||
const { response } = await request('GET', '/api/spaces/space', {
|
||||
preCheckLicenseImpl: () => Boom.forbidden('test forbidden message'),
|
||||
|
|
|
@ -5,9 +5,11 @@
|
|||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import Joi from 'joi';
|
||||
import { RequestQuery } from 'hapi';
|
||||
import { Space } from '../../../../common/model/space';
|
||||
import { wrapError } from '../../../lib/errors';
|
||||
import { SpacesClient } from '../../../lib/spaces_client';
|
||||
import { SpacesClient, GetSpacePurpose } from '../../../lib/spaces_client';
|
||||
import { ExternalRouteDeps, ExternalRouteRequestFacade } from '.';
|
||||
|
||||
export function initGetSpacesApi(deps: ExternalRouteDeps) {
|
||||
|
@ -19,16 +21,18 @@ export function initGetSpacesApi(deps: ExternalRouteDeps) {
|
|||
async handler(request: ExternalRouteRequestFacade) {
|
||||
log.debug(`Inside GET /api/spaces/space`);
|
||||
|
||||
const purpose: GetSpacePurpose = (request.query as RequestQuery).purpose as GetSpacePurpose;
|
||||
|
||||
const spacesClient: SpacesClient = await spacesService.scopedClient(request);
|
||||
|
||||
let spaces: Space[];
|
||||
|
||||
try {
|
||||
log.debug(`Attempting to retrieve all spaces`);
|
||||
spaces = await spacesClient.getAll();
|
||||
log.debug(`Retrieved ${spaces.length} spaces`);
|
||||
log.debug(`Attempting to retrieve all spaces for ${purpose} purpose`);
|
||||
spaces = await spacesClient.getAll(purpose);
|
||||
log.debug(`Retrieved ${spaces.length} spaces for ${purpose} purpose`);
|
||||
} catch (error) {
|
||||
log.debug(`Error retrieving spaces: ${error}`);
|
||||
log.debug(`Error retrieving spaces for ${purpose} purpose: ${error}`);
|
||||
return wrapError(error);
|
||||
}
|
||||
|
||||
|
@ -36,6 +40,13 @@ export function initGetSpacesApi(deps: ExternalRouteDeps) {
|
|||
},
|
||||
options: {
|
||||
pre: [routePreCheckLicenseFn],
|
||||
validate: {
|
||||
query: Joi.object().keys({
|
||||
purpose: Joi.string()
|
||||
.valid('any', 'copySavedObjectsIntoSpace')
|
||||
.default('any'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import { initPostSpacesApi } from './post';
|
|||
import { initPutSpacesApi } from './put';
|
||||
import { SpacesServiceSetup } from '../../../new_platform/spaces_service/spaces_service';
|
||||
import { SpacesHttpServiceSetup } from '../../../new_platform/plugin';
|
||||
import { initCopyToSpacesApi } from './copy_to_space';
|
||||
|
||||
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
|
||||
|
||||
|
@ -43,4 +44,5 @@ export function initExternalSpacesApi({ xpackMain, ...rest }: RouteDeps) {
|
|||
initGetSpacesApi(deps);
|
||||
initPostSpacesApi(deps);
|
||||
initPutSpacesApi(deps);
|
||||
initCopyToSpacesApi(deps);
|
||||
}
|
||||
|
|
|
@ -203,13 +203,15 @@ const buildKibanaFeatures = (savedObjectTypes: string[]) => {
|
|||
},
|
||||
privileges: {
|
||||
all: {
|
||||
api: ['copySavedObjectsToSpaces'],
|
||||
savedObject: {
|
||||
all: [...savedObjectTypes],
|
||||
read: [],
|
||||
},
|
||||
ui: ['read', 'edit', 'delete'],
|
||||
ui: ['read', 'edit', 'delete', 'copyIntoSpace'],
|
||||
},
|
||||
read: {
|
||||
api: ['copySavedObjectsToSpaces'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [...savedObjectTypes],
|
||||
|
|
|
@ -105,3 +105,274 @@
|
|||
"type": "_doc"
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "_doc",
|
||||
"value": {
|
||||
"id": "dashboard:cts_dashboard",
|
||||
"index": ".kibana",
|
||||
"source": {
|
||||
"dashboard": {
|
||||
"description": "Copy to Space Dashboard from the default space",
|
||||
"title": "This is the default test space CTS dashboard"
|
||||
},
|
||||
"references": [{
|
||||
"type": "visualization",
|
||||
"id": "cts_vis_1_default",
|
||||
"name": "CTS Vis 1"
|
||||
}, {
|
||||
"type": "visualization",
|
||||
"id": "cts_vis_2_default",
|
||||
"name": "CTS Vis 2"
|
||||
}, {
|
||||
"type": "visualization",
|
||||
"id": "cts_vis_3",
|
||||
"name": "CTS Vis 3"
|
||||
}],
|
||||
"type": "dashboard",
|
||||
"updated_at": "2017-09-21T18:49:16.270Z"
|
||||
},
|
||||
"type": "_doc"
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "_doc",
|
||||
"value": {
|
||||
"id": "visualization:cts_vis_1_default",
|
||||
"index": ".kibana",
|
||||
"source": {
|
||||
"visualization": {
|
||||
"title": "CTS vis 1 from default space",
|
||||
"description": "AreaChart",
|
||||
"kibanaSavedObjectMeta": {"searchSourceJSON": "{}"},
|
||||
"uiStateJSON": "{}",
|
||||
"version": 1,
|
||||
"visState": "{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}"
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"type": "index-pattern",
|
||||
"id": "cts_ip_1",
|
||||
"name": "CTS IP 1"
|
||||
}
|
||||
],
|
||||
"type": "visualization",
|
||||
"updated_at": "2017-09-21T18:49:16.270Z"
|
||||
},
|
||||
"type": "_doc"
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "_doc",
|
||||
"value": {
|
||||
"id": "visualization:cts_vis_2_default",
|
||||
"index": ".kibana",
|
||||
"source": {
|
||||
"visualization": {
|
||||
"title": "CTS vis 2 from default space",
|
||||
"description": "AreaChart",
|
||||
"kibanaSavedObjectMeta": {"searchSourceJSON": "{}"},
|
||||
"uiStateJSON": "{}",
|
||||
"version": 1,
|
||||
"visState": "{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}"
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"type": "index-pattern",
|
||||
"id": "cts_ip_1",
|
||||
"name": "CTS IP 1"
|
||||
}
|
||||
],
|
||||
"type": "visualization",
|
||||
"updated_at": "2017-09-21T18:49:16.270Z"
|
||||
},
|
||||
"type": "_doc"
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "_doc",
|
||||
"value": {
|
||||
"id": "visualization:cts_vis_3",
|
||||
"index": ".kibana",
|
||||
"source": {
|
||||
"visualization": {
|
||||
"title": "CTS vis 3 from default space",
|
||||
"description": "AreaChart",
|
||||
"kibanaSavedObjectMeta": {"searchSourceJSON": "{}"},
|
||||
"uiStateJSON": "{}",
|
||||
"version": 1,
|
||||
"visState": "{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}"
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"type": "index-pattern",
|
||||
"id": "cts_ip_1",
|
||||
"name": "CTS IP 1"
|
||||
}
|
||||
],
|
||||
"type": "visualization",
|
||||
"updated_at": "2017-09-21T18:49:16.270Z"
|
||||
},
|
||||
"type": "_doc"
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "_doc",
|
||||
"value": {
|
||||
"id": "space_1:dashboard:cts_dashboard",
|
||||
"index": ".kibana",
|
||||
"source": {
|
||||
"dashboard": {
|
||||
"description": "Copy to Space Dashboard from space_1 space",
|
||||
"title": "This is the space_1 test space CTS dashboard"
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"type": "visualization",
|
||||
"id": "cts_vis_1_space_1",
|
||||
"name": "CTS Vis 1"
|
||||
},
|
||||
{
|
||||
"type": "visualization",
|
||||
"id": "cts_vis_2_space_1",
|
||||
"name": "CTS Vis 2"
|
||||
},
|
||||
{
|
||||
"type": "visualization",
|
||||
"id": "cts_vis_3",
|
||||
"name": "CTS Vis 3"
|
||||
}
|
||||
],
|
||||
"type": "dashboard",
|
||||
"updated_at": "2017-09-21T18:49:16.270Z",
|
||||
"namespace": "space_1"
|
||||
},
|
||||
"type": "_doc"
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "_doc",
|
||||
"value": {
|
||||
"id": "space_1:visualization:cts_vis_1_space_1",
|
||||
"index": ".kibana",
|
||||
"source": {
|
||||
"visualization": {
|
||||
"title": "CTS vis 1 from space_1 space",
|
||||
"description": "AreaChart",
|
||||
"kibanaSavedObjectMeta": {"searchSourceJSON": "{}"},
|
||||
"uiStateJSON": "{}",
|
||||
"version": 1,
|
||||
"visState": "{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}"
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"type": "index-pattern",
|
||||
"id": "cts_ip_1",
|
||||
"name": "CTS IP 1"
|
||||
}
|
||||
],
|
||||
"type": "visualization",
|
||||
"updated_at": "2017-09-21T18:49:16.270Z",
|
||||
"namespace": "space_1"
|
||||
},
|
||||
"type": "_doc"
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "_doc",
|
||||
"value": {
|
||||
"id": "space_1:visualization:cts_vis_2_space_1",
|
||||
"index": ".kibana",
|
||||
"source": {
|
||||
"visualization": {
|
||||
"title": "CTS vis 2 from space_1 space",
|
||||
"description": "AreaChart",
|
||||
"kibanaSavedObjectMeta": {"searchSourceJSON": "{}"},
|
||||
"uiStateJSON": "{}",
|
||||
"version": 1,
|
||||
"visState": "{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}"
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"type": "index-pattern",
|
||||
"id": "cts_ip_1",
|
||||
"name": "CTS IP 1"
|
||||
}
|
||||
],
|
||||
"type": "visualization",
|
||||
"updated_at": "2017-09-21T18:49:16.270Z",
|
||||
"namespace": "space_1"
|
||||
},
|
||||
"type": "_doc"
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "_doc",
|
||||
"value": {
|
||||
"id": "space_1:visualization:cts_vis_3",
|
||||
"index": ".kibana",
|
||||
"source": {
|
||||
"visualization": {
|
||||
"title": "CTS vis 3 from space_1 space",
|
||||
"description": "AreaChart",
|
||||
"kibanaSavedObjectMeta": {"searchSourceJSON": "{}"},
|
||||
"uiStateJSON": "{}",
|
||||
"version": 1,
|
||||
"visState": "{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}"
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"type": "index-pattern",
|
||||
"id": "cts_ip_1",
|
||||
"name": "CTS IP 1"
|
||||
}
|
||||
],
|
||||
"type": "visualization",
|
||||
"updated_at": "2017-09-21T18:49:16.270Z",
|
||||
"namespace": "space_1"
|
||||
},
|
||||
"type": "_doc"
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "_doc",
|
||||
"value": {
|
||||
"id": "index-pattern:cts_ip_1",
|
||||
"index": ".kibana",
|
||||
"source": {
|
||||
"index-pattern": {
|
||||
"title": "Copy to Space index pattern 1 from default space"
|
||||
},
|
||||
"references": [],
|
||||
"type": "index-pattern",
|
||||
"updated_at": "2017-09-21T18:49:16.270Z"
|
||||
},
|
||||
"type": "_doc"
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "_doc",
|
||||
"value": {
|
||||
"id": "space_1:index-pattern:cts_ip_1",
|
||||
"index": ".kibana",
|
||||
"source": {
|
||||
"index-pattern": {
|
||||
"title": "Copy to Space index pattern 1 from space_1 space"
|
||||
},
|
||||
"references": [],
|
||||
"type": "index-pattern",
|
||||
"updated_at": "2017-09-21T18:49:16.270Z",
|
||||
"namespace": "space_1"
|
||||
},
|
||||
"type": "_doc"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,20 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"references": {
|
||||
"type": "nested",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"type": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"id": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"properties": {
|
||||
"description": {
|
||||
|
|
|
@ -65,6 +65,22 @@ export const AUTHENTICATION = {
|
|||
username: 'a_kibana_rbac_space_1_2_read_user',
|
||||
password: 'password',
|
||||
},
|
||||
KIBANA_RBAC_DEFAULT_SPACE_SAVED_OBJECTS_ALL_USER: {
|
||||
username: 'a_kibana_rbac_default_space_saved_objects_all_user',
|
||||
password: 'password',
|
||||
},
|
||||
KIBANA_RBAC_DEFAULT_SPACE_SAVED_OBJECTS_READ_USER: {
|
||||
username: 'a_kibana_rbac_default_space_saved_objects_read_user',
|
||||
password: 'password',
|
||||
},
|
||||
KIBANA_RBAC_SPACE_1_SAVED_OBJECTS_ALL_USER: {
|
||||
username: 'a_kibana_rbac_space_1_saved_objects_all_user',
|
||||
password: 'password',
|
||||
},
|
||||
KIBANA_RBAC_SPACE_1_SAVED_OBJECTS_READ_USER: {
|
||||
username: 'a_kibana_rbac_space_1_saved_objects_read_user',
|
||||
password: 'password',
|
||||
},
|
||||
APM_USER: {
|
||||
username: 'a_apm_user',
|
||||
password: 'password',
|
||||
|
|
|
@ -181,6 +181,66 @@ export const createUsersAndRoles = async (es: any, supertest: SuperTest<any>) =>
|
|||
})
|
||||
.expect(204);
|
||||
|
||||
await supertest
|
||||
.put('/api/security/role/kibana_rbac_default_space_saved_objects_all_user')
|
||||
.send({
|
||||
kibana: [
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
savedObjectsManagement: ['all'],
|
||||
},
|
||||
spaces: ['default'],
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect(204);
|
||||
|
||||
await supertest
|
||||
.put('/api/security/role/kibana_rbac_default_space_saved_objects_read_user')
|
||||
.send({
|
||||
kibana: [
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
savedObjectsManagement: ['read'],
|
||||
},
|
||||
spaces: ['default'],
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect(204);
|
||||
|
||||
await supertest
|
||||
.put('/api/security/role/kibana_rbac_space_1_saved_objects_all_user')
|
||||
.send({
|
||||
kibana: [
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
savedObjectsManagement: ['all'],
|
||||
},
|
||||
spaces: ['space_1'],
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect(204);
|
||||
|
||||
await supertest
|
||||
.put('/api/security/role/kibana_rbac_space_1_saved_objects_read_user')
|
||||
.send({
|
||||
kibana: [
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
savedObjectsManagement: ['read'],
|
||||
},
|
||||
spaces: ['space_1'],
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect(204);
|
||||
|
||||
await es.shield.putUser({
|
||||
username: AUTHENTICATION.NOT_A_KIBANA_USER.username,
|
||||
body: {
|
||||
|
@ -321,6 +381,46 @@ export const createUsersAndRoles = async (es: any, supertest: SuperTest<any>) =>
|
|||
},
|
||||
});
|
||||
|
||||
await es.shield.putUser({
|
||||
username: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_SAVED_OBJECTS_ALL_USER.username,
|
||||
body: {
|
||||
password: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_SAVED_OBJECTS_ALL_USER.password,
|
||||
roles: ['kibana_rbac_default_space_saved_objects_all_user'],
|
||||
full_name: 'a kibana rbac default space saved objects management all user',
|
||||
email: 'a_kibana_rbac_default_space_saved_objects_all_user@elastic.co',
|
||||
},
|
||||
});
|
||||
|
||||
await es.shield.putUser({
|
||||
username: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_SAVED_OBJECTS_READ_USER.username,
|
||||
body: {
|
||||
password: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_SAVED_OBJECTS_READ_USER.password,
|
||||
roles: ['kibana_rbac_default_space_saved_objects_read_user'],
|
||||
full_name: 'a kibana rbac default space saved objects management read user',
|
||||
email: 'a_kibana_rbac_default_space_saved_objects_read_user@elastic.co',
|
||||
},
|
||||
});
|
||||
|
||||
await es.shield.putUser({
|
||||
username: AUTHENTICATION.KIBANA_RBAC_SPACE_1_SAVED_OBJECTS_ALL_USER.username,
|
||||
body: {
|
||||
password: AUTHENTICATION.KIBANA_RBAC_SPACE_1_SAVED_OBJECTS_ALL_USER.password,
|
||||
roles: ['kibana_rbac_space_1_saved_objects_all_user'],
|
||||
full_name: 'a kibana rbac space 1 saved objects management all user',
|
||||
email: 'a_kibana_rbac_space_1_saved_objects_all_user@elastic.co',
|
||||
},
|
||||
});
|
||||
|
||||
await es.shield.putUser({
|
||||
username: AUTHENTICATION.KIBANA_RBAC_SPACE_1_SAVED_OBJECTS_READ_USER.username,
|
||||
body: {
|
||||
password: AUTHENTICATION.KIBANA_RBAC_SPACE_1_SAVED_OBJECTS_READ_USER.password,
|
||||
roles: ['kibana_rbac_space_1_saved_objects_read_user'],
|
||||
full_name: 'a kibana rbac space 1 saved objects management read user',
|
||||
email: 'a_kibana_rbac_space_1_saved_objects_read_user@elastic.co',
|
||||
},
|
||||
});
|
||||
|
||||
await es.shield.putUser({
|
||||
username: AUTHENTICATION.APM_USER.username,
|
||||
body: {
|
||||
|
|
|
@ -0,0 +1,552 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { SuperTest } from 'supertest';
|
||||
import { EsArchiver } from 'src/es_archiver';
|
||||
import { DEFAULT_SPACE_ID } from '../../../../legacy/plugins/spaces/common/constants';
|
||||
import { CopyResponse } from '../../../../legacy/plugins/spaces/server/lib/copy_to_spaces';
|
||||
import { getUrlPrefix } from '../lib/space_test_utils';
|
||||
import { DescribeFn, TestDefinitionAuthentication } from '../lib/types';
|
||||
|
||||
type TestResponse = Record<string, any>;
|
||||
|
||||
interface CopyToSpaceTest {
|
||||
statusCode: number;
|
||||
response: (resp: TestResponse) => Promise<void>;
|
||||
}
|
||||
|
||||
interface CopyToSpaceTests {
|
||||
noConflictsWithoutReferences: CopyToSpaceTest;
|
||||
noConflictsWithReferences: CopyToSpaceTest;
|
||||
withConflictsOverwriting: CopyToSpaceTest;
|
||||
withConflictsWithoutOverwriting: CopyToSpaceTest;
|
||||
nonExistentSpace: CopyToSpaceTest;
|
||||
multipleSpaces: {
|
||||
statusCode: number;
|
||||
withConflictsResponse: (resp: TestResponse) => Promise<void>;
|
||||
noConflictsResponse: (resp: TestResponse) => Promise<void>;
|
||||
};
|
||||
}
|
||||
|
||||
interface CopyToSpaceTestDefinition {
|
||||
user?: TestDefinitionAuthentication;
|
||||
spaceId?: string;
|
||||
tests: CopyToSpaceTests;
|
||||
}
|
||||
|
||||
interface CountByTypeBucket {
|
||||
key: string;
|
||||
doc_count: number;
|
||||
}
|
||||
interface SpaceBucket {
|
||||
doc_count: number;
|
||||
key: string;
|
||||
countByType: {
|
||||
doc_count_error_upper_bound: number;
|
||||
sum_other_doc_count: number;
|
||||
buckets: CountByTypeBucket[];
|
||||
};
|
||||
}
|
||||
|
||||
const INITIAL_COUNTS: Record<string, Record<string, number>> = {
|
||||
[DEFAULT_SPACE_ID]: {
|
||||
dashboard: 2,
|
||||
visualization: 3,
|
||||
'index-pattern': 1,
|
||||
},
|
||||
space_1: {
|
||||
dashboard: 2,
|
||||
visualization: 3,
|
||||
'index-pattern': 1,
|
||||
},
|
||||
space_2: {
|
||||
dashboard: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const getDestinationWithoutConflicts = () => 'space_2';
|
||||
const getDestinationWithConflicts = (originSpaceId?: string) => {
|
||||
if (!originSpaceId || originSpaceId === DEFAULT_SPACE_ID) {
|
||||
return 'space_1';
|
||||
}
|
||||
return DEFAULT_SPACE_ID;
|
||||
};
|
||||
|
||||
export function copyToSpaceTestSuiteFactory(
|
||||
es: any,
|
||||
esArchiver: EsArchiver,
|
||||
supertest: SuperTest<any>
|
||||
) {
|
||||
const collectSpaceContents = async () => {
|
||||
const response = await es.search({
|
||||
index: '.kibana',
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
must_not: {
|
||||
term: {
|
||||
// exclude spaces from the result set.
|
||||
// we don't assert on these.
|
||||
type: 'space',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
count: {
|
||||
terms: {
|
||||
field: 'namespace',
|
||||
missing: DEFAULT_SPACE_ID,
|
||||
size: 10,
|
||||
},
|
||||
aggs: {
|
||||
countByType: {
|
||||
terms: {
|
||||
field: 'type',
|
||||
missing: 'UNKNOWN',
|
||||
size: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
buckets: response.aggregations.count.buckets as SpaceBucket[],
|
||||
};
|
||||
};
|
||||
|
||||
const assertSpaceCounts = async (
|
||||
spaceId: string,
|
||||
expectedCounts: Record<string, number> = {}
|
||||
) => {
|
||||
const bucketSorter = (b1: CountByTypeBucket, b2: CountByTypeBucket) =>
|
||||
b1.key < b2.key ? -1 : 1;
|
||||
const { buckets } = await collectSpaceContents();
|
||||
|
||||
const spaceBucket = buckets.find(b => b.key === spaceId);
|
||||
|
||||
if (!spaceBucket) {
|
||||
expect(Object.keys(expectedCounts).length).to.eql(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const { countByType } = spaceBucket;
|
||||
const expectedBuckets = Object.entries(expectedCounts).reduce(
|
||||
(acc, entry) => {
|
||||
const [type, count] = entry;
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
key: type,
|
||||
doc_count: count,
|
||||
},
|
||||
];
|
||||
},
|
||||
[] as CountByTypeBucket[]
|
||||
);
|
||||
|
||||
expectedBuckets.sort(bucketSorter);
|
||||
countByType.buckets.sort(bucketSorter);
|
||||
|
||||
expect(countByType).to.eql({
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: expectedBuckets,
|
||||
});
|
||||
};
|
||||
|
||||
const expectRbacForbiddenResponse = async (resp: TestResponse) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 403,
|
||||
error: 'Forbidden',
|
||||
message: 'Unable to bulk_get dashboard',
|
||||
});
|
||||
};
|
||||
|
||||
const expectNotFoundResponse = async (resp: TestResponse) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Not Found',
|
||||
});
|
||||
};
|
||||
|
||||
const createExpectNoConflictsWithoutReferencesForSpace = (
|
||||
spaceId: string,
|
||||
expectedDashboardCount: number
|
||||
) => async (resp: TestResponse) => {
|
||||
const result = resp.body as CopyResponse;
|
||||
expect(result).to.eql({
|
||||
[spaceId]: {
|
||||
success: true,
|
||||
successCount: 1,
|
||||
},
|
||||
} as CopyResponse);
|
||||
|
||||
// Query ES to ensure that we copied everything we expected
|
||||
await assertSpaceCounts(spaceId, {
|
||||
dashboard: expectedDashboardCount,
|
||||
});
|
||||
};
|
||||
|
||||
const expectNoConflictsWithoutReferencesResult = createExpectNoConflictsWithoutReferencesForSpace(
|
||||
getDestinationWithoutConflicts(),
|
||||
2
|
||||
);
|
||||
|
||||
const expectNoConflictsForNonExistentSpaceResult = createExpectNoConflictsWithoutReferencesForSpace(
|
||||
'non_existent_space',
|
||||
1
|
||||
);
|
||||
|
||||
const expectNoConflictsWithReferencesResult = async (resp: TestResponse) => {
|
||||
const destination = getDestinationWithoutConflicts();
|
||||
const result = resp.body as CopyResponse;
|
||||
expect(result).to.eql({
|
||||
[destination]: {
|
||||
success: true,
|
||||
successCount: 5,
|
||||
},
|
||||
} as CopyResponse);
|
||||
|
||||
// Query ES to ensure that we copied everything we expected
|
||||
await assertSpaceCounts(destination, {
|
||||
dashboard: 2,
|
||||
visualization: 3,
|
||||
'index-pattern': 1,
|
||||
});
|
||||
};
|
||||
|
||||
const getDestinationSpace = (
|
||||
sourceSpaceId: string,
|
||||
type: 'with-conflicts' | 'without-conflicts' | 'non-existent'
|
||||
) => {
|
||||
if (type === 'non-existent') {
|
||||
return 'non_existent_space';
|
||||
}
|
||||
|
||||
return type === 'with-conflicts'
|
||||
? getDestinationWithConflicts(sourceSpaceId)
|
||||
: getDestinationWithoutConflicts();
|
||||
};
|
||||
|
||||
const createExpectUnauthorizedAtSpaceWithReferencesResult = (
|
||||
spaceId: string = DEFAULT_SPACE_ID,
|
||||
type: 'with-conflicts' | 'without-conflicts'
|
||||
) => async (resp: TestResponse) => {
|
||||
const destination = getDestinationSpace(spaceId, type);
|
||||
|
||||
const result = resp.body as CopyResponse;
|
||||
expect(result).to.eql({
|
||||
[destination]: {
|
||||
success: false,
|
||||
successCount: 0,
|
||||
errors: [
|
||||
{
|
||||
statusCode: 403,
|
||||
error: 'Forbidden',
|
||||
message: 'Unable to bulk_create dashboard,index-pattern,visualization',
|
||||
},
|
||||
],
|
||||
},
|
||||
} as CopyResponse);
|
||||
|
||||
// Query ES to ensure that nothing was copied
|
||||
await assertSpaceCounts(destination, INITIAL_COUNTS[destination]);
|
||||
};
|
||||
|
||||
const createExpectUnauthorizedAtSpaceWithoutReferencesResult = (
|
||||
spaceId: string = DEFAULT_SPACE_ID,
|
||||
type: 'with-conflicts' | 'without-conflicts' | 'non-existent'
|
||||
) => async (resp: TestResponse) => {
|
||||
const destination = getDestinationSpace(spaceId, type);
|
||||
|
||||
const result = resp.body as CopyResponse;
|
||||
expect(result).to.eql({
|
||||
[destination]: {
|
||||
success: false,
|
||||
successCount: 0,
|
||||
errors: [
|
||||
{
|
||||
statusCode: 403,
|
||||
error: 'Forbidden',
|
||||
message: 'Unable to bulk_create dashboard',
|
||||
},
|
||||
],
|
||||
},
|
||||
} as CopyResponse);
|
||||
|
||||
// Query ES to ensure that nothing was copied
|
||||
await assertSpaceCounts(destination, INITIAL_COUNTS[destination]);
|
||||
};
|
||||
|
||||
const createExpectWithConflictsOverwritingResult = (spaceId?: string) => async (resp: {
|
||||
[key: string]: any;
|
||||
}) => {
|
||||
const destination = getDestinationWithConflicts(spaceId);
|
||||
const result = resp.body as CopyResponse;
|
||||
expect(result).to.eql({
|
||||
[destination]: {
|
||||
success: true,
|
||||
successCount: 5,
|
||||
},
|
||||
} as CopyResponse);
|
||||
|
||||
// Query ES to ensure that we copied everything we expected
|
||||
await assertSpaceCounts(destination, {
|
||||
dashboard: 2,
|
||||
visualization: 5,
|
||||
'index-pattern': 1,
|
||||
});
|
||||
};
|
||||
|
||||
const createExpectWithConflictsWithoutOverwritingResult = (spaceId?: string) => async (resp: {
|
||||
[key: string]: any;
|
||||
}) => {
|
||||
const errorSorter = (e1: any, e2: any) => (e1.id < e2.id ? -1 : 1);
|
||||
|
||||
const destination = getDestinationWithConflicts(spaceId);
|
||||
|
||||
const result = resp.body as CopyResponse;
|
||||
result[destination].errors!.sort(errorSorter);
|
||||
|
||||
const expectedErrors = [
|
||||
{
|
||||
error: {
|
||||
type: 'conflict',
|
||||
},
|
||||
id: 'cts_dashboard',
|
||||
title: `This is the ${spaceId} test space CTS dashboard`,
|
||||
type: 'dashboard',
|
||||
},
|
||||
{
|
||||
error: {
|
||||
type: 'conflict',
|
||||
},
|
||||
id: 'cts_ip_1',
|
||||
title: `Copy to Space index pattern 1 from ${spaceId} space`,
|
||||
type: 'index-pattern',
|
||||
},
|
||||
{
|
||||
error: {
|
||||
type: 'conflict',
|
||||
},
|
||||
id: 'cts_vis_3',
|
||||
title: `CTS vis 3 from ${spaceId} space`,
|
||||
type: 'visualization',
|
||||
},
|
||||
];
|
||||
expectedErrors.sort(errorSorter);
|
||||
|
||||
expect(result).to.eql({
|
||||
[destination]: {
|
||||
success: false,
|
||||
successCount: 2,
|
||||
errors: expectedErrors,
|
||||
},
|
||||
} as CopyResponse);
|
||||
|
||||
// Query ES to ensure that we copied everything we expected
|
||||
await assertSpaceCounts(destination, {
|
||||
dashboard: 2,
|
||||
visualization: 5,
|
||||
'index-pattern': 1,
|
||||
});
|
||||
};
|
||||
|
||||
const makeCopyToSpaceTest = (describeFn: DescribeFn) => (
|
||||
description: string,
|
||||
{ user = {}, spaceId = DEFAULT_SPACE_ID, tests }: CopyToSpaceTestDefinition
|
||||
) => {
|
||||
describeFn(description, () => {
|
||||
before(() => {
|
||||
// test data only allows for the following spaces as the copy origin
|
||||
expect(['default', 'space_1']).to.contain(spaceId);
|
||||
});
|
||||
|
||||
beforeEach(() => esArchiver.load('saved_objects/spaces'));
|
||||
afterEach(() => esArchiver.unload('saved_objects/spaces'));
|
||||
|
||||
it(`should return ${tests.noConflictsWithoutReferences.statusCode} when copying to space without conflicts or references`, async () => {
|
||||
const destination = getDestinationWithoutConflicts();
|
||||
|
||||
await assertSpaceCounts(destination, INITIAL_COUNTS[destination]);
|
||||
|
||||
return supertest
|
||||
.post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`)
|
||||
.auth(user.username, user.password)
|
||||
.send({
|
||||
objects: [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'cts_dashboard',
|
||||
},
|
||||
],
|
||||
spaces: [destination],
|
||||
includeReferences: false,
|
||||
overwrite: false,
|
||||
})
|
||||
.expect(tests.noConflictsWithoutReferences.statusCode)
|
||||
.then(tests.noConflictsWithoutReferences.response);
|
||||
});
|
||||
|
||||
it(`should return ${tests.noConflictsWithReferences.statusCode} when copying to space without conflicts with references`, async () => {
|
||||
const destination = getDestinationWithoutConflicts();
|
||||
|
||||
await assertSpaceCounts(destination, INITIAL_COUNTS[destination]);
|
||||
|
||||
return supertest
|
||||
.post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`)
|
||||
.auth(user.username, user.password)
|
||||
.send({
|
||||
objects: [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'cts_dashboard',
|
||||
},
|
||||
],
|
||||
spaces: [destination],
|
||||
includeReferences: true,
|
||||
overwrite: false,
|
||||
})
|
||||
.expect(tests.noConflictsWithReferences.statusCode)
|
||||
.then(tests.noConflictsWithReferences.response);
|
||||
});
|
||||
|
||||
it(`should return ${tests.withConflictsOverwriting.statusCode} when copying to space with conflicts when overwriting`, async () => {
|
||||
const destination = getDestinationWithConflicts(spaceId);
|
||||
|
||||
await assertSpaceCounts(destination, INITIAL_COUNTS[destination]);
|
||||
|
||||
return supertest
|
||||
.post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`)
|
||||
.auth(user.username, user.password)
|
||||
.send({
|
||||
objects: [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'cts_dashboard',
|
||||
},
|
||||
],
|
||||
spaces: [destination],
|
||||
includeReferences: true,
|
||||
overwrite: true,
|
||||
})
|
||||
.expect(tests.withConflictsOverwriting.statusCode)
|
||||
.then(tests.withConflictsOverwriting.response);
|
||||
});
|
||||
|
||||
it(`should return ${tests.withConflictsWithoutOverwriting.statusCode} when copying to space with conflicts without overwriting`, async () => {
|
||||
const destination = getDestinationWithConflicts(spaceId);
|
||||
|
||||
await assertSpaceCounts(destination, INITIAL_COUNTS[destination]);
|
||||
|
||||
return supertest
|
||||
.post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`)
|
||||
.auth(user.username, user.password)
|
||||
.send({
|
||||
objects: [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'cts_dashboard',
|
||||
},
|
||||
],
|
||||
spaces: [destination],
|
||||
includeReferences: true,
|
||||
overwrite: false,
|
||||
})
|
||||
.expect(tests.withConflictsWithoutOverwriting.statusCode)
|
||||
.then(tests.withConflictsWithoutOverwriting.response);
|
||||
});
|
||||
|
||||
it(`should return ${tests.multipleSpaces.statusCode} when copying to multiple spaces`, async () => {
|
||||
const conflictDestination = getDestinationWithConflicts(spaceId);
|
||||
const noConflictDestination = getDestinationWithoutConflicts();
|
||||
|
||||
return supertest
|
||||
.post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`)
|
||||
.auth(user.username, user.password)
|
||||
.send({
|
||||
objects: [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'cts_dashboard',
|
||||
},
|
||||
],
|
||||
spaces: [conflictDestination, noConflictDestination],
|
||||
includeReferences: true,
|
||||
overwrite: true,
|
||||
})
|
||||
.expect(tests.multipleSpaces.statusCode)
|
||||
.then((response: TestResponse) => {
|
||||
if (tests.multipleSpaces.statusCode === 200) {
|
||||
expect(Object.keys(response.body).length).to.eql(2);
|
||||
return Promise.all([
|
||||
tests.multipleSpaces.noConflictsResponse({
|
||||
body: {
|
||||
[noConflictDestination]: response.body[noConflictDestination],
|
||||
},
|
||||
}),
|
||||
tests.multipleSpaces.withConflictsResponse({
|
||||
body: {
|
||||
[conflictDestination]: response.body[conflictDestination],
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
// non-200 status codes will not have a response body broken out by space id, like above.
|
||||
return Promise.all([
|
||||
tests.multipleSpaces.noConflictsResponse(response),
|
||||
tests.multipleSpaces.withConflictsResponse(response),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it(`should return ${tests.nonExistentSpace.statusCode} when copying to non-existent space`, async () => {
|
||||
return supertest
|
||||
.post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`)
|
||||
.auth(user.username, user.password)
|
||||
.send({
|
||||
objects: [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'cts_dashboard',
|
||||
},
|
||||
],
|
||||
spaces: ['non_existent_space'],
|
||||
includeReferences: false,
|
||||
overwrite: true,
|
||||
})
|
||||
.expect(tests.nonExistentSpace.statusCode)
|
||||
.then(tests.nonExistentSpace.response);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const copyToSpaceTest = makeCopyToSpaceTest(describe);
|
||||
// @ts-ignore
|
||||
copyToSpaceTest.only = makeCopyToSpaceTest(describe.only);
|
||||
|
||||
return {
|
||||
copyToSpaceTest,
|
||||
expectNoConflictsWithoutReferencesResult,
|
||||
expectNoConflictsWithReferencesResult,
|
||||
expectNoConflictsForNonExistentSpaceResult,
|
||||
createExpectWithConflictsOverwritingResult,
|
||||
createExpectWithConflictsWithoutOverwritingResult,
|
||||
expectRbacForbiddenResponse,
|
||||
expectNotFoundResponse,
|
||||
createExpectUnauthorizedAtSpaceWithReferencesResult,
|
||||
createExpectUnauthorizedAtSpaceWithoutReferencesResult,
|
||||
originSpaces: ['default', 'space_1'],
|
||||
};
|
||||
}
|
|
@ -66,11 +66,19 @@ export function deleteTestSuiteFactory(es: any, esArchiver: any, supertest: Supe
|
|||
const expectedBuckets = [
|
||||
{
|
||||
key: 'default',
|
||||
doc_count: 4,
|
||||
doc_count: 9,
|
||||
countByType: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [
|
||||
{
|
||||
key: 'visualization',
|
||||
doc_count: 3,
|
||||
},
|
||||
{
|
||||
key: 'dashboard',
|
||||
doc_count: 2,
|
||||
},
|
||||
{
|
||||
key: 'space',
|
||||
doc_count: 2,
|
||||
|
@ -80,25 +88,33 @@ export function deleteTestSuiteFactory(es: any, esArchiver: any, supertest: Supe
|
|||
doc_count: 1,
|
||||
},
|
||||
{
|
||||
key: 'dashboard',
|
||||
key: 'index-pattern',
|
||||
doc_count: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
doc_count: 2,
|
||||
doc_count: 7,
|
||||
key: 'space_1',
|
||||
countByType: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [
|
||||
{
|
||||
key: 'visualization',
|
||||
doc_count: 3,
|
||||
},
|
||||
{
|
||||
key: 'dashboard',
|
||||
doc_count: 2,
|
||||
},
|
||||
{
|
||||
key: 'config',
|
||||
doc_count: 1,
|
||||
},
|
||||
{
|
||||
key: 'dashboard',
|
||||
key: 'index-pattern',
|
||||
doc_count: 1,
|
||||
},
|
||||
],
|
||||
|
|
|
@ -15,6 +15,7 @@ interface GetAllTest {
|
|||
|
||||
interface GetAllTests {
|
||||
exists: GetAllTest;
|
||||
copySavedObjectsPurpose: GetAllTest;
|
||||
}
|
||||
|
||||
interface GetAllTestDefinition {
|
||||
|
@ -76,6 +77,17 @@ export function getAllTestSuiteFactory(esArchiver: any, supertest: SuperTest<any
|
|||
.expect(tests.exists.statusCode)
|
||||
.then(tests.exists.response);
|
||||
});
|
||||
|
||||
describe('copySavedObjects purpose', () => {
|
||||
it(`should return ${tests.copySavedObjectsPurpose.statusCode}`, async () => {
|
||||
return supertest
|
||||
.get(`${getUrlPrefix(spaceId)}/api/spaces/space`)
|
||||
.query({ purpose: 'copySavedObjectsIntoSpace' })
|
||||
.auth(user.username, user.password)
|
||||
.expect(tests.copySavedObjectsPurpose.statusCode)
|
||||
.then(tests.copySavedObjectsPurpose.response);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,437 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { SuperTest } from 'supertest';
|
||||
import { EsArchiver } from 'src/es_archiver';
|
||||
import { SavedObject } from 'src/core/server';
|
||||
import { DEFAULT_SPACE_ID } from '../../../../legacy/plugins/spaces/common/constants';
|
||||
import { CopyResponse } from '../../../../legacy/plugins/spaces/server/lib/copy_to_spaces';
|
||||
import { getUrlPrefix } from '../lib/space_test_utils';
|
||||
import { DescribeFn, TestDefinitionAuthentication } from '../lib/types';
|
||||
|
||||
type TestResponse = Record<string, any>;
|
||||
|
||||
interface ResolveCopyToSpaceTest {
|
||||
statusCode: number;
|
||||
response: (resp: TestResponse) => Promise<void>;
|
||||
}
|
||||
|
||||
interface ResolveCopyToSpaceTests {
|
||||
withReferencesNotOverwriting: ResolveCopyToSpaceTest;
|
||||
withReferencesOverwriting: ResolveCopyToSpaceTest;
|
||||
withoutReferencesOverwriting: ResolveCopyToSpaceTest;
|
||||
withoutReferencesNotOverwriting: ResolveCopyToSpaceTest;
|
||||
nonExistentSpace: ResolveCopyToSpaceTest;
|
||||
}
|
||||
|
||||
interface ResolveCopyToSpaceTestDefinition {
|
||||
user?: TestDefinitionAuthentication;
|
||||
spaceId?: string;
|
||||
tests: ResolveCopyToSpaceTests;
|
||||
}
|
||||
|
||||
const NON_EXISTENT_SPACE_ID = 'non_existent_space';
|
||||
|
||||
const getDestinationSpace = (originSpaceId?: string) => {
|
||||
if (!originSpaceId || originSpaceId === DEFAULT_SPACE_ID) {
|
||||
return 'space_1';
|
||||
}
|
||||
return DEFAULT_SPACE_ID;
|
||||
};
|
||||
|
||||
export function resolveCopyToSpaceConflictsSuite(
|
||||
esArchiver: EsArchiver,
|
||||
supertestWithAuth: SuperTest<any>,
|
||||
supertestWithoutAuth: SuperTest<any>
|
||||
) {
|
||||
const getVisualizationAtSpace = async (spaceId: string): Promise<SavedObject<any>> => {
|
||||
return supertestWithAuth
|
||||
.get(`${getUrlPrefix(spaceId)}/api/saved_objects/visualization/cts_vis_3`)
|
||||
.then((response: any) => response.body);
|
||||
};
|
||||
const getDashboardAtSpace = async (spaceId: string): Promise<SavedObject<any>> => {
|
||||
return supertestWithAuth
|
||||
.get(`${getUrlPrefix(spaceId)}/api/saved_objects/dashboard/cts_dashboard`)
|
||||
.then((response: any) => response.body);
|
||||
};
|
||||
|
||||
const getObjectsAtSpace = async (spaceId: string): Promise<[SavedObject, SavedObject]> => {
|
||||
const dashboard = await getDashboardAtSpace(spaceId);
|
||||
const visualization = await getVisualizationAtSpace(spaceId);
|
||||
return [dashboard, visualization];
|
||||
};
|
||||
|
||||
const createExpectOverriddenResponseWithReferences = (sourceSpaceId: string) => async (
|
||||
response: TestResponse
|
||||
) => {
|
||||
const destination = getDestinationSpace(sourceSpaceId);
|
||||
const result = response.body;
|
||||
expect(result).to.eql({
|
||||
[destination]: {
|
||||
success: true,
|
||||
successCount: 1,
|
||||
},
|
||||
});
|
||||
const [dashboard, visualization] = await getObjectsAtSpace(destination);
|
||||
expect(dashboard.attributes.title).to.eql(
|
||||
`This is the ${destination} test space CTS dashboard`
|
||||
);
|
||||
expect(visualization.attributes.title).to.eql(`CTS vis 3 from ${sourceSpaceId} space`);
|
||||
};
|
||||
|
||||
const createExpectOverriddenResponseWithoutReferences = (
|
||||
sourceSpaceId: string,
|
||||
destinationSpaceId: string = getDestinationSpace(sourceSpaceId)
|
||||
) => async (response: TestResponse) => {
|
||||
const result = response.body;
|
||||
expect(result).to.eql({
|
||||
[destinationSpaceId]: {
|
||||
success: true,
|
||||
successCount: 1,
|
||||
},
|
||||
});
|
||||
const [dashboard, visualization] = await getObjectsAtSpace(destinationSpaceId);
|
||||
expect(dashboard.attributes.title).to.eql(
|
||||
`This is the ${sourceSpaceId} test space CTS dashboard`
|
||||
);
|
||||
if (destinationSpaceId === NON_EXISTENT_SPACE_ID) {
|
||||
expect((visualization as any).statusCode).to.eql(404);
|
||||
} else {
|
||||
expect(visualization.attributes.title).to.eql(`CTS vis 3 from ${destinationSpaceId} space`);
|
||||
}
|
||||
};
|
||||
|
||||
const createExpectNonOverriddenResponseWithReferences = (sourceSpaceId: string) => async (
|
||||
response: TestResponse
|
||||
) => {
|
||||
const destination = getDestinationSpace(sourceSpaceId);
|
||||
|
||||
const result = response.body;
|
||||
expect(result).to.eql({
|
||||
[destination]: {
|
||||
success: false,
|
||||
successCount: 0,
|
||||
errors: [
|
||||
{
|
||||
error: {
|
||||
type: 'conflict',
|
||||
},
|
||||
id: 'cts_vis_3',
|
||||
title: `CTS vis 3 from ${sourceSpaceId} space`,
|
||||
type: 'visualization',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const [dashboard, visualization] = await getObjectsAtSpace(destination);
|
||||
expect(dashboard.attributes.title).to.eql(
|
||||
`This is the ${destination} test space CTS dashboard`
|
||||
);
|
||||
expect(visualization.attributes.title).to.eql(`CTS vis 3 from ${destination} space`);
|
||||
};
|
||||
|
||||
const createExpectNonOverriddenResponseWithoutReferences = (sourceSpaceId: string) => async (
|
||||
response: TestResponse
|
||||
) => {
|
||||
const destination = getDestinationSpace(sourceSpaceId);
|
||||
|
||||
const result = response.body;
|
||||
expect(result).to.eql({
|
||||
[destination]: {
|
||||
success: false,
|
||||
successCount: 0,
|
||||
errors: [
|
||||
{
|
||||
error: {
|
||||
type: 'conflict',
|
||||
},
|
||||
id: 'cts_dashboard',
|
||||
title: `This is the ${sourceSpaceId} test space CTS dashboard`,
|
||||
type: 'dashboard',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const [dashboard, visualization] = await getObjectsAtSpace(destination);
|
||||
expect(dashboard.attributes.title).to.eql(
|
||||
`This is the ${destination} test space CTS dashboard`
|
||||
);
|
||||
expect(visualization.attributes.title).to.eql(`CTS vis 3 from ${destination} space`);
|
||||
};
|
||||
|
||||
const expectNotFoundResponse = async (resp: TestResponse) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Not Found',
|
||||
});
|
||||
};
|
||||
|
||||
const createExpectUnauthorizedAtSpaceWithReferencesResult = (
|
||||
spaceId: string = DEFAULT_SPACE_ID
|
||||
) => async (resp: TestResponse) => {
|
||||
const destination = getDestinationSpace(spaceId);
|
||||
|
||||
const result = resp.body as CopyResponse;
|
||||
expect(result).to.eql({
|
||||
[destination]: {
|
||||
success: false,
|
||||
successCount: 0,
|
||||
errors: [
|
||||
{
|
||||
statusCode: 403,
|
||||
error: 'Forbidden',
|
||||
message: 'Unable to bulk_get index-pattern',
|
||||
},
|
||||
],
|
||||
},
|
||||
} as CopyResponse);
|
||||
|
||||
// Query ES to ensure that nothing was copied
|
||||
const [dashboard, visualization] = await getObjectsAtSpace(destination);
|
||||
expect(dashboard.attributes.title).to.eql(
|
||||
`This is the ${destination} test space CTS dashboard`
|
||||
);
|
||||
expect(visualization.attributes.title).to.eql(`CTS vis 3 from ${destination} space`);
|
||||
};
|
||||
|
||||
const createExpectReadonlyAtSpaceWithReferencesResult = (
|
||||
spaceId: string = DEFAULT_SPACE_ID
|
||||
) => async (resp: TestResponse) => {
|
||||
const destination = getDestinationSpace(spaceId);
|
||||
|
||||
const result = resp.body as CopyResponse;
|
||||
expect(result).to.eql({
|
||||
[destination]: {
|
||||
success: false,
|
||||
successCount: 0,
|
||||
errors: [
|
||||
{
|
||||
statusCode: 403,
|
||||
error: 'Forbidden',
|
||||
message: 'Unable to bulk_create visualization',
|
||||
},
|
||||
],
|
||||
},
|
||||
} as CopyResponse);
|
||||
|
||||
// Query ES to ensure that nothing was copied
|
||||
const [dashboard, visualization] = await getObjectsAtSpace(destination);
|
||||
expect(dashboard.attributes.title).to.eql(
|
||||
`This is the ${destination} test space CTS dashboard`
|
||||
);
|
||||
expect(visualization.attributes.title).to.eql(`CTS vis 3 from ${destination} space`);
|
||||
};
|
||||
|
||||
const createExpectUnauthorizedAtSpaceWithoutReferencesResult = (
|
||||
sourceSpaceId: string = DEFAULT_SPACE_ID,
|
||||
destinationSpaceId: string = getDestinationSpace(sourceSpaceId)
|
||||
) => async (resp: TestResponse) => {
|
||||
const result = resp.body as CopyResponse;
|
||||
expect(result).to.eql({
|
||||
[destinationSpaceId]: {
|
||||
success: false,
|
||||
successCount: 0,
|
||||
errors: [
|
||||
{
|
||||
statusCode: 403,
|
||||
error: 'Forbidden',
|
||||
message: 'Unable to bulk_create dashboard',
|
||||
},
|
||||
],
|
||||
},
|
||||
} as CopyResponse);
|
||||
|
||||
// Query ES to ensure that nothing was copied
|
||||
const [dashboard, visualization] = await getObjectsAtSpace(destinationSpaceId);
|
||||
|
||||
if (destinationSpaceId === NON_EXISTENT_SPACE_ID) {
|
||||
expect((dashboard as any).statusCode).to.eql(404);
|
||||
expect((visualization as any).statusCode).to.eql(404);
|
||||
} else {
|
||||
expect(dashboard.attributes.title).to.eql(
|
||||
`This is the ${destinationSpaceId} test space CTS dashboard`
|
||||
);
|
||||
expect(visualization.attributes.title).to.eql(`CTS vis 3 from ${destinationSpaceId} space`);
|
||||
}
|
||||
};
|
||||
|
||||
const makeResolveCopyToSpaceConflictsTest = (describeFn: DescribeFn) => (
|
||||
description: string,
|
||||
{ user = {}, spaceId = DEFAULT_SPACE_ID, tests }: ResolveCopyToSpaceTestDefinition
|
||||
) => {
|
||||
describeFn(description, () => {
|
||||
before(() => {
|
||||
// test data only allows for the following spaces as the copy origin
|
||||
expect(['default', 'space_1']).to.contain(spaceId);
|
||||
});
|
||||
|
||||
beforeEach(() => esArchiver.load('saved_objects/spaces'));
|
||||
afterEach(() => esArchiver.unload('saved_objects/spaces'));
|
||||
|
||||
it(`should return ${tests.withReferencesNotOverwriting.statusCode} when not overwriting, with references`, async () => {
|
||||
const destination = getDestinationSpace(spaceId);
|
||||
|
||||
return supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`)
|
||||
.auth(user.username, user.password)
|
||||
.send({
|
||||
objects: [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'cts_dashboard',
|
||||
},
|
||||
],
|
||||
includeReferences: true,
|
||||
retries: {
|
||||
[destination]: [
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'cts_vis_3',
|
||||
overwrite: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
.expect(tests.withReferencesNotOverwriting.statusCode)
|
||||
.then(tests.withReferencesNotOverwriting.response);
|
||||
});
|
||||
|
||||
it(`should return ${tests.withReferencesOverwriting.statusCode} when overwriting, with references`, async () => {
|
||||
const destination = getDestinationSpace(spaceId);
|
||||
|
||||
return supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`)
|
||||
.auth(user.username, user.password)
|
||||
.send({
|
||||
objects: [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'cts_dashboard',
|
||||
},
|
||||
],
|
||||
includeReferences: true,
|
||||
retries: {
|
||||
[destination]: [
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'cts_vis_3',
|
||||
overwrite: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
.expect(tests.withReferencesOverwriting.statusCode)
|
||||
.then(tests.withReferencesOverwriting.response);
|
||||
});
|
||||
|
||||
it(`should return ${tests.withoutReferencesOverwriting.statusCode} when overwriting, without references`, async () => {
|
||||
const destination = getDestinationSpace(spaceId);
|
||||
|
||||
return supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`)
|
||||
.auth(user.username, user.password)
|
||||
.send({
|
||||
objects: [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'cts_dashboard',
|
||||
},
|
||||
],
|
||||
includeReferences: false,
|
||||
retries: {
|
||||
[destination]: [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'cts_dashboard',
|
||||
overwrite: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
.expect(tests.withoutReferencesOverwriting.statusCode)
|
||||
.then(tests.withoutReferencesOverwriting.response);
|
||||
});
|
||||
|
||||
it(`should return ${tests.withoutReferencesNotOverwriting.statusCode} when not overwriting, without references`, async () => {
|
||||
const destination = getDestinationSpace(spaceId);
|
||||
|
||||
return supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`)
|
||||
.auth(user.username, user.password)
|
||||
.send({
|
||||
objects: [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'cts_dashboard',
|
||||
},
|
||||
],
|
||||
includeReferences: false,
|
||||
retries: {
|
||||
[destination]: [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'cts_dashboard',
|
||||
overwrite: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
.expect(tests.withoutReferencesNotOverwriting.statusCode)
|
||||
.then(tests.withoutReferencesNotOverwriting.response);
|
||||
});
|
||||
|
||||
it(`should return ${tests.nonExistentSpace.statusCode} when resolving within a non-existent space`, async () => {
|
||||
const destination = NON_EXISTENT_SPACE_ID;
|
||||
|
||||
return supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`)
|
||||
.auth(user.username, user.password)
|
||||
.send({
|
||||
objects: [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'cts_dashboard',
|
||||
},
|
||||
],
|
||||
includeReferences: false,
|
||||
retries: {
|
||||
[destination]: [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'cts_dashboard',
|
||||
overwrite: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
.expect(tests.nonExistentSpace.statusCode)
|
||||
.then(tests.nonExistentSpace.response);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const resolveCopyToSpaceConflictsTest = makeResolveCopyToSpaceConflictsTest(describe);
|
||||
// @ts-ignore
|
||||
resolveCopyToSpaceConflictsTest.only = makeResolveCopyToSpaceConflictsTest(describe.only);
|
||||
|
||||
return {
|
||||
resolveCopyToSpaceConflictsTest,
|
||||
expectNotFoundResponse,
|
||||
createExpectOverriddenResponseWithReferences,
|
||||
createExpectOverriddenResponseWithoutReferences,
|
||||
createExpectNonOverriddenResponseWithReferences,
|
||||
createExpectNonOverriddenResponseWithoutReferences,
|
||||
createExpectUnauthorizedAtSpaceWithReferencesResult,
|
||||
createExpectReadonlyAtSpaceWithReferencesResult,
|
||||
createExpectUnauthorizedAtSpaceWithoutReferencesResult,
|
||||
originSpaces: ['default', 'space_1'],
|
||||
NON_EXISTENT_SPACE_ID,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,379 @@
|
|||
/*
|
||||
* 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 { AUTHENTICATION } from '../../common/lib/authentication';
|
||||
import { SPACES } from '../../common/lib/spaces';
|
||||
import { TestInvoker } from '../../common/lib/types';
|
||||
import { copyToSpaceTestSuiteFactory } from '../../common/suites/copy_to_space';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function copyToSpaceSpacesAndSecuritySuite({ getService }: TestInvoker) {
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const es = getService('es');
|
||||
|
||||
const {
|
||||
copyToSpaceTest,
|
||||
expectNoConflictsWithoutReferencesResult,
|
||||
expectNoConflictsWithReferencesResult,
|
||||
expectNoConflictsForNonExistentSpaceResult,
|
||||
createExpectWithConflictsOverwritingResult,
|
||||
createExpectWithConflictsWithoutOverwritingResult,
|
||||
createExpectUnauthorizedAtSpaceWithReferencesResult,
|
||||
createExpectUnauthorizedAtSpaceWithoutReferencesResult,
|
||||
expectNotFoundResponse,
|
||||
} = copyToSpaceTestSuiteFactory(es, esArchiver, supertestWithoutAuth);
|
||||
|
||||
describe('copy to spaces', () => {
|
||||
[
|
||||
{
|
||||
spaceId: SPACES.DEFAULT.spaceId,
|
||||
users: {
|
||||
noAccess: AUTHENTICATION.NOT_A_KIBANA_USER,
|
||||
superuser: AUTHENTICATION.SUPERUSER,
|
||||
allGlobally: AUTHENTICATION.KIBANA_RBAC_USER,
|
||||
readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER,
|
||||
allAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER,
|
||||
legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER,
|
||||
dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER,
|
||||
dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER,
|
||||
},
|
||||
},
|
||||
{
|
||||
spaceId: SPACES.SPACE_1.spaceId,
|
||||
users: {
|
||||
noAccess: AUTHENTICATION.NOT_A_KIBANA_USER,
|
||||
superuser: AUTHENTICATION.SUPERUSER,
|
||||
allGlobally: AUTHENTICATION.KIBANA_RBAC_USER,
|
||||
readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER,
|
||||
allAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER,
|
||||
legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER,
|
||||
dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER,
|
||||
dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER,
|
||||
},
|
||||
},
|
||||
].forEach(scenario => {
|
||||
copyToSpaceTest(`user with no access from the ${scenario.spaceId} space`, {
|
||||
spaceId: scenario.spaceId,
|
||||
user: scenario.users.noAccess,
|
||||
tests: {
|
||||
noConflictsWithoutReferences: {
|
||||
statusCode: 404,
|
||||
response: expectNotFoundResponse,
|
||||
},
|
||||
noConflictsWithReferences: {
|
||||
statusCode: 404,
|
||||
response: expectNotFoundResponse,
|
||||
},
|
||||
withConflictsOverwriting: {
|
||||
statusCode: 404,
|
||||
response: expectNotFoundResponse,
|
||||
},
|
||||
withConflictsWithoutOverwriting: {
|
||||
statusCode: 404,
|
||||
response: expectNotFoundResponse,
|
||||
},
|
||||
multipleSpaces: {
|
||||
statusCode: 404,
|
||||
withConflictsResponse: expectNotFoundResponse,
|
||||
noConflictsResponse: expectNotFoundResponse,
|
||||
},
|
||||
nonExistentSpace: {
|
||||
statusCode: 404,
|
||||
response: expectNotFoundResponse,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
copyToSpaceTest(`superuser from the ${scenario.spaceId} space`, {
|
||||
spaceId: scenario.spaceId,
|
||||
user: scenario.users.superuser,
|
||||
tests: {
|
||||
noConflictsWithoutReferences: {
|
||||
statusCode: 200,
|
||||
response: expectNoConflictsWithoutReferencesResult,
|
||||
},
|
||||
noConflictsWithReferences: {
|
||||
statusCode: 200,
|
||||
response: expectNoConflictsWithReferencesResult,
|
||||
},
|
||||
withConflictsOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectWithConflictsOverwritingResult(scenario.spaceId),
|
||||
},
|
||||
withConflictsWithoutOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectWithConflictsWithoutOverwritingResult(scenario.spaceId),
|
||||
},
|
||||
multipleSpaces: {
|
||||
statusCode: 200,
|
||||
withConflictsResponse: createExpectWithConflictsOverwritingResult(scenario.spaceId),
|
||||
noConflictsResponse: expectNoConflictsWithReferencesResult,
|
||||
},
|
||||
nonExistentSpace: {
|
||||
statusCode: 200,
|
||||
response: expectNoConflictsForNonExistentSpaceResult,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
copyToSpaceTest(`rbac user with all globally from the ${scenario.spaceId} space`, {
|
||||
spaceId: scenario.spaceId,
|
||||
user: scenario.users.allGlobally,
|
||||
tests: {
|
||||
noConflictsWithoutReferences: {
|
||||
statusCode: 200,
|
||||
response: expectNoConflictsWithoutReferencesResult,
|
||||
},
|
||||
noConflictsWithReferences: {
|
||||
statusCode: 200,
|
||||
response: expectNoConflictsWithReferencesResult,
|
||||
},
|
||||
withConflictsOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectWithConflictsOverwritingResult(scenario.spaceId),
|
||||
},
|
||||
withConflictsWithoutOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectWithConflictsWithoutOverwritingResult(scenario.spaceId),
|
||||
},
|
||||
multipleSpaces: {
|
||||
statusCode: 200,
|
||||
withConflictsResponse: createExpectWithConflictsOverwritingResult(scenario.spaceId),
|
||||
noConflictsResponse: expectNoConflictsWithReferencesResult,
|
||||
},
|
||||
nonExistentSpace: {
|
||||
statusCode: 200,
|
||||
response: expectNoConflictsForNonExistentSpaceResult,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
copyToSpaceTest(`dual-privileges user from the ${scenario.spaceId} space`, {
|
||||
spaceId: scenario.spaceId,
|
||||
user: scenario.users.dualAll,
|
||||
tests: {
|
||||
noConflictsWithoutReferences: {
|
||||
statusCode: 200,
|
||||
response: expectNoConflictsWithoutReferencesResult,
|
||||
},
|
||||
noConflictsWithReferences: {
|
||||
statusCode: 200,
|
||||
response: expectNoConflictsWithReferencesResult,
|
||||
},
|
||||
withConflictsOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectWithConflictsOverwritingResult(scenario.spaceId),
|
||||
},
|
||||
withConflictsWithoutOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectWithConflictsWithoutOverwritingResult(scenario.spaceId),
|
||||
},
|
||||
multipleSpaces: {
|
||||
statusCode: 200,
|
||||
withConflictsResponse: createExpectWithConflictsOverwritingResult(scenario.spaceId),
|
||||
noConflictsResponse: expectNoConflictsWithReferencesResult,
|
||||
},
|
||||
nonExistentSpace: {
|
||||
statusCode: 200,
|
||||
response: expectNoConflictsForNonExistentSpaceResult,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
copyToSpaceTest(`legacy user from the ${scenario.spaceId} space`, {
|
||||
spaceId: scenario.spaceId,
|
||||
user: scenario.users.legacyAll,
|
||||
tests: {
|
||||
noConflictsWithoutReferences: {
|
||||
statusCode: 404,
|
||||
response: expectNotFoundResponse,
|
||||
},
|
||||
noConflictsWithReferences: {
|
||||
statusCode: 404,
|
||||
response: expectNotFoundResponse,
|
||||
},
|
||||
withConflictsOverwriting: {
|
||||
statusCode: 404,
|
||||
response: expectNotFoundResponse,
|
||||
},
|
||||
withConflictsWithoutOverwriting: {
|
||||
statusCode: 404,
|
||||
response: expectNotFoundResponse,
|
||||
},
|
||||
multipleSpaces: {
|
||||
statusCode: 404,
|
||||
withConflictsResponse: expectNotFoundResponse,
|
||||
noConflictsResponse: expectNotFoundResponse,
|
||||
},
|
||||
nonExistentSpace: {
|
||||
statusCode: 404,
|
||||
response: expectNotFoundResponse,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
copyToSpaceTest(`rbac user with read globally from the ${scenario.spaceId} space`, {
|
||||
spaceId: scenario.spaceId,
|
||||
user: scenario.users.readGlobally,
|
||||
tests: {
|
||||
noConflictsWithoutReferences: {
|
||||
statusCode: 200,
|
||||
response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(
|
||||
scenario.spaceId,
|
||||
'without-conflicts'
|
||||
),
|
||||
},
|
||||
noConflictsWithReferences: {
|
||||
statusCode: 200,
|
||||
response: createExpectUnauthorizedAtSpaceWithReferencesResult(
|
||||
scenario.spaceId,
|
||||
'without-conflicts'
|
||||
),
|
||||
},
|
||||
withConflictsOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectUnauthorizedAtSpaceWithReferencesResult(
|
||||
scenario.spaceId,
|
||||
'with-conflicts'
|
||||
),
|
||||
},
|
||||
withConflictsWithoutOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectUnauthorizedAtSpaceWithReferencesResult(
|
||||
scenario.spaceId,
|
||||
'with-conflicts'
|
||||
),
|
||||
},
|
||||
multipleSpaces: {
|
||||
statusCode: 200,
|
||||
withConflictsResponse: createExpectUnauthorizedAtSpaceWithReferencesResult(
|
||||
scenario.spaceId,
|
||||
'with-conflicts'
|
||||
),
|
||||
noConflictsResponse: createExpectUnauthorizedAtSpaceWithReferencesResult(
|
||||
scenario.spaceId,
|
||||
'without-conflicts'
|
||||
),
|
||||
},
|
||||
nonExistentSpace: {
|
||||
statusCode: 200,
|
||||
response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(
|
||||
scenario.spaceId,
|
||||
'non-existent'
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
copyToSpaceTest(`dual-privileges readonly user from the ${scenario.spaceId} space`, {
|
||||
spaceId: scenario.spaceId,
|
||||
user: scenario.users.dualRead,
|
||||
tests: {
|
||||
noConflictsWithoutReferences: {
|
||||
statusCode: 200,
|
||||
response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(
|
||||
scenario.spaceId,
|
||||
'without-conflicts'
|
||||
),
|
||||
},
|
||||
noConflictsWithReferences: {
|
||||
statusCode: 200,
|
||||
response: createExpectUnauthorizedAtSpaceWithReferencesResult(
|
||||
scenario.spaceId,
|
||||
'without-conflicts'
|
||||
),
|
||||
},
|
||||
withConflictsOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectUnauthorizedAtSpaceWithReferencesResult(
|
||||
scenario.spaceId,
|
||||
'with-conflicts'
|
||||
),
|
||||
},
|
||||
withConflictsWithoutOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectUnauthorizedAtSpaceWithReferencesResult(
|
||||
scenario.spaceId,
|
||||
'with-conflicts'
|
||||
),
|
||||
},
|
||||
multipleSpaces: {
|
||||
statusCode: 200,
|
||||
withConflictsResponse: createExpectUnauthorizedAtSpaceWithReferencesResult(
|
||||
scenario.spaceId,
|
||||
'with-conflicts'
|
||||
),
|
||||
noConflictsResponse: createExpectUnauthorizedAtSpaceWithReferencesResult(
|
||||
scenario.spaceId,
|
||||
'without-conflicts'
|
||||
),
|
||||
},
|
||||
nonExistentSpace: {
|
||||
statusCode: 200,
|
||||
response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(
|
||||
scenario.spaceId,
|
||||
'non-existent'
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
copyToSpaceTest(`rbac user with all at space from the ${scenario.spaceId} space`, {
|
||||
spaceId: scenario.spaceId,
|
||||
user: scenario.users.allAtSpace,
|
||||
tests: {
|
||||
noConflictsWithoutReferences: {
|
||||
statusCode: 200,
|
||||
response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(
|
||||
scenario.spaceId,
|
||||
'without-conflicts'
|
||||
),
|
||||
},
|
||||
noConflictsWithReferences: {
|
||||
statusCode: 200,
|
||||
response: createExpectUnauthorizedAtSpaceWithReferencesResult(
|
||||
scenario.spaceId,
|
||||
'without-conflicts'
|
||||
),
|
||||
},
|
||||
withConflictsOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectUnauthorizedAtSpaceWithReferencesResult(
|
||||
scenario.spaceId,
|
||||
'with-conflicts'
|
||||
),
|
||||
},
|
||||
withConflictsWithoutOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectUnauthorizedAtSpaceWithReferencesResult(
|
||||
scenario.spaceId,
|
||||
'with-conflicts'
|
||||
),
|
||||
},
|
||||
multipleSpaces: {
|
||||
statusCode: 200,
|
||||
withConflictsResponse: createExpectUnauthorizedAtSpaceWithReferencesResult(
|
||||
scenario.spaceId,
|
||||
'with-conflicts'
|
||||
),
|
||||
noConflictsResponse: createExpectUnauthorizedAtSpaceWithReferencesResult(
|
||||
scenario.spaceId,
|
||||
'without-conflicts'
|
||||
),
|
||||
},
|
||||
nonExistentSpace: {
|
||||
statusCode: 200,
|
||||
response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(
|
||||
scenario.spaceId,
|
||||
'non-existent'
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -32,6 +32,12 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
readAtSpace_1: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER,
|
||||
allAtDefaultSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER,
|
||||
readAtDefaultSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER,
|
||||
readSavedObjectsAtDefaultSpace:
|
||||
AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_SAVED_OBJECTS_READ_USER,
|
||||
allSavedObjectsAtDefaultSpace:
|
||||
AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_SAVED_OBJECTS_ALL_USER,
|
||||
readSavedObjectsAtSpace_1: AUTHENTICATION.KIBANA_RBAC_SPACE_1_SAVED_OBJECTS_READ_USER,
|
||||
allSavedObjectsAtSpace_1: AUTHENTICATION.KIBANA_RBAC_SPACE_1_SAVED_OBJECTS_ALL_USER,
|
||||
legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER,
|
||||
dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER,
|
||||
dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER,
|
||||
|
@ -52,6 +58,12 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
readAtSpace_1: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER,
|
||||
allAtDefaultSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER,
|
||||
readAtDefaultSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER,
|
||||
readSavedObjectsAtDefaultSpace:
|
||||
AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_SAVED_OBJECTS_READ_USER,
|
||||
allSavedObjectsAtDefaultSpace:
|
||||
AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_SAVED_OBJECTS_ALL_USER,
|
||||
readSavedObjectsAtSpace_1: AUTHENTICATION.KIBANA_RBAC_SPACE_1_SAVED_OBJECTS_READ_USER,
|
||||
allSavedObjectsAtSpace_1: AUTHENTICATION.KIBANA_RBAC_SPACE_1_SAVED_OBJECTS_ALL_USER,
|
||||
legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER,
|
||||
dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER,
|
||||
dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER,
|
||||
|
@ -70,6 +82,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
copySavedObjectsPurpose: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -81,6 +97,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 200,
|
||||
response: createExpectResults('default', 'space_1', 'space_2'),
|
||||
},
|
||||
copySavedObjectsPurpose: {
|
||||
statusCode: 200,
|
||||
response: createExpectResults('default', 'space_1', 'space_2'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -92,6 +112,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 200,
|
||||
response: createExpectResults('default', 'space_1', 'space_2'),
|
||||
},
|
||||
copySavedObjectsPurpose: {
|
||||
statusCode: 200,
|
||||
response: createExpectResults('default', 'space_1', 'space_2'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -103,6 +127,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 200,
|
||||
response: createExpectResults('default', 'space_1', 'space_2'),
|
||||
},
|
||||
copySavedObjectsPurpose: {
|
||||
statusCode: 200,
|
||||
response: createExpectResults('default', 'space_1', 'space_2'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -114,6 +142,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
copySavedObjectsPurpose: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -125,6 +157,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 200,
|
||||
response: createExpectResults('default', 'space_1', 'space_2'),
|
||||
},
|
||||
copySavedObjectsPurpose: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -136,6 +172,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 200,
|
||||
response: createExpectResults('default', 'space_1', 'space_2'),
|
||||
},
|
||||
copySavedObjectsPurpose: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -147,6 +187,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 200,
|
||||
response: createExpectResults('space_1'),
|
||||
},
|
||||
copySavedObjectsPurpose: {
|
||||
statusCode: 200,
|
||||
response: createExpectResults('space_1'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -158,6 +202,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 200,
|
||||
response: createExpectResults('space_1'),
|
||||
},
|
||||
copySavedObjectsPurpose: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -171,6 +219,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 200,
|
||||
response: createExpectResults('default'),
|
||||
},
|
||||
copySavedObjectsPurpose: {
|
||||
statusCode: 200,
|
||||
response: createExpectResults('default'),
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
@ -185,6 +237,82 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 200,
|
||||
response: createExpectResults('default'),
|
||||
},
|
||||
copySavedObjectsPurpose: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
getAllTest(
|
||||
`rbac user with saved objects management all at default space can access default from ${scenario.spaceId}`,
|
||||
{
|
||||
spaceId: scenario.spaceId,
|
||||
user: scenario.users.allSavedObjectsAtDefaultSpace,
|
||||
tests: {
|
||||
exists: {
|
||||
statusCode: 200,
|
||||
response: createExpectResults('default'),
|
||||
},
|
||||
copySavedObjectsPurpose: {
|
||||
statusCode: 200,
|
||||
response: createExpectResults('default'),
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
getAllTest(
|
||||
`rbac user with saved objects management read at default space can access default from ${scenario.spaceId}`,
|
||||
{
|
||||
spaceId: scenario.spaceId,
|
||||
user: scenario.users.readSavedObjectsAtDefaultSpace,
|
||||
tests: {
|
||||
exists: {
|
||||
statusCode: 200,
|
||||
response: createExpectResults('default'),
|
||||
},
|
||||
copySavedObjectsPurpose: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
getAllTest(
|
||||
`rbac user with saved objects management all at space_1 space can access space_1 from ${scenario.spaceId}`,
|
||||
{
|
||||
spaceId: scenario.spaceId,
|
||||
user: scenario.users.allSavedObjectsAtSpace_1,
|
||||
tests: {
|
||||
exists: {
|
||||
statusCode: 200,
|
||||
response: createExpectResults('space_1'),
|
||||
},
|
||||
copySavedObjectsPurpose: {
|
||||
statusCode: 200,
|
||||
response: createExpectResults('space_1'),
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
getAllTest(
|
||||
`rbac user with saved objects management read at space_1 space can access space_1 from ${scenario.spaceId}`,
|
||||
{
|
||||
spaceId: scenario.spaceId,
|
||||
user: scenario.users.readSavedObjectsAtSpace_1,
|
||||
tests: {
|
||||
exists: {
|
||||
statusCode: 200,
|
||||
response: createExpectResults('space_1'),
|
||||
},
|
||||
copySavedObjectsPurpose: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
@ -197,6 +325,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
copySavedObjectsPurpose: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -208,6 +340,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
copySavedObjectsPurpose: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -219,6 +355,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
copySavedObjectsPurpose: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -230,6 +370,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
copySavedObjectsPurpose: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,6 +19,8 @@ export default function({ loadTestFile, getService }: TestInvoker) {
|
|||
await createUsersAndRoles(es, supertest);
|
||||
});
|
||||
|
||||
loadTestFile(require.resolve('./copy_to_space'));
|
||||
loadTestFile(require.resolve('./resolve_copy_to_space_conflicts'));
|
||||
loadTestFile(require.resolve('./create'));
|
||||
loadTestFile(require.resolve('./delete'));
|
||||
loadTestFile(require.resolve('./get_all'));
|
||||
|
|
|
@ -0,0 +1,307 @@
|
|||
/*
|
||||
* 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 { AUTHENTICATION } from '../../common/lib/authentication';
|
||||
import { SPACES } from '../../common/lib/spaces';
|
||||
import { TestInvoker } from '../../common/lib/types';
|
||||
import { resolveCopyToSpaceConflictsSuite } from '../../common/suites/resolve_copy_to_space_conflicts';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function resolveCopyToSpaceConflictsTestSuite({ getService }: TestInvoker) {
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const supertestWithAuth = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
const {
|
||||
resolveCopyToSpaceConflictsTest,
|
||||
createExpectNonOverriddenResponseWithReferences,
|
||||
createExpectNonOverriddenResponseWithoutReferences,
|
||||
createExpectOverriddenResponseWithReferences,
|
||||
createExpectOverriddenResponseWithoutReferences,
|
||||
expectNotFoundResponse,
|
||||
createExpectUnauthorizedAtSpaceWithReferencesResult,
|
||||
createExpectReadonlyAtSpaceWithReferencesResult,
|
||||
createExpectUnauthorizedAtSpaceWithoutReferencesResult,
|
||||
NON_EXISTENT_SPACE_ID,
|
||||
} = resolveCopyToSpaceConflictsSuite(esArchiver, supertestWithAuth, supertestWithoutAuth);
|
||||
|
||||
describe('resolve copy to spaces conflicts', () => {
|
||||
[
|
||||
{
|
||||
spaceId: SPACES.DEFAULT.spaceId,
|
||||
users: {
|
||||
noAccess: AUTHENTICATION.NOT_A_KIBANA_USER,
|
||||
superuser: AUTHENTICATION.SUPERUSER,
|
||||
allGlobally: AUTHENTICATION.KIBANA_RBAC_USER,
|
||||
readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER,
|
||||
allAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER,
|
||||
legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER,
|
||||
dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER,
|
||||
dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER,
|
||||
},
|
||||
},
|
||||
{
|
||||
spaceId: SPACES.SPACE_1.spaceId,
|
||||
users: {
|
||||
noAccess: AUTHENTICATION.NOT_A_KIBANA_USER,
|
||||
superuser: AUTHENTICATION.SUPERUSER,
|
||||
allGlobally: AUTHENTICATION.KIBANA_RBAC_USER,
|
||||
readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER,
|
||||
allAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER,
|
||||
legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER,
|
||||
dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER,
|
||||
dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER,
|
||||
},
|
||||
},
|
||||
].forEach(scenario => {
|
||||
resolveCopyToSpaceConflictsTest(`user with no access from the ${scenario.spaceId} space`, {
|
||||
spaceId: scenario.spaceId,
|
||||
user: scenario.users.noAccess,
|
||||
tests: {
|
||||
withReferencesNotOverwriting: {
|
||||
statusCode: 404,
|
||||
response: expectNotFoundResponse,
|
||||
},
|
||||
withReferencesOverwriting: {
|
||||
statusCode: 404,
|
||||
response: expectNotFoundResponse,
|
||||
},
|
||||
withoutReferencesOverwriting: {
|
||||
statusCode: 404,
|
||||
response: expectNotFoundResponse,
|
||||
},
|
||||
withoutReferencesNotOverwriting: {
|
||||
statusCode: 404,
|
||||
response: expectNotFoundResponse,
|
||||
},
|
||||
nonExistentSpace: {
|
||||
statusCode: 404,
|
||||
response: expectNotFoundResponse,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
resolveCopyToSpaceConflictsTest(`superuser from the ${scenario.spaceId} space`, {
|
||||
spaceId: scenario.spaceId,
|
||||
user: scenario.users.superuser,
|
||||
tests: {
|
||||
withReferencesNotOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectNonOverriddenResponseWithReferences(scenario.spaceId),
|
||||
},
|
||||
withReferencesOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectOverriddenResponseWithReferences(scenario.spaceId),
|
||||
},
|
||||
withoutReferencesOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectOverriddenResponseWithoutReferences(scenario.spaceId),
|
||||
},
|
||||
withoutReferencesNotOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectNonOverriddenResponseWithoutReferences(scenario.spaceId),
|
||||
},
|
||||
nonExistentSpace: {
|
||||
statusCode: 200,
|
||||
response: createExpectOverriddenResponseWithoutReferences(
|
||||
scenario.spaceId,
|
||||
NON_EXISTENT_SPACE_ID
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
resolveCopyToSpaceConflictsTest(
|
||||
`rbac user with all globally from the ${scenario.spaceId} space`,
|
||||
{
|
||||
spaceId: scenario.spaceId,
|
||||
user: scenario.users.allGlobally,
|
||||
tests: {
|
||||
withReferencesNotOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectNonOverriddenResponseWithReferences(scenario.spaceId),
|
||||
},
|
||||
withReferencesOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectOverriddenResponseWithReferences(scenario.spaceId),
|
||||
},
|
||||
withoutReferencesOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectOverriddenResponseWithoutReferences(scenario.spaceId),
|
||||
},
|
||||
withoutReferencesNotOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectNonOverriddenResponseWithoutReferences(scenario.spaceId),
|
||||
},
|
||||
nonExistentSpace: {
|
||||
statusCode: 200,
|
||||
response: createExpectOverriddenResponseWithoutReferences(
|
||||
scenario.spaceId,
|
||||
NON_EXISTENT_SPACE_ID
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
resolveCopyToSpaceConflictsTest(`dual-privileges user from the ${scenario.spaceId} space`, {
|
||||
spaceId: scenario.spaceId,
|
||||
user: scenario.users.dualAll,
|
||||
tests: {
|
||||
withReferencesNotOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectNonOverriddenResponseWithReferences(scenario.spaceId),
|
||||
},
|
||||
withReferencesOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectOverriddenResponseWithReferences(scenario.spaceId),
|
||||
},
|
||||
withoutReferencesOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectOverriddenResponseWithoutReferences(scenario.spaceId),
|
||||
},
|
||||
withoutReferencesNotOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectNonOverriddenResponseWithoutReferences(scenario.spaceId),
|
||||
},
|
||||
nonExistentSpace: {
|
||||
statusCode: 200,
|
||||
response: createExpectOverriddenResponseWithoutReferences(
|
||||
scenario.spaceId,
|
||||
NON_EXISTENT_SPACE_ID
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
resolveCopyToSpaceConflictsTest(`legacy user from the ${scenario.spaceId} space`, {
|
||||
spaceId: scenario.spaceId,
|
||||
user: scenario.users.legacyAll,
|
||||
tests: {
|
||||
withReferencesNotOverwriting: {
|
||||
statusCode: 404,
|
||||
response: expectNotFoundResponse,
|
||||
},
|
||||
withReferencesOverwriting: {
|
||||
statusCode: 404,
|
||||
response: expectNotFoundResponse,
|
||||
},
|
||||
withoutReferencesOverwriting: {
|
||||
statusCode: 404,
|
||||
response: expectNotFoundResponse,
|
||||
},
|
||||
withoutReferencesNotOverwriting: {
|
||||
statusCode: 404,
|
||||
response: expectNotFoundResponse,
|
||||
},
|
||||
nonExistentSpace: {
|
||||
statusCode: 404,
|
||||
response: expectNotFoundResponse,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
resolveCopyToSpaceConflictsTest(
|
||||
`rbac user with read globally from the ${scenario.spaceId} space`,
|
||||
{
|
||||
spaceId: scenario.spaceId,
|
||||
user: scenario.users.readGlobally,
|
||||
tests: {
|
||||
withReferencesNotOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectReadonlyAtSpaceWithReferencesResult(scenario.spaceId),
|
||||
},
|
||||
withReferencesOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectReadonlyAtSpaceWithReferencesResult(scenario.spaceId),
|
||||
},
|
||||
withoutReferencesOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(scenario.spaceId),
|
||||
},
|
||||
withoutReferencesNotOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(scenario.spaceId),
|
||||
},
|
||||
nonExistentSpace: {
|
||||
statusCode: 200,
|
||||
response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(
|
||||
scenario.spaceId,
|
||||
NON_EXISTENT_SPACE_ID
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
resolveCopyToSpaceConflictsTest(
|
||||
`dual-privileges readonly user from the ${scenario.spaceId} space`,
|
||||
{
|
||||
spaceId: scenario.spaceId,
|
||||
user: scenario.users.dualRead,
|
||||
tests: {
|
||||
withReferencesNotOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectReadonlyAtSpaceWithReferencesResult(scenario.spaceId),
|
||||
},
|
||||
withReferencesOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectReadonlyAtSpaceWithReferencesResult(scenario.spaceId),
|
||||
},
|
||||
withoutReferencesOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(scenario.spaceId),
|
||||
},
|
||||
withoutReferencesNotOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(scenario.spaceId),
|
||||
},
|
||||
nonExistentSpace: {
|
||||
statusCode: 200,
|
||||
response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(
|
||||
scenario.spaceId,
|
||||
NON_EXISTENT_SPACE_ID
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
resolveCopyToSpaceConflictsTest(
|
||||
`rbac user with all at space from the ${scenario.spaceId} space`,
|
||||
{
|
||||
spaceId: scenario.spaceId,
|
||||
user: scenario.users.allAtSpace,
|
||||
tests: {
|
||||
withReferencesNotOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectUnauthorizedAtSpaceWithReferencesResult(scenario.spaceId),
|
||||
},
|
||||
withReferencesOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectUnauthorizedAtSpaceWithReferencesResult(scenario.spaceId),
|
||||
},
|
||||
withoutReferencesOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(scenario.spaceId),
|
||||
},
|
||||
withoutReferencesNotOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(scenario.spaceId),
|
||||
},
|
||||
nonExistentSpace: {
|
||||
statusCode: 200,
|
||||
response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(
|
||||
scenario.spaceId,
|
||||
NON_EXISTENT_SPACE_ID
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
|
||||
import { copyToSpaceTestSuiteFactory } from '../../common/suites/copy_to_space';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function copyToSpacesOnlySuite({ getService }: FtrProviderContext) {
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const es = getService('es');
|
||||
|
||||
const {
|
||||
copyToSpaceTest,
|
||||
expectNoConflictsWithoutReferencesResult,
|
||||
expectNoConflictsWithReferencesResult,
|
||||
expectNoConflictsForNonExistentSpaceResult,
|
||||
createExpectWithConflictsOverwritingResult,
|
||||
createExpectWithConflictsWithoutOverwritingResult,
|
||||
originSpaces,
|
||||
} = copyToSpaceTestSuiteFactory(es, esArchiver, supertestWithoutAuth);
|
||||
|
||||
describe('copy to spaces', () => {
|
||||
originSpaces.forEach(spaceId => {
|
||||
copyToSpaceTest(`from the ${spaceId} space`, {
|
||||
spaceId,
|
||||
tests: {
|
||||
noConflictsWithoutReferences: {
|
||||
statusCode: 200,
|
||||
response: expectNoConflictsWithoutReferencesResult,
|
||||
},
|
||||
noConflictsWithReferences: {
|
||||
statusCode: 200,
|
||||
response: expectNoConflictsWithReferencesResult,
|
||||
},
|
||||
withConflictsOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectWithConflictsOverwritingResult(spaceId),
|
||||
},
|
||||
withConflictsWithoutOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectWithConflictsWithoutOverwritingResult(spaceId),
|
||||
},
|
||||
multipleSpaces: {
|
||||
statusCode: 200,
|
||||
withConflictsResponse: createExpectWithConflictsOverwritingResult(spaceId),
|
||||
noConflictsResponse: expectNoConflictsWithReferencesResult,
|
||||
},
|
||||
nonExistentSpace: {
|
||||
statusCode: 200,
|
||||
response: expectNoConflictsForNonExistentSpaceResult,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -34,6 +34,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) {
|
|||
statusCode: 200,
|
||||
response: createExpectResults('default', 'space_1', 'space_2'),
|
||||
},
|
||||
copySavedObjectsPurpose: {
|
||||
statusCode: 200,
|
||||
response: createExpectResults('default', 'space_1', 'space_2'),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,6 +11,8 @@ export default function spacesOnlyTestSuite({ loadTestFile }: TestInvoker) {
|
|||
describe('spaces api without security', function() {
|
||||
this.tags('ciGroup5');
|
||||
|
||||
loadTestFile(require.resolve('./copy_to_space'));
|
||||
loadTestFile(require.resolve('./resolve_copy_to_space_conflicts'));
|
||||
loadTestFile(require.resolve('./create'));
|
||||
loadTestFile(require.resolve('./delete'));
|
||||
loadTestFile(require.resolve('./get_all'));
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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 { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
|
||||
import { resolveCopyToSpaceConflictsSuite } from '../../common/suites/resolve_copy_to_space_conflicts';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function resolveCopyToSpaceConflictsTestSuite({ getService }: FtrProviderContext) {
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const supertestWithAuth = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
const {
|
||||
resolveCopyToSpaceConflictsTest,
|
||||
createExpectNonOverriddenResponseWithReferences,
|
||||
createExpectNonOverriddenResponseWithoutReferences,
|
||||
createExpectOverriddenResponseWithReferences,
|
||||
createExpectOverriddenResponseWithoutReferences,
|
||||
NON_EXISTENT_SPACE_ID,
|
||||
originSpaces,
|
||||
} = resolveCopyToSpaceConflictsSuite(esArchiver, supertestWithAuth, supertestWithoutAuth);
|
||||
|
||||
describe('resolve copy to spaces conflicts', () => {
|
||||
originSpaces.forEach(spaceId => {
|
||||
resolveCopyToSpaceConflictsTest(`from the ${spaceId} space`, {
|
||||
spaceId,
|
||||
tests: {
|
||||
withReferencesNotOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectNonOverriddenResponseWithReferences(spaceId),
|
||||
},
|
||||
withReferencesOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectOverriddenResponseWithReferences(spaceId),
|
||||
},
|
||||
withoutReferencesOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectOverriddenResponseWithoutReferences(spaceId),
|
||||
},
|
||||
withoutReferencesNotOverwriting: {
|
||||
statusCode: 200,
|
||||
response: createExpectNonOverriddenResponseWithoutReferences(spaceId),
|
||||
},
|
||||
nonExistentSpace: {
|
||||
statusCode: 200,
|
||||
response: createExpectOverriddenResponseWithoutReferences(
|
||||
spaceId,
|
||||
NON_EXISTENT_SPACE_ID
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue