Skip to content

Commit

Permalink
[Alerting] Update rules detail page to resolve SO IDs if necessary (e…
Browse files Browse the repository at this point in the history
…lastic#108091)

* Adding internal resolve API to resolve rules given an ID

* Updating after merge

* Updating after merge

* Adding resolveRule api to client and adding spacesOss plugin dependency

* Handling 404 errors by calling resolve. Updating unit tests

* Handling aliasMatch and conflict results

* Fixing types

* Unit tests for spaces oss components

* Adding audit event for resolve

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
ymao1 and kibanamachine committed Aug 19, 2021
1 parent 0860203 commit cc179cb
Show file tree
Hide file tree
Showing 22 changed files with 1,287 additions and 112 deletions.
8 changes: 7 additions & 1 deletion x-pack/plugins/alerting/common/alert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
* 2.0.
*/

import { SavedObjectAttribute, SavedObjectAttributes } from 'kibana/server';
import {
SavedObjectAttribute,
SavedObjectAttributes,
SavedObjectsResolveResponse,
} from 'kibana/server';
import { AlertNotifyWhenType } from './alert_notify_when_type';

export type AlertTypeState = Record<string, unknown>;
Expand Down Expand Up @@ -76,6 +80,8 @@ export interface Alert<Params extends AlertTypeParams = never> {
}

export type SanitizedAlert<Params extends AlertTypeParams = never> = Omit<Alert<Params>, 'apiKey'>;
export type ResolvedSanitizedRule<Params extends AlertTypeParams = never> = SanitizedAlert<Params> &
Omit<SavedObjectsResolveResponse, 'saved_object'>;

export type SanitizedRuleConfig = Pick<
SanitizedAlert,
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 @@ -22,6 +22,7 @@ import { findRulesRoute } from './find_rules';
import { getRuleAlertSummaryRoute } from './get_rule_alert_summary';
import { getRuleStateRoute } from './get_rule_state';
import { healthRoute } from './health';
import { resolveRuleRoute } from './resolve_rule';
import { ruleTypesRoute } from './rule_types';
import { muteAllRuleRoute } from './mute_all_rule';
import { muteAlertRoute } from './mute_alert';
Expand All @@ -42,6 +43,7 @@ export function defineRoutes(opts: RouteOptions) {
defineLegacyRoutes(opts);
createRuleRoute(opts);
getRuleRoute(router, licenseState);
resolveRuleRoute(router, licenseState);
updateRuleRoute(router, licenseState);
deleteRuleRoute(router, licenseState);
aggregateRulesRoute(router, licenseState);
Expand Down
182 changes: 182 additions & 0 deletions x-pack/plugins/alerting/server/routes/resolve_rule.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/*
* 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 { resolveRuleRoute } from './resolve_rule';
import { httpServiceMock } from 'src/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 { ResolvedSanitizedRule } from '../types';
import { AsApiContract } from './lib';

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

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

describe('resolveRuleRoute', () => {
const mockedRule: ResolvedSanitizedRule<{
bar: boolean;
}> = {
id: '1',
alertTypeId: '1',
schedule: { interval: '10s' },
params: {
bar: true,
},
createdAt: new Date(),
updatedAt: new Date(),
actions: [
{
group: 'default',
id: '2',
actionTypeId: 'test',
params: {
foo: true,
},
},
],
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'),
},
outcome: 'aliasMatch',
alias_target_id: '2',
};

const resolveResult: AsApiContract<ResolvedSanitizedRule<{ bar: boolean }>> = {
...pick(
mockedRule,
'consumer',
'name',
'schedule',
'tags',
'params',
'throttle',
'enabled',
'alias_target_id'
),
rule_type_id: mockedRule.alertTypeId,
notify_when: mockedRule.notifyWhen,
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: [
{
group: mockedRule.actions[0].group,
id: mockedRule.actions[0].id,
params: mockedRule.actions[0].params,
connector_type_id: mockedRule.actions[0].actionTypeId,
},
],
outcome: 'aliasMatch',
};

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

resolveRuleRoute(router, licenseState);
const [config, handler] = router.get.mock.calls[0];

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

rulesClient.resolve.mockResolvedValueOnce(mockedRule);

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

expect(rulesClient.resolve).toHaveBeenCalledTimes(1);
expect(rulesClient.resolve.mock.calls[0][0].id).toEqual('1');

expect(res.ok).toHaveBeenCalledWith({
body: resolveResult,
});
});

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

resolveRuleRoute(router, licenseState);

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

rulesClient.resolve.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 getting rules', async () => {
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();

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

resolveRuleRoute(router, licenseState);

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

rulesClient.resolve.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);
});
});
84 changes: 84 additions & 0 deletions x-pack/plugins/alerting/server/routes/resolve_rule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* 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 { omit } from 'lodash';
import { schema } from '@kbn/config-schema';
import { IRouter } from 'kibana/server';
import { ILicenseState } from '../lib';
import { verifyAccessAndContext, RewriteResponseCase } from './lib';
import {
AlertTypeParams,
AlertingRequestHandlerContext,
INTERNAL_BASE_ALERTING_API_PATH,
ResolvedSanitizedRule,
} from '../types';

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

const rewriteBodyRes: RewriteResponseCase<ResolvedSanitizedRule<AlertTypeParams>> = ({
alertTypeId,
createdBy,
updatedBy,
createdAt,
updatedAt,
apiKeyOwner,
notifyWhen,
muteAll,
mutedInstanceIds,
executionStatus,
actions,
scheduledTaskId,
...rest
}) => ({
...rest,
rule_type_id: alertTypeId,
created_by: createdBy,
updated_by: updatedBy,
created_at: createdAt,
updated_at: updatedAt,
api_key_owner: apiKeyOwner,
notify_when: notifyWhen,
mute_all: muteAll,
muted_alert_ids: mutedInstanceIds,
scheduled_task_id: scheduledTaskId,
execution_status: executionStatus && {
...omit(executionStatus, 'lastExecutionDate'),
last_execution_date: executionStatus.lastExecutionDate,
},
actions: actions.map(({ group, id, actionTypeId, params }) => ({
group,
id,
params,
connector_type_id: actionTypeId,
})),
});

export const resolveRuleRoute = (
router: IRouter<AlertingRequestHandlerContext>,
licenseState: ILicenseState
) => {
router.get(
{
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rule/{id}/_resolve`,
validate: {
params: paramSchema,
},
},
router.handleLegacyErrors(
verifyAccessAndContext(licenseState, async function (context, req, res) {
const rulesClient = context.alerting.getRulesClient();
const { id } = req.params;
const rule = await rulesClient.resolve({ id });
return res.ok({
body: rewriteBodyRes(rule),
});
})
)
);
};
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 @@ -16,6 +16,7 @@ const createRulesClientMock = () => {
aggregate: jest.fn(),
create: jest.fn(),
get: jest.fn(),
resolve: jest.fn(),
getAlertState: jest.fn(),
find: jest.fn(),
delete: jest.fn(),
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 @@ -11,6 +11,7 @@ import { AuditEvent } from '../../../security/server';
export enum RuleAuditAction {
CREATE = 'rule_create',
GET = 'rule_get',
RESOLVE = 'rule_resolve',
UPDATE = 'rule_update',
UPDATE_API_KEY = 'rule_update_api_key',
ENABLE = 'rule_enable',
Expand All @@ -28,6 +29,7 @@ type VerbsTuple = [string, string, string];
const eventVerbs: Record<RuleAuditAction, VerbsTuple> = {
rule_create: ['create', 'creating', 'created'],
rule_get: ['access', 'accessing', 'accessed'],
rule_resolve: ['access', 'accessing', 'accessed'],
rule_update: ['update', 'updating', 'updated'],
rule_update_api_key: ['update API key of', 'updating API key of', 'updated API key of'],
rule_enable: ['enable', 'enabling', 'enabled'],
Expand All @@ -43,6 +45,7 @@ const eventVerbs: Record<RuleAuditAction, VerbsTuple> = {
const eventTypes: Record<RuleAuditAction, EcsEventType> = {
rule_create: 'creation',
rule_get: 'access',
rule_resolve: 'access',
rule_update: 'change',
rule_update_api_key: 'change',
rule_enable: 'change',
Expand Down
47 changes: 47 additions & 0 deletions x-pack/plugins/alerting/server/rules_client/rules_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
AlertExecutionStatusValues,
AlertNotifyWhenType,
AlertTypeParams,
ResolvedSanitizedRule,
} from '../types';
import {
validateAlertTypeParams,
Expand Down Expand Up @@ -411,6 +412,52 @@ export class RulesClient {
);
}

public async resolve<Params extends AlertTypeParams = never>({
id,
}: {
id: string;
}): Promise<ResolvedSanitizedRule<Params>> {
const {
saved_object: result,
...resolveResponse
} = await this.unsecuredSavedObjectsClient.resolve<RawAlert>('alert', id);
try {
await this.authorization.ensureAuthorized({
ruleTypeId: result.attributes.alertTypeId,
consumer: result.attributes.consumer,
operation: ReadOperations.Get,
entity: AlertingAuthorizationEntity.Rule,
});
} catch (error) {
this.auditLogger?.log(
ruleAuditEvent({
action: RuleAuditAction.RESOLVE,
savedObject: { type: 'alert', id },
error,
})
);
throw error;
}
this.auditLogger?.log(
ruleAuditEvent({
action: RuleAuditAction.RESOLVE,
savedObject: { type: 'alert', id },
})
);

const rule = this.getAlertFromRaw<Params>(
result.id,
result.attributes.alertTypeId,
result.attributes,
result.references
);

return {
...rule,
...resolveResponse,
};
}

public async getAlertState({ id }: { id: string }): Promise<AlertTaskState | void> {
const alert = await this.get({ id });
await this.authorization.ensureAuthorized({
Expand Down
Loading

0 comments on commit cc179cb

Please sign in to comment.