diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md index 3a5e84ffdc372..268dcdd77d6b4 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md @@ -11,8 +11,9 @@ core: { savedObjects: { client: SavedObjectsClientContract; typeRegistry: ISavedObjectTypeRegistry; - exporter: ISavedObjectsExporter; - importer: ISavedObjectsImporter; + getClient: (options?: SavedObjectsClientProviderOptions) => SavedObjectsClientContract; + getExporter: (client: SavedObjectsClientContract) => ISavedObjectsExporter; + getImporter: (client: SavedObjectsClientContract) => ISavedObjectsImporter; }; elasticsearch: { client: IScopedClusterClient; diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md index 5300c85cf9406..54d85910f823c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md @@ -18,5 +18,5 @@ export interface RequestHandlerContext | Property | Type | Description | | --- | --- | --- | -| [core](./kibana-plugin-core-server.requesthandlercontext.core.md) | {
savedObjects: {
client: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
exporter: ISavedObjectsExporter;
importer: ISavedObjectsImporter;
};
elasticsearch: {
client: IScopedClusterClient;
legacy: {
client: ILegacyScopedClusterClient;
};
};
uiSettings: {
client: IUiSettingsClient;
};
} | | +| [core](./kibana-plugin-core-server.requesthandlercontext.core.md) | {
savedObjects: {
client: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
getClient: (options?: SavedObjectsClientProviderOptions) => SavedObjectsClientContract;
getExporter: (client: SavedObjectsClientContract) => ISavedObjectsExporter;
getImporter: (client: SavedObjectsClientContract) => ISavedObjectsImporter;
};
elasticsearch: {
client: IScopedClusterClient;
legacy: {
client: ILegacyScopedClusterClient;
};
};
uiSettings: {
client: IUiSettingsClient;
};
} | | diff --git a/src/core/server/core_route_handler_context.ts b/src/core/server/core_route_handler_context.ts index f3e06ad8f1daa..f5123a91e7100 100644 --- a/src/core/server/core_route_handler_context.ts +++ b/src/core/server/core_route_handler_context.ts @@ -13,8 +13,7 @@ import { SavedObjectsClientContract } from './saved_objects/types'; import { InternalSavedObjectsServiceStart, ISavedObjectTypeRegistry, - ISavedObjectsExporter, - ISavedObjectsImporter, + SavedObjectsClientProviderOptions, } from './saved_objects'; import { InternalElasticsearchServiceStart, @@ -58,8 +57,6 @@ class CoreSavedObjectsRouteHandlerContext { ) {} #scopedSavedObjectsClient?: SavedObjectsClientContract; #typeRegistry?: ISavedObjectTypeRegistry; - #exporter?: ISavedObjectsExporter; - #importer?: ISavedObjectsImporter; public get client() { if (this.#scopedSavedObjectsClient == null) { @@ -75,19 +72,18 @@ class CoreSavedObjectsRouteHandlerContext { return this.#typeRegistry; } - public get exporter() { - if (this.#exporter == null) { - this.#exporter = this.savedObjectsStart.createExporter(this.client); - } - return this.#exporter; - } + public getClient = (options?: SavedObjectsClientProviderOptions) => { + if (!options) return this.client; + return this.savedObjectsStart.getScopedClient(this.request, options); + }; - public get importer() { - if (this.#importer == null) { - this.#importer = this.savedObjectsStart.createImporter(this.client); - } - return this.#importer; - } + public getExporter = (client: SavedObjectsClientContract) => { + return this.savedObjectsStart.createExporter(client); + }; + + public getImporter = (client: SavedObjectsClientContract) => { + return this.savedObjectsStart.createImporter(client); + }; } class CoreUiSettingsRouteHandlerContext { diff --git a/src/core/server/index.ts b/src/core/server/index.ts index dac2d210eb395..8e4cdc7d59e32 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -49,6 +49,7 @@ import { SavedObjectsServiceStart, ISavedObjectsExporter, ISavedObjectsImporter, + SavedObjectsClientProviderOptions, } from './saved_objects'; import { CapabilitiesSetup, CapabilitiesStart } from './capabilities'; import { MetricsServiceSetup, MetricsServiceStart } from './metrics'; @@ -415,8 +416,9 @@ export interface RequestHandlerContext { savedObjects: { client: SavedObjectsClientContract; typeRegistry: ISavedObjectTypeRegistry; - exporter: ISavedObjectsExporter; - importer: ISavedObjectsImporter; + getClient: (options?: SavedObjectsClientProviderOptions) => SavedObjectsClientContract; + getExporter: (client: SavedObjectsClientContract) => ISavedObjectsExporter; + getImporter: (client: SavedObjectsClientContract) => ISavedObjectsImporter; }; elasticsearch: { client: IScopedClusterClient; diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index a4ce94b177612..19056ae1b9bc7 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -196,8 +196,9 @@ function createCoreRequestHandlerContextMock() { savedObjects: { client: savedObjectsClientMock.create(), typeRegistry: savedObjectsTypeRegistryMock.create(), - exporter: savedObjectsServiceMock.createExporter(), - importer: savedObjectsServiceMock.createImporter(), + getClient: savedObjectsClientMock.create, + getExporter: savedObjectsServiceMock.createExporter, + getImporter: savedObjectsServiceMock.createImporter, }, elasticsearch: { client: elasticsearchServiceMock.createScopedClusterClient(), diff --git a/src/core/server/saved_objects/routes/delete.ts b/src/core/server/saved_objects/routes/delete.ts index 609ce2692c777..fe08acf23fd23 100644 --- a/src/core/server/saved_objects/routes/delete.ts +++ b/src/core/server/saved_objects/routes/delete.ts @@ -32,11 +32,13 @@ export const registerDeleteRoute = (router: IRouter, { coreUsageData }: RouteDep catchAndReturnBoomErrors(async (context, req, res) => { const { type, id } = req.params; const { force } = req.query; + const { getClient } = context.core.savedObjects; const usageStatsClient = coreUsageData.getClient(); usageStatsClient.incrementSavedObjectsDelete({ request: req }).catch(() => {}); - const result = await context.core.savedObjects.client.delete(type, id, { force }); + const client = getClient(); + const result = await client.delete(type, id, { force }); return res.ok({ body: result }); }) ); diff --git a/src/core/server/saved_objects/routes/export.ts b/src/core/server/saved_objects/routes/export.ts index fa5517303f18f..e0293a4522fc1 100644 --- a/src/core/server/saved_objects/routes/export.ts +++ b/src/core/server/saved_objects/routes/export.ts @@ -165,9 +165,9 @@ export const registerExportRoute = ( }, catchAndReturnBoomErrors(async (context, req, res) => { const cleaned = cleanOptions(req.body); - const supportedTypes = context.core.savedObjects.typeRegistry - .getImportableAndExportableTypes() - .map((t) => t.name); + const { typeRegistry, getExporter, getClient } = context.core.savedObjects; + const supportedTypes = typeRegistry.getImportableAndExportableTypes().map((t) => t.name); + let options: EitherExportOptions; try { options = validateOptions(cleaned, { @@ -181,7 +181,12 @@ export const registerExportRoute = ( }); } - const exporter = context.core.savedObjects.exporter; + const includedHiddenTypes = supportedTypes.filter((supportedType) => + typeRegistry.isHidden(supportedType) + ); + + const client = getClient({ includedHiddenTypes }); + const exporter = getExporter(client); const usageStatsClient = coreUsageData.getClient(); usageStatsClient diff --git a/src/core/server/saved_objects/routes/import.ts b/src/core/server/saved_objects/routes/import.ts index e84c638d3ec99..6f75bcf9fd5bf 100644 --- a/src/core/server/saved_objects/routes/import.ts +++ b/src/core/server/saved_objects/routes/import.ts @@ -63,6 +63,7 @@ export const registerImportRoute = ( }, catchAndReturnBoomErrors(async (context, req, res) => { const { overwrite, createNewCopies } = req.query; + const { getClient, getImporter, typeRegistry } = context.core.savedObjects; const usageStatsClient = coreUsageData.getClient(); usageStatsClient @@ -84,7 +85,15 @@ export const registerImportRoute = ( }); } - const { importer } = context.core.savedObjects; + const supportedTypes = typeRegistry.getImportableAndExportableTypes().map((t) => t.name); + + const includedHiddenTypes = supportedTypes.filter((supportedType) => + typeRegistry.isHidden(supportedType) + ); + + const client = getClient({ includedHiddenTypes }); + const importer = getImporter(client); + try { const result = await importer.import({ readStream, diff --git a/src/core/server/saved_objects/routes/integration_tests/delete.test.ts b/src/core/server/saved_objects/routes/integration_tests/delete.test.ts index 7b7a71b7ca858..eaec6e16cbd8c 100644 --- a/src/core/server/saved_objects/routes/integration_tests/delete.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/delete.test.ts @@ -26,7 +26,8 @@ describe('DELETE /api/saved_objects/{type}/{id}', () => { beforeEach(async () => { ({ server, httpSetup, handlerContext } = await setupServer()); - savedObjectsClient = handlerContext.savedObjects.client; + savedObjectsClient = handlerContext.savedObjects.getClient(); + handlerContext.savedObjects.getClient = jest.fn().mockImplementation(() => savedObjectsClient); const router = httpSetup.createRouter('/api/saved_objects/'); coreUsageStatsClient = coreUsageStatsClientMock.create(); 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 40f13064b53f0..09d475f29f362 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 @@ -40,9 +40,13 @@ describe('POST /api/saved_objects/_export', () => { handlerContext.savedObjects.typeRegistry.getImportableAndExportableTypes.mockReturnValue( allowedTypes.map(createExportableType) ); - exporter = handlerContext.savedObjects.exporter; + exporter = handlerContext.savedObjects.getExporter(); const router = httpSetup.createRouter('/api/saved_objects/'); + handlerContext.savedObjects.getExporter = jest + .fn() + .mockImplementation(() => exporter as ReturnType); + coreUsageStatsClient = coreUsageStatsClientMock.create(); coreUsageStatsClient.incrementSavedObjectsExport.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); @@ -77,6 +81,7 @@ describe('POST /api/saved_objects/_export', () => { ], }, ]; + exporter.exportByTypes.mockResolvedValueOnce(createListStream(sortedObjects)); const result = await supertest(httpSetup.server.listener) 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 24122c61c9f42..be4d2160a967b 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 @@ -68,9 +68,9 @@ describe(`POST ${URL}`, () => { typeRegistry: handlerContext.savedObjects.typeRegistry, importSizeLimit: 10000, }); - handlerContext.savedObjects.importer.import.mockImplementation((options) => - importer.import(options) - ); + handlerContext.savedObjects.getImporter = jest + .fn() + .mockImplementation(() => importer as jest.Mocked); const router = httpSetup.createRouter('/internal/saved_objects/'); coreUsageStatsClient = coreUsageStatsClientMock.create(); 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 b23211aef092f..d84b56156b543 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 @@ -66,7 +66,7 @@ describe(`POST ${URL}`, () => { } as any) ); - savedObjectsClient = handlerContext.savedObjects.client; + savedObjectsClient = handlerContext.savedObjects.getClient(); savedObjectsClient.checkConflicts.mockResolvedValue({ errors: [] }); const importer = new SavedObjectsImporter({ @@ -74,9 +74,10 @@ describe(`POST ${URL}`, () => { typeRegistry: handlerContext.savedObjects.typeRegistry, importSizeLimit: 10000, }); - handlerContext.savedObjects.importer.resolveImportErrors.mockImplementation((options) => - importer.resolveImportErrors(options) - ); + + handlerContext.savedObjects.getImporter = jest + .fn() + .mockImplementation(() => importer as jest.Mocked); const router = httpSetup.createRouter('/api/saved_objects/'); coreUsageStatsClient = coreUsageStatsClientMock.create(); 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 2a664328d4df2..a05c7d30b91fd 100644 --- a/src/core/server/saved_objects/routes/resolve_import_errors.ts +++ b/src/core/server/saved_objects/routes/resolve_import_errors.ts @@ -9,6 +9,7 @@ import { extname } from 'path'; import { Readable } from 'stream'; import { schema } from '@kbn/config-schema'; +import { chain } from 'lodash'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; import { SavedObjectConfig } from '../saved_objects_config'; @@ -91,7 +92,18 @@ export const registerResolveImportErrorsRoute = ( }); } - const { importer } = context.core.savedObjects; + const { getClient, getImporter, typeRegistry } = context.core.savedObjects; + + const includedHiddenTypes = chain(req.body.retries) + .map('type') + .uniq() + .filter( + (type) => typeRegistry.isHidden(type) && typeRegistry.isImportableAndExportable(type) + ) + .value(); + + const client = getClient({ includedHiddenTypes }); + const importer = getImporter(client); try { const result = await importer.resolveImportErrors({ diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 34df3bcf85324..377cd2bc2068a 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1924,8 +1924,9 @@ export interface RequestHandlerContext { savedObjects: { client: SavedObjectsClientContract; typeRegistry: ISavedObjectTypeRegistry; - exporter: ISavedObjectsExporter; - importer: ISavedObjectsImporter; + getClient: (options?: SavedObjectsClientProviderOptions) => SavedObjectsClientContract; + getExporter: (client: SavedObjectsClientContract) => ISavedObjectsExporter; + getImporter: (client: SavedObjectsClientContract) => ISavedObjectsImporter; }; elasticsearch: { client: IScopedClusterClient; diff --git a/src/plugins/home/server/services/sample_data/routes/install.ts b/src/plugins/home/server/services/sample_data/routes/install.ts index d32a854f2bc4b..7c00a46602e26 100644 --- a/src/plugins/home/server/services/sample_data/routes/install.ts +++ b/src/plugins/home/server/services/sample_data/routes/install.ts @@ -143,7 +143,15 @@ export function createInstallRoute( let createResults; try { - createResults = await context.core.savedObjects.client.bulkCreate( + const { getClient, typeRegistry } = context.core.savedObjects; + + const includedHiddenTypes = sampleDataset.savedObjects + .map((object) => object.type) + .filter((supportedType) => typeRegistry.isHidden(supportedType)); + + const client = getClient({ includedHiddenTypes }); + + createResults = await client.bulkCreate( sampleDataset.savedObjects.map(({ version, ...savedObject }) => savedObject), { overwrite: true } ); @@ -156,8 +164,8 @@ export function createInstallRoute( return Boolean(savedObjectCreateResult.error); }); if (errors.length > 0) { - const errMsg = `sample_data install errors while loading saved objects. Errors: ${errors.join( - ',' + const errMsg = `sample_data install errors while loading saved objects. Errors: ${JSON.stringify( + errors )}`; logger.warn(errMsg); return res.customError({ body: errMsg, statusCode: 403 }); diff --git a/src/plugins/home/server/services/sample_data/routes/uninstall.ts b/src/plugins/home/server/services/sample_data/routes/uninstall.ts index 54e6fa0936abc..aa8ed67cf840a 100644 --- a/src/plugins/home/server/services/sample_data/routes/uninstall.ts +++ b/src/plugins/home/server/services/sample_data/routes/uninstall.ts @@ -33,7 +33,7 @@ export function createUninstallRoute( client: { callAsCurrentUser }, }, }, - savedObjects: { client: savedObjectsClient }, + savedObjects: { getClient: getSavedObjectsClient, typeRegistry }, }, }, request, @@ -61,6 +61,12 @@ export function createUninstallRoute( } } + const includedHiddenTypes = sampleDataset.savedObjects + .map((object) => object.type) + .filter((supportedType) => typeRegistry.isHidden(supportedType)); + + const savedObjectsClient = getSavedObjectsClient({ includedHiddenTypes }); + const deletePromises = sampleDataset.savedObjects.map(({ type, id }) => savedObjectsClient.delete(type, id) ); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.tsx index b1570bb1fff0d..8b07351f6c2c2 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.tsx @@ -123,7 +123,10 @@ const CountIndicators: FC<{ importItems: ImportItem[] }> = ({ importItems }) => {errorCount && ( -

+

{ + const { query } = req; const managementService = await managementServicePromise; - const { client } = context.core.savedObjects; - const searchTypes = Array.isArray(req.query.type) ? req.query.type : [req.query.type]; - const includedFields = Array.isArray(req.query.fields) - ? req.query.fields - : [req.query.fields]; + const { getClient, typeRegistry } = context.core.savedObjects; + + const searchTypes = Array.isArray(query.type) ? query.type : [query.type]; + const includedFields = Array.isArray(query.fields) ? query.fields : [query.fields]; + const importAndExportableTypes = searchTypes.filter((type) => - managementService.isImportAndExportable(type) + typeRegistry.isImportableAndExportable(type) ); + const includedHiddenTypes = importAndExportableTypes.filter((type) => + typeRegistry.isHidden(type) + ); + + const client = getClient({ includedHiddenTypes }); const searchFields = new Set(); + importAndExportableTypes.forEach((type) => { const searchField = managementService.getDefaultSearchField(type); if (searchField) { @@ -64,7 +71,7 @@ export const registerFindRoute = ( }); const findResponse = await client.find({ - ...req.query, + ...query, fields: undefined, searchFields: [...searchFields], }); diff --git a/src/plugins/saved_objects_management/server/routes/get.ts b/src/plugins/saved_objects_management/server/routes/get.ts index 1e0115db9e43c..5a48f2f2affa7 100644 --- a/src/plugins/saved_objects_management/server/routes/get.ts +++ b/src/plugins/saved_objects_management/server/routes/get.ts @@ -26,10 +26,14 @@ export const registerGetRoute = ( }, }, router.handleLegacyErrors(async (context, req, res) => { + const { type, id } = req.params; const managementService = await managementServicePromise; - const { client } = context.core.savedObjects; + const { getClient, typeRegistry } = context.core.savedObjects; + const includedHiddenTypes = [type].filter( + (entry) => typeRegistry.isHidden(entry) && typeRegistry.isImportableAndExportable(entry) + ); - const { type, id } = req.params; + const client = getClient({ includedHiddenTypes }); const findResponse = await client.get(type, id); const enhancedSavedObject = injectMetaAttributes(findResponse, managementService); diff --git a/src/plugins/saved_objects_management/server/routes/relationships.ts b/src/plugins/saved_objects_management/server/routes/relationships.ts index 5e30f49cde67f..9e2c2031d8abd 100644 --- a/src/plugins/saved_objects_management/server/routes/relationships.ts +++ b/src/plugins/saved_objects_management/server/routes/relationships.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from 'src/core/server'; +import { chain } from 'lodash'; import { findRelationships } from '../lib'; import { ISavedObjectsManagement } from '../services'; @@ -31,12 +32,21 @@ export const registerRelationshipsRoute = ( }, router.handleLegacyErrors(async (context, req, res) => { const managementService = await managementServicePromise; - const { client } = context.core.savedObjects; + const { getClient, typeRegistry } = context.core.savedObjects; const { type, id } = req.params; - const { size } = req.query; - const savedObjectTypes = Array.isArray(req.query.savedObjectTypes) - ? req.query.savedObjectTypes - : [req.query.savedObjectTypes]; + const { size, savedObjectTypes: maybeArraySavedObjectTypes } = req.query; + const savedObjectTypes = Array.isArray(maybeArraySavedObjectTypes) + ? maybeArraySavedObjectTypes + : [maybeArraySavedObjectTypes]; + + const includedHiddenTypes = chain(maybeArraySavedObjectTypes) + .uniq() + .filter( + (entry) => typeRegistry.isHidden(entry) && typeRegistry.isImportableAndExportable(entry) + ) + .value(); + + const client = getClient({ includedHiddenTypes }); const findRelationsResponse = await findRelationships({ type, diff --git a/src/plugins/saved_objects_management/server/routes/scroll_count.ts b/src/plugins/saved_objects_management/server/routes/scroll_count.ts index dfe361e7b9649..89a895adf6008 100644 --- a/src/plugins/saved_objects_management/server/routes/scroll_count.ts +++ b/src/plugins/saved_objects_management/server/routes/scroll_count.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter, SavedObjectsFindOptions } from 'src/core/server'; +import { chain } from 'lodash'; import { findAll } from '../lib'; export const registerScrollForCountRoute = (router: IRouter) => { @@ -30,18 +31,27 @@ export const registerScrollForCountRoute = (router: IRouter) => { }, }, router.handleLegacyErrors(async (context, req, res) => { - const { client } = context.core.savedObjects; + const { getClient, typeRegistry } = context.core.savedObjects; + const { typesToInclude, searchString, references } = req.body; + const includedHiddenTypes = chain(typesToInclude) + .uniq() + .filter( + (type) => typeRegistry.isHidden(type) && typeRegistry.isImportableAndExportable(type) + ) + .value(); + + const client = getClient({ includedHiddenTypes }); const findOptions: SavedObjectsFindOptions = { - type: req.body.typesToInclude, + type: typesToInclude, perPage: 1000, }; - if (req.body.searchString) { - findOptions.search = `${req.body.searchString}*`; + if (searchString) { + findOptions.search = `${searchString}*`; findOptions.searchFields = ['title']; } - if (req.body.references) { - findOptions.hasReference = req.body.references; + if (references) { + findOptions.hasReference = references; findOptions.hasReferenceOperator = 'OR'; } @@ -54,7 +64,7 @@ export const registerScrollForCountRoute = (router: IRouter) => { return accum; }, {} as Record); - for (const type of req.body.typesToInclude) { + for (const type of typesToInclude) { if (!counts[type]) { counts[type] = 0; } diff --git a/src/plugins/saved_objects_management/server/routes/scroll_export.ts b/src/plugins/saved_objects_management/server/routes/scroll_export.ts index 3417efa709e5f..8d11437af661b 100644 --- a/src/plugins/saved_objects_management/server/routes/scroll_export.ts +++ b/src/plugins/saved_objects_management/server/routes/scroll_export.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from 'src/core/server'; +import { chain } from 'lodash'; import { findAll } from '../lib'; export const registerScrollForExportRoute = (router: IRouter) => { @@ -21,10 +22,20 @@ export const registerScrollForExportRoute = (router: IRouter) => { }, }, router.handleLegacyErrors(async (context, req, res) => { - const { client } = context.core.savedObjects; + const { typesToInclude } = req.body; + const { getClient, typeRegistry } = context.core.savedObjects; + const includedHiddenTypes = chain(typesToInclude) + .uniq() + .filter( + (type) => typeRegistry.isHidden(type) && typeRegistry.isImportableAndExportable(type) + ) + .value(); + + const client = getClient({ includedHiddenTypes }); + const objects = await findAll(client, { perPage: 1000, - type: req.body.typesToInclude, + type: typesToInclude, }); return res.ok({ diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects/data.json b/test/functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects/data.json new file mode 100644 index 0000000000000..6a272dc16e462 --- /dev/null +++ b/test/functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects/data.json @@ -0,0 +1,29 @@ +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "test-hidden-importable-exportable:ff3733a0-9fty-11e7-ahb3-3dcb94193fab", + "source": { + "type": "test-hidden-importable-exportable", + "updated_at": "2021-02-11T18:51:23.794Z", + "test-hidden-importable-exportable": { + "title": "Hidden Saved object type that is importable/exportable." + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "test-hidden-non-importable-exportable:op3767a1-9rcg-53u7-jkb3-3dnb74193awc", + "source": { + "type": "test-hidden-non-importable-exportable", + "updated_at": "2021-02-11T18:51:23.794Z", + "test-hidden-non-importable-exportable": { + "title": "Hidden Saved object type that is not importable/exportable." + } + } + } +} diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects/mappings.json b/test/functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects/mappings.json new file mode 100644 index 0000000000000..00d349a27795d --- /dev/null +++ b/test/functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects/mappings.json @@ -0,0 +1,513 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "settings": { + "index": { + "number_of_shards": "1", + "auto_expand_replicas": "0-1", + "number_of_replicas": "0" + } + }, + "mappings": { + "dynamic": "strict", + "properties": { + "test-export-transform": { + "properties": { + "title": { "type": "text" }, + "enabled": { "type": "boolean" } + } + }, + "test-export-add": { + "properties": { + "title": { "type": "text" } + } + }, + "test-export-add-dep": { + "properties": { + "title": { "type": "text" } + } + }, + "test-export-transform-error": { + "properties": { + "title": { "type": "text" } + } + }, + "test-export-invalid-transform": { + "properties": { + "title": { "type": "text" } + } + }, + "apm-telemetry": { + "properties": { + "has_any_services": { + "type": "boolean" + }, + "services_per_agent": { + "properties": { + "go": { + "type": "long", + "null_value": 0 + }, + "java": { + "type": "long", + "null_value": 0 + }, + "js-base": { + "type": "long", + "null_value": 0 + }, + "nodejs": { + "type": "long", + "null_value": 0 + }, + "python": { + "type": "long", + "null_value": 0 + }, + "ruby": { + "type": "long", + "null_value": 0 + } + } + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "id": { + "type": "text", + "index": false + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "accessibility:disableAnimations": { + "type": "boolean" + }, + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "defaultIndex": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "telemetry:optIn": { + "type": "boolean" + } + } + }, + "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" + } + } + }, + "map": { + "properties": { + "bounds": { + "type": "geo_shape", + "tree": "quadtree" + }, + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "index-pattern": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "space": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "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" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "initials": { + "type": "keyword" + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "spaceId": { + "type": "keyword" + }, + "telemetry": { + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "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" + } + } + }, + "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" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "test-hidden-non-importable-exportable": { + "properties": { + "title": { + "type": "text" + } + } + }, + "test-hidden-importable-exportable": { + "properties": { + "title": { + "type": "text" + } + } + } + } + } + } +} diff --git a/test/functional/page_objects/management/saved_objects_page.ts b/test/functional/page_objects/management/saved_objects_page.ts index 971bc2d48d22d..c28d351aa77fb 100644 --- a/test/functional/page_objects/management/saved_objects_page.ts +++ b/test/functional/page_objects/management/saved_objects_page.ts @@ -315,6 +315,18 @@ export function SavedObjectsPageProvider({ getService, getPageObjects }: FtrProv }) ); } + + async getImportErrorsCount() { + log.debug(`Toggling overwriteAll`); + const errorCountNode = await testSubjects.find('importSavedObjectsErrorsCount'); + const errorCountText = await errorCountNode.getVisibleText(); + const match = errorCountText.match(/(\d)+/); + if (!match) { + throw Error(`unable to parse error count from text ${errorCountText}`); + } + + return +match[1]; + } } return new SavedObjectsPage(); diff --git a/test/plugin_functional/config.ts b/test/plugin_functional/config.ts index bd5ef814ae6c0..fc747fcd71f17 100644 --- a/test/plugin_functional/config.ts +++ b/test/plugin_functional/config.ts @@ -30,6 +30,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./test_suites/application_links'), require.resolve('./test_suites/data_plugin'), require.resolve('./test_suites/saved_objects_management'), + require.resolve('./test_suites/saved_objects_hidden_type'), ], services: { ...functionalConfig.get('services'), diff --git a/test/plugin_functional/plugins/saved_objects_hidden_type/kibana.json b/test/plugin_functional/plugins/saved_objects_hidden_type/kibana.json new file mode 100644 index 0000000000000..baef662c695d4 --- /dev/null +++ b/test/plugin_functional/plugins/saved_objects_hidden_type/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "savedObjectsHiddenType", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["saved_objects_hidden_type"], + "server": true, + "ui": false +} diff --git a/test/plugin_functional/plugins/saved_objects_hidden_type/package.json b/test/plugin_functional/plugins/saved_objects_hidden_type/package.json new file mode 100644 index 0000000000000..af5212209d574 --- /dev/null +++ b/test/plugin_functional/plugins/saved_objects_hidden_type/package.json @@ -0,0 +1,14 @@ +{ + "name": "saved_objects_hidden_type", + "version": "1.0.0", + "main": "target/test/plugin_functional/plugins/saved_objects_hidden_type", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && ../../../../node_modules/.bin/tsc" + } +} \ No newline at end of file diff --git a/test/plugin_functional/plugins/saved_objects_hidden_type/server/index.ts b/test/plugin_functional/plugins/saved_objects_hidden_type/server/index.ts new file mode 100644 index 0000000000000..2093b6e8449a4 --- /dev/null +++ b/test/plugin_functional/plugins/saved_objects_hidden_type/server/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { SavedObjectsHiddenTypePlugin } from './plugin'; + +export const plugin = () => new SavedObjectsHiddenTypePlugin(); diff --git a/test/plugin_functional/plugins/saved_objects_hidden_type/server/plugin.ts b/test/plugin_functional/plugins/saved_objects_hidden_type/server/plugin.ts new file mode 100644 index 0000000000000..da2a0a2def1c2 --- /dev/null +++ b/test/plugin_functional/plugins/saved_objects_hidden_type/server/plugin.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Plugin, CoreSetup } from 'kibana/server'; + +export class SavedObjectsHiddenTypePlugin implements Plugin { + public setup({ savedObjects }: CoreSetup, deps: {}) { + // example of a SO type that is hidden and importableAndExportable + savedObjects.registerType({ + name: 'test-hidden-importable-exportable', + hidden: true, + namespaceType: 'single', + mappings: { + properties: { + title: { type: 'text' }, + }, + }, + management: { + importableAndExportable: true, + }, + }); + + // example of a SO type that is hidden and not importableAndExportable + savedObjects.registerType({ + name: 'test-hidden-non-importable-exportable', + hidden: true, + namespaceType: 'single', + mappings: { + properties: { + title: { type: 'text' }, + }, + }, + management: { + importableAndExportable: false, + }, + }); + } + + public start() {} + public stop() {} +} diff --git a/test/plugin_functional/plugins/saved_objects_hidden_type/tsconfig.json b/test/plugin_functional/plugins/saved_objects_hidden_type/tsconfig.json new file mode 100644 index 0000000000000..da457c9ba32fc --- /dev/null +++ b/test/plugin_functional/plugins/saved_objects_hidden_type/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "server/**/*.ts", + "../../../../typings/**/*", + ], + "exclude": [], + "references": [ + { "path": "../../../../src/core/tsconfig.json" } + ] +} diff --git a/test/plugin_functional/test_suites/saved_objects_hidden_type/delete.ts b/test/plugin_functional/test_suites/saved_objects_hidden_type/delete.ts new file mode 100644 index 0000000000000..666afe1acedca --- /dev/null +++ b/test/plugin_functional/test_suites/saved_objects_hidden_type/delete.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { PluginFunctionalProviderContext } from '../../services'; + +export default function ({ getService }: PluginFunctionalProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('delete', () => { + before(() => + esArchiver.load( + '../functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects' + ) + ); + after(() => + esArchiver.unload( + '../functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects' + ) + ); + + it('should return generic 404 when trying to delete a doc with importableAndExportable types', async () => + await supertest + .delete( + `/api/saved_objects/test-hidden-importable-exportable/ff3733a0-9fty-11e7-ahb3-3dcb94193fab` + ) + .set('kbn-xsrf', 'true') + .expect(404) + .then((resp) => { + expect(resp.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: + 'Saved object [test-hidden-importable-exportable/ff3733a0-9fty-11e7-ahb3-3dcb94193fab] not found', + }); + })); + + it('returns empty response for non importableAndExportable types', async () => + await supertest + .delete( + `/api/saved_objects/test-hidden-non-importable-exportable/op3767a1-9rcg-53u7-jkb3-3dnb74193awc` + ) + .set('kbn-xsrf', 'true') + .expect(404) + .then((resp) => { + expect(resp.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: + 'Saved object [test-hidden-non-importable-exportable/op3767a1-9rcg-53u7-jkb3-3dnb74193awc] not found', + }); + })); + }); +} diff --git a/test/plugin_functional/test_suites/saved_objects_hidden_type/export.ts b/test/plugin_functional/test_suites/saved_objects_hidden_type/export.ts new file mode 100644 index 0000000000000..af25835db5a81 --- /dev/null +++ b/test/plugin_functional/test_suites/saved_objects_hidden_type/export.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { PluginFunctionalProviderContext } from '../../services'; + +function ndjsonToObject(input: string): string[] { + return input.split('\n').map((str) => JSON.parse(str)); +} + +export default function ({ getService }: PluginFunctionalProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('export', () => { + before(() => + esArchiver.load( + '../functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects' + ) + ); + after(() => + esArchiver.unload( + '../functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects' + ) + ); + + it('exports objects with importableAndExportable types', async () => + await supertest + .post('/api/saved_objects/_export') + .set('kbn-xsrf', 'true') + .send({ + type: ['test-hidden-importable-exportable'], + }) + .expect(200) + .then((resp) => { + const objects = ndjsonToObject(resp.text); + expect(objects).to.have.length(2); + expect(objects[0]).to.have.property('id', 'ff3733a0-9fty-11e7-ahb3-3dcb94193fab'); + expect(objects[0]).to.have.property('type', 'test-hidden-importable-exportable'); + })); + + it('excludes objects with non importableAndExportable types', async () => + await supertest + .post('/api/saved_objects/_export') + .set('kbn-xsrf', 'true') + .send({ + type: ['test-hidden-non-importable-exportable'], + }) + .then((resp) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'Trying to export non-exportable type(s): test-hidden-non-importable-exportable', + }); + })); + }); +} diff --git a/test/plugin_functional/test_suites/saved_objects_hidden_type/find.ts b/test/plugin_functional/test_suites/saved_objects_hidden_type/find.ts new file mode 100644 index 0000000000000..723140f5c6bf5 --- /dev/null +++ b/test/plugin_functional/test_suites/saved_objects_hidden_type/find.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { PluginFunctionalProviderContext } from '../../services'; + +export default function ({ getService }: PluginFunctionalProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('find', () => { + before(() => + esArchiver.load( + '../functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects' + ) + ); + after(() => + esArchiver.unload( + '../functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects' + ) + ); + + it('returns empty response for importableAndExportable types', async () => + await supertest + .get('/api/saved_objects/_find?type=test-hidden-importable-exportable&fields=title') + .set('kbn-xsrf', 'true') + .expect(200) + .then((resp) => { + expect(resp.body).to.eql({ + page: 1, + per_page: 20, + total: 0, + saved_objects: [], + }); + })); + + it('returns empty response for non importableAndExportable types', async () => + await supertest + .get('/api/saved_objects/_find?type=test-hidden-non-importable-exportable&fields=title') + .set('kbn-xsrf', 'true') + .expect(200) + .then((resp) => { + expect(resp.body).to.eql({ + page: 1, + per_page: 20, + total: 0, + saved_objects: [], + }); + })); + }); +} diff --git a/test/plugin_functional/test_suites/saved_objects_hidden_type/import.ts b/test/plugin_functional/test_suites/saved_objects_hidden_type/import.ts new file mode 100644 index 0000000000000..5de7d8375dd8c --- /dev/null +++ b/test/plugin_functional/test_suites/saved_objects_hidden_type/import.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { PluginFunctionalProviderContext } from '../../services'; + +export default function ({ getService }: PluginFunctionalProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('import', () => { + before(() => + esArchiver.load( + '../functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects' + ) + ); + after(() => + esArchiver.unload( + '../functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects' + ) + ); + + it('imports objects with importableAndExportable type', async () => { + const fileBuffer = Buffer.from( + '{"id":"some-id-1","type":"test-hidden-importable-exportable","attributes":{"title":"my title"},"references":[]}', + 'utf8' + ); + await supertest + .post('/api/saved_objects/_import') + .set('kbn-xsrf', 'true') + .attach('file', fileBuffer, 'export.ndjson') + .expect(200) + .then((resp) => { + expect(resp.body).to.eql({ + successCount: 1, + success: true, + warnings: [], + successResults: [ + { + type: 'test-hidden-importable-exportable', + id: 'some-id-1', + meta: { + title: 'my title', + }, + }, + ], + }); + }); + }); + + it('does not import objects with non importableAndExportable type', async () => { + const fileBuffer = Buffer.from( + '{"id":"some-id-1","type":"test-hidden-non-importable-exportable","attributes":{"title":"my title"},"references":[]}', + 'utf8' + ); + await supertest + .post('/api/saved_objects/_import') + .set('kbn-xsrf', 'true') + .attach('file', fileBuffer, 'export.ndjson') + .expect(200) + .then((resp) => { + expect(resp.body).to.eql({ + successCount: 0, + success: false, + warnings: [], + errors: [ + { + id: 'some-id-1', + type: 'test-hidden-non-importable-exportable', + title: 'my title', + meta: { + title: 'my title', + }, + error: { + type: 'unsupported_type', + }, + }, + ], + }); + }); + }); + }); +} diff --git a/test/plugin_functional/test_suites/saved_objects_hidden_type/index.ts b/test/plugin_functional/test_suites/saved_objects_hidden_type/index.ts new file mode 100644 index 0000000000000..00ba74a988cf4 --- /dev/null +++ b/test/plugin_functional/test_suites/saved_objects_hidden_type/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PluginFunctionalProviderContext } from '../../services'; + +export default function ({ loadTestFile }: PluginFunctionalProviderContext) { + describe('Saved objects with hidden type', function () { + loadTestFile(require.resolve('./import')); + loadTestFile(require.resolve('./export')); + loadTestFile(require.resolve('./resolve_import_errors')); + loadTestFile(require.resolve('./find')); + loadTestFile(require.resolve('./delete')); + loadTestFile(require.resolve('./interface/saved_objects_management')); + }); +} diff --git a/test/plugin_functional/test_suites/saved_objects_hidden_type/interface/exports/_import_hidden_importable.ndjson b/test/plugin_functional/test_suites/saved_objects_hidden_type/interface/exports/_import_hidden_importable.ndjson new file mode 100644 index 0000000000000..a74585c07b868 --- /dev/null +++ b/test/plugin_functional/test_suites/saved_objects_hidden_type/interface/exports/_import_hidden_importable.ndjson @@ -0,0 +1 @@ +{"attributes": { "title": "Hidden Saved object type that is importable/exportable." }, "id":"ff3733a0-9fty-11e7-ahb3-3dcb94193fab", "references":[], "type":"test-hidden-importable-exportable", "version":1} diff --git a/test/plugin_functional/test_suites/saved_objects_hidden_type/interface/exports/_import_hidden_non_importable.ndjson b/test/plugin_functional/test_suites/saved_objects_hidden_type/interface/exports/_import_hidden_non_importable.ndjson new file mode 100644 index 0000000000000..25eea91b8bc43 --- /dev/null +++ b/test/plugin_functional/test_suites/saved_objects_hidden_type/interface/exports/_import_hidden_non_importable.ndjson @@ -0,0 +1 @@ +{"attributes": { "title": "Hidden Saved object type that is not importable/exportable." },"id":"op3767a1-9rcg-53u7-jkb3-3dnb74193awc","references":[],"type":"test-hidden-non-importable-exportable","version":1} diff --git a/test/plugin_functional/test_suites/saved_objects_hidden_type/interface/saved_objects_management.ts b/test/plugin_functional/test_suites/saved_objects_hidden_type/interface/saved_objects_management.ts new file mode 100644 index 0000000000000..dfd0b9dd07476 --- /dev/null +++ b/test/plugin_functional/test_suites/saved_objects_hidden_type/interface/saved_objects_management.ts @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import path from 'path'; +import expect from '@kbn/expect'; +import { PluginFunctionalProviderContext } from '../../../services'; + +export default function ({ getPageObjects, getService }: PluginFunctionalProviderContext) { + const PageObjects = getPageObjects(['common', 'settings', 'header', 'savedObjects']); + const esArchiver = getService('esArchiver'); + const fixturePaths = { + hiddenImportable: path.join(__dirname, 'exports', '_import_hidden_importable.ndjson'), + hiddenNonImportable: path.join(__dirname, 'exports', '_import_hidden_non_importable.ndjson'), + }; + + describe('Saved objects management Interface', () => { + before(() => esArchiver.emptyKibanaIndex()); + beforeEach(async () => { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaSavedObjects(); + }); + describe('importable/exportable hidden type', () => { + it('imports objects successfully', async () => { + await PageObjects.savedObjects.importFile(fixturePaths.hiddenImportable); + await PageObjects.savedObjects.checkImportSucceeded(); + }); + + it('shows test-hidden-importable-exportable in table', async () => { + await PageObjects.savedObjects.searchForObject('type:(test-hidden-importable-exportable)'); + const results = await PageObjects.savedObjects.getTableSummary(); + expect(results.length).to.be(1); + + const { title } = results[0]; + expect(title).to.be( + 'test-hidden-importable-exportable [id=ff3733a0-9fty-11e7-ahb3-3dcb94193fab]' + ); + }); + }); + + describe('non-importable/exportable hidden type', () => { + it('fails to import object', async () => { + await PageObjects.savedObjects.importFile(fixturePaths.hiddenNonImportable); + await PageObjects.savedObjects.checkImportSucceeded(); + + const errorsCount = await PageObjects.savedObjects.getImportErrorsCount(); + expect(errorsCount).to.be(1); + }); + }); + }); +} diff --git a/test/plugin_functional/test_suites/saved_objects_hidden_type/resolve_import_errors.ts b/test/plugin_functional/test_suites/saved_objects_hidden_type/resolve_import_errors.ts new file mode 100644 index 0000000000000..dddee085ae22b --- /dev/null +++ b/test/plugin_functional/test_suites/saved_objects_hidden_type/resolve_import_errors.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { PluginFunctionalProviderContext } from '../../services'; + +export default function ({ getService }: PluginFunctionalProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('export', () => { + before(() => + esArchiver.load( + '../functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects' + ) + ); + after(() => + esArchiver.unload( + '../functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects' + ) + ); + + it('resolves objects with importableAndExportable types', async () => { + const fileBuffer = Buffer.from( + '{"id":"ff3733a0-9fty-11e7-ahb3-3dcb94193fab","type":"test-hidden-importable-exportable","attributes":{"title":"new title!"},"references":[]}', + 'utf8' + ); + + await supertest + .post('/api/saved_objects/_resolve_import_errors') + .set('kbn-xsrf', 'true') + .field( + 'retries', + JSON.stringify([ + { + id: 'ff3733a0-9fty-11e7-ahb3-3dcb94193fab', + type: 'test-hidden-importable-exportable', + overwrite: true, + }, + ]) + ) + .attach('file', fileBuffer, 'import.ndjson') + .expect(200) + .then((resp) => { + expect(resp.body).to.eql({ + successCount: 1, + success: true, + warnings: [], + successResults: [ + { + type: 'test-hidden-importable-exportable', + id: 'ff3733a0-9fty-11e7-ahb3-3dcb94193fab', + meta: { + title: 'new title!', + }, + overwrite: true, + }, + ], + }); + }); + }); + + it('rejects objects with non importableAndExportable types', async () => { + const fileBuffer = Buffer.from( + '{"id":"op3767a1-9rcg-53u7-jkb3-3dnb74193awc","type":"test-hidden-non-importable-exportable","attributes":{"title":"new title!"},"references":[]}', + 'utf8' + ); + + await supertest + .post('/api/saved_objects/_resolve_import_errors') + .set('kbn-xsrf', 'true') + .field( + 'retries', + JSON.stringify([ + { + id: 'op3767a1-9rcg-53u7-jkb3-3dnb74193awc', + type: 'test-hidden-non-importable-exportable', + overwrite: true, + }, + ]) + ) + .attach('file', fileBuffer, 'import.ndjson') + .expect(200) + .then((resp) => { + expect(resp.body).to.eql({ + successCount: 0, + success: false, + warnings: [], + errors: [ + { + id: 'op3767a1-9rcg-53u7-jkb3-3dnb74193awc', + type: 'test-hidden-non-importable-exportable', + title: 'new title!', + meta: { + title: 'new title!', + }, + error: { + type: 'unsupported_type', + }, + overwrite: true, + }, + ], + }); + }); + }); + }); +} diff --git a/test/plugin_functional/test_suites/saved_objects_management/find.ts b/test/plugin_functional/test_suites/saved_objects_management/find.ts new file mode 100644 index 0000000000000..5dce8f43339a1 --- /dev/null +++ b/test/plugin_functional/test_suites/saved_objects_management/find.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { PluginFunctionalProviderContext } from '../../services'; + +export default function ({ getService }: PluginFunctionalProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('find', () => { + describe('saved objects with hidden type', () => { + before(() => + esArchiver.load( + '../functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects' + ) + ); + after(() => + esArchiver.unload( + '../functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects' + ) + ); + it('returns saved objects with importableAndExportable types', async () => + await supertest + .get( + '/api/kibana/management/saved_objects/_find?type=test-hidden-importable-exportable&fields=title' + ) + .set('kbn-xsrf', 'true') + .expect(200) + .then((resp) => { + expect(resp.body).to.eql({ + page: 1, + per_page: 20, + total: 1, + saved_objects: [ + { + type: 'test-hidden-importable-exportable', + id: 'ff3733a0-9fty-11e7-ahb3-3dcb94193fab', + attributes: { + title: 'Hidden Saved object type that is importable/exportable.', + }, + references: [], + updated_at: '2021-02-11T18:51:23.794Z', + version: 'WzIsMl0=', + namespaces: ['default'], + score: 0, + meta: { + namespaceType: 'single', + }, + }, + ], + }); + })); + + it('returns empty response for non importableAndExportable types', async () => + await supertest + .get( + '/api/kibana/management/saved_objects/_find?type=test-hidden-non-importable-exportable' + ) + .set('kbn-xsrf', 'true') + .expect(200) + .then((resp) => { + expect(resp.body).to.eql({ + page: 1, + per_page: 20, + total: 0, + saved_objects: [], + }); + })); + }); + }); +} diff --git a/test/plugin_functional/test_suites/saved_objects_management/get.ts b/test/plugin_functional/test_suites/saved_objects_management/get.ts new file mode 100644 index 0000000000000..fa35983df8301 --- /dev/null +++ b/test/plugin_functional/test_suites/saved_objects_management/get.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { PluginFunctionalProviderContext } from '../../services'; + +export default function ({ getService }: PluginFunctionalProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('get', () => { + describe('saved objects with hidden type', () => { + before(() => + esArchiver.load( + '../functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects' + ) + ); + after(() => + esArchiver.unload( + '../functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects' + ) + ); + const hiddenTypeExportableImportable = + 'test-hidden-importable-exportable/ff3733a0-9fty-11e7-ahb3-3dcb94193fab'; + const hiddenTypeNonExportableImportable = + 'test-hidden-non-importable-exportable/op3767a1-9rcg-53u7-jkb3-3dnb74193awc'; + + it('should return 200 for hidden types that are importableAndExportable', async () => + await supertest + .get(`/api/kibana/management/saved_objects/${hiddenTypeExportableImportable}`) + .set('kbn-xsrf', 'true') + .expect(200) + .then((resp) => { + const { body } = resp; + const { type, id, meta } = body; + expect(type).to.eql('test-hidden-importable-exportable'); + expect(id).to.eql('ff3733a0-9fty-11e7-ahb3-3dcb94193fab'); + expect(meta).to.not.equal(undefined); + })); + + it('should return 404 for hidden types that are not importableAndExportable', async () => + await supertest + .get(`/api/kibana/management/saved_objects/${hiddenTypeNonExportableImportable}`) + .set('kbn-xsrf', 'true') + .expect(404)); + }); + }); +} diff --git a/test/plugin_functional/test_suites/saved_objects_management/index.ts b/test/plugin_functional/test_suites/saved_objects_management/index.ts index f6d383e60388d..9f2d28b582f78 100644 --- a/test/plugin_functional/test_suites/saved_objects_management/index.ts +++ b/test/plugin_functional/test_suites/saved_objects_management/index.ts @@ -10,6 +10,9 @@ import { PluginFunctionalProviderContext } from '../../services'; export default function ({ loadTestFile }: PluginFunctionalProviderContext) { describe('Saved Objects Management', function () { + loadTestFile(require.resolve('./find')); + loadTestFile(require.resolve('./scroll_count')); + loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./export_transform')); loadTestFile(require.resolve('./import_warnings')); }); diff --git a/test/plugin_functional/test_suites/saved_objects_management/scroll_count.ts b/test/plugin_functional/test_suites/saved_objects_management/scroll_count.ts new file mode 100644 index 0000000000000..f74cd5b938447 --- /dev/null +++ b/test/plugin_functional/test_suites/saved_objects_management/scroll_count.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { PluginFunctionalProviderContext } from '../../services'; + +export default function ({ getService }: PluginFunctionalProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const apiUrl = '/api/kibana/management/saved_objects/scroll/counts'; + + describe('scroll_count', () => { + describe('saved objects with hidden type', () => { + before(() => + esArchiver.load( + '../functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects' + ) + ); + after(() => + esArchiver.unload( + '../functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects' + ) + ); + + it('only counts hidden types that are importableAndExportable', async () => { + const res = await supertest + .post(apiUrl) + .set('kbn-xsrf', 'true') + .send({ + typesToInclude: [ + 'test-hidden-non-importable-exportable', + 'test-hidden-importable-exportable', + ], + }) + .expect(200); + + expect(res.body).to.eql({ + 'test-hidden-importable-exportable': 1, + 'test-hidden-non-importable-exportable': 0, + }); + }); + }); + }); +}