diff --git a/packages/core/saved-objects/core-saved-objects-api-browser/src/apis/create.ts b/packages/core/saved-objects/core-saved-objects-api-browser/src/apis/create.ts index a58b4b4a2868dc..b9db75e204119a 100644 --- a/packages/core/saved-objects/core-saved-objects-api-browser/src/apis/create.ts +++ b/packages/core/saved-objects/core-saved-objects-api-browser/src/apis/create.ts @@ -35,4 +35,12 @@ export interface SavedObjectsCreateOptions { typeMigrationVersion?: string; /** Array of referenced saved objects. */ references?: SavedObjectReference[]; + /** + * Flag indicating if a saved object is managed by Kibana (default=false) + * + * This can be leveraged by applications to e.g. prevent edits to a managed + * saved object. Instead, users can be guided to create a copy first and + * make their edits to the copy. + */ + managed?: boolean; } diff --git a/packages/core/saved-objects/core-saved-objects-api-browser/src/simple_saved_object.ts b/packages/core/saved-objects/core-saved-objects-api-browser/src/simple_saved_object.ts index 12486a32a8c03a..2f3cb8dc8fc08e 100644 --- a/packages/core/saved-objects/core-saved-objects-api-browser/src/simple_saved_object.ts +++ b/packages/core/saved-objects/core-saved-objects-api-browser/src/simple_saved_object.ts @@ -49,6 +49,14 @@ export interface SimpleSavedObject { * `namespaceType: 'agnostic'`. */ namespaces: SavedObjectType['namespaces']; + /** + * Flag indicating if a saved object is managed by Kibana (default=false) + * + * This can be leveraged by applications to e.g. prevent edits to a managed + * saved object. Instead, users can be guided to create a copy first and + * make their edits to the copy. + */ + managed: SavedObjectType['managed']; /** * Gets an attribute of this object diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/included_fields.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/included_fields.test.ts index 13dacc4f07a641..3ccfd5ca6f2a9b 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/included_fields.test.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/included_fields.test.ts @@ -20,6 +20,7 @@ describe('getRootFields', () => { "migrationVersion", "coreMigrationVersion", "typeMigrationVersion", + "managed", "updated_at", "created_at", "originId", diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/included_fields.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/included_fields.ts index 8ee3f705854527..20e980f4707a1b 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/included_fields.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/included_fields.ts @@ -18,6 +18,7 @@ const ROOT_FIELDS = [ 'migrationVersion', 'coreMigrationVersion', 'typeMigrationVersion', + 'managed', 'updated_at', 'created_at', 'originId', diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_utils.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_utils.test.ts index 569142ef8dc5d6..cba31cbd021624 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_utils.test.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_utils.test.ts @@ -18,6 +18,7 @@ import { normalizeNamespace, rawDocExistsInNamespace, rawDocExistsInNamespaces, + setManaged, } from './internal_utils'; describe('#getBulkOperationError', () => { @@ -99,6 +100,7 @@ describe('#getSavedObjectFromSource', () => { const originId = 'originId'; // eslint-disable-next-line @typescript-eslint/naming-convention const updated_at = 'updatedAt'; + const managed = false; function createRawDoc( type: string, @@ -115,6 +117,7 @@ describe('#getSavedObjectFromSource', () => { migrationVersion, coreMigrationVersion, typeMigrationVersion, + managed, originId, updated_at, ...namespaceAttrs, @@ -131,6 +134,7 @@ describe('#getSavedObjectFromSource', () => { attributes, coreMigrationVersion, typeMigrationVersion, + managed, id, migrationVersion, namespaces: expect.anything(), // see specific test cases below @@ -406,3 +410,26 @@ describe('#getCurrentTime', () => { expect(getCurrentTime()).toEqual('2021-09-10T21:00:00.000Z'); }); }); + +describe('#setManaged', () => { + it('returns false if no arguments are provided', () => { + expect(setManaged({})).toEqual(false); + }); + + it('returns false if only one argument is provided as false', () => { + expect(setManaged({ optionsManaged: false })).toEqual(false); + expect(setManaged({ objectManaged: false })).toEqual(false); + }); + + it('returns true if only one argument is provided as true', () => { + expect(setManaged({ optionsManaged: true })).toEqual(true); + expect(setManaged({ objectManaged: true })).toEqual(true); + }); + + it('overrides objectManaged with optionsManaged', () => { + expect(setManaged({ optionsManaged: false, objectManaged: true })).toEqual(false); + expect(setManaged({ optionsManaged: true, objectManaged: false })).toEqual(true); + expect(setManaged({ optionsManaged: false, objectManaged: false })).toEqual(false); + expect(setManaged({ optionsManaged: true, objectManaged: true })).toEqual(true); + }); +}); diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_utils.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_utils.ts index 03f3c2c698c3e2..afafd67fcab3a6 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_utils.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_utils.ts @@ -144,6 +144,7 @@ export function getSavedObjectFromSource( created_at: createdAt, coreMigrationVersion, typeMigrationVersion, + managed, migrationVersion = migrationVersionCompatibility === 'compatible' && typeMigrationVersion ? { [type]: typeMigrationVersion } : undefined, @@ -169,6 +170,7 @@ export function getSavedObjectFromSource( version: encodeHitVersion(doc), attributes: doc._source[type], references: doc._source.references || [], + managed, }; } @@ -272,3 +274,30 @@ export function normalizeNamespace(namespace?: string) { export function getCurrentTime() { return new Date(Date.now()).toISOString(); } + +/** + * Returns the managed boolean to apply to a document as it's managed value. + * For use by applications to modify behavior for managed saved objects. + * The behavior is as follows: + * If `optionsManaged` is set, it will override any existing `managed` value in all the documents being created + * If `optionsManaged` is not provided, then the documents are created with whatever may be assigned to their `managed` property + * or default to `false`. + * + * @internal + */ + +export function setManaged({ + optionsManaged, + objectManaged, +}: { + optionsManaged?: boolean; + objectManaged?: boolean; +}): boolean { + if (optionsManaged !== undefined) { + return optionsManaged; + } else if (optionsManaged === undefined && objectManaged !== undefined) { + return objectManaged; + } else { + return false; + } +} diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts index ccbe414bf82f40..f0aea1afaf5fd2 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts @@ -189,6 +189,7 @@ describe('SavedObjectsRepository', () => { ...(doc.attributes?.title && { title: `${doc.attributes.title}!!` }), }, migrationVersion: mockMigrationVersion, + managed: doc.managed ?? false, references: [{ name: 'search_0', type: 'search', id: '123' }], }); @@ -205,12 +206,14 @@ describe('SavedObjectsRepository', () => { id: '6.0.0-alpha1', attributes: { title: 'Test One' }, references: [{ name: 'ref_0', type: 'test', id: '1' }], + managed: false, }; const obj2 = { type: 'index-pattern', id: 'logstash-*', attributes: { title: 'Test Two' }, references: [{ name: 'ref_0', type: 'test', id: '2' }], + managed: false, }; const namespace = 'foo-namespace'; @@ -259,7 +262,6 @@ describe('SavedObjectsRepository', () => { ...mockTimestampFields, }), ]; - describe('client calls', () => { it(`should use the ES bulk action by default`, async () => { await bulkCreateSuccess(client, repository, [obj1, obj2]); @@ -318,6 +320,7 @@ describe('SavedObjectsRepository', () => { const obj1WithSeq = { ...obj1, + managed: obj1.managed, if_seq_no: mockVersionProps._seq_no, if_primary_term: mockVersionProps._primary_term, }; @@ -330,6 +333,19 @@ describe('SavedObjectsRepository', () => { expectClientCallArgsAction([obj1, obj2], { method: 'create' }); }); + it(`should use the ES index method if ID is defined, overwrite=true and managed=true in a document`, async () => { + await bulkCreateSuccess(client, repository, [obj1, obj2], { + overwrite: true, + managed: true, + }); + expectClientCallArgsAction([obj1, obj2], { method: 'index' }); + }); + + it(`should use the ES create method if ID is defined, overwrite=false and managed=true in a document`, async () => { + await bulkCreateSuccess(client, repository, [obj1, obj2], { managed: true }); + expectClientCallArgsAction([obj1, obj2], { method: 'create' }); + }); + it(`formats the ES request`, async () => { await bulkCreateSuccess(client, repository, [obj1, obj2]); const body = [...expectObjArgs(obj1), ...expectObjArgs(obj2)]; @@ -338,6 +354,17 @@ describe('SavedObjectsRepository', () => { expect.anything() ); }); + // this test only ensures that the client accepts the managed field in a document + it(`formats the ES request with managed=true in a document`, async () => { + const obj1WithManagedTrue = { ...obj1, managed: true }; + const obj2WithManagedTrue = { ...obj2, managed: true }; + await bulkCreateSuccess(client, repository, [obj1WithManagedTrue, obj2WithManagedTrue]); + const body = [...expectObjArgs(obj1WithManagedTrue), ...expectObjArgs(obj2WithManagedTrue)]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + }); describe('originId', () => { it(`returns error if originId is set for non-multi-namespace type`, async () => { @@ -439,6 +466,27 @@ describe('SavedObjectsRepository', () => { ); }); + // this only ensures we don't override any other options + it(`adds managed=false to request body if declared for any types that are single-namespace`, async () => { + await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace, managed: false }); + const expected = expect.objectContaining({ namespace, managed: false }); + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + }); + // this only ensures we don't override any other options + it(`adds managed=true to request body if declared for any types that are single-namespace`, async () => { + await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace, managed: true }); + const expected = expect.objectContaining({ namespace, managed: true }); + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + }); + it(`normalizes options.namespace from 'default' to undefined`, async () => { await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace: 'default' }); const expected = expect.not.objectContaining({ namespace: 'default' }); @@ -825,10 +873,8 @@ describe('SavedObjectsRepository', () => { it(`migrates the docs and serializes the migrated docs`, async () => { migrator.migrateDocument.mockImplementation(mockMigrateDocument); const modifiedObj1 = { ...obj1, coreMigrationVersion: '8.0.0' }; - await bulkCreateSuccess(client, repository, [modifiedObj1, obj2]); const docs = [modifiedObj1, obj2].map((x) => ({ ...x, ...mockTimestampFieldsWithCreated })); - expectMigrationArgs(docs[0], true, 1); expectMigrationArgs(docs[1], true, 2); @@ -964,6 +1010,96 @@ describe('SavedObjectsRepository', () => { expect.stringMatching(/^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/) ); expect(result.saved_objects[1].id).toEqual(obj2.id); + + // Assert that managed is not changed + expect(result.saved_objects[0].managed).toBeFalsy(); + expect(result.saved_objects[1].managed).toEqual(obj2.managed); + }); + + it(`sets managed=false if not already set`, async () => { + const obj1WithoutManaged = { + type: 'config', + id: '6.0.0-alpha1', + attributes: { title: 'Test One' }, + references: [{ name: 'ref_0', type: 'test', id: '1' }], + }; + const obj2WithoutManaged = { + type: 'index-pattern', + id: 'logstash-*', + attributes: { title: 'Test Two' }, + references: [{ name: 'ref_0', type: 'test', id: '2' }], + }; + const result = await bulkCreateSuccess(client, repository, [ + obj1WithoutManaged, + obj2WithoutManaged, + ]); + expect(result).toEqual({ + saved_objects: [obj1, obj2].map((x) => expectCreateResult(x)), + }); + }); + + it(`sets managed=false only on documents without managed already set`, async () => { + const objWithoutManaged = { + type: 'config', + id: '6.0.0-alpha1', + attributes: { title: 'Test One' }, + references: [{ name: 'ref_0', type: 'test', id: '1' }], + }; + const result = await bulkCreateSuccess(client, repository, [objWithoutManaged, obj2]); + expect(result).toEqual({ + saved_objects: [obj1, obj2].map((x) => expectCreateResult(x)), + }); + }); + + it(`sets managed=true if provided as an override`, async () => { + const obj1WithoutManaged = { + type: 'config', + id: '6.0.0-alpha1', + attributes: { title: 'Test One' }, + references: [{ name: 'ref_0', type: 'test', id: '1' }], + }; + const obj2WithoutManaged = { + type: 'index-pattern', + id: 'logstash-*', + attributes: { title: 'Test Two' }, + references: [{ name: 'ref_0', type: 'test', id: '2' }], + }; + const result = await bulkCreateSuccess( + client, + repository, + [obj1WithoutManaged, obj2WithoutManaged], + { managed: true } + ); + expect(result).toEqual({ + saved_objects: [ + { ...obj1WithoutManaged, managed: true }, + { ...obj2WithoutManaged, managed: true }, + ].map((x) => expectCreateResult(x)), + }); + }); + + it(`sets managed=false if provided as an override`, async () => { + const obj1WithoutManaged = { + type: 'config', + id: '6.0.0-alpha1', + attributes: { title: 'Test One' }, + references: [{ name: 'ref_0', type: 'test', id: '1' }], + }; + const obj2WithoutManaged = { + type: 'index-pattern', + id: 'logstash-*', + attributes: { title: 'Test Two' }, + references: [{ name: 'ref_0', type: 'test', id: '2' }], + }; + const result = await bulkCreateSuccess( + client, + repository, + [obj1WithoutManaged, obj2WithoutManaged], + { managed: false } + ); + expect(result).toEqual({ + saved_objects: [obj1, obj2].map((x) => expectCreateResult(x)), + }); }); }); }); @@ -2392,18 +2528,42 @@ describe('SavedObjectsRepository', () => { }; describe('client calls', () => { - it(`should use the ES index action if ID is not defined and overwrite=true`, async () => { + it(`should use the ES index action if ID is not defined`, async () => { await createSuccess(type, attributes, { overwrite: true }); expect(mockPreflightCheckForCreate).not.toHaveBeenCalled(); expect(client.index).toHaveBeenCalled(); }); + it(`should use the ES index action if ID is not defined and a doc has managed=true`, async () => { + await createSuccess(type, attributes, { overwrite: true, managed: true }); + expect(mockPreflightCheckForCreate).not.toHaveBeenCalled(); + expect(client.index).toHaveBeenCalled(); + }); + + it(`should use the ES index action if ID is not defined and a doc has managed=false`, async () => { + await createSuccess(type, attributes, { overwrite: true, managed: false }); + expect(mockPreflightCheckForCreate).not.toHaveBeenCalled(); + expect(client.index).toHaveBeenCalled(); + }); + it(`should use the ES create action if ID is not defined and overwrite=false`, async () => { await createSuccess(type, attributes); expect(mockPreflightCheckForCreate).not.toHaveBeenCalled(); expect(client.create).toHaveBeenCalled(); }); + it(`should use the ES create action if ID is not defined, overwrite=false and a doc has managed=true`, async () => { + await createSuccess(type, attributes, { managed: true }); + expect(mockPreflightCheckForCreate).not.toHaveBeenCalled(); + expect(client.create).toHaveBeenCalled(); + }); + + it(`should use the ES create action if ID is not defined, overwrite=false and a doc has managed=false`, async () => { + await createSuccess(type, attributes, { managed: false }); + expect(mockPreflightCheckForCreate).not.toHaveBeenCalled(); + expect(client.create).toHaveBeenCalled(); + }); + it(`should use the ES index with version if ID and version are defined and overwrite=true`, async () => { await createSuccess(type, attributes, { id, overwrite: true, version: mockVersion }); expect(mockPreflightCheckForCreate).not.toHaveBeenCalled(); @@ -2885,17 +3045,20 @@ describe('SavedObjectsRepository', () => { it(`migrates a document and serializes the migrated doc`, async () => { const migrationVersion = mockMigrationVersion; const coreMigrationVersion = '8.0.0'; + const managed = false; await createSuccess(type, attributes, { id, references, migrationVersion, coreMigrationVersion, + managed, }); const doc = { type, id, attributes, references, + managed, migrationVersion, coreMigrationVersion, ...mockTimestampFieldsWithCreated, @@ -2906,6 +3069,60 @@ describe('SavedObjectsRepository', () => { expect(serializer.savedObjectToRaw).toHaveBeenLastCalledWith(migratedDoc); }); + it(`migrates a document, adds managed=false and serializes the migrated doc`, async () => { + const migrationVersion = mockMigrationVersion; + const coreMigrationVersion = '8.0.0'; + await createSuccess(type, attributes, { + id, + references, + migrationVersion, + coreMigrationVersion, + managed: undefined, + }); + const doc = { + type, + id, + attributes, + references, + managed: undefined, + migrationVersion, + coreMigrationVersion, + ...mockTimestampFieldsWithCreated, + }; + expectMigrationArgs({ ...doc, managed: false }); + + const migratedDoc = migrator.migrateDocument(doc); + expect(migratedDoc.managed).toBe(false); + expect(serializer.savedObjectToRaw).toHaveBeenLastCalledWith(migratedDoc); + }); + + it(`migrates a document, does not change managed=true to managed=false and serializes the migrated doc`, async () => { + const migrationVersion = mockMigrationVersion; + const coreMigrationVersion = '8.0.0'; + await createSuccess(type, attributes, { + id, + references, + migrationVersion, + coreMigrationVersion, + managed: true, + }); + const doc = { + type, + id, + attributes, + references, + managed: true, + migrationVersion, + coreMigrationVersion, + ...mockTimestampFieldsWithCreated, + }; + expectMigrationArgs(doc); + + const migratedDoc = migrator.migrateDocument(doc); + expect(migratedDoc.managed).toBe(true); + expect(serializer.savedObjectToRaw).toHaveBeenLastCalledWith(migratedDoc); + }); + it(`adds namespace to body when providing namespace for single-namespace type`, async () => { await createSuccess(type, attributes, { id, namespace }); expectMigrationArgs({ namespace }); @@ -2962,6 +3179,27 @@ describe('SavedObjectsRepository', () => { namespaces: [namespace ?? 'default'], coreMigrationVersion: expect.any(String), typeMigrationVersion: '1.1.1', + managed: false, + }); + }); + it(`allows setting 'managed' to true`, async () => { + const result = await createSuccess(MULTI_NAMESPACE_TYPE, attributes, { + id, + namespace, + references, + managed: true, + }); + expect(result).toEqual({ + type: MULTI_NAMESPACE_TYPE, + id, + ...mockTimestampFieldsWithCreated, + version: mockVersion, + attributes, + references, + namespaces: [namespace ?? 'default'], + coreMigrationVersion: expect.any(String), + typeMigrationVersion: '1.1.1', + managed: true, }); }); }); @@ -3549,6 +3787,7 @@ describe('SavedObjectsRepository', () => { 'migrationVersion', 'coreMigrationVersion', 'typeMigrationVersion', + 'managed', 'updated_at', 'created_at', 'originId', diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts index a84f6313d462cd..0bde80ea0f32f1 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts @@ -120,6 +120,7 @@ import { type Either, isLeft, isRight, + setManaged, } from './internal_utils'; import { collectMultiNamespaceReferences } from './collect_multi_namespace_references'; import { updateObjectsSpaces } from './update_objects_spaces'; @@ -309,6 +310,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { migrationVersion, coreMigrationVersion, typeMigrationVersion, + managed, overwrite = false, references = [], refresh = DEFAULT_REFRESH_SETTING, @@ -389,6 +391,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { migrationVersion, coreMigrationVersion, typeMigrationVersion, + managed: setManaged({ optionsManaged: managed }), created_at: time, updated_at: time, ...(Array.isArray(references) && { references }), @@ -442,6 +445,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { migrationVersionCompatibility, overwrite = false, refresh = DEFAULT_REFRESH_SETTING, + managed: optionsManaged, } = options; const time = getCurrentTime(); @@ -455,9 +459,10 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { } >; const expectedResults = objects.map((object) => { - const { type, id: requestId, initialNamespaces, version } = object; + const { type, id: requestId, initialNamespaces, version, managed } = object; let error: DecoratedError | undefined; let id: string = ''; // Assign to make TS happy, the ID will be validated (or randomly generated if needed) during getValidId below + const objectManaged = managed; if (!this._allowedTypes.includes(type)) { error = SavedObjectsErrorHelpers.createUnsupportedTypeError(type); } else { @@ -484,7 +489,11 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { tag: 'Right', value: { method, - object: { ...object, id }, + object: { + ...object, + id, + managed: setManaged({ optionsManaged, objectManaged }), + }, ...(requiresNamespacesCheck && { preflightCheckIndex: preflightCheckIndexCounter++ }), }, }; @@ -606,6 +615,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { typeMigrationVersion: object.typeMigrationVersion, ...(savedObjectNamespace && { namespace: savedObjectNamespace }), ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), + managed: setManaged({ optionsManaged, objectManaged: object.managed }), updated_at: time, created_at: time, references: object.references || [], @@ -686,7 +696,6 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { ); }), }; - return this.optionallyDecryptAndRedactBulkResult(result, authorizationResult?.typeMap, objects); } @@ -2338,6 +2347,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { refresh = DEFAULT_REFRESH_SETTING, initialize = false, upsertAttributes, + managed, } = options; if (!id) { @@ -2409,6 +2419,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { }, migrationVersion, typeMigrationVersion, + managed, updated_at: time, }); @@ -2462,6 +2473,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { references: body.get?._source.references ?? [], version: encodeHitVersion(body), attributes: body.get?._source[type], + ...(managed && { managed }), }; } diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/test_helpers/repository.test.common.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/test_helpers/repository.test.common.ts index 570a1704ab8546..43892f6719c197 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/test_helpers/repository.test.common.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/test_helpers/repository.test.common.ts @@ -583,13 +583,23 @@ export const expectBulkGetResult = ( export const getMockBulkCreateResponse = ( objects: SavedObjectsBulkCreateObject[], - namespace?: string + namespace?: string, + managed?: boolean ) => { return { errors: false, took: 1, items: objects.map( - ({ type, id, originId, attributes, references, migrationVersion, typeMigrationVersion }) => ({ + ({ + type, + id, + originId, + attributes, + references, + migrationVersion, + typeMigrationVersion, + managed: docManaged, + }) => ({ create: { // status: 1, // _index: '.kibana', @@ -602,6 +612,7 @@ export const getMockBulkCreateResponse = ( references, ...mockTimestampFieldsWithCreated, typeMigrationVersion: typeMigrationVersion || migrationVersion?.[type] || '1.1.1', + managed: managed ?? docManaged ?? false, }, ...mockVersionProps, }, @@ -616,7 +627,7 @@ export const bulkCreateSuccess = async ( objects: SavedObjectsBulkCreateObject[], options?: SavedObjectsCreateOptions ) => { - const mockResponse = getMockBulkCreateResponse(objects, options?.namespace); + const mockResponse = getMockBulkCreateResponse(objects, options?.namespace, options?.managed); client.bulk.mockResponse(mockResponse); const result = await repository.bulkCreate(objects, options); return result; @@ -626,10 +637,12 @@ export const expectCreateResult = (obj: { type: string; namespace?: string; namespaces?: string[]; + managed?: boolean; }) => ({ ...obj, coreMigrationVersion: expect.any(String), typeMigrationVersion: '1.1.1', + managed: obj.managed ?? false, version: mockVersion, namespaces: obj.namespaces ?? [obj.namespace ?? 'default'], ...mockTimestampFieldsWithCreated, diff --git a/packages/core/saved-objects/core-saved-objects-api-server/src/apis/bulk_create.ts b/packages/core/saved-objects/core-saved-objects-api-server/src/apis/bulk_create.ts index a933c1be438e9a..276682995a4f1f 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server/src/apis/bulk_create.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server/src/apis/bulk_create.ts @@ -55,4 +55,12 @@ export interface SavedObjectsBulkCreateObject { * * For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used. */ initialNamespaces?: string[]; + /** + * Flag indicating if a saved object is managed by Kibana (default=false) + * + * This can be leveraged by applications to e.g. prevent edits to a managed + * saved object. Instead, users can be guided to create a copy first and + * make their edits to the copy. + */ + managed?: boolean; } diff --git a/packages/core/saved-objects/core-saved-objects-api-server/src/apis/create.ts b/packages/core/saved-objects/core-saved-objects-api-server/src/apis/create.ts index 60b219056f935c..fe509b65252da5 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server/src/apis/create.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server/src/apis/create.ts @@ -61,6 +61,14 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions { * * For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used. */ initialNamespaces?: string[]; + /** + * Flag indicating if a saved object is managed by Kibana (default=false) + * + * This can be leveraged by applications to e.g. prevent edits to a managed + * saved object. Instead, users can be guided to create a copy first and + * make their edits to the copy. + */ + managed?: boolean; /** {@link SavedObjectsRawDocParseOptions.migrationVersionCompatibility} */ migrationVersionCompatibility?: 'compatible' | 'raw'; } diff --git a/packages/core/saved-objects/core-saved-objects-api-server/src/apis/increment_counter.ts b/packages/core/saved-objects/core-saved-objects-api-server/src/apis/increment_counter.ts index 3e186f38916de4..7edb0fc97ae132 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server/src/apis/increment_counter.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server/src/apis/increment_counter.ts @@ -39,6 +39,16 @@ export interface SavedObjectsIncrementCounterOptions * Attributes to use when upserting the document if it doesn't exist. */ upsertAttributes?: Attributes; + /** + * Flag indicating if a saved object is managed by Kibana (default=false). + * Only used when upserting a saved object. If the saved object already + * exist this option has no effect. + * + * This can be leveraged by applications to e.g. prevent edits to a managed + * saved object. Instead, users can be guided to create a copy first and + * make their edits to the copy. + */ + managed?: boolean; } /** diff --git a/packages/core/saved-objects/core-saved-objects-base-server-internal/src/serialization/serializer.test.ts b/packages/core/saved-objects/core-saved-objects-base-server-internal/src/serialization/serializer.test.ts index c3ac7222e20c8f..85035bfc38610d 100644 --- a/packages/core/saved-objects/core-saved-objects-base-server-internal/src/serialization/serializer.test.ts +++ b/packages/core/saved-objects/core-saved-objects-base-server-internal/src/serialization/serializer.test.ts @@ -218,6 +218,27 @@ describe('#rawToSavedObject', () => { expect(actual).not.toHaveProperty('coreMigrationVersion'); }); + test('if specified it copies the _source.managed property to managed', () => { + const actual = singleNamespaceSerializer.rawToSavedObject({ + _id: 'foo:bar', + _source: { + type: 'foo', + managed: false, + }, + }); + expect(actual).toHaveProperty('managed', false); + }); + + test(`if _source.managed is unspecified it doesn't set managed`, () => { + const actual = singleNamespaceSerializer.rawToSavedObject({ + _id: 'foo:bar', + _source: { + type: 'foo', + }, + }); + expect(actual).not.toHaveProperty('managed'); + }); + test(`if version is unspecified it doesn't set version`, () => { const actual = singleNamespaceSerializer.rawToSavedObject({ _id: 'foo:bar', @@ -756,6 +777,25 @@ describe('#savedObjectToRaw', () => { expect(actual._source).not.toHaveProperty('coreMigrationVersion'); }); + test('if specified, copies managed property to _source.managed', () => { + const actual = singleNamespaceSerializer.savedObjectToRaw({ + type: '', + attributes: {}, + managed: false, + } as any); + + expect(actual._source).toHaveProperty('managed', false); + }); + + test(`if unspecified it doesn't add managed property to _source`, () => { + const actual = singleNamespaceSerializer.savedObjectToRaw({ + type: '', + attributes: {}, + } as any); + + expect(actual._source.managed).toBe(undefined); + }); + test('it decodes the version property to _seq_no and _primary_term', () => { const actual = singleNamespaceSerializer.savedObjectToRaw({ type: '', diff --git a/packages/core/saved-objects/core-saved-objects-base-server-internal/src/serialization/serializer.ts b/packages/core/saved-objects/core-saved-objects-base-server-internal/src/serialization/serializer.ts index f18cc1e832ada7..96eded287975cd 100644 --- a/packages/core/saved-objects/core-saved-objects-base-server-internal/src/serialization/serializer.ts +++ b/packages/core/saved-objects/core-saved-objects-base-server-internal/src/serialization/serializer.ts @@ -94,6 +94,7 @@ export class SavedObjectsSerializer implements ISavedObjectsSerializer { references, coreMigrationVersion, typeMigrationVersion, + managed, migrationVersion = migrationVersionCompatibility === 'compatible' && typeMigrationVersion ? { [type]: typeMigrationVersion } : undefined, @@ -116,6 +117,7 @@ export class SavedObjectsSerializer implements ISavedObjectsSerializer { ...(originId && { originId }), attributes: _source[type], references: references || [], + ...(managed != null ? { managed } : {}), ...(migrationVersion && { migrationVersion }), ...(coreMigrationVersion && { coreMigrationVersion }), ...(typeMigrationVersion != null ? { typeMigrationVersion } : {}), @@ -146,11 +148,13 @@ export class SavedObjectsSerializer implements ISavedObjectsSerializer { references, coreMigrationVersion, typeMigrationVersion, + managed, } = savedObj; const source = { [type]: attributes, type, references, + ...(managed != null ? { managed } : {}), ...(namespace && this.registry.isSingleNamespace(type) && { namespace }), ...(namespaces && this.registry.isMultiNamespace(type) && { namespaces }), ...(originId && { originId }), @@ -160,7 +164,6 @@ export class SavedObjectsSerializer implements ISavedObjectsSerializer { ...(updated_at && { updated_at }), ...(createdAt && { created_at: createdAt }), }; - return { _id: this.generateRawId(namespace, type, id), _source: source, diff --git a/packages/core/saved-objects/core-saved-objects-base-server-internal/src/validation/schema.ts b/packages/core/saved-objects/core-saved-objects-base-server-internal/src/validation/schema.ts index 21c4c04d25afea..009bf2d89cf469 100644 --- a/packages/core/saved-objects/core-saved-objects-base-server-internal/src/validation/schema.ts +++ b/packages/core/saved-objects/core-saved-objects-base-server-internal/src/validation/schema.ts @@ -47,4 +47,5 @@ export const createSavedObjectSanitizedDocSchema = (attributesSchema: SavedObjec created_at: schema.maybe(schema.string()), version: schema.maybe(schema.string()), originId: schema.maybe(schema.string()), + managed: schema.maybe(schema.boolean()), }); diff --git a/packages/core/saved-objects/core-saved-objects-browser-internal/src/saved_objects_client.ts b/packages/core/saved-objects/core-saved-objects-browser-internal/src/saved_objects_client.ts index d500bbfcd7716a..0493652b1192f3 100644 --- a/packages/core/saved-objects/core-saved-objects-browser-internal/src/saved_objects_client.ts +++ b/packages/core/saved-objects/core-saved-objects-browser-internal/src/saved_objects_client.ts @@ -207,6 +207,7 @@ export class SavedObjectsClient implements SavedObjectsClientContract { attributes, migrationVersion: options.migrationVersion, typeMigrationVersion: options.typeMigrationVersion, + managed: options.managed, references: options.references, }), }); @@ -217,7 +218,7 @@ export class SavedObjectsClient implements SavedObjectsClientContract { /** * Creates multiple documents at once * - * @param {array} objects - [{ type, id, attributes, references, migrationVersion, typeMigrationVersion }] + * @param {array} objects - [{ type, id, attributes, references, migrationVersion, typeMigrationVersion, managed }] * @param {object} [options={}] * @property {boolean} [options.overwrite=false] * @returns The result of the create operation containing created saved objects. diff --git a/packages/core/saved-objects/core-saved-objects-browser-internal/src/simple_saved_object.ts b/packages/core/saved-objects/core-saved-objects-browser-internal/src/simple_saved_object.ts index 751d33f69e7e0e..0d57526c065e33 100644 --- a/packages/core/saved-objects/core-saved-objects-browser-internal/src/simple_saved_object.ts +++ b/packages/core/saved-objects/core-saved-objects-browser-internal/src/simple_saved_object.ts @@ -29,6 +29,7 @@ export class SimpleSavedObjectImpl implements SimpleSavedObject public migrationVersion: SavedObjectType['migrationVersion']; public coreMigrationVersion: SavedObjectType['coreMigrationVersion']; public typeMigrationVersion: SavedObjectType['typeMigrationVersion']; + public managed: SavedObjectType['managed']; public error: SavedObjectType['error']; public references: SavedObjectType['references']; public updatedAt: SavedObjectType['updated_at']; @@ -47,6 +48,7 @@ export class SimpleSavedObjectImpl implements SimpleSavedObject migrationVersion, coreMigrationVersion, typeMigrationVersion, + managed, namespaces, updated_at: updatedAt, created_at: createdAt, @@ -60,6 +62,7 @@ export class SimpleSavedObjectImpl implements SimpleSavedObject this.migrationVersion = migrationVersion; this.coreMigrationVersion = coreMigrationVersion; this.typeMigrationVersion = typeMigrationVersion; + this.managed = managed; this.namespaces = namespaces; this.updatedAt = updatedAt; this.createdAt = createdAt; @@ -96,6 +99,7 @@ export class SimpleSavedObjectImpl implements SimpleSavedObject migrationVersion: this.migrationVersion, coreMigrationVersion: this.coreMigrationVersion, typeMigrationVersion: this.typeMigrationVersion, + managed: this.managed, references: this.references, }) .then((sso) => { diff --git a/packages/core/saved-objects/core-saved-objects-browser-mocks/src/simple_saved_object.mock.ts b/packages/core/saved-objects/core-saved-objects-browser-mocks/src/simple_saved_object.mock.ts index 6268c796a5b825..396b7cfa3a9655 100644 --- a/packages/core/saved-objects/core-saved-objects-browser-mocks/src/simple_saved_object.mock.ts +++ b/packages/core/saved-objects/core-saved-objects-browser-mocks/src/simple_saved_object.mock.ts @@ -26,6 +26,7 @@ const simpleSavedObjectMockDefaults: Partial> = { updatedAt: '', createdAt: '', namespaces: undefined, + managed: false, }; const createSimpleSavedObjectMock = ( @@ -40,6 +41,7 @@ const createSimpleSavedObjectMock = ( migrationVersion: savedObject.migrationVersion, coreMigrationVersion: savedObject.coreMigrationVersion, typeMigrationVersion: savedObject.typeMigrationVersion, + managed: savedObject.managed, error: savedObject.error, references: savedObject.references, updatedAt: savedObject.updated_at, diff --git a/packages/core/saved-objects/core-saved-objects-common/src/server_types.ts b/packages/core/saved-objects/core-saved-objects-common/src/server_types.ts index fbdacb73309faa..9ea8f1c9f06688 100644 --- a/packages/core/saved-objects/core-saved-objects-common/src/server_types.ts +++ b/packages/core/saved-objects/core-saved-objects-common/src/server_types.ts @@ -101,4 +101,12 @@ export interface SavedObject { * space. */ originId?: string; + /** + * Flag indicating if a saved object is managed by Kibana (default=false) + * + * This can be leveraged by applications to e.g. prevent edits to a managed + * saved object. Instead, users can be guided to create a copy first and + * make their edits to the copy. + */ + managed?: boolean; } diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/__snapshots__/kibana_migrator.test.ts.snap b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/__snapshots__/kibana_migrator.test.ts.snap index efbdf0da12f261..b035c64617ca2e 100644 --- a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/__snapshots__/kibana_migrator.test.ts.snap +++ b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/__snapshots__/kibana_migrator.test.ts.snap @@ -8,6 +8,7 @@ Object { "bmap": "510f1f0adb69830cf8a1c5ce2923ed82", "coreMigrationVersion": "2f4316de49999235636386fe51dc06c1", "created_at": "00da57df13e94e9d98437d13ace4bfe0", + "managed": "88cf246b441a6362458cb6a56ca3f7d7", "namespace": "2f4316de49999235636386fe51dc06c1", "namespaces": "2f4316de49999235636386fe51dc06c1", "originId": "2f4316de49999235636386fe51dc06c1", @@ -39,6 +40,9 @@ Object { "created_at": Object { "type": "date", }, + "managed": Object { + "type": "boolean", + }, "namespace": Object { "type": "keyword", }, diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/core/__snapshots__/build_active_mappings.test.ts.snap b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/core/__snapshots__/build_active_mappings.test.ts.snap index 42aaff1b7f0dfe..9941f2a70e696c 100644 --- a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/core/__snapshots__/build_active_mappings.test.ts.snap +++ b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/core/__snapshots__/build_active_mappings.test.ts.snap @@ -8,6 +8,7 @@ Object { "bbb": "18c78c995965207ed3f6e7fc5c6e55fe", "coreMigrationVersion": "2f4316de49999235636386fe51dc06c1", "created_at": "00da57df13e94e9d98437d13ace4bfe0", + "managed": "88cf246b441a6362458cb6a56ca3f7d7", "namespace": "2f4316de49999235636386fe51dc06c1", "namespaces": "2f4316de49999235636386fe51dc06c1", "originId": "2f4316de49999235636386fe51dc06c1", @@ -31,6 +32,9 @@ Object { "created_at": Object { "type": "date", }, + "managed": Object { + "type": "boolean", + }, "namespace": Object { "type": "keyword", }, @@ -74,6 +78,7 @@ Object { "coreMigrationVersion": "2f4316de49999235636386fe51dc06c1", "created_at": "00da57df13e94e9d98437d13ace4bfe0", "firstType": "635418ab953d81d93f1190b70a8d3f57", + "managed": "88cf246b441a6362458cb6a56ca3f7d7", "namespace": "2f4316de49999235636386fe51dc06c1", "namespaces": "2f4316de49999235636386fe51dc06c1", "originId": "2f4316de49999235636386fe51dc06c1", @@ -101,6 +106,9 @@ Object { }, }, }, + "managed": Object { + "type": "boolean", + }, "namespace": Object { "type": "keyword", }, diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/core/build_active_mappings.ts b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/core/build_active_mappings.ts index 12ed0931ce0c36..7dd13acbe8c7fc 100644 --- a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/core/build_active_mappings.ts +++ b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/core/build_active_mappings.ts @@ -156,6 +156,9 @@ export function getBaseMappings(): IndexMapping { typeMigrationVersion: { type: 'version', }, + managed: { + type: 'boolean', + }, }, }; } diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/document_migrator/document_migrator.test.ts b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/document_migrator/document_migrator.test.ts index de44f163ec94e7..df710068da3249 100644 --- a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/document_migrator/document_migrator.test.ts +++ b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/document_migrator/document_migrator.test.ts @@ -817,6 +817,7 @@ describe('DocumentMigrator', () => { references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }], coreMigrationVersion: '8.8.0', typeMigrationVersion: '1.0.0', + managed: false, }, ]); }); @@ -832,6 +833,7 @@ describe('DocumentMigrator', () => { references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }], coreMigrationVersion: '8.8.0', typeMigrationVersion: '1.0.0', + managed: false, namespace: 'foo-namespace', }, ]); @@ -865,6 +867,7 @@ describe('DocumentMigrator', () => { attributes: { name: 'Sweet Peach' }, references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }], // no change coreMigrationVersion: '8.8.0', + managed: false, }, ]); }); @@ -881,6 +884,7 @@ describe('DocumentMigrator', () => { references: [{ id: 'uuidv5', type: 'toy', name: 'BALL!' }], // changed coreMigrationVersion: '8.8.0', namespace: 'foo-namespace', + managed: false, }, ]); }); @@ -978,6 +982,7 @@ describe('DocumentMigrator', () => { references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }], // no change coreMigrationVersion: '8.8.0', typeMigrationVersion: '1.0.0', + managed: false, namespaces: ['default'], }, ]); @@ -1008,6 +1013,7 @@ describe('DocumentMigrator', () => { typeMigrationVersion: '1.0.0', namespaces: ['foo-namespace'], originId: 'cute', + managed: false, }, { id: 'foo-namespace:dog:cute', @@ -1063,6 +1069,7 @@ describe('DocumentMigrator', () => { references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }], // no change coreMigrationVersion: '8.8.0', typeMigrationVersion: '2.0.0', + managed: false, }, ]); }); @@ -1080,6 +1087,7 @@ describe('DocumentMigrator', () => { coreMigrationVersion: '8.8.0', typeMigrationVersion: '2.0.0', namespace: 'foo-namespace', + managed: false, }, ]); }); @@ -1190,6 +1198,7 @@ describe('DocumentMigrator', () => { coreMigrationVersion: '8.8.0', typeMigrationVersion: '2.0.0', namespaces: ['default'], + managed: false, }, ]); }); @@ -1219,6 +1228,7 @@ describe('DocumentMigrator', () => { typeMigrationVersion: '2.0.0', namespaces: ['foo-namespace'], originId: 'pretty', + managed: false, }, { id: 'foo-namespace:dog:pretty', diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/document_migrator/migrations/index.ts b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/document_migrator/migrations/index.ts index f5a8b313a96e7e..04b33a5cc2ac2b 100644 --- a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/document_migrator/migrations/index.ts +++ b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/document_migrator/migrations/index.ts @@ -6,9 +6,17 @@ * Side Public License, v 1. */ +import { flow } from 'lodash'; +import get from 'lodash/fp/get'; import { TransformFn } from '../types'; import { transformMigrationVersion } from './transform_migration_version'; +import { transformSetManagedDefault } from './transform_set_managed_default'; export const migrations = { - '8.8.0': transformMigrationVersion, + '8.8.0': flow( + transformMigrationVersion, + // extract transformedDoc from TransformResult as input to next transform + get('transformedDoc'), + transformSetManagedDefault + ), } as Record; diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/document_migrator/migrations/transform_set_managed_default.test.ts b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/document_migrator/migrations/transform_set_managed_default.test.ts new file mode 100644 index 00000000000000..1b355a176ab6f4 --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/document_migrator/migrations/transform_set_managed_default.test.ts @@ -0,0 +1,40 @@ +/* + * 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 { transformSetManagedDefault } from './transform_set_managed_default'; + +describe('transformAddManaged', () => { + it('should add managed if not defined', () => { + expect( + transformSetManagedDefault({ + id: 'a', + attributes: {}, + type: 'something', + }) + ).toHaveProperty('transformedDoc.managed'); + }); + it('should not change managed if already defined', () => { + const docWithManagedFalse = transformSetManagedDefault({ + id: 'a', + attributes: {}, + type: 'something', + managed: false, + }); + const docWithManagedTrue = transformSetManagedDefault({ + id: 'a', + attributes: {}, + type: 'something', + managed: true, + }); + [docWithManagedFalse, docWithManagedTrue].forEach((doc) => { + expect(doc.transformedDoc.managed).toBeDefined(); + }); + expect(docWithManagedFalse.transformedDoc.managed).not.toBeTruthy(); + expect(docWithManagedTrue.transformedDoc.managed).toBeTruthy(); + }); +}); diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/document_migrator/migrations/transform_set_managed_default.ts b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/document_migrator/migrations/transform_set_managed_default.ts new file mode 100644 index 00000000000000..8e3e1b82bc8cd7 --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/document_migrator/migrations/transform_set_managed_default.ts @@ -0,0 +1,14 @@ +/* + * 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 { SavedObjectUnsanitizedDoc } from '@kbn/core-saved-objects-server'; + +export const transformSetManagedDefault = (doc: SavedObjectUnsanitizedDoc) => ({ + transformedDoc: { ...doc, managed: doc.managed ?? false }, + additionalDocs: [], +}); diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_create.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_create.ts index 68d1a52e63edae..c9341a0af49a41 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_create.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_create.ts @@ -76,6 +76,7 @@ export const registerBulkCreateRoute = ( if (!allowHttpApiAccess) { throwIfAnyTypeNotVisibleByAPI(typesToCheck, savedObjects.typeRegistry); } + const result = await savedObjects.client.bulkCreate(req.body, { overwrite, migrationVersionCompatibility: 'compatible', diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/lib/import_dashboards.test.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/lib/import_dashboards.test.ts index 0496db94fa1d69..d6382efd673fa7 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/lib/import_dashboards.test.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/lib/import_dashboards.test.ts @@ -26,7 +26,12 @@ describe('importDashboards(req)', () => { references: [], version: 'foo', }, - { id: 'panel-01', type: 'visualization', attributes: { visState: '{}' }, references: [] }, + { + id: 'panel-01', + type: 'visualization', + attributes: { visState: '{}' }, + references: [], + }, ]; }); diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/lib/import_dashboards.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/lib/import_dashboards.ts index 0c32f158abd67b..808ed9aa9d592c 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/lib/import_dashboards.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/lib/import_dashboards.ts @@ -21,8 +21,8 @@ export async function importDashboards( // docs are not seen as automatically up-to-date. const docs = objects .filter((item) => !exclude.includes(item.type)) - // filter out any document version, if present - .map(({ version, ...doc }) => ({ + // filter out any document version and managed, if present + .map(({ version, managed, ...doc }) => ({ ...doc, ...(!doc.migrationVersion && !doc.typeMigrationVersion ? { typeMigrationVersion: '' } : {}), })); diff --git a/packages/core/saved-objects/core-saved-objects-server/src/serialization.ts b/packages/core/saved-objects/core-saved-objects-server/src/serialization.ts index 873dff4ac1b33c..8b3b6b4924d790 100644 --- a/packages/core/saved-objects/core-saved-objects-server/src/serialization.ts +++ b/packages/core/saved-objects/core-saved-objects-server/src/serialization.ts @@ -84,6 +84,7 @@ export interface SavedObjectsRawDocSource { created_at?: string; references?: SavedObjectReference[]; originId?: string; + managed?: boolean; [typeMapping: string]: any; } @@ -106,6 +107,7 @@ interface SavedObjectDoc { updated_at?: string; created_at?: string; originId?: string; + managed?: boolean; } /** diff --git a/src/core/server/integration_tests/saved_objects/migrations/group2/batch_size_bytes.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group2/batch_size_bytes.test.ts index cba827a6e4d5aa..7f468e11bae4aa 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group2/batch_size_bytes.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group2/batch_size_bytes.test.ts @@ -118,7 +118,7 @@ describe('migration v2', () => { await root.preboot(); await root.setup(); await expect(root.start()).rejects.toMatchInlineSnapshot( - `[Error: Unable to complete saved object migrations for the [.kibana] index: The document with _id "canvas-workpad-template:workpad-template-061d7868-2b4e-4dc8-8bf7-3772b52926e5" is 1715248 bytes which exceeds the configured maximum batch size of 1015275 bytes. To proceed, please increase the 'migrations.maxBatchSizeBytes' Kibana configuration option and ensure that the Elasticsearch 'http.max_content_length' configuration option is set to an equal or larger value.]` + `[Error: Unable to complete saved object migrations for the [.kibana] index: The document with _id "canvas-workpad-template:workpad-template-061d7868-2b4e-4dc8-8bf7-3772b52926e5" is 1715264 bytes which exceeds the configured maximum batch size of 1015275 bytes. To proceed, please increase the 'migrations.maxBatchSizeBytes' Kibana configuration option and ensure that the Elasticsearch 'http.max_content_length' configuration option is set to an equal or larger value.]` ); await retryAsync( @@ -131,7 +131,7 @@ describe('migration v2', () => { expect( records.find((rec) => rec.message.startsWith( - `Unable to complete saved object migrations for the [.kibana] index: The document with _id "canvas-workpad-template:workpad-template-061d7868-2b4e-4dc8-8bf7-3772b52926e5" is 1715248 bytes which exceeds the configured maximum batch size of 1015275 bytes. To proceed, please increase the 'migrations.maxBatchSizeBytes' Kibana configuration option and ensure that the Elasticsearch 'http.max_content_length' configuration option is set to an equal or larger value.` + `Unable to complete saved object migrations for the [.kibana] index: The document with _id "canvas-workpad-template:workpad-template-061d7868-2b4e-4dc8-8bf7-3772b52926e5" is 1715264 bytes which exceeds the configured maximum batch size of 1015275 bytes. To proceed, please increase the 'migrations.maxBatchSizeBytes' Kibana configuration option and ensure that the Elasticsearch 'http.max_content_length' configuration option is set to an equal or larger value.` ) ) ).toBeDefined(); diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/rewriting_id.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/rewriting_id.test.ts index b5cad9ec534499..6079469cf49822 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/rewriting_id.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/rewriting_id.test.ts @@ -189,6 +189,7 @@ describe('migration v2', () => { namespaces: ['default'], coreMigrationVersion: expect.any(String), typeMigrationVersion: '8.0.0', + managed: false, }, { id: `foo:${newFooId}`, @@ -199,6 +200,7 @@ describe('migration v2', () => { originId: '1', coreMigrationVersion: expect.any(String), typeMigrationVersion: '8.0.0', + managed: false, }, { // new object for spacex:foo:1 @@ -223,6 +225,7 @@ describe('migration v2', () => { namespaces: ['default'], coreMigrationVersion: expect.any(String), typeMigrationVersion: '8.0.0', + managed: false, }, { id: `bar:${newBarId}`, @@ -233,6 +236,7 @@ describe('migration v2', () => { originId: '1', coreMigrationVersion: expect.any(String), typeMigrationVersion: '8.0.0', + managed: false, }, { // new object for spacex:bar:1 diff --git a/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts index ba8f3c31fc7426..687372ed0a559a 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts @@ -28,6 +28,7 @@ export const getSavedObjects = (): SavedObject[] => [ id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', coreMigrationVersion: '8.8.0', typeMigrationVersion: '7.11.0', + managed: false, references: [], type: 'index-pattern', updated_at: '2021-08-05T12:23:57.577Z', @@ -50,6 +51,7 @@ export const getSavedObjects = (): SavedObject[] => [ id: 'b80e6540-b891-11e8-a6d9-e546fe2bba5f', coreMigrationVersion: '8.8.0', typeMigrationVersion: '7.14.0', + managed: false, references: [ { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', diff --git a/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts index afc9033efa4937..7aa32a5c5584cd 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts @@ -19,6 +19,7 @@ export const getSavedObjects = (): SavedObject[] => [ version: '1', coreMigrationVersion: '8.8.0', typeMigrationVersion: '7.9.3', + managed: false, attributes: { title: i18n.translate('home.sampleData.flightsSpec.flightLogTitle', { defaultMessage: '[Flights] Flight Log', @@ -78,6 +79,7 @@ export const getSavedObjects = (): SavedObject[] => [ version: '1', coreMigrationVersion: '8.8.0', typeMigrationVersion: '7.14.0', + managed: false, attributes: { title: i18n.translate('home.sampleData.flightsSpec.departuresCountMapTitle', { defaultMessage: '[Flights] Departures Count Map', @@ -290,5 +292,6 @@ export const getSavedObjects = (): SavedObject[] => [ ], coreMigrationVersion: '8.8.0', typeMigrationVersion: '8.7.0', + managed: false, }, ]; diff --git a/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts index 6d8a2b46f4e723..c0140bd89283de 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts @@ -18,6 +18,7 @@ export const getSavedObjects = (): SavedObject[] => [ version: '1', coreMigrationVersion: '8.8.0', typeMigrationVersion: '8.0.0', + managed: false, attributes: { title: i18n.translate('home.sampleData.logsSpec.visitorsMapTitle', { defaultMessage: '[Logs] Visitors Map', @@ -47,6 +48,7 @@ export const getSavedObjects = (): SavedObject[] => [ version: '1', coreMigrationVersion: '8.8.0', typeMigrationVersion: '7.14.0', + managed: false, attributes: { title: i18n.translate('home.sampleData.logsSpec.heatmapTitle', { defaultMessage: '[Logs] Unique Destination Heatmap', @@ -89,6 +91,7 @@ export const getSavedObjects = (): SavedObject[] => [ version: '1', coreMigrationVersion: '8.8.0', typeMigrationVersion: '7.14.0', + managed: false, attributes: { title: i18n.translate('home.sampleData.logsSpec.bytesDistributionTitle', { defaultMessage: '[Logs] Bytes distribution', @@ -399,6 +402,7 @@ export const getSavedObjects = (): SavedObject[] => [ ], coreMigrationVersion: '8.8.0', typeMigrationVersion: '8.7.0', + managed: false, }, { id: '2f360f30-ea74-11eb-b4c6-3d2afc1cb389', @@ -407,6 +411,7 @@ export const getSavedObjects = (): SavedObject[] => [ version: '1', coreMigrationVersion: '8.8.0', typeMigrationVersion: '7.9.3', + managed: false, attributes: { title: i18n.translate('home.sampleData.logsSpec.discoverTitle', { defaultMessage: '[Logs] Visits', diff --git a/test/api_integration/apis/saved_objects/bulk_create.ts b/test/api_integration/apis/saved_objects/bulk_create.ts index 975eb73a49532e..b365df10f7d741 100644 --- a/test/api_integration/apis/saved_objects/bulk_create.ts +++ b/test/api_integration/apis/saved_objects/bulk_create.ts @@ -75,6 +75,7 @@ export default function ({ getService }: FtrProviderContext) { }, coreMigrationVersion: '8.8.0', typeMigrationVersion: resp.body.saved_objects[1].typeMigrationVersion, + managed: resp.body.saved_objects[1].managed, references: [], namespaces: [SPACE_ID], }, diff --git a/test/api_integration/apis/saved_objects/bulk_get.ts b/test/api_integration/apis/saved_objects/bulk_get.ts index d1873b22b0d627..6c97efd94d70a0 100644 --- a/test/api_integration/apis/saved_objects/bulk_get.ts +++ b/test/api_integration/apis/saved_objects/bulk_get.ts @@ -74,6 +74,7 @@ export default function ({ getService }: FtrProviderContext) { migrationVersion: resp.body.saved_objects[0].migrationVersion, coreMigrationVersion: '8.8.0', typeMigrationVersion: resp.body.saved_objects[0].typeMigrationVersion, + managed: resp.body.saved_objects[0].managed, namespaces: ['default'], references: [ { @@ -106,6 +107,7 @@ export default function ({ getService }: FtrProviderContext) { migrationVersion: resp.body.saved_objects[2].migrationVersion, coreMigrationVersion: '8.8.0', typeMigrationVersion: resp.body.saved_objects[2].typeMigrationVersion, + managed: resp.body.saved_objects[2].managed, references: [], }, ], diff --git a/test/api_integration/apis/saved_objects/create.ts b/test/api_integration/apis/saved_objects/create.ts index c1977589255a73..865eb44596ec15 100644 --- a/test/api_integration/apis/saved_objects/create.ts +++ b/test/api_integration/apis/saved_objects/create.ts @@ -52,6 +52,7 @@ export default function ({ getService }: FtrProviderContext) { migrationVersion: resp.body.migrationVersion, coreMigrationVersion: '8.8.0', typeMigrationVersion: resp.body.typeMigrationVersion, + managed: resp.body.managed, updated_at: resp.body.updated_at, created_at: resp.body.created_at, version: resp.body.version, @@ -63,6 +64,7 @@ export default function ({ getService }: FtrProviderContext) { }); expect(resp.body.migrationVersion).to.be.ok(); expect(resp.body.typeMigrationVersion).to.be.ok(); + expect(resp.body.managed).to.not.be.ok(); }); }); diff --git a/test/api_integration/apis/saved_objects/export.ts b/test/api_integration/apis/saved_objects/export.ts index bec4305ab7d4c2..ec7b82453ffcec 100644 --- a/test/api_integration/apis/saved_objects/export.ts +++ b/test/api_integration/apis/saved_objects/export.ts @@ -350,6 +350,7 @@ export default function ({ getService }: FtrProviderContext) { id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', coreMigrationVersion: '8.8.0', typeMigrationVersion: objects[0].typeMigrationVersion, + managed: objects[0].managed, references: [ { id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', @@ -363,6 +364,7 @@ export default function ({ getService }: FtrProviderContext) { version: objects[0].version, }); expect(objects[0].typeMigrationVersion).to.be.ok(); + expect(objects[0].managed).to.not.be.ok(); expect(() => JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON) ).not.to.throwError(); @@ -411,6 +413,7 @@ export default function ({ getService }: FtrProviderContext) { id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', coreMigrationVersion: '8.8.0', typeMigrationVersion: objects[0].typeMigrationVersion, + managed: objects[0].managed, references: [ { id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', @@ -424,6 +427,7 @@ export default function ({ getService }: FtrProviderContext) { version: objects[0].version, }); expect(objects[0].typeMigrationVersion).to.be.ok(); + expect(objects[0].managed).to.not.be.ok(); expect(() => JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON) ).not.to.throwError(); @@ -477,6 +481,7 @@ export default function ({ getService }: FtrProviderContext) { id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', coreMigrationVersion: '8.8.0', typeMigrationVersion: objects[0].typeMigrationVersion, + managed: objects[0].managed, references: [ { id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', @@ -490,6 +495,7 @@ export default function ({ getService }: FtrProviderContext) { version: objects[0].version, }); expect(objects[0].typeMigrationVersion).to.be.ok(); + expect(objects[0].managed).to.not.be.ok(); expect(() => JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON) ).not.to.throwError(); diff --git a/test/api_integration/apis/saved_objects/get.ts b/test/api_integration/apis/saved_objects/get.ts index db127924b5ec65..a7b1f58c531f4f 100644 --- a/test/api_integration/apis/saved_objects/get.ts +++ b/test/api_integration/apis/saved_objects/get.ts @@ -39,6 +39,7 @@ export default function ({ getService }: FtrProviderContext) { migrationVersion: resp.body.migrationVersion, coreMigrationVersion: '8.8.0', typeMigrationVersion: resp.body.typeMigrationVersion, + managed: resp.body.managed, attributes: { title: 'Count of requests', description: '', @@ -59,6 +60,7 @@ export default function ({ getService }: FtrProviderContext) { }); expect(resp.body.migrationVersion).to.be.ok(); expect(resp.body.typeMigrationVersion).to.be.ok(); + expect(resp.body.managed).to.not.be.ok(); })); describe('doc does not exist', () => { diff --git a/test/api_integration/apis/saved_objects/resolve.ts b/test/api_integration/apis/saved_objects/resolve.ts index eaae0cb1be4c20..73f7ad03582cf3 100644 --- a/test/api_integration/apis/saved_objects/resolve.ts +++ b/test/api_integration/apis/saved_objects/resolve.ts @@ -44,6 +44,7 @@ export default function ({ getService }: FtrProviderContext) { migrationVersion: resp.body.saved_object.migrationVersion, coreMigrationVersion: '8.8.0', typeMigrationVersion: resp.body.saved_object.typeMigrationVersion, + managed: resp.body.saved_object.managed, attributes: { title: 'Count of requests', description: '', @@ -66,6 +67,7 @@ export default function ({ getService }: FtrProviderContext) { }); expect(resp.body.saved_object.migrationVersion).to.be.ok(); expect(resp.body.saved_object.typeMigrationVersion).to.be.ok(); + expect(resp.body.saved_object.managed).to.not.be.ok(); })); describe('doc does not exist', () => {