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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,10 +5,10 @@
* 2.0. * 2.0.
*/ */
import type { PluginInitializerContext } from '@kbn/core-plugins-server';
import { LensServerPlugin } from './plugin'; import { LensServerPlugin } from './plugin';
export type { LensServerPluginSetup } 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'; export type { LensDocShape715 } from './migrations/types';

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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