diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/bulk_edit/schemas/v1.ts b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_edit/schemas/v1.ts index 54e70cde689ac..958bb61863a0e 100644 --- a/x-pack/plugins/alerting/common/routes/rule/apis/bulk_edit/schemas/v1.ts +++ b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_edit/schemas/v1.ts @@ -38,7 +38,7 @@ const ruleSnoozeScheduleSchemaWithValidation = schema.object( ); const ruleActionSchema = schema.object({ - group: schema.string(), + group: schema.maybe(schema.string()), id: schema.string(), params: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), uuid: schema.maybe(schema.string()), diff --git a/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.test.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.test.ts index 34e78f7c28a92..06ccb726afd2d 100644 --- a/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.test.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.test.ts @@ -18,7 +18,7 @@ import { import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; import { ruleTypeRegistryMock } from '../../../../rule_type_registry.mock'; import { alertingAuthorizationMock } from '../../../../authorization/alerting_authorization.mock'; -import { RecoveredActionGroup, RuleTypeParams } from '../../../../../common'; +import { RecoveredActionGroup, RuleActionTypes, RuleTypeParams } from '../../../../../common'; import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks'; import { actionsAuthorizationMock } from '@kbn/actions-plugin/server/mocks'; import { AlertingAuthorization } from '../../../../authorization/alerting_authorization'; @@ -26,7 +26,6 @@ import { ActionsAuthorization, ActionsClient } from '@kbn/actions-plugin/server' import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks'; import { getBeforeSetup, setGlobalDate } from '../../../../rules_client/tests/lib'; import { bulkMarkApiKeysForInvalidation } from '../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation'; -import { NormalizedAlertAction } from '../../../../rules_client/types'; import { enabledRule1, enabledRule2, @@ -36,6 +35,10 @@ import { import { migrateLegacyActions } from '../../../../rules_client/lib'; import { migrateLegacyActionsMock } from '../../../../rules_client/lib/siem_legacy_actions/retrieve_migrated_legacy_actions.mock'; import { ConnectorAdapterRegistry } from '../../../../connector_adapters/connector_adapter_registry'; +import { ConnectorAdapter } from '../../../../connector_adapters/types'; +import { RuleAttributes } from '../../../../data/rule/types'; +import { SavedObject } from '@kbn/core/server'; +import { bulkEditOperationsSchema } from './schemas'; jest.mock('../../../../rules_client/lib/siem_legacy_actions/migrate_legacy_actions', () => { return { @@ -104,6 +107,7 @@ const rulesClientParams: jest.Mocked = { isAuthenticationTypeAPIKey: isAuthenticationTypeApiKeyMock, getAuthenticationAPIKey: getAuthenticationApiKeyMock, connectorAdapterRegistry: new ConnectorAdapterRegistry(), + isSystemAction: jest.fn(), getAlertIndicesAlias: jest.fn(), alertsService: null, }; @@ -244,14 +248,17 @@ describe('bulkEdit()', () => { return { state: {} }; }, category: 'test', + validLegacyConsumers: [], producer: 'alerts', validate: { params: { validate: (params) => params }, }, - validLegacyConsumers: [], }); (migrateLegacyActions as jest.Mock).mockResolvedValue(migrateLegacyActionsMock); + + rulesClientParams.isSystemAction.mockImplementation((id: string) => id === 'system_action-id'); + actionsClient.isSystemAction.mockImplementation((id: string) => id === 'system_action-id'); }); describe('tags operations', () => { @@ -536,6 +543,14 @@ describe('bulkEdit()', () => { }); describe('actions operations', () => { + const connectorAdapter: ConnectorAdapter = { + connectorTypeId: '.test', + ruleActionParamsSchema: schema.object({ foo: schema.string() }), + buildActionParams: jest.fn(), + }; + + rulesClientParams.connectorAdapterRegistry.register(connectorAdapter); + beforeEach(() => { mockCreatePointInTimeFinderAsInternalUser({ saved_objects: [existingDecryptedRule], @@ -545,7 +560,7 @@ describe('bulkEdit()', () => { test('should add uuid to new actions', async () => { const existingAction = { frequency: { - notifyWhen: 'onActiveAlert', + notifyWhen: 'onActiveAlert' as const, summary: false, throttle: null, }, @@ -553,26 +568,31 @@ describe('bulkEdit()', () => { id: '1', params: {}, uuid: '111', + type: RuleActionTypes.DEFAULT, }; + const newAction = { frequency: { - notifyWhen: 'onActiveAlert', + notifyWhen: 'onActiveAlert' as const, summary: false, throttle: null, }, group: 'default', id: '2', params: {}, + type: RuleActionTypes.DEFAULT, }; + const newAction2 = { frequency: { - notifyWhen: 'onActiveAlert', + notifyWhen: 'onActiveAlert' as const, summary: false, throttle: null, }, group: 'default', id: '3', params: {}, + type: RuleActionTypes.DEFAULT, }; unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({ @@ -585,10 +605,12 @@ describe('bulkEdit()', () => { { ...existingAction, actionRef: 'action_0', + actionTypeId: 'test-0', }, { ...newAction, actionRef: 'action_1', + actionTypeId: 'test-1', uuid: '222', }, ], @@ -615,7 +637,7 @@ describe('bulkEdit()', () => { { field: 'actions', operation: 'add', - value: [existingAction, newAction, newAction2] as NormalizedAlertAction[], + value: [existingAction, newAction, newAction2], }, ], }); @@ -677,7 +699,10 @@ describe('bulkEdit()', () => { ...existingRule.attributes.executionStatus, lastExecutionDate: new Date(existingRule.attributes.executionStatus.lastExecutionDate), }, - actions: [existingAction, { ...newAction, uuid: '222' }], + actions: [ + { ...existingAction, type: RuleActionTypes.DEFAULT, actionTypeId: 'test-0' }, + { ...newAction, uuid: '222', type: RuleActionTypes.DEFAULT, actionTypeId: 'test-1' }, + ], id: existingRule.id, snoozeSchedule: [], }); @@ -708,6 +733,7 @@ describe('bulkEdit()', () => { params: { message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + type: RuleActionTypes.DEFAULT, }, ], }, @@ -742,7 +768,6 @@ describe('bulkEdit()', () => { async executor() { return { state: {} }; }, - category: 'test', producer: 'alerts', validate: { params: { validate: (params) => params }, @@ -752,11 +777,13 @@ describe('bulkEdit()', () => { mappings: { fieldMap: { field: { type: 'keyword', required: false } } }, shouldWrite: true, }, + category: 'test', validLegacyConsumers: [], }); + const existingAction = { frequency: { - notifyWhen: 'onActiveAlert', + notifyWhen: 'onActiveAlert' as const, summary: false, throttle: null, }, @@ -776,10 +803,11 @@ describe('bulkEdit()', () => { timezone: 'UTC', }, }, + type: RuleActionTypes.DEFAULT, }; const newAction = { frequency: { - notifyWhen: 'onActiveAlert', + notifyWhen: 'onActiveAlert' as const, summary: false, throttle: null, }, @@ -788,6 +816,7 @@ describe('bulkEdit()', () => { params: {}, uuid: '222', alertsFilter: { query: { kql: 'test:1', dsl: 'test', filters: [] } }, + type: RuleActionTypes.DEFAULT, }; unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({ @@ -833,7 +862,7 @@ describe('bulkEdit()', () => { { field: 'actions', operation: 'add', - value: [existingAction, newAction] as NormalizedAlertAction[], + value: [existingAction, newAction], }, ], }); @@ -911,6 +940,565 @@ describe('bulkEdit()', () => { snoozeSchedule: [], }); }); + + test('should add system and default actions', async () => { + const defaultAction = { + frequency: { + notifyWhen: 'onActiveAlert' as const, + summary: false, + throttle: null, + }, + group: 'default', + id: '1', + params: {}, + type: RuleActionTypes.DEFAULT, + }; + + const systemAction = { + id: 'system_action-id', + params: {}, + type: RuleActionTypes.SYSTEM, + }; + + unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({ + saved_objects: [ + { + ...existingRule, + attributes: { + ...existingRule.attributes, + actions: [ + { + frequency: { + notifyWhen: 'onActiveAlert' as const, + summary: false, + throttle: null, + }, + group: 'default', + id: '1', + params: {}, + actionRef: 'action_0', + actionTypeId: 'test-1', + uuid: '222', + }, + { + id: 'system_action-id', + params: {}, + actionRef: 'system_action:system_action-id', + actionTypeId: 'test-2', + uuid: '222', + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }, + ], + }); + + actionsClient.getBulk.mockResolvedValue([ + { + id: '1', + actionTypeId: 'test-1', + config: {}, + isMissingSecrets: false, + name: 'test default connector', + isPreconfigured: false, + isDeprecated: false, + isSystemAction: false, + }, + { + id: 'system_action-id', + actionTypeId: 'test-2', + config: {}, + isMissingSecrets: false, + name: 'system action connector', + isPreconfigured: false, + isDeprecated: false, + isSystemAction: true, + }, + ]); + + const result = await rulesClient.bulkEdit({ + filter: '', + operations: [ + { + field: 'actions', + operation: 'add', + value: [defaultAction, systemAction], + }, + ], + }); + + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledWith( + [ + { + ...existingRule, + attributes: { + ...existingRule.attributes, + actions: [ + { + actionRef: 'action_0', + actionTypeId: 'test-1', + frequency: { notifyWhen: 'onActiveAlert', summary: false, throttle: null }, + group: 'default', + params: {}, + uuid: '103', + }, + { + actionRef: 'system_action:system_action-id', + actionTypeId: 'test-2', + params: {}, + uuid: '104', + }, + ], + apiKey: null, + apiKeyOwner: null, + apiKeyCreatedByUser: null, + meta: { versionApiKeyLastmodified: 'v8.2.0' }, + name: 'my rule name', + enabled: false, + updatedAt: '2019-02-12T21:01:22.479Z', + updatedBy: 'elastic', + tags: ['foo'], + revision: 1, + }, + references: [{ id: '1', name: 'action_0', type: 'action' }], + }, + ], + { overwrite: true } + ); + + expect(result.rules[0]).toEqual({ + ...omit(existingRule.attributes, 'legacyId'), + createdAt: new Date(existingRule.attributes.createdAt), + updatedAt: new Date(existingRule.attributes.updatedAt), + executionStatus: { + ...existingRule.attributes.executionStatus, + lastExecutionDate: new Date(existingRule.attributes.executionStatus.lastExecutionDate), + }, + actions: [ + { ...defaultAction, actionTypeId: 'test-1', uuid: '222' }, + { ...systemAction, actionTypeId: 'test-2', uuid: '222' }, + ], + id: existingRule.id, + snoozeSchedule: [], + }); + }); + + test('should construct the refs correctly and not persist the type of the action', async () => { + const defaultAction = { + frequency: { + notifyWhen: 'onActiveAlert' as const, + summary: false, + throttle: null, + }, + group: 'default', + id: '1', + params: {}, + type: RuleActionTypes.DEFAULT, + }; + + const systemAction = { + id: 'system_action-id', + params: {}, + type: RuleActionTypes.SYSTEM, + }; + + unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({ + saved_objects: [ + { + ...existingRule, + attributes: { + ...existingRule.attributes, + actions: [ + { + frequency: { + notifyWhen: 'onActiveAlert' as const, + summary: false, + throttle: null, + }, + group: 'default', + id: '1', + params: {}, + actionRef: 'action_0', + actionTypeId: 'test-1', + uuid: '222', + }, + { + id: 'system_action-id', + params: {}, + actionRef: 'system_action:system_action-id', + actionTypeId: 'test-2', + uuid: '222', + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }, + ], + }); + + actionsClient.getBulk.mockResolvedValue([ + { + id: '1', + actionTypeId: 'test-1', + config: {}, + isMissingSecrets: false, + name: 'test default connector', + isPreconfigured: false, + isDeprecated: false, + isSystemAction: false, + }, + { + id: 'system_action-id', + actionTypeId: 'test-2', + config: {}, + isMissingSecrets: false, + name: 'system action connector', + isPreconfigured: false, + isDeprecated: false, + isSystemAction: true, + }, + ]); + + await rulesClient.bulkEdit({ + filter: '', + operations: [ + { + field: 'actions', + operation: 'add', + value: [defaultAction, systemAction], + }, + ], + }); + + const rule = unsecuredSavedObjectsClient.bulkCreate.mock.calls[0][0] as Array< + SavedObject + >; + + expect(rule[0].attributes.actions).toEqual([ + { + actionRef: 'action_0', + actionTypeId: 'test-1', + frequency: { notifyWhen: 'onActiveAlert', summary: false, throttle: null }, + group: 'default', + params: {}, + uuid: '105', + }, + { + actionRef: 'system_action:system_action-id', + actionTypeId: 'test-2', + params: {}, + uuid: '106', + }, + ]); + }); + + test('should add the actions type to the response correctly', async () => { + const defaultAction = { + frequency: { + notifyWhen: 'onActiveAlert' as const, + summary: false, + throttle: null, + }, + group: 'default', + id: '1', + params: {}, + type: RuleActionTypes.DEFAULT, + }; + + const systemAction = { + id: 'system_action-id', + params: {}, + type: RuleActionTypes.SYSTEM, + }; + + unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({ + saved_objects: [ + { + ...existingRule, + attributes: { + ...existingRule.attributes, + actions: [ + { + frequency: { + notifyWhen: 'onActiveAlert' as const, + summary: false, + throttle: null, + }, + group: 'default', + id: '1', + params: {}, + actionRef: 'action_0', + actionTypeId: 'test-1', + uuid: '222', + }, + { + id: 'system_action-id', + params: {}, + actionRef: 'system_action:system_action-id', + actionTypeId: 'test-2', + uuid: '222', + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }, + ], + }); + + actionsClient.getBulk.mockResolvedValue([ + { + id: '1', + actionTypeId: 'test-1', + config: {}, + isMissingSecrets: false, + name: 'test default connector', + isPreconfigured: false, + isDeprecated: false, + isSystemAction: false, + }, + { + id: 'system_action-id', + actionTypeId: 'test-2', + config: {}, + isMissingSecrets: false, + name: 'system action connector', + isPreconfigured: false, + isDeprecated: false, + isSystemAction: true, + }, + ]); + + const result = await rulesClient.bulkEdit({ + filter: '', + operations: [ + { + field: 'actions', + operation: 'add', + value: [defaultAction, systemAction], + }, + ], + }); + + expect(result.rules[0].actions).toEqual([ + { ...defaultAction, actionTypeId: 'test-1', uuid: '222' }, + { ...systemAction, actionTypeId: 'test-2', uuid: '222' }, + ]); + }); + + it('should return an error if the system action does not exist', async () => { + const action = { + id: 'system_action-id', + uuid: '123', + params: {}, + type: RuleActionTypes.SYSTEM, + }; + + actionsClient.isSystemAction.mockReturnValue(false); + actionsClient.getBulk.mockResolvedValue([ + { + id: 'system_action-id', + actionTypeId: 'test-2', + config: {}, + isMissingSecrets: false, + name: 'system action connector', + isPreconfigured: false, + isDeprecated: false, + isSystemAction: true, + }, + ]); + + const result = await rulesClient.bulkEdit({ + filter: '', + operations: [ + { + field: 'actions', + operation: 'add', + value: [action], + }, + ], + }); + + expect(result).toMatchInlineSnapshot(` + Object { + "errors": Array [ + Object { + "message": "Action system_action-id is not a system action", + "rule": Object { + "id": "1", + "name": "my rule name", + }, + }, + ], + "rules": Array [], + "skipped": Array [], + "total": 1, + } + `); + + expect(actionsClient.getBulk).toBeCalledWith({ + ids: ['system_action-id'], + throwIfSystemAction: false, + }); + }); + + it('should throw an error if the system action contains the frequency', async () => { + const action = { + id: 'system_action-id', + uuid: '123', + params: {}, + frequency: { + notifyWhen: 'onActiveAlert' as const, + summary: false, + throttle: null, + }, + type: RuleActionTypes.SYSTEM, + }; + + actionsClient.isSystemAction.mockReturnValue(true); + actionsClient.getBulk.mockResolvedValue([ + { + id: 'system_action-id', + actionTypeId: 'test-2', + config: {}, + isMissingSecrets: false, + name: 'system action connector', + isPreconfigured: false, + isDeprecated: false, + isSystemAction: true, + }, + ]); + + await expect( + rulesClient.bulkEdit({ + filter: '', + operations: [ + { + field: 'actions', + operation: 'add', + value: [action], + }, + ], + }) + ).rejects.toMatchInlineSnapshot(` + [Error: Error validating bulk edit rules operations - [0]: types that failed validation: + - [0.0.field]: expected value to equal [tags] + - [0.1.value.0]: types that failed validation: + - [0.value.0.0.group]: expected value of type [string] but got [undefined] + - [0.value.0.1.frequency]: definition for this key is missing + - [0.2.operation]: expected value to equal [set] + - [0.3.operation]: expected value to equal [set] + - [0.4.operation]: expected value to equal [set] + - [0.5.operation]: expected value to equal [set] + - [0.6.operation]: expected value to equal [delete] + - [0.7.operation]: expected value to equal [set]] + `); + }); + + it('should throw an error if the system action contains the alertsFilter', async () => { + const action = { + id: 'system_action-id', + uuid: '123', + params: {}, + alertsFilter: { + query: { kql: 'test:1', filters: [] }, + }, + type: RuleActionTypes.SYSTEM, + }; + + actionsClient.isSystemAction.mockReturnValue(true); + actionsClient.getBulk.mockResolvedValue([ + { + id: 'system_action-id', + actionTypeId: 'test-2', + config: {}, + isMissingSecrets: false, + name: 'system action connector', + isPreconfigured: false, + isDeprecated: false, + isSystemAction: true, + }, + ]); + + await expect( + rulesClient.bulkEdit({ + filter: '', + operations: [ + { + field: 'actions', + operation: 'add', + value: [action], + }, + ], + }) + ).rejects.toMatchInlineSnapshot(` + [Error: Error validating bulk edit rules operations - [0]: types that failed validation: + - [0.0.field]: expected value to equal [tags] + - [0.1.value.0]: types that failed validation: + - [0.value.0.0.group]: expected value of type [string] but got [undefined] + - [0.value.0.1.alertsFilter]: definition for this key is missing + - [0.2.operation]: expected value to equal [set] + - [0.3.operation]: expected value to equal [set] + - [0.4.operation]: expected value to equal [set] + - [0.5.operation]: expected value to equal [set] + - [0.6.operation]: expected value to equal [delete] + - [0.7.operation]: expected value to equal [set]] + `); + }); + + it('should throw an error if the default action does not contain the group', async () => { + const action = { + id: '1', + params: {}, + type: RuleActionTypes.DEFAULT, + }; + + actionsClient.isSystemAction.mockReturnValue(false); + + await expect( + rulesClient.bulkEdit({ + filter: '', + operations: [ + { + field: 'actions', + operation: 'add', + // @ts-expect-error: group is missing + value: [action], + }, + ], + }) + ).rejects.toMatchInlineSnapshot(` + [Error: Error validating bulk edit rules operations - [0]: types that failed validation: + - [0.0.field]: expected value to equal [tags] + - [0.1.value.0]: types that failed validation: + - [0.value.0.0.group]: expected value of type [string] but got [undefined] + - [0.value.0.1.type]: expected value to equal [system] + - [0.2.operation]: expected value to equal [set] + - [0.3.operation]: expected value to equal [set] + - [0.4.operation]: expected value to equal [set] + - [0.5.operation]: expected value to equal [set] + - [0.6.operation]: expected value to equal [delete] + - [0.7.operation]: expected value to equal [set]] + `); + }); }); describe('index pattern operations', () => { @@ -968,7 +1556,13 @@ describe('bulkEdit()', () => { const result = await rulesClient.bulkEdit({ filter: '', - operations: [], + operations: [ + { + field: 'tags', + operation: 'add', + value: ['test-tag'], + }, + ], paramsModifier, }); @@ -1037,7 +1631,13 @@ describe('bulkEdit()', () => { const result = await rulesClient.bulkEdit({ filter: '', - operations: [], + operations: [ + { + field: 'tags', + operation: 'add', + value: ['test-tag'], + }, + ], paramsModifier, }); @@ -1062,6 +1662,9 @@ describe('bulkEdit()', () => { }); test('should skip operation when params modifiers does not modify index pattern array', async () => { + const originalValidate = bulkEditOperationsSchema.validate; + bulkEditOperationsSchema.validate = jest.fn(); + paramsModifier.mockResolvedValue({ modifiedParams: { index: ['test-1', 'test-2'], @@ -1080,6 +1683,8 @@ describe('bulkEdit()', () => { expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledTimes(0); expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledTimes(0); + + bulkEditOperationsSchema.validate = originalValidate; }); }); @@ -2324,8 +2929,8 @@ describe('bulkEdit()', () => { async executor() { return { state: {} }; }, - category: 'test', producer: 'alerts', + category: 'test', validLegacyConsumers: [], }); @@ -2370,8 +2975,8 @@ describe('bulkEdit()', () => { async executor() { return { state: {} }; }, - category: 'test', producer: 'alerts', + category: 'test', validLegacyConsumers: [], }); @@ -2410,7 +3015,13 @@ describe('bulkEdit()', () => { const result = await rulesClient.bulkEdit({ filter: '', - operations: [], + operations: [ + { + field: 'tags', + operation: 'add', + value: ['test-1'], + }, + ], paramsModifier: async (params) => { params.index = ['test-index-*']; @@ -2545,6 +3156,50 @@ describe('bulkEdit()', () => { expect(validateScheduleLimit).toHaveBeenCalledTimes(1); }); + + test('should not validate scheduling on system actions', async () => { + mockCreatePointInTimeFinderAsInternalUser({ + saved_objects: [ + { + ...existingDecryptedRule, + attributes: { + ...existingDecryptedRule.attributes, + actions: [ + { + actionRef: 'action_0', + actionTypeId: 'test', + params: {}, + uuid: '111', + type: RuleActionTypes.SYSTEM, + }, + ], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ] as any, + }, + ], + }); + + const result = await rulesClient.bulkEdit({ + operations: [ + { + field: 'schedule', + operation: 'set', + value: { interval: '10m' }, + }, + ], + }); + + expect(result.errors).toHaveLength(0); + expect(result.rules).toHaveLength(1); + }); }); describe('paramsModifier', () => { @@ -2578,7 +3233,13 @@ describe('bulkEdit()', () => { const result = await rulesClient.bulkEdit({ filter: '', - operations: [], + operations: [ + { + field: 'tags', + operation: 'add', + value: ['test-1'], + }, + ], paramsModifier: async (params) => { params.index = ['test-index-*']; diff --git a/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.ts index 294945ad82037..efa589919c39d 100644 --- a/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.ts @@ -15,6 +15,9 @@ import { SavedObjectsFindResult, SavedObjectsUpdateResponse, } from '@kbn/core/server'; +import { validateSystemActions } from '../../../../lib/validate_system_actions'; +import { RuleActionTypes, RuleDefaultAction, RuleSystemAction } from '../../../../../common'; +import { isSystemAction } from '../../../../../common/system_actions/is_system_action'; import { BulkActionSkipResult } from '../../../../../common/bulk_edit'; import { RuleTypeRegistry } from '../../../../types'; import { @@ -31,7 +34,6 @@ import { retryIfBulkEditConflicts, applyBulkEditOperation, buildKueryNodeFilter, - injectReferencesIntoActions, getBulkSnooze, getBulkUnsnooze, verifySnoozeScheduleLimit, @@ -77,6 +79,8 @@ import { transformRuleDomainToRule, } from '../../transforms'; import { validateScheduleLimit, ValidateScheduleLimitResult } from '../get_schedule_frequency'; +import { bulkEditOperationsSchema } from './schemas'; +import { denormalizeActions } from '../../../../rules_client/lib/denormalize_actions'; const isValidInterval = (interval: string | undefined): interval is string => { return interval !== undefined; @@ -114,8 +118,15 @@ export async function bulkEditRules( context: RulesClientContext, options: BulkEditOptions ): Promise> { + try { + bulkEditOperationsSchema.validate(options.operations); + } catch (error) { + throw Boom.badRequest(`Error validating bulk edit rules operations - ${error.message}`); + } + const queryFilter = (options as BulkEditOptionsFilter).filter; const ids = (options as BulkEditOptionsIds).ids; + const actionsClient = await context.getActionsClient(); if (ids && queryFilter) { throw Boom.badRequest( @@ -230,13 +241,17 @@ export async function bulkEditRules( // fix the type cast from SavedObjectsBulkUpdateObject to SavedObjectsBulkUpdateObject // when we are doing the bulk create 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); } catch (e) { @@ -464,12 +479,6 @@ async function updateRuleAttributesAndParamsInMemory( rule.references = migratedActions.resultedReferences; } - const ruleActions = injectReferencesIntoActions( - rule.id, - rule.attributes.actions || [], - rule.references || [] - ); - const ruleDomain: RuleDomain = transformRuleAttributesToRuleDomain( rule.attributes, { @@ -477,7 +486,8 @@ async function updateRuleAttributesAndParamsInMemory( logger: context.logger, ruleType: context.ruleTypeRegistry.get(rule.attributes.alertTypeId), references: rule.references, - } + }, + context.isSystemAction ); const { @@ -489,7 +499,7 @@ async function updateRuleAttributesAndParamsInMemory( context, operations, rule: ruleDomain, - ruleActions, + ruleActions: ruleDomain.actions, ruleType, }); @@ -530,21 +540,25 @@ async function updateRuleAttributesAndParamsInMemory( ruleType.validate.params ); - const { - actions: rawAlertActions, - references, - params: updatedParams, - } = await extractReferences( + const { references, params: updatedParams } = await extractReferences( context, ruleType, updatedRuleActions as NormalizedAlertActionWithGeneratedValues[], validatedMutatedAlertTypeParams ); - const ruleAttributes = transformRuleDomainToRuleAttributes(updatedRule, { - legacyId: rule.attributes.legacyId, - actionsWithRefs: rawAlertActions, - paramsWithRefs: updatedParams as RuleAttributes['params'], + const { actions: actionsWithRefs } = await denormalizeActions( + context, + updatedRuleActions as NormalizedAlertActionWithGeneratedValues[] + ); + + const ruleAttributes = transformRuleDomainToRuleAttributes({ + actionsWithRefs, + rule: updatedRule, + params: { + legacyId: rule.attributes.legacyId, + paramsWithRefs: updatedParams as RuleAttributes['params'], + }, }); const { apiKeyAttributes } = await prepareApiKeys( @@ -562,7 +576,7 @@ async function updateRuleAttributesAndParamsInMemory( ruleAttributes, apiKeyAttributes, updatedParams, - rawAlertActions, + ruleAttributes.actions, username ); @@ -623,6 +637,8 @@ async function getUpdatedAttributesFromOperations({ ruleActions: RuleDomain['actions']; ruleType: RuleType; }) { + const actionsClient = await context.getActionsClient(); + let updatedRule = cloneDeep(rule); let updatedRuleActions = ruleActions; let hasUpdateApiKeyOperation = false; @@ -640,6 +656,16 @@ async function getUpdatedAttributesFromOperations({ value: addGeneratedActionValues(operation.value), }; + const systemActions = operation.value.filter( + (action): action is RuleSystemAction => action.type === RuleActionTypes.SYSTEM + ); + + await validateSystemActions({ + actionsClient, + connectorAdapterRegistry: context.connectorAdapterRegistry, + systemActions, + }); + try { await validateActions(context, ruleType, { ...updatedRule, @@ -668,23 +694,27 @@ async function getUpdatedAttributesFromOperations({ break; } + case 'snoozeSchedule': { if (operation.operation === 'set') { const snoozeAttributes = getBulkSnooze( updatedRule, operation.value as RuleSnoozeSchedule ); + try { verifySnoozeScheduleLimit(snoozeAttributes.snoozeSchedule); } catch (error) { throw Error(`Error updating rule: could not add snooze - ${error.message}`); } + updatedRule = { ...updatedRule, muteAll: snoozeAttributes.muteAll, snoozeSchedule: snoozeAttributes.snoozeSchedule as RuleDomain['snoozeSchedule'], }; } + if (operation.operation === 'delete') { const idsToDelete = operation.value && [...operation.value]; if (idsToDelete?.length === 0) { @@ -701,18 +731,22 @@ async function getUpdatedAttributesFromOperations({ snoozeSchedule: snoozeAttributes.snoozeSchedule as RuleDomain['snoozeSchedule'], }; } + isAttributesUpdateSkipped = false; break; } + case 'apiKey': { hasUpdateApiKeyOperation = true; isAttributesUpdateSkipped = false; break; } + default: { if (operation.field === 'schedule') { validateScheduleOperation(operation.value, updatedRule.actions, rule.id); } + const { modifiedAttributes, isAttributeModified } = applyBulkEditOperation( operation, updatedRule @@ -777,8 +811,11 @@ function validateScheduleOperation( ): void { const scheduleInterval = parseDuration(schedule.interval); const actionsWithInvalidThrottles = []; + const actionsWithoutSystemActions = actions.filter( + (action): action is RuleDefaultAction => !isSystemAction(action) + ); - for (const action of actions) { + for (const action of actionsWithoutSystemActions) { // check for actions throttled shorter than the rule schedule if ( action.frequency?.notifyWhen === ruleNotifyWhen.THROTTLE && diff --git a/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/schemas/bulk_edit_rules_option_schemas.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/schemas/bulk_edit_rules_option_schemas.ts index f80d63210cf4a..e8ecb18af52b0 100644 --- a/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/schemas/bulk_edit_rules_option_schemas.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/schemas/bulk_edit_rules_option_schemas.ts @@ -5,8 +5,9 @@ * 2.0. */ import { schema } from '@kbn/config-schema'; +import { RuleActionTypes } from '../../../../../../common'; import { rRuleRequestSchema } from '../../../../r_rule/schemas'; -import { notifyWhenSchema } from '../../../schemas'; +import { notifyWhenSchema, actionAlertsFilterSchema } from '../../../schemas'; import { validateDuration } from '../../../validation'; import { validateSnoozeSchedule } from '../validation'; @@ -26,7 +27,7 @@ const bulkEditRuleSnoozeScheduleSchemaWithValidation = schema.object( { validate: validateSnoozeSchedule } ); -const bulkEditActionSchema = schema.object({ +const bulkEditDefaultActionSchema = schema.object({ group: schema.string(), id: schema.string(), params: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), @@ -38,6 +39,15 @@ const bulkEditActionSchema = schema.object({ notifyWhen: notifyWhenSchema, }) ), + alertsFilter: schema.maybe(actionAlertsFilterSchema), + type: schema.literal(RuleActionTypes.DEFAULT), +}); + +export const bulkEditSystemActionSchema = schema.object({ + id: schema.string(), + params: schema.recordOf(schema.string(), schema.maybe(schema.any()), { defaultValue: {} }), + uuid: schema.maybe(schema.string()), + type: schema.literal(RuleActionTypes.SYSTEM), }); const bulkEditTagSchema = schema.object({ @@ -49,7 +59,7 @@ const bulkEditTagSchema = schema.object({ const bulkEditActionsSchema = schema.object({ operation: schema.oneOf([schema.literal('add'), schema.literal('set')]), field: schema.literal('actions'), - value: schema.arrayOf(bulkEditActionSchema), + value: schema.arrayOf(schema.oneOf([bulkEditDefaultActionSchema, bulkEditSystemActionSchema])), }); const bulkEditScheduleSchema = schema.object({ diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/bulk_edit/bulk_edit_rules_route.test.ts b/x-pack/plugins/alerting/server/routes/rule/apis/bulk_edit/bulk_edit_rules_route.test.ts index 1b1ed454c5207..bf27692bece81 100644 --- a/x-pack/plugins/alerting/server/routes/rule/apis/bulk_edit/bulk_edit_rules_route.test.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/bulk_edit/bulk_edit_rules_route.test.ts @@ -13,7 +13,14 @@ import { verifyApiAccess } from '../../../../lib/license_api_access'; import { RuleTypeDisabledError } from '../../../../lib/errors/rule_type_disabled'; import { mockHandlerArguments } from '../../../_mock_handler_arguments'; import { rulesClientMock } from '../../../../rules_client.mock'; -import { SanitizedRule } from '../../../../types'; +import { + RuleActionTypes, + RuleDefaultAction, + RuleSystemAction, + SanitizedRule, +} from '../../../../types'; +import { actionsClientMock } from '@kbn/actions-plugin/server/mocks'; +import { omit } from 'lodash'; const rulesClient = rulesClientMock.create(); jest.mock('../../../../lib/license_api_access', () => ({ @@ -42,6 +49,7 @@ describe('bulkEditRulesRoute', () => { foo: true, }, uuid: '123-456', + type: RuleActionTypes.DEFAULT, }, ], consumer: 'bar', @@ -189,4 +197,187 @@ describe('bulkEditRulesRoute', () => { expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); }); + + describe('actions', () => { + 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 mockedActionAlerts: Array> = [ + { ...mockedAlert, actions: [action, systemAction] }, + ]; + + const bulkEditActionsRequest = { + filter: '', + operations: [ + { + operation: 'add', + field: 'actions', + value: [omit(action, 'type'), omit(systemAction, 'type')], + }, + ], + }; + + const bulkEditActionsResult = { rules: mockedActionAlerts, errors: [], total: 1, skipped: [] }; + + it('adds the type of the actions correctly before passing the request to the rules client', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + const actionsClient = actionsClientMock.create(); + actionsClient.isSystemAction.mockImplementation((id: string) => id === 'system_action-id'); + + bulkEditInternalRulesRoute(router, licenseState); + + const [_, handler] = router.post.mock.calls[0]; + + rulesClient.bulkEdit.mockResolvedValueOnce(bulkEditActionsResult); + + const [context, req, res] = mockHandlerArguments( + { rulesClient, actionsClient }, + { + body: bulkEditActionsRequest, + }, + ['ok'] + ); + + await handler(context, req, res); + + expect(rulesClient.bulkEdit.mock.calls[0][0]).toMatchInlineSnapshot(` + Object { + "filter": "", + "ids": undefined, + "operations": Array [ + Object { + "field": "actions", + "operation": "add", + "value": Array [ + Object { + "frequency": undefined, + "group": "default", + "id": "2", + "params": Object { + "foo": true, + }, + "type": "default", + "uuid": "123-456", + }, + Object { + "id": "system_action-id", + "params": Object { + "foo": true, + }, + "type": "system", + "uuid": "123-456", + }, + ], + }, + ], + } + `); + }); + + 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'); + + bulkEditInternalRulesRoute(router, licenseState); + + const [_, handler] = router.post.mock.calls[0]; + + rulesClient.bulkEdit.mockResolvedValueOnce(bulkEditActionsResult); + + const [context, req, res] = mockHandlerArguments( + { rulesClient, actionsClient }, + { + body: bulkEditActionsRequest, + }, + ['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', + }, + ]); + }); + + it('fails if the action contains a type in the request', async () => { + const actionToValidate = { + group: 'default', + id: '2', + params: { + foo: true, + }, + uuid: '123-456', + type: RuleActionTypes.DEFAULT, + }; + + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + bulkEditInternalRulesRoute(router, licenseState); + + const [config, _] = router.post.mock.calls[0]; + + expect(() => + // @ts-expect-error: body exists + config.validate.body.validate({ + ...bulkEditActionsRequest, + operations: [ + { + operation: 'add', + field: 'actions', + value: [actionToValidate], + }, + ], + }) + ).toThrowErrorMatchingInlineSnapshot(` + "[operations.0]: types that failed validation: + - [operations.0.0.field]: expected value to equal [tags] + - [operations.0.1.value.0.type]: definition for this key is missing + - [operations.0.2.operation]: expected value to equal [set] + - [operations.0.3.operation]: expected value to equal [set] + - [operations.0.4.operation]: expected value to equal [set] + - [operations.0.5.operation]: expected value to equal [set] + - [operations.0.6.operation]: expected value to equal [delete] + - [operations.0.7.operation]: expected value to equal [set]" + `); + }); + }); }); diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/bulk_edit/bulk_edit_rules_route.ts b/x-pack/plugins/alerting/server/routes/rule/apis/bulk_edit/bulk_edit_rules_route.ts index ae39ceba1ceb3..9574c6c475ae4 100644 --- a/x-pack/plugins/alerting/server/routes/rule/apis/bulk_edit/bulk_edit_rules_route.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/bulk_edit/bulk_edit_rules_route.ts @@ -20,6 +20,7 @@ import { Rule } from '../../../../application/rule/types'; import type { RuleParamsV1 } from '../../../../../common/routes/rule/response'; import { transformRuleToRuleResponseV1 } from '../../transforms'; +import { transformOperationsV1 } from './transforms'; interface BuildBulkEditRulesRouteParams { licenseState: ILicenseState; @@ -39,15 +40,19 @@ const buildBulkEditRulesRoute = ({ licenseState, path, router }: BuildBulkEditRu router.handleLegacyErrors( verifyAccessAndContext(licenseState, async function (context, req, res) { const rulesClient = (await context.alerting).getRulesClient(); - const bulkEditData: BulkEditRulesRequestBodyV1 = req.body; + const actionsClient = (await context.actions).getActionsClient(); + const bulkEditData: BulkEditRulesRequestBodyV1 = req.body; const { filter, operations, ids } = bulkEditData; try { const bulkEditResults = await rulesClient.bulkEdit({ filter, ids, - operations, + operations: transformOperationsV1({ + operations, + isSystemAction: (connectorId: string) => actionsClient.isSystemAction(connectorId), + }), }); const resultBody: BulkEditRulesResponseV1 = { diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/bulk_edit/transforms/index.ts b/x-pack/plugins/alerting/server/routes/rule/apis/bulk_edit/transforms/index.ts new file mode 100644 index 0000000000000..e7d1a1dc43478 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/rule/apis/bulk_edit/transforms/index.ts @@ -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. + */ + +export { transformOperations } from './transform_operations/latest'; + +export { transformOperations as transformOperationsV1 } from './transform_operations/v1'; diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/bulk_edit/transforms/transform_operations/latest.ts b/x-pack/plugins/alerting/server/routes/rule/apis/bulk_edit/transforms/transform_operations/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/rule/apis/bulk_edit/transforms/transform_operations/latest.ts @@ -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 * from './v1'; diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/bulk_edit/transforms/transform_operations/v1.test.ts b/x-pack/plugins/alerting/server/routes/rule/apis/bulk_edit/transforms/transform_operations/v1.test.ts new file mode 100644 index 0000000000000..e186621490dea --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/rule/apis/bulk_edit/transforms/transform_operations/v1.test.ts @@ -0,0 +1,66 @@ +/* + * 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 { transformOperations } from './v1'; + +describe('transformOperations', () => { + const isSystemAction = (id: string) => id === 'my-system-action-id'; + + describe('actions', () => { + const defaultAction = { + id: 'default-action', + params: {}, + }; + + const systemAction = { + id: 'my-system-action-id', + params: {}, + }; + + it('transform the actions correctly', async () => { + expect( + transformOperations({ + operations: [ + { field: 'actions', operation: 'add', value: [defaultAction, systemAction] }, + ], + isSystemAction, + }) + ).toEqual([ + { + field: 'actions', + operation: 'add', + value: [ + { + group: 'default', + id: 'default-action', + params: {}, + type: 'default', + }, + { id: 'my-system-action-id', params: {}, type: 'system' }, + ], + }, + ]); + }); + + it('returns an empty array if the operations are empty', async () => { + expect( + transformOperations({ + operations: [], + isSystemAction, + }) + ).toEqual([]); + }); + + it('returns an empty array if the operations are undefined', async () => { + expect( + transformOperations({ + isSystemAction, + }) + ).toEqual([]); + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/bulk_edit/transforms/transform_operations/v1.ts b/x-pack/plugins/alerting/server/routes/rule/apis/bulk_edit/transforms/transform_operations/v1.ts new file mode 100644 index 0000000000000..0b6838bbdaacc --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/rule/apis/bulk_edit/transforms/transform_operations/v1.ts @@ -0,0 +1,56 @@ +/* + * 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 { RuleActionTypes } from '../../../../../../../common'; +import { BulkEditOperation } from '../../../../../../application/rule/methods/bulk_edit'; +import { BulkEditRulesRequestBodyV1 } from '../../../../../../../common/routes/rule/apis/bulk_edit'; + +export const transformOperations = ({ + operations, + isSystemAction, +}: { + operations?: BulkEditRulesRequestBodyV1['operations']; + isSystemAction: (connectorId: string) => boolean; +}): BulkEditOperation[] => { + if (operations == null || operations.length === 0) { + return []; + } + + return operations.map((operation) => { + if (operation.field !== 'actions') { + return operation; + } + + const actions = operation.value.map((action) => { + if (isSystemAction(action.id)) { + return { + id: action.id, + params: action.params, + ...(action.uuid && { uuid: action.uuid }), + type: RuleActionTypes.SYSTEM, + }; + } + + return { + id: action.id, + group: action.group ?? 'default', + params: action.params, + uuid: action.uuid, + ...(action.frequency && { frequency: action.frequency }), + ...(action.uuid && { uuid: action.uuid }), + frequency: action.frequency, + type: RuleActionTypes.DEFAULT, + }; + }); + + return { + field: operation.field, + operation: operation.operation, + value: actions, + }; + }); +}; diff --git a/x-pack/plugins/alerting/server/rules_client/common/apply_bulk_edit_operation.test.ts b/x-pack/plugins/alerting/server/rules_client/common/apply_bulk_edit_operation.test.ts index 49ed183ceb39d..0ca691dc9dded 100644 --- a/x-pack/plugins/alerting/server/rules_client/common/apply_bulk_edit_operation.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/common/apply_bulk_edit_operation.test.ts @@ -6,7 +6,7 @@ */ import { applyBulkEditOperation } from './apply_bulk_edit_operation'; -import type { Rule } from '../../types'; +import { Rule, RuleActionTypes } from '../../types'; describe('applyBulkEditOperation', () => { describe('tags operations', () => { @@ -180,61 +180,120 @@ describe('applyBulkEditOperation', () => { describe('actions operations', () => { test('should add actions', () => { const ruleMock = { - actions: [{ id: 'mock-action-id', group: 'default', params: {} }], + actions: [ + { id: 'mock-action-id', group: 'default', params: {}, type: RuleActionTypes.DEFAULT }, + ], }; + const { modifiedAttributes, isAttributeModified } = applyBulkEditOperation( { field: 'actions', value: [ - { id: 'mock-add-action-id-1', group: 'default', params: {} }, - { id: 'mock-add-action-id-2', group: 'default', params: {} }, + { + id: 'mock-add-action-id-1', + group: 'default', + params: {}, + type: RuleActionTypes.DEFAULT, + }, + { + id: 'mock-add-action-id-2', + group: 'default', + params: {}, + type: RuleActionTypes.DEFAULT, + }, ], operation: 'add', }, ruleMock ); + expect(modifiedAttributes).toHaveProperty('actions', [ - { id: 'mock-action-id', group: 'default', params: {} }, - { id: 'mock-add-action-id-1', group: 'default', params: {} }, - { id: 'mock-add-action-id-2', group: 'default', params: {} }, + { id: 'mock-action-id', group: 'default', params: {}, type: RuleActionTypes.DEFAULT }, + { id: 'mock-add-action-id-1', group: 'default', params: {}, type: RuleActionTypes.DEFAULT }, + { id: 'mock-add-action-id-2', group: 'default', params: {}, type: RuleActionTypes.DEFAULT }, ]); + expect(isAttributeModified).toBe(true); }); test('should add action with different params and same id', () => { const ruleMock = { - actions: [{ id: 'mock-action-id', group: 'default', params: { test: 1 } }], + actions: [ + { + id: 'mock-action-id', + group: 'default', + params: { test: 1 }, + type: RuleActionTypes.DEFAULT, + }, + ], }; + const { modifiedAttributes, isAttributeModified } = applyBulkEditOperation( { field: 'actions', - value: [{ id: 'mock-action-id', group: 'default', params: { test: 2 } }], + value: [ + { + id: 'mock-action-id', + group: 'default', + params: { test: 2 }, + type: RuleActionTypes.DEFAULT, + }, + ], operation: 'add', }, ruleMock ); + expect(modifiedAttributes).toHaveProperty('actions', [ - { id: 'mock-action-id', group: 'default', params: { test: 1 } }, - { id: 'mock-action-id', group: 'default', params: { test: 2 } }, + { + id: 'mock-action-id', + group: 'default', + params: { test: 1 }, + type: RuleActionTypes.DEFAULT, + }, + { + id: 'mock-action-id', + group: 'default', + params: { test: 2 }, + type: RuleActionTypes.DEFAULT, + }, ]); + expect(isAttributeModified).toBe(true); }); test('should rewrite actions', () => { const ruleMock = { - actions: [{ id: 'mock-action-id', group: 'default', params: {} }], + actions: [ + { id: 'mock-action-id', group: 'default', params: {}, type: RuleActionTypes.DEFAULT }, + ], }; + const { modifiedAttributes, isAttributeModified } = applyBulkEditOperation( { field: 'actions', - value: [{ id: 'mock-rewrite-action-id-1', group: 'default', params: {} }], + value: [ + { + id: 'mock-rewrite-action-id-1', + group: 'default', + params: {}, + type: RuleActionTypes.DEFAULT, + }, + ], operation: 'set', }, ruleMock ); + expect(modifiedAttributes).toHaveProperty('actions', [ - { id: 'mock-rewrite-action-id-1', group: 'default', params: {} }, + { + id: 'mock-rewrite-action-id-1', + group: 'default', + params: {}, + type: RuleActionTypes.DEFAULT, + }, ]); + expect(isAttributeModified).toBe(true); }); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/bulk_edit.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/bulk_edit.ts new file mode 100644 index 0000000000000..c1311bd16329a --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/bulk_edit.ts @@ -0,0 +1,135 @@ +/* + * 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 expect from '@kbn/expect'; +import { omit } from 'lodash'; +import { Spaces } from '../../../scenarios'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function createUpdateTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('bulkEdit', () => { + const objectRemover = new ObjectRemover(supertest); + + after(() => objectRemover.removeAll()); + + describe('system actions', () => { + const systemAction = { + id: 'system-connector-test.system-action', + uuid: '123', + params: {}, + }; + + it('should bulk edit system actions correctly', async () => { + const { body: rule } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send(getTestRuleData()) + .expect(200); + + objectRemover.add(Spaces.space1.id, rule.id, 'rule', 'alerting'); + + const payload = { + ids: [rule.id], + operations: [ + { + operation: 'add', + field: 'actions', + value: [systemAction], + }, + ], + }; + + const res = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/_bulk_edit`) + .set('kbn-xsrf', 'foo') + .send(payload) + .expect(200); + + expect(res.body.rules[0].actions).to.eql([ + { + id: 'system-connector-test.system-action', + connector_type_id: 'test.system-action', + params: {}, + uuid: '123', + }, + ]); + }); + + it('should throw 400 if the system action is missing required properties', async () => { + const { body: rule } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send(getTestRuleData()) + .expect(200); + + objectRemover.add(Spaces.space1.id, rule.id, 'rule', 'alerting'); + + for (const propertyToOmit of ['id']) { + const systemActionWithoutProperty = omit(systemAction, propertyToOmit); + + const payload = { + ids: [rule.id], + operations: [ + { + operation: 'add', + field: 'actions', + value: [systemActionWithoutProperty], + }, + ], + }; + + await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/_bulk_edit`) + .set('kbn-xsrf', 'foo') + .send(payload) + .expect(400); + } + }); + + it('should throw 400 if the system action is missing required params', async () => { + const { body: rule } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send(getTestRuleData()) + .expect(200); + + objectRemover.add(Spaces.space1.id, rule.id, 'rule', 'alerting'); + + const payload = { + ids: [rule.id], + operations: [ + { + operation: 'add', + field: 'actions', + value: [ + { + ...systemAction, + params: {}, + id: 'system-connector-test.system-action-connector-adapter', + }, + ], + }, + ], + }; + + const res = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/_bulk_edit`) + .set('kbn-xsrf', 'foo') + .send(payload) + .expect(200); + + expect(res.body.errors[0].message).to.eql( + 'Invalid system action params. System action type: test.system-action-connector-adapter - [myParam]: expected value of type [string] but got [undefined]' + ); + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/index.ts index 1ccbb1c8f722d..5b03010a60235 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/index.ts @@ -27,5 +27,6 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC loadTestFile(require.resolve('./alerts_default_space')); loadTestFile(require.resolve('./transform_rule_types')); loadTestFile(require.resolve('./ml_rule_types')); + loadTestFile(require.resolve('./bulk_edit')); }); }