Skip to content

Commit

Permalink
use request context to access exportable types
Browse files Browse the repository at this point in the history
  • Loading branch information
pgayvallet committed Mar 6, 2020
1 parent 026eea6 commit d8451dc
Show file tree
Hide file tree
Showing 13 changed files with 142 additions and 54 deletions.
49 changes: 33 additions & 16 deletions src/core/server/saved_objects/routes/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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,
Expand Down
12 changes: 6 additions & 6 deletions src/core/server/saved_objects/routes/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 3 additions & 5 deletions src/core/server/saved_objects/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,11 @@ export function registerRoutes({
http,
logger,
config,
getImportableAndExportableTypes,
migratorPromise,
}: {
http: InternalHttpServiceSetup;
logger: Logger;
config: SavedObjectConfig;
getImportableAndExportableTypes: () => string[];
migratorPromise: Promise<IKibanaMigrator>;
}) {
const router = http.createRouter('/api/saved_objects/');
Expand All @@ -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/');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReturnType<typeof setupServer>>;
const exportSavedObjectsToStream = exportMock.exportSavedObjectsToStream as jest.Mock;
Expand All @@ -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();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReturnType<typeof setupServer>>;

Expand All @@ -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();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReturnType<typeof setupServer>>;
Expand All @@ -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();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand All @@ -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,
},
};
};
13 changes: 7 additions & 6 deletions src/core/server/saved_objects/routes/resolve_import_errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 0 additions & 8 deletions src/core/server/saved_objects/saved_objects_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<SavedObjectsConfigType>('savedObjects')
.pipe(first())
Expand All @@ -323,7 +316,6 @@ export class SavedObjectsService
logger: this.logger,
config: this.config,
migratorPromise: this.migrator$.pipe(first()).toPromise(),
getImportableAndExportableTypes,
});

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,20 @@ const createRegistryMock = (): jest.Mocked<ISavedObjectTypeRegistry &
registerType: jest.fn(),
getType: jest.fn(),
getAllTypes: jest.fn(),
getImportableAndExportableTypes: jest.fn(),
isNamespaceAgnostic: jest.fn(),
isHidden: jest.fn(),
getIndex: jest.fn(),
isImportableAndExportable: jest.fn(),
};

mock.getAllTypes.mockReturnValue([]);
mock.getImportableAndExportableTypes.mockReturnValue([]);
mock.getIndex.mockReturnValue('.kibana-test');
mock.getIndex.mockReturnValue('.kibana-test');
mock.isHidden.mockReturnValue(false);
mock.isNamespaceAgnostic.mockImplementation((type: string) => type === 'global');
mock.isImportableAndExportable.mockReturnValue(true);

return mock;
};
Expand Down
17 changes: 17 additions & 0 deletions src/core/server/saved_objects/saved_objects_type_registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
});
});
});
15 changes: 14 additions & 1 deletion src/core/server/saved_objects/saved_objects_type_registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
>;

/**
Expand Down Expand Up @@ -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.
Expand Down
26 changes: 22 additions & 4 deletions test/api_integration/apis/saved_objects/export.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});
});
});
Expand Down

0 comments on commit d8451dc

Please sign in to comment.