[CM] Soften response validation (#166919)

## Summary

Close https://github.com/elastic/kibana/issues/167152

Log a warning instead of throwing an error in
`saved_object_content_storage` when response validation failed.

We decided to do this as a precaution and as a follow up to an issue
found in saved search https://github.com/elastic/kibana/pull/166886
where storage started failing because of too strict validation.

As of this PR the saved_object_content_storage covers and this change
cover:

- `search`
- `index_pattern`
- `dashboard`
- `lens`
- `maps`

For other types we agreed with @dej611 that instead of applying the same
change for other types (visualization, graph, annotation) the team would
look into migrating their types to also use
`saved_object_content_storage`
https://github.com/elastic/kibana/issues/167421
This commit is contained in:
Anton Dosov 2023-09-28 16:33:04 +02:00 committed by GitHub
parent 88b8b8c190
commit 6fd9909b5e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 811 additions and 43 deletions

View file

@ -0,0 +1,600 @@
/*
* 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 { SOContentStorage } from './saved_object_content_storage';
import { CMCrudTypes } from './types';
import { loggerMock, MockedLogger } from '@kbn/logging-mocks';
import { schema } from '@kbn/config-schema';
import type {
ContentManagementServicesDefinition as ServicesDefinition,
Version,
} from '@kbn/object-versioning';
import { getContentManagmentServicesTransforms } from '@kbn/object-versioning';
import { savedObjectSchema, objectTypeToGetResultSchema, createResultSchema } from './schema';
import { coreMock } from '@kbn/core/server/mocks';
import type { SavedObject } from '@kbn/core/server';
const testAttributesSchema = schema.object(
{
title: schema.string(),
description: schema.string(),
},
{ unknowns: 'forbid' }
);
const testSavedObjectSchema = savedObjectSchema(testAttributesSchema);
export const serviceDefinition: ServicesDefinition = {
get: {
out: {
result: {
schema: objectTypeToGetResultSchema(testSavedObjectSchema),
},
},
},
create: {
out: {
result: {
schema: createResultSchema(testSavedObjectSchema),
},
},
},
update: {
out: {
result: {
schema: createResultSchema(testSavedObjectSchema),
},
},
},
search: {
out: {
result: {
schema: schema.object({ hits: schema.arrayOf(testSavedObjectSchema) }),
},
},
},
mSearch: {
out: {
result: {
schema: testSavedObjectSchema,
},
},
},
};
export const cmServicesDefinition: { [version: Version]: ServicesDefinition } = {
1: serviceDefinition,
};
const transforms = getContentManagmentServicesTransforms(cmServicesDefinition, 1);
class TestSOContentStorage extends SOContentStorage<CMCrudTypes> {
constructor({
throwOnResultValidationError,
logger,
}: { throwOnResultValidationError?: boolean; logger?: MockedLogger } = {}) {
super({
savedObjectType: 'test',
cmServicesDefinition,
allowedSavedObjectAttributes: ['title', 'description'],
logger: logger ?? loggerMock.create(),
throwOnResultValidationError: throwOnResultValidationError ?? false,
enableMSearch: true,
});
}
}
const setup = ({ storage }: { storage?: TestSOContentStorage } = {}) => {
storage = storage ?? new TestSOContentStorage();
const requestHandlerCoreContext = coreMock.createRequestHandlerContext();
const requestHandlerContext = {
core: Promise.resolve(requestHandlerCoreContext),
resolve: jest.fn(),
};
return {
get: (mockSavedObject: SavedObject) => {
requestHandlerCoreContext.savedObjects.client.resolve.mockResolvedValue({
saved_object: mockSavedObject,
outcome: 'exactMatch',
});
return storage!.get(
{
requestHandlerContext,
version: {
request: 1,
latest: 1,
},
utils: {
getTransforms: () => transforms,
},
},
mockSavedObject.id
);
},
create: (mockSavedObject: SavedObject<{}>) => {
requestHandlerCoreContext.savedObjects.client.create.mockResolvedValue(mockSavedObject);
return storage!.create(
{
requestHandlerContext,
version: {
request: 1,
latest: 1,
},
utils: {
getTransforms: () => transforms,
},
},
mockSavedObject.attributes,
{}
);
},
update: (mockSavedObject: SavedObject<{}>) => {
requestHandlerCoreContext.savedObjects.client.update.mockResolvedValue(mockSavedObject);
return storage!.update(
{
requestHandlerContext,
version: {
request: 1,
latest: 1,
},
utils: {
getTransforms: () => transforms,
},
},
mockSavedObject.id,
mockSavedObject.attributes,
{}
);
},
search: (mockSavedObject: SavedObject<{}>) => {
requestHandlerCoreContext.savedObjects.client.find.mockResolvedValue({
saved_objects: [{ ...mockSavedObject, score: 100 }],
total: 1,
per_page: 10,
page: 1,
});
return storage!.search(
{
requestHandlerContext,
version: {
request: 1,
latest: 1,
},
utils: {
getTransforms: () => transforms,
},
},
{},
{}
);
},
mSearch: async (mockSavedObject: SavedObject<{}>) => {
return storage!.mSearch!.toItemResult(
{
requestHandlerContext,
version: {
request: 1,
latest: 1,
},
utils: {
getTransforms: () => transforms,
},
},
{ ...mockSavedObject, score: 100 }
);
},
};
};
describe('get', () => {
test('returns the storage get() result', async () => {
const get = setup().get;
const testSavedObject = {
id: 'id',
type: 'test',
references: [],
attributes: {
title: 'title',
description: 'description',
},
};
const result = await get(testSavedObject);
expect(result).toEqual({ item: testSavedObject, meta: { outcome: 'exactMatch' } });
});
test('filters out unknown attributes', async () => {
const get = setup().get;
const testSavedObject = {
id: 'id',
type: 'test',
references: [],
attributes: {
title: 'title',
description: 'description',
unknown: 'unknown',
},
};
const result = await get(testSavedObject);
expect(result.item.attributes).not.toHaveProperty('unknown');
});
test('throws response validation error', async () => {
const get = setup({
storage: new TestSOContentStorage({ throwOnResultValidationError: true }),
}).get;
const testSavedObject = {
id: 'id',
type: 'test',
references: [],
attributes: {
title: 'title',
description: null,
},
};
await expect(get(testSavedObject)).rejects.toThrowErrorMatchingInlineSnapshot(
`"Invalid response. [item.attributes.description]: expected value of type [string] but got [null]"`
);
});
test('logs response validation error', async () => {
const logger = loggerMock.create();
const get = setup({
storage: new TestSOContentStorage({ throwOnResultValidationError: false, logger }),
}).get;
const testSavedObject = {
id: 'id',
type: 'test',
references: [],
attributes: {
title: 'title',
description: null,
},
};
await expect(get(testSavedObject)).resolves.toBeDefined();
expect(logger.warn).toBeCalledWith(
`Invalid response. [item.attributes.description]: expected value of type [string] but got [null]`
);
});
});
describe('create', () => {
test('returns the storage create() result', async () => {
const create = setup().create;
const testSavedObject = {
id: 'id',
type: 'test',
references: [],
attributes: {
title: 'title',
description: 'description',
},
};
const result = await create(testSavedObject);
expect(result).toEqual({ item: testSavedObject });
});
test('filters out unknown attributes', async () => {
const create = setup().create;
const testSavedObject = {
id: 'id',
type: 'test',
references: [],
attributes: {
title: 'title',
description: 'description',
unknown: 'unknown',
},
};
const result = await create(testSavedObject);
expect(result.item.attributes).not.toHaveProperty('unknown');
});
test('throws response validation error', async () => {
const create = setup({
storage: new TestSOContentStorage({ throwOnResultValidationError: true }),
}).create;
const testSavedObject = {
id: 'id',
type: 'test',
references: [],
attributes: {
title: 'title',
description: null,
},
};
await expect(create(testSavedObject)).rejects.toThrowErrorMatchingInlineSnapshot(
`"Invalid response. [item.attributes.description]: expected value of type [string] but got [null]"`
);
});
test('logs response validation error', async () => {
const logger = loggerMock.create();
const create = setup({
storage: new TestSOContentStorage({ throwOnResultValidationError: false, logger }),
}).create;
const testSavedObject = {
id: 'id',
type: 'test',
references: [],
attributes: {
title: 'title',
description: null,
},
};
await expect(create(testSavedObject)).resolves.toBeDefined();
expect(logger.warn).toBeCalledWith(
`Invalid response. [item.attributes.description]: expected value of type [string] but got [null]`
);
});
});
describe('update', () => {
test('returns the storage update() result', async () => {
const update = setup().update;
const testSavedObject = {
id: 'id',
type: 'test',
references: [],
attributes: {
title: 'title',
description: 'description',
},
};
const result = await update(testSavedObject);
expect(result).toEqual({ item: testSavedObject });
});
test('filters out unknown attributes', async () => {
const update = setup().update;
const testSavedObject = {
id: 'id',
type: 'test',
references: [],
attributes: {
title: 'title',
description: 'description',
unknown: 'unknown',
},
};
const result = await update(testSavedObject);
expect(result.item.attributes).not.toHaveProperty('unknown');
});
test('throws response validation error', async () => {
const update = setup({
storage: new TestSOContentStorage({ throwOnResultValidationError: true }),
}).update;
const testSavedObject = {
id: 'id',
type: 'test',
references: [],
attributes: {
title: 'title',
description: null,
},
};
await expect(update(testSavedObject)).rejects.toThrowErrorMatchingInlineSnapshot(
`"Invalid response. [item.attributes.description]: expected value of type [string] but got [null]"`
);
});
test('logs response validation error', async () => {
const logger = loggerMock.create();
const update = setup({
storage: new TestSOContentStorage({ throwOnResultValidationError: false, logger }),
}).update;
const testSavedObject = {
id: 'id',
type: 'test',
references: [],
attributes: {
title: 'title',
description: null,
},
};
await expect(update(testSavedObject)).resolves.toBeDefined();
expect(logger.warn).toBeCalledWith(
`Invalid response. [item.attributes.description]: expected value of type [string] but got [null]`
);
});
});
describe('search', () => {
test('returns the storage search() result', async () => {
const search = setup().search;
const testSavedObject = {
id: 'id',
type: 'test',
references: [],
attributes: {
title: 'title',
description: 'description',
},
};
const result = await search(testSavedObject);
expect(result).toEqual({ hits: [testSavedObject], pagination: { total: 1 } });
});
test('filters out unknown attributes', async () => {
const search = setup().search;
const testSavedObject = {
id: 'id',
type: 'test',
references: [],
attributes: {
title: 'title',
description: 'description',
unknown: 'unknown',
},
};
const result = await search(testSavedObject);
expect(result.hits[0].attributes).not.toHaveProperty('unknown');
});
test('throws response validation error', async () => {
const search = setup({
storage: new TestSOContentStorage({ throwOnResultValidationError: true }),
}).search;
const testSavedObject = {
id: 'id',
type: 'test',
references: [],
attributes: {
title: 'title',
description: null,
},
};
await expect(search(testSavedObject)).rejects.toThrowErrorMatchingInlineSnapshot(
`"Invalid response. [hits.0.attributes.description]: expected value of type [string] but got [null]"`
);
});
test('logs response validation error', async () => {
const logger = loggerMock.create();
const update = setup({
storage: new TestSOContentStorage({ throwOnResultValidationError: false, logger }),
}).search;
const testSavedObject = {
id: 'id',
type: 'test',
references: [],
attributes: {
title: 'title',
description: null,
},
};
await expect(update(testSavedObject)).resolves.toBeDefined();
expect(logger.warn).toBeCalledWith(
`Invalid response. [hits.0.attributes.description]: expected value of type [string] but got [null]`
);
});
});
describe('mSearch', () => {
test('returns the storage mSearch() result', async () => {
const mSearch = setup().mSearch;
const testSavedObject = {
id: 'id',
type: 'test',
references: [],
attributes: {
title: 'title',
description: 'description',
},
};
const result = await mSearch(testSavedObject);
expect(result).toEqual(testSavedObject);
});
test('filters out unknown attributes', async () => {
const mSearch = setup().mSearch;
const testSavedObject = {
id: 'id',
type: 'test',
references: [],
attributes: {
title: 'title',
description: 'description',
unknown: 'unknown',
},
};
const result = await mSearch(testSavedObject);
expect(result.attributes).not.toHaveProperty('unknown');
});
test('throws response validation error', async () => {
const mSearch = setup({
storage: new TestSOContentStorage({ throwOnResultValidationError: true }),
}).mSearch;
const testSavedObject = {
id: 'id',
type: 'test',
references: [],
attributes: {
title: 'title',
description: null,
},
};
await expect(mSearch(testSavedObject)).rejects.toThrowErrorMatchingInlineSnapshot(
`"Invalid response. [attributes.description]: expected value of type [string] but got [null]"`
);
});
test('logs response validation error', async () => {
const logger = loggerMock.create();
const mSearch = setup({
storage: new TestSOContentStorage({ throwOnResultValidationError: false, logger }),
}).mSearch;
const testSavedObject = {
id: 'id',
type: 'test',
references: [],
attributes: {
title: 'title',
description: null,
},
};
await expect(mSearch(testSavedObject)).resolves.toBeDefined();
expect(logger.warn).toBeCalledWith(
'Invalid response. [attributes.description]: expected value of type [string] but got [null]'
);
});
});

View file

@ -21,6 +21,7 @@ import type {
SavedObjectsUpdateOptions,
SavedObjectsFindResult,
} from '@kbn/core-saved-objects-api-server';
import type { Logger } from '@kbn/logging';
import { pick } from 'lodash';
import type {
CMCrudTypes,
@ -138,6 +139,9 @@ export interface SOContentStorageConstrutorParams<Types extends CMCrudTypes> {
searchArgsToSOFindOptions?: SearchArgsToSOFindOptions<Types>;
enableMSearch?: boolean;
mSearchAdditionalSearchFields?: string[];
logger: Logger;
throwOnResultValidationError: boolean;
}
export abstract class SOContentStorage<Types extends CMCrudTypes>
@ -157,7 +161,11 @@ export abstract class SOContentStorage<Types extends CMCrudTypes>
enableMSearch,
allowedSavedObjectAttributes,
mSearchAdditionalSearchFields,
logger,
throwOnResultValidationError,
}: SOContentStorageConstrutorParams<Types>) {
this.logger = logger;
this.throwOnResultValidationError = throwOnResultValidationError ?? false;
this.savedObjectType = savedObjectType;
this.cmServicesDefinition = cmServicesDefinition;
this.createArgsToSoCreateOptions =
@ -174,16 +182,29 @@ export abstract class SOContentStorage<Types extends CMCrudTypes>
toItemResult: (ctx: StorageContext, savedObject: SavedObjectsFindResult): Types['Item'] => {
const transforms = ctx.utils.getTransforms(this.cmServicesDefinition);
const contentItem = savedObjectToItem(
savedObject as SavedObjectsFindResult<Types['Attributes']>,
this.allowedSavedObjectAttributes,
false
);
const validationError = transforms.mSearch.out.result.validate(contentItem);
if (validationError) {
if (this.throwOnResultValidationError) {
throw Boom.badRequest(`Invalid response. ${validationError.message}`);
} else {
this.logger.warn(`Invalid response. ${validationError.message}`);
}
}
// Validate DB response and DOWN transform to the request version
const { value, error: resultError } = transforms.mSearch.out.result.down<
Types['Item'],
Types['Item']
>(
savedObjectToItem(
savedObject as SavedObjectsFindResult<Types['Attributes']>,
this.allowedSavedObjectAttributes,
false
)
contentItem,
undefined, // do not override version
{ validate: false } // validation is done above
);
if (resultError) {
@ -196,6 +217,8 @@ export abstract class SOContentStorage<Types extends CMCrudTypes>
}
}
private throwOnResultValidationError: boolean;
private logger: Logger;
private savedObjectType: SOContentStorageConstrutorParams<Types>['savedObjectType'];
private cmServicesDefinition: SOContentStorageConstrutorParams<Types>['cmServicesDefinition'];
private createArgsToSoCreateOptions: CreateArgsToSoCreateOptions<Types>;
@ -230,11 +253,24 @@ export abstract class SOContentStorage<Types extends CMCrudTypes>
},
};
const validationError = transforms.get.out.result.validate(response);
if (validationError) {
if (this.throwOnResultValidationError) {
throw Boom.badRequest(`Invalid response. ${validationError.message}`);
} else {
this.logger.warn(`Invalid response. ${validationError.message}`);
}
}
// Validate DB response and DOWN transform to the request version
const { value, error: resultError } = transforms.get.out.result.down<
Types['GetOut'],
Types['GetOut']
>(response);
>(
response,
undefined, // do not override version
{ validate: false } // validation is done above
);
if (resultError) {
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
@ -282,13 +318,28 @@ export abstract class SOContentStorage<Types extends CMCrudTypes>
createOptions
);
const result = {
item: savedObjectToItem(savedObject, this.allowedSavedObjectAttributes, false),
};
const validationError = transforms.create.out.result.validate(result);
if (validationError) {
if (this.throwOnResultValidationError) {
throw Boom.badRequest(`Invalid response. ${validationError.message}`);
} else {
this.logger.warn(`Invalid response. ${validationError.message}`);
}
}
// Validate DB response and DOWN transform to the request version
const { value, error: resultError } = transforms.create.out.result.down<
Types['CreateOut'],
Types['CreateOut']
>({
item: savedObjectToItem(savedObject, this.allowedSavedObjectAttributes, false),
});
>(
result,
undefined, // do not override version
{ validate: false } // validation is done above
);
if (resultError) {
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
@ -333,13 +384,28 @@ export abstract class SOContentStorage<Types extends CMCrudTypes>
updateOptions
);
const result = {
item: savedObjectToItem(partialSavedObject, this.allowedSavedObjectAttributes, true),
};
const validationError = transforms.update.out.result.validate(result);
if (validationError) {
if (this.throwOnResultValidationError) {
throw Boom.badRequest(`Invalid response. ${validationError.message}`);
} else {
this.logger.warn(`Invalid response. ${validationError.message}`);
}
}
// Validate DB response and DOWN transform to the request version
const { value, error: resultError } = transforms.update.out.result.down<
Types['UpdateOut'],
Types['UpdateOut']
>({
item: savedObjectToItem(partialSavedObject, this.allowedSavedObjectAttributes, true),
});
>(
result,
undefined, // do not override version
{ validate: false } // validation is done above
);
if (resultError) {
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
@ -382,20 +448,34 @@ export abstract class SOContentStorage<Types extends CMCrudTypes>
options: optionsToLatest,
});
// Execute the query in the DB
const response = await soClient.find<Types['Attributes']>(soQuery);
const soResponse = await soClient.find<Types['Attributes']>(soQuery);
const response = {
hits: soResponse.saved_objects.map((so) =>
savedObjectToItem(so, this.allowedSavedObjectAttributes, false)
),
pagination: {
total: soResponse.total,
},
};
const validationError = transforms.search.out.result.validate(response);
if (validationError) {
if (this.throwOnResultValidationError) {
throw Boom.badRequest(`Invalid response. ${validationError.message}`);
} else {
this.logger.warn(`Invalid response. ${validationError.message}`);
}
}
// Validate the response and DOWN transform to the request version
const { value, error: resultError } = transforms.search.out.result.down<
Types['SearchOut'],
Types['SearchOut']
>({
hits: response.saved_objects.map((so) =>
savedObjectToItem(so, this.allowedSavedObjectAttributes, false)
),
pagination: {
total: response.total,
},
});
>(
response,
undefined, // do not override version
{ validate: false } // validation is done above
);
if (resultError) {
throw Boom.badRequest(`Invalid response. ${resultError.message}`);

View file

@ -21,5 +21,8 @@
"@kbn/core-saved-objects-api-server",
"@kbn/config-schema",
"@kbn/object-versioning",
"@kbn/logging",
"@kbn/logging-mocks",
"@kbn/core",
]
}

View file

@ -8,6 +8,7 @@
import { SOContentStorage, tagsToFindOptions } from '@kbn/content-management-utils';
import { SavedObjectsFindOptions } from '@kbn/core-saved-objects-api-server';
import type { Logger } from '@kbn/logging';
import { CONTENT_ID } from '../../common/content_management';
import { cmServicesDefinition } from '../../common/content_management/cm_services';
@ -31,7 +32,13 @@ const searchArgsToSOFindOptions = (
};
export class DashboardStorage extends SOContentStorage<DashboardCrudTypes> {
constructor() {
constructor({
logger,
throwOnResultValidationError,
}: {
logger: Logger;
throwOnResultValidationError: boolean;
}) {
super({
savedObjectType: CONTENT_ID,
cmServicesDefinition,
@ -50,6 +57,8 @@ export class DashboardStorage extends SOContentStorage<DashboardCrudTypes> {
'timeTo',
'title',
],
logger,
throwOnResultValidationError,
});
}
}

View file

@ -45,7 +45,7 @@ export class DashboardPlugin
{
private readonly logger: Logger;
constructor(initializerContext: PluginInitializerContext) {
constructor(private initializerContext: PluginInitializerContext) {
this.logger = initializerContext.logger.get();
}
@ -62,7 +62,10 @@ export class DashboardPlugin
plugins.contentManagement.register({
id: CONTENT_ID,
storage: new DashboardStorage(),
storage: new DashboardStorage({
throwOnResultValidationError: this.initializerContext.env.mode.dev,
logger: this.logger.get('storage'),
}),
version: {
latest: LATEST_VERSION,
},

View file

@ -67,7 +67,8 @@
"@kbn/serverless",
"@kbn/no-data-page-plugin",
"@kbn/react-kibana-mount",
"@kbn/core-lifecycle-browser"
"@kbn/core-lifecycle-browser",
"@kbn/logging"
],
"exclude": ["target/**/*"]
}

