diff --git a/x-pack/plugins/alerting/server/maintenance_window_client/methods/get_active_maintenance_windows.ts b/x-pack/plugins/alerting/server/maintenance_window_client/methods/get_active_maintenance_windows.ts index 5b9478a4276451..004e68bbc5c7d6 100644 --- a/x-pack/plugins/alerting/server/maintenance_window_client/methods/get_active_maintenance_windows.ts +++ b/x-pack/plugins/alerting/server/maintenance_window_client/methods/get_active_maintenance_windows.ts @@ -29,7 +29,7 @@ export interface MaintenanceWindowAggregationResult { export interface ActiveParams { start?: string; - interval: string; + interval?: string; } export async function getActiveMaintenanceWindows( @@ -40,7 +40,7 @@ export async function getActiveMaintenanceWindows( const { start, interval } = params; const startDate = start ? new Date(start) : new Date(); - const duration = parseDuration(interval); + const duration = interval ? parseDuration(interval) : 0; const endDate = moment.utc(startDate).add(duration, 'ms').toDate(); const startDateISO = startDate.toISOString(); diff --git a/x-pack/plugins/alerting/server/routes/index.ts b/x-pack/plugins/alerting/server/routes/index.ts index 9a0b8be33db42e..4c13090f80bd32 100644 --- a/x-pack/plugins/alerting/server/routes/index.ts +++ b/x-pack/plugins/alerting/server/routes/index.ts @@ -53,6 +53,7 @@ import { deleteMaintenanceWindowRoute } from './maintenance_window/delete_mainte import { findMaintenanceWindowsRoute } from './maintenance_window/find_maintenance_windows'; import { archiveMaintenanceWindowRoute } from './maintenance_window/archive_maintenance_window'; import { finishMaintenanceWindowRoute } from './maintenance_window/finish_maintenance_window'; +import { activeMaintenanceWindowsRoute } from './maintenance_window/active_maintenance_windows'; export interface RouteOptions { router: IRouter; @@ -108,4 +109,5 @@ export function defineRoutes(opts: RouteOptions) { findMaintenanceWindowsRoute(router, licenseState); archiveMaintenanceWindowRoute(router, licenseState); finishMaintenanceWindowRoute(router, licenseState); + activeMaintenanceWindowsRoute(router, licenseState); } diff --git a/x-pack/plugins/alerting/server/routes/maintenance_window/active_maintenance_windows.test.ts b/x-pack/plugins/alerting/server/routes/maintenance_window/active_maintenance_windows.test.ts new file mode 100644 index 00000000000000..70ebcde490a65f --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/maintenance_window/active_maintenance_windows.test.ts @@ -0,0 +1,97 @@ +/* + * 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 { httpServiceMock } from '@kbn/core/server/mocks'; +import { licenseStateMock } from '../../lib/license_state.mock'; +import { verifyApiAccess } from '../../lib/license_api_access'; +import { mockHandlerArguments } from '../_mock_handler_arguments'; +import { maintenanceWindowClientMock } from '../../maintenance_window_client.mock'; +import { activeMaintenanceWindowsRoute } from './active_maintenance_windows'; +import { getMockMaintenanceWindow } from '../../maintenance_window_client/methods/test_helpers'; +import { MaintenanceWindowStatus } from '../../../common'; +import { rewriteMaintenanceWindowRes } from '../lib'; + +const maintenanceWindowClient = maintenanceWindowClientMock.create(); + +jest.mock('../../lib/license_api_access', () => ({ + verifyApiAccess: jest.fn(), +})); + +const mockMaintenanceWindows = [ + { + ...getMockMaintenanceWindow(), + eventStartTime: new Date().toISOString(), + eventEndTime: new Date().toISOString(), + status: MaintenanceWindowStatus.Running, + id: 'test-id1', + }, + { + ...getMockMaintenanceWindow(), + eventStartTime: new Date().toISOString(), + eventEndTime: new Date().toISOString(), + status: MaintenanceWindowStatus.Running, + id: 'test-id2', + }, +]; + +describe('activeMaintenanceWindowsRoute', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + test('should get the currently active maintenance windows', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + activeMaintenanceWindowsRoute(router, licenseState); + + maintenanceWindowClient.getActiveMaintenanceWindows.mockResolvedValueOnce( + mockMaintenanceWindows + ); + const [config, handler] = router.get.mock.calls[0]; + const [context, req, res] = mockHandlerArguments({ maintenanceWindowClient }, { body: {} }); + + expect(config.path).toEqual('/internal/alerting/rules/maintenance_window/_active'); + expect(config.options?.tags?.[0]).toEqual('access:read-maintenance-window'); + + await handler(context, req, res); + + expect(maintenanceWindowClient.getActiveMaintenanceWindows).toHaveBeenCalled(); + expect(res.ok).toHaveBeenLastCalledWith({ + body: mockMaintenanceWindows.map((data) => rewriteMaintenanceWindowRes(data)), + }); + }); + + test('ensures the license allows for getting active maintenance windows', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + activeMaintenanceWindowsRoute(router, licenseState); + + maintenanceWindowClient.getActiveMaintenanceWindows.mockResolvedValueOnce( + mockMaintenanceWindows + ); + const [, handler] = router.get.mock.calls[0]; + const [context, req, res] = mockHandlerArguments({ maintenanceWindowClient }, { body: {} }); + await handler(context, req, res); + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); + + test('ensures the license check prevents for getting active maintenance windows', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + activeMaintenanceWindowsRoute(router, licenseState); + + (verifyApiAccess as jest.Mock).mockImplementation(() => { + throw new Error('Failure'); + }); + const [, handler] = router.get.mock.calls[0]; + const [context, req, res] = mockHandlerArguments({ maintenanceWindowClient }, { body: {} }); + expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: Failure]`); + }); +}); diff --git a/x-pack/plugins/alerting/server/routes/maintenance_window/active_maintenance_windows.ts b/x-pack/plugins/alerting/server/routes/maintenance_window/active_maintenance_windows.ts new file mode 100644 index 00000000000000..706630edfbd4ad --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/maintenance_window/active_maintenance_windows.ts @@ -0,0 +1,37 @@ +/* + * 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 { IRouter } from '@kbn/core/server'; +import { ILicenseState } from '../../lib'; +import { verifyAccessAndContext, rewriteMaintenanceWindowRes } from '../lib'; +import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../types'; +import { MAINTENANCE_WINDOW_API_PRIVILEGES } from '../../../common'; + +export const activeMaintenanceWindowsRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.get( + { + path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/_active`, + validate: {}, + options: { + tags: [`access:${MAINTENANCE_WINDOW_API_PRIVILEGES.READ_MAINTENANCE_WINDOW}`], + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const maintenanceWindowClient = (await context.alerting).getMaintenanceWindowClient(); + const result = await maintenanceWindowClient.getActiveMaintenanceWindows({}); + + return res.ok({ + body: result.map((maintenanceWindow) => rewriteMaintenanceWindowRes(maintenanceWindow)), + }); + }) + ) + ); +}; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/maintenance_window/active_maintenance_windows.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/maintenance_window/active_maintenance_windows.ts new file mode 100644 index 00000000000000..abd837485c7ecd --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/maintenance_window/active_maintenance_windows.ts @@ -0,0 +1,153 @@ +/* + * 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 moment from 'moment'; +import expect from '@kbn/expect'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { getUrlPrefix, ObjectRemover } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function activeMaintenanceWindowTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('activeMaintenanceWindow', () => { + const objectRemover = new ObjectRemover(supertest); + const createParams = { + title: 'test-maintenance-window', + duration: 60 * 60 * 1000, // 1 hr + r_rule: { + dtstart: new Date().toISOString(), + tzid: 'UTC', + freq: 2, // weekly + }, + }; + after(() => objectRemover.removeAll()); + + for (const scenario of UserAtSpaceScenarios) { + const { user, space } = scenario; + describe(scenario.id, () => { + afterEach(() => objectRemover.removeAll()); + it('should handle update maintenance window request appropriately', async () => { + // Create 2 active and 1 inactive maintenance window + const { body: createdMaintenanceWindow1 } = await supertest + .post(`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window`) + .set('kbn-xsrf', 'foo') + .send(createParams); + + const { body: createdMaintenanceWindow2 } = await supertest + .post(`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window`) + .set('kbn-xsrf', 'foo') + .send(createParams); + + const { body: createdMaintenanceWindow3 } = await supertest + .post(`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window`) + .set('kbn-xsrf', 'foo') + .send({ + ...createParams, + r_rule: { + ...createParams.r_rule, + dtstart: moment.utc().add(1, 'day').toISOString(), + }, + }); + + objectRemover.add( + space.id, + createdMaintenanceWindow1.id, + 'rules/maintenance_window', + 'alerting', + true + ); + objectRemover.add( + space.id, + createdMaintenanceWindow2.id, + 'rules/maintenance_window', + 'alerting', + true + ); + objectRemover.add( + space.id, + createdMaintenanceWindow3.id, + 'rules/maintenance_window', + 'alerting', + true + ); + + const response = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window/_active`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .send({}); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'space_1_all_with_restricted_fixture at space1': + case 'space_1_all_alerts_none_actions at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: 'Forbidden', + statusCode: 403, + }); + break; + case 'global_read at space1': + case 'superuser at space1': + case 'space_1_all at space1': + expect(response.body.length).to.eql(2); + expect(response.statusCode).to.eql(200); + expect(response.body[0].title).to.eql('test-maintenance-window'); + expect(response.body[0].duration).to.eql(3600000); + expect(response.body[0].r_rule.dtstart).to.eql(createParams.r_rule.dtstart); + expect(response.body[0].events.length).to.be.greaterThan(0); + expect(response.body[0].status).to.eql('running'); + + const ids = response.body.map( + (maintenanceWindow: { id: string }) => maintenanceWindow.id + ); + expect(ids.sort()).to.eql( + [createdMaintenanceWindow1.id, createdMaintenanceWindow2.id].sort() + ); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + }); + } + + it('should return an empty array if there are no active maintenance windows', async () => { + const { body: createdMaintenanceWindow } = await supertest + .post(`${getUrlPrefix('space1')}/internal/alerting/rules/maintenance_window`) + .set('kbn-xsrf', 'foo') + .send({ + ...createParams, + r_rule: { + ...createParams.r_rule, + dtstart: moment.utc().add(1, 'day').toISOString(), + }, + }); + + objectRemover.add( + 'space1', + createdMaintenanceWindow.id, + 'rules/maintenance_window', + 'alerting', + true + ); + + const response = await supertest + .get(`${getUrlPrefix('space1')}/internal/alerting/rules/maintenance_window/_active`) + .set('kbn-xsrf', 'foo') + .send({}) + .expect(200); + + expect(response.body).to.eql([]); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/maintenance_window/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/maintenance_window/index.ts index 70a90a4f63c7c0..a848a14d3e1b74 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/maintenance_window/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/maintenance_window/index.ts @@ -27,6 +27,7 @@ export default function maintenanceWindowTests({ loadTestFile, getService }: Ftr loadTestFile(require.resolve('./archive_maintenance_window')); loadTestFile(require.resolve('./finish_maintenance_window')); loadTestFile(require.resolve('./find_maintenance_windows')); + loadTestFile(require.resolve('./active_maintenance_windows')); }); }); }