diff --git a/x-pack/platform/plugins/shared/ml/common/types/saved_objects.ts b/x-pack/platform/plugins/shared/ml/common/types/saved_objects.ts index adaf00fd9405f..0fbd48f0c28ae 100644 --- a/x-pack/platform/plugins/shared/ml/common/types/saved_objects.ts +++ b/x-pack/platform/plugins/shared/ml/common/types/saved_objects.ts @@ -39,6 +39,10 @@ export interface CanDeleteMLSpaceAwareItemsResponse { }; } +export interface CanSyncToAllSpacesResponse { + canSync: boolean; +} + export type JobsSpacesResponse = { [jobType in JobType]: { [jobId: string]: string[] }; }; diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/job_spaces_sync/job_spaces_sync_flyout.tsx b/x-pack/platform/plugins/shared/ml/public/application/components/job_spaces_sync/job_spaces_sync_flyout.tsx index fe4b23fbdf3a1..1b16df01e49a3 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/job_spaces_sync/job_spaces_sync_flyout.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/components/job_spaces_sync/job_spaces_sync_flyout.tsx @@ -28,6 +28,7 @@ import { useMlApi } from '../../contexts/kibana'; import type { SyncSavedObjectResponse, SyncResult } from '../../../../common/types/saved_objects'; import { SyncList } from './sync_list'; import { useToastNotificationService } from '../../services/toast_notification_service'; +import { SyncToAllSpacesWarning } from './sync_to_all_spaces_warning'; export interface Props { onClose: () => void; @@ -37,17 +38,22 @@ export const JobSpacesSyncFlyout: FC = ({ onClose }) => { const { displayErrorToast, displaySuccessToast } = useToastNotificationService(); const [loading, setLoading] = useState(false); const [canSync, setCanSync] = useState(false); + const [canSyncToAllSpaces, setCanSyncToAllSpaces] = useState(true); const [syncResp, setSyncResp] = useState(null); const { - savedObjects: { syncSavedObjects }, + savedObjects: { syncSavedObjects, canSyncToAllSpaces: canSyncToAllSpacesFunc }, } = useMlApi(); async function loadSyncList(simulate: boolean = true) { setLoading(true); try { - const resp = await syncSavedObjects(simulate); + const resp = await syncSavedObjects(simulate, canSyncToAllSpaces); setSyncResp(resp); + if (simulate === true) { + setCanSyncToAllSpaces((await canSyncToAllSpacesFunc()).canSync); + } + const count = Object.values(resp).reduce((acc, cur) => acc + Object.keys(cur).length, 0); setCanSync(count > 0); setLoading(false); @@ -118,6 +124,12 @@ export const JobSpacesSyncFlyout: FC = ({ onClose }) => { /> + {canSyncToAllSpaces === false ? ( + <> + + + + ) : null} diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/job_spaces_sync/sync_to_all_spaces_warning.tsx b/x-pack/platform/plugins/shared/ml/public/application/components/job_spaces_sync/sync_to_all_spaces_warning.tsx new file mode 100644 index 0000000000000..e97f319fb6554 --- /dev/null +++ b/x-pack/platform/plugins/shared/ml/public/application/components/job_spaces_sync/sync_to_all_spaces_warning.tsx @@ -0,0 +1,51 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { FC } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiCallOut, EuiLink, EuiText } from '@elastic/eui'; +import { useMlKibana } from '../../contexts/kibana/kibana_context'; + +export const SyncToAllSpacesWarning: FC = () => { + const { + services: { + docLinks: { links }, + }, + } = useMlKibana(); + const docLink = links.security.kibanaPrivileges; + return ( + + } + color="warning" + > + + + + + ), + }} + /> + + + ); +}; diff --git a/x-pack/platform/plugins/shared/ml/public/application/routing/resolvers.ts b/x-pack/platform/plugins/shared/ml/public/application/routing/resolvers.ts index 7d7ca41ebf9c3..c504013eb3ec3 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/routing/resolvers.ts +++ b/x-pack/platform/plugins/shared/ml/public/application/routing/resolvers.ts @@ -22,3 +22,7 @@ export const basicResolvers = (): Resolvers => ({ getMlNodeCount, loadMlServerInfo, }); + +export const initSavedObjects = async (mlApi: MlApi) => { + return mlApi.savedObjects.initSavedObjects().catch(() => {}); +}; diff --git a/x-pack/platform/plugins/shared/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx b/x-pack/platform/plugins/shared/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx index cdfb854314a72..b494d78b66294 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx @@ -14,7 +14,7 @@ import type { NavigateToPath } from '../../../contexts/kibana'; import type { MlRoute } from '../../router'; import { createPath, PageLoader } from '../../router'; import { useRouteResolver } from '../../use_resolver'; -import { basicResolvers } from '../../resolvers'; +import { basicResolvers, initSavedObjects } from '../../resolvers'; import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; const Page = dynamic(async () => ({ @@ -45,7 +45,10 @@ export const analyticsJobsListRouteFactory = ( }); const PageWrapper: FC = () => { - const { context } = useRouteResolver('full', ['canGetDataFrameAnalytics'], basicResolvers()); + const { context } = useRouteResolver('full', ['canGetDataFrameAnalytics'], { + ...basicResolvers(), + initSavedObjects, + }); return ( diff --git a/x-pack/platform/plugins/shared/ml/public/application/routing/routes/jobs_list.tsx b/x-pack/platform/plugins/shared/ml/public/application/routing/routes/jobs_list.tsx index 28a71b5e7c819..359fc7236ddd6 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/routing/routes/jobs_list.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/routing/routes/jobs_list.tsx @@ -24,7 +24,7 @@ import { useRouteResolver } from '../use_resolver'; import { getBreadcrumbWithUrlForApp } from '../breadcrumbs'; import { AnnotationUpdatesService } from '../../services/annotations_service'; import { MlAnnotationUpdatesContext } from '../../contexts/ml/ml_annotation_updates_context'; -import { basicResolvers } from '../resolvers'; +import { basicResolvers, initSavedObjects } from '../resolvers'; const JobsPage = dynamic(async () => ({ default: (await import('../../jobs/jobs_list')).JobsPage, @@ -51,7 +51,10 @@ export const jobListRouteFactory = (navigateToPath: NavigateToPath, basePath: st }); const PageWrapper: FC = () => { - const { context } = useRouteResolver('full', ['canGetJobs'], basicResolvers()); + const { context } = useRouteResolver('full', ['canGetJobs'], { + ...basicResolvers(), + initSavedObjects, + }); const timefilter = useTimefilter({ timeRangeSelector: false, autoRefreshSelector: true }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/routing/routes/overview.tsx b/x-pack/platform/plugins/shared/ml/public/application/routing/routes/overview.tsx index 4fec14bc41294..c578cc17544e7 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/routing/routes/overview.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/routing/routes/overview.tsx @@ -19,6 +19,7 @@ import { getBreadcrumbWithUrlForApp } from '../breadcrumbs'; import type { MlRoute, PageProps } from '../router'; import { createPath, PageLoader } from '../router'; import { useRouteResolver } from '../use_resolver'; +import { initSavedObjects } from '../resolvers'; const OverviewPage = React.lazy(() => import('../../overview/overview_page')); @@ -48,6 +49,7 @@ const PageWrapper: FC = () => { const { context } = useRouteResolver('full', ['canGetMlInfo'], { getMlNodeCount, loadMlServerInfo, + initSavedObjects, }); useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false }); diff --git a/x-pack/platform/plugins/shared/ml/public/application/routing/routes/trained_models/models_list.tsx b/x-pack/platform/plugins/shared/ml/public/application/routing/routes/trained_models/models_list.tsx index cee2a92f03ada..2fa7f47a83ead 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/routing/routes/trained_models/models_list.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/routing/routes/trained_models/models_list.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { type FC, useCallback } from 'react'; +import type { FC } from 'react'; import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; @@ -16,10 +16,9 @@ import type { NavigateToPath } from '../../../contexts/kibana'; import type { MlRoute } from '../../router'; import { createPath, PageLoader } from '../../router'; import { useRouteResolver } from '../../use_resolver'; -import { basicResolvers } from '../../resolvers'; +import { basicResolvers, initSavedObjects } from '../../resolvers'; import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; import { MlPageHeader } from '../../../components/page_header'; -import { useSavedObjectsApiService } from '../../../services/ml_api_service/saved_objects'; const ModelsList = dynamic(async () => ({ default: (await import('../../../model_management/models_list')).ModelsList, @@ -49,19 +48,9 @@ export const modelsListRouteFactory = ( }); const PageWrapper: FC = () => { - const { initSavedObjects } = useSavedObjectsApiService(); - - const initSavedObjectsWrapper = useCallback(async () => { - try { - await initSavedObjects(); - } catch (error) { - // ignore error as user may not have permission to sync - } - }, [initSavedObjects]); - const { context } = useRouteResolver('full', ['canGetTrainedModels'], { ...basicResolvers(), - initSavedObjectsWrapper, + initSavedObjects, }); return ( diff --git a/x-pack/platform/plugins/shared/ml/public/application/services/ml_api_service/saved_objects.ts b/x-pack/platform/plugins/shared/ml/public/application/services/ml_api_service/saved_objects.ts index 125256770f1d4..e382eee7661a3 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/services/ml_api_service/saved_objects.ts +++ b/x-pack/platform/plugins/shared/ml/public/application/services/ml_api_service/saved_objects.ts @@ -23,6 +23,7 @@ import type { JobsSpacesResponse, TrainedModelsSpacesResponse, SyncCheckResponse, + CanSyncToAllSpacesResponse, } from '../../../../common/types/saved_objects'; export const savedObjectsApiProvider = (httpService: HttpService) => ({ @@ -56,11 +57,11 @@ export const savedObjectsApiProvider = (httpService: HttpService) => ({ version: '1', }); }, - syncSavedObjects(simulate: boolean = false) { + syncSavedObjects(simulate: boolean = false, addToAllSpaces?: boolean) { return httpService.http({ path: `${ML_EXTERNAL_BASE_PATH}/saved_objects/sync`, method: 'GET', - query: { simulate }, + query: { simulate, addToAllSpaces }, version: '2023-10-31', }); }, @@ -90,6 +91,15 @@ export const savedObjectsApiProvider = (httpService: HttpService) => ({ version: '1', }); }, + canSyncToAllSpaces(mlSavedObjectType?: MlSavedObjectType) { + return httpService.http({ + path: `${ML_INTERNAL_BASE_PATH}/saved_objects/can_sync_to_all_spaces${ + mlSavedObjectType !== undefined ? `/${mlSavedObjectType}` : '' + }`, + method: 'GET', + version: '1', + }); + }, trainedModelsSpaces() { return httpService.http({ path: `${ML_INTERNAL_BASE_PATH}/saved_objects/trained_models_spaces`, diff --git a/x-pack/platform/plugins/shared/ml/server/models/data_recognizer/data_recognizer.ts b/x-pack/platform/plugins/shared/ml/server/models/data_recognizer/data_recognizer.ts index e585d73a9d894..41a8f60f912f3 100644 --- a/x-pack/platform/plugins/shared/ml/server/models/data_recognizer/data_recognizer.ts +++ b/x-pack/platform/plugins/shared/ml/server/models/data_recognizer/data_recognizer.ts @@ -870,8 +870,8 @@ export class DataRecognizer { ); if (applyToAllSpaces === true) { const canCreateGlobalJobs = await this._mlSavedObjectService.canCreateGlobalMlSavedObjects( - 'anomaly-detector', - this._request + this._request, + 'anomaly-detector' ); if (canCreateGlobalJobs === true) { await this._mlSavedObjectService.updateJobsSpaces( diff --git a/x-pack/platform/plugins/shared/ml/server/routes/saved_objects.ts b/x-pack/platform/plugins/shared/ml/server/routes/saved_objects.ts index 437f0a80eb1d7..962678cbfa867 100644 --- a/x-pack/platform/plugins/shared/ml/server/routes/saved_objects.ts +++ b/x-pack/platform/plugins/shared/ml/server/routes/saved_objects.ts @@ -92,9 +92,9 @@ export function savedObjectsRoutes( routeGuard.fullLicenseAPIGuard( async ({ client, request, response, mlSavedObjectService }) => { try { - const { simulate } = request.query; + const { simulate, addToAllSpaces } = request.query; const { syncSavedObjects } = syncSavedObjectsFactory(client, mlSavedObjectService); - const savedObjects = await syncSavedObjects(simulate); + const savedObjects = await syncSavedObjects(simulate, addToAllSpaces ?? true); return response.ok({ body: savedObjects, @@ -450,4 +450,46 @@ export function savedObjectsRoutes( } ) ); + + router.versioned + .get({ + path: `${ML_INTERNAL_BASE_PATH}/saved_objects/can_sync_to_all_spaces/{mlSavedObjectType?}`, + access: 'internal', + security: { + authz: { + requiredPrivileges: [ + 'ml:canGetJobs', + 'ml:canGetDataFrameAnalytics', + 'ml:canGetTrainedModels', + ], + }, + }, + summary: 'Check whether user can sync a job or trained model to the * space', + description: `Check the user's ability to sync jobs or trained models to the * space. Returns whether they are able to sync the job or trained model to the * space.`, + }) + .addVersion( + { + version: '1', + validate: { + request: { + params: syncCheckSchema, + }, + }, + }, + routeGuard.fullLicenseAPIGuard(async ({ request, response, mlSavedObjectService }) => { + try { + const { mlSavedObjectType } = request.params; + const canSync = await mlSavedObjectService.canCreateGlobalMlSavedObjects( + request, + mlSavedObjectType as MlSavedObjectType + ); + + return response.ok({ + body: { canSync }, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); } diff --git a/x-pack/platform/plugins/shared/ml/server/routes/schemas/saved_objects.ts b/x-pack/platform/plugins/shared/ml/server/routes/schemas/saved_objects.ts index d40638c496f00..8feac4f005d0c 100644 --- a/x-pack/platform/plugins/shared/ml/server/routes/schemas/saved_objects.ts +++ b/x-pack/platform/plugins/shared/ml/server/routes/schemas/saved_objects.ts @@ -39,7 +39,10 @@ export const itemsAndCurrentSpace = schema.object({ ids: schema.arrayOf(schema.string()), }); -export const syncJobObjects = schema.object({ simulate: schema.maybe(schema.boolean()) }); +export const syncJobObjects = schema.object({ + simulate: schema.maybe(schema.boolean()), + addToAllSpaces: schema.maybe(schema.boolean()), +}); export const syncCheckSchema = schema.object({ mlSavedObjectType: schema.maybe(schema.string()) }); diff --git a/x-pack/platform/plugins/shared/ml/server/saved_objects/checks.ts b/x-pack/platform/plugins/shared/ml/server/saved_objects/checks.ts index f8e1827c78a87..73a3601ca748b 100644 --- a/x-pack/platform/plugins/shared/ml/server/saved_objects/checks.ts +++ b/x-pack/platform/plugins/shared/ml/server/saved_objects/checks.ts @@ -319,8 +319,8 @@ export function checksFactory( }, {} as DeleteMLSpaceAwareItemsCheckResponse); } const canCreateGlobalMlSavedObjects = await mlSavedObjectService.canCreateGlobalMlSavedObjects( - mlSavedObjectType, - request + request, + mlSavedObjectType ); const savedObjects = diff --git a/x-pack/platform/plugins/shared/ml/server/saved_objects/service.ts b/x-pack/platform/plugins/shared/ml/server/saved_objects/service.ts index 3e0f81a8ba13b..a8e829d8401c3 100644 --- a/x-pack/platform/plugins/shared/ml/server/saved_objects/service.ts +++ b/x-pack/platform/plugins/shared/ml/server/saved_objects/service.ts @@ -102,7 +102,12 @@ export function mlSavedObjectServiceFactory( return jobs.saved_objects; } - async function _createJob(jobType: JobType, jobId: string, datafeedId?: string) { + async function _createJob( + jobType: JobType, + jobId: string, + datafeedId?: string, + addToAllSpaces = false + ) { await isMlReady(); const job: JobObject = { @@ -133,6 +138,7 @@ export function mlSavedObjectServiceFactory( await savedObjectsClient.create(ML_JOB_SAVED_OBJECT_TYPE, job, { id, + ...(addToAllSpaces ? { initialNamespaces: ['*'] } : {}), }); _clearSavedObjectsClientCache(); } @@ -182,8 +188,12 @@ export function mlSavedObjectServiceFactory( _clearSavedObjectsClientCache(); } - async function createAnomalyDetectionJob(jobId: string, datafeedId?: string) { - await _createJob('anomaly-detector', jobId, datafeedId); + async function createAnomalyDetectionJob( + jobId: string, + datafeedId?: string, + addToAllSpaces = false + ) { + await _createJob('anomaly-detector', jobId, datafeedId, addToAllSpaces); } async function deleteAnomalyDetectionJob(jobId: string) { @@ -194,8 +204,8 @@ export function mlSavedObjectServiceFactory( await _forceDeleteJob('anomaly-detector', jobId, namespace); } - async function createDataFrameAnalyticsJob(jobId: string) { - await _createJob('data-frame-analytics', jobId); + async function createDataFrameAnalyticsJob(jobId: string, addToAllSpaces = false) { + await _createJob('data-frame-analytics', jobId, undefined, addToAllSpaces); } async function deleteDataFrameAnalyticsJob(jobId: string) { @@ -418,8 +428,8 @@ export function mlSavedObjectServiceFactory( } async function canCreateGlobalMlSavedObjects( - mlSavedObjectType: MlSavedObjectType, - request: KibanaRequest + request: KibanaRequest, + mlSavedObjectType?: MlSavedObjectType ) { if (authorization === undefined) { return true; @@ -428,6 +438,10 @@ export function mlSavedObjectServiceFactory( const { canCreateJobsGlobally, canCreateTrainedModelsGlobally } = await authorizationCheck( request ); + if (mlSavedObjectType === undefined) { + return canCreateJobsGlobally && canCreateTrainedModelsGlobally; + } + return mlSavedObjectType === 'trained-model' ? canCreateTrainedModelsGlobally : canCreateJobsGlobally; @@ -441,8 +455,12 @@ export function mlSavedObjectServiceFactory( return modelObject; } - async function createTrainedModel(modelId: string, job: TrainedModelJob | null) { - await _createTrainedModel(modelId, job); + async function createTrainedModel( + modelId: string, + job: TrainedModelJob | null, + addToAllSpaces = false + ) { + await _createTrainedModel(modelId, job, addToAllSpaces); } async function bulkCreateTrainedModel(models: TrainedModelObject[], namespaceFallback?: string) { @@ -486,7 +504,11 @@ export function mlSavedObjectServiceFactory( return models.saved_objects; } - async function _createTrainedModel(modelId: string, job: TrainedModelJob | null) { + async function _createTrainedModel( + modelId: string, + job: TrainedModelJob | null, + addToAllSpaces = false + ) { await isMlReady(); const modelObject: TrainedModelObject = { @@ -513,7 +535,7 @@ export function mlSavedObjectServiceFactory( // the saved object may exist if a previous job with the same ID has been deleted. // if not, this error will be throw which we ignore. } - let initialNamespaces; + let initialNamespaces = addToAllSpaces ? ['*'] : undefined; // if a job exists for this model, ensure the initial namespaces for the model // are the same as the job if (job !== null) { @@ -522,7 +544,9 @@ export function mlSavedObjectServiceFactory( job.job_id ); - initialNamespaces = existingJobObject?.namespaces ?? undefined; + if (existingJobObject?.namespaces !== undefined) { + initialNamespaces = existingJobObject?.namespaces; + } } await savedObjectsClient.create( diff --git a/x-pack/platform/plugins/shared/ml/server/saved_objects/sync.ts b/x-pack/platform/plugins/shared/ml/server/saved_objects/sync.ts index f96233debf9a1..edb4e7dfca6c9 100644 --- a/x-pack/platform/plugins/shared/ml/server/saved_objects/sync.ts +++ b/x-pack/platform/plugins/shared/ml/server/saved_objects/sync.ts @@ -34,7 +34,7 @@ export function syncSavedObjectsFactory( ) { const { checkStatus } = checksFactory(client, mlSavedObjectService); - async function syncSavedObjects(simulate: boolean = false) { + async function syncSavedObjects(simulate: boolean = false, addToAllSpaces = false) { const results: SyncSavedObjectResponse = { savedObjectsCreated: {}, savedObjectsDeleted: {}, @@ -71,7 +71,11 @@ export function syncSavedObjectsFactory( const datafeedId = job.datafeedId; tasks.push(async () => { try { - await mlSavedObjectService.createAnomalyDetectionJob(jobId, datafeedId ?? undefined); + await mlSavedObjectService.createAnomalyDetectionJob( + jobId, + datafeedId ?? undefined, + addToAllSpaces + ); results.savedObjectsCreated[type]![job.jobId] = { success: true }; } catch (error) { results.savedObjectsCreated[type]![job.jobId] = { @@ -97,7 +101,7 @@ export function syncSavedObjectsFactory( const jobId = job.jobId; tasks.push(async () => { try { - await mlSavedObjectService.createDataFrameAnalyticsJob(jobId); + await mlSavedObjectService.createDataFrameAnalyticsJob(jobId, addToAllSpaces); results.savedObjectsCreated[type]![job.jobId] = { success: true, }; @@ -136,7 +140,11 @@ export function syncSavedObjectsFactory( return; } const job = getJobDetailsFromTrainedModel(mod); - await mlSavedObjectService.createTrainedModel(modelId, job); + await mlSavedObjectService.createTrainedModel( + modelId, + job, + addToAllSpaces || modelId.startsWith('.') + ); if (modelId.startsWith('.')) { // if the model id starts with a dot, it is an internal model and should be in all spaces await mlSavedObjectService.updateTrainedModelsSpaces([modelId], ['*'], []); @@ -344,9 +352,9 @@ export function syncSavedObjectsFactory( const jobObjects: Array<{ job: JobObject; namespaces: string[] }> = []; const datafeeds: Array<{ jobId: string; datafeedId: string }> = []; - const types: JobType[] = ['anomaly-detector', 'data-frame-analytics']; + const jobTypes: JobType[] = ['anomaly-detector', 'data-frame-analytics']; - types.forEach((type) => { + jobTypes.forEach((type) => { status.jobs[type].forEach((job) => { if (job.checks.savedObjectExits === false) { if (simulate === true) { diff --git a/x-pack/test/api_integration/apis/ml/saved_objects/can_sync_to_all_spaces.ts b/x-pack/test/api_integration/apis/ml/saved_objects/can_sync_to_all_spaces.ts new file mode 100644 index 0000000000000..59b3c0d13f1e2 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/saved_objects/can_sync_to_all_spaces.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { getCommonRequestHeader } from '../../../../functional/services/ml/common_api'; + +export default ({ getService }: FtrProviderContext) => { + const ml = getService('ml'); + const spacesService = getService('spaces'); + const supertest = getService('supertestWithoutAuth'); + + const idSpace1 = 'space1'; + const idSpace2 = 'space2'; + + async function runRequest(user: USER, expectedStatusCode: number) { + const { body, status } = await supertest + .get(`/internal/ml/saved_objects/can_sync_to_all_spaces`) + .auth(user, ml.securityCommon.getPasswordForUser(user)) + .set(getCommonRequestHeader('1')); + ml.api.assertResponseStatusCode(expectedStatusCode, status, body); + + return body; + } + + describe('GET saved_objects/can_sync_to_all_spaces', () => { + beforeEach(async () => { + await spacesService.create({ id: idSpace1, name: 'space_one', disabledFeatures: [] }); + await spacesService.create({ id: idSpace2, name: 'space_two', disabledFeatures: [] }); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + afterEach(async () => { + await spacesService.delete(idSpace1); + await spacesService.delete(idSpace2); + }); + + it('user can sync to all spaces', async () => { + const body = await runRequest(USER.ML_POWERUSER, 200); + expect(body).to.eql({ canSync: true }); + }); + it('user can not sync to all spaces', async () => { + const body = await runRequest(USER.ML_POWERUSER_SPACE1, 200); + expect(body).to.eql({ canSync: false }); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/saved_objects/index.ts b/x-pack/test/api_integration/apis/ml/saved_objects/index.ts index b4b8e1d0653f3..de56161d5aef3 100644 --- a/x-pack/test/api_integration/apis/ml/saved_objects/index.ts +++ b/x-pack/test/api_integration/apis/ml/saved_objects/index.ts @@ -21,5 +21,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./update_jobs_spaces')); loadTestFile(require.resolve('./update_trained_model_spaces')); loadTestFile(require.resolve('./remove_from_current_space')); + loadTestFile(require.resolve('./can_sync_to_all_spaces')); }); } diff --git a/x-pack/test/api_integration/apis/ml/saved_objects/sync_trained_models.ts b/x-pack/test/api_integration/apis/ml/saved_objects/sync_trained_models.ts index 33c5da4d2e01c..7b5956de227b6 100644 --- a/x-pack/test/api_integration/apis/ml/saved_objects/sync_trained_models.ts +++ b/x-pack/test/api_integration/apis/ml/saved_objects/sync_trained_models.ts @@ -187,7 +187,7 @@ export default ({ getService }: FtrProviderContext) => { const model1 = getTestModel(modelIdSpace1, 'classification', dfaJobId1); await ml.api.createTrainedModelES(model1.model_id, model1.body); - // create trained model not linked to job, it should have the current space + // create trained model not linked to job, it should have * space after sync const model2 = getTestModel(modelIdSpace2, 'classification'); await ml.api.createTrainedModelES(model2.model_id, model2.body); @@ -199,7 +199,7 @@ export default ({ getService }: FtrProviderContext) => { await runSyncRequest(USER.ML_POWERUSER_ALL_SPACES, 200); await ml.api.assertTrainedModelSpaces(modelIdSpace1, [idSpace1, idSpace2]); - await ml.api.assertTrainedModelSpaces(modelIdSpace2, [idSpace1]); + await ml.api.assertTrainedModelSpaces(modelIdSpace2, ['*']); }); }); }; diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/synchronize.ts b/x-pack/test/functional/apps/ml/stack_management_jobs/synchronize.ts index 42a462d259812..e8c255212251b 100644 --- a/x-pack/test/functional/apps/ml/stack_management_jobs/synchronize.ts +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/synchronize.ts @@ -63,6 +63,12 @@ export default function ({ getService }: FtrProviderContext) { await ml.stackManagementJobs.assertSyncFlyoutSyncButtonEnabled(false); }); + it('should not have objects to sync', async () => { + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToAnomalyDetection(); + await ml.overviewPage.assertJobSyncRequiredWarningNotExists(); + }); + it('should prepare test data', async () => { // create jobs @@ -102,8 +108,8 @@ export default function ({ getService }: FtrProviderContext) { }); it('should have objects to sync', async () => { - // sync required warning is displayed - await ml.navigation.navigateToMl(); + await ml.jobTable.refreshJobList(); + await ml.overviewPage.assertJobSyncRequiredWarningExists(); // object counts in sync flyout are all 1, sync button is enabled