Skip to content

Commit

Permalink
Merging in main
Browse files Browse the repository at this point in the history
  • Loading branch information
ymao1 committed Mar 18, 2022
2 parents 553b61c + a467450 commit f0be261
Show file tree
Hide file tree
Showing 25 changed files with 922 additions and 8 deletions.
1 change: 1 addition & 0 deletions x-pack/plugins/alerting/common/alert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export interface Alert<Params extends AlertTypeParams = never> {
mutedInstanceIds: string[];
executionStatus: AlertExecutionStatus;
monitoring?: RuleMonitoring;
snoozeEndTime?: Date | null; // Remove ? when this parameter is made available in the public API
}

export type SanitizedAlert<Params extends AlertTypeParams = never> = Omit<Alert<Params>, 'apiKey'>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export enum WriteOperations {
UnmuteAll = 'unmuteAll',
MuteAlert = 'muteAlert',
UnmuteAlert = 'unmuteAlert',
Snooze = 'snooze',
}

export interface EnsureAuthorizedOpts {
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/alerting/server/lib/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
19 changes: 19 additions & 0 deletions x-pack/plugins/alerting/server/lib/errors/rule_muted.ts
Original file line number Diff line number Diff line change
@@ -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 } });
}
}
2 changes: 1 addition & 1 deletion x-pack/plugins/alerting/server/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export type {
ErrorThatHandlesItsOwnResponse,
ElasticsearchError,
} from './errors';
export { AlertTypeDisabledError, isErrorThatHandlesItsOwnResponse } from './errors';
export { AlertTypeDisabledError, RuleMutedError, isErrorThatHandlesItsOwnResponse } from './errors';
export {
executionStatusFromState,
executionStatusFromError,
Expand Down
13 changes: 13 additions & 0 deletions x-pack/plugins/alerting/server/lib/validate_snooze_date.ts
Original file line number Diff line number Diff line change
@@ -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;
};
3 changes: 3 additions & 0 deletions x-pack/plugins/alerting/server/routes/find_rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ const rewriteBodyRes: RewriteResponseCase<FindResult<AlertTypeParams>> = ({
executionStatus,
actions,
scheduledTaskId,
snoozeEndTime,
...rest
}) => ({
...rest,
Expand All @@ -97,6 +98,8 @@ const rewriteBodyRes: RewriteResponseCase<FindResult<AlertTypeParams>> = ({
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,
Expand Down
3 changes: 3 additions & 0 deletions x-pack/plugins/alerting/server/routes/get_rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const rewriteBodyRes: RewriteResponseCase<SanitizedAlert<AlertTypeParams>> = ({
executionStatus,
actions,
scheduledTaskId,
snoozeEndTime,
...rest
}) => ({
...rest,
Expand All @@ -47,6 +48,8 @@ const rewriteBodyRes: RewriteResponseCase<SanitizedAlert<AlertTypeParams>> = ({
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'),
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/alerting/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,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<AlertingRequestHandlerContext>;
Expand Down Expand Up @@ -63,4 +64,5 @@ export function defineRoutes(opts: RouteOptions) {
unmuteAllRuleRoute(router, licenseState);
unmuteAlertRoute(router, licenseState);
updateRuleApiKeyRoute(router, licenseState);
snoozeRuleRoute(router, licenseState);
}
134 changes: 134 additions & 0 deletions x-pack/plugins/alerting/server/routes/snooze_rule.test.ts
Original file line number Diff line number Diff line change
@@ -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' } });
});
});
62 changes: 62 additions & 0 deletions x-pack/plugins/alerting/server/routes/snooze_rule.ts
Original file line number Diff line number Diff line change
@@ -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<SnoozeOptions> = ({ snooze_end_time: snoozeEndTime }) => ({
snoozeEndTime,
});

export const snoozeRuleRoute = (
router: IRouter<AlertingRequestHandlerContext>,
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;
}
})
)
);
};
1 change: 1 addition & 0 deletions x-pack/plugins/alerting/server/rules_client.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const createRulesClientMock = () => {
getAlertSummary: jest.fn(),
getExecutionLogForRule: jest.fn(),
getSpaceId: jest.fn(),
snooze: jest.fn(),
};
return mocked;
};
Expand Down
3 changes: 3 additions & 0 deletions x-pack/plugins/alerting/server/rules_client/audit_events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export enum RuleAuditAction {
UNMUTE_ALERT = 'rule_alert_unmute',
AGGREGATE = 'rule_aggregate',
GET_EXECUTION_LOG = 'rule_get_execution_log',
SNOOZE = 'rule_snooze',
}

type VerbsTuple = [string, string, string];
Expand All @@ -48,6 +49,7 @@ const eventVerbs: Record<RuleAuditAction, VerbsTuple> = {
'accessing execution log for',
'accessed execution log for',
],
rule_snooze: ['snooze', 'snoozing', 'snoozed'],
};

const eventTypes: Record<RuleAuditAction, EcsEventType> = {
Expand All @@ -66,6 +68,7 @@ const eventTypes: Record<RuleAuditAction, EcsEventType> = {
rule_alert_unmute: 'change',
rule_aggregate: 'access',
rule_get_execution_log: 'access',
rule_snooze: 'change',
};

export interface RuleAuditEventParams {
Expand Down
Loading

0 comments on commit f0be261

Please sign in to comment.