mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Saved Objects] Provide ability to remove SO type from global SO HTTP API without hiding from the client (#149166)
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> resolves https://github.com/elastic/kibana/issues/147150
This commit is contained in:
parent
2dd13289e3
commit
f7b25f5e46
47 changed files with 5035 additions and 28 deletions
|
@ -257,3 +257,39 @@ the error should be verbose and informative so that the corrupt document can be
|
|||
### Testing Migrations
|
||||
|
||||
Bugs in a migration function cause downtime for our users and therefore have a very high impact. Follow the <DocLink id="kibDevTutorialTestingPlugins" section="saved-objects-migrations" text="Saved Object migrations section in the plugin testing guide"/>.
|
||||
|
||||
### How to opt-out of the global savedObjects APIs?
|
||||
|
||||
There are 2 options, depending on the amount of flexibility you need:
|
||||
For complete control over your HTTP APIs and custom handling, declare your type as `hidden`, as shown in the example.
|
||||
The other option that allows you to build your own HTTP APIs and still use the client as-is is to declare your type as hidden from the global saved objects HTTP APIs as `hiddenFromHttpApis: true`
|
||||
|
||||
```ts
|
||||
import { SavedObjectsType } from 'src/core/server';
|
||||
|
||||
export const foo: SavedObjectsType = {
|
||||
name: 'foo',
|
||||
hidden: false, [1]
|
||||
hiddenFromHttpApis: true, [2]
|
||||
namespaceType: 'multiple-isolated',
|
||||
mappings: {
|
||||
dynamic: false,
|
||||
properties: {
|
||||
description: {
|
||||
type: 'text',
|
||||
},
|
||||
hits: {
|
||||
type: 'integer',
|
||||
},
|
||||
},
|
||||
},
|
||||
migrations: {
|
||||
'1.0.0': migratedashboardVisualizationToV1,
|
||||
'2.0.0': migratedashboardVisualizationToV2,
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
[1] Needs to be `false` to use the `hiddenFromHttpApis` option
|
||||
|
||||
[2] Set this to `true` to build your own HTTP API and have complete control over the route handler.
|
||||
|
|
|
@ -271,6 +271,11 @@ in this code, there's really no reason not to aim for 100% test code coverage.
|
|||
==== Type visibility
|
||||
It is recommended that plugins only expose Saved Object types that are necessary.
|
||||
That is so to provide better backward compatibility.
|
||||
|
||||
There are two options to register a type: either as completely unexposed to the global Saved Objects HTTP APIs and client or to only expose it to the client but not to the APIs.
|
||||
|
||||
===== Hidden types
|
||||
|
||||
In case when the type is not hidden, it will be exposed via the global Saved Objects HTTP API.
|
||||
That brings the limitation of introducing backward incompatible changes as there might be a service consuming the API.
|
||||
|
||||
|
@ -302,6 +307,40 @@ class SomePlugin implements Plugin {
|
|||
}
|
||||
----
|
||||
|
||||
===== Hidden from the HTTP APIs
|
||||
|
||||
When a saved object is registered as hidden from the HTTP APIs, it will remain exposed to the global Saved Objects client:
|
||||
|
||||
[source,typescript]
|
||||
----
|
||||
import { SavedObjectsType } from 'src/core/server';
|
||||
|
||||
export const myCustomVisualization: SavedObjectsType = {
|
||||
name: 'my_custom_visualization', // <1>
|
||||
hidden: false,
|
||||
hiddenFromHttpApis: true, // <2>
|
||||
namespaceType: 'multiple-isolated',
|
||||
mappings: {
|
||||
dynamic: false,
|
||||
properties: {
|
||||
description: {
|
||||
type: 'text',
|
||||
},
|
||||
hits: {
|
||||
type: 'integer',
|
||||
},
|
||||
},
|
||||
},
|
||||
migrations: {
|
||||
'1.0.0': migrateMyCustomVisualizationToV1,
|
||||
'2.0.0': migrateMyCustomVisualizationToV2,
|
||||
},
|
||||
};
|
||||
----
|
||||
|
||||
<1> MyCustomVisualization types have their own domain-specific HTTP API's that leverage the global Saved Objects client
|
||||
<2> This field determines "hidden from http apis" behavior -- any attempts to use the global Saved Objects HTTP APIs will throw errors
|
||||
|
||||
=== Client side usage
|
||||
|
||||
==== References
|
||||
|
|
|
@ -139,6 +139,67 @@ describe('SavedObjectTypeRegistry', () => {
|
|||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('throws when `hidden` is true and `hiddenFromHttpApis` is false', () => {
|
||||
expect(() => {
|
||||
registry.registerType(
|
||||
createType({
|
||||
name: 'typeHiddenA',
|
||||
hidden: true,
|
||||
hiddenFromHttpApis: false,
|
||||
})
|
||||
);
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Type typeHiddenA: 'hiddenFromHttpApis' cannot be 'false' when specifying 'hidden' as 'true'"`
|
||||
);
|
||||
|
||||
expect(() => {
|
||||
registry.registerType(
|
||||
createType({
|
||||
name: 'typeHiddenA',
|
||||
hidden: true,
|
||||
hiddenFromHttpApis: true,
|
||||
})
|
||||
);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(() => {
|
||||
registry.registerType(
|
||||
createType({
|
||||
name: 'typeHiddenA2',
|
||||
hidden: true,
|
||||
})
|
||||
);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(() => {
|
||||
registry.registerType(
|
||||
createType({
|
||||
name: 'typeVisibleA',
|
||||
hidden: false,
|
||||
})
|
||||
);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(() => {
|
||||
registry.registerType(
|
||||
createType({
|
||||
name: 'typeVisibleA1',
|
||||
hidden: false,
|
||||
hiddenFromHttpApis: false,
|
||||
})
|
||||
);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(() => {
|
||||
registry.registerType(
|
||||
createType({
|
||||
name: 'typeVisibleA2',
|
||||
hidden: false,
|
||||
hiddenFromHttpApis: true,
|
||||
})
|
||||
);
|
||||
}).not.toThrow();
|
||||
});
|
||||
// TODO: same test with 'onImport'
|
||||
});
|
||||
|
||||
|
@ -383,6 +444,22 @@ describe('SavedObjectTypeRegistry', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#isHiddenFromHttpApis', () => {
|
||||
it('returns correct value for the type', () => {
|
||||
registry.registerType(createType({ name: 'typeA', hiddenFromHttpApis: true }));
|
||||
registry.registerType(createType({ name: 'typeB', hiddenFromHttpApis: false }));
|
||||
|
||||
expect(registry.isHiddenFromHttpApis('typeA')).toEqual(true);
|
||||
expect(registry.isHiddenFromHttpApis('typeB')).toEqual(false);
|
||||
});
|
||||
it('returns true when the type is not registered', () => {
|
||||
registry.registerType(createType({ name: 'typeA', hiddenFromHttpApis: false }));
|
||||
registry.registerType(createType({ name: 'typeB', hiddenFromHttpApis: true }));
|
||||
|
||||
expect(registry.isHiddenFromHttpApis('unknownType')).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getIndex', () => {
|
||||
it('returns correct value for the type', () => {
|
||||
registry.registerType(createType({ name: 'typeA', indexPattern: '.custom-index' }));
|
||||
|
|
|
@ -41,6 +41,11 @@ export class SavedObjectTypeRegistry implements ISavedObjectTypeRegistry {
|
|||
return [...this.types.values()].filter((type) => !this.isHidden(type.name));
|
||||
}
|
||||
|
||||
/** {@inheritDoc ISavedObjectTypeRegistry.getVisibleToHttpApisTypes} */
|
||||
public getVisibleToHttpApisTypes() {
|
||||
return [...this.types.values()].filter((type) => !this.isHiddenFromHttpApis(type.name));
|
||||
}
|
||||
|
||||
/** {@inheritDoc ISavedObjectTypeRegistry.getAllTypes} */
|
||||
public getAllTypes() {
|
||||
return [...this.types.values()];
|
||||
|
@ -78,6 +83,11 @@ export class SavedObjectTypeRegistry implements ISavedObjectTypeRegistry {
|
|||
return this.types.get(type)?.hidden ?? false;
|
||||
}
|
||||
|
||||
/** {@inheritDoc ISavedObjectTypeRegistry.isHiddenFromHttpApi} */
|
||||
public isHiddenFromHttpApis(type: string) {
|
||||
return !!this.types.get(type)?.hiddenFromHttpApis;
|
||||
}
|
||||
|
||||
/** {@inheritDoc ISavedObjectTypeRegistry.getType} */
|
||||
public getIndex(type: string) {
|
||||
return this.types.get(type)?.indexPattern;
|
||||
|
@ -89,7 +99,7 @@ export class SavedObjectTypeRegistry implements ISavedObjectTypeRegistry {
|
|||
}
|
||||
}
|
||||
|
||||
const validateType = ({ name, management }: SavedObjectsType) => {
|
||||
const validateType = ({ name, management, hidden, hiddenFromHttpApis }: SavedObjectsType) => {
|
||||
if (management) {
|
||||
if (management.onExport && !management.importableAndExportable) {
|
||||
throw new Error(
|
||||
|
@ -102,4 +112,10 @@ const validateType = ({ name, management }: SavedObjectsType) => {
|
|||
);
|
||||
}
|
||||
}
|
||||
// throw error if a type is registered as `hidden:true` and `hiddenFromHttpApis:false` explicitly
|
||||
if (hidden === true && hiddenFromHttpApis === false) {
|
||||
throw new Error(
|
||||
`Type ${name}: 'hiddenFromHttpApis' cannot be 'false' when specifying 'hidden' as 'true'`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -16,6 +16,7 @@ const createRegistryMock = (): jest.Mocked<
|
|||
registerType: jest.fn(),
|
||||
getType: jest.fn(),
|
||||
getVisibleTypes: jest.fn(),
|
||||
getVisibleToHttpApisTypes: jest.fn(),
|
||||
getAllTypes: jest.fn(),
|
||||
getImportableAndExportableTypes: jest.fn(),
|
||||
isNamespaceAgnostic: jest.fn(),
|
||||
|
@ -23,6 +24,7 @@ const createRegistryMock = (): jest.Mocked<
|
|||
isMultiNamespace: jest.fn(),
|
||||
isShareable: jest.fn(),
|
||||
isHidden: jest.fn(),
|
||||
isHiddenFromHttpApis: jest.fn(),
|
||||
getIndex: jest.fn(),
|
||||
isImportableAndExportable: jest.fn(),
|
||||
};
|
||||
|
@ -33,6 +35,7 @@ const createRegistryMock = (): jest.Mocked<
|
|||
mock.getIndex.mockReturnValue('.kibana-test');
|
||||
mock.getIndex.mockReturnValue('.kibana-test');
|
||||
mock.isHidden.mockReturnValue(false);
|
||||
mock.isHiddenFromHttpApis.mockReturnValue(false);
|
||||
mock.isNamespaceAgnostic.mockImplementation((type: string) => type === 'global');
|
||||
mock.isSingleNamespace.mockImplementation(
|
||||
(type: string) => type !== 'global' && type !== 'shared'
|
||||
|
@ -40,6 +43,7 @@ const createRegistryMock = (): jest.Mocked<
|
|||
mock.isMultiNamespace.mockImplementation((type: string) => type === 'shared');
|
||||
mock.isShareable.mockImplementation((type: string) => type === 'shared');
|
||||
mock.isImportableAndExportable.mockReturnValue(true);
|
||||
mock.getVisibleToHttpApisTypes.mockReturnValue(false);
|
||||
|
||||
return mock;
|
||||
};
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import { schema } from '@kbn/config-schema';
|
||||
import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal';
|
||||
import type { InternalSavedObjectRouter } from '../internal_types';
|
||||
import { catchAndReturnBoomErrors } from './utils';
|
||||
import { catchAndReturnBoomErrors, throwIfAnyTypeNotVisibleByAPI } from './utils';
|
||||
|
||||
interface RouteDependencies {
|
||||
coreUsageData: InternalCoreUsageDataSetup;
|
||||
|
@ -55,6 +55,9 @@ export const registerBulkCreateRoute = (
|
|||
usageStatsClient.incrementSavedObjectsBulkCreate({ request: req }).catch(() => {});
|
||||
|
||||
const { savedObjects } = await context.core;
|
||||
|
||||
const typesToCheck = [...new Set(req.body.map(({ type }) => type))];
|
||||
throwIfAnyTypeNotVisibleByAPI(typesToCheck, savedObjects.typeRegistry);
|
||||
const result = await savedObjects.client.bulkCreate(req.body, { overwrite });
|
||||
return res.ok({ body: result });
|
||||
})
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import { schema } from '@kbn/config-schema';
|
||||
import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal';
|
||||
import type { InternalSavedObjectRouter } from '../internal_types';
|
||||
import { catchAndReturnBoomErrors } from './utils';
|
||||
import { catchAndReturnBoomErrors, throwIfAnyTypeNotVisibleByAPI } from './utils';
|
||||
|
||||
interface RouteDependencies {
|
||||
coreUsageData: InternalCoreUsageDataSetup;
|
||||
|
@ -41,6 +41,9 @@ export const registerBulkDeleteRoute = (
|
|||
|
||||
const { savedObjects } = await context.core;
|
||||
|
||||
const typesToCheck = [...new Set(req.body.map(({ type }) => type))];
|
||||
throwIfAnyTypeNotVisibleByAPI(typesToCheck, savedObjects.typeRegistry);
|
||||
|
||||
const statuses = await savedObjects.client.bulkDelete(req.body, { force });
|
||||
return res.ok({ body: statuses });
|
||||
})
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import { schema } from '@kbn/config-schema';
|
||||
import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal';
|
||||
import type { InternalSavedObjectRouter } from '../internal_types';
|
||||
import { catchAndReturnBoomErrors } from './utils';
|
||||
import { catchAndReturnBoomErrors, throwIfAnyTypeNotVisibleByAPI } from './utils';
|
||||
|
||||
interface RouteDependencies {
|
||||
coreUsageData: InternalCoreUsageDataSetup;
|
||||
|
@ -38,6 +38,9 @@ export const registerBulkGetRoute = (
|
|||
usageStatsClient.incrementSavedObjectsBulkGet({ request: req }).catch(() => {});
|
||||
|
||||
const { savedObjects } = await context.core;
|
||||
const typesToCheck = [...new Set(req.body.map(({ type }) => type))];
|
||||
throwIfAnyTypeNotVisibleByAPI(typesToCheck, savedObjects.typeRegistry);
|
||||
|
||||
const result = await savedObjects.client.bulkGet(req.body);
|
||||
return res.ok({ body: result });
|
||||
})
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import { schema } from '@kbn/config-schema';
|
||||
import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal';
|
||||
import type { InternalSavedObjectRouter } from '../internal_types';
|
||||
import { catchAndReturnBoomErrors } from './utils';
|
||||
import { catchAndReturnBoomErrors, throwIfAnyTypeNotVisibleByAPI } from './utils';
|
||||
|
||||
interface RouteDependencies {
|
||||
coreUsageData: InternalCoreUsageDataSetup;
|
||||
|
@ -36,6 +36,8 @@ export const registerBulkResolveRoute = (
|
|||
usageStatsClient.incrementSavedObjectsBulkResolve({ request: req }).catch(() => {});
|
||||
|
||||
const { savedObjects } = await context.core;
|
||||
const typesToCheck = [...new Set(req.body.map(({ type }) => type))];
|
||||
throwIfAnyTypeNotVisibleByAPI(typesToCheck, savedObjects.typeRegistry);
|
||||
const result = await savedObjects.client.bulkResolve(req.body);
|
||||
return res.ok({ body: result });
|
||||
})
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import { schema } from '@kbn/config-schema';
|
||||
import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal';
|
||||
import type { InternalSavedObjectRouter } from '../internal_types';
|
||||
import { catchAndReturnBoomErrors } from './utils';
|
||||
import { catchAndReturnBoomErrors, throwIfAnyTypeNotVisibleByAPI } from './utils';
|
||||
|
||||
interface RouteDependencies {
|
||||
coreUsageData: InternalCoreUsageDataSetup;
|
||||
|
@ -48,6 +48,10 @@ export const registerBulkUpdateRoute = (
|
|||
usageStatsClient.incrementSavedObjectsBulkUpdate({ request: req }).catch(() => {});
|
||||
|
||||
const { savedObjects } = await context.core;
|
||||
|
||||
const typesToCheck = [...new Set(req.body.map(({ type }) => type))];
|
||||
throwIfAnyTypeNotVisibleByAPI(typesToCheck, savedObjects.typeRegistry);
|
||||
|
||||
const savedObject = await savedObjects.client.bulkUpdate(req.body);
|
||||
return res.ok({ body: savedObject });
|
||||
})
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import { schema } from '@kbn/config-schema';
|
||||
import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal';
|
||||
import type { InternalSavedObjectRouter } from '../internal_types';
|
||||
import { catchAndReturnBoomErrors } from './utils';
|
||||
import { catchAndReturnBoomErrors, throwIfTypeNotVisibleByAPI } from './utils';
|
||||
|
||||
interface RouteDependencies {
|
||||
coreUsageData: InternalCoreUsageDataSetup;
|
||||
|
@ -56,6 +56,10 @@ export const registerCreateRoute = (
|
|||
const usageStatsClient = coreUsageData.getClient();
|
||||
usageStatsClient.incrementSavedObjectsCreate({ request: req }).catch(() => {});
|
||||
|
||||
const { savedObjects } = await context.core;
|
||||
|
||||
throwIfTypeNotVisibleByAPI(type, savedObjects.typeRegistry);
|
||||
|
||||
const options = {
|
||||
id,
|
||||
overwrite,
|
||||
|
@ -64,7 +68,6 @@ export const registerCreateRoute = (
|
|||
references,
|
||||
initialNamespaces,
|
||||
};
|
||||
const { savedObjects } = await context.core;
|
||||
const result = await savedObjects.client.create(type, attributes, options);
|
||||
return res.ok({ body: result });
|
||||
})
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import { schema } from '@kbn/config-schema';
|
||||
import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal';
|
||||
import type { InternalSavedObjectRouter } from '../internal_types';
|
||||
import { catchAndReturnBoomErrors } from './utils';
|
||||
import { catchAndReturnBoomErrors, throwIfTypeNotVisibleByAPI } from './utils';
|
||||
|
||||
interface RouteDependencies {
|
||||
coreUsageData: InternalCoreUsageDataSetup;
|
||||
|
@ -35,10 +35,11 @@ export const registerDeleteRoute = (
|
|||
catchAndReturnBoomErrors(async (context, req, res) => {
|
||||
const { type, id } = req.params;
|
||||
const { force } = req.query;
|
||||
const { getClient } = (await context.core).savedObjects;
|
||||
const { getClient, typeRegistry } = (await context.core).savedObjects;
|
||||
|
||||
const usageStatsClient = coreUsageData.getClient();
|
||||
usageStatsClient.incrementSavedObjectsDelete({ request: req }).catch(() => {});
|
||||
throwIfTypeNotVisibleByAPI(type, typeRegistry);
|
||||
|
||||
const client = getClient();
|
||||
const result = await client.delete(type, id, { force });
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import { schema } from '@kbn/config-schema';
|
||||
import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal';
|
||||
import type { InternalSavedObjectRouter } from '../internal_types';
|
||||
import { catchAndReturnBoomErrors } from './utils';
|
||||
import { catchAndReturnBoomErrors, throwOnHttpHiddenTypes } from './utils';
|
||||
|
||||
interface RouteDependencies {
|
||||
coreUsageData: InternalCoreUsageDataSetup;
|
||||
|
@ -67,7 +67,7 @@ export const registerFindRoute = (
|
|||
const usageStatsClient = coreUsageData.getClient();
|
||||
usageStatsClient.incrementSavedObjectsFind({ request: req }).catch(() => {});
|
||||
|
||||
// manually validation to avoid using JSON.parse twice
|
||||
// manually validate to avoid using JSON.parse twice
|
||||
let aggs;
|
||||
if (query.aggs) {
|
||||
try {
|
||||
|
@ -81,10 +81,25 @@ export const registerFindRoute = (
|
|||
}
|
||||
}
|
||||
const { savedObjects } = await context.core;
|
||||
|
||||
// check if registered type(s)are exposed to the global SO Http API's.
|
||||
const findForTypes = Array.isArray(query.type) ? query.type : [query.type];
|
||||
|
||||
const unsupportedTypes = [...new Set(findForTypes)].filter((tname) => {
|
||||
const fullType = savedObjects.typeRegistry.getType(tname);
|
||||
// pass unknown types through to the registry to handle
|
||||
if (!fullType?.hidden && fullType?.hiddenFromHttpApis) {
|
||||
return fullType.name;
|
||||
}
|
||||
});
|
||||
if (unsupportedTypes.length > 0) {
|
||||
throwOnHttpHiddenTypes(unsupportedTypes);
|
||||
}
|
||||
|
||||
const result = await savedObjects.client.find({
|
||||
perPage: query.per_page,
|
||||
page: query.page,
|
||||
type: Array.isArray(query.type) ? query.type : [query.type],
|
||||
type: findForTypes,
|
||||
search: query.search,
|
||||
defaultSearchOperator: query.default_search_operator,
|
||||
searchFields:
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import { schema } from '@kbn/config-schema';
|
||||
import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal';
|
||||
import type { InternalSavedObjectRouter } from '../internal_types';
|
||||
import { catchAndReturnBoomErrors } from './utils';
|
||||
import { catchAndReturnBoomErrors, throwIfTypeNotVisibleByAPI } from './utils';
|
||||
|
||||
interface RouteDependencies {
|
||||
coreUsageData: InternalCoreUsageDataSetup;
|
||||
|
@ -36,6 +36,8 @@ export const registerGetRoute = (
|
|||
usageStatsClient.incrementSavedObjectsGet({ request: req }).catch(() => {});
|
||||
|
||||
const { savedObjects } = await context.core;
|
||||
throwIfTypeNotVisibleByAPI(type, savedObjects.typeRegistry);
|
||||
|
||||
const object = await savedObjects.client.get(type, id);
|
||||
return res.ok({ body: object });
|
||||
})
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import { schema } from '@kbn/config-schema';
|
||||
import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal';
|
||||
import type { InternalSavedObjectRouter } from '../internal_types';
|
||||
import { throwIfTypeNotVisibleByAPI } from './utils';
|
||||
|
||||
interface RouteDependencies {
|
||||
coreUsageData: InternalCoreUsageDataSetup;
|
||||
|
@ -35,6 +36,8 @@ export const registerResolveRoute = (
|
|||
const usageStatsClient = coreUsageData.getClient();
|
||||
usageStatsClient.incrementSavedObjectsResolve({ request: req }).catch(() => {});
|
||||
|
||||
throwIfTypeNotVisibleByAPI(type, savedObjects.typeRegistry);
|
||||
|
||||
const result = await savedObjects.client.resolve(type, id);
|
||||
return res.ok({ body: result });
|
||||
})
|
||||
|
|
|
@ -10,7 +10,7 @@ import { schema } from '@kbn/config-schema';
|
|||
import type { SavedObjectsUpdateOptions } from '@kbn/core-saved-objects-api-server';
|
||||
import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal';
|
||||
import type { InternalSavedObjectRouter } from '../internal_types';
|
||||
import { catchAndReturnBoomErrors } from './utils';
|
||||
import { catchAndReturnBoomErrors, throwIfTypeNotVisibleByAPI } from './utils';
|
||||
|
||||
interface RouteDependencies {
|
||||
coreUsageData: InternalCoreUsageDataSetup;
|
||||
|
@ -51,8 +51,10 @@ export const registerUpdateRoute = (
|
|||
|
||||
const usageStatsClient = coreUsageData.getClient();
|
||||
usageStatsClient.incrementSavedObjectsUpdate({ request: req }).catch(() => {});
|
||||
|
||||
const { savedObjects } = await context.core;
|
||||
|
||||
throwIfTypeNotVisibleByAPI(type, savedObjects.typeRegistry);
|
||||
|
||||
const result = await savedObjects.client.update(type, id, attributes, options);
|
||||
return res.ok({ body: result });
|
||||
})
|
||||
|
|
|
@ -6,7 +6,15 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { createSavedObjectsStreamFromNdJson, validateTypes, validateObjects } from './utils';
|
||||
import {
|
||||
createSavedObjectsStreamFromNdJson,
|
||||
validateTypes,
|
||||
validateObjects,
|
||||
throwOnHttpHiddenTypes,
|
||||
throwOnGloballyHiddenTypes,
|
||||
throwIfTypeNotVisibleByAPI,
|
||||
throwIfAnyTypeNotVisibleByAPI,
|
||||
} from './utils';
|
||||
import { Readable } from 'stream';
|
||||
import { createPromiseFromStreams, createConcatStream } from '@kbn/utils';
|
||||
import { catchAndReturnBoomErrors } from './utils';
|
||||
|
@ -18,6 +26,7 @@ import type {
|
|||
KibanaResponseFactory,
|
||||
} from '@kbn/core-http-server';
|
||||
import { kibanaResponseFactory } from '@kbn/core-http-router-server-internal';
|
||||
import { typeRegistryInstanceMock } from '../saved_objects_service.test.mocks';
|
||||
|
||||
async function readStreamToCompletion(stream: Readable) {
|
||||
return createPromiseFromStreams([stream, createConcatStream([])]);
|
||||
|
@ -236,3 +245,99 @@ describe('catchAndReturnBoomErrors', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('throwOnHttpHiddenTypes', () => {
|
||||
it('should throw on types hidden from the HTTP Apis', () => {
|
||||
expect(() => {
|
||||
throwOnHttpHiddenTypes(['not-allowed-type']);
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unsupported saved object type(s): not-allowed-type: Bad Request"`
|
||||
);
|
||||
expect(() => {
|
||||
throwOnHttpHiddenTypes(['index-pattern', 'not-allowed-type', 'not-allowed-type-2']);
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unsupported saved object type(s): index-pattern, not-allowed-type, not-allowed-type-2: Bad Request"`
|
||||
);
|
||||
});
|
||||
it("returns if there aren't any types provided to check", () => {
|
||||
expect(() => {
|
||||
throwOnHttpHiddenTypes([]);
|
||||
}).not.toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('throwOnGloballyHiddenTypes', () => {
|
||||
const httpVisibleTypes = ['config', 'index-pattern', 'dashboard'];
|
||||
|
||||
it('throws if some objects are not globally visible', () => {
|
||||
expect(() => {
|
||||
throwOnGloballyHiddenTypes(httpVisibleTypes, ['not-allowed-type']);
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unsupported saved object type(s): not-allowed-type: Bad Request"`
|
||||
);
|
||||
});
|
||||
|
||||
it("returns if there aren't any types provided to check", () => {
|
||||
expect(() => {
|
||||
throwOnGloballyHiddenTypes(httpVisibleTypes, []);
|
||||
}).not.toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('throwIfTypeNotVisibleByAPI', () => {
|
||||
const registry = typeRegistryInstanceMock;
|
||||
registry.getType.mockImplementation((name: string) => {
|
||||
return {
|
||||
name,
|
||||
hidden: name === 'hidden' ? true : false,
|
||||
hiddenFromHttpApis: name === 'hiddenFromHttpApis' ? true : false,
|
||||
namespaceType: 'multiple-isolated',
|
||||
mappings: { properties: {} },
|
||||
};
|
||||
});
|
||||
|
||||
it('throws if a type is not visible by to the HTTP APIs', () => {
|
||||
expect(() =>
|
||||
throwIfTypeNotVisibleByAPI('hiddenFromHttpApis', registry)
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unsupported saved object type: 'hiddenFromHttpApis': Bad Request"`
|
||||
);
|
||||
});
|
||||
|
||||
it('does not throw when a type is not hidden from the HTTP APIS', () => {
|
||||
expect(() => throwIfTypeNotVisibleByAPI('hidden', registry)).not.toThrowError();
|
||||
});
|
||||
|
||||
it('does not throw on visible types', () => {
|
||||
expect(() => throwIfTypeNotVisibleByAPI('config', registry)).not.toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('throwIfAnyTypeNotVisibleByAPI', () => {
|
||||
const registry = typeRegistryInstanceMock;
|
||||
registry.getType.mockImplementation((name: string) => {
|
||||
return {
|
||||
name,
|
||||
hidden: name === 'hidden' ? true : false,
|
||||
hiddenFromHttpApis: name === 'hiddenFromHttpApis' ? true : false,
|
||||
namespaceType: 'multiple-isolated',
|
||||
mappings: { properties: {} },
|
||||
};
|
||||
});
|
||||
|
||||
it('throws if the provided types contains any that are not visible by to the HTTP APIs', () => {
|
||||
expect(() =>
|
||||
throwIfAnyTypeNotVisibleByAPI(['hiddenFromHttpApis'], registry)
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unsupported saved object type(s): hiddenFromHttpApis: Bad Request"`
|
||||
);
|
||||
});
|
||||
|
||||
it('does not throw when a type is not hidden from the HTTP APIS', () => {
|
||||
expect(() => throwIfAnyTypeNotVisibleByAPI(['hidden'], registry)).not.toThrowError();
|
||||
});
|
||||
|
||||
it('does not throw on visible types', () => {
|
||||
expect(() => throwIfAnyTypeNotVisibleByAPI(['config'], registry)).not.toThrowError();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,7 +18,11 @@ import {
|
|||
import Boom from '@hapi/boom';
|
||||
import type { RequestHandlerWrapper } from '@kbn/core-http-server';
|
||||
import type { SavedObject } from '@kbn/core-saved-objects-common';
|
||||
import type { SavedObjectsExportResultDetails } from '@kbn/core-saved-objects-server';
|
||||
import type {
|
||||
ISavedObjectTypeRegistry,
|
||||
SavedObjectsExportResultDetails,
|
||||
} from '@kbn/core-saved-objects-server';
|
||||
import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-utils-server';
|
||||
|
||||
export async function createSavedObjectsStreamFromNdJson(ndJsonStream: Readable) {
|
||||
const savedObjects = await createPromiseFromStreams([
|
||||
|
@ -82,3 +86,69 @@ export const catchAndReturnBoomErrors: RequestHandlerWrapper = (handler) => {
|
|||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string[]} exposedVisibleTypes all registered types with hidden:false and hiddenFromHttpApis:false|undefined
|
||||
* @param {string[]} typesToCheck saved object types provided to the httpApi request
|
||||
*/
|
||||
export function throwOnGloballyHiddenTypes(
|
||||
allHttpApisVisibleTypes: string[],
|
||||
typesToCheck: string[]
|
||||
) {
|
||||
if (!typesToCheck.length) {
|
||||
return;
|
||||
}
|
||||
const denyRequestForTypes = typesToCheck.filter(
|
||||
(type: string) => !allHttpApisVisibleTypes.includes(type)
|
||||
);
|
||||
if (denyRequestForTypes.length > 0) {
|
||||
throw SavedObjectsErrorHelpers.createBadRequestError(
|
||||
`Unsupported saved object type(s): ${denyRequestForTypes.join(', ')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {string[]} unsupportedTypes saved object types registered with hidden=false and hiddenFromHttpApis=true
|
||||
*/
|
||||
|
||||
export function throwOnHttpHiddenTypes(unsupportedTypes: string[]) {
|
||||
if (unsupportedTypes.length > 0) {
|
||||
throw SavedObjectsErrorHelpers.createBadRequestError(
|
||||
`Unsupported saved object type(s): ${unsupportedTypes.join(', ')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {string[]} type saved object type
|
||||
* @param {ISavedObjectTypeRegistry} registry the saved object type registry
|
||||
*/
|
||||
|
||||
export function throwIfTypeNotVisibleByAPI(type: string, registry: ISavedObjectTypeRegistry) {
|
||||
if (!type) return;
|
||||
const fullType = registry.getType(type);
|
||||
if (!fullType?.hidden && fullType?.hiddenFromHttpApis) {
|
||||
throw SavedObjectsErrorHelpers.createUnsupportedTypeError(type);
|
||||
}
|
||||
}
|
||||
|
||||
export function throwIfAnyTypeNotVisibleByAPI(
|
||||
typesToCheck: string[],
|
||||
registry: ISavedObjectTypeRegistry
|
||||
) {
|
||||
const unsupportedTypes = typesToCheck.filter((tname) => {
|
||||
const fullType = registry.getType(tname);
|
||||
if (!fullType?.hidden && fullType?.hiddenFromHttpApis) {
|
||||
return fullType.name;
|
||||
}
|
||||
});
|
||||
if (unsupportedTypes.length > 0) {
|
||||
throwOnHttpHiddenTypes(unsupportedTypes);
|
||||
}
|
||||
}
|
||||
export interface BulkGetItem {
|
||||
type: string;
|
||||
id: string;
|
||||
fields?: string[];
|
||||
namespaces?: string[];
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@
|
|||
"@kbn/core-elasticsearch-server-mocks",
|
||||
"@kbn/utils",
|
||||
"@kbn/core-http-router-server-internal",
|
||||
"@kbn/core-saved-objects-utils-server",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -33,6 +33,14 @@ export interface SavedObjectsType<Attributes = any> {
|
|||
* See {@link SavedObjectsServiceStart.createInternalRepository | createInternalRepository}.
|
||||
*/
|
||||
hidden: boolean;
|
||||
/**
|
||||
* Is the type hidden from the http APIs. If `hiddenFromHttpApis:true`, repositories will have access to the type but the type is not exposed via the HTTP APIs.
|
||||
* It is recommended to hide types registered with 'hidden=false' from the httpApis for backward compatibility in the HTTP layer.
|
||||
*
|
||||
* @remarks Setting this property for hidden types is not recommended and will fail validation if set to `false`.
|
||||
* @internalRemarks Using 'hiddenFromHttpApis' is an alternative to registering types as `hidden:true` to hide a type from the HTTP APIs without effecting repositories access.
|
||||
*/
|
||||
hiddenFromHttpApis?: boolean;
|
||||
/**
|
||||
* The {@link SavedObjectsNamespaceType | namespace type} for the type.
|
||||
*/
|
||||
|
|
|
@ -24,6 +24,13 @@ export interface ISavedObjectTypeRegistry {
|
|||
*/
|
||||
getVisibleTypes(): SavedObjectsType[];
|
||||
|
||||
/**
|
||||
* Returns all visible {@link SavedObjectsType | types} exposed to the global SO HTTP APIs
|
||||
*
|
||||
* A visibleToHttpApis type is a type that doesn't explicitly define `hidden=true` nor `hiddenFromHttpApis=true` during registration.
|
||||
*/
|
||||
getVisibleToHttpApisTypes(): SavedObjectsType[];
|
||||
|
||||
/**
|
||||
* Return all {@link SavedObjectsType | types} currently registered, including the hidden ones.
|
||||
*
|
||||
|
@ -66,6 +73,11 @@ export interface ISavedObjectTypeRegistry {
|
|||
*/
|
||||
isHidden(type: string): boolean;
|
||||
|
||||
/**
|
||||
* Returns the `hiddenFromHttpApis` property for a given type, or `false` if
|
||||
* the type is not registered
|
||||
*/
|
||||
isHiddenFromHttpApis(type: string): boolean;
|
||||
/**
|
||||
* Returns the `indexPattern` property for given type, or `undefined` if
|
||||
* the type is not registered.
|
||||
|
|
|
@ -8,3 +8,4 @@
|
|||
|
||||
export { setupServer } from './src/setup_server';
|
||||
export { createExportableType } from './src/create_exportable_type';
|
||||
export { createHiddenTypeVariants } from './src/create_hidden_type_variants';
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 { SavedObjectsType } from '@kbn/core-saved-objects-server';
|
||||
|
||||
export const createHiddenTypeVariants = (createOptions: {
|
||||
name: string;
|
||||
hide?: boolean;
|
||||
hideFromHttpApis?: boolean;
|
||||
}): SavedObjectsType => {
|
||||
return {
|
||||
name: createOptions.name,
|
||||
hidden: createOptions.hide ?? false,
|
||||
hiddenFromHttpApis: createOptions.hideFromHttpApis ?? undefined,
|
||||
namespaceType: createOptions.name === 'index-pattern' ? 'multiple' : 'single',
|
||||
mappings: {
|
||||
properties: {},
|
||||
},
|
||||
};
|
||||
};
|
|
@ -18,10 +18,15 @@ import {
|
|||
registerBulkCreateRoute,
|
||||
type InternalSavedObjectsRequestHandlerContext,
|
||||
} from '@kbn/core-saved-objects-server-internal';
|
||||
import { setupServer } from '@kbn/core-test-helpers-test-utils';
|
||||
import { createHiddenTypeVariants, setupServer } from '@kbn/core-test-helpers-test-utils';
|
||||
|
||||
type SetupServerReturn = Awaited<ReturnType<typeof setupServer>>;
|
||||
|
||||
const testTypes = [
|
||||
{ name: 'index-pattern', hide: false },
|
||||
{ name: 'hidden-from-http', hide: false, hideFromHttpApis: true },
|
||||
];
|
||||
|
||||
describe('POST /api/saved_objects/_bulk_create', () => {
|
||||
let server: SetupServerReturn['server'];
|
||||
let httpSetup: SetupServerReturn['httpSetup'];
|
||||
|
@ -34,6 +39,12 @@ describe('POST /api/saved_objects/_bulk_create', () => {
|
|||
savedObjectsClient = handlerContext.savedObjects.client;
|
||||
savedObjectsClient.bulkCreate.mockResolvedValue({ saved_objects: [] });
|
||||
|
||||
handlerContext.savedObjects.typeRegistry.getType.mockImplementation((typename: string) => {
|
||||
return testTypes
|
||||
.map((typeDesc) => createHiddenTypeVariants(typeDesc))
|
||||
.find((fullTest) => fullTest.name === typename);
|
||||
});
|
||||
|
||||
const router =
|
||||
httpSetup.createRouter<InternalSavedObjectsRequestHandlerContext>('/api/saved_objects/');
|
||||
coreUsageStatsClient = coreUsageStatsClientMock.create();
|
||||
|
@ -131,4 +142,23 @@ describe('POST /api/saved_objects/_bulk_create', () => {
|
|||
const args = savedObjectsClient.bulkCreate.mock.calls[0];
|
||||
expect(args[1]).toEqual({ overwrite: true });
|
||||
});
|
||||
|
||||
it('returns with status 400 when a type is hidden from the HTTP APIs', async () => {
|
||||
const result = await supertest(httpSetup.server.listener)
|
||||
.post('/api/saved_objects/_bulk_create')
|
||||
.send([
|
||||
{
|
||||
id: 'hiddenID',
|
||||
type: 'hidden-from-http',
|
||||
attributes: {
|
||||
title: 'bar',
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
])
|
||||
.expect(400);
|
||||
expect(result.body.message).toContain(
|
||||
'Unsupported saved object type(s): hidden-from-http: Bad Request'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
coreUsageStatsClientMock,
|
||||
coreUsageDataServiceMock,
|
||||
} from '@kbn/core-usage-data-server-mocks';
|
||||
import { setupServer } from '@kbn/core-test-helpers-test-utils';
|
||||
import { createHiddenTypeVariants, setupServer } from '@kbn/core-test-helpers-test-utils';
|
||||
import {
|
||||
registerBulkDeleteRoute,
|
||||
type InternalSavedObjectsRequestHandlerContext,
|
||||
|
@ -21,6 +21,11 @@ import {
|
|||
|
||||
type SetupServerReturn = Awaited<ReturnType<typeof setupServer>>;
|
||||
|
||||
const testTypes = [
|
||||
{ name: 'index-pattern', hide: false },
|
||||
{ name: 'hidden-from-http', hide: false, hideFromHttpApis: true },
|
||||
];
|
||||
|
||||
describe('POST /api/saved_objects/_bulk_delete', () => {
|
||||
let server: SetupServerReturn['server'];
|
||||
let httpSetup: SetupServerReturn['httpSetup'];
|
||||
|
@ -35,6 +40,13 @@ describe('POST /api/saved_objects/_bulk_delete', () => {
|
|||
savedObjectsClient.bulkDelete.mockResolvedValue({
|
||||
statuses: [],
|
||||
});
|
||||
|
||||
handlerContext.savedObjects.typeRegistry.getType.mockImplementation((typename: string) => {
|
||||
return testTypes
|
||||
.map((typeDesc) => createHiddenTypeVariants(typeDesc))
|
||||
.find((fullTest) => fullTest.name === typename);
|
||||
});
|
||||
|
||||
const router =
|
||||
httpSetup.createRouter<InternalSavedObjectsRequestHandlerContext>('/api/saved_objects/');
|
||||
coreUsageStatsClient = coreUsageStatsClientMock.create();
|
||||
|
@ -94,4 +106,31 @@ describe('POST /api/saved_objects/_bulk_delete', () => {
|
|||
expect(savedObjectsClient.bulkDelete).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.bulkDelete).toHaveBeenCalledWith(docs, { force: true });
|
||||
});
|
||||
|
||||
it('returns with status 400 when a type is hidden from the HTTP APIs', async () => {
|
||||
const result = await supertest(httpSetup.server.listener)
|
||||
.post('/api/saved_objects/_bulk_delete')
|
||||
.send([
|
||||
{
|
||||
id: 'hiddenID',
|
||||
type: 'hidden-from-http',
|
||||
},
|
||||
])
|
||||
.expect(400);
|
||||
expect(result.body.message).toContain('Unsupported saved object type(s):');
|
||||
});
|
||||
|
||||
it('returns with status 400 with `force` when a type is hidden from the HTTP APIs', async () => {
|
||||
const result = await supertest(httpSetup.server.listener)
|
||||
.post('/api/saved_objects/_bulk_delete')
|
||||
.send([
|
||||
{
|
||||
id: 'hiddenID',
|
||||
type: 'hidden-from-http',
|
||||
},
|
||||
])
|
||||
.query({ force: true })
|
||||
.expect(400);
|
||||
expect(result.body.message).toContain('Unsupported saved object type(s):');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
coreUsageStatsClientMock,
|
||||
coreUsageDataServiceMock,
|
||||
} from '@kbn/core-usage-data-server-mocks';
|
||||
import { setupServer } from '@kbn/core-test-helpers-test-utils';
|
||||
import { createHiddenTypeVariants, setupServer } from '@kbn/core-test-helpers-test-utils';
|
||||
import {
|
||||
registerBulkGetRoute,
|
||||
type InternalSavedObjectsRequestHandlerContext,
|
||||
|
@ -21,6 +21,11 @@ import {
|
|||
|
||||
type SetupServerReturn = Awaited<ReturnType<typeof setupServer>>;
|
||||
|
||||
const testTypes = [
|
||||
{ name: 'index-pattern', hide: false },
|
||||
{ name: 'hidden-from-http', hide: false, hideFromHttpApis: true },
|
||||
];
|
||||
|
||||
describe('POST /api/saved_objects/_bulk_get', () => {
|
||||
let server: SetupServerReturn['server'];
|
||||
let httpSetup: SetupServerReturn['httpSetup'];
|
||||
|
@ -35,6 +40,12 @@ describe('POST /api/saved_objects/_bulk_get', () => {
|
|||
savedObjectsClient.bulkGet.mockResolvedValue({
|
||||
saved_objects: [],
|
||||
});
|
||||
|
||||
handlerContext.savedObjects.typeRegistry.getType.mockImplementation((typename: string) => {
|
||||
return testTypes
|
||||
.map((typeDesc) => createHiddenTypeVariants(typeDesc))
|
||||
.find((fullTest) => fullTest.name === typename);
|
||||
});
|
||||
const router =
|
||||
httpSetup.createRouter<InternalSavedObjectsRequestHandlerContext>('/api/saved_objects/');
|
||||
coreUsageStatsClient = coreUsageStatsClientMock.create();
|
||||
|
@ -96,4 +107,17 @@ describe('POST /api/saved_objects/_bulk_get', () => {
|
|||
expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith(docs);
|
||||
});
|
||||
|
||||
it('returns with status 400 when a type is hidden from the HTTP APIs', async () => {
|
||||
const result = await supertest(httpSetup.server.listener)
|
||||
.post('/api/saved_objects/_bulk_get')
|
||||
.send([
|
||||
{
|
||||
id: 'hiddenID',
|
||||
type: 'hidden-from-http',
|
||||
},
|
||||
])
|
||||
.expect(400);
|
||||
expect(result.body.message).toContain('Unsupported saved object type(s):');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
coreUsageStatsClientMock,
|
||||
coreUsageDataServiceMock,
|
||||
} from '@kbn/core-usage-data-server-mocks';
|
||||
import { setupServer } from '@kbn/core-test-helpers-test-utils';
|
||||
import { createHiddenTypeVariants, setupServer } from '@kbn/core-test-helpers-test-utils';
|
||||
import {
|
||||
registerBulkResolveRoute,
|
||||
type InternalSavedObjectsRequestHandlerContext,
|
||||
|
@ -21,6 +21,11 @@ import {
|
|||
|
||||
type SetupServerReturn = Awaited<ReturnType<typeof setupServer>>;
|
||||
|
||||
const testTypes = [
|
||||
{ name: 'index-pattern', hide: false },
|
||||
{ name: 'hidden-from-http', hide: false, hideFromHttpApis: true },
|
||||
];
|
||||
|
||||
describe('POST /api/saved_objects/_bulk_resolve', () => {
|
||||
let server: SetupServerReturn['server'];
|
||||
let httpSetup: SetupServerReturn['httpSetup'];
|
||||
|
@ -35,6 +40,13 @@ describe('POST /api/saved_objects/_bulk_resolve', () => {
|
|||
savedObjectsClient.bulkResolve.mockResolvedValue({
|
||||
resolved_objects: [],
|
||||
});
|
||||
|
||||
handlerContext.savedObjects.typeRegistry.getType.mockImplementation((typename: string) => {
|
||||
return testTypes
|
||||
.map((typeDesc) => createHiddenTypeVariants(typeDesc))
|
||||
.find((fullTest) => fullTest.name === typename);
|
||||
});
|
||||
|
||||
const router =
|
||||
httpSetup.createRouter<InternalSavedObjectsRequestHandlerContext>('/api/saved_objects/');
|
||||
coreUsageStatsClient = coreUsageStatsClientMock.create();
|
||||
|
@ -99,4 +111,17 @@ describe('POST /api/saved_objects/_bulk_resolve', () => {
|
|||
expect(savedObjectsClient.bulkResolve).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.bulkResolve).toHaveBeenCalledWith(docs);
|
||||
});
|
||||
|
||||
it('returns with status 400 when a type is hidden from the HTTP APIs', async () => {
|
||||
const result = await supertest(httpSetup.server.listener)
|
||||
.post('/api/saved_objects/_bulk_resolve')
|
||||
.send([
|
||||
{
|
||||
id: 'hiddenID',
|
||||
type: 'hidden-from-http',
|
||||
},
|
||||
])
|
||||
.expect(400);
|
||||
expect(result.body.message).toContain('Unsupported saved object type(s):');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
coreUsageStatsClientMock,
|
||||
coreUsageDataServiceMock,
|
||||
} from '@kbn/core-usage-data-server-mocks';
|
||||
import { setupServer } from '@kbn/core-test-helpers-test-utils';
|
||||
import { createHiddenTypeVariants, setupServer } from '@kbn/core-test-helpers-test-utils';
|
||||
import {
|
||||
registerBulkUpdateRoute,
|
||||
type InternalSavedObjectsRequestHandlerContext,
|
||||
|
@ -21,6 +21,13 @@ import {
|
|||
|
||||
type SetupServerReturn = Awaited<ReturnType<typeof setupServer>>;
|
||||
|
||||
const testTypes = [
|
||||
{ name: 'visualization', hide: false },
|
||||
{ name: 'dashboard', hide: false },
|
||||
{ name: 'index-pattern', hide: false },
|
||||
{ name: 'hidden-from-http', hide: false, hideFromHttpApis: true },
|
||||
];
|
||||
|
||||
describe('PUT /api/saved_objects/_bulk_update', () => {
|
||||
let server: SetupServerReturn['server'];
|
||||
let httpSetup: SetupServerReturn['httpSetup'];
|
||||
|
@ -30,8 +37,15 @@ describe('PUT /api/saved_objects/_bulk_update', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
({ server, httpSetup, handlerContext } = await setupServer());
|
||||
|
||||
savedObjectsClient = handlerContext.savedObjects.client;
|
||||
|
||||
handlerContext.savedObjects.typeRegistry.getType.mockImplementation((typename: string) => {
|
||||
return testTypes
|
||||
.map((typeDesc) => createHiddenTypeVariants(typeDesc))
|
||||
.find((fullTest) => fullTest.name === typename);
|
||||
});
|
||||
|
||||
const router =
|
||||
httpSetup.createRouter<InternalSavedObjectsRequestHandlerContext>('/api/saved_objects/');
|
||||
coreUsageStatsClient = coreUsageStatsClientMock.create();
|
||||
|
@ -139,4 +153,20 @@ describe('PUT /api/saved_objects/_bulk_update', () => {
|
|||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns with status 400 when a type is hidden from the HTTP APIs', async () => {
|
||||
const result = await supertest(httpSetup.server.listener)
|
||||
.put('/api/saved_objects/_bulk_update')
|
||||
.send([
|
||||
{
|
||||
type: 'hidden-from-http',
|
||||
id: 'hiddenID',
|
||||
attributes: {
|
||||
title: 'bar',
|
||||
},
|
||||
},
|
||||
])
|
||||
.expect(400);
|
||||
expect(result.body.message).toContain('Unsupported saved object type(s):');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
coreUsageStatsClientMock,
|
||||
coreUsageDataServiceMock,
|
||||
} from '@kbn/core-usage-data-server-mocks';
|
||||
import { setupServer } from '@kbn/core-test-helpers-test-utils';
|
||||
import { setupServer, createHiddenTypeVariants } from '@kbn/core-test-helpers-test-utils';
|
||||
import {
|
||||
registerCreateRoute,
|
||||
type InternalSavedObjectsRequestHandlerContext,
|
||||
|
@ -21,6 +21,11 @@ import {
|
|||
|
||||
type SetupServerReturn = Awaited<ReturnType<typeof setupServer>>;
|
||||
|
||||
const testTypes = [
|
||||
{ name: 'index-pattern', hide: false },
|
||||
{ name: 'hidden-type', hide: true },
|
||||
{ name: 'hidden-from-http', hide: false, hideFromHttpApis: true },
|
||||
];
|
||||
describe('POST /api/saved_objects/{type}', () => {
|
||||
let server: SetupServerReturn['server'];
|
||||
let httpSetup: SetupServerReturn['httpSetup'];
|
||||
|
@ -49,6 +54,12 @@ describe('POST /api/saved_objects/{type}', () => {
|
|||
const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient);
|
||||
registerCreateRoute(router, { coreUsageData });
|
||||
|
||||
handlerContext.savedObjects.typeRegistry.getType.mockImplementation((typename: string) => {
|
||||
return testTypes
|
||||
.map((typeDesc) => createHiddenTypeVariants(typeDesc))
|
||||
.find((fullTest) => fullTest.name === typename);
|
||||
});
|
||||
|
||||
await server.start();
|
||||
});
|
||||
|
||||
|
@ -121,4 +132,17 @@ describe('POST /api/saved_objects/{type}', () => {
|
|||
{ overwrite: false, id: 'logstash-*' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns with status 400 if the type is hidden from the HTTP APIs', async () => {
|
||||
const result = await supertest(httpSetup.server.listener)
|
||||
.post('/api/saved_objects/hidden-from-http')
|
||||
.send({
|
||||
attributes: {
|
||||
properties: {},
|
||||
},
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
expect(result.body.message).toContain("Unsupported saved object type: 'hidden-from-http'");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
coreUsageStatsClientMock,
|
||||
coreUsageDataServiceMock,
|
||||
} from '@kbn/core-usage-data-server-mocks';
|
||||
import { setupServer } from '@kbn/core-test-helpers-test-utils';
|
||||
import { createHiddenTypeVariants, setupServer } from '@kbn/core-test-helpers-test-utils';
|
||||
import {
|
||||
registerDeleteRoute,
|
||||
type InternalSavedObjectsRequestHandlerContext,
|
||||
|
@ -21,6 +21,12 @@ import {
|
|||
|
||||
type SetupServerReturn = Awaited<ReturnType<typeof setupServer>>;
|
||||
|
||||
const testTypes = [
|
||||
{ name: 'index-pattern', hide: false },
|
||||
{ name: 'hidden-type', hide: true },
|
||||
{ name: 'hidden-from-http', hide: false, hideFromHttpApis: true },
|
||||
];
|
||||
|
||||
describe('DELETE /api/saved_objects/{type}/{id}', () => {
|
||||
let server: SetupServerReturn['server'];
|
||||
let httpSetup: SetupServerReturn['httpSetup'];
|
||||
|
@ -32,6 +38,11 @@ describe('DELETE /api/saved_objects/{type}/{id}', () => {
|
|||
({ server, httpSetup, handlerContext } = await setupServer());
|
||||
savedObjectsClient = handlerContext.savedObjects.getClient();
|
||||
handlerContext.savedObjects.getClient = jest.fn().mockImplementation(() => savedObjectsClient);
|
||||
handlerContext.savedObjects.typeRegistry.getType.mockImplementation((typename: string) => {
|
||||
return testTypes
|
||||
.map((typeDesc) => createHiddenTypeVariants(typeDesc))
|
||||
.find((fullTest) => fullTest.name === typename);
|
||||
});
|
||||
|
||||
const router =
|
||||
httpSetup.createRouter<InternalSavedObjectsRequestHandlerContext>('/api/saved_objects/');
|
||||
|
@ -78,4 +89,19 @@ describe('DELETE /api/saved_objects/{type}/{id}', () => {
|
|||
force: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns with status 400 if a type is hidden from the HTTP APIs', async () => {
|
||||
const result = await supertest(httpSetup.server.listener)
|
||||
.delete('/api/saved_objects/hidden-from-http/hiddenId')
|
||||
.expect(400);
|
||||
expect(result.body.message).toContain("Unsupported saved object type: 'hidden-from-http'");
|
||||
});
|
||||
|
||||
it('returns with status 400 if a type is hidden from the HTTP APIs with `force` option', async () => {
|
||||
const result = await supertest(httpSetup.server.listener)
|
||||
.delete('/api/saved_objects/hidden-from-http/hiddenId')
|
||||
.query({ force: true })
|
||||
.expect(400);
|
||||
expect(result.body.message).toContain("Unsupported saved object type: 'hidden-from-http'");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
coreUsageStatsClientMock,
|
||||
coreUsageDataServiceMock,
|
||||
} from '@kbn/core-usage-data-server-mocks';
|
||||
import { setupServer } from '@kbn/core-test-helpers-test-utils';
|
||||
import { createHiddenTypeVariants, setupServer } from '@kbn/core-test-helpers-test-utils';
|
||||
import {
|
||||
registerFindRoute,
|
||||
type InternalSavedObjectsRequestHandlerContext,
|
||||
|
@ -23,6 +23,15 @@ import {
|
|||
|
||||
type SetupServerReturn = Awaited<ReturnType<typeof setupServer>>;
|
||||
|
||||
const testTypes = [
|
||||
{ name: 'index-pattern', hide: false },
|
||||
{ name: 'visualization', hide: false },
|
||||
{ name: 'dashboard', hide: false },
|
||||
{ name: 'foo', hide: false },
|
||||
{ name: 'bar', hide: false },
|
||||
{ name: 'hidden-type', hide: true },
|
||||
{ name: 'hidden-from-http', hide: false, hideFromHttpApis: true },
|
||||
];
|
||||
describe('GET /api/saved_objects/_find', () => {
|
||||
let server: SetupServerReturn['server'];
|
||||
let httpSetup: SetupServerReturn['httpSetup'];
|
||||
|
@ -39,6 +48,13 @@ describe('GET /api/saved_objects/_find', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
({ server, httpSetup, handlerContext } = await setupServer());
|
||||
|
||||
handlerContext.savedObjects.typeRegistry.getType.mockImplementation((typename: string) => {
|
||||
return testTypes
|
||||
.map((typeDesc) => createHiddenTypeVariants(typeDesc))
|
||||
.find((fullTest) => fullTest.name === typename);
|
||||
});
|
||||
|
||||
savedObjectsClient = handlerContext.savedObjects.client;
|
||||
|
||||
savedObjectsClient.find.mockResolvedValue(clientResponse);
|
||||
|
@ -67,6 +83,33 @@ describe('GET /api/saved_objects/_find', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('returns with status 400 when type is hidden from the HTTP APIs', async () => {
|
||||
const findResponse = {
|
||||
error: 'Bad Request',
|
||||
message: 'Unsupported saved object type(s): hidden-from-http: Bad Request',
|
||||
statusCode: 400,
|
||||
};
|
||||
const result = await supertest(httpSetup.server.listener)
|
||||
.get('/api/saved_objects/_find?type=hidden-from-http')
|
||||
.expect(400);
|
||||
|
||||
expect(result.body).toEqual(findResponse);
|
||||
});
|
||||
|
||||
it('returns with status 200 when type is hidden', async () => {
|
||||
const findResponse = {
|
||||
total: 0,
|
||||
per_page: 0,
|
||||
page: 0,
|
||||
saved_objects: [],
|
||||
};
|
||||
const result = await supertest(httpSetup.server.listener)
|
||||
.get('/api/saved_objects/_find?type=hidden-type')
|
||||
.expect(200);
|
||||
|
||||
expect(result.body).toEqual(findResponse);
|
||||
});
|
||||
|
||||
it('formats successful response and records usage stats', async () => {
|
||||
const findResponse = {
|
||||
total: 2,
|
||||
|
|
|
@ -22,9 +22,16 @@ import {
|
|||
registerGetRoute,
|
||||
type InternalSavedObjectsRequestHandlerContext,
|
||||
} from '@kbn/core-saved-objects-server-internal';
|
||||
import { createHiddenTypeVariants } from '@kbn/core-test-helpers-test-utils';
|
||||
|
||||
const coreId = Symbol('core');
|
||||
|
||||
const testTypes = [
|
||||
{ name: 'index-pattern', hide: false },
|
||||
{ name: 'hidden-type', hide: true },
|
||||
{ name: 'hidden-from-http', hide: false, hideFromHttpApis: true },
|
||||
];
|
||||
|
||||
describe('GET /api/saved_objects/{type}/{id}', () => {
|
||||
let server: HttpService;
|
||||
let httpSetup: InternalHttpServiceSetup;
|
||||
|
@ -44,6 +51,12 @@ describe('GET /api/saved_objects/{type}/{id}', () => {
|
|||
});
|
||||
|
||||
handlerContext = coreMock.createRequestHandlerContext();
|
||||
handlerContext.savedObjects.typeRegistry.getType.mockImplementation((typename: string) => {
|
||||
return testTypes
|
||||
.map((typeDesc) => createHiddenTypeVariants(typeDesc))
|
||||
.find((fullTest) => fullTest.name === typename);
|
||||
});
|
||||
|
||||
savedObjectsClient = handlerContext.savedObjects.client;
|
||||
|
||||
httpSetup.registerRouteHandlerContext<InternalSavedObjectsRequestHandlerContext, 'core'>(
|
||||
|
@ -101,4 +114,11 @@ describe('GET /api/saved_objects/{type}/{id}', () => {
|
|||
const args = savedObjectsClient.get.mock.calls[0];
|
||||
expect(args).toEqual(['index-pattern', 'logstash-*']);
|
||||
});
|
||||
|
||||
it('returns with status 400 when a type is hidden from the http APIs', async () => {
|
||||
const result = await supertest(httpSetup.server.listener)
|
||||
.get('/api/saved_objects/hidden-from-http/hiddenId')
|
||||
.expect(400);
|
||||
expect(result.body.message).toContain("Unsupported saved object type: 'hidden-from-http'");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -22,9 +22,16 @@ import {
|
|||
registerResolveRoute,
|
||||
type InternalSavedObjectsRequestHandlerContext,
|
||||
} from '@kbn/core-saved-objects-server-internal';
|
||||
import { createHiddenTypeVariants } from '@kbn/core-test-helpers-test-utils';
|
||||
|
||||
const coreId = Symbol('core');
|
||||
|
||||
const testTypes = [
|
||||
{ name: 'index-pattern', hide: false },
|
||||
{ name: 'hidden-type', hide: true },
|
||||
{ name: 'hidden-from-http', hide: false, hideFromHttpApis: true },
|
||||
];
|
||||
|
||||
describe('GET /api/saved_objects/resolve/{type}/{id}', () => {
|
||||
let server: HttpService;
|
||||
let httpSetup: InternalHttpServiceSetup;
|
||||
|
@ -44,6 +51,13 @@ describe('GET /api/saved_objects/resolve/{type}/{id}', () => {
|
|||
});
|
||||
|
||||
handlerContext = coreMock.createRequestHandlerContext();
|
||||
|
||||
handlerContext.savedObjects.typeRegistry.getType.mockImplementation((typename: string) => {
|
||||
return testTypes
|
||||
.map((typeDesc) => createHiddenTypeVariants(typeDesc))
|
||||
.find((fullTest) => fullTest.name === typename);
|
||||
});
|
||||
|
||||
savedObjectsClient = handlerContext.savedObjects.client;
|
||||
|
||||
httpSetup.registerRouteHandlerContext<InternalSavedObjectsRequestHandlerContext, 'core'>(
|
||||
|
@ -101,4 +115,11 @@ describe('GET /api/saved_objects/resolve/{type}/{id}', () => {
|
|||
const args = savedObjectsClient.resolve.mock.calls[0];
|
||||
expect(args).toEqual(['index-pattern', 'logstash-*']);
|
||||
});
|
||||
|
||||
it('returns with status 400 is a type is hidden from the HTTP APIs', async () => {
|
||||
const result = await supertest(httpSetup.server.listener)
|
||||
.get('/api/saved_objects/resolve/hidden-from-http/hiddenId')
|
||||
.expect(400);
|
||||
expect(result.body.message).toContain("Unsupported saved object type: 'hidden-from-http'");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
coreUsageStatsClientMock,
|
||||
coreUsageDataServiceMock,
|
||||
} from '@kbn/core-usage-data-server-mocks';
|
||||
import { setupServer } from '@kbn/core-test-helpers-test-utils';
|
||||
import { createHiddenTypeVariants, setupServer } from '@kbn/core-test-helpers-test-utils';
|
||||
import {
|
||||
registerUpdateRoute,
|
||||
type InternalSavedObjectsRequestHandlerContext,
|
||||
|
@ -21,6 +21,12 @@ import {
|
|||
|
||||
type SetupServerReturn = Awaited<ReturnType<typeof setupServer>>;
|
||||
|
||||
const testTypes = [
|
||||
{ name: 'index-pattern', hide: false },
|
||||
{ name: 'hidden-type', hide: true },
|
||||
{ name: 'hidden-from-http', hide: false, hideFromHttpApis: true },
|
||||
];
|
||||
|
||||
describe('PUT /api/saved_objects/{type}/{id?}', () => {
|
||||
let server: SetupServerReturn['server'];
|
||||
let httpSetup: SetupServerReturn['httpSetup'];
|
||||
|
@ -43,6 +49,12 @@ describe('PUT /api/saved_objects/{type}/{id?}', () => {
|
|||
savedObjectsClient = handlerContext.savedObjects.client;
|
||||
savedObjectsClient.update.mockResolvedValue(clientResponse);
|
||||
|
||||
handlerContext.savedObjects.typeRegistry.getType.mockImplementation((typename: string) => {
|
||||
return testTypes
|
||||
.map((typeDesc) => createHiddenTypeVariants(typeDesc))
|
||||
.find((fullTest) => fullTest.name === typename);
|
||||
});
|
||||
|
||||
const router =
|
||||
httpSetup.createRouter<InternalSavedObjectsRequestHandlerContext>('/api/saved_objects/');
|
||||
coreUsageStatsClient = coreUsageStatsClientMock.create();
|
||||
|
@ -101,4 +113,14 @@ describe('PUT /api/saved_objects/{type}/{id?}', () => {
|
|||
{ version: 'foo' }
|
||||
);
|
||||
});
|
||||
|
||||
it('returns with status 400 for types hidden from the HTTP APIs', async () => {
|
||||
const result = await supertest(httpSetup.server.listener)
|
||||
.put('/api/saved_objects/hidden-from-http/hiddenId')
|
||||
.send({
|
||||
attributes: { title: 'does not matter' },
|
||||
})
|
||||
.expect(400);
|
||||
expect(result.body.message).toContain("Unsupported saved object type: 'hidden-from-http'");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"id": "test-hidden-from-http-apis-importable-exportable:hidden-from-http-apis-1",
|
||||
"index": ".kibana_1",
|
||||
"source": {
|
||||
"test-hidden-from-http-apis-importable-exportable": {
|
||||
"title": "I am hidden from http apis but the client can still see me"
|
||||
},
|
||||
"type": "test-hidden-from-http-apis-importable-exportable"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"id": "test-not-hidden-from-http-apis-importable-exportable:not-hidden-from-http-apis-1",
|
||||
"index": ".kibana_1",
|
||||
"source": {
|
||||
"test-not-hidden-from-http-apis-importable-exportable": {
|
||||
"title": "I am not hidden from http apis"
|
||||
},
|
||||
"type": "test-not-hidden-from-http-apis-importable-exportable"
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"attributes": {
|
||||
"title": "I am hidden from http apis but the client can still see me"
|
||||
},
|
||||
"id": "hidden-from-http-apis-1",
|
||||
"references": [],
|
||||
"type": "test-hidden-from-http-apis-importable-exportable",
|
||||
"version": "WzQ0LDFd"
|
||||
}
|
||||
|
||||
{
|
||||
"attributes": {
|
||||
"title": "I am not hidden from http apis"
|
||||
},
|
||||
"id": "not-hidden-from-http-apis-1",
|
||||
"references": [],
|
||||
"type": "test-not-hidden-from-http-apis-importable-exportable",
|
||||
"version": "WzQ5LDFd"
|
||||
}
|
||||
|
||||
{
|
||||
"attributes": {
|
||||
"title": "I am also hidden from http apis but the client can still see me"
|
||||
},
|
||||
"id": "hidden-from-http-apis-2",
|
||||
"references": [],
|
||||
"type": "test-hidden-from-http-apis-importable-exportable",
|
||||
"version": "WzcyLDFd"
|
||||
}
|
||||
|
||||
{
|
||||
"attributes": {
|
||||
"title": "I am also not hidden from http apis"
|
||||
},
|
||||
"id": "not-hidden-from-http-apis-2",
|
||||
"references": [],
|
||||
"type": "test-not-hidden-from-http-apis-importable-exportable",
|
||||
"version": "WzczLDFd"
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"id": "savedObjectsHiddenFromHttpApisType",
|
||||
"owner": {
|
||||
"name": "Core",
|
||||
"githubTeam": "kibana-core"
|
||||
},
|
||||
"version": "0.0.1",
|
||||
"kibanaVersion": "kibana",
|
||||
"configPath": ["saved_objects_hidden_from_http_apis_type"],
|
||||
"server": true,
|
||||
"ui": false
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"name": "saved_objects_hidden_from_http_apis_type",
|
||||
"version": "1.0.0",
|
||||
"main": "target/test/plugin_functional/plugins/saved_objects_hidden_from_http_apis_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"
|
||||
}
|
||||
}
|
|
@ -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 { SavedObjectsHiddenFromHttpApisTypePlugin } from './plugin';
|
||||
|
||||
export const plugin = () => new SavedObjectsHiddenFromHttpApisTypePlugin();
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 '@kbn/core/server';
|
||||
|
||||
export class SavedObjectsHiddenFromHttpApisTypePlugin implements Plugin {
|
||||
public setup({ savedObjects }: CoreSetup, deps: {}) {
|
||||
// example of a SO type that not hidden but is hidden from the http apis.
|
||||
savedObjects.registerType({
|
||||
name: 'test-hidden-from-http-apis-importable-exportable',
|
||||
hidden: false,
|
||||
hiddenFromHttpApis: true,
|
||||
namespaceType: 'single',
|
||||
mappings: {
|
||||
properties: {
|
||||
title: { type: 'text' },
|
||||
},
|
||||
},
|
||||
management: {
|
||||
importableAndExportable: true,
|
||||
visibleInManagement: true,
|
||||
},
|
||||
});
|
||||
savedObjects.registerType({
|
||||
name: 'test-not-hidden-from-http-apis-importable-exportable',
|
||||
hidden: false,
|
||||
hiddenFromHttpApis: false,
|
||||
namespaceType: 'single',
|
||||
mappings: {
|
||||
properties: {
|
||||
title: { type: 'text' },
|
||||
},
|
||||
},
|
||||
management: {
|
||||
importableAndExportable: true,
|
||||
visibleInManagement: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public start() {}
|
||||
public stop() {}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types"
|
||||
},
|
||||
"include": [
|
||||
"index.ts",
|
||||
"server/**/*.ts",
|
||||
"../../../../typings/**/*",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/core"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
{"attributes": {"title": "I am hidden from http apis but the client can still see me"},"id": "hidden-from-http-apis-import1","references": [],"type":"test-hidden-from-http-apis-importable-exportable","version": 1}
|
||||
{"attributes": {"title": "I am not hidden from http apis"},"id": "not-hidden-from-http-apis-import1","references": [],"type": "test-not-hidden-from-http-apis-importable-exportable","version": 1}
|
|
@ -0,0 +1,209 @@
|
|||
/*
|
||||
* 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 { join } from 'path';
|
||||
import expect from '@kbn/expect';
|
||||
import type { Response } from 'supertest';
|
||||
import { SavedObject } from '@kbn/core/types';
|
||||
import type { PluginFunctionalProviderContext } from '../../services';
|
||||
|
||||
function parseNdJson(input: string): Array<SavedObject<any>> {
|
||||
return input.split('\n').map((str) => JSON.parse(str));
|
||||
}
|
||||
|
||||
export default function ({ getService }: PluginFunctionalProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const kbnServer = getService('kibanaServer');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('types with `hiddenFromHttpApis` ', () => {
|
||||
before(async () => {
|
||||
// `kbnServer.savedObjects.cleanStandardList` uses global saved objects `delete`.
|
||||
// If there are any remaining saved objects registered as `hiddenFromHttpApis:true`,
|
||||
// cleaning them up will fail.
|
||||
await kbnServer.savedObjects.cleanStandardList();
|
||||
|
||||
await kbnServer.importExport.load(
|
||||
'test/functional/fixtures/kbn_archiver/saved_objects_management/hidden_from_http_apis.json'
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
// We cannot use `kbnServer.importExport.unload` to clean up test fixtures.
|
||||
// `kbnServer.importExport.unload` uses the global SOM `delete` HTTP API
|
||||
// and will throw on `hiddenFromHttpApis:true` objects
|
||||
await esArchiver.unload(
|
||||
'test/functional/fixtures/es_archiver/saved_objects_management/hidden_from_http_apis'
|
||||
);
|
||||
});
|
||||
|
||||
describe('APIS', () => {
|
||||
const hiddenFromHttpApisType = {
|
||||
type: 'test-hidden-from-http-apis-importable-exportable',
|
||||
id: 'hidden-from-http-apis-1',
|
||||
};
|
||||
const notHiddenFromHttpApisType = {
|
||||
type: 'test-not-hidden-from-http-apis-importable-exportable',
|
||||
id: 'not-hidden-from-http-apis-1',
|
||||
};
|
||||
|
||||
describe('_bulk_get', () => {
|
||||
describe('saved objects with hiddenFromHttpApis type', () => {
|
||||
const URL = '/api/kibana/management/saved_objects/_bulk_get';
|
||||
|
||||
it('should return 200 for types that are not hidden from the http apis', async () =>
|
||||
await supertest
|
||||
.post(URL)
|
||||
.send([notHiddenFromHttpApisType])
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(200)
|
||||
.then((response: Response) => {
|
||||
expect(response.body).to.have.length(1);
|
||||
const { type, id, meta, error } = response.body[0];
|
||||
expect(type).to.eql(notHiddenFromHttpApisType.type);
|
||||
expect(id).to.eql(notHiddenFromHttpApisType.id);
|
||||
expect(meta).to.not.equal(undefined);
|
||||
expect(error).to.equal(undefined);
|
||||
}));
|
||||
|
||||
it('should return 200 for types that are hidden from the http apis', async () =>
|
||||
await supertest
|
||||
.post(URL)
|
||||
.send([hiddenFromHttpApisType])
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(200)
|
||||
.then((response: Response) => {
|
||||
expect(response.body).to.have.length(1);
|
||||
const { type, id, meta, error } = response.body[0];
|
||||
expect(type).to.eql(hiddenFromHttpApisType.type);
|
||||
expect(id).to.eql(hiddenFromHttpApisType.id);
|
||||
expect(meta).to.not.equal(undefined);
|
||||
expect(error).to.equal(undefined);
|
||||
}));
|
||||
|
||||
it('should return 200 for a mix of types', async () =>
|
||||
await supertest
|
||||
.post(URL)
|
||||
.send([hiddenFromHttpApisType, notHiddenFromHttpApisType])
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(200)
|
||||
.expect(200)
|
||||
.then((response: Response) => {
|
||||
expect(response.body).to.have.length(2);
|
||||
const { type, id, meta, error } = response.body[0];
|
||||
expect(type).to.eql(hiddenFromHttpApisType.type);
|
||||
expect(id).to.eql(hiddenFromHttpApisType.id);
|
||||
expect(meta).to.not.equal(undefined);
|
||||
expect(error).to.equal(undefined);
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('find', () => {
|
||||
it('returns saved objects registered as hidden from the http Apis', async () => {
|
||||
await supertest
|
||||
.get(
|
||||
`/api/kibana/management/saved_objects/_find?type=${hiddenFromHttpApisType.type}&fields=title`
|
||||
)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
expect(
|
||||
resp.body.saved_objects.map((so: { id: string; type: string }) => ({
|
||||
id: so.id,
|
||||
type: so.type,
|
||||
}))
|
||||
).to.eql([
|
||||
{
|
||||
id: 'hidden-from-http-apis-1',
|
||||
type: 'test-hidden-from-http-apis-importable-exportable',
|
||||
},
|
||||
{
|
||||
id: 'hidden-from-http-apis-2',
|
||||
type: 'test-hidden-from-http-apis-importable-exportable',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('export', () => {
|
||||
it('allows to export them directly by id', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_export')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
objects: [
|
||||
{
|
||||
type: 'test-hidden-from-http-apis-importable-exportable',
|
||||
id: 'hidden-from-http-apis-1',
|
||||
},
|
||||
],
|
||||
excludeExportDetails: true,
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
const objects = parseNdJson(resp.text);
|
||||
expect(objects.map((obj) => obj.id)).to.eql(['hidden-from-http-apis-1']);
|
||||
});
|
||||
});
|
||||
|
||||
it('allows to export them directly by type', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_export')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
type: ['test-hidden-from-http-apis-importable-exportable'],
|
||||
excludeExportDetails: true,
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
const objects = parseNdJson(resp.text);
|
||||
expect(objects.map((obj) => obj.id)).to.eql([
|
||||
'hidden-from-http-apis-1',
|
||||
'hidden-from-http-apis-2',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('import', () => {
|
||||
it('allows to import them', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_import')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach('file', join(__dirname, './exports/_import_hidden_from_http_apis.ndjson'))
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
expect(resp.body).to.eql({
|
||||
success: true,
|
||||
successCount: 2,
|
||||
successResults: [
|
||||
{
|
||||
id: 'hidden-from-http-apis-import1',
|
||||
meta: {
|
||||
title: 'I am hidden from http apis but the client can still see me',
|
||||
},
|
||||
type: 'test-hidden-from-http-apis-importable-exportable',
|
||||
},
|
||||
{
|
||||
id: 'not-hidden-from-http-apis-import1',
|
||||
meta: {
|
||||
title: 'I am not hidden from http apis',
|
||||
},
|
||||
type: 'test-not-hidden-from-http-apis-importable-exportable',
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -17,5 +17,6 @@ export default function ({ loadTestFile }: PluginFunctionalProviderContext) {
|
|||
loadTestFile(require.resolve('./import_warnings'));
|
||||
loadTestFile(require.resolve('./hidden_types'));
|
||||
loadTestFile(require.resolve('./visible_in_management'));
|
||||
loadTestFile(require.resolve('./hidden_from_http_apis'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
|
|||
|
||||
async function getSavedObjectCounters() {
|
||||
// wait until ES indexes the counter SavedObject;
|
||||
await new Promise((res) => setTimeout(res, 7 * 1000));
|
||||
await new Promise((res) => setTimeout(res, 10 * 1000));
|
||||
|
||||
return await supertest
|
||||
.get('/api/saved_objects/_find?type=usage-counters')
|
||||
|
|
|
@ -958,6 +958,8 @@
|
|||
"@kbn/saved-object-import-warnings-plugin/*": ["test/plugin_functional/plugins/saved_object_import_warnings/*"],
|
||||
"@kbn/saved-objects-finder-plugin": ["src/plugins/saved_objects_finder"],
|
||||
"@kbn/saved-objects-finder-plugin/*": ["src/plugins/saved_objects_finder/*"],
|
||||
"@kbn/saved-objects-hidden-from-http-apis-type-plugin": ["test/plugin_functional/plugins/saved_objects_hidden_from_http_apis_type"],
|
||||
"@kbn/saved-objects-hidden-from-http-apis-type-plugin/*": ["test/plugin_functional/plugins/saved_objects_hidden_from_http_apis_type/*"],
|
||||
"@kbn/saved-objects-hidden-type-plugin": ["test/plugin_functional/plugins/saved_objects_hidden_type"],
|
||||
"@kbn/saved-objects-hidden-type-plugin/*": ["test/plugin_functional/plugins/saved_objects_hidden_type/*"],
|
||||
"@kbn/saved-objects-management-plugin": ["src/plugins/saved_objects_management"],
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue