Skip to content

Commit a5f3352

Browse files
cnasikaselasticmachine
authored andcommitted
[ResponseOps][Rules] Prevent internally managed rule types to be snoozing/unsnoozing from the APIs (elastic#236678)
## Summary This PR prevents internally managed rule types from being snoozed/unsnoozed through the Rule Snooze and Unsnooze APIs. The prevention logic is on he route level because we want to support updating internally managed rules from the alerts client. Partial fixes: elastic#222101 ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
1 parent 06407e2 commit a5f3352

File tree

18 files changed

+561
-68
lines changed

18 files changed

+561
-68
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { validateInternalRuleType } from './validate_internal_rule_type';
9+
10+
describe('validateInternalRuleTypes', () => {
11+
const ruleTypeId = 'internal';
12+
const ruleTypes = new Map();
13+
const operationText = 'edit';
14+
15+
beforeEach(() => {
16+
ruleTypes.clear();
17+
});
18+
19+
it('should throw an error for invalid rule types', async () => {
20+
ruleTypes.set(ruleTypeId, { internallyManaged: true });
21+
22+
expect(() =>
23+
validateInternalRuleType({ ruleTypeId, ruleTypes, operationText })
24+
).toThrowErrorMatchingInlineSnapshot(
25+
`"Cannot edit rule of type \\"internal\\" because it is internally managed."`
26+
);
27+
});
28+
29+
it('should not throw an error for valid rule types', async () => {
30+
ruleTypes.set(ruleTypeId, { internallyManaged: false });
31+
32+
expect(() =>
33+
validateInternalRuleType({ ruleTypeId, ruleTypes, operationText })
34+
).not.toThrowError();
35+
});
36+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import Boom from '@hapi/boom';
9+
10+
interface ValidateRuleTypeParams {
11+
ruleTypeId: string;
12+
ruleTypes: Map<string, { internallyManaged?: boolean }>;
13+
operationText: string;
14+
}
15+
16+
export const validateInternalRuleType = ({
17+
ruleTypeId,
18+
ruleTypes,
19+
operationText,
20+
}: ValidateRuleTypeParams) => {
21+
const ruleType = ruleTypes.get(ruleTypeId);
22+
23+
/**
24+
* Throws a bad request (400) if the rule type is internallyManaged
25+
* ruleType will always exist here because ruleTypes.get will throw a 400
26+
* error if the rule type is not registered.
27+
*/
28+
if (ruleType?.internallyManaged) {
29+
throw Boom.badRequest(
30+
`Cannot ${operationText} rule of type "${ruleTypeId}" because it is internally managed.`
31+
);
32+
}
33+
};

x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/create/create_rule_route.test.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -948,14 +948,9 @@ describe('createRuleRoute', () => {
948948
['ok']
949949
);
950950

951-
await handler(context, req, res);
952-
953-
expect(res.badRequest).toHaveBeenCalledWith({
954-
body: {
955-
message:
956-
'Cannot create rule of type "test.internal-rule-type" because it is internally managed.',
957-
},
958-
});
951+
await expect(handler(context, req, res)).rejects.toThrowErrorMatchingInlineSnapshot(
952+
`"Cannot create rule of type \\"test.internal-rule-type\\" because it is internally managed."`
953+
);
959954
});
960955
});
961956
});

x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/create/create_rule_route.ts

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
handleDisabledApiKeysError,
2828
verifyAccessAndContext,
2929
} from '../../../lib';
30+
import { validateInternalRuleType } from '../../../lib/validate_internal_rule_type';
3031
import { transformRuleToRuleResponseV1 } from '../../transforms';
3132
import { validateRequiredGroupInDefaultActionsV1 } from '../../validation';
3233
import { transformCreateBodyV1 } from './transforms';
@@ -84,20 +85,11 @@ export const createRuleRoute = ({ router, licenseState, usageCounter }: RouteOpt
8485
});
8586

