From 2e9d0c0ee7fca526cea3a5ee98b22fecc1e9db75 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Wed, 3 Nov 2021 22:39:08 -0400 Subject: [PATCH] Delete legacy URL aliases when objects are deleted or unshared (#117056) --- .../object_types/registration.ts | 3 +- ...ct_multi_namespace_references.test.mock.ts | 4 +- .../lib/collect_multi_namespace_references.ts | 2 +- .../delete_legacy_url_aliases.test.mock.ts | 23 +++ .../delete_legacy_url_aliases.test.ts | 152 ++++++++++++++++++ .../delete_legacy_url_aliases.ts | 113 +++++++++++++ .../find_legacy_url_aliases.test.ts | 10 +- .../find_legacy_url_aliases.ts | 7 +- .../service/lib/legacy_url_aliases/index.ts | 12 ++ .../preflight_check_for_create.test.mock.ts | 4 +- .../service/lib/preflight_check_for_create.ts | 2 +- .../service/lib/repository.test.js | 70 +++++++- .../service/lib/repository.test.mock.ts | 8 + .../saved_objects/service/lib/repository.ts | 22 +++ .../lib/update_objects_spaces.test.mock.ts | 8 + .../service/lib/update_objects_spaces.test.ts | 150 +++++++++++++++++ .../service/lib/update_objects_spaces.ts | 77 +++++++-- .../saved_objects/spaces/data.json | 37 +++-- .../common/suites/delete.ts | 27 +++- .../security_and_spaces/apis/delete.ts | 5 +- .../spaces_only/apis/delete.ts | 5 +- .../saved_objects/spaces/data.json | 88 ++++++++++ .../common/lib/saved_object_test_cases.ts | 8 + .../common/suites/delete.ts | 16 +- .../common/suites/update_objects_spaces.ts | 36 ++++- .../apis/update_objects_spaces.ts | 4 + .../spaces_only/apis/update_objects_spaces.ts | 57 ++++++- 27 files changed, 895 insertions(+), 55 deletions(-) create mode 100644 src/core/server/saved_objects/service/lib/legacy_url_aliases/delete_legacy_url_aliases.test.mock.ts create mode 100644 src/core/server/saved_objects/service/lib/legacy_url_aliases/delete_legacy_url_aliases.test.ts create mode 100644 src/core/server/saved_objects/service/lib/legacy_url_aliases/delete_legacy_url_aliases.ts rename src/core/server/saved_objects/service/lib/{ => legacy_url_aliases}/find_legacy_url_aliases.test.ts (94%) rename src/core/server/saved_objects/service/lib/{ => legacy_url_aliases}/find_legacy_url_aliases.ts (87%) create mode 100644 src/core/server/saved_objects/service/lib/legacy_url_aliases/index.ts diff --git a/src/core/server/saved_objects/object_types/registration.ts b/src/core/server/saved_objects/object_types/registration.ts index ce108967471783..a199ce947f96b2 100644 --- a/src/core/server/saved_objects/object_types/registration.ts +++ b/src/core/server/saved_objects/object_types/registration.ts @@ -16,8 +16,9 @@ const legacyUrlAliasType: SavedObjectsType = { dynamic: false, properties: { sourceId: { type: 'keyword' }, - targetType: { type: 'keyword' }, targetNamespace: { type: 'keyword' }, + targetType: { type: 'keyword' }, + targetId: { type: 'keyword' }, resolveCounter: { type: 'long' }, disabled: { type: 'boolean' }, // other properties exist, but we aren't querying or aggregating on those, so we don't need to specify them (because we use `dynamic: false` above) diff --git a/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.test.mock.ts b/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.test.mock.ts index 382212cfbbd11f..728f3b847b6317 100644 --- a/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.test.mock.ts +++ b/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.test.mock.ts @@ -6,14 +6,14 @@ * Side Public License, v 1. */ -import type { findLegacyUrlAliases } from './find_legacy_url_aliases'; +import type { findLegacyUrlAliases } from './legacy_url_aliases'; import type * as InternalUtils from './internal_utils'; export const mockFindLegacyUrlAliases = jest.fn() as jest.MockedFunction< typeof findLegacyUrlAliases >; -jest.mock('./find_legacy_url_aliases', () => { +jest.mock('./legacy_url_aliases', () => { return { findLegacyUrlAliases: mockFindLegacyUrlAliases }; }); diff --git a/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.ts b/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.ts index 87bb5017aab956..fd2afea999a079 100644 --- a/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.ts +++ b/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.ts @@ -9,7 +9,7 @@ import type { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import type { SavedObjectsSerializer } from '../../serialization'; import type { SavedObject, SavedObjectsBaseOptions } from '../../types'; -import { findLegacyUrlAliases } from './find_legacy_url_aliases'; +import { findLegacyUrlAliases } from './legacy_url_aliases'; import { getRootFields } from './included_fields'; import { getObjectKey, diff --git a/src/core/server/saved_objects/service/lib/legacy_url_aliases/delete_legacy_url_aliases.test.mock.ts b/src/core/server/saved_objects/service/lib/legacy_url_aliases/delete_legacy_url_aliases.test.mock.ts new file mode 100644 index 00000000000000..9585c40e6a161a --- /dev/null +++ b/src/core/server/saved_objects/service/lib/legacy_url_aliases/delete_legacy_url_aliases.test.mock.ts @@ -0,0 +1,23 @@ +/* + * 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 type { getErrorMessage } from '../../../../elasticsearch'; + +export const mockGetEsErrorMessage = jest.fn() as jest.MockedFunction; + +jest.mock('../../../../elasticsearch', () => { + return { getErrorMessage: mockGetEsErrorMessage }; +}); + +// Mock these functions to return empty results, as this simplifies test cases and we don't need to exercise alternate code paths for these +jest.mock('@kbn/es-query', () => { + return { nodeTypes: { function: { buildNode: jest.fn() } } }; +}); +jest.mock('../search_dsl', () => { + return { getSearchDsl: jest.fn() }; +}); diff --git a/src/core/server/saved_objects/service/lib/legacy_url_aliases/delete_legacy_url_aliases.test.ts b/src/core/server/saved_objects/service/lib/legacy_url_aliases/delete_legacy_url_aliases.test.ts new file mode 100644 index 00000000000000..22c57fe3f280f4 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/legacy_url_aliases/delete_legacy_url_aliases.test.ts @@ -0,0 +1,152 @@ +/* + * 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 { mockGetEsErrorMessage } from './delete_legacy_url_aliases.test.mock'; // Note: importing this file applies default mocks for other functions too + +import { errors as EsErrors } from '@elastic/elasticsearch'; + +import { elasticsearchClientMock } from '../../../../elasticsearch/client/mocks'; +import { typeRegistryMock } from '../../../saved_objects_type_registry.mock'; +import { ALL_NAMESPACES_STRING } from '../utils'; +import { deleteLegacyUrlAliases } from './delete_legacy_url_aliases'; +import type { DeleteLegacyUrlAliasesParams } from './delete_legacy_url_aliases'; + +type SetupParams = Pick< + DeleteLegacyUrlAliasesParams, + 'type' | 'id' | 'namespaces' | 'deleteBehavior' +>; + +describe('deleteLegacyUrlAliases', () => { + function setup(setupParams: SetupParams) { + return { + mappings: { properties: {} }, // doesn't matter, only used as an argument to getSearchDsl which is mocked + registry: typeRegistryMock.create(), // doesn't matter, only used as an argument to getSearchDsl which is mocked + client: elasticsearchClientMock.createElasticsearchClient(), + getIndexForType: jest.fn(), // doesn't matter + ...setupParams, + }; + } + + const type = 'obj-type'; + const id = 'obj-id'; + + it('throws an error if namespaces includes the "all namespaces" string', async () => { + const namespaces = [ALL_NAMESPACES_STRING]; + const params = setup({ type, id, namespaces, deleteBehavior: 'inclusive' }); + + await expect(() => deleteLegacyUrlAliases(params)).rejects.toThrowError( + `Failed to delete legacy URL aliases for ${type}/${id}: "namespaces" cannot include the * string` + ); + expect(params.client.updateByQuery).not.toHaveBeenCalled(); + }); + + it('throws an error if updateByQuery fails', async () => { + const namespaces = ['space-a', 'space-b']; + const params = setup({ type, id, namespaces, deleteBehavior: 'inclusive' }); + const esError = new EsErrors.ResponseError( + elasticsearchClientMock.createApiResponse({ + statusCode: 500, + body: { error: { type: 'es_type', reason: 'es_reason' } }, + }) + ); + params.client.updateByQuery.mockResolvedValueOnce( + elasticsearchClientMock.createErrorTransportRequestPromise(esError) + ); + mockGetEsErrorMessage.mockClear(); + mockGetEsErrorMessage.mockReturnValue('Oh no!'); + + await expect(() => deleteLegacyUrlAliases(params)).rejects.toThrowError( + `Failed to delete legacy URL aliases for ${type}/${id}: Oh no!` + ); + expect(params.client.updateByQuery).toHaveBeenCalledTimes(1); + expect(mockGetEsErrorMessage).toHaveBeenCalledTimes(1); + expect(mockGetEsErrorMessage).toHaveBeenCalledWith(esError); + }); + + describe('deleteBehavior "inclusive"', () => { + const deleteBehavior = 'inclusive' as const; + + it('when filtered namespaces is not empty, returns early', async () => { + const namespaces = ['default']; + const params = setup({ type, id, namespaces, deleteBehavior }); + + await deleteLegacyUrlAliases(params); + expect(params.client.updateByQuery).not.toHaveBeenCalled(); + }); + + it('when filtered namespaces is not empty, calls updateByQuery with expected script params', async () => { + const namespaces = ['space-a', 'default', 'space-b']; + const params = setup({ type, id, namespaces, deleteBehavior }); + + await deleteLegacyUrlAliases(params); + expect(params.client.updateByQuery).toHaveBeenCalledTimes(1); + expect(params.client.updateByQuery).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ + script: expect.objectContaining({ + params: { + namespaces: ['space-a', 'space-b'], // 'default' is filtered out + matchTargetNamespaceOp: 'delete', + notMatchTargetNamespaceOp: 'noop', + }, + }), + }), + }), + expect.anything() + ); + }); + }); + + describe('deleteBehavior "exclusive"', () => { + const deleteBehavior = 'exclusive' as const; + + it('when filtered namespaces is empty, calls updateByQuery with expected script params', async () => { + const namespaces = ['default']; + const params = setup({ type, id, namespaces, deleteBehavior }); + + await deleteLegacyUrlAliases(params); + expect(params.client.updateByQuery).toHaveBeenCalledTimes(1); + expect(params.client.updateByQuery).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ + script: expect.objectContaining({ + params: { + namespaces: [], // 'default' is filtered out + matchTargetNamespaceOp: 'noop', + notMatchTargetNamespaceOp: 'delete', + }, + }), + }), + }), + expect.anything() + ); + }); + + it('when filtered namespaces is not empty, calls updateByQuery with expected script params', async () => { + const namespaces = ['space-a', 'default', 'space-b']; + const params = setup({ type, id, namespaces, deleteBehavior }); + + await deleteLegacyUrlAliases(params); + expect(params.client.updateByQuery).toHaveBeenCalledTimes(1); + expect(params.client.updateByQuery).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ + script: expect.objectContaining({ + params: { + namespaces: ['space-a', 'space-b'], // 'default' is filtered out + matchTargetNamespaceOp: 'noop', + notMatchTargetNamespaceOp: 'delete', + }, + }), + }), + }), + expect.anything() + ); + }); + }); +}); diff --git a/src/core/server/saved_objects/service/lib/legacy_url_aliases/delete_legacy_url_aliases.ts b/src/core/server/saved_objects/service/lib/legacy_url_aliases/delete_legacy_url_aliases.ts new file mode 100644 index 00000000000000..59c73d1f781a23 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/legacy_url_aliases/delete_legacy_url_aliases.ts @@ -0,0 +1,113 @@ +/* + * 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 * as esKuery from '@kbn/es-query'; + +import { getErrorMessage as getEsErrorMessage } from '../../../../elasticsearch'; +import type { ISavedObjectTypeRegistry } from '../../../saved_objects_type_registry'; +import type { IndexMapping } from '../../../mappings'; +import { LEGACY_URL_ALIAS_TYPE } from '../../../object_types'; +import type { RepositoryEsClient } from '../repository_es_client'; +import { getSearchDsl } from '../search_dsl'; +import { ALL_NAMESPACES_STRING, DEFAULT_NAMESPACE_STRING } from '../utils'; + +/** @internal */ +export interface DeleteLegacyUrlAliasesParams { + mappings: IndexMapping; + registry: ISavedObjectTypeRegistry; + client: RepositoryEsClient; + getIndexForType: (type: string) => string; + /** The object type. */ + type: string; + /** The object ID. */ + id: string; + /** + * The namespaces to include or exclude when searching for legacy URL alias targets (depends on the `deleteBehavior` parameter). + * Note that using `namespaces: [], deleteBehavior: 'exclusive'` will delete all aliases for this object in all spaces. + */ + namespaces: string[]; + /** + * If this is equal to 'inclusive', all aliases with a `targetNamespace` in the `namespaces` array will be deleted. + * If this is equal to 'exclusive', all aliases with a `targetNamespace` _not_ in the `namespaces` array will be deleted. + */ + deleteBehavior: 'inclusive' | 'exclusive'; +} + +/** + * Deletes legacy URL aliases that point to a given object. + * + * Note that aliases are only created when an object is converted to become share-capable, and each targetId is deterministically generated + * with uuidv5 -- this means that the chances of there actually being _multiple_ legacy URL aliases that target a given type/ID are slim to + * none. However, we don't always know exactly what space an alias could be in (if an object exists in multiple spaces, or in all spaces), + * so the most straightforward way for us to ensure that aliases are reliably deleted is to use updateByQuery, which is what this function + * does. + * + * @internal + */ +export async function deleteLegacyUrlAliases(params: DeleteLegacyUrlAliasesParams) { + const { mappings, registry, client, getIndexForType, type, id, namespaces, deleteBehavior } = + params; + + if (namespaces.includes(ALL_NAMESPACES_STRING)) { + throwError(type, id, '"namespaces" cannot include the * string'); + } + + // Legacy URL aliases cannot exist in the default space; filter that out + const filteredNamespaces = namespaces.filter( + (namespace) => namespace !== DEFAULT_NAMESPACE_STRING + ); + if (!filteredNamespaces.length && deleteBehavior === 'inclusive') { + // nothing to do, return early + return; + } + + const { buildNode } = esKuery.nodeTypes.function; + const match1 = buildNode('is', `${LEGACY_URL_ALIAS_TYPE}.targetType`, type); + const match2 = buildNode('is', `${LEGACY_URL_ALIAS_TYPE}.targetId`, id); + const kueryNode = buildNode('and', [match1, match2]); + + try { + await client.updateByQuery( + { + index: getIndexForType(LEGACY_URL_ALIAS_TYPE), + refresh: false, // This could be called many times in succession, intentionally do not wait for a refresh + body: { + ...getSearchDsl(mappings, registry, { + type: LEGACY_URL_ALIAS_TYPE, + kueryNode, + }), + script: { + // Intentionally use one script source with variable params to take advantage of ES script caching + source: ` + if (params['namespaces'].indexOf(ctx._source['${LEGACY_URL_ALIAS_TYPE}']['targetNamespace']) > -1) { + ctx.op = params['matchTargetNamespaceOp']; + } else { + ctx.op = params['notMatchTargetNamespaceOp']; + } + `, + params: { + namespaces: filteredNamespaces, + matchTargetNamespaceOp: deleteBehavior === 'inclusive' ? 'delete' : 'noop', + notMatchTargetNamespaceOp: deleteBehavior === 'inclusive' ? 'noop' : 'delete', + }, + lang: 'painless', + }, + conflicts: 'proceed', + }, + }, + { ignore: [404] } + ); + } catch (err) { + const errorMessage = getEsErrorMessage(err); + throwError(type, id, `${errorMessage}`); + } +} + +function throwError(type: string, id: string, message: string) { + throw new Error(`Failed to delete legacy URL aliases for ${type}/${id}: ${message}`); +} diff --git a/src/core/server/saved_objects/service/lib/find_legacy_url_aliases.test.ts b/src/core/server/saved_objects/service/lib/legacy_url_aliases/find_legacy_url_aliases.test.ts similarity index 94% rename from src/core/server/saved_objects/service/lib/find_legacy_url_aliases.test.ts rename to src/core/server/saved_objects/service/lib/legacy_url_aliases/find_legacy_url_aliases.test.ts index 134ea26d53b7de..755fa5794b5755 100644 --- a/src/core/server/saved_objects/service/lib/find_legacy_url_aliases.test.ts +++ b/src/core/server/saved_objects/service/lib/legacy_url_aliases/find_legacy_url_aliases.test.ts @@ -8,12 +8,12 @@ import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; -import { LegacyUrlAlias, LEGACY_URL_ALIAS_TYPE } from '../../object_types'; +import { LegacyUrlAlias, LEGACY_URL_ALIAS_TYPE } from '../../../object_types'; +import type { CreatePointInTimeFinderFn, PointInTimeFinder } from '../point_in_time_finder'; +import { savedObjectsPointInTimeFinderMock } from '../point_in_time_finder.mock'; +import type { ISavedObjectsRepository } from '../repository'; +import { savedObjectsRepositoryMock } from '../repository.mock'; import { findLegacyUrlAliases } from './find_legacy_url_aliases'; -import type { CreatePointInTimeFinderFn, PointInTimeFinder } from './point_in_time_finder'; -import { savedObjectsPointInTimeFinderMock } from './point_in_time_finder.mock'; -import type { ISavedObjectsRepository } from './repository'; -import { savedObjectsRepositoryMock } from './repository.mock'; describe('findLegacyUrlAliases', () => { let savedObjectsMock: jest.Mocked; diff --git a/src/core/server/saved_objects/service/lib/find_legacy_url_aliases.ts b/src/core/server/saved_objects/service/lib/legacy_url_aliases/find_legacy_url_aliases.ts similarity index 87% rename from src/core/server/saved_objects/service/lib/find_legacy_url_aliases.ts rename to src/core/server/saved_objects/service/lib/legacy_url_aliases/find_legacy_url_aliases.ts index aac022fc320984..7c1ce82129710c 100644 --- a/src/core/server/saved_objects/service/lib/find_legacy_url_aliases.ts +++ b/src/core/server/saved_objects/service/lib/legacy_url_aliases/find_legacy_url_aliases.ts @@ -7,9 +7,9 @@ */ import * as esKuery from '@kbn/es-query'; -import { LegacyUrlAlias, LEGACY_URL_ALIAS_TYPE } from '../../object_types'; -import { getObjectKey } from './internal_utils'; -import type { CreatePointInTimeFinderFn } from './point_in_time_finder'; +import { LegacyUrlAlias, LEGACY_URL_ALIAS_TYPE } from '../../../object_types'; +import { getObjectKey } from '../internal_utils'; +import type { CreatePointInTimeFinderFn } from '../point_in_time_finder'; interface FindLegacyUrlAliasesObject { type: string; @@ -68,6 +68,7 @@ export async function findLegacyUrlAliases( function createAliasKueryFilter(objects: Array<{ type: string; id: string }>) { const { buildNode } = esKuery.nodeTypes.function; + // Note: these nodes include '.attributes' for type-level fields because these are eventually passed to `validateConvertFilterToKueryNode`, which requires it const kueryNodes = objects.reduce((acc, { type, id }) => { const match1 = buildNode('is', `${LEGACY_URL_ALIAS_TYPE}.attributes.targetType`, type); const match2 = buildNode('is', `${LEGACY_URL_ALIAS_TYPE}.attributes.sourceId`, id); diff --git a/src/core/server/saved_objects/service/lib/legacy_url_aliases/index.ts b/src/core/server/saved_objects/service/lib/legacy_url_aliases/index.ts new file mode 100644 index 00000000000000..ec10668940d721 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/legacy_url_aliases/index.ts @@ -0,0 +1,12 @@ +/* + * 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. + */ + +export { findLegacyUrlAliases } from './find_legacy_url_aliases'; + +export { deleteLegacyUrlAliases } from './delete_legacy_url_aliases'; +export type { DeleteLegacyUrlAliasesParams } from './delete_legacy_url_aliases'; diff --git a/src/core/server/saved_objects/service/lib/preflight_check_for_create.test.mock.ts b/src/core/server/saved_objects/service/lib/preflight_check_for_create.test.mock.ts index e774a178abd492..fe8076b51e5dd4 100644 --- a/src/core/server/saved_objects/service/lib/preflight_check_for_create.test.mock.ts +++ b/src/core/server/saved_objects/service/lib/preflight_check_for_create.test.mock.ts @@ -6,14 +6,14 @@ * Side Public License, v 1. */ -import type { findLegacyUrlAliases } from './find_legacy_url_aliases'; +import type { findLegacyUrlAliases } from './legacy_url_aliases'; import type * as InternalUtils from './internal_utils'; export const mockFindLegacyUrlAliases = jest.fn() as jest.MockedFunction< typeof findLegacyUrlAliases >; -jest.mock('./find_legacy_url_aliases', () => { +jest.mock('./legacy_url_aliases', () => { return { findLegacyUrlAliases: mockFindLegacyUrlAliases }; }); diff --git a/src/core/server/saved_objects/service/lib/preflight_check_for_create.ts b/src/core/server/saved_objects/service/lib/preflight_check_for_create.ts index 6788cd8aa3abf6..e5b96a22631c17 100644 --- a/src/core/server/saved_objects/service/lib/preflight_check_for_create.ts +++ b/src/core/server/saved_objects/service/lib/preflight_check_for_create.ts @@ -13,7 +13,7 @@ import type { SavedObjectsRawDocSource, SavedObjectsSerializer, } from '../../serialization'; -import { findLegacyUrlAliases } from './find_legacy_url_aliases'; +import { findLegacyUrlAliases } from './legacy_url_aliases'; import { Either, rawDocExistsInNamespaces } from './internal_utils'; import { getObjectKey, isLeft, isRight } from './internal_utils'; import type { CreatePointInTimeFinderFn } from './point_in_time_finder'; 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 51ec81503956a3..f61a79ca9de662 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -14,6 +14,7 @@ import { mockUpdateObjectsSpaces, mockGetCurrentTime, mockPreflightCheckForCreate, + mockDeleteLegacyUrlAliases, } from './repository.test.mock'; import { SavedObjectsRepository } from './repository'; @@ -2394,9 +2395,11 @@ describe('SavedObjectsRepository', () => { const id = 'logstash-*'; const namespace = 'foo-namespace'; - const deleteSuccess = async (type, id, options) => { + const deleteSuccess = async (type, id, options, internalOptions = {}) => { + const { mockGetResponseValue } = internalOptions; if (registry.isMultiNamespace(type)) { - const mockGetResponse = getMockGetResponse({ type, id }, options?.namespace); + const mockGetResponse = + mockGetResponseValue ?? getMockGetResponse({ type, id }, options?.namespace); client.get.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise(mockGetResponse) ); @@ -2409,6 +2412,11 @@ describe('SavedObjectsRepository', () => { return result; }; + beforeEach(() => { + mockDeleteLegacyUrlAliases.mockClear(); + mockDeleteLegacyUrlAliases.mockResolvedValue(); + }); + describe('client calls', () => { it(`should use the ES delete action when not using a multi-namespace type`, async () => { await deleteSuccess(type, id); @@ -2482,6 +2490,64 @@ describe('SavedObjectsRepository', () => { }); }); + describe('legacy URL aliases', () => { + it(`doesn't delete legacy URL aliases for single-namespace object types`, async () => { + await deleteSuccess(type, id, { namespace }); + expect(mockDeleteLegacyUrlAliases).not.toHaveBeenCalled(); + }); + + // We intentionally do not include a test case for a multi-namespace object with a "not found" preflight result, because that throws + // an error (without deleting aliases) and we already have a test case for that + + it(`deletes legacy URL aliases for multi-namespace object types (all spaces)`, async () => { + const internalOptions = { + mockGetResponseValue: getMockGetResponse( + { type: MULTI_NAMESPACE_TYPE, id }, + ALL_NAMESPACES_STRING + ), + }; + await deleteSuccess(MULTI_NAMESPACE_TYPE, id, { namespace, force: true }, internalOptions); + expect(mockDeleteLegacyUrlAliases).toHaveBeenCalledWith( + expect.objectContaining({ + type: MULTI_NAMESPACE_TYPE, + id, + namespaces: [], + deleteBehavior: 'exclusive', + }) + ); + }); + + it(`deletes legacy URL aliases for multi-namespace object types (specific spaces)`, async () => { + await deleteSuccess(MULTI_NAMESPACE_TYPE, id, { namespace }); // this function mocks a preflight response with the given namespace by default + expect(mockDeleteLegacyUrlAliases).toHaveBeenCalledWith( + expect.objectContaining({ + type: MULTI_NAMESPACE_TYPE, + id, + namespaces: [namespace], + deleteBehavior: 'inclusive', + }) + ); + }); + + it(`logs a message when deleteLegacyUrlAliases returns an error`, async () => { + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + getMockGetResponse({ type: MULTI_NAMESPACE_ISOLATED_TYPE, id, namespace }) + ) + ); + client.delete.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ result: 'deleted' }) + ); + mockDeleteLegacyUrlAliases.mockRejectedValueOnce(new Error('Oh no!')); + await savedObjectsRepository.delete(MULTI_NAMESPACE_ISOLATED_TYPE, id, { namespace }); + expect(client.get).toHaveBeenCalledTimes(1); + expect(logger.error).toHaveBeenCalledTimes(1); + expect(logger.error).toHaveBeenCalledWith( + 'Unable to delete aliases when deleting an object: Oh no!' + ); + }); + }); + describe('errors', () => { const expectNotFoundError = async (type, id, options) => { await expect(savedObjectsRepository.delete(type, id, options)).rejects.toThrowError( diff --git a/src/core/server/saved_objects/service/lib/repository.test.mock.ts b/src/core/server/saved_objects/service/lib/repository.test.mock.ts index 88eb13e3ca46b8..3ec2cb0a5d8b95 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.mock.ts +++ b/src/core/server/saved_objects/service/lib/repository.test.mock.ts @@ -11,6 +11,7 @@ import type { internalBulkResolve } from './internal_bulk_resolve'; import type * as InternalUtils from './internal_utils'; import type { preflightCheckForCreate } from './preflight_check_for_create'; import type { updateObjectsSpaces } from './update_objects_spaces'; +import type { deleteLegacyUrlAliases } from './legacy_url_aliases'; export const mockCollectMultiNamespaceReferences = jest.fn() as jest.MockedFunction< typeof collectMultiNamespaceReferences @@ -60,3 +61,10 @@ export const pointInTimeFinderMock = jest.fn(); jest.doMock('./point_in_time_finder', () => ({ PointInTimeFinder: pointInTimeFinderMock, })); + +export const mockDeleteLegacyUrlAliases = jest.fn() as jest.MockedFunction< + typeof deleteLegacyUrlAliases +>; +jest.mock('./legacy_url_aliases', () => ({ + deleteLegacyUrlAliases: mockDeleteLegacyUrlAliases, +})); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index ab802d2f366778..d538690fb19203 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -100,6 +100,7 @@ import { preflightCheckForCreate, PreflightCheckForCreateObject, } from './preflight_check_for_create'; +import { deleteLegacyUrlAliases } from './legacy_url_aliases'; // BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository // so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient. @@ -717,6 +718,25 @@ export class SavedObjectsRepository { const deleted = body.result === 'deleted'; if (deleted) { + const namespaces = preflightResult?.savedObjectNamespaces; + if (namespaces) { + // This is a multi-namespace object type, and it might have legacy URL aliases that need to be deleted. + await deleteLegacyUrlAliases({ + mappings: this._mappings, + registry: this._registry, + client: this.client, + getIndexForType: this.getIndexForType.bind(this), + type, + id, + ...(namespaces.includes(ALL_NAMESPACES_STRING) + ? { namespaces: [], deleteBehavior: 'exclusive' } // delete legacy URL aliases for this type/ID for all spaces + : { namespaces, deleteBehavior: 'inclusive' }), // delete legacy URL aliases for this type/ID for these specific spaces + }).catch((err) => { + // The object has already been deleted, but we caught an error when attempting to delete aliases. + // A consumer cannot attempt to delete the object again, so just log the error and swallow it. + this._logger.error(`Unable to delete aliases when deleting an object: ${err.message}`); + }); + } return {}; } @@ -1344,10 +1364,12 @@ export class SavedObjectsRepository { options?: SavedObjectsUpdateObjectsSpacesOptions ) { return updateObjectsSpaces({ + mappings: this._mappings, registry: this._registry, allowedTypes: this._allowedTypes, client: this.client, serializer: this._serializer, + logger: this._logger, getIndexForType: this.getIndexForType.bind(this), objects, spacesToAdd, diff --git a/src/core/server/saved_objects/service/lib/update_objects_spaces.test.mock.ts b/src/core/server/saved_objects/service/lib/update_objects_spaces.test.mock.ts index d7aa762e01aab7..043975d5bb52b1 100644 --- a/src/core/server/saved_objects/service/lib/update_objects_spaces.test.mock.ts +++ b/src/core/server/saved_objects/service/lib/update_objects_spaces.test.mock.ts @@ -7,6 +7,7 @@ */ import type * as InternalUtils from './internal_utils'; +import type { deleteLegacyUrlAliases } from './legacy_url_aliases'; export const mockGetBulkOperationError = jest.fn() as jest.MockedFunction< typeof InternalUtils['getBulkOperationError'] @@ -27,3 +28,10 @@ jest.mock('./internal_utils', () => { rawDocExistsInNamespace: mockRawDocExistsInNamespace, }; }); + +export const mockDeleteLegacyUrlAliases = jest.fn() as jest.MockedFunction< + typeof deleteLegacyUrlAliases +>; +jest.mock('./legacy_url_aliases', () => ({ + deleteLegacyUrlAliases: mockDeleteLegacyUrlAliases, +})); diff --git a/src/core/server/saved_objects/service/lib/update_objects_spaces.test.ts b/src/core/server/saved_objects/service/lib/update_objects_spaces.test.ts index 11dbe6149878c1..d5b79b70a55a12 100644 --- a/src/core/server/saved_objects/service/lib/update_objects_spaces.test.ts +++ b/src/core/server/saved_objects/service/lib/update_objects_spaces.test.ts @@ -10,12 +10,14 @@ import { mockGetBulkOperationError, mockGetExpectedVersionProperties, mockRawDocExistsInNamespace, + mockDeleteLegacyUrlAliases, } from './update_objects_spaces.test.mock'; import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import type { ElasticsearchClient } from 'src/core/server/elasticsearch'; import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; +import { loggerMock } from '../../../logging/logger.mock'; import { typeRegistryMock } from '../../saved_objects_type_registry.mock'; import { SavedObjectsSerializer } from '../../serialization'; import type { @@ -23,6 +25,7 @@ import type { UpdateObjectsSpacesParams, } from './update_objects_spaces'; import { updateObjectsSpaces } from './update_objects_spaces'; +import { ALL_NAMESPACES_STRING } from './utils'; type SetupParams = Partial< Pick @@ -53,6 +56,8 @@ beforeEach(() => { mockGetExpectedVersionProperties.mockReturnValue(EXPECTED_VERSION_PROPS); mockRawDocExistsInNamespace.mockReset(); mockRawDocExistsInNamespace.mockReturnValue(true); // return true by default + mockDeleteLegacyUrlAliases.mockReset(); + mockDeleteLegacyUrlAliases.mockResolvedValue(); // resolve an empty promise by default }); afterAll(() => { @@ -71,10 +76,12 @@ describe('#updateObjectsSpaces', () => { client = elasticsearchClientMock.createElasticsearchClient(); const serializer = new SavedObjectsSerializer(registry); return { + mappings: { properties: {} }, // doesn't matter, only used as an argument to deleteLegacyUrlAliases which is mocked registry, allowedTypes: [SHAREABLE_OBJ_TYPE, NON_SHAREABLE_OBJ_TYPE], // SHAREABLE_HIDDEN_OBJ_TYPE is excluded client, serializer, + logger: loggerMock.create(), getIndexForType: (type: string) => `index-for-${type}`, objects, spacesToAdd, @@ -382,6 +389,149 @@ describe('#updateObjectsSpaces', () => { expect(client.bulk).not.toHaveBeenCalled(); }); }); + + describe('legacy URL aliases', () => { + it('does not delete aliases for objects that were not removed from any spaces', async () => { + const space1 = 'space-to-add'; + const space2 = 'space-to-remove'; + const space3 = 'other-space'; + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space1] }; // will not be changed + const obj2 = { type: SHAREABLE_OBJ_TYPE, id: 'id-2', spaces: [space3] }; // will be updated + + const objects = [obj1, obj2]; + const spacesToAdd = [space1]; + const spacesToRemove = [space2]; + const params = setup({ objects, spacesToAdd, spacesToRemove }); + // this test case does not call mget + mockBulkResults({ error: false }); // result for obj2 + + await updateObjectsSpaces(params); + expect(client.bulk).toHaveBeenCalledTimes(1); + expectBulkArgs({ action: 'update', object: { ...obj2, namespaces: [space3, space1] } }); + expect(mockDeleteLegacyUrlAliases).not.toHaveBeenCalled(); + expect(params.logger.error).not.toHaveBeenCalled(); + }); + + it('does not delete aliases for objects that were removed from spaces but were also added to All Spaces (*)', async () => { + const space2 = 'space-to-remove'; + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space2] }; + + const objects = [obj1]; + const spacesToAdd = [ALL_NAMESPACES_STRING]; + const spacesToRemove = [space2]; + const params = setup({ objects, spacesToAdd, spacesToRemove }); + // this test case does not call mget + mockBulkResults({ error: false }); // result for obj1 + + await updateObjectsSpaces(params); + expect(client.bulk).toHaveBeenCalledTimes(1); + expectBulkArgs({ + action: 'update', + object: { ...obj1, namespaces: [ALL_NAMESPACES_STRING] }, + }); + expect(mockDeleteLegacyUrlAliases).not.toHaveBeenCalled(); + expect(params.logger.error).not.toHaveBeenCalled(); + }); + + it('deletes aliases for objects that were removed from specific spaces using "deleteBehavior: exclusive"', async () => { + const space1 = 'space-to-remove'; + const space2 = 'another-space-to-remove'; + const space3 = 'other-space'; + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space3] }; // will not be changed + const obj2 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space1, space2, space3] }; // will be updated + const obj3 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space1] }; // will be deleted + + const objects = [obj1, obj2, obj3]; + const spacesToRemove = [space1, space2]; + const params = setup({ objects, spacesToRemove }); + // this test case does not call mget + mockBulkResults({ error: false }, { error: false }); // result2 for obj2 and obj3 + + await updateObjectsSpaces(params); + expect(client.bulk).toHaveBeenCalledTimes(1); + expectBulkArgs( + { action: 'update', object: { ...obj2, namespaces: [space3] } }, + { action: 'delete', object: obj3 } + ); + expect(mockDeleteLegacyUrlAliases).toHaveBeenCalledTimes(2); + expect(mockDeleteLegacyUrlAliases).toHaveBeenNthCalledWith( + 1, // the first call resulted in an error which generated a log message (see assertion below) + expect.objectContaining({ + type: obj2.type, + id: obj2.id, + namespaces: [space1, space2], + deleteBehavior: 'inclusive', + }) + ); + expect(mockDeleteLegacyUrlAliases).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + type: obj3.type, + id: obj3.id, + namespaces: [space1], + deleteBehavior: 'inclusive', + }) + ); + expect(params.logger.error).not.toHaveBeenCalled(); + }); + + it('deletes aliases for objects that were removed from all spaces using "deleteBehavior: inclusive"', async () => { + const space1 = 'space-to-add'; + const space2 = 'other-space'; + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space2] }; // will be updated to add space1 + const obj2 = { + type: SHAREABLE_OBJ_TYPE, + id: 'id-2', + spaces: [space2, ALL_NAMESPACES_STRING], // will be updated to add space1 and remove * + }; + + const objects = [obj1, obj2]; + const spacesToAdd = [space1]; + const spacesToRemove = [ALL_NAMESPACES_STRING]; + const params = setup({ objects, spacesToAdd, spacesToRemove }); + // this test case does not call mget + mockBulkResults({ error: false }); // result for obj1 + + await updateObjectsSpaces(params); + expect(client.bulk).toHaveBeenCalledTimes(1); + expectBulkArgs( + { action: 'update', object: { ...obj1, namespaces: [space2, space1] } }, + { action: 'update', object: { ...obj2, namespaces: [space2, space1] } } + ); + expect(mockDeleteLegacyUrlAliases).toHaveBeenCalledTimes(1); + expect(mockDeleteLegacyUrlAliases).toHaveBeenCalledWith( + expect.objectContaining({ + type: obj2.type, + id: obj2.id, + namespaces: [space2, space1], + deleteBehavior: 'exclusive', + }) + ); + expect(params.logger.error).not.toHaveBeenCalled(); + }); + + it('logs a message when deleteLegacyUrlAliases returns an error', async () => { + const space1 = 'space-to-remove'; + const space2 = 'other-space'; + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space1, space2] }; // will be updated + + const objects = [obj1]; + const spacesToRemove = [space1]; + const params = setup({ objects, spacesToRemove }); + // this test case does not call mget + mockBulkResults({ error: false }); // result for obj1 + mockDeleteLegacyUrlAliases.mockRejectedValueOnce(new Error('Oh no!')); // result for deleting aliases for obj1 + + await updateObjectsSpaces(params); + expect(client.bulk).toHaveBeenCalledTimes(1); + expectBulkArgs({ action: 'update', object: { ...obj1, namespaces: [space2] } }); + expect(mockDeleteLegacyUrlAliases).toHaveBeenCalledTimes(1); // don't assert deleteLegacyUrlAliases args, we have tests for that above + expect(params.logger.error).toHaveBeenCalledTimes(1); + expect(params.logger.error).toHaveBeenCalledWith( + 'Unable to delete aliases when unsharing an object: Oh no!' + ); + }); + }); }); describe('returns expected results', () => { diff --git a/src/core/server/saved_objects/service/lib/update_objects_spaces.ts b/src/core/server/saved_objects/service/lib/update_objects_spaces.ts index d88bf700a900e6..90e914b9796c33 100644 --- a/src/core/server/saved_objects/service/lib/update_objects_spaces.ts +++ b/src/core/server/saved_objects/service/lib/update_objects_spaces.ts @@ -6,9 +6,12 @@ * Side Public License, v 1. */ +import pMap from 'p-map'; import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import intersection from 'lodash/intersection'; +import type { Logger } from '../../../logging'; +import type { IndexMapping } from '../../mappings'; import type { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import type { SavedObjectsRawDocSource, SavedObjectsSerializer } from '../../serialization'; import type { @@ -28,6 +31,9 @@ import { } from './internal_utils'; import { DEFAULT_REFRESH_SETTING } from './repository'; import type { RepositoryEsClient } from './repository_es_client'; +import { ALL_NAMESPACES_STRING } from './utils'; +import type { DeleteLegacyUrlAliasesParams } from './legacy_url_aliases'; +import { deleteLegacyUrlAliases } from './legacy_url_aliases'; /** * An object that should have its spaces updated. @@ -94,10 +100,12 @@ export interface SavedObjectsUpdateObjectsSpacesResponseObject { * @internal */ export interface UpdateObjectsSpacesParams { + mappings: IndexMapping; registry: ISavedObjectTypeRegistry; allowedTypes: string[]; client: RepositoryEsClient; serializer: SavedObjectsSerializer; + logger: Logger; getIndexForType: (type: string) => string; objects: SavedObjectsUpdateObjectsSpacesObject[]; spacesToAdd: string[]; @@ -105,15 +113,24 @@ export interface UpdateObjectsSpacesParams { options?: SavedObjectsUpdateObjectsSpacesOptions; } +type ObjectToDeleteAliasesFor = Pick< + DeleteLegacyUrlAliasesParams, + 'type' | 'id' | 'namespaces' | 'deleteBehavior' +>; + +const MAX_CONCURRENT_ALIAS_DELETIONS = 10; + /** * Gets all references and transitive references of the given objects. Ignores any object and/or reference that is not a multi-namespace * type. */ export async function updateObjectsSpaces({ + mappings, registry, allowedTypes, client, serializer, + logger, getIndexForType, objects, spacesToAdd, @@ -190,8 +207,12 @@ export async function updateObjectsSpaces({ const time = new Date().toISOString(); let bulkOperationRequestIndexCounter = 0; const bulkOperationParams: estypes.BulkOperationContainer[] = []; + const objectsToDeleteAliasesFor: ObjectToDeleteAliasesFor[] = []; const expectedBulkOperationResults: Array< - Either> + Either< + SavedObjectsUpdateObjectsSpacesResponseObject, + { type: string; id: string; updatedSpaces: string[]; esRequestIndex?: number } + > > = expectedBulkGetResults.map((expectedBulkGetResult) => { if (isLeft(expectedBulkGetResult)) { return expectedBulkGetResult; @@ -225,7 +246,7 @@ export async function updateObjectsSpaces({ versionProperties = getExpectedVersionProperties(version); } - const { newSpaces, isUpdateRequired } = getNewSpacesArray( + const { updatedSpaces, removedSpaces, isUpdateRequired } = analyzeSpaceChanges( currentSpaces, spacesToAdd, spacesToRemove @@ -233,7 +254,7 @@ export async function updateObjectsSpaces({ const expectedResult = { type, id, - newSpaces, + updatedSpaces, ...(isUpdateRequired && { esRequestIndex: bulkOperationRequestIndexCounter++ }), }; @@ -243,13 +264,24 @@ export async function updateObjectsSpaces({ _index: getIndexForType(type), ...versionProperties, }; - if (newSpaces.length) { - const documentToSave = { updated_at: time, namespaces: newSpaces }; + if (updatedSpaces.length) { + const documentToSave = { updated_at: time, namespaces: updatedSpaces }; // @ts-expect-error BulkOperation.retry_on_conflict, BulkOperation.routing. BulkOperation.version, and BulkOperation.version_type are optional bulkOperationParams.push({ update: documentMetadata }, { doc: documentToSave }); } else { bulkOperationParams.push({ delete: documentMetadata }); } + + if (removedSpaces.length && !updatedSpaces.includes(ALL_NAMESPACES_STRING)) { + // The object is being removed from at least one space; make sure to delete aliases appropriately + objectsToDeleteAliasesFor.push({ + type, + id, + ...(removedSpaces.includes(ALL_NAMESPACES_STRING) + ? { namespaces: updatedSpaces, deleteBehavior: 'exclusive' } + : { namespaces: removedSpaces, deleteBehavior: 'inclusive' }), + }); + } } return { tag: 'Right', value: expectedResult }; @@ -260,6 +292,24 @@ export async function updateObjectsSpaces({ ? await client.bulk({ refresh, body: bulkOperationParams, require_alias: true }) : undefined; + // Delete aliases if necessary, ensuring we don't have too many concurrent operations running. + const mapper = async ({ type, id, namespaces, deleteBehavior }: ObjectToDeleteAliasesFor) => + deleteLegacyUrlAliases({ + mappings, + registry, + client, + getIndexForType, + type, + id, + namespaces, + deleteBehavior, + }).catch((err) => { + // The object has already been unshared, but we caught an error when attempting to delete aliases. + // A consumer cannot attempt to unshare the object again, so just log the error and swallow it. + logger.error(`Unable to delete aliases when unsharing an object: ${err.message}`); + }); + await pMap(objectsToDeleteAliasesFor, mapper, { concurrency: MAX_CONCURRENT_ALIAS_DELETIONS }); + return { objects: expectedBulkOperationResults.map( (expectedResult) => { @@ -267,7 +317,7 @@ export async function updateObjectsSpaces({ return expectedResult.value; } - const { type, id, newSpaces, esRequestIndex } = expectedResult.value; + const { type, id, updatedSpaces, esRequestIndex } = expectedResult.value; if (esRequestIndex !== undefined) { const response = bulkOperationResponse?.body.items[esRequestIndex] ?? {}; const rawResponse = Object.values(response)[0] as any; @@ -277,7 +327,7 @@ export async function updateObjectsSpaces({ } } - return { id, type, spaces: newSpaces }; + return { id, type, spaces: updatedSpaces }; } ), }; @@ -289,17 +339,22 @@ function errorContent(error: DecoratedError) { } /** Gets the remaining spaces for an object after adding new ones and removing old ones. */ -function getNewSpacesArray( +function analyzeSpaceChanges( existingSpaces: string[], spacesToAdd: string[], spacesToRemove: string[] ) { const addSet = new Set(spacesToAdd); const removeSet = new Set(spacesToRemove); - const newSpaces = existingSpaces + const removedSpaces: string[] = []; + const updatedSpaces = existingSpaces .filter((x) => { addSet.delete(x); - return !removeSet.delete(x); + const removed = removeSet.delete(x); + if (removed) { + removedSpaces.push(x); + } + return !removed; }) .concat(Array.from(addSet)); @@ -307,5 +362,5 @@ function getNewSpacesArray( const isAnySpaceRemoved = removeSet.size < spacesToRemove.length; const isUpdateRequired = isAnySpaceAdded || isAnySpaceRemoved; - return { newSpaces, isUpdateRequired }; + return { updatedSpaces, removedSpaces, isUpdateRequired }; } diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json index 84c31dc2b7db60..e7d2c630fc130d 100644 --- a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json +++ b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json @@ -57,11 +57,12 @@ "index": ".kibana", "source": { "index-pattern": { - "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"@message\"}}},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"@tags\"}}},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"agent\"}}},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"extension\"}}},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"headings\"}}},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"host\"}}},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"index\"}}},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"links\"}}},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"machine.os\"}}},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.article:section\"}}},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.article:tag\"}}},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:description\"}}},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:image\"}}},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:image:height\"}}},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:image:width\"}}},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:site_name\"}}},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:title\"}}},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:type\"}}},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:url\"}}},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.twitter:card\"}}},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.twitter:description\"}}},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.twitter:image\"}}},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.twitter:site\"}}},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.twitter:title\"}}},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.url\"}}},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"request\"}}},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"response\"}}},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"spaces\"}}},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"url\"}}},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"xss\"}}}]", "timeFieldName": "@timestamp", "title": "logstash-*" }, + "namespaces": ["default"], "type": "index-pattern", + "migrationVersion": { "index-pattern": "8.0.0" }, "updated_at": "2017-09-21T18:49:16.270Z" }, "type": "doc" @@ -145,16 +146,16 @@ { "type": "doc", "value": { - "id": "space_1:index-pattern:space1-index-pattern-id", + "id": "index-pattern:space1-index-pattern-id", "index": ".kibana", "source": { "index-pattern": { - "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"@message\"}}},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"@tags\"}}},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"agent\"}}},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"extension\"}}},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"headings\"}}},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"host\"}}},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"index\"}}},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"links\"}}},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"machine.os\"}}},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.article:section\"}}},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.article:tag\"}}},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:description\"}}},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:image\"}}},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:image:height\"}}},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:image:width\"}}},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:site_name\"}}},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:title\"}}},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:type\"}}},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:url\"}}},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.twitter:card\"}}},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.twitter:description\"}}},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.twitter:image\"}}},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.twitter:site\"}}},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.twitter:title\"}}},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.url\"}}},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"request\"}}},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"response\"}}},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"spaces\"}}},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"url\"}}},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"xss\"}}}]", "timeFieldName": "@timestamp", "title": "logstash-*" }, - "namespace": "space_1", + "namespaces": ["space_1"], "type": "index-pattern", + "migrationVersion": { "index-pattern": "8.0.0" }, "updated_at": "2017-09-21T18:49:16.270Z" }, "type": "doc" @@ -240,16 +241,16 @@ { "type": "doc", "value": { - "id": "space_2:index-pattern:space2-index-pattern-id", + "id": "index-pattern:space2-index-pattern-id", "index": ".kibana", "source": { "index-pattern": { - "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"@message\"}}},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"@tags\"}}},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"agent\"}}},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"extension\"}}},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"headings\"}}},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"host\"}}},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"index\"}}},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"links\"}}},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"machine.os\"}}},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.article:section\"}}},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.article:tag\"}}},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:description\"}}},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:image\"}}},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:image:height\"}}},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:image:width\"}}},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:site_name\"}}},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:title\"}}},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:type\"}}},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:url\"}}},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.twitter:card\"}}},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.twitter:description\"}}},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.twitter:image\"}}},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.twitter:site\"}}},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.twitter:title\"}}},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.url\"}}},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"request\"}}},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"response\"}}},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"spaces\"}}},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"url\"}}},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"xss\"}}}]", "timeFieldName": "@timestamp", "title": "logstash-*" }, - "namespace": "space_2", + "namespaces": ["space_2"], "type": "index-pattern", + "migrationVersion": { "index-pattern": "8.0.0" }, "updated_at": "2017-09-21T18:49:16.270Z" }, "type": "doc" @@ -554,6 +555,22 @@ } } +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "resolvetype:all_spaces", + "source": { + "type": "resolvetype", + "updated_at": "2017-09-21T18:51:23.794Z", + "resolvetype": { + "title": "This is used to test that 1. the 'disabled' alias does not resolve to this target (because the alias is disabled), and 2. when this object that exists in all spaces is deleted, the alias that targets it is deleted too (even though the alias is disabled)" + }, + "namespaces": ["*"] + } + } +} + { "type": "doc", "value": { @@ -566,7 +583,7 @@ "sourceId": "disabled", "targetNamespace": "space_1", "targetType": "resolvetype", - "targetId": "alias-match-newid", + "targetId": "disabled-newid", "disabled": true } } @@ -585,7 +602,7 @@ "sourceId": "alias-match", "targetNamespace": "space_x", "targetType": "resolvetype", - "targetId": "doesnt-matter" + "targetId": "alias-match-newid" } } } @@ -603,7 +620,7 @@ "sourceId": "alias-match", "targetNamespace": "space_y", "targetType": "resolvetype", - "targetId": "doesnt-matter", + "targetId": "alias-match-newid", "disabled": true } } diff --git a/x-pack/test/saved_object_api_integration/common/suites/delete.ts b/x-pack/test/saved_object_api_integration/common/suites/delete.ts index 844da51d16e80b..4dbd7901a05c4c 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/delete.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/delete.ts @@ -6,7 +6,9 @@ */ import { SuperTest } from 'supertest'; +import type { Client } from '@elastic/elasticsearch'; import expect from '@kbn/expect'; +import type { SearchTotalHits } from '@elastic/elasticsearch/lib/api/types'; import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; import { SPACES } from '../lib/spaces'; import { expectResponses, getUrlPrefix, getTestTitle } from '../lib/saved_object_test_utils'; @@ -21,9 +23,13 @@ export interface DeleteTestCase extends TestCase { failure?: 400 | 403 | 404; } +const ALIAS_DELETE_INCLUSIVE = Object.freeze({ type: 'resolvetype', id: 'alias-match-newid' }); // exists in three specific spaces; deleting this should also delete the alias that targets it in space 1 +const ALIAS_DELETE_EXCLUSIVE = Object.freeze({ type: 'resolvetype', id: 'all_spaces' }); // exists in all spaces; deleting this should also delete the alias that targets it in space 1 const DOES_NOT_EXIST = Object.freeze({ type: 'dashboard', id: 'does-not-exist' }); export const TEST_CASES: Record = Object.freeze({ ...CASES, + ALIAS_DELETE_INCLUSIVE, + ALIAS_DELETE_EXCLUSIVE, DOES_NOT_EXIST, }); @@ -32,7 +38,7 @@ export const TEST_CASES: Record = Object.freeze({ */ const createRequest = ({ type, id, force }: DeleteTestCase) => ({ type, id, force }); -export function deleteTestSuiteFactory(esArchiver: any, supertest: SuperTest) { +export function deleteTestSuiteFactory(es: Client, esArchiver: any, supertest: SuperTest) { const expectSavedObjectForbidden = expectResponses.forbiddenTypes('delete'); const expectResponseBody = (testCase: DeleteTestCase): ExpectResponseBody => @@ -47,6 +53,25 @@ export function deleteTestSuiteFactory(esArchiver: any, supertest: SuperTest testCase.type === type && testCase.id === id + ); + expect((searchResponse.hits.total as SearchTotalHits).value).to.eql( + // Five aliases exist but only one should be deleted in each case (for the "inclusive" case, this asserts that the aliases + // targeting that object in space x and space y were *not* deleted) + expectAliasWasDeleted ? 4 : 5 + ); } } }; diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/delete.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/delete.ts index 6a6fc8a15decfd..8970070645f4df 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/delete.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/delete.ts @@ -51,6 +51,8 @@ const createTestCases = (spaceId: string) => { }, { ...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, CASES.NAMESPACE_AGNOSTIC, + { ...CASES.ALIAS_DELETE_INCLUSIVE, force: true }, + { ...CASES.ALIAS_DELETE_EXCLUSIVE, force: true }, { ...CASES.DOES_NOT_EXIST, ...fail404() }, ]; const hiddenType = [{ ...CASES.HIDDEN, ...fail404() }]; @@ -61,8 +63,9 @@ const createTestCases = (spaceId: string) => { export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); + const es = getService('es'); - const { addTests, createTestDefinitions } = deleteTestSuiteFactory(esArchiver, supertest); + const { addTests, createTestDefinitions } = deleteTestSuiteFactory(es, esArchiver, supertest); const createTests = (spaceId: string) => { const { normalTypes, hiddenType, allTypes } = createTestCases(spaceId); return { diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/delete.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/delete.ts index 1a168bac948bef..28674e8fd45aa5 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/delete.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/delete.ts @@ -45,6 +45,8 @@ const createTestCases = (spaceId: string) => [ }, { ...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, CASES.NAMESPACE_AGNOSTIC, + { ...CASES.ALIAS_DELETE_INCLUSIVE, force: true }, + { ...CASES.ALIAS_DELETE_EXCLUSIVE, force: true }, { ...CASES.HIDDEN, ...fail404() }, { ...CASES.DOES_NOT_EXIST, ...fail404() }, ]; @@ -52,8 +54,9 @@ const createTestCases = (spaceId: string) => [ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); + const es = getService('es'); - const { addTests, createTestDefinitions } = deleteTestSuiteFactory(esArchiver, supertest); + const { addTests, createTestDefinitions } = deleteTestSuiteFactory(es, esArchiver, supertest); const createTests = (spaceId: string) => { const testCases = createTestCases(spaceId); return createTestDefinitions(testCases, false, { spaceId }); diff --git a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json index c9b09456a9a492..c5dc147b45123c 100644 --- a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json +++ b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json @@ -582,6 +582,94 @@ } } +{ + "type": "doc", + "value": { + "id": "sharedtype:alias_delete_inclusive", + "index": ".kibana", + "source": { + "sharedtype": { + "title": "This is used to test that when an object is unshared from a space, inbound aliases for just those spaces are removed" + }, + "type": "sharedtype", + "namespaces": ["default", "space_1", "space_2"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "legacy-url-alias:space_1:sharedtype:doesnt-matter", + "index": ".kibana", + "source": { + "type": "legacy-url-alias", + "updated_at": "2017-09-21T18:51:23.794Z", + "legacy-url-alias": { + "sourceId": "doesnt-matter", + "targetNamespace": "space_1", + "targetType": "sharedtype", + "targetId": "alias_delete_inclusive" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "legacy-url-alias:space_2:sharedtype:doesnt-matter", + "index": ".kibana", + "source": { + "type": "legacy-url-alias", + "updated_at": "2017-09-21T18:51:23.794Z", + "legacy-url-alias": { + "sourceId": "doesnt-matter", + "targetNamespace": "space_2", + "targetType": "sharedtype", + "targetId": "alias_delete_inclusive" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "sharedtype:alias_delete_exclusive", + "index": ".kibana", + "source": { + "sharedtype": { + "title": "This is used to test that when an object is unshared from all space, inbound aliases for all spaces are removed" + }, + "type": "sharedtype", + "namespaces": ["*"], + "updated_at": "2017-09-21T18:59:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "legacy-url-alias:other_space:sharedtype:doesnt-matter", + "index": ".kibana", + "source": { + "type": "legacy-url-alias", + "updated_at": "2017-09-21T18:51:23.794Z", + "legacy-url-alias": { + "sourceId": "doesnt-matter", + "targetNamespace": "other_space", + "targetType": "sharedtype", + "targetId": "alias_delete_exclusive" + } + } + } +} + { "type": "doc", "value": { diff --git a/x-pack/test/spaces_api_integration/common/lib/saved_object_test_cases.ts b/x-pack/test/spaces_api_integration/common/lib/saved_object_test_cases.ts index ddbcf8f5f31c17..8d9af1170f288f 100644 --- a/x-pack/test/spaces_api_integration/common/lib/saved_object_test_cases.ts +++ b/x-pack/test/spaces_api_integration/common/lib/saved_object_test_cases.ts @@ -38,6 +38,14 @@ export const MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES = Object.freeze({ id: 'all_spaces', existingNamespaces: ['*'], // all current and future spaces }), + ALIAS_DELETE_INCLUSIVE: Object.freeze({ + id: 'alias_delete_inclusive', + existingNamespaces: ['default', 'space_1', 'space_2'], // each individual space + }), + ALIAS_DELETE_EXCLUSIVE: Object.freeze({ + id: 'alias_delete_exclusive', + existingNamespaces: ['*'], // all current and future spaces + }), DOES_NOT_EXIST: Object.freeze({ id: 'does_not_exist', existingNamespaces: [] as string[], diff --git a/x-pack/test/spaces_api_integration/common/suites/delete.ts b/x-pack/test/spaces_api_integration/common/suites/delete.ts index e0f222af707c54..ae8b73535c2c65 100644 --- a/x-pack/test/spaces_api_integration/common/suites/delete.ts +++ b/x-pack/test/spaces_api_integration/common/suites/delete.ts @@ -53,8 +53,8 @@ export function deleteTestSuiteFactory(es: Client, esArchiver: any, supertest: S // @ts-expect-error @elastic/elasticsearch doesn't defined `count.buckets`. const buckets = response.aggregations?.count.buckets; - // The test fixture contains three legacy URL aliases: - // (1) one for "space_1", (2) one for "space_2", and (3) one for "other_space", which is a non-existent space. + // The test fixture contains six legacy URL aliases: + // (1) two for "space_1", (2) two for "space_2", and (3) two for "other_space", which is a non-existent space. // Each test deletes "space_2", so the agg buckets should reflect that aliases (1) and (3) still exist afterwards. // Space 2 deleted, all others should exist @@ -75,26 +75,26 @@ export function deleteTestSuiteFactory(es: Client, esArchiver: any, supertest: S }, }, { - doc_count: 6, + doc_count: 7, key: 'space_1', countByType: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'visualization', doc_count: 3 }, + { key: 'legacy-url-alias', doc_count: 2 }, // aliases (1) { key: 'dashboard', doc_count: 1 }, { key: 'index-pattern', doc_count: 1 }, - { key: 'legacy-url-alias', doc_count: 1 }, // alias (1) ], }, }, { - doc_count: 1, + doc_count: 2, key: 'other_space', countByType: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, - buckets: [{ key: 'legacy-url-alias', doc_count: 1 }], // alias (3) + buckets: [{ key: 'legacy-url-alias', doc_count: 2 }], // aliases (3) }, }, ]; @@ -110,8 +110,8 @@ export function deleteTestSuiteFactory(es: Client, esArchiver: any, supertest: S body: { query: { terms: { type: ['sharedtype'] } } }, }); const docs = multiNamespaceResponse.hits.hits; - // Just 12 results, since spaces_2_only, conflict_1_space_2 and conflict_2_space_2 got deleted. - expect(docs).length(12); + // Just 14 results, since spaces_2_only, conflict_1_space_2 and conflict_2_space_2 got deleted. + expect(docs).length(14); docs.forEach((doc) => () => { const containsSpace2 = doc?._source?.namespaces.includes('space_2'); expect(containsSpace2).to.eql(false); diff --git a/x-pack/test/spaces_api_integration/common/suites/update_objects_spaces.ts b/x-pack/test/spaces_api_integration/common/suites/update_objects_spaces.ts index ecd0d15b522e17..3b795ae719db8d 100644 --- a/x-pack/test/spaces_api_integration/common/suites/update_objects_spaces.ts +++ b/x-pack/test/spaces_api_integration/common/suites/update_objects_spaces.ts @@ -6,6 +6,8 @@ */ import expect from '@kbn/expect'; +import type { Client } from '@elastic/elasticsearch'; +import type { SearchTotalHits } from '@elastic/elasticsearch/lib/api/types'; import { without, uniq } from 'lodash'; import { SuperTest } from 'supertest'; import { @@ -35,6 +37,7 @@ export interface UpdateObjectsSpacesTestCase { objects: Array<{ id: string; existingNamespaces: string[]; + expectAliasDifference?: number; failure?: 400 | 404; }>; spacesToAdd: string[]; @@ -54,7 +57,11 @@ const getTestTitle = ({ objects, spacesToAdd, spacesToRemove }: UpdateObjectsSpa return `{objects: [${objStr}], spacesToAdd: [${addStr}], spacesToRemove: [${remStr}]}`; }; -export function updateObjectsSpacesTestSuiteFactory(esArchiver: any, supertest: SuperTest) { +export function updateObjectsSpacesTestSuiteFactory( + es: Client, + esArchiver: any, + supertest: SuperTest +) { const expectForbidden = expectResponses.forbiddenTypes('share_to_space'); const expectResponseBody = ( @@ -68,7 +75,10 @@ export function updateObjectsSpacesTestSuiteFactory(esArchiver: any, supertest: } else { const { objects, spacesToAdd, spacesToRemove } = testCase; const apiResponse = response.body as SavedObjectsUpdateObjectsSpacesResponse; - objects.forEach(({ id, existingNamespaces, failure }, i) => { + + let hasRefreshed = false; + for (let i = 0; i < objects.length; i++) { + const { id, existingNamespaces, expectAliasDifference, failure } = objects[i]; const object = apiResponse.objects[i]; if (failure === 404) { const error = SavedObjectsErrorHelpers.createGenericNotFoundError(TYPE, id); @@ -84,8 +94,28 @@ export function updateObjectsSpacesTestSuiteFactory(esArchiver: any, supertest: expect(result.type).to.eql(TYPE); expect(result.id).to.eql(id); expect(result.spaces.sort()).to.eql(expectedSpaces.sort()); + + if (expectAliasDifference !== undefined) { + // if we deleted an object that had an alias pointing to it, the alias should have been deleted as well + if (!hasRefreshed) { + await es.indices.refresh({ index: '.kibana' }); // alias deletion uses refresh: false, so we need to manually refresh the index before searching + hasRefreshed = true; + } + const searchResponse = await es.search({ + index: '.kibana', + body: { + size: 0, + query: { terms: { type: ['legacy-url-alias'] } }, + track_total_hits: true, + }, + }); + expect((searchResponse.hits.total as SearchTotalHits).value).to.eql( + // Six aliases exist in the test fixtures + 6 + expectAliasDifference + ); + } } - }); + } } }; const createTestDefinitions = ( diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/update_objects_spaces.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/update_objects_spaces.ts index 36f50aa165e72d..c6a97337e6ad93 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/update_objects_spaces.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/update_objects_spaces.ts @@ -28,6 +28,8 @@ const { fail404 } = testCaseFailures; const createTestCases = (spaceId: string): UpdateObjectsSpacesTestCase[] => { const eachSpace = [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID]; + // Note: we intentionally exclude ALIAS_DELETION test cases because they are already covered in spaces_only test suite, and there is no + // authZ-specific logic that affects alias deletion, all of that happens at the Saved Objects Repository level. return [ // Test case to check adding and removing all spaces ("*") to a saved object { @@ -125,8 +127,10 @@ const calculateSingleSpaceAuthZ = (testCases: UpdateObjectsSpacesTestCase[], spa export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); + const es = getService('es'); const { addTests, createTestDefinitions } = updateObjectsSpacesTestSuiteFactory( + es, esArchiver, supertest ); diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/update_objects_spaces.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/update_objects_spaces.ts index 865d5eca22cbd8..fc95f513f55195 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/update_objects_spaces.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/update_objects_spaces.ts @@ -11,6 +11,7 @@ import { getTestScenarios, } from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../../common/lib/saved_object_test_cases'; +import type { UpdateObjectsSpacesTestCase } from '../../common/suites/update_objects_spaces'; import { updateObjectsSpacesTestSuiteFactory } from '../../common/suites/update_objects_spaces'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -51,7 +52,55 @@ const createSinglePartTestCases = (spaceId: string) => { const createMultiPartTestCases = () => { const nonExistentSpace = 'does_not_exist'; // space that doesn't exist const eachSpace = [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID]; - const group1 = [ + const group1: UpdateObjectsSpacesTestCase[] = [ + // These test cases ensure that aliases are deleted when objects are unshared. + // For simplicity these are done separately, before the others. + { + objects: [ + { + id: CASES.ALIAS_DELETE_INCLUSIVE.id, + existingNamespaces: eachSpace, + expectAliasDifference: -1, // one alias should have been deleted from space_2 + }, + ], + spacesToAdd: [], + spacesToRemove: [SPACE_2_ID], + }, + { + objects: [ + { + id: CASES.ALIAS_DELETE_INCLUSIVE.id, + existingNamespaces: [DEFAULT_SPACE_ID, SPACE_1_ID], + expectAliasDifference: -2, // one alias should have been deleted from space_1 + }, + ], + spacesToAdd: [], + spacesToRemove: [SPACE_1_ID], + }, + { + objects: [ + { + id: CASES.ALIAS_DELETE_INCLUSIVE.id, + existingNamespaces: [DEFAULT_SPACE_ID], + expectAliasDifference: -2, // no aliases can exist in the default space, so no aliases were deleted + }, + ], + spacesToAdd: [], + spacesToRemove: [DEFAULT_SPACE_ID], + }, + { + objects: [ + { + id: CASES.ALIAS_DELETE_EXCLUSIVE.id, + existingNamespaces: [SPACE_1_ID], + expectAliasDifference: -3, // one alias should have been deleted from other_space + }, + ], + spacesToAdd: [SPACE_1_ID], + spacesToRemove: ['*'], + }, + ]; + const group2 = [ // first, add this object to each space and remove it from nonExistentSpace // this will succeed even though the object already exists in the default space and it doesn't exist in nonExistentSpace { objects: [CASES.DEFAULT_ONLY], spacesToAdd: eachSpace, spacesToRemove: [nonExistentSpace] }, @@ -87,7 +136,7 @@ const createMultiPartTestCases = () => { spacesToRemove: [SPACE_1_ID], }, ]; - const group2 = [ + const group3 = [ // first, add this object to space_2 and remove it from space_1 { objects: [CASES.DEFAULT_AND_SPACE_1], @@ -111,15 +160,17 @@ const createMultiPartTestCases = () => { spacesToRemove: [], }, ]; - return [...group1, ...group2]; + return [...group1, ...group2, ...group3]; }; // eslint-disable-next-line import/no-default-export export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); + const es = getService('es'); const { addTests, createTestDefinitions } = updateObjectsSpacesTestSuiteFactory( + es, esArchiver, supertest );