import RE2 from 're2' ; import { KibanaRequest , SavedObjectsClientContract , SavedObjectsFindOptions , SavedObjectsFindResult , } from 'kibana/server' ; import type { SecurityPluginSetup } from '../../../security/server' ; import { JobType , ML_SAVED_OBJECT_TYPE } from '../../common/types/saved_objects' ; import { MLJobNotFound } from '../lib/ml_client' ; import { getSavedObjectClientError } from './util' ; import { authorizationProvider } from './authorization' ; export interface JobObject { job_id : string ; datafeed_id : string | null ; type : JobType ; } type JobObjectFilter = { [ k in keyof JobObject ] ? : string ; } ; export type JobSavedObjectService = ReturnType < typeof jobSavedObjectServiceFactory > ; export function jobSavedObjectServiceFactory ( savedObjectsClient , internalSavedObjectsClient , spacesEnabled , authorization , isMlReady ) { async function _getJobObjects ( jobType ? , jobId ? , datafeedId ? , currentSpaceOnly = true ) { await isMlReady ( ) ; const filterObject = { } ; if ( jobType !== undefined ) { filterObject . type = jobType ; } if ( jobId !== undefined ) { filterObject . job_id = jobId ; } else if ( datafeedId !== undefined ) { filterObject . datafeed_id = datafeedId ; } const { filter , searchFields } = createSavedObjectFilter ( filterObject ) ; const options = { type : ML_SAVED_OBJECT_TYPE , perPage : 10000 , ... ( spacesEnabled === false || currentSpaceOnly === true ? { } : { namespaces : [ '*' ] } ) , searchFields , filter , } ; const jobs = await savedObjectsClient . find < JobObject > ( options ) ; return jobs . saved_objects ; } async function _createJob ( jobType , jobId , datafeedId ? ) { await isMlReady ( ) ; const job = { job_id : jobId , datafeed_id : datafeedId ?? null , type : jobType , } ; const id = savedObjectId ( job ) ; try { const [ existingJobObject ] = await getAllJobObjectsForAllSpaces ( jobType , jobId ) ; if ( existingJobObject !== undefined ) { if ( existingJobObject . namespaces ?. length ) { await _forceDeleteJob ( jobType , jobId , existingJobObject . namespaces [ 0 ] ) ; } else { await savedObjectsClient . delete ( ML_SAVED_OBJECT_TYPE , id , { force : true } ) ; } } } catch ( error ) { } await savedObjectsClient . create < JobObject > ( ML_SAVED_OBJECT_TYPE , job , { id , } ) ; } async function _bulkCreateJobs ( jobs ) { await isMlReady ( ) ; return await savedObjectsClient . bulkCreate < JobObject > ( jobs . map ( ( j ) => ( { type : ML_SAVED_OBJECT_TYPE , id : savedObjectId ( j . job ) , attributes : j . job , initialNamespaces : j . namespaces , } ) ) ) ; } function savedObjectId ( job ) { return `${ job . type }-${ job . job_id }` ; } async function _deleteJob ( jobType , jobId ) { const jobs = await _getJobObjects ( jobType , jobId ) ; const job = jobs [ 0 ] ; if ( job === undefined ) { throw new MLJobNotFound ( 'job not found' ) ; } await savedObjectsClient . delete ( ML_SAVED_OBJECT_TYPE , job . id , { force : true } ) ; } async function _forceDeleteJob ( jobType , jobId , namespace ) { const id = savedObjectId ( { job_id : jobId , datafeed_id : null , type : jobType , } ) ; await internalSavedObjectsClient . delete ( ML_SAVED_OBJECT_TYPE , id , { namespace : namespace === '*' ? undefined : namespace , force : true , } ) ; } async function createAnomalyDetectionJob ( jobId , datafeedId ? ) { await _createJob ( 'anomaly-detector' , jobId , datafeedId ) ; } async function deleteAnomalyDetectionJob ( jobId ) { await _deleteJob ( 'anomaly-detector' , jobId ) ; } async function forceDeleteAnomalyDetectionJob ( jobId , namespace ) { await _forceDeleteJob ( 'anomaly-detector' , jobId , namespace ) ; } async function createDataFrameAnalyticsJob ( jobId ) { await _createJob ( 'data-frame-analytics' , jobId ) ; } async function deleteDataFrameAnalyticsJob ( jobId ) { await _deleteJob ( 'data-frame-analytics' , jobId ) ; } async function forceDeleteDataFrameAnalyticsJob ( jobId , namespace ) { await _forceDeleteJob ( 'data-frame-analytics' , jobId , namespace ) ; } async function bulkCreateJobs ( jobs ) { return await _bulkCreateJobs ( jobs ) ; } async function getAllJobObjects ( jobType ? , currentSpaceOnly = true ) { return await _getJobObjects ( jobType , undefined , undefined , currentSpaceOnly ) ; } async function getJobObject ( jobType , jobId , currentSpaceOnly = true ) { const [ jobObject ] = await _getJobObjects ( jobType , jobId , undefined , currentSpaceOnly ) ; return jobObject ; } async function getAllJobObjectsForAllSpaces ( jobType ? , jobId ? ) { await isMlReady ( ) ; const filterObject = { } ; if ( jobType !== undefined ) { filterObject . type = jobType ; } if ( jobId !== undefined ) { filterObject . job_id = jobId ; } const { filter , searchFields } = createSavedObjectFilter ( filterObject ) ; const options = { type : ML_SAVED_OBJECT_TYPE , perPage : 10000 , ... ( spacesEnabled === false ? { } : { namespaces : [ '*' ] } ) , searchFields , filter , } ; return ( await internalSavedObjectsClient . find < JobObject > ( options ) ) . saved_objects ; } async function addDatafeed ( datafeedId , jobId ) { const jobs = await _getJobObjects ( 'anomaly-detector' , jobId ) ; const job = jobs [ 0 ] ; if ( job === undefined ) { throw new MLJobNotFound ( `'${ datafeedId }' not found` ) ; } const jobObject = job . attributes ; jobObject . datafeed_id = datafeedId ; await savedObjectsClient . update < JobObject > ( ML_SAVED_OBJECT_TYPE , job . id , jobObject ) ; } async function deleteDatafeed ( datafeedId ) { const jobs = await _getJobObjects ( 'anomaly-detector' , undefined , datafeedId ) ; const job = jobs [ 0 ] ; if ( job === undefined ) { throw new MLJobNotFound ( `'${ datafeedId }' not found` ) ; } const jobObject = job . attributes ; jobObject . datafeed_id = null ; await savedObjectsClient . update < JobObject > ( ML_SAVED_OBJECT_TYPE , job . id , jobObject ) ; } async function getIds ( jobType , idType ) { const jobs = await _getJobObjects ( jobType ) ; return jobs . map ( ( o ) => o . attributes [ idType ] ) ; } async function filterJobObjectsForSpace < T > ( jobType , list , field , key ) { if ( list . length === 0 ) { return [ ] ; } const jobIds = await getIds ( jobType , key ) ; return list . filter ( ( j ) => jobIds . includes ( j [ field ] as unknown as string ) ) ; } async function filterJobsForSpace < T > ( jobType , list , field ) { return filterJobObjectsForSpace < T > ( jobType , list , field , 'job_id' ) ; } async function filterDatafeedsForSpace < T > ( jobType , list , field ) { return filterJobObjectsForSpace < T > ( jobType , list , field , 'datafeed_id' ) ; } async function filterJobObjectIdsForSpace ( jobType , ids , key , allowWildcards = false ) { if ( ids . length === 0 ) { return [ ] ; } const jobIds = await getIds ( jobType , key ) ; if ( allowWildcards === false || ids . join ( ) . match ( '\\*' ) === null ) { return ids . filter ( ( id ) => jobIds . includes ( id ) ) ; } return ids . filter ( ( id ) => { if ( id . match ( '\\*' ) === null ) { return jobIds . includes ( id ) ; } const regex = new RE2 ( id . replace ( '*' , '.*' ) ) ; return jobIds . some ( ( jId ) => typeof jId === 'string' && regex . exec ( jId ) ) ; } ) ; } async function filterJobIdsForSpace ( jobType , ids , allowWildcards = false ) { return filterJobObjectIdsForSpace ( jobType , ids , 'job_id' , allowWildcards ) ; } async function filterDatafeedIdsForSpace ( ids , allowWildcards = false ) { return filterJobObjectIdsForSpace ( 'anomaly-detector' , ids , 'datafeed_id' , allowWildcards ) ; } async function updateJobsSpaces ( jobType , jobIds , spacesToAdd , spacesToRemove ) { const results = { } ; const jobs = await _getJobObjects ( jobType ) ; const jobObjectIdMap = new Map < string , string > ( ) ; const objectsToUpdate = [ ] ; for ( const jobId of jobIds ) { const job = jobs . find ( ( j ) => j . attributes . job_id === jobId ) ; if ( job === undefined ) { results [ jobId ] = { success : false , error : createError ( jobId , 'job_id' ) , } ; } else { jobObjectIdMap . set ( job . id , jobId ) ; objectsToUpdate . push ( { type : ML_SAVED_OBJECT_TYPE , id : job . id } ) ; } } try { const updateResult = await savedObjectsClient . updateObjectsSpaces ( objectsToUpdate , spacesToAdd , spacesToRemove ) ; updateResult . objects . forEach ( ( { id : objectId , error } ) => { const jobId = jobObjectIdMap . get ( objectId ) ! ; if ( error ) { results [ jobId ] = { success : false , error : getSavedObjectClientError ( error ) , } ; } else { results [ jobId ] = { success : true , } ; } } ) ; } catch ( error ) { const clientError = getSavedObjectClientError ( error ) ; objectsToUpdate . forEach ( ( { id : objectId } ) => { const jobId = jobObjectIdMap . get ( objectId ) ! ; results [ jobId ] = { success : false , error : clientError , } ; } ) ; } return results ; } async function canCreateGlobalJobs ( request ) { if ( authorization === undefined ) { return true ; } const { authorizationCheck } = authorizationProvider ( authorization ) ; return ( await authorizationCheck ( request ) ) . canCreateGlobally ; } return { getAllJobObjects , getJobObject , createAnomalyDetectionJob , createDataFrameAnalyticsJob , deleteAnomalyDetectionJob , forceDeleteAnomalyDetectionJob , deleteDataFrameAnalyticsJob , forceDeleteDataFrameAnalyticsJob , addDatafeed , deleteDatafeed , filterJobsForSpace , filterJobIdsForSpace , filterDatafeedsForSpace , filterDatafeedIdsForSpace , updateJobsSpaces , bulkCreateJobs , getAllJobObjectsForAllSpaces , canCreateGlobalJobs , } ; } export function createError ( id , key ) { let reason = `'${ id }' not found` ; if ( key === 'job_id' ) { reason = `No known job with id '${ id }'` ; } else if ( key === 'datafeed_id' ) { reason = `No known datafeed with id '${ id }'` ; } return { error : { reason , } , status : 404 , } ; } function createSavedObjectFilter ( filterObject ) { const searchFields = [ ] ; const filter = Object . entries ( filterObject ) . map ( ( [ k , v ] ) => { searchFields . push ( k ) ; return `${ ML_SAVED_OBJECT_TYPE }.attributes.${ k }: "${ v }"` ; } ) . join ( ' AND ' ) ; return { filter , searchFields } ; }