From 66c2db2428a95350f80991c4033b1324682f0a00 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 9 May 2023 10:25:31 +0200 Subject: [PATCH 01/34] start extracting all the things --- .../src/lib/apis/create.ts | 19 + .../src/lib/helpers/common.ts | 75 ++++ .../src/lib/helpers/encryption.ts | 90 ++++ .../src/lib/helpers/index.ts | 16 + .../src/lib/helpers/preflight_check.ts | 147 ++++++ .../src/lib/helpers/validation.ts | 118 +++++ .../src/lib/preflight_check_for_create.ts | 6 +- .../src/lib/repository.ts | 421 ++++-------------- .../src/lib/utils/es_responses.ts | 21 + .../src/lib/utils/index.ts | 10 + .../src/lib/utils/namespaces.ts | 28 ++ 11 files changed, 611 insertions(+), 340 deletions(-) create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/create.ts create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/common.ts create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/encryption.ts create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/index.ts create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/preflight_check.ts create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/validation.ts create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/es_responses.ts create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/index.ts create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/namespaces.ts diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/create.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/create.ts new file mode 100644 index 000000000000..93d6fe010be4 --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/create.ts @@ -0,0 +1,19 @@ +/* + * 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 SavedObject, +} from '@kbn/core-saved-objects-server'; +import { SavedObjectsCreateOptions } from '@kbn/core-saved-objects-api-server'; + + +export interface PerformCreateParams { + type: strin +} + +export const performCreate = ({}: PerformCreateParams) diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/common.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/common.ts new file mode 100644 index 000000000000..eff0bf3af2b0 --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/common.ts @@ -0,0 +1,75 @@ +/* + * 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 { + ISavedObjectTypeRegistry, + ISavedObjectsSpacesExtension, +} from '@kbn/core-saved-objects-server'; +import { getIndexForType } from '@kbn/core-saved-objects-base-server-internal'; +import { normalizeNamespace } from '../internal_utils'; + +export class CommonHelper { + private registry: ISavedObjectTypeRegistry; + private spaceExtension?: ISavedObjectsSpacesExtension; + private defaultIndex: string; + private kibanaVersion: string; + + constructor({ + registry, + spaceExtension, + kibanaVersion, + defaultIndex, + }: { + registry: ISavedObjectTypeRegistry; + spaceExtension?: ISavedObjectsSpacesExtension; + defaultIndex: string; + kibanaVersion: string; + }) { + this.registry = registry; + this.spaceExtension = spaceExtension; + this.kibanaVersion = kibanaVersion; + this.defaultIndex = defaultIndex; + } + + /** + * Returns index specified by the given type or the default index + * + * @param type - the type + */ + public getIndexForType(type: string) { + return getIndexForType({ + type, + defaultIndex: this.defaultIndex, + typeRegistry: this.registry, + kibanaVersion: this.kibanaVersion, + }); + } + + /** + * Returns an array of indices as specified in `this._registry` for each of the + * given `types`. If any of the types don't have an associated index, the + * default index `this._index` will be included. + * + * @param types The types whose indices should be retrieved + */ + public getIndicesForTypes(types: string[]) { + return unique(types.map((t) => this.getIndexForType(t))); + } + + /** + * {@inheritDoc ISavedObjectsRepository.getCurrentNamespace} + */ + public getCurrentNamespace(namespace?: string) { + if (this.spaceExtension) { + return this.spaceExtension.getCurrentNamespace(namespace); + } + return normalizeNamespace(namespace); + } +} + +const unique = (array: string[]) => [...new Set(array)]; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/encryption.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/encryption.ts new file mode 100644 index 000000000000..3c80dc97e6f4 --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/encryption.ts @@ -0,0 +1,90 @@ +/* + * 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 { SavedObject } from '@kbn/core-saved-objects-common/src/server_types'; +import type { + AuthorizationTypeMap, + ISavedObjectsSecurityExtension, + ISavedObjectsEncryptionExtension, +} from '@kbn/core-saved-objects-server'; + +export class EncryptionHelper { + private securityExtension?: ISavedObjectsSecurityExtension; + private encryptionExtension?: ISavedObjectsEncryptionExtension; + + constructor({ + securityExtension, + encryptionExtension, + }: { + securityExtension?: ISavedObjectsSecurityExtension; + encryptionExtension?: ISavedObjectsEncryptionExtension; + }) { + this.securityExtension = securityExtension; + this.encryptionExtension = encryptionExtension; + } + + async optionallyEncryptAttributes( + type: string, + id: string, + namespaceOrNamespaces: string | string[] | undefined, + attributes: T + ): Promise { + if (!this.encryptionExtension?.isEncryptableType(type)) { + return attributes; + } + const namespace = Array.isArray(namespaceOrNamespaces) + ? namespaceOrNamespaces[0] + : namespaceOrNamespaces; + const descriptor = { type, id, namespace }; + return this.encryptionExtension.encryptAttributes( + descriptor, + attributes as Record + ) as unknown as T; + } + + async optionallyDecryptAndRedactSingleResult( + object: SavedObject, + typeMap: AuthorizationTypeMap | undefined, + originalAttributes?: T + ) { + if (this.encryptionExtension?.isEncryptableType(object.type)) { + object = await this.encryptionExtension.decryptOrStripResponseAttributes( + object, + originalAttributes + ); + } + if (typeMap) { + return this.securityExtension!.redactNamespaces({ typeMap, savedObject: object }); + } + return object; + } + + async optionallyDecryptAndRedactBulkResult< + T, + R extends { saved_objects: Array> }, + A extends string, + O extends Array<{ attributes: T }> + >(response: R, typeMap: AuthorizationTypeMap | undefined, originalObjects?: O) { + const modifiedObjects = await Promise.all( + response.saved_objects.map(async (object, index) => { + if (object.error) { + // If the bulk operation failed, the object will not have an attributes field at all, it will have an error field instead. + // In this case, don't attempt to decrypt, just return the object. + return object; + } + const originalAttributes = originalObjects?.[index].attributes; + return await this.optionallyDecryptAndRedactSingleResult( + object, + typeMap, + originalAttributes + ); + }) + ); + return { ...response, saved_objects: modifiedObjects }; + } +} diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/index.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/index.ts new file mode 100644 index 000000000000..bca048a9db17 --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { CommonHelper } from './common'; +export { EncryptionHelper } from './encryption'; +export { ValidationHelper } from './validation'; +export { + PreflightCheckHelper, + type PreflightCheckNamespacesParams, + type PreflightCheckNamespacesResult, +} from './preflight_check'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/preflight_check.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/preflight_check.ts new file mode 100644 index 000000000000..1d35a291ed60 --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/preflight_check.ts @@ -0,0 +1,147 @@ +/* + * 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 { isNotFoundFromUnsupportedServer } from '@kbn/core-elasticsearch-server-internal'; +import type { + ISavedObjectTypeRegistry, + ISavedObjectsSerializer, +} from '@kbn/core-saved-objects-server'; +import { ISavedObjectsRepository } from '@kbn/core-saved-objects-api-server'; +import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server'; +import { SavedObjectsErrorHelpers, SavedObjectsRawDocSource } from '@kbn/core-saved-objects-server'; +import { rawDocExistsInNamespaces } from '../internal_utils'; +import { getSavedObjectNamespaces } from '../utils/namespaces'; +import { GetResponseFound, isFoundGetResponse } from '../utils/es_responses'; +import { preflightCheckForCreate } from '../preflight_check_for_create'; +import type { RepositoryEsClient } from '../repository_es_client'; + +export class PreflightCheckHelper { + private registry: ISavedObjectTypeRegistry; + private serializer: ISavedObjectsSerializer; + private client: RepositoryEsClient; + private getIndexForType: (type: string) => string; + private createPointInTimeFinder: ISavedObjectsRepository['createPointInTimeFinder']; + + constructor({ + registry, + serializer, + client, + getIndexForType, + createPointInTimeFinder, + }: { + registry: ISavedObjectTypeRegistry; + serializer: ISavedObjectsSerializer; + client: RepositoryEsClient; + getIndexForType: (type: string) => string; + createPointInTimeFinder: ISavedObjectsRepository['createPointInTimeFinder']; + }) { + this.registry = registry; + this.serializer = serializer; + this.client = client; + this.getIndexForType = getIndexForType; + this.createPointInTimeFinder = createPointInTimeFinder; + } + + /** + * Pre-flight check to ensure that a multi-namespace object exists in the current namespace. + */ + public async preflightCheckNamespaces({ + type, + id, + namespace, + initialNamespaces, + }: PreflightCheckNamespacesParams): Promise { + if (!this.registry.isMultiNamespace(type)) { + throw new Error(`Cannot make preflight get request for non-multi-namespace type '${type}'.`); + } + + const { body, statusCode, headers } = await this.client.get( + { + id: this.serializer.generateRawId(undefined, type, id), + index: this.getIndexForType(type), + }, + { + ignore: [404], + meta: true, + } + ); + + const namespaces = initialNamespaces ?? [SavedObjectsUtils.namespaceIdToString(namespace)]; + + const indexFound = statusCode !== 404; + if (indexFound && isFoundGetResponse(body)) { + if (!rawDocExistsInNamespaces(this.registry, body, namespaces)) { + return { checkResult: 'found_outside_namespace' }; + } + return { + checkResult: 'found_in_namespace', + savedObjectNamespaces: initialNamespaces ?? getSavedObjectNamespaces(namespace, body), + rawDocSource: body, + }; + } else if (isNotFoundFromUnsupportedServer({ statusCode, headers })) { + // checking if the 404 is from Elasticsearch + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + return { + checkResult: 'not_found', + savedObjectNamespaces: initialNamespaces ?? getSavedObjectNamespaces(namespace), + }; + } + + /** + * Pre-flight check to ensure that an upsert which would create a new object does not result in an alias conflict. + */ + public async preflightCheckForUpsertAliasConflict( + type: string, + id: string, + namespace: string | undefined + ) { + const namespaceString = SavedObjectsUtils.namespaceIdToString(namespace); + const [{ error }] = await preflightCheckForCreate({ + registry: this.registry, + client: this.client, + serializer: this.serializer, + getIndexForType: this.getIndexForType.bind(this), + createPointInTimeFinder: this.createPointInTimeFinder.bind(this), + objects: [{ type, id, namespaces: [namespaceString] }], + }); + if (error?.type === 'aliasConflict') { + throw SavedObjectsErrorHelpers.createConflictError(type, id); + } + // any other error from this check does not matter + } +} + +/** + * @internal + */ +export interface PreflightCheckNamespacesParams { + /** The object type to fetch */ + type: string; + /** The object ID to fetch */ + id: string; + /** The current space */ + namespace: string | undefined; + /** Optional; for an object that is being created, this specifies the initial namespace(s) it will exist in (overriding the current space) */ + initialNamespaces?: string[]; +} + +/** + * @internal + */ +export interface PreflightCheckNamespacesResult { + /** If the object exists, and whether or not it exists in the current space */ + checkResult: 'not_found' | 'found_in_namespace' | 'found_outside_namespace'; + /** + * What namespace(s) the object should exist in, if it needs to be created; practically speaking, this will never be undefined if + * checkResult == not_found or checkResult == found_in_namespace + */ + savedObjectNamespaces?: string[]; + /** The source of the raw document, if the object already exists */ + rawDocSource?: GetResponseFound; +} diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/validation.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/validation.ts new file mode 100644 index 000000000000..6ffb80e71a9c --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/validation.ts @@ -0,0 +1,118 @@ +/* + * 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 { Logger } from '@kbn/logging'; +import type { ISavedObjectTypeRegistry } from '@kbn/core-saved-objects-server'; +import { SavedObjectsTypeValidator } from '@kbn/core-saved-objects-base-server-internal'; +import { SavedObjectSanitizedDoc, SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; +import { ALL_NAMESPACES_STRING } from '@kbn/core-saved-objects-utils-server'; + +export class ValidationHelper { + private registry: ISavedObjectTypeRegistry; + private logger: Logger; + private kibanaVersion: string; + private typeValidatorMap: Record = {}; + + constructor({ + registry, + logger, + kibanaVersion, + }: { + registry: ISavedObjectTypeRegistry; + logger: Logger; + kibanaVersion: string; + }) { + this.registry = registry; + this.logger = logger; + this.kibanaVersion = kibanaVersion; + } + + /** The `initialNamespaces` field (create, bulkCreate) is used to create an object in an initial set of spaces. */ + public validateInitialNamespaces(type: string, initialNamespaces: string[] | undefined) { + if (!initialNamespaces) { + return; + } + + if (this.registry.isNamespaceAgnostic(type)) { + throw SavedObjectsErrorHelpers.createBadRequestError( + '"initialNamespaces" cannot be used on space-agnostic types' + ); + } else if (!initialNamespaces.length) { + throw SavedObjectsErrorHelpers.createBadRequestError( + '"initialNamespaces" must be a non-empty array of strings' + ); + } else if ( + !this.registry.isShareable(type) && + (initialNamespaces.length > 1 || initialNamespaces.includes(ALL_NAMESPACES_STRING)) + ) { + throw SavedObjectsErrorHelpers.createBadRequestError( + '"initialNamespaces" can only specify a single space when used with space-isolated types' + ); + } + } + + /** The object-specific `namespaces` field (bulkGet) is used to check if an object exists in any of a given number of spaces. */ + public validateObjectNamespaces(type: string, id: string, namespaces: string[] | undefined) { + if (!namespaces) { + return; + } + + if (this.registry.isNamespaceAgnostic(type)) { + throw SavedObjectsErrorHelpers.createBadRequestError( + '"namespaces" cannot be used on space-agnostic types' + ); + } else if (!namespaces.length) { + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } else if ( + !this.registry.isShareable(type) && + (namespaces.length > 1 || namespaces.includes(ALL_NAMESPACES_STRING)) + ) { + throw SavedObjectsErrorHelpers.createBadRequestError( + '"namespaces" can only specify a single space when used with space-isolated types' + ); + } + } + + /** Validate a migrated doc against the registered saved object type's schema. */ + public validateObjectForCreate(type: string, doc: SavedObjectSanitizedDoc) { + if (!this.registry.getType(type)) { + return; + } + const validator = this.getTypeValidator(type); + try { + validator.validate(doc, this.kibanaVersion); + } catch (error) { + throw SavedObjectsErrorHelpers.createBadRequestError(error.message); + } + } + + private getTypeValidator(type: string): SavedObjectsTypeValidator { + if (!this.typeValidatorMap[type]) { + const savedObjectType = this.registry.getType(type); + this.typeValidatorMap[type] = new SavedObjectsTypeValidator({ + logger: this.logger.get('type-validator'), + type, + validationMap: savedObjectType!.schemas ?? {}, + defaultVersion: this.kibanaVersion, + }); + } + return this.typeValidatorMap[type]!; + } + + /** This is used when objects are created. */ + public validateOriginId(type: string, objectOrOptions: { originId?: string }) { + if ( + Object.keys(objectOrOptions).includes('originId') && + !this.registry.isMultiNamespace(type) + ) { + throw SavedObjectsErrorHelpers.createBadRequestError( + '"originId" can only be set for multi-namespace object types' + ); + } + } +} diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/preflight_check_for_create.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/preflight_check_for_create.ts index ae09b0e1e422..c3faa7d71a96 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/preflight_check_for_create.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/preflight_check_for_create.ts @@ -10,6 +10,7 @@ import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { isNotFoundFromUnsupportedServer } from '@kbn/core-elasticsearch-server-internal'; import { type ISavedObjectTypeRegistry, + type ISavedObjectsSerializer, type SavedObjectsRawDoc, type SavedObjectsRawDocSource, SavedObjectsErrorHelpers, @@ -19,7 +20,6 @@ import { LEGACY_URL_ALIAS_TYPE, getObjectKey, type LegacyUrlAlias, - type SavedObjectsSerializer, } from '@kbn/core-saved-objects-base-server-internal'; import { findLegacyUrlAliases } from './legacy_url_aliases'; import { type Either, rawDocExistsInNamespaces } from './internal_utils'; @@ -56,7 +56,7 @@ export interface PreflightCheckForCreateObject { export interface PreflightCheckForCreateParams { registry: ISavedObjectTypeRegistry; client: RepositoryEsClient; - serializer: SavedObjectsSerializer; + serializer: ISavedObjectsSerializer; getIndexForType: (type: string) => string; createPointInTimeFinder: CreatePointInTimeFinderFn; objects: PreflightCheckForCreateObject[]; @@ -250,7 +250,7 @@ async function optionallyFindAliases( async function bulkGetObjectsAndAliases( client: RepositoryEsClient, - serializer: SavedObjectsSerializer, + serializer: ISavedObjectsSerializer, getIndexForType: (type: string) => string, objectsAndAliasesToBulkGet: Array ) { diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts index 3a0ff953dbff..a655e7552e91 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts @@ -75,7 +75,6 @@ import { type ISavedObjectsSecurityExtension, type ISavedObjectsSpacesExtension, type CheckAuthorizationResult, - type AuthorizationTypeMap, AuthorizeCreateObject, AuthorizeUpdateObject, type AuthorizeBulkGetObject, @@ -91,13 +90,11 @@ import { } from '@kbn/core-saved-objects-utils-server'; import { SavedObjectsSerializer, - SavedObjectsTypeValidator, decodeRequestVersion, encodeVersion, encodeHitVersion, getRootPropertiesObjects, LEGACY_URL_ALIAS_TYPE, - getIndexForType, type IndexMapping, type IKibanaMigrator, } from '@kbn/core-saved-objects-base-server-internal'; @@ -140,6 +137,14 @@ import type { ExpectedBulkDeleteMultiNamespaceDocsParams, ObjectToDeleteAliasesFor, } from './repository_bulk_delete_internal_types'; +import { + CommonHelper, + EncryptionHelper, + ValidationHelper, + PreflightCheckHelper, + type PreflightCheckNamespacesResult, +} from './helpers'; +import { isFoundGetResponse, getSavedObjectNamespaces } from './utils'; // 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. @@ -161,35 +166,6 @@ export const DEFAULT_RETRY_COUNT = 3; const MAX_CONCURRENT_ALIAS_DELETIONS = 10; -/** - * @internal - */ -interface PreflightCheckNamespacesParams { - /** The object type to fetch */ - type: string; - /** The object ID to fetch */ - id: string; - /** The current space */ - namespace: string | undefined; - /** Optional; for an object that is being created, this specifies the initial namespace(s) it will exist in (overriding the current space) */ - initialNamespaces?: string[]; -} - -/** - * @internal - */ -interface PreflightCheckNamespacesResult { - /** If the object exists, and whether or not it exists in the current space */ - checkResult: 'not_found' | 'found_in_namespace' | 'found_outside_namespace'; - /** - * What namespace(s) the object should exist in, if it needs to be created; practically speaking, this will never be undefined if - * checkResult == not_found or checkResult == found_in_namespace - */ - savedObjectNamespaces?: string[]; - /** The source of the raw document, if the object already exists */ - rawDocSource?: GetResponseFound; -} - function isMgetDoc(doc?: estypes.MgetResponseItem): doc is estypes.GetGetResult { return Boolean(doc && 'found' in doc); } @@ -204,17 +180,19 @@ function isMgetDoc(doc?: estypes.MgetResponseItem): doc is estypes.GetG */ export class SavedObjectsRepository implements ISavedObjectsRepository { private _migrator: IKibanaMigrator; - private _index: string; private _mappings: IndexMapping; private _registry: ISavedObjectTypeRegistry; private _allowedTypes: string[]; - private typeValidatorMap: Record = {}; private readonly client: RepositoryEsClient; private readonly _encryptionExtension?: ISavedObjectsEncryptionExtension; private readonly _securityExtension?: ISavedObjectsSecurityExtension; private readonly _spacesExtension?: ISavedObjectsSpacesExtension; private _serializer: SavedObjectsSerializer; private _logger: Logger; + private commonHelper: CommonHelper; + private encryptionHelper: EncryptionHelper; + private validationHelper: ValidationHelper; + private preflightCheckHelper: PreflightCheckHelper; /** * A factory function for creating SavedObjectRepository instances. @@ -275,27 +253,42 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { extensions, } = options; - // It's important that we migrate documents / mark them as up-to-date - // prior to writing them to the index. Otherwise, we'll cause unnecessary - // index migrations to run at Kibana startup, and those will probably fail - // due to invalidly versioned documents in the index. - // - // The migrator performs double-duty, and validates the documents prior - // to returning them. + if (allowedTypes.length === 0) { + throw new Error('Empty or missing types for saved object repository!'); + } + this._migrator = migrator; - this._index = index; this._mappings = mappings; this._registry = typeRegistry; this.client = createRepositoryEsClient(client); - if (allowedTypes.length === 0) { - throw new Error('Empty or missing types for saved object repository!'); - } this._allowedTypes = allowedTypes; this._serializer = serializer; this._logger = logger; this._encryptionExtension = extensions?.encryptionExtension; this._securityExtension = extensions?.securityExtension; this._spacesExtension = extensions?.spacesExtension; + this.commonHelper = new CommonHelper({ + spaceExtension: extensions?.spacesExtension, + defaultIndex: index, + kibanaVersion: migrator.kibanaVersion, + registry: typeRegistry, + }); + this.encryptionHelper = new EncryptionHelper({ + encryptionExtension: extensions?.encryptionExtension, + securityExtension: extensions?.securityExtension, + }); + this.validationHelper = new ValidationHelper({ + registry: typeRegistry, + logger, + kibanaVersion: migrator.kibanaVersion, + }); + this.preflightCheckHelper = new PreflightCheckHelper({ + getIndexForType: this.commonHelper.getIndexForType.bind(this.commonHelper), + createPointInTimeFinder: this.createPointInTimeFinder.bind(this), + serializer, + registry: typeRegistry, + client: this.client, + }); } /** @@ -323,8 +316,8 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { throw SavedObjectsErrorHelpers.createUnsupportedTypeError(type); } const id = this.getValidId(type, options.id, options.version, options.overwrite); - this.validateInitialNamespaces(type, initialNamespaces); - this.validateOriginId(type, options); + this.validationHelper.validateInitialNamespaces(type, initialNamespaces); + this.validationHelper.validateOriginId(type, options); const time = getCurrentTime(); let savedObjectNamespace: string | undefined; @@ -383,7 +376,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { ...(savedObjectNamespace && { namespace: savedObjectNamespace }), ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), originId, - attributes: await this.optionallyEncryptAttributes( + attributes: await this.encryptionHelper.optionallyEncryptAttributes( type, id, savedObjectNamespace, // if single namespace type, this is the first in initialNamespaces. If multi-namespace type this is options.namespace/current namespace. @@ -404,7 +397,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { * migration to fail, but it's the best we can do without devising a way to run validations * inside the migration algorithm itself. */ - this.validateObjectForCreate(type, migrated as SavedObjectSanitizedDoc); + this.validationHelper.validateObjectForCreate(type, migrated as SavedObjectSanitizedDoc); const raw = this._serializer.savedObjectToRaw(migrated as SavedObjectSanitizedDoc); @@ -427,7 +420,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(id, type); } - return this.optionallyDecryptAndRedactSingleResult( + return this.encryptionHelper.optionallyDecryptAndRedactSingleResult( this._rawToSavedObject({ ...raw, ...body }, { migrationVersionCompatibility }), authorizationResult?.typeMap, attributes @@ -469,8 +462,8 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { } else { try { id = this.getValidId(type, requestId, version, overwrite); - this.validateInitialNamespaces(type, initialNamespaces); - this.validateOriginId(type, object); + this.validationHelper.validateInitialNamespaces(type, initialNamespaces); + this.validationHelper.validateOriginId(type, object); } catch (e) { error = e; } @@ -605,7 +598,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { const migrated = this._migrator.migrateDocument({ id: object.id, type: object.type, - attributes: await this.optionallyEncryptAttributes( + attributes: await this.encryptionHelper.optionallyEncryptAttributes( object.type, object.id, savedObjectNamespace, // only used for multi-namespace object types @@ -630,7 +623,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { * inside the migration algorithm itself. */ try { - this.validateObjectForCreate(object.type, migrated); + this.validationHelper.validateObjectForCreate(object.type, migrated); } catch (error) { return { tag: 'Left', @@ -697,7 +690,11 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { ); }), }; - return this.optionallyDecryptAndRedactBulkResult(result, authorizationResult?.typeMap, objects); + return this.encryptionHelper.optionallyDecryptAndRedactBulkResult( + result, + authorizationResult?.typeMap, + objects + ); } /** @@ -825,7 +822,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { if (this._registry.isMultiNamespace(type)) { // note: this check throws an error if the object is found but does not exist in this namespace - preflightResult = await this.preflightCheckNamespaces({ + preflightResult = await this.preflightCheckHelper.preflightCheckNamespaces({ type, id, namespace, @@ -1493,7 +1490,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { }), }); - return this.optionallyDecryptAndRedactBulkResult( + return this.encryptionHelper.optionallyDecryptAndRedactBulkResult( result, redactTypeMap ?? authorizationResult?.typeMap // If the redact type map is valid, use that one; otherwise, fall back to the authorization check ); @@ -1544,7 +1541,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { error = SavedObjectsErrorHelpers.createUnsupportedTypeError(type); } else { try { - this.validateObjectNamespaces(type, id, object.namespaces); + this.validationHelper.validateObjectNamespaces(type, id, object.namespaces); } catch (e) { error = e; } @@ -1664,7 +1661,10 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { objects: authObjects, }); - return this.optionallyDecryptAndRedactBulkResult(result, authorizationResult?.typeMap); + return this.encryptionHelper.optionallyDecryptAndRedactBulkResult( + result, + authorizationResult?.typeMap + ); } /** @@ -1751,7 +1751,10 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { migrationVersionCompatibility, }); - return this.optionallyDecryptAndRedactSingleResult(result, authorizationResult?.typeMap); + return this.encryptionHelper.optionallyDecryptAndRedactSingleResult( + result, + authorizationResult?.typeMap + ); } /** @@ -1810,7 +1813,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { let preflightResult: PreflightCheckNamespacesResult | undefined; if (this._registry.isMultiNamespace(type)) { - preflightResult = await this.preflightCheckNamespaces({ + preflightResult = await this.preflightCheckHelper.preflightCheckNamespaces({ type, id, namespace, @@ -1834,7 +1837,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { // If an upsert would result in the creation of a new object, we need to check for alias conflicts too. // This takes an extra round trip to Elasticsearch, but this won't happen often. // TODO: improve performance by combining these into a single preflight check - await this.preflightCheckForUpsertAliasConflict(type, id, namespace); + await this.preflightCheckHelper.preflightCheckForUpsertAliasConflict(type, id, namespace); } const time = getCurrentTime(); @@ -1856,7 +1859,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { ...(savedObjectNamespace && { namespace: savedObjectNamespace }), ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), attributes: { - ...(await this.optionallyEncryptAttributes(type, id, namespace, upsert)), + ...(await this.encryptionHelper.optionallyEncryptAttributes(type, id, namespace, upsert)), }, updated_at: time, }); @@ -1864,7 +1867,12 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { } const doc = { - [type]: await this.optionallyEncryptAttributes(type, id, namespace, attributes), + [type]: await this.encryptionHelper.optionallyEncryptAttributes( + type, + id, + namespace, + attributes + ), updated_at: time, ...(Array.isArray(references) && { references }), }; @@ -1913,7 +1921,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { attributes, } as SavedObject; - return this.optionallyDecryptAndRedactSingleResult( + return this.encryptionHelper.optionallyDecryptAndRedactSingleResult( result, authorizationResult?.typeMap, attributes @@ -2169,7 +2177,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { { doc: { ...documentToSave, - [type]: await this.optionallyEncryptAttributes( + [type]: await this.encryptionHelper.optionallyEncryptAttributes( type, id, objectNamespace || namespace, @@ -2230,7 +2238,11 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { }), }; - return this.optionallyDecryptAndRedactBulkResult(result, authorizationResult?.typeMap, objects); + return this.encryptionHelper.optionallyDecryptAndRedactBulkResult( + result, + authorizationResult?.typeMap, + objects + ); } /** @@ -2385,7 +2397,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { savedObjectNamespace = namespace; } else if (this._registry.isMultiNamespace(type)) { // note: this check throws an error if the object is found but does not exist in this namespace - const preflightResult = await this.preflightCheckNamespaces({ + const preflightResult = await this.preflightCheckHelper.preflightCheckNamespaces({ type, id, namespace, @@ -2398,7 +2410,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { // If an upsert would result in the creation of a new object, we need to check for alias conflicts too. // This takes an extra round trip to Elasticsearch, but this won't happen often. // TODO: improve performance by combining these into a single preflight check - await this.preflightCheckForUpsertAliasConflict(type, id, namespace); + await this.preflightCheckHelper.preflightCheckForUpsertAliasConflict(type, id, namespace); } savedObjectNamespaces = preflightResult.savedObjectNamespaces; @@ -2590,35 +2602,15 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { * {@inheritDoc ISavedObjectsRepository.getCurrentNamespace} */ getCurrentNamespace(namespace?: string) { - if (this._spacesExtension) { - return this._spacesExtension.getCurrentNamespace(namespace); - } - return normalizeNamespace(namespace); + return this.commonHelper.getCurrentNamespace(namespace); } - /** - * Returns index specified by the given type or the default index - * - * @param type - the type - */ private getIndexForType(type: string) { - return getIndexForType({ - type, - defaultIndex: this._index, - typeRegistry: this._registry, - kibanaVersion: this._migrator.kibanaVersion, - }); + return this.commonHelper.getIndexForType(type); } - /** - * Returns an array of indices as specified in `this._registry` for each of the - * given `types`. If any of the types don't have an associated index, the - * default index `this._index` will be included. - * - * @param types The types whose indices should be retrieved - */ private getIndicesForTypes(types: string[]) { - return unique(types.map((t) => this.getIndexForType(t))); + return this.commonHelper.getIndicesForTypes(types); } private _rawToSavedObject( @@ -2642,159 +2634,6 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { return rawDocExistsInNamespace(this._registry, raw, namespace); } - /** - * Pre-flight check to ensure that a multi-namespace object exists in the current namespace. - */ - private async preflightCheckNamespaces({ - type, - id, - namespace, - initialNamespaces, - }: PreflightCheckNamespacesParams): Promise { - if (!this._registry.isMultiNamespace(type)) { - throw new Error(`Cannot make preflight get request for non-multi-namespace type '${type}'.`); - } - - const { body, statusCode, headers } = await this.client.get( - { - id: this._serializer.generateRawId(undefined, type, id), - index: this.getIndexForType(type), - }, - { - ignore: [404], - meta: true, - } - ); - - const namespaces = initialNamespaces ?? [SavedObjectsUtils.namespaceIdToString(namespace)]; - - const indexFound = statusCode !== 404; - if (indexFound && isFoundGetResponse(body)) { - if (!this.rawDocExistsInNamespaces(body, namespaces)) { - return { checkResult: 'found_outside_namespace' }; - } - return { - checkResult: 'found_in_namespace', - savedObjectNamespaces: initialNamespaces ?? getSavedObjectNamespaces(namespace, body), - rawDocSource: body, - }; - } else if (isNotFoundFromUnsupportedServer({ statusCode, headers })) { - // checking if the 404 is from Elasticsearch - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - return { - checkResult: 'not_found', - savedObjectNamespaces: initialNamespaces ?? getSavedObjectNamespaces(namespace), - }; - } - - /** - * Pre-flight check to ensure that an upsert which would create a new object does not result in an alias conflict. - */ - private async preflightCheckForUpsertAliasConflict( - type: string, - id: string, - namespace: string | undefined - ) { - const namespaceString = SavedObjectsUtils.namespaceIdToString(namespace); - const [{ error }] = await preflightCheckForCreate({ - registry: this._registry, - client: this.client, - serializer: this._serializer, - getIndexForType: this.getIndexForType.bind(this), - createPointInTimeFinder: this.createPointInTimeFinder.bind(this), - objects: [{ type, id, namespaces: [namespaceString] }], - }); - if (error?.type === 'aliasConflict') { - throw SavedObjectsErrorHelpers.createConflictError(type, id); - } - // any other error from this check does not matter - } - - /** The `initialNamespaces` field (create, bulkCreate) is used to create an object in an initial set of spaces. */ - private validateInitialNamespaces(type: string, initialNamespaces: string[] | undefined) { - if (!initialNamespaces) { - return; - } - - if (this._registry.isNamespaceAgnostic(type)) { - throw SavedObjectsErrorHelpers.createBadRequestError( - '"initialNamespaces" cannot be used on space-agnostic types' - ); - } else if (!initialNamespaces.length) { - throw SavedObjectsErrorHelpers.createBadRequestError( - '"initialNamespaces" must be a non-empty array of strings' - ); - } else if ( - !this._registry.isShareable(type) && - (initialNamespaces.length > 1 || initialNamespaces.includes(ALL_NAMESPACES_STRING)) - ) { - throw SavedObjectsErrorHelpers.createBadRequestError( - '"initialNamespaces" can only specify a single space when used with space-isolated types' - ); - } - } - - /** The object-specific `namespaces` field (bulkGet) is used to check if an object exists in any of a given number of spaces. */ - private validateObjectNamespaces(type: string, id: string, namespaces: string[] | undefined) { - if (!namespaces) { - return; - } - - if (this._registry.isNamespaceAgnostic(type)) { - throw SavedObjectsErrorHelpers.createBadRequestError( - '"namespaces" cannot be used on space-agnostic types' - ); - } else if (!namespaces.length) { - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } else if ( - !this._registry.isShareable(type) && - (namespaces.length > 1 || namespaces.includes(ALL_NAMESPACES_STRING)) - ) { - throw SavedObjectsErrorHelpers.createBadRequestError( - '"namespaces" can only specify a single space when used with space-isolated types' - ); - } - } - - /** Validate a migrated doc against the registered saved object type's schema. */ - private validateObjectForCreate(type: string, doc: SavedObjectSanitizedDoc) { - if (!this._registry.getType(type)) { - return; - } - const validator = this.getTypeValidator(type); - try { - validator.validate(doc, this._migrator.kibanaVersion); - } catch (error) { - throw SavedObjectsErrorHelpers.createBadRequestError(error.message); - } - } - - private getTypeValidator(type: string): SavedObjectsTypeValidator { - if (!this.typeValidatorMap[type]) { - const savedObjectType = this._registry.getType(type); - this.typeValidatorMap[type] = new SavedObjectsTypeValidator({ - logger: this._logger.get('type-validator'), - type, - validationMap: savedObjectType!.schemas ?? {}, - defaultVersion: this._migrator.kibanaVersion, - }); - } - return this.typeValidatorMap[type]!; - } - - /** This is used when objects are created. */ - private validateOriginId(type: string, objectOrOptions: { originId?: string }) { - if ( - Object.keys(objectOrOptions).includes('originId') && - !this._registry.isMultiNamespace(type) - ) { - throw SavedObjectsErrorHelpers.createBadRequestError( - '"originId" can only be set for multi-namespace object types' - ); - } - } - /** * Saved objects with encrypted attributes should have IDs that are hard to guess, especially since IDs are part of the AAD used during * encryption, that's why we control them within this function and don't allow consumers to specify their own IDs directly for encryptable @@ -2823,101 +2662,9 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { } return id; } - - private async optionallyEncryptAttributes( - type: string, - id: string, - namespaceOrNamespaces: string | string[] | undefined, - attributes: T - ): Promise { - if (!this._encryptionExtension?.isEncryptableType(type)) { - return attributes; - } - const namespace = Array.isArray(namespaceOrNamespaces) - ? namespaceOrNamespaces[0] - : namespaceOrNamespaces; - const descriptor = { type, id, namespace }; - return this._encryptionExtension.encryptAttributes( - descriptor, - attributes as Record - ) as unknown as T; - } - - private async optionallyDecryptAndRedactSingleResult( - object: SavedObject, - typeMap: AuthorizationTypeMap | undefined, - originalAttributes?: T - ) { - if (this._encryptionExtension?.isEncryptableType(object.type)) { - object = await this._encryptionExtension.decryptOrStripResponseAttributes( - object, - originalAttributes - ); - } - if (typeMap) { - return this._securityExtension!.redactNamespaces({ typeMap, savedObject: object }); - } - return object; - } - - private async optionallyDecryptAndRedactBulkResult< - T, - R extends { saved_objects: Array> }, - A extends string, - O extends Array<{ attributes: T }> - >(response: R, typeMap: AuthorizationTypeMap | undefined, originalObjects?: O) { - const modifiedObjects = await Promise.all( - response.saved_objects.map(async (object, index) => { - if (object.error) { - // If the bulk operation failed, the object will not have an attributes field at all, it will have an error field instead. - // In this case, don't attempt to decrypt, just return the object. - return object; - } - const originalAttributes = originalObjects?.[index].attributes; - return await this.optionallyDecryptAndRedactSingleResult( - object, - typeMap, - originalAttributes - ); - }) - ); - return { ...response, saved_objects: modifiedObjects }; - } -} - -/** - * Returns a string array of namespaces for a given saved object. If the saved object is undefined, the result is an array that contains the - * current namespace. Value may be undefined if an existing saved object has no namespaces attribute; this should not happen in normal - * operations, but it is possible if the Elasticsearch document is manually modified. - * - * @param namespace The current namespace. - * @param document Optional existing saved object that was obtained in a preflight operation. - */ -function getSavedObjectNamespaces( - namespace?: string, - document?: SavedObjectsRawDoc -): string[] | undefined { - if (document) { - return document._source?.namespaces; - } - return [SavedObjectsUtils.namespaceIdToString(namespace)]; } /** * Extracts the contents of a decorated error to return the attributes for bulk operations. */ const errorContent = (error: DecoratedError) => error.output.payload; - -const unique = (array: string[]) => [...new Set(array)]; - -/** - * Type and type guard function for converting a possibly not existent doc to an existent doc. - */ -type GetResponseFound = estypes.GetResponse & - Required< - Pick, '_primary_term' | '_seq_no' | '_version' | '_source'> - >; - -const isFoundGetResponse = ( - doc: estypes.GetResponse -): doc is GetResponseFound => doc.found; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/es_responses.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/es_responses.ts new file mode 100644 index 000000000000..01f7dc3cd79a --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/es_responses.ts @@ -0,0 +1,21 @@ +/* + * 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 estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +/** + * Type and type guard function for converting a possibly not existent doc to an existent doc. + */ +export type GetResponseFound = estypes.GetResponse & + Required< + Pick, '_primary_term' | '_seq_no' | '_version' | '_source'> + >; + +export const isFoundGetResponse = ( + doc: estypes.GetResponse +): doc is GetResponseFound => doc.found; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/index.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/index.ts new file mode 100644 index 000000000000..b8ac751aafe0 --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/index.ts @@ -0,0 +1,10 @@ +/* + * 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 { getSavedObjectNamespaces } from './namespaces'; +export { isFoundGetResponse, type GetResponseFound } from './es_responses'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/namespaces.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/namespaces.ts new file mode 100644 index 000000000000..1592d00c0c42 --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/namespaces.ts @@ -0,0 +1,28 @@ +/* + * 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 { SavedObjectsRawDoc } from '@kbn/core-saved-objects-server'; +import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server'; + +/** + * Returns a string array of namespaces for a given saved object. If the saved object is undefined, the result is an array that contains the + * current namespace. Value may be undefined if an existing saved object has no namespaces attribute; this should not happen in normal + * operations, but it is possible if the Elasticsearch document is manually modified. + * + * @param namespace The current namespace. + * @param document Optional existing saved object that was obtained in a preflight operation. + */ +export function getSavedObjectNamespaces( + namespace?: string, + document?: SavedObjectsRawDoc +): string[] | undefined { + if (document) { + return document._source?.namespaces; + } + return [SavedObjectsUtils.namespaceIdToString(namespace)]; +} From 1baed7a55bc1d70cfe55584fcd4cd34270071fde Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 9 May 2023 11:10:08 +0200 Subject: [PATCH 02/34] extract 'create' --- .../src/lib/apis/create.ts | 177 +++++++++++++++- .../src/lib/apis/index.ts | 9 + .../src/lib/constants.ts | 10 + .../src/lib/helpers/common.ts | 36 ++++ .../src/lib/helpers/index.ts | 15 ++ .../src/lib/helpers/preflight_check.ts | 16 +- .../src/lib/helpers/serializer.ts | 48 +++++ .../src/lib/repository.ts | 191 ++++-------------- 8 files changed, 345 insertions(+), 157 deletions(-) create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/constants.ts create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/serializer.ts diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/create.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/create.ts index 93d6fe010be4..a7be46af2ab4 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/create.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/create.ts @@ -6,14 +6,185 @@ * Side Public License, v 1. */ +import { isNotFoundFromUnsupportedServer } from '@kbn/core-elasticsearch-server-internal'; import { + SavedObjectsErrorHelpers, type SavedObject, + type ISavedObjectTypeRegistry, + type SavedObjectsExtensions, + type SavedObjectSanitizedDoc, + type ISavedObjectsSerializer, } from '@kbn/core-saved-objects-server'; +import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server'; +import { + decodeRequestVersion, + type IKibanaMigrator, +} from '@kbn/core-saved-objects-base-server-internal'; import { SavedObjectsCreateOptions } from '@kbn/core-saved-objects-api-server'; +import type { RepositoryHelpers } from '../helpers'; +import { DEFAULT_REFRESH_SETTING } from '../constants'; +import { getCurrentTime, normalizeNamespace, setManaged } from '../internal_utils'; +import type { RepositoryEsClient } from '../repository_es_client'; +import type { PreflightCheckForCreateResult } from '../preflight_check_for_create'; +import { getSavedObjectNamespaces } from '../utils'; +export interface PerformCreateParams { + type: string; + attributes: T; + options: SavedObjectsCreateOptions; +} -export interface PerformCreateParams { - type: strin +export interface PerformCreatContext { + registry: ISavedObjectTypeRegistry; + helpers: RepositoryHelpers; + extensions: SavedObjectsExtensions; + client: RepositoryEsClient; + allowedTypes: string[]; + serializer: ISavedObjectsSerializer; + migrator: IKibanaMigrator; } -export const performCreate = ({}: PerformCreateParams) +export const performCreate = async ( + { type, attributes, options }: PerformCreateParams, + { + registry, + helpers, + allowedTypes, + client, + serializer, + migrator, + extensions = {}, + }: PerformCreatContext +): Promise> => { + const { + common: commonHelper, + validation: validationHelper, + encryption: encryptionHelper, + preflight: preflightHelper, + serializer: serializerHelper, + } = helpers; + const { securityExtension } = extensions; + + const namespace = commonHelper.getCurrentNamespace(options.namespace); + const { + migrationVersion, + coreMigrationVersion, + typeMigrationVersion, + managed, + overwrite = false, + references = [], + refresh = DEFAULT_REFRESH_SETTING, + initialNamespaces, + version, + } = options; + const { migrationVersionCompatibility } = options; + if (!allowedTypes.includes(type)) { + throw SavedObjectsErrorHelpers.createUnsupportedTypeError(type); + } + const id = commonHelper.getValidId(type, options.id, options.version, options.overwrite); + validationHelper.validateInitialNamespaces(type, initialNamespaces); + validationHelper.validateOriginId(type, options); + + const time = getCurrentTime(); + let savedObjectNamespace: string | undefined; + let savedObjectNamespaces: string[] | undefined; + let existingOriginId: string | undefined; + const namespaceString = SavedObjectsUtils.namespaceIdToString(namespace); + + let preflightResult: PreflightCheckForCreateResult | undefined; + if (registry.isSingleNamespace(type)) { + savedObjectNamespace = initialNamespaces ? normalizeNamespace(initialNamespaces[0]) : namespace; + } else if (registry.isMultiNamespace(type)) { + if (options.id) { + // we will overwrite a multi-namespace saved object if it exists; if that happens, ensure we preserve its included namespaces + // note: this check throws an error if the object is found but does not exist in this namespace + preflightResult = ( + await preflightHelper.preflightCheckForCreate([ + { + type, + id, + overwrite, + namespaces: initialNamespaces ?? [namespaceString], + }, + ]) + )[0]; + } + savedObjectNamespaces = + initialNamespaces || getSavedObjectNamespaces(namespace, preflightResult?.existingDocument); + existingOriginId = preflightResult?.existingDocument?._source?.originId; + } + + const authorizationResult = await securityExtension?.authorizeCreate({ + namespace, + object: { + type, + id, + initialNamespaces, + existingNamespaces: preflightResult?.existingDocument?._source?.namespaces ?? [], + }, + }); + + if (preflightResult?.error) { + // This intentionally occurs _after_ the authZ enforcement (which may throw a 403 error earlier) + throw SavedObjectsErrorHelpers.createConflictError(type, id); + } + + // 1. If the originId has been *explicitly set* in the options (defined or undefined), respect that. + // 2. Otherwise, preserve the originId of the existing object that is being overwritten, if any. + const originId = Object.keys(options).includes('originId') ? options.originId : existingOriginId; + const migrated = migrator.migrateDocument({ + id, + type, + ...(savedObjectNamespace && { namespace: savedObjectNamespace }), + ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), + originId, + attributes: await encryptionHelper.optionallyEncryptAttributes( + type, + id, + savedObjectNamespace, // if single namespace type, this is the first in initialNamespaces. If multi-namespace type this is options.namespace/current namespace. + attributes + ), + migrationVersion, + coreMigrationVersion, + typeMigrationVersion, + managed: setManaged({ optionsManaged: managed }), + created_at: time, + updated_at: time, + ...(Array.isArray(references) && { references }), + }); + + /** + * If a validation has been registered for this type, we run it against the migrated attributes. + * This is an imperfect solution because malformed attributes could have already caused the + * migration to fail, but it's the best we can do without devising a way to run validations + * inside the migration algorithm itself. + */ + validationHelper.validateObjectForCreate(type, migrated as SavedObjectSanitizedDoc); + + const raw = serializer.savedObjectToRaw(migrated as SavedObjectSanitizedDoc); + + const requestParams = { + id: raw._id, + index: commonHelper.getIndexForType(type), + refresh, + body: raw._source, + ...(overwrite && version ? decodeRequestVersion(version) : {}), + require_alias: true, + }; + + const { body, statusCode, headers } = + id && overwrite + ? await client.index(requestParams, { meta: true }) + : await client.create(requestParams, { meta: true }); + + // throw if we can't verify a 404 response is from Elasticsearch + if (isNotFoundFromUnsupportedServer({ statusCode, headers })) { + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(id, type); + } + + return encryptionHelper.optionallyDecryptAndRedactSingleResult( + serializerHelper.rawToSavedObject({ ...raw, ...body }, { migrationVersionCompatibility }), + authorizationResult?.typeMap, + attributes + ); +}; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts new file mode 100644 index 000000000000..344c43fcaa54 --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { performCreate } from './create'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/constants.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/constants.ts new file mode 100644 index 000000000000..d0eb4e7827d2 --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/constants.ts @@ -0,0 +1,10 @@ +/* + * 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 const DEFAULT_REFRESH_SETTING = 'wait_for'; +export const DEFAULT_RETRY_COUNT = 3; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/common.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/common.ts index eff0bf3af2b0..aed103ba1c51 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/common.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/common.ts @@ -9,29 +9,36 @@ import type { ISavedObjectTypeRegistry, ISavedObjectsSpacesExtension, + ISavedObjectsEncryptionExtension, } from '@kbn/core-saved-objects-server'; import { getIndexForType } from '@kbn/core-saved-objects-base-server-internal'; +import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server'; +import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; import { normalizeNamespace } from '../internal_utils'; export class CommonHelper { private registry: ISavedObjectTypeRegistry; private spaceExtension?: ISavedObjectsSpacesExtension; + private encryptionExtension?: ISavedObjectsEncryptionExtension; private defaultIndex: string; private kibanaVersion: string; constructor({ registry, spaceExtension, + encryptionExtension, kibanaVersion, defaultIndex, }: { registry: ISavedObjectTypeRegistry; spaceExtension?: ISavedObjectsSpacesExtension; + encryptionExtension?: ISavedObjectsEncryptionExtension; defaultIndex: string; kibanaVersion: string; }) { this.registry = registry; this.spaceExtension = spaceExtension; + this.encryptionExtension = encryptionExtension; this.kibanaVersion = kibanaVersion; this.defaultIndex = defaultIndex; } @@ -70,6 +77,35 @@ export class CommonHelper { } return normalizeNamespace(namespace); } + + /** + * Saved objects with encrypted attributes should have IDs that are hard to guess, especially since IDs are part of the AAD used during + * encryption, that's why we control them within this function and don't allow consumers to specify their own IDs directly for encryptable + * types unless overwriting the original document. + */ + public getValidId( + type: string, + id: string | undefined, + version: string | undefined, + overwrite: boolean | undefined + ) { + if (!this.encryptionExtension?.isEncryptableType(type)) { + return id || SavedObjectsUtils.generateId(); + } + if (!id) { + return SavedObjectsUtils.generateId(); + } + // only allow a specified ID if we're overwriting an existing ESO with a Version + // this helps us ensure that the document really was previously created using ESO + // and not being used to get around the specified ID limitation + const canSpecifyID = (overwrite && version) || SavedObjectsUtils.isRandomId(id); + if (!canSpecifyID) { + throw SavedObjectsErrorHelpers.createBadRequestError( + 'Predefined IDs are not allowed for saved objects with encrypted attributes unless the ID is a UUID.' + ); + } + return id; + } } const unique = (array: string[]) => [...new Set(array)]; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/index.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/index.ts index bca048a9db17..2331d3dd799e 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/index.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/index.ts @@ -6,11 +6,26 @@ * Side Public License, v 1. */ +import type { CommonHelper } from './common'; +import type { EncryptionHelper } from './encryption'; +import type { ValidationHelper } from './validation'; +import type { PreflightCheckHelper } from './preflight_check'; +import type { SerializerHelper } from './serializer'; + export { CommonHelper } from './common'; export { EncryptionHelper } from './encryption'; export { ValidationHelper } from './validation'; +export { SerializerHelper } from './serializer'; export { PreflightCheckHelper, type PreflightCheckNamespacesParams, type PreflightCheckNamespacesResult, } from './preflight_check'; + +export interface RepositoryHelpers { + common: CommonHelper; + encryption: EncryptionHelper; + validation: ValidationHelper; + preflight: PreflightCheckHelper; + serializer: SerializerHelper; +} diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/preflight_check.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/preflight_check.ts index 1d35a291ed60..868d0d849dd1 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/preflight_check.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/preflight_check.ts @@ -17,7 +17,10 @@ import { SavedObjectsErrorHelpers, SavedObjectsRawDocSource } from '@kbn/core-sa import { rawDocExistsInNamespaces } from '../internal_utils'; import { getSavedObjectNamespaces } from '../utils/namespaces'; import { GetResponseFound, isFoundGetResponse } from '../utils/es_responses'; -import { preflightCheckForCreate } from '../preflight_check_for_create'; +import { + preflightCheckForCreate, + PreflightCheckForCreateObject, +} from '../preflight_check_for_create'; import type { RepositoryEsClient } from '../repository_es_client'; export class PreflightCheckHelper { @@ -47,6 +50,17 @@ export class PreflightCheckHelper { this.createPointInTimeFinder = createPointInTimeFinder; } + public async preflightCheckForCreate(objects: PreflightCheckForCreateObject[]) { + return await preflightCheckForCreate({ + objects, + registry: this.registry, + client: this.client, + serializer: this.serializer, + getIndexForType: this.getIndexForType.bind(this), + createPointInTimeFinder: this.createPointInTimeFinder.bind(this), + }); + } + /** * Pre-flight check to ensure that a multi-namespace object exists in the current namespace. */ diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/serializer.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/serializer.ts new file mode 100644 index 000000000000..52e68f2a25c8 --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/serializer.ts @@ -0,0 +1,48 @@ +/* + * 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 { omit } from 'lodash'; +import type { + ISavedObjectTypeRegistry, + ISavedObjectsSerializer, +} from '@kbn/core-saved-objects-server'; +import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server'; +import { + SavedObject, + SavedObjectsRawDoc, + SavedObjectsRawDocParseOptions, +} from '@kbn/core-saved-objects-server'; + +export class SerializerHelper { + private registry: ISavedObjectTypeRegistry; + private serializer: ISavedObjectsSerializer; + + constructor({ + registry, + serializer, + }: { + registry: ISavedObjectTypeRegistry; + serializer: ISavedObjectsSerializer; + }) { + this.registry = registry; + this.serializer = serializer; + } + + public rawToSavedObject( + raw: SavedObjectsRawDoc, + options?: SavedObjectsRawDocParseOptions + ): SavedObject { + const savedObject = this.serializer.rawToSavedObject(raw, options); + const { namespace, type } = savedObject; + if (this.registry.isSingleNamespace(type)) { + savedObject.namespaces = [SavedObjectsUtils.namespaceIdToString(namespace)]; + } + + return omit(savedObject, ['namespace']) as SavedObject; + } +} diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts index a655e7552e91..8291dfbaf5db 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts @@ -90,7 +90,6 @@ import { } from '@kbn/core-saved-objects-utils-server'; import { SavedObjectsSerializer, - decodeRequestVersion, encodeVersion, encodeHitVersion, getRootPropertiesObjects, @@ -124,7 +123,6 @@ import { updateObjectsSpaces } from './update_objects_spaces'; import { preflightCheckForCreate, type PreflightCheckForCreateObject, - type PreflightCheckForCreateResult, } from './preflight_check_for_create'; import { deleteLegacyUrlAliases } from './legacy_url_aliases'; import type { @@ -138,13 +136,17 @@ import type { ObjectToDeleteAliasesFor, } from './repository_bulk_delete_internal_types'; import { + RepositoryHelpers, CommonHelper, EncryptionHelper, ValidationHelper, PreflightCheckHelper, + SerializerHelper, type PreflightCheckNamespacesResult, } from './helpers'; import { isFoundGetResponse, getSavedObjectNamespaces } from './utils'; +import { DEFAULT_REFRESH_SETTING, DEFAULT_RETRY_COUNT } from './constants'; +import { performCreate } from './apis'; // 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. @@ -161,9 +163,6 @@ export interface SavedObjectsRepositoryOptions { extensions?: SavedObjectsExtensions; } -export const DEFAULT_REFRESH_SETTING = 'wait_for'; -export const DEFAULT_RETRY_COUNT = 3; - const MAX_CONCURRENT_ALIAS_DELETIONS = 10; function isMgetDoc(doc?: estypes.MgetResponseItem): doc is estypes.GetGetResult { @@ -193,6 +192,10 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { private encryptionHelper: EncryptionHelper; private validationHelper: ValidationHelper; private preflightCheckHelper: PreflightCheckHelper; + private serializerHelper: SerializerHelper; + + private readonly extensions: SavedObjectsExtensions; + private readonly helpers: RepositoryHelpers; /** * A factory function for creating SavedObjectRepository instances. @@ -250,7 +253,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { migrator, allowedTypes = [], logger, - extensions, + extensions = {}, } = options; if (allowedTypes.length === 0) { @@ -264,9 +267,10 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { this._allowedTypes = allowedTypes; this._serializer = serializer; this._logger = logger; - this._encryptionExtension = extensions?.encryptionExtension; - this._securityExtension = extensions?.securityExtension; - this._spacesExtension = extensions?.spacesExtension; + this.extensions = extensions; + this._encryptionExtension = extensions.encryptionExtension; + this._securityExtension = extensions.securityExtension; + this._spacesExtension = extensions.spacesExtension; this.commonHelper = new CommonHelper({ spaceExtension: extensions?.spacesExtension, defaultIndex: index, @@ -289,6 +293,17 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { registry: typeRegistry, client: this.client, }); + this.serializerHelper = new SerializerHelper({ + registry: typeRegistry, + serializer, + }); + this.helpers = { + common: this.commonHelper, + preflight: this.preflightCheckHelper, + validation: this.validationHelper, + encryption: this.encryptionHelper, + serializer: this.serializerHelper, + }; } /** @@ -299,131 +314,21 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { attributes: T, options: SavedObjectsCreateOptions = {} ): Promise> { - const namespace = this.getCurrentNamespace(options.namespace); - const { - migrationVersion, - coreMigrationVersion, - typeMigrationVersion, - managed, - overwrite = false, - references = [], - refresh = DEFAULT_REFRESH_SETTING, - initialNamespaces, - version, - } = options; - const { migrationVersionCompatibility } = options; - if (!this._allowedTypes.includes(type)) { - throw SavedObjectsErrorHelpers.createUnsupportedTypeError(type); - } - const id = this.getValidId(type, options.id, options.version, options.overwrite); - this.validationHelper.validateInitialNamespaces(type, initialNamespaces); - this.validationHelper.validateOriginId(type, options); - - const time = getCurrentTime(); - let savedObjectNamespace: string | undefined; - let savedObjectNamespaces: string[] | undefined; - let existingOriginId: string | undefined; - const namespaceString = SavedObjectsUtils.namespaceIdToString(namespace); - - let preflightResult: PreflightCheckForCreateResult | undefined; - if (this._registry.isSingleNamespace(type)) { - savedObjectNamespace = initialNamespaces - ? normalizeNamespace(initialNamespaces[0]) - : namespace; - } else if (this._registry.isMultiNamespace(type)) { - if (options.id) { - // we will overwrite a multi-namespace saved object if it exists; if that happens, ensure we preserve its included namespaces - // note: this check throws an error if the object is found but does not exist in this namespace - preflightResult = ( - await preflightCheckForCreate({ - registry: this._registry, - client: this.client, - serializer: this._serializer, - getIndexForType: this.getIndexForType.bind(this), - createPointInTimeFinder: this.createPointInTimeFinder.bind(this), - objects: [{ type, id, overwrite, namespaces: initialNamespaces ?? [namespaceString] }], - }) - )[0]; - } - savedObjectNamespaces = - initialNamespaces || getSavedObjectNamespaces(namespace, preflightResult?.existingDocument); - existingOriginId = preflightResult?.existingDocument?._source?.originId; - } - - const authorizationResult = await this._securityExtension?.authorizeCreate({ - namespace, - object: { + return performCreate( + { type, - id, - initialNamespaces, - existingNamespaces: preflightResult?.existingDocument?._source?.namespaces ?? [], + attributes, + options, }, - }); - - if (preflightResult?.error) { - // This intentionally occurs _after_ the authZ enforcement (which may throw a 403 error earlier) - throw SavedObjectsErrorHelpers.createConflictError(type, id); - } - - // 1. If the originId has been *explicitly set* in the options (defined or undefined), respect that. - // 2. Otherwise, preserve the originId of the existing object that is being overwritten, if any. - const originId = Object.keys(options).includes('originId') - ? options.originId - : existingOriginId; - const migrated = this._migrator.migrateDocument({ - id, - type, - ...(savedObjectNamespace && { namespace: savedObjectNamespace }), - ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), - originId, - attributes: await this.encryptionHelper.optionallyEncryptAttributes( - type, - id, - savedObjectNamespace, // if single namespace type, this is the first in initialNamespaces. If multi-namespace type this is options.namespace/current namespace. - attributes - ), - migrationVersion, - coreMigrationVersion, - typeMigrationVersion, - managed: setManaged({ optionsManaged: managed }), - created_at: time, - updated_at: time, - ...(Array.isArray(references) && { references }), - }); - - /** - * If a validation has been registered for this type, we run it against the migrated attributes. - * This is an imperfect solution because malformed attributes could have already caused the - * migration to fail, but it's the best we can do without devising a way to run validations - * inside the migration algorithm itself. - */ - this.validationHelper.validateObjectForCreate(type, migrated as SavedObjectSanitizedDoc); - - const raw = this._serializer.savedObjectToRaw(migrated as SavedObjectSanitizedDoc); - - const requestParams = { - id: raw._id, - index: this.getIndexForType(type), - refresh, - body: raw._source, - ...(overwrite && version ? decodeRequestVersion(version) : {}), - require_alias: true, - }; - - const { body, statusCode, headers } = - id && overwrite - ? await this.client.index(requestParams, { meta: true }) - : await this.client.create(requestParams, { meta: true }); - - // throw if we can't verify a 404 response is from Elasticsearch - if (isNotFoundFromUnsupportedServer({ statusCode, headers })) { - throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(id, type); - } - - return this.encryptionHelper.optionallyDecryptAndRedactSingleResult( - this._rawToSavedObject({ ...raw, ...body }, { migrationVersionCompatibility }), - authorizationResult?.typeMap, - attributes + { + client: this.client, + extensions: this.extensions, + helpers: this.helpers, + allowedTypes: this._allowedTypes, + registry: this._registry, + serializer: this._serializer, + migrator: this._migrator, + } ); } @@ -2634,33 +2539,13 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { return rawDocExistsInNamespace(this._registry, raw, namespace); } - /** - * Saved objects with encrypted attributes should have IDs that are hard to guess, especially since IDs are part of the AAD used during - * encryption, that's why we control them within this function and don't allow consumers to specify their own IDs directly for encryptable - * types unless overwriting the original document. - */ private getValidId( type: string, id: string | undefined, version: string | undefined, overwrite: boolean | undefined ) { - if (!this._encryptionExtension?.isEncryptableType(type)) { - return id || SavedObjectsUtils.generateId(); - } - if (!id) { - return SavedObjectsUtils.generateId(); - } - // only allow a specified ID if we're overwriting an existing ESO with a Version - // this helps us ensure that the document really was previously created using ESO - // and not being used to get around the specified ID limitation - const canSpecifyID = (overwrite && version) || SavedObjectsUtils.isRandomId(id); - if (!canSpecifyID) { - throw SavedObjectsErrorHelpers.createBadRequestError( - 'Predefined IDs are not allowed for saved objects with encrypted attributes unless the ID is a UUID.' - ); - } - return id; + return this.commonHelper.getValidId(type, id, version, overwrite); } } From 31040230a089a3d22576ff4aac588064c8eb52d8 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 9 May 2023 11:22:13 +0200 Subject: [PATCH 03/34] extract bulk_create --- .../src/lib/apis/bulk_create.ts | 337 ++++++++++++++++++ .../src/lib/apis/index.ts | 1 + .../src/lib/internal_utils.ts | 6 + .../src/lib/repository.ts | 287 +-------------- 4 files changed, 358 insertions(+), 273 deletions(-) create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_create.ts diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_create.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_create.ts new file mode 100644 index 000000000000..4bcdc231b3eb --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_create.ts @@ -0,0 +1,337 @@ +/* + * 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 { Payload } from '@hapi/boom'; +import { + SavedObjectsErrorHelpers, + type SavedObject, + type ISavedObjectTypeRegistry, + type SavedObjectsExtensions, + type SavedObjectSanitizedDoc, + type ISavedObjectsSerializer, + DecoratedError, + AuthorizeCreateObject, + SavedObjectsRawDoc, +} from '@kbn/core-saved-objects-server'; +import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server'; +import { type IKibanaMigrator } from '@kbn/core-saved-objects-base-server-internal'; +import { + SavedObjectsCreateOptions, + SavedObjectsBulkCreateObject, + SavedObjectsBulkResponse, +} from '@kbn/core-saved-objects-api-server'; +import type { RepositoryHelpers } from '../helpers'; +import { DEFAULT_REFRESH_SETTING } from '../constants'; +import { + Either, + getBulkOperationError, + getCurrentTime, + getExpectedVersionProperties, + isLeft, + isRight, + normalizeNamespace, + setManaged, + errorContent, +} from '../internal_utils'; +import type { RepositoryEsClient } from '../repository_es_client'; +import { getSavedObjectNamespaces } from '../utils'; +import { PreflightCheckForCreateObject } from '../preflight_check_for_create'; + +export interface PerformBulkCreateParams { + objects: Array>; + options: SavedObjectsCreateOptions; +} + +export interface PerformBulkCreatContext { + registry: ISavedObjectTypeRegistry; + helpers: RepositoryHelpers; + extensions: SavedObjectsExtensions; + client: RepositoryEsClient; + allowedTypes: string[]; + serializer: ISavedObjectsSerializer; + migrator: IKibanaMigrator; +} + +export const performBulkCreate = async ( + { objects, options }: PerformBulkCreateParams, + { + registry, + helpers, + allowedTypes, + client, + serializer, + migrator, + extensions = {}, + }: PerformBulkCreatContext +): Promise> => { + const { + common: commonHelper, + validation: validationHelper, + encryption: encryptionHelper, + preflight: preflightHelper, + serializer: serializerHelper, + } = helpers; + const { securityExtension } = extensions; + const namespace = commonHelper.getCurrentNamespace(options.namespace); + + const { + migrationVersionCompatibility, + overwrite = false, + refresh = DEFAULT_REFRESH_SETTING, + managed: optionsManaged, + } = options; + const time = getCurrentTime(); + + let preflightCheckIndexCounter = 0; + type ExpectedResult = Either< + { type: string; id?: string; error: Payload }, + { + method: 'index' | 'create'; + object: SavedObjectsBulkCreateObject & { id: string }; + preflightCheckIndex?: number; + } + >; + const expectedResults = objects.map((object) => { + const { type, id: requestId, initialNamespaces, version, managed } = object; + let error: DecoratedError | undefined; + let id: string = ''; // Assign to make TS happy, the ID will be validated (or randomly generated if needed) during getValidId below + const objectManaged = managed; + if (!allowedTypes.includes(type)) { + error = SavedObjectsErrorHelpers.createUnsupportedTypeError(type); + } else { + try { + id = commonHelper.getValidId(type, requestId, version, overwrite); + validationHelper.validateInitialNamespaces(type, initialNamespaces); + validationHelper.validateOriginId(type, object); + } catch (e) { + error = e; + } + } + + if (error) { + return { + tag: 'Left', + value: { id: requestId, type, error: errorContent(error) }, + }; + } + + const method = requestId && overwrite ? 'index' : 'create'; + const requiresNamespacesCheck = requestId && registry.isMultiNamespace(type); + + return { + tag: 'Right', + value: { + method, + object: { + ...object, + id, + managed: setManaged({ optionsManaged, objectManaged }), + }, + ...(requiresNamespacesCheck && { preflightCheckIndex: preflightCheckIndexCounter++ }), + }, + }; + }); + + const validObjects = expectedResults.filter(isRight); + if (validObjects.length === 0) { + // We only have error results; return early to avoid potentially trying authZ checks for 0 types which would result in an exception. + return { + // Technically the returned array should only contain SavedObject results, but for errors this is not true (we cast to 'unknown' below) + saved_objects: expectedResults.map>( + ({ value }) => value as unknown as SavedObject + ), + }; + } + + const namespaceString = SavedObjectsUtils.namespaceIdToString(namespace); + const preflightCheckObjects = validObjects + .filter(({ value }) => value.preflightCheckIndex !== undefined) + .map(({ value }) => { + const { type, id, initialNamespaces } = value.object; + const namespaces = initialNamespaces ?? [namespaceString]; + return { type, id, overwrite, namespaces }; + }); + const preflightCheckResponse = await preflightHelper.preflightCheckForCreate( + preflightCheckObjects + ); + + const authObjects: AuthorizeCreateObject[] = validObjects.map((element) => { + const { object, preflightCheckIndex: index } = element.value; + const preflightResult = index !== undefined ? preflightCheckResponse[index] : undefined; + return { + type: object.type, + id: object.id, + initialNamespaces: object.initialNamespaces, + existingNamespaces: preflightResult?.existingDocument?._source.namespaces ?? [], + }; + }); + + const authorizationResult = await securityExtension?.authorizeBulkCreate({ + namespace, + objects: authObjects, + }); + + let bulkRequestIndexCounter = 0; + const bulkCreateParams: object[] = []; + type ExpectedBulkResult = Either< + { type: string; id?: string; error: Payload }, + { esRequestIndex: number; requestedId: string; rawMigratedDoc: SavedObjectsRawDoc } + >; + const expectedBulkResults = await Promise.all( + expectedResults.map>(async (expectedBulkGetResult) => { + if (isLeft(expectedBulkGetResult)) { + return expectedBulkGetResult; + } + + let savedObjectNamespace: string | undefined; + let savedObjectNamespaces: string[] | undefined; + let existingOriginId: string | undefined; + let versionProperties; + const { + preflightCheckIndex, + object: { initialNamespaces, version, ...object }, + method, + } = expectedBulkGetResult.value; + if (preflightCheckIndex !== undefined) { + const preflightResult = preflightCheckResponse[preflightCheckIndex]; + const { type, id, existingDocument, error } = preflightResult; + if (error) { + const { metadata } = error; + return { + tag: 'Left', + value: { + id, + type, + error: { + ...errorContent(SavedObjectsErrorHelpers.createConflictError(type, id)), + ...(metadata && { metadata }), + }, + }, + }; + } + savedObjectNamespaces = + initialNamespaces || getSavedObjectNamespaces(namespace, existingDocument); + versionProperties = getExpectedVersionProperties(version); + existingOriginId = existingDocument?._source?.originId; + } else { + if (registry.isSingleNamespace(object.type)) { + savedObjectNamespace = initialNamespaces + ? normalizeNamespace(initialNamespaces[0]) + : namespace; + } else if (registry.isMultiNamespace(object.type)) { + savedObjectNamespaces = initialNamespaces || getSavedObjectNamespaces(namespace); + } + versionProperties = getExpectedVersionProperties(version); + } + + // 1. If the originId has been *explicitly set* in the options (defined or undefined), respect that. + // 2. Otherwise, preserve the originId of the existing object that is being overwritten, if any. + const originId = Object.keys(object).includes('originId') + ? object.originId + : existingOriginId; + const migrated = migrator.migrateDocument({ + id: object.id, + type: object.type, + attributes: await encryptionHelper.optionallyEncryptAttributes( + object.type, + object.id, + savedObjectNamespace, // only used for multi-namespace object types + object.attributes + ), + migrationVersion: object.migrationVersion, + coreMigrationVersion: object.coreMigrationVersion, + typeMigrationVersion: object.typeMigrationVersion, + ...(savedObjectNamespace && { namespace: savedObjectNamespace }), + ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), + managed: setManaged({ optionsManaged, objectManaged: object.managed }), + updated_at: time, + created_at: time, + references: object.references || [], + originId, + }) as SavedObjectSanitizedDoc; + + /** + * If a validation has been registered for this type, we run it against the migrated attributes. + * This is an imperfect solution because malformed attributes could have already caused the + * migration to fail, but it's the best we can do without devising a way to run validations + * inside the migration algorithm itself. + */ + try { + validationHelper.validateObjectForCreate(object.type, migrated); + } catch (error) { + return { + tag: 'Left', + value: { + id: object.id, + type: object.type, + error, + }, + }; + } + + const expectedResult = { + esRequestIndex: bulkRequestIndexCounter++, + requestedId: object.id, + rawMigratedDoc: serializer.savedObjectToRaw(migrated), + }; + + bulkCreateParams.push( + { + [method]: { + _id: expectedResult.rawMigratedDoc._id, + _index: commonHelper.getIndexForType(object.type), + ...(overwrite && versionProperties), + }, + }, + expectedResult.rawMigratedDoc._source + ); + + return { tag: 'Right', value: expectedResult }; + }) + ); + + const bulkResponse = bulkCreateParams.length + ? await client.bulk({ + refresh, + require_alias: true, + body: bulkCreateParams, + }) + : undefined; + + const result = { + saved_objects: expectedBulkResults.map((expectedResult) => { + if (isLeft(expectedResult)) { + return expectedResult.value as any; + } + + const { requestedId, rawMigratedDoc, esRequestIndex } = expectedResult.value; + const rawResponse = Object.values(bulkResponse?.items[esRequestIndex] ?? {})[0] as any; + + const error = getBulkOperationError(rawMigratedDoc._source.type, requestedId, rawResponse); + if (error) { + return { type: rawMigratedDoc._source.type, id: requestedId, error }; + } + + // When method == 'index' the bulkResponse doesn't include the indexed + // _source so we return rawMigratedDoc but have to spread the latest + // _seq_no and _primary_term values from the rawResponse. + return serializerHelper.rawToSavedObject( + { + ...rawMigratedDoc, + ...{ _seq_no: rawResponse._seq_no, _primary_term: rawResponse._primary_term }, + }, + { migrationVersionCompatibility } + ); + }), + }; + return encryptionHelper.optionallyDecryptAndRedactBulkResult( + result, + authorizationResult?.typeMap, + objects + ); +}; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts index 344c43fcaa54..34cb2359fac8 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts @@ -7,3 +7,4 @@ */ export { performCreate } from './create'; +export { performBulkCreate } from './bulk_create'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_utils.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_utils.ts index 1ffc5fcde62d..12b53fb480e9 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_utils.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_utils.ts @@ -14,6 +14,7 @@ import { type SavedObject, SavedObjectsErrorHelpers, type SavedObjectsRawDocParseOptions, + type DecoratedError, } from '@kbn/core-saved-objects-server'; import { SavedObjectsUtils, ALL_NAMESPACES_STRING } from '@kbn/core-saved-objects-utils-server'; import { @@ -295,3 +296,8 @@ export function setManaged({ }): boolean { return optionsManaged ?? objectManaged ?? false; } + +/** + * Extracts the contents of a decorated error to return the attributes for bulk operations. + */ +export const errorContent = (error: DecoratedError) => error.output.payload; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts index 8291dfbaf5db..b4f825d9e1b7 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts @@ -113,6 +113,7 @@ import { normalizeNamespace, rawDocExistsInNamespace, rawDocExistsInNamespaces, + errorContent, type Either, isLeft, isRight, @@ -146,7 +147,7 @@ import { } from './helpers'; import { isFoundGetResponse, getSavedObjectNamespaces } from './utils'; import { DEFAULT_REFRESH_SETTING, DEFAULT_RETRY_COUNT } from './constants'; -import { performCreate } from './apis'; +import { performCreate, performBulkCreate } from './apis'; // 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. @@ -339,266 +340,20 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { objects: Array>, options: SavedObjectsCreateOptions = {} ): Promise> { - const namespace = this.getCurrentNamespace(options.namespace); - const { - migrationVersionCompatibility, - overwrite = false, - refresh = DEFAULT_REFRESH_SETTING, - managed: optionsManaged, - } = options; - const time = getCurrentTime(); - - let preflightCheckIndexCounter = 0; - type ExpectedResult = Either< - { type: string; id?: string; error: Payload }, + return performBulkCreate( { - method: 'index' | 'create'; - object: SavedObjectsBulkCreateObject & { id: string }; - preflightCheckIndex?: number; - } - >; - const expectedResults = objects.map((object) => { - const { type, id: requestId, initialNamespaces, version, managed } = object; - let error: DecoratedError | undefined; - let id: string = ''; // Assign to make TS happy, the ID will be validated (or randomly generated if needed) during getValidId below - const objectManaged = managed; - if (!this._allowedTypes.includes(type)) { - error = SavedObjectsErrorHelpers.createUnsupportedTypeError(type); - } else { - try { - id = this.getValidId(type, requestId, version, overwrite); - this.validationHelper.validateInitialNamespaces(type, initialNamespaces); - this.validationHelper.validateOriginId(type, object); - } catch (e) { - error = e; - } - } - - if (error) { - return { - tag: 'Left', - value: { id: requestId, type, error: errorContent(error) }, - }; + objects, + options, + }, + { + client: this.client, + extensions: this.extensions, + helpers: this.helpers, + allowedTypes: this._allowedTypes, + registry: this._registry, + serializer: this._serializer, + migrator: this._migrator, } - - const method = requestId && overwrite ? 'index' : 'create'; - const requiresNamespacesCheck = requestId && this._registry.isMultiNamespace(type); - - return { - tag: 'Right', - value: { - method, - object: { - ...object, - id, - managed: setManaged({ optionsManaged, objectManaged }), - }, - ...(requiresNamespacesCheck && { preflightCheckIndex: preflightCheckIndexCounter++ }), - }, - }; - }); - - const validObjects = expectedResults.filter(isRight); - if (validObjects.length === 0) { - // We only have error results; return early to avoid potentially trying authZ checks for 0 types which would result in an exception. - return { - // Technically the returned array should only contain SavedObject results, but for errors this is not true (we cast to 'unknown' below) - saved_objects: expectedResults.map>( - ({ value }) => value as unknown as SavedObject - ), - }; - } - - const namespaceString = SavedObjectsUtils.namespaceIdToString(namespace); - const preflightCheckObjects = validObjects - .filter(({ value }) => value.preflightCheckIndex !== undefined) - .map(({ value }) => { - const { type, id, initialNamespaces } = value.object; - const namespaces = initialNamespaces ?? [namespaceString]; - return { type, id, overwrite, namespaces }; - }); - const preflightCheckResponse = await preflightCheckForCreate({ - registry: this._registry, - client: this.client, - serializer: this._serializer, - getIndexForType: this.getIndexForType.bind(this), - createPointInTimeFinder: this.createPointInTimeFinder.bind(this), - objects: preflightCheckObjects, - }); - - const authObjects: AuthorizeCreateObject[] = validObjects.map((element) => { - const { object, preflightCheckIndex: index } = element.value; - const preflightResult = index !== undefined ? preflightCheckResponse[index] : undefined; - return { - type: object.type, - id: object.id, - initialNamespaces: object.initialNamespaces, - existingNamespaces: preflightResult?.existingDocument?._source.namespaces ?? [], - }; - }); - - const authorizationResult = await this._securityExtension?.authorizeBulkCreate({ - namespace, - objects: authObjects, - }); - - let bulkRequestIndexCounter = 0; - const bulkCreateParams: object[] = []; - type ExpectedBulkResult = Either< - { type: string; id?: string; error: Payload }, - { esRequestIndex: number; requestedId: string; rawMigratedDoc: SavedObjectsRawDoc } - >; - const expectedBulkResults = await Promise.all( - expectedResults.map>(async (expectedBulkGetResult) => { - if (isLeft(expectedBulkGetResult)) { - return expectedBulkGetResult; - } - - let savedObjectNamespace: string | undefined; - let savedObjectNamespaces: string[] | undefined; - let existingOriginId: string | undefined; - let versionProperties; - const { - preflightCheckIndex, - object: { initialNamespaces, version, ...object }, - method, - } = expectedBulkGetResult.value; - if (preflightCheckIndex !== undefined) { - const preflightResult = preflightCheckResponse[preflightCheckIndex]; - const { type, id, existingDocument, error } = preflightResult; - if (error) { - const { metadata } = error; - return { - tag: 'Left', - value: { - id, - type, - error: { - ...errorContent(SavedObjectsErrorHelpers.createConflictError(type, id)), - ...(metadata && { metadata }), - }, - }, - }; - } - savedObjectNamespaces = - initialNamespaces || getSavedObjectNamespaces(namespace, existingDocument); - versionProperties = getExpectedVersionProperties(version); - existingOriginId = existingDocument?._source?.originId; - } else { - if (this._registry.isSingleNamespace(object.type)) { - savedObjectNamespace = initialNamespaces - ? normalizeNamespace(initialNamespaces[0]) - : namespace; - } else if (this._registry.isMultiNamespace(object.type)) { - savedObjectNamespaces = initialNamespaces || getSavedObjectNamespaces(namespace); - } - versionProperties = getExpectedVersionProperties(version); - } - - // 1. If the originId has been *explicitly set* in the options (defined or undefined), respect that. - // 2. Otherwise, preserve the originId of the existing object that is being overwritten, if any. - const originId = Object.keys(object).includes('originId') - ? object.originId - : existingOriginId; - const migrated = this._migrator.migrateDocument({ - id: object.id, - type: object.type, - attributes: await this.encryptionHelper.optionallyEncryptAttributes( - object.type, - object.id, - savedObjectNamespace, // only used for multi-namespace object types - object.attributes - ), - migrationVersion: object.migrationVersion, - coreMigrationVersion: object.coreMigrationVersion, - typeMigrationVersion: object.typeMigrationVersion, - ...(savedObjectNamespace && { namespace: savedObjectNamespace }), - ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), - managed: setManaged({ optionsManaged, objectManaged: object.managed }), - updated_at: time, - created_at: time, - references: object.references || [], - originId, - }) as SavedObjectSanitizedDoc; - - /** - * If a validation has been registered for this type, we run it against the migrated attributes. - * This is an imperfect solution because malformed attributes could have already caused the - * migration to fail, but it's the best we can do without devising a way to run validations - * inside the migration algorithm itself. - */ - try { - this.validationHelper.validateObjectForCreate(object.type, migrated); - } catch (error) { - return { - tag: 'Left', - value: { - id: object.id, - type: object.type, - error, - }, - }; - } - - const expectedResult = { - esRequestIndex: bulkRequestIndexCounter++, - requestedId: object.id, - rawMigratedDoc: this._serializer.savedObjectToRaw(migrated), - }; - - bulkCreateParams.push( - { - [method]: { - _id: expectedResult.rawMigratedDoc._id, - _index: this.getIndexForType(object.type), - ...(overwrite && versionProperties), - }, - }, - expectedResult.rawMigratedDoc._source - ); - - return { tag: 'Right', value: expectedResult }; - }) - ); - - const bulkResponse = bulkCreateParams.length - ? await this.client.bulk({ - refresh, - require_alias: true, - body: bulkCreateParams, - }) - : undefined; - - const result = { - saved_objects: expectedBulkResults.map((expectedResult) => { - if (isLeft(expectedResult)) { - return expectedResult.value as any; - } - - const { requestedId, rawMigratedDoc, esRequestIndex } = expectedResult.value; - const rawResponse = Object.values(bulkResponse?.items[esRequestIndex] ?? {})[0] as any; - - const error = getBulkOperationError(rawMigratedDoc._source.type, requestedId, rawResponse); - if (error) { - return { type: rawMigratedDoc._source.type, id: requestedId, error }; - } - - // When method == 'index' the bulkResponse doesn't include the indexed - // _source so we return rawMigratedDoc but have to spread the latest - // _seq_no and _primary_term values from the rawResponse. - return this._rawToSavedObject( - { - ...rawMigratedDoc, - ...{ _seq_no: rawResponse._seq_no, _primary_term: rawResponse._primary_term }, - }, - { migrationVersionCompatibility } - ); - }), - }; - return this.encryptionHelper.optionallyDecryptAndRedactBulkResult( - result, - authorizationResult?.typeMap, - objects ); } @@ -2538,18 +2293,4 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { private rawDocExistsInNamespace(raw: SavedObjectsRawDoc, namespace: string | undefined) { return rawDocExistsInNamespace(this._registry, raw, namespace); } - - private getValidId( - type: string, - id: string | undefined, - version: string | undefined, - overwrite: boolean | undefined - ) { - return this.commonHelper.getValidId(type, id, version, overwrite); - } } - -/** - * Extracts the contents of a decorated error to return the attributes for bulk operations. - */ -const errorContent = (error: DecoratedError) => error.output.payload; From e9c8d57d717b542f1f92492d63cbbfcf83ce41e9 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 9 May 2023 11:38:41 +0200 Subject: [PATCH 04/34] extract delete --- .../src/lib/apis/bulk_create.ts | 19 +-- .../src/lib/apis/create.ts | 19 +-- .../src/lib/apis/delete.ts | 135 +++++++++++++++++ .../src/lib/apis/index.ts | 2 + .../src/lib/apis/types.ts | 29 ++++ .../src/lib/repository.ts | 140 +++--------------- 6 files changed, 192 insertions(+), 152 deletions(-) create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/delete.ts create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/types.ts diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_create.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_create.ts index 4bcdc231b3eb..fbef1c940735 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_create.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_create.ts @@ -10,22 +10,17 @@ import type { Payload } from '@hapi/boom'; import { SavedObjectsErrorHelpers, type SavedObject, - type ISavedObjectTypeRegistry, - type SavedObjectsExtensions, type SavedObjectSanitizedDoc, - type ISavedObjectsSerializer, DecoratedError, AuthorizeCreateObject, SavedObjectsRawDoc, } from '@kbn/core-saved-objects-server'; import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server'; -import { type IKibanaMigrator } from '@kbn/core-saved-objects-base-server-internal'; import { SavedObjectsCreateOptions, SavedObjectsBulkCreateObject, SavedObjectsBulkResponse, } from '@kbn/core-saved-objects-api-server'; -import type { RepositoryHelpers } from '../helpers'; import { DEFAULT_REFRESH_SETTING } from '../constants'; import { Either, @@ -38,25 +33,15 @@ import { setManaged, errorContent, } from '../internal_utils'; -import type { RepositoryEsClient } from '../repository_es_client'; import { getSavedObjectNamespaces } from '../utils'; import { PreflightCheckForCreateObject } from '../preflight_check_for_create'; +import { ApiExecutionContext } from './types'; export interface PerformBulkCreateParams { objects: Array>; options: SavedObjectsCreateOptions; } -export interface PerformBulkCreatContext { - registry: ISavedObjectTypeRegistry; - helpers: RepositoryHelpers; - extensions: SavedObjectsExtensions; - client: RepositoryEsClient; - allowedTypes: string[]; - serializer: ISavedObjectsSerializer; - migrator: IKibanaMigrator; -} - export const performBulkCreate = async ( { objects, options }: PerformBulkCreateParams, { @@ -67,7 +52,7 @@ export const performBulkCreate = async ( serializer, migrator, extensions = {}, - }: PerformBulkCreatContext + }: ApiExecutionContext ): Promise> => { const { common: commonHelper, diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/create.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/create.ts index a7be46af2ab4..6fa685d17ee1 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/create.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/create.ts @@ -10,23 +10,18 @@ import { isNotFoundFromUnsupportedServer } from '@kbn/core-elasticsearch-server- import { SavedObjectsErrorHelpers, type SavedObject, - type ISavedObjectTypeRegistry, - type SavedObjectsExtensions, type SavedObjectSanitizedDoc, - type ISavedObjectsSerializer, } from '@kbn/core-saved-objects-server'; import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server'; import { decodeRequestVersion, - type IKibanaMigrator, } from '@kbn/core-saved-objects-base-server-internal'; import { SavedObjectsCreateOptions } from '@kbn/core-saved-objects-api-server'; -import type { RepositoryHelpers } from '../helpers'; import { DEFAULT_REFRESH_SETTING } from '../constants'; import { getCurrentTime, normalizeNamespace, setManaged } from '../internal_utils'; -import type { RepositoryEsClient } from '../repository_es_client'; import type { PreflightCheckForCreateResult } from '../preflight_check_for_create'; import { getSavedObjectNamespaces } from '../utils'; +import { ApiExecutionContext } from './types'; export interface PerformCreateParams { type: string; @@ -34,16 +29,6 @@ export interface PerformCreateParams { options: SavedObjectsCreateOptions; } -export interface PerformCreatContext { - registry: ISavedObjectTypeRegistry; - helpers: RepositoryHelpers; - extensions: SavedObjectsExtensions; - client: RepositoryEsClient; - allowedTypes: string[]; - serializer: ISavedObjectsSerializer; - migrator: IKibanaMigrator; -} - export const performCreate = async ( { type, attributes, options }: PerformCreateParams, { @@ -54,7 +39,7 @@ export const performCreate = async ( serializer, migrator, extensions = {}, - }: PerformCreatContext + }: ApiExecutionContext ): Promise> => { const { common: commonHelper, diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/delete.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/delete.ts new file mode 100644 index 000000000000..4c9b4665faf1 --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/delete.ts @@ -0,0 +1,135 @@ +/* + * 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 { isNotFoundFromUnsupportedServer } from '@kbn/core-elasticsearch-server-internal'; +import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; +import { ALL_NAMESPACES_STRING } from '@kbn/core-saved-objects-utils-server'; +import { SavedObjectsDeleteOptions } from '@kbn/core-saved-objects-api-server'; +import { DEFAULT_REFRESH_SETTING } from '../constants'; +import { getExpectedVersionProperties } from '../internal_utils'; +import { PreflightCheckNamespacesResult } from '../helpers'; +import type { ApiExecutionContext } from './types'; +import { deleteLegacyUrlAliases } from '../legacy_url_aliases'; + +export interface PerformDeleteParams { + type: string; + id: string; + options: SavedObjectsDeleteOptions; +} + +export const performDelete = async ( + { type, id, options }: PerformDeleteParams, + { + registry, + helpers, + allowedTypes, + client, + serializer, + extensions = {}, + logger, + mappings, + }: ApiExecutionContext +): Promise<{}> => { + const { common: commonHelper, preflight: preflightHelper } = helpers; + const { securityExtension } = extensions; + const namespace = commonHelper.getCurrentNamespace(options.namespace); + + if (!allowedTypes.includes(type)) { + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + + const { refresh = DEFAULT_REFRESH_SETTING, force } = options; + + // we don't need to pass existing namespaces in because we're only concerned with authorizing + // the current space. This saves us from performing the preflight check if we're unauthorized + await securityExtension?.authorizeDelete({ + namespace, + object: { type, id }, + }); + + const rawId = serializer.generateRawId(namespace, type, id); + let preflightResult: PreflightCheckNamespacesResult | undefined; + + if (registry.isMultiNamespace(type)) { + // note: this check throws an error if the object is found but does not exist in this namespace + preflightResult = await preflightHelper.preflightCheckNamespaces({ + type, + id, + namespace, + }); + if ( + preflightResult.checkResult === 'found_outside_namespace' || + preflightResult.checkResult === 'not_found' + ) { + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + const existingNamespaces = preflightResult.savedObjectNamespaces ?? []; + if ( + !force && + (existingNamespaces.length > 1 || existingNamespaces.includes(ALL_NAMESPACES_STRING)) + ) { + throw SavedObjectsErrorHelpers.createBadRequestError( + 'Unable to delete saved object that exists in multiple namespaces, use the `force` option to delete it anyway' + ); + } + } + + const { body, statusCode, headers } = await client.delete( + { + id: rawId, + index: commonHelper.getIndexForType(type), + ...getExpectedVersionProperties(undefined), + refresh, + }, + { ignore: [404], meta: true } + ); + + if (isNotFoundFromUnsupportedServer({ statusCode, headers })) { + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(type, id); + } + + 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, + registry, + client, + getIndexForType: commonHelper.getIndexForType.bind(commonHelper), + 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. + logger.error(`Unable to delete aliases when deleting an object: ${err.message}`); + }); + } + return {}; + } + + const deleteDocNotFound = body.result === 'not_found'; + // @ts-expect-error @elastic/elasticsearch doesn't declare error on DeleteResponse + const deleteIndexNotFound = body.error && body.error.type === 'index_not_found_exception'; + if (deleteDocNotFound || deleteIndexNotFound) { + // see "404s from missing index" above + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + + throw new Error( + `Unexpected Elasticsearch DELETE response: ${JSON.stringify({ + type, + id, + response: { body, statusCode }, + })}` + ); +}; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts index 34cb2359fac8..0955eecd269d 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts @@ -6,5 +6,7 @@ * Side Public License, v 1. */ +export type { ApiExecutionContext } from './types'; export { performCreate } from './create'; export { performBulkCreate } from './bulk_create'; +export { performDelete } from './delete'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/types.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/types.ts new file mode 100644 index 000000000000..da3e19bdf56a --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/types.ts @@ -0,0 +1,29 @@ +/* + * 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 { Logger } from '@kbn/logging'; +import { + type ISavedObjectTypeRegistry, + type SavedObjectsExtensions, + type ISavedObjectsSerializer, +} from '@kbn/core-saved-objects-server'; +import { IKibanaMigrator, IndexMapping } from '@kbn/core-saved-objects-base-server-internal'; +import type { RepositoryHelpers } from '../helpers'; +import type { RepositoryEsClient } from '../repository_es_client'; + +export interface ApiExecutionContext { + registry: ISavedObjectTypeRegistry; + helpers: RepositoryHelpers; + extensions: SavedObjectsExtensions; + client: RepositoryEsClient; + allowedTypes: string[]; + serializer: ISavedObjectsSerializer; + migrator: IKibanaMigrator; + logger: Logger; + mappings: IndexMapping; +} diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts index b4f825d9e1b7..4706a8eb6b75 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts @@ -117,14 +117,9 @@ import { type Either, isLeft, isRight, - setManaged, } from './internal_utils'; import { collectMultiNamespaceReferences } from './collect_multi_namespace_references'; import { updateObjectsSpaces } from './update_objects_spaces'; -import { - preflightCheckForCreate, - type PreflightCheckForCreateObject, -} from './preflight_check_for_create'; import { deleteLegacyUrlAliases } from './legacy_url_aliases'; import type { BulkDeleteParams, @@ -145,9 +140,9 @@ import { SerializerHelper, type PreflightCheckNamespacesResult, } from './helpers'; -import { isFoundGetResponse, getSavedObjectNamespaces } from './utils'; +import { isFoundGetResponse } from './utils'; import { DEFAULT_REFRESH_SETTING, DEFAULT_RETRY_COUNT } from './constants'; -import { performCreate, performBulkCreate } from './apis'; +import { type ApiExecutionContext, performCreate, performBulkCreate, performDelete } from './apis'; // 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. @@ -195,6 +190,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { private preflightCheckHelper: PreflightCheckHelper; private serializerHelper: SerializerHelper; + private apiExecutionContext: ApiExecutionContext; private readonly extensions: SavedObjectsExtensions; private readonly helpers: RepositoryHelpers; @@ -305,6 +301,17 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { encryption: this.encryptionHelper, serializer: this.serializerHelper, }; + this.apiExecutionContext = { + client: this.client, + extensions: this.extensions, + helpers: this.helpers, + allowedTypes: this._allowedTypes, + registry: this._registry, + serializer: this._serializer, + migrator: this._migrator, + mappings: this._mappings, + logger: this._logger, + }; } /** @@ -315,21 +322,13 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { attributes: T, options: SavedObjectsCreateOptions = {} ): Promise> { - return performCreate( + return await performCreate( { type, attributes, options, }, - { - client: this.client, - extensions: this.extensions, - helpers: this.helpers, - allowedTypes: this._allowedTypes, - registry: this._registry, - serializer: this._serializer, - migrator: this._migrator, - } + this.apiExecutionContext ); } @@ -340,20 +339,12 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { objects: Array>, options: SavedObjectsCreateOptions = {} ): Promise> { - return performBulkCreate( + return await performBulkCreate( { objects, options, }, - { - client: this.client, - extensions: this.extensions, - helpers: this.helpers, - allowedTypes: this._allowedTypes, - registry: this._registry, - serializer: this._serializer, - migrator: this._migrator, - } + this.apiExecutionContext ); } @@ -462,100 +453,13 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { * {@inheritDoc ISavedObjectsRepository.delete} */ async delete(type: string, id: string, options: SavedObjectsDeleteOptions = {}): Promise<{}> { - const namespace = this.getCurrentNamespace(options.namespace); - - if (!this._allowedTypes.includes(type)) { - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - - const { refresh = DEFAULT_REFRESH_SETTING, force } = options; - - // we don't need to pass existing namespaces in because we're only concerned with authorizing - // the current space. This saves us from performing the preflight check if we're unauthorized - await this._securityExtension?.authorizeDelete({ - namespace, - object: { type, id }, - }); - - const rawId = this._serializer.generateRawId(namespace, type, id); - let preflightResult: PreflightCheckNamespacesResult | undefined; - - if (this._registry.isMultiNamespace(type)) { - // note: this check throws an error if the object is found but does not exist in this namespace - preflightResult = await this.preflightCheckHelper.preflightCheckNamespaces({ - type, - id, - namespace, - }); - if ( - preflightResult.checkResult === 'found_outside_namespace' || - preflightResult.checkResult === 'not_found' - ) { - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - const existingNamespaces = preflightResult.savedObjectNamespaces ?? []; - if ( - !force && - (existingNamespaces.length > 1 || existingNamespaces.includes(ALL_NAMESPACES_STRING)) - ) { - throw SavedObjectsErrorHelpers.createBadRequestError( - 'Unable to delete saved object that exists in multiple namespaces, use the `force` option to delete it anyway' - ); - } - } - - const { body, statusCode, headers } = await this.client.delete( + return await performDelete( { - id: rawId, - index: this.getIndexForType(type), - ...getExpectedVersionProperties(undefined), - refresh, - }, - { ignore: [404], meta: true } - ); - - if (isNotFoundFromUnsupportedServer({ statusCode, headers })) { - throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(type, id); - } - - 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 {}; - } - - const deleteDocNotFound = body.result === 'not_found'; - // @ts-expect-error @elastic/elasticsearch doesn't declare error on DeleteResponse - const deleteIndexNotFound = body.error && body.error.type === 'index_not_found_exception'; - if (deleteDocNotFound || deleteIndexNotFound) { - // see "404s from missing index" above - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - - throw new Error( - `Unexpected Elasticsearch DELETE response: ${JSON.stringify({ type, id, - response: { body, statusCode }, - })}` + options, + }, + this.apiExecutionContext ); } From 2e1add95844f28ba8cf5c62396e7f0d6d22adf21 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 9 May 2023 11:46:41 +0200 Subject: [PATCH 05/34] extract checkConflicts --- .../src/lib/apis/check_conflicts.ts | 134 ++++++++++++++++++ .../src/lib/apis/index.ts | 1 + .../src/lib/internal_utils.ts | 5 + .../src/lib/repository.ts | 113 ++------------- 4 files changed, 155 insertions(+), 98 deletions(-) create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/check_conflicts.ts diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/check_conflicts.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/check_conflicts.ts new file mode 100644 index 000000000000..0ec44db1d174 --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/check_conflicts.ts @@ -0,0 +1,134 @@ +/* + * 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 { Payload } from '@hapi/boom'; +import { isNotFoundFromUnsupportedServer } from '@kbn/core-elasticsearch-server-internal'; +import { + SavedObjectsErrorHelpers, + SavedObjectsRawDocSource, + SavedObjectsRawDoc, +} from '@kbn/core-saved-objects-server'; +import { + SavedObjectsCheckConflictsObject, + SavedObjectsBaseOptions, + SavedObjectsCheckConflictsResponse, +} from '@kbn/core-saved-objects-api-server'; +import { + Either, + errorContent, + isLeft, + isRight, + isMgetDoc, + rawDocExistsInNamespace, +} from '../internal_utils'; +import { ApiExecutionContext } from './types'; + +export interface PerformCheckConflictsParams { + objects: SavedObjectsCheckConflictsObject[]; + options: SavedObjectsBaseOptions; +} + +export const performCheckConflicts = async ( + { objects, options }: PerformCheckConflictsParams, + { registry, helpers, allowedTypes, client, serializer, extensions = {} }: ApiExecutionContext +): Promise => { + const { common: commonHelper } = helpers; + const { securityExtension } = extensions; + + const namespace = commonHelper.getCurrentNamespace(options.namespace); + + if (objects.length === 0) { + return { errors: [] }; + } + + let bulkGetRequestIndexCounter = 0; + type ExpectedBulkGetResult = Either< + { type: string; id: string; error: Payload }, + { type: string; id: string; esRequestIndex: number } + >; + const expectedBulkGetResults = objects.map((object) => { + const { type, id } = object; + + if (!allowedTypes.includes(type)) { + return { + tag: 'Left', + value: { + id, + type, + error: errorContent(SavedObjectsErrorHelpers.createUnsupportedTypeError(type)), + }, + }; + } + + return { + tag: 'Right', + value: { + type, + id, + esRequestIndex: bulkGetRequestIndexCounter++, + }, + }; + }); + + const validObjects = expectedBulkGetResults.filter(isRight); + await securityExtension?.authorizeCheckConflicts({ + namespace, + objects: validObjects.map((element) => ({ type: element.value.type, id: element.value.id })), + }); + + const bulkGetDocs = validObjects.map(({ value: { type, id } }) => ({ + _id: serializer.generateRawId(namespace, type, id), + _index: commonHelper.getIndexForType(type), + _source: { includes: ['type', 'namespaces'] }, + })); + const bulkGetResponse = bulkGetDocs.length + ? await client.mget( + { + body: { + docs: bulkGetDocs, + }, + }, + { ignore: [404], meta: true } + ) + : undefined; + // throw if we can't verify a 404 response is from Elasticsearch + if ( + bulkGetResponse && + isNotFoundFromUnsupportedServer({ + statusCode: bulkGetResponse.statusCode, + headers: bulkGetResponse.headers, + }) + ) { + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(); + } + + const errors: SavedObjectsCheckConflictsResponse['errors'] = []; + expectedBulkGetResults.forEach((expectedResult) => { + if (isLeft(expectedResult)) { + errors.push(expectedResult.value as any); + return; + } + + const { type, id, esRequestIndex } = expectedResult.value; + const doc = bulkGetResponse?.body.docs[esRequestIndex]; + if (isMgetDoc(doc) && doc.found) { + errors.push({ + id, + type, + error: { + ...errorContent(SavedObjectsErrorHelpers.createConflictError(type, id)), + ...(!rawDocExistsInNamespace(registry, doc! as SavedObjectsRawDoc, namespace) && { + metadata: { isNotOverwritable: true }, + }), + }, + }); + } + }); + + return { errors }; +}; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts index 0955eecd269d..cee363d7296b 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts @@ -10,3 +10,4 @@ export type { ApiExecutionContext } from './types'; export { performCreate } from './create'; export { performBulkCreate } from './bulk_create'; export { performDelete } from './delete'; +export { performCheckConflicts } from './check_conflicts'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_utils.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_utils.ts index 12b53fb480e9..688e292be9fe 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_utils.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_utils.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { Payload } from '@hapi/boom'; import { type ISavedObjectTypeRegistry, @@ -301,3 +302,7 @@ export function setManaged({ * Extracts the contents of a decorated error to return the attributes for bulk operations. */ export const errorContent = (error: DecoratedError) => error.output.payload; + +export function isMgetDoc(doc?: estypes.MgetResponseItem): doc is estypes.GetGetResult { + return Boolean(doc && 'found' in doc); +} diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts index 4706a8eb6b75..e56c1e3622dd 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts @@ -75,7 +75,6 @@ import { type ISavedObjectsSecurityExtension, type ISavedObjectsSpacesExtension, type CheckAuthorizationResult, - AuthorizeCreateObject, AuthorizeUpdateObject, type AuthorizeBulkGetObject, type SavedObject, @@ -117,6 +116,7 @@ import { type Either, isLeft, isRight, + isMgetDoc, } from './internal_utils'; import { collectMultiNamespaceReferences } from './collect_multi_namespace_references'; import { updateObjectsSpaces } from './update_objects_spaces'; @@ -142,7 +142,13 @@ import { } from './helpers'; import { isFoundGetResponse } from './utils'; import { DEFAULT_REFRESH_SETTING, DEFAULT_RETRY_COUNT } from './constants'; -import { type ApiExecutionContext, performCreate, performBulkCreate, performDelete } from './apis'; +import { + type ApiExecutionContext, + performCreate, + performBulkCreate, + performDelete, + performCheckConflicts, +} from './apis'; // 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. @@ -161,10 +167,6 @@ export interface SavedObjectsRepositoryOptions { const MAX_CONCURRENT_ALIAS_DELETIONS = 10; -function isMgetDoc(doc?: estypes.MgetResponseItem): doc is estypes.GetGetResult { - return Boolean(doc && 'found' in doc); -} - /** * Saved Objects Respositiry - the client entry point for saved object manipulation. * @@ -355,98 +357,13 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { objects: SavedObjectsCheckConflictsObject[] = [], options: SavedObjectsBaseOptions = {} ): Promise { - const namespace = this.getCurrentNamespace(options.namespace); - - if (objects.length === 0) { - return { errors: [] }; - } - - let bulkGetRequestIndexCounter = 0; - type ExpectedBulkGetResult = Either< - { type: string; id: string; error: Payload }, - { type: string; id: string; esRequestIndex: number } - >; - const expectedBulkGetResults = objects.map((object) => { - const { type, id } = object; - - if (!this._allowedTypes.includes(type)) { - return { - tag: 'Left', - value: { - id, - type, - error: errorContent(SavedObjectsErrorHelpers.createUnsupportedTypeError(type)), - }, - }; - } - - return { - tag: 'Right', - value: { - type, - id, - esRequestIndex: bulkGetRequestIndexCounter++, - }, - }; - }); - - const validObjects = expectedBulkGetResults.filter(isRight); - await this._securityExtension?.authorizeCheckConflicts({ - namespace, - objects: validObjects.map((element) => ({ type: element.value.type, id: element.value.id })), - }); - - const bulkGetDocs = validObjects.map(({ value: { type, id } }) => ({ - _id: this._serializer.generateRawId(namespace, type, id), - _index: this.getIndexForType(type), - _source: { includes: ['type', 'namespaces'] }, - })); - const bulkGetResponse = bulkGetDocs.length - ? await this.client.mget( - { - body: { - docs: bulkGetDocs, - }, - }, - { ignore: [404], meta: true } - ) - : undefined; - // throw if we can't verify a 404 response is from Elasticsearch - if ( - bulkGetResponse && - isNotFoundFromUnsupportedServer({ - statusCode: bulkGetResponse.statusCode, - headers: bulkGetResponse.headers, - }) - ) { - throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(); - } - - const errors: SavedObjectsCheckConflictsResponse['errors'] = []; - expectedBulkGetResults.forEach((expectedResult) => { - if (isLeft(expectedResult)) { - errors.push(expectedResult.value as any); - return; - } - - const { type, id, esRequestIndex } = expectedResult.value; - const doc = bulkGetResponse?.body.docs[esRequestIndex]; - if (isMgetDoc(doc) && doc.found) { - errors.push({ - id, - type, - error: { - ...errorContent(SavedObjectsErrorHelpers.createConflictError(type, id)), - // @ts-expect-error MultiGetHit._source is optional - ...(!this.rawDocExistsInNamespace(doc!, namespace) && { - metadata: { isNotOverwritable: true }, - }), - }, - }); - } - }); - - return { errors }; + return await performCheckConflicts( + { + objects, + options, + }, + this.apiExecutionContext + ); } /** From bd51cd397d274dd3161b55dc5117a9229554f17a Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 9 May 2023 12:08:44 +0200 Subject: [PATCH 06/34] extracting bulkDelete --- .../src/lib/apis/bulk_delete.ts | 338 ++++++++++++++++++ .../src/lib/apis/index.ts | 1 + .../src/lib/constants.ts | 1 + .../src/lib/helpers/preflight_check.ts | 39 +- .../src/lib/repository.ts | 330 +---------------- 5 files changed, 386 insertions(+), 323 deletions(-) create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_delete.ts diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_delete.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_delete.ts new file mode 100644 index 000000000000..c3680fff6370 --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_delete.ts @@ -0,0 +1,338 @@ +/* + * 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 pMap from 'p-map'; +import { + AuthorizeUpdateObject, + SavedObjectsErrorHelpers, + ISavedObjectTypeRegistry, + SavedObjectsRawDoc, +} from '@kbn/core-saved-objects-server'; +import { ALL_NAMESPACES_STRING, SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server'; +import { + SavedObjectsBulkDeleteObject, + SavedObjectsBulkDeleteOptions, + SavedObjectsBulkDeleteResponse, +} from '@kbn/core-saved-objects-api-server'; +import { DEFAULT_REFRESH_SETTING, MAX_CONCURRENT_ALIAS_DELETIONS } from '../constants'; +import { + errorContent, + getBulkOperationError, + getExpectedVersionProperties, + isLeft, + isMgetDoc, + rawDocExistsInNamespace, + isRight, +} from '../internal_utils'; +import type { ApiExecutionContext } from './types'; +import { deleteLegacyUrlAliases } from '../legacy_url_aliases'; +import { + BulkDeleteExpectedBulkGetResult, + BulkDeleteItemErrorResult, + BulkDeleteParams, + ExpectedBulkDeleteMultiNamespaceDocsParams, + ExpectedBulkDeleteResult, + NewBulkItemResponse, + ObjectToDeleteAliasesFor, +} from '../repository_bulk_delete_internal_types'; + +export interface PerformBulkDeleteParams { + objects: SavedObjectsBulkDeleteObject[]; + options: SavedObjectsBulkDeleteOptions; +} + +export const performBulkDelete = async ( + { objects, options }: PerformBulkDeleteParams, + { + registry, + helpers, + allowedTypes, + client, + serializer, + extensions = {}, + logger, + mappings, + }: ApiExecutionContext +): Promise => { + const { common: commonHelper, preflight: preflightHelper } = helpers; + const { securityExtension } = extensions; + + const { refresh = DEFAULT_REFRESH_SETTING, force } = options; + const namespace = commonHelper.getCurrentNamespace(options.namespace); + + const expectedBulkGetResults = presortObjectsByNamespaceType(objects, allowedTypes, registry); + if (expectedBulkGetResults.length === 0) { + return { statuses: [] }; + } + + const multiNamespaceDocsResponse = await preflightHelper.preflightCheckForBulkDelete({ + expectedBulkGetResults, + namespace, + }); + + // First round of filtering (Left: object doesn't exist/doesn't exist in namespace, Right: good to proceed) + const expectedBulkDeleteMultiNamespaceDocsResults = + getExpectedBulkDeleteMultiNamespaceDocsResults( + { + expectedBulkGetResults, + multiNamespaceDocsResponse, + namespace, + force, + }, + registry + ); + + if (securityExtension) { + // Perform Auth Check (on both L/R, we'll deal with that later) + const authObjects: AuthorizeUpdateObject[] = expectedBulkDeleteMultiNamespaceDocsResults.map( + (element) => { + const index = (element.value as { esRequestIndex: number }).esRequestIndex; + const { type, id } = element.value; + const preflightResult = + index !== undefined ? multiNamespaceDocsResponse?.body.docs[index] : undefined; + + return { + type, + id, + // @ts-expect-error _source optional here + existingNamespaces: preflightResult?._source?.namespaces ?? [], + }; + } + ); + await securityExtension.authorizeBulkDelete({ namespace, objects: authObjects }); + } + + // Filter valid objects + const validObjects = expectedBulkDeleteMultiNamespaceDocsResults.filter(isRight); + if (validObjects.length === 0) { + // We only have error results; return early to avoid potentially trying authZ checks for 0 types which would result in an exception. + const savedObjects = expectedBulkDeleteMultiNamespaceDocsResults + .filter(isLeft) + .map((expectedResult) => { + return { ...expectedResult.value, success: false }; + }); + return { statuses: [...savedObjects] }; + } + + // Create the bulkDeleteParams + const bulkDeleteParams: BulkDeleteParams[] = []; + validObjects.map((expectedResult) => { + bulkDeleteParams.push({ + delete: { + _id: serializer.generateRawId( + namespace, + expectedResult.value.type, + expectedResult.value.id + ), + _index: commonHelper.getIndexForType(expectedResult.value.type), + ...getExpectedVersionProperties(undefined), + }, + }); + }); + + const bulkDeleteResponse = bulkDeleteParams.length + ? await client.bulk({ + refresh, + body: bulkDeleteParams, + require_alias: true, + }) + : undefined; + + // extracted to ensure consistency in the error results returned + let errorResult: BulkDeleteItemErrorResult; + const objectsToDeleteAliasesFor: ObjectToDeleteAliasesFor[] = []; + + const savedObjects = expectedBulkDeleteMultiNamespaceDocsResults.map((expectedResult) => { + if (isLeft(expectedResult)) { + return { ...expectedResult.value, success: false }; + } + const { type, id, namespaces, esRequestIndex: esBulkDeleteRequestIndex } = expectedResult.value; + // we assume this wouldn't happen but is needed to ensure type consistency + if (bulkDeleteResponse === undefined) { + throw new Error( + `Unexpected error in bulkDelete saved objects: bulkDeleteResponse is undefined` + ); + } + const rawResponse = Object.values( + bulkDeleteResponse.items[esBulkDeleteRequestIndex] + )[0] as NewBulkItemResponse; + + const error = getBulkOperationError(type, id, rawResponse); + if (error) { + errorResult = { success: false, type, id, error }; + return errorResult; + } + if (rawResponse.result === 'not_found') { + errorResult = { + success: false, + type, + id, + error: errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)), + }; + return errorResult; + } + + if (rawResponse.result === 'deleted') { + // `namespaces` should only exist in the expectedResult.value if the type is multi-namespace. + if (namespaces) { + objectsToDeleteAliasesFor.push({ + type, + id, + ...(namespaces.includes(ALL_NAMESPACES_STRING) + ? { namespaces: [], deleteBehavior: 'exclusive' } + : { namespaces, deleteBehavior: 'inclusive' }), + }); + } + } + const successfulResult = { + success: true, + id, + type, + }; + return successfulResult; + }); + + // Delete aliases if necessary, ensuring we don't have too many concurrent operations running. + const mapper = async ({ type, id, namespaces, deleteBehavior }: ObjectToDeleteAliasesFor) => { + await deleteLegacyUrlAliases({ + mappings, + registry, + client, + getIndexForType: commonHelper.getIndexForType.bind(commonHelper), + type, + id, + namespaces, + deleteBehavior, + }).catch((err) => { + logger.error(`Unable to delete aliases when deleting an object: ${err.message}`); + }); + }; + await pMap(objectsToDeleteAliasesFor, mapper, { concurrency: MAX_CONCURRENT_ALIAS_DELETIONS }); + + return { statuses: [...savedObjects] }; +}; + +/** + * Performs initial checks on object type validity and flags multi-namespace objects for preflight checks by adding an `esRequestIndex` + * @returns array BulkDeleteExpectedBulkGetResult[] + */ +function presortObjectsByNamespaceType( + objects: SavedObjectsBulkDeleteObject[], + allowedTypes: string[], + registry: ISavedObjectTypeRegistry +) { + let bulkGetRequestIndexCounter = 0; + return objects.map((object) => { + const { type, id } = object; + if (!allowedTypes.includes(type)) { + return { + tag: 'Left', + value: { + id, + type, + error: errorContent(SavedObjectsErrorHelpers.createUnsupportedTypeError(type)), + }, + }; + } + const requiresNamespacesCheck = registry.isMultiNamespace(type); + return { + tag: 'Right', + value: { + type, + id, + ...(requiresNamespacesCheck && { esRequestIndex: bulkGetRequestIndexCounter++ }), + }, + }; + }); +} + +/** + * @returns array of objects sorted by expected delete success or failure result + * @internal + */ +function getExpectedBulkDeleteMultiNamespaceDocsResults( + params: ExpectedBulkDeleteMultiNamespaceDocsParams, + registry: ISavedObjectTypeRegistry +): ExpectedBulkDeleteResult[] { + const { expectedBulkGetResults, multiNamespaceDocsResponse, namespace, force } = params; + let indexCounter = 0; + const expectedBulkDeleteMultiNamespaceDocsResults = + expectedBulkGetResults.map((expectedBulkGetResult) => { + if (isLeft(expectedBulkGetResult)) { + return { ...expectedBulkGetResult }; + } + const { esRequestIndex: esBulkGetRequestIndex, id, type } = expectedBulkGetResult.value; + + let namespaces; + + if (esBulkGetRequestIndex !== undefined) { + const indexFound = multiNamespaceDocsResponse?.statusCode !== 404; + + const actualResult = indexFound + ? multiNamespaceDocsResponse?.body.docs[esBulkGetRequestIndex] + : undefined; + + const docFound = indexFound && isMgetDoc(actualResult) && actualResult.found; + + // return an error if the doc isn't found at all or the doc doesn't exist in the namespaces + if (!docFound) { + return { + tag: 'Left', + value: { + id, + type, + error: errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)), + }, + }; + } + // the following check should be redundant since we're retrieving the docs from elasticsearch but we check just to make sure + if (!rawDocExistsInNamespace(registry, actualResult as SavedObjectsRawDoc, namespace)) { + return { + tag: 'Left', + value: { + id, + type, + error: errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)), + }, + }; + } + // @ts-expect-error MultiGetHit is incorrectly missing _id, _source + namespaces = actualResult!._source.namespaces ?? [ + SavedObjectsUtils.namespaceIdToString(namespace), + ]; + const useForce = force && force === true; + // the document is shared to more than one space and can only be deleted by force. + if (!useForce && (namespaces.length > 1 || namespaces.includes(ALL_NAMESPACES_STRING))) { + return { + tag: 'Left', + value: { + success: false, + id, + type, + error: errorContent( + SavedObjectsErrorHelpers.createBadRequestError( + 'Unable to delete saved object that exists in multiple namespaces, use the `force` option to delete it anyway' + ) + ), + }, + }; + } + } + // contains all objects that passed initial preflight checks, including single namespace objects that skipped the mget call + // single namespace objects will have namespaces:undefined + const expectedResult = { + type, + id, + namespaces, + esRequestIndex: indexCounter++, + }; + + return { tag: 'Right', value: expectedResult }; + }); + return expectedBulkDeleteMultiNamespaceDocsResults; +} diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts index cee363d7296b..566f4bb506e3 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts @@ -11,3 +11,4 @@ export { performCreate } from './create'; export { performBulkCreate } from './bulk_create'; export { performDelete } from './delete'; export { performCheckConflicts } from './check_conflicts'; +export { performBulkDelete } from './bulk_delete'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/constants.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/constants.ts index d0eb4e7827d2..3b429a2a7dfa 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/constants.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/constants.ts @@ -8,3 +8,4 @@ export const DEFAULT_REFRESH_SETTING = 'wait_for'; export const DEFAULT_RETRY_COUNT = 3; +export const MAX_CONCURRENT_ALIAS_DELETIONS = 10; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/preflight_check.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/preflight_check.ts index 868d0d849dd1..42f7c6daccd3 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/preflight_check.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/preflight_check.ts @@ -14,7 +14,7 @@ import type { import { ISavedObjectsRepository } from '@kbn/core-saved-objects-api-server'; import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server'; import { SavedObjectsErrorHelpers, SavedObjectsRawDocSource } from '@kbn/core-saved-objects-server'; -import { rawDocExistsInNamespaces } from '../internal_utils'; +import { isRight, rawDocExistsInNamespaces } from '../internal_utils'; import { getSavedObjectNamespaces } from '../utils/namespaces'; import { GetResponseFound, isFoundGetResponse } from '../utils/es_responses'; import { @@ -22,6 +22,7 @@ import { PreflightCheckForCreateObject, } from '../preflight_check_for_create'; import type { RepositoryEsClient } from '../repository_es_client'; +import type { PreflightCheckForBulkDeleteParams } from '../repository_bulk_delete_internal_types'; export class PreflightCheckHelper { private registry: ISavedObjectTypeRegistry; @@ -61,6 +62,42 @@ export class PreflightCheckHelper { }); } + /** + * Fetch multi-namespace saved objects + * @returns MgetResponse + * @notes multi-namespace objects shared to more than one space require special handling. We fetch these docs to retrieve their namespaces. + * @internal + */ + public async preflightCheckForBulkDelete(params: PreflightCheckForBulkDeleteParams) { + const { expectedBulkGetResults, namespace } = params; + const bulkGetMultiNamespaceDocs = expectedBulkGetResults + .filter(isRight) + .filter(({ value }) => value.esRequestIndex !== undefined) + .map(({ value: { type, id } }) => ({ + _id: this.serializer.generateRawId(namespace, type, id), + _index: this.getIndexForType(type), + _source: ['type', 'namespaces'], + })); + + const bulkGetMultiNamespaceDocsResponse = bulkGetMultiNamespaceDocs.length + ? await this.client.mget( + { body: { docs: bulkGetMultiNamespaceDocs } }, + { ignore: [404], meta: true } + ) + : undefined; + // fail fast if we can't verify a 404 response is from Elasticsearch + if ( + bulkGetMultiNamespaceDocsResponse && + isNotFoundFromUnsupportedServer({ + statusCode: bulkGetMultiNamespaceDocsResponse.statusCode, + headers: bulkGetMultiNamespaceDocsResponse.headers, + }) + ) { + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(); + } + return bulkGetMultiNamespaceDocsResponse; + } + /** * Pre-flight check to ensure that a multi-namespace object exists in the current namespace. */ diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts index e56c1e3622dd..9f0851f10b9f 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts @@ -96,7 +96,6 @@ import { type IndexMapping, type IKibanaMigrator, } from '@kbn/core-saved-objects-base-server-internal'; -import pMap from 'p-map'; import { PointInTimeFinder } from './point_in_time_finder'; import { createRepositoryEsClient, type RepositoryEsClient } from './repository_es_client'; import { getSearchDsl } from './search_dsl'; @@ -120,17 +119,6 @@ import { } from './internal_utils'; import { collectMultiNamespaceReferences } from './collect_multi_namespace_references'; import { updateObjectsSpaces } from './update_objects_spaces'; -import { deleteLegacyUrlAliases } from './legacy_url_aliases'; -import type { - BulkDeleteParams, - ExpectedBulkDeleteResult, - BulkDeleteItemErrorResult, - NewBulkItemResponse, - BulkDeleteExpectedBulkGetResult, - PreflightCheckForBulkDeleteParams, - ExpectedBulkDeleteMultiNamespaceDocsParams, - ObjectToDeleteAliasesFor, -} from './repository_bulk_delete_internal_types'; import { RepositoryHelpers, CommonHelper, @@ -148,6 +136,7 @@ import { performBulkCreate, performDelete, performCheckConflicts, + performBulkDelete, } from './apis'; // BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository @@ -165,8 +154,6 @@ export interface SavedObjectsRepositoryOptions { extensions?: SavedObjectsExtensions; } -const MAX_CONCURRENT_ALIAS_DELETIONS = 10; - /** * Saved Objects Respositiry - the client entry point for saved object manipulation. * @@ -380,160 +367,6 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { ); } - /** - * Performs initial checks on object type validity and flags multi-namespace objects for preflight checks by adding an `esRequestIndex` - * @param objects SavedObjectsBulkDeleteObject[] - * @returns array BulkDeleteExpectedBulkGetResult[] - * @internal - */ - private presortObjectsByNamespaceType(objects: SavedObjectsBulkDeleteObject[]) { - let bulkGetRequestIndexCounter = 0; - return objects.map((object) => { - const { type, id } = object; - if (!this._allowedTypes.includes(type)) { - return { - tag: 'Left', - value: { - id, - type, - error: errorContent(SavedObjectsErrorHelpers.createUnsupportedTypeError(type)), - }, - }; - } - const requiresNamespacesCheck = this._registry.isMultiNamespace(type); - return { - tag: 'Right', - value: { - type, - id, - ...(requiresNamespacesCheck && { esRequestIndex: bulkGetRequestIndexCounter++ }), - }, - }; - }); - } - - /** - * Fetch multi-namespace saved objects - * @returns MgetResponse - * @notes multi-namespace objects shared to more than one space require special handling. We fetch these docs to retrieve their namespaces. - * @internal - */ - private async preflightCheckForBulkDelete(params: PreflightCheckForBulkDeleteParams) { - const { expectedBulkGetResults, namespace } = params; - const bulkGetMultiNamespaceDocs = expectedBulkGetResults - .filter(isRight) - .filter(({ value }) => value.esRequestIndex !== undefined) - .map(({ value: { type, id } }) => ({ - _id: this._serializer.generateRawId(namespace, type, id), - _index: this.getIndexForType(type), - _source: ['type', 'namespaces'], - })); - - const bulkGetMultiNamespaceDocsResponse = bulkGetMultiNamespaceDocs.length - ? await this.client.mget( - { body: { docs: bulkGetMultiNamespaceDocs } }, - { ignore: [404], meta: true } - ) - : undefined; - // fail fast if we can't verify a 404 response is from Elasticsearch - if ( - bulkGetMultiNamespaceDocsResponse && - isNotFoundFromUnsupportedServer({ - statusCode: bulkGetMultiNamespaceDocsResponse.statusCode, - headers: bulkGetMultiNamespaceDocsResponse.headers, - }) - ) { - throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(); - } - return bulkGetMultiNamespaceDocsResponse; - } - - /** - * @returns array of objects sorted by expected delete success or failure result - * @internal - */ - private getExpectedBulkDeleteMultiNamespaceDocsResults( - params: ExpectedBulkDeleteMultiNamespaceDocsParams - ): ExpectedBulkDeleteResult[] { - const { expectedBulkGetResults, multiNamespaceDocsResponse, namespace, force } = params; - let indexCounter = 0; - const expectedBulkDeleteMultiNamespaceDocsResults = - expectedBulkGetResults.map((expectedBulkGetResult) => { - if (isLeft(expectedBulkGetResult)) { - return { ...expectedBulkGetResult }; - } - const { esRequestIndex: esBulkGetRequestIndex, id, type } = expectedBulkGetResult.value; - - let namespaces; - - if (esBulkGetRequestIndex !== undefined) { - const indexFound = multiNamespaceDocsResponse?.statusCode !== 404; - - const actualResult = indexFound - ? multiNamespaceDocsResponse?.body.docs[esBulkGetRequestIndex] - : undefined; - - const docFound = indexFound && isMgetDoc(actualResult) && actualResult.found; - - // return an error if the doc isn't found at all or the doc doesn't exist in the namespaces - if (!docFound) { - return { - tag: 'Left', - value: { - id, - type, - error: errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)), - }, - }; - } - // the following check should be redundant since we're retrieving the docs from elasticsearch but we check just to make sure - // @ts-expect-error MultiGetHit is incorrectly missing _id, _source - if (!this.rawDocExistsInNamespace(actualResult, namespace)) { - return { - tag: 'Left', - value: { - id, - type, - error: errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)), - }, - }; - } - // @ts-expect-error MultiGetHit is incorrectly missing _id, _source - namespaces = actualResult!._source.namespaces ?? [ - SavedObjectsUtils.namespaceIdToString(namespace), - ]; - const useForce = force && force === true; - // the document is shared to more than one space and can only be deleted by force. - if (!useForce && (namespaces.length > 1 || namespaces.includes(ALL_NAMESPACES_STRING))) { - return { - tag: 'Left', - value: { - success: false, - id, - type, - error: errorContent( - SavedObjectsErrorHelpers.createBadRequestError( - 'Unable to delete saved object that exists in multiple namespaces, use the `force` option to delete it anyway' - ) - ), - }, - }; - } - } - // contains all objects that passed initial preflight checks, including single namespace objects that skipped the mget call - // single namespace objects will have namespaces:undefined - const expectedResult = { - type, - id, - namespaces, - esRequestIndex: indexCounter++, - }; - - return { tag: 'Right', value: expectedResult }; - }); - return expectedBulkDeleteMultiNamespaceDocsResults; - } - /** * {@inheritDoc ISavedObjectsRepository.bulkDelete} */ @@ -541,160 +374,13 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { objects: SavedObjectsBulkDeleteObject[], options: SavedObjectsBulkDeleteOptions = {} ): Promise { - const { refresh = DEFAULT_REFRESH_SETTING, force } = options; - const namespace = this.getCurrentNamespace(options.namespace); - const expectedBulkGetResults = this.presortObjectsByNamespaceType(objects); - if (expectedBulkGetResults.length === 0) { - return { statuses: [] }; - } - - const multiNamespaceDocsResponse = await this.preflightCheckForBulkDelete({ - expectedBulkGetResults, - namespace, - }); - - // First round of filtering (Left: object doesn't exist/doesn't exist in namespace, Right: good to proceed) - const expectedBulkDeleteMultiNamespaceDocsResults = - this.getExpectedBulkDeleteMultiNamespaceDocsResults({ - expectedBulkGetResults, - multiNamespaceDocsResponse, - namespace, - force, - }); - - if (this._securityExtension) { - // Perform Auth Check (on both L/R, we'll deal with that later) - const authObjects: AuthorizeUpdateObject[] = expectedBulkDeleteMultiNamespaceDocsResults.map( - (element) => { - const index = (element.value as { esRequestIndex: number }).esRequestIndex; - const { type, id } = element.value; - const preflightResult = - index !== undefined ? multiNamespaceDocsResponse?.body.docs[index] : undefined; - - return { - type, - id, - // @ts-expect-error MultiGetHit._source is optional - existingNamespaces: preflightResult?._source?.namespaces ?? [], - }; - } - ); - await this._securityExtension.authorizeBulkDelete({ namespace, objects: authObjects }); - } - - // Filter valid objects - const validObjects = expectedBulkDeleteMultiNamespaceDocsResults.filter(isRight); - if (validObjects.length === 0) { - // We only have error results; return early to avoid potentially trying authZ checks for 0 types which would result in an exception. - const savedObjects = expectedBulkDeleteMultiNamespaceDocsResults - .filter(isLeft) - .map((expectedResult) => { - return { ...expectedResult.value, success: false }; - }); - return { statuses: [...savedObjects] }; - } - - // Create the bulkDeleteParams - const bulkDeleteParams: BulkDeleteParams[] = []; - validObjects.map((expectedResult) => { - bulkDeleteParams.push({ - delete: { - _id: this._serializer.generateRawId( - namespace, - expectedResult.value.type, - expectedResult.value.id - ), - _index: this.getIndexForType(expectedResult.value.type), - ...getExpectedVersionProperties(undefined), - }, - }); - }); - - const bulkDeleteResponse = bulkDeleteParams.length - ? await this.client.bulk({ - refresh, - body: bulkDeleteParams, - require_alias: true, - }) - : undefined; - - // extracted to ensure consistency in the error results returned - let errorResult: BulkDeleteItemErrorResult; - const objectsToDeleteAliasesFor: ObjectToDeleteAliasesFor[] = []; - - const savedObjects = expectedBulkDeleteMultiNamespaceDocsResults.map((expectedResult) => { - if (isLeft(expectedResult)) { - return { ...expectedResult.value, success: false }; - } - const { - type, - id, - namespaces, - esRequestIndex: esBulkDeleteRequestIndex, - } = expectedResult.value; - // we assume this wouldn't happen but is needed to ensure type consistency - if (bulkDeleteResponse === undefined) { - throw new Error( - `Unexpected error in bulkDelete saved objects: bulkDeleteResponse is undefined` - ); - } - const rawResponse = Object.values( - bulkDeleteResponse.items[esBulkDeleteRequestIndex] - )[0] as NewBulkItemResponse; - - const error = getBulkOperationError(type, id, rawResponse); - if (error) { - errorResult = { success: false, type, id, error }; - return errorResult; - } - if (rawResponse.result === 'not_found') { - errorResult = { - success: false, - type, - id, - error: errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)), - }; - return errorResult; - } - - if (rawResponse.result === 'deleted') { - // `namespaces` should only exist in the expectedResult.value if the type is multi-namespace. - if (namespaces) { - objectsToDeleteAliasesFor.push({ - type, - id, - ...(namespaces.includes(ALL_NAMESPACES_STRING) - ? { namespaces: [], deleteBehavior: 'exclusive' } - : { namespaces, deleteBehavior: 'inclusive' }), - }); - } - } - const successfulResult = { - success: true, - id, - type, - }; - return successfulResult; - }); - - // Delete aliases if necessary, ensuring we don't have too many concurrent operations running. - const mapper = async ({ type, id, namespaces, deleteBehavior }: ObjectToDeleteAliasesFor) => { - await deleteLegacyUrlAliases({ - mappings: this._mappings, - registry: this._registry, - client: this.client, - getIndexForType: this.getIndexForType.bind(this), - type, - id, - namespaces, - deleteBehavior, - }).catch((err) => { - this._logger.error(`Unable to delete aliases when deleting an object: ${err.message}`); - }); - }; - await pMap(objectsToDeleteAliasesFor, mapper, { concurrency: MAX_CONCURRENT_ALIAS_DELETIONS }); - - return { statuses: [...savedObjects] }; + return await performBulkDelete( + { + objects, + options, + }, + this.apiExecutionContext + ); } /** From e268959554764b366624ac59c0430186f4b0abcf Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 9 May 2023 12:16:37 +0200 Subject: [PATCH 07/34] extract delete by namespace --- .../src/lib/apis/create.ts | 4 +- .../src/lib/apis/delete_by_namespace.ts | 82 +++++++++++++++++++ .../src/lib/apis/index.ts | 1 + .../src/lib/repository.ts | 57 ++----------- 4 files changed, 89 insertions(+), 55 deletions(-) create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/delete_by_namespace.ts diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/create.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/create.ts index 6fa685d17ee1..1bf86bda3708 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/create.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/create.ts @@ -13,9 +13,7 @@ import { type SavedObjectSanitizedDoc, } from '@kbn/core-saved-objects-server'; import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server'; -import { - decodeRequestVersion, -} from '@kbn/core-saved-objects-base-server-internal'; +import { decodeRequestVersion } from '@kbn/core-saved-objects-base-server-internal'; import { SavedObjectsCreateOptions } from '@kbn/core-saved-objects-api-server'; import { DEFAULT_REFRESH_SETTING } from '../constants'; import { getCurrentTime, normalizeNamespace, setManaged } from '../internal_utils'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/delete_by_namespace.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/delete_by_namespace.ts new file mode 100644 index 000000000000..2848f8d60065 --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/delete_by_namespace.ts @@ -0,0 +1,82 @@ +/* + * 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 { isNotFoundFromUnsupportedServer } from '@kbn/core-elasticsearch-server-internal'; +import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; +import type { SavedObjectsDeleteByNamespaceOptions } from '@kbn/core-saved-objects-api-server'; +import { + getRootPropertiesObjects, + LEGACY_URL_ALIAS_TYPE, +} from '@kbn/core-saved-objects-base-server-internal'; +import type { ApiExecutionContext } from './types'; +import { getSearchDsl } from '../search_dsl'; + +export interface PerformDeleteByNamespaceParams { + namespace: string; + options: SavedObjectsDeleteByNamespaceOptions; +} + +export const performDeleteByNamespace = async ( + { namespace, options }: PerformDeleteByNamespaceParams, + { registry, helpers, client, mappings, extensions = {} }: ApiExecutionContext +): Promise => { + const { common: commonHelper } = helpers; + // This is not exposed on the SOC; authorization and audit logging is handled by the Spaces plugin + if (!namespace || typeof namespace !== 'string' || namespace === '*') { + throw new TypeError(`namespace is required, and must be a string that is not equal to '*'`); + } + + const allTypes = Object.keys(getRootPropertiesObjects(mappings)); + const typesToUpdate = [ + ...allTypes.filter((type) => !registry.isNamespaceAgnostic(type)), + LEGACY_URL_ALIAS_TYPE, + ]; + + // Construct kueryNode to filter legacy URL aliases (these space-agnostic objects do not use root-level "namespace/s" fields) + const { buildNode } = esKuery.nodeTypes.function; + const match1 = buildNode('is', `${LEGACY_URL_ALIAS_TYPE}.targetNamespace`, namespace); + const match2 = buildNode('not', buildNode('is', 'type', LEGACY_URL_ALIAS_TYPE)); + const kueryNode = buildNode('or', [match1, match2]); + + const { body, statusCode, headers } = await client.updateByQuery( + { + index: commonHelper.getIndicesForTypes(typesToUpdate), + refresh: options.refresh, + body: { + script: { + source: ` + if (!ctx._source.containsKey('namespaces')) { + ctx.op = "delete"; + } else { + ctx._source['namespaces'].removeAll(Collections.singleton(params['namespace'])); + if (ctx._source['namespaces'].empty) { + ctx.op = "delete"; + } + } + `, + lang: 'painless', + params: { namespace }, + }, + conflicts: 'proceed', + ...getSearchDsl(mappings, registry, { + namespaces: [namespace], + type: typesToUpdate, + kueryNode, + }), + }, + }, + { ignore: [404], meta: true } + ); + // throw if we can't verify a 404 response is from Elasticsearch + if (isNotFoundFromUnsupportedServer({ statusCode, headers })) { + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(); + } + + return body; +}; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts index 566f4bb506e3..3c58dbebbbf8 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts @@ -12,3 +12,4 @@ export { performBulkCreate } from './bulk_create'; export { performDelete } from './delete'; export { performCheckConflicts } from './check_conflicts'; export { performBulkDelete } from './bulk_delete'; +export { performDeleteByNamespace } from './delete_by_namespace'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts index 9f0851f10b9f..58ba0973ff9a 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts @@ -10,7 +10,6 @@ import { omit, isObject } from 'lodash'; import Boom from '@hapi/boom'; import type { Payload } from '@hapi/boom'; import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import * as esKuery from '@kbn/es-query'; import type { Logger } from '@kbn/logging'; import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import { @@ -91,8 +90,6 @@ import { SavedObjectsSerializer, encodeVersion, encodeHitVersion, - getRootPropertiesObjects, - LEGACY_URL_ALIAS_TYPE, type IndexMapping, type IKibanaMigrator, } from '@kbn/core-saved-objects-base-server-internal'; @@ -137,6 +134,7 @@ import { performDelete, performCheckConflicts, performBulkDelete, + performDeleteByNamespace, } from './apis'; // BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository @@ -390,58 +388,13 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { namespace: string, options: SavedObjectsDeleteByNamespaceOptions = {} ): Promise { - // This is not exposed on the SOC; authorization and audit logging is handled by the Spaces plugin - if (!namespace || typeof namespace !== 'string' || namespace === '*') { - throw new TypeError(`namespace is required, and must be a string that is not equal to '*'`); - } - - const allTypes = Object.keys(getRootPropertiesObjects(this._mappings)); - const typesToUpdate = [ - ...allTypes.filter((type) => !this._registry.isNamespaceAgnostic(type)), - LEGACY_URL_ALIAS_TYPE, - ]; - - // Construct kueryNode to filter legacy URL aliases (these space-agnostic objects do not use root-level "namespace/s" fields) - const { buildNode } = esKuery.nodeTypes.function; - const match1 = buildNode('is', `${LEGACY_URL_ALIAS_TYPE}.targetNamespace`, namespace); - const match2 = buildNode('not', buildNode('is', 'type', LEGACY_URL_ALIAS_TYPE)); - const kueryNode = buildNode('or', [match1, match2]); - - const { body, statusCode, headers } = await this.client.updateByQuery( + return await performDeleteByNamespace( { - index: this.getIndicesForTypes(typesToUpdate), - refresh: options.refresh, - body: { - script: { - source: ` - if (!ctx._source.containsKey('namespaces')) { - ctx.op = "delete"; - } else { - ctx._source['namespaces'].removeAll(Collections.singleton(params['namespace'])); - if (ctx._source['namespaces'].empty) { - ctx.op = "delete"; - } - } - `, - lang: 'painless', - params: { namespace }, - }, - conflicts: 'proceed', - ...getSearchDsl(this._mappings, this._registry, { - namespaces: [namespace], - type: typesToUpdate, - kueryNode, - }), - }, + namespace, + options, }, - { ignore: [404], meta: true } + this.apiExecutionContext ); - // throw if we can't verify a 404 response is from Elasticsearch - if (isNotFoundFromUnsupportedServer({ statusCode, headers })) { - throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(); - } - - return body; } /** From 7b5a8b8568cc4c587bde4d47c9aca72c8a93eb9a Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 9 May 2023 12:39:50 +0200 Subject: [PATCH 08/34] extract find --- .../src/lib/apis/find.ts | 268 ++++++++++++++++++ .../src/lib/apis/index.ts | 1 + .../src/lib/repository.ts | 246 +--------------- 3 files changed, 278 insertions(+), 237 deletions(-) create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/find.ts diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/find.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/find.ts new file mode 100644 index 000000000000..ec24818df90a --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/find.ts @@ -0,0 +1,268 @@ +/* + * 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 Boom from '@hapi/boom'; +import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { isSupportedEsServer } from '@kbn/core-elasticsearch-server-internal'; +import { + SavedObjectsErrorHelpers, + type SavedObjectsRawDoc, + CheckAuthorizationResult, + SavedObjectsRawDocSource, +} from '@kbn/core-saved-objects-server'; +import { + DEFAULT_NAMESPACE_STRING, + FIND_DEFAULT_PAGE, + FIND_DEFAULT_PER_PAGE, + SavedObjectsUtils, +} from '@kbn/core-saved-objects-utils-server'; +import { + SavedObjectsFindOptions, + SavedObjectsFindInternalOptions, + SavedObjectsFindResult, + SavedObjectsFindResponse, +} from '@kbn/core-saved-objects-api-server'; +import { ApiExecutionContext } from './types'; +import { validateConvertFilterToKueryNode } from '../filter_utils'; +import { validateAndConvertAggregations } from '../aggregations'; +import { includedFields } from '../included_fields'; +import { getSearchDsl } from '../search_dsl'; + +export interface PerformFindParams { + options: SavedObjectsFindOptions; + internalOptions: SavedObjectsFindInternalOptions; +} + +export const performFind = async ( + { options, internalOptions }: PerformFindParams, + { + registry, + helpers, + allowedTypes: rawAllowedTypes, + mappings, + client, + serializer, + migrator, + extensions = {}, + }: ApiExecutionContext +): Promise> => { + const { + common: commonHelper, + encryption: encryptionHelper, + serializer: serializerHelper, + } = helpers; + const { securityExtension, spacesExtension } = extensions; + let namespaces!: string[]; + const { disableExtensions } = internalOptions; + if (disableExtensions || !spacesExtension) { + namespaces = options.namespaces ?? [DEFAULT_NAMESPACE_STRING]; + // If the consumer specified `namespaces: []`, throw a Bad Request error + if (namespaces.length === 0) + throw SavedObjectsErrorHelpers.createBadRequestError( + 'options.namespaces cannot be an empty array' + ); + } + + const { + search, + defaultSearchOperator = 'OR', + searchFields, + rootSearchFields, + hasReference, + hasReferenceOperator, + hasNoReference, + hasNoReferenceOperator, + page = FIND_DEFAULT_PAGE, + perPage = FIND_DEFAULT_PER_PAGE, + pit, + searchAfter, + sortField, + sortOrder, + fields, + type, + filter, + preference, + aggs, + migrationVersionCompatibility, + } = options; + + if (!type) { + throw SavedObjectsErrorHelpers.createBadRequestError( + 'options.type must be a string or an array of strings' + ); + } else if (preference?.length && pit) { + throw SavedObjectsErrorHelpers.createBadRequestError( + 'options.preference must be excluded when options.pit is used' + ); + } + + const types = Array.isArray(type) ? type : [type]; + const allowedTypes = types.filter((t) => rawAllowedTypes.includes(t)); + if (allowedTypes.length === 0) { + return SavedObjectsUtils.createEmptyFindResponse(options); + } + + if (searchFields && !Array.isArray(searchFields)) { + throw SavedObjectsErrorHelpers.createBadRequestError('options.searchFields must be an array'); + } + + if (fields && !Array.isArray(fields)) { + throw SavedObjectsErrorHelpers.createBadRequestError('options.fields must be an array'); + } + + let kueryNode; + if (filter) { + try { + kueryNode = validateConvertFilterToKueryNode(allowedTypes, filter, mappings); + } catch (e) { + if (e.name === 'KQLSyntaxError') { + throw SavedObjectsErrorHelpers.createBadRequestError(`KQLSyntaxError: ${e.message}`); + } else { + throw e; + } + } + } + + let aggsObject; + if (aggs) { + try { + aggsObject = validateAndConvertAggregations(allowedTypes, aggs, mappings); + } catch (e) { + throw SavedObjectsErrorHelpers.createBadRequestError(`Invalid aggregation: ${e.message}`); + } + } + + if (!disableExtensions && spacesExtension) { + try { + namespaces = await spacesExtension.getSearchableNamespaces(options.namespaces); + } catch (err) { + if (Boom.isBoom(err) && err.output.payload.statusCode === 403) { + // The user is not authorized to access any space, return an empty response. + return SavedObjectsUtils.createEmptyFindResponse(options); + } + throw err; + } + if (namespaces.length === 0) { + // The user is authorized to access *at least one space*, but not any of the spaces they requested; return an empty response. + return SavedObjectsUtils.createEmptyFindResponse(options); + } + } + + // We have to first perform an initial authorization check so that we can construct the search DSL accordingly + const spacesToAuthorize = new Set(namespaces); + const typesToAuthorize = new Set(types); + let typeToNamespacesMap: Map | undefined; + let authorizationResult: CheckAuthorizationResult | undefined; + if (!disableExtensions && securityExtension) { + authorizationResult = await securityExtension.authorizeFind({ + namespaces: spacesToAuthorize, + types: typesToAuthorize, + }); + if (authorizationResult?.status === 'unauthorized') { + // If the user is unauthorized to find *anything* they requested, return an empty response + return SavedObjectsUtils.createEmptyFindResponse(options); + } + if (authorizationResult?.status === 'partially_authorized') { + typeToNamespacesMap = new Map(); + for (const [objType, entry] of authorizationResult.typeMap) { + if (!entry.find) continue; + // This ensures that the query DSL can filter only for object types that the user is authorized to access for a given space + const { authorizedSpaces, isGloballyAuthorized } = entry.find; + typeToNamespacesMap.set(objType, isGloballyAuthorized ? namespaces : authorizedSpaces); + } + } + } + + const esOptions = { + // If `pit` is provided, we drop the `index`, otherwise ES returns 400. + index: pit ? undefined : commonHelper.getIndicesForTypes(allowedTypes), + // If `searchAfter` is provided, we drop `from` as it will not be used for pagination. + from: searchAfter ? undefined : perPage * (page - 1), + _source: includedFields(allowedTypes, fields), + preference, + rest_total_hits_as_int: true, + size: perPage, + body: { + size: perPage, + seq_no_primary_term: true, + from: perPage * (page - 1), + _source: includedFields(allowedTypes, fields), + ...(aggsObject ? { aggs: aggsObject } : {}), + ...getSearchDsl(mappings, registry, { + search, + defaultSearchOperator, + searchFields, + pit, + rootSearchFields, + type: allowedTypes, + searchAfter, + sortField, + sortOrder, + namespaces, + typeToNamespacesMap, // If defined, this takes precedence over the `type` and `namespaces` fields + hasReference, + hasReferenceOperator, + hasNoReference, + hasNoReferenceOperator, + kueryNode, + }), + }, + }; + + const { body, statusCode, headers } = await client.search(esOptions, { + ignore: [404], + meta: true, + }); + if (statusCode === 404) { + if (!isSupportedEsServer(headers)) { + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(); + } + // 404 is only possible here if the index is missing, which + // we don't want to leak, see "404s from missing index" above + return SavedObjectsUtils.createEmptyFindResponse(options); + } + + const result = { + ...(body.aggregations ? { aggregations: body.aggregations as unknown as A } : {}), + page, + per_page: perPage, + total: body.hits.total, + saved_objects: body.hits.hits.map( + (hit: estypes.SearchHit): SavedObjectsFindResult => ({ + ...serializerHelper.rawToSavedObject(hit as SavedObjectsRawDoc, { + migrationVersionCompatibility, + }), + score: hit._score!, + sort: hit.sort, + }) + ), + pit_id: body.pit_id, + } as SavedObjectsFindResponse; + + if (disableExtensions) { + return result; + } + + // Now that we have a full set of results with all existing namespaces for each object, + // we need an updated authorization type map to pass on to the redact method + const redactTypeMap = await securityExtension?.getFindRedactTypeMap({ + previouslyCheckedNamespaces: spacesToAuthorize, + objects: result.saved_objects.map((obj) => { + return { + type: obj.type, + id: obj.id, + existingNamespaces: obj.namespaces ?? [], + }; + }), + }); + + return encryptionHelper.optionallyDecryptAndRedactBulkResult( + result, + redactTypeMap ?? authorizationResult?.typeMap // If the redact type map is valid, use that one; otherwise, fall back to the authorization check + ); +}; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts index 3c58dbebbbf8..2cf4e99687aa 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts @@ -13,3 +13,4 @@ export { performDelete } from './delete'; export { performCheckConflicts } from './check_conflicts'; export { performBulkDelete } from './bulk_delete'; export { performDeleteByNamespace } from './delete_by_namespace'; +export { performFind } from './find'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts index 58ba0973ff9a..d0237fdcb9f5 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts @@ -6,20 +6,16 @@ * Side Public License, v 1. */ -import { omit, isObject } from 'lodash'; +import { isObject } from 'lodash'; import Boom from '@hapi/boom'; import type { Payload } from '@hapi/boom'; -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { Logger } from '@kbn/logging'; import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import { isSupportedEsServer, isNotFoundFromUnsupportedServer, } from '@kbn/core-elasticsearch-server-internal'; -import type { - BulkResolveError, - SavedObjectsRawDocParseOptions, -} from '@kbn/core-saved-objects-server'; +import type { BulkResolveError } from '@kbn/core-saved-objects-server'; import type { SavedObjectsBaseOptions, SavedObjectsIncrementCounterOptions, @@ -40,7 +36,6 @@ import type { SavedObjectsCheckConflictsObject, SavedObjectsCheckConflictsResponse, SavedObjectsBulkUpdateOptions, - SavedObjectsFindResult, SavedObjectsRemoveReferencesToOptions, SavedObjectsDeleteOptions, SavedObjectsOpenPointInTimeResponse, @@ -73,19 +68,13 @@ import { type ISavedObjectsEncryptionExtension, type ISavedObjectsSecurityExtension, type ISavedObjectsSpacesExtension, - type CheckAuthorizationResult, AuthorizeUpdateObject, type AuthorizeBulkGetObject, type SavedObject, } from '@kbn/core-saved-objects-server'; import { DEFAULT_NAMESPACE_STRING } from '@kbn/core-saved-objects-utils-server'; import { SavedObjectsErrorHelpers, type DecoratedError } from '@kbn/core-saved-objects-server'; -import { - ALL_NAMESPACES_STRING, - FIND_DEFAULT_PAGE, - FIND_DEFAULT_PER_PAGE, - SavedObjectsUtils, -} from '@kbn/core-saved-objects-utils-server'; +import { ALL_NAMESPACES_STRING, SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server'; import { SavedObjectsSerializer, encodeVersion, @@ -98,8 +87,6 @@ import { createRepositoryEsClient, type RepositoryEsClient } from './repository_ import { getSearchDsl } from './search_dsl'; import { includedFields } from './included_fields'; import { internalBulkResolve, isBulkResolveError } from './internal_bulk_resolve'; -import { validateConvertFilterToKueryNode } from './filter_utils'; -import { validateAndConvertAggregations } from './aggregations'; import { getBulkOperationError, getCurrentTime, @@ -135,6 +122,7 @@ import { performCheckConflicts, performBulkDelete, performDeleteByNamespace, + performFind, } from './apis'; // BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository @@ -404,215 +392,12 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { options: SavedObjectsFindOptions, internalOptions: SavedObjectsFindInternalOptions = {} ): Promise> { - let namespaces!: string[]; - const { disableExtensions } = internalOptions; - if (disableExtensions || !this._spacesExtension) { - namespaces = options.namespaces ?? [DEFAULT_NAMESPACE_STRING]; - // If the consumer specified `namespaces: []`, throw a Bad Request error - if (namespaces.length === 0) - throw SavedObjectsErrorHelpers.createBadRequestError( - 'options.namespaces cannot be an empty array' - ); - } - - const { - search, - defaultSearchOperator = 'OR', - searchFields, - rootSearchFields, - hasReference, - hasReferenceOperator, - hasNoReference, - hasNoReferenceOperator, - page = FIND_DEFAULT_PAGE, - perPage = FIND_DEFAULT_PER_PAGE, - pit, - searchAfter, - sortField, - sortOrder, - fields, - type, - filter, - preference, - aggs, - migrationVersionCompatibility, - } = options; - - if (!type) { - throw SavedObjectsErrorHelpers.createBadRequestError( - 'options.type must be a string or an array of strings' - ); - } else if (preference?.length && pit) { - throw SavedObjectsErrorHelpers.createBadRequestError( - 'options.preference must be excluded when options.pit is used' - ); - } - - const types = Array.isArray(type) ? type : [type]; - const allowedTypes = types.filter((t) => this._allowedTypes.includes(t)); - if (allowedTypes.length === 0) { - return SavedObjectsUtils.createEmptyFindResponse(options); - } - - if (searchFields && !Array.isArray(searchFields)) { - throw SavedObjectsErrorHelpers.createBadRequestError('options.searchFields must be an array'); - } - - if (fields && !Array.isArray(fields)) { - throw SavedObjectsErrorHelpers.createBadRequestError('options.fields must be an array'); - } - - let kueryNode; - if (filter) { - try { - kueryNode = validateConvertFilterToKueryNode(allowedTypes, filter, this._mappings); - } catch (e) { - if (e.name === 'KQLSyntaxError') { - throw SavedObjectsErrorHelpers.createBadRequestError(`KQLSyntaxError: ${e.message}`); - } else { - throw e; - } - } - } - - let aggsObject; - if (aggs) { - try { - aggsObject = validateAndConvertAggregations(allowedTypes, aggs, this._mappings); - } catch (e) { - throw SavedObjectsErrorHelpers.createBadRequestError(`Invalid aggregation: ${e.message}`); - } - } - - if (!disableExtensions && this._spacesExtension) { - try { - namespaces = await this._spacesExtension.getSearchableNamespaces(options.namespaces); - } catch (err) { - if (Boom.isBoom(err) && err.output.payload.statusCode === 403) { - // The user is not authorized to access any space, return an empty response. - return SavedObjectsUtils.createEmptyFindResponse(options); - } - throw err; - } - if (namespaces.length === 0) { - // The user is authorized to access *at least one space*, but not any of the spaces they requested; return an empty response. - return SavedObjectsUtils.createEmptyFindResponse(options); - } - } - - // We have to first perform an initial authorization check so that we can construct the search DSL accordingly - const spacesToAuthorize = new Set(namespaces); - const typesToAuthorize = new Set(types); - let typeToNamespacesMap: Map | undefined; - let authorizationResult: CheckAuthorizationResult | undefined; - if (!disableExtensions && this._securityExtension) { - authorizationResult = await this._securityExtension.authorizeFind({ - namespaces: spacesToAuthorize, - types: typesToAuthorize, - }); - if (authorizationResult?.status === 'unauthorized') { - // If the user is unauthorized to find *anything* they requested, return an empty response - return SavedObjectsUtils.createEmptyFindResponse(options); - } - if (authorizationResult?.status === 'partially_authorized') { - typeToNamespacesMap = new Map(); - for (const [objType, entry] of authorizationResult.typeMap) { - if (!entry.find) continue; - // This ensures that the query DSL can filter only for object types that the user is authorized to access for a given space - const { authorizedSpaces, isGloballyAuthorized } = entry.find; - typeToNamespacesMap.set(objType, isGloballyAuthorized ? namespaces : authorizedSpaces); - } - } - } - - const esOptions = { - // If `pit` is provided, we drop the `index`, otherwise ES returns 400. - index: pit ? undefined : this.getIndicesForTypes(allowedTypes), - // If `searchAfter` is provided, we drop `from` as it will not be used for pagination. - from: searchAfter ? undefined : perPage * (page - 1), - _source: includedFields(allowedTypes, fields), - preference, - rest_total_hits_as_int: true, - size: perPage, - body: { - size: perPage, - seq_no_primary_term: true, - from: perPage * (page - 1), - _source: includedFields(allowedTypes, fields), - ...(aggsObject ? { aggs: aggsObject } : {}), - ...getSearchDsl(this._mappings, this._registry, { - search, - defaultSearchOperator, - searchFields, - pit, - rootSearchFields, - type: allowedTypes, - searchAfter, - sortField, - sortOrder, - namespaces, - typeToNamespacesMap, // If defined, this takes precedence over the `type` and `namespaces` fields - hasReference, - hasReferenceOperator, - hasNoReference, - hasNoReferenceOperator, - kueryNode, - }), - }, - }; - - const { body, statusCode, headers } = await this.client.search( - esOptions, + return await performFind( { - ignore: [404], - meta: true, - } - ); - if (statusCode === 404) { - if (!isSupportedEsServer(headers)) { - throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(); - } - // 404 is only possible here if the index is missing, which - // we don't want to leak, see "404s from missing index" above - return SavedObjectsUtils.createEmptyFindResponse(options); - } - - const result = { - ...(body.aggregations ? { aggregations: body.aggregations as unknown as A } : {}), - page, - per_page: perPage, - total: body.hits.total, - saved_objects: body.hits.hits.map( - (hit: estypes.SearchHit): SavedObjectsFindResult => ({ - // @ts-expect-error @elastic/elasticsearch _source is optional - ...this._rawToSavedObject(hit, { migrationVersionCompatibility }), - score: hit._score!, - sort: hit.sort, - }) - ), - pit_id: body.pit_id, - } as SavedObjectsFindResponse; - - if (disableExtensions) { - return result; - } - - // Now that we have a full set of results with all existing namespaces for each object, - // we need an updated authorization type map to pass on to the redact method - const redactTypeMap = await this._securityExtension?.getFindRedactTypeMap({ - previouslyCheckedNamespaces: spacesToAuthorize, - objects: result.saved_objects.map((obj) => { - return { - type: obj.type, - id: obj.id, - existingNamespaces: obj.namespaces ?? [], - }; - }), - }); - - return this.encryptionHelper.optionallyDecryptAndRedactBulkResult( - result, - redactTypeMap ?? authorizationResult?.typeMap // If the redact type map is valid, use that one; otherwise, fall back to the authorization check + options, + internalOptions, + }, + this.apiExecutionContext ); } @@ -1733,19 +1518,6 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { return this.commonHelper.getIndicesForTypes(types); } - private _rawToSavedObject( - raw: SavedObjectsRawDoc, - options?: SavedObjectsRawDocParseOptions - ): SavedObject { - const savedObject = this._serializer.rawToSavedObject(raw, options); - const { namespace, type } = savedObject; - if (this._registry.isSingleNamespace(type)) { - savedObject.namespaces = [SavedObjectsUtils.namespaceIdToString(namespace)]; - } - - return omit(savedObject, ['namespace']) as SavedObject; - } - private rawDocExistsInNamespaces(raw: SavedObjectsRawDoc, namespaces: string[]) { return rawDocExistsInNamespaces(this._registry, raw, namespaces); } From 2dc4634d20c7209353b19da2a7e2303ce66c9a85 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 9 May 2023 12:50:13 +0200 Subject: [PATCH 09/34] extract bulk_get --- .../src/lib/apis/bulk_get.ts | 206 ++++++++++++++++++ .../src/lib/apis/index.ts | 1 + .../src/lib/repository.ts | 178 +-------------- 3 files changed, 214 insertions(+), 171 deletions(-) create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_get.ts diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_get.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_get.ts new file mode 100644 index 000000000000..a5a87639bfb5 --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_get.ts @@ -0,0 +1,206 @@ +/* + * 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 Boom, { Payload } from '@hapi/boom'; +import { isNotFoundFromUnsupportedServer } from '@kbn/core-elasticsearch-server-internal'; +import { + SavedObjectsErrorHelpers, + type SavedObject, + DecoratedError, + SavedObjectsRawDocSource, + AuthorizeBulkGetObject, +} from '@kbn/core-saved-objects-server'; +import { ALL_NAMESPACES_STRING, SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server'; +import { + SavedObjectsBulkGetObject, + SavedObjectsBulkResponse, + SavedObjectsGetOptions, +} from '@kbn/core-saved-objects-api-server'; +import { Either, errorContent, getSavedObjectFromSource, isLeft, isRight } from '../internal_utils'; +import { ApiExecutionContext } from './types'; +import { includedFields } from '../included_fields'; + +export interface PerformBulkGetParams { + objects: SavedObjectsBulkGetObject[]; + options: SavedObjectsGetOptions; +} + +export const performBulkGet = async ( + { objects, options }: PerformBulkGetParams, + { helpers, allowedTypes, client, serializer, extensions = {} }: ApiExecutionContext +): Promise> => { + const { + common: commonHelper, + validation: validationHelper, + encryption: encryptionHelper, + } = helpers; + const { securityExtension, spacesExtension } = extensions; + + const namespace = commonHelper.getCurrentNamespace(options.namespace); + const { migrationVersionCompatibility } = options; + + if (objects.length === 0) { + return { saved_objects: [] }; + } + + let availableSpacesPromise: Promise | undefined; + const getAvailableSpaces = async () => { + if (!availableSpacesPromise) { + availableSpacesPromise = spacesExtension! + .getSearchableNamespaces([ALL_NAMESPACES_STRING]) + .catch((err) => { + if (Boom.isBoom(err) && err.output.payload.statusCode === 403) { + // the user doesn't have access to any spaces; return the current space ID and allow the SOR authZ check to fail + return [SavedObjectsUtils.namespaceIdToString(namespace)]; + } else { + throw err; + } + }); + } + return availableSpacesPromise; + }; + + let bulkGetRequestIndexCounter = 0; + type ExpectedBulkGetResult = Either< + { type: string; id: string; error: Payload }, + { type: string; id: string; fields?: string[]; namespaces?: string[]; esRequestIndex: number } + >; + const expectedBulkGetResults = await Promise.all( + objects.map>(async (object) => { + const { type, id, fields } = object; + + let error: DecoratedError | undefined; + if (!allowedTypes.includes(type)) { + error = SavedObjectsErrorHelpers.createUnsupportedTypeError(type); + } else { + try { + validationHelper.validateObjectNamespaces(type, id, object.namespaces); + } catch (e) { + error = e; + } + } + + if (error) { + return { + tag: 'Left', + value: { id, type, error: errorContent(error) }, + }; + } + + let namespaces = object.namespaces; + if (spacesExtension && namespaces?.includes(ALL_NAMESPACES_STRING)) { + namespaces = await getAvailableSpaces(); + } + return { + tag: 'Right', + value: { + type, + id, + fields, + namespaces, + esRequestIndex: bulkGetRequestIndexCounter++, + }, + }; + }) + ); + + const validObjects = expectedBulkGetResults.filter(isRight); + if (validObjects.length === 0) { + // We only have error results; return early to avoid potentially trying authZ checks for 0 types which would result in an exception. + return { + // Technically the returned array should only contain SavedObject results, but for errors this is not true (we cast to 'any' below) + saved_objects: expectedBulkGetResults.map>( + ({ value }) => value as unknown as SavedObject + ), + }; + } + + const getNamespaceId = (namespaces?: string[]) => + namespaces !== undefined ? SavedObjectsUtils.namespaceStringToId(namespaces[0]) : namespace; + const bulkGetDocs = validObjects.map(({ value: { type, id, fields, namespaces } }) => ({ + _id: serializer.generateRawId(getNamespaceId(namespaces), type, id), // the namespace prefix is only used for single-namespace object types + _index: commonHelper.getIndexForType(type), + _source: { includes: includedFields(type, fields) }, + })); + const bulkGetResponse = bulkGetDocs.length + ? await client.mget( + { + body: { + docs: bulkGetDocs, + }, + }, + { ignore: [404], meta: true } + ) + : undefined; + // fail fast if we can't verify a 404 is from Elasticsearch + if ( + bulkGetResponse && + isNotFoundFromUnsupportedServer({ + statusCode: bulkGetResponse.statusCode, + headers: bulkGetResponse.headers, + }) + ) { + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(); + } + + const authObjects: AuthorizeBulkGetObject[] = []; + const result = { + saved_objects: expectedBulkGetResults.map((expectedResult) => { + if (isLeft(expectedResult)) { + const { type, id } = expectedResult.value; + authObjects.push({ type, id, existingNamespaces: [], error: true }); + return expectedResult.value as any; + } + + const { + type, + id, + // set to default namespaces value for `rawDocExistsInNamespaces` check below + namespaces = [SavedObjectsUtils.namespaceIdToString(namespace)], + esRequestIndex, + } = expectedResult.value; + + const doc = bulkGetResponse?.body.docs[esRequestIndex]; + + // @ts-expect-error MultiGetHit._source is optional + const docNotFound = !doc?.found || !this.rawDocExistsInNamespaces(doc, namespaces); + + authObjects.push({ + type, + id, + objectNamespaces: namespaces, + // @ts-expect-error MultiGetHit._source is optional + existingNamespaces: doc?._source?.namespaces ?? [], + error: docNotFound, + }); + + if (docNotFound) { + return { + id, + type, + error: errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)), + } as any as SavedObject; + } + + // @ts-expect-error MultiGetHit._source is optional + return getSavedObjectFromSource(this._registry, type, id, doc, { + migrationVersionCompatibility, + }); + }), + }; + + const authorizationResult = await securityExtension?.authorizeBulkGet({ + namespace, + objects: authObjects, + }); + + return encryptionHelper.optionallyDecryptAndRedactBulkResult( + result, + authorizationResult?.typeMap + ); +}; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts index 2cf4e99687aa..1ab5b69eb0af 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts @@ -14,3 +14,4 @@ export { performCheckConflicts } from './check_conflicts'; export { performBulkDelete } from './bulk_delete'; export { performDeleteByNamespace } from './delete_by_namespace'; export { performFind } from './find'; +export { performBulkGet } from './bulk_get'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts index d0237fdcb9f5..0c7a85471279 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts @@ -69,7 +69,6 @@ import { type ISavedObjectsSecurityExtension, type ISavedObjectsSpacesExtension, AuthorizeUpdateObject, - type AuthorizeBulkGetObject, type SavedObject, } from '@kbn/core-saved-objects-server'; import { DEFAULT_NAMESPACE_STRING } from '@kbn/core-saved-objects-utils-server'; @@ -85,7 +84,6 @@ import { import { PointInTimeFinder } from './point_in_time_finder'; import { createRepositoryEsClient, type RepositoryEsClient } from './repository_es_client'; import { getSearchDsl } from './search_dsl'; -import { includedFields } from './included_fields'; import { internalBulkResolve, isBulkResolveError } from './internal_bulk_resolve'; import { getBulkOperationError, @@ -94,7 +92,6 @@ import { getSavedObjectFromSource, normalizeNamespace, rawDocExistsInNamespace, - rawDocExistsInNamespaces, errorContent, type Either, isLeft, @@ -123,11 +120,9 @@ import { performBulkDelete, performDeleteByNamespace, performFind, + performBulkGet, } from './apis'; -// 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. - export interface SavedObjectsRepositoryOptions { index: string; mappings: IndexMapping; @@ -408,167 +403,12 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { objects: SavedObjectsBulkGetObject[] = [], options: SavedObjectsGetOptions = {} ): Promise> { - const namespace = this.getCurrentNamespace(options.namespace); - const { migrationVersionCompatibility } = options; - - if (objects.length === 0) { - return { saved_objects: [] }; - } - - let availableSpacesPromise: Promise | undefined; - const getAvailableSpaces = async (spacesExtension: ISavedObjectsSpacesExtension) => { - if (!availableSpacesPromise) { - availableSpacesPromise = spacesExtension - .getSearchableNamespaces([ALL_NAMESPACES_STRING]) - .catch((err) => { - if (Boom.isBoom(err) && err.output.payload.statusCode === 403) { - // the user doesn't have access to any spaces; return the current space ID and allow the SOR authZ check to fail - return [SavedObjectsUtils.namespaceIdToString(namespace)]; - } else { - throw err; - } - }); - } - return availableSpacesPromise; - }; - - let bulkGetRequestIndexCounter = 0; - type ExpectedBulkGetResult = Either< - { type: string; id: string; error: Payload }, - { type: string; id: string; fields?: string[]; namespaces?: string[]; esRequestIndex: number } - >; - const expectedBulkGetResults = await Promise.all( - objects.map>(async (object) => { - const { type, id, fields } = object; - - let error: DecoratedError | undefined; - if (!this._allowedTypes.includes(type)) { - error = SavedObjectsErrorHelpers.createUnsupportedTypeError(type); - } else { - try { - this.validationHelper.validateObjectNamespaces(type, id, object.namespaces); - } catch (e) { - error = e; - } - } - - if (error) { - return { - tag: 'Left', - value: { id, type, error: errorContent(error) }, - }; - } - - let namespaces = object.namespaces; - if (this._spacesExtension && namespaces?.includes(ALL_NAMESPACES_STRING)) { - namespaces = await getAvailableSpaces(this._spacesExtension); - } - return { - tag: 'Right', - value: { - type, - id, - fields, - namespaces, - esRequestIndex: bulkGetRequestIndexCounter++, - }, - }; - }) - ); - - const validObjects = expectedBulkGetResults.filter(isRight); - if (validObjects.length === 0) { - // We only have error results; return early to avoid potentially trying authZ checks for 0 types which would result in an exception. - return { - // Technically the returned array should only contain SavedObject results, but for errors this is not true (we cast to 'any' below) - saved_objects: expectedBulkGetResults.map>( - ({ value }) => value as unknown as SavedObject - ), - }; - } - - const getNamespaceId = (namespaces?: string[]) => - namespaces !== undefined ? SavedObjectsUtils.namespaceStringToId(namespaces[0]) : namespace; - const bulkGetDocs = validObjects.map(({ value: { type, id, fields, namespaces } }) => ({ - _id: this._serializer.generateRawId(getNamespaceId(namespaces), type, id), // the namespace prefix is only used for single-namespace object types - _index: this.getIndexForType(type), - _source: { includes: includedFields(type, fields) }, - })); - const bulkGetResponse = bulkGetDocs.length - ? await this.client.mget( - { - body: { - docs: bulkGetDocs, - }, - }, - { ignore: [404], meta: true } - ) - : undefined; - // fail fast if we can't verify a 404 is from Elasticsearch - if ( - bulkGetResponse && - isNotFoundFromUnsupportedServer({ - statusCode: bulkGetResponse.statusCode, - headers: bulkGetResponse.headers, - }) - ) { - throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(); - } - - const authObjects: AuthorizeBulkGetObject[] = []; - const result = { - saved_objects: expectedBulkGetResults.map((expectedResult) => { - if (isLeft(expectedResult)) { - const { type, id } = expectedResult.value; - authObjects.push({ type, id, existingNamespaces: [], error: true }); - return expectedResult.value as any; - } - - const { - type, - id, - // set to default namespaces value for `rawDocExistsInNamespaces` check below - namespaces = [SavedObjectsUtils.namespaceIdToString(namespace)], - esRequestIndex, - } = expectedResult.value; - - const doc = bulkGetResponse?.body.docs[esRequestIndex]; - - // @ts-expect-error MultiGetHit._source is optional - const docNotFound = !doc?.found || !this.rawDocExistsInNamespaces(doc, namespaces); - - authObjects.push({ - type, - id, - objectNamespaces: namespaces, - // @ts-expect-error MultiGetHit._source is optional - existingNamespaces: doc?._source?.namespaces ?? [], - error: docNotFound, - }); - - if (docNotFound) { - return { - id, - type, - error: errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)), - } as any as SavedObject; - } - - // @ts-expect-error MultiGetHit._source is optional - return getSavedObjectFromSource(this._registry, type, id, doc, { - migrationVersionCompatibility, - }); - }), - }; - - const authorizationResult = await this._securityExtension?.authorizeBulkGet({ - namespace, - objects: authObjects, - }); - - return this.encryptionHelper.optionallyDecryptAndRedactBulkResult( - result, - authorizationResult?.typeMap + return await performBulkGet( + { + objects, + options, + }, + this.apiExecutionContext ); } @@ -1518,10 +1358,6 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { return this.commonHelper.getIndicesForTypes(types); } - private rawDocExistsInNamespaces(raw: SavedObjectsRawDoc, namespaces: string[]) { - return rawDocExistsInNamespaces(this._registry, raw, namespaces); - } - private rawDocExistsInNamespace(raw: SavedObjectsRawDoc, namespace: string | undefined) { return rawDocExistsInNamespace(this._registry, raw, namespace); } From a2f90e8a19a35dbce723b27dfd69b72028b225c4 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 9 May 2023 13:03:29 +0200 Subject: [PATCH 10/34] extract get --- .../src/lib/apis/get.ts | 80 +++++++++++++++++++ .../src/lib/apis/index.ts | 1 + .../src/lib/repository.ts | 46 +---------- 3 files changed, 85 insertions(+), 42 deletions(-) create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/get.ts diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/get.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/get.ts new file mode 100644 index 000000000000..c18916cf5c65 --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/get.ts @@ -0,0 +1,80 @@ +/* + * 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 { isSupportedEsServer } from '@kbn/core-elasticsearch-server-internal'; +import { + SavedObjectsErrorHelpers, + type SavedObject, + SavedObjectsRawDocSource, +} from '@kbn/core-saved-objects-server'; +import { SavedObjectsGetOptions } from '@kbn/core-saved-objects-api-server'; +import { getSavedObjectFromSource, rawDocExistsInNamespace } from '../internal_utils'; +import { isFoundGetResponse } from '../utils'; +import { ApiExecutionContext } from './types'; + +export interface PerformGetParams { + type: string; + id: string; + options: SavedObjectsGetOptions; +} + +export const performGet = async ( + { type, id, options }: PerformGetParams, + { registry, helpers, allowedTypes, client, serializer, extensions = {} }: ApiExecutionContext +): Promise> => { + const { common: commonHelper, encryption: encryptionHelper } = helpers; + const { securityExtension } = extensions; + + const namespace = commonHelper.getCurrentNamespace(options.namespace); + const { migrationVersionCompatibility } = options; + + if (!allowedTypes.includes(type)) { + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + const { body, statusCode, headers } = await client.get( + { + id: serializer.generateRawId(namespace, type, id), + index: commonHelper.getIndexForType(type), + }, + { ignore: [404], meta: true } + ); + const indexNotFound = statusCode === 404; + // check if we have the elasticsearch header when index is not found and, if we do, ensure it is from Elasticsearch + if (indexNotFound && !isSupportedEsServer(headers)) { + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(type, id); + } + + const objectNotFound = + !isFoundGetResponse(body) || + indexNotFound || + !rawDocExistsInNamespace(registry, body, namespace); + + const authorizationResult = await securityExtension?.authorizeGet({ + namespace, + object: { + type, + id, + existingNamespaces: body?._source?.namespaces ?? [], + }, + objectNotFound, + }); + + if (objectNotFound) { + // see "404s from missing index" above + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + + const result = getSavedObjectFromSource(registry, type, id, body, { + migrationVersionCompatibility, + }); + + return encryptionHelper.optionallyDecryptAndRedactSingleResult( + result, + authorizationResult?.typeMap + ); +}; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts index 1ab5b69eb0af..3913b887900b 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts @@ -15,3 +15,4 @@ export { performBulkDelete } from './bulk_delete'; export { performDeleteByNamespace } from './delete_by_namespace'; export { performFind } from './find'; export { performBulkGet } from './bulk_get'; +export { performGet } from './get'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts index 0c7a85471279..6ecd9496c633 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts @@ -89,7 +89,6 @@ import { getBulkOperationError, getCurrentTime, getExpectedVersionProperties, - getSavedObjectFromSource, normalizeNamespace, rawDocExistsInNamespace, errorContent, @@ -109,7 +108,6 @@ import { SerializerHelper, type PreflightCheckNamespacesResult, } from './helpers'; -import { isFoundGetResponse } from './utils'; import { DEFAULT_REFRESH_SETTING, DEFAULT_RETRY_COUNT } from './constants'; import { type ApiExecutionContext, @@ -121,6 +119,7 @@ import { performDeleteByNamespace, performFind, performBulkGet, + performGet, } from './apis'; export interface SavedObjectsRepositoryOptions { @@ -455,50 +454,13 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { id: string, options: SavedObjectsGetOptions = {} ): Promise> { - const namespace = this.getCurrentNamespace(options.namespace); - const { migrationVersionCompatibility } = options; - - if (!this._allowedTypes.includes(type)) { - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - const { body, statusCode, headers } = await this.client.get( + return await performGet( { - id: this._serializer.generateRawId(namespace, type, id), - index: this.getIndexForType(type), - }, - { ignore: [404], meta: true } - ); - const indexNotFound = statusCode === 404; - // check if we have the elasticsearch header when index is not found and, if we do, ensure it is from Elasticsearch - if (indexNotFound && !isSupportedEsServer(headers)) { - throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(type, id); - } - - const objectNotFound = - !isFoundGetResponse(body) || indexNotFound || !this.rawDocExistsInNamespace(body, namespace); - - const authorizationResult = await this._securityExtension?.authorizeGet({ - namespace, - object: { type, id, - existingNamespaces: body?._source?.namespaces ?? [], + options, }, - objectNotFound, - }); - - if (objectNotFound) { - // see "404s from missing index" above - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - - const result = getSavedObjectFromSource(this._registry, type, id, body, { - migrationVersionCompatibility, - }); - - return this.encryptionHelper.optionallyDecryptAndRedactSingleResult( - result, - authorizationResult?.typeMap + this.apiExecutionContext ); } From 1ed1f1c47369cba79e0e569e580bcb1e129782db Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 9 May 2023 13:13:43 +0200 Subject: [PATCH 11/34] extract update --- .../src/lib/apis/index.ts | 1 + .../src/lib/apis/update.ts | 179 ++++++++++++++++++ .../src/lib/repository.ts | 139 +------------- 3 files changed, 188 insertions(+), 131 deletions(-) create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/update.ts diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts index 3913b887900b..570c281cf121 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts @@ -16,3 +16,4 @@ export { performDeleteByNamespace } from './delete_by_namespace'; export { performFind } from './find'; export { performBulkGet } from './bulk_get'; export { performGet } from './get'; +export { performUpdate } from './update'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/update.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/update.ts new file mode 100644 index 000000000000..92f77741c86a --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/update.ts @@ -0,0 +1,179 @@ +/* + * 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 { + SavedObjectsErrorHelpers, + type SavedObject, + type SavedObjectSanitizedDoc, + SavedObjectsRawDoc, + SavedObjectsRawDocSource, +} from '@kbn/core-saved-objects-server'; +import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server'; +import { encodeHitVersion } from '@kbn/core-saved-objects-base-server-internal'; +import { + SavedObjectsUpdateOptions, + SavedObjectsUpdateResponse, +} from '@kbn/core-saved-objects-api-server'; +import { DEFAULT_REFRESH_SETTING, DEFAULT_RETRY_COUNT } from '../constants'; +import { getCurrentTime, getExpectedVersionProperties } from '../internal_utils'; +import { ApiExecutionContext } from './types'; +import { PreflightCheckNamespacesResult } from '../helpers'; + +export interface PerformUpdateParams { + type: string; + id: string; + attributes: T; + options: SavedObjectsUpdateOptions; +} + +export const performUpdate = async ( + { id, type, attributes, options }: PerformUpdateParams, + { + registry, + helpers, + allowedTypes, + client, + serializer, + migrator, + extensions = {}, + }: ApiExecutionContext +): Promise> => { + const { + common: commonHelper, + encryption: encryptionHelper, + preflight: preflightHelper, + } = helpers; + const { securityExtension } = extensions; + + const namespace = commonHelper.getCurrentNamespace(options.namespace); + if (!allowedTypes.includes(type)) { + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + if (!id) { + throw SavedObjectsErrorHelpers.createBadRequestError('id cannot be empty'); // prevent potentially upserting a saved object with an empty ID + } + + const { + version, + references, + upsert, + refresh = DEFAULT_REFRESH_SETTING, + retryOnConflict = version ? 0 : DEFAULT_RETRY_COUNT, + } = options; + + let preflightResult: PreflightCheckNamespacesResult | undefined; + if (registry.isMultiNamespace(type)) { + preflightResult = await preflightHelper.preflightCheckNamespaces({ + type, + id, + namespace, + }); + } + + const existingNamespaces = preflightResult?.savedObjectNamespaces ?? []; + + const authorizationResult = await securityExtension?.authorizeUpdate({ + namespace, + object: { type, id, existingNamespaces }, + }); + + if ( + preflightResult?.checkResult === 'found_outside_namespace' || + (!upsert && preflightResult?.checkResult === 'not_found') + ) { + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + if (upsert && preflightResult?.checkResult === 'not_found') { + // If an upsert would result in the creation of a new object, we need to check for alias conflicts too. + // This takes an extra round trip to Elasticsearch, but this won't happen often. + // TODO: improve performance by combining these into a single preflight check + await preflightHelper.preflightCheckForUpsertAliasConflict(type, id, namespace); + } + const time = getCurrentTime(); + + let rawUpsert: SavedObjectsRawDoc | undefined; + // don't include upsert if the object already exists; ES doesn't allow upsert in combination with version properties + if (upsert && (!preflightResult || preflightResult.checkResult === 'not_found')) { + let savedObjectNamespace: string | undefined; + let savedObjectNamespaces: string[] | undefined; + + if (registry.isSingleNamespace(type) && namespace) { + savedObjectNamespace = namespace; + } else if (registry.isMultiNamespace(type)) { + savedObjectNamespaces = preflightResult!.savedObjectNamespaces; + } + + const migrated = migrator.migrateDocument({ + id, + type, + ...(savedObjectNamespace && { namespace: savedObjectNamespace }), + ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), + attributes: { + ...(await encryptionHelper.optionallyEncryptAttributes(type, id, namespace, upsert)), + }, + updated_at: time, + }); + rawUpsert = serializer.savedObjectToRaw(migrated as SavedObjectSanitizedDoc); + } + + const doc = { + [type]: await encryptionHelper.optionallyEncryptAttributes(type, id, namespace, attributes), + updated_at: time, + ...(Array.isArray(references) && { references }), + }; + + const body = await client + .update({ + id: serializer.generateRawId(namespace, type, id), + index: commonHelper.getIndexForType(type), + ...getExpectedVersionProperties(version), + refresh, + retry_on_conflict: retryOnConflict, + body: { + doc, + ...(rawUpsert && { upsert: rawUpsert._source }), + }, + _source_includes: ['namespace', 'namespaces', 'originId'], + require_alias: true, + }) + .catch((err) => { + if (SavedObjectsErrorHelpers.isEsUnavailableError(err)) { + throw err; + } + if (SavedObjectsErrorHelpers.isNotFoundError(err)) { + // see "404s from missing index" above + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + throw err; + }); + + const { originId } = body.get?._source ?? {}; + let namespaces: string[] = []; + if (!registry.isNamespaceAgnostic(type)) { + namespaces = body.get?._source.namespaces ?? [ + SavedObjectsUtils.namespaceIdToString(body.get?._source.namespace), + ]; + } + + const result = { + id, + type, + updated_at: time, + version: encodeHitVersion(body), + namespaces, + ...(originId && { originId }), + references, + attributes, + } as SavedObject; + + return encryptionHelper.optionallyDecryptAndRedactSingleResult( + result, + authorizationResult?.typeMap, + attributes + ); +}; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts index 6ecd9496c633..c9a819dc6217 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts @@ -106,9 +106,8 @@ import { ValidationHelper, PreflightCheckHelper, SerializerHelper, - type PreflightCheckNamespacesResult, } from './helpers'; -import { DEFAULT_REFRESH_SETTING, DEFAULT_RETRY_COUNT } from './constants'; +import { DEFAULT_REFRESH_SETTING } from './constants'; import { type ApiExecutionContext, performCreate, @@ -120,6 +119,7 @@ import { performFind, performBulkGet, performGet, + performUpdate, } from './apis'; export interface SavedObjectsRepositoryOptions { @@ -501,137 +501,14 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { attributes: Partial, options: SavedObjectsUpdateOptions = {} ): Promise> { - const namespace = this.getCurrentNamespace(options.namespace); - - if (!this._allowedTypes.includes(type)) { - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - if (!id) { - throw SavedObjectsErrorHelpers.createBadRequestError('id cannot be empty'); // prevent potentially upserting a saved object with an empty ID - } - - const { - version, - references, - upsert, - refresh = DEFAULT_REFRESH_SETTING, - retryOnConflict = version ? 0 : DEFAULT_RETRY_COUNT, - } = options; - - let preflightResult: PreflightCheckNamespacesResult | undefined; - if (this._registry.isMultiNamespace(type)) { - preflightResult = await this.preflightCheckHelper.preflightCheckNamespaces({ - type, - id, - namespace, - }); - } - - const existingNamespaces = preflightResult?.savedObjectNamespaces ?? []; - - const authorizationResult = await this._securityExtension?.authorizeUpdate({ - namespace, - object: { type, id, existingNamespaces }, - }); - - if ( - preflightResult?.checkResult === 'found_outside_namespace' || - (!upsert && preflightResult?.checkResult === 'not_found') - ) { - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - if (upsert && preflightResult?.checkResult === 'not_found') { - // If an upsert would result in the creation of a new object, we need to check for alias conflicts too. - // This takes an extra round trip to Elasticsearch, but this won't happen often. - // TODO: improve performance by combining these into a single preflight check - await this.preflightCheckHelper.preflightCheckForUpsertAliasConflict(type, id, namespace); - } - const time = getCurrentTime(); - - let rawUpsert: SavedObjectsRawDoc | undefined; - // don't include upsert if the object already exists; ES doesn't allow upsert in combination with version properties - if (upsert && (!preflightResult || preflightResult.checkResult === 'not_found')) { - let savedObjectNamespace: string | undefined; - let savedObjectNamespaces: string[] | undefined; - - if (this._registry.isSingleNamespace(type) && namespace) { - savedObjectNamespace = namespace; - } else if (this._registry.isMultiNamespace(type)) { - savedObjectNamespaces = preflightResult!.savedObjectNamespaces; - } - - const migrated = this._migrator.migrateDocument({ - id, - type, - ...(savedObjectNamespace && { namespace: savedObjectNamespace }), - ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), - attributes: { - ...(await this.encryptionHelper.optionallyEncryptAttributes(type, id, namespace, upsert)), - }, - updated_at: time, - }); - rawUpsert = this._serializer.savedObjectToRaw(migrated as SavedObjectSanitizedDoc); - } - - const doc = { - [type]: await this.encryptionHelper.optionallyEncryptAttributes( + return await performUpdate( + { type, id, - namespace, - attributes - ), - updated_at: time, - ...(Array.isArray(references) && { references }), - }; - - const body = await this.client - .update({ - id: this._serializer.generateRawId(namespace, type, id), - index: this.getIndexForType(type), - ...getExpectedVersionProperties(version), - refresh, - retry_on_conflict: retryOnConflict, - body: { - doc, - ...(rawUpsert && { upsert: rawUpsert._source }), - }, - _source_includes: ['namespace', 'namespaces', 'originId'], - require_alias: true, - }) - .catch((err) => { - if (SavedObjectsErrorHelpers.isEsUnavailableError(err)) { - throw err; - } - if (SavedObjectsErrorHelpers.isNotFoundError(err)) { - // see "404s from missing index" above - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - throw err; - }); - - const { originId } = body.get?._source ?? {}; - let namespaces: string[] = []; - if (!this._registry.isNamespaceAgnostic(type)) { - namespaces = body.get?._source.namespaces ?? [ - SavedObjectsUtils.namespaceIdToString(body.get?._source.namespace), - ]; - } - - const result = { - id, - type, - updated_at: time, - version: encodeHitVersion(body), - namespaces, - ...(originId && { originId }), - references, - attributes, - } as SavedObject; - - return this.encryptionHelper.optionallyDecryptAndRedactSingleResult( - result, - authorizationResult?.typeMap, - attributes + attributes, + options, + }, + this.apiExecutionContext ); } From 8b550de316aeeb03b7c15cd7586291855af3c10a Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 9 May 2023 13:24:08 +0200 Subject: [PATCH 12/34] extract bulk_update --- .../src/lib/apis/bulk_update.ts | 309 ++++++++++++++++++ .../src/lib/apis/index.ts | 1 + .../src/lib/repository.ts | 281 +--------------- 3 files changed, 317 insertions(+), 274 deletions(-) create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_update.ts diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_update.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_update.ts new file mode 100644 index 000000000000..0707f8bdf3e5 --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_update.ts @@ -0,0 +1,309 @@ +/* + * 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 { Payload } from '@hapi/boom'; +import { isNotFoundFromUnsupportedServer } from '@kbn/core-elasticsearch-server-internal'; +import { + SavedObjectsErrorHelpers, + type SavedObject, + DecoratedError, + AuthorizeUpdateObject, + SavedObjectsRawDoc, +} from '@kbn/core-saved-objects-server'; +import { ALL_NAMESPACES_STRING, SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server'; +import { encodeVersion } from '@kbn/core-saved-objects-base-server-internal'; +import { + SavedObjectsBulkUpdateObject, + SavedObjectsBulkUpdateOptions, + SavedObjectsBulkUpdateResponse, +} from '@kbn/core-saved-objects-api-server'; +import { DEFAULT_REFRESH_SETTING } from '../constants'; +import { + Either, + errorContent, + getBulkOperationError, + getCurrentTime, + getExpectedVersionProperties, + isLeft, + isMgetDoc, + isRight, + rawDocExistsInNamespace, +} from '../internal_utils'; +import { ApiExecutionContext } from './types'; + +export interface PerformUpdateParams { + objects: Array>; + options: SavedObjectsBulkUpdateOptions; +} + +export const performBulkUpdate = async ( + { objects, options }: PerformUpdateParams, + { registry, helpers, allowedTypes, client, serializer, extensions = {} }: ApiExecutionContext +): Promise> => { + const { common: commonHelper, encryption: encryptionHelper } = helpers; + const { securityExtension } = extensions; + + const namespace = commonHelper.getCurrentNamespace(options.namespace); + const time = getCurrentTime(); + + let bulkGetRequestIndexCounter = 0; + type DocumentToSave = Record; + type ExpectedBulkGetResult = Either< + { type: string; id: string; error: Payload }, + { + type: string; + id: string; + version?: string; + documentToSave: DocumentToSave; + objectNamespace?: string; + esRequestIndex?: number; + } + >; + const expectedBulkGetResults = objects.map((object) => { + const { type, id, attributes, references, version, namespace: objectNamespace } = object; + let error: DecoratedError | undefined; + if (!allowedTypes.includes(type)) { + error = SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } else { + try { + if (objectNamespace === ALL_NAMESPACES_STRING) { + error = SavedObjectsErrorHelpers.createBadRequestError('"namespace" cannot be "*"'); + } + } catch (e) { + error = e; + } + } + + if (error) { + return { + tag: 'Left', + value: { id, type, error: errorContent(error) }, + }; + } + + const documentToSave = { + [type]: attributes, + updated_at: time, + ...(Array.isArray(references) && { references }), + }; + + const requiresNamespacesCheck = registry.isMultiNamespace(object.type); + + return { + tag: 'Right', + value: { + type, + id, + version, + documentToSave, + objectNamespace, + ...(requiresNamespacesCheck && { esRequestIndex: bulkGetRequestIndexCounter++ }), + }, + }; + }); + + const validObjects = expectedBulkGetResults.filter(isRight); + if (validObjects.length === 0) { + // We only have error results; return early to avoid potentially trying authZ checks for 0 types which would result in an exception. + return { + // Technically the returned array should only contain SavedObject results, but for errors this is not true (we cast to 'any' below) + saved_objects: expectedBulkGetResults.map>( + ({ value }) => value as unknown as SavedObject + ), + }; + } + + // `objectNamespace` is a namespace string, while `namespace` is a namespace ID. + // The object namespace string, if defined, will supersede the operation's namespace ID. + const namespaceString = SavedObjectsUtils.namespaceIdToString(namespace); + const getNamespaceId = (objectNamespace?: string) => + objectNamespace !== undefined + ? SavedObjectsUtils.namespaceStringToId(objectNamespace) + : namespace; + const getNamespaceString = (objectNamespace?: string) => objectNamespace ?? namespaceString; + const bulkGetDocs = validObjects + .filter(({ value }) => value.esRequestIndex !== undefined) + .map(({ value: { type, id, objectNamespace } }) => ({ + _id: serializer.generateRawId(getNamespaceId(objectNamespace), type, id), + _index: commonHelper.getIndexForType(type), + _source: ['type', 'namespaces'], + })); + const bulkGetResponse = bulkGetDocs.length + ? await client.mget({ body: { docs: bulkGetDocs } }, { ignore: [404], meta: true }) + : undefined; + // fail fast if we can't verify a 404 response is from Elasticsearch + if ( + bulkGetResponse && + isNotFoundFromUnsupportedServer({ + statusCode: bulkGetResponse.statusCode, + headers: bulkGetResponse.headers, + }) + ) { + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(); + } + + const authObjects: AuthorizeUpdateObject[] = validObjects.map((element) => { + const { type, id, objectNamespace, esRequestIndex: index } = element.value; + const preflightResult = index !== undefined ? bulkGetResponse?.body.docs[index] : undefined; + return { + type, + id, + objectNamespace, + // @ts-expect-error MultiGetHit._source is optional + existingNamespaces: preflightResult?._source?.namespaces ?? [], + }; + }); + + const authorizationResult = await securityExtension?.authorizeBulkUpdate({ + namespace, + objects: authObjects, + }); + + let bulkUpdateRequestIndexCounter = 0; + const bulkUpdateParams: object[] = []; + type ExpectedBulkUpdateResult = Either< + { type: string; id: string; error: Payload }, + { + type: string; + id: string; + namespaces: string[]; + documentToSave: DocumentToSave; + esRequestIndex: number; + } + >; + const expectedBulkUpdateResults = await Promise.all( + expectedBulkGetResults.map>(async (expectedBulkGetResult) => { + if (isLeft(expectedBulkGetResult)) { + return expectedBulkGetResult; + } + + const { esRequestIndex, id, type, version, documentToSave, objectNamespace } = + expectedBulkGetResult.value; + + let namespaces; + let versionProperties; + if (esRequestIndex !== undefined) { + const indexFound = bulkGetResponse?.statusCode !== 404; + const actualResult = indexFound ? bulkGetResponse?.body.docs[esRequestIndex] : undefined; + const docFound = indexFound && isMgetDoc(actualResult) && actualResult.found; + if ( + !docFound || + !rawDocExistsInNamespace( + registry, + actualResult as SavedObjectsRawDoc, + getNamespaceId(objectNamespace) + ) + ) { + return { + tag: 'Left', + value: { + id, + type, + error: errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)), + }, + }; + } + // @ts-expect-error MultiGetHit is incorrectly missing _id, _source + namespaces = actualResult!._source.namespaces ?? [ + // @ts-expect-error MultiGetHit is incorrectly missing _id, _source + SavedObjectsUtils.namespaceIdToString(actualResult!._source.namespace), + ]; + versionProperties = getExpectedVersionProperties(version); + } else { + if (registry.isSingleNamespace(type)) { + // if `objectNamespace` is undefined, fall back to `options.namespace` + namespaces = [getNamespaceString(objectNamespace)]; + } + versionProperties = getExpectedVersionProperties(version); + } + + const expectedResult = { + type, + id, + namespaces, + esRequestIndex: bulkUpdateRequestIndexCounter++, + documentToSave: expectedBulkGetResult.value.documentToSave, + }; + + bulkUpdateParams.push( + { + update: { + _id: serializer.generateRawId(getNamespaceId(objectNamespace), type, id), + _index: commonHelper.getIndexForType(type), + ...versionProperties, + }, + }, + { + doc: { + ...documentToSave, + [type]: await encryptionHelper.optionallyEncryptAttributes( + type, + id, + objectNamespace || namespace, + documentToSave[type] + ), + }, + } + ); + + return { tag: 'Right', value: expectedResult }; + }) + ); + + const { refresh = DEFAULT_REFRESH_SETTING } = options; + const bulkUpdateResponse = bulkUpdateParams.length + ? await client.bulk({ + refresh, + body: bulkUpdateParams, + _source_includes: ['originId'], + require_alias: true, + }) + : undefined; + + const result = { + saved_objects: expectedBulkUpdateResults.map((expectedResult) => { + if (isLeft(expectedResult)) { + return expectedResult.value as any; + } + + const { type, id, namespaces, documentToSave, esRequestIndex } = expectedResult.value; + const response = bulkUpdateResponse?.items[esRequestIndex] ?? {}; + const rawResponse = Object.values(response)[0] as any; + + const error = getBulkOperationError(type, id, rawResponse); + if (error) { + return { type, id, error }; + } + + // When a bulk update operation is completed, any fields specified in `_sourceIncludes` will be found in the "get" value of the + // returned object. We need to retrieve the `originId` if it exists so we can return it to the consumer. + const { _seq_no: seqNo, _primary_term: primaryTerm, get } = rawResponse; + + // eslint-disable-next-line @typescript-eslint/naming-convention + const { [type]: attributes, references, updated_at } = documentToSave; + + const { originId } = get._source; + return { + id, + type, + ...(namespaces && { namespaces }), + ...(originId && { originId }), + updated_at, + version: encodeVersion(seqNo, primaryTerm), + attributes, + references, + }; + }), + }; + + return encryptionHelper.optionallyDecryptAndRedactBulkResult( + result, + authorizationResult?.typeMap, + objects + ); +}; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts index 570c281cf121..9f660da4cf5a 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts @@ -17,3 +17,4 @@ export { performFind } from './find'; export { performBulkGet } from './bulk_get'; export { performGet } from './get'; export { performUpdate } from './update'; +export { performBulkUpdate } from './bulk_update'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts index c9a819dc6217..25e3627904d6 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts @@ -8,7 +8,6 @@ import { isObject } from 'lodash'; import Boom from '@hapi/boom'; -import type { Payload } from '@hapi/boom'; import type { Logger } from '@kbn/logging'; import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import { @@ -68,15 +67,12 @@ import { type ISavedObjectsEncryptionExtension, type ISavedObjectsSecurityExtension, type ISavedObjectsSpacesExtension, - AuthorizeUpdateObject, type SavedObject, } from '@kbn/core-saved-objects-server'; import { DEFAULT_NAMESPACE_STRING } from '@kbn/core-saved-objects-utils-server'; -import { SavedObjectsErrorHelpers, type DecoratedError } from '@kbn/core-saved-objects-server'; -import { ALL_NAMESPACES_STRING, SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server'; +import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; import { SavedObjectsSerializer, - encodeVersion, encodeHitVersion, type IndexMapping, type IKibanaMigrator, @@ -86,16 +82,10 @@ import { createRepositoryEsClient, type RepositoryEsClient } from './repository_ import { getSearchDsl } from './search_dsl'; import { internalBulkResolve, isBulkResolveError } from './internal_bulk_resolve'; import { - getBulkOperationError, getCurrentTime, - getExpectedVersionProperties, normalizeNamespace, rawDocExistsInNamespace, errorContent, - type Either, - isLeft, - isRight, - isMgetDoc, } from './internal_utils'; import { collectMultiNamespaceReferences } from './collect_multi_namespace_references'; import { updateObjectsSpaces } from './update_objects_spaces'; @@ -120,6 +110,7 @@ import { performBulkGet, performGet, performUpdate, + performBulkUpdate, } from './apis'; export interface SavedObjectsRepositoryOptions { @@ -566,266 +557,12 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { objects: Array>, options: SavedObjectsBulkUpdateOptions = {} ): Promise> { - const namespace = this.getCurrentNamespace(options.namespace); - const time = getCurrentTime(); - - let bulkGetRequestIndexCounter = 0; - type DocumentToSave = Record; - type ExpectedBulkGetResult = Either< - { type: string; id: string; error: Payload }, - { - type: string; - id: string; - version?: string; - documentToSave: DocumentToSave; - objectNamespace?: string; - esRequestIndex?: number; - } - >; - const expectedBulkGetResults = objects.map((object) => { - const { type, id, attributes, references, version, namespace: objectNamespace } = object; - let error: DecoratedError | undefined; - if (!this._allowedTypes.includes(type)) { - error = SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } else { - try { - if (objectNamespace === ALL_NAMESPACES_STRING) { - error = SavedObjectsErrorHelpers.createBadRequestError('"namespace" cannot be "*"'); - } - } catch (e) { - error = e; - } - } - - if (error) { - return { - tag: 'Left', - value: { id, type, error: errorContent(error) }, - }; - } - - const documentToSave = { - [type]: attributes, - updated_at: time, - ...(Array.isArray(references) && { references }), - }; - - const requiresNamespacesCheck = this._registry.isMultiNamespace(object.type); - - return { - tag: 'Right', - value: { - type, - id, - version, - documentToSave, - objectNamespace, - ...(requiresNamespacesCheck && { esRequestIndex: bulkGetRequestIndexCounter++ }), - }, - }; - }); - - const validObjects = expectedBulkGetResults.filter(isRight); - if (validObjects.length === 0) { - // We only have error results; return early to avoid potentially trying authZ checks for 0 types which would result in an exception. - return { - // Technically the returned array should only contain SavedObject results, but for errors this is not true (we cast to 'any' below) - saved_objects: expectedBulkGetResults.map>( - ({ value }) => value as unknown as SavedObject - ), - }; - } - - // `objectNamespace` is a namespace string, while `namespace` is a namespace ID. - // The object namespace string, if defined, will supersede the operation's namespace ID. - const namespaceString = SavedObjectsUtils.namespaceIdToString(namespace); - const getNamespaceId = (objectNamespace?: string) => - objectNamespace !== undefined - ? SavedObjectsUtils.namespaceStringToId(objectNamespace) - : namespace; - const getNamespaceString = (objectNamespace?: string) => objectNamespace ?? namespaceString; - const bulkGetDocs = validObjects - .filter(({ value }) => value.esRequestIndex !== undefined) - .map(({ value: { type, id, objectNamespace } }) => ({ - _id: this._serializer.generateRawId(getNamespaceId(objectNamespace), type, id), - _index: this.getIndexForType(type), - _source: ['type', 'namespaces'], - })); - const bulkGetResponse = bulkGetDocs.length - ? await this.client.mget({ body: { docs: bulkGetDocs } }, { ignore: [404], meta: true }) - : undefined; - // fail fast if we can't verify a 404 response is from Elasticsearch - if ( - bulkGetResponse && - isNotFoundFromUnsupportedServer({ - statusCode: bulkGetResponse.statusCode, - headers: bulkGetResponse.headers, - }) - ) { - throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(); - } - - const authObjects: AuthorizeUpdateObject[] = validObjects.map((element) => { - const { type, id, objectNamespace, esRequestIndex: index } = element.value; - const preflightResult = index !== undefined ? bulkGetResponse?.body.docs[index] : undefined; - return { - type, - id, - objectNamespace, - // @ts-expect-error MultiGetHit._source is optional - existingNamespaces: preflightResult?._source?.namespaces ?? [], - }; - }); - - const authorizationResult = await this._securityExtension?.authorizeBulkUpdate({ - namespace, - objects: authObjects, - }); - - let bulkUpdateRequestIndexCounter = 0; - const bulkUpdateParams: object[] = []; - type ExpectedBulkUpdateResult = Either< - { type: string; id: string; error: Payload }, + return await performBulkUpdate( { - type: string; - id: string; - namespaces: string[]; - documentToSave: DocumentToSave; - esRequestIndex: number; - } - >; - const expectedBulkUpdateResults = await Promise.all( - expectedBulkGetResults.map>( - async (expectedBulkGetResult) => { - if (isLeft(expectedBulkGetResult)) { - return expectedBulkGetResult; - } - - const { esRequestIndex, id, type, version, documentToSave, objectNamespace } = - expectedBulkGetResult.value; - - let namespaces; - let versionProperties; - if (esRequestIndex !== undefined) { - const indexFound = bulkGetResponse?.statusCode !== 404; - const actualResult = indexFound - ? bulkGetResponse?.body.docs[esRequestIndex] - : undefined; - const docFound = indexFound && isMgetDoc(actualResult) && actualResult.found; - if ( - !docFound || - // @ts-expect-error MultiGetHit is incorrectly missing _id, _source - !this.rawDocExistsInNamespace(actualResult, getNamespaceId(objectNamespace)) - ) { - return { - tag: 'Left', - value: { - id, - type, - error: errorContent( - SavedObjectsErrorHelpers.createGenericNotFoundError(type, id) - ), - }, - }; - } - // @ts-expect-error MultiGetHit is incorrectly missing _id, _source - namespaces = actualResult!._source.namespaces ?? [ - // @ts-expect-error MultiGetHit is incorrectly missing _id, _source - SavedObjectsUtils.namespaceIdToString(actualResult!._source.namespace), - ]; - versionProperties = getExpectedVersionProperties(version); - } else { - if (this._registry.isSingleNamespace(type)) { - // if `objectNamespace` is undefined, fall back to `options.namespace` - namespaces = [getNamespaceString(objectNamespace)]; - } - versionProperties = getExpectedVersionProperties(version); - } - - const expectedResult = { - type, - id, - namespaces, - esRequestIndex: bulkUpdateRequestIndexCounter++, - documentToSave: expectedBulkGetResult.value.documentToSave, - }; - - bulkUpdateParams.push( - { - update: { - _id: this._serializer.generateRawId(getNamespaceId(objectNamespace), type, id), - _index: this.getIndexForType(type), - ...versionProperties, - }, - }, - { - doc: { - ...documentToSave, - [type]: await this.encryptionHelper.optionallyEncryptAttributes( - type, - id, - objectNamespace || namespace, - documentToSave[type] - ), - }, - } - ); - - return { tag: 'Right', value: expectedResult }; - } - ) - ); - - const { refresh = DEFAULT_REFRESH_SETTING } = options; - const bulkUpdateResponse = bulkUpdateParams.length - ? await this.client.bulk({ - refresh, - body: bulkUpdateParams, - _source_includes: ['originId'], - require_alias: true, - }) - : undefined; - - const result = { - saved_objects: expectedBulkUpdateResults.map((expectedResult) => { - if (isLeft(expectedResult)) { - return expectedResult.value as any; - } - - const { type, id, namespaces, documentToSave, esRequestIndex } = expectedResult.value; - const response = bulkUpdateResponse?.items[esRequestIndex] ?? {}; - const rawResponse = Object.values(response)[0] as any; - - const error = getBulkOperationError(type, id, rawResponse); - if (error) { - return { type, id, error }; - } - - // When a bulk update operation is completed, any fields specified in `_sourceIncludes` will be found in the "get" value of the - // returned object. We need to retrieve the `originId` if it exists so we can return it to the consumer. - const { _seq_no: seqNo, _primary_term: primaryTerm, get } = rawResponse; - - // eslint-disable-next-line @typescript-eslint/naming-convention - const { [type]: attributes, references, updated_at } = documentToSave; - - const { originId } = get._source; - return { - id, - type, - ...(namespaces && { namespaces }), - ...(originId && { originId }), - updated_at, - version: encodeVersion(seqNo, primaryTerm), - attributes, - references, - }; - }), - }; - - return this.encryptionHelper.optionallyDecryptAndRedactBulkResult( - result, - authorizationResult?.typeMap, - objects + objects, + options, + }, + this.apiExecutionContext ); } @@ -1196,8 +933,4 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { private getIndicesForTypes(types: string[]) { return this.commonHelper.getIndicesForTypes(types); } - - private rawDocExistsInNamespace(raw: SavedObjectsRawDoc, namespace: string | undefined) { - return rawDocExistsInNamespace(this._registry, raw, namespace); - } } From cb47abb09426bed9b9442de0b8d4aa7a155fcccb Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 9 May 2023 13:30:19 +0200 Subject: [PATCH 13/34] extract remove_references_to --- .../src/lib/apis/index.ts | 1 + .../src/lib/apis/remove_references_to.ts | 90 +++++++++++++++++++ .../src/lib/repository.ts | 76 ++-------------- 3 files changed, 99 insertions(+), 68 deletions(-) create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/remove_references_to.ts diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts index 9f660da4cf5a..addb77e91511 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts @@ -18,3 +18,4 @@ export { performBulkGet } from './bulk_get'; export { performGet } from './get'; export { performUpdate } from './update'; export { performBulkUpdate } from './bulk_update'; +export { performRemoveReferencesTo } from './remove_references_to'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/remove_references_to.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/remove_references_to.ts new file mode 100644 index 000000000000..5b3117a0cd52 --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/remove_references_to.ts @@ -0,0 +1,90 @@ +/* + * 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 { isNotFoundFromUnsupportedServer } from '@kbn/core-elasticsearch-server-internal'; +import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; +import { + SavedObjectsRemoveReferencesToOptions, + SavedObjectsRemoveReferencesToResponse, +} from '@kbn/core-saved-objects-api-server'; +import { ApiExecutionContext } from './types'; +import { getSearchDsl } from '../search_dsl'; + +export interface PerformRemoveReferencesToParams { + type: string; + id: string; + options: SavedObjectsRemoveReferencesToOptions; +} + +export const performRemoveReferencesTo = async ( + { type, id, options }: PerformRemoveReferencesToParams, + { registry, helpers, client, mappings, extensions = {} }: ApiExecutionContext +): Promise => { + const { common: commonHelper } = helpers; + const { securityExtension } = extensions; + + const namespace = commonHelper.getCurrentNamespace(options.namespace); + const { refresh = true } = options; + + await securityExtension?.authorizeRemoveReferences({ namespace, object: { type, id } }); + + const allTypes = registry.getAllTypes().map((t) => t.name); + + // we need to target all SO indices as all types of objects may have references to the given SO. + const targetIndices = commonHelper.getIndicesForTypes(allTypes); + + const { body, statusCode, headers } = await client.updateByQuery( + { + index: targetIndices, + refresh, + body: { + script: { + source: ` + if (ctx._source.containsKey('references')) { + def items_to_remove = []; + for (item in ctx._source.references) { + if ( (item['type'] == params['type']) && (item['id'] == params['id']) ) { + items_to_remove.add(item); + } + } + ctx._source.references.removeAll(items_to_remove); + } + `, + params: { + type, + id, + }, + lang: 'painless', + }, + conflicts: 'proceed', + ...getSearchDsl(mappings, registry, { + namespaces: namespace ? [namespace] : undefined, + type: allTypes, + hasReference: { type, id }, + }), + }, + }, + { ignore: [404], meta: true } + ); + // fail fast if we can't verify a 404 is from Elasticsearch + if (isNotFoundFromUnsupportedServer({ statusCode, headers })) { + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(type, id); + } + + if (body.failures?.length) { + throw SavedObjectsErrorHelpers.createConflictError( + type, + id, + `${body.failures.length} references could not be removed` + ); + } + + return { + updated: body.updated!, + }; +}; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts index 25e3627904d6..68a8b7d7b9c3 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts @@ -10,10 +10,7 @@ import { isObject } from 'lodash'; import Boom from '@hapi/boom'; import type { Logger } from '@kbn/logging'; import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; -import { - isSupportedEsServer, - isNotFoundFromUnsupportedServer, -} from '@kbn/core-elasticsearch-server-internal'; +import { isSupportedEsServer } from '@kbn/core-elasticsearch-server-internal'; import type { BulkResolveError } from '@kbn/core-saved-objects-server'; import type { SavedObjectsBaseOptions, @@ -60,7 +57,6 @@ import type { } from '@kbn/core-saved-objects-api-server'; import { type SavedObjectSanitizedDoc, - type SavedObjectsRawDoc, type SavedObjectsRawDocSource, type ISavedObjectTypeRegistry, type SavedObjectsExtensions, @@ -79,14 +75,8 @@ import { } from '@kbn/core-saved-objects-base-server-internal'; import { PointInTimeFinder } from './point_in_time_finder'; import { createRepositoryEsClient, type RepositoryEsClient } from './repository_es_client'; -import { getSearchDsl } from './search_dsl'; import { internalBulkResolve, isBulkResolveError } from './internal_bulk_resolve'; -import { - getCurrentTime, - normalizeNamespace, - rawDocExistsInNamespace, - errorContent, -} from './internal_utils'; +import { getCurrentTime, normalizeNamespace, errorContent } from './internal_utils'; import { collectMultiNamespaceReferences } from './collect_multi_namespace_references'; import { updateObjectsSpaces } from './update_objects_spaces'; import { @@ -111,6 +101,7 @@ import { performGet, performUpdate, performBulkUpdate, + performRemoveReferencesTo, } from './apis'; export interface SavedObjectsRepositoryOptions { @@ -574,65 +565,14 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { id: string, options: SavedObjectsRemoveReferencesToOptions = {} ): Promise { - const namespace = this.getCurrentNamespace(options.namespace); - const { refresh = true } = options; - - await this._securityExtension?.authorizeRemoveReferences({ namespace, object: { type, id } }); - - const allTypes = this._registry.getAllTypes().map((t) => t.name); - - // we need to target all SO indices as all types of objects may have references to the given SO. - const targetIndices = this.getIndicesForTypes(allTypes); - - const { body, statusCode, headers } = await this.client.updateByQuery( + return await performRemoveReferencesTo( { - index: targetIndices, - refresh, - body: { - script: { - source: ` - if (ctx._source.containsKey('references')) { - def items_to_remove = []; - for (item in ctx._source.references) { - if ( (item['type'] == params['type']) && (item['id'] == params['id']) ) { - items_to_remove.add(item); - } - } - ctx._source.references.removeAll(items_to_remove); - } - `, - params: { - type, - id, - }, - lang: 'painless', - }, - conflicts: 'proceed', - ...getSearchDsl(this._mappings, this._registry, { - namespaces: namespace ? [namespace] : undefined, - type: allTypes, - hasReference: { type, id }, - }), - }, - }, - { ignore: [404], meta: true } - ); - // fail fast if we can't verify a 404 is from Elasticsearch - if (isNotFoundFromUnsupportedServer({ statusCode, headers })) { - throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(type, id); - } - - if (body.failures?.length) { - throw SavedObjectsErrorHelpers.createConflictError( type, id, - `${body.failures.length} references could not be removed` - ); - } - - return { - updated: body.updated!, - }; + options, + }, + this.apiExecutionContext + ); } /** From d3b338f937ea3d06cadf7db1bd22655de100b97f Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 9 May 2023 13:37:24 +0200 Subject: [PATCH 14/34] extract open point in time --- .../src/lib/apis/index.ts | 1 + .../src/lib/apis/open_point_in_time.ts | 95 +++++++++++++++++++ .../src/lib/repository.ts | 80 ++-------------- 3 files changed, 105 insertions(+), 71 deletions(-) create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/open_point_in_time.ts diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts index addb77e91511..aaf3ede7ab9c 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts @@ -19,3 +19,4 @@ export { performGet } from './get'; export { performUpdate } from './update'; export { performBulkUpdate } from './bulk_update'; export { performRemoveReferencesTo } from './remove_references_to'; +export { performOpenPointInTime } from './open_point_in_time'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/open_point_in_time.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/open_point_in_time.ts new file mode 100644 index 000000000000..e5cf78c4185d --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/open_point_in_time.ts @@ -0,0 +1,95 @@ +/* + * 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 Boom from '@hapi/boom'; +import { isSupportedEsServer } from '@kbn/core-elasticsearch-server-internal'; +import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; +import { DEFAULT_NAMESPACE_STRING } from '@kbn/core-saved-objects-utils-server'; +import { + SavedObjectsOpenPointInTimeOptions, + SavedObjectsFindInternalOptions, + SavedObjectsOpenPointInTimeResponse, +} from '@kbn/core-saved-objects-api-server'; +import { ApiExecutionContext } from './types'; + +export interface PerforOpenPointInTimeParams { + type: string | string[]; + options: SavedObjectsOpenPointInTimeOptions; + internalOptions: SavedObjectsFindInternalOptions; +} + +export const performOpenPointInTime = async ( + { type, options, internalOptions }: PerforOpenPointInTimeParams, + { helpers, allowedTypes: rawAllowedTypes, client, extensions = {} }: ApiExecutionContext +): Promise => { + const { common: commonHelper } = helpers; + const { securityExtension, spacesExtension } = extensions; + const { disableExtensions } = internalOptions; + let namespaces!: string[]; + if (disableExtensions || !spacesExtension) { + namespaces = options.namespaces ?? [DEFAULT_NAMESPACE_STRING]; + // If the consumer specified `namespaces: []`, throw a Bad Request error + if (namespaces.length === 0) + throw SavedObjectsErrorHelpers.createBadRequestError( + 'options.namespaces cannot be an empty array' + ); + } + + const { keepAlive = '5m', preference } = options; + const types = Array.isArray(type) ? type : [type]; + const allowedTypes = types.filter((t) => rawAllowedTypes.includes(t)); + if (allowedTypes.length === 0) { + throw SavedObjectsErrorHelpers.createGenericNotFoundError(); + } + + if (!disableExtensions && spacesExtension) { + try { + namespaces = await spacesExtension.getSearchableNamespaces(options.namespaces); + } catch (err) { + if (Boom.isBoom(err) && err.output.payload.statusCode === 403) { + // The user is not authorized to access any space, throw a bad request error. + throw SavedObjectsErrorHelpers.createBadRequestError(); + } + throw err; + } + if (namespaces.length === 0) { + // The user is authorized to access *at least one space*, but not any of the spaces they requested; throw a bad request error. + throw SavedObjectsErrorHelpers.createBadRequestError(); + } + } + + if (!disableExtensions && securityExtension) { + await securityExtension.authorizeOpenPointInTime({ + namespaces: new Set(namespaces), + types: new Set(types), + }); + } + + const esOptions = { + index: commonHelper.getIndicesForTypes(allowedTypes), + keep_alive: keepAlive, + ...(preference ? { preference } : {}), + }; + + const { body, statusCode, headers } = await client.openPointInTime(esOptions, { + ignore: [404], + meta: true, + }); + + if (statusCode === 404) { + if (!isSupportedEsServer(headers)) { + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(); + } else { + throw SavedObjectsErrorHelpers.createGenericNotFoundError(); + } + } + + return { + id: body.id, + }; +}; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts index 68a8b7d7b9c3..223c35b9f6e1 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts @@ -7,10 +7,8 @@ */ import { isObject } from 'lodash'; -import Boom from '@hapi/boom'; import type { Logger } from '@kbn/logging'; import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; -import { isSupportedEsServer } from '@kbn/core-elasticsearch-server-internal'; import type { BulkResolveError } from '@kbn/core-saved-objects-server'; import type { SavedObjectsBaseOptions, @@ -65,7 +63,6 @@ import { type ISavedObjectsSpacesExtension, type SavedObject, } from '@kbn/core-saved-objects-server'; -import { DEFAULT_NAMESPACE_STRING } from '@kbn/core-saved-objects-utils-server'; import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; import { SavedObjectsSerializer, @@ -102,6 +99,7 @@ import { performUpdate, performBulkUpdate, performRemoveReferencesTo, + performOpenPointInTime, } from './apis'; export interface SavedObjectsRepositoryOptions { @@ -759,69 +757,14 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { options: SavedObjectsOpenPointInTimeOptions = {}, internalOptions: SavedObjectsFindInternalOptions = {} ): Promise { - const { disableExtensions } = internalOptions; - let namespaces!: string[]; - if (disableExtensions || !this._spacesExtension) { - namespaces = options.namespaces ?? [DEFAULT_NAMESPACE_STRING]; - // If the consumer specified `namespaces: []`, throw a Bad Request error - if (namespaces.length === 0) - throw SavedObjectsErrorHelpers.createBadRequestError( - 'options.namespaces cannot be an empty array' - ); - } - - const { keepAlive = '5m', preference } = options; - const types = Array.isArray(type) ? type : [type]; - const allowedTypes = types.filter((t) => this._allowedTypes.includes(t)); - if (allowedTypes.length === 0) { - throw SavedObjectsErrorHelpers.createGenericNotFoundError(); - } - - if (!disableExtensions && this._spacesExtension) { - try { - namespaces = await this._spacesExtension.getSearchableNamespaces(options.namespaces); - } catch (err) { - if (Boom.isBoom(err) && err.output.payload.statusCode === 403) { - // The user is not authorized to access any space, throw a bad request error. - throw SavedObjectsErrorHelpers.createBadRequestError(); - } - throw err; - } - if (namespaces.length === 0) { - // The user is authorized to access *at least one space*, but not any of the spaces they requested; throw a bad request error. - throw SavedObjectsErrorHelpers.createBadRequestError(); - } - } - - if (!disableExtensions && this._securityExtension) { - await this._securityExtension.authorizeOpenPointInTime({ - namespaces: new Set(namespaces), - types: new Set(types), - }); - } - - const esOptions = { - index: this.getIndicesForTypes(allowedTypes), - keep_alive: keepAlive, - ...(preference ? { preference } : {}), - }; - - const { body, statusCode, headers } = await this.client.openPointInTime(esOptions, { - ignore: [404], - meta: true, - }); - - if (statusCode === 404) { - if (!isSupportedEsServer(headers)) { - throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(); - } else { - throw SavedObjectsErrorHelpers.createGenericNotFoundError(); - } - } - - return { - id: body.id, - }; + return await performOpenPointInTime( + { + type, + options, + internalOptions, + }, + this.apiExecutionContext + ); } /** @@ -833,7 +776,6 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { internalOptions: SavedObjectsFindInternalOptions = {} ): Promise { const { disableExtensions } = internalOptions; - if (!disableExtensions && this._securityExtension) { this._securityExtension.auditClosePointInTime(); } @@ -869,8 +811,4 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { private getIndexForType(type: string) { return this.commonHelper.getIndexForType(type); } - - private getIndicesForTypes(types: string[]) { - return this.commonHelper.getIndicesForTypes(types); - } } From aeed9123d3f06e0de8edf15e384df3a033800cb3 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 9 May 2023 13:55:19 +0200 Subject: [PATCH 15/34] extract increment_counter --- .../src/lib/apis/increment_counter.ts | 52 +++++ .../src/lib/apis/index.ts | 2 + .../internals/increment_counter_internal.ts | 179 +++++++++++++++++ .../src/lib/apis/internals/index.ts | 9 + .../src/lib/repository.ts | 186 ++---------------- .../src/lib/update_objects_spaces.ts | 2 +- 6 files changed, 262 insertions(+), 168 deletions(-) create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/increment_counter.ts create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/increment_counter_internal.ts create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/index.ts diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/increment_counter.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/increment_counter.ts new file mode 100644 index 000000000000..0a22b108d53b --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/increment_counter.ts @@ -0,0 +1,52 @@ +/* + * 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 { isObject } from 'lodash'; +import { SavedObjectsErrorHelpers, type SavedObject } from '@kbn/core-saved-objects-server'; +import { + SavedObjectsIncrementCounterField, + SavedObjectsIncrementCounterOptions, +} from '@kbn/core-saved-objects-api-server'; +import { ApiExecutionContext } from './types'; +import { incrementCounterInternal } from './internals'; + +export interface PerformIncrementCounterParams { + type: string; + id: string; + counterFields: Array; + options: SavedObjectsIncrementCounterOptions; +} + +export const performIncrementCounter = async ( + { type, id, counterFields, options }: PerformIncrementCounterParams, + apiExecutionContext: ApiExecutionContext +): Promise> => { + const { allowedTypes } = apiExecutionContext; + // This is not exposed on the SOC, there are no authorization or audit logging checks + if (typeof type !== 'string') { + throw new Error('"type" argument must be a string'); + } + + const isArrayOfCounterFields = + Array.isArray(counterFields) && + counterFields.every( + (field) => + typeof field === 'string' || (isObject(field) && typeof field.fieldName === 'string') + ); + + if (!isArrayOfCounterFields) { + throw new Error( + '"counterFields" argument must be of type Array' + ); + } + if (!allowedTypes.includes(type)) { + throw SavedObjectsErrorHelpers.createUnsupportedTypeError(type); + } + + return incrementCounterInternal({ type, id, counterFields, options }, apiExecutionContext); +}; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts index aaf3ede7ab9c..958d8edfdc2a 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts @@ -20,3 +20,5 @@ export { performUpdate } from './update'; export { performBulkUpdate } from './bulk_update'; export { performRemoveReferencesTo } from './remove_references_to'; export { performOpenPointInTime } from './open_point_in_time'; +export { performIncrementCounter } from './increment_counter'; +export { incrementCounterInternal } from './internals'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/increment_counter_internal.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/increment_counter_internal.ts new file mode 100644 index 000000000000..a99222fe9a43 --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/increment_counter_internal.ts @@ -0,0 +1,179 @@ +/* + * 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 { + SavedObjectsErrorHelpers, + type SavedObject, + type SavedObjectSanitizedDoc, + SavedObjectsRawDocSource, +} from '@kbn/core-saved-objects-server'; +import { encodeHitVersion } from '@kbn/core-saved-objects-base-server-internal'; +import { + SavedObjectsIncrementCounterOptions, + SavedObjectsIncrementCounterField, +} from '@kbn/core-saved-objects-api-server'; +import { DEFAULT_REFRESH_SETTING } from '../../constants'; +import { getCurrentTime, normalizeNamespace } from '../../internal_utils'; +import { ApiExecutionContext } from '../types'; + +export interface PerformIncrementCounterInternalParams { + type: string; + id: string; + counterFields: Array; + options: SavedObjectsIncrementCounterOptions; +} + +export const incrementCounterInternal = async ( + { type, id, counterFields, options }: PerformIncrementCounterInternalParams, + { + registry, + helpers, + allowedTypes, + client, + serializer, + migrator, + extensions = {}, + }: ApiExecutionContext +): Promise> => { + const { common: commonHelper, preflight: preflightHelper } = helpers; + + const { + migrationVersion, + typeMigrationVersion, + refresh = DEFAULT_REFRESH_SETTING, + initialize = false, + upsertAttributes, + managed, + } = options; + + if (!id) { + throw SavedObjectsErrorHelpers.createBadRequestError('id cannot be empty'); // prevent potentially upserting a saved object with an empty ID + } + + const normalizedCounterFields = counterFields.map((counterField) => { + /** + * no counterField configs provided, instead a field name string was passed. + * ie `incrementCounter(so_type, id, ['my_field_name'])` + * Using the default of incrementing by 1 + */ + if (typeof counterField === 'string') { + return { + fieldName: counterField, + incrementBy: initialize ? 0 : 1, + }; + } + + const { incrementBy = 1, fieldName } = counterField; + + return { + fieldName, + incrementBy: initialize ? 0 : incrementBy, + }; + }); + const namespace = normalizeNamespace(options.namespace); + + const time = getCurrentTime(); + let savedObjectNamespace; + let savedObjectNamespaces: string[] | undefined; + + if (registry.isSingleNamespace(type) && namespace) { + savedObjectNamespace = namespace; + } else if (registry.isMultiNamespace(type)) { + // note: this check throws an error if the object is found but does not exist in this namespace + const preflightResult = await preflightHelper.preflightCheckNamespaces({ + type, + id, + namespace, + }); + if (preflightResult.checkResult === 'found_outside_namespace') { + throw SavedObjectsErrorHelpers.createConflictError(type, id); + } + + if (preflightResult.checkResult === 'not_found') { + // If an upsert would result in the creation of a new object, we need to check for alias conflicts too. + // This takes an extra round trip to Elasticsearch, but this won't happen often. + // TODO: improve performance by combining these into a single preflight check + await preflightHelper.preflightCheckForUpsertAliasConflict(type, id, namespace); + } + + savedObjectNamespaces = preflightResult.savedObjectNamespaces; + } + + // attributes: { [counterFieldName]: incrementBy }, + const migrated = migrator.migrateDocument({ + id, + type, + ...(savedObjectNamespace && { namespace: savedObjectNamespace }), + ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), + attributes: { + ...(upsertAttributes ?? {}), + ...normalizedCounterFields.reduce((acc, counterField) => { + const { fieldName, incrementBy } = counterField; + acc[fieldName] = incrementBy; + return acc; + }, {} as Record), + }, + migrationVersion, + typeMigrationVersion, + managed, + updated_at: time, + }); + + const raw = serializer.savedObjectToRaw(migrated as SavedObjectSanitizedDoc); + + const body = await client.update({ + id: raw._id, + index: commonHelper.getIndexForType(type), + refresh, + require_alias: true, + _source: true, + body: { + script: { + source: ` + for (int i = 0; i < params.counterFieldNames.length; i++) { + def counterFieldName = params.counterFieldNames[i]; + def count = params.counts[i]; + + if (ctx._source[params.type][counterFieldName] == null) { + ctx._source[params.type][counterFieldName] = count; + } + else { + ctx._source[params.type][counterFieldName] += count; + } + } + ctx._source.updated_at = params.time; + `, + lang: 'painless', + params: { + counts: normalizedCounterFields.map( + (normalizedCounterField) => normalizedCounterField.incrementBy + ), + counterFieldNames: normalizedCounterFields.map( + (normalizedCounterField) => normalizedCounterField.fieldName + ), + time, + type, + }, + }, + upsert: raw._source, + }, + }); + + const { originId } = body.get?._source ?? {}; + return { + id, + type, + ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), + ...(originId && { originId }), + updated_at: time, + references: body.get?._source.references ?? [], + version: encodeHitVersion(body), + attributes: body.get?._source[type], + ...(managed && { managed }), + }; +}; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/index.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/index.ts new file mode 100644 index 000000000000..fb0a5b660ef6 --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { incrementCounterInternal } from './increment_counter_internal'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts index 223c35b9f6e1..d79e88383707 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import { isObject } from 'lodash'; import type { Logger } from '@kbn/logging'; import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import type { BulkResolveError } from '@kbn/core-saved-objects-server'; @@ -54,8 +53,6 @@ import type { ISavedObjectsRepository, } from '@kbn/core-saved-objects-api-server'; import { - type SavedObjectSanitizedDoc, - type SavedObjectsRawDocSource, type ISavedObjectTypeRegistry, type SavedObjectsExtensions, type ISavedObjectsEncryptionExtension, @@ -63,17 +60,15 @@ import { type ISavedObjectsSpacesExtension, type SavedObject, } from '@kbn/core-saved-objects-server'; -import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; import { SavedObjectsSerializer, - encodeHitVersion, type IndexMapping, type IKibanaMigrator, } from '@kbn/core-saved-objects-base-server-internal'; import { PointInTimeFinder } from './point_in_time_finder'; import { createRepositoryEsClient, type RepositoryEsClient } from './repository_es_client'; import { internalBulkResolve, isBulkResolveError } from './internal_bulk_resolve'; -import { getCurrentTime, normalizeNamespace, errorContent } from './internal_utils'; +import { errorContent } from './internal_utils'; import { collectMultiNamespaceReferences } from './collect_multi_namespace_references'; import { updateObjectsSpaces } from './update_objects_spaces'; import { @@ -84,7 +79,6 @@ import { PreflightCheckHelper, SerializerHelper, } from './helpers'; -import { DEFAULT_REFRESH_SETTING } from './constants'; import { type ApiExecutionContext, performCreate, @@ -100,6 +94,8 @@ import { performBulkUpdate, performRemoveReferencesTo, performOpenPointInTime, + incrementCounterInternal, + performIncrementCounter, } from './apis'; export interface SavedObjectsRepositoryOptions { @@ -580,173 +576,29 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { type: string, id: string, counterFields: Array, - options?: SavedObjectsIncrementCounterOptions + options: SavedObjectsIncrementCounterOptions = {} ) { - // This is not exposed on the SOC, there are no authorization or audit logging checks - if (typeof type !== 'string') { - throw new Error('"type" argument must be a string'); - } - - const isArrayOfCounterFields = - Array.isArray(counterFields) && - counterFields.every( - (field) => - typeof field === 'string' || (isObject(field) && typeof field.fieldName === 'string') - ); - - if (!isArrayOfCounterFields) { - throw new Error( - '"counterFields" argument must be of type Array' - ); - } - if (!this._allowedTypes.includes(type)) { - throw SavedObjectsErrorHelpers.createUnsupportedTypeError(type); - } - - return this.incrementCounterInternal(type, id, counterFields, options); + return await performIncrementCounter( + { + type, + id, + counterFields, + options, + }, + this.apiExecutionContext + ); } - /** @internal incrementCounter function that is used internally and bypasses validation checks. */ - private async incrementCounterInternal( + async incrementCounterInternal( type: string, id: string, counterFields: Array, options: SavedObjectsIncrementCounterOptions = {} - ): Promise> { - const { - migrationVersion, - typeMigrationVersion, - refresh = DEFAULT_REFRESH_SETTING, - initialize = false, - upsertAttributes, - managed, - } = options; - - if (!id) { - throw SavedObjectsErrorHelpers.createBadRequestError('id cannot be empty'); // prevent potentially upserting a saved object with an empty ID - } - - const normalizedCounterFields = counterFields.map((counterField) => { - /** - * no counterField configs provided, instead a field name string was passed. - * ie `incrementCounter(so_type, id, ['my_field_name'])` - * Using the default of incrementing by 1 - */ - if (typeof counterField === 'string') { - return { - fieldName: counterField, - incrementBy: initialize ? 0 : 1, - }; - } - - const { incrementBy = 1, fieldName } = counterField; - - return { - fieldName, - incrementBy: initialize ? 0 : incrementBy, - }; - }); - const namespace = normalizeNamespace(options.namespace); - - const time = getCurrentTime(); - let savedObjectNamespace; - let savedObjectNamespaces: string[] | undefined; - - if (this._registry.isSingleNamespace(type) && namespace) { - savedObjectNamespace = namespace; - } else if (this._registry.isMultiNamespace(type)) { - // note: this check throws an error if the object is found but does not exist in this namespace - const preflightResult = await this.preflightCheckHelper.preflightCheckNamespaces({ - type, - id, - namespace, - }); - if (preflightResult.checkResult === 'found_outside_namespace') { - throw SavedObjectsErrorHelpers.createConflictError(type, id); - } - - if (preflightResult.checkResult === 'not_found') { - // If an upsert would result in the creation of a new object, we need to check for alias conflicts too. - // This takes an extra round trip to Elasticsearch, but this won't happen often. - // TODO: improve performance by combining these into a single preflight check - await this.preflightCheckHelper.preflightCheckForUpsertAliasConflict(type, id, namespace); - } - - savedObjectNamespaces = preflightResult.savedObjectNamespaces; - } - - // attributes: { [counterFieldName]: incrementBy }, - const migrated = this._migrator.migrateDocument({ - id, - type, - ...(savedObjectNamespace && { namespace: savedObjectNamespace }), - ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), - attributes: { - ...(upsertAttributes ?? {}), - ...normalizedCounterFields.reduce((acc, counterField) => { - const { fieldName, incrementBy } = counterField; - acc[fieldName] = incrementBy; - return acc; - }, {} as Record), - }, - migrationVersion, - typeMigrationVersion, - managed, - updated_at: time, - }); - - const raw = this._serializer.savedObjectToRaw(migrated as SavedObjectSanitizedDoc); - - const body = await this.client.update({ - id: raw._id, - index: this.getIndexForType(type), - refresh, - require_alias: true, - _source: true, - body: { - script: { - source: ` - for (int i = 0; i < params.counterFieldNames.length; i++) { - def counterFieldName = params.counterFieldNames[i]; - def count = params.counts[i]; - - if (ctx._source[params.type][counterFieldName] == null) { - ctx._source[params.type][counterFieldName] = count; - } - else { - ctx._source[params.type][counterFieldName] += count; - } - } - ctx._source.updated_at = params.time; - `, - lang: 'painless', - params: { - counts: normalizedCounterFields.map( - (normalizedCounterField) => normalizedCounterField.incrementBy - ), - counterFieldNames: normalizedCounterFields.map( - (normalizedCounterField) => normalizedCounterField.fieldName - ), - time, - type, - }, - }, - upsert: raw._source, - }, - }); - - const { originId } = body.get?._source ?? {}; - return { - id, - type, - ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), - ...(originId && { originId }), - updated_at: time, - references: body.get?._source.references ?? [], - version: encodeHitVersion(body), - attributes: body.get?._source[type], - ...(managed && { managed }), - }; + ) { + return incrementCounterInternal( + { type, id, counterFields, options }, + this.apiExecutionContext + ); } /** diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/update_objects_spaces.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/update_objects_spaces.ts index dd6bdc5c3e17..f4d6139908ae 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/update_objects_spaces.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/update_objects_spaces.ts @@ -39,7 +39,7 @@ import { isLeft, isRight, } from './internal_utils'; -import { DEFAULT_REFRESH_SETTING } from './repository'; +import { DEFAULT_REFRESH_SETTING } from './constants'; import type { RepositoryEsClient } from './repository_es_client'; import type { DeleteLegacyUrlAliasesParams } from './legacy_url_aliases'; import { deleteLegacyUrlAliases } from './legacy_url_aliases'; From 109017d73bc47243406df38592a61dd1de878dee Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 9 May 2023 14:11:23 +0200 Subject: [PATCH 16/34] implement resolve and bulk resolve --- .../src/lib/apis/bulk_resolve.ts | 68 +++++++++++++++++++ .../src/lib/apis/index.ts | 2 + .../internals/increment_counter_internal.ts | 10 +-- .../src/lib/apis/resolve.ts | 59 ++++++++++++++++ .../src/lib/internal_bulk_resolve.ts | 7 +- .../src/lib/repository.ts | 64 +++++------------ 6 files changed, 151 insertions(+), 59 deletions(-) create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_resolve.ts create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/resolve.ts diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_resolve.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_resolve.ts new file mode 100644 index 000000000000..209dbc51f517 --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_resolve.ts @@ -0,0 +1,68 @@ +/* + * 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 SavedObject, BulkResolveError } from '@kbn/core-saved-objects-server'; +import { + SavedObjectsBulkResolveObject, + SavedObjectsBulkResolveResponse, + SavedObjectsResolveOptions, + SavedObjectsResolveResponse, +} from '@kbn/core-saved-objects-api-server'; +import { errorContent } from '../internal_utils'; +import { ApiExecutionContext } from './types'; +import { internalBulkResolve, isBulkResolveError } from '../internal_bulk_resolve'; +import { incrementCounterInternal } from './internals/increment_counter_internal'; + +export interface PerformCreateParams { + objects: SavedObjectsBulkResolveObject[]; + options: SavedObjectsResolveOptions; +} + +export const performBulkResolve = async ( + { objects, options }: PerformCreateParams, + apiExecutionContext: ApiExecutionContext +): Promise> => { + const { + registry, + helpers, + allowedTypes, + client, + serializer, + extensions = {}, + } = apiExecutionContext; + const { common: commonHelper } = helpers; + const { securityExtension, encryptionExtension } = extensions; + const namespace = commonHelper.getCurrentNamespace(options.namespace); + + const { resolved_objects: bulkResults } = await internalBulkResolve({ + registry, + allowedTypes, + client, + serializer, + getIndexForType: commonHelper.getIndexForType.bind(commonHelper), + incrementCounterInternal: (type, id, counterFields, opts = {}) => + incrementCounterInternal({ type, id, counterFields, options: opts }, apiExecutionContext), + encryptionExtension, + securityExtension, + objects, + options: { ...options, namespace }, + }); + const resolvedObjects = bulkResults.map>((result) => { + // extract payloads from saved object errors + if (isBulkResolveError(result)) { + const errorResult = result as BulkResolveError; + const { type, id, error } = errorResult; + return { + saved_object: { type, id, error: errorContent(error) } as unknown as SavedObject, + outcome: 'exactMatch', + }; + } + return result; + }); + return { resolved_objects: resolvedObjects }; +}; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts index 958d8edfdc2a..d4703158b92c 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts @@ -21,4 +21,6 @@ export { performBulkUpdate } from './bulk_update'; export { performRemoveReferencesTo } from './remove_references_to'; export { performOpenPointInTime } from './open_point_in_time'; export { performIncrementCounter } from './increment_counter'; +export { performBulkResolve } from './bulk_resolve'; +export { performResolve } from './resolve'; export { incrementCounterInternal } from './internals'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/increment_counter_internal.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/increment_counter_internal.ts index a99222fe9a43..0853e57100ed 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/increment_counter_internal.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/increment_counter_internal.ts @@ -30,15 +30,7 @@ export interface PerformIncrementCounterInternalParams { export const incrementCounterInternal = async ( { type, id, counterFields, options }: PerformIncrementCounterInternalParams, - { - registry, - helpers, - allowedTypes, - client, - serializer, - migrator, - extensions = {}, - }: ApiExecutionContext + { registry, helpers, client, serializer, migrator }: ApiExecutionContext ): Promise> => { const { common: commonHelper, preflight: preflightHelper } = helpers; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/resolve.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/resolve.ts new file mode 100644 index 000000000000..8728696e589c --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/resolve.ts @@ -0,0 +1,59 @@ +/* + * 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 { + SavedObjectsResolveOptions, + SavedObjectsResolveResponse, +} from '@kbn/core-saved-objects-api-server'; +import { ApiExecutionContext } from './types'; +import { internalBulkResolve, isBulkResolveError } from '../internal_bulk_resolve'; +import { incrementCounterInternal } from './internals/increment_counter_internal'; + +export interface PerformCreateParams { + type: string; + id: string; + options: SavedObjectsResolveOptions; +} + +export const performResolve = async ( + { type, id, options }: PerformCreateParams, + apiExecutionContext: ApiExecutionContext +): Promise> => { + const { + registry, + helpers, + allowedTypes, + client, + serializer, + extensions = {}, + } = apiExecutionContext; + const { common: commonHelper } = helpers; + const { securityExtension, encryptionExtension } = extensions; + const namespace = commonHelper.getCurrentNamespace(options.namespace); + const { resolved_objects: bulkResults } = await internalBulkResolve({ + registry, + allowedTypes, + client, + serializer, + getIndexForType: commonHelper.getIndexForType.bind(this), + incrementCounterInternal: (t, i, counterFields, opts = {}) => + incrementCounterInternal( + { type: t, id: i, counterFields, options: opts }, + apiExecutionContext + ), + encryptionExtension, + securityExtension, + objects: [{ type, id }], + options: { ...options, namespace }, + }); + const [result] = bulkResults; + if (isBulkResolveError(result)) { + throw result.error; + } + return result; +}; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_bulk_resolve.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_bulk_resolve.ts index 907474a008a3..b2c9a6a7e5db 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_bulk_resolve.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_bulk_resolve.ts @@ -23,12 +23,12 @@ import { type SavedObjectsRawDocSource, type SavedObject, type BulkResolveError, + type ISavedObjectsSerializer, SavedObjectsErrorHelpers, } from '@kbn/core-saved-objects-server'; import { LEGACY_URL_ALIAS_TYPE, type LegacyUrlAlias, - type SavedObjectsSerializer, } from '@kbn/core-saved-objects-base-server-internal'; import { CORE_USAGE_STATS_ID, @@ -59,7 +59,7 @@ export interface InternalBulkResolveParams { registry: ISavedObjectTypeRegistry; allowedTypes: string[]; client: RepositoryEsClient; - serializer: SavedObjectsSerializer; + serializer: ISavedObjectsSerializer; getIndexForType: (type: string) => string; incrementCounterInternal: ( type: string, @@ -290,7 +290,7 @@ function validateObjectTypes(objects: SavedObjectsBulkResolveObject[], allowedTy async function fetchAndUpdateAliases( validObjects: Array>, client: RepositoryEsClient, - serializer: SavedObjectsSerializer, + serializer: ISavedObjectsSerializer, getIndexForType: (type: string) => string, namespace: string | undefined ) { @@ -342,6 +342,7 @@ async function fetchAndUpdateAliases( return item.update?.get; }); } + class ResolveCounter { private record = new Map(); diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts index d79e88383707..dee0dbb06e60 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts @@ -8,7 +8,6 @@ import type { Logger } from '@kbn/logging'; import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; -import type { BulkResolveError } from '@kbn/core-saved-objects-server'; import type { SavedObjectsBaseOptions, SavedObjectsIncrementCounterOptions, @@ -67,8 +66,6 @@ import { } from '@kbn/core-saved-objects-base-server-internal'; import { PointInTimeFinder } from './point_in_time_finder'; import { createRepositoryEsClient, type RepositoryEsClient } from './repository_es_client'; -import { internalBulkResolve, isBulkResolveError } from './internal_bulk_resolve'; -import { errorContent } from './internal_utils'; import { collectMultiNamespaceReferences } from './collect_multi_namespace_references'; import { updateObjectsSpaces } from './update_objects_spaces'; import { @@ -96,6 +93,8 @@ import { performOpenPointInTime, incrementCounterInternal, performIncrementCounter, + performBulkResolve, + performResolve, } from './apis'; export interface SavedObjectsRepositoryOptions { @@ -394,32 +393,13 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { objects: SavedObjectsBulkResolveObject[], options: SavedObjectsResolveOptions = {} ): Promise> { - const namespace = this.getCurrentNamespace(options.namespace); - const { resolved_objects: bulkResults } = await internalBulkResolve({ - registry: this._registry, - allowedTypes: this._allowedTypes, - client: this.client, - serializer: this._serializer, - getIndexForType: this.getIndexForType.bind(this), - incrementCounterInternal: this.incrementCounterInternal.bind(this), - encryptionExtension: this._encryptionExtension, - securityExtension: this._securityExtension, - objects, - options: { ...options, namespace }, - }); - const resolvedObjects = bulkResults.map>((result) => { - // extract payloads from saved object errors - if (isBulkResolveError(result)) { - const errorResult = result as BulkResolveError; - const { type, id, error } = errorResult; - return { - saved_object: { type, id, error: errorContent(error) } as unknown as SavedObject, - outcome: 'exactMatch', - }; - } - return result; - }); - return { resolved_objects: resolvedObjects }; + return await performBulkResolve( + { + objects, + options, + }, + this.apiExecutionContext + ); } /** @@ -448,24 +428,14 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { id: string, options: SavedObjectsResolveOptions = {} ): Promise> { - const namespace = this.getCurrentNamespace(options.namespace); - const { resolved_objects: bulkResults } = await internalBulkResolve({ - registry: this._registry, - allowedTypes: this._allowedTypes, - client: this.client, - serializer: this._serializer, - getIndexForType: this.getIndexForType.bind(this), - incrementCounterInternal: this.incrementCounterInternal.bind(this), - encryptionExtension: this._encryptionExtension, - securityExtension: this._securityExtension, - objects: [{ type, id }], - options: { ...options, namespace }, - }); - const [result] = bulkResults; - if (isBulkResolveError(result)) { - throw result.error; - } - return result; + return await performResolve( + { + type, + id, + options, + }, + this.apiExecutionContext + ); } /** From e7f1e341159b1d925eb255fc7a2d53d91193e95b Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 9 May 2023 14:17:22 +0200 Subject: [PATCH 17/34] move internals --- .../src/lib/apis/bulk_resolve.ts | 2 +- .../src/lib/apis/internals/index.ts | 6 ++++++ .../{ => apis/internals}/internal_bulk_resolve.test.mock.ts | 6 +++--- .../lib/{ => apis/internals}/internal_bulk_resolve.test.ts | 6 +++--- .../src/lib/{ => apis/internals}/internal_bulk_resolve.ts | 4 ++-- .../src/lib/apis/resolve.ts | 2 +- .../src/lib/repository.test.mock.ts | 6 +++--- 7 files changed, 19 insertions(+), 13 deletions(-) rename packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/{ => apis/internals}/internal_bulk_resolve.test.mock.ts (88%) rename packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/{ => apis/internals}/internal_bulk_resolve.test.ts (98%) rename packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/{ => apis/internals}/internal_bulk_resolve.ts (99%) diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_resolve.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_resolve.ts index 209dbc51f517..07b5f25af947 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_resolve.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_resolve.ts @@ -15,7 +15,7 @@ import { } from '@kbn/core-saved-objects-api-server'; import { errorContent } from '../internal_utils'; import { ApiExecutionContext } from './types'; -import { internalBulkResolve, isBulkResolveError } from '../internal_bulk_resolve'; +import { internalBulkResolve, isBulkResolveError } from './internals/internal_bulk_resolve'; import { incrementCounterInternal } from './internals/increment_counter_internal'; export interface PerformCreateParams { diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/index.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/index.ts index fb0a5b660ef6..a7e0b895bdc1 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/index.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/index.ts @@ -7,3 +7,9 @@ */ export { incrementCounterInternal } from './increment_counter_internal'; +export { + internalBulkResolve, + isBulkResolveError, + type InternalBulkResolveParams, + type InternalSavedObjectsBulkResolveResponse, +} from './internal_bulk_resolve'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_bulk_resolve.test.mock.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve.test.mock.ts similarity index 88% rename from packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_bulk_resolve.test.mock.ts rename to packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve.test.mock.ts index 277d1ae4af34..e30e059f5e76 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_bulk_resolve.test.mock.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve.test.mock.ts @@ -7,7 +7,7 @@ */ import type { isNotFoundFromUnsupportedServer } from '@kbn/core-elasticsearch-server-internal'; -import type * as InternalUtils from './internal_utils'; +import type * as InternalUtils from '../../internal_utils'; export const mockGetSavedObjectFromSource = jest.fn() as jest.MockedFunction< typeof InternalUtils['getSavedObjectFromSource'] @@ -16,8 +16,8 @@ export const mockRawDocExistsInNamespace = jest.fn() as jest.MockedFunction< typeof InternalUtils['rawDocExistsInNamespace'] >; -jest.mock('./internal_utils', () => { - const actual = jest.requireActual('./internal_utils'); +jest.mock('../../internal_utils', () => { + const actual = jest.requireActual('../../internal_utils'); return { ...actual, getSavedObjectFromSource: mockGetSavedObjectFromSource, diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_bulk_resolve.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve.test.ts similarity index 98% rename from packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_bulk_resolve.test.ts rename to packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve.test.ts index 232c19fa7a84..c8ec49ca59a8 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_bulk_resolve.test.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve.test.ts @@ -24,7 +24,7 @@ import { } from '@kbn/core-saved-objects-base-server-internal'; import { typeRegistryMock } from '@kbn/core-saved-objects-base-server-mocks'; import { internalBulkResolve, type InternalBulkResolveParams } from './internal_bulk_resolve'; -import { normalizeNamespace } from './internal_utils'; +import { normalizeNamespace } from '../../internal_utils'; import { type ISavedObjectsEncryptionExtension, type ISavedObjectsSecurityExtension, @@ -36,8 +36,8 @@ import { enforceError, setupAuthorizeAndRedactInternalBulkResolveFailure, setupAuthorizeAndRedactInternalBulkResolveSuccess, -} from '../test_helpers/repository.test.common'; -import { savedObjectsExtensionsMock } from '../mocks/saved_objects_extensions.mock'; +} from '../../../test_helpers/repository.test.common'; +import { savedObjectsExtensionsMock } from '../../../mocks/saved_objects_extensions.mock'; const VERSION_PROPS = { _seq_no: 1, _primary_term: 1 }; const OBJ_TYPE = 'obj-type'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_bulk_resolve.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve.ts similarity index 99% rename from packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_bulk_resolve.ts rename to packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve.ts index b2c9a6a7e5db..dd58f6a4d581 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_bulk_resolve.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve.ts @@ -45,8 +45,8 @@ import { type Right, isLeft, isRight, -} from './internal_utils'; -import type { RepositoryEsClient } from './repository_es_client'; +} from '../../internal_utils'; +import type { RepositoryEsClient } from '../../repository_es_client'; const MAX_CONCURRENT_RESOLVE = 10; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/resolve.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/resolve.ts index 8728696e589c..de4c4d4d0e45 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/resolve.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/resolve.ts @@ -11,7 +11,7 @@ import { SavedObjectsResolveResponse, } from '@kbn/core-saved-objects-api-server'; import { ApiExecutionContext } from './types'; -import { internalBulkResolve, isBulkResolveError } from '../internal_bulk_resolve'; +import { internalBulkResolve, isBulkResolveError } from './internals/internal_bulk_resolve'; import { incrementCounterInternal } from './internals/increment_counter_internal'; export interface PerformCreateParams { diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.mock.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.mock.ts index a9c1871e2488..a3a891664cb8 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.mock.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.mock.ts @@ -7,7 +7,7 @@ */ import type { collectMultiNamespaceReferences } from './collect_multi_namespace_references'; -import type { internalBulkResolve } from './internal_bulk_resolve'; +import type { internalBulkResolve } from './apis/internals/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'; @@ -23,8 +23,8 @@ jest.mock('./collect_multi_namespace_references', () => ({ export const mockInternalBulkResolve = jest.fn() as jest.MockedFunction; -jest.mock('./internal_bulk_resolve', () => ({ - ...jest.requireActual('./internal_bulk_resolve'), +jest.mock('./apis/internals/internal_bulk_resolve', () => ({ + ...jest.requireActual('./apis/internals/internal_bulk_resolve'), internalBulkResolve: mockInternalBulkResolve, })); From aef0cfa2f772955996df9c1906459cf8bbb4fe5a Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 9 May 2023 14:17:52 +0200 Subject: [PATCH 18/34] delete dead code --- .../src/lib/priority_collection.test.ts | 59 ------------------- .../src/lib/priority_collection.ts | 37 ------------ 2 files changed, 96 deletions(-) delete mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/priority_collection.test.ts delete mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/priority_collection.ts diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/priority_collection.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/priority_collection.test.ts deleted file mode 100644 index 300dc1349b59..000000000000 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/priority_collection.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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 { PriorityCollection } from './priority_collection'; - -test(`1, 2, 3`, () => { - const priorityCollection = new PriorityCollection(); - priorityCollection.add(1, 1); - priorityCollection.add(2, 2); - priorityCollection.add(3, 3); - expect(priorityCollection.toPrioritizedArray()).toEqual([1, 2, 3]); -}); - -test(`3, 2, 1`, () => { - const priorityCollection = new PriorityCollection(); - priorityCollection.add(3, 3); - priorityCollection.add(2, 2); - priorityCollection.add(1, 1); - expect(priorityCollection.toPrioritizedArray()).toEqual([1, 2, 3]); -}); - -test(`2, 3, 1`, () => { - const priorityCollection = new PriorityCollection(); - priorityCollection.add(2, 2); - priorityCollection.add(3, 3); - priorityCollection.add(1, 1); - expect(priorityCollection.toPrioritizedArray()).toEqual([1, 2, 3]); -}); - -test(`Number.MAX_VALUE, NUMBER.MIN_VALUE, 1`, () => { - const priorityCollection = new PriorityCollection(); - priorityCollection.add(Number.MAX_VALUE, 3); - priorityCollection.add(Number.MIN_VALUE, 1); - priorityCollection.add(1, 2); - expect(priorityCollection.toPrioritizedArray()).toEqual([1, 2, 3]); -}); - -test(`1, 1 throws Error`, () => { - const priorityCollection = new PriorityCollection(); - priorityCollection.add(1, 1); - expect(() => priorityCollection.add(1, 1)).toThrowErrorMatchingSnapshot(); -}); - -test(`#has when empty returns false`, () => { - const priorityCollection = new PriorityCollection(); - expect(priorityCollection.has(() => true)).toEqual(false); -}); - -test(`#has returns result of predicate`, () => { - const priorityCollection = new PriorityCollection(); - priorityCollection.add(1, 'foo'); - expect(priorityCollection.has((val) => val === 'foo')).toEqual(true); - expect(priorityCollection.has((val) => val === 'bar')).toEqual(false); -}); diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/priority_collection.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/priority_collection.ts deleted file mode 100644 index 66751101dd8b..000000000000 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/priority_collection.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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. - */ - -interface PriorityCollectionEntry { - priority: number; - value: T; -} - -export class PriorityCollection { - private readonly array: Array> = []; - - public add(priority: number, value: T) { - const foundIndex = this.array.findIndex((current) => { - if (priority === current.priority) { - throw new Error('Already have entry with this priority'); - } - - return priority < current.priority; - }); - - const spliceIndex = foundIndex === -1 ? this.array.length : foundIndex; - this.array.splice(spliceIndex, 0, { priority, value }); - } - - public has(predicate: (value: T) => boolean): boolean { - return this.array.some((entry) => predicate(entry.value)); - } - - public toPrioritizedArray(): T[] { - return this.array.map((entry) => entry.value); - } -} From 91cfdc0b1c5c2765fa8f58c33ac51f3a7b495e6f Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 9 May 2023 14:23:25 +0200 Subject: [PATCH 19/34] remove obsolete snapshot --- .../priority_collection.test.ts.snap | 3 --- .../src/lib/apis/bulk_get.ts | 15 +++++++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) delete mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/__snapshots__/priority_collection.test.ts.snap diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/__snapshots__/priority_collection.test.ts.snap b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/__snapshots__/priority_collection.test.ts.snap deleted file mode 100644 index fd96c54450cf..000000000000 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/__snapshots__/priority_collection.test.ts.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`1, 1 throws Error 1`] = `"Already have entry with this priority"`; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_get.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_get.ts index a5a87639bfb5..d4df9e7e08dd 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_get.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_get.ts @@ -21,7 +21,14 @@ import { SavedObjectsBulkResponse, SavedObjectsGetOptions, } from '@kbn/core-saved-objects-api-server'; -import { Either, errorContent, getSavedObjectFromSource, isLeft, isRight } from '../internal_utils'; +import { + Either, + errorContent, + getSavedObjectFromSource, + isLeft, + isRight, + rawDocExistsInNamespaces, +} from '../internal_utils'; import { ApiExecutionContext } from './types'; import { includedFields } from '../included_fields'; @@ -32,7 +39,7 @@ export interface PerformBulkGetParams { export const performBulkGet = async ( { objects, options }: PerformBulkGetParams, - { helpers, allowedTypes, client, serializer, extensions = {} }: ApiExecutionContext + { helpers, allowedTypes, client, serializer, registry, extensions = {} }: ApiExecutionContext ): Promise> => { const { common: commonHelper, @@ -168,7 +175,7 @@ export const performBulkGet = async ( const doc = bulkGetResponse?.body.docs[esRequestIndex]; // @ts-expect-error MultiGetHit._source is optional - const docNotFound = !doc?.found || !this.rawDocExistsInNamespaces(doc, namespaces); + const docNotFound = !doc?.found || !rawDocExistsInNamespaces(registry, doc, namespaces); authObjects.push({ type, @@ -188,7 +195,7 @@ export const performBulkGet = async ( } // @ts-expect-error MultiGetHit._source is optional - return getSavedObjectFromSource(this._registry, type, id, doc, { + return getSavedObjectFromSource(registry, type, id, doc, { migrationVersionCompatibility, }); }), From 81ea64713c8ecd201e89e2886a3fe6686d4da4a2 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 9 May 2023 14:44:30 +0200 Subject: [PATCH 20/34] some more cleanup --- .../src/lib/apis/index.ts | 1 - ...ct_multi_namespace_references.test.mock.ts | 4 +- .../lib/collect_multi_namespace_references.ts | 2 +- .../src/lib/repository.ts | 68 ++++++------------- .../find_shared_origin_objects.test.ts | 4 +- .../{ => utils}/find_shared_origin_objects.ts | 0 .../src/lib/utils/index.ts | 1 + 7 files changed, 25 insertions(+), 55 deletions(-) rename packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/{ => utils}/find_shared_origin_objects.test.ts (97%) rename packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/{ => utils}/find_shared_origin_objects.ts (100%) diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts index d4703158b92c..fe490d80ff4d 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts @@ -23,4 +23,3 @@ export { performOpenPointInTime } from './open_point_in_time'; export { performIncrementCounter } from './increment_counter'; export { performBulkResolve } from './bulk_resolve'; export { performResolve } from './resolve'; -export { incrementCounterInternal } from './internals'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.test.mock.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.test.mock.ts index 5476f99c3b37..fb28f2c6687e 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.test.mock.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.test.mock.ts @@ -7,7 +7,7 @@ */ import type { findLegacyUrlAliases } from './legacy_url_aliases'; -import type { findSharedOriginObjects } from './find_shared_origin_objects'; +import type { findSharedOriginObjects } from './utils'; import type * as InternalUtils from './internal_utils'; export const mockFindLegacyUrlAliases = jest.fn() as jest.MockedFunction< @@ -22,7 +22,7 @@ export const mockFindSharedOriginObjects = jest.fn() as jest.MockedFunction< typeof findSharedOriginObjects >; -jest.mock('./find_shared_origin_objects', () => { +jest.mock('./utils/find_shared_origin_objects', () => { return { findSharedOriginObjects: mockFindSharedOriginObjects }; }); diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.ts index d8b25dc886f2..8e629fbdb7e5 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.ts @@ -30,7 +30,7 @@ import { getRootFields } from './included_fields'; import { getSavedObjectFromSource, rawDocExistsInNamespace } from './internal_utils'; import type { CreatePointInTimeFinderFn } from './point_in_time_finder'; import type { RepositoryEsClient } from './repository_es_client'; -import { findSharedOriginObjects } from './find_shared_origin_objects'; +import { findSharedOriginObjects } from './utils'; /** * When we collect an object's outbound references, we will only go a maximum of this many levels deep before we throw an error. diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts index dee0dbb06e60..f433c1292d51 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts @@ -54,9 +54,6 @@ import type { import { type ISavedObjectTypeRegistry, type SavedObjectsExtensions, - type ISavedObjectsEncryptionExtension, - type ISavedObjectsSecurityExtension, - type ISavedObjectsSpacesExtension, type SavedObject, } from '@kbn/core-saved-objects-server'; import { @@ -91,7 +88,6 @@ import { performBulkUpdate, performRemoveReferencesTo, performOpenPointInTime, - incrementCounterInternal, performIncrementCounter, performBulkResolve, performResolve, @@ -123,16 +119,8 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { private _registry: ISavedObjectTypeRegistry; private _allowedTypes: string[]; private readonly client: RepositoryEsClient; - private readonly _encryptionExtension?: ISavedObjectsEncryptionExtension; - private readonly _securityExtension?: ISavedObjectsSecurityExtension; - private readonly _spacesExtension?: ISavedObjectsSpacesExtension; private _serializer: SavedObjectsSerializer; private _logger: Logger; - private commonHelper: CommonHelper; - private encryptionHelper: EncryptionHelper; - private validationHelper: ValidationHelper; - private preflightCheckHelper: PreflightCheckHelper; - private serializerHelper: SerializerHelper; private apiExecutionContext: ApiExecutionContext; private readonly extensions: SavedObjectsExtensions; @@ -209,41 +197,39 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { this._serializer = serializer; this._logger = logger; this.extensions = extensions; - this._encryptionExtension = extensions.encryptionExtension; - this._securityExtension = extensions.securityExtension; - this._spacesExtension = extensions.spacesExtension; - this.commonHelper = new CommonHelper({ + + const commonHelper = new CommonHelper({ spaceExtension: extensions?.spacesExtension, defaultIndex: index, kibanaVersion: migrator.kibanaVersion, registry: typeRegistry, }); - this.encryptionHelper = new EncryptionHelper({ + const encryptionHelper = new EncryptionHelper({ encryptionExtension: extensions?.encryptionExtension, securityExtension: extensions?.securityExtension, }); - this.validationHelper = new ValidationHelper({ + const validationHelper = new ValidationHelper({ registry: typeRegistry, logger, kibanaVersion: migrator.kibanaVersion, }); - this.preflightCheckHelper = new PreflightCheckHelper({ - getIndexForType: this.commonHelper.getIndexForType.bind(this.commonHelper), + const preflightCheckHelper = new PreflightCheckHelper({ + getIndexForType: commonHelper.getIndexForType.bind(commonHelper), createPointInTimeFinder: this.createPointInTimeFinder.bind(this), serializer, registry: typeRegistry, client: this.client, }); - this.serializerHelper = new SerializerHelper({ + const serializerHelper = new SerializerHelper({ registry: typeRegistry, serializer, }); this.helpers = { - common: this.commonHelper, - preflight: this.preflightCheckHelper, - validation: this.validationHelper, - encryption: this.encryptionHelper, - serializer: this.serializerHelper, + common: commonHelper, + preflight: preflightCheckHelper, + validation: validationHelper, + encryption: encryptionHelper, + serializer: serializerHelper, }; this.apiExecutionContext = { client: this.client, @@ -471,9 +457,9 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { allowedTypes: this._allowedTypes, client: this.client, serializer: this._serializer, - getIndexForType: this.getIndexForType.bind(this), + getIndexForType: this.helpers.common.getIndexForType.bind(this.helpers.common), createPointInTimeFinder: this.createPointInTimeFinder.bind(this), - securityExtension: this._securityExtension, + securityExtension: this.extensions.securityExtension, objects, options: { ...options, namespace }, }); @@ -496,8 +482,8 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { client: this.client, serializer: this._serializer, logger: this._logger, - getIndexForType: this.getIndexForType.bind(this), - securityExtension: this._securityExtension, + getIndexForType: this.helpers.common.getIndexForType.bind(this.helpers.common), + securityExtension: this.extensions.securityExtension, objects, spacesToAdd, spacesToRemove, @@ -559,18 +545,6 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { ); } - async incrementCounterInternal( - type: string, - id: string, - counterFields: Array, - options: SavedObjectsIncrementCounterOptions = {} - ) { - return incrementCounterInternal( - { type, id, counterFields, options }, - this.apiExecutionContext - ); - } - /** * {@inheritDoc ISavedObjectsRepository.openPointInTimeForType} */ @@ -598,8 +572,8 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { internalOptions: SavedObjectsFindInternalOptions = {} ): Promise { const { disableExtensions } = internalOptions; - if (!disableExtensions && this._securityExtension) { - this._securityExtension.auditClosePointInTime(); + if (!disableExtensions && this.extensions.securityExtension) { + this.extensions.securityExtension.auditClosePointInTime(); } return await this.client.closePointInTime({ @@ -627,10 +601,6 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { * {@inheritDoc ISavedObjectsRepository.getCurrentNamespace} */ getCurrentNamespace(namespace?: string) { - return this.commonHelper.getCurrentNamespace(namespace); - } - - private getIndexForType(type: string) { - return this.commonHelper.getIndexForType(type); + return this.helpers.common.getCurrentNamespace(namespace); } } diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/find_shared_origin_objects.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/find_shared_origin_objects.test.ts similarity index 97% rename from packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/find_shared_origin_objects.test.ts rename to packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/find_shared_origin_objects.test.ts index 44c81bc6eb49..da9bc1f74d48 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/find_shared_origin_objects.test.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/find_shared_origin_objects.test.ts @@ -7,8 +7,8 @@ */ import { DeeplyMockedKeys } from '@kbn/utility-types-jest'; -import { CreatePointInTimeFinderFn, PointInTimeFinder } from './point_in_time_finder'; -import { savedObjectsPointInTimeFinderMock } from '../mocks/point_in_time_finder.mock'; +import { CreatePointInTimeFinderFn, PointInTimeFinder } from '../point_in_time_finder'; +import { savedObjectsPointInTimeFinderMock } from '../../mocks/point_in_time_finder.mock'; import { findSharedOriginObjects } from './find_shared_origin_objects'; import { SavedObjectsPointInTimeFinderClient } from '@kbn/core-saved-objects-api-server'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/find_shared_origin_objects.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/find_shared_origin_objects.ts similarity index 100% rename from packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/find_shared_origin_objects.ts rename to packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/find_shared_origin_objects.ts diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/index.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/index.ts index b8ac751aafe0..723615cc230a 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/index.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/index.ts @@ -8,3 +8,4 @@ export { getSavedObjectNamespaces } from './namespaces'; export { isFoundGetResponse, type GetResponseFound } from './es_responses'; +export { findSharedOriginObjects } from './find_shared_origin_objects'; From ccfe1c91d3caa27ade3d6c2caaa9a8de0adeb02a Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 9 May 2023 16:23:40 +0200 Subject: [PATCH 21/34] fix import path --- .../src/lib/utils/find_shared_origin_objects.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/find_shared_origin_objects.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/find_shared_origin_objects.ts index a489e4afa91c..06f3cc27a66b 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/find_shared_origin_objects.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/find_shared_origin_objects.ts @@ -9,7 +9,7 @@ import * as esKuery from '@kbn/es-query'; import { ALL_NAMESPACES_STRING } from '@kbn/core-saved-objects-utils-server'; import { getObjectKey } from '@kbn/core-saved-objects-base-server-internal'; -import type { CreatePointInTimeFinderFn } from './point_in_time_finder'; +import type { CreatePointInTimeFinderFn } from '../point_in_time_finder'; interface ObjectOrigin { /** The object's type. */ From 65b4bfac0679e8a6c727d73f38e0e5c7fb62c269 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 9 May 2023 16:38:07 +0200 Subject: [PATCH 22/34] fix import path --- .../server/saved_objects/saved_objects_security_extension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security/server/saved_objects/saved_objects_security_extension.ts b/x-pack/plugins/security/server/saved_objects/saved_objects_security_extension.ts index 0bcdb817940c..fe62b40285ca 100644 --- a/x-pack/plugins/security/server/saved_objects/saved_objects_security_extension.ts +++ b/x-pack/plugins/security/server/saved_objects/saved_objects_security_extension.ts @@ -11,7 +11,7 @@ import type { SavedObjectsResolveResponse, } from '@kbn/core-saved-objects-api-server'; import type { SavedObjectsClient } from '@kbn/core-saved-objects-api-server-internal'; -import { isBulkResolveError } from '@kbn/core-saved-objects-api-server-internal/src/lib/internal_bulk_resolve'; +import { isBulkResolveError } from '@kbn/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve'; import { LEGACY_URL_ALIAS_TYPE } from '@kbn/core-saved-objects-base-server-internal'; import type { LegacyUrlAliasTarget } from '@kbn/core-saved-objects-common'; import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; From b6007d8d65b8bda06bfa9ca85201eb75bb9204c4 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 9 May 2023 17:41:05 +0200 Subject: [PATCH 23/34] fix binding --- .../src/lib/apis/resolve.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/resolve.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/resolve.ts index de4c4d4d0e45..884afc573262 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/resolve.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/resolve.ts @@ -40,7 +40,7 @@ export const performResolve = async ( allowedTypes, client, serializer, - getIndexForType: commonHelper.getIndexForType.bind(this), + getIndexForType: commonHelper.getIndexForType.bind(commonHelper), incrementCounterInternal: (t, i, counterFields, opts = {}) => incrementCounterInternal( { type: t, id: i, counterFields, options: opts }, From ba8563a51cb5762d962db67d65b31d7540e47f36 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 9 May 2023 20:05:54 +0200 Subject: [PATCH 24/34] this was a tricky one --- .../core-saved-objects-api-server-internal/src/lib/repository.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts index f433c1292d51..7561687b0348 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts @@ -200,6 +200,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { const commonHelper = new CommonHelper({ spaceExtension: extensions?.spacesExtension, + encryptionExtension: extensions?.encryptionExtension, defaultIndex: index, kibanaVersion: migrator.kibanaVersion, registry: typeRegistry, From f6402268e2d4962b95ff377df1befa777c713ee5 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Wed, 10 May 2023 08:26:57 +0200 Subject: [PATCH 25/34] implement remaining missing methods --- .../collect_multinamespaces_references.ts | 41 +++++++++++ .../src/lib/apis/index.ts | 2 + .../src/lib/apis/types.ts | 10 +-- .../src/lib/apis/update_objects_spaces.ts | 55 +++++++++++++++ .../lib/collect_multi_namespace_references.ts | 9 +-- .../src/lib/helpers/common.ts | 6 ++ .../src/lib/helpers/preflight_check.ts | 6 +- .../src/lib/repository.ts | 70 ++++++++----------- .../src/lib/update_objects_spaces.ts | 8 +-- 9 files changed, 148 insertions(+), 59 deletions(-) create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/collect_multinamespaces_references.ts create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/update_objects_spaces.ts diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/collect_multinamespaces_references.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/collect_multinamespaces_references.ts new file mode 100644 index 000000000000..91ccb77223fa --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/collect_multinamespaces_references.ts @@ -0,0 +1,41 @@ +/* + * 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 { + SavedObjectsCollectMultiNamespaceReferencesObject, + SavedObjectsCollectMultiNamespaceReferencesOptions, + SavedObjectsCollectMultiNamespaceReferencesResponse, +} from '@kbn/core-saved-objects-api-server'; +import { ApiExecutionContext } from './types'; +import { collectMultiNamespaceReferences } from '../collect_multi_namespace_references'; + +export interface PerformCreateParams { + objects: SavedObjectsCollectMultiNamespaceReferencesObject[]; + options: SavedObjectsCollectMultiNamespaceReferencesOptions; +} + +export const performCollectMultiNamespaceReferences = async ( + { objects, options }: PerformCreateParams, + { registry, helpers, allowedTypes, client, serializer, extensions = {} }: ApiExecutionContext +): Promise => { + const { common: commonHelper } = helpers; + const { securityExtension } = extensions; + + const namespace = commonHelper.getCurrentNamespace(options.namespace); + return collectMultiNamespaceReferences({ + registry, + allowedTypes, + client, + serializer, + getIndexForType: commonHelper.getIndexForType.bind(commonHelper), + createPointInTimeFinder: commonHelper.createPointInTimeFinder.bind(commonHelper), + securityExtension, + objects, + options: { ...options, namespace }, + }); +}; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts index fe490d80ff4d..3271f1ab25f2 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/index.ts @@ -23,3 +23,5 @@ export { performOpenPointInTime } from './open_point_in_time'; export { performIncrementCounter } from './increment_counter'; export { performBulkResolve } from './bulk_resolve'; export { performResolve } from './resolve'; +export { performUpdateObjectsSpaces } from './update_objects_spaces'; +export { performCollectMultiNamespaceReferences } from './collect_multinamespaces_references'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/types.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/types.ts index da3e19bdf56a..7d90d72cb549 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/types.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/types.ts @@ -7,12 +7,12 @@ */ import type { Logger } from '@kbn/logging'; -import { - type ISavedObjectTypeRegistry, - type SavedObjectsExtensions, - type ISavedObjectsSerializer, +import type { + ISavedObjectTypeRegistry, + SavedObjectsExtensions, + ISavedObjectsSerializer, } from '@kbn/core-saved-objects-server'; -import { IKibanaMigrator, IndexMapping } from '@kbn/core-saved-objects-base-server-internal'; +import type { IKibanaMigrator, IndexMapping } from '@kbn/core-saved-objects-base-server-internal'; import type { RepositoryHelpers } from '../helpers'; import type { RepositoryEsClient } from '../repository_es_client'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/update_objects_spaces.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/update_objects_spaces.ts new file mode 100644 index 000000000000..96e18d360a46 --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/update_objects_spaces.ts @@ -0,0 +1,55 @@ +/* + * 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 { + SavedObjectsUpdateObjectsSpacesObject, + SavedObjectsUpdateObjectsSpacesOptions, + SavedObjectsUpdateObjectsSpacesResponse, +} from '@kbn/core-saved-objects-api-server'; +import { ApiExecutionContext } from './types'; +import { updateObjectsSpaces } from '../update_objects_spaces'; + +export interface PerformCreateParams { + objects: SavedObjectsUpdateObjectsSpacesObject[]; + spacesToAdd: string[]; + spacesToRemove: string[]; + options: SavedObjectsUpdateObjectsSpacesOptions; +} + +export const performUpdateObjectsSpaces = async ( + { objects, spacesToAdd, spacesToRemove, options }: PerformCreateParams, + { + registry, + helpers, + allowedTypes, + client, + serializer, + logger, + mappings, + extensions = {}, + }: ApiExecutionContext +): Promise => { + const { common: commonHelper } = helpers; + const { securityExtension } = extensions; + + const namespace = commonHelper.getCurrentNamespace(options.namespace); + return updateObjectsSpaces({ + mappings, + registry, + allowedTypes, + client, + serializer, + logger, + getIndexForType: commonHelper.getIndexForType.bind(commonHelper), + securityExtension, + objects, + spacesToAdd, + spacesToRemove, + options: { ...options, namespace }, + }); +}; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.ts index 8e629fbdb7e5..c8e7423e03f9 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.ts @@ -17,14 +17,11 @@ import { type ISavedObjectsSecurityExtension, type ISavedObjectTypeRegistry, type SavedObject, + type ISavedObjectsSerializer, SavedObjectsErrorHelpers, } from '@kbn/core-saved-objects-server'; import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server'; -import { - type SavedObjectsSerializer, - getObjectKey, - parseObjectKey, -} from '@kbn/core-saved-objects-base-server-internal'; +import { getObjectKey, parseObjectKey } from '@kbn/core-saved-objects-base-server-internal'; import { findLegacyUrlAliases } from './legacy_url_aliases'; import { getRootFields } from './included_fields'; import { getSavedObjectFromSource, rawDocExistsInNamespace } from './internal_utils'; @@ -55,7 +52,7 @@ export interface CollectMultiNamespaceReferencesParams { registry: ISavedObjectTypeRegistry; allowedTypes: string[]; client: RepositoryEsClient; - serializer: SavedObjectsSerializer; + serializer: ISavedObjectsSerializer; getIndexForType: (type: string) => string; createPointInTimeFinder: CreatePointInTimeFinderFn; securityExtension: ISavedObjectsSecurityExtension | undefined; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/common.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/common.ts index aed103ba1c51..8ee47e1b2035 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/common.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/common.ts @@ -15,6 +15,7 @@ import { getIndexForType } from '@kbn/core-saved-objects-base-server-internal'; import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server'; import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; import { normalizeNamespace } from '../internal_utils'; +import type { CreatePointInTimeFinderFn } from '../point_in_time_finder'; export class CommonHelper { private registry: ISavedObjectTypeRegistry; @@ -23,8 +24,11 @@ export class CommonHelper { private defaultIndex: string; private kibanaVersion: string; + public readonly createPointInTimeFinder: CreatePointInTimeFinderFn; + constructor({ registry, + createPointInTimeFinder, spaceExtension, encryptionExtension, kibanaVersion, @@ -33,6 +37,7 @@ export class CommonHelper { registry: ISavedObjectTypeRegistry; spaceExtension?: ISavedObjectsSpacesExtension; encryptionExtension?: ISavedObjectsEncryptionExtension; + createPointInTimeFinder: CreatePointInTimeFinderFn; defaultIndex: string; kibanaVersion: string; }) { @@ -41,6 +46,7 @@ export class CommonHelper { this.encryptionExtension = encryptionExtension; this.kibanaVersion = kibanaVersion; this.defaultIndex = defaultIndex; + this.createPointInTimeFinder = createPointInTimeFinder; } /** diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/preflight_check.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/preflight_check.ts index 42f7c6daccd3..8834842ec270 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/preflight_check.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/preflight_check.ts @@ -11,7 +11,6 @@ import type { ISavedObjectTypeRegistry, ISavedObjectsSerializer, } from '@kbn/core-saved-objects-server'; -import { ISavedObjectsRepository } from '@kbn/core-saved-objects-api-server'; import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server'; import { SavedObjectsErrorHelpers, SavedObjectsRawDocSource } from '@kbn/core-saved-objects-server'; import { isRight, rawDocExistsInNamespaces } from '../internal_utils'; @@ -23,13 +22,14 @@ import { } from '../preflight_check_for_create'; import type { RepositoryEsClient } from '../repository_es_client'; import type { PreflightCheckForBulkDeleteParams } from '../repository_bulk_delete_internal_types'; +import type { CreatePointInTimeFinderFn } from '../point_in_time_finder'; export class PreflightCheckHelper { private registry: ISavedObjectTypeRegistry; private serializer: ISavedObjectsSerializer; private client: RepositoryEsClient; private getIndexForType: (type: string) => string; - private createPointInTimeFinder: ISavedObjectsRepository['createPointInTimeFinder']; + private createPointInTimeFinder: CreatePointInTimeFinderFn; constructor({ registry, @@ -42,7 +42,7 @@ export class PreflightCheckHelper { serializer: ISavedObjectsSerializer; client: RepositoryEsClient; getIndexForType: (type: string) => string; - createPointInTimeFinder: ISavedObjectsRepository['createPointInTimeFinder']; + createPointInTimeFinder: CreatePointInTimeFinderFn; }) { this.registry = registry; this.serializer = serializer; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts index 7561687b0348..33bfd2688498 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts @@ -39,6 +39,7 @@ import type { SavedObjectsResolveResponse, SavedObjectsCollectMultiNamespaceReferencesObject, SavedObjectsUpdateObjectsSpacesObject, + SavedObjectsUpdateObjectsSpacesResponse, SavedObjectsUpdateOptions, SavedObjectsOpenPointInTimeOptions, SavedObjectsClosePointInTimeOptions, @@ -51,10 +52,10 @@ import type { SavedObjectsFindInternalOptions, ISavedObjectsRepository, } from '@kbn/core-saved-objects-api-server'; -import { - type ISavedObjectTypeRegistry, - type SavedObjectsExtensions, - type SavedObject, +import type { + ISavedObjectTypeRegistry, + SavedObjectsExtensions, + SavedObject, } from '@kbn/core-saved-objects-server'; import { SavedObjectsSerializer, @@ -63,8 +64,6 @@ import { } from '@kbn/core-saved-objects-base-server-internal'; import { PointInTimeFinder } from './point_in_time_finder'; import { createRepositoryEsClient, type RepositoryEsClient } from './repository_es_client'; -import { collectMultiNamespaceReferences } from './collect_multi_namespace_references'; -import { updateObjectsSpaces } from './update_objects_spaces'; import { RepositoryHelpers, CommonHelper, @@ -91,6 +90,8 @@ import { performIncrementCounter, performBulkResolve, performResolve, + performUpdateObjectsSpaces, + performCollectMultiNamespaceReferences, } from './apis'; export interface SavedObjectsRepositoryOptions { @@ -145,11 +146,6 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { /** The injectedConstructor is only used for unit testing */ injectedConstructor: any = SavedObjectsRepository ): ISavedObjectsRepository { - const mappings = migrator.getActiveMappings(); - const allTypes = typeRegistry.getAllTypes().map((t) => t.name); - const serializer = new SavedObjectsSerializer(typeRegistry); - const visibleTypes = allTypes.filter((type) => !typeRegistry.isHidden(type)); - const missingTypeMappings = includedHiddenTypes.filter((type) => !allTypes.includes(type)); if (missingTypeMappings.length > 0) { throw new Error( @@ -157,6 +153,10 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { ); } + const mappings = migrator.getActiveMappings(); + const allTypes = typeRegistry.getAllTypes().map((t) => t.name); + const serializer = new SavedObjectsSerializer(typeRegistry); + const visibleTypes = allTypes.filter((type) => !typeRegistry.isHidden(type)); const allowedTypes = [...new Set(visibleTypes.concat(includedHiddenTypes))]; return new injectedConstructor({ @@ -201,6 +201,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { const commonHelper = new CommonHelper({ spaceExtension: extensions?.spacesExtension, encryptionExtension: extensions?.encryptionExtension, + createPointInTimeFinder: this.createPointInTimeFinder.bind(this), defaultIndex: index, kibanaVersion: migrator.kibanaVersion, registry: typeRegistry, @@ -216,7 +217,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { }); const preflightCheckHelper = new PreflightCheckHelper({ getIndexForType: commonHelper.getIndexForType.bind(commonHelper), - createPointInTimeFinder: this.createPointInTimeFinder.bind(this), + createPointInTimeFinder: commonHelper.createPointInTimeFinder.bind(commonHelper), serializer, registry: typeRegistry, client: this.client, @@ -452,18 +453,13 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { objects: SavedObjectsCollectMultiNamespaceReferencesObject[], options: SavedObjectsCollectMultiNamespaceReferencesOptions = {} ) { - const namespace = this.getCurrentNamespace(options.namespace); - return collectMultiNamespaceReferences({ - registry: this._registry, - allowedTypes: this._allowedTypes, - client: this.client, - serializer: this._serializer, - getIndexForType: this.helpers.common.getIndexForType.bind(this.helpers.common), - createPointInTimeFinder: this.createPointInTimeFinder.bind(this), - securityExtension: this.extensions.securityExtension, - objects, - options: { ...options, namespace }, - }); + return await performCollectMultiNamespaceReferences( + { + objects, + options, + }, + this.apiExecutionContext + ); } /** @@ -474,22 +470,16 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { spacesToAdd: string[], spacesToRemove: string[], options: SavedObjectsUpdateObjectsSpacesOptions = {} - ) { - const namespace = this.getCurrentNamespace(options.namespace); - return updateObjectsSpaces({ - mappings: this._mappings, - registry: this._registry, - allowedTypes: this._allowedTypes, - client: this.client, - serializer: this._serializer, - logger: this._logger, - getIndexForType: this.helpers.common.getIndexForType.bind(this.helpers.common), - securityExtension: this.extensions.securityExtension, - objects, - spacesToAdd, - spacesToRemove, - options: { ...options, namespace }, - }); + ): Promise { + return await performUpdateObjectsSpaces( + { + objects, + spacesToAdd, + spacesToRemove, + options, + }, + this.apiExecutionContext + ); } /** diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/update_objects_spaces.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/update_objects_spaces.ts index f4d6139908ae..0e24f55bca3f 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/update_objects_spaces.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/update_objects_spaces.ts @@ -23,14 +23,12 @@ import type { AuthorizeObjectWithExistingSpaces, ISavedObjectsSecurityExtension, ISavedObjectTypeRegistry, + ISavedObjectsSerializer, SavedObjectsRawDocSource, } from '@kbn/core-saved-objects-server'; import { ALL_NAMESPACES_STRING } from '@kbn/core-saved-objects-utils-server'; import { SavedObjectsErrorHelpers, type DecoratedError } from '@kbn/core-saved-objects-server'; -import type { - IndexMapping, - SavedObjectsSerializer, -} from '@kbn/core-saved-objects-base-server-internal'; +import type { IndexMapping } from '@kbn/core-saved-objects-base-server-internal'; import { getBulkOperationError, getExpectedVersionProperties, @@ -54,7 +52,7 @@ export interface UpdateObjectsSpacesParams { registry: ISavedObjectTypeRegistry; allowedTypes: string[]; client: RepositoryEsClient; - serializer: SavedObjectsSerializer; + serializer: ISavedObjectsSerializer; logger: Logger; getIndexForType: (type: string) => string; securityExtension: ISavedObjectsSecurityExtension | undefined; From 85a9c00d74c5ed616475bdd6c2c7a060c7f5d1f9 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Wed, 10 May 2023 08:33:01 +0200 Subject: [PATCH 26/34] small repository cleanup --- .../src/lib/repository.ts | 52 ++++++++++--------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts index 33bfd2688498..f87f8990b291 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts @@ -94,6 +94,10 @@ import { performCollectMultiNamespaceReferences, } from './apis'; +/** + * Constructor options for {@link SavedObjectsRepository} + * @internal + */ export interface SavedObjectsRepositoryOptions { index: string; mappings: IndexMapping; @@ -107,31 +111,29 @@ export interface SavedObjectsRepositoryOptions { } /** - * Saved Objects Respositiry - the client entry point for saved object manipulation. + * Saved Objects Repository - the client entry point for all saved object manipulation. * * The SOR calls the Elasticsearch client and leverages extension implementations to * support spaces, security, and encryption features. * - * @public + * @internal */ export class SavedObjectsRepository implements ISavedObjectsRepository { - private _migrator: IKibanaMigrator; - private _mappings: IndexMapping; - private _registry: ISavedObjectTypeRegistry; - private _allowedTypes: string[]; + private readonly migrator: IKibanaMigrator; + private readonly mappings: IndexMapping; + private readonly registry: ISavedObjectTypeRegistry; + private readonly allowedTypes: string[]; private readonly client: RepositoryEsClient; - private _serializer: SavedObjectsSerializer; - private _logger: Logger; - - private apiExecutionContext: ApiExecutionContext; + private readonly serializer: SavedObjectsSerializer; + private readonly logger: Logger; + private readonly apiExecutionContext: ApiExecutionContext; private readonly extensions: SavedObjectsExtensions; private readonly helpers: RepositoryHelpers; /** * A factory function for creating SavedObjectRepository instances. * - * @internalRemarks - * Tests are located in ./repository_create_repository.test.ts + * @internalRemarks Tests are located in ./repository_create_repository.test.ts * * @internal */ @@ -189,13 +191,13 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { throw new Error('Empty or missing types for saved object repository!'); } - this._migrator = migrator; - this._mappings = mappings; - this._registry = typeRegistry; + this.migrator = migrator; + this.mappings = mappings; + this.registry = typeRegistry; this.client = createRepositoryEsClient(client); - this._allowedTypes = allowedTypes; - this._serializer = serializer; - this._logger = logger; + this.allowedTypes = allowedTypes; + this.serializer = serializer; + this.logger = logger; this.extensions = extensions; const commonHelper = new CommonHelper({ @@ -237,12 +239,12 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { client: this.client, extensions: this.extensions, helpers: this.helpers, - allowedTypes: this._allowedTypes, - registry: this._registry, - serializer: this._serializer, - migrator: this._migrator, - mappings: this._mappings, - logger: this._logger, + allowedTypes: this.allowedTypes, + registry: this.registry, + serializer: this.serializer, + migrator: this.migrator, + mappings: this.mappings, + logger: this.logger, }; } @@ -581,7 +583,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { internalOptions?: SavedObjectsFindInternalOptions ): ISavedObjectsPointInTimeFinder { return new PointInTimeFinder(findOptions, { - logger: this._logger, + logger: this.logger, client: this, ...dependencies, internalOptions, From c83830d3d9f361a5a23cca6c59711b17ec1454ac Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Wed, 10 May 2023 09:08:33 +0200 Subject: [PATCH 27/34] move ALL the things again --- .../src/lib/apis/bulk_create.ts | 6 +-- .../src/lib/apis/bulk_delete.ts | 2 +- .../src/lib/apis/bulk_get.ts | 2 +- .../src/lib/apis/bulk_resolve.ts | 2 +- .../src/lib/apis/bulk_update.ts | 4 +- .../src/lib/apis/check_conflicts.ts | 2 +- .../collect_multinamespaces_references.ts | 2 +- .../src/lib/apis/create.ts | 5 +-- .../src/lib/apis/delete.ts | 6 +-- .../src/lib/apis/get.ts | 3 +- .../src/lib/{ => apis}/helpers/common.ts | 4 +- .../src/lib/{ => apis}/helpers/encryption.ts | 0 .../src/lib/{ => apis}/helpers/index.ts | 0 .../lib/{ => apis}/helpers/preflight_check.ts | 18 ++++---- .../src/lib/{ => apis}/helpers/serializer.ts | 0 .../src/lib/{ => apis}/helpers/validation.ts | 5 ++- ...ct_multi_namespace_references.test.mock.ts | 14 +++---- ...collect_multi_namespace_references.test.ts | 6 +-- .../collect_multi_namespace_references.ts | 11 +++-- .../internals/increment_counter_internal.ts | 2 +- .../internal_bulk_resolve.test.mock.ts | 6 +-- .../internals/internal_bulk_resolve.test.ts | 2 +- .../apis/internals/internal_bulk_resolve.ts | 2 +- .../preflight_check_for_create.test.mock.ts | 10 ++--- .../preflight_check_for_create.test.ts | 2 +- .../internals}/preflight_check_for_create.ts | 9 ++-- .../update_objects_spaces.test.mock.ts | 10 ++--- .../internals}/update_objects_spaces.test.ts | 4 +- .../internals}/update_objects_spaces.ts | 12 +++--- .../src/lib/apis/types.ts | 2 +- .../src/lib/apis/update.ts | 4 +- .../src/lib/apis/update_objects_spaces.ts | 2 +- .../src/lib/apis/utils/either.ts | 42 +++++++++++++++++++ .../src/lib/{ => apis}/utils/es_responses.ts | 0 .../utils/find_shared_origin_objects.test.ts | 4 +- .../utils/find_shared_origin_objects.ts | 2 +- .../src/lib/{ => apis}/utils/index.ts | 14 +++++++ .../{ => apis/utils}/internal_utils.test.ts | 0 .../lib/{ => apis/utils}/internal_utils.ts | 37 +--------------- .../src/lib/{ => apis}/utils/namespaces.ts | 0 .../src/lib/repository.test.mock.ts | 18 ++++---- .../src/lib/repository.ts | 2 +- .../repository_bulk_delete_internal_types.ts | 2 +- 43 files changed, 153 insertions(+), 127 deletions(-) rename packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/{ => apis}/helpers/common.ts (96%) rename packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/{ => apis}/helpers/encryption.ts (100%) rename packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/{ => apis}/helpers/index.ts (100%) rename packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/{ => apis}/helpers/preflight_check.ts (93%) rename packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/{ => apis}/helpers/serializer.ts (100%) rename packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/{ => apis}/helpers/validation.ts (97%) rename packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/{ => apis/internals}/collect_multi_namespace_references.test.mock.ts (68%) rename packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/{ => apis/internals}/collect_multi_namespace_references.test.ts (99%) rename packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/{ => apis/internals}/collect_multi_namespace_references.ts (95%) rename packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/{ => apis/internals}/preflight_check_for_create.test.mock.ts (72%) rename packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/{ => apis/internals}/preflight_check_for_create.test.ts (99%) rename packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/{ => apis/internals}/preflight_check_for_create.ts (97%) rename packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/{ => apis/internals}/update_objects_spaces.test.mock.ts (79%) rename packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/{ => apis/internals}/update_objects_spaces.test.ts (99%) rename packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/{ => apis/internals}/update_objects_spaces.ts (97%) create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/either.ts rename packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/{ => apis}/utils/es_responses.ts (100%) rename packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/{ => apis}/utils/find_shared_origin_objects.test.ts (98%) rename packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/{ => apis}/utils/find_shared_origin_objects.ts (98%) rename packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/{ => apis}/utils/index.ts (59%) rename packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/{ => apis/utils}/internal_utils.test.ts (100%) rename packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/{ => apis/utils}/internal_utils.ts (90%) rename packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/{ => apis}/utils/namespaces.ts (100%) diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_create.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_create.ts index fbef1c940735..219e6071ce33 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_create.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_create.ts @@ -32,9 +32,9 @@ import { normalizeNamespace, setManaged, errorContent, -} from '../internal_utils'; -import { getSavedObjectNamespaces } from '../utils'; -import { PreflightCheckForCreateObject } from '../preflight_check_for_create'; +} from './utils'; +import { getSavedObjectNamespaces } from './utils'; +import { PreflightCheckForCreateObject } from './internals/preflight_check_for_create'; import { ApiExecutionContext } from './types'; export interface PerformBulkCreateParams { diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_delete.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_delete.ts index c3680fff6370..db978199bbe7 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_delete.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_delete.ts @@ -28,7 +28,7 @@ import { isMgetDoc, rawDocExistsInNamespace, isRight, -} from '../internal_utils'; +} from './utils'; import type { ApiExecutionContext } from './types'; import { deleteLegacyUrlAliases } from '../legacy_url_aliases'; import { diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_get.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_get.ts index d4df9e7e08dd..c10e81d0e3c9 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_get.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_get.ts @@ -28,7 +28,7 @@ import { isLeft, isRight, rawDocExistsInNamespaces, -} from '../internal_utils'; +} from './utils'; import { ApiExecutionContext } from './types'; import { includedFields } from '../included_fields'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_resolve.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_resolve.ts index 07b5f25af947..74ab7026eb17 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_resolve.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_resolve.ts @@ -13,7 +13,7 @@ import { SavedObjectsResolveOptions, SavedObjectsResolveResponse, } from '@kbn/core-saved-objects-api-server'; -import { errorContent } from '../internal_utils'; +import { errorContent } from './utils'; import { ApiExecutionContext } from './types'; import { internalBulkResolve, isBulkResolveError } from './internals/internal_bulk_resolve'; import { incrementCounterInternal } from './internals/increment_counter_internal'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_update.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_update.ts index 0707f8bdf3e5..5f6981152191 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_update.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_update.ts @@ -24,7 +24,7 @@ import { } from '@kbn/core-saved-objects-api-server'; import { DEFAULT_REFRESH_SETTING } from '../constants'; import { - Either, + type Either, errorContent, getBulkOperationError, getCurrentTime, @@ -33,7 +33,7 @@ import { isMgetDoc, isRight, rawDocExistsInNamespace, -} from '../internal_utils'; +} from './utils'; import { ApiExecutionContext } from './types'; export interface PerformUpdateParams { diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/check_conflicts.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/check_conflicts.ts index 0ec44db1d174..a90aada8dd4c 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/check_conflicts.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/check_conflicts.ts @@ -25,7 +25,7 @@ import { isRight, isMgetDoc, rawDocExistsInNamespace, -} from '../internal_utils'; +} from './utils'; import { ApiExecutionContext } from './types'; export interface PerformCheckConflictsParams { diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/collect_multinamespaces_references.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/collect_multinamespaces_references.ts index 91ccb77223fa..a107e74dcb53 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/collect_multinamespaces_references.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/collect_multinamespaces_references.ts @@ -12,7 +12,7 @@ import { SavedObjectsCollectMultiNamespaceReferencesResponse, } from '@kbn/core-saved-objects-api-server'; import { ApiExecutionContext } from './types'; -import { collectMultiNamespaceReferences } from '../collect_multi_namespace_references'; +import { collectMultiNamespaceReferences } from './internals/collect_multi_namespace_references'; export interface PerformCreateParams { objects: SavedObjectsCollectMultiNamespaceReferencesObject[]; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/create.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/create.ts index 1bf86bda3708..b8b9b8a0d2db 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/create.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/create.ts @@ -16,9 +16,8 @@ import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server'; import { decodeRequestVersion } from '@kbn/core-saved-objects-base-server-internal'; import { SavedObjectsCreateOptions } from '@kbn/core-saved-objects-api-server'; import { DEFAULT_REFRESH_SETTING } from '../constants'; -import { getCurrentTime, normalizeNamespace, setManaged } from '../internal_utils'; -import type { PreflightCheckForCreateResult } from '../preflight_check_for_create'; -import { getSavedObjectNamespaces } from '../utils'; +import type { PreflightCheckForCreateResult } from './internals/preflight_check_for_create'; +import { getSavedObjectNamespaces, getCurrentTime, normalizeNamespace, setManaged } from './utils'; import { ApiExecutionContext } from './types'; export interface PerformCreateParams { diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/delete.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/delete.ts index 4c9b4665faf1..f687091ca8ab 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/delete.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/delete.ts @@ -11,10 +11,10 @@ import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; import { ALL_NAMESPACES_STRING } from '@kbn/core-saved-objects-utils-server'; import { SavedObjectsDeleteOptions } from '@kbn/core-saved-objects-api-server'; import { DEFAULT_REFRESH_SETTING } from '../constants'; -import { getExpectedVersionProperties } from '../internal_utils'; -import { PreflightCheckNamespacesResult } from '../helpers'; -import type { ApiExecutionContext } from './types'; import { deleteLegacyUrlAliases } from '../legacy_url_aliases'; +import { getExpectedVersionProperties } from './utils'; +import { PreflightCheckNamespacesResult } from './helpers'; +import type { ApiExecutionContext } from './types'; export interface PerformDeleteParams { type: string; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/get.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/get.ts index c18916cf5c65..d2fbe46fc6c8 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/get.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/get.ts @@ -13,8 +13,7 @@ import { SavedObjectsRawDocSource, } from '@kbn/core-saved-objects-server'; import { SavedObjectsGetOptions } from '@kbn/core-saved-objects-api-server'; -import { getSavedObjectFromSource, rawDocExistsInNamespace } from '../internal_utils'; -import { isFoundGetResponse } from '../utils'; +import { isFoundGetResponse, getSavedObjectFromSource, rawDocExistsInNamespace } from './utils'; import { ApiExecutionContext } from './types'; export interface PerformGetParams { diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/common.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/common.ts similarity index 96% rename from packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/common.ts rename to packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/common.ts index 8ee47e1b2035..ce47fa16e7eb 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/common.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/common.ts @@ -14,8 +14,8 @@ import type { import { getIndexForType } from '@kbn/core-saved-objects-base-server-internal'; import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server'; import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; -import { normalizeNamespace } from '../internal_utils'; -import type { CreatePointInTimeFinderFn } from '../point_in_time_finder'; +import { normalizeNamespace } from '../utils'; +import type { CreatePointInTimeFinderFn } from '../../point_in_time_finder'; export class CommonHelper { private registry: ISavedObjectTypeRegistry; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/encryption.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/encryption.ts similarity index 100% rename from packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/encryption.ts rename to packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/encryption.ts diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/index.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/index.ts similarity index 100% rename from packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/index.ts rename to packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/index.ts diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/preflight_check.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/preflight_check.ts similarity index 93% rename from packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/preflight_check.ts rename to packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/preflight_check.ts index 8834842ec270..2853e84729f5 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/preflight_check.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/preflight_check.ts @@ -13,16 +13,20 @@ import type { } from '@kbn/core-saved-objects-server'; import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server'; import { SavedObjectsErrorHelpers, SavedObjectsRawDocSource } from '@kbn/core-saved-objects-server'; -import { isRight, rawDocExistsInNamespaces } from '../internal_utils'; -import { getSavedObjectNamespaces } from '../utils/namespaces'; -import { GetResponseFound, isFoundGetResponse } from '../utils/es_responses'; +import type { RepositoryEsClient } from '../../repository_es_client'; +import type { PreflightCheckForBulkDeleteParams } from '../../repository_bulk_delete_internal_types'; +import type { CreatePointInTimeFinderFn } from '../../point_in_time_finder'; +import { + getSavedObjectNamespaces, + isRight, + rawDocExistsInNamespaces, + isFoundGetResponse, + type GetResponseFound, +} from '../utils'; import { preflightCheckForCreate, PreflightCheckForCreateObject, -} from '../preflight_check_for_create'; -import type { RepositoryEsClient } from '../repository_es_client'; -import type { PreflightCheckForBulkDeleteParams } from '../repository_bulk_delete_internal_types'; -import type { CreatePointInTimeFinderFn } from '../point_in_time_finder'; +} from '../internals/preflight_check_for_create'; export class PreflightCheckHelper { private registry: ISavedObjectTypeRegistry; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/serializer.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/serializer.ts similarity index 100% rename from packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/serializer.ts rename to packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/serializer.ts diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/validation.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/validation.ts similarity index 97% rename from packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/validation.ts rename to packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/validation.ts index 6ffb80e71a9c..3c63c59cb05e 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/helpers/validation.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/validation.ts @@ -9,7 +9,10 @@ import type { Logger } from '@kbn/logging'; import type { ISavedObjectTypeRegistry } from '@kbn/core-saved-objects-server'; import { SavedObjectsTypeValidator } from '@kbn/core-saved-objects-base-server-internal'; -import { SavedObjectSanitizedDoc, SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; +import { + SavedObjectsErrorHelpers, + type SavedObjectSanitizedDoc, +} from '@kbn/core-saved-objects-server'; import { ALL_NAMESPACES_STRING } from '@kbn/core-saved-objects-utils-server'; export class ValidationHelper { diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.test.mock.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/collect_multi_namespace_references.test.mock.ts similarity index 68% rename from packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.test.mock.ts rename to packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/collect_multi_namespace_references.test.mock.ts index fb28f2c6687e..4debe9b5b399 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.test.mock.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/collect_multi_namespace_references.test.mock.ts @@ -6,15 +6,15 @@ * Side Public License, v 1. */ -import type { findLegacyUrlAliases } from './legacy_url_aliases'; -import type { findSharedOriginObjects } from './utils'; -import type * as InternalUtils from './internal_utils'; +import type { findLegacyUrlAliases } from '../../legacy_url_aliases'; +import type { findSharedOriginObjects } from '../utils/find_shared_origin_objects'; +import type * as InternalUtils from '../utils/internal_utils'; export const mockFindLegacyUrlAliases = jest.fn() as jest.MockedFunction< typeof findLegacyUrlAliases >; -jest.mock('./legacy_url_aliases', () => { +jest.mock('../../legacy_url_aliases', () => { return { findLegacyUrlAliases: mockFindLegacyUrlAliases }; }); @@ -22,7 +22,7 @@ export const mockFindSharedOriginObjects = jest.fn() as jest.MockedFunction< typeof findSharedOriginObjects >; -jest.mock('./utils/find_shared_origin_objects', () => { +jest.mock('../utils/find_shared_origin_objects', () => { return { findSharedOriginObjects: mockFindSharedOriginObjects }; }); @@ -30,8 +30,8 @@ export const mockRawDocExistsInNamespace = jest.fn() as jest.MockedFunction< typeof InternalUtils['rawDocExistsInNamespace'] >; -jest.mock('./internal_utils', () => { - const actual = jest.requireActual('./internal_utils'); +jest.mock('../utils/internal_utils', () => { + const actual = jest.requireActual('../utils/internal_utils'); return { ...actual, rawDocExistsInNamespace: mockRawDocExistsInNamespace, diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/collect_multi_namespace_references.test.ts similarity index 99% rename from packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.test.ts rename to packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/collect_multi_namespace_references.test.ts index b4e788dd2973..5450f0c739ce 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.test.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/collect_multi_namespace_references.test.ts @@ -24,13 +24,13 @@ import { type CollectMultiNamespaceReferencesParams, } from './collect_multi_namespace_references'; import { collectMultiNamespaceReferences } from './collect_multi_namespace_references'; -import type { CreatePointInTimeFinderFn } from './point_in_time_finder'; +import type { CreatePointInTimeFinderFn } from '../../point_in_time_finder'; import { enforceError, setupAuthorizeAndRedactMultiNamespaceReferenencesFailure, setupAuthorizeAndRedactMultiNamespaceReferenencesSuccess, -} from '../test_helpers/repository.test.common'; -import { savedObjectsExtensionsMock } from '../mocks/saved_objects_extensions.mock'; +} from '../../../test_helpers/repository.test.common'; +import { savedObjectsExtensionsMock } from '../../../mocks/saved_objects_extensions.mock'; import { type ISavedObjectsSecurityExtension, SavedObjectsErrorHelpers, diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/collect_multi_namespace_references.ts similarity index 95% rename from packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.ts rename to packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/collect_multi_namespace_references.ts index c8e7423e03f9..91767ce0079a 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/collect_multi_namespace_references.ts @@ -22,12 +22,11 @@ import { } from '@kbn/core-saved-objects-server'; import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server'; import { getObjectKey, parseObjectKey } from '@kbn/core-saved-objects-base-server-internal'; -import { findLegacyUrlAliases } from './legacy_url_aliases'; -import { getRootFields } from './included_fields'; -import { getSavedObjectFromSource, rawDocExistsInNamespace } from './internal_utils'; -import type { CreatePointInTimeFinderFn } from './point_in_time_finder'; -import type { RepositoryEsClient } from './repository_es_client'; -import { findSharedOriginObjects } from './utils'; +import { findLegacyUrlAliases } from '../../legacy_url_aliases'; +import { getRootFields } from '../../included_fields'; +import type { CreatePointInTimeFinderFn } from '../../point_in_time_finder'; +import type { RepositoryEsClient } from '../../repository_es_client'; +import { findSharedOriginObjects, getSavedObjectFromSource, rawDocExistsInNamespace } from '../utils'; /** * When we collect an object's outbound references, we will only go a maximum of this many levels deep before we throw an error. diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/increment_counter_internal.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/increment_counter_internal.ts index 0853e57100ed..6ff63bc43718 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/increment_counter_internal.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/increment_counter_internal.ts @@ -18,7 +18,7 @@ import { SavedObjectsIncrementCounterField, } from '@kbn/core-saved-objects-api-server'; import { DEFAULT_REFRESH_SETTING } from '../../constants'; -import { getCurrentTime, normalizeNamespace } from '../../internal_utils'; +import { getCurrentTime, normalizeNamespace } from '../utils'; import { ApiExecutionContext } from '../types'; export interface PerformIncrementCounterInternalParams { diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve.test.mock.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve.test.mock.ts index e30e059f5e76..51a88b595128 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve.test.mock.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve.test.mock.ts @@ -7,7 +7,7 @@ */ import type { isNotFoundFromUnsupportedServer } from '@kbn/core-elasticsearch-server-internal'; -import type * as InternalUtils from '../../internal_utils'; +import type * as InternalUtils from '../utils/internal_utils'; export const mockGetSavedObjectFromSource = jest.fn() as jest.MockedFunction< typeof InternalUtils['getSavedObjectFromSource'] @@ -16,8 +16,8 @@ export const mockRawDocExistsInNamespace = jest.fn() as jest.MockedFunction< typeof InternalUtils['rawDocExistsInNamespace'] >; -jest.mock('../../internal_utils', () => { - const actual = jest.requireActual('../../internal_utils'); +jest.mock('../utils/internal_utils', () => { + const actual = jest.requireActual('../utils/internal_utils'); return { ...actual, getSavedObjectFromSource: mockGetSavedObjectFromSource, diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve.test.ts index c8ec49ca59a8..4b619764a042 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve.test.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve.test.ts @@ -24,7 +24,7 @@ import { } from '@kbn/core-saved-objects-base-server-internal'; import { typeRegistryMock } from '@kbn/core-saved-objects-base-server-mocks'; import { internalBulkResolve, type InternalBulkResolveParams } from './internal_bulk_resolve'; -import { normalizeNamespace } from '../../internal_utils'; +import { normalizeNamespace } from '../utils'; import { type ISavedObjectsEncryptionExtension, type ISavedObjectsSecurityExtension, diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve.ts index dd58f6a4d581..7c1124f376f8 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve.ts @@ -45,7 +45,7 @@ import { type Right, isLeft, isRight, -} from '../../internal_utils'; +} from '../utils'; import type { RepositoryEsClient } from '../../repository_es_client'; const MAX_CONCURRENT_RESOLVE = 10; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/preflight_check_for_create.test.mock.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/preflight_check_for_create.test.mock.ts similarity index 72% rename from packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/preflight_check_for_create.test.mock.ts rename to packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/preflight_check_for_create.test.mock.ts index fe8076b51e5d..e48f4ed1f690 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/preflight_check_for_create.test.mock.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/preflight_check_for_create.test.mock.ts @@ -6,14 +6,14 @@ * Side Public License, v 1. */ -import type { findLegacyUrlAliases } from './legacy_url_aliases'; -import type * as InternalUtils from './internal_utils'; +import type { findLegacyUrlAliases } from '../../legacy_url_aliases'; +import type * as InternalUtils from '../utils/internal_utils'; export const mockFindLegacyUrlAliases = jest.fn() as jest.MockedFunction< typeof findLegacyUrlAliases >; -jest.mock('./legacy_url_aliases', () => { +jest.mock('../../legacy_url_aliases', () => { return { findLegacyUrlAliases: mockFindLegacyUrlAliases }; }); @@ -21,8 +21,8 @@ export const mockRawDocExistsInNamespaces = jest.fn() as jest.MockedFunction< typeof InternalUtils['rawDocExistsInNamespaces'] >; -jest.mock('./internal_utils', () => { - const actual = jest.requireActual('./internal_utils'); +jest.mock('../utils/internal_utils', () => { + const actual = jest.requireActual('../utils/internal_utils'); return { ...actual, rawDocExistsInNamespaces: mockRawDocExistsInNamespaces, diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/preflight_check_for_create.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/preflight_check_for_create.test.ts similarity index 99% rename from packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/preflight_check_for_create.test.ts rename to packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/preflight_check_for_create.test.ts index f2901e4b5318..c07134259d4b 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/preflight_check_for_create.test.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/preflight_check_for_create.test.ts @@ -20,7 +20,7 @@ import { LEGACY_URL_ALIAS_TYPE, } from '@kbn/core-saved-objects-base-server-internal'; import { typeRegistryMock } from '@kbn/core-saved-objects-base-server-mocks'; -import type { CreatePointInTimeFinderFn } from './point_in_time_finder'; +import type { CreatePointInTimeFinderFn } from '../../point_in_time_finder'; import { ALIAS_SEARCH_PER_PAGE, type PreflightCheckForCreateObject, diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/preflight_check_for_create.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/preflight_check_for_create.ts similarity index 97% rename from packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/preflight_check_for_create.ts rename to packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/preflight_check_for_create.ts index c3faa7d71a96..bc899adda83d 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/preflight_check_for_create.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/preflight_check_for_create.ts @@ -21,11 +21,10 @@ import { getObjectKey, type LegacyUrlAlias, } from '@kbn/core-saved-objects-base-server-internal'; -import { findLegacyUrlAliases } from './legacy_url_aliases'; -import { type Either, rawDocExistsInNamespaces } from './internal_utils'; -import { isLeft, isRight } from './internal_utils'; -import type { CreatePointInTimeFinderFn } from './point_in_time_finder'; -import type { RepositoryEsClient } from './repository_es_client'; +import { findLegacyUrlAliases } from '../../legacy_url_aliases'; +import type { CreatePointInTimeFinderFn } from '../../point_in_time_finder'; +import type { RepositoryEsClient } from '../../repository_es_client'; +import { isLeft, isRight, rawDocExistsInNamespaces, type Either } from '../utils'; /** * If the object will be created in this many spaces (or "*" all current and future spaces), we use find to fetch all aliases. diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/update_objects_spaces.test.mock.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/update_objects_spaces.test.mock.ts similarity index 79% rename from packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/update_objects_spaces.test.mock.ts rename to packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/update_objects_spaces.test.mock.ts index 043975d5bb52..20fa8daaac16 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/update_objects_spaces.test.mock.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/update_objects_spaces.test.mock.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import type * as InternalUtils from './internal_utils'; -import type { deleteLegacyUrlAliases } from './legacy_url_aliases'; +import type * as InternalUtils from '../utils/internal_utils'; +import type { deleteLegacyUrlAliases } from '../../legacy_url_aliases'; export const mockGetBulkOperationError = jest.fn() as jest.MockedFunction< typeof InternalUtils['getBulkOperationError'] @@ -19,8 +19,8 @@ export const mockRawDocExistsInNamespace = jest.fn() as jest.MockedFunction< typeof InternalUtils['rawDocExistsInNamespace'] >; -jest.mock('./internal_utils', () => { - const actual = jest.requireActual('./internal_utils'); +jest.mock('../utils/internal_utils', () => { + const actual = jest.requireActual('../utils/internal_utils'); return { ...actual, getBulkOperationError: mockGetBulkOperationError, @@ -32,6 +32,6 @@ jest.mock('./internal_utils', () => { export const mockDeleteLegacyUrlAliases = jest.fn() as jest.MockedFunction< typeof deleteLegacyUrlAliases >; -jest.mock('./legacy_url_aliases', () => ({ +jest.mock('../../legacy_url_aliases', () => ({ deleteLegacyUrlAliases: mockDeleteLegacyUrlAliases, })); diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/update_objects_spaces.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/update_objects_spaces.test.ts similarity index 99% rename from packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/update_objects_spaces.test.ts rename to packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/update_objects_spaces.test.ts index 7432b7ae3e6a..f96c3dfddc8d 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/update_objects_spaces.test.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/update_objects_spaces.test.ts @@ -32,8 +32,8 @@ import { setupRedactPassthrough, authMap, setupAuthorizeFunc, -} from '../test_helpers/repository.test.common'; -import { savedObjectsExtensionsMock } from '../mocks/saved_objects_extensions.mock'; +} from '../../../test_helpers/repository.test.common'; +import { savedObjectsExtensionsMock } from '../../../mocks/saved_objects_extensions.mock'; type SetupParams = Partial< Pick diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/update_objects_spaces.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/update_objects_spaces.ts similarity index 97% rename from packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/update_objects_spaces.ts rename to packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/update_objects_spaces.ts index 0e24f55bca3f..9cdc7fe1b9ef 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/update_objects_spaces.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/update_objects_spaces.ts @@ -36,11 +36,13 @@ import { type Either, isLeft, isRight, -} from './internal_utils'; -import { DEFAULT_REFRESH_SETTING } from './constants'; -import type { RepositoryEsClient } from './repository_es_client'; -import type { DeleteLegacyUrlAliasesParams } from './legacy_url_aliases'; -import { deleteLegacyUrlAliases } from './legacy_url_aliases'; +} from '../utils'; +import { DEFAULT_REFRESH_SETTING } from '../../constants'; +import type { RepositoryEsClient } from '../../repository_es_client'; +import { + deleteLegacyUrlAliases, + type DeleteLegacyUrlAliasesParams, +} from '../../legacy_url_aliases'; /** * Parameters for the updateObjectsSpaces function. diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/types.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/types.ts index 7d90d72cb549..bfa07e4bf5dc 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/types.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/types.ts @@ -13,7 +13,7 @@ import type { ISavedObjectsSerializer, } from '@kbn/core-saved-objects-server'; import type { IKibanaMigrator, IndexMapping } from '@kbn/core-saved-objects-base-server-internal'; -import type { RepositoryHelpers } from '../helpers'; +import type { RepositoryHelpers } from './helpers'; import type { RepositoryEsClient } from '../repository_es_client'; export interface ApiExecutionContext { diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/update.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/update.ts index 92f77741c86a..eceb738ac7ae 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/update.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/update.ts @@ -20,9 +20,9 @@ import { SavedObjectsUpdateResponse, } from '@kbn/core-saved-objects-api-server'; import { DEFAULT_REFRESH_SETTING, DEFAULT_RETRY_COUNT } from '../constants'; -import { getCurrentTime, getExpectedVersionProperties } from '../internal_utils'; +import { getCurrentTime, getExpectedVersionProperties } from './utils'; import { ApiExecutionContext } from './types'; -import { PreflightCheckNamespacesResult } from '../helpers'; +import { PreflightCheckNamespacesResult } from './helpers'; export interface PerformUpdateParams { type: string; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/update_objects_spaces.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/update_objects_spaces.ts index 96e18d360a46..d90d4fc9403e 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/update_objects_spaces.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/update_objects_spaces.ts @@ -12,7 +12,7 @@ import { SavedObjectsUpdateObjectsSpacesResponse, } from '@kbn/core-saved-objects-api-server'; import { ApiExecutionContext } from './types'; -import { updateObjectsSpaces } from '../update_objects_spaces'; +import { updateObjectsSpaces } from './internals/update_objects_spaces'; export interface PerformCreateParams { objects: SavedObjectsUpdateObjectsSpacesObject[]; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/either.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/either.ts new file mode 100644 index 000000000000..e573ea87ce6d --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/either.ts @@ -0,0 +1,42 @@ +/* + * 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. + */ + +/** + * Discriminated union (TypeScript approximation of an algebraic data type); this design pattern is used for internal repository operations. + * @internal + */ +export type Either = Left | Right; + +/** + * Left part of discriminated union ({@link Either}). + * @internal + */ +export interface Left { + tag: 'Left'; + value: L; +} + +/** + * Right part of discriminated union ({@link Either}). + * @internal + */ +export interface Right { + tag: 'Right'; + value: R; +} + +/** + * Type guard for left part of discriminated union ({@link Left}, {@link Either}). + * @internal + */ +export const isLeft = (either: Either): either is Left => either.tag === 'Left'; +/** + * Type guard for right part of discriminated union ({@link Right}, {@link Either}). + * @internal + */ +export const isRight = (either: Either): either is Right => either.tag === 'Right'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/es_responses.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/es_responses.ts similarity index 100% rename from packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/es_responses.ts rename to packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/es_responses.ts diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/find_shared_origin_objects.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/find_shared_origin_objects.test.ts similarity index 98% rename from packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/find_shared_origin_objects.test.ts rename to packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/find_shared_origin_objects.test.ts index da9bc1f74d48..c9f90073da24 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/find_shared_origin_objects.test.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/find_shared_origin_objects.test.ts @@ -7,8 +7,8 @@ */ import { DeeplyMockedKeys } from '@kbn/utility-types-jest'; -import { CreatePointInTimeFinderFn, PointInTimeFinder } from '../point_in_time_finder'; -import { savedObjectsPointInTimeFinderMock } from '../../mocks/point_in_time_finder.mock'; +import { savedObjectsPointInTimeFinderMock } from '../../../mocks/point_in_time_finder.mock'; +import { CreatePointInTimeFinderFn, PointInTimeFinder } from '../../point_in_time_finder'; import { findSharedOriginObjects } from './find_shared_origin_objects'; import { SavedObjectsPointInTimeFinderClient } from '@kbn/core-saved-objects-api-server'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/find_shared_origin_objects.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/find_shared_origin_objects.ts similarity index 98% rename from packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/find_shared_origin_objects.ts rename to packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/find_shared_origin_objects.ts index 06f3cc27a66b..8ed1f0a3c965 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/find_shared_origin_objects.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/find_shared_origin_objects.ts @@ -9,7 +9,7 @@ import * as esKuery from '@kbn/es-query'; import { ALL_NAMESPACES_STRING } from '@kbn/core-saved-objects-utils-server'; import { getObjectKey } from '@kbn/core-saved-objects-base-server-internal'; -import type { CreatePointInTimeFinderFn } from '../point_in_time_finder'; +import type { CreatePointInTimeFinderFn } from '../../point_in_time_finder'; interface ObjectOrigin { /** The object's type. */ diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/index.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/index.ts similarity index 59% rename from packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/index.ts rename to packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/index.ts index 723615cc230a..ab84387e525a 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/index.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/index.ts @@ -9,3 +9,17 @@ export { getSavedObjectNamespaces } from './namespaces'; export { isFoundGetResponse, type GetResponseFound } from './es_responses'; export { findSharedOriginObjects } from './find_shared_origin_objects'; +export { + rawDocExistsInNamespace, + errorContent, + rawDocExistsInNamespaces, + isMgetDoc, + getCurrentTime, + getBulkOperationError, + getExpectedVersionProperties, + getSavedObjectFromSource, + setManaged, + normalizeNamespace, + type GetSavedObjectFromSourceOptions, +} from './internal_utils'; +export { type Left, type Either, type Right, isLeft, isRight } from './either'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_utils.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/internal_utils.test.ts similarity index 100% rename from packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_utils.test.ts rename to packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/internal_utils.test.ts diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_utils.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/internal_utils.ts similarity index 90% rename from packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_utils.ts rename to packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/internal_utils.ts index 688e292be9fe..49f02df61414 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_utils.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/internal_utils.ts @@ -9,11 +9,11 @@ import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { Payload } from '@hapi/boom'; import { + SavedObjectsErrorHelpers, type ISavedObjectTypeRegistry, type SavedObjectsRawDoc, type SavedObjectsRawDocSource, type SavedObject, - SavedObjectsErrorHelpers, type SavedObjectsRawDocParseOptions, type DecoratedError, } from '@kbn/core-saved-objects-server'; @@ -23,41 +23,6 @@ import { encodeHitVersion, } from '@kbn/core-saved-objects-base-server-internal'; -/** - * Discriminated union (TypeScript approximation of an algebraic data type); this design pattern is used for internal repository operations. - * @internal - */ -export type Either = Left | Right; - -/** - * Left part of discriminated union ({@link Either}). - * @internal - */ -export interface Left { - tag: 'Left'; - value: L; -} - -/** - * Right part of discriminated union ({@link Either}). - * @internal - */ -export interface Right { - tag: 'Right'; - value: R; -} - -/** - * Type guard for left part of discriminated union ({@link Left}, {@link Either}). - * @internal - */ -export const isLeft = (either: Either): either is Left => either.tag === 'Left'; -/** - * Type guard for right part of discriminated union ({@link Right}, {@link Either}). - * @internal - */ -export const isRight = (either: Either): either is Right => either.tag === 'Right'; - /** * Checks the raw response of a bulk operation and returns an error if necessary. * diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/namespaces.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/namespaces.ts similarity index 100% rename from packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/utils/namespaces.ts rename to packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/namespaces.ts diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.mock.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.mock.ts index a3a891664cb8..6a2cfa16e247 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.mock.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.mock.ts @@ -6,18 +6,18 @@ * Side Public License, v 1. */ -import type { collectMultiNamespaceReferences } from './collect_multi_namespace_references'; +import type { collectMultiNamespaceReferences } from './apis/internals/collect_multi_namespace_references'; import type { internalBulkResolve } from './apis/internals/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 * as InternalUtils from './apis/utils/internal_utils'; +import type { preflightCheckForCreate } from './apis/internals/preflight_check_for_create'; +import type { updateObjectsSpaces } from './apis/internals/update_objects_spaces'; import type { deleteLegacyUrlAliases } from './legacy_url_aliases'; export const mockCollectMultiNamespaceReferences = jest.fn() as jest.MockedFunction< typeof collectMultiNamespaceReferences >; -jest.mock('./collect_multi_namespace_references', () => ({ +jest.mock('./apis/internals/collect_multi_namespace_references', () => ({ collectMultiNamespaceReferences: mockCollectMultiNamespaceReferences, })); @@ -35,8 +35,8 @@ export const mockGetCurrentTime = jest.fn() as jest.MockedFunction< typeof InternalUtils['getCurrentTime'] >; -jest.mock('./internal_utils', () => { - const actual = jest.requireActual('./internal_utils'); +jest.mock('./apis/utils/internal_utils', () => { + const actual = jest.requireActual('./apis/utils/internal_utils'); return { ...actual, getBulkOperationError: mockGetBulkOperationError, @@ -48,13 +48,13 @@ export const mockPreflightCheckForCreate = jest.fn() as jest.MockedFunction< typeof preflightCheckForCreate >; -jest.mock('./preflight_check_for_create', () => ({ +jest.mock('./apis/internals/preflight_check_for_create', () => ({ preflightCheckForCreate: mockPreflightCheckForCreate, })); export const mockUpdateObjectsSpaces = jest.fn() as jest.MockedFunction; -jest.mock('./update_objects_spaces', () => ({ +jest.mock('./apis/internals/update_objects_spaces', () => ({ updateObjectsSpaces: mockUpdateObjectsSpaces, })); diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts index f87f8990b291..f1659d37826c 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts @@ -71,7 +71,7 @@ import { ValidationHelper, PreflightCheckHelper, SerializerHelper, -} from './helpers'; +} from './apis/helpers'; import { type ApiExecutionContext, performCreate, diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository_bulk_delete_internal_types.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository_bulk_delete_internal_types.ts index 6319662e8bb9..91df89b9be07 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository_bulk_delete_internal_types.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository_bulk_delete_internal_types.ts @@ -12,7 +12,7 @@ import type { ErrorCause, } from '@elastic/elasticsearch/lib/api/types'; import type { estypes, TransportResult } from '@elastic/elasticsearch'; -import type { Either } from './internal_utils'; +import type { Either } from './apis/utils'; import type { DeleteLegacyUrlAliasesParams } from './legacy_url_aliases'; /** From eec0fc12288874cf99e9bb1109f98f3128d008cf Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Wed, 10 May 2023 09:52:42 +0200 Subject: [PATCH 28/34] fix constructor error --- .../src/lib/repository.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts index f1659d37826c..409138cd635d 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts @@ -148,6 +148,11 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { /** The injectedConstructor is only used for unit testing */ injectedConstructor: any = SavedObjectsRepository ): ISavedObjectsRepository { + const mappings = migrator.getActiveMappings(); + const allTypes = typeRegistry.getAllTypes().map((t) => t.name); + const serializer = new SavedObjectsSerializer(typeRegistry); + const visibleTypes = allTypes.filter((type) => !typeRegistry.isHidden(type)); + const allowedTypes = [...new Set(visibleTypes.concat(includedHiddenTypes))]; const missingTypeMappings = includedHiddenTypes.filter((type) => !allTypes.includes(type)); if (missingTypeMappings.length > 0) { throw new Error( @@ -155,12 +160,6 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { ); } - const mappings = migrator.getActiveMappings(); - const allTypes = typeRegistry.getAllTypes().map((t) => t.name); - const serializer = new SavedObjectsSerializer(typeRegistry); - const visibleTypes = allTypes.filter((type) => !typeRegistry.isHidden(type)); - const allowedTypes = [...new Set(visibleTypes.concat(includedHiddenTypes))]; - return new injectedConstructor({ index: indexName, migrator, From 2464efd5781dab8249ae2cee95c1fa51c6e44a5f Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Wed, 10 May 2023 10:32:23 +0200 Subject: [PATCH 29/34] add mocks and test example --- .../src/lib/apis/helpers/common.ts | 3 + .../src/lib/apis/helpers/encryption.ts | 3 + .../src/lib/apis/helpers/index.ts | 20 ++-- .../src/lib/apis/helpers/preflight_check.ts | 3 + .../src/lib/apis/helpers/serializer.ts | 3 + .../src/lib/apis/helpers/validation.ts | 3 + .../src/lib/apis/remove_references_to.test.ts | 76 ++++++++++++ .../src/mocks/api_context.mock.ts | 47 ++++++++ .../src/mocks/api_helpers.mocks.ts | 110 ++++++++++++++++++ .../src/mocks/index.ts | 10 ++ .../src/mocks/migrator.mock.ts | 22 ++++ .../test_helpers/repository.test.common.ts | 4 +- 12 files changed, 293 insertions(+), 11 deletions(-) create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/remove_references_to.test.ts create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/mocks/api_context.mock.ts create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/mocks/api_helpers.mocks.ts create mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/mocks/migrator.mock.ts diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/common.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/common.ts index ce47fa16e7eb..51b8723cd6d0 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/common.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/common.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type { PublicMethodsOf } from '@kbn/utility-types'; import type { ISavedObjectTypeRegistry, ISavedObjectsSpacesExtension, @@ -17,6 +18,8 @@ import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; import { normalizeNamespace } from '../utils'; import type { CreatePointInTimeFinderFn } from '../../point_in_time_finder'; +export type ICommonHelper = PublicMethodsOf; + export class CommonHelper { private registry: ISavedObjectTypeRegistry; private spaceExtension?: ISavedObjectsSpacesExtension; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/encryption.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/encryption.ts index 3c80dc97e6f4..e70f08b225c5 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/encryption.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/encryption.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type { PublicMethodsOf } from '@kbn/utility-types'; import { SavedObject } from '@kbn/core-saved-objects-common/src/server_types'; import type { AuthorizationTypeMap, @@ -13,6 +14,8 @@ import type { ISavedObjectsEncryptionExtension, } from '@kbn/core-saved-objects-server'; +export type IEncryptionHelper = PublicMethodsOf; + export class EncryptionHelper { private securityExtension?: ISavedObjectsSecurityExtension; private encryptionExtension?: ISavedObjectsEncryptionExtension; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/index.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/index.ts index 2331d3dd799e..7c59a738ad3d 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/index.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/index.ts @@ -6,11 +6,11 @@ * Side Public License, v 1. */ -import type { CommonHelper } from './common'; -import type { EncryptionHelper } from './encryption'; -import type { ValidationHelper } from './validation'; -import type { PreflightCheckHelper } from './preflight_check'; -import type { SerializerHelper } from './serializer'; +import type { ICommonHelper } from './common'; +import type { IEncryptionHelper } from './encryption'; +import type { IValidationHelper } from './validation'; +import type { IPreflightCheckHelper } from './preflight_check'; +import type { ISerializerHelper } from './serializer'; export { CommonHelper } from './common'; export { EncryptionHelper } from './encryption'; @@ -23,9 +23,9 @@ export { } from './preflight_check'; export interface RepositoryHelpers { - common: CommonHelper; - encryption: EncryptionHelper; - validation: ValidationHelper; - preflight: PreflightCheckHelper; - serializer: SerializerHelper; + common: ICommonHelper; + encryption: IEncryptionHelper; + validation: IValidationHelper; + preflight: IPreflightCheckHelper; + serializer: ISerializerHelper; } diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/preflight_check.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/preflight_check.ts index 2853e84729f5..d51727ec3a85 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/preflight_check.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/preflight_check.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type { PublicMethodsOf } from '@kbn/utility-types'; import { isNotFoundFromUnsupportedServer } from '@kbn/core-elasticsearch-server-internal'; import type { ISavedObjectTypeRegistry, @@ -28,6 +29,8 @@ import { PreflightCheckForCreateObject, } from '../internals/preflight_check_for_create'; +export type IPreflightCheckHelper = PublicMethodsOf; + export class PreflightCheckHelper { private registry: ISavedObjectTypeRegistry; private serializer: ISavedObjectsSerializer; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/serializer.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/serializer.ts index 52e68f2a25c8..e5d609f59083 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/serializer.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/serializer.ts @@ -7,6 +7,7 @@ */ import { omit } from 'lodash'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import type { ISavedObjectTypeRegistry, ISavedObjectsSerializer, @@ -18,6 +19,8 @@ import { SavedObjectsRawDocParseOptions, } from '@kbn/core-saved-objects-server'; +export type ISerializerHelper = PublicMethodsOf; + export class SerializerHelper { private registry: ISavedObjectTypeRegistry; private serializer: ISavedObjectsSerializer; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/validation.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/validation.ts index 3c63c59cb05e..96224953ba45 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/validation.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/helpers/validation.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type { PublicMethodsOf } from '@kbn/utility-types'; import type { Logger } from '@kbn/logging'; import type { ISavedObjectTypeRegistry } from '@kbn/core-saved-objects-server'; import { SavedObjectsTypeValidator } from '@kbn/core-saved-objects-base-server-internal'; @@ -15,6 +16,8 @@ import { } from '@kbn/core-saved-objects-server'; import { ALL_NAMESPACES_STRING } from '@kbn/core-saved-objects-utils-server'; +export type IValidationHelper = PublicMethodsOf; + export class ValidationHelper { private registry: ISavedObjectTypeRegistry; private logger: Logger; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/remove_references_to.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/remove_references_to.test.ts new file mode 100644 index 000000000000..78c3e8d1faf9 --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/remove_references_to.test.ts @@ -0,0 +1,76 @@ +/* + * 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 { apiContextMock, ApiExecutionContextMock } from '../../mocks'; +import { createType } from '../../test_helpers/repository.test.common'; +import { performRemoveReferencesTo } from './remove_references_to'; + +const fooType = createType('foo', {}); +const barType = createType('bar', {}); + +describe('performRemoveReferencesTo', () => { + const namespace = 'some_ns'; + const indices = ['.kib_1', '.kib_2']; + let apiExecutionContext: ApiExecutionContextMock; + + beforeEach(() => { + apiExecutionContext = apiContextMock.create(); + apiExecutionContext.registry.registerType(fooType); + apiExecutionContext.registry.registerType(barType); + + apiExecutionContext.helpers.common.getCurrentNamespace.mockImplementation( + (space) => space ?? 'default' + ); + apiExecutionContext.helpers.common.getIndicesForTypes.mockReturnValue(indices); + }); + + describe('with all extensions enabled', () => { + it('calls getCurrentNamespace with the correct parameters', async () => { + await performRemoveReferencesTo( + { type: 'foo', id: 'id', options: { namespace } }, + apiExecutionContext + ); + + const commonHelper = apiExecutionContext.helpers.common; + expect(commonHelper.getCurrentNamespace).toHaveBeenCalledTimes(1); + expect(commonHelper.getCurrentNamespace).toHaveBeenLastCalledWith(namespace); + }); + + it('calls authorizeRemoveReferences with the correct parameters', async () => { + await performRemoveReferencesTo( + { type: 'foo', id: 'id', options: { namespace } }, + apiExecutionContext + ); + + const securityExt = apiExecutionContext.extensions.securityExtension!; + expect(securityExt.authorizeRemoveReferences).toHaveBeenCalledTimes(1); + expect(securityExt.authorizeRemoveReferences).toHaveBeenLastCalledWith({ + namespace, + object: { type: 'foo', id: 'id' }, + }); + }); + + it('calls client.updateByQuery with the correct parameters', async () => { + await performRemoveReferencesTo( + { type: 'foo', id: 'id', options: { namespace, refresh: false } }, + apiExecutionContext + ); + + const client = apiExecutionContext.client; + expect(client.updateByQuery).toHaveBeenCalledTimes(1); + expect(client.updateByQuery).toHaveBeenLastCalledWith( + { + refresh: false, + index: indices, + body: expect.any(Object), + }, + { ignore: [404], meta: true } + ); + }); + }); +}); diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/mocks/api_context.mock.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/mocks/api_context.mock.ts new file mode 100644 index 000000000000..3b4da315868f --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/mocks/api_context.mock.ts @@ -0,0 +1,47 @@ +/* + * 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 { loggerMock, MockedLogger } from '@kbn/logging-mocks'; +import { + elasticsearchClientMock, + ElasticsearchClientMock, +} from '@kbn/core-elasticsearch-client-server-mocks'; +import { SavedObjectTypeRegistry } from '@kbn/core-saved-objects-base-server-internal'; +import { serializerMock } from '@kbn/core-saved-objects-base-server-mocks'; +import type { ApiExecutionContext } from '../lib/apis/types'; +import { apiHelperMocks, RepositoryHelpersMock } from './api_helpers.mocks'; +import { savedObjectsExtensionsMock } from './saved_objects_extensions.mock'; +import { createMigratorMock, KibanaMigratorMock } from './migrator.mock'; + +export type ApiExecutionContextMock = Pick & { + registry: SavedObjectTypeRegistry; + helpers: RepositoryHelpersMock; + extensions: ReturnType; + client: ElasticsearchClientMock; + serializer: ReturnType; + migrator: KibanaMigratorMock; + logger: MockedLogger; +}; + +const createApiExecutionContextMock = (): ApiExecutionContextMock => { + return { + registry: new SavedObjectTypeRegistry(), + helpers: apiHelperMocks.create(), + extensions: savedObjectsExtensionsMock.create(), + client: elasticsearchClientMock.createElasticsearchClient(), + serializer: serializerMock.create(), + migrator: createMigratorMock(), + logger: loggerMock.create(), + allowedTypes: ['foo', 'bar'], + mappings: { properties: { mockMappings: { type: 'text' } } }, + }; +}; + +export const apiContextMock = { + create: createApiExecutionContextMock, +}; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/mocks/api_helpers.mocks.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/mocks/api_helpers.mocks.ts new file mode 100644 index 000000000000..d5caf01cf59c --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/mocks/api_helpers.mocks.ts @@ -0,0 +1,110 @@ +/* + * 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 { PublicMethodsOf } from '@kbn/utility-types'; +import type { + CommonHelper, + EncryptionHelper, + ValidationHelper, + PreflightCheckHelper, + SerializerHelper, +} from '../lib/apis/helpers'; + +export type CommonHelperMock = jest.Mocked>; + +const createCommonHelperMock = (): CommonHelperMock => { + const mock: CommonHelperMock = { + createPointInTimeFinder: jest.fn(), + getIndexForType: jest.fn(), + getIndicesForTypes: jest.fn(), + getCurrentNamespace: jest.fn(), + getValidId: jest.fn(), + }; + + mock.getIndexForType.mockReturnValue('.kibana_mock'); + mock.getIndicesForTypes.mockReturnValue(['.kibana_mock']); + mock.getCurrentNamespace.mockImplementation((space) => space ?? 'default'); + mock.getValidId.mockReturnValue('valid-id'); + + return mock; +}; + +export type EncryptionHelperMock = jest.Mocked>; + +const createEncryptionHelperMock = (): EncryptionHelperMock => { + const mock: EncryptionHelperMock = { + optionallyEncryptAttributes: jest.fn(), + optionallyDecryptAndRedactSingleResult: jest.fn(), + optionallyDecryptAndRedactBulkResult: jest.fn(), + }; + + return mock; +}; + +export type ValidationHelperMock = jest.Mocked>; + +const createValidationHelperMock = (): ValidationHelperMock => { + const mock: ValidationHelperMock = { + validateInitialNamespaces: jest.fn(), + validateObjectNamespaces: jest.fn(), + validateObjectForCreate: jest.fn(), + validateOriginId: jest.fn(), + }; + + return mock; +}; + +export type SerializerHelperMock = jest.Mocked>; + +const createSerializerHelperMock = (): SerializerHelperMock => { + const mock: SerializerHelperMock = { + rawToSavedObject: jest.fn(), + }; + + return mock; +}; + +export type PreflightCheckHelperMock = jest.Mocked>; + +const createPreflightCheckHelperMock = (): PreflightCheckHelperMock => { + const mock: PreflightCheckHelperMock = { + preflightCheckForCreate: jest.fn(), + preflightCheckForBulkDelete: jest.fn(), + preflightCheckNamespaces: jest.fn(), + preflightCheckForUpsertAliasConflict: jest.fn(), + }; + + return mock; +}; + +export interface RepositoryHelpersMock { + common: CommonHelperMock; + encryption: EncryptionHelperMock; + validation: ValidationHelperMock; + preflight: PreflightCheckHelperMock; + serializer: SerializerHelperMock; +} + +const createRepositoryHelpersMock = (): RepositoryHelpersMock => { + return { + common: createCommonHelperMock(), + encryption: createEncryptionHelperMock(), + validation: createValidationHelperMock(), + preflight: createPreflightCheckHelperMock(), + serializer: createSerializerHelperMock(), + }; +}; + +export const apiHelperMocks = { + create: createRepositoryHelpersMock, + createCommonHelper: createCommonHelperMock, + createEncryptionHelper: createEncryptionHelperMock, + createValidationHelper: createValidationHelperMock, + createSerializerHelper: createSerializerHelperMock, + createPreflightCheckHelper: createPreflightCheckHelperMock, +}; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/mocks/index.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/mocks/index.ts index a2d933b58404..22569e943789 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/mocks/index.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/mocks/index.ts @@ -9,3 +9,13 @@ export { savedObjectsPointInTimeFinderMock } from './point_in_time_finder.mock'; export { kibanaMigratorMock } from './kibana_migrator.mock'; export { repositoryMock } from './repository.mock'; +export { + apiHelperMocks, + type SerializerHelperMock, + type CommonHelperMock, + type ValidationHelperMock, + type EncryptionHelperMock, + type RepositoryHelpersMock, + type PreflightCheckHelperMock, +} from './api_helpers.mocks'; +export { apiContextMock, type ApiExecutionContextMock } from './api_context.mock'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/mocks/migrator.mock.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/mocks/migrator.mock.ts new file mode 100644 index 000000000000..94f221e7cfbc --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/mocks/migrator.mock.ts @@ -0,0 +1,22 @@ +/* + * 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 { IKibanaMigrator } from '@kbn/core-saved-objects-base-server-internal'; + +export type KibanaMigratorMock = jest.Mocked; + +export const createMigratorMock = (kibanaVersion: string = '8.0.0'): KibanaMigratorMock => { + return { + kibanaVersion, + runMigrations: jest.fn(), + prepareMigrations: jest.fn(), + getStatus$: jest.fn(), + getActiveMappings: jest.fn(), + migrateDocument: jest.fn(), + }; +}; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/test_helpers/repository.test.common.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/test_helpers/repository.test.common.ts index abeab2a8f2e7..78d81a8b86be 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/test_helpers/repository.test.common.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/test_helpers/repository.test.common.ts @@ -333,7 +333,9 @@ export const createType = ( hidden: false, namespaceType: 'single', mappings: { - properties: mappings.properties[type].properties! as SavedObjectsMappingProperties, + properties: (mappings.properties[type] + ? mappings.properties[type].properties! + : {}) as SavedObjectsMappingProperties, }, migrations: { '1.1.1': (doc) => doc }, ...parts, From 7b8e9661896aebcbe43d8703ad50dae71347e385 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Wed, 10 May 2023 10:33:36 +0200 Subject: [PATCH 30/34] lint --- .../src/lib/apis/check_conflicts.ts | 9 +-------- .../apis/internals/collect_multi_namespace_references.ts | 6 +++++- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/check_conflicts.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/check_conflicts.ts index a90aada8dd4c..b0f7b1bf38da 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/check_conflicts.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/check_conflicts.ts @@ -18,14 +18,7 @@ import { SavedObjectsBaseOptions, SavedObjectsCheckConflictsResponse, } from '@kbn/core-saved-objects-api-server'; -import { - Either, - errorContent, - isLeft, - isRight, - isMgetDoc, - rawDocExistsInNamespace, -} from './utils'; +import { Either, errorContent, isLeft, isRight, isMgetDoc, rawDocExistsInNamespace } from './utils'; import { ApiExecutionContext } from './types'; export interface PerformCheckConflictsParams { diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/collect_multi_namespace_references.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/collect_multi_namespace_references.ts index 91767ce0079a..260fa7c6adf1 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/collect_multi_namespace_references.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/collect_multi_namespace_references.ts @@ -26,7 +26,11 @@ import { findLegacyUrlAliases } from '../../legacy_url_aliases'; import { getRootFields } from '../../included_fields'; import type { CreatePointInTimeFinderFn } from '../../point_in_time_finder'; import type { RepositoryEsClient } from '../../repository_es_client'; -import { findSharedOriginObjects, getSavedObjectFromSource, rawDocExistsInNamespace } from '../utils'; +import { + findSharedOriginObjects, + getSavedObjectFromSource, + rawDocExistsInNamespace, +} from '../utils'; /** * When we collect an object's outbound references, we will only go a maximum of this many levels deep before we throw an error. From 48a1be0498689a4b0c87d2599578595ca92843d8 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 10 May 2023 08:39:09 +0000 Subject: [PATCH 31/34] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- .../core-saved-objects-api-server-internal/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/tsconfig.json b/packages/core/saved-objects/core-saved-objects-api-server-internal/tsconfig.json index 6f7ca16e5d58..dbb7f83fa94b 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/tsconfig.json +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/tsconfig.json @@ -32,6 +32,7 @@ "@kbn/core-http-server", "@kbn/core-http-server-mocks", "@kbn/core-saved-objects-migration-server-internal", + "@kbn/utility-types", ], "exclude": [ "target/**/*", From 37ccab39b9f09082eb8afb54a40f65b4bd51c30d Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Wed, 10 May 2023 14:06:43 +0200 Subject: [PATCH 32/34] merge util files --- .../src/lib/apis/utils/index.ts | 2 +- .../src/lib/apis/utils/internal_utils.ts | 18 ++++++++++++ .../src/lib/apis/utils/namespaces.ts | 28 ------------------- 3 files changed, 19 insertions(+), 29 deletions(-) delete mode 100644 packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/namespaces.ts diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/index.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/index.ts index ab84387e525a..db96a742b2bb 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/index.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/index.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -export { getSavedObjectNamespaces } from './namespaces'; export { isFoundGetResponse, type GetResponseFound } from './es_responses'; export { findSharedOriginObjects } from './find_shared_origin_objects'; export { @@ -20,6 +19,7 @@ export { getSavedObjectFromSource, setManaged, normalizeNamespace, + getSavedObjectNamespaces, type GetSavedObjectFromSourceOptions, } from './internal_utils'; export { type Left, type Either, type Right, isLeft, isRight } from './either'; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/internal_utils.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/internal_utils.ts index 49f02df61414..f06894bcca1e 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/internal_utils.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/internal_utils.ts @@ -263,6 +263,24 @@ export function setManaged({ return optionsManaged ?? objectManaged ?? false; } +/** + * Returns a string array of namespaces for a given saved object. If the saved object is undefined, the result is an array that contains the + * current namespace. Value may be undefined if an existing saved object has no namespaces attribute; this should not happen in normal + * operations, but it is possible if the Elasticsearch document is manually modified. + * + * @param namespace The current namespace. + * @param document Optional existing saved object that was obtained in a preflight operation. + */ +export function getSavedObjectNamespaces( + namespace?: string, + document?: SavedObjectsRawDoc +): string[] | undefined { + if (document) { + return document._source?.namespaces; + } + return [SavedObjectsUtils.namespaceIdToString(namespace)]; +} + /** * Extracts the contents of a decorated error to return the attributes for bulk operations. */ diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/namespaces.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/namespaces.ts deleted file mode 100644 index 1592d00c0c42..000000000000 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/namespaces.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 { SavedObjectsRawDoc } from '@kbn/core-saved-objects-server'; -import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server'; - -/** - * Returns a string array of namespaces for a given saved object. If the saved object is undefined, the result is an array that contains the - * current namespace. Value may be undefined if an existing saved object has no namespaces attribute; this should not happen in normal - * operations, but it is possible if the Elasticsearch document is manually modified. - * - * @param namespace The current namespace. - * @param document Optional existing saved object that was obtained in a preflight operation. - */ -export function getSavedObjectNamespaces( - namespace?: string, - document?: SavedObjectsRawDoc -): string[] | undefined { - if (document) { - return document._source?.namespaces; - } - return [SavedObjectsUtils.namespaceIdToString(namespace)]; -} From 32f9866b634124b2bad59914a2fcce2ffa1b507f Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Thu, 11 May 2023 08:07:37 +0200 Subject: [PATCH 33/34] add left and right helpers --- .../src/lib/apis/bulk_create.ts | 75 ++++++++---------- .../src/lib/apis/bulk_delete.ts | 79 ++++++++----------- .../src/lib/apis/bulk_get.ts | 24 +++--- .../src/lib/apis/bulk_update.ts | 43 +++++----- .../src/lib/apis/check_conflicts.ts | 37 +++++---- .../apis/internals/internal_bulk_resolve.ts | 20 ++--- .../internals/preflight_check_for_create.ts | 13 +-- .../apis/internals/update_objects_spaces.ts | 34 +++----- .../src/lib/apis/utils/either.ts | 18 +++++ .../src/lib/apis/utils/index.ts | 2 +- 10 files changed, 160 insertions(+), 185 deletions(-) diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_create.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_create.ts index 219e6071ce33..c7802397739c 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_create.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_create.ts @@ -27,6 +27,8 @@ import { getBulkOperationError, getCurrentTime, getExpectedVersionProperties, + left, + right, isLeft, isRight, normalizeNamespace, @@ -42,6 +44,15 @@ export interface PerformBulkCreateParams { options: SavedObjectsCreateOptions; } +type ExpectedResult = Either< + { type: string; id?: string; error: Payload }, + { + method: 'index' | 'create'; + object: SavedObjectsBulkCreateObject & { id: string }; + preflightCheckIndex?: number; + } +>; + export const performBulkCreate = async ( { objects, options }: PerformBulkCreateParams, { @@ -73,14 +84,6 @@ export const performBulkCreate = async ( const time = getCurrentTime(); let preflightCheckIndexCounter = 0; - type ExpectedResult = Either< - { type: string; id?: string; error: Payload }, - { - method: 'index' | 'create'; - object: SavedObjectsBulkCreateObject & { id: string }; - preflightCheckIndex?: number; - } - >; const expectedResults = objects.map((object) => { const { type, id: requestId, initialNamespaces, version, managed } = object; let error: DecoratedError | undefined; @@ -99,27 +102,21 @@ export const performBulkCreate = async ( } if (error) { - return { - tag: 'Left', - value: { id: requestId, type, error: errorContent(error) }, - }; + return left({ id: requestId, type, error: errorContent(error) }); } const method = requestId && overwrite ? 'index' : 'create'; const requiresNamespacesCheck = requestId && registry.isMultiNamespace(type); - return { - tag: 'Right', - value: { - method, - object: { - ...object, - id, - managed: setManaged({ optionsManaged, objectManaged }), - }, - ...(requiresNamespacesCheck && { preflightCheckIndex: preflightCheckIndexCounter++ }), + return right({ + method, + object: { + ...object, + id, + managed: setManaged({ optionsManaged, objectManaged }), }, - }; + ...(requiresNamespacesCheck && { preflightCheckIndex: preflightCheckIndexCounter++ }), + }) as ExpectedResult; }); const validObjects = expectedResults.filter(isRight); @@ -187,17 +184,14 @@ export const performBulkCreate = async ( const { type, id, existingDocument, error } = preflightResult; if (error) { const { metadata } = error; - return { - tag: 'Left', - value: { - id, - type, - error: { - ...errorContent(SavedObjectsErrorHelpers.createConflictError(type, id)), - ...(metadata && { metadata }), - }, + return left({ + id, + type, + error: { + ...errorContent(SavedObjectsErrorHelpers.createConflictError(type, id)), + ...(metadata && { metadata }), }, - }; + }); } savedObjectNamespaces = initialNamespaces || getSavedObjectNamespaces(namespace, existingDocument); @@ -249,14 +243,11 @@ export const performBulkCreate = async ( try { validationHelper.validateObjectForCreate(object.type, migrated); } catch (error) { - return { - tag: 'Left', - value: { - id: object.id, - type: object.type, - error, - }, - }; + return left({ + id: object.id, + type: object.type, + error, + }); } const expectedResult = { @@ -276,7 +267,7 @@ export const performBulkCreate = async ( expectedResult.rawMigratedDoc._source ); - return { tag: 'Right', value: expectedResult }; + return right(expectedResult); }) ); diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_delete.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_delete.ts index db978199bbe7..768dcf91b61c 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_delete.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_delete.ts @@ -28,6 +28,8 @@ import { isMgetDoc, rawDocExistsInNamespace, isRight, + left, + right, } from './utils'; import type { ApiExecutionContext } from './types'; import { deleteLegacyUrlAliases } from '../legacy_url_aliases'; @@ -230,24 +232,18 @@ function presortObjectsByNamespaceType( return objects.map((object) => { const { type, id } = object; if (!allowedTypes.includes(type)) { - return { - tag: 'Left', - value: { - id, - type, - error: errorContent(SavedObjectsErrorHelpers.createUnsupportedTypeError(type)), - }, - }; + return left({ + id, + type, + error: errorContent(SavedObjectsErrorHelpers.createUnsupportedTypeError(type)), + }); } const requiresNamespacesCheck = registry.isMultiNamespace(type); - return { - tag: 'Right', - value: { - type, - id, - ...(requiresNamespacesCheck && { esRequestIndex: bulkGetRequestIndexCounter++ }), - }, - }; + return right({ + type, + id, + ...(requiresNamespacesCheck && { esRequestIndex: bulkGetRequestIndexCounter++ }), + }); }); } @@ -281,25 +277,19 @@ function getExpectedBulkDeleteMultiNamespaceDocsResults( // return an error if the doc isn't found at all or the doc doesn't exist in the namespaces if (!docFound) { - return { - tag: 'Left', - value: { - id, - type, - error: errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)), - }, - }; + return left({ + id, + type, + error: errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)), + }); } // the following check should be redundant since we're retrieving the docs from elasticsearch but we check just to make sure if (!rawDocExistsInNamespace(registry, actualResult as SavedObjectsRawDoc, namespace)) { - return { - tag: 'Left', - value: { - id, - type, - error: errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)), - }, - }; + return left({ + id, + type, + error: errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)), + }); } // @ts-expect-error MultiGetHit is incorrectly missing _id, _source namespaces = actualResult!._source.namespaces ?? [ @@ -308,19 +298,16 @@ function getExpectedBulkDeleteMultiNamespaceDocsResults( const useForce = force && force === true; // the document is shared to more than one space and can only be deleted by force. if (!useForce && (namespaces.length > 1 || namespaces.includes(ALL_NAMESPACES_STRING))) { - return { - tag: 'Left', - value: { - success: false, - id, - type, - error: errorContent( - SavedObjectsErrorHelpers.createBadRequestError( - 'Unable to delete saved object that exists in multiple namespaces, use the `force` option to delete it anyway' - ) - ), - }, - }; + return left({ + success: false, + id, + type, + error: errorContent( + SavedObjectsErrorHelpers.createBadRequestError( + 'Unable to delete saved object that exists in multiple namespaces, use the `force` option to delete it anyway' + ) + ), + }); } } // contains all objects that passed initial preflight checks, including single namespace objects that skipped the mget call @@ -332,7 +319,7 @@ function getExpectedBulkDeleteMultiNamespaceDocsResults( esRequestIndex: indexCounter++, }; - return { tag: 'Right', value: expectedResult }; + return right(expectedResult); }); return expectedBulkDeleteMultiNamespaceDocsResults; } diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_get.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_get.ts index c10e81d0e3c9..19ac91083c8c 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_get.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_get.ts @@ -27,6 +27,8 @@ import { getSavedObjectFromSource, isLeft, isRight, + left, + right, rawDocExistsInNamespaces, } from './utils'; import { ApiExecutionContext } from './types'; @@ -93,26 +95,20 @@ export const performBulkGet = async ( } if (error) { - return { - tag: 'Left', - value: { id, type, error: errorContent(error) }, - }; + return left({ id, type, error: errorContent(error) }); } let namespaces = object.namespaces; if (spacesExtension && namespaces?.includes(ALL_NAMESPACES_STRING)) { namespaces = await getAvailableSpaces(); } - return { - tag: 'Right', - value: { - type, - id, - fields, - namespaces, - esRequestIndex: bulkGetRequestIndexCounter++, - }, - }; + return right({ + type, + id, + fields, + namespaces, + esRequestIndex: bulkGetRequestIndexCounter++, + }); }) ); diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_update.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_update.ts index 5f6981152191..b9c0f10a9021 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_update.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_update.ts @@ -29,8 +29,10 @@ import { getBulkOperationError, getCurrentTime, getExpectedVersionProperties, - isLeft, isMgetDoc, + left, + right, + isLeft, isRight, rawDocExistsInNamespace, } from './utils'; @@ -80,10 +82,7 @@ export const performBulkUpdate = async ( } if (error) { - return { - tag: 'Left', - value: { id, type, error: errorContent(error) }, - }; + return left({ id, type, error: errorContent(error) }); } const documentToSave = { @@ -94,17 +93,14 @@ export const performBulkUpdate = async ( const requiresNamespacesCheck = registry.isMultiNamespace(object.type); - return { - tag: 'Right', - value: { - type, - id, - version, - documentToSave, - objectNamespace, - ...(requiresNamespacesCheck && { esRequestIndex: bulkGetRequestIndexCounter++ }), - }, - }; + return right({ + type, + id, + version, + documentToSave, + objectNamespace, + ...(requiresNamespacesCheck && { esRequestIndex: bulkGetRequestIndexCounter++ }), + }); }); const validObjects = expectedBulkGetResults.filter(isRight); @@ -199,14 +195,11 @@ export const performBulkUpdate = async ( getNamespaceId(objectNamespace) ) ) { - return { - tag: 'Left', - value: { - id, - type, - error: errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)), - }, - }; + return left({ + id, + type, + error: errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)), + }); } // @ts-expect-error MultiGetHit is incorrectly missing _id, _source namespaces = actualResult!._source.namespaces ?? [ @@ -251,7 +244,7 @@ export const performBulkUpdate = async ( } ); - return { tag: 'Right', value: expectedResult }; + return right(expectedResult); }) ); diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/check_conflicts.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/check_conflicts.ts index b0f7b1bf38da..6f110c3d6f90 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/check_conflicts.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/check_conflicts.ts @@ -18,7 +18,16 @@ import { SavedObjectsBaseOptions, SavedObjectsCheckConflictsResponse, } from '@kbn/core-saved-objects-api-server'; -import { Either, errorContent, isLeft, isRight, isMgetDoc, rawDocExistsInNamespace } from './utils'; +import { + Either, + errorContent, + left, + right, + isLeft, + isRight, + isMgetDoc, + rawDocExistsInNamespace, +} from './utils'; import { ApiExecutionContext } from './types'; export interface PerformCheckConflictsParams { @@ -48,24 +57,18 @@ export const performCheckConflicts = async ( const { type, id } = object; if (!allowedTypes.includes(type)) { - return { - tag: 'Left', - value: { - id, - type, - error: errorContent(SavedObjectsErrorHelpers.createUnsupportedTypeError(type)), - }, - }; + return left({ + id, + type, + error: errorContent(SavedObjectsErrorHelpers.createUnsupportedTypeError(type)), + }); } - return { - tag: 'Right', - value: { - type, - id, - esRequestIndex: bulkGetRequestIndexCounter++, - }, - }; + return right({ + type, + id, + esRequestIndex: bulkGetRequestIndexCounter++, + }); }); const validObjects = expectedBulkGetResults.filter(isRight); diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve.ts index 7c1124f376f8..6e143004dfb3 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve.ts @@ -45,6 +45,8 @@ import { type Right, isLeft, isRight, + left, + right, } from '../utils'; import type { RepositoryEsClient } from '../../repository_es_client'; @@ -271,19 +273,13 @@ function validateObjectTypes(objects: SavedObjectsBulkResolveObject[], allowedTy return objects.map>((object) => { const { type, id } = object; if (!allowedTypes.includes(type)) { - return { - tag: 'Left', - value: { - type, - id, - error: SavedObjectsErrorHelpers.createUnsupportedTypeError(type), - }, - }; + return left({ + type, + id, + error: SavedObjectsErrorHelpers.createUnsupportedTypeError(type), + }); } - return { - tag: 'Right', - value: object, - }; + return right(object); }); } diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/preflight_check_for_create.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/preflight_check_for_create.ts index bc899adda83d..aec1ac9991d4 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/preflight_check_for_create.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/preflight_check_for_create.ts @@ -24,7 +24,7 @@ import { import { findLegacyUrlAliases } from '../../legacy_url_aliases'; import type { CreatePointInTimeFinderFn } from '../../point_in_time_finder'; import type { RepositoryEsClient } from '../../repository_es_client'; -import { isLeft, isRight, rawDocExistsInNamespaces, type Either } from '../utils'; +import { left, right, isLeft, isRight, rawDocExistsInNamespaces, type Either } from '../utils'; /** * If the object will be created in this many spaces (or "*" all current and future spaces), we use find to fetch all aliases. @@ -200,9 +200,10 @@ async function optionallyFindAliases( const objectsToGetOrObjectsToFind = objects.map>((object) => { const { type, id, namespaces, overwrite = false } = object; const spaces = new Set(namespaces); - const tag = - spaces.size > FIND_ALIASES_THRESHOLD || spaces.has(ALL_NAMESPACES_STRING) ? 'Right' : 'Left'; - return { tag, value: { type, id, overwrite, spaces } }; + const value = { type, id, overwrite, spaces }; + return spaces.size > FIND_ALIASES_THRESHOLD || spaces.has(ALL_NAMESPACES_STRING) + ? right(value) + : left(value); }); const objectsToFind = objectsToGetOrObjectsToFind @@ -235,13 +236,13 @@ async function optionallyFindAliases( } if (spacesWithConflictingAliases.length) { // we found one or more conflicting aliases, this is an error result - return { tag: 'Left', value: { ...either.value, spacesWithConflictingAliases } }; + return left({ ...either.value, spacesWithConflictingAliases }); } } // we checked for aliases but did not detect any conflicts; make sure we don't check for aliases again during mget checkAliases = false; } - return { tag: 'Right', value: { ...either.value, checkAliases } }; + return right({ ...either.value, checkAliases }); }); return result; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/update_objects_spaces.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/update_objects_spaces.ts index 9cdc7fe1b9ef..62b006cc0d9a 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/update_objects_spaces.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/internals/update_objects_spaces.ts @@ -36,6 +36,8 @@ import { type Either, isLeft, isRight, + left, + right, } from '../utils'; import { DEFAULT_REFRESH_SETTING } from '../../constants'; import type { RepositoryEsClient } from '../../repository_es_client'; @@ -117,10 +119,7 @@ export async function updateObjectsSpaces({ if (!allowedTypes.includes(type)) { const error = errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)); - return { - tag: 'Left', - value: { id, type, spaces: [], error }, - }; + return left({ id, type, spaces: [], error }); } if (!registry.isShareable(type)) { const error = errorContent( @@ -128,21 +127,15 @@ export async function updateObjectsSpaces({ `${type} doesn't support multiple namespaces` ) ); - return { - tag: 'Left', - value: { id, type, spaces: [], error }, - }; + return left({ id, type, spaces: [], error }); } - return { - tag: 'Right', - value: { - type, - id, - version, - esRequestIndex: bulkGetRequestIndexCounter++, - }, - }; + return right({ + type, + id, + version, + esRequestIndex: bulkGetRequestIndexCounter++, + }); }); const validObjects = expectedBulkGetResults.filter(isRight); @@ -217,10 +210,7 @@ export async function updateObjectsSpaces({ !rawDocExistsInNamespace(registry, doc, namespace) ) { const error = errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)); - return { - tag: 'Left', - value: { id, type, spaces: [], error }, - }; + return left({ id, type, spaces: [], error }); } const currentSpaces = doc._source?.namespaces ?? []; @@ -265,7 +255,7 @@ export async function updateObjectsSpaces({ } } - return { tag: 'Right', value: expectedResult }; + return right(expectedResult); }); const { refresh = DEFAULT_REFRESH_SETTING } = options; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/either.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/either.ts index e573ea87ce6d..9403945a9a65 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/either.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/either.ts @@ -30,6 +30,24 @@ export interface Right { value: R; } +/** + * Returns a {@link Left} part holding the provided value. + * @internal + */ +export const left = (value: L): Left => ({ + tag: 'Left', + value, +}); + +/** + * Returns a {@link Right} part holding the provided value. + * @internal + */ +export const right = (value: R): Right => ({ + tag: 'Right', + value, +}); + /** * Type guard for left part of discriminated union ({@link Left}, {@link Either}). * @internal diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/index.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/index.ts index db96a742b2bb..f3562dffb1e8 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/index.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/utils/index.ts @@ -22,4 +22,4 @@ export { getSavedObjectNamespaces, type GetSavedObjectFromSourceOptions, } from './internal_utils'; -export { type Left, type Either, type Right, isLeft, isRight } from './either'; +export { type Left, type Either, type Right, isLeft, isRight, left, right } from './either'; From 69bab51f3d3c734141f82c0b153bf1dd4df06a18 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Thu, 11 May 2023 08:23:04 +0200 Subject: [PATCH 34/34] add description to the package's readme --- .../README.md | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/README.md b/packages/core/saved-objects/core-saved-objects-api-server-internal/README.md index 69dca03cec05..b25fd29f5441 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/README.md +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/README.md @@ -1,3 +1,31 @@ # @kbn/core-saved-objects-api-server-internal This package contains the internal implementation of core's server-side savedObjects client and repository. + +## Structure of the package + +``` +@kbn/core-saved-objects-api-server-internal +- /src/lib + - repository.ts + - /apis + - create.ts + - delete.ts + - .... + - /helpers + - /utils + - /internals +``` + +### lib/apis/utils +Base utility functions, receiving (mostly) parameters from a given API call's option +(e.g the type or id of a document, but not the type registry). + +### lib/apis/helpers +'Stateful' helpers. These helpers were mostly here to receive the utility functions that were extracted from the SOR. +They are instantiated with the SOR's context (e.g type registry, mappings and so on), to avoid the caller to such +helpers to have to pass all the parameters again. + +### lib/apis/internals +I would call them 'utilities with business logic'. These are the 'big' chunks of logic called by the APIs. +E.g preflightCheckForCreate, internalBulkResolve and so on. \ No newline at end of file