View file

@ -7,13 +7,20 @@
*/
import { SOContentStorage } from '@kbn/content-management-utils';
import type { Logger } from '@kbn/logging';
import type { DataViewCrudTypes } from '../../common/content_management';
import { DataViewSOType } from '../../common/content_management';
import { cmServicesDefinition } from '../../common/content_management/cm_services';
export class DataViewsStorage extends SOContentStorage<DataViewCrudTypes> {
constructor() {
constructor({
logger,
throwOnResultValidationError,
}: {
logger: Logger;
throwOnResultValidationError: boolean;
}) {
super({
savedObjectType: DataViewSOType,
cmServicesDefinition,
@ -32,6 +39,8 @@ export class DataViewsStorage extends SOContentStorage<DataViewCrudTypes> {
'name',
],
mSearchAdditionalSearchFields: ['name'],
logger,
throwOnResultValidationError,
});
}
}

View file

@ -61,7 +61,10 @@ export class DataViewsServerPlugin
contentManagement.register({
id: DATA_VIEW_SAVED_OBJECT_TYPE,
storage: new DataViewsStorage(),
storage: new DataViewsStorage({
throwOnResultValidationError: this.initializerContext.env.mode.dev,
logger: this.logger.get('storage'),
}),
version: {
latest: LATEST_VERSION,
},

View file

@ -32,6 +32,7 @@
"@kbn/content-management-utils",
"@kbn/object-versioning",
"@kbn/core-saved-objects-server",
"@kbn/logging",
],
"exclude": [
"target/**/*",

View file

@ -7,13 +7,20 @@
*/
import { SOContentStorage } from '@kbn/content-management-utils';
import type { Logger } from '@kbn/logging';
import type { SavedSearchCrudTypes } from '../../common/content_management';
import { SavedSearchType } from '../../common/content_management';
import { cmServicesDefinition } from '../../common/content_management/cm_services';
export class SavedSearchStorage extends SOContentStorage<SavedSearchCrudTypes> {
constructor() {
constructor({
logger,
throwOnResultValidationError,
}: {
logger: Logger;
throwOnResultValidationError: boolean;
}) {
super({
savedObjectType: SavedSearchType,
cmServicesDefinition,
@ -37,6 +44,8 @@ export class SavedSearchStorage extends SOContentStorage<SavedSearchCrudTypes> {
'rowsPerPage',
'breakdownField',
],
logger,
throwOnResultValidationError,
});
}
}

View file

@ -6,8 +6,10 @@
* Side Public License, v 1.
*/
import type { PluginInitializerContext } from '@kbn/core-plugins-server';
import { SavedSearchServerPlugin } from './plugin';
export { getSavedSearch } from './services/saved_searches';
export const plugin = () => new SavedSearchServerPlugin();
export const plugin = (initContext: PluginInitializerContext) =>
new SavedSearchServerPlugin(initContext);

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { CoreSetup, CoreStart, Plugin } from '@kbn/core/server';
import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/server';
import { StartServicesAccessor } from '@kbn/core/server';
import type {
PluginSetup as DataPluginSetup,
@ -37,13 +37,18 @@ export interface SavedSearchServerStartDeps {
export class SavedSearchServerPlugin
implements Plugin<object, object, object, SavedSearchServerStartDeps>
{
constructor(private initializerContext: PluginInitializerContext) {}
public setup(
core: CoreSetup,
{ data, contentManagement, expressions }: SavedSearchPublicSetupDependencies
) {
contentManagement.register({
id: SavedSearchType,
storage: new SavedSearchStorage(),
storage: new SavedSearchStorage({
throwOnResultValidationError: this.initializerContext.env.mode.dev,
logger: this.initializerContext.logger.get('storage'),
}),
version: {
latest: LATEST_VERSION,
},

View file

@ -29,6 +29,8 @@
"@kbn/saved-objects-plugin",
"@kbn/es-query",
"@kbn/discover-utils",
"@kbn/logging",
"@kbn/core-plugins-server",
],
"exclude": [
"target/**/*",

View file

@ -9,6 +9,7 @@ import type { SavedObjectsFindOptions } from '@kbn/core-saved-objects-api-server
import type { StorageContext } from '@kbn/content-management-plugin/server';
import { SOContentStorage, tagsToFindOptions } from '@kbn/content-management-utils';
import type { SavedObject, SavedObjectReference } from '@kbn/core-saved-objects-api-server';
import type { Logger } from '@kbn/logging';
import {
CONTENT_ID,
@ -82,13 +83,20 @@ function savedObjectToLensSavedObject(
}
export class LensStorage extends SOContentStorage<LensCrudTypes> {
constructor() {
constructor(
private params: {
logger: Logger;
throwOnResultValidationError: boolean;
}
) {
super({
savedObjectType: CONTENT_ID,
cmServicesDefinition,
searchArgsToSOFindOptions,
enableMSearch: true,
allowedSavedObjectAttributes: ['title', 'description', 'visualizationType', 'state'],
logger: params.logger,
throwOnResultValidationError: params.throwOnResultValidationError,
});
}
@ -134,13 +142,28 @@ export class LensStorage extends SOContentStorage<LensCrudTypes> {
...optionsToLatest,
});
const result = {
item: savedObjectToLensSavedObject(savedObject),
};
const validationError = transforms.update.out.result.validate(result);
if (validationError) {
if (this.params.throwOnResultValidationError) {
throw Boom.badRequest(`Invalid response. ${validationError.message}`);
} else {
this.params.logger.warn(`Invalid response. ${validationError.message}`);
}
}
// Validate DB response and DOWN transform to the request version
const { value, error: resultError } = transforms.update.out.result.down<
LensCrudTypes['UpdateOut'],
LensCrudTypes['UpdateOut']
>({
item: savedObjectToLensSavedObject(savedObject),
});
>(
result,
undefined, // do not override version
{ validate: false } // validation is done above
);
if (resultError) {
throw Boom.badRequest(`Invalid response. ${resultError.message}`);

View file

@ -5,10 +5,10 @@
* 2.0.
*/
import type { PluginInitializerContext } from '@kbn/core-plugins-server';
import { LensServerPlugin } from './plugin';
export type { LensServerPluginSetup } from './plugin';
export const plugin = () => new LensServerPlugin();
export const plugin = (initContext: PluginInitializerContext) => new LensServerPlugin(initContext);
export type { LensDocShape715 } from './migrations/types';

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { Plugin, CoreSetup, CoreStart } from '@kbn/core/server';
import { Plugin, CoreSetup, CoreStart, PluginInitializerContext } from '@kbn/core/server';
import { PluginStart as DataViewsServerPluginStart } from '@kbn/data-views-plugin/server';
import {
PluginStart as DataPluginStart,
@ -64,7 +64,7 @@ export interface LensServerPluginSetup {
export class LensServerPlugin implements Plugin<LensServerPluginSetup, {}, {}, {}> {
private customVisualizationMigrations: CustomVisualizationMigrations = {};
constructor() {}
constructor(private initializerContext: PluginInitializerContext) {}
setup(core: CoreSetup<PluginStartContract>, plugins: PluginSetupContract) {
const getFilterMigrations = plugins.data.query.filterManager.getAllMigrations.bind(
@ -79,7 +79,10 @@ export class LensServerPlugin implements Plugin<LensServerPluginSetup, {}, {}, {
plugins.contentManagement.register({
id: CONTENT_ID,
storage: new LensStorage(),
storage: new LensStorage({
throwOnResultValidationError: this.initializerContext.env.mode.dev,
logger: this.initializerContext.logger.get('storage'),
}),
version: {
latest: LATEST_VERSION,
},

View file

@ -88,6 +88,8 @@
"@kbn/ebt-tools",
"@kbn/chart-expressions-common",
"@kbn/search-response-warnings",
"@kbn/logging",
"@kbn/core-plugins-server",
],
"exclude": [
"target/**/*",

View file

@ -7,6 +7,7 @@
import { SOContentStorage, tagsToFindOptions } from '@kbn/content-management-utils';
import { SavedObjectsFindOptions } from '@kbn/core-saved-objects-api-server';
import type { Logger } from '@kbn/logging';
import { CONTENT_ID } from '../../common/content_management';
import { cmServicesDefinition } from '../../common/content_management/cm_services';
import type { MapCrudTypes } from '../../common/content_management';
@ -27,7 +28,13 @@ const searchArgsToSOFindOptions = (args: MapCrudTypes['SearchIn']): SavedObjects
};
export class MapsStorage extends SOContentStorage<MapCrudTypes> {
constructor() {
constructor({
logger,
throwOnResultValidationError,
}: {
logger: Logger;
throwOnResultValidationError: boolean;
}) {
super({
savedObjectType: CONTENT_ID,
cmServicesDefinition,
@ -40,6 +47,8 @@ export class MapsStorage extends SOContentStorage<MapCrudTypes> {
'layerListJSON',
'uiStateJSON',
],
logger,
throwOnResultValidationError,
});
}
}

View file

@ -204,7 +204,10 @@ export class MapsPlugin implements Plugin {
contentManagement.register({
id: CONTENT_ID,
storage: new MapsStorage(),
storage: new MapsStorage({
throwOnResultValidationError: this._initializerContext.env.mode.dev,
logger: this._logger.get('storage'),
}),
version: {
latest: LATEST_VERSION,
},

View file

@ -73,6 +73,7 @@
"@kbn/content-management-table-list-view-table",
"@kbn/content-management-table-list-view",
"@kbn/serverless",
"@kbn/logging",
],
"exclude": [
"target/**/*",

View file

@ -142,7 +142,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const errorMessages = await Promise.all(failureElements.map((el) => el.getVisibleText()));
expect(errorMessages).to.eql([
'Bad Request',
'Visualization type not found.',
'The visualization type lnsUNKNOWN could not be resolved.',
'Could not find datasource for the visualization',
]);