import pMap from 'p-map' ; import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey' ; import intersection from 'lodash/intersection' ; import type { Logger } from '../../../logging' ; import { isNotFoundFromUnsupportedServer } from '../../../elasticsearch' ; import type { IndexMapping } from '../../mappings' ; import type { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry' ; import type { SavedObjectsRawDocSource , SavedObjectsSerializer } from '../../serialization' ; import type { MutatingOperationRefreshSetting , SavedObjectError , SavedObjectsBaseOptions , } from '../../types' ; import type { DecoratedError } from './errors' ; import { SavedObjectsErrorHelpers } from './errors' ; import { getBulkOperationError , getExpectedVersionProperties , rawDocExistsInNamespace , Either , isLeft , isRight , } from './internal_utils' ; import { DEFAULT_REFRESH_SETTING } from './repository' ; import type { RepositoryEsClient } from './repository_es_client' ; import { ALL_NAMESPACES_STRING } from './utils' ; import type { DeleteLegacyUrlAliasesParams } from './legacy_url_aliases' ; import { deleteLegacyUrlAliases } from './legacy_url_aliases' ; public export interface SavedObjectsUpdateObjectsSpacesObject { /** The type of the object to update */ id : string ; /** The ID of the object to update */ type : string ; internal spaces ? : string [ ] ; internal version ? : string ; } public export interface SavedObjectsUpdateObjectsSpacesOptions extends SavedObjectsBaseOptions { /** The Elasticsearch Refresh setting for this operation */ refresh ? : MutatingOperationRefreshSetting ; } public export interface SavedObjectsUpdateObjectsSpacesResponse { objects : SavedObjectsUpdateObjectsSpacesResponseObject [ ] ; } public export interface SavedObjectsUpdateObjectsSpacesResponseObject { /** The type of the referenced object */ type : string ; /** The ID of the referenced object */ id : string ; /** The space(s) that the referenced object exists in */ spaces : string [ ] ; /** Included if there was an error updating this object's spaces */ error ? : SavedObjectError ; } internal export interface UpdateObjectsSpacesParams { mappings : IndexMapping ; registry : ISavedObjectTypeRegistry ; allowedTypes : string [ ] ; client : RepositoryEsClient ; serializer : SavedObjectsSerializer ; logger : Logger ; getIndexForType : ( type : string ) => string ; objects : SavedObjectsUpdateObjectsSpacesObject [ ] ; spacesToAdd : string [ ] ; spacesToRemove : string [ ] ; options ? : SavedObjectsUpdateObjectsSpacesOptions ; } type ObjectToDeleteAliasesFor = Pick < DeleteLegacyUrlAliasesParams , 'type' | 'id' | 'namespaces' | 'deleteBehavior' > ; const MAX_CONCURRENT_ALIAS_DELETIONS = 10 ; function isMgetError ( doc ? ) { return Boolean ( doc && 'error' in doc ) ; } /**
 * Gets all references and transitive references of the given objects. Ignores any object and/or reference that is not a multi-namespace
 * type.
 */ export async function updateObjectsSpaces ( { mappings , registry , allowedTypes , client , serializer , logger , getIndexForType , objects , spacesToAdd , spacesToRemove , options = { } , } ) { if ( ! spacesToAdd . length && ! spacesToRemove . length ) { throw SavedObjectsErrorHelpers . createBadRequestError ( 'spacesToAdd and/or spacesToRemove must be a non-empty array of strings' ) ; } if ( intersection ( spacesToAdd , spacesToRemove ) . length > 0 ) { throw SavedObjectsErrorHelpers . createBadRequestError ( 'spacesToAdd and spacesToRemove cannot contain any of the same strings' ) ; } const { namespace } = options ; let bulkGetRequestIndexCounter = 0 ; const expectedBulkGetResults = objects . map ( ( object ) => { const { type , id , spaces , version } = object ; if ( ! allowedTypes . includes ( type ) ) { const error = errorContent ( SavedObjectsErrorHelpers . createGenericNotFoundError ( type , id ) ) ; return { tag : 'Left' , value : { id , type , spaces : [ ] , error } , } ; } if ( ! registry . isShareable ( type ) ) { const error = errorContent ( SavedObjectsErrorHelpers . createBadRequestError ( `${ type } doesn't support multiple namespaces` ) ) ; return { tag : 'Left' , value : { id , type , spaces : [ ] , error } , } ; } return { tag : 'Right' , value : { type , id , spaces , version , ... ( ! spaces && { esRequestIndex : bulkGetRequestIndexCounter ++ } ) , } , } ; } ) ; const bulkGetDocs = expectedBulkGetResults . reduce < estypes . MgetOperation [ ] > ( ( acc , x ) => { if ( isRight ( x ) && x . value . esRequestIndex !== undefined ) { acc . push ( { _id : serializer . generateRawId ( undefined , x . value . type , x . value . id ) , _index : getIndexForType ( x . value . type ) , _source : [ 'type' , 'namespaces' ] , } ) ; } return acc ; } , [ ] ) ; const bulkGetResponse = bulkGetDocs . length ? await client . mget < SavedObjectsRawDocSource > ( { body : { docs : bulkGetDocs } } , { ignore : [ 404 ] } ) : undefined ; if ( bulkGetResponse && isNotFoundFromUnsupportedServer ( { statusCode : bulkGetResponse . statusCode , headers : bulkGetResponse . headers , } ) ) { throw SavedObjectsErrorHelpers . createGenericNotFoundEsUnavailableError ( ) ; } const time = new Date ( ) . toISOString ( ) ; let bulkOperationRequestIndexCounter = 0 ; const bulkOperationParams = [ ] ; const objectsToDeleteAliasesFor = [ ] ; const expectedBulkOperationResults = expectedBulkGetResults . map ( ( expectedBulkGetResult ) => { if ( isLeft ( expectedBulkGetResult ) ) { return expectedBulkGetResult ; } const { id , type , spaces , version , esRequestIndex } = expectedBulkGetResult . value ; let currentSpaces = spaces ; let versionProperties ; if ( esRequestIndex !== undefined ) { const doc = bulkGetResponse ?. body . docs [ esRequestIndex ] ; const isErrorDoc = isMgetError ( doc ) ; if ( isErrorDoc || ! doc ?. found || ! rawDocExistsInNamespace ( registry , doc , namespace ) ) { const error = errorContent ( SavedObjectsErrorHelpers . createGenericNotFoundError ( type , id ) ) ; return { tag : 'Left' , value : { id , type , spaces : [ ] , error } , } ; } currentSpaces = doc . _source ?. namespaces ?? [ ] ; versionProperties = getExpectedVersionProperties ( version , doc ) ; } else if ( spaces ?. length === 0 ) { const error = errorContent ( SavedObjectsErrorHelpers . createGenericNotFoundError ( type , id ) ) ; return { tag : 'Left' , value : { id , type , spaces : [ ] , error } , } ; } else { versionProperties = getExpectedVersionProperties ( version ) ; } const { updatedSpaces , removedSpaces , isUpdateRequired } = analyzeSpaceChanges ( currentSpaces , spacesToAdd , spacesToRemove ) ; const expectedResult = { type , id , updatedSpaces , ... ( isUpdateRequired && { esRequestIndex : bulkOperationRequestIndexCounter ++ } ) , } ; if ( isUpdateRequired ) { const documentMetadata = { _id : serializer . generateRawId ( undefined , type , id ) , _index : getIndexForType ( type ) , ... versionProperties , } ; if ( updatedSpaces . length ) { const documentToSave = { updated_at : time , namespaces : updatedSpaces } ; bulkOperationParams . push ( { update : documentMetadata } , { doc : documentToSave } ) ; } else { bulkOperationParams . push ( { delete : documentMetadata } ) ; } if ( removedSpaces . length && ! updatedSpaces . includes ( ALL_NAMESPACES_STRING ) ) { objectsToDeleteAliasesFor . push ( { type , id , ... ( removedSpaces . includes ( ALL_NAMESPACES_STRING ) ? { namespaces : updatedSpaces , deleteBehavior : 'exclusive' } : { namespaces : removedSpaces , deleteBehavior : 'inclusive' } ) , } ) ; } } return { tag : 'Right' , value : expectedResult } ; } ) ; const { refresh = DEFAULT_REFRESH_SETTING } = options ; const bulkOperationResponse = bulkOperationParams . length ? await client . bulk ( { refresh , body : bulkOperationParams , require_alias : true } ) : undefined ; const mapper = async ( { type , id , namespaces , deleteBehavior } ) => deleteLegacyUrlAliases ( { mappings , registry , client , getIndexForType , type , id , namespaces , deleteBehavior , } ) . catch ( ( err ) => { logger . error ( `Unable to delete aliases when unsharing an object: ${ err . message }` ) ; } ) ; await pMap ( objectsToDeleteAliasesFor , mapper , { concurrency : MAX_CONCURRENT_ALIAS_DELETIONS } ) ; return { objects : expectedBulkOperationResults . map < SavedObjectsUpdateObjectsSpacesResponseObject > ( ( expectedResult ) => { if ( isLeft ( expectedResult ) ) { return expectedResult . value ; } const { type , id , updatedSpaces , esRequestIndex } = expectedResult . value ; if ( esRequestIndex !== undefined ) { const response = bulkOperationResponse ?. body . items [ esRequestIndex ] ?? { } ; const rawResponse = Object . values ( response ) [ 0 ] as any ; const error = getBulkOperationError ( type , id , rawResponse ) ; if ( error ) { return { id , type , spaces : [ ] , error } ; } } return { id , type , spaces : updatedSpaces } ; } ) , } ; } /** Extracts the contents of a decorated error to return the attributes for bulk operations. */ function errorContent ( error ) { return error . output . payload ; } /** Gets the remaining spaces for an object after adding new ones and removing old ones. */ function analyzeSpaceChanges ( existingSpaces , spacesToAdd , spacesToRemove ) { const addSet = new Set ( spacesToAdd ) ; const removeSet = new Set ( spacesToRemove ) ; const removedSpaces = [ ] ; const updatedSpaces = existingSpaces . filter ( ( x ) => { addSet . delete ( x ) ; const removed = removeSet . delete ( x ) ; if ( removed ) { removedSpaces . push ( x ) ; } return ! removed ; } ) . concat ( Array . from ( addSet ) ) ; const isAnySpaceAdded = addSet . size > 0 ; const isAnySpaceRemoved = removeSet . size < spacesToRemove . length ; const isUpdateRequired = isAnySpaceAdded || isAnySpaceRemoved ; return { updatedSpaces , removedSpaces , isUpdateRequired } ; }