diff --git a/src/core/server/saved_objects/mappings/lib/get_root_properties_objects.ts b/src/core/server/saved_objects/mappings/lib/get_root_properties_objects.ts index 81ba1d8235561f..e170e2017b9b08 100644 --- a/src/core/server/saved_objects/mappings/lib/get_root_properties_objects.ts +++ b/src/core/server/saved_objects/mappings/lib/get_root_properties_objects.ts @@ -39,7 +39,7 @@ import { getRootProperties } from './get_root_properties'; * @return {EsPropertyMappings} */ -const blacklist = ['migrationVersion', 'references']; +const blacklist = ['migrationVersion', 'references', 'valid', 'invalid_attributes']; export function getRootPropertiesObjects(mappings: IndexMapping) { const rootProperties = getRootProperties(mappings); diff --git a/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap b/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap index bc9a66926e8801..db23cf5cd41ad9 100644 --- a/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap +++ b/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap @@ -10,7 +10,9 @@ Object { "namespace": "2f4316de49999235636386fe51dc06c1", "namespaces": "2f4316de49999235636386fe51dc06c1", "references": "7997cf5a56cc02bdc9c93361bde732b0", + "status": "2f4316de49999235636386fe51dc06c1", "type": "2f4316de49999235636386fe51dc06c1", + "unsafe_properties": "5ef305b18111b77789afefbd36b66171", "updated_at": "00da57df13e94e9d98437d13ace4bfe0", }, }, @@ -46,9 +48,16 @@ Object { }, "type": "nested", }, + "status": Object { + "type": "keyword", + }, "type": Object { "type": "keyword", }, + "unsafe_properties": Object { + "enabled": false, + "type": "object", + }, "updated_at": Object { "type": "date", }, @@ -66,8 +75,10 @@ Object { "namespaces": "2f4316de49999235636386fe51dc06c1", "references": "7997cf5a56cc02bdc9c93361bde732b0", "secondType": "72d57924f415fbadb3ee293b67d233ab", + "status": "2f4316de49999235636386fe51dc06c1", "thirdType": "510f1f0adb69830cf8a1c5ce2923ed82", "type": "2f4316de49999235636386fe51dc06c1", + "unsafe_properties": "5ef305b18111b77789afefbd36b66171", "updated_at": "00da57df13e94e9d98437d13ace4bfe0", }, }, @@ -113,6 +124,9 @@ Object { }, }, }, + "status": Object { + "type": "keyword", + }, "thirdType": Object { "properties": Object { "field": Object { @@ -123,6 +137,10 @@ Object { "type": Object { "type": "keyword", }, + "unsafe_properties": Object { + "enabled": false, + "type": "object", + }, "updated_at": Object { "type": "date", }, diff --git a/src/core/server/saved_objects/migrations/core/build_active_mappings.ts b/src/core/server/saved_objects/migrations/core/build_active_mappings.ts index c2a7b11e057cd3..82c4ba3949bbd9 100644 --- a/src/core/server/saved_objects/migrations/core/build_active_mappings.ts +++ b/src/core/server/saved_objects/migrations/core/build_active_mappings.ts @@ -125,7 +125,7 @@ function findChangedProp(actual: any, expected: any) { * * @returns {IndexMapping} */ -function defaultMapping(): IndexMapping { +export function defaultMapping(): IndexMapping { return { dynamic: 'strict', properties: { @@ -159,6 +159,14 @@ function defaultMapping(): IndexMapping { }, }, }, + // "private" fields, not exposed through any API's + status: { + type: 'keyword', + }, + unsafe_properties: { + enabled: false, // Don't index unsafe properties + type: 'object', + }, }, }; } diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts index a3647103225240..41ad5ee5e8e2b6 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts @@ -45,7 +45,6 @@ const createRegistry = (...types: Array>) => { describe('DocumentMigrator', () => { function testOpts() { return { - kibanaVersion: '25.2.3', typeRegistry: createRegistry(), validateDoc: _.noop, log: mockLogger, @@ -54,7 +53,6 @@ describe('DocumentMigrator', () => { it('validates individual migration definitions', () => { const invalidDefinition = { - kibanaVersion: '3.2.3', typeRegistry: createRegistry({ name: 'foo', migrations: _.noop as any, @@ -69,7 +67,6 @@ describe('DocumentMigrator', () => { it('validates individual migration semvers', () => { const invalidDefinition = { - kibanaVersion: '3.2.3', typeRegistry: createRegistry({ name: 'foo', migrations: { @@ -86,7 +83,6 @@ describe('DocumentMigrator', () => { it('validates the migration function', () => { const invalidDefinition = { - kibanaVersion: '3.2.3', typeRegistry: createRegistry({ name: 'foo', migrations: { @@ -283,7 +279,6 @@ describe('DocumentMigrator', () => { it('rejects docs that belong to a newer Kibana instance', () => { const migrator = new DocumentMigrator({ ...testOpts(), - kibanaVersion: '8.0.1', }); expect(() => migrator.migrate({ @@ -555,7 +550,9 @@ describe('DocumentMigrator', () => { name: 'dog', migrations: { '1.2.3': () => { - throw new Error('Dang diggity!'); + const err = new Error('Dang diggity!'); + err.stack = 'stack trace...'; + throw err; }, }, }), @@ -571,10 +568,24 @@ describe('DocumentMigrator', () => { migrator.migrate(_.cloneDeep(failedDoc)); expect('Did not throw').toEqual('But it should have!'); } catch (error) { - expect(error.message).toMatch(/Dang diggity!/); - const warning = loggingServiceMock.collect(mockLoggerFactory).warn[0][0]; - expect(warning).toContain(JSON.stringify(failedDoc)); - expect(warning).toContain('dog:1.2.3'); + expect(loggingServiceMock.collect(mockLoggerFactory).warn[0]).toMatchInlineSnapshot(` + Array [ + "Failed to apply the migration transform 'dog:1.2.3' to the document: dog:smelly", + Object { + "document": Object { + "attributes": Object {}, + "id": "smelly", + "migrationVersion": Object {}, + "type": "dog", + }, + "error": Object { + "message": "Dang diggity!", + "name": "Error", + "stack": "stack trace...", + }, + }, + ] + `); } }); diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts index 376f823267ebe6..953b5aff9dbdc3 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts @@ -76,7 +76,6 @@ export type TransformFn = (doc: SavedObjectUnsanitizedDoc) => SavedObjectUnsanit type ValidateDoc = (doc: SavedObjectUnsanitizedDoc) => void; interface DocumentMigratorOptions { - kibanaVersion: string; typeRegistry: ISavedObjectTypeRegistry; validateDoc: ValidateDoc; log: Logger; @@ -118,12 +117,11 @@ export class DocumentMigrator implements VersionedTransformer { * @prop {Logger} log - The migration logger * @memberof DocumentMigrator */ - constructor({ typeRegistry, kibanaVersion, log, validateDoc }: DocumentMigratorOptions) { + constructor({ typeRegistry, log, validateDoc }: DocumentMigratorOptions) { validateMigrationDefinition(typeRegistry); this.migrations = buildActiveMigrations(typeRegistry, log); this.transformDoc = buildDocumentTransform({ - kibanaVersion, migrations: this.migrations, validateDoc, }); @@ -231,11 +229,9 @@ function buildActiveMigrations( * Creates a function which migrates and validates any document that is passed to it. */ function buildDocumentTransform({ - kibanaVersion, migrations, validateDoc, }: { - kibanaVersion: string; migrations: ActiveMigrations; validateDoc: ValidateDoc; }): TransformFn { @@ -321,9 +317,9 @@ function wrapWithTry( return result; } catch (error) { const failedTransform = `${type}:${version}`; - const failedDoc = JSON.stringify(doc); log.warn( - `Failed to transform document ${doc}. Transform: ${failedTransform}\nDoc: ${failedDoc}` + `Failed to apply the migration transform '${failedTransform}' to the document: ${type}:${doc.id}`, + { document: doc, error: { message: error.message, name: error.name, stack: error.stack } } ); throw error; } diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts index 392089c69f5a08..af562a52ddec2a 100644 --- a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts @@ -40,6 +40,7 @@ describe('IndexMigrator', () => { migrate: _.identity, }, serializer: new SavedObjectsSerializer(new SavedObjectTypeRegistry()), + kibanaVersion: '7.0.0-test', }; }); @@ -59,16 +60,19 @@ describe('IndexMigrator', () => { _meta: { migrationMappingPropertyHashes: { foo: '18c78c995965207ed3f6e7fc5c6e55fe', + unsafe_properties: '5ef305b18111b77789afefbd36b66171', migrationVersion: '4a1746014a75ade3a714e1db5763276f', namespace: '2f4316de49999235636386fe51dc06c1', namespaces: '2f4316de49999235636386fe51dc06c1', references: '7997cf5a56cc02bdc9c93361bde732b0', type: '2f4316de49999235636386fe51dc06c1', updated_at: '00da57df13e94e9d98437d13ace4bfe0', + status: '2f4316de49999235636386fe51dc06c1', }, }, properties: { foo: { type: 'long' }, + unsafe_properties: { enabled: false, type: 'object' }, migrationVersion: { dynamic: 'true', type: 'object' }, namespace: { type: 'keyword' }, namespaces: { type: 'keyword' }, @@ -82,6 +86,7 @@ describe('IndexMigrator', () => { id: { type: 'keyword' }, }, }, + status: { type: 'keyword' }, }, }, settings: { number_of_shards: 1, auto_expand_replicas: '0-1' }, @@ -178,17 +183,20 @@ describe('IndexMigrator', () => { _meta: { migrationMappingPropertyHashes: { foo: '625b32086eb1d1203564cf85062dd22e', + unsafe_properties: '5ef305b18111b77789afefbd36b66171', migrationVersion: '4a1746014a75ade3a714e1db5763276f', namespace: '2f4316de49999235636386fe51dc06c1', namespaces: '2f4316de49999235636386fe51dc06c1', references: '7997cf5a56cc02bdc9c93361bde732b0', type: '2f4316de49999235636386fe51dc06c1', updated_at: '00da57df13e94e9d98437d13ace4bfe0', + status: '2f4316de49999235636386fe51dc06c1', }, }, properties: { author: { type: 'text' }, foo: { type: 'text' }, + unsafe_properties: { enabled: false, type: 'object' }, migrationVersion: { dynamic: 'true', type: 'object' }, namespace: { type: 'keyword' }, namespaces: { type: 'keyword' }, @@ -202,6 +210,7 @@ describe('IndexMigrator', () => { id: { type: 'keyword' }, }, }, + status: { type: 'keyword' }, }, }, settings: { number_of_shards: 1, auto_expand_replicas: '0-1' }, @@ -292,7 +301,7 @@ describe('IndexMigrator', () => { { body: [ { index: { _id: 'foo:1', _index: '.kibana_2' } }, - { foo: { name: 1 }, type: 'foo', migrationVersion: {}, references: [] }, + { foo: { name: 1 }, type: 'foo', migrationVersion: {}, references: [], status: 'valid' }, ], }, ]); @@ -301,7 +310,7 @@ describe('IndexMigrator', () => { { body: [ { index: { _id: 'foo:2', _index: '.kibana_2' } }, - { foo: { name: 2 }, type: 'foo', migrationVersion: {}, references: [] }, + { foo: { name: 2 }, type: 'foo', migrationVersion: {}, references: [], status: 'valid' }, ], }, ]); diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.ts b/src/core/server/saved_objects/migrations/core/index_migrator.ts index b2ffe2ad04a880..ca449d00de313b 100644 --- a/src/core/server/saved_objects/migrations/core/index_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/index_migrator.ts @@ -195,7 +195,7 @@ async function migrateSourceToDest(context: Context) { await Index.write( callCluster, dest.indexName, - migrateRawDocs(serializer, documentMigrator.migrate, docs, log) + migrateRawDocs(serializer, documentMigrator.migrate, docs, log, context.kibanaVersion) ); } } diff --git a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts index e55b72be2436d9..1f7bc07e12632d 100644 --- a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts +++ b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts @@ -24,7 +24,7 @@ import { migrateRawDocs } from './migrate_raw_docs'; import { createSavedObjectsMigrationLoggerMock } from '../../migrations/mocks'; describe('migrateRawDocs', () => { - test('converts raw docs to saved objects', async () => { + it('deserializes raw docs to saved objects before applying transform and serializing results to raw docs', async () => { const transform = jest.fn((doc: any) => _.set(doc, 'attributes.name', 'HOI!')); const result = migrateRawDocs( new SavedObjectsSerializer(new SavedObjectTypeRegistry()), @@ -33,24 +33,80 @@ describe('migrateRawDocs', () => { { _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }, { _id: 'c:d', _source: { type: 'c', c: { name: 'DDD' } } }, ], - createSavedObjectsMigrationLoggerMock() + createSavedObjectsMigrationLoggerMock(), + '7.0.0-test' ); expect(result).toEqual([ { _id: 'a:b', - _source: { type: 'a', a: { name: 'HOI!' }, migrationVersion: {}, references: [] }, + _source: { + type: 'a', + a: { name: 'HOI!' }, + migrationVersion: {}, + references: [], + status: 'valid', + }, }, { _id: 'c:d', - _source: { type: 'c', c: { name: 'HOI!' }, migrationVersion: {}, references: [] }, + _source: { + type: 'c', + c: { name: 'HOI!' }, + migrationVersion: {}, + references: [], + status: 'valid', + }, }, ]); expect(transform).toHaveBeenCalled(); }); - test('passes invalid docs through untouched and logs error', async () => { + it('if the transform function throws it serializes the raw doc as invalid', () => { + const throwsOnTypeATransform = jest.fn((doc: any) => { + if (doc.type === 'a') throw new Error("type 'a' transform exception"); + else return _.set(doc, 'attributes.name', 'HOI!'); + }); + const result = migrateRawDocs( + new SavedObjectsSerializer(new SavedObjectTypeRegistry()), + throwsOnTypeATransform, + [ + { _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }, + { _id: 'c:d', _source: { type: 'c', c: { name: 'DDD' } } }, + ], + createSavedObjectsMigrationLoggerMock(), + '7.0.0-test' + ); + + expect(result).toEqual([ + { + _id: 'a:b', + _source: { + type: 'a', + unsafe_properties: { a: { name: 'AAA' } }, + migrationVersion: { + '_invalid:a': '7.0.0-test', + }, + status: 'invalid', + }, + }, + { + _id: 'c:d', + _source: { + type: 'c', + c: { name: 'HOI!' }, + migrationVersion: {}, + references: [], + status: 'valid', + }, + }, + ]); + + expect(throwsOnTypeATransform).toHaveBeenCalled(); + }); + + it('if isRawSavedObject=false it skips the transform and serializes the raw doc as corrupt', async () => { const logger = createSavedObjectsMigrationLoggerMock(); const transform = jest.fn((doc: any) => _.set(_.cloneDeep(doc), 'attributes.name', 'TADA') @@ -62,14 +118,29 @@ describe('migrateRawDocs', () => { { _id: 'foo:b', _source: { type: 'a', a: { name: 'AAA' } } }, { _id: 'c:d', _source: { type: 'c', c: { name: 'DDD' } } }, ], - logger + logger, + '7.0.0-test' ); expect(result).toEqual([ - { _id: 'foo:b', _source: { type: 'a', a: { name: 'AAA' } } }, + { + _id: 'foo:b', + _source: { + type: 'a', + unsafe_properties: { a: { name: 'AAA' } }, + migrationVersion: { _corrupt: '7.0.0-test' }, + status: 'corrupt', + }, + }, { _id: 'c:d', - _source: { type: 'c', c: { name: 'TADA' }, migrationVersion: {}, references: [] }, + _source: { + type: 'c', + c: { name: 'TADA' }, + references: [], + migrationVersion: {}, + status: 'valid', + }, }, ]); @@ -87,6 +158,6 @@ describe('migrateRawDocs', () => { ], ]); - expect(logger.error).toBeCalledTimes(1); + expect(logger.warn).toBeCalledTimes(1); }); }); diff --git a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts index a2b72ea76c1a28..41b2b980b0e828 100644 --- a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts +++ b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts @@ -16,18 +16,21 @@ * specific language governing permissions and limitations * under the License. */ - -/* - * This file provides logic for migrating raw documents. - */ - import { SavedObjectsRawDoc, SavedObjectsSerializer } from '../../serialization'; import { TransformFn } from './document_migrator'; import { SavedObjectsMigrationLogger } from '.'; +import { defaultMapping } from './build_active_mappings'; /** - * Applies the specified migration function to every saved object document in the list - * of raw docs. Any raw docs that are not valid saved objects will simply be passed through. + * Applies the specified migration function to every saved object document in + * the list of raw docs. + * + * If the raw document is corrupt or migrating the document fails: + * 1. Serialize the doc as unsafe to prevent write errors due to a mapping + * mismatch. This also marks the document with status = 'invalid' | + * 'corrupt' which will exclude it from future migrations. + * 2. Set a special invalid migration version number, so that future versions + * of Kibana can possibly attempt to recover this doc. * * @param {TransformFn} migrateDoc * @param {SavedObjectsRawDoc[]} rawDocs @@ -37,22 +40,86 @@ export function migrateRawDocs( serializer: SavedObjectsSerializer, migrateDoc: TransformFn, rawDocs: SavedObjectsRawDoc[], - log: SavedObjectsMigrationLogger + logger: SavedObjectsMigrationLogger, + kibanaVersion: string ): SavedObjectsRawDoc[] { return rawDocs.map((raw) => { - if (serializer.isRawSavedObject(raw)) { + // corrupt - unable to deserialize due to missing type field, id doesn't match type / namespace fields + // invalid:type - can serialize but cannot apply migration function + + if (!serializer.isRawSavedObject(raw)) { + logger.warn('marking object as corrupt please contact support'); + return sanitizeUnsafeRawDoc(raw, kibanaVersion, 'corrupt'); + } + + try { const savedObject = serializer.rawToSavedObject(raw); savedObject.migrationVersion = savedObject.migrationVersion || {}; + + const migrated = migrateDoc(savedObject); return serializer.savedObjectToRaw({ references: [], - ...migrateDoc(savedObject), + ...migrated, }); + } catch (err) { + logger.warn('marking object as invalid please contact support'); + return sanitizeUnsafeRawDoc(raw, kibanaVersion, 'invalid'); } - - log.error( - `Error: Unable to migrate the corrupt Saved Object document ${raw._id}. To prevent Kibana from performing a migration on every restart, please delete or fix this document by ensuring that the namespace and type in the document's id matches the values in the namespace and type fields.`, - { rawDocument: raw } - ); - return raw; }); } + +// TODO: Does this belong in the serialization layer? +/** + * When we can't serialize corrupt documents or can't migrate invalid + * documents we can't guarantee that their fields match the mappings in the + * saved objects index. This function transforms unsafe documents into a + * format that's safe to persist by doing the following: + * 1. To prevent indexing errors we move all but the rootProperties out of + * _source and into _source.unsafeProperties + * 2. We set the status root property to 'invalid' or 'corrupt' to prevent + * this document from triggering future migrations. + * @param raw + * @param kibanaVersion + * @param failureReason + */ +function sanitizeUnsafeRawDoc( + raw: SavedObjectsRawDoc, + kibanaVersion: string, + failureReason: 'invalid' | 'corrupt' +) { + const invalidMigrationVersionType = + failureReason === 'corrupt' ? '_corrupt' : '_invalid:' + raw._source.type; + + const rootProperties = Object.keys(defaultMapping().properties); + + let safeRawSource: SavedObjectsRawDoc['_source'] = Object.keys(raw._source).reduce( + (acc, key) => { + if (rootProperties.includes(key)) { + acc[key] = raw._source[key]; + } else { + acc.unsafe_properties[key] = raw._source[key]; + } + + return acc; + }, + { unsafe_properties: {} } as any + ); + + safeRawSource = { + ...safeRawSource, + ...{ + status: failureReason, + migrationVersion: { + ...safeRawSource.migrationVersion, + ...{ [invalidMigrationVersionType]: kibanaVersion }, + }, + }, + }; + + return { + ...raw, + ...{ + _source: safeRawSource, + }, + }; +} diff --git a/src/core/server/saved_objects/migrations/core/migration_context.ts b/src/core/server/saved_objects/migrations/core/migration_context.ts index 3a6145f5d9623f..a3e7989243762f 100644 --- a/src/core/server/saved_objects/migrations/core/migration_context.ts +++ b/src/core/server/saved_objects/migrations/core/migration_context.ts @@ -44,6 +44,7 @@ export interface MigrationOpts { documentMigrator: VersionedTransformer; serializer: SavedObjectsSerializer; convertToAliasScript?: string; + kibanaVersion: string; /** * If specified, templates matching the specified pattern will be removed @@ -65,6 +66,7 @@ export interface Context { serializer: SavedObjectsSerializer; obsoleteIndexTemplatePattern?: string; convertToAliasScript?: string; + kibanaVersion: string; } /** @@ -90,6 +92,7 @@ export async function migrationContext(opts: MigrationOpts): Promise { serializer: opts.serializer, obsoleteIndexTemplatePattern: opts.obsoleteIndexTemplatePattern, convertToAliasScript: opts.convertToAliasScript, + kibanaVersion: opts.kibanaVersion, }; } diff --git a/src/core/server/saved_objects/migrations/core/migration_logger.ts b/src/core/server/saved_objects/migrations/core/migration_logger.ts index 00ed8bf0b73fc5..2e340e1990bee3 100644 --- a/src/core/server/saved_objects/migrations/core/migration_logger.ts +++ b/src/core/server/saved_objects/migrations/core/migration_logger.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - import { Logger, LogMeta } from '../../../logging'; /* @@ -28,13 +27,13 @@ export type LogFn = (path: string[], message: string) => void; /** @public */ export interface SavedObjectsMigrationLogger { - debug: (msg: string) => void; - info: (msg: string) => void; + debug: (msg: string, meta?: LogMeta) => void; + info: (msg: string, meta?: LogMeta) => void; /** * @deprecated Use `warn` instead. */ warning: (msg: string) => void; - warn: (msg: string) => void; + warn: (msg: string, meta?: LogMeta) => void; error: (msg: string, meta: LogMeta) => void; } @@ -45,9 +44,9 @@ export class MigrationLogger implements SavedObjectsMigrationLogger { this.logger = log; } - public info = (msg: string) => this.logger.info(msg); - public debug = (msg: string) => this.logger.debug(msg); + public info = (msg: string, meta?: LogMeta) => this.logger.info(msg, meta); + public debug = (msg: string, meta?: LogMeta) => this.logger.debug(msg, meta); public warning = (msg: string) => this.logger.warn(msg); - public warn = (msg: string) => this.logger.warn(msg); + public warn = (msg: string, meta?: LogMeta) => this.logger.warn(msg, meta); public error = (msg: string, meta: LogMeta) => this.logger.error(msg, meta); } diff --git a/src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap b/src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap index 3453f3fc803103..f71eedb95a8213 100644 --- a/src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap +++ b/src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap @@ -10,7 +10,9 @@ Object { "namespace": "2f4316de49999235636386fe51dc06c1", "namespaces": "2f4316de49999235636386fe51dc06c1", "references": "7997cf5a56cc02bdc9c93361bde732b0", + "status": "2f4316de49999235636386fe51dc06c1", "type": "2f4316de49999235636386fe51dc06c1", + "unsafe_properties": "5ef305b18111b77789afefbd36b66171", "updated_at": "00da57df13e94e9d98437d13ace4bfe0", }, }, @@ -54,9 +56,16 @@ Object { }, "type": "nested", }, + "status": Object { + "type": "keyword", + }, "type": Object { "type": "keyword", }, + "unsafe_properties": Object { + "enabled": false, + "type": "object", + }, "updated_at": Object { "type": "date", }, diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts index 7a5c044924d0ee..5fa86541416153 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts @@ -111,7 +111,6 @@ function mockOptions(): KibanaMigratorOptions { const callCluster = jest.fn(); return { logger: loggingServiceMock.create().get(), - kibanaVersion: '8.2.3', savedObjectValidations: {}, typeRegistry: createRegistry([ { @@ -149,5 +148,6 @@ function mockOptions(): KibanaMigratorOptions { skip: false, }, callCluster, + kibanaVersion: '7.0.0-test', }; } diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts index 69b57a498936e8..90531023155dc2 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts @@ -46,9 +46,9 @@ export interface KibanaMigratorOptions { typeRegistry: ISavedObjectTypeRegistry; savedObjectsConfig: SavedObjectsMigrationConfigType; kibanaConfig: KibanaConfigType; - kibanaVersion: string; logger: Logger; savedObjectValidations: PropertyValidators; + kibanaVersion: string; } export type IKibanaMigrator = Pick; @@ -75,6 +75,7 @@ export class KibanaMigrator { status: 'waiting', }); private readonly activeMappings: IndexMapping; + private readonly kibanaVersion: string; /** * Creates an instance of KibanaMigrator. @@ -85,8 +86,8 @@ export class KibanaMigrator { kibanaConfig, savedObjectsConfig, savedObjectValidations, - kibanaVersion, logger, + kibanaVersion, }: KibanaMigratorOptions) { this.callCluster = callCluster; this.kibanaConfig = kibanaConfig; @@ -96,7 +97,6 @@ export class KibanaMigrator { this.mappingProperties = mergeTypes(this.typeRegistry.getAllTypes()); this.log = logger; this.documentMigrator = new DocumentMigrator({ - kibanaVersion, typeRegistry, validateDoc: docValidator(savedObjectValidations || {}), log: this.log, @@ -104,6 +104,7 @@ export class KibanaMigrator { // Building the active mappings (and associated md5sums) is an expensive // operation so we cache the result this.activeMappings = buildActiveMappings(this.mappingProperties); + this.kibanaVersion = kibanaVersion; } /** @@ -165,6 +166,7 @@ export class KibanaMigrator { obsoleteIndexTemplatePattern: index === kibanaIndexName ? 'kibana_index_template*' : undefined, convertToAliasScript: indexMap[index].script, + kibanaVersion: this.kibanaVersion, }); }); diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index 48b1e12fc187ed..286d74e3076950 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -490,7 +490,6 @@ export class SavedObjectsService return new KibanaMigrator({ typeRegistry: this.typeRegistry, logger: this.logger, - kibanaVersion: this.coreContext.env.packageInfo.version, savedObjectsConfig, savedObjectValidations: this.validations, kibanaConfig, @@ -499,6 +498,7 @@ export class SavedObjectsService this.logger, migrationsRetryDelay ), + kibanaVersion: this.coreContext.env.packageInfo.version, }); } } diff --git a/src/core/server/saved_objects/serialization/serializer.test.ts b/src/core/server/saved_objects/serialization/serializer.test.ts index 1a7dfdd2d130e7..fe329cbc9ff390 100644 --- a/src/core/server/saved_objects/serialization/serializer.test.ts +++ b/src/core/server/saved_objects/serialization/serializer.test.ts @@ -51,7 +51,7 @@ const createSampleDoc = (raw: any, template = sampleTemplate): SavedObjectsRawDo _.defaultsDeep(raw, template); describe('#rawToSavedObject', () => { - test('it copies the _source.type property to type', () => { + it('copies the _source.type property to type', () => { const actual = singleNamespaceSerializer.rawToSavedObject({ _id: 'foo:bar', _source: { @@ -61,7 +61,7 @@ describe('#rawToSavedObject', () => { expect(actual).toHaveProperty('type', 'foo'); }); - test('it copies the _source.references property to references', () => { + it('copies the _source.references property to references', () => { const actual = singleNamespaceSerializer.rawToSavedObject({ _id: 'foo:bar', _source: { @@ -78,7 +78,7 @@ describe('#rawToSavedObject', () => { ]); }); - test('if specified it copies the _source.migrationVersion property to migrationVersion', () => { + it('if specified it copies the _source.migrationVersion property to migrationVersion', () => { const actual = singleNamespaceSerializer.rawToSavedObject({ _id: 'foo:bar', _source: { @@ -105,7 +105,7 @@ describe('#rawToSavedObject', () => { expect(actual).not.toHaveProperty('migrationVersion'); }); - test('it converts the id and type properties, and retains migrationVersion', () => { + it('converts the id and type properties, and retains migrationVersion', () => { const now = String(new Date()); const actual = singleNamespaceSerializer.rawToSavedObject({ _id: 'hello:world', @@ -179,7 +179,7 @@ describe('#rawToSavedObject', () => { ).toThrowErrorMatchingInlineSnapshot(`"_primary_term from elasticsearch must be an integer"`); }); - test(`if only _primary_term is throws`, () => { + test(`if only _primary_term is specified it throws`, () => { expect(() => singleNamespaceSerializer.rawToSavedObject({ _id: 'foo:bar', @@ -192,7 +192,7 @@ describe('#rawToSavedObject', () => { ).toThrowErrorMatchingInlineSnapshot(`"_seq_no from elasticsearch must be an integer"`); }); - test('if specified it copies the _source.updated_at property to updated_at', () => { + it('if specified it copies the _source.updated_at property to updated_at', () => { const now = Date(); const actual = singleNamespaceSerializer.rawToSavedObject({ _id: 'foo:bar', @@ -214,7 +214,7 @@ describe('#rawToSavedObject', () => { expect(actual).not.toHaveProperty('updated_at'); }); - test('it does not pass unknown properties through', () => { + it('does not pass unknown properties through', () => { const actual = singleNamespaceSerializer.rawToSavedObject({ _id: 'universe', _source: { @@ -235,7 +235,57 @@ describe('#rawToSavedObject', () => { }); }); - test('it does not create attributes if [type] is missing', () => { + it("if status: 'valid', copies [type].attributes to attributes", () => { + const actual = singleNamespaceSerializer.rawToSavedObject({ + _id: '1', + _source: { + type: 'dashboard', + dashboard: { name: 'my dashboard' }, + status: 'valid', + }, + }); + expect(actual).toEqual({ + id: '1', + type: 'dashboard', + attributes: { name: 'my dashboard' }, + references: [], + }); + }); + + it("if status: 'invalid', copies unsafe_properties[type] to attributes", () => { + const actual = singleNamespaceSerializer.rawToSavedObject({ + _id: '1', + _source: { + type: 'dashboard', + unsafe_properties: { + dashboard: { name: 'my invalid dashboard' }, + }, + status: 'invalid', + }, + }); + expect(actual).toEqual({ + id: '1', + type: 'dashboard', + attributes: { name: 'my invalid dashboard' }, + references: [], + }); + }); + + it("if status: 'corrupt', throws", () => { + expect(() => { + singleNamespaceSerializer.rawToSavedObject({ + _id: '1', + _source: { + type: 'dashboard', + status: 'corrupt', + }, + }); + }).toThrowErrorMatchingInlineSnapshot( + `"Cannot deserialize corrupt raw saved object document: 1"` + ); + }); + + it('does not create attributes if [type] is missing', () => { const actual = singleNamespaceSerializer.rawToSavedObject({ _id: 'universe', _source: { @@ -249,7 +299,7 @@ describe('#rawToSavedObject', () => { }); }); - test('it fails for documents which do not specify a type', () => { + it('throws for documents which do not specify a type', () => { expect(() => singleNamespaceSerializer.rawToSavedObject({ _id: 'universe', @@ -262,7 +312,7 @@ describe('#rawToSavedObject', () => { ).toThrow(/Expected "undefined" to be a saved object type/); }); - test('it is complimentary with savedObjectToRaw', () => { + it('is complimentary with savedObjectToRaw', () => { const raw = { _id: 'foo-namespace:foo:bar', _primary_term: 24, @@ -280,6 +330,7 @@ describe('#rawToSavedObject', () => { namespace: 'foo-namespace', updated_at: String(new Date()), references: [], + status: 'valid' as 'valid', }, }; @@ -290,7 +341,7 @@ describe('#rawToSavedObject', () => { ).toEqual(raw); }); - test('it handles unprefixed ids', () => { + it('handles unprefixed ids', () => { const actual = singleNamespaceSerializer.rawToSavedObject({ _id: 'universe', _source: { @@ -413,7 +464,7 @@ describe('#rawToSavedObject', () => { }); describe('#savedObjectToRaw', () => { - test('it copies the type property to _source.type and uses the ROOT_TYPE as _type', () => { + it('copies the type property to _source.type', () => { const actual = singleNamespaceSerializer.savedObjectToRaw({ type: 'foo', attributes: {}, @@ -422,13 +473,13 @@ describe('#savedObjectToRaw', () => { expect(actual._source).toHaveProperty('type', 'foo'); }); - test('it copies the references property to _source.references', () => { + it('copies the references property to _source.references', () => { const actual = singleNamespaceSerializer.savedObjectToRaw({ id: '1', type: 'foo', attributes: {}, references: [{ name: 'ref_0', type: 'index-pattern', id: 'pattern*' }], - }); + } as any); expect(actual._source).toHaveProperty('references', [ { name: 'ref_0', @@ -438,7 +489,7 @@ describe('#savedObjectToRaw', () => { ]); }); - test('if specified it copies the updated_at property to _source.updated_at', () => { + it('if specified it copies the updated_at property to _source.updated_at', () => { const now = new Date(); const actual = singleNamespaceSerializer.savedObjectToRaw({ type: '', @@ -449,7 +500,7 @@ describe('#savedObjectToRaw', () => { expect(actual._source).toHaveProperty('updated_at', now); }); - test(`if unspecified it doesn't add updated_at property to _source`, () => { + test("if unspecified it doesn't add updated_at property to _source", () => { const actual = singleNamespaceSerializer.savedObjectToRaw({ type: '', attributes: {}, @@ -458,7 +509,7 @@ describe('#savedObjectToRaw', () => { expect(actual._source).not.toHaveProperty('updated_at'); }); - test('it copies the migrationVersion property to _source.migrationVersion', () => { + it('copies the migrationVersion property to _source.migrationVersion', () => { const actual = singleNamespaceSerializer.savedObjectToRaw({ type: '', attributes: {}, @@ -483,7 +534,7 @@ describe('#savedObjectToRaw', () => { expect(actual._source).not.toHaveProperty('migrationVersion'); }); - test('it decodes the version property to _seq_no and _primary_term', () => { + it('decodes the version property to _seq_no and _primary_term', () => { const actual = singleNamespaceSerializer.savedObjectToRaw({ type: '', attributes: {}, @@ -514,7 +565,7 @@ describe('#savedObjectToRaw', () => { ).toThrowErrorMatchingInlineSnapshot(`"Invalid version [foo]"`); }); - test('it copies attributes to _source[type]', () => { + it('copies attributes to _source[type]', () => { const actual = singleNamespaceSerializer.savedObjectToRaw({ type: 'foo', attributes: { @@ -530,7 +581,7 @@ describe('#savedObjectToRaw', () => { }); describe('single-namespace type without a namespace', () => { - test('generates an id prefixed with type, if no id is specified', () => { + it('generates an id prefixed with type, if no id is specified', () => { const v1 = singleNamespaceSerializer.savedObjectToRaw({ type: 'foo', attributes: { bar: true }, @@ -556,7 +607,7 @@ describe('#savedObjectToRaw', () => { }); describe('single-namespace type with a namespace', () => { - test('generates an id prefixed with namespace and type, if no id is specified', () => { + it('generates an id prefixed with namespace and type, if no id is specified', () => { const v1 = singleNamespaceSerializer.savedObjectToRaw({ type: 'foo', namespace: 'bar', @@ -585,7 +636,7 @@ describe('#savedObjectToRaw', () => { }); describe('single-namespace type with namespaces', () => { - test('generates an id prefixed with type, if no id is specified', () => { + it('generates an id prefixed with type, if no id is specified', () => { const v1 = namespaceAgnosticSerializer.savedObjectToRaw({ type: 'foo', namespaces: ['bar'], @@ -614,7 +665,7 @@ describe('#savedObjectToRaw', () => { }); describe('namespace-agnostic type with a namespace', () => { - test('generates an id prefixed with type, if no id is specified', () => { + it('generates an id prefixed with type, if no id is specified', () => { const v1 = namespaceAgnosticSerializer.savedObjectToRaw({ type: 'foo', namespace: 'bar', @@ -643,7 +694,7 @@ describe('#savedObjectToRaw', () => { }); describe('namespace-agnostic type with namespaces', () => { - test('generates an id prefixed with type, if no id is specified', () => { + it('generates an id prefixed with type, if no id is specified', () => { const v1 = namespaceAgnosticSerializer.savedObjectToRaw({ type: 'foo', namespaces: ['bar'], @@ -672,7 +723,7 @@ describe('#savedObjectToRaw', () => { }); describe('multi-namespace type with a namespace', () => { - test('generates an id prefixed with type, if no id is specified', () => { + it('generates an id prefixed with type, if no id is specified', () => { const v1 = multiNamespaceSerializer.savedObjectToRaw({ type: 'foo', namespace: 'bar', @@ -701,7 +752,7 @@ describe('#savedObjectToRaw', () => { }); describe('multi-namespace type with namespaces', () => { - test('generates an id prefixed with type, if no id is specified', () => { + it('generates an id prefixed with type, if no id is specified', () => { const v1 = multiNamespaceSerializer.savedObjectToRaw({ type: 'foo', namespaces: ['bar'], @@ -732,7 +783,7 @@ describe('#savedObjectToRaw', () => { describe('#isRawSavedObject', () => { describe('single-namespace type without a namespace', () => { - test('is true if the id is prefixed and the type matches', () => { + it('is true if the id is prefixed and the type matches', () => { expect( singleNamespaceSerializer.isRawSavedObject({ _id: 'hello:world', @@ -744,7 +795,7 @@ describe('#isRawSavedObject', () => { ).toBeTruthy(); }); - test('is false if the id is not prefixed', () => { + it('is false if the id is not prefixed', () => { expect( singleNamespaceSerializer.isRawSavedObject({ _id: 'world', @@ -756,7 +807,7 @@ describe('#isRawSavedObject', () => { ).toBeFalsy(); }); - test('is false if the type attribute is missing', () => { + it('is false if the type attribute is missing', () => { expect( singleNamespaceSerializer.isRawSavedObject({ _id: 'hello:world', @@ -779,7 +830,7 @@ describe('#isRawSavedObject', () => { ).toBeFalsy(); }); - test('is false if the type attribute does not match the id', () => { + it('is false if the type attribute does not match the id', () => { expect( singleNamespaceSerializer.isRawSavedObject({ _id: 'hello:world', @@ -792,7 +843,7 @@ describe('#isRawSavedObject', () => { ).toBeFalsy(); }); - test('is false if there is no [type] attribute', () => { + it('is false if there is no [type] attribute', () => { expect( singleNamespaceSerializer.isRawSavedObject({ _id: 'hello:world', @@ -806,7 +857,7 @@ describe('#isRawSavedObject', () => { }); describe('single-namespace type with a namespace', () => { - test('is true if the id is prefixed with type and namespace and the type matches', () => { + it('is true if the id is prefixed with type and namespace and the type matches', () => { expect( singleNamespaceSerializer.isRawSavedObject({ _id: 'foo:hello:world', @@ -819,7 +870,7 @@ describe('#isRawSavedObject', () => { ).toBeTruthy(); }); - test('is false if the id is not prefixed by anything', () => { + it('is false if the id is not prefixed by anything', () => { expect( singleNamespaceSerializer.isRawSavedObject({ _id: 'world', @@ -832,7 +883,7 @@ describe('#isRawSavedObject', () => { ).toBeFalsy(); }); - test('is false if the id is prefixed only with type and the type matches', () => { + it('is false if the id is prefixed only with type and the type matches', () => { expect( singleNamespaceSerializer.isRawSavedObject({ _id: 'hello:world', @@ -845,7 +896,7 @@ describe('#isRawSavedObject', () => { ).toBeFalsy(); }); - test('is false if the id is prefixed only with namespace and the namespace matches', () => { + it('is false if the id is prefixed only with namespace and the namespace matches', () => { expect( singleNamespaceSerializer.isRawSavedObject({ _id: 'foo:world', @@ -871,7 +922,7 @@ describe('#isRawSavedObject', () => { ).toBeFalsy(); }); - test('is false if the type attribute is missing', () => { + it('is false if the type attribute is missing', () => { expect( singleNamespaceSerializer.isRawSavedObject({ _id: 'foo:hello:world', @@ -883,7 +934,7 @@ describe('#isRawSavedObject', () => { ).toBeFalsy(); }); - test('is false if the type attribute does not match the id', () => { + it('is false if the type attribute does not match the id', () => { expect( singleNamespaceSerializer.isRawSavedObject({ _id: 'foo:hello:world', @@ -897,7 +948,7 @@ describe('#isRawSavedObject', () => { ).toBeFalsy(); }); - test('is false if the namespace attribute does not match the id', () => { + it('is false if the namespace attribute does not match the id', () => { expect( singleNamespaceSerializer.isRawSavedObject({ _id: 'bar:jam:world', @@ -911,7 +962,7 @@ describe('#isRawSavedObject', () => { ).toBeFalsy(); }); - test('is false if there is no [type] attribute', () => { + it('is false if there is no [type] attribute', () => { expect( singleNamespaceSerializer.isRawSavedObject({ _id: 'hello:world', @@ -926,7 +977,7 @@ describe('#isRawSavedObject', () => { }); describe('namespace-agnostic type with a namespace', () => { - test('is true if the id is prefixed with type and the type matches', () => { + it('is true if the id is prefixed with type and the type matches', () => { expect( namespaceAgnosticSerializer.isRawSavedObject({ _id: 'hello:world', @@ -939,7 +990,7 @@ describe('#isRawSavedObject', () => { ).toBeTruthy(); }); - test('is false if the id is not prefixed', () => { + it('is false if the id is not prefixed', () => { expect( namespaceAgnosticSerializer.isRawSavedObject({ _id: 'world', @@ -952,7 +1003,7 @@ describe('#isRawSavedObject', () => { ).toBeFalsy(); }); - test('is false if the id is prefixed with type and namespace', () => { + it('is false if the id is prefixed with type and namespace', () => { expect( namespaceAgnosticSerializer.isRawSavedObject({ _id: 'foo:hello:world', @@ -978,7 +1029,7 @@ describe('#isRawSavedObject', () => { ).toBeFalsy(); }); - test('is false if the type attribute is missing', () => { + it('is false if the type attribute is missing', () => { expect( namespaceAgnosticSerializer.isRawSavedObject({ _id: 'hello:world', @@ -990,7 +1041,7 @@ describe('#isRawSavedObject', () => { ).toBeFalsy(); }); - test('is false if the type attribute does not match the id', () => { + it('is false if the type attribute does not match the id', () => { expect( namespaceAgnosticSerializer.isRawSavedObject({ _id: 'hello:world', @@ -1004,7 +1055,7 @@ describe('#isRawSavedObject', () => { ).toBeFalsy(); }); - test('is false if there is no [type] attribute', () => { + it('is false if there is no [type] attribute', () => { expect( namespaceAgnosticSerializer.isRawSavedObject({ _id: 'hello:world', @@ -1021,24 +1072,24 @@ describe('#isRawSavedObject', () => { describe('#generateRawId', () => { describe('single-namespace type without a namespace', () => { - test('generates an id if none is specified', () => { + it('generates an id if none is specified', () => { const id = singleNamespaceSerializer.generateRawId('', 'goodbye'); expect(id).toMatch(/^goodbye\:[\w-]+$/); }); - test('uses the id that is specified', () => { + it('uses the id that is specified', () => { const id = singleNamespaceSerializer.generateRawId('', 'hello', 'world'); expect(id).toEqual('hello:world'); }); }); describe('single-namespace type with a namespace', () => { - test('generates an id if none is specified and prefixes namespace', () => { + it('generates an id if none is specified and prefixes namespace', () => { const id = singleNamespaceSerializer.generateRawId('foo', 'goodbye'); expect(id).toMatch(/^foo:goodbye\:[\w-]+$/); }); - test('uses the id that is specified and prefixes the namespace', () => { + it('uses the id that is specified and prefixes the namespace', () => { const id = singleNamespaceSerializer.generateRawId('foo', 'hello', 'world'); expect(id).toEqual('foo:hello:world'); }); diff --git a/src/core/server/saved_objects/serialization/serializer.ts b/src/core/server/saved_objects/serialization/serializer.ts index 3b19d494d8ecf2..cbe76412eb17c8 100644 --- a/src/core/server/saved_objects/serialization/serializer.ts +++ b/src/core/server/saved_objects/serialization/serializer.ts @@ -64,7 +64,10 @@ export class SavedObjectsSerializer { */ public rawToSavedObject(doc: SavedObjectsRawDoc): SavedObjectSanitizedDoc { const { _id, _source, _seq_no, _primary_term } = doc; - const { type, namespace, namespaces } = _source; + const { type, namespace, namespaces, status = 'valid' } = _source; + + if (status === 'corrupt') + throw new Error(`Cannot deserialize corrupt raw saved object document: ${_id}`); const version = _seq_no != null || _primary_term != null @@ -76,7 +79,9 @@ export class SavedObjectsSerializer { id: this.trimIdPrefix(namespace, type, _id), ...(namespace && this.registry.isSingleNamespace(type) && { namespace }), ...(namespaces && this.registry.isMultiNamespace(type) && { namespaces }), - attributes: _source[type], + ...(status === 'valid' + ? { attributes: _source[type] } + : { attributes: _source.unsafe_properties?.[type] }), references: _source.references || [], ...(_source.migrationVersion && { migrationVersion: _source.migrationVersion }), ...(_source.updated_at && { updated_at: _source.updated_at }), @@ -102,13 +107,14 @@ export class SavedObjectsSerializer { references, } = savedObj; const source = { - [type]: attributes, type, references, ...(namespace && this.registry.isSingleNamespace(type) && { namespace }), ...(namespaces && this.registry.isMultiNamespace(type) && { namespaces }), + ...(attributes && { [type]: attributes }), ...(migrationVersion && { migrationVersion }), ...(updated_at && { updated_at }), + status: 'valid' as 'valid', }; return { diff --git a/src/core/server/saved_objects/serialization/types.ts b/src/core/server/saved_objects/serialization/types.ts index acd2c7b5284aa2..7b99a858ecfac5 100644 --- a/src/core/server/saved_objects/serialization/types.ts +++ b/src/core/server/saved_objects/serialization/types.ts @@ -40,6 +40,8 @@ export interface SavedObjectsRawDocSource { migrationVersion?: SavedObjectsMigrationVersion; updated_at?: string; references?: SavedObjectReference[]; + status?: 'valid' | 'invalid' | 'corrupt'; + unsafe_properties?: Record; [typeMapping: string]: any; } diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 83e037fb2da66f..8ec5e83dd411a7 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -444,6 +444,7 @@ describe('SavedObjectsRepository', () => { references, ...mockTimestampFields, migrationVersion: migrationVersion || { [type]: '1.1.1' }, + status: 'valid', }, ...mockVersionProps, }, diff --git a/src/core/server/saved_objects/service/lib/repository_create_repository.test.ts b/src/core/server/saved_objects/service/lib/repository_create_repository.test.ts index ea881805e1ae6b..e99229620d17ba 100644 --- a/src/core/server/saved_objects/service/lib/repository_create_repository.test.ts +++ b/src/core/server/saved_objects/service/lib/repository_create_repository.test.ts @@ -102,6 +102,7 @@ describe('SavedObjectsRepository#createRepository', () => { expect(repository).toBeDefined(); expect(RepositoryConstructor.mock.calls[0][0].allowedTypes).toMatchInlineSnapshot(` Array [ + "unsafe_properties", "nsAgnosticType", "nsType", ] @@ -120,6 +121,7 @@ describe('SavedObjectsRepository#createRepository', () => { expect(repository).toBeDefined(); expect(RepositoryConstructor.mock.calls[0][0].allowedTypes).toMatchInlineSnapshot(` Array [ + "unsafe_properties", "nsAgnosticType", "nsType", "hiddenType",