mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
Basic server side import API for saved objects (#32158)
* Initial work * Add overwrite and skip support * Cleanup and add tests * Move code into separate files * Remove reduce * New API parameters * Add support to replace references * Add better error handling * Add spaces tests * Fix return type in collectSavedObjects * Apply PR feedback * Update jest tests due to jest version upgrade * Add docs * WIP * Split import routes pt1 * Add tests * Fix broken tests * Update docs and fix broken test * Add successCount to _import endpoint * Make skip by default in resolution API * Update tests for removal of skips * Add back support for skips * Add success count * Add back resolve import conflicts x-pack tests * Remove writev from filter stream * Delete _mock_server.d.ts file * Rename lib/import_saved_objects to lib/import * Filter records at stream level for conflict resolution * Update docs * Add tests to validate documentation * Return 200 instead of other code for errors, include errors array * Change [] to {} * Apply PR feedback * Fix import object limit to not return 500 * Change some wording in the docs * Fix status code * Apply PR feedback pt2 * Lower maxImportPayloadBytes to 10MB * Add unknown type tests for import * Add unknown type tests for resolve_import_conflicts * Fix tslint issues
This commit is contained in:
parent
a1a9511446
commit
7cf91316ca
37 changed files with 3646 additions and 6 deletions
|
@ -19,6 +19,8 @@ NOTE: You cannot access these endpoints via the Console in Kibana.
|
|||
* <<saved-objects-api-update>>
|
||||
* <<saved-objects-api-delete>>
|
||||
* <<saved-objects-api-export>>
|
||||
* <<saved-objects-api-import>>
|
||||
* <<saved-objects-api-resolve-import-conflicts>>
|
||||
|
||||
include::saved-objects/get.asciidoc[]
|
||||
include::saved-objects/bulk_get.asciidoc[]
|
||||
|
@ -28,3 +30,5 @@ include::saved-objects/bulk_create.asciidoc[]
|
|||
include::saved-objects/update.asciidoc[]
|
||||
include::saved-objects/delete.asciidoc[]
|
||||
include::saved-objects/export.asciidoc[]
|
||||
include::saved-objects/import.asciidoc[]
|
||||
include::saved-objects/resolve_import_conflicts.asciidoc[]
|
||||
|
|
96
docs/api/saved-objects/import.asciidoc
Normal file
96
docs/api/saved-objects/import.asciidoc
Normal file
|
@ -0,0 +1,96 @@
|
|||
[[saved-objects-api-import]]
|
||||
=== Import Objects
|
||||
|
||||
experimental[This functionality is *experimental* and may be changed or removed completely in a future release.]
|
||||
|
||||
The import saved objects API enables you to create a set of Kibana saved objects from a file created by the export API.
|
||||
|
||||
Note: You cannot access this endpoint via the Console in Kibana.
|
||||
|
||||
==== Request
|
||||
|
||||
`POST /api/saved_objects/_import`
|
||||
|
||||
==== Query Parameters
|
||||
|
||||
`overwrite` (optional)::
|
||||
(boolean) Overwrite saved objects if they exist already
|
||||
|
||||
==== Request body
|
||||
|
||||
The request body must be of type multipart/form-data.
|
||||
|
||||
`file`::
|
||||
A file exported using the export API.
|
||||
|
||||
==== Response body
|
||||
|
||||
The response body will have a top level `success` property that indicates
|
||||
if the import was successful or not as well as a `successCount` indicating how many records are successfully imported.
|
||||
In the scenario the import wasn't successful a top level `errors` array will contain the objects that failed to import.
|
||||
|
||||
==== Examples
|
||||
|
||||
The following example imports an index pattern and dashboard.
|
||||
|
||||
[source,js]
|
||||
--------------------------------------------------
|
||||
POST api/saved_objects/_import
|
||||
Content-Type: multipart/form-data; boundary=EXAMPLE
|
||||
--EXAMPLE
|
||||
Content-Disposition: form-data; name="file"; filename="export.ndjson"
|
||||
Content-Type: application/ndjson
|
||||
|
||||
{"type":"index-pattern","id":"my-pattern","attributes":{"title":"my-pattern-*"}}
|
||||
{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"}}
|
||||
--EXAMPLE--
|
||||
--------------------------------------------------
|
||||
// KIBANA
|
||||
|
||||
A successful call returns a response code of `200` and a response body
|
||||
containing a JSON structure similar to the following example:
|
||||
|
||||
[source,js]
|
||||
--------------------------------------------------
|
||||
{
|
||||
"success": true,
|
||||
"successCount": 2
|
||||
}
|
||||
--------------------------------------------------
|
||||
|
||||
The following example imports an index pattern and dashboard but has a conflict on the index pattern.
|
||||
|
||||
[source,js]
|
||||
--------------------------------------------------
|
||||
POST api/saved_objects/_import
|
||||
Content-Type: multipart/form-data; boundary=EXAMPLE
|
||||
--EXAMPLE
|
||||
Content-Disposition: form-data; name="file"; filename="export.ndjson"
|
||||
Content-Type: application/ndjson
|
||||
|
||||
{"type":"index-pattern","id":"my-pattern","attributes":{"title":"my-pattern-*"}}
|
||||
{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"}}
|
||||
--EXAMPLE--
|
||||
--------------------------------------------------
|
||||
// KIBANA
|
||||
|
||||
The call returns a response code of `200` and a response body
|
||||
containing a JSON structure similar to the following example:
|
||||
|
||||
[source,js]
|
||||
--------------------------------------------------
|
||||
{
|
||||
"success": false,
|
||||
"successCount": 1,
|
||||
"errors": [
|
||||
{
|
||||
"id": "my-pattern",
|
||||
"type": "index-pattern",
|
||||
"error": {
|
||||
"statusCode": 409,
|
||||
"message": "version conflict, document already exists",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
--------------------------------------------------
|
104
docs/api/saved-objects/resolve_import_conflicts.asciidoc
Normal file
104
docs/api/saved-objects/resolve_import_conflicts.asciidoc
Normal file
|
@ -0,0 +1,104 @@
|
|||
[[saved-objects-api-resolve-import-conflicts]]
|
||||
=== Resolve Import Conflicts
|
||||
|
||||
experimental[This functionality is *experimental* and may be changed or removed completely in a future release.]
|
||||
|
||||
The resolve import conflicts API enables you to resolve conflicts given by the import API by either overwriting specific saved objects or changing references to a newly created object.
|
||||
|
||||
Note: You cannot access this endpoint via the Console in Kibana.
|
||||
|
||||
==== Request
|
||||
|
||||
`POST /api/saved_objects/_resolve_import_conflicts`
|
||||
|
||||
==== Request body
|
||||
|
||||
The request body must be of type multipart/form-data.
|
||||
|
||||
`file`::
|
||||
(ndjson) The same new line delimited JSON objects given to the import API.
|
||||
|
||||
`overwrites` (optional)::
|
||||
(array) A list of `type` and `id` objects allowed to be overwritten on import.
|
||||
|
||||
`replaceReferences` (optional)::
|
||||
(array) A list of `type`, `from` and `to` used to change imported saved object references to.
|
||||
|
||||
`skips` (optional)::
|
||||
(array) A list of `type` and `id` objects to skip importing.
|
||||
|
||||
==== Response body
|
||||
|
||||
The response body will have a top level `success` property that indicates
|
||||
if the import was successful or not as well as a `successCount` indicating how many records are successfully resolved.
|
||||
In the scenario the import wasn't successful a top level `errors` array will contain the objects that failed to import.
|
||||
|
||||
==== Examples
|
||||
|
||||
The following example resolves conflicts for an index pattern and dashboard but indicates to skip the index pattern.
|
||||
This will cause the index pattern to not be in the system and the dashboard to overwrite the existing saved object.
|
||||
|
||||
[source,js]
|
||||
--------------------------------------------------
|
||||
POST api/saved_objects/_resolve_import_conflicts
|
||||
Content-Type: multipart/form-data; boundary=EXAMPLE
|
||||
--EXAMPLE
|
||||
Content-Disposition: form-data; name="file"; filename="export.ndjson"
|
||||
Content-Type: application/ndjson
|
||||
|
||||
{"type":"index-pattern","id":"my-pattern","attributes":{"title":"my-pattern-*"}}
|
||||
{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"}}
|
||||
--EXAMPLE
|
||||
Content-Disposition: form-data; name="skips"
|
||||
|
||||
[{"type":"index-pattern","id":"my-pattern"}]
|
||||
--EXAMPLE
|
||||
Content-Disposition: form-data; name="overwrites"
|
||||
|
||||
[{"type":"dashboard","id":"my-dashboard"}]
|
||||
--EXAMPLE--
|
||||
--------------------------------------------------
|
||||
// KIBANA
|
||||
|
||||
A successful call returns a response code of `200` and a response body
|
||||
containing a JSON structure similar to the following example:
|
||||
|
||||
[source,js]
|
||||
--------------------------------------------------
|
||||
{
|
||||
"success": true,
|
||||
"successCount": 1
|
||||
}
|
||||
--------------------------------------------------
|
||||
|
||||
The following example resolves conflicts for a visualization and dashboard but indicates
|
||||
to replace the dashboard references to another visualization.
|
||||
|
||||
[source,js]
|
||||
--------------------------------------------------
|
||||
POST api/saved_objects/_resolve_import_conflicts
|
||||
Content-Type: multipart/form-data; boundary=EXAMPLE
|
||||
--EXAMPLE
|
||||
Content-Disposition: form-data; name="file"; filename="export.ndjson"
|
||||
Content-Type: application/ndjson
|
||||
|
||||
{"type":"visualization","id":"my-vis","attributes":{"title":"Look at my visualization"}}
|
||||
{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"},"references":[{"name":"panel_0","type":"visualization","id":"my-vis"}]}
|
||||
--EXAMPLE
|
||||
Content-Disposition: form-data; name="replaceReferences"
|
||||
|
||||
[{"type":"visualization","from":"my-vis","to":"my-vis-2"}]
|
||||
--EXAMPLE--
|
||||
--------------------------------------------------
|
||||
// KIBANA
|
||||
|
||||
A successful call returns a response code of `200` and a response body
|
||||
containing a JSON structure similar to the following example:
|
||||
|
||||
[source,js]
|
||||
--------------------------------------------------
|
||||
{
|
||||
"success": true,
|
||||
"successCount": 1
|
||||
}
|
||||
--------------------------------------------------
|
|
@ -252,6 +252,7 @@ export default () => Joi.object({
|
|||
}).default(),
|
||||
|
||||
savedObjects: Joi.object({
|
||||
maxImportPayloadBytes: Joi.number().default(10485760),
|
||||
maxImportExportSize: Joi.number().default(10000),
|
||||
}).default(),
|
||||
|
||||
|
|
815
src/legacy/server/saved_objects/lib/import.test.ts
Normal file
815
src/legacy/server/saved_objects/lib/import.test.ts
Normal file
|
@ -0,0 +1,815 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { Readable } from 'stream';
|
||||
import {
|
||||
createConcatStream,
|
||||
createListStream,
|
||||
createPromiseFromStreams,
|
||||
} from '../../../utils/streams';
|
||||
import { SavedObject } from '../service';
|
||||
import {
|
||||
collectSavedObjects,
|
||||
createLimitStream,
|
||||
createObjectsFilter,
|
||||
extractErrors,
|
||||
importSavedObjects,
|
||||
resolveImportConflicts,
|
||||
} from './import';
|
||||
|
||||
describe('extractErrors()', () => {
|
||||
test('returns empty array when no errors exist', () => {
|
||||
const savedObjects: SavedObject[] = [];
|
||||
const result = extractErrors(savedObjects);
|
||||
expect(result).toMatchInlineSnapshot(`Array []`);
|
||||
});
|
||||
|
||||
test('extracts errors from saved objects', () => {
|
||||
const savedObjects: SavedObject[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'dashboard',
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'dashboard',
|
||||
attributes: {},
|
||||
references: [],
|
||||
error: {
|
||||
statusCode: 409,
|
||||
message: 'Conflict',
|
||||
},
|
||||
},
|
||||
];
|
||||
const result = extractErrors(savedObjects);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"error": Object {
|
||||
"message": "Conflict",
|
||||
"statusCode": 409,
|
||||
},
|
||||
"id": "2",
|
||||
"type": "dashboard",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createLimitStream()', () => {
|
||||
test('limit of 5 allows 5 items through', async () => {
|
||||
await createPromiseFromStreams([createListStream([1, 2, 3, 4, 5]), createLimitStream(5)]);
|
||||
});
|
||||
|
||||
test('limit of 5 errors out when 6 items are through', async () => {
|
||||
await expect(
|
||||
createPromiseFromStreams([createListStream([1, 2, 3, 4, 5, 6]), createLimitStream(5)])
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"Can't import more than 5 objects"`);
|
||||
});
|
||||
|
||||
test('send the values on the output stream', async () => {
|
||||
const result = await createPromiseFromStreams([
|
||||
createListStream([1, 2, 3]),
|
||||
createLimitStream(3),
|
||||
createConcatStream([]),
|
||||
]);
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('collectSavedObjects()', () => {
|
||||
test('collects nothing when stream is empty', async () => {
|
||||
const readStream = new Readable({
|
||||
read() {
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
const objects = await collectSavedObjects(readStream, 10);
|
||||
expect(objects).toMatchInlineSnapshot(`Array []`);
|
||||
});
|
||||
|
||||
test('collects objects from stream', async () => {
|
||||
const readStream = new Readable({
|
||||
read() {
|
||||
this.push('{"foo":true}');
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
const objects = await collectSavedObjects(readStream, 1);
|
||||
expect(objects).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"foo": true,
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('filters out empty lines', async () => {
|
||||
const readStream = new Readable({
|
||||
read() {
|
||||
this.push('{"foo":true}\n\n');
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
const objects = await collectSavedObjects(readStream, 1);
|
||||
expect(objects).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"foo": true,
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('throws error when object limit is reached', async () => {
|
||||
const readStream = new Readable({
|
||||
read() {
|
||||
this.push('{"foo":true}\n');
|
||||
this.push('{"bar":true}\n');
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
await expect(collectSavedObjects(readStream, 1)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Can't import more than 1 objects"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createObjectsFilter()', () => {
|
||||
test('filters should return false when contains empty parameters', () => {
|
||||
const fn = createObjectsFilter([], [], []);
|
||||
expect(fn({ type: 'a', id: '1', attributes: {}, references: [] })).toEqual(false);
|
||||
});
|
||||
|
||||
test('filters should exclude skips', () => {
|
||||
const fn = createObjectsFilter(
|
||||
[
|
||||
{
|
||||
type: 'a',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
[],
|
||||
[
|
||||
{
|
||||
type: 'b',
|
||||
from: '1',
|
||||
to: '2',
|
||||
},
|
||||
]
|
||||
);
|
||||
expect(
|
||||
fn({
|
||||
type: 'a',
|
||||
id: '1',
|
||||
attributes: {},
|
||||
references: [{ name: 'ref_0', type: 'b', id: '1' }],
|
||||
})
|
||||
).toEqual(false);
|
||||
expect(
|
||||
fn({
|
||||
type: 'a',
|
||||
id: '2',
|
||||
attributes: {},
|
||||
references: [{ name: 'ref_0', type: 'b', id: '1' }],
|
||||
})
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
test('filter should include references to replace', () => {
|
||||
const fn = createObjectsFilter(
|
||||
[],
|
||||
[],
|
||||
[
|
||||
{
|
||||
type: 'b',
|
||||
from: '1',
|
||||
to: '2',
|
||||
},
|
||||
]
|
||||
);
|
||||
expect(
|
||||
fn({
|
||||
type: 'a',
|
||||
id: '1',
|
||||
attributes: {},
|
||||
references: [
|
||||
{
|
||||
name: 'ref_0',
|
||||
type: 'b',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
})
|
||||
).toEqual(true);
|
||||
expect(
|
||||
fn({
|
||||
type: 'a',
|
||||
id: '1',
|
||||
attributes: {},
|
||||
references: [
|
||||
{
|
||||
name: 'ref_0',
|
||||
type: 'b',
|
||||
id: '2',
|
||||
},
|
||||
],
|
||||
})
|
||||
).toEqual(false);
|
||||
});
|
||||
|
||||
test('filter should include objects to overwrite', () => {
|
||||
const fn = createObjectsFilter(
|
||||
[],
|
||||
[
|
||||
{
|
||||
type: 'a',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
expect(fn({ type: 'a', id: '1', attributes: {}, references: [] })).toEqual(true);
|
||||
expect(fn({ type: 'a', id: '2', attributes: {}, references: [] })).toEqual(false);
|
||||
});
|
||||
|
||||
test('filter should work with skips, overwrites and replaceReferences', () => {
|
||||
const fn = createObjectsFilter(
|
||||
[
|
||||
{
|
||||
type: 'a',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
type: 'a',
|
||||
id: '2',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
type: 'b',
|
||||
from: '1',
|
||||
to: '2',
|
||||
},
|
||||
]
|
||||
);
|
||||
expect(
|
||||
fn({
|
||||
type: 'a',
|
||||
id: '1',
|
||||
attributes: {},
|
||||
references: [
|
||||
{
|
||||
name: 'ref_0',
|
||||
type: 'b',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
})
|
||||
).toEqual(false);
|
||||
expect(
|
||||
fn({
|
||||
type: 'a',
|
||||
id: '2',
|
||||
attributes: {},
|
||||
references: [
|
||||
{
|
||||
name: 'ref_0',
|
||||
type: 'b',
|
||||
id: '2',
|
||||
},
|
||||
],
|
||||
})
|
||||
).toEqual(true);
|
||||
expect(
|
||||
fn({
|
||||
type: 'a',
|
||||
id: '3',
|
||||
attributes: {},
|
||||
references: [
|
||||
{
|
||||
name: 'ref_0',
|
||||
type: 'b',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
})
|
||||
).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('importSavedObjects()', () => {
|
||||
const savedObjects: SavedObject[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'index-pattern',
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'search',
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'visualization',
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
type: 'dashboard',
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
];
|
||||
const savedObjectsClient = {
|
||||
errors: {} as any,
|
||||
bulkCreate: jest.fn(),
|
||||
bulkGet: jest.fn(),
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
find: jest.fn(),
|
||||
get: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
savedObjectsClient.bulkCreate.mockReset();
|
||||
savedObjectsClient.bulkGet.mockReset();
|
||||
savedObjectsClient.create.mockReset();
|
||||
savedObjectsClient.delete.mockReset();
|
||||
savedObjectsClient.find.mockReset();
|
||||
savedObjectsClient.get.mockReset();
|
||||
savedObjectsClient.update.mockReset();
|
||||
});
|
||||
|
||||
test('calls bulkCreate without overwrite', async () => {
|
||||
const readStream = new Readable({
|
||||
read() {
|
||||
savedObjects.forEach(obj => this.push(JSON.stringify(obj) + '\n'));
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
savedObjectsClient.bulkCreate.mockResolvedValue({
|
||||
saved_objects: savedObjects,
|
||||
});
|
||||
const result = await importSavedObjects({
|
||||
readStream,
|
||||
objectLimit: 4,
|
||||
overwrite: false,
|
||||
savedObjectsClient,
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"success": true,
|
||||
"successCount": 4,
|
||||
}
|
||||
`);
|
||||
expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(`
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "2",
|
||||
"references": Array [],
|
||||
"type": "search",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "3",
|
||||
"references": Array [],
|
||||
"type": "visualization",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "4",
|
||||
"references": Array [],
|
||||
"type": "dashboard",
|
||||
},
|
||||
],
|
||||
Object {
|
||||
"overwrite": false,
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('calls bulkCreate with overwrite', async () => {
|
||||
const readStream = new Readable({
|
||||
read() {
|
||||
savedObjects.forEach(obj => this.push(JSON.stringify(obj) + '\n'));
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
savedObjectsClient.bulkCreate.mockResolvedValue({
|
||||
saved_objects: savedObjects,
|
||||
});
|
||||
const result = await importSavedObjects({
|
||||
readStream,
|
||||
objectLimit: 4,
|
||||
overwrite: true,
|
||||
savedObjectsClient,
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"success": true,
|
||||
"successCount": 4,
|
||||
}
|
||||
`);
|
||||
expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(`
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "2",
|
||||
"references": Array [],
|
||||
"type": "search",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "3",
|
||||
"references": Array [],
|
||||
"type": "visualization",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "4",
|
||||
"references": Array [],
|
||||
"type": "dashboard",
|
||||
},
|
||||
],
|
||||
Object {
|
||||
"overwrite": true,
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('extracts errors', async () => {
|
||||
const readStream = new Readable({
|
||||
read() {
|
||||
savedObjects.forEach(obj => this.push(JSON.stringify(obj) + '\n'));
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
savedObjectsClient.bulkCreate.mockResolvedValue({
|
||||
saved_objects: savedObjects.map(savedObject => ({
|
||||
type: savedObject.type,
|
||||
id: savedObject.id,
|
||||
error: {
|
||||
statusCode: 409,
|
||||
message: 'conflict',
|
||||
},
|
||||
})),
|
||||
});
|
||||
const result = await importSavedObjects({
|
||||
readStream,
|
||||
objectLimit: 4,
|
||||
overwrite: false,
|
||||
savedObjectsClient,
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"errors": Array [
|
||||
Object {
|
||||
"error": Object {
|
||||
"message": "conflict",
|
||||
"statusCode": 409,
|
||||
},
|
||||
"id": "1",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"error": Object {
|
||||
"message": "conflict",
|
||||
"statusCode": 409,
|
||||
},
|
||||
"id": "2",
|
||||
"type": "search",
|
||||
},
|
||||
Object {
|
||||
"error": Object {
|
||||
"message": "conflict",
|
||||
"statusCode": 409,
|
||||
},
|
||||
"id": "3",
|
||||
"type": "visualization",
|
||||
},
|
||||
Object {
|
||||
"error": Object {
|
||||
"message": "conflict",
|
||||
"statusCode": 409,
|
||||
},
|
||||
"id": "4",
|
||||
"type": "dashboard",
|
||||
},
|
||||
],
|
||||
"success": false,
|
||||
"successCount": 0,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveImportConflicts()', () => {
|
||||
const savedObjects: SavedObject[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'index-pattern',
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'search',
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'visualization',
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
type: 'dashboard',
|
||||
attributes: {},
|
||||
references: [
|
||||
{
|
||||
name: 'panel_0',
|
||||
type: 'visualization',
|
||||
id: '3',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const savedObjectsClient = {
|
||||
errors: {} as any,
|
||||
bulkCreate: jest.fn(),
|
||||
bulkGet: jest.fn(),
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
find: jest.fn(),
|
||||
get: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
savedObjectsClient.bulkCreate.mockReset();
|
||||
savedObjectsClient.bulkGet.mockReset();
|
||||
savedObjectsClient.create.mockReset();
|
||||
savedObjectsClient.delete.mockReset();
|
||||
savedObjectsClient.find.mockReset();
|
||||
savedObjectsClient.get.mockReset();
|
||||
savedObjectsClient.update.mockReset();
|
||||
});
|
||||
|
||||
test('works with empty parameters', async () => {
|
||||
const readStream = new Readable({
|
||||
read() {
|
||||
savedObjects.forEach(obj => this.push(JSON.stringify(obj) + '\n'));
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
savedObjectsClient.bulkCreate.mockResolvedValue({
|
||||
saved_objects: savedObjects,
|
||||
});
|
||||
const result = await resolveImportConflicts({
|
||||
readStream,
|
||||
objectLimit: 4,
|
||||
skips: [],
|
||||
overwrites: [],
|
||||
savedObjectsClient,
|
||||
replaceReferences: [],
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"success": true,
|
||||
"successCount": 0,
|
||||
}
|
||||
`);
|
||||
expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(`[MockFunction]`);
|
||||
});
|
||||
|
||||
test('works with skips', async () => {
|
||||
const readStream = new Readable({
|
||||
read() {
|
||||
savedObjects.forEach(obj => this.push(JSON.stringify(obj) + '\n'));
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
savedObjectsClient.bulkCreate.mockResolvedValue({
|
||||
saved_objects: savedObjects,
|
||||
});
|
||||
const result = await resolveImportConflicts({
|
||||
readStream,
|
||||
objectLimit: 4,
|
||||
skips: [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: '4',
|
||||
},
|
||||
],
|
||||
overwrites: [],
|
||||
savedObjectsClient,
|
||||
replaceReferences: [
|
||||
{
|
||||
type: 'visualization',
|
||||
from: '3',
|
||||
to: '30',
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"success": true,
|
||||
"successCount": 0,
|
||||
}
|
||||
`);
|
||||
expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(`[MockFunction]`);
|
||||
});
|
||||
|
||||
test('works with overwrites', async () => {
|
||||
const readStream = new Readable({
|
||||
read() {
|
||||
savedObjects.forEach(obj => this.push(JSON.stringify(obj) + '\n'));
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
savedObjectsClient.bulkCreate.mockResolvedValue({
|
||||
saved_objects: savedObjects,
|
||||
});
|
||||
const result = await resolveImportConflicts({
|
||||
readStream,
|
||||
objectLimit: 4,
|
||||
skips: [],
|
||||
overwrites: [
|
||||
{
|
||||
type: 'index-pattern',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
savedObjectsClient,
|
||||
replaceReferences: [],
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"success": true,
|
||||
"successCount": 1,
|
||||
}
|
||||
`);
|
||||
expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(`
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
Object {
|
||||
"overwrite": true,
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('works wtih replaceReferences', async () => {
|
||||
const readStream = new Readable({
|
||||
read() {
|
||||
savedObjects.forEach(obj => this.push(JSON.stringify(obj) + '\n'));
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
savedObjectsClient.bulkCreate.mockResolvedValue({
|
||||
saved_objects: savedObjects,
|
||||
});
|
||||
const result = await resolveImportConflicts({
|
||||
readStream,
|
||||
objectLimit: 4,
|
||||
skips: [],
|
||||
overwrites: [],
|
||||
savedObjectsClient,
|
||||
replaceReferences: [
|
||||
{
|
||||
type: 'visualization',
|
||||
from: '3',
|
||||
to: '13',
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"success": true,
|
||||
"successCount": 1,
|
||||
}
|
||||
`);
|
||||
expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(`
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "4",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "13",
|
||||
"name": "panel_0",
|
||||
"type": "visualization",
|
||||
},
|
||||
],
|
||||
"type": "dashboard",
|
||||
},
|
||||
],
|
||||
Object {
|
||||
"overwrite": true,
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
218
src/legacy/server/saved_objects/lib/import.ts
Normal file
218
src/legacy/server/saved_objects/lib/import.ts
Normal file
|
@ -0,0 +1,218 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import { Readable, Transform } from 'stream';
|
||||
import {
|
||||
createConcatStream,
|
||||
createFilterStream,
|
||||
createMapStream,
|
||||
createPromiseFromStreams,
|
||||
createSplitStream,
|
||||
} from '../../../utils/streams';
|
||||
import { SavedObject, SavedObjectsClient } from '../service';
|
||||
|
||||
interface CustomError {
|
||||
id: string;
|
||||
type: string;
|
||||
error: {
|
||||
message: string;
|
||||
statusCode: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface ImportResponse {
|
||||
success: boolean;
|
||||
successCount: number;
|
||||
errors?: CustomError[];
|
||||
}
|
||||
|
||||
interface ImportSavedObjectsOptions {
|
||||
readStream: Readable;
|
||||
objectLimit: number;
|
||||
overwrite: boolean;
|
||||
savedObjectsClient: SavedObjectsClient;
|
||||
}
|
||||
|
||||
interface ResolveImportConflictsOptions {
|
||||
readStream: Readable;
|
||||
objectLimit: number;
|
||||
savedObjectsClient: SavedObjectsClient;
|
||||
overwrites: Array<{
|
||||
type: string;
|
||||
id: string;
|
||||
}>;
|
||||
replaceReferences: Array<{
|
||||
type: string;
|
||||
from: string;
|
||||
to: string;
|
||||
}>;
|
||||
skips: Array<{
|
||||
type: string;
|
||||
id: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function extractErrors(savedObjects: SavedObject[]) {
|
||||
const errors: CustomError[] = [];
|
||||
for (const savedObject of savedObjects) {
|
||||
if (savedObject.error) {
|
||||
errors.push({
|
||||
id: savedObject.id,
|
||||
type: savedObject.type,
|
||||
error: savedObject.error,
|
||||
});
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function createLimitStream(limit: number) {
|
||||
let counter = 0;
|
||||
return new Transform({
|
||||
objectMode: true,
|
||||
async transform(obj, enc, done) {
|
||||
if (counter >= limit) {
|
||||
return done(Boom.badRequest(`Can't import more than ${limit} objects`));
|
||||
}
|
||||
counter++;
|
||||
done(undefined, obj);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function collectSavedObjects(
|
||||
readStream: Readable,
|
||||
objectLimit: number,
|
||||
filter?: (obj: SavedObject) => boolean
|
||||
): Promise<SavedObject[]> {
|
||||
return (await createPromiseFromStreams([
|
||||
readStream,
|
||||
createSplitStream('\n'),
|
||||
createMapStream((str: string) => {
|
||||
if (str && str !== '') {
|
||||
return JSON.parse(str);
|
||||
}
|
||||
}),
|
||||
createFilterStream<SavedObject>(obj => !!obj),
|
||||
createLimitStream(objectLimit),
|
||||
createFilterStream<SavedObject>(obj => (filter ? filter(obj) : true)),
|
||||
createConcatStream([]),
|
||||
])) as SavedObject[];
|
||||
}
|
||||
|
||||
export function createObjectsFilter(
|
||||
skips: Array<{
|
||||
type: string;
|
||||
id: string;
|
||||
}>,
|
||||
overwrites: Array<{
|
||||
type: string;
|
||||
id: string;
|
||||
}>,
|
||||
replaceReferences: Array<{
|
||||
type: string;
|
||||
from: string;
|
||||
to: string;
|
||||
}>
|
||||
) {
|
||||
const refReplacements = replaceReferences.map(ref => `${ref.type}:${ref.from}`);
|
||||
return (obj: SavedObject) => {
|
||||
if (skips.some(skipObj => skipObj.type === obj.type && skipObj.id === obj.id)) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
overwrites.some(overwriteObj => overwriteObj.type === obj.type && overwriteObj.id === obj.id)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
for (const reference of obj.references || []) {
|
||||
if (refReplacements.includes(`${reference.type}:${reference.id}`)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
export async function importSavedObjects({
|
||||
readStream,
|
||||
objectLimit,
|
||||
overwrite,
|
||||
savedObjectsClient,
|
||||
}: ImportSavedObjectsOptions): Promise<ImportResponse> {
|
||||
const objectsToImport = await collectSavedObjects(readStream, objectLimit);
|
||||
|
||||
if (objectsToImport.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
successCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const bulkCreateResult = await savedObjectsClient.bulkCreate(objectsToImport, {
|
||||
overwrite,
|
||||
});
|
||||
const errors = extractErrors(bulkCreateResult.saved_objects);
|
||||
|
||||
return {
|
||||
success: errors.length === 0,
|
||||
successCount: objectsToImport.length - errors.length,
|
||||
...(errors.length ? { errors } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export async function resolveImportConflicts({
|
||||
readStream,
|
||||
objectLimit,
|
||||
skips,
|
||||
overwrites,
|
||||
savedObjectsClient,
|
||||
replaceReferences,
|
||||
}: ResolveImportConflictsOptions): Promise<ImportResponse> {
|
||||
let errors: CustomError[] = [];
|
||||
const filter = createObjectsFilter(skips, overwrites, replaceReferences);
|
||||
const objectsToResolve = await collectSavedObjects(readStream, objectLimit, filter);
|
||||
|
||||
// Replace references
|
||||
const refReplacementsMap: Record<string, string> = {};
|
||||
for (const { type, to, from } of replaceReferences) {
|
||||
refReplacementsMap[`${type}:${from}`] = to;
|
||||
}
|
||||
for (const savedObject of objectsToResolve) {
|
||||
for (const reference of savedObject.references || []) {
|
||||
if (refReplacementsMap[`${reference.type}:${reference.id}`]) {
|
||||
reference.id = refReplacementsMap[`${reference.type}:${reference.id}`];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (objectsToResolve.length) {
|
||||
const bulkCreateResult = await savedObjectsClient.bulkCreate(objectsToResolve, {
|
||||
overwrite: true,
|
||||
});
|
||||
errors = extractErrors(bulkCreateResult.saved_objects);
|
||||
}
|
||||
|
||||
return {
|
||||
success: errors.length === 0,
|
||||
successCount: objectsToResolve.length - errors.length,
|
||||
...(errors.length ? { errors } : {}),
|
||||
};
|
||||
}
|
|
@ -17,6 +17,5 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import Hapi from 'hapi';
|
||||
|
||||
export function MockServer(config?: { [key: string]: any }): Hapi.Server;
|
||||
export { importSavedObjects, resolveImportConflicts } from './import';
|
||||
export { getSortedObjectsForExport } from './export';
|
|
@ -23,6 +23,7 @@ import { defaultValidationErrorHandler } from '../../../../core/server/http/http
|
|||
const defaultConfig = {
|
||||
'kibana.index': '.kibana',
|
||||
'savedObjects.maxImportExportSize': 10000,
|
||||
'savedObjects.maxImportPayloadBytes': 52428800,
|
||||
};
|
||||
|
||||
export function createMockServer(config: { [key: string]: any } = defaultConfig) {
|
||||
|
|
|
@ -21,7 +21,7 @@ import Hapi from 'hapi';
|
|||
import Joi from 'joi';
|
||||
import stringify from 'json-stable-stringify';
|
||||
import { SavedObjectsClient } from '../';
|
||||
import { getSortedObjectsForExport } from '../lib/export';
|
||||
import { getSortedObjectsForExport } from '../lib';
|
||||
import { Prerequisites } from './types';
|
||||
|
||||
const ALLOWED_TYPES = ['index-pattern', 'search', 'visualization', 'dashboard'];
|
||||
|
|
188
src/legacy/server/saved_objects/routes/import.test.ts
Normal file
188
src/legacy/server/saved_objects/routes/import.test.ts
Normal file
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import Hapi from 'hapi';
|
||||
import { createMockServer } from './_mock_server';
|
||||
import { createImportRoute } from './import';
|
||||
|
||||
describe('POST /api/saved_objects/_import', () => {
|
||||
let server: Hapi.Server;
|
||||
const savedObjectsClient = {
|
||||
errors: {} as any,
|
||||
bulkCreate: jest.fn(),
|
||||
bulkGet: jest.fn(),
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
find: jest.fn(),
|
||||
get: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
server = createMockServer();
|
||||
savedObjectsClient.bulkCreate.mockReset();
|
||||
savedObjectsClient.bulkGet.mockReset();
|
||||
savedObjectsClient.create.mockReset();
|
||||
savedObjectsClient.delete.mockReset();
|
||||
savedObjectsClient.find.mockReset();
|
||||
savedObjectsClient.get.mockReset();
|
||||
savedObjectsClient.update.mockReset();
|
||||
|
||||
const prereqs = {
|
||||
getSavedObjectsClient: {
|
||||
assign: 'savedObjectsClient',
|
||||
method() {
|
||||
return savedObjectsClient;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
server.route(createImportRoute(prereqs, server));
|
||||
});
|
||||
|
||||
test('formats successful response', async () => {
|
||||
const request = {
|
||||
method: 'POST',
|
||||
url: '/api/saved_objects/_import',
|
||||
payload: [
|
||||
'--BOUNDARY',
|
||||
'Content-Disposition: form-data; name="file"; filename="export.ndjson"',
|
||||
'Content-Type: application/ndjson',
|
||||
'',
|
||||
'',
|
||||
'--BOUNDARY--',
|
||||
].join('\r\n'),
|
||||
headers: {
|
||||
'content-Type': 'multipart/form-data; boundary=BOUNDARY',
|
||||
},
|
||||
};
|
||||
const { payload, statusCode } = await server.inject(request);
|
||||
const response = JSON.parse(payload);
|
||||
expect(statusCode).toBe(200);
|
||||
expect(response).toEqual({
|
||||
success: true,
|
||||
successCount: 0,
|
||||
});
|
||||
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('imports an index pattern and dashboard', async () => {
|
||||
// NOTE: changes to this scenario should be reflected in the docs
|
||||
const request = {
|
||||
method: 'POST',
|
||||
url: '/api/saved_objects/_import',
|
||||
payload: [
|
||||
'--EXAMPLE',
|
||||
'Content-Disposition: form-data; name="file"; filename="export.ndjson"',
|
||||
'Content-Type: application/ndjson',
|
||||
'',
|
||||
'{"type":"index-pattern","id":"my-pattern","attributes":{"title":"my-pattern-*"}}',
|
||||
'{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"}}',
|
||||
'--EXAMPLE--',
|
||||
].join('\r\n'),
|
||||
headers: {
|
||||
'content-Type': 'multipart/form-data; boundary=EXAMPLE',
|
||||
},
|
||||
};
|
||||
savedObjectsClient.bulkCreate.mockResolvedValueOnce({
|
||||
saved_objects: [
|
||||
{
|
||||
type: 'index-pattern',
|
||||
id: 'my-pattern',
|
||||
attributes: {
|
||||
title: 'my-pattern-*',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'my-dashboard',
|
||||
attributes: {
|
||||
title: 'Look at my dashboard',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const { payload, statusCode } = await server.inject(request);
|
||||
const response = JSON.parse(payload);
|
||||
expect(statusCode).toBe(200);
|
||||
expect(response).toEqual({
|
||||
success: true,
|
||||
successCount: 2,
|
||||
});
|
||||
});
|
||||
|
||||
test('imports an index pattern and dashboard but has a conflict on the index pattern', async () => {
|
||||
// NOTE: changes to this scenario should be reflected in the docs
|
||||
const request = {
|
||||
method: 'POST',
|
||||
url: '/api/saved_objects/_import',
|
||||
payload: [
|
||||
'--EXAMPLE',
|
||||
'Content-Disposition: form-data; name="file"; filename="export.ndjson"',
|
||||
'Content-Type: application/ndjson',
|
||||
'',
|
||||
'{"type":"index-pattern","id":"my-pattern","attributes":{"title":"my-pattern-*"}}',
|
||||
'{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"}}',
|
||||
'--EXAMPLE--',
|
||||
].join('\r\n'),
|
||||
headers: {
|
||||
'content-Type': 'multipart/form-data; boundary=EXAMPLE',
|
||||
},
|
||||
};
|
||||
savedObjectsClient.bulkCreate.mockResolvedValueOnce({
|
||||
saved_objects: [
|
||||
{
|
||||
type: 'index-pattern',
|
||||
id: 'my-pattern',
|
||||
attributes: {},
|
||||
references: [],
|
||||
error: {
|
||||
statusCode: 409,
|
||||
message: 'version conflict, document already exists',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'my-dashboard',
|
||||
attributes: {
|
||||
title: 'Look at my dashboard',
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
const { payload, statusCode } = await server.inject(request);
|
||||
const response = JSON.parse(payload);
|
||||
expect(statusCode).toBe(200);
|
||||
expect(response).toEqual({
|
||||
success: false,
|
||||
successCount: 1,
|
||||
errors: [
|
||||
{
|
||||
id: 'my-pattern',
|
||||
type: 'index-pattern',
|
||||
error: {
|
||||
statusCode: 409,
|
||||
message: 'version conflict, document already exists',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
82
src/legacy/server/saved_objects/routes/import.ts
Normal file
82
src/legacy/server/saved_objects/routes/import.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import Hapi from 'hapi';
|
||||
import Joi from 'joi';
|
||||
import { extname } from 'path';
|
||||
import { Readable } from 'stream';
|
||||
import { SavedObjectsClient } from '../';
|
||||
import { importSavedObjects } from '../lib';
|
||||
import { Prerequisites, WithoutQueryAndParams } from './types';
|
||||
|
||||
interface HapiReadableStream extends Readable {
|
||||
hapi: {
|
||||
filename: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ImportRequest extends WithoutQueryAndParams<Hapi.Request> {
|
||||
pre: {
|
||||
savedObjectsClient: SavedObjectsClient;
|
||||
};
|
||||
query: {
|
||||
overwrite: boolean;
|
||||
};
|
||||
payload: {
|
||||
file: HapiReadableStream;
|
||||
};
|
||||
}
|
||||
|
||||
export const createImportRoute = (prereqs: Prerequisites, server: Hapi.Server) => ({
|
||||
path: '/api/saved_objects/_import',
|
||||
method: 'POST',
|
||||
config: {
|
||||
pre: [prereqs.getSavedObjectsClient],
|
||||
payload: {
|
||||
maxBytes: server.config().get('savedObjects.maxImportPayloadBytes'),
|
||||
output: 'stream',
|
||||
allow: 'multipart/form-data',
|
||||
},
|
||||
validate: {
|
||||
query: Joi.object()
|
||||
.keys({
|
||||
overwrite: Joi.boolean().default(false),
|
||||
})
|
||||
.default(),
|
||||
payload: Joi.object({
|
||||
file: Joi.object().required(),
|
||||
}).default(),
|
||||
},
|
||||
},
|
||||
async handler(request: ImportRequest, h: Hapi.ResponseToolkit) {
|
||||
const { savedObjectsClient } = request.pre;
|
||||
const { filename } = request.payload.file.hapi;
|
||||
const fileExtension = extname(filename).toLowerCase();
|
||||
if (fileExtension !== '.ndjson') {
|
||||
return Boom.badRequest(`Invalid file extension ${fileExtension}`);
|
||||
}
|
||||
return await importSavedObjects({
|
||||
savedObjectsClient,
|
||||
readStream: request.payload.file,
|
||||
objectLimit: request.server.config().get('savedObjects.maxImportExportSize'),
|
||||
overwrite: request.query.overwrite,
|
||||
});
|
||||
},
|
||||
});
|
|
@ -23,5 +23,7 @@ export { createCreateRoute } from './create';
|
|||
export { createDeleteRoute } from './delete';
|
||||
export { createFindRoute } from './find';
|
||||
export { createGetRoute } from './get';
|
||||
export { createImportRoute } from './import';
|
||||
export { createResolveImportConflictsRoute } from './resolve_import_conflicts';
|
||||
export { createUpdateRoute } from './update';
|
||||
export { createExportRoute } from './export';
|
||||
|
|
|
@ -0,0 +1,229 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import Hapi from 'hapi';
|
||||
import { createMockServer } from './_mock_server';
|
||||
import { createResolveImportConflictsRoute } from './resolve_import_conflicts';
|
||||
|
||||
describe('POST /api/saved_objects/_resolve_import_conflicts', () => {
|
||||
let server: Hapi.Server;
|
||||
const savedObjectsClient = {
|
||||
errors: {} as any,
|
||||
bulkCreate: jest.fn(),
|
||||
bulkGet: jest.fn(),
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
find: jest.fn(),
|
||||
get: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
server = createMockServer();
|
||||
savedObjectsClient.bulkCreate.mockReset();
|
||||
savedObjectsClient.bulkGet.mockReset();
|
||||
savedObjectsClient.create.mockReset();
|
||||
savedObjectsClient.delete.mockReset();
|
||||
savedObjectsClient.find.mockReset();
|
||||
savedObjectsClient.get.mockReset();
|
||||
savedObjectsClient.update.mockReset();
|
||||
|
||||
const prereqs = {
|
||||
getSavedObjectsClient: {
|
||||
assign: 'savedObjectsClient',
|
||||
method() {
|
||||
return savedObjectsClient;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
server.route(createResolveImportConflictsRoute(prereqs, server));
|
||||
});
|
||||
|
||||
test('formats successful response', async () => {
|
||||
const request = {
|
||||
method: 'POST',
|
||||
url: '/api/saved_objects/_resolve_import_conflicts',
|
||||
payload: [
|
||||
'--BOUNDARY',
|
||||
'Content-Disposition: form-data; name="file"; filename="export.ndjson"',
|
||||
'Content-Type: application/ndjson',
|
||||
'',
|
||||
'',
|
||||
'--BOUNDARY--',
|
||||
].join('\r\n'),
|
||||
headers: {
|
||||
'content-Type': 'multipart/form-data; boundary=BOUNDARY',
|
||||
},
|
||||
};
|
||||
const { payload, statusCode } = await server.inject(request);
|
||||
const response = JSON.parse(payload);
|
||||
expect(statusCode).toBe(200);
|
||||
expect(response).toEqual({ success: true, successCount: 0 });
|
||||
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('resolves conflicts for an index pattern and dashboard but skips the index pattern', async () => {
|
||||
// NOTE: changes to this scenario should be reflected in the docs
|
||||
const request = {
|
||||
method: 'POST',
|
||||
url: '/api/saved_objects/_resolve_import_conflicts',
|
||||
payload: [
|
||||
'--EXAMPLE',
|
||||
'Content-Disposition: form-data; name="file"; filename="export.ndjson"',
|
||||
'Content-Type: application/ndjson',
|
||||
'',
|
||||
'{"type":"index-pattern","id":"my-pattern","attributes":{"title":"my-pattern-*"}}',
|
||||
'{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"}}',
|
||||
'--EXAMPLE',
|
||||
'Content-Disposition: form-data; name="skips"',
|
||||
'',
|
||||
'[{"type":"index-pattern","id":"my-pattern"}]',
|
||||
'--EXAMPLE',
|
||||
'Content-Disposition: form-data; name="overwrites"',
|
||||
'',
|
||||
'[{"type":"dashboard","id":"my-dashboard"}]',
|
||||
'--EXAMPLE--',
|
||||
].join('\r\n'),
|
||||
headers: {
|
||||
'content-Type': 'multipart/form-data; boundary=EXAMPLE',
|
||||
},
|
||||
};
|
||||
savedObjectsClient.bulkCreate.mockResolvedValueOnce({
|
||||
saved_objects: [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'my-dashboard',
|
||||
attributes: {
|
||||
title: 'Look at my dashboard',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const { payload, statusCode } = await server.inject(request);
|
||||
const response = JSON.parse(payload);
|
||||
expect(statusCode).toBe(200);
|
||||
expect(response).toEqual({ success: true, successCount: 1 });
|
||||
expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(`
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"title": "Look at my dashboard",
|
||||
},
|
||||
"id": "my-dashboard",
|
||||
"type": "dashboard",
|
||||
},
|
||||
],
|
||||
Object {
|
||||
"overwrite": true,
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('resolves conflicts by replacing the visualization references', async () => {
|
||||
// NOTE: changes to this scenario should be reflected in the docs
|
||||
const request = {
|
||||
method: 'POST',
|
||||
url: '/api/saved_objects/_resolve_import_conflicts',
|
||||
payload: [
|
||||
'--EXAMPLE',
|
||||
'Content-Disposition: form-data; name="file"; filename="export.ndjson"',
|
||||
'Content-Type: application/ndjson',
|
||||
'',
|
||||
'{"type":"visualization","id":"my-vis","attributes":{"title":"Look at my visualization"}}',
|
||||
'{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"},"references":[{"name":"panel_0","type":"visualization","id":"my-vis"}]}',
|
||||
'--EXAMPLE',
|
||||
'Content-Disposition: form-data; name="replaceReferences"',
|
||||
'',
|
||||
'[{"type":"visualization","from":"my-vis","to":"my-vis-2"}]',
|
||||
'--EXAMPLE--',
|
||||
].join('\r\n'),
|
||||
headers: {
|
||||
'content-Type': 'multipart/form-data; boundary=EXAMPLE',
|
||||
},
|
||||
};
|
||||
savedObjectsClient.bulkCreate.mockResolvedValueOnce({
|
||||
saved_objects: [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'my-dashboard',
|
||||
attributes: {
|
||||
title: 'Look at my dashboard',
|
||||
},
|
||||
references: [
|
||||
{
|
||||
name: 'panel_0',
|
||||
type: 'visualization',
|
||||
id: 'my-vis-2',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
const { payload, statusCode } = await server.inject(request);
|
||||
const response = JSON.parse(payload);
|
||||
expect(statusCode).toBe(200);
|
||||
expect(response).toEqual({ success: true, successCount: 1 });
|
||||
expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(`
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"title": "Look at my dashboard",
|
||||
},
|
||||
"id": "my-dashboard",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "my-vis-2",
|
||||
"name": "panel_0",
|
||||
"type": "visualization",
|
||||
},
|
||||
],
|
||||
"type": "dashboard",
|
||||
},
|
||||
],
|
||||
Object {
|
||||
"overwrite": true,
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import Hapi from 'hapi';
|
||||
import Joi from 'joi';
|
||||
import { extname } from 'path';
|
||||
import { Readable } from 'stream';
|
||||
import { SavedObjectsClient } from '../';
|
||||
import { resolveImportConflicts } from '../lib';
|
||||
import { Prerequisites } from './types';
|
||||
|
||||
interface HapiReadableStream extends Readable {
|
||||
hapi: {
|
||||
filename: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ImportRequest extends Hapi.Request {
|
||||
pre: {
|
||||
savedObjectsClient: SavedObjectsClient;
|
||||
};
|
||||
payload: {
|
||||
file: HapiReadableStream;
|
||||
overwrites: Array<{
|
||||
type: string;
|
||||
id: string;
|
||||
}>;
|
||||
replaceReferences: Array<{
|
||||
type: string;
|
||||
from: string;
|
||||
to: string;
|
||||
}>;
|
||||
skips: Array<{
|
||||
type: string;
|
||||
id: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
export const createResolveImportConflictsRoute = (prereqs: Prerequisites, server: Hapi.Server) => ({
|
||||
path: '/api/saved_objects/_resolve_import_conflicts',
|
||||
method: 'POST',
|
||||
config: {
|
||||
pre: [prereqs.getSavedObjectsClient],
|
||||
payload: {
|
||||
maxBytes: server.config().get('savedObjects.maxImportPayloadBytes'),
|
||||
output: 'stream',
|
||||
allow: 'multipart/form-data',
|
||||
},
|
||||
validate: {
|
||||
payload: Joi.object({
|
||||
file: Joi.object().required(),
|
||||
overwrites: Joi.array()
|
||||
.items(
|
||||
Joi.object({
|
||||
type: Joi.string().required(),
|
||||
id: Joi.string().required(),
|
||||
})
|
||||
)
|
||||
.default([]),
|
||||
replaceReferences: Joi.array()
|
||||
.items(
|
||||
Joi.object({
|
||||
type: Joi.string().required(),
|
||||
from: Joi.string().required(),
|
||||
to: Joi.string().required(),
|
||||
})
|
||||
)
|
||||
.default([]),
|
||||
skips: Joi.array()
|
||||
.items(
|
||||
Joi.object({
|
||||
type: Joi.string().required(),
|
||||
id: Joi.string().required(),
|
||||
})
|
||||
)
|
||||
.default([]),
|
||||
}).default(),
|
||||
},
|
||||
},
|
||||
async handler(request: ImportRequest) {
|
||||
const { savedObjectsClient } = request.pre;
|
||||
const { filename } = request.payload.file.hapi;
|
||||
const fileExtension = extname(filename).toLowerCase();
|
||||
if (fileExtension !== '.ndjson') {
|
||||
return Boom.badRequest(`Invalid file extension ${fileExtension}`);
|
||||
}
|
||||
return await resolveImportConflicts({
|
||||
savedObjectsClient,
|
||||
readStream: request.payload.file,
|
||||
objectLimit: request.server.config().get('savedObjects.maxImportExportSize'),
|
||||
skips: request.payload.skips,
|
||||
overwrites: request.payload.overwrites,
|
||||
replaceReferences: request.payload.replaceReferences,
|
||||
});
|
||||
},
|
||||
});
|
|
@ -32,6 +32,8 @@ import {
|
|||
createGetRoute,
|
||||
createUpdateRoute,
|
||||
createExportRoute,
|
||||
createImportRoute,
|
||||
createResolveImportConflictsRoute,
|
||||
} from './routes';
|
||||
|
||||
export function savedObjectsMixin(kbnServer, server) {
|
||||
|
@ -63,6 +65,8 @@ export function savedObjectsMixin(kbnServer, server) {
|
|||
server.route(createGetRoute(prereqs));
|
||||
server.route(createUpdateRoute(prereqs));
|
||||
server.route(createExportRoute(prereqs, server));
|
||||
server.route(createImportRoute(prereqs, server));
|
||||
server.route(createResolveImportConflictsRoute(prereqs, server));
|
||||
|
||||
const schema = new SavedObjectsSchema(kbnServer.uiExports.savedObjectSchemas);
|
||||
const serializer = new SavedObjectsSerializer(schema);
|
||||
|
|
|
@ -102,9 +102,9 @@ describe('Saved Objects Mixin', () => {
|
|||
});
|
||||
|
||||
describe('Routes', () => {
|
||||
it('should create 8 routes', () => {
|
||||
it('should create 10 routes', () => {
|
||||
savedObjectsMixin(mockKbnServer, mockServer);
|
||||
expect(mockServer.route).toHaveBeenCalledTimes(8);
|
||||
expect(mockServer.route).toHaveBeenCalledTimes(10);
|
||||
});
|
||||
it('should add POST /api/saved_objects/_bulk_create', () => {
|
||||
savedObjectsMixin(mockKbnServer, mockServer);
|
||||
|
@ -138,6 +138,15 @@ describe('Saved Objects Mixin', () => {
|
|||
savedObjectsMixin(mockKbnServer, mockServer);
|
||||
expect(mockServer.route).toHaveBeenCalledWith(expect.objectContaining({ path: '/api/saved_objects/_export', method: 'POST' }));
|
||||
});
|
||||
it('should add POST /api/saved_objects/_import', () => {
|
||||
savedObjectsMixin(mockKbnServer, mockServer);
|
||||
expect(mockServer.route).toHaveBeenCalledWith(expect.objectContaining({ path: '/api/saved_objects/_import', method: 'POST' }));
|
||||
});
|
||||
it('should add POST /api/saved_objects/_resolve_import_conflicts', () => {
|
||||
savedObjectsMixin(mockKbnServer, mockServer);
|
||||
expect(mockServer.route)
|
||||
.toHaveBeenCalledWith(expect.objectContaining({ path: '/api/saved_objects/_resolve_import_conflicts', method: 'POST' }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Saved object service', () => {
|
||||
|
|
|
@ -26,6 +26,7 @@ export interface BaseOptions {
|
|||
export interface CreateOptions extends BaseOptions {
|
||||
id?: string;
|
||||
overwrite?: boolean;
|
||||
migrationVersion?: MigrationVersion;
|
||||
references?: SavedObjectReference[];
|
||||
}
|
||||
|
||||
|
@ -89,6 +90,7 @@ export interface SavedObject<T extends SavedObjectAttributes = any> {
|
|||
updated_at?: string;
|
||||
error?: {
|
||||
message: string;
|
||||
statusCode: number;
|
||||
};
|
||||
attributes: T;
|
||||
references: SavedObjectReference[];
|
||||
|
|
|
@ -102,7 +102,9 @@ export class SavedObjectsClient {
|
|||
* @param {object} [options={}]
|
||||
* @property {string} [options.id] - force id on creation, not recommended
|
||||
* @property {boolean} [options.overwrite=false]
|
||||
* @property {object} [options.migrationVersion=undefined]
|
||||
* @property {string} [options.namespace]
|
||||
* @property {array} [options.references] - [{ name, type, id }]
|
||||
* @returns {promise} - { id, type, version, attributes }
|
||||
*/
|
||||
async create(type, attributes = {}, options = {}) {
|
||||
|
|
77
src/legacy/utils/streams/filter_stream.test.ts
Normal file
77
src/legacy/utils/streams/filter_stream.test.ts
Normal file
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
createConcatStream,
|
||||
createFilterStream,
|
||||
createListStream,
|
||||
createPromiseFromStreams,
|
||||
} from './';
|
||||
|
||||
describe('createFilterStream()', () => {
|
||||
test('calls the function with each item in the source stream', async () => {
|
||||
const filter = jest.fn().mockReturnValue(true);
|
||||
|
||||
await createPromiseFromStreams([createListStream(['a', 'b', 'c']), createFilterStream(filter)]);
|
||||
|
||||
expect(filter).toMatchInlineSnapshot(`
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
"a",
|
||||
],
|
||||
Array [
|
||||
"b",
|
||||
],
|
||||
Array [
|
||||
"c",
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": true,
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": true,
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": true,
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('send the filtered values on the output stream', async () => {
|
||||
const result = await createPromiseFromStreams([
|
||||
createListStream([1, 2, 3]),
|
||||
createFilterStream<number>(n => n % 2 === 0),
|
||||
createConcatStream([]),
|
||||
]);
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
2,
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
33
src/legacy/utils/streams/filter_stream.ts
Normal file
33
src/legacy/utils/streams/filter_stream.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { Transform } from 'stream';
|
||||
|
||||
export function createFilterStream<T>(fn: (obj: T) => boolean) {
|
||||
return new Transform({
|
||||
objectMode: true,
|
||||
async transform(obj, enc, done) {
|
||||
const canPushDownStream = fn(obj);
|
||||
if (canPushDownStream) {
|
||||
this.push(obj);
|
||||
}
|
||||
done();
|
||||
},
|
||||
});
|
||||
}
|
31
src/legacy/utils/streams/index.d.ts
vendored
Normal file
31
src/legacy/utils/streams/index.d.ts
vendored
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { Readable, Transform, Writable, TransformOptions } from 'stream';
|
||||
|
||||
export function concatStreamProviders(sourceProviders: Readable[], options: TransformOptions): Transform;
|
||||
export function createIntersperseStream(intersperseChunk: string | Buffer): Transform;
|
||||
export function createSplitStream<T>(splitChunk: T): Transform;
|
||||
export function createListStream(items: any[]): Readable;
|
||||
export function createReduceStream<T>(reducer: (value: any, chunk: T, enc: string) => T): Transform;
|
||||
export function createPromiseFromStreams<T>([first, ...rest]: [Readable, ...Writable[]]): Promise<T>;
|
||||
export function createConcatStream(initial: any): Transform;
|
||||
export function createMapStream<T>(fn: (value: T, i: number) => void): Transform;
|
||||
export function createReplaceStream(toReplace: string, replacement: string | Buffer): Transform;
|
||||
export function createFilterStream<T>(fn: (obj: T) => boolean): Transform;
|
|
@ -26,3 +26,4 @@ export { createPromiseFromStreams } from './promise_from_streams';
|
|||
export { createConcatStream } from './concat_stream';
|
||||
export { createMapStream } from './map_stream';
|
||||
export { createReplaceStream } from './replace_stream';
|
||||
export { createFilterStream } from './filter_stream';
|
||||
|
|
135
test/api_integration/apis/saved_objects/import.js
Normal file
135
test/api_integration/apis/saved_objects/import.js
Normal file
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import expect from 'expect.js';
|
||||
import { join } from 'path';
|
||||
|
||||
export default function ({ getService }) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('import', () => {
|
||||
describe('with kibana index', () => {
|
||||
describe('with basic data existing', () => {
|
||||
before(() => esArchiver.load('saved_objects/basic'));
|
||||
after(() => esArchiver.unload('saved_objects/basic'));
|
||||
|
||||
it('should return 200', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_import')
|
||||
.query({ overwrite: true })
|
||||
.attach('file', join(__dirname, '../../fixtures/import.ndjson'))
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
expect(resp.body).to.eql({
|
||||
success: true,
|
||||
successCount: 3,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 415 when no file passed in', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_import')
|
||||
.expect(415)
|
||||
.then((resp) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 415,
|
||||
error: 'Unsupported Media Type',
|
||||
message: 'Unsupported Media Type',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 409 when conflicts exist', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_import')
|
||||
.attach('file', join(__dirname, '../../fixtures/import.ndjson'))
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
expect(resp.body).to.eql({
|
||||
success: false,
|
||||
successCount: 0,
|
||||
errors: [
|
||||
{
|
||||
id: '91200a00-9efd-11e7-acb3-3dab96693fab',
|
||||
type: 'index-pattern',
|
||||
error: {
|
||||
statusCode: 409,
|
||||
message: 'version conflict, document already exists',
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
type: 'visualization',
|
||||
error: {
|
||||
statusCode: 409,
|
||||
message: 'version conflict, document already exists',
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
type: 'dashboard',
|
||||
error: {
|
||||
statusCode: 409,
|
||||
message: 'version conflict, document already exists',
|
||||
}
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 200 when conflicts exist but overwrite is passed in', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_import')
|
||||
.query({
|
||||
overwrite: true,
|
||||
})
|
||||
.attach('file', join(__dirname, '../../fixtures/import.ndjson'))
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
expect(resp.body).to.eql({
|
||||
success: true,
|
||||
successCount: 3,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 when trying to import more than 10,000 objects', async () => {
|
||||
const fileChunks = [];
|
||||
for (let i = 0; i < 10001; i++) {
|
||||
fileChunks.push(`{"type":"visualization","id":"${i}","attributes":{},"references":[]}`);
|
||||
}
|
||||
await supertest
|
||||
.post('/api/saved_objects/_import')
|
||||
.attach('file', Buffer.from(fileChunks.join('\n'), 'utf8'), 'export.ndjson')
|
||||
.expect(400)
|
||||
.then((resp) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: 'Can\'t import more than 10000 objects',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -26,6 +26,8 @@ export default function ({ loadTestFile }) {
|
|||
loadTestFile(require.resolve('./export'));
|
||||
loadTestFile(require.resolve('./find'));
|
||||
loadTestFile(require.resolve('./get'));
|
||||
loadTestFile(require.resolve('./import'));
|
||||
loadTestFile(require.resolve('./resolve_import_conflicts'));
|
||||
loadTestFile(require.resolve('./update'));
|
||||
loadTestFile(require.resolve('./migrations'));
|
||||
});
|
||||
|
|
|
@ -0,0 +1,239 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import expect from 'expect.js';
|
||||
import { join } from 'path';
|
||||
|
||||
export default function ({ getService }) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('resolve_import_conflicts', () => {
|
||||
describe('without kibana index', () => {
|
||||
// Cleanup data that got created in import
|
||||
after(() => esArchiver.unload('saved_objects/basic'));
|
||||
|
||||
it('should return 200 and import nothing when empty parameters are passed in', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_resolve_import_conflicts')
|
||||
.attach('file', join(__dirname, '../../fixtures/import.ndjson'))
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
expect(resp.body).to.eql({
|
||||
success: true,
|
||||
successCount: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 200 and import everything when overwrite parameters contains all objects', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_resolve_import_conflicts')
|
||||
.field('overwrites', JSON.stringify([
|
||||
{
|
||||
type: 'index-pattern',
|
||||
id: '91200a00-9efd-11e7-acb3-3dab96693fab',
|
||||
},
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
},
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
},
|
||||
]))
|
||||
.attach('file', join(__dirname, '../../fixtures/import.ndjson'))
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
expect(resp.body).to.eql({
|
||||
success: true,
|
||||
successCount: 3,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 when no file passed in', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_resolve_import_conflicts')
|
||||
.field('skips', '[]')
|
||||
.expect(400)
|
||||
.then((resp) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: 'child "file" fails because ["file" is required]',
|
||||
validation: { source: 'payload', keys: [ 'file' ] }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 200 when replacing references', async () => {
|
||||
const objToInsert = {
|
||||
id: '1',
|
||||
type: 'visualization',
|
||||
attributes: {
|
||||
title: 'My favorite vis',
|
||||
},
|
||||
references: [
|
||||
{
|
||||
name: 'ref_0',
|
||||
type: 'search',
|
||||
id: '1',
|
||||
},
|
||||
]
|
||||
};
|
||||
await supertest
|
||||
.post('/api/saved_objects/_resolve_import_conflicts')
|
||||
.field('replaceReferences', JSON.stringify(
|
||||
[
|
||||
{
|
||||
type: 'search',
|
||||
from: '1',
|
||||
to: '2',
|
||||
}
|
||||
]
|
||||
))
|
||||
.attach('file', Buffer.from(JSON.stringify(objToInsert), 'utf8'), 'export.ndjson')
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
expect(resp.body).to.eql({
|
||||
success: true,
|
||||
successCount: 1,
|
||||
});
|
||||
});
|
||||
await supertest
|
||||
.get('/api/saved_objects/visualization/1')
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
expect(resp.body.references).to.eql([
|
||||
{
|
||||
name: 'ref_0',
|
||||
type: 'search',
|
||||
id: '2',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 when resolving conflicts with a file containing more than 10,000 objects', async () => {
|
||||
const fileChunks = [];
|
||||
for (let i = 0; i < 10001; i++) {
|
||||
fileChunks.push(`{"type":"visualization","id":"${i}","attributes":{},"references":[]}`);
|
||||
}
|
||||
await supertest
|
||||
.post('/api/saved_objects/_resolve_import_conflicts')
|
||||
.attach('file', Buffer.from(fileChunks.join('\n'), 'utf8'), 'export.ndjson')
|
||||
.expect(400)
|
||||
.then((resp) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: 'Can\'t import more than 10000 objects',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with kibana index', () => {
|
||||
describe('with basic data existing', () => {
|
||||
before(() => esArchiver.load('saved_objects/basic'));
|
||||
after(() => esArchiver.unload('saved_objects/basic'));
|
||||
|
||||
it('should return 200 when skipping all the records', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_resolve_import_conflicts')
|
||||
.field('skips', JSON.stringify(
|
||||
[
|
||||
{
|
||||
id: '91200a00-9efd-11e7-acb3-3dab96693fab',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
{
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
type: 'visualization',
|
||||
},
|
||||
{
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
type: 'dashboard',
|
||||
},
|
||||
]
|
||||
))
|
||||
.attach('file', join(__dirname, '../../fixtures/import.ndjson'))
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
expect(resp.body).to.eql({ success: true, successCount: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 200 when manually overwriting each object', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_resolve_import_conflicts')
|
||||
.field('overwrites', JSON.stringify(
|
||||
[
|
||||
{
|
||||
id: '91200a00-9efd-11e7-acb3-3dab96693fab',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
{
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
type: 'visualization',
|
||||
},
|
||||
{
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
type: 'dashboard',
|
||||
},
|
||||
]
|
||||
))
|
||||
.attach('file', join(__dirname, '../../fixtures/import.ndjson'))
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
expect(resp.body).to.eql({ success: true, successCount: 3 });
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 200 with only one record when overwriting 1 and skipping 1', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_resolve_import_conflicts')
|
||||
.field('overwrites', JSON.stringify(
|
||||
[
|
||||
{
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
type: 'visualization',
|
||||
},
|
||||
]
|
||||
))
|
||||
.field('skips', JSON.stringify(
|
||||
[
|
||||
{
|
||||
id: '91200a00-9efd-11e7-acb3-3dab96693fab',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
]
|
||||
))
|
||||
.attach('file', join(__dirname, '../../fixtures/import.ndjson'))
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
expect(resp.body).to.eql({ success: true, successCount: 1 });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
3
test/api_integration/fixtures/import.ndjson
Normal file
3
test/api_integration/fixtures/import.ndjson
Normal file
File diff suppressed because one or more lines are too long
152
x-pack/test/saved_object_api_integration/common/suites/import.ts
Normal file
152
x-pack/test/saved_object_api_integration/common/suites/import.ts
Normal file
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from 'expect.js';
|
||||
import { SuperTest } from 'supertest';
|
||||
import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants';
|
||||
import { getIdPrefix, getUrlPrefix } from '../lib/space_test_utils';
|
||||
import { DescribeFn, TestDefinitionAuthentication } from '../lib/types';
|
||||
|
||||
interface ImportTest {
|
||||
statusCode: number;
|
||||
response: (resp: { [key: string]: any }) => void;
|
||||
}
|
||||
|
||||
interface ImportTests {
|
||||
default: ImportTest;
|
||||
unknownType: ImportTest;
|
||||
}
|
||||
|
||||
interface ImportTestDefinition {
|
||||
user?: TestDefinitionAuthentication;
|
||||
spaceId?: string;
|
||||
tests: ImportTests;
|
||||
}
|
||||
|
||||
const createImportData = (spaceId: string) => [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: `${getIdPrefix(spaceId)}a01b2f57-fcfd-4864-b735-09e28f0d815e`,
|
||||
attributes: {
|
||||
title: 'A great new dashboard',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'globaltype',
|
||||
id: '05976c65-1145-4858-bbf0-d225cc78a06e',
|
||||
attributes: {
|
||||
name: 'A new globaltype object',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function importTestSuiteFactory(es: any, esArchiver: any, supertest: SuperTest<any>) {
|
||||
const createExpectResults = (spaceId = DEFAULT_SPACE_ID) => async (resp: {
|
||||
[key: string]: any;
|
||||
}) => {
|
||||
expect(resp.body).to.eql({
|
||||
success: true,
|
||||
successCount: 2,
|
||||
});
|
||||
};
|
||||
|
||||
const expectUnknownType = (resp: { [key: string]: any }) => {
|
||||
expect(resp.body).to.eql({
|
||||
success: false,
|
||||
successCount: 2,
|
||||
errors: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'wigwags',
|
||||
error: {
|
||||
message: `Unsupported saved object type: 'wigwags': Bad Request`,
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const expectRbacForbidden = (resp: { [key: string]: any }) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 403,
|
||||
error: 'Forbidden',
|
||||
message: `Unable to bulk_create dashboard,globaltype, missing action:saved_objects/dashboard/bulk_create,action:saved_objects/globaltype/bulk_create`,
|
||||
});
|
||||
};
|
||||
|
||||
const expectRbacForbiddenWithUnknownType = (resp: { [key: string]: any }) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 403,
|
||||
error: 'Forbidden',
|
||||
message: `Unable to bulk_create dashboard,globaltype,wigwags, missing action:saved_objects/dashboard/bulk_create,action:saved_objects/globaltype/bulk_create,action:saved_objects/wigwags/bulk_create`,
|
||||
});
|
||||
};
|
||||
|
||||
const expectRbacForbiddenForUnknownType = (resp: { [key: string]: any }) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 403,
|
||||
error: 'Forbidden',
|
||||
message: `Unable to bulk_create dashboard,globaltype,wigwags, missing action:saved_objects/wigwags/bulk_create`,
|
||||
});
|
||||
};
|
||||
|
||||
const makeImportTest = (describeFn: DescribeFn) => (
|
||||
description: string,
|
||||
definition: ImportTestDefinition
|
||||
) => {
|
||||
const { user = {}, spaceId = DEFAULT_SPACE_ID, tests } = definition;
|
||||
|
||||
describeFn(description, () => {
|
||||
before(() => esArchiver.load('saved_objects/spaces'));
|
||||
after(() => esArchiver.unload('saved_objects/spaces'));
|
||||
|
||||
it(`should return ${tests.default.statusCode}`, async () => {
|
||||
const data = createImportData(spaceId);
|
||||
await supertest
|
||||
.post(`${getUrlPrefix(spaceId)}/api/saved_objects/_import`)
|
||||
.auth(user.username, user.password)
|
||||
.attach('file', Buffer.from(JSON.stringify(data), 'utf8'), 'export.ndjson')
|
||||
.expect(tests.default.statusCode)
|
||||
.then(tests.default.response);
|
||||
});
|
||||
|
||||
describe('unknown type', () => {
|
||||
it(`should return ${tests.unknownType.statusCode}`, async () => {
|
||||
const data = createImportData(spaceId);
|
||||
data.push({
|
||||
type: 'wigwags',
|
||||
id: '1',
|
||||
attributes: {
|
||||
title: 'Wigwags title',
|
||||
},
|
||||
});
|
||||
await supertest
|
||||
.post(`${getUrlPrefix(spaceId)}/api/saved_objects/_import`)
|
||||
.query({ overwrite: true })
|
||||
.auth(user.username, user.password)
|
||||
.attach('file', Buffer.from(JSON.stringify(data), 'utf8'), 'export.ndjson')
|
||||
.expect(tests.unknownType.statusCode)
|
||||
.then(tests.unknownType.response);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const importTest = makeImportTest(describe);
|
||||
// @ts-ignore
|
||||
importTest.only = makeImportTest(describe.only);
|
||||
|
||||
return {
|
||||
importTest,
|
||||
createExpectResults,
|
||||
expectRbacForbidden,
|
||||
expectUnknownType,
|
||||
expectRbacForbiddenWithUnknownType,
|
||||
expectRbacForbiddenForUnknownType,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from 'expect.js';
|
||||
import { SuperTest } from 'supertest';
|
||||
import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants';
|
||||
import { getIdPrefix, getUrlPrefix } from '../lib/space_test_utils';
|
||||
import { DescribeFn, TestDefinitionAuthentication } from '../lib/types';
|
||||
|
||||
interface ResolveImportConflictsTest {
|
||||
statusCode: number;
|
||||
response: (resp: { [key: string]: any }) => void;
|
||||
}
|
||||
|
||||
interface ResolveImportConflictsTests {
|
||||
default: ResolveImportConflictsTest;
|
||||
unknownType: ResolveImportConflictsTest;
|
||||
}
|
||||
|
||||
interface ResolveImportConflictsTestDefinition {
|
||||
user?: TestDefinitionAuthentication;
|
||||
spaceId?: string;
|
||||
tests: ResolveImportConflictsTests;
|
||||
}
|
||||
|
||||
const createImportData = (spaceId: string) => [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: `${getIdPrefix(spaceId)}a01b2f57-fcfd-4864-b735-09e28f0d815e`,
|
||||
attributes: {
|
||||
title: 'A great new dashboard',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'globaltype',
|
||||
id: '05976c65-1145-4858-bbf0-d225cc78a06e',
|
||||
attributes: {
|
||||
name: 'A new globaltype object',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function resolveImportConflictsTestSuiteFactory(
|
||||
es: any,
|
||||
esArchiver: any,
|
||||
supertest: SuperTest<any>
|
||||
) {
|
||||
const createExpectResults = (spaceId = DEFAULT_SPACE_ID) => async (resp: {
|
||||
[key: string]: any;
|
||||
}) => {
|
||||
expect(resp.body).to.eql({
|
||||
success: true,
|
||||
successCount: 1,
|
||||
});
|
||||
};
|
||||
|
||||
const expectUnknownType = (resp: { [key: string]: any }) => {
|
||||
expect(resp.body).to.eql({
|
||||
success: false,
|
||||
successCount: 1,
|
||||
errors: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'wigwags',
|
||||
error: {
|
||||
message: `Unsupported saved object type: 'wigwags': Bad Request`,
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const expectRbacForbidden = (resp: { [key: string]: any }) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 403,
|
||||
error: 'Forbidden',
|
||||
message: `Unable to bulk_create dashboard, missing action:saved_objects/dashboard/bulk_create`,
|
||||
});
|
||||
};
|
||||
|
||||
const expectRbacForbiddenWithUnknownType = (resp: { [key: string]: any }) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 403,
|
||||
error: 'Forbidden',
|
||||
message: `Unable to bulk_create dashboard,wigwags, missing action:saved_objects/dashboard/bulk_create,action:saved_objects/wigwags/bulk_create`,
|
||||
});
|
||||
};
|
||||
|
||||
const expectRbacForbiddenForUnknownType = (resp: { [key: string]: any }) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 403,
|
||||
error: 'Forbidden',
|
||||
message: `Unable to bulk_create dashboard,wigwags, missing action:saved_objects/wigwags/bulk_create`,
|
||||
});
|
||||
};
|
||||
|
||||
const makeResolveImportConflictsTest = (describeFn: DescribeFn) => (
|
||||
description: string,
|
||||
definition: ResolveImportConflictsTestDefinition
|
||||
) => {
|
||||
const { user = {}, spaceId = DEFAULT_SPACE_ID, tests } = definition;
|
||||
|
||||
describeFn(description, () => {
|
||||
before(() => esArchiver.load('saved_objects/spaces'));
|
||||
after(() => esArchiver.unload('saved_objects/spaces'));
|
||||
|
||||
it(`should return ${tests.default.statusCode}`, async () => {
|
||||
const data = createImportData(spaceId);
|
||||
await supertest
|
||||
.post(`${getUrlPrefix(spaceId)}/api/saved_objects/_resolve_import_conflicts`)
|
||||
.auth(user.username, user.password)
|
||||
.field(
|
||||
'overwrites',
|
||||
JSON.stringify([
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: `${getIdPrefix(spaceId)}a01b2f57-fcfd-4864-b735-09e28f0d815e`,
|
||||
},
|
||||
])
|
||||
)
|
||||
.attach(
|
||||
'file',
|
||||
Buffer.from(data.map(obj => JSON.stringify(obj)).join('\n'), 'utf8'),
|
||||
'export.ndjson'
|
||||
)
|
||||
.expect(tests.default.statusCode)
|
||||
.then(tests.default.response);
|
||||
});
|
||||
|
||||
describe('unknown type', () => {
|
||||
it(`should return ${tests.unknownType.statusCode}`, async () => {
|
||||
const data = createImportData(spaceId);
|
||||
data.push({
|
||||
type: 'wigwags',
|
||||
id: '1',
|
||||
attributes: {
|
||||
title: 'Wigwags title',
|
||||
},
|
||||
});
|
||||
await supertest
|
||||
.post(`${getUrlPrefix(spaceId)}/api/saved_objects/_resolve_import_conflicts`)
|
||||
.auth(user.username, user.password)
|
||||
.field(
|
||||
'overwrites',
|
||||
JSON.stringify([
|
||||
{
|
||||
type: 'wigwags',
|
||||
id: '1',
|
||||
},
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: `${getIdPrefix(spaceId)}a01b2f57-fcfd-4864-b735-09e28f0d815e`,
|
||||
},
|
||||
])
|
||||
)
|
||||
.attach(
|
||||
'file',
|
||||
Buffer.from(data.map(obj => JSON.stringify(obj)).join('\n'), 'utf8'),
|
||||
'export.ndjson'
|
||||
)
|
||||
.expect(tests.unknownType.statusCode)
|
||||
.then(tests.unknownType.response);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const resolveImportConflictsTest = makeResolveImportConflictsTest(describe);
|
||||
// @ts-ignore
|
||||
resolveImportConflictsTest.only = makeResolveImportConflictsTest(describe.only);
|
||||
|
||||
return {
|
||||
resolveImportConflictsTest,
|
||||
createExpectResults,
|
||||
expectRbacForbidden,
|
||||
expectUnknownType,
|
||||
expectRbacForbiddenWithUnknownType,
|
||||
expectRbacForbiddenForUnknownType,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,211 @@
|
|||
/*
|
||||
* 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 { importTestSuiteFactory } from '../../common/suites/import';
|
||||
|
||||
// tslint:disable:no-default-export
|
||||
export default function({ getService }: TestInvoker) {
|
||||
const supertest = getService('supertestWithoutAuth');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const es = getService('es');
|
||||
|
||||
const {
|
||||
importTest,
|
||||
createExpectResults,
|
||||
expectRbacForbidden,
|
||||
expectUnknownType,
|
||||
expectRbacForbiddenWithUnknownType,
|
||||
expectRbacForbiddenForUnknownType,
|
||||
} = importTestSuiteFactory(es, esArchiver, supertest);
|
||||
|
||||
describe('_import', () => {
|
||||
[
|
||||
{
|
||||
spaceId: SPACES.DEFAULT.spaceId,
|
||||
users: {
|
||||
noAccess: AUTHENTICATION.NOT_A_KIBANA_USER,
|
||||
superuser: AUTHENTICATION.SUPERUSER,
|
||||
legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER,
|
||||
allGlobally: AUTHENTICATION.KIBANA_RBAC_USER,
|
||||
readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER,
|
||||
dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER,
|
||||
dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER,
|
||||
allAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER,
|
||||
readAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER,
|
||||
allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER,
|
||||
},
|
||||
},
|
||||
{
|
||||
spaceId: SPACES.SPACE_1.spaceId,
|
||||
users: {
|
||||
noAccess: AUTHENTICATION.NOT_A_KIBANA_USER,
|
||||
superuser: AUTHENTICATION.SUPERUSER,
|
||||
legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER,
|
||||
allGlobally: AUTHENTICATION.KIBANA_RBAC_USER,
|
||||
readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER,
|
||||
dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER,
|
||||
dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER,
|
||||
allAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER,
|
||||
readAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER,
|
||||
allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER,
|
||||
},
|
||||
},
|
||||
].forEach(scenario => {
|
||||
importTest(`user with no access within the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.noAccess,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
importTest(`superuser within the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.superuser,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 200,
|
||||
response: createExpectResults(scenario.spaceId),
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 200,
|
||||
response: expectUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
importTest(`legacy user within the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.legacyAll,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
importTest(`dual-privileges user within the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.dualAll,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 200,
|
||||
response: createExpectResults(scenario.spaceId),
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenForUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
importTest(`dual-privileges readonly user within the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.dualRead,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
importTest(`rbac user with all globally within the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.allGlobally,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 200,
|
||||
response: createExpectResults(scenario.spaceId),
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenForUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
importTest(`rbac user with read globally within the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.readGlobally,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
importTest(`rbac user with all at the space within the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.allAtSpace,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 200,
|
||||
response: createExpectResults(scenario.spaceId),
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenForUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
importTest(`rbac user with read at the space within the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.readAtSpace,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
importTest(`rbac user with all at other space within the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.allAtOtherSpace,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -26,6 +26,8 @@ export default function({ getService, loadTestFile }: TestInvoker) {
|
|||
loadTestFile(require.resolve('./export'));
|
||||
loadTestFile(require.resolve('./find'));
|
||||
loadTestFile(require.resolve('./get'));
|
||||
loadTestFile(require.resolve('./import'));
|
||||
loadTestFile(require.resolve('./resolve_import_conflicts'));
|
||||
loadTestFile(require.resolve('./update'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,229 @@
|
|||
/*
|
||||
* 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 { resolveImportConflictsTestSuiteFactory } from '../../common/suites/resolve_import_conflicts';
|
||||
|
||||
// tslint:disable:no-default-export
|
||||
export default function({ getService }: TestInvoker) {
|
||||
const supertest = getService('supertestWithoutAuth');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const es = getService('es');
|
||||
|
||||
const {
|
||||
resolveImportConflictsTest,
|
||||
createExpectResults,
|
||||
expectRbacForbidden,
|
||||
expectUnknownType,
|
||||
expectRbacForbiddenWithUnknownType,
|
||||
expectRbacForbiddenForUnknownType,
|
||||
} = resolveImportConflictsTestSuiteFactory(es, esArchiver, supertest);
|
||||
|
||||
describe('_resolve_import_conflicts', () => {
|
||||
[
|
||||
{
|
||||
spaceId: SPACES.DEFAULT.spaceId,
|
||||
users: {
|
||||
noAccess: AUTHENTICATION.NOT_A_KIBANA_USER,
|
||||
superuser: AUTHENTICATION.SUPERUSER,
|
||||
legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER,
|
||||
allGlobally: AUTHENTICATION.KIBANA_RBAC_USER,
|
||||
readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER,
|
||||
dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER,
|
||||
dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER,
|
||||
allAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER,
|
||||
readAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER,
|
||||
allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER,
|
||||
},
|
||||
},
|
||||
{
|
||||
spaceId: SPACES.SPACE_1.spaceId,
|
||||
users: {
|
||||
noAccess: AUTHENTICATION.NOT_A_KIBANA_USER,
|
||||
superuser: AUTHENTICATION.SUPERUSER,
|
||||
legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER,
|
||||
allGlobally: AUTHENTICATION.KIBANA_RBAC_USER,
|
||||
readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER,
|
||||
dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER,
|
||||
dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER,
|
||||
allAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER,
|
||||
readAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER,
|
||||
allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER,
|
||||
},
|
||||
},
|
||||
].forEach(scenario => {
|
||||
resolveImportConflictsTest(`user with no access within the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.noAccess,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
resolveImportConflictsTest(`superuser within the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.superuser,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 200,
|
||||
response: createExpectResults(scenario.spaceId),
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 200,
|
||||
response: expectUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
resolveImportConflictsTest(`legacy user within the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.legacyAll,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
resolveImportConflictsTest(`dual-privileges user within the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.dualAll,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 200,
|
||||
response: createExpectResults(scenario.spaceId),
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenForUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
resolveImportConflictsTest(
|
||||
`dual-privileges readonly user within the ${scenario.spaceId} space`,
|
||||
{
|
||||
user: scenario.users.dualRead,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
resolveImportConflictsTest(
|
||||
`rbac user with all globally within the ${scenario.spaceId} space`,
|
||||
{
|
||||
user: scenario.users.allGlobally,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 200,
|
||||
response: createExpectResults(scenario.spaceId),
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenForUnknownType,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
resolveImportConflictsTest(
|
||||
`rbac user with read globally within the ${scenario.spaceId} space`,
|
||||
{
|
||||
user: scenario.users.readGlobally,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
resolveImportConflictsTest(
|
||||
`rbac user with all at the space within the ${scenario.spaceId} space`,
|
||||
{
|
||||
user: scenario.users.allAtSpace,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 200,
|
||||
response: createExpectResults(scenario.spaceId),
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenForUnknownType,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
resolveImportConflictsTest(
|
||||
`rbac user with read at the space within the ${scenario.spaceId} space`,
|
||||
{
|
||||
user: scenario.users.readAtSpace,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
resolveImportConflictsTest(
|
||||
`rbac user with all at other space within the ${scenario.spaceId} space`,
|
||||
{
|
||||
user: scenario.users.allAtOtherSpace,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* 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 { TestInvoker } from '../../common/lib/types';
|
||||
import { importTestSuiteFactory } from '../../common/suites/import';
|
||||
|
||||
// tslint:disable:no-default-export
|
||||
export default function({ getService }: TestInvoker) {
|
||||
const supertest = getService('supertestWithoutAuth');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const es = getService('es');
|
||||
|
||||
const {
|
||||
importTest,
|
||||
createExpectResults,
|
||||
expectRbacForbidden,
|
||||
expectUnknownType,
|
||||
expectRbacForbiddenWithUnknownType,
|
||||
expectRbacForbiddenForUnknownType,
|
||||
} = importTestSuiteFactory(es, esArchiver, supertest);
|
||||
|
||||
describe('_import', () => {
|
||||
importTest(`user with no access`, {
|
||||
user: AUTHENTICATION.NOT_A_KIBANA_USER,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
importTest(`superuser`, {
|
||||
user: AUTHENTICATION.SUPERUSER,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 200,
|
||||
response: createExpectResults(),
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 200,
|
||||
response: expectUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
importTest(`legacy user`, {
|
||||
user: AUTHENTICATION.KIBANA_LEGACY_USER,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
importTest(`dual-privileges user`, {
|
||||
user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 200,
|
||||
response: createExpectResults(),
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenForUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
importTest(`dual-privileges readonly user`, {
|
||||
user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
importTest(`rbac user with all globally`, {
|
||||
user: AUTHENTICATION.KIBANA_RBAC_USER,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 200,
|
||||
response: createExpectResults(),
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenForUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
importTest(`rbac readonly user`, {
|
||||
user: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
importTest(`rbac user with all at default space`, {
|
||||
user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
importTest(`rbac user with read at default space`, {
|
||||
user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
importTest(`rbac user with all at space_1`, {
|
||||
user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
importTest(`rbac user with read at space_1`, {
|
||||
user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
|
@ -26,6 +26,8 @@ export default function({ getService, loadTestFile }: TestInvoker) {
|
|||
loadTestFile(require.resolve('./export'));
|
||||
loadTestFile(require.resolve('./find'));
|
||||
loadTestFile(require.resolve('./get'));
|
||||
loadTestFile(require.resolve('./import'));
|
||||
loadTestFile(require.resolve('./resolve_import_conflicts'));
|
||||
loadTestFile(require.resolve('./update'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* 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 { TestInvoker } from '../../common/lib/types';
|
||||
import { resolveImportConflictsTestSuiteFactory } from '../../common/suites/resolve_import_conflicts';
|
||||
|
||||
// tslint:disable:no-default-export
|
||||
export default function({ getService }: TestInvoker) {
|
||||
const supertest = getService('supertestWithoutAuth');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const es = getService('es');
|
||||
|
||||
const {
|
||||
resolveImportConflictsTest,
|
||||
createExpectResults,
|
||||
expectRbacForbidden,
|
||||
expectUnknownType,
|
||||
expectRbacForbiddenWithUnknownType,
|
||||
expectRbacForbiddenForUnknownType,
|
||||
} = resolveImportConflictsTestSuiteFactory(es, esArchiver, supertest);
|
||||
|
||||
describe('_resolve_import_conflicts', () => {
|
||||
resolveImportConflictsTest(`user with no access`, {
|
||||
user: AUTHENTICATION.NOT_A_KIBANA_USER,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
resolveImportConflictsTest(`superuser`, {
|
||||
user: AUTHENTICATION.SUPERUSER,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 200,
|
||||
response: createExpectResults(),
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 200,
|
||||
response: expectUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
resolveImportConflictsTest(`legacy user`, {
|
||||
user: AUTHENTICATION.KIBANA_LEGACY_USER,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
resolveImportConflictsTest(`dual-privileges user`, {
|
||||
user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 200,
|
||||
response: createExpectResults(),
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenForUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
resolveImportConflictsTest(`dual-privileges readonly user`, {
|
||||
user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
resolveImportConflictsTest(`rbac user with all globally`, {
|
||||
user: AUTHENTICATION.KIBANA_RBAC_USER,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 200,
|
||||
response: createExpectResults(),
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenForUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
resolveImportConflictsTest(`rbac readonly user`, {
|
||||
user: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
resolveImportConflictsTest(`rbac user with all at default space`, {
|
||||
user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
resolveImportConflictsTest(`rbac user with read at default space`, {
|
||||
user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
resolveImportConflictsTest(`rbac user with all at space_1`, {
|
||||
user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
resolveImportConflictsTest(`rbac user with read at space_1`, {
|
||||
user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 { SPACES } from '../../common/lib/spaces';
|
||||
import { TestInvoker } from '../../common/lib/types';
|
||||
import { importTestSuiteFactory } from '../../common/suites/import';
|
||||
|
||||
// tslint:disable:no-default-export
|
||||
export default function({ getService }: TestInvoker) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const es = getService('es');
|
||||
|
||||
const { importTest, createExpectResults, expectUnknownType } = importTestSuiteFactory(
|
||||
es,
|
||||
esArchiver,
|
||||
supertest
|
||||
);
|
||||
|
||||
describe('_import', () => {
|
||||
importTest('in the current space (space_1)', {
|
||||
...SPACES.SPACE_1,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 200,
|
||||
response: createExpectResults(SPACES.SPACE_1.spaceId),
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 200,
|
||||
response: expectUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
importTest('in the default space', {
|
||||
...SPACES.DEFAULT,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 200,
|
||||
response: createExpectResults(SPACES.DEFAULT.spaceId),
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 200,
|
||||
response: expectUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
|
@ -18,6 +18,8 @@ export default function({ loadTestFile }: TestInvoker) {
|
|||
loadTestFile(require.resolve('./export'));
|
||||
loadTestFile(require.resolve('./find'));
|
||||
loadTestFile(require.resolve('./get'));
|
||||
loadTestFile(require.resolve('./import'));
|
||||
loadTestFile(require.resolve('./resolve_import_conflicts'));
|
||||
loadTestFile(require.resolve('./update'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 { SPACES } from '../../common/lib/spaces';
|
||||
import { TestInvoker } from '../../common/lib/types';
|
||||
import { resolveImportConflictsTestSuiteFactory } from '../../common/suites/resolve_import_conflicts';
|
||||
|
||||
// tslint:disable:no-default-export
|
||||
export default function({ getService }: TestInvoker) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const es = getService('es');
|
||||
|
||||
const {
|
||||
resolveImportConflictsTest,
|
||||
createExpectResults,
|
||||
expectUnknownType,
|
||||
} = resolveImportConflictsTestSuiteFactory(es, esArchiver, supertest);
|
||||
|
||||
describe('_resolve_import_conflicts', () => {
|
||||
resolveImportConflictsTest('in the current space (space_1)', {
|
||||
...SPACES.SPACE_1,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 200,
|
||||
response: createExpectResults(SPACES.SPACE_1.spaceId),
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 200,
|
||||
response: expectUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
resolveImportConflictsTest('in the default space', {
|
||||
...SPACES.DEFAULT,
|
||||
tests: {
|
||||
default: {
|
||||
statusCode: 200,
|
||||
response: createExpectResults(SPACES.DEFAULT.spaceId),
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 200,
|
||||
response: expectUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue