mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
* Initial work for new server side export API * Revert UI changes, API only in this PR * Remove whitespace at top of export.asciidoc * Add tests around limitations * Add comment * Convert some files to typescript * Move Boom.boomify to where the errors are created * Use Boom.badRequest for now * Fix lint issue * Move files * Update tests * Add functional test * Export all documents by default * Update test assertions * Use ~10000 saved objects in export api integration test * Convert route to typescript, add content-type response header * Move some tests to api_integration * Use new sort and rename functions/variables * Move tests to API integration * Cleanup and finalize api integration tests * Make type or objects required but not both in the same call * Add spaces / security tests * Add noTypeOrObjects to security / spaces tests * Use json-stable-stringify and add tests for export ordering * Address self feedback, add without kibana index test * Only allow export API to export index-pattern, dashboard, visualization and search type objects * Make import export size configurable and fix broken tests * Fix broken tests * Move test config to mock server * Add more typescript types instead of using any * Convert request from GET to POST * Fix saved objects mixin test * Update src/legacy/server/saved_objects/lib/export.ts Co-Authored-By: mikecote <mikecote@users.noreply.github.com> * Apply PR feedback * Fix lint error * Update test snapshots due to jest upgrade * Add error handling for bulkGet * Split export API into two endpoints * Update src/legacy/server/saved_objects/routes/export_by_type.test.ts Co-Authored-By: mikecote <mikecote@users.noreply.github.com> * Update docs/api/saved-objects/export_by_type.asciidoc Co-Authored-By: mikecote <mikecote@users.noreply.github.com> * Update docs/api/saved-objects/export_by_type.asciidoc Co-Authored-By: mikecote <mikecote@users.noreply.github.com> * Update src/legacy/server/saved_objects/routes/export_objects.test.ts Co-Authored-By: mikecote <mikecote@users.noreply.github.com> * Apply PR feedback * MockServer -> createMockServer * Revert back to single API * Re-apply PR feedback
This commit is contained in:
parent
01a0ea6731
commit
d54690e064
25 changed files with 2218 additions and 25 deletions
|
@ -18,6 +18,7 @@ NOTE: You cannot access these endpoints via the Console in Kibana.
|
|||
* <<saved-objects-api-bulk-create>>
|
||||
* <<saved-objects-api-update>>
|
||||
* <<saved-objects-api-delete>>
|
||||
* <<saved-objects-api-export>>
|
||||
|
||||
include::saved-objects/get.asciidoc[]
|
||||
include::saved-objects/bulk_get.asciidoc[]
|
||||
|
@ -26,3 +27,4 @@ include::saved-objects/create.asciidoc[]
|
|||
include::saved-objects/bulk_create.asciidoc[]
|
||||
include::saved-objects/update.asciidoc[]
|
||||
include::saved-objects/delete.asciidoc[]
|
||||
include::saved-objects/export.asciidoc[]
|
||||
|
|
39
docs/api/saved-objects/export.asciidoc
Normal file
39
docs/api/saved-objects/export.asciidoc
Normal file
|
@ -0,0 +1,39 @@
|
|||
[[saved-objects-api-export]]
|
||||
=== Export Objects
|
||||
|
||||
experimental[This functionality is *experimental* and may be changed or removed completely in a future release.]
|
||||
|
||||
The export saved objects API enables you to retrieve a set of saved objects that can later be imported into Kibana.
|
||||
|
||||
Note: You cannot access this endpoint via the Console in Kibana.
|
||||
|
||||
==== Request
|
||||
|
||||
`POST /api/saved_objects/_export`
|
||||
|
||||
==== Request Body
|
||||
`type` (optional)::
|
||||
(array|string) The saved object type(s) that the export should be limited to
|
||||
`objects` (optional)::
|
||||
(array) A list of objects to export
|
||||
|
||||
Note: At least `type` or `objects` must be passed in.
|
||||
|
||||
==== Response body
|
||||
|
||||
The response body will have a format of newline delimited JSON.
|
||||
|
||||
==== Examples
|
||||
|
||||
The following example exports all index pattern saved objects.
|
||||
|
||||
[source,js]
|
||||
--------------------------------------------------
|
||||
POST api/saved_objects/_export
|
||||
{
|
||||
"type": "index-patterns"
|
||||
}
|
||||
--------------------------------------------------
|
||||
// KIBANA
|
||||
|
||||
A successful call returns a response code of `200` along with the exported objects as the response body.
|
|
@ -108,6 +108,7 @@
|
|||
"@kbn/pm": "1.0.0",
|
||||
"@kbn/test-subj-selector": "0.2.1",
|
||||
"@kbn/ui-framework": "1.0.0",
|
||||
"@types/json-stable-stringify": "^1.0.32",
|
||||
"@types/lodash.clonedeep": "^4.5.4",
|
||||
"JSONStream": "1.1.1",
|
||||
"abortcontroller-polyfill": "^1.1.9",
|
||||
|
@ -161,6 +162,7 @@
|
|||
"joi": "^13.5.2",
|
||||
"jquery": "^3.3.1",
|
||||
"js-yaml": "3.4.1",
|
||||
"json-stable-stringify": "^1.0.1",
|
||||
"json-stringify-pretty-compact": "1.0.4",
|
||||
"json-stringify-safe": "5.0.1",
|
||||
"leaflet": "1.0.3",
|
||||
|
@ -364,7 +366,6 @@
|
|||
"jest-cli": "^24.1.0",
|
||||
"jest-raw-loader": "^1.0.1",
|
||||
"jimp": "0.2.28",
|
||||
"json-stable-stringify": "^1.0.1",
|
||||
"json5": "^1.0.1",
|
||||
"karma": "3.1.4",
|
||||
"karma-chrome-launcher": "2.1.1",
|
||||
|
|
|
@ -251,4 +251,8 @@ export default () => Joi.object({
|
|||
locale: Joi.string().default('en'),
|
||||
}).default(),
|
||||
|
||||
savedObjects: Joi.object({
|
||||
maxImportExportSize: Joi.number().default(10000),
|
||||
}).default(),
|
||||
|
||||
}).default();
|
||||
|
|
521
src/legacy/server/saved_objects/lib/export.test.ts
Normal file
521
src/legacy/server/saved_objects/lib/export.test.ts
Normal file
|
@ -0,0 +1,521 @@
|
|||
/*
|
||||
* 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 { getSortedObjectsForExport, sortObjects } from './export';
|
||||
|
||||
describe('getSortedObjectsForExport()', () => {
|
||||
const savedObjectsClient = {
|
||||
errors: {} as any,
|
||||
find: jest.fn(),
|
||||
bulkGet: jest.fn(),
|
||||
create: jest.fn(),
|
||||
bulkCreate: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
get: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
savedObjectsClient.find.mockReset();
|
||||
savedObjectsClient.bulkGet.mockReset();
|
||||
savedObjectsClient.create.mockReset();
|
||||
savedObjectsClient.bulkCreate.mockReset();
|
||||
savedObjectsClient.delete.mockReset();
|
||||
savedObjectsClient.get.mockReset();
|
||||
savedObjectsClient.update.mockReset();
|
||||
});
|
||||
|
||||
test('exports selected types and sorts them', async () => {
|
||||
savedObjectsClient.find.mockResolvedValueOnce({
|
||||
total: 2,
|
||||
saved_objects: [
|
||||
{
|
||||
id: '2',
|
||||
type: 'search',
|
||||
references: [
|
||||
{
|
||||
type: 'index-pattern',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
type: 'index-pattern',
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
const response = await getSortedObjectsForExport({
|
||||
savedObjectsClient,
|
||||
exportSizeLimit: 500,
|
||||
types: ['index-pattern', 'search'],
|
||||
});
|
||||
expect(response).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"id": "2",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(savedObjectsClient.find).toMatchInlineSnapshot(`
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
Object {
|
||||
"perPage": 500,
|
||||
"sortField": "_id",
|
||||
"sortOrder": "asc",
|
||||
"type": Array [
|
||||
"index-pattern",
|
||||
"search",
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('export selected types throws error when exceeding exportSizeLimit', async () => {
|
||||
savedObjectsClient.find.mockResolvedValueOnce({
|
||||
total: 2,
|
||||
saved_objects: [
|
||||
{
|
||||
id: '2',
|
||||
type: 'search',
|
||||
references: [
|
||||
{
|
||||
type: 'index-pattern',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
type: 'index-pattern',
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
await expect(
|
||||
getSortedObjectsForExport({
|
||||
savedObjectsClient,
|
||||
exportSizeLimit: 1,
|
||||
types: ['index-pattern', 'search'],
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"Can't export more than 1 objects"`);
|
||||
});
|
||||
|
||||
test('exports selected objects and sorts them', async () => {
|
||||
savedObjectsClient.bulkGet.mockResolvedValueOnce({
|
||||
saved_objects: [
|
||||
{
|
||||
id: '2',
|
||||
type: 'search',
|
||||
references: [
|
||||
{
|
||||
type: 'index-pattern',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
type: 'index-pattern',
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
const response = await getSortedObjectsForExport({
|
||||
exportSizeLimit: 10000,
|
||||
savedObjectsClient,
|
||||
types: ['index-pattern', 'search'],
|
||||
objects: [
|
||||
{
|
||||
type: 'index-pattern',
|
||||
id: '1',
|
||||
},
|
||||
{
|
||||
type: 'search',
|
||||
id: '2',
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(response).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"id": "2",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(`
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"id": "2",
|
||||
"type": "search",
|
||||
},
|
||||
],
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('export selected objects throws error when exceeding exportSizeLimit', async () => {
|
||||
const exportOpts = {
|
||||
exportSizeLimit: 1,
|
||||
savedObjectsClient,
|
||||
types: ['index-pattern', 'search'],
|
||||
objects: [
|
||||
{
|
||||
type: 'index-pattern',
|
||||
id: '1',
|
||||
},
|
||||
{
|
||||
type: 'search',
|
||||
id: '2',
|
||||
},
|
||||
],
|
||||
};
|
||||
await expect(getSortedObjectsForExport(exportOpts)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Can't export more than 1 objects"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sortObjects()', () => {
|
||||
test('should return on empty array', () => {
|
||||
expect(sortObjects([])).toEqual([]);
|
||||
});
|
||||
|
||||
test('should not change sorted array', () => {
|
||||
const docs = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'index-pattern',
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'search',
|
||||
attributes: {},
|
||||
references: [
|
||||
{
|
||||
name: 'ref1',
|
||||
type: 'index-pattern',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
expect(sortObjects(docs)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "2",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "ref1",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('should not mutate parameter', () => {
|
||||
const docs = [
|
||||
{
|
||||
id: '2',
|
||||
type: 'search',
|
||||
attributes: {},
|
||||
references: [
|
||||
{
|
||||
name: 'ref1',
|
||||
type: 'index-pattern',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
type: 'index-pattern',
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
];
|
||||
expect(sortObjects(docs)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "2",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "ref1",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(docs).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "2",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "ref1",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('should sort unordered array', () => {
|
||||
const docs = [
|
||||
{
|
||||
id: '5',
|
||||
type: 'dashboard',
|
||||
attributes: {},
|
||||
references: [
|
||||
{
|
||||
name: 'ref1',
|
||||
type: 'visualization',
|
||||
id: '3',
|
||||
},
|
||||
{
|
||||
name: 'ref2',
|
||||
type: 'visualization',
|
||||
id: '4',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
type: 'visualization',
|
||||
attributes: {},
|
||||
references: [
|
||||
{
|
||||
name: 'ref1',
|
||||
type: 'index-pattern',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'visualization',
|
||||
attributes: {},
|
||||
references: [
|
||||
{
|
||||
name: 'ref1',
|
||||
type: 'search',
|
||||
id: '2',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'search',
|
||||
attributes: {},
|
||||
references: [
|
||||
{
|
||||
name: 'ref1',
|
||||
type: 'index-pattern',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
type: 'index-pattern',
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
];
|
||||
expect(sortObjects(docs)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "2",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "ref1",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "3",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "2",
|
||||
"name": "ref1",
|
||||
"type": "search",
|
||||
},
|
||||
],
|
||||
"type": "visualization",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "4",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "ref1",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "visualization",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "5",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "3",
|
||||
"name": "ref1",
|
||||
"type": "visualization",
|
||||
},
|
||||
Object {
|
||||
"id": "4",
|
||||
"name": "ref2",
|
||||
"type": "visualization",
|
||||
},
|
||||
],
|
||||
"type": "dashboard",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('detects circular dependencies', () => {
|
||||
const docs = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'foo',
|
||||
attributes: {},
|
||||
references: [
|
||||
{
|
||||
name: 'ref1',
|
||||
type: 'foo',
|
||||
id: '2',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'foo',
|
||||
attributes: {},
|
||||
references: [
|
||||
{
|
||||
name: 'ref1',
|
||||
type: 'foo',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
expect(() => sortObjects(docs)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"circular reference: [foo:1] ref-> [foo:2] ref-> [foo:1]"`
|
||||
);
|
||||
});
|
||||
});
|
103
src/legacy/server/saved_objects/lib/export.ts
Normal file
103
src/legacy/server/saved_objects/lib/export.ts
Normal file
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* 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 { SavedObject, SavedObjectsClient } from '../service/saved_objects_client';
|
||||
|
||||
interface ObjectToExport {
|
||||
id: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface ExportObjectsOptions {
|
||||
types?: string[];
|
||||
objects?: ObjectToExport[];
|
||||
savedObjectsClient: SavedObjectsClient;
|
||||
exportSizeLimit: number;
|
||||
}
|
||||
|
||||
export async function getSortedObjectsForExport({
|
||||
types,
|
||||
objects,
|
||||
savedObjectsClient,
|
||||
exportSizeLimit,
|
||||
}: ExportObjectsOptions) {
|
||||
let objectsToExport: SavedObject[] = [];
|
||||
if (objects) {
|
||||
if (objects.length > exportSizeLimit) {
|
||||
throw Boom.badRequest(`Can't export more than ${exportSizeLimit} objects`);
|
||||
}
|
||||
({ saved_objects: objectsToExport } = await savedObjectsClient.bulkGet(objects));
|
||||
const erroredObjects = objectsToExport.filter(obj => !!obj.error);
|
||||
if (erroredObjects.length) {
|
||||
const err = Boom.badRequest();
|
||||
err.output.payload.attributes = {
|
||||
objects: erroredObjects,
|
||||
};
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
const findResponse = await savedObjectsClient.find({
|
||||
type: types,
|
||||
sortField: '_id',
|
||||
sortOrder: 'asc',
|
||||
perPage: exportSizeLimit,
|
||||
});
|
||||
if (findResponse.total > exportSizeLimit) {
|
||||
throw Boom.badRequest(`Can't export more than ${exportSizeLimit} objects`);
|
||||
}
|
||||
({ saved_objects: objectsToExport } = findResponse);
|
||||
}
|
||||
return sortObjects(objectsToExport);
|
||||
}
|
||||
|
||||
export function sortObjects(savedObjects: SavedObject[]) {
|
||||
const path = new Set();
|
||||
const sorted = new Set();
|
||||
const objectsByTypeId = new Map(
|
||||
savedObjects.map(object => [`${object.type}:${object.id}`, object] as [string, SavedObject])
|
||||
);
|
||||
|
||||
function includeObjects(objects: SavedObject[]) {
|
||||
for (const object of objects) {
|
||||
if (path.has(object)) {
|
||||
throw Boom.badRequest(
|
||||
`circular reference: ${[...path, object]
|
||||
.map(obj => `[${obj.type}:${obj.id}]`)
|
||||
.join(' ref-> ')}`
|
||||
);
|
||||
}
|
||||
|
||||
const refdObjects = object.references
|
||||
.map(ref => objectsByTypeId.get(`${ref.type}:${ref.id}`))
|
||||
.filter((ref): ref is SavedObject => !!ref);
|
||||
|
||||
if (refdObjects.length) {
|
||||
path.add(object);
|
||||
includeObjects(refdObjects);
|
||||
path.delete(object);
|
||||
}
|
||||
|
||||
sorted.add(object);
|
||||
}
|
||||
}
|
||||
|
||||
includeObjects(savedObjects);
|
||||
return [...sorted];
|
||||
}
|
22
src/legacy/server/saved_objects/routes/_mock_server.d.ts
vendored
Normal file
22
src/legacy/server/saved_objects/routes/_mock_server.d.ts
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
export function MockServer(config?: { [key: string]: any }): Hapi.Server;
|
|
@ -22,6 +22,7 @@ import { defaultValidationErrorHandler } from '../../../../core/server/http/http
|
|||
|
||||
const defaultConfig = {
|
||||
'kibana.index': '.kibana',
|
||||
'savedObjects.maxImportExportSize': 10000,
|
||||
};
|
||||
|
||||
export function createMockServer(config: { [key: string]: any } = defaultConfig) {
|
||||
|
|
118
src/legacy/server/saved_objects/routes/export.test.ts
Normal file
118
src/legacy/server/saved_objects/routes/export.test.ts
Normal file
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* 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 { createExportRoute } from './export';
|
||||
|
||||
describe('POST /api/saved_objects/_export', () => {
|
||||
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();
|
||||
const prereqs = {
|
||||
getSavedObjectsClient: {
|
||||
assign: 'savedObjectsClient',
|
||||
method() {
|
||||
return savedObjectsClient;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
server.route(createExportRoute(prereqs, server));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
savedObjectsClient.bulkCreate.mockReset();
|
||||
savedObjectsClient.bulkGet.mockReset();
|
||||
savedObjectsClient.create.mockReset();
|
||||
savedObjectsClient.delete.mockReset();
|
||||
savedObjectsClient.find.mockReset();
|
||||
savedObjectsClient.get.mockReset();
|
||||
savedObjectsClient.update.mockReset();
|
||||
});
|
||||
|
||||
test('formats successful response', async () => {
|
||||
const request = {
|
||||
method: 'POST',
|
||||
url: '/api/saved_objects/_export',
|
||||
payload: {
|
||||
type: 'index-pattern',
|
||||
},
|
||||
};
|
||||
savedObjectsClient.find.mockResolvedValueOnce({
|
||||
total: 1,
|
||||
saved_objects: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'index-pattern',
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { payload, statusCode, headers } = await server.inject(request);
|
||||
const objects = payload.split('\n').map(row => JSON.parse(row));
|
||||
|
||||
expect(statusCode).toBe(200);
|
||||
expect(headers).toHaveProperty('content-disposition', 'attachment; filename="export.ndjson"');
|
||||
expect(headers).toHaveProperty('content-type', 'application/ndjson');
|
||||
expect(objects).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(savedObjectsClient.find).toMatchInlineSnapshot(`
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
Object {
|
||||
"perPage": 10000,
|
||||
"sortField": "_id",
|
||||
"sortOrder": "asc",
|
||||
"type": Array [
|
||||
"index-pattern",
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
81
src/legacy/server/saved_objects/routes/export.ts
Normal file
81
src/legacy/server/saved_objects/routes/export.ts
Normal file
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* 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 Joi from 'joi';
|
||||
import stringify from 'json-stable-stringify';
|
||||
import { SavedObjectsClient } from '../';
|
||||
import { getSortedObjectsForExport } from '../lib/export';
|
||||
import { Prerequisites } from './types';
|
||||
|
||||
const ALLOWED_TYPES = ['index-pattern', 'search', 'visualization', 'dashboard'];
|
||||
|
||||
interface ExportRequest extends Hapi.Request {
|
||||
pre: {
|
||||
savedObjectsClient: SavedObjectsClient;
|
||||
};
|
||||
payload: {
|
||||
type?: string[];
|
||||
objects?: Array<{
|
||||
type: string;
|
||||
id: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
export const createExportRoute = (prereqs: Prerequisites, server: Hapi.Server) => ({
|
||||
path: '/api/saved_objects/_export',
|
||||
method: 'POST',
|
||||
config: {
|
||||
pre: [prereqs.getSavedObjectsClient],
|
||||
validate: {
|
||||
payload: Joi.object()
|
||||
.keys({
|
||||
type: Joi.array()
|
||||
.items(Joi.string().valid(ALLOWED_TYPES))
|
||||
.single()
|
||||
.optional(),
|
||||
objects: Joi.array()
|
||||
.items({
|
||||
type: Joi.string()
|
||||
.valid(ALLOWED_TYPES)
|
||||
.required(),
|
||||
id: Joi.string().required(),
|
||||
})
|
||||
.max(server.config().get('savedObjects.maxImportExportSize'))
|
||||
.optional(),
|
||||
})
|
||||
.xor('type', 'objects')
|
||||
.default(),
|
||||
},
|
||||
async handler(request: ExportRequest, h: Hapi.ResponseToolkit) {
|
||||
const { savedObjectsClient } = request.pre;
|
||||
const docsToExport = await getSortedObjectsForExport({
|
||||
savedObjectsClient,
|
||||
types: request.payload.type,
|
||||
objects: request.payload.objects,
|
||||
exportSizeLimit: server.config().get('savedObjects.maxImportExportSize'),
|
||||
});
|
||||
return h
|
||||
.response(docsToExport.map(doc => stringify(doc)).join('\n'))
|
||||
.header('Content-Disposition', `attachment; filename="export.ndjson"`)
|
||||
.header('Content-Type', 'application/ndjson');
|
||||
},
|
||||
},
|
||||
});
|
|
@ -24,3 +24,4 @@ export { createDeleteRoute } from './delete';
|
|||
export { createFindRoute } from './find';
|
||||
export { createGetRoute } from './get';
|
||||
export { createUpdateRoute } from './update';
|
||||
export { createExportRoute } from './export';
|
||||
|
|
|
@ -31,6 +31,7 @@ import {
|
|||
createFindRoute,
|
||||
createGetRoute,
|
||||
createUpdateRoute,
|
||||
createExportRoute,
|
||||
} from './routes';
|
||||
|
||||
export function savedObjectsMixin(kbnServer, server) {
|
||||
|
@ -61,6 +62,7 @@ export function savedObjectsMixin(kbnServer, server) {
|
|||
server.route(createFindRoute(prereqs));
|
||||
server.route(createGetRoute(prereqs));
|
||||
server.route(createUpdateRoute(prereqs));
|
||||
server.route(createExportRoute(prereqs, server));
|
||||
|
||||
const schema = new SavedObjectsSchema(kbnServer.uiExports.savedObjectSchemas);
|
||||
const serializer = new SavedObjectsSerializer(schema);
|
||||
|
|
|
@ -24,7 +24,13 @@ describe('Saved Objects Mixin', () => {
|
|||
let mockServer;
|
||||
const mockCallCluster = jest.fn();
|
||||
const stubCallCluster = jest.fn();
|
||||
const stubConfig = jest.fn();
|
||||
const config = {
|
||||
'kibana.index': 'kibana.index',
|
||||
'savedObjects.maxImportExportSize': 10000,
|
||||
};
|
||||
const stubConfig = jest.fn((key) => {
|
||||
return config[key];
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockServer = {
|
||||
|
@ -96,9 +102,9 @@ describe('Saved Objects Mixin', () => {
|
|||
});
|
||||
|
||||
describe('Routes', () => {
|
||||
it('should create 7 routes', () => {
|
||||
it('should create 8 routes', () => {
|
||||
savedObjectsMixin(mockKbnServer, mockServer);
|
||||
expect(mockServer.route).toHaveBeenCalledTimes(7);
|
||||
expect(mockServer.route).toHaveBeenCalledTimes(8);
|
||||
});
|
||||
it('should add POST /api/saved_objects/_bulk_create', () => {
|
||||
savedObjectsMixin(mockKbnServer, mockServer);
|
||||
|
@ -128,6 +134,10 @@ describe('Saved Objects Mixin', () => {
|
|||
savedObjectsMixin(mockKbnServer, mockServer);
|
||||
expect(mockServer.route).toHaveBeenCalledWith(expect.objectContaining({ path: '/api/saved_objects/{type}/{id}', method: 'PUT' }));
|
||||
});
|
||||
it('should add GET /api/saved_objects/_export', () => {
|
||||
savedObjectsMixin(mockKbnServer, mockServer);
|
||||
expect(mockServer.route).toHaveBeenCalledWith(expect.objectContaining({ path: '/api/saved_objects/_export', method: 'POST' }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Saved object service', () => {
|
||||
|
@ -211,7 +221,6 @@ describe('Saved Objects Mixin', () => {
|
|||
return [];
|
||||
}
|
||||
});
|
||||
stubConfig.mockImplementation(() => 'kibana-index');
|
||||
const client = await service.getScopedSavedObjectsClient();
|
||||
await client.create('testtype');
|
||||
expect(stubCallCluster).toHaveBeenCalled();
|
||||
|
|
|
@ -18,36 +18,44 @@
|
|||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
|
||||
import { getProperty } from '../../../../mappings';
|
||||
|
||||
const TOP_LEVEL_FIELDS = ['_id'];
|
||||
|
||||
export function getSortingParams(mappings, type, sortField, sortOrder) {
|
||||
if (!sortField) {
|
||||
return {};
|
||||
}
|
||||
|
||||
let typeField = type;
|
||||
const types = [].concat(type);
|
||||
|
||||
if (Array.isArray(type)) {
|
||||
if (type.length === 1) {
|
||||
typeField = type[0];
|
||||
} else {
|
||||
const rootField = getProperty(mappings, sortField);
|
||||
if (!rootField) {
|
||||
throw Boom.badRequest(`Unable to sort multiple types by field ${sortField}, not a root property`);
|
||||
}
|
||||
|
||||
return {
|
||||
sort: [{
|
||||
[sortField]: {
|
||||
order: sortOrder,
|
||||
unmapped_type: rootField.type
|
||||
}
|
||||
}]
|
||||
};
|
||||
}
|
||||
if (TOP_LEVEL_FIELDS.includes(sortField)) {
|
||||
return {
|
||||
sort: [{
|
||||
[sortField]: {
|
||||
order: sortOrder,
|
||||
},
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
if (types.length > 1) {
|
||||
const rootField = getProperty(mappings, sortField);
|
||||
if (!rootField) {
|
||||
throw Boom.badRequest(`Unable to sort multiple types by field ${sortField}, not a root property`);
|
||||
}
|
||||
|
||||
return {
|
||||
sort: [{
|
||||
[sortField]: {
|
||||
order: sortOrder,
|
||||
unmapped_type: rootField.type
|
||||
}
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
const [typeField] = types;
|
||||
const key = `${typeField}.${sortField}`;
|
||||
const field = getProperty(mappings, key);
|
||||
if (!field) {
|
||||
|
|
396
test/api_integration/apis/saved_objects/export.js
Normal file
396
test/api_integration/apis/saved_objects/export.js
Normal file
|
@ -0,0 +1,396 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
export default function ({ getService }) {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('es');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('export', () => {
|
||||
describe('with kibana index', () => {
|
||||
describe('basic amount of saved objects', () => {
|
||||
before(() => esArchiver.load('saved_objects/basic'));
|
||||
after(() => esArchiver.unload('saved_objects/basic'));
|
||||
|
||||
it('should return objects in dependency order', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_export')
|
||||
.send({
|
||||
type: ['index-pattern', 'search', 'visualization', 'dashboard'],
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
const objects = resp.text.split('\n').map(JSON.parse);
|
||||
expect(objects).to.have.length(3);
|
||||
expect(objects[0]).to.have.property('id', '91200a00-9efd-11e7-acb3-3dab96693fab');
|
||||
expect(objects[0]).to.have.property('type', 'index-pattern');
|
||||
expect(objects[1]).to.have.property('id', 'dd7caf20-9efd-11e7-acb3-3dab96693fab');
|
||||
expect(objects[1]).to.have.property('type', 'visualization');
|
||||
expect(objects[2]).to.have.property('id', 'be3733a0-9efe-11e7-acb3-3dab96693fab');
|
||||
expect(objects[2]).to.have.property('type', 'dashboard');
|
||||
});
|
||||
});
|
||||
|
||||
it('should validate types', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_export')
|
||||
.send({
|
||||
type: ['foo'],
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
// eslint-disable-next-line max-len
|
||||
message: 'child "type" fails because ["type" at position 0 fails because ["0" must be one of [index-pattern, search, visualization, dashboard]]]',
|
||||
validation: { source: 'payload', keys: [ 'type.0' ] },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should validate types in objects', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_export')
|
||||
.send({
|
||||
objects: [
|
||||
{
|
||||
type: 'foo',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
// eslint-disable-next-line max-len
|
||||
message: 'child "objects" fails because ["objects" at position 0 fails because [child "type" fails because ["type" must be one of [index-pattern, search, visualization, dashboard]]]]',
|
||||
validation: { source: 'payload', keys: [ 'objects.0.type' ] },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it(`should throw error when object doesn't exist`, async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_export')
|
||||
.send({
|
||||
objects: [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: 'Bad Request',
|
||||
attributes: {
|
||||
objects: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'dashboard',
|
||||
error: {
|
||||
statusCode: 404,
|
||||
message: 'Not found',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('10,000 objects', () => {
|
||||
before(() => esArchiver.load('saved_objects/10k'));
|
||||
after(() => esArchiver.unload('saved_objects/10k'));
|
||||
|
||||
it('should return 400 when exporting without type or objects passed in', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_export')
|
||||
.expect(400)
|
||||
.then((resp) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: '"value" must be an object',
|
||||
validation: { source: 'payload', keys: [ 'value' ] },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 200 when exporting by single type', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_export')
|
||||
.send({
|
||||
type: 'dashboard',
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
expect(resp.headers['content-disposition']).to.eql('attachment; filename="export.ndjson"');
|
||||
expect(resp.headers['content-type']).to.eql('application/ndjson');
|
||||
const objects = resp.text.split('\n').map(JSON.parse);
|
||||
expect(objects).to.eql([{
|
||||
attributes: {
|
||||
description: '',
|
||||
hits: 0,
|
||||
kibanaSavedObjectMeta: {
|
||||
searchSourceJSON: objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON,
|
||||
},
|
||||
optionsJSON: objects[0].attributes.optionsJSON,
|
||||
panelsJSON: objects[0].attributes.panelsJSON,
|
||||
refreshInterval: {
|
||||
display: 'Off',
|
||||
pause: false,
|
||||
value: 0,
|
||||
},
|
||||
timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700',
|
||||
timeRestore: true,
|
||||
timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700',
|
||||
title: 'Requests',
|
||||
uiStateJSON: '{}',
|
||||
version: 1,
|
||||
},
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
migrationVersion: {
|
||||
dashboard: '7.0.0',
|
||||
},
|
||||
references: [
|
||||
{
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
name: 'panel_0',
|
||||
type: 'visualization',
|
||||
},
|
||||
],
|
||||
type: 'dashboard',
|
||||
updated_at: '2017-09-21T18:57:40.826Z',
|
||||
version: objects[0].version,
|
||||
}]);
|
||||
expect(() => JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON)).not.to.throwError();
|
||||
expect(() => JSON.parse(objects[0].attributes.optionsJSON)).not.to.throwError();
|
||||
expect(() => JSON.parse(objects[0].attributes.panelsJSON)).not.to.throwError();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 200 when exporting by array type', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_export')
|
||||
.send({
|
||||
type: ['dashboard'],
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
expect(resp.headers['content-disposition']).to.eql('attachment; filename="export.ndjson"');
|
||||
expect(resp.headers['content-type']).to.eql('application/ndjson');
|
||||
const objects = resp.text.split('\n').map(JSON.parse);
|
||||
expect(objects).to.eql([{
|
||||
attributes: {
|
||||
description: '',
|
||||
hits: 0,
|
||||
kibanaSavedObjectMeta: {
|
||||
searchSourceJSON: objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON,
|
||||
},
|
||||
optionsJSON: objects[0].attributes.optionsJSON,
|
||||
panelsJSON: objects[0].attributes.panelsJSON,
|
||||
refreshInterval: {
|
||||
display: 'Off',
|
||||
pause: false,
|
||||
value: 0,
|
||||
},
|
||||
timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700',
|
||||
timeRestore: true,
|
||||
timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700',
|
||||
title: 'Requests',
|
||||
uiStateJSON: '{}',
|
||||
version: 1,
|
||||
},
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
migrationVersion: {
|
||||
dashboard: '7.0.0',
|
||||
},
|
||||
references: [
|
||||
{
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
name: 'panel_0',
|
||||
type: 'visualization',
|
||||
},
|
||||
],
|
||||
type: 'dashboard',
|
||||
updated_at: '2017-09-21T18:57:40.826Z',
|
||||
version: objects[0].version,
|
||||
}]);
|
||||
expect(() => JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON)).not.to.throwError();
|
||||
expect(() => JSON.parse(objects[0].attributes.optionsJSON)).not.to.throwError();
|
||||
expect(() => JSON.parse(objects[0].attributes.panelsJSON)).not.to.throwError();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 200 when exporting by objects', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_export')
|
||||
.send({
|
||||
objects: [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
expect(resp.headers['content-disposition']).to.eql('attachment; filename="export.ndjson"');
|
||||
expect(resp.headers['content-type']).to.eql('application/ndjson');
|
||||
const objects = resp.text.split('\n').map(JSON.parse);
|
||||
expect(objects).to.eql([{
|
||||
attributes: {
|
||||
description: '',
|
||||
hits: 0,
|
||||
kibanaSavedObjectMeta: {
|
||||
searchSourceJSON: objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON,
|
||||
},
|
||||
optionsJSON: objects[0].attributes.optionsJSON,
|
||||
panelsJSON: objects[0].attributes.panelsJSON,
|
||||
refreshInterval: {
|
||||
display: 'Off',
|
||||
pause: false,
|
||||
value: 0,
|
||||
},
|
||||
timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700',
|
||||
timeRestore: true,
|
||||
timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700',
|
||||
title: 'Requests',
|
||||
uiStateJSON: '{}',
|
||||
version: 1,
|
||||
},
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
migrationVersion: {
|
||||
dashboard: '7.0.0',
|
||||
},
|
||||
references: [
|
||||
{
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
name: 'panel_0',
|
||||
type: 'visualization',
|
||||
},
|
||||
],
|
||||
type: 'dashboard',
|
||||
updated_at: '2017-09-21T18:57:40.826Z',
|
||||
version: objects[0].version,
|
||||
}]);
|
||||
expect(() => JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON)).not.to.throwError();
|
||||
expect(() => JSON.parse(objects[0].attributes.optionsJSON)).not.to.throwError();
|
||||
expect(() => JSON.parse(objects[0].attributes.panelsJSON)).not.to.throwError();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 when exporting by type and objects', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_export')
|
||||
.send({
|
||||
type: 'dashboard',
|
||||
objects: [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: '"value" contains a conflict between exclusive peers [type, objects]',
|
||||
validation: { source: 'payload', keys: [ 'value' ] },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('10,001 objects', () => {
|
||||
let customVisId;
|
||||
before(async () => {
|
||||
await esArchiver.load('saved_objects/10k');
|
||||
await supertest
|
||||
.post('/api/saved_objects/visualization')
|
||||
.send({
|
||||
attributes: {
|
||||
title: 'My favorite vis',
|
||||
},
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
customVisId = resp.body.id;
|
||||
});
|
||||
});
|
||||
after(async () => {
|
||||
await supertest
|
||||
.delete(`/api/saved_objects/visualization/${customVisId}`)
|
||||
.expect(200);
|
||||
await esArchiver.unload('saved_objects/10k');
|
||||
});
|
||||
|
||||
it('should return 400 when exporting more than 10,000', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_export')
|
||||
.send({
|
||||
type: ['dashboard', 'visualization', 'search', 'index-pattern'],
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: `Can't export more than 10000 objects`
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('without kibana index', () => {
|
||||
before(async () => (
|
||||
// just in case the kibana server has recreated it
|
||||
await es.indices.delete({
|
||||
index: '.kibana',
|
||||
ignore: [404],
|
||||
})
|
||||
));
|
||||
|
||||
it('should return empty response', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_export')
|
||||
.send({
|
||||
type: ['index-pattern', 'search', 'visualization', 'dashboard'],
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
expect(resp.text).to.eql('');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -23,6 +23,7 @@ export default function ({ loadTestFile }) {
|
|||
loadTestFile(require.resolve('./bulk_get'));
|
||||
loadTestFile(require.resolve('./create'));
|
||||
loadTestFile(require.resolve('./delete'));
|
||||
loadTestFile(require.resolve('./export'));
|
||||
loadTestFile(require.resolve('./find'));
|
||||
loadTestFile(require.resolve('./get'));
|
||||
loadTestFile(require.resolve('./update'));
|
||||
|
|
Binary file not shown.
|
@ -0,0 +1,253 @@
|
|||
{
|
||||
"type": "index",
|
||||
"value": {
|
||||
"index": ".kibana",
|
||||
"settings": {
|
||||
"index": {
|
||||
"number_of_shards": "1",
|
||||
"number_of_replicas": "1"
|
||||
}
|
||||
},
|
||||
"mappings": {
|
||||
"dynamic": "strict",
|
||||
"properties": {
|
||||
"config": {
|
||||
"dynamic": "true",
|
||||
"properties": {
|
||||
"buildNum": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"defaultIndex": {
|
||||
"type": "text",
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 256
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"hits": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"optionsJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"panelsJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"refreshInterval": {
|
||||
"properties": {
|
||||
"display": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"pause": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"section": {
|
||||
"type": "integer"
|
||||
},
|
||||
"value": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeFrom": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"timeRestore": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"timeTo": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"uiStateJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"index-pattern": {
|
||||
"properties": {
|
||||
"fieldFormatMap": {
|
||||
"type": "text"
|
||||
},
|
||||
"fields": {
|
||||
"type": "text"
|
||||
},
|
||||
"intervalName": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"notExpandable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"sourceFilters": {
|
||||
"type": "text"
|
||||
},
|
||||
"timeFieldName": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"properties": {
|
||||
"columns": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"hits": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sort": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"properties": {
|
||||
"uuid": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timelion-sheet": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"hits": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timelion_chart_height": {
|
||||
"type": "integer"
|
||||
},
|
||||
"timelion_columns": {
|
||||
"type": "integer"
|
||||
},
|
||||
"timelion_interval": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"timelion_other_interval": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"timelion_rows": {
|
||||
"type": "integer"
|
||||
},
|
||||
"timelion_sheet": {
|
||||
"type": "text"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"namespace": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"type": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "date"
|
||||
},
|
||||
"url": {
|
||||
"properties": {
|
||||
"accessCount": {
|
||||
"type": "long"
|
||||
},
|
||||
"accessDate": {
|
||||
"type": "date"
|
||||
},
|
||||
"createDate": {
|
||||
"type": "date"
|
||||
},
|
||||
"url": {
|
||||
"type": "text",
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 2048
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"visualization": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"savedSearchId": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"uiStateJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
},
|
||||
"visState": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
144
x-pack/test/saved_object_api_integration/common/suites/export.ts
Normal file
144
x-pack/test/saved_object_api_integration/common/suites/export.ts
Normal file
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* 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 ExportTest {
|
||||
statusCode: number;
|
||||
description: string;
|
||||
response: (resp: { [key: string]: any }) => void;
|
||||
}
|
||||
|
||||
interface ExportTests {
|
||||
spaceAwareType: ExportTest;
|
||||
noTypeOrObjects: ExportTest;
|
||||
}
|
||||
|
||||
interface ExportTestDefinition {
|
||||
user?: TestDefinitionAuthentication;
|
||||
spaceId?: string;
|
||||
tests: ExportTests;
|
||||
}
|
||||
|
||||
export function exportTestSuiteFactory(esArchiver: any, supertest: SuperTest<any>) {
|
||||
const createExpectRbacForbidden = (type: string) => (resp: { [key: string]: any }) => {
|
||||
// In export only, the API uses "bulk_get" or "find" depending on the parameters it receives.
|
||||
// The best that could be done here is to have an if statement to ensure at least one of the
|
||||
// two errors has been thrown.
|
||||
if (resp.body.message.indexOf(`bulk_get`) !== -1) {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 403,
|
||||
error: 'Forbidden',
|
||||
message: `Unable to bulk_get ${type}, missing action:saved_objects/${type}/bulk_get`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 403,
|
||||
error: 'Forbidden',
|
||||
message: `Unable to find ${type}, missing action:saved_objects/${type}/find`,
|
||||
});
|
||||
};
|
||||
|
||||
const expectTypeOrObjectsRequired = (resp: { [key: string]: any }) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: '"value" must be an object',
|
||||
validation: { source: 'payload', keys: ['value'] },
|
||||
});
|
||||
};
|
||||
|
||||
const createExpectVisualizationResults = (spaceId = DEFAULT_SPACE_ID) => (resp: {
|
||||
[key: string]: any;
|
||||
}) => {
|
||||
const response = JSON.parse(resp.text);
|
||||
expect(response).to.eql({
|
||||
type: 'visualization',
|
||||
id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`,
|
||||
version: response.version,
|
||||
attributes: response.attributes,
|
||||
references: [
|
||||
{
|
||||
name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
|
||||
type: 'index-pattern',
|
||||
id: `${getIdPrefix(spaceId)}91200a00-9efd-11e7-acb3-3dab96693fab`,
|
||||
},
|
||||
],
|
||||
migrationVersion: { visualization: '7.0.0' },
|
||||
updated_at: '2017-09-21T18:51:23.794Z',
|
||||
});
|
||||
};
|
||||
|
||||
const makeExportTest = (describeFn: DescribeFn) => (
|
||||
description: string,
|
||||
definition: ExportTestDefinition
|
||||
) => {
|
||||
const { user = {}, spaceId = DEFAULT_SPACE_ID, tests } = definition;
|
||||
|
||||
describeFn(description, () => {
|
||||
before(() => esArchiver.load('saved_objects/spaces'));
|
||||
after(() => esArchiver.unload('saved_objects/spaces'));
|
||||
|
||||
it(`space aware type should return ${tests.spaceAwareType.statusCode} with ${
|
||||
tests.spaceAwareType.description
|
||||
} when querying by type`, async () => {
|
||||
await supertest
|
||||
.post(`${getUrlPrefix(spaceId)}/api/saved_objects/_export`)
|
||||
.send({
|
||||
type: 'visualization',
|
||||
})
|
||||
.auth(user.username, user.password)
|
||||
.expect(tests.spaceAwareType.statusCode)
|
||||
.then(tests.spaceAwareType.response);
|
||||
});
|
||||
|
||||
it(`space aware type should return ${tests.spaceAwareType.statusCode} with ${
|
||||
tests.spaceAwareType.description
|
||||
} when querying by objects`, async () => {
|
||||
await supertest
|
||||
.post(`${getUrlPrefix(spaceId)}/api/saved_objects/_export`)
|
||||
.send({
|
||||
objects: [
|
||||
{
|
||||
type: 'visualization',
|
||||
id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`,
|
||||
},
|
||||
],
|
||||
})
|
||||
.auth(user.username, user.password)
|
||||
.expect(tests.spaceAwareType.statusCode)
|
||||
.then(tests.spaceAwareType.response);
|
||||
});
|
||||
|
||||
describe('no type or objects', () => {
|
||||
it(`should return ${tests.noTypeOrObjects.statusCode} with ${
|
||||
tests.noTypeOrObjects.description
|
||||
}`, async () => {
|
||||
await supertest
|
||||
.post(`${getUrlPrefix(spaceId)}/api/saved_objects/_export`)
|
||||
.auth(user.username, user.password)
|
||||
.expect(tests.noTypeOrObjects.statusCode)
|
||||
.then(tests.noTypeOrObjects.response);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const exportTest = makeExportTest(describe);
|
||||
// @ts-ignore
|
||||
exportTest.only = makeExportTest(describe.only);
|
||||
|
||||
return {
|
||||
createExpectRbacForbidden,
|
||||
expectTypeOrObjectsRequired,
|
||||
createExpectVisualizationResults,
|
||||
exportTest,
|
||||
};
|
||||
}
|
|
@ -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 { exportTestSuiteFactory } from '../../common/suites/export';
|
||||
|
||||
// tslint:disable:no-default-export
|
||||
|
||||
export default function({ getService }: TestInvoker) {
|
||||
const supertest = getService('supertestWithoutAuth');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('export', () => {
|
||||
const {
|
||||
createExpectRbacForbidden,
|
||||
expectTypeOrObjectsRequired,
|
||||
createExpectVisualizationResults,
|
||||
exportTest,
|
||||
} = exportTestSuiteFactory(esArchiver, supertest);
|
||||
|
||||
[
|
||||
{
|
||||
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 => {
|
||||
exportTest(`user with no access within the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.noAccess,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
spaceAwareType: {
|
||||
description: 'forbidden login and find visualization message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('visualization'),
|
||||
},
|
||||
noTypeOrObjects: {
|
||||
description: 'bad request, type or object is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeOrObjectsRequired,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
exportTest(`superuser with the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.superuser,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
spaceAwareType: {
|
||||
description: 'only the visualization',
|
||||
statusCode: 200,
|
||||
response: createExpectVisualizationResults(scenario.spaceId),
|
||||
},
|
||||
noTypeOrObjects: {
|
||||
description: 'bad request, type or object is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeOrObjectsRequired,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
exportTest(`legacy user within the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.legacyAll,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
spaceAwareType: {
|
||||
description: 'forbidden login and find visualization message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('visualization'),
|
||||
},
|
||||
noTypeOrObjects: {
|
||||
description: 'bad request, type or object is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeOrObjectsRequired,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
exportTest(`dual-privileges user within the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.dualAll,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
spaceAwareType: {
|
||||
description: 'only the visualization',
|
||||
statusCode: 200,
|
||||
response: createExpectVisualizationResults(scenario.spaceId),
|
||||
},
|
||||
noTypeOrObjects: {
|
||||
description: 'bad request, type or object is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeOrObjectsRequired,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
exportTest(`dual-privileges readonly user within the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.dualRead,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
spaceAwareType: {
|
||||
description: 'only the visualization',
|
||||
statusCode: 200,
|
||||
response: createExpectVisualizationResults(scenario.spaceId),
|
||||
},
|
||||
noTypeOrObjects: {
|
||||
description: 'bad request, type or object is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeOrObjectsRequired,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
exportTest(`rbac user with all globally within the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.allGlobally,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
spaceAwareType: {
|
||||
description: 'only the visualization',
|
||||
statusCode: 200,
|
||||
response: createExpectVisualizationResults(scenario.spaceId),
|
||||
},
|
||||
noTypeOrObjects: {
|
||||
description: 'bad request, type or object is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeOrObjectsRequired,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
exportTest(`rbac user with read globally within the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.readGlobally,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
spaceAwareType: {
|
||||
description: 'only the visualization',
|
||||
statusCode: 200,
|
||||
response: createExpectVisualizationResults(scenario.spaceId),
|
||||
},
|
||||
noTypeOrObjects: {
|
||||
description: 'bad request, type or object is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeOrObjectsRequired,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
exportTest(`rbac user with all at the space within the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.allAtSpace,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
spaceAwareType: {
|
||||
description: 'only the visualization',
|
||||
statusCode: 200,
|
||||
response: createExpectVisualizationResults(scenario.spaceId),
|
||||
},
|
||||
noTypeOrObjects: {
|
||||
description: 'bad request, type or object is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeOrObjectsRequired,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
exportTest(`rbac user with read at the space within the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.readAtSpace,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
spaceAwareType: {
|
||||
description: 'only the visualization',
|
||||
statusCode: 200,
|
||||
response: createExpectVisualizationResults(scenario.spaceId),
|
||||
},
|
||||
noTypeOrObjects: {
|
||||
description: 'bad request, type or object is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeOrObjectsRequired,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
exportTest(`rbac user with all at the other space within ${scenario.spaceId} space`, {
|
||||
user: scenario.users.allAtOtherSpace,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
spaceAwareType: {
|
||||
description: 'forbidden login and find visualization message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('visualization'),
|
||||
},
|
||||
noTypeOrObjects: {
|
||||
description: 'bad request, type or object is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeOrObjectsRequired,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -23,6 +23,7 @@ export default function({ getService, loadTestFile }: TestInvoker) {
|
|||
loadTestFile(require.resolve('./bulk_get'));
|
||||
loadTestFile(require.resolve('./create'));
|
||||
loadTestFile(require.resolve('./delete'));
|
||||
loadTestFile(require.resolve('./export'));
|
||||
loadTestFile(require.resolve('./find'));
|
||||
loadTestFile(require.resolve('./get'));
|
||||
loadTestFile(require.resolve('./update'));
|
||||
|
|
|
@ -0,0 +1,200 @@
|
|||
/*
|
||||
* 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 { exportTestSuiteFactory } from '../../common/suites/export';
|
||||
|
||||
// tslint:disable:no-default-export
|
||||
export default function({ getService }: TestInvoker) {
|
||||
const supertest = getService('supertestWithoutAuth');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('export', () => {
|
||||
const {
|
||||
createExpectRbacForbidden,
|
||||
expectTypeOrObjectsRequired,
|
||||
createExpectVisualizationResults,
|
||||
exportTest,
|
||||
} = exportTestSuiteFactory(esArchiver, supertest);
|
||||
|
||||
exportTest('user with no access', {
|
||||
user: AUTHENTICATION.NOT_A_KIBANA_USER,
|
||||
tests: {
|
||||
spaceAwareType: {
|
||||
description: 'forbidden login and find visualization message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('visualization'),
|
||||
},
|
||||
noTypeOrObjects: {
|
||||
description: 'bad request, type or object is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeOrObjectsRequired,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
exportTest('superuser', {
|
||||
user: AUTHENTICATION.SUPERUSER,
|
||||
tests: {
|
||||
spaceAwareType: {
|
||||
description: 'only the visualization',
|
||||
statusCode: 200,
|
||||
response: createExpectVisualizationResults(),
|
||||
},
|
||||
noTypeOrObjects: {
|
||||
description: 'bad request, type or object is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeOrObjectsRequired,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
exportTest('legacy user', {
|
||||
user: AUTHENTICATION.KIBANA_LEGACY_USER,
|
||||
tests: {
|
||||
spaceAwareType: {
|
||||
description: 'forbidden login and find visualization message',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('visualization'),
|
||||
},
|
||||
noTypeOrObjects: {
|
||||
description: 'bad request, type or object is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeOrObjectsRequired,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
exportTest('dual-privileges user', {
|
||||
user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER,
|
||||
tests: {
|
||||
spaceAwareType: {
|
||||
description: 'only the visualization',
|
||||
statusCode: 200,
|
||||
response: createExpectVisualizationResults(),
|
||||
},
|
||||
noTypeOrObjects: {
|
||||
description: 'bad request, type or object is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeOrObjectsRequired,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
exportTest('dual-privileges readonly user', {
|
||||
user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER,
|
||||
tests: {
|
||||
spaceAwareType: {
|
||||
description: 'only the visualization',
|
||||
statusCode: 200,
|
||||
response: createExpectVisualizationResults(),
|
||||
},
|
||||
noTypeOrObjects: {
|
||||
description: 'bad request, type or object is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeOrObjectsRequired,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
exportTest('rbac user with all globally', {
|
||||
user: AUTHENTICATION.KIBANA_RBAC_USER,
|
||||
tests: {
|
||||
spaceAwareType: {
|
||||
description: 'only the visualization',
|
||||
statusCode: 200,
|
||||
response: createExpectVisualizationResults(),
|
||||
},
|
||||
noTypeOrObjects: {
|
||||
description: 'bad request, type or object is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeOrObjectsRequired,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
exportTest('rbac user with read globally', {
|
||||
user: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER,
|
||||
tests: {
|
||||
spaceAwareType: {
|
||||
description: 'only the visualization',
|
||||
statusCode: 200,
|
||||
response: createExpectVisualizationResults(),
|
||||
},
|
||||
noTypeOrObjects: {
|
||||
description: 'bad request, type or object is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeOrObjectsRequired,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
exportTest('rbac user with all at default space', {
|
||||
user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER,
|
||||
tests: {
|
||||
spaceAwareType: {
|
||||
description: 'only the visualization',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('visualization'),
|
||||
},
|
||||
noTypeOrObjects: {
|
||||
description: 'bad request, type or object is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeOrObjectsRequired,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
exportTest('rbac user with read at default space', {
|
||||
user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER,
|
||||
tests: {
|
||||
spaceAwareType: {
|
||||
description: 'only the visualization',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('visualization'),
|
||||
},
|
||||
noTypeOrObjects: {
|
||||
description: 'bad request, type or object is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeOrObjectsRequired,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
exportTest('rbac user with all at space_1', {
|
||||
user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER,
|
||||
tests: {
|
||||
spaceAwareType: {
|
||||
description: 'only the visualization',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('visualization'),
|
||||
},
|
||||
noTypeOrObjects: {
|
||||
description: 'bad request, type or object is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeOrObjectsRequired,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
exportTest('rbac user with read at space_1', {
|
||||
user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER,
|
||||
tests: {
|
||||
spaceAwareType: {
|
||||
description: 'only the visualization',
|
||||
statusCode: 403,
|
||||
response: createExpectRbacForbidden('visualization'),
|
||||
},
|
||||
noTypeOrObjects: {
|
||||
description: 'bad request, type or object is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeOrObjectsRequired,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
|
@ -23,6 +23,7 @@ export default function({ getService, loadTestFile }: TestInvoker) {
|
|||
loadTestFile(require.resolve('./bulk_get'));
|
||||
loadTestFile(require.resolve('./create'));
|
||||
loadTestFile(require.resolve('./delete'));
|
||||
loadTestFile(require.resolve('./export'));
|
||||
loadTestFile(require.resolve('./find'));
|
||||
loadTestFile(require.resolve('./get'));
|
||||
loadTestFile(require.resolve('./update'));
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 { exportTestSuiteFactory } from '../../common/suites/export';
|
||||
|
||||
// tslint:disable:no-default-export
|
||||
export default function({ getService }: TestInvoker) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
const {
|
||||
expectTypeOrObjectsRequired,
|
||||
createExpectVisualizationResults,
|
||||
exportTest,
|
||||
} = exportTestSuiteFactory(esArchiver, supertest);
|
||||
|
||||
describe('export', () => {
|
||||
exportTest('objects only within the current space (space_1)', {
|
||||
...SPACES.SPACE_1,
|
||||
tests: {
|
||||
spaceAwareType: {
|
||||
description: 'only the visualization',
|
||||
statusCode: 200,
|
||||
response: createExpectVisualizationResults(SPACES.SPACE_1.spaceId),
|
||||
},
|
||||
noTypeOrObjects: {
|
||||
description: 'bad request, type or object is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeOrObjectsRequired,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
exportTest('objects only within the current space (default)', {
|
||||
...SPACES.DEFAULT,
|
||||
tests: {
|
||||
spaceAwareType: {
|
||||
description: 'only the visualization',
|
||||
statusCode: 200,
|
||||
response: createExpectVisualizationResults(SPACES.DEFAULT.spaceId),
|
||||
},
|
||||
noTypeOrObjects: {
|
||||
description: 'bad request, type or object is required',
|
||||
statusCode: 400,
|
||||
response: expectTypeOrObjectsRequired,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
|
@ -15,6 +15,7 @@ export default function({ loadTestFile }: TestInvoker) {
|
|||
loadTestFile(require.resolve('./bulk_get'));
|
||||
loadTestFile(require.resolve('./create'));
|
||||
loadTestFile(require.resolve('./delete'));
|
||||
loadTestFile(require.resolve('./export'));
|
||||
loadTestFile(require.resolve('./find'));
|
||||
loadTestFile(require.resolve('./get'));
|
||||
loadTestFile(require.resolve('./update'));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue