Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/main' into investigation-guide…
Browse files Browse the repository at this point in the history
…-insights
  • Loading branch information
kqualters-elastic committed Nov 16, 2022
2 parents b5fa33d + dd1ad53 commit 89bdd49
Show file tree
Hide file tree
Showing 75 changed files with 2,862 additions and 690 deletions.
1 change: 1 addition & 0 deletions packages/kbn-rule-data-utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export * from './src/technical_field_names';
export * from './src/alerts_as_data_rbac';
export * from './src/alerts_as_data_severity';
export * from './src/alerts_as_data_status';
export * from './src/routes/stack_rule_paths';
12 changes: 12 additions & 0 deletions packages/kbn-rule-data-utils/src/routes/stack_rule_paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

export const ruleDetailsRoute = '/rule/:ruleId' as const;
export const triggersActionsRoute = '/app/management/insightsAndAlerting/triggersActions' as const;

export const getRuleDetailsRoute = (ruleId: string) => ruleDetailsRoute.replace(':ruleId', ruleId);
214 changes: 214 additions & 0 deletions x-pack/plugins/alerting/server/routes/clone_rule.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
/*
* 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 { pick } from 'lodash';
import { httpServiceMock } from '@kbn/core/server/mocks';
import { licenseStateMock } from '../lib/license_state.mock';
import { verifyApiAccess } from '../lib/license_api_access';
import { mockHandlerArguments } from './_mock_handler_arguments';
import { rulesClientMock } from '../rules_client.mock';
import { RuleTypeDisabledError } from '../lib/errors/rule_type_disabled';
import { cloneRuleRoute } from './clone_rule';
import { SanitizedRule } from '../types';
import { AsApiContract } from './lib';
import { CreateOptions } from '../rules_client';

const rulesClient = rulesClientMock.create();
jest.mock('../lib/license_api_access', () => ({
verifyApiAccess: jest.fn(),
}));

beforeEach(() => {
jest.resetAllMocks();
});

describe('cloneRuleRoute', () => {
const createdAt = new Date();
const updatedAt = new Date();

const mockedRule: SanitizedRule<{ bar: boolean }> = {
alertTypeId: '1',
consumer: 'bar',
name: 'abc',
schedule: { interval: '10s' },
tags: ['foo'],
params: {
bar: true,
},
throttle: '30s',
actions: [
{
actionTypeId: 'test',
group: 'default',
id: '2',
params: {
foo: true,
},
},
],
enabled: true,
muteAll: false,
createdBy: '',
updatedBy: '',
apiKeyOwner: '',
mutedInstanceIds: [],
notifyWhen: 'onActionGroupChange',
createdAt,
updatedAt,
id: '123',
executionStatus: {
status: 'unknown',
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
},
};

const ruleToClone: AsApiContract<CreateOptions<{ bar: boolean }>['data']> = {
...pick(mockedRule, 'consumer', 'name', 'schedule', 'tags', 'params', 'throttle', 'enabled'),
rule_type_id: mockedRule.alertTypeId,
notify_when: mockedRule.notifyWhen,
actions: [
{
group: mockedRule.actions[0].group,
id: mockedRule.actions[0].id,
params: mockedRule.actions[0].params,
},
],
};

const cloneResult: AsApiContract<SanitizedRule<{ bar: boolean }>> = {
...ruleToClone,
mute_all: mockedRule.muteAll,
created_by: mockedRule.createdBy,
updated_by: mockedRule.updatedBy,
api_key_owner: mockedRule.apiKeyOwner,
muted_alert_ids: mockedRule.mutedInstanceIds,
created_at: mockedRule.createdAt,
updated_at: mockedRule.updatedAt,
id: mockedRule.id,
execution_status: {
status: mockedRule.executionStatus.status,
last_execution_date: mockedRule.executionStatus.lastExecutionDate,
},
actions: [
{
...ruleToClone.actions[0],
connector_type_id: 'test',
},
],
};

it('clone a rule with proper parameters', async () => {
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();

cloneRuleRoute(router, licenseState);

const [config, handler] = router.post.mock.calls[0];

expect(config.path).toMatchInlineSnapshot(`"/internal/alerting/rule/{id}/_clone/{newId?}"`);

rulesClient.clone.mockResolvedValueOnce(mockedRule);

const [context, req, res] = mockHandlerArguments(
{ rulesClient },
{
params: {
id: '1',
},
},
['ok']
);

expect(await handler(context, req, res)).toEqual({ body: cloneResult });

expect(rulesClient.clone).toHaveBeenCalledTimes(1);
expect(rulesClient.clone.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"1",
Object {
"newId": undefined,
},
]
`);

expect(res.ok).toHaveBeenCalled();
});

it('ensures the license allows updating rules', async () => {
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();

cloneRuleRoute(router, licenseState);

const [, handler] = router.post.mock.calls[0];

rulesClient.clone.mockResolvedValueOnce(mockedRule);

const [context, req, res] = mockHandlerArguments(
{ rulesClient },
{
params: {
id: '1',
},
},
['ok']
);

await handler(context, req, res);

expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
});

it('ensures the license check prevents updating rules', async () => {
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();

(verifyApiAccess as jest.Mock).mockImplementation(() => {
throw new Error('OMG');
});

cloneRuleRoute(router, licenseState);

const [, handler] = router.post.mock.calls[0];

rulesClient.clone.mockResolvedValueOnce(mockedRule);

const [context, req, res] = mockHandlerArguments(
{ rulesClient },
{
params: {
id: '1',
},
},
['ok']
);

expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`);

expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
});

it('ensures the rule type gets validated for the license', async () => {
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();

cloneRuleRoute(router, licenseState);

const [, handler] = router.post.mock.calls[0];

rulesClient.clone.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' } });
});
});
115 changes: 115 additions & 0 deletions x-pack/plugins/alerting/server/routes/clone_rule.ts
Original file line number Diff line number Diff line change
@@ -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 { schema } from '@kbn/config-schema';
import { IRouter } from '@kbn/core/server';
import { ILicenseState, RuleTypeDisabledError } from '../lib';
import {
verifyAccessAndContext,
RewriteResponseCase,
handleDisabledApiKeysError,
rewriteRuleLastRun,
} from './lib';
import {
RuleTypeParams,
AlertingRequestHandlerContext,
INTERNAL_BASE_ALERTING_API_PATH,
PartialRule,
} from '../types';

const paramSchema = schema.object({
id: schema.string(),
newId: schema.maybe(schema.string()),
});

const rewriteBodyRes: RewriteResponseCase<PartialRule<RuleTypeParams>> = ({
actions,
alertTypeId,
scheduledTaskId,
createdBy,
updatedBy,
createdAt,
updatedAt,
apiKeyOwner,
notifyWhen,
muteAll,
mutedInstanceIds,
executionStatus,
snoozeSchedule,
isSnoozedUntil,
lastRun,
nextRun,
...rest
}) => ({
...rest,
api_key_owner: apiKeyOwner,
created_by: createdBy,
updated_by: updatedBy,
snooze_schedule: snoozeSchedule,
...(isSnoozedUntil ? { is_snoozed_until: isSnoozedUntil } : {}),
...(alertTypeId ? { rule_type_id: alertTypeId } : {}),
...(scheduledTaskId ? { scheduled_task_id: scheduledTaskId } : {}),
...(createdAt ? { created_at: createdAt } : {}),
...(updatedAt ? { updated_at: updatedAt } : {}),
...(notifyWhen ? { notify_when: notifyWhen } : {}),
...(muteAll !== undefined ? { mute_all: muteAll } : {}),
...(mutedInstanceIds ? { muted_alert_ids: mutedInstanceIds } : {}),
...(executionStatus
? {
execution_status: {
status: executionStatus.status,
last_execution_date: executionStatus.lastExecutionDate,
last_duration: executionStatus.lastDuration,
},
}
: {}),
...(actions
? {
actions: actions.map(({ group, id, actionTypeId, params }) => ({
group,
id,
params,
connector_type_id: actionTypeId,
})),
}
: {}),
...(lastRun ? { last_run: rewriteRuleLastRun(lastRun) } : {}),
...(nextRun ? { next_run: nextRun } : {}),
});

export const cloneRuleRoute = (
router: IRouter<AlertingRequestHandlerContext>,
licenseState: ILicenseState
) => {
router.post(
{
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rule/{id}/_clone/{newId?}`,
validate: {
params: paramSchema,
},
},
handleDisabledApiKeysError(
router.handleLegacyErrors(
verifyAccessAndContext(licenseState, async function (context, req, res) {
const rulesClient = (await context.alerting).getRulesClient();
const { id, newId } = req.params;
try {
const cloneRule = await rulesClient.clone(id, { newId });
return res.ok({
body: rewriteBodyRes(cloneRule),
});
} catch (e) {
if (e instanceof RuleTypeDisabledError) {
return e.sendResponse(res);
}
throw e;
}
})
)
)
);
};
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 @@ -39,6 +39,7 @@ import { snoozeRuleRoute } from './snooze_rule';
import { unsnoozeRuleRoute } from './unsnooze_rule';
import { runSoonRoute } from './run_soon';
import { bulkDeleteRulesRoute } from './bulk_delete_rules';
import { cloneRuleRoute } from './clone_rule';

export interface RouteOptions {
router: IRouter<AlertingRequestHandlerContext>;
Expand Down Expand Up @@ -81,4 +82,5 @@ export function defineRoutes(opts: RouteOptions) {
snoozeRuleRoute(router, licenseState);
unsnoozeRuleRoute(router, licenseState);
runSoonRoute(router, licenseState);
cloneRuleRoute(router, licenseState);
}
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 @@ -43,6 +43,7 @@ const createRulesClientMock = () => {
calculateIsSnoozedUntil: jest.fn(),
clearExpiredSnoozes: jest.fn(),
runSoon: jest.fn(),
clone: jest.fn(),
};
return mocked;
};
Expand Down
Loading

0 comments on commit 89bdd49

Please sign in to comment.