diff --git a/src/core/server/saved_objects/routes/export.ts b/src/core/server/saved_objects/routes/export.ts index ede253c6206c6..d8dde9dfc3d70 100644 --- a/src/core/server/saved_objects/routes/export.ts +++ b/src/core/server/saved_objects/routes/export.ts @@ -28,31 +28,19 @@ import { IRouter } from '../../http'; import { SavedObjectConfig } from '../saved_objects_config'; import { exportSavedObjectsToStream } from '../export'; -export const registerExportRoute = ( - router: IRouter, - config: SavedObjectConfig, - getSupportedTypes: () => string[] -) => { +export const registerExportRoute = (router: IRouter, config: SavedObjectConfig) => { const { maxImportExportSize } = config; - const typeSchema = schema.string({ - validate: (type: string) => { - if (!getSupportedTypes().includes(type)) { - return `${type} is not exportable`; - } - }, - }); - router.post( { path: '/_export', validate: { body: schema.object({ - type: schema.maybe(schema.oneOf([typeSchema, schema.arrayOf(typeSchema)])), + type: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])), objects: schema.maybe( schema.arrayOf( schema.object({ - type: typeSchema, + type: schema.string(), id: schema.string(), }), { maxSize: maxImportExportSize } @@ -67,9 +55,38 @@ export const registerExportRoute = ( router.handleLegacyErrors(async (context, req, res) => { const savedObjectsClient = context.core.savedObjects.client; const { type, objects, search, excludeExportDetails, includeReferencesDeep } = req.body; + const types = typeof type === 'string' ? [type] : type; + + // need to access the registry for type validation, can't use the schema for this + const supportedTypes = context.core.savedObjects.typeRegistry + .getImportableAndExportableTypes() + .map(t => t.name); + if (types) { + const invalidTypes = types.filter(t => !supportedTypes.includes(t)); + if (invalidTypes.length) { + return res.badRequest({ + body: { + message: `Trying to export non-exportable type(s): ${invalidTypes.join(', ')}`, + }, + }); + } + } + if (objects) { + const invalidObjects = objects.filter(obj => !supportedTypes.includes(obj.type)); + if (invalidObjects.length) { + return res.badRequest({ + body: { + message: `Trying to export object(s) with non-exportable types: ${invalidObjects + .map(obj => `${obj.type}-${obj.id}`) + .join(', ')}`, + }, + }); + } + } + const exportStream = await exportSavedObjectsToStream({ savedObjectsClient, - types: typeof type === 'string' ? [type] : type, + types, search, objects, exportSizeLimit: maxImportExportSize, diff --git a/src/core/server/saved_objects/routes/import.ts b/src/core/server/saved_objects/routes/import.ts index ee381344c1235..0731d4159356d 100644 --- a/src/core/server/saved_objects/routes/import.ts +++ b/src/core/server/saved_objects/routes/import.ts @@ -31,11 +31,7 @@ interface FileStream extends Readable { }; } -export const registerImportRoute = ( - router: IRouter, - config: SavedObjectConfig, - getSupportedTypes: () => string[] -) => { +export const registerImportRoute = (router: IRouter, config: SavedObjectConfig) => { const { maxImportExportSize, maxImportPayloadBytes } = config; router.post( @@ -65,8 +61,12 @@ export const registerImportRoute = ( return res.badRequest({ body: `Invalid file extension ${fileExtension}` }); } + const supportedTypes = context.core.savedObjects.typeRegistry + .getImportableAndExportableTypes() + .map(type => type.name); + const result = await importSavedObjectsFromStream({ - supportedTypes: getSupportedTypes(), + supportedTypes, savedObjectsClient: context.core.savedObjects.client, readStream: createSavedObjectsStreamFromNdJson(file), objectLimit: maxImportExportSize, diff --git a/src/core/server/saved_objects/routes/index.ts b/src/core/server/saved_objects/routes/index.ts index ddc1204fce611..fd57a9f3059e3 100644 --- a/src/core/server/saved_objects/routes/index.ts +++ b/src/core/server/saved_objects/routes/index.ts @@ -39,13 +39,11 @@ export function registerRoutes({ http, logger, config, - getImportableAndExportableTypes, migratorPromise, }: { http: InternalHttpServiceSetup; logger: Logger; config: SavedObjectConfig; - getImportableAndExportableTypes: () => string[]; migratorPromise: Promise; }) { const router = http.createRouter('/api/saved_objects/'); @@ -59,9 +57,9 @@ export function registerRoutes({ registerBulkCreateRoute(router); registerBulkUpdateRoute(router); registerLogLegacyImportRoute(router, logger); - registerExportRoute(router, config, getImportableAndExportableTypes); - registerImportRoute(router, config, getImportableAndExportableTypes); - registerResolveImportErrorsRoute(router, config, getImportableAndExportableTypes); + registerExportRoute(router, config); + registerImportRoute(router, config); + registerResolveImportErrorsRoute(router, config); const internalRouter = http.createRouter('/internal/saved_objects/'); diff --git a/src/core/server/saved_objects/routes/integration_tests/export.test.ts b/src/core/server/saved_objects/routes/integration_tests/export.test.ts index 5601171d89a93..858d34d5a93bf 100644 --- a/src/core/server/saved_objects/routes/integration_tests/export.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/export.test.ts @@ -27,7 +27,7 @@ import supertest from 'supertest'; import { UnwrapPromise } from '@kbn/utility-types'; import { SavedObjectConfig } from '../../saved_objects_config'; import { registerExportRoute } from '../export'; -import { setupServer } from './test_utils'; +import { setupServer, createExportableType } from './test_utils'; type setupServerReturn = UnwrapPromise>; const exportSavedObjectsToStream = exportMock.exportSavedObjectsToStream as jest.Mock; @@ -40,12 +40,16 @@ const config = { describe('POST /api/saved_objects/_export', () => { let server: setupServerReturn['server']; let httpSetup: setupServerReturn['httpSetup']; + let handlerContext: setupServerReturn['handlerContext']; beforeEach(async () => { - ({ server, httpSetup } = await setupServer()); + ({ server, httpSetup, handlerContext } = await setupServer()); + handlerContext.savedObjects.typeRegistry.getImportableAndExportableTypes.mockReturnValue( + allowedTypes.map(createExportableType) + ); const router = httpSetup.createRouter('/api/saved_objects/'); - registerExportRoute(router, config, () => allowedTypes); + registerExportRoute(router, config); await server.start(); }); diff --git a/src/core/server/saved_objects/routes/integration_tests/import.test.ts b/src/core/server/saved_objects/routes/integration_tests/import.test.ts index 0a0af94701ce9..c72d3e241b882 100644 --- a/src/core/server/saved_objects/routes/integration_tests/import.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/import.test.ts @@ -22,7 +22,7 @@ import { UnwrapPromise } from '@kbn/utility-types'; import { registerImportRoute } from '../import'; import { savedObjectsClientMock } from '../../../../../core/server/mocks'; import { SavedObjectConfig } from '../../saved_objects_config'; -import { setupServer } from './test_utils'; +import { setupServer, createExportableType } from './test_utils'; type setupServerReturn = UnwrapPromise>; @@ -47,12 +47,15 @@ describe('POST /internal/saved_objects/_import', () => { beforeEach(async () => { ({ server, httpSetup, handlerContext } = await setupServer()); - savedObjectsClient = handlerContext.savedObjects.client; + handlerContext.savedObjects.typeRegistry.getImportableAndExportableTypes.mockReturnValue( + allowedTypes.map(createExportableType) + ); + savedObjectsClient = handlerContext.savedObjects.client; savedObjectsClient.find.mockResolvedValue(emptyResponse); const router = httpSetup.createRouter('/internal/saved_objects/'); - registerImportRoute(router, config, () => allowedTypes); + registerImportRoute(router, config); await server.start(); }); diff --git a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts index b1146273d301d..a36f246f9dbc5 100644 --- a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts @@ -21,7 +21,7 @@ import supertest from 'supertest'; import { UnwrapPromise } from '@kbn/utility-types'; import { registerResolveImportErrorsRoute } from '../resolve_import_errors'; import { savedObjectsClientMock } from '../../../../../core/server/mocks'; -import { setupServer } from './test_utils'; +import { setupServer, createExportableType } from './test_utils'; import { SavedObjectConfig } from '../../saved_objects_config'; type setupServerReturn = UnwrapPromise>; @@ -40,10 +40,14 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => { beforeEach(async () => { ({ server, httpSetup, handlerContext } = await setupServer()); + handlerContext.savedObjects.typeRegistry.getImportableAndExportableTypes.mockReturnValue( + allowedTypes.map(createExportableType) + ); + savedObjectsClient = handlerContext.savedObjects.client; const router = httpSetup.createRouter('/api/saved_objects/'); - registerResolveImportErrorsRoute(router, config, () => allowedTypes); + registerResolveImportErrorsRoute(router, config); await server.start(); }); diff --git a/src/core/server/saved_objects/routes/integration_tests/test_utils.ts b/src/core/server/saved_objects/routes/integration_tests/test_utils.ts index 093b36a413214..82a889f75d3c1 100644 --- a/src/core/server/saved_objects/routes/integration_tests/test_utils.ts +++ b/src/core/server/saved_objects/routes/integration_tests/test_utils.ts @@ -20,6 +20,7 @@ import { ContextService } from '../../../context'; import { createHttpServer, createCoreContext } from '../../../http/test_utils'; import { coreMock } from '../../../mocks'; +import { SavedObjectsType } from '../../types'; const coreId = Symbol('core'); @@ -43,3 +44,17 @@ export const setupServer = async () => { handlerContext, }; }; + +export const createExportableType = (name: string): SavedObjectsType => { + return { + name, + hidden: false, + namespaceAgnostic: false, + mappings: { + properties: {}, + }, + management: { + importableAndExportable: true, + }, + }; +}; diff --git a/src/core/server/saved_objects/routes/resolve_import_errors.ts b/src/core/server/saved_objects/routes/resolve_import_errors.ts index 05de7c3885b60..05bff871b3520 100644 --- a/src/core/server/saved_objects/routes/resolve_import_errors.ts +++ b/src/core/server/saved_objects/routes/resolve_import_errors.ts @@ -31,11 +31,7 @@ interface FileStream extends Readable { }; } -export const registerResolveImportErrorsRoute = ( - router: IRouter, - config: SavedObjectConfig, - getSupportedTypes: () => string[] -) => { +export const registerResolveImportErrorsRoute = (router: IRouter, config: SavedObjectConfig) => { const { maxImportExportSize, maxImportPayloadBytes } = config; router.post( @@ -75,8 +71,13 @@ export const registerResolveImportErrorsRoute = ( if (fileExtension !== '.ndjson') { return res.badRequest({ body: `Invalid file extension ${fileExtension}` }); } + + const supportedTypes = context.core.savedObjects.typeRegistry + .getImportableAndExportableTypes() + .map(type => type.name); + const result = await resolveSavedObjectsImportErrors({ - supportedTypes: getSupportedTypes(), + supportedTypes, savedObjectsClient: context.core.savedObjects.client, readStream: createSavedObjectsStreamFromNdJson(file), retries: req.body.retries, diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index 9fe2ca0bf7b98..175eac3c1bd95 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -301,13 +301,6 @@ export class SavedObjectsService legacyTypes.forEach(type => this.typeRegistry.registerType(type)); this.validations = setupDeps.legacyPlugins.uiExports.savedObjectValidations || {}; - const getImportableAndExportableTypes = () => { - return this.typeRegistry - .getAllTypes() - .map(type => type.name) - .filter(type => this.typeRegistry.isImportableAndExportable(type)); - }; - const savedObjectsConfig = await this.coreContext.configService .atPath('savedObjects') .pipe(first()) @@ -323,7 +316,6 @@ export class SavedObjectsService logger: this.logger, config: this.config, migratorPromise: this.migrator$.pipe(first()).toPromise(), - getImportableAndExportableTypes, }); return { diff --git a/src/core/server/saved_objects/saved_objects_type_registry.mock.ts b/src/core/server/saved_objects/saved_objects_type_registry.mock.ts index 435e352335ecf..8c8458d7a5ce4 100644 --- a/src/core/server/saved_objects/saved_objects_type_registry.mock.ts +++ b/src/core/server/saved_objects/saved_objects_type_registry.mock.ts @@ -25,14 +25,20 @@ const createRegistryMock = (): jest.Mocked type === 'global'); + mock.isImportableAndExportable.mockReturnValue(true); return mock; }; diff --git a/src/core/server/saved_objects/saved_objects_type_registry.test.ts b/src/core/server/saved_objects/saved_objects_type_registry.test.ts index d481d4f9b0430..4d1d5c1eacc25 100644 --- a/src/core/server/saved_objects/saved_objects_type_registry.test.ts +++ b/src/core/server/saved_objects/saved_objects_type_registry.test.ts @@ -236,4 +236,21 @@ describe('SavedObjectTypeRegistry', () => { expect(registry.isImportableAndExportable('unknownType')).toBe(false); }); }); + + describe('#getImportableAndExportableTypes', () => { + it('returns all registered types that are importable/exportable', () => { + const typeA = createType({ name: 'typeA', management: { importableAndExportable: true } }); + const typeB = createType({ name: 'typeB' }); + const typeC = createType({ name: 'typeC', management: { importableAndExportable: false } }); + const typeD = createType({ name: 'typeD', management: { importableAndExportable: true } }); + registry.registerType(typeA); + registry.registerType(typeB); + registry.registerType(typeC); + registry.registerType(typeD); + + const types = registry.getImportableAndExportableTypes(); + expect(types.length).toEqual(2); + expect(types.map(t => t.name)).toEqual(['typeA', 'typeD']); + }); + }); }); diff --git a/src/core/server/saved_objects/saved_objects_type_registry.ts b/src/core/server/saved_objects/saved_objects_type_registry.ts index 7faab9d485312..5580ce3815d0d 100644 --- a/src/core/server/saved_objects/saved_objects_type_registry.ts +++ b/src/core/server/saved_objects/saved_objects_type_registry.ts @@ -27,7 +27,13 @@ import { SavedObjectsType } from './types'; */ export type ISavedObjectTypeRegistry = Pick< SavedObjectTypeRegistry, - 'getType' | 'getAllTypes' | 'getIndex' | 'isNamespaceAgnostic' | 'isHidden' + | 'getType' + | 'getAllTypes' + | 'getIndex' + | 'isNamespaceAgnostic' + | 'isHidden' + | 'getImportableAndExportableTypes' + | 'isImportableAndExportable' >; /** @@ -63,6 +69,13 @@ export class SavedObjectTypeRegistry { return [...this.types.values()]; } + /** + * Return all {@link SavedObjectsType | types} currently registered that are importable/exportable. + */ + public getImportableAndExportableTypes() { + return this.getAllTypes().filter(type => this.isImportableAndExportable(type.name)); + } + /** * Returns the `namespaceAgnostic` property for given type, or `false` if * the type is not registered. diff --git a/test/api_integration/apis/saved_objects/export.js b/test/api_integration/apis/saved_objects/export.js index ace65f190dec2..df687807cd071 100644 --- a/test/api_integration/apis/saved_objects/export.js +++ b/test/api_integration/apis/saved_objects/export.js @@ -191,10 +191,28 @@ export default function({ getService }) { expect(resp.body).to.eql({ statusCode: 400, error: 'Bad Request', - message: - '[request body.type]: types that failed validation:\n' + - '- [request body.type.0]: expected value of type [string] but got [Array]\n' + - '- [request body.type.1.0]: wigwags is not exportable', + message: 'Trying to export non-exportable type(s): wigwags', + }); + }); + }); + + it(`should return 400 when exporting objects with unsupported type`, async () => { + await supertest + .post('/api/saved_objects/_export') + .send({ + objects: [ + { + type: 'wigwags', + id: '1', + }, + ], + }) + .expect(400) + .then(resp => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: 'Trying to export object(s) with non-exportable types: wigwags-1', }); }); });