From 44910cc345e4009e7802e162ee6bb548a4362bca Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Mon, 3 Mar 2025 14:44:36 +0000 Subject: [PATCH 1/6] initial commit --- .../unsnooze/{ => external}/schemas/latest.ts | 0 .../rule/apis/unsnooze/external/schemas/v1.ts | 13 +++ .../apis/unsnooze/external/types}/latest.ts | 0 .../rule/apis/unsnooze/external/types/v1.ts | 10 +++ .../common/routes/rule/apis/unsnooze/index.ts | 15 +++- .../apis/unsnooze/internal/schemas/latest.ts | 8 ++ .../unsnooze/{ => internal}/schemas/v1.ts | 4 +- .../shared/alerting/server/routes/index.ts | 3 +- .../external/unsnooze_rule_route.test.ts | 86 +++++++++++++++++++ .../unsnooze/external/unsnooze_rule_route.ts | 68 +++++++++++++++ .../server/routes/rule/apis/unsnooze/index.ts | 3 +- .../{ => internal}/transforms/index.ts | 0 .../transform_unsnooze_body/latest.ts | 8 ++ .../transforms/transform_unsnooze_body/v1.ts | 0 .../unsnooze_rule_route.test.ts | 10 +-- .../{ => internal}/unsnooze_rule_route.ts | 23 ++--- 16 files changed, 228 insertions(+), 23 deletions(-) rename x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/{ => external}/schemas/latest.ts (100%) create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/external/schemas/v1.ts rename x-pack/platform/plugins/shared/alerting/{server/routes/rule/apis/unsnooze/transforms/transform_unsnooze_body => common/routes/rule/apis/unsnooze/external/types}/latest.ts (100%) create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/external/types/v1.ts create mode 100644 x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/internal/schemas/latest.ts rename x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/{ => internal}/schemas/v1.ts (78%) create mode 100644 x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/external/unsnooze_rule_route.test.ts create mode 100644 x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/external/unsnooze_rule_route.ts rename x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/{ => internal}/transforms/index.ts (100%) create mode 100644 x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/internal/transforms/transform_unsnooze_body/latest.ts rename x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/{ => internal}/transforms/transform_unsnooze_body/v1.ts (100%) rename x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/{ => internal}/unsnooze_rule_route.test.ts (85%) rename x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/{ => internal}/unsnooze_rule_route.ts (72%) diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/schemas/latest.ts b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/external/schemas/latest.ts similarity index 100% rename from x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/schemas/latest.ts rename to x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/external/schemas/latest.ts diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/external/schemas/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/external/schemas/v1.ts new file mode 100644 index 0000000000000..9c6e9919e2d88 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/external/schemas/v1.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. + */ + +import { schema } from '@kbn/config-schema'; + +export const unsnoozeParamsSchema = schema.object({ + rule_id: schema.string(), + schedule_id: schema.string(), +}); diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/transforms/transform_unsnooze_body/latest.ts b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/external/types/latest.ts similarity index 100% rename from x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/transforms/transform_unsnooze_body/latest.ts rename to x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/external/types/latest.ts diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/external/types/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/external/types/v1.ts new file mode 100644 index 0000000000000..d8f1c1016e61b --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/external/types/v1.ts @@ -0,0 +1,10 @@ +/* + * 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 { TypeOf } from '@kbn/config-schema'; +import { unsnoozeParamsSchemaV1 } from '../..'; + +export type UnsnoozeParams = TypeOf; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/index.ts b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/index.ts index 1fb769c46d345..0714b30074df7 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/index.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/index.ts @@ -5,9 +5,16 @@ * 2.0. */ -export { unsnoozeParamsSchema, unsnoozeBodySchema } from './schemas/latest'; +export { + unsnoozeParamsInternalSchema, + unsnoozeBodyInternalSchema, +} from './internal/schemas/latest'; +export { unsnoozeParamsSchema } from './external/schemas/latest'; +export type { UnsnoozeParams } from './external/types/latest'; export { - unsnoozeParamsSchema as unsnoozeParamsSchemaV1, - unsnoozeBodySchema as unsnoozeBodySchemaV1, -} from './schemas/v1'; + unsnoozeParamsInternalSchema as unsnoozeParamsInternalSchemaV1, + unsnoozeBodyInternalSchema as unsnoozeBodyInternalSchemaV1, +} from './internal/schemas/v1'; +export { unsnoozeParamsSchema as unsnoozeParamsSchemaV1 } from './external/schemas/v1'; +export type { UnsnoozeParams as UnsnoozeParamsV1 } from './external/types/v1'; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/internal/schemas/latest.ts b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/internal/schemas/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/internal/schemas/latest.ts @@ -0,0 +1,8 @@ +/* + * 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 * from './v1'; diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/schemas/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/internal/schemas/v1.ts similarity index 78% rename from x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/schemas/v1.ts rename to x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/internal/schemas/v1.ts index 0da0e1b4ecd0b..aaa642a9d5514 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/schemas/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/internal/schemas/v1.ts @@ -7,12 +7,12 @@ import { schema } from '@kbn/config-schema'; -export const unsnoozeParamsSchema = schema.object({ +export const unsnoozeParamsInternalSchema = schema.object({ id: schema.string(), }); const scheduleIdsSchema = schema.maybe(schema.arrayOf(schema.string())); -export const unsnoozeBodySchema = schema.object({ +export const unsnoozeBodyInternalSchema = schema.object({ schedule_ids: scheduleIdsSchema, }); diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/index.ts b/x-pack/platform/plugins/shared/alerting/server/routes/index.ts index cedd8de9446f6..eb71e9c880e3e 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/index.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/index.ts @@ -38,7 +38,7 @@ import { unmuteAlertRoute } from './rule/apis/unmute_alert/unmute_alert_route'; import { updateRuleApiKeyRoute } from './rule/apis/update_api_key/update_rule_api_key_route'; import { bulkEditInternalRulesRoute } from './rule/apis/bulk_edit/bulk_edit_rules_route'; import { snoozeRuleRoute } from './rule/apis/snooze'; -import { unsnoozeRuleRoute } from './rule/apis/unsnooze'; +import { unsnoozeRuleRoute, unsnoozeRuleInternalRoute } from './rule/apis/unsnooze'; import { runSoonRoute } from './run_soon'; import { bulkDeleteRulesRoute } from './rule/apis/bulk_delete/bulk_delete_rules_route'; import { bulkEnableRulesRoute } from './rule/apis/bulk_enable/bulk_enable_rules_route'; @@ -123,6 +123,7 @@ export function defineRoutes(opts: RouteOptions) { bulkDisableRulesRoute({ router, licenseState }); snoozeRuleRoute(router, licenseState); unsnoozeRuleRoute(router, licenseState); + unsnoozeRuleInternalRoute(router, licenseState); cloneRuleRoute(router, licenseState); getRuleTagsRoute(router, licenseState); registerRulesValueSuggestionsRoute(router, licenseState, config$!); diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/external/unsnooze_rule_route.test.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/external/unsnooze_rule_route.test.ts new file mode 100644 index 0000000000000..f9410ec6df609 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/external/unsnooze_rule_route.test.ts @@ -0,0 +1,86 @@ +/* + * 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 { unsnoozeRuleRoute } from './unsnooze_rule_route'; +import { httpServiceMock } from '@kbn/core/server/mocks'; +import { licenseStateMock } from '../../../../../lib/license_state.mock'; +import { mockHandlerArguments } from '../../../../_mock_handler_arguments'; +import { rulesClientMock } from '../../../../../rules_client.mock'; +import { RuleTypeDisabledError } from '../../../../../lib/errors/rule_type_disabled'; + +const rulesClient = rulesClientMock.create(); +jest.mock('../../../../../lib/license_api_access', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('unsnoozeAlertRoute', () => { + it('unsnoozes an alert', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + unsnoozeRuleRoute(router, licenseState); + + const [config, handler] = router.delete.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot( + `"/api/alerting/rule/{rule_id}/snooze_schedule/{schedule_id}"` + ); + + rulesClient.unsnooze.mockResolvedValueOnce(); + + const [context, req, res] = mockHandlerArguments( + { rulesClient }, + { + params: { + rule_id: 'rule_1', + schedule_id: 'snooze_schedule_1', + }, + }, + ['noContent'] + ); + + expect(await handler(context, req, res)).toEqual(undefined); + + expect(rulesClient.unsnooze).toHaveBeenCalledTimes(1); + expect(rulesClient.unsnooze.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "id": "rule_1", + "scheduleIds": Array [ + "snooze_schedule_1", + ], + }, + ] + `); + + expect(res.noContent).toHaveBeenCalled(); + }); + + it('ensures the rule type gets validated for the license', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + unsnoozeRuleRoute(router, licenseState); + + const [, handler] = router.delete.mock.calls[0]; + + rulesClient.unsnooze.mockRejectedValue(new RuleTypeDisabledError('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/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/external/unsnooze_rule_route.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/external/unsnooze_rule_route.ts new file mode 100644 index 0000000000000..fdcf8b576aafb --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/external/unsnooze_rule_route.ts @@ -0,0 +1,68 @@ +/* + * 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 { + unsnoozeParamsSchema, + type UnsnoozeParams, +} from '../../../../../../common/routes/rule/apis/unsnooze'; +import { ILicenseState, RuleMutedError } from '../../../../../lib'; +import { verifyAccessAndContext } from '../../../../lib'; +import { AlertingRequestHandlerContext, BASE_ALERTING_API_PATH } from '../../../../../types'; +import { DEFAULT_ALERTING_ROUTE_SECURITY } from '../../../../constants'; + +export const unsnoozeRuleRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.delete( + { + path: `${BASE_ALERTING_API_PATH}/rule/{rule_id}/snooze_schedule/{schedule_id}`, + security: DEFAULT_ALERTING_ROUTE_SECURITY, + options: { + access: 'public', + summary: 'Delete a snooze schedule for a rule', + tags: ['oas-tag:alerting'], + }, + validate: { + request: { + params: unsnoozeParamsSchema, + }, + response: { + 204: { + description: 'Indicates a successful call.', + }, + 400: { + description: 'Indicates an invalid schema.', + }, + 403: { + description: 'Indicates that this call is forbidden.', + }, + 404: { + description: 'Indicates a rule with the given id does not exist.', + }, + }, + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const alertingContext = await context.alerting; + const rulesClient = await alertingContext.getRulesClient(); + const { rule_id, schedule_id }: UnsnoozeParams = req.params; + try { + await rulesClient.unsnooze({ id: rule_id, scheduleIds: [schedule_id] }); + return res.noContent(); + } catch (e) { + if (e instanceof RuleMutedError) { + return e.sendResponse(res); + } + throw e; + } + }) + ) + ); +}; diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/index.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/index.ts index 5702c1a2da283..b7ecdf8e180dc 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/index.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/index.ts @@ -5,4 +5,5 @@ * 2.0. */ -export { unsnoozeRuleRoute } from './unsnooze_rule_route'; +export { unsnoozeRuleRoute as unsnoozeRuleInternalRoute } from './internal/unsnooze_rule_route'; +export { unsnoozeRuleRoute } from './external/unsnooze_rule_route'; diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/transforms/index.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/internal/transforms/index.ts similarity index 100% rename from x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/transforms/index.ts rename to x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/internal/transforms/index.ts diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/internal/transforms/transform_unsnooze_body/latest.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/internal/transforms/transform_unsnooze_body/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/internal/transforms/transform_unsnooze_body/latest.ts @@ -0,0 +1,8 @@ +/* + * 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 * from './v1'; diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/transforms/transform_unsnooze_body/v1.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/internal/transforms/transform_unsnooze_body/v1.ts similarity index 100% rename from x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/transforms/transform_unsnooze_body/v1.ts rename to x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/internal/transforms/transform_unsnooze_body/v1.ts diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/unsnooze_rule_route.test.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/internal/unsnooze_rule_route.test.ts similarity index 85% rename from x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/unsnooze_rule_route.test.ts rename to x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/internal/unsnooze_rule_route.test.ts index 1d69b00eb8486..22a93ec60aaf4 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/unsnooze_rule_route.test.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/internal/unsnooze_rule_route.test.ts @@ -7,13 +7,13 @@ import { unsnoozeRuleRoute } from './unsnooze_rule_route'; import { httpServiceMock } from '@kbn/core/server/mocks'; -import { licenseStateMock } from '../../../../lib/license_state.mock'; -import { mockHandlerArguments } from '../../../_mock_handler_arguments'; -import { rulesClientMock } from '../../../../rules_client.mock'; -import { RuleTypeDisabledError } from '../../../../lib/errors/rule_type_disabled'; +import { licenseStateMock } from '../../../../../lib/license_state.mock'; +import { mockHandlerArguments } from '../../../../_mock_handler_arguments'; +import { rulesClientMock } from '../../../../../rules_client.mock'; +import { RuleTypeDisabledError } from '../../../../../lib/errors/rule_type_disabled'; const rulesClient = rulesClientMock.create(); -jest.mock('../../../../lib/license_api_access', () => ({ +jest.mock('../../../../../lib/license_api_access', () => ({ verifyApiAccess: jest.fn(), })); diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/unsnooze_rule_route.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/internal/unsnooze_rule_route.ts similarity index 72% rename from x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/unsnooze_rule_route.ts rename to x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/internal/unsnooze_rule_route.ts index e0ac5df1d8dc8..59f52a6a23632 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/unsnooze_rule_route.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/internal/unsnooze_rule_route.ts @@ -8,16 +8,19 @@ import { TypeOf } from '@kbn/config-schema'; import { IRouter } from '@kbn/core/server'; import { - unsnoozeBodySchema, - unsnoozeParamsSchema, -} from '../../../../../common/routes/rule/apis/unsnooze'; -import { ILicenseState, RuleMutedError } from '../../../../lib'; -import { verifyAccessAndContext } from '../../../lib'; -import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../../../types'; + unsnoozeBodyInternalSchema, + unsnoozeParamsInternalSchema, +} from '../../../../../../common/routes/rule/apis/unsnooze'; +import { ILicenseState, RuleMutedError } from '../../../../../lib'; +import { verifyAccessAndContext } from '../../../../lib'; +import { + AlertingRequestHandlerContext, + INTERNAL_BASE_ALERTING_API_PATH, +} from '../../../../../types'; import { transformUnsnoozeBodyV1 } from './transforms'; -import { DEFAULT_ALERTING_ROUTE_SECURITY } from '../../../constants'; +import { DEFAULT_ALERTING_ROUTE_SECURITY } from '../../../../constants'; -export type UnsnoozeRuleRequestParamsV1 = TypeOf; +export type UnsnoozeRuleRequestParamsV1 = TypeOf; export const unsnoozeRuleRoute = ( router: IRouter, @@ -29,8 +32,8 @@ export const unsnoozeRuleRoute = ( security: DEFAULT_ALERTING_ROUTE_SECURITY, options: { access: 'internal' }, validate: { - params: unsnoozeParamsSchema, - body: unsnoozeBodySchema, + params: unsnoozeParamsInternalSchema, + body: unsnoozeBodyInternalSchema, }, }, router.handleLegacyErrors( From 24eda2d61b82ac4fd1410b5137b0240dd56c5b17 Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Tue, 4 Mar 2025 14:00:03 +0000 Subject: [PATCH 2/6] add integration tests, add checks for schedule id in rules client --- .../rule/apis/unsnooze/external/schemas/v1.ts | 4 +- .../methods/unsnooze/unsnooze_rule.test.ts | 115 +++++++ .../rule/methods/unsnooze/unsnooze_rule.ts | 24 +- .../external/unsnooze_rule_route.test.ts | 7 +- .../unsnooze/external/unsnooze_rule_route.ts | 6 +- .../common/lib/alert_utils.ts | 15 +- .../group4/tests/alerting/index.ts | 1 + .../group4/tests/alerting/unsnooze.ts | 77 ++++- .../tests/alerting/unsnooze_internal.ts | 287 ++++++++++++++++++ .../tests/alerting/group4/index.ts | 1 + .../tests/alerting/group4/unsnooze.ts | 24 +- .../alerting/group4/unsnooze_internal.ts | 83 +++++ 12 files changed, 626 insertions(+), 18 deletions(-) create mode 100644 x-pack/platform/plugins/shared/alerting/server/application/rule/methods/unsnooze/unsnooze_rule.test.ts create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/unsnooze_internal.ts create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/unsnooze_internal.ts diff --git a/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/external/schemas/v1.ts b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/external/schemas/v1.ts index 9c6e9919e2d88..298e3109a2203 100644 --- a/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/external/schemas/v1.ts +++ b/x-pack/platform/plugins/shared/alerting/common/routes/rule/apis/unsnooze/external/schemas/v1.ts @@ -8,6 +8,6 @@ import { schema } from '@kbn/config-schema'; export const unsnoozeParamsSchema = schema.object({ - rule_id: schema.string(), - schedule_id: schema.string(), + ruleId: schema.string(), + scheduleId: schema.string(), }); diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/unsnooze/unsnooze_rule.test.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/unsnooze/unsnooze_rule.test.ts new file mode 100644 index 0000000000000..9ed1f81231c16 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/unsnooze/unsnooze_rule.test.ts @@ -0,0 +1,115 @@ +/* + * 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 { RulesClientContext } from '../../../../rules_client'; +import { unsnoozeRule } from './unsnooze_rule'; +import { savedObjectsRepositoryMock } from '@kbn/core-saved-objects-api-server-mocks'; + +const loggerErrorMock = jest.fn(); +const getBulkMock = jest.fn(); + +const savedObjectsMock = savedObjectsRepositoryMock.create(); +savedObjectsMock.get = jest.fn().mockReturnValue({ + attributes: { + actions: [], + snoozeSchedule: [ + { + duration: 600000, + rRule: { + interval: 1, + freq: 3, + dtstart: '2025-03-01T06:30:37.011Z', + tzid: 'UTC', + }, + id: 'snooze_schedule_1', + }, + ], + }, + version: '9.0.0', +}); + +const context = { + logger: { error: loggerErrorMock }, + getActionsClient: () => { + return { + getBulk: getBulkMock, + }; + }, + unsecuredSavedObjectsClient: savedObjectsMock, + authorization: { ensureAuthorized: async () => {} }, + ruleTypeRegistry: { + ensureRuleTypeEnabled: () => {}, + }, + getUserName: async () => {}, +} as unknown as RulesClientContext; + +describe('validate unsnooze params', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should validate params correctly', async () => { + await expect( + unsnoozeRule(context, { id: '123', scheduleIds: ['snooze_schedule_1'] }) + ).resolves.toBeUndefined(); + }); + + it('should validate params with empty schedule ids correctly', async () => { + await expect(unsnoozeRule(context, { id: '123', scheduleIds: [] })).resolves.toBeUndefined(); + }); + + it('should throw bad request for invalid params', async () => { + // @ts-expect-error: testing invalid params + await expect(unsnoozeRule(context, {})).rejects.toThrowErrorMatchingInlineSnapshot( + `"Error validating unsnooze params - [id]: expected value of type [string] but got [undefined]"` + ); + }); + + it('should throw bad request for when snooze schedule is empty is route is public', async () => { + savedObjectsMock.get = jest.fn().mockReturnValue({ + attributes: { + actions: [], + snoozeSchedule: [], + }, + version: '9.0.0', + }); + await expect( + unsnoozeRule(context, { id: '123', scheduleIds: ['snooze_schedule_1'], isPublic: true }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Rule has no snooze schedules to unsnooze."`); + }); + + it('should throw bad request for invalid snooze schedule id for public route', async () => { + savedObjectsMock.get = jest.fn().mockReturnValue({ + attributes: { + actions: [], + snoozeSchedule: [ + { + duration: 600000, + rRule: { + interval: 1, + freq: 3, + dtstart: '2025-03-01T06:30:37.011Z', + tzid: 'UTC', + }, + id: 'snooze_schedule_1', + }, + ], + }, + version: '9.0.0', + }); + + const invalidParams = { + id: '123', + scheduleIds: ['random_schedule_id'], + isPublic: true, + }; + + await expect(unsnoozeRule(context, invalidParams)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Rule has no snooze schedule with id random_schedule_id."` + ); + }); +}); diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/unsnooze/unsnooze_rule.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/unsnooze/unsnooze_rule.ts index 13a9a96b53ad4..ea4989d8a7feb 100644 --- a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/unsnooze/unsnooze_rule.ts +++ b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/unsnooze/unsnooze_rule.ts @@ -21,20 +21,24 @@ import { unsnoozeRuleParamsSchema } from './schemas'; export interface UnsnoozeParams { id: string; scheduleIds?: string[]; + isPublic?: boolean; } export async function unsnoozeRule( context: RulesClientContext, - { id, scheduleIds }: UnsnoozeParams + { id, scheduleIds, isPublic = false }: UnsnoozeParams ): Promise { return await retryIfConflicts( context.logger, `rulesClient.unsnooze('${id}')`, - async () => await unsnoozeWithOCC(context, { id, scheduleIds }) + async () => await unsnoozeWithOCC(context, { id, scheduleIds, isPublic }) ); } -async function unsnoozeWithOCC(context: RulesClientContext, { id, scheduleIds }: UnsnoozeParams) { +async function unsnoozeWithOCC( + context: RulesClientContext, + { id, scheduleIds, isPublic }: UnsnoozeParams +) { try { unsnoozeRuleParamsSchema.validate({ id, scheduleIds }); } catch (error) { @@ -48,6 +52,20 @@ async function unsnoozeWithOCC(context: RulesClientContext, { id, scheduleIds }: }) ); + if (isPublic && scheduleIds?.length) { + if (!attributes.snoozeSchedule?.length) { + throw Boom.badRequest('Rule has no snooze schedules to unsnooze.'); + } + + const scheduleToUnsnooze = attributes.snoozeSchedule?.find( + (schedule) => schedule.id === scheduleIds[0] + ); + + if (!scheduleToUnsnooze) { + throw Boom.badRequest(`Rule has no snooze schedule with id ${scheduleIds[0]}.`); + } + } + try { await context.authorization.ensureAuthorized({ ruleTypeId: attributes.alertTypeId, diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/external/unsnooze_rule_route.test.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/external/unsnooze_rule_route.test.ts index f9410ec6df609..195c55b10acf4 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/external/unsnooze_rule_route.test.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/external/unsnooze_rule_route.test.ts @@ -31,7 +31,7 @@ describe('unsnoozeAlertRoute', () => { const [config, handler] = router.delete.mock.calls[0]; expect(config.path).toMatchInlineSnapshot( - `"/api/alerting/rule/{rule_id}/snooze_schedule/{schedule_id}"` + `"/api/alerting/rule/{ruleId}/snooze_schedule/{scheduleId}"` ); rulesClient.unsnooze.mockResolvedValueOnce(); @@ -40,8 +40,8 @@ describe('unsnoozeAlertRoute', () => { { rulesClient }, { params: { - rule_id: 'rule_1', - schedule_id: 'snooze_schedule_1', + ruleId: 'rule_1', + scheduleId: 'snooze_schedule_1', }, }, ['noContent'] @@ -54,6 +54,7 @@ describe('unsnoozeAlertRoute', () => { Array [ Object { "id": "rule_1", + "isPublic": true, "scheduleIds": Array [ "snooze_schedule_1", ], diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/external/unsnooze_rule_route.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/external/unsnooze_rule_route.ts index fdcf8b576aafb..78f56b3b65d4d 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/external/unsnooze_rule_route.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/external/unsnooze_rule_route.ts @@ -21,7 +21,7 @@ export const unsnoozeRuleRoute = ( ) => { router.delete( { - path: `${BASE_ALERTING_API_PATH}/rule/{rule_id}/snooze_schedule/{schedule_id}`, + path: `${BASE_ALERTING_API_PATH}/rule/{ruleId}/snooze_schedule/{scheduleId}`, security: DEFAULT_ALERTING_ROUTE_SECURITY, options: { access: 'public', @@ -52,9 +52,9 @@ export const unsnoozeRuleRoute = ( verifyAccessAndContext(licenseState, async function (context, req, res) { const alertingContext = await context.alerting; const rulesClient = await alertingContext.getRulesClient(); - const { rule_id, schedule_id }: UnsnoozeParams = req.params; + const { ruleId, scheduleId }: UnsnoozeParams = req.params; try { - await rulesClient.unsnooze({ id: rule_id, scheduleIds: [schedule_id] }); + await rulesClient.unsnooze({ id: ruleId, scheduleIds: [scheduleId], isPublic: true }); return res.noContent(); } catch (e) { if (e instanceof RuleMutedError) { 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 4c693cfaa1b8d..9a99c40a82890 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 @@ -130,7 +130,7 @@ export class AlertUtils { return request; } - public getUnsnoozeRequest(alertId: string) { + public getUnsnoozeInternalRequest(alertId: string) { const request = this.supertestWithoutAuth .post(`${getUrlPrefix(this.space.id)}/internal/alerting/rule/${alertId}/_unsnooze`) .set('kbn-xsrf', 'foo') @@ -144,6 +144,19 @@ export class AlertUtils { return request; } + public getUnsnoozeRequest(alertId: string, scheduleId: string) { + const request = this.supertestWithoutAuth + .delete( + `${getUrlPrefix(this.space.id)}/api/alerting/rule/${alertId}/snooze_schedule/${scheduleId}` + ) + .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/group4/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/index.ts index 8eb5a0c2006be..62741f615f6e9 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/index.ts @@ -27,6 +27,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC loadTestFile(require.resolve('./excluded')); loadTestFile(require.resolve('./snooze')); loadTestFile(require.resolve('./unsnooze')); + loadTestFile(require.resolve('./unsnooze_internal')); loadTestFile(require.resolve('./global_execution_log')); loadTestFile(require.resolve('./get_global_execution_kpi')); loadTestFile(require.resolve('./get_action_error_log')); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/unsnooze.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/unsnooze.ts index 8f509e6104c3d..e27172714cb7b 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/unsnooze.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/unsnooze.ts @@ -22,6 +22,7 @@ import { export default function createUnsnoozeRuleTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); + const NOW = new Date().toISOString(); describe('unsnooze', () => { const objectRemover = new ObjectRemover(supertest); @@ -62,7 +63,24 @@ export default function createUnsnoozeRuleTests({ getService }: FtrProviderConte .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); - const response = await alertUtils.getUnsnoozeRequest(createdAlert.id); + const { body: snoozeSchedule } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerting/rule/${createdAlert.id}/snooze_schedule`) + .set('kbn-xsrf', 'foo') + .set('content-type', 'application/json') + .send({ + schedule: { + custom: { + duration: '240h', + start: NOW, + recurring: { + occurrences: 1, + }, + }, + }, + }) + .expect(200); + + const response = await alertUtils.getUnsnoozeRequest(createdAlert.id, snoozeSchedule.id); switch (scenario.id) { case 'no_kibana_privileges at space1': @@ -122,7 +140,24 @@ export default function createUnsnoozeRuleTests({ getService }: FtrProviderConte .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); - const response = await alertUtils.getUnsnoozeRequest(createdAlert.id); + const { body: snoozeSchedule } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerting/rule/${createdAlert.id}/snooze_schedule`) + .set('kbn-xsrf', 'foo') + .set('content-type', 'application/json') + .send({ + schedule: { + custom: { + duration: '240h', + start: NOW, + recurring: { + occurrences: 1, + }, + }, + }, + }) + .expect(200); + + const response = await alertUtils.getUnsnoozeRequest(createdAlert.id, snoozeSchedule?.id); switch (scenario.id) { case 'no_kibana_privileges at space1': @@ -179,7 +214,24 @@ export default function createUnsnoozeRuleTests({ getService }: FtrProviderConte .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); - const response = await alertUtils.getUnsnoozeRequest(createdAlert.id); + const { body: snoozeSchedule } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerting/rule/${createdAlert.id}/snooze_schedule`) + .set('kbn-xsrf', 'foo') + .set('content-type', 'application/json') + .send({ + schedule: { + custom: { + duration: '240h', + start: NOW, + recurring: { + occurrences: 1, + }, + }, + }, + }) + .expect(200); + + const response = await alertUtils.getUnsnoozeRequest(createdAlert.id, snoozeSchedule?.id); switch (scenario.id) { case 'no_kibana_privileges at space1': @@ -236,7 +288,24 @@ export default function createUnsnoozeRuleTests({ getService }: FtrProviderConte .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); - const response = await alertUtils.getUnsnoozeRequest(createdAlert.id); + const { body: snoozeSchedule } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerting/rule/${createdAlert.id}/snooze_schedule`) + .set('kbn-xsrf', 'foo') + .set('content-type', 'application/json') + .send({ + schedule: { + custom: { + duration: '240h', + start: NOW, + recurring: { + occurrences: 1, + }, + }, + }, + }) + .expect(200); + + const response = await alertUtils.getUnsnoozeRequest(createdAlert.id, snoozeSchedule?.id); switch (scenario.id) { case 'no_kibana_privileges at space1': diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/unsnooze_internal.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/unsnooze_internal.ts new file mode 100644 index 0000000000000..052743ef5b189 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/unsnooze_internal.ts @@ -0,0 +1,287 @@ +/* + * 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 { RULE_SAVED_OBJECT_TYPE } from '@kbn/alerting-plugin/server'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { + AlertUtils, + checkAAD, + getUrlPrefix, + getTestRuleData, + ObjectRemover, + getUnauthorizedErrorMessage, +} from '../../../../common/lib'; + +// eslint-disable-next-line import/no-default-export +export default function createUnsnoozeRuleTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('unsnooze_internal', () => { + 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 unsnooze 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.getUnsnoozeInternalRequest(createdAlert.id); + + 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: getUnauthorizedErrorMessage('unsnooze', '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_schedule).to.eql([]); + expect(updatedAlert.mute_all).to.eql(false); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: RULE_SAVED_OBJECT_TYPE, + id: createdAlert.id, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle unsnooze 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.getUnsnoozeInternalRequest(createdAlert.id); + + 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: getUnauthorizedErrorMessage( + 'unsnooze', + '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_schedule).to.eql([]); + expect(updatedAlert.mute_all).to.eql(false); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: RULE_SAVED_OBJECT_TYPE, + id: createdAlert.id, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle unsnooze 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.getUnsnoozeInternalRequest(createdAlert.id); + + 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: getUnauthorizedErrorMessage( + 'unsnooze', + 'test.unrestricted-noop', + 'alertsFixture' + ), + statusCode: 403, + }); + break; + case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': + 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_schedule).to.eql([]); + expect(updatedAlert.mute_all).to.eql(false); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: RULE_SAVED_OBJECT_TYPE, + id: createdAlert.id, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle unsnooze 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.getUnsnoozeInternalRequest(createdAlert.id); + + 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: getUnauthorizedErrorMessage('unsnooze', '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: getUnauthorizedErrorMessage('unsnooze', 'test.restricted-noop', 'alerts'), + 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_schedule).to.eql([]); + expect(updatedAlert.mute_all).to.eql(false); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: RULE_SAVED_OBJECT_TYPE, + 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/group4/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/index.ts index b2753cb17245d..506a7cad0f68e 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/index.ts @@ -22,6 +22,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC loadTestFile(require.resolve('./event_log_alerts')); loadTestFile(require.resolve('./snooze')); loadTestFile(require.resolve('./unsnooze')); + loadTestFile(require.resolve('./unsnooze_internal')); loadTestFile(require.resolve('./bulk_edit')); loadTestFile(require.resolve('./bulk_disable')); loadTestFile(require.resolve('./capped_action_type')); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/unsnooze.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/unsnooze.ts index 80222af10f10c..2ee072b9f4d83 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/unsnooze.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/unsnooze.ts @@ -21,8 +21,9 @@ import { export default function createSnoozeRuleTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); + const NOW = new Date().toISOString(); - describe('unsnooze', function () { + describe('unsnooze_internal', function () { this.tags('skipFIPS'); const objectRemover = new ObjectRemover(supertest); @@ -60,7 +61,26 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); - const response = await alertUtils.getUnsnoozeRequest(createdAlert.id); + const { body: snoozeSchedule } = await supertest + .post( + `${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule/${createdAlert.id}/snooze_schedule` + ) + .set('kbn-xsrf', 'foo') + .set('content-type', 'application/json') + .send({ + schedule: { + custom: { + duration: '240h', + start: NOW, + recurring: { + occurrences: 1, + }, + }, + }, + }) + .expect(200); + + const response = await alertUtils.getUnsnoozeRequest(createdAlert.id, snoozeSchedule.id); expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/unsnooze_internal.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/unsnooze_internal.ts new file mode 100644 index 0000000000000..79744e6372bb4 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/unsnooze_internal.ts @@ -0,0 +1,83 @@ +/* + * 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 { RULE_SAVED_OBJECT_TYPE } from '@kbn/alerting-plugin/server'; +import { Spaces } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { + AlertUtils, + checkAAD, + getUrlPrefix, + getTestRuleData, + ObjectRemover, +} from '../../../../common/lib'; + +// eslint-disable-next-line import/no-default-export +export default function createSnoozeRuleTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('unsnooze_internal', function () { + this.tags('skipFIPS'); + const objectRemover = new ObjectRemover(supertest); + + after(() => objectRemover.removeAll()); + + const alertUtils = new AlertUtils({ space: Spaces.space1, supertestWithoutAuth }); + + it('should handle unsnooze 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.getUnsnoozeInternalRequest(createdAlert.id); + + 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.is_snoozed_until).to.eql(null); + expect(updatedAlert.snooze_schedule.length).to.eql(0); + expect(updatedAlert.mute_all).to.eql(false); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: Spaces.space1.id, + type: RULE_SAVED_OBJECT_TYPE, + id: createdAlert.id, + }); + }); + }); +} From 840e8d14e65d3a0655607c969d37ac7b80bbe7be Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Wed, 5 Mar 2025 14:16:20 +0000 Subject: [PATCH 3/6] clean up --- .../application/rule/methods/unsnooze/unsnooze_rule.test.ts | 4 ++-- .../server/application/rule/methods/unsnooze/unsnooze_rule.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/unsnooze/unsnooze_rule.test.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/unsnooze/unsnooze_rule.test.ts index 9ed1f81231c16..86ed085c723a5 100644 --- a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/unsnooze/unsnooze_rule.test.ts +++ b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/unsnooze/unsnooze_rule.test.ts @@ -69,7 +69,7 @@ describe('validate unsnooze params', () => { ); }); - it('should throw bad request for when snooze schedule is empty is route is public', async () => { + it('should throw bad request for when snooze schedule is empty for public route', async () => { savedObjectsMock.get = jest.fn().mockReturnValue({ attributes: { actions: [], @@ -79,7 +79,7 @@ describe('validate unsnooze params', () => { }); await expect( unsnoozeRule(context, { id: '123', scheduleIds: ['snooze_schedule_1'], isPublic: true }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"Rule has no snooze schedules to unsnooze."`); + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Rule has no snooze schedules."`); }); it('should throw bad request for invalid snooze schedule id for public route', async () => { diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/unsnooze/unsnooze_rule.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/unsnooze/unsnooze_rule.ts index ea4989d8a7feb..491f1919f57f6 100644 --- a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/unsnooze/unsnooze_rule.ts +++ b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/unsnooze/unsnooze_rule.ts @@ -54,7 +54,7 @@ async function unsnoozeWithOCC( if (isPublic && scheduleIds?.length) { if (!attributes.snoozeSchedule?.length) { - throw Boom.badRequest('Rule has no snooze schedules to unsnooze.'); + throw Boom.badRequest('Rule has no snooze schedules.'); } const scheduleToUnsnooze = attributes.snoozeSchedule?.find( From 00cce8824590192c5bbbee082f84513ddfbc0917 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 5 Mar 2025 14:35:55 +0000 Subject: [PATCH 4/6] [CI] Auto-commit changed files from 'node scripts/capture_oas_snapshot --include-path /api/status --include-path /api/alerting/rule/ --include-path /api/alerting/rules --include-path /api/actions --include-path /api/security/role --include-path /api/spaces --include-path /api/fleet --include-path /api/dashboards --update' --- oas_docs/bundle.json | 51 +++++++++++++++++++++++++++++++++ oas_docs/bundle.serverless.json | 51 +++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json index 64924423c40a5..4e1150601d930 100644 --- a/oas_docs/bundle.json +++ b/oas_docs/bundle.json @@ -4365,6 +4365,57 @@ ] } }, + "/api/alerting/rule/{ruleId}/snooze_schedule/{scheduleId}": { + "delete": { + "operationId": "delete-alerting-rule-ruleid-snooze-schedule-scheduleid", + "parameters": [ + { + "description": "A required header to protect against CSRF attacks", + "in": "header", + "name": "kbn-xsrf", + "required": true, + "schema": { + "example": "true", + "type": "string" + } + }, + { + "in": "path", + "name": "ruleId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "scheduleId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Indicates a successful call." + }, + "400": { + "description": "Indicates an invalid schema." + }, + "403": { + "description": "Indicates that this call is forbidden." + }, + "404": { + "description": "Indicates a rule with the given id does not exist." + } + }, + "summary": "Delete a snooze schedule for a rule", + "tags": [ + "alerting" + ] + } + }, "/api/alerting/rule/{rule_id}/alert/{alert_id}/_mute": { "post": { "operationId": "post-alerting-rule-rule-id-alert-alert-id-mute", diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json index 6d3e356f8d5f9..2a1aeedfb084e 100644 --- a/oas_docs/bundle.serverless.json +++ b/oas_docs/bundle.serverless.json @@ -4365,6 +4365,57 @@ ] } }, + "/api/alerting/rule/{ruleId}/snooze_schedule/{scheduleId}": { + "delete": { + "operationId": "delete-alerting-rule-ruleid-snooze-schedule-scheduleid", + "parameters": [ + { + "description": "A required header to protect against CSRF attacks", + "in": "header", + "name": "kbn-xsrf", + "required": true, + "schema": { + "example": "true", + "type": "string" + } + }, + { + "in": "path", + "name": "ruleId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "scheduleId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Indicates a successful call." + }, + "400": { + "description": "Indicates an invalid schema." + }, + "403": { + "description": "Indicates that this call is forbidden." + }, + "404": { + "description": "Indicates a rule with the given id does not exist." + } + }, + "summary": "Delete a snooze schedule for a rule", + "tags": [ + "alerting" + ] + } + }, "/api/alerting/rule/{rule_id}/alert/{alert_id}/_mute": { "post": { "operationId": "post-alerting-rule-rule-id-alert-alert-id-mute", From ae34c60244ca88de26b1d640504b88b6a761b617 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 5 Mar 2025 14:55:19 +0000 Subject: [PATCH 5/6] [CI] Auto-commit changed files from 'make api-docs' --- oas_docs/output/kibana.serverless.yaml | 34 ++++++++++++++++++++++++++ oas_docs/output/kibana.yaml | 33 +++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index 24af10be0be64..2304529aa0d60 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -3475,6 +3475,40 @@ paths: tags: - alerting x-beta: true + /api/alerting/rule/{ruleId}/snooze_schedule/{scheduleId}: + delete: + operationId: delete-alerting-rule-ruleid-snooze-schedule-scheduleid + parameters: + - description: A required header to protect against CSRF attacks + in: header + name: kbn-xsrf + required: true + schema: + example: 'true' + type: string + - in: path + name: ruleId + required: true + schema: + type: string + - in: path + name: scheduleId + required: true + schema: + type: string + responses: + '204': + description: Indicates a successful call. + '400': + description: Indicates an invalid schema. + '403': + description: Indicates that this call is forbidden. + '404': + description: Indicates a rule with the given id does not exist. + summary: Delete a snooze schedule for a rule + tags: + - alerting + x-beta: true /api/alerting/rules/_find: get: operationId: get-alerting-rules-find diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index 445efda44d4f9..15bbd9e00dc9a 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -3818,6 +3818,39 @@ paths: summary: Unmute an alert tags: - alerting + /api/alerting/rule/{ruleId}/snooze_schedule/{scheduleId}: + delete: + operationId: delete-alerting-rule-ruleid-snooze-schedule-scheduleid + parameters: + - description: A required header to protect against CSRF attacks + in: header + name: kbn-xsrf + required: true + schema: + example: 'true' + type: string + - in: path + name: ruleId + required: true + schema: + type: string + - in: path + name: scheduleId + required: true + schema: + type: string + responses: + '204': + description: Indicates a successful call. + '400': + description: Indicates an invalid schema. + '403': + description: Indicates that this call is forbidden. + '404': + description: Indicates a rule with the given id does not exist. + summary: Delete a snooze schedule for a rule + tags: + - alerting /api/alerting/rules/_find: get: operationId: get-alerting-rules-find From fa71994048196f71b1e478bc8c95800b4b274f7c Mon Sep 17 00:00:00 2001 From: Janki Salvi Date: Thu, 6 Mar 2025 14:55:31 +0000 Subject: [PATCH 6/6] handle errors at route level --- .../methods/unsnooze/unsnooze_rule.test.ts | 44 --------- .../rule/methods/unsnooze/unsnooze_rule.ts | 24 +---- .../external/unsnooze_rule_route.test.ts | 95 ++++++++++++++++++- .../unsnooze/external/unsnooze_rule_route.ts | 17 +++- .../tests/alerting/group4/unsnooze.ts | 64 ++++++++++++- 5 files changed, 172 insertions(+), 72 deletions(-) diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/unsnooze/unsnooze_rule.test.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/unsnooze/unsnooze_rule.test.ts index 86ed085c723a5..b4469bc1ce78f 100644 --- a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/unsnooze/unsnooze_rule.test.ts +++ b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/unsnooze/unsnooze_rule.test.ts @@ -68,48 +68,4 @@ describe('validate unsnooze params', () => { `"Error validating unsnooze params - [id]: expected value of type [string] but got [undefined]"` ); }); - - it('should throw bad request for when snooze schedule is empty for public route', async () => { - savedObjectsMock.get = jest.fn().mockReturnValue({ - attributes: { - actions: [], - snoozeSchedule: [], - }, - version: '9.0.0', - }); - await expect( - unsnoozeRule(context, { id: '123', scheduleIds: ['snooze_schedule_1'], isPublic: true }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"Rule has no snooze schedules."`); - }); - - it('should throw bad request for invalid snooze schedule id for public route', async () => { - savedObjectsMock.get = jest.fn().mockReturnValue({ - attributes: { - actions: [], - snoozeSchedule: [ - { - duration: 600000, - rRule: { - interval: 1, - freq: 3, - dtstart: '2025-03-01T06:30:37.011Z', - tzid: 'UTC', - }, - id: 'snooze_schedule_1', - }, - ], - }, - version: '9.0.0', - }); - - const invalidParams = { - id: '123', - scheduleIds: ['random_schedule_id'], - isPublic: true, - }; - - await expect(unsnoozeRule(context, invalidParams)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Rule has no snooze schedule with id random_schedule_id."` - ); - }); }); diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/unsnooze/unsnooze_rule.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/unsnooze/unsnooze_rule.ts index 491f1919f57f6..13a9a96b53ad4 100644 --- a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/unsnooze/unsnooze_rule.ts +++ b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/unsnooze/unsnooze_rule.ts @@ -21,24 +21,20 @@ import { unsnoozeRuleParamsSchema } from './schemas'; export interface UnsnoozeParams { id: string; scheduleIds?: string[]; - isPublic?: boolean; } export async function unsnoozeRule( context: RulesClientContext, - { id, scheduleIds, isPublic = false }: UnsnoozeParams + { id, scheduleIds }: UnsnoozeParams ): Promise { return await retryIfConflicts( context.logger, `rulesClient.unsnooze('${id}')`, - async () => await unsnoozeWithOCC(context, { id, scheduleIds, isPublic }) + async () => await unsnoozeWithOCC(context, { id, scheduleIds }) ); } -async function unsnoozeWithOCC( - context: RulesClientContext, - { id, scheduleIds, isPublic }: UnsnoozeParams -) { +async function unsnoozeWithOCC(context: RulesClientContext, { id, scheduleIds }: UnsnoozeParams) { try { unsnoozeRuleParamsSchema.validate({ id, scheduleIds }); } catch (error) { @@ -52,20 +48,6 @@ async function unsnoozeWithOCC( }) ); - if (isPublic && scheduleIds?.length) { - if (!attributes.snoozeSchedule?.length) { - throw Boom.badRequest('Rule has no snooze schedules.'); - } - - const scheduleToUnsnooze = attributes.snoozeSchedule?.find( - (schedule) => schedule.id === scheduleIds[0] - ); - - if (!scheduleToUnsnooze) { - throw Boom.badRequest(`Rule has no snooze schedule with id ${scheduleIds[0]}.`); - } - } - try { await context.authorization.ensureAuthorized({ ruleTypeId: attributes.alertTypeId, diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/external/unsnooze_rule_route.test.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/external/unsnooze_rule_route.test.ts index 195c55b10acf4..52d1aa6df1755 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/external/unsnooze_rule_route.test.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/external/unsnooze_rule_route.test.ts @@ -11,6 +11,7 @@ import { licenseStateMock } from '../../../../../lib/license_state.mock'; import { mockHandlerArguments } from '../../../../_mock_handler_arguments'; import { rulesClientMock } from '../../../../../rules_client.mock'; import { RuleTypeDisabledError } from '../../../../../lib/errors/rule_type_disabled'; +import { SanitizedRule } from '../../../../../types'; const rulesClient = rulesClientMock.create(); jest.mock('../../../../../lib/license_api_access', () => ({ @@ -22,6 +23,48 @@ beforeEach(() => { }); describe('unsnoozeAlertRoute', () => { + const mockedAlert: SanitizedRule<{ + bar: boolean; + }> = { + id: '1', + alertTypeId: '1', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + createdAt: new Date('2020-08-20T19:23:38Z'), + updatedAt: new Date('2020-08-20T19:23:38Z'), + actions: [], + snoozeSchedule: [ + { + id: 'snooze_schedule_1', + duration: 600000, + rRule: { + interval: 1, + freq: 3, + dtstart: '2025-03-01T06:30:37.011Z', + tzid: 'UTC', + }, + }, + ], + consumer: 'bar', + name: 'abc', + tags: ['foo'], + enabled: true, + muteAll: false, + notifyWhen: 'onActionGroupChange', + createdBy: '', + updatedBy: '', + apiKeyOwner: '', + throttle: '30s', + mutedInstanceIds: [], + executionStatus: { + status: 'unknown', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + }, + revision: 0, + }; + it('unsnoozes an alert', async () => { const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); @@ -34,6 +77,7 @@ describe('unsnoozeAlertRoute', () => { `"/api/alerting/rule/{ruleId}/snooze_schedule/{scheduleId}"` ); + rulesClient.get.mockResolvedValueOnce(mockedAlert); rulesClient.unsnooze.mockResolvedValueOnce(); const [context, req, res] = mockHandlerArguments( @@ -54,7 +98,6 @@ describe('unsnoozeAlertRoute', () => { Array [ Object { "id": "rule_1", - "isPublic": true, "scheduleIds": Array [ "snooze_schedule_1", ], @@ -68,6 +111,7 @@ describe('unsnoozeAlertRoute', () => { it('ensures the rule type gets validated for the license', async () => { const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); + rulesClient.get.mockResolvedValueOnce(mockedAlert); unsnoozeRuleRoute(router, licenseState); @@ -75,13 +119,54 @@ describe('unsnoozeAlertRoute', () => { rulesClient.unsnooze.mockRejectedValue(new RuleTypeDisabledError('Fail', 'license_invalid')); - const [context, req, res] = mockHandlerArguments({ rulesClient }, { params: {}, body: {} }, [ - 'ok', - 'forbidden', - ]); + const [context, req, res] = mockHandlerArguments( + { rulesClient }, + { params: { ruleId: 'rule_1', scheduleId: 'snooze_schedule_1' }, body: {} }, + ['ok', 'forbidden'] + ); await handler(context, req, res); expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); }); + + it('should throw bad request for when snooze schedule is empty', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + rulesClient.get.mockResolvedValueOnce({ ...mockedAlert, snoozeSchedule: [] }); + + unsnoozeRuleRoute(router, licenseState); + + const [, handler] = router.delete.mock.calls[0]; + + const [context, req, res] = mockHandlerArguments( + { rulesClient }, + { params: { ruleId: 'rule_1', scheduleId: 'snooze_schedule_1' }, body: {} }, + ['ok', 'forbidden'] + ); + + await expect(handler(context, req, res)).rejects.toMatchInlineSnapshot( + `[Error: Rule has no snooze schedules.]` + ); + }); + + it('should throw bad request for invalid snooze schedule id', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + rulesClient.get.mockResolvedValueOnce(mockedAlert); + + unsnoozeRuleRoute(router, licenseState); + + const [, handler] = router.delete.mock.calls[0]; + + const [context, req, res] = mockHandlerArguments( + { rulesClient }, + { params: { ruleId: 'rule_1', scheduleId: 'random_schedule_1' }, body: {} }, + ['ok', 'forbidden'] + ); + + await expect(handler(context, req, res)).rejects.toMatchInlineSnapshot( + `[Error: Rule has no snooze schedule with id random_schedule_1.]` + ); + }); }); diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/external/unsnooze_rule_route.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/external/unsnooze_rule_route.ts index 78f56b3b65d4d..67b8a93073c09 100644 --- a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/external/unsnooze_rule_route.ts +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/unsnooze/external/unsnooze_rule_route.ts @@ -5,6 +5,7 @@ * 2.0. */ +import Boom from '@hapi/boom'; import { IRouter } from '@kbn/core/server'; import { unsnoozeParamsSchema, @@ -54,7 +55,21 @@ export const unsnoozeRuleRoute = ( const rulesClient = await alertingContext.getRulesClient(); const { ruleId, scheduleId }: UnsnoozeParams = req.params; try { - await rulesClient.unsnooze({ id: ruleId, scheduleIds: [scheduleId], isPublic: true }); + const cuurentRule = await rulesClient.get({ id: ruleId }); + + if (!cuurentRule.snoozeSchedule?.length) { + throw Boom.badRequest('Rule has no snooze schedules.'); + } + + const scheduleToUnsnooze = cuurentRule.snoozeSchedule?.find( + (schedule) => schedule.id === scheduleId + ); + + if (!scheduleToUnsnooze) { + throw Boom.badRequest(`Rule has no snooze schedule with id ${scheduleId}.`); + } + + await rulesClient.unsnooze({ id: ruleId, scheduleIds: [scheduleId] }); return res.noContent(); } catch (e) { if (e instanceof RuleMutedError) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/unsnooze.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/unsnooze.ts index 2ee072b9f4d83..4a234eb545843 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/unsnooze.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/unsnooze.ts @@ -23,7 +23,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext const supertestWithoutAuth = getService('supertestWithoutAuth'); const NOW = new Date().toISOString(); - describe('unsnooze_internal', function () { + describe('unsnooze', function () { this.tags('skipFIPS'); const objectRemover = new ObjectRemover(supertest); @@ -99,5 +99,67 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext id: createdAlert.id, }); }); + + describe('validation', function () { + this.tags('skipFIPS'); + it('should return 400 for when rule has no snooze schedule', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + enabled: false, + actions: [], + }) + ) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); + + const response = await alertUtils.getUnsnoozeRequest(createdAlert.id, 'random_id'); + + expect(response.statusCode).to.eql(400); + expect(response.body.message).to.eql('Rule has no snooze schedules.'); + }); + + it('should return 400 for when invalid snooze schedule id', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + enabled: false, + actions: [], + }) + ) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); + + await supertest + .post( + `${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule/${createdAlert.id}/snooze_schedule` + ) + .set('kbn-xsrf', 'foo') + .set('content-type', 'application/json') + .send({ + schedule: { + custom: { + duration: '240h', + start: NOW, + recurring: { + occurrences: 1, + }, + }, + }, + }) + .expect(200); + + const response = await alertUtils.getUnsnoozeRequest(createdAlert.id, 'random_id'); + + expect(response.statusCode).to.eql(400); + expect(response.body.message).to.eql( + 'Rule has no snooze schedule with id random_schedule_1.' + ); + }); + }); }); }