From a467450a476cf7d76e6ad2aa84f2d89199019a12 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Fri, 18 Mar 2022 13:22:27 -0500 Subject: [PATCH] [RAM] Add _snooze API (#127081) * [RAM] Add _snooze API * Switch empty snoozeEndTime to -1 * Convert API to internal * Ensure snoozeEndTime is in the future * Update x-pack/plugins/alerting/server/routes/snooze_rule.ts Co-authored-by: Gidi Meir Morris * Add integration tests for snooze API * Fix tests Co-authored-by: Gidi Meir Morris --- x-pack/plugins/alerting/common/alert.ts | 1 + .../authorization/alerting_authorization.ts | 1 + .../alerting/server/lib/errors/index.ts | 1 + .../alerting/server/lib/errors/rule_muted.ts | 19 + x-pack/plugins/alerting/server/lib/index.ts | 2 +- .../server/lib/validate_snooze_date.ts | 13 + .../alerting/server/routes/find_rules.ts | 3 + .../alerting/server/routes/get_rule.ts | 3 + .../plugins/alerting/server/routes/index.ts | 2 + .../server/routes/snooze_rule.test.ts | 134 ++++++ .../alerting/server/routes/snooze_rule.ts | 62 +++ .../alerting/server/rules_client.mock.ts | 1 + .../server/rules_client/audit_events.ts | 3 + .../server/rules_client/rules_client.ts | 88 ++++ .../server/rules_client/tests/create.test.ts | 18 + .../alerting/server/saved_objects/index.ts | 4 +- x-pack/plugins/alerting/server/types.ts | 2 +- .../common/lib/alert_utils.ts | 11 + .../tests/alerting/find.ts | 8 +- .../security_and_spaces/tests/alerting/get.ts | 7 +- .../tests/alerting/index.ts | 1 + .../tests/alerting/snooze.ts | 403 ++++++++++++++++++ .../spaces_only/tests/alerting/find.ts | 4 +- .../spaces_only/tests/alerting/get.ts | 4 +- .../spaces_only/tests/alerting/snooze.ts | 135 ++++++ 25 files changed, 922 insertions(+), 8 deletions(-) create mode 100644 x-pack/plugins/alerting/server/lib/errors/rule_muted.ts create mode 100644 x-pack/plugins/alerting/server/lib/validate_snooze_date.ts create mode 100644 x-pack/plugins/alerting/server/routes/snooze_rule.test.ts create mode 100644 x-pack/plugins/alerting/server/routes/snooze_rule.ts create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/snooze.ts create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/snooze.ts diff --git a/x-pack/plugins/alerting/common/alert.ts b/x-pack/plugins/alerting/common/alert.ts index da916ee7ed98a..7ca88c83af9d4 100644 --- a/x-pack/plugins/alerting/common/alert.ts +++ b/x-pack/plugins/alerting/common/alert.ts @@ -94,6 +94,7 @@ export interface Alert { mutedInstanceIds: string[]; executionStatus: AlertExecutionStatus; monitoring?: RuleMonitoring; + snoozeEndTime?: Date | null; // Remove ? when this parameter is made available in the public API } export type SanitizedAlert = Omit, 'apiKey'>; diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts index aafeb5003eaab..c275053874efa 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts @@ -44,6 +44,7 @@ export enum WriteOperations { UnmuteAll = 'unmuteAll', MuteAlert = 'muteAlert', UnmuteAlert = 'unmuteAlert', + Snooze = 'snooze', } export interface EnsureAuthorizedOpts { diff --git a/x-pack/plugins/alerting/server/lib/errors/index.ts b/x-pack/plugins/alerting/server/lib/errors/index.ts index 36ca30bc95ba8..7ac8d9fced5cd 100644 --- a/x-pack/plugins/alerting/server/lib/errors/index.ts +++ b/x-pack/plugins/alerting/server/lib/errors/index.ts @@ -18,3 +18,4 @@ export type { ErrorThatHandlesItsOwnResponse, ElasticsearchError }; export { getEsErrorMessage }; export type { AlertTypeDisabledReason } from './alert_type_disabled'; export { AlertTypeDisabledError } from './alert_type_disabled'; +export { RuleMutedError } from './rule_muted'; diff --git a/x-pack/plugins/alerting/server/lib/errors/rule_muted.ts b/x-pack/plugins/alerting/server/lib/errors/rule_muted.ts new file mode 100644 index 0000000000000..d7c1325b8464d --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/errors/rule_muted.ts @@ -0,0 +1,19 @@ +/* + * 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 { KibanaResponseFactory } from '../../../../../../src/core/server'; +import { ErrorThatHandlesItsOwnResponse } from './types'; + +export class RuleMutedError extends Error implements ErrorThatHandlesItsOwnResponse { + constructor(message: string) { + super(message); + } + + public sendResponse(res: KibanaResponseFactory) { + return res.badRequest({ body: { message: this.message } }); + } +} diff --git a/x-pack/plugins/alerting/server/lib/index.ts b/x-pack/plugins/alerting/server/lib/index.ts index d9692bac86c16..22dbeff82b2d1 100644 --- a/x-pack/plugins/alerting/server/lib/index.ts +++ b/x-pack/plugins/alerting/server/lib/index.ts @@ -17,7 +17,7 @@ export type { ErrorThatHandlesItsOwnResponse, ElasticsearchError, } from './errors'; -export { AlertTypeDisabledError, isErrorThatHandlesItsOwnResponse } from './errors'; +export { AlertTypeDisabledError, RuleMutedError, isErrorThatHandlesItsOwnResponse } from './errors'; export { executionStatusFromState, executionStatusFromError, diff --git a/x-pack/plugins/alerting/server/lib/validate_snooze_date.ts b/x-pack/plugins/alerting/server/lib/validate_snooze_date.ts new file mode 100644 index 0000000000000..deb951a717a06 --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/validate_snooze_date.ts @@ -0,0 +1,13 @@ +/* + * 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. + */ + +export const validateSnoozeDate = (date: string) => { + const parsedValue = Date.parse(date); + if (isNaN(parsedValue)) return `Invalid date: ${date}`; + if (parsedValue <= Date.now()) return `Invalid snooze date as it is in the past: ${date}`; + return; +}; diff --git a/x-pack/plugins/alerting/server/routes/find_rules.ts b/x-pack/plugins/alerting/server/routes/find_rules.ts index f8898327705ed..1202d56edb21c 100644 --- a/x-pack/plugins/alerting/server/routes/find_rules.ts +++ b/x-pack/plugins/alerting/server/routes/find_rules.ts @@ -84,6 +84,7 @@ const rewriteBodyRes: RewriteResponseCase> = ({ executionStatus, actions, scheduledTaskId, + snoozeEndTime, ...rest }) => ({ ...rest, @@ -97,6 +98,8 @@ const rewriteBodyRes: RewriteResponseCase> = ({ mute_all: muteAll, muted_alert_ids: mutedInstanceIds, scheduled_task_id: scheduledTaskId, + // Remove this object spread boolean check after snoozeEndTime is added to the public API + ...(snoozeEndTime !== undefined ? { snooze_end_time: snoozeEndTime } : {}), execution_status: executionStatus && { ...omit(executionStatus, 'lastExecutionDate', 'lastDuration'), last_execution_date: executionStatus.lastExecutionDate, diff --git a/x-pack/plugins/alerting/server/routes/get_rule.ts b/x-pack/plugins/alerting/server/routes/get_rule.ts index 4c704ce9484e7..8e0e89c379fcc 100644 --- a/x-pack/plugins/alerting/server/routes/get_rule.ts +++ b/x-pack/plugins/alerting/server/routes/get_rule.ts @@ -35,6 +35,7 @@ const rewriteBodyRes: RewriteResponseCase> = ({ executionStatus, actions, scheduledTaskId, + snoozeEndTime, ...rest }) => ({ ...rest, @@ -47,6 +48,8 @@ const rewriteBodyRes: RewriteResponseCase> = ({ notify_when: notifyWhen, mute_all: muteAll, muted_alert_ids: mutedInstanceIds, + // Remove this object spread boolean check after snoozeEndTime is added to the public API + ...(snoozeEndTime !== undefined ? { snooze_end_time: snoozeEndTime } : {}), scheduled_task_id: scheduledTaskId, execution_status: executionStatus && { ...omit(executionStatus, 'lastExecutionDate', 'lastDuration'), diff --git a/x-pack/plugins/alerting/server/routes/index.ts b/x-pack/plugins/alerting/server/routes/index.ts index f43190ec6d1c2..1cb58fd6d0657 100644 --- a/x-pack/plugins/alerting/server/routes/index.ts +++ b/x-pack/plugins/alerting/server/routes/index.ts @@ -29,6 +29,7 @@ import { muteAlertRoute } from './mute_alert'; import { unmuteAllRuleRoute } from './unmute_all_rule'; import { unmuteAlertRoute } from './unmute_alert'; import { updateRuleApiKeyRoute } from './update_rule_api_key'; +import { snoozeRuleRoute } from './snooze_rule'; export interface RouteOptions { router: IRouter; @@ -61,4 +62,5 @@ export function defineRoutes(opts: RouteOptions) { unmuteAllRuleRoute(router, licenseState); unmuteAlertRoute(router, licenseState); updateRuleApiKeyRoute(router, licenseState); + snoozeRuleRoute(router, licenseState); } diff --git a/x-pack/plugins/alerting/server/routes/snooze_rule.test.ts b/x-pack/plugins/alerting/server/routes/snooze_rule.test.ts new file mode 100644 index 0000000000000..567ff3a5653d6 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/snooze_rule.test.ts @@ -0,0 +1,134 @@ +/* + * 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 { snoozeRuleRoute } from './snooze_rule'; +import { httpServiceMock } from 'src/core/server/mocks'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { rulesClientMock } from '../rules_client.mock'; +import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled'; + +const rulesClient = rulesClientMock.create(); +jest.mock('../lib/license_api_access.ts', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +const SNOOZE_END_TIME = '2025-03-07T00:00:00.000Z'; + +describe('snoozeAlertRoute', () => { + beforeAll(() => { + jest.useFakeTimers('modern'); + jest.setSystemTime(new Date(2020, 3, 1)); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + it('snoozes an alert', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + snoozeRuleRoute(router, licenseState); + + const [config, handler] = router.post.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/internal/alerting/rule/{id}/_snooze"`); + + rulesClient.snooze.mockResolvedValueOnce(); + + const [context, req, res] = mockHandlerArguments( + { rulesClient }, + { + params: { + id: '1', + }, + body: { + snooze_end_time: SNOOZE_END_TIME, + }, + }, + ['noContent'] + ); + + expect(await handler(context, req, res)).toEqual(undefined); + + expect(rulesClient.snooze).toHaveBeenCalledTimes(1); + expect(rulesClient.snooze.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "snoozeEndTime": "${SNOOZE_END_TIME}", + }, + ] + `); + + expect(res.noContent).toHaveBeenCalled(); + }); + + it('also snoozes an alert when passed snoozeEndTime of -1', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + snoozeRuleRoute(router, licenseState); + + const [config, handler] = router.post.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/internal/alerting/rule/{id}/_snooze"`); + + rulesClient.snooze.mockResolvedValueOnce(); + + const [context, req, res] = mockHandlerArguments( + { rulesClient }, + { + params: { + id: '1', + }, + body: { + snooze_end_time: -1, + }, + }, + ['noContent'] + ); + + expect(await handler(context, req, res)).toEqual(undefined); + + expect(rulesClient.snooze).toHaveBeenCalledTimes(1); + expect(rulesClient.snooze.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "snoozeEndTime": -1, + }, + ] + `); + + expect(res.noContent).toHaveBeenCalled(); + }); + + it('ensures the rule type gets validated for the license', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + snoozeRuleRoute(router, licenseState); + + const [, handler] = router.post.mock.calls[0]; + + rulesClient.snooze.mockRejectedValue(new AlertTypeDisabledError('Fail', 'license_invalid')); + + const [context, req, res] = mockHandlerArguments({ rulesClient }, { params: {}, body: {} }, [ + 'ok', + 'forbidden', + ]); + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); + }); +}); diff --git a/x-pack/plugins/alerting/server/routes/snooze_rule.ts b/x-pack/plugins/alerting/server/routes/snooze_rule.ts new file mode 100644 index 0000000000000..3b2167cc79fa3 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/snooze_rule.ts @@ -0,0 +1,62 @@ +/* + * 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 'kibana/server'; +import { schema } from '@kbn/config-schema'; +import { ILicenseState, RuleMutedError } from '../lib'; +import { verifyAccessAndContext, RewriteRequestCase } from './lib'; +import { SnoozeOptions } from '../rules_client'; +import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../types'; +import { validateSnoozeDate } from '../lib/validate_snooze_date'; + +const paramSchema = schema.object({ + id: schema.string(), +}); + +const bodySchema = schema.object({ + snooze_end_time: schema.oneOf([ + schema.string({ + validate: validateSnoozeDate, + }), + schema.literal(-1), + ]), +}); + +const rewriteBodyReq: RewriteRequestCase = ({ snooze_end_time: snoozeEndTime }) => ({ + snoozeEndTime, +}); + +export const snoozeRuleRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.post( + { + path: `${INTERNAL_BASE_ALERTING_API_PATH}/rule/{id}/_snooze`, + validate: { + params: paramSchema, + body: bodySchema, + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const rulesClient = context.alerting.getRulesClient(); + const params = req.params; + const body = rewriteBodyReq(req.body); + try { + await rulesClient.snooze({ ...params, ...body }); + return res.noContent(); + } catch (e) { + if (e instanceof RuleMutedError) { + return e.sendResponse(res); + } + throw e; + } + }) + ) + ); +}; diff --git a/x-pack/plugins/alerting/server/rules_client.mock.ts b/x-pack/plugins/alerting/server/rules_client.mock.ts index 2395e7f041846..2a7fb7177ce4c 100644 --- a/x-pack/plugins/alerting/server/rules_client.mock.ts +++ b/x-pack/plugins/alerting/server/rules_client.mock.ts @@ -31,6 +31,7 @@ const createRulesClientMock = () => { listAlertTypes: jest.fn(), getAlertSummary: jest.fn(), getSpaceId: jest.fn(), + snooze: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/alerting/server/rules_client/audit_events.ts b/x-pack/plugins/alerting/server/rules_client/audit_events.ts index 487de19691503..96d6b9a5d17ef 100644 --- a/x-pack/plugins/alerting/server/rules_client/audit_events.ts +++ b/x-pack/plugins/alerting/server/rules_client/audit_events.ts @@ -23,6 +23,7 @@ export enum RuleAuditAction { MUTE_ALERT = 'rule_alert_mute', UNMUTE_ALERT = 'rule_alert_unmute', AGGREGATE = 'rule_aggregate', + SNOOZE = 'rule_snooze', } type VerbsTuple = [string, string, string]; @@ -42,6 +43,7 @@ const eventVerbs: Record = { rule_alert_mute: ['mute alert of', 'muting alert of', 'muted alert of'], rule_alert_unmute: ['unmute alert of', 'unmuting alert of', 'unmuted alert of'], rule_aggregate: ['access', 'accessing', 'accessed'], + rule_snooze: ['snooze', 'snoozing', 'snoozed'], }; const eventTypes: Record = { @@ -59,6 +61,7 @@ const eventTypes: Record = { rule_alert_mute: 'change', rule_alert_unmute: 'change', rule_aggregate: 'access', + rule_snooze: 'change', }; export interface RuleAuditEventParams { diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index a44053053ef17..921a3a31e2df9 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -84,6 +84,8 @@ import { getModifiedSearch, modifyFilterKueryNode, } from './lib/mapped_params_utils'; +import { validateSnoozeDate } from '../lib/validate_snooze_date'; +import { RuleMutedError } from '../lib/errors/rule_muted'; export interface RegistryAlertTypeWithAuth extends RegistryRuleType { authorizedConsumers: string[]; @@ -144,6 +146,10 @@ export interface MuteOptions extends IndexType { alertInstanceId: string; } +export interface SnoozeOptions extends IndexType { + snoozeEndTime: string | -1; +} + export interface FindOptions extends IndexType { perPage?: number; page?: number; @@ -202,6 +208,7 @@ export interface CreateOptions { | 'mutedInstanceIds' | 'actions' | 'executionStatus' + | 'snoozeEndTime' > & { actions: NormalizedAlertAction[] }; options?: { id?: string; @@ -260,6 +267,7 @@ export class RulesClient { private readonly fieldsToExcludeFromPublicApi: Array = [ 'monitoring', 'mapped_params', + 'snoozeEndTime', ]; constructor({ @@ -372,6 +380,7 @@ export class RulesClient { updatedBy: username, createdAt: new Date(createTime).toISOString(), updatedAt: new Date(createTime).toISOString(), + snoozeEndTime: null, params: updatedParams as RawRule['params'], muteAll: false, mutedInstanceIds: [], @@ -1476,6 +1485,85 @@ export class RulesClient { } } + public async snooze({ + id, + snoozeEndTime, + }: { + id: string; + snoozeEndTime: string | -1; + }): Promise { + if (typeof snoozeEndTime === 'string') { + const snoozeDateValidationMsg = validateSnoozeDate(snoozeEndTime); + if (snoozeDateValidationMsg) { + throw new RuleMutedError(snoozeDateValidationMsg); + } + } + return await retryIfConflicts( + this.logger, + `rulesClient.snooze('${id}', ${snoozeEndTime})`, + async () => await this.snoozeWithOCC({ id, snoozeEndTime }) + ); + } + + private async snoozeWithOCC({ id, snoozeEndTime }: { id: string; snoozeEndTime: string | -1 }) { + const { attributes, version } = await this.unsecuredSavedObjectsClient.get( + 'alert', + id + ); + + try { + await this.authorization.ensureAuthorized({ + ruleTypeId: attributes.alertTypeId, + consumer: attributes.consumer, + operation: WriteOperations.MuteAll, + entity: AlertingAuthorizationEntity.Rule, + }); + + if (attributes.actions.length) { + await this.actionsAuthorization.ensureAuthorized('execute'); + } + } catch (error) { + this.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.SNOOZE, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + + this.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.SNOOZE, + outcome: 'unknown', + savedObject: { type: 'alert', id }, + }) + ); + + this.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); + + // If snoozeEndTime is -1, instead mute all + const newAttrs = + snoozeEndTime === -1 + ? { muteAll: true, snoozeEndTime: null } + : { snoozeEndTime: new Date(snoozeEndTime).toISOString(), muteAll: false }; + + const updateAttributes = this.updateMeta({ + ...newAttrs, + updatedBy: await this.getUserName(), + updatedAt: new Date().toISOString(), + }); + const updateOptions = { version }; + + await partiallyUpdateAlert( + this.unsecuredSavedObjectsClient, + id, + updateAttributes, + updateOptions + ); + } + public async muteAll({ id }: { id: string }): Promise { return await retryIfConflicts( this.logger, diff --git a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts index 8cecb47f23a88..17b384e4f18b2 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts @@ -294,6 +294,7 @@ describe('create()', () => { updatedBy: 'elastic', updatedAt: '2019-02-12T21:01:22.479Z', muteAll: false, + snoozeEndTime: null, mutedInstanceIds: [], actions: [ { @@ -426,6 +427,7 @@ describe('create()', () => { "schedule": Object { "interval": "1m", }, + "snoozeEndTime": null, "tags": Array [ "foo", ], @@ -496,6 +498,7 @@ describe('create()', () => { updatedBy: 'elastic', updatedAt: '2019-02-12T21:01:22.479Z', muteAll: false, + snoozeEndTime: null, mutedInstanceIds: [], actions: [ { @@ -555,6 +558,7 @@ describe('create()', () => { updatedBy: 'elastic', updatedAt: '2019-02-12T21:01:22.479Z', muteAll: false, + snoozeEndTime: null, mutedInstanceIds: [], actions: [ { @@ -627,6 +631,7 @@ describe('create()', () => { "schedule": Object { "interval": "1m", }, + "snoozeEndTime": null, "tags": Array [ "foo", ], @@ -1034,6 +1039,7 @@ describe('create()', () => { monitoring: getDefaultRuleMonitoring(), meta: { versionApiKeyLastmodified: kibanaVersion }, muteAll: false, + snoozeEndTime: null, mutedInstanceIds: [], name: 'abc', notifyWhen: 'onActiveAlert', @@ -1231,6 +1237,7 @@ describe('create()', () => { monitoring: getDefaultRuleMonitoring(), meta: { versionApiKeyLastmodified: kibanaVersion }, muteAll: false, + snoozeEndTime: null, mutedInstanceIds: [], name: 'abc', notifyWhen: 'onActiveAlert', @@ -1397,6 +1404,7 @@ describe('create()', () => { monitoring: getDefaultRuleMonitoring(), meta: { versionApiKeyLastmodified: kibanaVersion }, muteAll: false, + snoozeEndTime: null, mutedInstanceIds: [], name: 'abc', notifyWhen: 'onActiveAlert', @@ -1505,6 +1513,7 @@ describe('create()', () => { updatedBy: 'elastic', updatedAt: '2019-02-12T21:01:22.479Z', muteAll: false, + snoozeEndTime: null, mutedInstanceIds: [], notifyWhen: 'onActionGroupChange', actions: [ @@ -1561,6 +1570,7 @@ describe('create()', () => { throttle: '10m', notifyWhen: 'onActionGroupChange', muteAll: false, + snoozeEndTime: null, mutedInstanceIds: [], tags: ['foo'], executionStatus: { @@ -1634,6 +1644,7 @@ describe('create()', () => { updatedBy: 'elastic', updatedAt: '2019-02-12T21:01:22.479Z', muteAll: false, + snoozeEndTime: null, mutedInstanceIds: [], notifyWhen: 'onThrottleInterval', actions: [ @@ -1690,6 +1701,7 @@ describe('create()', () => { throttle: '10m', notifyWhen: 'onThrottleInterval', muteAll: false, + snoozeEndTime: null, mutedInstanceIds: [], tags: ['foo'], executionStatus: { @@ -1763,6 +1775,7 @@ describe('create()', () => { updatedBy: 'elastic', updatedAt: '2019-02-12T21:01:22.479Z', muteAll: false, + snoozeEndTime: null, mutedInstanceIds: [], notifyWhen: 'onActiveAlert', actions: [ @@ -1819,6 +1832,7 @@ describe('create()', () => { throttle: null, notifyWhen: 'onActiveAlert', muteAll: false, + snoozeEndTime: null, mutedInstanceIds: [], tags: ['foo'], executionStatus: { @@ -1901,6 +1915,7 @@ describe('create()', () => { updatedBy: 'elastic', updatedAt: '2019-02-12T21:01:22.479Z', muteAll: false, + snoozeEndTime: null, mutedInstanceIds: [], actions: [ { @@ -1964,6 +1979,7 @@ describe('create()', () => { createdAt: '2019-02-12T21:01:22.479Z', updatedAt: '2019-02-12T21:01:22.479Z', muteAll: false, + snoozeEndTime: null, mutedInstanceIds: [], executionStatus: { status: 'pending', @@ -2332,6 +2348,7 @@ describe('create()', () => { throttle: null, notifyWhen: 'onActiveAlert', muteAll: false, + snoozeEndTime: null, mutedInstanceIds: [], tags: ['foo'], executionStatus: { @@ -2432,6 +2449,7 @@ describe('create()', () => { throttle: null, notifyWhen: 'onActiveAlert', muteAll: false, + snoozeEndTime: null, mutedInstanceIds: [], tags: ['foo'], executionStatus: { diff --git a/x-pack/plugins/alerting/server/saved_objects/index.ts b/x-pack/plugins/alerting/server/saved_objects/index.ts index 4768a1f779542..16736a24a9a38 100644 --- a/x-pack/plugins/alerting/server/saved_objects/index.ts +++ b/x-pack/plugins/alerting/server/saved_objects/index.ts @@ -30,6 +30,7 @@ export const AlertAttributesExcludedFromAAD = [ 'updatedAt', 'executionStatus', 'monitoring', + 'snoozeEndTime', ]; // useful for Pick which is a @@ -43,7 +44,8 @@ export type AlertAttributesExcludedFromAADType = | 'updatedBy' | 'updatedAt' | 'executionStatus' - | 'monitoring'; + | 'monitoring' + | 'snoozeEndTime'; export function setupSavedObjects( savedObjects: SavedObjectsServiceSetup, diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index ea7e12b320d18..aacdcef511614 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -244,7 +244,7 @@ export interface RawRule extends SavedObjectAttributes { meta?: AlertMeta; executionStatus: RawRuleExecutionStatus; monitoring?: RuleMonitoring; - snoozeEndTime?: string; + snoozeEndTime?: string | null; // Remove ? when this parameter is made available in the public API } export type AlertInfoParams = Pick< diff --git a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts index 9da73e1ca6f43..9610fc8d076d2 100644 --- a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts +++ b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts @@ -86,6 +86,17 @@ export class AlertUtils { return request; } + public getSnoozeRequest(alertId: string) { + const request = this.supertestWithoutAuth + .post(`${getUrlPrefix(this.space.id)}/internal/alerting/rule/${alertId}/_snooze`) + .set('kbn-xsrf', 'foo') + .set('content-type', 'application/json'); + if (this.user) { + return request.auth(this.user.username, this.user.password); + } + return request; + } + public getMuteAllRequest(alertId: string) { const request = this.supertestWithoutAuth .post(`${getUrlPrefix(this.space.id)}/api/alerting/rule/${alertId}/_mute_all`) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts index f6ba70e7c2197..84f0d7709d01a 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts @@ -82,7 +82,9 @@ const findTestUtils = ( mute_all: false, muted_alert_ids: [], execution_status: match.execution_status, - ...(describeType === 'internal' ? { monitoring: match.monitoring } : {}), + ...(describeType === 'internal' + ? { monitoring: match.monitoring, snooze_end_time: match.snooze_end_time } + : {}), }); expect(Date.parse(match.created_at)).to.be.greaterThan(0); expect(Date.parse(match.updated_at)).to.be.greaterThan(0); @@ -281,7 +283,9 @@ const findTestUtils = ( created_at: match.created_at, updated_at: match.updated_at, execution_status: match.execution_status, - ...(describeType === 'internal' ? { monitoring: match.monitoring } : {}), + ...(describeType === 'internal' + ? { monitoring: match.monitoring, snooze_end_time: match.snooze_end_time } + : {}), }); expect(Date.parse(match.created_at)).to.be.greaterThan(0); expect(Date.parse(match.updated_at)).to.be.greaterThan(0); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts index 6d072b2e26f45..180a3cf36e27f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts @@ -81,7 +81,12 @@ const getTestUtils = ( mute_all: false, muted_alert_ids: [], execution_status: response.body.execution_status, - ...(describeType === 'internal' ? { monitoring: response.body.monitoring } : {}), + ...(describeType === 'internal' + ? { + monitoring: response.body.monitoring, + snooze_end_time: response.body.snooze_end_time, + } + : {}), }); expect(Date.parse(response.body.created_at)).to.be.greaterThan(0); expect(Date.parse(response.body.updated_at)).to.be.greaterThan(0); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts index b7ef734d40c12..8134b03e4ba69 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts @@ -53,6 +53,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC loadTestFile(require.resolve('./mustache_templates')); loadTestFile(require.resolve('./health')); loadTestFile(require.resolve('./excluded')); + loadTestFile(require.resolve('./snooze')); }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/snooze.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/snooze.ts new file mode 100644 index 0000000000000..406373a756da3 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/snooze.ts @@ -0,0 +1,403 @@ +/* + * 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 { UserAtSpaceScenarios } from '../../scenarios'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { + AlertUtils, + checkAAD, + getUrlPrefix, + getTestRuleData, + ObjectRemover, + getConsumerUnauthorizedErrorMessage, + getProducerUnauthorizedErrorMessage, +} from '../../../common/lib'; + +const FUTURE_SNOOZE_TIME = '9999-12-31T06:00:00.000Z'; + +// eslint-disable-next-line import/no-default-export +export default function createSnoozeRuleTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('snooze', () => { + const objectRemover = new ObjectRemover(supertest); + + after(() => objectRemover.removeAll()); + + for (const scenario of UserAtSpaceScenarios) { + const { user, space } = scenario; + const alertUtils = new AlertUtils({ user, space, supertestWithoutAuth }); + + describe(scenario.id, () => { + it('should handle snooze rule request appropriately', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY action', + connector_type_id: 'test.noop', + config: {}, + secrets: {}, + }) + .expect(200); + + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + enabled: false, + actions: [ + { + id: createdAction.id, + group: 'default', + params: {}, + }, + ], + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); + + const response = await alertUtils + .getSnoozeRequest(createdAlert.id) + .send({ snooze_end_time: FUTURE_SNOOZE_TIME }); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'global_read at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'muteAll', + 'test.noop', + 'alertsFixture' + ), + statusCode: 403, + }); + break; + case 'space_1_all_alerts_none_actions at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to execute actions`, + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/internal/alerting/rule/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .expect(200); + expect(updatedAlert.snooze_end_time).to.eql(FUTURE_SNOOZE_TIME); + expect(updatedAlert.mute_all).to.eql(false); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: 'alert', + id: createdAlert.id, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle snooze rule request appropriately when consumer is the same as producer', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + enabled: false, + rule_type_id: 'test.restricted-noop', + consumer: 'alertsRestrictedFixture', + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); + + const response = await alertUtils + .getSnoozeRequest(createdAlert.id) + .send({ snooze_end_time: FUTURE_SNOOZE_TIME }); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'global_read at space1': + case 'space_1_all 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: getConsumerUnauthorizedErrorMessage( + 'muteAll', + 'test.restricted-noop', + 'alertsRestrictedFixture' + ), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/internal/alerting/rule/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .expect(200); + expect(updatedAlert.snooze_end_time).to.eql(FUTURE_SNOOZE_TIME); + expect(updatedAlert.mute_all).to.eql(false); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: 'alert', + id: createdAlert.id, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle snooze rule request appropriately when consumer is not the producer', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + enabled: false, + rule_type_id: 'test.unrestricted-noop', + consumer: 'alertsFixture', + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); + + const response = await alertUtils + .getSnoozeRequest(createdAlert.id) + .send({ snooze_end_time: FUTURE_SNOOZE_TIME }); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'global_read at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'muteAll', + 'test.unrestricted-noop', + 'alertsFixture' + ), + statusCode: 403, + }); + break; + case 'space_1_all 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: getProducerUnauthorizedErrorMessage( + 'muteAll', + 'test.unrestricted-noop', + 'alertsRestrictedFixture' + ), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/internal/alerting/rule/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .expect(200); + expect(updatedAlert.snooze_end_time).to.eql(FUTURE_SNOOZE_TIME); + expect(updatedAlert.mute_all).to.eql(false); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: 'alert', + id: createdAlert.id, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle snooze rule request appropriately when consumer is "alerts"', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + enabled: false, + rule_type_id: 'test.restricted-noop', + consumer: 'alerts', + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); + + const response = await alertUtils + .getSnoozeRequest(createdAlert.id) + .send({ snooze_end_time: FUTURE_SNOOZE_TIME }); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'muteAll', + 'test.restricted-noop', + 'alerts' + ), + statusCode: 403, + }); + break; + case 'global_read at space1': + case 'space_1_all 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: getProducerUnauthorizedErrorMessage( + 'muteAll', + 'test.restricted-noop', + 'alertsRestrictedFixture' + ), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/internal/alerting/rule/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .expect(200); + expect(updatedAlert.snooze_end_time).to.eql(FUTURE_SNOOZE_TIME); + expect(updatedAlert.mute_all).to.eql(false); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: 'alert', + id: createdAlert.id, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle snooze rule request appropriately when snoozeEndTime is -1', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY action', + connector_type_id: 'test.noop', + config: {}, + secrets: {}, + }) + .expect(200); + + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + enabled: false, + actions: [ + { + id: createdAction.id, + group: 'default', + params: {}, + }, + ], + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); + + const response = await alertUtils + .getSnoozeRequest(createdAlert.id) + .send({ snooze_end_time: -1 }); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'global_read at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'muteAll', + 'test.noop', + 'alertsFixture' + ), + statusCode: 403, + }); + break; + case 'space_1_all_alerts_none_actions at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to execute actions`, + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/internal/alerting/rule/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .expect(200); + expect(updatedAlert.snooze_end_time).to.eql(null); + expect(updatedAlert.mute_all).to.eql(true); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: 'alert', + id: createdAlert.id, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + }); + } + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts index 5ab632c6a66b8..a1b0f5c7eeb14 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts @@ -72,7 +72,9 @@ const findTestUtils = ( created_at: match.created_at, updated_at: match.updated_at, execution_status: match.execution_status, - ...(describeType === 'internal' ? { monitoring: match.monitoring } : {}), + ...(describeType === 'internal' + ? { monitoring: match.monitoring, snooze_end_time: match.snooze_end_time } + : {}), }); expect(Date.parse(match.created_at)).to.be.greaterThan(0); expect(Date.parse(match.updated_at)).to.be.greaterThan(0); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts index 81f67c8d49e33..58c68def04372 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts @@ -54,7 +54,9 @@ const getTestUtils = ( created_at: response.body.created_at, updated_at: response.body.updated_at, execution_status: response.body.execution_status, - ...(describeType === 'internal' ? { monitoring: response.body.monitoring } : {}), + ...(describeType === 'internal' + ? { monitoring: response.body.monitoring, snooze_end_time: response.body.snooze_end_time } + : {}), }); expect(Date.parse(response.body.created_at)).to.be.greaterThan(0); expect(Date.parse(response.body.updated_at)).to.be.greaterThan(0); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/snooze.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/snooze.ts new file mode 100644 index 0000000000000..bb3e0cea469e4 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/snooze.ts @@ -0,0 +1,135 @@ +/* + * 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 { Spaces } from '../../scenarios'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { + AlertUtils, + checkAAD, + getUrlPrefix, + getTestRuleData, + ObjectRemover, +} from '../../../common/lib'; + +const FUTURE_SNOOZE_TIME = '9999-12-31T06:00:00.000Z'; + +// eslint-disable-next-line import/no-default-export +export default function createSnoozeRuleTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('snooze', () => { + const objectRemover = new ObjectRemover(supertest); + + after(() => objectRemover.removeAll()); + + const alertUtils = new AlertUtils({ space: Spaces.space1, supertestWithoutAuth }); + + it('should handle snooze rule request appropriately', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY action', + connector_type_id: 'test.noop', + config: {}, + secrets: {}, + }) + .expect(200); + + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + enabled: false, + actions: [ + { + id: createdAction.id, + group: 'default', + params: {}, + }, + ], + }) + ) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); + + const response = await alertUtils + .getSnoozeRequest(createdAlert.id) + .send({ snooze_end_time: FUTURE_SNOOZE_TIME }); + + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .expect(200); + expect(updatedAlert.snooze_end_time).to.eql(FUTURE_SNOOZE_TIME); + expect(updatedAlert.mute_all).to.eql(false); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: Spaces.space1.id, + type: 'alert', + id: createdAlert.id, + }); + }); + + it('should handle snooze rule request appropriately when snoozeEndTime is -1', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY action', + connector_type_id: 'test.noop', + config: {}, + secrets: {}, + }) + .expect(200); + + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + enabled: false, + actions: [ + { + id: createdAction.id, + group: 'default', + params: {}, + }, + ], + }) + ) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); + + const response = await alertUtils + .getSnoozeRequest(createdAlert.id) + .send({ snooze_end_time: -1 }); + + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .expect(200); + expect(updatedAlert.snooze_end_time).to.eql(null); + expect(updatedAlert.mute_all).to.eql(true); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: Spaces.space1.id, + type: 'alert', + id: createdAlert.id, + }); + }); + }); +}