8687
try {
87-
const ruleType = ruleTypes.get(createRuleData.rule_type_id);
88-
89-
/**
90-
* Throws a bad request (400) if the rule type is internallyManaged
91-
* ruleType will always exist here because ruleTypes.get will throw a 400
92-
* error if the rule type is not registered.
93-
*/
94-
if (ruleType?.internallyManaged) {
95-
return res.badRequest({
96-
body: {
97-
message: `Cannot create rule of type "${createRuleData.rule_type_id}" because it is internally managed.`,
98-
},
99-
});
100-
}
88+
validateInternalRuleType({
89+
ruleTypeId: createRuleData.rule_type_id,
90+
ruleTypes,
91+
operationText: 'create',
92+
});
10193

10294
/**
10395
* Throws an error if the group is not defined in default actions

x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ rulesClient.update.mockResolvedValueOnce(mockedRule as unknown as SanitizedRule)
6868
describe('snoozeAlertRoute', () => {
6969
beforeEach(() => {
7070
jest.clearAllMocks();
71+
rulesClient.get = jest.fn().mockResolvedValue(mockedRule);
7172
});
7273

7374
it('snoozes a rule', async () => {
@@ -351,4 +352,44 @@ describe('snoozeAlertRoute', () => {
351352

352353
expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } });
353354
});
355+
356+
describe('internally managed rule types', () => {
357+
it('returns 400 if the rule type is internally managed', async () => {
358+
const licenseState = licenseStateMock.create();
359+
const router = httpServiceMock.createRouter();
360+
rulesClient.get = jest
361+
.fn()
362+
.mockResolvedValue({ ...mockedRule, alertTypeId: 'test.internal-rule-type' });
363+
364+
snoozeRuleRoute(router, licenseState);
365+
366+
const [config, handler] = router.post.mock.calls[0];
367+
368+
expect(config.path).toMatchInlineSnapshot(`"/api/alerting/rule/{id}/snooze_schedule"`);
369+
370+
rulesClient.snooze.mockResolvedValueOnce(mockedRule as unknown as SanitizedRule);
371+
372+
const [context, req, res] = mockHandlerArguments(
373+
{
374+
rulesClient, // @ts-expect-error: not all args are required for this test
375+
listTypes: new Map([
376+
['test.internal-rule-type', { id: 'test.internal-rule-type', internallyManaged: true }],
377+
]),
378+
},
379+
{
380+
params: {
381+
id: '1',
382+
},
383+
body: {
384+
schedule,
385+
},
386+
},
387+
['noContent']
388+
);
389+
390+
await expect(handler(context, req, res)).rejects.toThrowErrorMatchingInlineSnapshot(
391+
`"Cannot snooze rule of type \\"test.internal-rule-type\\" because it is internally managed."`
392+
);
393+
});
394+
});
354395
});

x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/external/snooze_rule_route.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import Boom from '@hapi/boom';
99
import { v4 } from 'uuid';
1010
import type { IRouter } from '@kbn/core/server';
11+
import { validateInternalRuleType } from '../../../../lib/validate_internal_rule_type';
1112
import {
1213
type SnoozeParamsV1,
1314
type SnoozeResponseV1,
@@ -71,6 +72,8 @@ export const snoozeRuleRoute = (
7172
verifyAccessAndContext(licenseState, async function (context, req, res) {
7273
const alertingContext = await context.alerting;
7374
const rulesClient = await alertingContext.getRulesClient();
75+
const ruleTypes = alertingContext.listTypes();
76+
7477
const params: SnoozeParamsV1 = req.params;
7578
const customSchedule = req.body.schedule?.custom;
7679

@@ -83,6 +86,14 @@ export const snoozeRuleRoute = (
8386
const snoozeScheduleId = v4();
8487

8588
try {
89+
const rule = await rulesClient.get({ id: params.id });
90+
91+
validateInternalRuleType({
92+
ruleTypeId: rule.alertTypeId,
93+
ruleTypes,
94+
operationText: 'snooze',
95+
});
96+
8697
const snoozedRule = await rulesClient.snooze({
8798
id: params.id,
8899
snoozeSchedule: {

x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.test.ts

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,6 @@ jest.mock('../../../../../lib/license_api_access', () => ({
1818
verifyApiAccess: jest.fn(),
1919
}));
2020

21-
beforeEach(() => {
22-
jest.resetAllMocks();
23-
});
24-
2521
const SNOOZE_SCHEDULE = {
2622
rRule: {
2723
dtstart: '2021-03-07T00:00:00.000Z',
@@ -62,6 +58,11 @@ const mockedRule = {
6258
};
6359

6460
describe('snoozeAlertRoute', () => {
61+
beforeEach(() => {
62+
jest.resetAllMocks();
63+
rulesClient.get = jest.fn().mockResolvedValue(mockedRule);
64+
});
65+
6566
it('snoozes an alert', async () => {
6667
const licenseState = licenseStateMock.create();
6768
const router = httpServiceMock.createRouter();
@@ -178,4 +179,44 @@ describe('snoozeAlertRoute', () => {
178179

179180
expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } });
180181
});
182+
183+
describe('internally managed rule types', () => {
184+
it('returns 400 if the rule type is internally managed', async () => {
185+
const licenseState = licenseStateMock.create();
186+
const router = httpServiceMock.createRouter();
187+
rulesClient.get = jest
188+
.fn()
189+
.mockResolvedValue({ ...mockedRule, alertTypeId: 'test.internal-rule-type' });
190+
191+
snoozeRuleRoute(router, licenseState);
192+
193+
const [config, handler] = router.post.mock.calls[0];
194+
195+
expect(config.path).toMatchInlineSnapshot(`"/internal/alerting/rule/{id}/_snooze"`);
196+
197+
rulesClient.snooze.mockResolvedValueOnce(mockedRule as unknown as SanitizedRule);
198+
199+
const [context, req, res] = mockHandlerArguments(
200+
{
201+
rulesClient, // @ts-expect-error: not all args are required for this test
202+
listTypes: new Map([
203+
['test.internal-rule-type', { id: 'test.internal-rule-type', internallyManaged: true }],
204+
]),
205+
},
206+
{
207+
params: {
208+
id: '1',
209+
},
210+
body: {
211+
snooze_schedule: SNOOZE_SCHEDULE,
212+
},
213+
},
214+
['noContent']
215+
);
216+
217+
await expect(handler(context, req, res)).rejects.toThrowErrorMatchingInlineSnapshot(
218+
`"Cannot snooze rule of type \\"test.internal-rule-type\\" because it is internally managed."`
219+
);
220+
});
221+
});
181222
});

x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/snooze/internal/snooze_rule_route.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import type { TypeOf } from '@kbn/config-schema';
99
import type { IRouter } from '@kbn/core/server';
10+
import { validateInternalRuleType } from '../../../../lib/validate_internal_rule_type';
1011
import {
1112
snoozeBodyInternalSchemaV1,
1213
snoozeParamsInternalSchemaV1,
@@ -39,9 +40,20 @@ export const snoozeRuleRoute = (
3940
verifyAccessAndContext(licenseState, async function (context, req, res) {
4041
const alertingContext = await context.alerting;
4142
const rulesClient = await alertingContext.getRulesClient();
43+
const ruleTypes = alertingContext.listTypes();
44+
4245
const params: SnoozeRuleRequestInternalParamsV1 = req.params;
4346
const body = transformSnoozeBodyV1(req.body);
47+
4448
try {
49+
const rule = await rulesClient.get({ id: params.id });
50+
51+
validateInternalRuleType({
52+
ruleTypeId: rule.alertTypeId,
53+
ruleTypes,
54+
operationText: 'snooze',
55+
});
56+
4557
await rulesClient.snooze({ ...params, ...body });
4658
return res.noContent();
4759
} catch (e) {

0 commit comments

Comments
 (0)