Skip to content

Commit

Permalink
[ResponseOps][Rules] Version unmute alert route (#188830)
Browse files Browse the repository at this point in the history
## Summary

Versions the `POST
/api/alerting/rule/{rule_id}/alert/{alert_id}/_unmute` enpoint.

## References

Parent issue: #187572
Closes #187574

### Checklist

- [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: Antonio <antoniodcoelho@gmail.com>
  • Loading branch information
umbopepato and adcoelho authored Jul 30, 2024
1 parent fa6d4aa commit d88a1b4
Show file tree
Hide file tree
Showing 19 changed files with 227 additions and 74 deletions.
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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export { unmuteAlertParamsSchema } from './schemas/latest';
export { unmuteAlertParamsSchema as unmuteAlertParamsSchemaV1 } from './schemas/v1';

export type { UnmuteAlertRequestParams } from './types/latest';
export type { UnmuteAlertRequestParams as UnmuteAlertRequestParamsV1 } from './types/v1';
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* 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 { unmuteAlertParamsSchema } from './v1';
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* 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 unmuteAlertParamsSchema = schema.object({
rule_id: schema.string({
meta: {
description: 'The identifier for the rule.',
},
}),
alert_id: schema.string({
meta: {
description: 'The identifier for the alert.',
},
}),
});
Original file line number Diff line number Diff line change
@@ -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 type { UnmuteAlertRequestParams } from './v1';
Original file line number Diff line number Diff line change
@@ -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 type { TypeOf } from '@kbn/config-schema';
import { unmuteAlertParamsSchemaV1 } from '..';

export type UnmuteAlertRequestParams = TypeOf<typeof unmuteAlertParamsSchemaV1>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* 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 { unmuteAlertParamsSchema } from './unmute_alert_params_schema';
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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { schema } from '@kbn/config-schema';

export const unmuteAlertParamsSchema = schema.object({
alertId: schema.string(),
alertInstanceId: schema.string(),
});
Original file line number Diff line number Diff line change
@@ -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 type { UnmuteAlertParams } from './unmute_alert_params';
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* 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 { unmuteAlertParamsSchema } from '../schemas';

export type UnmuteAlertParams = TypeOf<typeof unmuteAlertParamsSchema>;
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,25 @@
* 2.0.
*/

import { RulesClient, ConstructorOptions } from '../rules_client';
import { RulesClient, ConstructorOptions } from '../../../../rules_client/rules_client';
import {
savedObjectsClientMock,
loggingSystemMock,
savedObjectsRepositoryMock,
uiSettingsServiceMock,
} from '@kbn/core/server/mocks';
import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks';
import { ruleTypeRegistryMock } from '../../rule_type_registry.mock';
import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock';
import { ruleTypeRegistryMock } from '../../../../rule_type_registry.mock';
import { alertingAuthorizationMock } from '../../../../authorization/alerting_authorization.mock';
import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
import { actionsAuthorizationMock } from '@kbn/actions-plugin/server/mocks';
import { AlertingAuthorization } from '../../authorization/alerting_authorization';
import { AlertingAuthorization } from '../../../../authorization/alerting_authorization';
import { ActionsAuthorization } from '@kbn/actions-plugin/server';
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
import { getBeforeSetup, setGlobalDate } from './lib';
import { ConnectorAdapterRegistry } from '../../connector_adapters/connector_adapter_registry';
import { RULE_SAVED_OBJECT_TYPE } from '../../saved_objects';
import { backfillClientMock } from '../../backfill_client/backfill_client.mock';
import { getBeforeSetup, setGlobalDate } from '../../../../rules_client/tests/lib';
import { ConnectorAdapterRegistry } from '../../../../connector_adapters/connector_adapter_registry';
import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects';
import { backfillClientMock } from '../../../../backfill_client/backfill_client.mock';

const taskManager = taskManagerMock.createStart();
const ruleTypeRegistry = ruleTypeRegistryMock.create();
Expand Down Expand Up @@ -203,6 +203,17 @@ describe('unmuteInstance()', () => {
ruleTypeId: 'myType',
});
});

test('throws an error if API params do not match the schema', async () => {
const rulesClient = new RulesClient(rulesClientParams);
await expect(
// @ts-expect-error: Wrong params for testing purposes
rulesClient.unmuteInstance({ alertId: 1 })
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Failed to validate params: [alertId]: expected value of type [string] but got [number]"`
);
expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled();
});
});

