diff --git a/x-pack/plugins/alerting/server/application/rule/methods/bulk_delete/bulk_delete_rules.test.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_delete/bulk_delete_rules.test.ts index 40ac0e02a3d8e..0b9b7ea8c3131 100644 --- a/x-pack/plugins/alerting/server/application/rule/methods/bulk_delete/bulk_delete_rules.test.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_delete/bulk_delete_rules.test.ts @@ -14,6 +14,7 @@ import { actionsAuthorizationMock } from '@kbn/actions-plugin/server/mocks'; import { ActionsAuthorization } from '@kbn/actions-plugin/server'; import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks'; import { loggerMock } from '@kbn/logging-mocks'; +import { ActionsClient } from '@kbn/actions-plugin/server'; import { ruleTypeRegistryMock } from '../../../../rule_type_registry.mock'; import { alertingAuthorizationMock } from '../../../../authorization/alerting_authorization.mock'; import { RecoveredActionGroup } from '../../../../../common'; @@ -28,6 +29,10 @@ import { returnedRuleForBulkOps2, returnedRuleForBulkOps3, siemRuleForBulkOps1, + enabledRuleForBulkOpsWithActions1, + enabledRuleForBulkOpsWithActions2, + returnedRuleForBulkEnableWithActions1, + returnedRuleForBulkEnableWithActions2, } from '../../../../rules_client/tests/test_helpers'; import { migrateLegacyActions } from '../../../../rules_client/lib'; import { ConnectorAdapterRegistry } from '../../../../connector_adapters/connector_adapter_registry'; @@ -106,11 +111,12 @@ setGlobalDate(); describe('bulkDelete', () => { let rulesClient: RulesClient; + let actionsClient: jest.Mocked; const mockCreatePointInTimeFinderAsInternalUser = ( response = { saved_objects: [enabledRuleForBulkOps1, enabledRuleForBulkOps2, enabledRuleForBulkOps3], - } + } as unknown ) => { encryptedSavedObjects.createPointInTimeFinderDecryptedAsInternalUser = jest .fn() @@ -162,6 +168,48 @@ describe('bulkDelete', () => { }, validLegacyConsumers: [], }); + + actionsClient = (await rulesClientParams.getActionsClient()) as jest.Mocked; + actionsClient.isSystemAction.mockImplementation((id: string) => id === 'system_action:id'); + rulesClientParams.getActionsClient.mockResolvedValue(actionsClient); + }); + + test('should successfully delete two rule and return right actions', async () => { + mockCreatePointInTimeFinderAsInternalUser({ + saved_objects: [enabledRuleForBulkOpsWithActions1, enabledRuleForBulkOpsWithActions2], + }); + unsecuredSavedObjectsClient.bulkDelete.mockResolvedValue({ + statuses: [ + { id: 'id1', type: 'alert', success: true }, + { id: 'id2', type: 'alert', success: true }, + ], + }); + + const result = await rulesClient.bulkDeleteRules({ filter: 'fake_filter' }); + + expect(unsecuredSavedObjectsClient.bulkDelete).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.bulkDelete).toHaveBeenCalledWith( + [enabledRuleForBulkOps1, enabledRuleForBulkOps2].map(({ id }) => ({ + id, + type: 'alert', + })), + undefined + ); + + expect(taskManager.bulkRemove).toHaveBeenCalledTimes(1); + expect(taskManager.bulkRemove).toHaveBeenCalledWith(['id1', 'id2']); + expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledTimes(1); + expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledWith( + { apiKeys: ['MTIzOmFiYw==', 'MzIxOmFiYw=='] }, + expect.anything(), + expect.anything() + ); + expect(result).toStrictEqual({ + rules: [returnedRuleForBulkEnableWithActions1, returnedRuleForBulkEnableWithActions2], + errors: [], + total: 2, + taskIdsFailedToBeDeleted: [], + }); }); test('should try to delete rules, two successful and one with 500 error', async () => { diff --git a/x-pack/plugins/alerting/server/application/rule/methods/bulk_delete/bulk_delete_rules.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_delete/bulk_delete_rules.ts index 33da0fd6b2e07..1d87eed7f6661 100644 --- a/x-pack/plugins/alerting/server/application/rule/methods/bulk_delete/bulk_delete_rules.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_delete/bulk_delete_rules.ts @@ -49,6 +49,7 @@ export const bulkDeleteRules = async ( } const { ids, filter } = options; + const actionsClient = await context.getActionsClient(); const kueryNodeFilter = ids ? convertRuleIdsToKueryNode(ids) : buildKueryNodeFilter(filter); const authorizationFilter = await getAuthorizationFilter(context, { action: 'DELETE' }); @@ -95,13 +96,17 @@ export const bulkDeleteRules = async ( // fix the type cast from SavedObjectsBulkUpdateObject to SavedObjectsBulkUpdateObject // when we are doing the bulk delete and this should fix itself const ruleType = context.ruleTypeRegistry.get(attributes.alertTypeId!); - const ruleDomain = transformRuleAttributesToRuleDomain(attributes as RuleAttributes, { - id, - logger: context.logger, - ruleType, - references, - omitGeneratedValues: false, - }); + const ruleDomain = transformRuleAttributesToRuleDomain( + attributes as RuleAttributes, + { + id, + logger: context.logger, + ruleType, + references, + omitGeneratedValues: false, + }, + (connectorId: string) => actionsClient.isSystemAction(connectorId) + ); try { ruleDomainSchema.validate(ruleDomain); diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/bulk_delete/bulk_delete_rules_route.test.ts b/x-pack/plugins/alerting/server/routes/rule/apis/bulk_delete/bulk_delete_rules_route.test.ts index 21099f6ba6086..6b1e55b7f750c 100644 --- a/x-pack/plugins/alerting/server/routes/rule/apis/bulk_delete/bulk_delete_rules_route.test.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/bulk_delete/bulk_delete_rules_route.test.ts @@ -6,13 +6,19 @@ */ import { httpServiceMock } from '@kbn/core/server/mocks'; - +import { actionsClientMock } from '@kbn/actions-plugin/server/mocks'; import { bulkDeleteRulesRoute } from './bulk_delete_rules_route'; 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 { verifyApiAccess } from '../../../../lib/license_api_access'; +import { + RuleActionTypes, + RuleDefaultAction, + RuleSystemAction, + SanitizedRule, +} from '../../../../types'; const rulesClient = rulesClientMock.create(); @@ -123,4 +129,120 @@ describe('bulkDeleteRulesRoute', () => { expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); }); + + describe('actions', () => { + const mockedRule: SanitizedRule<{}> = { + 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, + }, + uuid: '123-456', + type: RuleActionTypes.DEFAULT, + }, + ], + 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'), + }, + revision: 0, + }; + + const action: RuleDefaultAction = { + actionTypeId: 'test', + group: 'default', + id: '2', + params: { + foo: true, + }, + uuid: '123-456', + type: RuleActionTypes.DEFAULT, + }; + + const systemAction: RuleSystemAction = { + actionTypeId: 'test-2', + id: 'system_action-id', + params: { + foo: true, + }, + uuid: '123-456', + type: RuleActionTypes.SYSTEM, + }; + + const mockedRules: Array> = [ + { ...mockedRule, actions: [action, systemAction] }, + ]; + + const bulkDeleteActionsResult = { + rules: mockedRules, + errors: [], + total: 1, + taskIdsFailedToBeDeleted: [], + }; + + it('removes the type from the actions correctly before sending the response', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + const actionsClient = actionsClientMock.create(); + actionsClient.isSystemAction.mockImplementation((id: string) => id === 'system_action-id'); + + bulkDeleteRulesRoute({ router, licenseState }); + const [_, handler] = router.patch.mock.calls[0]; + + rulesClient.bulkDeleteRules.mockResolvedValueOnce(bulkDeleteActionsResult); + + const [context, req, res] = mockHandlerArguments( + { rulesClient, actionsClient }, + { + body: bulkDeleteRequest, + }, + ['ok'] + ); + + const routeRes = await handler(context, req, res); + + // @ts-expect-error: body exists + expect(routeRes.body.rules[0].actions).toEqual([ + { + connector_type_id: 'test', + group: 'default', + id: '2', + params: { + foo: true, + }, + uuid: '123-456', + }, + { + connector_type_id: 'test-2', + id: 'system_action-id', + params: { + foo: true, + }, + uuid: '123-456', + }, + ]); + }); + }); }); diff --git a/x-pack/plugins/alerting/server/rules_client/lib/get_alert_from_raw.ts b/x-pack/plugins/alerting/server/rules_client/lib/get_alert_from_raw.ts index ddf1e40728b28..1be307eb0cdf7 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/get_alert_from_raw.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/get_alert_from_raw.ts @@ -15,6 +15,7 @@ import { RuleTypeParams, RuleWithLegacyId, PartialRuleWithLegacyId, + isSystemAction, } from '../../types'; import { ruleExecutionStatusFromRaw, @@ -23,8 +24,9 @@ import { } from '../../lib'; import { UntypedNormalizedRuleType } from '../../rule_type_registry'; import { getActiveScheduledSnoozes } from '../../lib/is_rule_snoozed'; -import { injectReferencesIntoActions, injectReferencesIntoParams } from '../common'; +import { injectReferencesIntoParams } from '../common'; import { RulesClientContext } from '../types'; +import { transformRawActionsToDomainActions } from '../../application/rule/transforms/transform_raw_actions_to_domain_actions'; export interface GetAlertFromRawParams { id: string; @@ -118,6 +120,7 @@ export function getPartialRuleFromRaw( }) : null; const includeMonitoring = monitoring && !excludeFromPublicApi; + const rule: PartialRule = { id, notifyWhen, @@ -125,7 +128,14 @@ export function getPartialRuleFromRaw( // we currently only support the Interval Schedule type // Once we support additional types, this type signature will likely change schedule: schedule as IntervalSchedule, - actions: actions ? injectReferencesIntoActions(id, actions, references || []) : [], + actions: actions + ? transformRawActionsToDomainActions({ + ruleId: id, + actions, + references: references || [], + isSystemAction: context.isSystemAction, + }) + : [], params: injectReferencesIntoParams(id, ruleType, params, references || []) as Params, ...(excludeFromPublicApi ? {} : { snoozeSchedule: snoozeScheduleDates ?? [] }), ...(includeSnoozeData && !excludeFromPublicApi @@ -161,7 +171,12 @@ export function getPartialRuleFromRaw( if (omitGeneratedValues) { if (rule.actions) { - rule.actions = rule.actions.map((ruleAction) => omit(ruleAction, 'alertsFilter.query.dsl')); + rule.actions = rule.actions.map((ruleAction) => { + if (!isSystemAction(ruleAction)) { + return omit(ruleAction, 'alertsFilter.query.dsl'); + } + return ruleAction; + }); } }