From 0ae74553773799bbb03219439a698578fa1dce36 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Thu, 2 Sep 2021 14:43:14 +0200 Subject: [PATCH] [ML] Add job audit messages API integration tests (#110793) --- x-pack/plugins/ml/.gitignore | 1 + .../ml/server/routes/job_audit_messages.ts | 2 +- x-pack/test/api_integration/apis/ml/index.ts | 1 + .../ml/job_audit_messages/clear_messages.ts | 122 ++++++++++++++++++ .../get_job_audit_messages.ts | 109 ++++++++++++++++ .../apis/ml/job_audit_messages/index.ts | 43 ++++++ 6 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 x-pack/test/api_integration/apis/ml/job_audit_messages/clear_messages.ts create mode 100644 x-pack/test/api_integration/apis/ml/job_audit_messages/get_job_audit_messages.ts create mode 100644 x-pack/test/api_integration/apis/ml/job_audit_messages/index.ts diff --git a/x-pack/plugins/ml/.gitignore b/x-pack/plugins/ml/.gitignore index 708c5b199467b9..e0f20bbc48bda9 100644 --- a/x-pack/plugins/ml/.gitignore +++ b/x-pack/plugins/ml/.gitignore @@ -1 +1,2 @@ routes_doc +server/routes/apidoc_scripts/header.md diff --git a/x-pack/plugins/ml/server/routes/job_audit_messages.ts b/x-pack/plugins/ml/server/routes/job_audit_messages.ts index 4dcaca573fc177..cdef5a9c20daeb 100644 --- a/x-pack/plugins/ml/server/routes/job_audit_messages.ts +++ b/x-pack/plugins/ml/server/routes/job_audit_messages.ts @@ -101,7 +101,7 @@ export function jobAuditMessagesRoutes({ router, routeGuard }: RouteInitializati /** * @apiGroup JobAuditMessages * - * @api {put} /api/ml/job_audit_messages/clear_messages/{jobId} Index annotation + * @api {put} /api/ml/job_audit_messages/clear_messages Index annotation * @apiName ClearJobAuditMessages * @apiDescription Clear the job audit messages. * diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index 394672ac07fc52..e44d0cd10e9f27 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -77,6 +77,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./filters')); loadTestFile(require.resolve('./indices')); loadTestFile(require.resolve('./job_validation')); + loadTestFile(require.resolve('./job_audit_messages')); loadTestFile(require.resolve('./jobs')); loadTestFile(require.resolve('./modules')); loadTestFile(require.resolve('./results')); diff --git a/x-pack/test/api_integration/apis/ml/job_audit_messages/clear_messages.ts b/x-pack/test/api_integration/apis/ml/job_audit_messages/clear_messages.ts new file mode 100644 index 00000000000000..d085f360859eca --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/job_audit_messages/clear_messages.ts @@ -0,0 +1,122 @@ +/* + * 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 { omit } from 'lodash'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { getJobConfig } from './index'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + let notificationIndices: string[] = []; + + describe('clear_messages', function () { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + for (const jobConfig of getJobConfig(2)) { + await ml.api.createAnomalyDetectionJob(jobConfig); + } + + const { body } = await supertest + .get(`/api/ml/job_audit_messages/messages`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + notificationIndices = body.notificationIndices; + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('should mark audit messages as cleared for provided job', async () => { + const timestamp = Date.now(); + + const { body } = await supertest + .put(`/api/ml/job_audit_messages/clear_messages`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send({ + jobId: 'test_get_job_audit_messages_1', + notificationIndices, + }) + .expect(200); + + expect(body.success).to.eql(true); + expect(body.last_cleared).to.be.above(timestamp); + + const { body: getBody } = await supertest + .get(`/api/ml/job_audit_messages/messages/test_get_job_audit_messages_1`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(getBody.messages.length).to.eql(1); + + expect(omit(getBody.messages[0], 'timestamp')).to.eql({ + job_id: 'test_get_job_audit_messages_1', + message: 'Job created', + level: 'info', + node_name: 'node-01', + job_type: 'anomaly_detector', + cleared: true, + }); + }); + + it('should not mark audit messages as cleared for the user with ML read permissions', async () => { + const { body } = await supertest + .put(`/api/ml/job_audit_messages/clear_messages`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .send({ + jobId: 'test_get_job_audit_messages_2', + notificationIndices, + }) + .expect(403); + expect(body.error).to.eql('Forbidden'); + expect(body.message).to.eql('Forbidden'); + + const { body: getBody } = await supertest + .get(`/api/ml/job_audit_messages/messages/test_get_job_audit_messages_2`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(getBody.messages[0].cleared).to.not.eql(true); + }); + + it('should not mark audit messages as cleared for unauthorized user', async () => { + const { body } = await supertest + .put(`/api/ml/job_audit_messages/clear_messages`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .send({ + jobId: 'test_get_job_audit_messages_2', + notificationIndices, + }) + .expect(403); + expect(body.error).to.eql('Forbidden'); + expect(body.message).to.eql('Forbidden'); + + const { body: getBody } = await supertest + .get(`/api/ml/job_audit_messages/messages/test_get_job_audit_messages_2`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(getBody.messages[0].cleared).to.not.eql(true); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/job_audit_messages/get_job_audit_messages.ts b/x-pack/test/api_integration/apis/ml/job_audit_messages/get_job_audit_messages.ts new file mode 100644 index 00000000000000..2211103b2d4047 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/job_audit_messages/get_job_audit_messages.ts @@ -0,0 +1,109 @@ +/* + * 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 { omit, keyBy } from 'lodash'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { getJobConfig } from './index'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + describe('get_job_audit_messages', function () { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + for (const jobConfig of getJobConfig(2)) { + await ml.api.createAnomalyDetectionJob(jobConfig); + } + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('should fetch all audit messages', async () => { + const { body } = await supertest + .get(`/api/ml/job_audit_messages/messages`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body.messages.length).to.eql(2); + + const messagesDict = keyBy(body.messages, 'job_id'); + + expect(omit(messagesDict.test_get_job_audit_messages_2, 'timestamp')).to.eql({ + job_id: 'test_get_job_audit_messages_2', + message: 'Job created', + level: 'info', + node_name: 'node-01', + job_type: 'anomaly_detector', + }); + expect(omit(messagesDict.test_get_job_audit_messages_1, 'timestamp')).to.eql({ + job_id: 'test_get_job_audit_messages_1', + message: 'Job created', + level: 'info', + node_name: 'node-01', + job_type: 'anomaly_detector', + }); + expect(body.notificationIndices).to.eql(['.ml-notifications-000002']); + }); + + it('should fetch audit messages for specified job', async () => { + const { body } = await supertest + .get(`/api/ml/job_audit_messages/messages/test_get_job_audit_messages_1`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body.messages.length).to.eql(1); + expect(omit(body.messages[0], 'timestamp')).to.eql({ + job_id: 'test_get_job_audit_messages_1', + message: 'Job created', + level: 'info', + node_name: 'node-01', + job_type: 'anomaly_detector', + }); + expect(body.notificationIndices).to.eql(['.ml-notifications-000002']); + }); + + it('should fetch audit messages for user with ML read permissions', async () => { + const { body } = await supertest + .get(`/api/ml/job_audit_messages/messages/test_get_job_audit_messages_1`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body.messages.length).to.eql(1); + expect(omit(body.messages[0], 'timestamp')).to.eql({ + job_id: 'test_get_job_audit_messages_1', + message: 'Job created', + level: 'info', + node_name: 'node-01', + job_type: 'anomaly_detector', + }); + expect(body.notificationIndices).to.eql(['.ml-notifications-000002']); + }); + + it('should not allow to fetch audit messages for unauthorized user', async () => { + const { body } = await supertest + .get(`/api/ml/job_audit_messages/messages/test_get_job_audit_messages_1`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .expect(403); + + expect(body.error).to.eql('Forbidden'); + expect(body.message).to.eql('Forbidden'); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/job_audit_messages/index.ts b/x-pack/test/api_integration/apis/ml/job_audit_messages/index.ts new file mode 100644 index 00000000000000..4779a3a181e3bf --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/job_audit_messages/index.ts @@ -0,0 +1,43 @@ +/* + * 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 { MlJob } from '@elastic/elasticsearch/api/types'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('job_audit_messages', function () { + loadTestFile(require.resolve('./get_job_audit_messages')); + loadTestFile(require.resolve('./clear_messages')); + }); +} + +export const getJobConfig = (numOfJobs: number) => { + return new Array(numOfJobs).fill(null).map( + (v, i) => + (({ + job_id: `test_get_job_audit_messages_${i + 1}`, + description: 'job_audit_messages', + groups: ['farequote', 'automated', 'single-metric'], + analysis_config: { + bucket_span: '15m', + influencers: [], + detectors: [ + { + function: 'mean', + field_name: 'responsetime', + }, + { + function: 'min', + field_name: 'responsetime', + }, + ], + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '10mb' }, + } as unknown) as MlJob) + ); +};