describe('auditLogger', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,43 @@
* 2.0.
*/

import { Rule, RawRule } from '../../types';
import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization';
import { retryIfConflicts } from '../../lib/retry_if_conflicts';
import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
import { MuteOptions } from '../types';
import { RulesClientContext } from '../types';
import { updateMeta } from '../lib';
import { RULE_SAVED_OBJECT_TYPE } from '../../saved_objects';
import Boom from '@hapi/boom';
import type { Rule } from '../../../../types';
import type { RulesClientContext } from '../../../../rules_client/types';
import type { UnmuteAlertParams } from './types';
import { retryIfConflicts } from '../../../../lib/retry_if_conflicts';
import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects';
import { ruleAuditEvent, RuleAuditAction } from '../../../../rules_client/common/audit_events';
import { unmuteAlertParamsSchema } from './schemas';
import { updateMeta } from '../../../../rules_client/lib';
import { updateRuleSo } from '../../../../data/rule';
import { WriteOperations, AlertingAuthorizationEntity } from '../../../../authorization';

export async function unmuteInstance(
context: RulesClientContext,
{ alertId, alertInstanceId }: MuteOptions
params: UnmuteAlertParams
): Promise<void> {
const ruleId = params.alertId;
try {
unmuteAlertParamsSchema.validate(params);
} catch (error) {
throw Boom.badRequest(`Failed to validate params: ${error.message}`);
}

return await retryIfConflicts(
context.logger,
`rulesClient.unmuteInstance('${alertId}')`,
async () => await unmuteInstanceWithOCC(context, { alertId, alertInstanceId })
`rulesClient.unmuteInstance('${ruleId}')`,
async () => await unmuteInstanceWithOCC(context, params)
);
}

async function unmuteInstanceWithOCC(
context: RulesClientContext,
{
alertId,
alertInstanceId,
}: {
alertId: string;
alertInstanceId: string;
}
{ alertId: ruleId, alertInstanceId }: UnmuteAlertParams
) {
const { attributes, version } = await context.unsecuredSavedObjectsClient.get<Rule>(
RULE_SAVED_OBJECT_TYPE,
alertId
ruleId
);

try {
Expand All @@ -54,7 +58,7 @@ async function unmuteInstanceWithOCC(
context.auditLogger?.log(
ruleAuditEvent({
action: RuleAuditAction.UNMUTE_ALERT,
savedObject: { type: RULE_SAVED_OBJECT_TYPE, id: alertId },
savedObject: { type: RULE_SAVED_OBJECT_TYPE, id: ruleId },
error,
})
);
Expand All @@ -65,23 +69,23 @@ async function unmuteInstanceWithOCC(
ruleAuditEvent({
action: RuleAuditAction.UNMUTE_ALERT,
outcome: 'unknown',
savedObject: { type: RULE_SAVED_OBJECT_TYPE, id: alertId },
savedObject: { type: RULE_SAVED_OBJECT_TYPE, id: ruleId },
})
);

context.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId);

const mutedInstanceIds = attributes.mutedInstanceIds || [];
if (!attributes.muteAll && mutedInstanceIds.includes(alertInstanceId)) {
await context.unsecuredSavedObjectsClient.update<RawRule>(
RULE_SAVED_OBJECT_TYPE,
alertId,
updateMeta(context, {
await updateRuleSo({
savedObjectsClient: context.unsecuredSavedObjectsClient,
savedObjectsUpdateOptions: { version },
id: ruleId,
updateRuleAttributes: updateMeta(context, {
mutedInstanceIds: mutedInstanceIds.filter((id: string) => id !== alertInstanceId),
updatedBy: await context.getUserName(),
updatedAt: new Date().toISOString(),
mutedInstanceIds: mutedInstanceIds.filter((id: string) => id !== alertInstanceId),
}),
{ version }
);
});
}
}
2 changes: 1 addition & 1 deletion x-pack/plugins/alerting/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { ruleTypesRoute } from './rule_types';
import { muteAllRuleRoute } from './mute_all_rule';
import { muteAlertRoute } from './rule/apis/mute_alert/mute_alert';
import { unmuteAllRuleRoute } from './unmute_all_rule';
import { unmuteAlertRoute } from './unmute_alert';
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';
Expand Down
Original file line number Diff line number Diff line change
@@ -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 { transformRequestParamsToApplication } from './transform_request_params_to_application/latest';
export { transformRequestParamsToApplication as transformRequestParamsToApplicationV1 } from './transform_request_params_to_application/v1';
Original file line number Diff line number Diff line change
@@ -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 { transformRequestParamsToApplication } from './v1';
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* 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 { transformRequestParamsToApplication } from '..';

describe('transformRequestParamsToApplication', () => {
it('changes the parameters case', () => {
const transformed = transformRequestParamsToApplication({
rule_id: 'test-rule-id',
alert_id: 'test-alert-id',
});
expect(transformed).toEqual({ alertId: 'test-rule-id', alertInstanceId: 'test-alert-id' });
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* 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 { UnmuteAlertParams } from '../../../../../../application/rule/methods/unmute_alert/types';
import { RewriteRequestCase } from '../../../../../lib';
import { UnmuteAlertRequestParamsV1 } from '../../../../../../../common/routes/rule/apis/unmute_alert';

export const transformRequestParamsToApplication: RewriteRequestCase<UnmuteAlertParams> = ({
rule_id: alertId,
alert_id: alertInstanceId,
}: UnmuteAlertRequestParamsV1) => ({
alertId,
alertInstanceId,
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
* 2.0.
*/

import { unmuteAlertRoute } from './unmute_alert';
import { unmuteAlertRoute } from './unmute_alert_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(),
}));

Expand Down
Loading

0 comments on commit d88a1b4

Please sign in to comment.