From 07d936507bf7a426c6a0a0405a4b36438a0f1a2f Mon Sep 17 00:00:00 2001 From: Ying Date: Wed, 20 Nov 2024 15:21:52 -0500 Subject: [PATCH] Adding ability to run actions for backfill rule runs --- .../current_fields.json | 1 + .../current_mappings.json | 3 + .../check_registered_types.test.ts | 4 +- .../server/create_execute_function.test.ts | 207 +++++ .../actions/server/create_execute_function.ts | 13 +- .../actions/server/saved_objects/mappings.ts | 3 + .../action_task_params_model_versions.ts | 10 +- .../schemas/action_task_params/index.ts | 1 + .../schemas/action_task_params/v2.ts | 13 + .../backfill/apis/schedule/schemas/v1.ts | 1 + .../methods/delete/delete_backfill.test.ts | 2 +- .../methods/find/find_backfill.test.ts | 200 ++++- .../backfill/methods/find/find_backfill.ts | 7 +- .../backfill/methods/get/get_backfill.test.ts | 131 ++- .../backfill/methods/get/get_backfill.ts | 6 +- .../schedule/schedule_backfill.test.ts | 9 +- .../methods/schedule/schedule_backfill.ts | 1 + .../schedule_backfill_params_schema.ts | 1 + .../backfill/result/schemas/index.ts | 3 + ...form_ad_hoc_run_to_backfill_result.test.ts | 311 +++++-- ...transform_ad_hoc_run_to_backfill_result.ts | 49 +- ...sform_backfill_param_to_ad_hoc_run.test.ts | 103 ++- .../transform_backfill_param_to_ad_hoc_run.ts | 3 + .../backfill_client/backfill_client.test.ts | 774 +++++++++++++++++- .../server/backfill_client/backfill_client.ts | 102 ++- .../data/ad_hoc_run/types/ad_hoc_run.ts | 6 +- .../invalidate_pending_api_keys/task.test.ts | 471 ++++++++++- .../invalidate_pending_api_keys/task.ts | 61 +- .../apis/find/find_backfill_route.test.ts | 2 + .../apis/get/get_backfill_route.test.ts | 1 + .../schedule/schedule_backfill_route.test.ts | 2 + .../transforms/transform_request/v1.ts | 7 +- .../v1.test.ts | 2 + .../rules_client/lib/denormalize_actions.ts | 10 +- .../rules_client/lib/extract_references.ts | 6 +- .../ad_hoc_run_params_model_versions.ts | 12 +- .../schemas/raw_ad_hoc_run_params/index.ts | 1 + .../schemas/raw_ad_hoc_run_params/latest.ts | 11 + .../schemas/raw_ad_hoc_run_params/v1.ts | 2 +- .../schemas/raw_ad_hoc_run_params/v2.ts | 99 +++ .../action_scheduler/action_scheduler.test.ts | 177 +++- .../action_scheduler/lib/build_rule_url.ts | 5 +- .../lib/format_action_to_enqueue.test.ts | 119 +++ .../lib/format_action_to_enqueue.ts | 17 +- .../per_alert_action_scheduler.test.ts | 69 +- .../schedulers/per_alert_action_scheduler.ts | 2 + .../summary_action_scheduler.test.ts | 113 ++- .../schedulers/summary_action_scheduler.ts | 2 + .../system_action_scheduler.test.ts | 87 +- .../schedulers/system_action_scheduler.ts | 2 + .../task_runner/action_scheduler/types.ts | 9 +- .../task_runner/ad_hoc_task_runner.test.ts | 147 +++- .../server/task_runner/ad_hoc_task_runner.ts | 52 +- .../alerting/server/task_runner/fixtures.ts | 12 +- .../task_runner/transform_action_params.ts | 4 +- .../mark_available_tasks_as_claimed.ts | 4 +- .../server/saved_objects/mappings.ts | 3 + .../model_versions/task_model_versions.ts | 16 +- .../server/saved_objects/schemas/task.ts | 4 + x-pack/plugins/task_manager/server/task.ts | 5 + .../strategy_update_by_query.test.ts | 4 +- .../group1/tests/alerting/backfill/index.ts | 1 + .../tests/alerting/backfill/schedule.ts | 315 ++++++- .../tests/alerting/backfill/task_runner.ts | 170 +--- .../backfill/task_runner_with_actions.ts | 267 ++++++ .../tests/alerting/backfill/test_utils.ts | 166 ++++ 66 files changed, 3997 insertions(+), 426 deletions(-) create mode 100644 x-pack/plugins/actions/server/saved_objects/schemas/action_task_params/v2.ts create mode 100644 x-pack/plugins/alerting/server/saved_objects/schemas/raw_ad_hoc_run_params/latest.ts create mode 100644 x-pack/plugins/alerting/server/saved_objects/schemas/raw_ad_hoc_run_params/v2.ts create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/task_runner_with_actions.ts create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/test_utils.ts diff --git a/packages/kbn-check-mappings-update-cli/current_fields.json b/packages/kbn-check-mappings-update-cli/current_fields.json index 619bdd6c29321..c13cb9d831987 100644 --- a/packages/kbn-check-mappings-update-cli/current_fields.json +++ b/packages/kbn-check-mappings-update-cli/current_fields.json @@ -1094,6 +1094,7 @@ "enabled", "ownerId", "partition", + "priority", "retryAt", "runAt", "schedule", diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json index d6ec30393e099..a2c350596ce00 100644 --- a/packages/kbn-check-mappings-update-cli/current_mappings.json +++ b/packages/kbn-check-mappings-update-cli/current_mappings.json @@ -3616,6 +3616,9 @@ "partition": { "type": "integer" }, + "priority": { + "type": "integer" + }, "retryAt": { "type": "date" }, diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts index f16c956107c7b..bad85653139bf 100644 --- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts +++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts @@ -58,7 +58,7 @@ describe('checking migration metadata changes on all registered SO types', () => Object { "action": "0e6fc0b74c7312a8c11ff6b14437b93a997358b8", "action_task_params": "b50cb5c8a493881474918e8d4985e61374ca4c30", - "ad_hoc_run_params": "d4e3c5c794151d0a4f5c71e886b2aa638da73ad2", + "ad_hoc_run_params": "c7419760e878207231c3c8a25ec4d78360e07bf7", "alert": "556a03378f5ee1c31593c3a37c66b54555ee14ff", "api_key_pending_invalidation": "8f5554d1984854011b8392d9a6f7ef985bcac03c", "apm-custom-dashboards": "b67128f78160c288bd7efe25b2da6e2afd5e82fc", @@ -170,7 +170,7 @@ describe('checking migration metadata changes on all registered SO types', () => "synthetics-private-location": "8cecc9e4f39637d2f8244eb7985c0690ceab24be", "synthetics-privates-locations": "f53d799d5c9bc8454aaa32c6abc99a899b025d5c", "tag": "e2544392fe6563e215bb677abc8b01c2601ef2dc", - "task": "3c89a7c918d5b896a5f8800f06e9114ad7e7aea3", + "task": "ca8020259e46f713965a754ffae286c02d3cf05d", "telemetry": "7b00bcf1c7b4f6db1192bb7405a6a63e78b699fd", "threshold-explorer-view": "175306806f9fc8e13fcc1c8953ec4ba89bda1b70", "ui-metric": "d227284528fd19904e9d972aea0a13716fc5fe24", diff --git a/x-pack/plugins/actions/server/create_execute_function.test.ts b/x-pack/plugins/actions/server/create_execute_function.test.ts index 7be187743e634..cb8057d771013 100644 --- a/x-pack/plugins/actions/server/create_execute_function.test.ts +++ b/x-pack/plugins/actions/server/create_execute_function.test.ts @@ -16,6 +16,7 @@ import { asSavedObjectExecutionSource, } from './lib/action_execution_source'; import { actionsConfigMock } from './actions_config.mock'; +import { TaskPriority } from '@kbn/task-manager-plugin/server'; const mockTaskManager = taskManagerMock.createStart(); const savedObjectsClient = savedObjectsClientMock.create(); @@ -1189,4 +1190,210 @@ describe('bulkExecute()', () => { ] `); }); + + test('uses priority if specified', async () => { + mockTaskManager.aggregate.mockResolvedValue({ + took: 1, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { total: { value: 2, relation: 'eq' }, max_score: null, hits: [] }, + aggregations: {}, + }); + mockActionsConfig.getMaxQueued.mockReturnValueOnce(3); + const executeFn = createBulkExecutionEnqueuerFunction({ + taskManager: mockTaskManager, + actionTypeRegistry: actionTypeRegistryMock.create(), + isESOCanEncrypt: true, + inMemoryConnectors: [], + configurationUtilities: mockActionsConfig, + logger: mockLogger, + }); + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { id: '123', type: 'action', attributes: { actionTypeId: 'mock-action' }, references: [] }, + ], + }); + savedObjectsClient.bulkCreate.mockResolvedValueOnce({ + saved_objects: [ + { id: '234', type: 'action_task_params', attributes: { actionId: '123' }, references: [] }, + ], + }); + expect( + await executeFn(savedObjectsClient, [ + { + id: '123', + params: { baz: false }, + spaceId: 'default', + executionId: '123abc', + apiKey: null, + source: asHttpRequestExecutionSource(request), + actionTypeId: 'mock-action', + priority: TaskPriority.Low, + }, + { + id: '123', + params: { baz: false }, + spaceId: 'default', + executionId: '456xyz', + apiKey: null, + source: asHttpRequestExecutionSource(request), + actionTypeId: 'mock-action', + }, + ]) + ).toMatchInlineSnapshot(` + Object { + "errors": true, + "items": Array [ + Object { + "actionTypeId": "mock-action", + "id": "123", + "response": "success", + "uuid": undefined, + }, + Object { + "actionTypeId": "mock-action", + "id": "123", + "response": "queuedActionsLimitError", + "uuid": undefined, + }, + ], + } + `); + expect(mockTaskManager.bulkSchedule).toHaveBeenCalledTimes(1); + expect(mockTaskManager.bulkSchedule.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "params": Object { + "actionTaskParamsId": "234", + "spaceId": "default", + }, + "priority": 1, + "scope": Array [ + "actions", + ], + "state": Object {}, + "taskType": "actions:mock-action", + }, + ], + ] + `); + }); + + test('uses apiKeyId if specified', async () => { + mockTaskManager.aggregate.mockResolvedValue({ + took: 1, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { total: { value: 2, relation: 'eq' }, max_score: null, hits: [] }, + aggregations: {}, + }); + mockActionsConfig.getMaxQueued.mockReturnValueOnce(3); + const executeFn = createBulkExecutionEnqueuerFunction({ + taskManager: mockTaskManager, + actionTypeRegistry: actionTypeRegistryMock.create(), + isESOCanEncrypt: true, + inMemoryConnectors: [], + configurationUtilities: mockActionsConfig, + logger: mockLogger, + }); + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { id: '123', type: 'action', attributes: { actionTypeId: 'mock-action' }, references: [] }, + ], + }); + savedObjectsClient.bulkCreate.mockResolvedValueOnce({ + saved_objects: [ + { id: '234', type: 'action_task_params', attributes: { actionId: '123' }, references: [] }, + ], + }); + expect( + await executeFn(savedObjectsClient, [ + { + id: '123', + params: { baz: false }, + spaceId: 'default', + executionId: '123abc', + apiKey: null, + source: asHttpRequestExecutionSource(request), + actionTypeId: 'mock-action', + apiKeyId: '235qgbdbqet', + }, + { + id: '123', + params: { baz: false }, + spaceId: 'default', + executionId: '456xyz', + apiKey: null, + source: asHttpRequestExecutionSource(request), + actionTypeId: 'mock-action', + apiKeyId: '235qgbdbqet', + }, + ]) + ).toMatchInlineSnapshot(` + Object { + "errors": true, + "items": Array [ + Object { + "actionTypeId": "mock-action", + "id": "123", + "response": "success", + "uuid": undefined, + }, + Object { + "actionTypeId": "mock-action", + "id": "123", + "response": "queuedActionsLimitError", + "uuid": undefined, + }, + ], + } + `); + + expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( + [ + { + attributes: { + actionId: '123', + apiKey: null, + apiKeyId: '235qgbdbqet', + consumer: undefined, + executionId: '123abc', + params: { + baz: false, + }, + relatedSavedObjects: undefined, + source: 'HTTP_REQUEST', + }, + references: [ + { + id: '123', + name: 'actionRef', + type: 'action', + }, + ], + type: 'action_task_params', + }, + ], + { refresh: false } + ); + expect(mockTaskManager.bulkSchedule).toHaveBeenCalledTimes(1); + expect(mockTaskManager.bulkSchedule.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "params": Object { + "actionTaskParamsId": "234", + "spaceId": "default", + }, + "scope": Array [ + "actions", + ], + "state": Object {}, + "taskType": "actions:mock-action", + }, + ], + ] + `); + }); }); diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts index a92bff9719559..03863da041a3c 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -6,7 +6,11 @@ */ import { SavedObjectsBulkResponse, SavedObjectsClientContract, Logger } from '@kbn/core/server'; -import { RunNowResult, TaskManagerStartContract } from '@kbn/task-manager-plugin/server'; +import { + RunNowResult, + TaskManagerStartContract, + TaskPriority, +} from '@kbn/task-manager-plugin/server'; import { RawAction, ActionTypeRegistryContract, @@ -33,9 +37,11 @@ export interface ExecuteOptions id: string; uuid?: string; spaceId: string; + apiKeyId?: string; apiKey: string | null; executionId: string; actionTypeId: string; + priority?: TaskPriority; } interface ActionTaskParams @@ -171,15 +177,17 @@ export function createBulkExecutionEnqueuerFunction({ executionId: actionToExecute.executionId, consumer: actionToExecute.consumer, relatedSavedObjects: relatedSavedObjectWithRefs, + ...(actionToExecute.apiKeyId ? { apiKeyId: actionToExecute.apiKeyId } : {}), ...(actionToExecute.source ? { source: actionToExecute.source.type } : {}), }, references: taskReferences, }; }); + const actionTaskParamsRecords: SavedObjectsBulkResponse = await unsecuredSavedObjectsClient.bulkCreate(actions, { refresh: false }); - const taskInstances = actionTaskParamsRecords.saved_objects.map((so) => { + const taskInstances = actionTaskParamsRecords.saved_objects.map((so, index) => { const actionId = so.attributes.actionId; return { taskType: `actions:${actionTypeIds[actionId]}`, @@ -189,6 +197,7 @@ export function createBulkExecutionEnqueuerFunction({ }, state: {}, scope: ['actions'], + ...(runnableActions[index]?.priority ? { priority: runnableActions[index].priority } : {}), }; }); diff --git a/x-pack/plugins/actions/server/saved_objects/mappings.ts b/x-pack/plugins/actions/server/saved_objects/mappings.ts index 70e3e7447a6a0..856431198c73a 100644 --- a/x-pack/plugins/actions/server/saved_objects/mappings.ts +++ b/x-pack/plugins/actions/server/saved_objects/mappings.ts @@ -40,6 +40,9 @@ export const actionMappings: SavedObjectsTypeMappingDefinition = { export const actionTaskParamsMappings: SavedObjectsTypeMappingDefinition = { dynamic: false, properties: { + apiKeyId: { + type: 'keyword', + }, // NO NEED TO BE INDEXED // actionId: { // type: 'keyword', diff --git a/x-pack/plugins/actions/server/saved_objects/model_versions/action_task_params_model_versions.ts b/x-pack/plugins/actions/server/saved_objects/model_versions/action_task_params_model_versions.ts index 570ebff743c17..3212adfe85d4d 100644 --- a/x-pack/plugins/actions/server/saved_objects/model_versions/action_task_params_model_versions.ts +++ b/x-pack/plugins/actions/server/saved_objects/model_versions/action_task_params_model_versions.ts @@ -6,13 +6,21 @@ */ import { SavedObjectsModelVersionMap } from '@kbn/core-saved-objects-server'; -import { actionTaskParamsSchemaV1 } from '../schemas/action_task_params'; +import { actionTaskParamsSchemaV1, actionTaskParamsSchemaV2 } from '../schemas/action_task_params'; export const actionTaskParamsModelVersions: SavedObjectsModelVersionMap = { '1': { changes: [], schemas: { + forwardCompatibility: actionTaskParamsSchemaV1.extends({}, { unknowns: 'ignore' }), create: actionTaskParamsSchemaV1, }, }, + '2': { + changes: [], + schemas: { + forwardCompatibility: actionTaskParamsSchemaV2.extends({}, { unknowns: 'ignore' }), + create: actionTaskParamsSchemaV2, + }, + }, }; diff --git a/x-pack/plugins/actions/server/saved_objects/schemas/action_task_params/index.ts b/x-pack/plugins/actions/server/saved_objects/schemas/action_task_params/index.ts index 4ce0e679adf5e..cd49049abdfbc 100644 --- a/x-pack/plugins/actions/server/saved_objects/schemas/action_task_params/index.ts +++ b/x-pack/plugins/actions/server/saved_objects/schemas/action_task_params/index.ts @@ -6,3 +6,4 @@ */ export { actionTaskParamsSchema as actionTaskParamsSchemaV1 } from './v1'; +export { actionTaskParamsSchema as actionTaskParamsSchemaV2 } from './v2'; diff --git a/x-pack/plugins/actions/server/saved_objects/schemas/action_task_params/v2.ts b/x-pack/plugins/actions/server/saved_objects/schemas/action_task_params/v2.ts new file mode 100644 index 0000000000000..b0fd0c3b9f3b4 --- /dev/null +++ b/x-pack/plugins/actions/server/saved_objects/schemas/action_task_params/v2.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { actionTaskParamsSchema as actionTaskParamsSchemaV1 } from './v1'; + +export const actionTaskParamsSchema = actionTaskParamsSchemaV1.extends({ + apiKeyId: schema.maybe(schema.string()), +}); diff --git a/x-pack/plugins/alerting/common/routes/backfill/apis/schedule/schemas/v1.ts b/x-pack/plugins/alerting/common/routes/backfill/apis/schedule/schemas/v1.ts index 527134a2b5138..44844a5bcc52b 100644 --- a/x-pack/plugins/alerting/common/routes/backfill/apis/schedule/schemas/v1.ts +++ b/x-pack/plugins/alerting/common/routes/backfill/apis/schedule/schemas/v1.ts @@ -15,6 +15,7 @@ export const scheduleBodySchema = schema.arrayOf( rule_id: schema.string(), start: schema.string(), end: schema.maybe(schema.string()), + run_actions: schema.maybe(schema.boolean({ defaultValue: true })), }, { validate({ start, end }) { diff --git a/x-pack/plugins/alerting/server/application/backfill/methods/delete/delete_backfill.test.ts b/x-pack/plugins/alerting/server/application/backfill/methods/delete/delete_backfill.test.ts index b0879613d069f..d31c830c970ca 100644 --- a/x-pack/plugins/alerting/server/application/backfill/methods/delete/delete_backfill.test.ts +++ b/x-pack/plugins/alerting/server/application/backfill/methods/delete/delete_backfill.test.ts @@ -85,8 +85,8 @@ const mockAdHocRunSO: SavedObject = { name: fakeRuleName, tags: ['foo'], alertTypeId: 'myType', - // @ts-expect-error params: {}, + actions: [], apiKeyOwner: 'user', apiKeyCreatedByUser: false, consumer: 'myApp', diff --git a/x-pack/plugins/alerting/server/application/backfill/methods/find/find_backfill.test.ts b/x-pack/plugins/alerting/server/application/backfill/methods/find/find_backfill.test.ts index dab9d5f38f036..5938d8fc07ad1 100644 --- a/x-pack/plugins/alerting/server/application/backfill/methods/find/find_backfill.test.ts +++ b/x-pack/plugins/alerting/server/application/backfill/methods/find/find_backfill.test.ts @@ -6,7 +6,7 @@ */ import { ActionsAuthorization } from '@kbn/actions-plugin/server'; -import { actionsAuthorizationMock } from '@kbn/actions-plugin/server/mocks'; +import { actionsAuthorizationMock, actionsClientMock } from '@kbn/actions-plugin/server/mocks'; import { RULE_SAVED_OBJECT_TYPE } from '../../../..'; import { AlertingAuthorization } from '../../../../authorization'; import { alertingAuthorizationMock } from '../../../../authorization/alerting_authorization.mock'; @@ -22,7 +22,7 @@ import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/s import { fromKueryExpression } from '@kbn/es-query'; import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks'; import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; -import { ConstructorOptions, RulesClient } from '../../../../rules_client'; +import { RulesClient } from '../../../../rules_client'; import { adHocRunStatus } from '../../../../../common/constants'; import { ConnectorAdapterRegistry } from '../../../../connector_adapters/connector_adapter_registry'; import { SavedObject } from '@kbn/core/server'; @@ -142,35 +142,7 @@ const authDslFilter = { type: 'function', }; -const rulesClientParams: jest.Mocked = { - taskManager, - ruleTypeRegistry, - unsecuredSavedObjectsClient, - authorization: authorization as unknown as AlertingAuthorization, - actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization, - spaceId: 'default', - namespace: 'default', - getUserName: jest.fn(), - createAPIKey: jest.fn(), - logger: loggingSystemMock.create().get(), - internalSavedObjectsRepository, - encryptedSavedObjectsClient: encryptedSavedObjects, - getActionsClient: jest.fn(), - getEventLogClient: jest.fn(), - kibanaVersion, - auditLogger, - maxScheduledPerMinute: 10000, - minimumScheduleInterval: { value: '1m', enforce: false }, - isAuthenticationTypeAPIKey: jest.fn(), - getAuthenticationAPIKey: jest.fn(), - getAlertIndicesAlias: jest.fn(), - alertsService: null, - backfillClient, - isSystemAction: jest.fn(), - connectorAdapterRegistry: new ConnectorAdapterRegistry(), - uiSettings: uiSettingsServiceMock.createStartContract(), -}; - +const mockActionsClient = actionsClientMock.create(); const fakeRuleName = 'fakeRuleName'; const mockAdHocRunSO: SavedObject = { @@ -186,7 +158,6 @@ const mockAdHocRunSO: SavedObject = { name: fakeRuleName, tags: ['foo'], alertTypeId: 'myType', - // @ts-expect-error params: {}, apiKeyOwner: 'user', apiKeyCreatedByUser: false, @@ -195,6 +166,7 @@ const mockAdHocRunSO: SavedObject = { schedule: { interval: '12h', }, + actions: [], createdBy: 'user', updatedBy: 'user', createdAt: '2019-02-12T21:01:22.479Z', @@ -222,10 +194,41 @@ const mockAdHocRunSO: SavedObject = { describe('findBackfill()', () => { let rulesClient: RulesClient; + let isSystemAction: jest.Mock; beforeEach(async () => { jest.resetAllMocks(); - rulesClient = new RulesClient(rulesClientParams); + isSystemAction = jest.fn().mockReturnValue(false); + mockActionsClient.isSystemAction.mockImplementation(isSystemAction); + + rulesClient = new RulesClient({ + taskManager, + ruleTypeRegistry, + unsecuredSavedObjectsClient, + authorization: authorization as unknown as AlertingAuthorization, + actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + internalSavedObjectsRepository, + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn().mockResolvedValue(mockActionsClient), + getEventLogClient: jest.fn(), + kibanaVersion, + auditLogger, + maxScheduledPerMinute: 10000, + minimumScheduleInterval: { value: '1m', enforce: false }, + isAuthenticationTypeAPIKey: jest.fn(), + getAuthenticationAPIKey: jest.fn(), + getAlertIndicesAlias: jest.fn(), + alertsService: null, + backfillClient, + isSystemAction: jest.fn(), + connectorAdapterRegistry: new ConnectorAdapterRegistry(), + uiSettings: uiSettingsServiceMock.createStartContract(), + }); authorization.getFindAuthorizationFilter.mockResolvedValue({ filter, ensureRuleTypeIsAuthorized() {}, @@ -279,7 +282,7 @@ describe('findBackfill()', () => { page: 1, perPage: 10, total: 1, - data: [transformAdHocRunToBackfillResult(mockAdHocRunSO)], + data: [transformAdHocRunToBackfillResult({ adHocRunSO: mockAdHocRunSO, isSystemAction })], }); }); @@ -325,7 +328,7 @@ describe('findBackfill()', () => { page: 1, perPage: 10, total: 1, - data: [transformAdHocRunToBackfillResult(mockAdHocRunSO)], + data: [transformAdHocRunToBackfillResult({ adHocRunSO: mockAdHocRunSO, isSystemAction })], }); }); @@ -389,7 +392,7 @@ describe('findBackfill()', () => { page: 1, perPage: 10, total: 1, - data: [transformAdHocRunToBackfillResult(mockAdHocRunSO)], + data: [transformAdHocRunToBackfillResult({ adHocRunSO: mockAdHocRunSO, isSystemAction })], }); }); @@ -453,7 +456,7 @@ describe('findBackfill()', () => { page: 1, perPage: 10, total: 1, - data: [transformAdHocRunToBackfillResult(mockAdHocRunSO)], + data: [transformAdHocRunToBackfillResult({ adHocRunSO: mockAdHocRunSO, isSystemAction })], }); }); @@ -533,7 +536,7 @@ describe('findBackfill()', () => { page: 1, perPage: 10, total: 1, - data: [transformAdHocRunToBackfillResult(mockAdHocRunSO)], + data: [transformAdHocRunToBackfillResult({ adHocRunSO: mockAdHocRunSO, isSystemAction })], }); }); @@ -615,7 +618,124 @@ describe('findBackfill()', () => { page: 1, perPage: 10, total: 1, - data: [transformAdHocRunToBackfillResult(mockAdHocRunSO)], + data: [transformAdHocRunToBackfillResult({ adHocRunSO: mockAdHocRunSO, isSystemAction })], + }); + }); + + test('should successfully find backfill for rule with actions', async () => { + const mockAdHocRunSOWithActions = { + ...mockAdHocRunSO, + attributes: { + ...mockAdHocRunSO.attributes, + rule: { + ...mockAdHocRunSO.attributes.rule, + actions: [ + { + uuid: '123abc', + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: {}, + }, + ], + }, + }, + references: [ + { id: 'abc', name: 'rule', type: RULE_SAVED_OBJECT_TYPE }, + { id: '4', name: 'action_0', type: 'action' }, + ], + score: 0, + }; + unsecuredSavedObjectsClient.find.mockResolvedValue({ + saved_objects: [mockAdHocRunSOWithActions], + per_page: 10, + page: 1, + total: 1, + }); + + const result = await rulesClient.findBackfill({ + page: 1, + perPage: 10, + start: '2024-02-09T02:07:55Z', + end: '2024-03-29T02:07:55Z', + ruleIds: 'abc', + }); + + expect(authorization.getFindAuthorizationFilter).toHaveBeenCalledWith('rule', { + fieldNames: { + consumer: 'ad_hoc_run_params.attributes.rule.consumer', + ruleTypeId: 'ad_hoc_run_params.attributes.rule.alertTypeId', + }, + type: 'kql', + }); + + expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledWith({ + filter: { + type: 'function', + function: 'and', + arguments: [ + { + type: 'function', + function: 'and', + arguments: [ + { + type: 'function', + function: 'range', + arguments: [ + { isQuoted: false, type: 'literal', value: 'ad_hoc_run_params.attributes.start' }, + 'gte', + { isQuoted: true, type: 'literal', value: '2024-02-09T02:07:55Z' }, + ], + }, + { + type: 'function', + function: 'range', + arguments: [ + { isQuoted: false, type: 'literal', value: 'ad_hoc_run_params.attributes.end' }, + 'lte', + { isQuoted: true, type: 'literal', value: '2024-03-29T02:07:55Z' }, + ], + }, + ], + }, + authDslFilter, + ], + }, + hasReference: [{ id: 'abc', type: RULE_SAVED_OBJECT_TYPE }], + page: 1, + perPage: 10, + type: AD_HOC_RUN_SAVED_OBJECT_TYPE, + }); + + expect(auditLogger.log).toHaveBeenCalledTimes(1); + expect(auditLogger.log).toHaveBeenNthCalledWith(1, { + event: { + action: 'ad_hoc_run_find', + category: ['database'], + outcome: 'success', + type: ['access'], + }, + kibana: { + saved_object: { + id: '1', + type: AD_HOC_RUN_SAVED_OBJECT_TYPE, + name: 'backfill for rule "fakeRuleName"', + }, + }, + message: + 'User has found ad hoc run for ad_hoc_run_params [id=1] backfill for rule "fakeRuleName"', + }); + + expect(result).toEqual({ + page: 1, + perPage: 10, + total: 1, + data: [ + transformAdHocRunToBackfillResult({ + adHocRunSO: mockAdHocRunSOWithActions, + isSystemAction, + }), + ], }); }); @@ -667,7 +787,7 @@ describe('findBackfill()', () => { page: 1, perPage: 10, total: 1, - data: [transformAdHocRunToBackfillResult(mockAdHocRunSO)], + data: [transformAdHocRunToBackfillResult({ adHocRunSO: mockAdHocRunSO, isSystemAction })], }); }); diff --git a/x-pack/plugins/alerting/server/application/backfill/methods/find/find_backfill.ts b/x-pack/plugins/alerting/server/application/backfill/methods/find/find_backfill.ts index 522128d0385f8..f5ff139571621 100644 --- a/x-pack/plugins/alerting/server/application/backfill/methods/find/find_backfill.ts +++ b/x-pack/plugins/alerting/server/application/backfill/methods/find/find_backfill.ts @@ -98,6 +98,8 @@ export async function findBackfill( ...(params.sortOrder ? { sortOrder: params.sortOrder } : {}), }); + const actionsClient = await context.getActionsClient(); + const transformedData: Backfill[] = data.map((so: SavedObject) => { context.auditLogger?.log( adHocRunAuditEvent({ @@ -110,7 +112,10 @@ export async function findBackfill( }) ); - return transformAdHocRunToBackfillResult(so) as Backfill; + return transformAdHocRunToBackfillResult({ + adHocRunSO: so, + isSystemAction: (id: string) => actionsClient.isSystemAction(id), + }) as Backfill; }); return { diff --git a/x-pack/plugins/alerting/server/application/backfill/methods/get/get_backfill.test.ts b/x-pack/plugins/alerting/server/application/backfill/methods/get/get_backfill.test.ts index cbf516bfd446e..7ed0db8a0a9fd 100644 --- a/x-pack/plugins/alerting/server/application/backfill/methods/get/get_backfill.test.ts +++ b/x-pack/plugins/alerting/server/application/backfill/methods/get/get_backfill.test.ts @@ -6,7 +6,7 @@ */ import { ActionsAuthorization } from '@kbn/actions-plugin/server'; -import { actionsAuthorizationMock } from '@kbn/actions-plugin/server/mocks'; +import { actionsAuthorizationMock, actionsClientMock } from '@kbn/actions-plugin/server/mocks'; import { RULE_SAVED_OBJECT_TYPE } from '../../../..'; import { AlertingAuthorization } from '../../../../authorization'; import { alertingAuthorizationMock } from '../../../../authorization/alerting_authorization.mock'; @@ -21,7 +21,7 @@ import { uiSettingsServiceMock } from '@kbn/core-ui-settings-server-mocks'; import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks'; import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks'; import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; -import { ConstructorOptions, RulesClient } from '../../../../rules_client'; +import { RulesClient } from '../../../../rules_client'; import { adHocRunStatus } from '../../../../../common/constants'; import { ConnectorAdapterRegistry } from '../../../../connector_adapters/connector_adapter_registry'; import { AD_HOC_RUN_SAVED_OBJECT_TYPE } from '../../../../saved_objects'; @@ -41,37 +41,9 @@ const internalSavedObjectsRepository = savedObjectsRepositoryMock.create(); const backfillClient = backfillClientMock.create(); const logger = loggingSystemMock.create().get(); -const rulesClientParams: jest.Mocked = { - taskManager, - ruleTypeRegistry, - unsecuredSavedObjectsClient, - authorization: authorization as unknown as AlertingAuthorization, - actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization, - spaceId: 'default', - namespace: 'default', - getUserName: jest.fn(), - createAPIKey: jest.fn(), - logger, - internalSavedObjectsRepository, - encryptedSavedObjectsClient: encryptedSavedObjects, - getActionsClient: jest.fn(), - getEventLogClient: jest.fn(), - kibanaVersion, - auditLogger, - maxScheduledPerMinute: 10000, - minimumScheduleInterval: { value: '1m', enforce: false }, - isAuthenticationTypeAPIKey: jest.fn(), - getAuthenticationAPIKey: jest.fn(), - getAlertIndicesAlias: jest.fn(), - alertsService: null, - backfillClient, - isSystemAction: jest.fn(), - connectorAdapterRegistry: new ConnectorAdapterRegistry(), - uiSettings: uiSettingsServiceMock.createStartContract(), -}; - const fakeRuleName = 'fakeRuleName'; +const mockActionsClient = actionsClientMock.create(); const mockAdHocRunSO: SavedObject = { id: '1', type: AD_HOC_RUN_SAVED_OBJECT_TYPE, @@ -85,7 +57,7 @@ const mockAdHocRunSO: SavedObject = { name: fakeRuleName, tags: ['foo'], alertTypeId: 'myType', - // @ts-expect-error + actions: [], params: {}, apiKeyOwner: 'user', apiKeyCreatedByUser: false, @@ -121,10 +93,41 @@ const mockAdHocRunSO: SavedObject = { describe('getBackfill()', () => { let rulesClient: RulesClient; + let isSystemAction: jest.Mock; beforeEach(async () => { jest.resetAllMocks(); - rulesClient = new RulesClient(rulesClientParams); + isSystemAction = jest.fn().mockReturnValue(false); + mockActionsClient.isSystemAction.mockImplementation(isSystemAction); + + rulesClient = new RulesClient({ + taskManager, + ruleTypeRegistry, + unsecuredSavedObjectsClient, + authorization: authorization as unknown as AlertingAuthorization, + actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + logger, + internalSavedObjectsRepository, + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn().mockResolvedValue(mockActionsClient), + getEventLogClient: jest.fn(), + kibanaVersion, + auditLogger, + maxScheduledPerMinute: 10000, + minimumScheduleInterval: { value: '1m', enforce: false }, + isAuthenticationTypeAPIKey: jest.fn(), + getAuthenticationAPIKey: jest.fn(), + getAlertIndicesAlias: jest.fn(), + alertsService: null, + backfillClient, + isSystemAction: jest.fn(), + connectorAdapterRegistry: new ConnectorAdapterRegistry(), + uiSettings: uiSettingsServiceMock.createStartContract(), + }); unsecuredSavedObjectsClient.get.mockResolvedValue(mockAdHocRunSO); }); @@ -158,7 +161,67 @@ describe('getBackfill()', () => { }); expect(logger.error).not.toHaveBeenCalled(); - expect(result).toEqual(transformAdHocRunToBackfillResult(mockAdHocRunSO)); + expect(result).toEqual( + transformAdHocRunToBackfillResult({ adHocRunSO: mockAdHocRunSO, isSystemAction }) + ); + }); + + test('should successfully get backfill with actions', async () => { + const mockAdHocRunSOWithActions = { + ...mockAdHocRunSO, + attributes: { + ...mockAdHocRunSO.attributes, + rule: { + ...mockAdHocRunSO.attributes.rule, + actions: [ + { + uuid: '123abc', + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: {}, + }, + ], + }, + }, + references: [ + { id: 'abc', name: 'rule', type: RULE_SAVED_OBJECT_TYPE }, + { id: '4', name: 'action_0', type: 'action' }, + ], + }; + unsecuredSavedObjectsClient.get.mockResolvedValue(mockAdHocRunSOWithActions); + const result = await rulesClient.getBackfill('1'); + + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith(AD_HOC_RUN_SAVED_OBJECT_TYPE, '1'); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + entity: 'rule', + consumer: 'myApp', + operation: 'getBackfill', + ruleTypeId: 'myType', + }); + expect(auditLogger.log).toHaveBeenCalledTimes(1); + expect(auditLogger.log).toHaveBeenCalledWith({ + event: { + action: 'ad_hoc_run_get', + category: ['database'], + outcome: 'success', + type: ['access'], + }, + kibana: { + saved_object: { + id: '1', + type: AD_HOC_RUN_SAVED_OBJECT_TYPE, + name: `backfill for rule "fakeRuleName"`, + }, + }, + message: + 'User has got ad hoc run for ad_hoc_run_params [id=1] backfill for rule "fakeRuleName"', + }); + expect(logger.error).not.toHaveBeenCalled(); + + expect(result).toEqual( + transformAdHocRunToBackfillResult({ adHocRunSO: mockAdHocRunSOWithActions, isSystemAction }) + ); }); describe('error handling', () => { diff --git a/x-pack/plugins/alerting/server/application/backfill/methods/get/get_backfill.ts b/x-pack/plugins/alerting/server/application/backfill/methods/get/get_backfill.ts index 6f14dba88684c..4bd2ff3a5d4b3 100644 --- a/x-pack/plugins/alerting/server/application/backfill/methods/get/get_backfill.ts +++ b/x-pack/plugins/alerting/server/application/backfill/methods/get/get_backfill.ts @@ -73,7 +73,11 @@ export async function getBackfill(context: RulesClientContext, id: string): Prom }) ); - return transformAdHocRunToBackfillResult(result) as Backfill; + const actionsClient = await context.getActionsClient(); + return transformAdHocRunToBackfillResult({ + adHocRunSO: result, + isSystemAction: (connectorId: string) => actionsClient.isSystemAction(connectorId), + }) as Backfill; } catch (err) { const errorMessage = `Failed to get backfill by id: ${id}`; context.logger.error(`${errorMessage} - ${err}`); diff --git a/x-pack/plugins/alerting/server/application/backfill/methods/schedule/schedule_backfill.test.ts b/x-pack/plugins/alerting/server/application/backfill/methods/schedule/schedule_backfill.test.ts index b8f1e5af9c869..106a2713b2367 100644 --- a/x-pack/plugins/alerting/server/application/backfill/methods/schedule/schedule_backfill.test.ts +++ b/x-pack/plugins/alerting/server/application/backfill/methods/schedule/schedule_backfill.test.ts @@ -180,6 +180,7 @@ function getMockData(overwrites: Record = {}): ScheduleBackfill return { ruleId: '1', start: '2023-11-16T08:00:00.000Z', + runActions: true, ...overwrites, }; } @@ -478,13 +479,13 @@ describe('scheduleBackfill()', () => { // @ts-expect-error rulesClient.scheduleBackfill(getMockData()) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Error validating backfill schedule parameters \\"{\\"ruleId\\":\\"1\\",\\"start\\":\\"2023-11-16T08:00:00.000Z\\"}\\" - expected value of type [array] but got [Object]"` + `"Error validating backfill schedule parameters \\"{\\"ruleId\\":\\"1\\",\\"start\\":\\"2023-11-16T08:00:00.000Z\\",\\"runActions\\":true}\\" - expected value of type [array] but got [Object]"` ); await expect( rulesClient.scheduleBackfill([getMockData({ ruleId: 1 })]) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Error validating backfill schedule parameters \\"[{\\"ruleId\\":1,\\"start\\":\\"2023-11-16T08:00:00.000Z\\"}]\\" - [0.ruleId]: expected value of type [string] but got [number]"` + `"Error validating backfill schedule parameters \\"[{\\"ruleId\\":1,\\"start\\":\\"2023-11-16T08:00:00.000Z\\",\\"runActions\\":true}]\\" - [0.ruleId]: expected value of type [string] but got [number]"` ); }); @@ -499,7 +500,7 @@ describe('scheduleBackfill()', () => { }), ]) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Error validating backfill schedule parameters \\"[{\\"ruleId\\":\\"1\\",\\"start\\":\\"2023-11-16T08:00:00.000Z\\"},{\\"ruleId\\":\\"2\\",\\"start\\":\\"2023-11-17T08:00:00.000Z\\",\\"end\\":\\"2023-11-17T08:00:00.000Z\\"}]\\" - [1]: Backfill end must be greater than backfill start"` + `"Error validating backfill schedule parameters \\"[{\\"ruleId\\":\\"1\\",\\"start\\":\\"2023-11-16T08:00:00.000Z\\",\\"runActions\\":true},{\\"ruleId\\":\\"2\\",\\"start\\":\\"2023-11-17T08:00:00.000Z\\",\\"runActions\\":true,\\"end\\":\\"2023-11-17T08:00:00.000Z\\"}]\\" - [1]: Backfill end must be greater than backfill start"` ); await expect( @@ -512,7 +513,7 @@ describe('scheduleBackfill()', () => { }), ]) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Error validating backfill schedule parameters \\"[{\\"ruleId\\":\\"1\\",\\"start\\":\\"2023-11-16T08:00:00.000Z\\"},{\\"ruleId\\":\\"2\\",\\"start\\":\\"2023-11-17T08:00:00.000Z\\",\\"end\\":\\"2023-11-16T08:00:00.000Z\\"}]\\" - [1]: Backfill end must be greater than backfill start"` + `"Error validating backfill schedule parameters \\"[{\\"ruleId\\":\\"1\\",\\"start\\":\\"2023-11-16T08:00:00.000Z\\",\\"runActions\\":true},{\\"ruleId\\":\\"2\\",\\"start\\":\\"2023-11-17T08:00:00.000Z\\",\\"runActions\\":true,\\"end\\":\\"2023-11-16T08:00:00.000Z\\"}]\\" - [1]: Backfill end must be greater than backfill start"` ); }); diff --git a/x-pack/plugins/alerting/server/application/backfill/methods/schedule/schedule_backfill.ts b/x-pack/plugins/alerting/server/application/backfill/methods/schedule/schedule_backfill.ts index 534262aa31c31..2f2816bf95867 100644 --- a/x-pack/plugins/alerting/server/application/backfill/methods/schedule/schedule_backfill.ts +++ b/x-pack/plugins/alerting/server/application/backfill/methods/schedule/schedule_backfill.ts @@ -145,6 +145,7 @@ export async function scheduleBackfill( const actionsClient = await context.getActionsClient(); return await context.backfillClient.bulkQueue({ + actionsClient, auditLogger: context.auditLogger, params, rules: rulesToSchedule.map(({ id, attributes, references }) => { diff --git a/x-pack/plugins/alerting/server/application/backfill/methods/schedule/schemas/schedule_backfill_params_schema.ts b/x-pack/plugins/alerting/server/application/backfill/methods/schedule/schemas/schedule_backfill_params_schema.ts index c4a469da1b5db..7b54afc428e48 100644 --- a/x-pack/plugins/alerting/server/application/backfill/methods/schedule/schemas/schedule_backfill_params_schema.ts +++ b/x-pack/plugins/alerting/server/application/backfill/methods/schedule/schemas/schedule_backfill_params_schema.ts @@ -14,6 +14,7 @@ export const scheduleBackfillParamSchema = schema.object( ruleId: schema.string(), start: schema.string(), end: schema.maybe(schema.string()), + runActions: schema.maybe(schema.boolean({ defaultValue: true })), }, { validate({ start, end }) { diff --git a/x-pack/plugins/alerting/server/application/backfill/result/schemas/index.ts b/x-pack/plugins/alerting/server/application/backfill/result/schemas/index.ts index b454d41dd40ca..ecba6cc45ae1c 100644 --- a/x-pack/plugins/alerting/server/application/backfill/result/schemas/index.ts +++ b/x-pack/plugins/alerting/server/application/backfill/result/schemas/index.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; import { ruleParamsSchema } from '@kbn/response-ops-rule-params'; import { adHocRunStatus } from '../../../../../common/constants'; +import { actionSchema as ruleActionSchema } from '../../../rule/schemas/action_schemas'; export const statusSchema = schema.oneOf([ schema.literal(adHocRunStatus.COMPLETE), @@ -32,6 +33,7 @@ export const backfillSchema = schema.object({ id: schema.string(), name: schema.string(), tags: schema.arrayOf(schema.string()), + actions: schema.arrayOf(ruleActionSchema), alertTypeId: schema.string(), params: ruleParamsSchema, apiKeyOwner: schema.nullable(schema.string()), @@ -50,4 +52,5 @@ export const backfillSchema = schema.object({ status: statusSchema, end: schema.maybe(schema.string()), schedule: schema.arrayOf(backfillScheduleSchema), + warnings: schema.maybe(schema.arrayOf(schema.string())), }); diff --git a/x-pack/plugins/alerting/server/application/backfill/transforms/transform_ad_hoc_run_to_backfill_result.test.ts b/x-pack/plugins/alerting/server/application/backfill/transforms/transform_ad_hoc_run_to_backfill_result.test.ts index 995240cbbd023..2d0e6ff20da08 100644 --- a/x-pack/plugins/alerting/server/application/backfill/transforms/transform_ad_hoc_run_to_backfill_result.test.ts +++ b/x-pack/plugins/alerting/server/application/backfill/transforms/transform_ad_hoc_run_to_backfill_result.test.ts @@ -8,17 +8,24 @@ import { AdHocRunSO } from '../../../data/ad_hoc_run/types'; import { SavedObject } from '@kbn/core/server'; import { adHocRunStatus } from '../../../../common/constants'; -import { transformAdHocRunToBackfillResult } from './transform_ad_hoc_run_to_backfill_result'; +import { + transformAdHocRunToAdHocRunData, + transformAdHocRunToBackfillResult, +} from './transform_ad_hoc_run_to_backfill_result'; +import { RawRule } from '../../../types'; + +const isSystemAction = jest.fn().mockReturnValue(false); function getMockAdHocRunAttributes({ ruleId, - overwrites, omitApiKey = false, + actions, }: { ruleId?: string; - overwrites?: Record; omitApiKey?: boolean; + actions?: RawRule['actions']; } = {}): AdHocRunSO { + // @ts-expect-error return { ...(omitApiKey ? {} : { apiKeyId: '123', apiKeyToUse: 'MTIzOmFiYw==' }), createdAt: '2024-01-30T00:00:00.000Z', @@ -29,7 +36,7 @@ function getMockAdHocRunAttributes({ name: 'my rule name', tags: ['foo'], alertTypeId: 'myType', - // @ts-expect-error + actions: actions ? actions : [], params: {}, apiKeyOwner: 'user', apiKeyCreatedByUser: false, @@ -59,14 +66,14 @@ function getMockAdHocRunAttributes({ runAt: '2023-10-20T15:07:40.011Z', }, ], - ...overwrites, }; } function getBulkCreateResponse( id: string, ruleId: string, - attributes: AdHocRunSO + attributes: AdHocRunSO, + additionalReferences?: Array<{ id: string; name: string; type: string }> ): SavedObject { return { type: 'ad_hoc_rule_run_params', @@ -79,6 +86,7 @@ function getBulkCreateResponse( name: 'rule', type: 'alert', }, + ...(additionalReferences ?? []), ], managed: false, coreMigrationVersion: '8.8.0', @@ -91,9 +99,74 @@ function getBulkCreateResponse( describe('transformAdHocRunToBackfillResult', () => { test('should transform bulk create response', () => { expect( - transformAdHocRunToBackfillResult( - getBulkCreateResponse('abc', '1', getMockAdHocRunAttributes()) - ) + transformAdHocRunToBackfillResult({ + adHocRunSO: getBulkCreateResponse('abc', '1', getMockAdHocRunAttributes()), + isSystemAction, + }) + ).toEqual({ + id: 'abc', + createdAt: '2024-01-30T00:00:00.000Z', + duration: '12h', + enabled: true, + rule: { + id: '1', + name: 'my rule name', + tags: ['foo'], + actions: [], + alertTypeId: 'myType', + params: {}, + apiKeyOwner: 'user', + apiKeyCreatedByUser: false, + consumer: 'myApp', + enabled: true, + schedule: { + interval: '12h', + }, + createdBy: 'user', + updatedBy: 'user', + createdAt: '2019-02-12T21:01:22.479Z', + updatedAt: '2019-02-12T21:01:22.479Z', + revision: 0, + }, + spaceId: 'default', + start: '2023-10-19T15:07:40.011Z', + status: adHocRunStatus.PENDING, + schedule: [ + { + interval: '12h', + status: adHocRunStatus.PENDING, + runAt: '2023-10-20T03:07:40.011Z', + }, + { + interval: '12h', + status: adHocRunStatus.PENDING, + runAt: '2023-10-20T15:07:40.011Z', + }, + ], + }); + }); + + test('should transform bulk create response with actions', () => { + expect( + transformAdHocRunToBackfillResult({ + adHocRunSO: getBulkCreateResponse( + 'abc', + '1', + getMockAdHocRunAttributes({ + actions: [ + { + uuid: '123abc', + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: {}, + }, + ], + }), + [{ id: '4', name: 'action_0', type: 'action' }] + ), + isSystemAction, + }) ).toEqual({ id: 'abc', createdAt: '2024-01-30T00:00:00.000Z', @@ -103,6 +176,7 @@ describe('transformAdHocRunToBackfillResult', () => { id: '1', name: 'my rule name', tags: ['foo'], + actions: [{ actionTypeId: 'test', group: 'default', id: '4', params: {}, uuid: '123abc' }], alertTypeId: 'myType', params: {}, apiKeyOwner: 'user', @@ -138,10 +212,10 @@ describe('transformAdHocRunToBackfillResult', () => { test('should return error for malformed responses when original create request is not provided', () => { expect( - transformAdHocRunToBackfillResult( + transformAdHocRunToBackfillResult({ // missing id // @ts-expect-error - { + adHocRunSO: { type: 'ad_hoc_rule_run_params', namespaces: ['default'], attributes: getMockAdHocRunAttributes(), @@ -151,8 +225,9 @@ describe('transformAdHocRunToBackfillResult', () => { updated_at: '2024-02-07T16:05:39.296Z', created_at: '2024-02-07T16:05:39.296Z', version: 'WzcsMV0=', - } - ) + }, + isSystemAction, + }) ).toEqual({ error: { message: 'Malformed saved object in bulkCreate response - Missing "id".', @@ -160,10 +235,10 @@ describe('transformAdHocRunToBackfillResult', () => { }, }); expect( - transformAdHocRunToBackfillResult( + transformAdHocRunToBackfillResult({ // missing attributes // @ts-expect-error - { + adHocRunSO: { type: 'ad_hoc_rule_run_params', id: 'abc', namespaces: ['default'], @@ -173,8 +248,9 @@ describe('transformAdHocRunToBackfillResult', () => { updated_at: '2024-02-07T16:05:39.296Z', created_at: '2024-02-07T16:05:39.296Z', version: 'WzcsMV0=', - } - ) + }, + isSystemAction, + }) ).toEqual({ error: { message: 'Malformed saved object in bulkCreate response - Missing "attributes".', @@ -182,10 +258,10 @@ describe('transformAdHocRunToBackfillResult', () => { }, }); expect( - transformAdHocRunToBackfillResult( + transformAdHocRunToBackfillResult({ // missing references // @ts-expect-error - { + adHocRunSO: { type: 'ad_hoc_rule_run_params', id: 'def', namespaces: ['default'], @@ -195,8 +271,9 @@ describe('transformAdHocRunToBackfillResult', () => { updated_at: '2024-02-07T16:05:39.296Z', created_at: '2024-02-07T16:05:39.296Z', version: 'WzcsMV0=', - } - ) + }, + isSystemAction, + }) ).toEqual({ error: { message: 'Malformed saved object in bulkCreate response - Missing "references".', @@ -204,9 +281,9 @@ describe('transformAdHocRunToBackfillResult', () => { }, }); expect( - transformAdHocRunToBackfillResult( + transformAdHocRunToBackfillResult({ // empty references - { + adHocRunSO: { type: 'ad_hoc_rule_run_params', id: 'ghi', namespaces: ['default'], @@ -217,8 +294,9 @@ describe('transformAdHocRunToBackfillResult', () => { updated_at: '2024-02-07T16:05:39.296Z', created_at: '2024-02-07T16:05:39.296Z', version: 'WzcsMV0=', - } - ) + }, + isSystemAction, + }) ).toEqual({ error: { message: 'Malformed saved object in bulkCreate response - Missing "references".', @@ -230,10 +308,10 @@ describe('transformAdHocRunToBackfillResult', () => { test('should return error for malformed responses when original create request is provided', () => { const attributes = getMockAdHocRunAttributes(); expect( - transformAdHocRunToBackfillResult( + transformAdHocRunToBackfillResult({ // missing id // @ts-expect-error - { + adHocRunSO: { type: 'ad_hoc_rule_run_params', namespaces: ['default'], attributes, @@ -244,12 +322,13 @@ describe('transformAdHocRunToBackfillResult', () => { created_at: '2024-02-07T16:05:39.296Z', version: 'WzcsMV0=', }, - { + originalSO: { type: 'ad_hoc_rule_run_params', attributes, references: [{ id: '1', name: 'rule', type: 'alert' }], - } - ) + }, + isSystemAction, + }) ).toEqual({ error: { message: 'Malformed saved object in bulkCreate response - Missing "id".', @@ -257,10 +336,10 @@ describe('transformAdHocRunToBackfillResult', () => { }, }); expect( - transformAdHocRunToBackfillResult( + transformAdHocRunToBackfillResult({ // missing attributes // @ts-expect-error - { + adHocRunSO: { type: 'ad_hoc_rule_run_params', id: 'abc', namespaces: ['default'], @@ -271,12 +350,13 @@ describe('transformAdHocRunToBackfillResult', () => { created_at: '2024-02-07T16:05:39.296Z', version: 'WzcsMV0=', }, - { + originalSO: { type: 'ad_hoc_rule_run_params', attributes, references: [{ id: '1', name: 'rule', type: 'alert' }], - } - ) + }, + isSystemAction, + }) ).toEqual({ error: { message: 'Malformed saved object in bulkCreate response - Missing "attributes".', @@ -284,10 +364,10 @@ describe('transformAdHocRunToBackfillResult', () => { }, }); expect( - transformAdHocRunToBackfillResult( + transformAdHocRunToBackfillResult({ // missing references // @ts-expect-error - { + adHocRunSO: { type: 'ad_hoc_rule_run_params', id: 'def', namespaces: ['default'], @@ -298,12 +378,13 @@ describe('transformAdHocRunToBackfillResult', () => { created_at: '2024-02-07T16:05:39.296Z', version: 'WzcsMV0=', }, - { + originalSO: { type: 'ad_hoc_rule_run_params', attributes, references: [{ id: '1', name: 'rule', type: 'alert' }], - } - ) + }, + isSystemAction, + }) ).toEqual({ error: { message: 'Malformed saved object in bulkCreate response - Missing "references".', @@ -311,9 +392,9 @@ describe('transformAdHocRunToBackfillResult', () => { }, }); expect( - transformAdHocRunToBackfillResult( + transformAdHocRunToBackfillResult({ // empty references - { + adHocRunSO: { type: 'ad_hoc_rule_run_params', id: 'ghi', namespaces: ['default'], @@ -325,12 +406,13 @@ describe('transformAdHocRunToBackfillResult', () => { created_at: '2024-02-07T16:05:39.296Z', version: 'WzcsMV0=', }, - { + originalSO: { type: 'ad_hoc_rule_run_params', attributes, references: [{ id: '1', name: 'rule', type: 'alert' }], - } - ) + }, + isSystemAction, + }) ).toEqual({ error: { message: 'Malformed saved object in bulkCreate response - Missing "references".', @@ -341,9 +423,9 @@ describe('transformAdHocRunToBackfillResult', () => { test('should pass through error if saved object error when original create request is not provided', () => { expect( - transformAdHocRunToBackfillResult( + transformAdHocRunToBackfillResult({ // @ts-expect-error - { + adHocRunSO: { type: 'ad_hoc_rule_run_params', id: '788a2784-c021-484f-a53e-0c1c63c7567c', error: { @@ -351,8 +433,9 @@ describe('transformAdHocRunToBackfillResult', () => { message: 'Unable to create', statusCode: 404, }, - } - ) + }, + isSystemAction, + }) ).toEqual({ error: { message: 'Unable to create', @@ -363,9 +446,9 @@ describe('transformAdHocRunToBackfillResult', () => { test('should pass through error if saved object error when original create request is provided', () => { expect( - transformAdHocRunToBackfillResult( + transformAdHocRunToBackfillResult({ // @ts-expect-error - { + adHocRunSO: { type: 'ad_hoc_rule_run_params', id: '788a2784-c021-484f-a53e-0c1c63c7567c', error: { @@ -374,12 +457,13 @@ describe('transformAdHocRunToBackfillResult', () => { statusCode: 404, }, }, - { + originalSO: { type: 'ad_hoc_rule_run_params', attributes: getMockAdHocRunAttributes(), references: [{ id: '1', name: 'rule', type: 'alert' }], - } - ) + }, + isSystemAction, + }) ).toEqual({ error: { message: 'Unable to create', @@ -388,3 +472,122 @@ describe('transformAdHocRunToBackfillResult', () => { }); }); }); + +describe('transformAdHocRunToAdHocRunData', () => { + test('should transform bulk create response and include api key', () => { + expect( + transformAdHocRunToAdHocRunData({ + adHocRunSO: getBulkCreateResponse('abc', '1', getMockAdHocRunAttributes()), + isSystemAction, + }) + ).toEqual({ + id: 'abc', + apiKeyId: '123', + apiKeyToUse: 'MTIzOmFiYw==', + createdAt: '2024-01-30T00:00:00.000Z', + duration: '12h', + enabled: true, + rule: { + id: '1', + name: 'my rule name', + tags: ['foo'], + actions: [], + alertTypeId: 'myType', + params: {}, + apiKeyOwner: 'user', + apiKeyCreatedByUser: false, + consumer: 'myApp', + enabled: true, + schedule: { + interval: '12h', + }, + createdBy: 'user', + updatedBy: 'user', + createdAt: '2019-02-12T21:01:22.479Z', + updatedAt: '2019-02-12T21:01:22.479Z', + revision: 0, + }, + spaceId: 'default', + start: '2023-10-19T15:07:40.011Z', + status: adHocRunStatus.PENDING, + schedule: [ + { + interval: '12h', + status: adHocRunStatus.PENDING, + runAt: '2023-10-20T03:07:40.011Z', + }, + { + interval: '12h', + status: adHocRunStatus.PENDING, + runAt: '2023-10-20T15:07:40.011Z', + }, + ], + }); + }); + + test('should transform bulk create response with actions and include api key', () => { + expect( + transformAdHocRunToAdHocRunData({ + adHocRunSO: getBulkCreateResponse( + 'abc', + '1', + getMockAdHocRunAttributes({ + actions: [ + { + uuid: '123abc', + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: {}, + }, + ], + }), + [{ id: '4', name: 'action_0', type: 'action' }] + ), + isSystemAction, + }) + ).toEqual({ + id: 'abc', + apiKeyId: '123', + apiKeyToUse: 'MTIzOmFiYw==', + createdAt: '2024-01-30T00:00:00.000Z', + duration: '12h', + enabled: true, + rule: { + id: '1', + name: 'my rule name', + tags: ['foo'], + actions: [{ actionTypeId: 'test', group: 'default', id: '4', params: {}, uuid: '123abc' }], + alertTypeId: 'myType', + params: {}, + apiKeyOwner: 'user', + apiKeyCreatedByUser: false, + consumer: 'myApp', + enabled: true, + schedule: { + interval: '12h', + }, + createdBy: 'user', + updatedBy: 'user', + createdAt: '2019-02-12T21:01:22.479Z', + updatedAt: '2019-02-12T21:01:22.479Z', + revision: 0, + }, + spaceId: 'default', + start: '2023-10-19T15:07:40.011Z', + status: adHocRunStatus.PENDING, + schedule: [ + { + interval: '12h', + status: adHocRunStatus.PENDING, + runAt: '2023-10-20T03:07:40.011Z', + }, + { + interval: '12h', + status: adHocRunStatus.PENDING, + runAt: '2023-10-20T15:07:40.011Z', + }, + ], + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/application/backfill/transforms/transform_ad_hoc_run_to_backfill_result.ts b/x-pack/plugins/alerting/server/application/backfill/transforms/transform_ad_hoc_run_to_backfill_result.ts index 13257742b7005..78b228711df7d 100644 --- a/x-pack/plugins/alerting/server/application/backfill/transforms/transform_ad_hoc_run_to_backfill_result.ts +++ b/x-pack/plugins/alerting/server/application/backfill/transforms/transform_ad_hoc_run_to_backfill_result.ts @@ -6,14 +6,25 @@ */ import { SavedObject, SavedObjectsBulkCreateObject } from '@kbn/core/server'; -import { AdHocRunSO } from '../../../data/ad_hoc_run/types'; +import { AdHocRun, AdHocRunSO } from '../../../data/ad_hoc_run/types'; import { createBackfillError } from '../../../backfill_client/lib'; import { ScheduleBackfillResult } from '../methods/schedule/types'; +import { transformRawActionsToDomainActions } from '../../rule/transforms'; -export const transformAdHocRunToBackfillResult = ( - { id, attributes, references, error }: SavedObject, - originalSO?: SavedObjectsBulkCreateObject -): ScheduleBackfillResult => { +interface TransformAdHocRunToBackfillResultOpts { + adHocRunSO: SavedObject; + isSystemAction: (connectorId: string) => boolean; + originalSO?: SavedObjectsBulkCreateObject; + omitGeneratedActionValues?: boolean; +} + +export const transformAdHocRunToBackfillResult = ({ + adHocRunSO, + isSystemAction, + originalSO, + omitGeneratedActionValues = true, +}: TransformAdHocRunToBackfillResultOpts): ScheduleBackfillResult => { + const { id, attributes, references, error } = adHocRunSO; const ruleId = references?.[0]?.id ?? originalSO?.references?.[0]?.id ?? 'unknown'; const ruleName = attributes?.rule?.name ?? originalSO?.attributes?.rule.name; if (error) { @@ -55,6 +66,13 @@ export const transformAdHocRunToBackfillResult = ( rule: { ...attributes.rule, id: references[0].id, + actions: transformRawActionsToDomainActions({ + ruleId: id, + actions: attributes.rule.actions, + references, + isSystemAction, + omitGeneratedValues: omitGeneratedActionValues, + }), }, spaceId: attributes.spaceId, start: attributes.start, @@ -62,3 +80,24 @@ export const transformAdHocRunToBackfillResult = ( schedule: attributes.schedule, }; }; + +// includes API key information +export const transformAdHocRunToAdHocRunData = ({ + adHocRunSO, + isSystemAction, + originalSO, + omitGeneratedActionValues = true, +}: TransformAdHocRunToBackfillResultOpts): AdHocRun => { + const result = transformAdHocRunToBackfillResult({ + adHocRunSO, + isSystemAction, + originalSO, + omitGeneratedActionValues, + }); + + return { + ...result, + apiKeyId: adHocRunSO.attributes.apiKeyId, + apiKeyToUse: adHocRunSO.attributes.apiKeyToUse, + } as AdHocRun; +}; diff --git a/x-pack/plugins/alerting/server/application/backfill/transforms/transform_backfill_param_to_ad_hoc_run.test.ts b/x-pack/plugins/alerting/server/application/backfill/transforms/transform_backfill_param_to_ad_hoc_run.test.ts index 0dd1995e05b98..822ffed9dfce6 100644 --- a/x-pack/plugins/alerting/server/application/backfill/transforms/transform_backfill_param_to_ad_hoc_run.test.ts +++ b/x-pack/plugins/alerting/server/application/backfill/transforms/transform_backfill_param_to_ad_hoc_run.test.ts @@ -14,6 +14,7 @@ function getMockData(overwrites: Record = {}): ScheduleBackfill return { ruleId: '1', start: '2023-11-16T08:00:00.000Z', + runActions: true, ...overwrites, }; } @@ -63,7 +64,7 @@ describe('transformBackfillParamToAdHocRun', () => { }); test('should transform backfill param with start', () => { - expect(transformBackfillParamToAdHocRun(getMockData(), getMockRule(), 'default')).toEqual({ + expect(transformBackfillParamToAdHocRun(getMockData(), getMockRule(), [], 'default')).toEqual({ apiKeyId: '123', apiKeyToUse: 'MTIzOmFiYw==', createdAt: '2024-01-30T00:00:00.000Z', @@ -75,6 +76,7 @@ describe('transformBackfillParamToAdHocRun', () => { name: 'my rule name', tags: ['foo'], alertTypeId: 'myType', + actions: [], params: {}, apiKeyOwner: 'user', apiKeyCreatedByUser: false, @@ -107,6 +109,7 @@ describe('transformBackfillParamToAdHocRun', () => { transformBackfillParamToAdHocRun( getMockData({ end: '2023-11-17T08:00:00.000Z' }), getMockRule(), + [], 'default' ) ).toEqual({ @@ -120,6 +123,7 @@ describe('transformBackfillParamToAdHocRun', () => { name: 'my rule name', tags: ['foo'], alertTypeId: 'myType', + actions: [], params: {}, apiKeyOwner: 'user', apiKeyCreatedByUser: false, @@ -151,4 +155,101 @@ describe('transformBackfillParamToAdHocRun', () => { ], }); }); + + test('should transform backfill param with rule actions', () => { + const actions = [ + { uuid: '123abc', group: 'default', actionRef: 'action_0', actionTypeId: 'test', params: {} }, + ]; + expect( + transformBackfillParamToAdHocRun(getMockData(), getMockRule(), actions, 'default') + ).toEqual({ + apiKeyId: '123', + apiKeyToUse: 'MTIzOmFiYw==', + createdAt: '2024-01-30T00:00:00.000Z', + duration: '12h', + enabled: true, + // injects end parameter + end: '2023-11-16T20:00:00.000Z', + rule: { + name: 'my rule name', + tags: ['foo'], + alertTypeId: 'myType', + actions, + params: {}, + apiKeyOwner: 'user', + apiKeyCreatedByUser: false, + consumer: 'myApp', + enabled: true, + schedule: { + interval: '12h', + }, + createdBy: 'user', + updatedBy: 'user', + createdAt: '2019-02-12T21:01:22.479Z', + updatedAt: '2019-02-12T21:01:22.479Z', + revision: 0, + }, + spaceId: 'default', + start: '2023-11-16T08:00:00.000Z', + status: adHocRunStatus.PENDING, + schedule: [ + { + runAt: '2023-11-16T20:00:00.000Z', + interval: '12h', + status: adHocRunStatus.PENDING, + }, + ], + }); + }); + + test('should omit rule actions when runActions=false', () => { + const actions = [ + { uuid: '123abc', group: 'default', actionRef: 'action_0', actionTypeId: 'test', params: {} }, + ]; + expect( + transformBackfillParamToAdHocRun( + getMockData({ runActions: false }), + getMockRule(), + actions, + 'default' + ) + ).toEqual({ + apiKeyId: '123', + apiKeyToUse: 'MTIzOmFiYw==', + createdAt: '2024-01-30T00:00:00.000Z', + duration: '12h', + enabled: true, + // injects end parameter + end: '2023-11-16T20:00:00.000Z', + rule: { + name: 'my rule name', + tags: ['foo'], + alertTypeId: 'myType', + actions: [], + params: {}, + apiKeyOwner: 'user', + apiKeyCreatedByUser: false, + consumer: 'myApp', + enabled: true, + schedule: { + interval: '12h', + }, + createdBy: 'user', + updatedBy: 'user', + createdAt: '2019-02-12T21:01:22.479Z', + updatedAt: '2019-02-12T21:01:22.479Z', + revision: 0, + }, + spaceId: 'default', + start: '2023-11-16T08:00:00.000Z', + status: adHocRunStatus.PENDING, + schedule: [ + { + runAt: '2023-11-16T20:00:00.000Z', + interval: '12h', + status: adHocRunStatus.PENDING, + }, + ], + }); + }); }); diff --git a/x-pack/plugins/alerting/server/application/backfill/transforms/transform_backfill_param_to_ad_hoc_run.ts b/x-pack/plugins/alerting/server/application/backfill/transforms/transform_backfill_param_to_ad_hoc_run.ts index 4dc01a6c8939e..4f455a515d998 100644 --- a/x-pack/plugins/alerting/server/application/backfill/transforms/transform_backfill_param_to_ad_hoc_run.ts +++ b/x-pack/plugins/alerting/server/application/backfill/transforms/transform_backfill_param_to_ad_hoc_run.ts @@ -6,6 +6,7 @@ */ import { isString } from 'lodash'; +import { DenormalizedAction } from '../../../rules_client'; import { AdHocRunSO } from '../../../data/ad_hoc_run/types'; import { calculateSchedule } from '../../../backfill_client/lib'; import { adHocRunStatus } from '../../../../common/constants'; @@ -15,6 +16,7 @@ import { ScheduleBackfillParam } from '../methods/schedule/types'; export const transformBackfillParamToAdHocRun = ( param: ScheduleBackfillParam, rule: RuleDomain, + actions: DenormalizedAction[], spaceId: string ): AdHocRunSO => { const schedule = calculateSchedule(param.start, rule.schedule.interval, param.end); @@ -32,6 +34,7 @@ export const transformBackfillParamToAdHocRun = ( params: rule.params, apiKeyOwner: rule.apiKeyOwner, apiKeyCreatedByUser: rule.apiKeyCreatedByUser, + actions: param.runActions ? actions : [], consumer: rule.consumer, enabled: rule.enabled, schedule: rule.schedule, diff --git a/x-pack/plugins/alerting/server/backfill_client/backfill_client.test.ts b/x-pack/plugins/alerting/server/backfill_client/backfill_client.test.ts index 771f5a4db34b9..cb38b0004a887 100644 --- a/x-pack/plugins/alerting/server/backfill_client/backfill_client.test.ts +++ b/x-pack/plugins/alerting/server/backfill_client/backfill_client.test.ts @@ -22,6 +22,8 @@ import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; import { TaskRunnerFactory } from '../task_runner'; import { TaskPriority } from '@kbn/task-manager-plugin/server'; import { UntypedNormalizedRuleType } from '../rule_type_registry'; +import { actionsClientMock } from '@kbn/actions-plugin/server/mocks'; +import { RawRule, RawRuleAction } from '../types'; const logger = loggingSystemMock.create().get(); const taskManagerSetup = taskManagerMock.createSetup(); @@ -29,11 +31,13 @@ const taskManagerStart = taskManagerMock.createStart(); const ruleTypeRegistry = ruleTypeRegistryMock.create(); const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const auditLogger = auditLoggerMock.create(); +const actionsClient = actionsClientMock.create(); function getMockData(overwrites: Record = {}): ScheduleBackfillParam { return { ruleId: '1', start: '2023-11-16T08:00:00.000Z', + runActions: true, ...overwrites, }; } @@ -98,11 +102,14 @@ function getMockAdHocRunAttributes({ ruleId, overwrites, omitApiKey = false, + actions = [], }: { ruleId?: string; overwrites?: Record; omitApiKey?: boolean; + actions?: RawRule['actions']; } = {}): AdHocRunSO { + // @ts-expect-error return { ...(omitApiKey ? {} : { apiKeyId: '123', apiKeyToUse: 'MTIzOmFiYw==' }), createdAt: '2024-01-30T00:00:00.000Z', @@ -113,7 +120,7 @@ function getMockAdHocRunAttributes({ name: 'my rule name', tags: ['foo'], alertTypeId: 'myType', - // @ts-expect-error + actions, params: {}, apiKeyOwner: 'user', apiKeyCreatedByUser: false, @@ -189,6 +196,7 @@ const mockCreatePointInTimeFinderAsInternalUser = ( describe('BackfillClient', () => { let backfillClient: BackfillClient; + let isSystemAction: jest.Mock; beforeAll(() => { jest.useFakeTimers().setSystemTime(new Date('2024-01-30T00:00:00.000Z')); @@ -196,6 +204,9 @@ describe('BackfillClient', () => { beforeEach(() => { jest.resetAllMocks(); + isSystemAction = jest.fn().mockReturnValue(false); + actionsClient.isSystemAction.mockImplementation(isSystemAction); + ruleTypeRegistry.get.mockReturnValue(mockRuleType); backfillClient = new BackfillClient({ logger, @@ -271,6 +282,7 @@ describe('BackfillClient', () => { unsecuredSavedObjectsClient.bulkCreate.mockResolvedValueOnce(bulkCreateResult); const result = await backfillClient.bulkQueue({ + actionsClient, auditLogger, params: mockData, rules: mockRules, @@ -332,11 +344,750 @@ describe('BackfillClient', () => { ]); expect(result).toEqual( bulkCreateResult.saved_objects.map((so, index) => - transformAdHocRunToBackfillResult(so, bulkCreateParams?.[index]) + transformAdHocRunToBackfillResult({ + adHocRunSO: so, + isSystemAction, + originalSO: bulkCreateParams?.[index], + }) + ) + ); + }); + + test('should successfully schedule backfill for rule with actions when runActions=true', async () => { + actionsClient.getBulk.mockResolvedValue([ + { + id: '987', + actionTypeId: 'test', + config: { + from: 'me@me.com', + hasAuth: false, + host: 'hello', + port: 22, + secure: null, + service: null, + }, + isMissingSecrets: false, + name: 'email connector', + isPreconfigured: false, + isSystemAction: false, + isDeprecated: false, + }, + ]); + const mockData = [ + getMockData(), + getMockData({ ruleId: '2', end: '2023-11-17T08:00:00.000Z' }), + ]; + const rule1 = getMockRule({ + actions: [ + { + group: 'default', + id: '987', + actionTypeId: 'test', + params: {}, + uuid: '123abc', + frequency: { notifyWhen: 'onActiveAlert', summary: false, throttle: null }, + }, + { + group: 'default', + id: '987', + actionTypeId: 'test', + params: {}, + uuid: 'xyz987', + frequency: { notifyWhen: 'onActiveAlert', summary: true, throttle: null }, + }, + ], + }); + const rule2 = getMockRule({ + id: '2', + actions: [ + { + group: 'default', + id: '987', + actionTypeId: 'test', + params: {}, + frequency: { notifyWhen: 'onActiveAlert', summary: false, throttle: null }, + }, + ], + }); + const mockRules = [rule1, rule2]; + ruleTypeRegistry.get.mockReturnValue({ ...mockRuleType, ruleTaskTimeout: '1d' }); + + const mockAttributes1 = getMockAdHocRunAttributes({ + overwrites: { + start: '2023-11-16T08:00:00.000Z', + end: '2023-11-16T20:00:00.000Z', + schedule: [ + { + runAt: '2023-11-16T20:00:00.000Z', + interval: '12h', + status: adHocRunStatus.PENDING, + }, + ], + }, + actions: [], + }); + const mockAttributes2 = getMockAdHocRunAttributes({ + overwrites: { + start: '2023-11-16T08:00:00.000Z', + end: '2023-11-17T08:00:00.000Z', + schedule: [ + { + runAt: '2023-11-16T20:00:00.000Z', + interval: '12h', + status: adHocRunStatus.PENDING, + }, + { + runAt: '2023-11-17T08:00:00.000Z', + interval: '12h', + status: adHocRunStatus.PENDING, + }, + ], + }, + }); + const bulkCreateResult = { + saved_objects: [ + getBulkCreateParam('abc', '1', mockAttributes1), + getBulkCreateParam('def', '2', mockAttributes2), + ], + }; + + unsecuredSavedObjectsClient.bulkCreate.mockResolvedValueOnce(bulkCreateResult); + + const result = await backfillClient.bulkQueue({ + actionsClient, + auditLogger, + params: mockData, + rules: mockRules, + ruleTypeRegistry, + spaceId: 'default', + unsecuredSavedObjectsClient, + }); + + const bulkCreateParams = [ + { + type: AD_HOC_RUN_SAVED_OBJECT_TYPE, + attributes: { + ...mockAttributes1, + rule: { + ...mockAttributes1.rule, + actions: [ + { + actionRef: 'action_0', + group: 'default', + actionTypeId: 'test', + params: {}, + uuid: '123abc', + frequency: { notifyWhen: 'onActiveAlert', summary: false, throttle: null }, + } as RawRuleAction, + { + actionRef: 'action_1', + group: 'default', + actionTypeId: 'test', + params: {}, + uuid: 'xyz987', + frequency: { notifyWhen: 'onActiveAlert', summary: true, throttle: null }, + } as RawRuleAction, + ], + }, + }, + references: [ + { id: rule1.id, name: 'rule', type: RULE_SAVED_OBJECT_TYPE }, + { id: '987', name: 'action_0', type: 'action' }, + { id: '987', name: 'action_1', type: 'action' }, + ], + }, + { + type: AD_HOC_RUN_SAVED_OBJECT_TYPE, + attributes: { + ...mockAttributes2, + rule: { + ...mockAttributes1.rule, + actions: [ + { + actionRef: 'action_0', + group: 'default', + actionTypeId: 'test', + params: {}, + frequency: { notifyWhen: 'onActiveAlert', summary: false, throttle: null }, + } as RawRuleAction, + ], + }, + }, + references: [ + { id: rule2.id, name: 'rule', type: RULE_SAVED_OBJECT_TYPE }, + { id: '987', name: 'action_0', type: 'action' }, + ], + }, + ]; + + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledWith(bulkCreateParams); + expect(auditLogger.log).toHaveBeenCalledTimes(2); + expect(auditLogger.log).toHaveBeenNthCalledWith(1, { + event: { + action: 'ad_hoc_run_create', + category: ['database'], + outcome: 'success', + type: ['creation'], + }, + kibana: { saved_object: { id: 'abc', type: 'ad_hoc_run_params' } }, + message: 'User has created ad hoc run for ad_hoc_run_params [id=abc]', + }); + expect(auditLogger.log).toHaveBeenNthCalledWith(2, { + event: { + action: 'ad_hoc_run_create', + category: ['database'], + outcome: 'success', + type: ['creation'], + }, + kibana: { saved_object: { id: 'def', type: 'ad_hoc_run_params' } }, + message: 'User has created ad hoc run for ad_hoc_run_params [id=def]', + }); + expect(taskManagerStart.bulkSchedule).toHaveBeenCalledWith([ + { + id: 'abc', + taskType: 'ad_hoc_run-backfill', + state: {}, + timeoutOverride: '1d', + params: { adHocRunParamsId: 'abc', spaceId: 'default' }, + }, + { + id: 'def', + taskType: 'ad_hoc_run-backfill', + state: {}, + timeoutOverride: '1d', + params: { adHocRunParamsId: 'def', spaceId: 'default' }, + }, + ]); + expect(result).toEqual( + bulkCreateResult.saved_objects.map((so, index) => + transformAdHocRunToBackfillResult({ + adHocRunSO: so, + isSystemAction, + originalSO: bulkCreateParams?.[index], + }) ) ); }); + test('should ignore actions for rule with actions when runActions=true', async () => { + const mockData = [ + getMockData({ runActions: false }), + getMockData({ ruleId: '2', end: '2023-11-17T08:00:00.000Z', runActions: false }), + ]; + const rule1 = getMockRule({ + actions: [ + { + group: 'default', + id: '987', + actionTypeId: 'test', + params: {}, + uuid: '123abc', + frequency: { notifyWhen: 'onActiveAlert', summary: false, throttle: null }, + }, + { + group: 'default', + id: '987', + actionTypeId: 'test', + params: {}, + uuid: 'xyz987', + frequency: { notifyWhen: 'onActiveAlert', summary: true, throttle: null }, + }, + ], + }); + const rule2 = getMockRule({ + id: '2', + actions: [ + { + group: 'default', + id: '987', + actionTypeId: 'test', + params: {}, + frequency: { notifyWhen: 'onActiveAlert', summary: false, throttle: null }, + }, + ], + }); + const mockRules = [rule1, rule2]; + ruleTypeRegistry.get.mockReturnValue({ ...mockRuleType, ruleTaskTimeout: '1d' }); + + const mockAttributes1 = getMockAdHocRunAttributes({ + overwrites: { + start: '2023-11-16T08:00:00.000Z', + end: '2023-11-16T20:00:00.000Z', + schedule: [ + { + runAt: '2023-11-16T20:00:00.000Z', + interval: '12h', + status: adHocRunStatus.PENDING, + }, + ], + }, + actions: [], + }); + const mockAttributes2 = getMockAdHocRunAttributes({ + overwrites: { + start: '2023-11-16T08:00:00.000Z', + end: '2023-11-17T08:00:00.000Z', + schedule: [ + { + runAt: '2023-11-16T20:00:00.000Z', + interval: '12h', + status: adHocRunStatus.PENDING, + }, + { + runAt: '2023-11-17T08:00:00.000Z', + interval: '12h', + status: adHocRunStatus.PENDING, + }, + ], + }, + }); + const bulkCreateResult = { + saved_objects: [ + getBulkCreateParam('abc', '1', mockAttributes1), + getBulkCreateParam('def', '2', mockAttributes2), + ], + }; + + unsecuredSavedObjectsClient.bulkCreate.mockResolvedValueOnce(bulkCreateResult); + + const result = await backfillClient.bulkQueue({ + actionsClient, + auditLogger, + params: mockData, + rules: mockRules, + ruleTypeRegistry, + spaceId: 'default', + unsecuredSavedObjectsClient, + }); + + const bulkCreateParams = [ + { + type: AD_HOC_RUN_SAVED_OBJECT_TYPE, + attributes: mockAttributes1, + references: [{ id: rule1.id, name: 'rule', type: RULE_SAVED_OBJECT_TYPE }], + }, + { + type: AD_HOC_RUN_SAVED_OBJECT_TYPE, + attributes: mockAttributes2, + references: [{ id: rule2.id, name: 'rule', type: RULE_SAVED_OBJECT_TYPE }], + }, + ]; + + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledWith(bulkCreateParams); + expect(auditLogger.log).toHaveBeenCalledTimes(2); + expect(auditLogger.log).toHaveBeenNthCalledWith(1, { + event: { + action: 'ad_hoc_run_create', + category: ['database'], + outcome: 'success', + type: ['creation'], + }, + kibana: { saved_object: { id: 'abc', type: 'ad_hoc_run_params' } }, + message: 'User has created ad hoc run for ad_hoc_run_params [id=abc]', + }); + expect(auditLogger.log).toHaveBeenNthCalledWith(2, { + event: { + action: 'ad_hoc_run_create', + category: ['database'], + outcome: 'success', + type: ['creation'], + }, + kibana: { saved_object: { id: 'def', type: 'ad_hoc_run_params' } }, + message: 'User has created ad hoc run for ad_hoc_run_params [id=def]', + }); + expect(taskManagerStart.bulkSchedule).toHaveBeenCalledWith([ + { + id: 'abc', + taskType: 'ad_hoc_run-backfill', + state: {}, + timeoutOverride: '1d', + params: { adHocRunParamsId: 'abc', spaceId: 'default' }, + }, + { + id: 'def', + taskType: 'ad_hoc_run-backfill', + state: {}, + timeoutOverride: '1d', + params: { adHocRunParamsId: 'def', spaceId: 'default' }, + }, + ]); + expect(result).toEqual( + bulkCreateResult.saved_objects.map((so, index) => + transformAdHocRunToBackfillResult({ + adHocRunSO: so, + isSystemAction, + originalSO: bulkCreateParams?.[index], + }) + ) + ); + }); + + test('should successfully schedule backfill for rule with system actions when runActions=true', async () => { + actionsClient.isSystemAction.mockReturnValueOnce(false); + actionsClient.isSystemAction.mockReturnValueOnce(true); + actionsClient.getBulk.mockResolvedValue([ + { + id: '987', + actionTypeId: 'test', + config: { + from: 'me@me.com', + hasAuth: false, + host: 'hello', + port: 22, + secure: null, + service: null, + }, + isMissingSecrets: false, + name: 'email connector', + isPreconfigured: false, + isSystemAction: false, + isDeprecated: false, + }, + { + id: 'system_456', + actionTypeId: 'test.system', + name: 'System action: .cases', + config: {}, + isDeprecated: false, + isMissingSecrets: false, + isPreconfigured: false, + isSystemAction: true, + }, + ]); + const mockData = [getMockData()]; + const rule = getMockRule({ + actions: [ + { + group: 'default', + id: '987', + actionTypeId: 'test', + params: {}, + uuid: '123abc', + frequency: { notifyWhen: 'onActiveAlert', summary: false, throttle: null }, + }, + ], + systemActions: [ + { + id: 'system_456', + actionTypeId: 'test.system', + params: {}, + uuid: 'aaaaaa', + }, + ], + }); + const mockRules = [rule]; + ruleTypeRegistry.get.mockReturnValue({ ...mockRuleType, ruleTaskTimeout: '1d' }); + + const mockAttributes = getMockAdHocRunAttributes({ + overwrites: { + start: '2023-11-16T08:00:00.000Z', + end: '2023-11-16T20:00:00.000Z', + schedule: [ + { + runAt: '2023-11-16T20:00:00.000Z', + interval: '12h', + status: adHocRunStatus.PENDING, + }, + ], + }, + actions: [], + }); + + const bulkCreateResult = { + saved_objects: [getBulkCreateParam('abc', '1', mockAttributes)], + }; + + unsecuredSavedObjectsClient.bulkCreate.mockResolvedValueOnce(bulkCreateResult); + + const result = await backfillClient.bulkQueue({ + actionsClient, + auditLogger, + params: mockData, + rules: mockRules, + ruleTypeRegistry, + spaceId: 'default', + unsecuredSavedObjectsClient, + }); + + const bulkCreateParams = [ + { + type: AD_HOC_RUN_SAVED_OBJECT_TYPE, + attributes: { + ...mockAttributes, + rule: { + ...mockAttributes.rule, + actions: [ + { + actionRef: 'action_0', + group: 'default', + actionTypeId: 'test', + params: {}, + uuid: '123abc', + frequency: { notifyWhen: 'onActiveAlert', summary: false, throttle: null }, + } as RawRuleAction, + { + actionRef: 'system_action:system_456', + actionTypeId: 'test.system', + params: {}, + uuid: 'aaaaaa', + } as RawRuleAction, + ], + }, + }, + references: [ + { id: rule.id, name: 'rule', type: RULE_SAVED_OBJECT_TYPE }, + { id: '987', name: 'action_0', type: 'action' }, + ], + }, + ]; + + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledWith(bulkCreateParams); + expect(auditLogger.log).toHaveBeenCalledTimes(1); + expect(auditLogger.log).toHaveBeenNthCalledWith(1, { + event: { + action: 'ad_hoc_run_create', + category: ['database'], + outcome: 'success', + type: ['creation'], + }, + kibana: { saved_object: { id: 'abc', type: 'ad_hoc_run_params' } }, + message: 'User has created ad hoc run for ad_hoc_run_params [id=abc]', + }); + expect(taskManagerStart.bulkSchedule).toHaveBeenCalledWith([ + { + id: 'abc', + taskType: 'ad_hoc_run-backfill', + state: {}, + timeoutOverride: '1d', + params: { adHocRunParamsId: 'abc', spaceId: 'default' }, + }, + ]); + expect(result).toEqual( + bulkCreateResult.saved_objects.map((so, index) => + transformAdHocRunToBackfillResult({ + adHocRunSO: so, + isSystemAction, + originalSO: bulkCreateParams?.[index], + }) + ) + ); + }); + + test('should schedule backfill for rule with unsupported actions and return warning', async () => { + actionsClient.getBulk.mockResolvedValue([ + { + id: '987', + actionTypeId: 'test', + config: { + from: 'me@me.com', + hasAuth: false, + host: 'hello', + port: 22, + secure: null, + service: null, + }, + isMissingSecrets: false, + name: 'email connector', + isPreconfigured: false, + isSystemAction: false, + isDeprecated: false, + }, + ]); + const mockData = [ + getMockData(), + getMockData({ ruleId: '2', end: '2023-11-17T08:00:00.000Z' }), + ]; + const rule1 = getMockRule({ + actions: [ + { + group: 'default', + id: '987', + actionTypeId: 'test', + params: {}, + uuid: '123abc', + frequency: { notifyWhen: 'onActiveAlert', summary: false, throttle: null }, + }, + { + group: 'default', + id: '987', + actionTypeId: 'test', + params: {}, + uuid: 'xyz987', + frequency: { notifyWhen: 'onThrottleInterval', summary: true, throttle: '1h' }, + }, + ], + }); + const rule2 = getMockRule({ + id: '2', + actions: [ + { + group: 'default', + id: '987', + actionTypeId: 'test', + params: {}, + frequency: { notifyWhen: 'onActiveAlert', summary: false, throttle: null }, + }, + ], + }); + const mockRules = [rule1, rule2]; + ruleTypeRegistry.get.mockReturnValue({ ...mockRuleType, ruleTaskTimeout: '1d' }); + + const mockAttributes1 = getMockAdHocRunAttributes({ + overwrites: { + start: '2023-11-16T08:00:00.000Z', + end: '2023-11-16T20:00:00.000Z', + schedule: [ + { + runAt: '2023-11-16T20:00:00.000Z', + interval: '12h', + status: adHocRunStatus.PENDING, + }, + ], + }, + actions: [], + }); + const mockAttributes2 = getMockAdHocRunAttributes({ + overwrites: { + start: '2023-11-16T08:00:00.000Z', + end: '2023-11-17T08:00:00.000Z', + schedule: [ + { + runAt: '2023-11-16T20:00:00.000Z', + interval: '12h', + status: adHocRunStatus.PENDING, + }, + { + runAt: '2023-11-17T08:00:00.000Z', + interval: '12h', + status: adHocRunStatus.PENDING, + }, + ], + }, + }); + const bulkCreateResult = { + saved_objects: [ + getBulkCreateParam('abc', '1', mockAttributes1), + getBulkCreateParam('def', '2', mockAttributes2), + ], + }; + + unsecuredSavedObjectsClient.bulkCreate.mockResolvedValueOnce(bulkCreateResult); + + const result = await backfillClient.bulkQueue({ + actionsClient, + auditLogger, + params: mockData, + rules: mockRules, + ruleTypeRegistry, + spaceId: 'default', + unsecuredSavedObjectsClient, + }); + + const bulkCreateParams = [ + { + type: AD_HOC_RUN_SAVED_OBJECT_TYPE, + attributes: { + ...mockAttributes1, + rule: { + ...mockAttributes1.rule, + actions: [ + { + actionRef: 'action_0', + group: 'default', + actionTypeId: 'test', + params: {}, + uuid: '123abc', + frequency: { notifyWhen: 'onActiveAlert', summary: false, throttle: null }, + } as RawRuleAction, + ], + }, + }, + references: [ + { id: rule1.id, name: 'rule', type: RULE_SAVED_OBJECT_TYPE }, + { id: '987', name: 'action_0', type: 'action' }, + ], + }, + { + type: AD_HOC_RUN_SAVED_OBJECT_TYPE, + attributes: { + ...mockAttributes2, + rule: { + ...mockAttributes1.rule, + actions: [ + { + actionRef: 'action_0', + group: 'default', + actionTypeId: 'test', + params: {}, + frequency: { notifyWhen: 'onActiveAlert', summary: false, throttle: null }, + } as RawRuleAction, + ], + }, + }, + references: [ + { id: rule2.id, name: 'rule', type: RULE_SAVED_OBJECT_TYPE }, + { id: '987', name: 'action_0', type: 'action' }, + ], + }, + ]; + + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledWith(bulkCreateParams); + expect(auditLogger.log).toHaveBeenCalledTimes(2); + expect(auditLogger.log).toHaveBeenNthCalledWith(1, { + event: { + action: 'ad_hoc_run_create', + category: ['database'], + outcome: 'success', + type: ['creation'], + }, + kibana: { saved_object: { id: 'abc', type: 'ad_hoc_run_params' } }, + message: 'User has created ad hoc run for ad_hoc_run_params [id=abc]', + }); + expect(auditLogger.log).toHaveBeenNthCalledWith(2, { + event: { + action: 'ad_hoc_run_create', + category: ['database'], + outcome: 'success', + type: ['creation'], + }, + kibana: { saved_object: { id: 'def', type: 'ad_hoc_run_params' } }, + message: 'User has created ad hoc run for ad_hoc_run_params [id=def]', + }); + expect(taskManagerStart.bulkSchedule).toHaveBeenCalledWith([ + { + id: 'abc', + taskType: 'ad_hoc_run-backfill', + state: {}, + timeoutOverride: '1d', + params: { adHocRunParamsId: 'abc', spaceId: 'default' }, + }, + { + id: 'def', + taskType: 'ad_hoc_run-backfill', + state: {}, + timeoutOverride: '1d', + params: { adHocRunParamsId: 'def', spaceId: 'default' }, + }, + ]); + expect(result).toEqual([ + { + ...transformAdHocRunToBackfillResult({ + adHocRunSO: bulkCreateResult.saved_objects[0], + isSystemAction, + originalSO: bulkCreateParams?.[0], + }), + warnings: [ + 'Rule has actions that are not supported for backfill. Those actions will be skipped.', + ], + }, + transformAdHocRunToBackfillResult({ + adHocRunSO: bulkCreateResult.saved_objects[1], + isSystemAction, + originalSO: bulkCreateParams?.[1], + }), + ]); + }); + test('should successfully create multiple backfill saved objects for a single rule', async () => { const mockData = [getMockData(), getMockData({ end: '2023-11-17T08:00:00.000Z' })]; const rule1 = getMockRule(); @@ -383,6 +1134,7 @@ describe('BackfillClient', () => { unsecuredSavedObjectsClient.bulkCreate.mockResolvedValueOnce(bulkCreateResult); const result = await backfillClient.bulkQueue({ + actionsClient, auditLogger, params: mockData, rules: mockRules, @@ -442,7 +1194,11 @@ describe('BackfillClient', () => { ]); expect(result).toEqual( bulkCreateResult.saved_objects.map((so, index) => - transformAdHocRunToBackfillResult(so, bulkCreateParams?.[index]) + transformAdHocRunToBackfillResult({ + adHocRunSO: so, + isSystemAction, + originalSO: bulkCreateParams?.[index], + }) ) ); }); @@ -476,6 +1232,7 @@ describe('BackfillClient', () => { unsecuredSavedObjectsClient.bulkCreate.mockResolvedValueOnce(bulkCreateResult); const result = await backfillClient.bulkQueue({ + actionsClient, auditLogger, params: mockData, rules: mockRules, @@ -504,7 +1261,7 @@ describe('BackfillClient', () => { message: 'User has created ad hoc run for ad_hoc_run_params [id=abc]', }); expect(logger.warn).toHaveBeenCalledWith( - `No rule found for ruleId 2 - not scheduling backfill for {\"ruleId\":\"2\",\"start\":\"2023-11-16T08:00:00.000Z\",\"end\":\"2023-11-17T08:00:00.000Z\"}` + `Error for ruleId 2 - not scheduling backfill for {\"ruleId\":\"2\",\"start\":\"2023-11-16T08:00:00.000Z\",\"runActions\":true,\"end\":\"2023-11-17T08:00:00.000Z\"}` ); expect(taskManagerStart.bulkSchedule).toHaveBeenCalledWith([ { @@ -516,7 +1273,11 @@ describe('BackfillClient', () => { ]); expect(result).toEqual([ ...bulkCreateResult.saved_objects.map((so, index) => - transformAdHocRunToBackfillResult(so, bulkCreateParams?.[0]) + transformAdHocRunToBackfillResult({ + adHocRunSO: so, + isSystemAction, + originalSO: bulkCreateParams?.[index], + }) ), { error: { @@ -591,6 +1352,7 @@ describe('BackfillClient', () => { bulkCreateResult as SavedObjectsBulkResponse ); const result = await backfillClient.bulkQueue({ + actionsClient, auditLogger, params: mockData, rules: mockRules, @@ -739,6 +1501,7 @@ describe('BackfillClient', () => { ]; const result = await backfillClient.bulkQueue({ + actionsClient, auditLogger, params: mockData, rules: [], @@ -842,6 +1605,7 @@ describe('BackfillClient', () => { bulkCreateResult as SavedObjectsBulkResponse ); const result = await backfillClient.bulkQueue({ + actionsClient, params: mockData, rules: mockRules, ruleTypeRegistry, diff --git a/x-pack/plugins/alerting/server/backfill_client/backfill_client.ts b/x-pack/plugins/alerting/server/backfill_client/backfill_client.ts index 48b5e49c428c0..f0143850309e2 100644 --- a/x-pack/plugins/alerting/server/backfill_client/backfill_client.ts +++ b/x-pack/plugins/alerting/server/backfill_client/backfill_client.ts @@ -23,9 +23,9 @@ import { TaskPriority, } from '@kbn/task-manager-plugin/server'; import { isNumber } from 'lodash'; +import { ActionsClient } from '@kbn/actions-plugin/server'; import { ScheduleBackfillError, - ScheduleBackfillParam, ScheduleBackfillParams, ScheduleBackfillResult, ScheduleBackfillResults, @@ -42,6 +42,8 @@ import { AD_HOC_RUN_SAVED_OBJECT_TYPE, RULE_SAVED_OBJECT_TYPE } from '../saved_o import { TaskRunnerFactory } from '../task_runner'; import { RuleTypeRegistry } from '../types'; import { createBackfillError } from './lib'; +import { denormalizeActions } from '../rules_client/lib/denormalize_actions'; +import { DenormalizedAction, NormalizedAlertActionWithGeneratedValues } from '../rules_client'; export const BACKFILL_TASK_TYPE = 'ad_hoc_run-backfill'; @@ -53,6 +55,7 @@ interface ConstructorOpts { } interface BulkQueueOpts { + actionsClient: ActionsClient; auditLogger?: AuditLogger; params: ScheduleBackfillParams; rules: RuleDomain[]; @@ -86,6 +89,7 @@ export class BackfillClient { } public async bulkQueue({ + actionsClient, auditLogger, params, rules, @@ -117,10 +121,16 @@ export class BackfillClient { */ const soToCreateIndexOrErrorMap: Map = new Map(); + const rulesWithUnsupportedActions = new Set(); - params.forEach((param: ScheduleBackfillParam, ndx: number) => { + for (let ndx = 0; ndx < params.length; ndx++) { + const param = params[ndx]; // For this schedule request, look up the rule or return error - const { rule, error } = getRuleOrError(param.ruleId, rules, ruleTypeRegistry); + const { rule, error } = getRuleOrError({ + ruleId: param.ruleId, + rules, + ruleTypeRegistry, + }); if (rule) { // keep track of index of this request in the adHocSOsToCreate array soToCreateIndexOrErrorMap.set(ndx, adHocSOsToCreate.length); @@ -129,22 +139,31 @@ export class BackfillClient { name: `rule`, type: RULE_SAVED_OBJECT_TYPE, }; + + const { actions, hasUnsupportedActions, references } = await extractRuleActions({ + actionsClient, + rule, + runActions: param.runActions, + }); + + if (hasUnsupportedActions) { + rulesWithUnsupportedActions.add(ndx); + } + adHocSOsToCreate.push({ type: AD_HOC_RUN_SAVED_OBJECT_TYPE, - attributes: transformBackfillParamToAdHocRun(param, rule, spaceId), - references: [reference], + attributes: transformBackfillParamToAdHocRun(param, rule, actions, spaceId), + references: [reference, ...references], }); } else if (error) { // keep track of the error encountered for this request by index so // we can return it in order soToCreateIndexOrErrorMap.set(ndx, error); this.logger.warn( - `No rule found for ruleId ${param.ruleId} - not scheduling backfill for ${JSON.stringify( - param - )}` + `Error for ruleId ${param.ruleId} - not scheduling backfill for ${JSON.stringify(param)}` ); } - }); + } // Every request encountered an error, so short-circuit the logic here if (!adHocSOsToCreate.length) { @@ -175,7 +194,11 @@ export class BackfillClient { }) ); } - return transformAdHocRunToBackfillResult(so, adHocSOsToCreate?.[index]); + return transformAdHocRunToBackfillResult({ + adHocRunSO: so, + isSystemAction: (id: string) => actionsClient.isSystemAction(id), + originalSO: adHocSOsToCreate?.[index], + }); } ); @@ -202,6 +225,15 @@ export class BackfillClient { if (isNumber(indexOrError)) { // This number is the index of the response from the savedObjects bulkCreate function + const response = transformedResponse[indexOrError]; + if (rulesWithUnsupportedActions.has(indexOrError)) { + return { + ...response, + warnings: [ + `Rule has actions that are not supported for backfill. Those actions will be skipped.`, + ], + }; + } return transformedResponse[indexOrError]; } else { // Return the error we encountered @@ -301,11 +333,16 @@ export class BackfillClient { } } -function getRuleOrError( - ruleId: string, - rules: RuleDomain[], - ruleTypeRegistry: RuleTypeRegistry -): { rule?: RuleDomain; error?: ScheduleBackfillError } { +interface GetRuleOrErrorOpts { + ruleId: string; + rules: RuleDomain[]; + ruleTypeRegistry: RuleTypeRegistry; +} + +function getRuleOrError({ ruleId, rules, ruleTypeRegistry }: GetRuleOrErrorOpts): { + rule?: RuleDomain; + error?: ScheduleBackfillError; +} { const rule = rules.find((r: RuleDomain) => r.id === ruleId); // if rule not found, return not found error @@ -345,3 +382,38 @@ function getRuleOrError( return { rule }; } + +interface ExtractRuleActions { + actionsClient: ActionsClient; + rule: RuleDomain; + runActions?: boolean; +} + +interface ExtractRuleActionsResult { + actions: DenormalizedAction[]; + hasUnsupportedActions: boolean; + references: SavedObjectReference[]; +} + +async function extractRuleActions({ + actionsClient, + rule, + runActions, +}: ExtractRuleActions): Promise { + if (!runActions) { + return { hasUnsupportedActions: false, actions: [], references: [] }; + } + + const hasUnsupportedActions = rule.actions.some( + (action) => action.frequency?.notifyWhen !== 'onActiveAlert' + ); + + const allActions = [ + ...rule.actions.filter((action) => action.frequency?.notifyWhen === 'onActiveAlert'), + ...(rule.systemActions ?? []), + ] as NormalizedAlertActionWithGeneratedValues[]; + + const { references, actions } = await denormalizeActions(actionsClient, allActions); + + return { hasUnsupportedActions, actions, references }; +} diff --git a/x-pack/plugins/alerting/server/data/ad_hoc_run/types/ad_hoc_run.ts b/x-pack/plugins/alerting/server/data/ad_hoc_run/types/ad_hoc_run.ts index be03f45749c5d..ca4bbe91cd41f 100644 --- a/x-pack/plugins/alerting/server/data/ad_hoc_run/types/ad_hoc_run.ts +++ b/x-pack/plugins/alerting/server/data/ad_hoc_run/types/ad_hoc_run.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { RawRule } from '../../../types'; import { RuleDomain } from '../../../application/rule/types'; import { AdHocRunStatus } from '../../../../common/constants'; @@ -23,10 +24,11 @@ export interface AdHocRunSchedule extends Record { // the backfill job was scheduled. if there are updates to the rule configuration // after the backfill is scheduled, they will not be reflected during the backfill run. type AdHocRunSORule = Pick< - RuleDomain, + RawRule, | 'name' | 'tags' | 'alertTypeId' + | 'actions' | 'params' | 'apiKeyOwner' | 'apiKeyCreatedByUser' @@ -43,7 +45,7 @@ type AdHocRunSORule = Pick< // This is the rule information after loaded from persistence with the // rule ID injected from the SO references array -type AdHocRunRule = AdHocRunSORule & Pick; +type AdHocRunRule = Omit & Pick; export interface AdHocRunSO extends Record { apiKeyId: string; diff --git a/x-pack/plugins/alerting/server/invalidate_pending_api_keys/task.test.ts b/x-pack/plugins/alerting/server/invalidate_pending_api_keys/task.test.ts index a7177b2489740..3c70c6fd314d4 100644 --- a/x-pack/plugins/alerting/server/invalidate_pending_api_keys/task.test.ts +++ b/x-pack/plugins/alerting/server/invalidate_pending_api_keys/task.test.ts @@ -17,6 +17,7 @@ import { invalidateApiKeysAndDeletePendingApiKeySavedObject, runInvalidate, } from './task'; +import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from '@kbn/actions-plugin/server/constants/saved_objects'; let fakeTimer: sinon.SinonFakeTimers; const encryptedSavedObjectsClient = encryptedSavedObjectsMock.createClient(); @@ -92,17 +93,24 @@ describe('Invalidate API Keys Task', () => { encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce( mockInvalidatePendingApiKeyObject2 ); + // first call to find aggregates any AD_HOC_RUN_SAVED_OBJECT_TYPE SOs by apiKeyId internalSavedObjectsRepository.find.mockResolvedValueOnce({ saved_objects: [], total: 2, page: 1, per_page: 10, aggregations: { - apiKeyId: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [], - }, + apiKeyId: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + }); + // second call to find aggregates any ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE SOs by apiKeyId + internalSavedObjectsRepository.find.mockResolvedValueOnce({ + saved_objects: [], + total: 1, + page: 1, + per_page: 10, + aggregations: { + apiKeyId: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, }, }); @@ -166,13 +174,114 @@ describe('Invalidate API Keys Task', () => { }); }); - test('should get decrypted api key pending invalidation saved object when some api keys are still in use', async () => { + test('should get decrypted api key pending invalidation saved object when some api keys are still in use by AD_HOC_RUN_SAVED_OBJECT_TYPE', async () => { encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce( mockInvalidatePendingApiKeyObject1 ); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce( mockInvalidatePendingApiKeyObject2 ); + // first call to find aggregates any AD_HOC_RUN_SAVED_OBJECT_TYPE SOs by apiKeyId + internalSavedObjectsRepository.find.mockResolvedValueOnce({ + saved_objects: [], + total: 2, + page: 1, + per_page: 10, + aggregations: { + apiKeyId: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'abcd====!', doc_count: 1 }], + }, + }, + }); + // second call to find aggregates any ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE SOs by apiKeyId + internalSavedObjectsRepository.find.mockResolvedValueOnce({ + saved_objects: [], + total: 1, + page: 1, + per_page: 10, + aggregations: { + apiKeyId: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + }); + + const result = await getApiKeyIdsToInvalidate({ + apiKeySOsPendingInvalidation: { + saved_objects: [ + { + id: '1', + type: API_KEY_PENDING_INVALIDATION_TYPE, + score: 0, + attributes: { apiKeyId: 'encryptedencrypted', createdAt: '2024-04-11T17:08:44.035Z' }, + references: [], + }, + { + id: '2', + type: API_KEY_PENDING_INVALIDATION_TYPE, + score: 0, + attributes: { apiKeyId: 'encryptedencrypted', createdAt: '2024-04-11T17:08:44.035Z' }, + references: [], + }, + ], + total: 2, + per_page: 10, + page: 1, + }, + encryptedSavedObjectsClient, + savedObjectsClient: internalSavedObjectsRepository, + }); + + expect(encryptedSavedObjectsClient.getDecryptedAsInternalUser).toHaveBeenCalledTimes(2); + expect(encryptedSavedObjectsClient.getDecryptedAsInternalUser).toHaveBeenNthCalledWith( + 1, + API_KEY_PENDING_INVALIDATION_TYPE, + '1' + ); + expect(encryptedSavedObjectsClient.getDecryptedAsInternalUser).toHaveBeenNthCalledWith( + 2, + API_KEY_PENDING_INVALIDATION_TYPE, + '2' + ); + expect(internalSavedObjectsRepository.find).toHaveBeenCalledWith({ + type: AD_HOC_RUN_SAVED_OBJECT_TYPE, + perPage: 0, + filter: `ad_hoc_run_params.attributes.apiKeyId: "abcd====!" OR ad_hoc_run_params.attributes.apiKeyId: "xyz!==!"`, + namespaces: ['*'], + aggs: { + apiKeyId: { + terms: { + field: `ad_hoc_run_params.attributes.apiKeyId`, + size: 100, + }, + }, + }, + }); + expect(result).toEqual({ + apiKeyIdsToInvalidate: [{ id: '2', apiKeyId: 'xyz!==!' }], + apiKeyIdsToExclude: [{ id: '1', apiKeyId: 'abcd====!' }], + }); + }); + + test('should get decrypted api key pending invalidation saved object when some api keys are still in use by ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE', async () => { + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce( + mockInvalidatePendingApiKeyObject1 + ); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce( + mockInvalidatePendingApiKeyObject2 + ); + // first call to find aggregates any AD_HOC_RUN_SAVED_OBJECT_TYPE SOs by apiKeyId + internalSavedObjectsRepository.find.mockResolvedValueOnce({ + saved_objects: [], + total: 1, + page: 1, + per_page: 10, + aggregations: { + apiKeyId: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + }); + + // second call to find aggregates any ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE SOs by apiKeyId internalSavedObjectsRepository.find.mockResolvedValueOnce({ saved_objects: [], total: 2, @@ -299,6 +408,15 @@ describe('Invalidate API Keys Task', () => { per_page: 10, // missing aggregations }); + internalSavedObjectsRepository.find.mockResolvedValueOnce({ + saved_objects: [], + total: 1, + page: 1, + per_page: 10, + aggregations: { + apiKeyId: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + }); const result = await getApiKeyIdsToInvalidate({ apiKeySOsPendingInvalidation: { @@ -547,19 +665,28 @@ describe('Invalidate API Keys Task', () => { encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce( mockInvalidatePendingApiKeyObject2 ); + // first call to find aggregates any AD_HOC_RUN_SAVED_OBJECT_TYPE SOs by apiKeyId + internalSavedObjectsRepository.find.mockResolvedValueOnce({ + saved_objects: [], + total: 1, + page: 1, + per_page: 10, + aggregations: { + apiKeyId: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + }); + + // second call to find aggregates any ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE SOs by apiKeyId internalSavedObjectsRepository.find.mockResolvedValueOnce({ saved_objects: [], total: 2, page: 1, - per_page: 0, + per_page: 10, aggregations: { - apiKeyId: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [], - }, + apiKeyId: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, }, }); + securityMockStart.authc.apiKeys.invalidateAsInternalUser.mockResolvedValue({ invalidated_api_keys: ['1', '2'], previously_invalidated_api_keys: [], @@ -576,7 +703,7 @@ describe('Invalidate API Keys Task', () => { }); expect(result).toEqual(2); - expect(internalSavedObjectsRepository.find).toHaveBeenCalledTimes(2); + expect(internalSavedObjectsRepository.find).toHaveBeenCalledTimes(3); expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(1, { type: API_KEY_PENDING_INVALIDATION_TYPE, filter: `api_key_pending_invalidation.attributes.createdAt <= "1969-12-31T23:00:00.000Z"`, @@ -610,6 +737,20 @@ describe('Invalidate API Keys Task', () => { }, }, }); + expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(3, { + type: ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, + perPage: 0, + filter: `action_task_params.attributes.apiKeyId: "abcd====!" OR action_task_params.attributes.apiKeyId: "xyz!==!"`, + namespaces: ['*'], + aggs: { + apiKeyId: { + terms: { + field: `action_task_params.attributes.apiKeyId`, + size: 100, + }, + }, + }, + }); expect(securityMockStart.authc.apiKeys.invalidateAsInternalUser).toHaveBeenCalledTimes(1); expect(securityMockStart.authc.apiKeys.invalidateAsInternalUser).toHaveBeenCalledWith({ ids: ['abcd====!', 'xyz!==!'], @@ -628,7 +769,7 @@ describe('Invalidate API Keys Task', () => { expect(logger.debug).toHaveBeenCalledWith(`Total invalidated API keys "2"`); }); - test('should succeed when there are API keys to invalidate and API keys to exclude', async () => { + test('should succeed when there are API keys to invalidate and API keys to exclude (AD_HOC_RUN_SAVED_OBJECT_TYPE using apiKeyId)', async () => { internalSavedObjectsRepository.find.mockResolvedValueOnce({ saved_objects: [ { @@ -656,6 +797,8 @@ describe('Invalidate API Keys Task', () => { encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce( mockInvalidatePendingApiKeyObject2 ); + + // first call to find aggregates any AD_HOC_RUN_SAVED_OBJECT_TYPE SOs by apiKeyId internalSavedObjectsRepository.find.mockResolvedValueOnce({ saved_objects: [], total: 2, @@ -669,6 +812,18 @@ describe('Invalidate API Keys Task', () => { }, }, }); + + // second call to find aggregates any ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE SOs by apiKeyId + internalSavedObjectsRepository.find.mockResolvedValueOnce({ + saved_objects: [], + total: 2, + page: 1, + per_page: 10, + aggregations: { + apiKeyId: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + }); + securityMockStart.authc.apiKeys.invalidateAsInternalUser.mockResolvedValue({ invalidated_api_keys: ['1'], previously_invalidated_api_keys: [], @@ -685,7 +840,7 @@ describe('Invalidate API Keys Task', () => { }); expect(result).toEqual(1); - expect(internalSavedObjectsRepository.find).toHaveBeenCalledTimes(2); + expect(internalSavedObjectsRepository.find).toHaveBeenCalledTimes(3); expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(1, { type: API_KEY_PENDING_INVALIDATION_TYPE, filter: `api_key_pending_invalidation.attributes.createdAt <= "1969-12-31T23:00:00.000Z"`, @@ -719,6 +874,152 @@ describe('Invalidate API Keys Task', () => { }, }, }); + expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(3, { + type: ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, + perPage: 0, + filter: `action_task_params.attributes.apiKeyId: "abcd====!" OR action_task_params.attributes.apiKeyId: "xyz!==!"`, + namespaces: ['*'], + aggs: { + apiKeyId: { + terms: { + field: `action_task_params.attributes.apiKeyId`, + size: 100, + }, + }, + }, + }); + expect(securityMockStart.authc.apiKeys.invalidateAsInternalUser).toHaveBeenCalledTimes(1); + expect(securityMockStart.authc.apiKeys.invalidateAsInternalUser).toHaveBeenCalledWith({ + ids: ['xyz!==!'], + }); + expect(internalSavedObjectsRepository.delete).toHaveBeenCalledTimes(1); + expect(internalSavedObjectsRepository.delete).toHaveBeenNthCalledWith( + 1, + API_KEY_PENDING_INVALIDATION_TYPE, + '2' + ); + expect(logger.debug).toHaveBeenCalledWith(`Total invalidated API keys "1"`); + }); + + test('should succeed when there are API keys to invalidate and API keys to exclude (ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE using apiKeyId)', async () => { + internalSavedObjectsRepository.find.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: API_KEY_PENDING_INVALIDATION_TYPE, + score: 0, + attributes: { apiKeyId: 'encryptedencrypted', createdAt: '2024-04-11T17:08:44.035Z' }, + references: [], + }, + { + id: '2', + type: API_KEY_PENDING_INVALIDATION_TYPE, + score: 0, + attributes: { apiKeyId: 'encryptedencrypted', createdAt: '2024-04-11T17:08:44.035Z' }, + references: [], + }, + ], + total: 2, + per_page: 100, + page: 1, + }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce( + mockInvalidatePendingApiKeyObject1 + ); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce( + mockInvalidatePendingApiKeyObject2 + ); + + // first call to find aggregates any AD_HOC_RUN_SAVED_OBJECT_TYPE SOs by apiKeyId + internalSavedObjectsRepository.find.mockResolvedValueOnce({ + saved_objects: [], + total: 2, + page: 1, + per_page: 10, + aggregations: { + apiKeyId: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + }); + + // second call to find aggregates any ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE SOs by apiKeyId + internalSavedObjectsRepository.find.mockResolvedValueOnce({ + saved_objects: [], + total: 2, + page: 1, + per_page: 0, + aggregations: { + apiKeyId: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'abcd====!', doc_count: 1 }], + }, + }, + }); + + securityMockStart.authc.apiKeys.invalidateAsInternalUser.mockResolvedValue({ + invalidated_api_keys: ['1'], + previously_invalidated_api_keys: [], + error_count: 0, + }); + + const result = await runInvalidate({ + // @ts-expect-error + config: { invalidateApiKeysTask: { interval: '1m', removalDelay: '1h' } }, + encryptedSavedObjectsClient, + logger, + savedObjectsClient: internalSavedObjectsRepository, + security: securityMockStart, + }); + expect(result).toEqual(1); + + expect(internalSavedObjectsRepository.find).toHaveBeenCalledTimes(3); + expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(1, { + type: API_KEY_PENDING_INVALIDATION_TYPE, + filter: `api_key_pending_invalidation.attributes.createdAt <= "1969-12-31T23:00:00.000Z"`, + page: 1, + sortField: 'createdAt', + sortOrder: 'asc', + perPage: 100, + }); + expect(encryptedSavedObjectsClient.getDecryptedAsInternalUser).toHaveBeenCalledTimes(2); + expect(encryptedSavedObjectsClient.getDecryptedAsInternalUser).toHaveBeenNthCalledWith( + 1, + API_KEY_PENDING_INVALIDATION_TYPE, + '1' + ); + expect(encryptedSavedObjectsClient.getDecryptedAsInternalUser).toHaveBeenNthCalledWith( + 2, + API_KEY_PENDING_INVALIDATION_TYPE, + '2' + ); + expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(2, { + type: AD_HOC_RUN_SAVED_OBJECT_TYPE, + perPage: 0, + filter: `ad_hoc_run_params.attributes.apiKeyId: "abcd====!" OR ad_hoc_run_params.attributes.apiKeyId: "xyz!==!"`, + namespaces: ['*'], + aggs: { + apiKeyId: { + terms: { + field: `ad_hoc_run_params.attributes.apiKeyId`, + size: 100, + }, + }, + }, + }); + expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(3, { + type: ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, + perPage: 0, + filter: `action_task_params.attributes.apiKeyId: "abcd====!" OR action_task_params.attributes.apiKeyId: "xyz!==!"`, + namespaces: ['*'], + aggs: { + apiKeyId: { + terms: { + field: `action_task_params.attributes.apiKeyId`, + size: 100, + }, + }, + }, + }); expect(securityMockStart.authc.apiKeys.invalidateAsInternalUser).toHaveBeenCalledTimes(1); expect(securityMockStart.authc.apiKeys.invalidateAsInternalUser).toHaveBeenCalledWith({ ids: ['xyz!==!'], @@ -760,6 +1061,8 @@ describe('Invalidate API Keys Task', () => { encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce( mockInvalidatePendingApiKeyObject2 ); + + // first call to find aggregates any AD_HOC_RUN_SAVED_OBJECT_TYPE SOs by apiKeyId internalSavedObjectsRepository.find.mockResolvedValueOnce({ saved_objects: [], total: 2, @@ -777,6 +1080,21 @@ describe('Invalidate API Keys Task', () => { }, }); + // second call to find aggregates any ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE SOs by apiKeyId + internalSavedObjectsRepository.find.mockResolvedValueOnce({ + saved_objects: [], + total: 2, + page: 1, + per_page: 0, + aggregations: { + apiKeyId: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'abcd====!', doc_count: 3 }], + }, + }, + }); + const result = await runInvalidate({ // @ts-expect-error config: { invalidateApiKeysTask: { interval: '1m', removalDelay: '1h' } }, @@ -787,8 +1105,8 @@ describe('Invalidate API Keys Task', () => { }); expect(result).toEqual(0); - expect(internalSavedObjectsRepository.find).toHaveBeenCalledTimes(2); - expect(internalSavedObjectsRepository.find).toHaveBeenCalledWith({ + expect(internalSavedObjectsRepository.find).toHaveBeenCalledTimes(3); + expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(1, { type: API_KEY_PENDING_INVALIDATION_TYPE, filter: `api_key_pending_invalidation.attributes.createdAt <= "1969-12-31T23:00:00.000Z"`, page: 1, @@ -807,7 +1125,7 @@ describe('Invalidate API Keys Task', () => { API_KEY_PENDING_INVALIDATION_TYPE, '2' ); - expect(internalSavedObjectsRepository.find).toHaveBeenCalledWith({ + expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(2, { type: AD_HOC_RUN_SAVED_OBJECT_TYPE, perPage: 0, filter: `ad_hoc_run_params.attributes.apiKeyId: "abcd====!" OR ad_hoc_run_params.attributes.apiKeyId: "xyz!==!"`, @@ -821,6 +1139,20 @@ describe('Invalidate API Keys Task', () => { }, }, }); + expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(3, { + type: ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, + perPage: 0, + filter: `action_task_params.attributes.apiKeyId: "abcd====!" OR action_task_params.attributes.apiKeyId: "xyz!==!"`, + namespaces: ['*'], + aggs: { + apiKeyId: { + terms: { + field: `action_task_params.attributes.apiKeyId`, + size: 100, + }, + }, + }, + }); expect(securityMockStart.authc.apiKeys.invalidateAsInternalUser).not.toHaveBeenCalled(); expect(internalSavedObjectsRepository.delete).not.toHaveBeenCalled(); }); @@ -844,19 +1176,29 @@ describe('Invalidate API Keys Task', () => { encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce( mockInvalidatePendingApiKeyObject1 ); + + // first call to find aggregates any AD_HOC_RUN_SAVED_OBJECT_TYPE SOs by apiKeyId internalSavedObjectsRepository.find.mockResolvedValueOnce({ saved_objects: [], total: 2, page: 1, per_page: 0, aggregations: { - apiKeyId: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [], - }, + apiKeyId: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, }, }); + + // second call to find aggregates any ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE SOs by apiKeyId + internalSavedObjectsRepository.find.mockResolvedValueOnce({ + saved_objects: [], + total: 2, + page: 1, + per_page: 0, + aggregations: { + apiKeyId: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + }); + securityMockStart.authc.apiKeys.invalidateAsInternalUser.mockResolvedValue({ invalidated_api_keys: ['1'], previously_invalidated_api_keys: [], @@ -880,17 +1222,25 @@ describe('Invalidate API Keys Task', () => { encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce( mockInvalidatePendingApiKeyObject2 ); + // first call to find aggregates any AD_HOC_RUN_SAVED_OBJECT_TYPE SOs by apiKeyId internalSavedObjectsRepository.find.mockResolvedValueOnce({ saved_objects: [], total: 2, page: 1, per_page: 0, aggregations: { - apiKeyId: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [], - }, + apiKeyId: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + }); + + // second call to find aggregates any ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE SOs by apiKeyId + internalSavedObjectsRepository.find.mockResolvedValueOnce({ + saved_objects: [], + total: 2, + page: 1, + per_page: 0, + aggregations: { + apiKeyId: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, }, }); securityMockStart.authc.apiKeys.invalidateAsInternalUser.mockResolvedValue({ @@ -909,7 +1259,7 @@ describe('Invalidate API Keys Task', () => { }); expect(result).toEqual(2); - expect(internalSavedObjectsRepository.find).toHaveBeenCalledTimes(4); + expect(internalSavedObjectsRepository.find).toHaveBeenCalledTimes(6); expect(encryptedSavedObjectsClient.getDecryptedAsInternalUser).toHaveBeenCalledTimes(2); expect(securityMockStart.authc.apiKeys.invalidateAsInternalUser).toHaveBeenCalledTimes(2); expect(internalSavedObjectsRepository.delete).toHaveBeenCalledTimes(2); @@ -943,6 +1293,20 @@ describe('Invalidate API Keys Task', () => { }, }, }); + expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(3, { + type: ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, + perPage: 0, + filter: `action_task_params.attributes.apiKeyId: "abcd====!"`, + namespaces: ['*'], + aggs: { + apiKeyId: { + terms: { + field: `action_task_params.attributes.apiKeyId`, + size: 100, + }, + }, + }, + }); expect(securityMockStart.authc.apiKeys.invalidateAsInternalUser).toHaveBeenNthCalledWith(1, { ids: ['abcd====!'], }); @@ -954,7 +1318,7 @@ describe('Invalidate API Keys Task', () => { expect(logger.debug).toHaveBeenNthCalledWith(1, `Total invalidated API keys "1"`); // second iteration - expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(3, { + expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(4, { type: API_KEY_PENDING_INVALIDATION_TYPE, filter: `api_key_pending_invalidation.attributes.createdAt <= "1969-12-31T23:00:00.000Z"`, page: 1, @@ -967,7 +1331,7 @@ describe('Invalidate API Keys Task', () => { API_KEY_PENDING_INVALIDATION_TYPE, '2' ); - expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(4, { + expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(5, { type: AD_HOC_RUN_SAVED_OBJECT_TYPE, perPage: 0, filter: `ad_hoc_run_params.attributes.apiKeyId: "xyz!==!"`, @@ -981,6 +1345,20 @@ describe('Invalidate API Keys Task', () => { }, }, }); + expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(6, { + type: ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, + perPage: 0, + filter: `action_task_params.attributes.apiKeyId: "xyz!==!"`, + namespaces: ['*'], + aggs: { + apiKeyId: { + terms: { + field: `action_task_params.attributes.apiKeyId`, + size: 100, + }, + }, + }, + }); expect(securityMockStart.authc.apiKeys.invalidateAsInternalUser).toHaveBeenNthCalledWith(2, { ids: ['xyz!==!'], }); @@ -1021,6 +1399,7 @@ describe('Invalidate API Keys Task', () => { encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce( mockInvalidatePendingApiKeyObject2 ); + // first call to find aggregates any AD_HOC_RUN_SAVED_OBJECT_TYPE SOs by apiKeyId internalSavedObjectsRepository.find.mockResolvedValueOnce({ saved_objects: [], total: 2, @@ -1034,6 +1413,18 @@ describe('Invalidate API Keys Task', () => { }, }, }); + + // second call to find aggregates any ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE SOs by apiKeyId + internalSavedObjectsRepository.find.mockResolvedValueOnce({ + saved_objects: [], + total: 2, + page: 1, + per_page: 0, + aggregations: { + apiKeyId: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + }); + securityMockStart.authc.apiKeys.invalidateAsInternalUser.mockResolvedValue({ invalidated_api_keys: ['1'], previously_invalidated_api_keys: [], @@ -1057,7 +1448,7 @@ describe('Invalidate API Keys Task', () => { }); expect(result).toEqual(1); - expect(internalSavedObjectsRepository.find).toHaveBeenCalledTimes(3); + expect(internalSavedObjectsRepository.find).toHaveBeenCalledTimes(4); expect(encryptedSavedObjectsClient.getDecryptedAsInternalUser).toHaveBeenCalledTimes(2); expect(securityMockStart.authc.apiKeys.invalidateAsInternalUser).toHaveBeenCalledTimes(1); expect(internalSavedObjectsRepository.delete).toHaveBeenCalledTimes(1); @@ -1096,6 +1487,20 @@ describe('Invalidate API Keys Task', () => { }, }, }); + expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(3, { + type: ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, + perPage: 0, + filter: `action_task_params.attributes.apiKeyId: "abcd====!" OR action_task_params.attributes.apiKeyId: "xyz!==!"`, + namespaces: ['*'], + aggs: { + apiKeyId: { + terms: { + field: `action_task_params.attributes.apiKeyId`, + size: 100, + }, + }, + }, + }); expect(securityMockStart.authc.apiKeys.invalidateAsInternalUser).toHaveBeenNthCalledWith(1, { ids: ['xyz!==!'], }); @@ -1107,7 +1512,7 @@ describe('Invalidate API Keys Task', () => { expect(logger.debug).toHaveBeenNthCalledWith(1, `Total invalidated API keys "1"`); // second iteration - expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(3, { + expect(internalSavedObjectsRepository.find).toHaveBeenNthCalledWith(4, { type: API_KEY_PENDING_INVALIDATION_TYPE, filter: `api_key_pending_invalidation.attributes.createdAt <= "1969-12-31T23:00:00.000Z" AND NOT api_key_pending_invalidation.id: "api_key_pending_invalidation:1"`, page: 1, diff --git a/x-pack/plugins/alerting/server/invalidate_pending_api_keys/task.ts b/x-pack/plugins/alerting/server/invalidate_pending_api_keys/task.ts index 48eea48246c78..18364299b99a6 100644 --- a/x-pack/plugins/alerting/server/invalidate_pending_api_keys/task.ts +++ b/x-pack/plugins/alerting/server/invalidate_pending_api_keys/task.ts @@ -22,6 +22,7 @@ import { AggregationsStringTermsBucketKeys, AggregationsTermsAggregateBase, } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from '@kbn/actions-plugin/server/constants/saved_objects'; import { InvalidateAPIKeyResult } from '../rules_client'; import { AlertingConfig } from '../config'; import { timePeriodBeforeDate } from '../lib/get_cadence'; @@ -116,6 +117,7 @@ export function taskRunner( const savedObjectsClient = savedObjects.createInternalRepository([ API_KEY_PENDING_INVALIDATION_TYPE, AD_HOC_RUN_SAVED_OBJECT_TYPE, + ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, ]); const encryptedSavedObjectsClient = encryptedSavedObjects.getClient({ includedHiddenTypes: [API_KEY_PENDING_INVALIDATION_TYPE], @@ -248,29 +250,14 @@ export async function getApiKeyIdsToInvalidate({ ); // Query saved objects index to see if any API keys are in use - const filter = `${apiKeyIds - .map(({ apiKeyId }) => `${AD_HOC_RUN_SAVED_OBJECT_TYPE}.attributes.apiKeyId: "${apiKeyId}"`) - .join(' OR ')}`; - const { aggregations } = await savedObjectsClient.find< - AdHocRunSO, - { apiKeyId: AggregationsTermsAggregateBase } - >({ - type: AD_HOC_RUN_SAVED_OBJECT_TYPE, - filter, - perPage: 0, - namespaces: ['*'], - aggs: { - apiKeyId: { - terms: { - field: `${AD_HOC_RUN_SAVED_OBJECT_TYPE}.attributes.apiKeyId`, - size: PAGE_SIZE, - }, - }, - }, - }); + const apiKeyIdStrings = apiKeyIds.map(({ apiKeyId }) => apiKeyId); + let apiKeyIdsInUseBuckets: AggregationsStringTermsBucketKeys[] = []; - const apiKeyIdsInUseBuckets: AggregationsStringTermsBucketKeys[] = - (aggregations?.apiKeyId?.buckets as AggregationsStringTermsBucketKeys[]) ?? []; + for (const soType of [AD_HOC_RUN_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE]) { + apiKeyIdsInUseBuckets = apiKeyIdsInUseBuckets.concat( + await queryForApiKeysInUse(apiKeyIdStrings, soType, savedObjectsClient) + ); + } const apiKeyIdsToInvalidate: ApiKeyIdAndSOId[] = []; const apiKeyIdsToExclude: ApiKeyIdAndSOId[] = []; @@ -335,3 +322,33 @@ export async function invalidateApiKeysAndDeletePendingApiKeySavedObject({ logger.debug(`Total invalidated API keys "${totalInvalidated}"`); return totalInvalidated; } + +async function queryForApiKeysInUse( + apiKeyIds: string[], + savedObjectType: string, + savedObjectsClient: SavedObjectsClientContract +): Promise { + const filter = `${apiKeyIds + .map((apiKeyId) => `${savedObjectType}.attributes.apiKeyId: "${apiKeyId}"`) + .join(' OR ')}`; + + const { aggregations } = await savedObjectsClient.find< + AdHocRunSO, + { apiKeyId: AggregationsTermsAggregateBase } + >({ + type: savedObjectType, + filter, + perPage: 0, + namespaces: ['*'], + aggs: { + apiKeyId: { + terms: { + field: `${savedObjectType}.attributes.apiKeyId`, + size: PAGE_SIZE, + }, + }, + }, + }); + + return (aggregations?.apiKeyId?.buckets as AggregationsStringTermsBucketKeys[]) ?? []; +} diff --git a/x-pack/plugins/alerting/server/routes/backfill/apis/find/find_backfill_route.test.ts b/x-pack/plugins/alerting/server/routes/backfill/apis/find/find_backfill_route.test.ts index a8a0a210c8817..47d8e7c8f69aa 100644 --- a/x-pack/plugins/alerting/server/routes/backfill/apis/find/find_backfill_route.test.ts +++ b/x-pack/plugins/alerting/server/routes/backfill/apis/find/find_backfill_route.test.ts @@ -46,6 +46,7 @@ describe('findBackfillRoute', () => { tags: ['foo'], alertTypeId: 'myType', params: {}, + actions: [], apiKeyOwner: 'user', apiKeyCreatedByUser: false, consumer: 'myApp', @@ -73,6 +74,7 @@ describe('findBackfillRoute', () => { tags: ['foo'], alertTypeId: 'myType', params: {}, + actions: [], apiKeyOwner: 'user', apiKeyCreatedByUser: false, consumer: 'myApp', diff --git a/x-pack/plugins/alerting/server/routes/backfill/apis/get/get_backfill_route.test.ts b/x-pack/plugins/alerting/server/routes/backfill/apis/get/get_backfill_route.test.ts index 79ac8df1ed4b5..6b7aafbc14e86 100644 --- a/x-pack/plugins/alerting/server/routes/backfill/apis/get/get_backfill_route.test.ts +++ b/x-pack/plugins/alerting/server/routes/backfill/apis/get/get_backfill_route.test.ts @@ -35,6 +35,7 @@ describe('getBackfillRoute', () => { tags: ['foo'], alertTypeId: 'myType', params: {}, + actions: [], apiKeyOwner: 'user', apiKeyCreatedByUser: false, consumer: 'myApp', diff --git a/x-pack/plugins/alerting/server/routes/backfill/apis/schedule/schedule_backfill_route.test.ts b/x-pack/plugins/alerting/server/routes/backfill/apis/schedule/schedule_backfill_route.test.ts index c08cca6dfe683..670f86671ff8b 100644 --- a/x-pack/plugins/alerting/server/routes/backfill/apis/schedule/schedule_backfill_route.test.ts +++ b/x-pack/plugins/alerting/server/routes/backfill/apis/schedule/schedule_backfill_route.test.ts @@ -44,6 +44,7 @@ describe('scheduleBackfillRoute', () => { tags: ['foo'], alertTypeId: 'myType', params: {}, + actions: [], apiKeyOwner: 'user', apiKeyCreatedByUser: false, consumer: 'myApp', @@ -71,6 +72,7 @@ describe('scheduleBackfillRoute', () => { tags: ['foo'], alertTypeId: 'myType', params: {}, + actions: [], apiKeyOwner: 'user', apiKeyCreatedByUser: false, consumer: 'myApp', diff --git a/x-pack/plugins/alerting/server/routes/backfill/apis/schedule/transforms/transform_request/v1.ts b/x-pack/plugins/alerting/server/routes/backfill/apis/schedule/transforms/transform_request/v1.ts index 170d85c4f862b..83e8a6e82c5ca 100644 --- a/x-pack/plugins/alerting/server/routes/backfill/apis/schedule/transforms/transform_request/v1.ts +++ b/x-pack/plugins/alerting/server/routes/backfill/apis/schedule/transforms/transform_request/v1.ts @@ -10,4 +10,9 @@ import { ScheduleBackfillRequestBodyV1 } from '../../../../../../../common/route import { ScheduleBackfillParams } from '../../../../../../application/backfill/methods/schedule/types'; export const transformRequest = (request: ScheduleBackfillRequestBodyV1): ScheduleBackfillParams => - request.map(({ rule_id, start, end }) => ({ ruleId: rule_id, start, end })); + request.map(({ rule_id, start, end, run_actions }) => ({ + ruleId: rule_id, + start, + end, + runActions: run_actions, + })); diff --git a/x-pack/plugins/alerting/server/routes/backfill/transforms/transform_backfill_to_backfill_response/v1.test.ts b/x-pack/plugins/alerting/server/routes/backfill/transforms/transform_backfill_to_backfill_response/v1.test.ts index 88582bea15b92..fe4a0c96e6d4f 100644 --- a/x-pack/plugins/alerting/server/routes/backfill/transforms/transform_backfill_to_backfill_response/v1.test.ts +++ b/x-pack/plugins/alerting/server/routes/backfill/transforms/transform_backfill_to_backfill_response/v1.test.ts @@ -19,6 +19,7 @@ describe('transformBackfillToBackfillResponse', () => { tags: ['foo'], alertTypeId: 'myType', params: {}, + actions: [], apiKeyOwner: 'user', apiKeyCreatedByUser: false, consumer: 'myApp', @@ -49,6 +50,7 @@ describe('transformBackfillToBackfillResponse', () => { name: 'my rule name', tags: ['foo'], rule_type_id: 'myType', + actions: [], params: {}, api_key_owner: 'user', api_key_created_by_user: false, diff --git a/x-pack/plugins/alerting/server/rules_client/lib/denormalize_actions.ts b/x-pack/plugins/alerting/server/rules_client/lib/denormalize_actions.ts index 3cd1113a13628..ed27fe651ea11 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/denormalize_actions.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/denormalize_actions.ts @@ -5,25 +5,21 @@ * 2.0. */ import { SavedObjectReference } from '@kbn/core/server'; +import { ActionsClient } from '@kbn/actions-plugin/server'; import { preconfiguredConnectorActionRefPrefix, systemConnectorActionRefPrefix, } from '../common/constants'; -import { - DenormalizedAction, - NormalizedAlertActionWithGeneratedValues, - RulesClientContext, -} from '../types'; +import { DenormalizedAction, NormalizedAlertActionWithGeneratedValues } from '../types'; export async function denormalizeActions( - context: RulesClientContext, + actionsClient: ActionsClient, alertActions: NormalizedAlertActionWithGeneratedValues[] ): Promise<{ actions: DenormalizedAction[]; references: SavedObjectReference[] }> { const references: SavedObjectReference[] = []; const actions: DenormalizedAction[] = []; if (alertActions.length) { - const actionsClient = await context.getActionsClient(); const actionIds = [...new Set(alertActions.map((alertAction) => alertAction.id))]; const actionResults = await actionsClient.getBulk({ diff --git a/x-pack/plugins/alerting/server/rules_client/lib/extract_references.ts b/x-pack/plugins/alerting/server/rules_client/lib/extract_references.ts index 9dfca0897ca08..6a131e9ee9179 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/extract_references.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/extract_references.ts @@ -26,7 +26,11 @@ export async function extractReferences< params: ExtractedParams; references: SavedObjectReference[]; }> { - const { references: actionReferences, actions } = await denormalizeActions(context, ruleActions); + const actionsClient = await context.getActionsClient(); + const { references: actionReferences, actions } = await denormalizeActions( + actionsClient, + ruleActions + ); // Extracts any references using configured reference extractor if available const extractedRefsAndParams = ruleType?.useSavedObjectReferences?.extractReferences diff --git a/x-pack/plugins/alerting/server/saved_objects/model_versions/ad_hoc_run_params_model_versions.ts b/x-pack/plugins/alerting/server/saved_objects/model_versions/ad_hoc_run_params_model_versions.ts index 95f544be5c8e2..91a8418a42a37 100644 --- a/x-pack/plugins/alerting/server/saved_objects/model_versions/ad_hoc_run_params_model_versions.ts +++ b/x-pack/plugins/alerting/server/saved_objects/model_versions/ad_hoc_run_params_model_versions.ts @@ -6,7 +6,10 @@ */ import { SavedObjectsModelVersionMap } from '@kbn/core-saved-objects-server'; -import { rawAdHocRunParamsSchemaV1 } from '../schemas/raw_ad_hoc_run_params'; +import { + rawAdHocRunParamsSchemaV1, + rawAdHocRunParamsSchemaV2, +} from '../schemas/raw_ad_hoc_run_params'; export const adHocRunParamsModelVersions: SavedObjectsModelVersionMap = { '1': { @@ -16,4 +19,11 @@ export const adHocRunParamsModelVersions: SavedObjectsModelVersionMap = { create: rawAdHocRunParamsSchemaV1, }, }, + '2': { + changes: [], + schemas: { + forwardCompatibility: rawAdHocRunParamsSchemaV2.extends({}, { unknowns: 'ignore' }), + create: rawAdHocRunParamsSchemaV2, + }, + }, }; diff --git a/x-pack/plugins/alerting/server/saved_objects/schemas/raw_ad_hoc_run_params/index.ts b/x-pack/plugins/alerting/server/saved_objects/schemas/raw_ad_hoc_run_params/index.ts index 977a13f3a7e4b..17907c6405830 100644 --- a/x-pack/plugins/alerting/server/saved_objects/schemas/raw_ad_hoc_run_params/index.ts +++ b/x-pack/plugins/alerting/server/saved_objects/schemas/raw_ad_hoc_run_params/index.ts @@ -6,3 +6,4 @@ */ export { rawAdHocRunParamsSchema as rawAdHocRunParamsSchemaV1 } from './v1'; +export { rawAdHocRunParamsSchema as rawAdHocRunParamsSchemaV2 } from './v2'; diff --git a/x-pack/plugins/alerting/server/saved_objects/schemas/raw_ad_hoc_run_params/latest.ts b/x-pack/plugins/alerting/server/saved_objects/schemas/raw_ad_hoc_run_params/latest.ts new file mode 100644 index 0000000000000..03c55b706231d --- /dev/null +++ b/x-pack/plugins/alerting/server/saved_objects/schemas/raw_ad_hoc_run_params/latest.ts @@ -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 { rawAdHocRunParamsSchema } from './v2'; + +export type RawAdHocRunParams = TypeOf; diff --git a/x-pack/plugins/alerting/server/saved_objects/schemas/raw_ad_hoc_run_params/v1.ts b/x-pack/plugins/alerting/server/saved_objects/schemas/raw_ad_hoc_run_params/v1.ts index 8676c2c606912..5970599492422 100644 --- a/x-pack/plugins/alerting/server/saved_objects/schemas/raw_ad_hoc_run_params/v1.ts +++ b/x-pack/plugins/alerting/server/saved_objects/schemas/raw_ad_hoc_run_params/v1.ts @@ -21,7 +21,7 @@ const rawAdHocRunSchedule = schema.object({ runAt: schema.string(), }); -const rawAdHocRunParamsRuleSchema = schema.object({ +export const rawAdHocRunParamsRuleSchema = schema.object({ name: schema.string(), tags: schema.arrayOf(schema.string()), alertTypeId: schema.string(), diff --git a/x-pack/plugins/alerting/server/saved_objects/schemas/raw_ad_hoc_run_params/v2.ts b/x-pack/plugins/alerting/server/saved_objects/schemas/raw_ad_hoc_run_params/v2.ts new file mode 100644 index 0000000000000..d0597786ff6ed --- /dev/null +++ b/x-pack/plugins/alerting/server/saved_objects/schemas/raw_ad_hoc_run_params/v2.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { FilterStateStore } from '@kbn/es-query'; +import { + rawAdHocRunParamsSchema as rawAdHocRunParamsSchemaV1, + rawAdHocRunParamsRuleSchema as rawAdHocRunParamsRuleSchemaV1, +} from './v1'; + +const ISOWeekdaysSchema = schema.oneOf([ + schema.literal(1), + schema.literal(2), + schema.literal(3), + schema.literal(4), + schema.literal(5), + schema.literal(6), + schema.literal(7), +]); + +const rawRuleAlertsFilterSchema = schema.object({ + query: schema.maybe( + schema.object({ + kql: schema.string(), + filters: schema.arrayOf( + schema.object({ + query: schema.maybe(schema.recordOf(schema.string(), schema.any())), + meta: schema.object({ + alias: schema.maybe(schema.nullable(schema.string())), + disabled: schema.maybe(schema.boolean()), + negate: schema.maybe(schema.boolean()), + controlledBy: schema.maybe(schema.string()), + group: schema.maybe(schema.string()), + index: schema.maybe(schema.string()), + isMultiIndex: schema.maybe(schema.boolean()), + type: schema.maybe(schema.string()), + key: schema.maybe(schema.string()), + params: schema.maybe(schema.any()), + value: schema.maybe(schema.string()), + field: schema.maybe(schema.string()), + relation: schema.maybe(schema.oneOf([schema.literal('OR'), schema.literal('AND')])), + }), + $state: schema.maybe( + schema.object({ + store: schema.oneOf([ + schema.literal(FilterStateStore.APP_STATE), // change + schema.literal(FilterStateStore.GLOBAL_STATE), // change + ]), + }) + ), + }) + ), + dsl: schema.string(), // change + }) + ), + timeframe: schema.maybe( + schema.object({ + days: schema.arrayOf(ISOWeekdaysSchema), + hours: schema.object({ + start: schema.string(), + end: schema.string(), + }), + timezone: schema.string(), + }) + ), +}); + +const rawAdHocRunParamsRuleActionSchema = schema.object({ + uuid: schema.string(), + group: schema.maybe(schema.string()), + actionRef: schema.string(), + actionTypeId: schema.string(), + params: schema.recordOf(schema.string(), schema.any()), + frequency: schema.maybe( + schema.object({ + summary: schema.boolean(), + notifyWhen: schema.oneOf([ + schema.literal('onActionGroupChange'), + schema.literal('onActiveAlert'), + schema.literal('onThrottleInterval'), + ]), + throttle: schema.nullable(schema.string()), + }) + ), + alertsFilter: schema.maybe(rawRuleAlertsFilterSchema), + useAlertDataForTemplate: schema.maybe(schema.boolean()), +}); + +const rawAdHocRunParamsRuleSchema = rawAdHocRunParamsRuleSchemaV1.extends({ + actions: schema.arrayOf(rawAdHocRunParamsRuleActionSchema), +}); + +export const rawAdHocRunParamsSchema = rawAdHocRunParamsSchemaV1.extends({ + rule: rawAdHocRunParamsRuleSchema, +}); diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.test.ts index 00f1a87aefd71..fdc268925c9c1 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.test.ts @@ -18,7 +18,11 @@ import { InjectActionParamsOpts, injectActionParams } from '../inject_action_par import { RuleTypeParams, SanitizedRule, GetViewInAppRelativeUrlFnOpts } from '../../types'; import { RuleRunMetricsStore } from '../../lib/rule_run_metrics_store'; import { alertingEventLoggerMock } from '../../lib/alerting_event_logger/alerting_event_logger.mock'; -import { ConcreteTaskInstance, TaskErrorSource } from '@kbn/task-manager-plugin/server'; +import { + ConcreteTaskInstance, + TaskErrorSource, + TaskPriority, +} from '@kbn/task-manager-plugin/server'; import { RuleNotifyWhen } from '../../../common'; import { asSavedObjectExecutionSource } from '@kbn/actions-plugin/server'; import sinon from 'sinon'; @@ -116,6 +120,7 @@ describe('Action Scheduler', () => { Object { "actionTypeId": "test", "apiKey": "MTIzOmFiYw==", + "apiKeyId": undefined, "consumer": "rule-consumer", "executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", "id": "1", @@ -125,6 +130,7 @@ describe('Action Scheduler', () => { "foo": true, "stateVal": "My goes here", }, + "priority": undefined, "relatedSavedObjects": Array [ Object { "id": "1", @@ -366,6 +372,7 @@ describe('Action Scheduler', () => { Object { "actionTypeId": "test", "apiKey": "MTIzOmFiYw==", + "apiKeyId": undefined, "consumer": "rule-consumer", "executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", "id": "1", @@ -375,6 +382,7 @@ describe('Action Scheduler', () => { "foo": true, "stateVal": "My goes here", }, + "priority": undefined, "relatedSavedObjects": Array [ Object { "id": "1", @@ -411,6 +419,7 @@ describe('Action Scheduler', () => { Object { "actionTypeId": "test", "apiKey": "MTIzOmFiYw==", + "apiKeyId": undefined, "consumer": "rule-consumer", "executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", "id": "1", @@ -420,6 +429,7 @@ describe('Action Scheduler', () => { "foo": true, "stateVal": "My state-val goes here", }, + "priority": undefined, "relatedSavedObjects": Array [ Object { "id": "1", @@ -769,6 +779,7 @@ describe('Action Scheduler', () => { Object { "actionTypeId": "test", "apiKey": "MTIzOmFiYw==", + "apiKeyId": undefined, "consumer": "rule-consumer", "executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", "id": "action-2", @@ -778,6 +789,7 @@ describe('Action Scheduler', () => { "foo": true, "stateVal": "My goes here", }, + "priority": undefined, "relatedSavedObjects": Array [ Object { "id": "1", @@ -1014,12 +1026,14 @@ describe('Action Scheduler', () => { Object { "actionTypeId": "testActionTypeId", "apiKey": "MTIzOmFiYw==", + "apiKeyId": undefined, "consumer": "rule-consumer", "executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", "id": "1", "params": Object { "message": "New: 1 Ongoing: 0 Recovered: 0", }, + "priority": undefined, "relatedSavedObjects": Array [ Object { "id": "1", @@ -1161,12 +1175,14 @@ describe('Action Scheduler', () => { Object { "actionTypeId": "testActionTypeId", "apiKey": "MTIzOmFiYw==", + "apiKeyId": undefined, "consumer": "rule-consumer", "executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", "id": "1", "params": Object { "message": "New: 1 Ongoing: 0 Recovered: 0", }, + "priority": undefined, "relatedSavedObjects": Array [ Object { "id": "1", @@ -1413,6 +1429,7 @@ describe('Action Scheduler', () => { Object { "actionTypeId": "test", "apiKey": "MTIzOmFiYw==", + "apiKeyId": undefined, "consumer": "rule-consumer", "executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", "id": "1", @@ -1422,6 +1439,7 @@ describe('Action Scheduler', () => { "foo": true, "stateVal": "My goes here", }, + "priority": undefined, "relatedSavedObjects": Array [ Object { "id": "1", @@ -1443,6 +1461,7 @@ describe('Action Scheduler', () => { Object { "actionTypeId": "test", "apiKey": "MTIzOmFiYw==", + "apiKeyId": undefined, "consumer": "rule-consumer", "executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", "id": "2", @@ -1452,6 +1471,7 @@ describe('Action Scheduler', () => { "foo": true, "stateVal": "My goes here", }, + "priority": undefined, "relatedSavedObjects": Array [ Object { "id": "1", @@ -1974,6 +1994,154 @@ describe('Action Scheduler', () => { Object { "actionTypeId": "test", "apiKey": "MTIzOmFiYw==", + "apiKeyId": undefined, + "consumer": "rule-consumer", + "executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + "id": "1", + "params": Object { + "alertVal": "My 1 name-of-alert test1 tag-A,tag-B 1 goes here", + "contextVal": "My goes here", + "foo": true, + "stateVal": "My goes here", + }, + "priority": undefined, + "relatedSavedObjects": Array [ + Object { + "id": "1", + "namespace": "test1", + "type": "alert", + "typeId": "test", + }, + ], + "source": Object { + "source": Object { + "id": "1", + "type": "alert", + }, + "type": "SAVED_OBJECT", + }, + "spaceId": "test1", + "uuid": "111-111", + }, + Object { + "actionTypeId": "test", + "apiKey": "MTIzOmFiYw==", + "apiKeyId": undefined, + "consumer": "rule-consumer", + "executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + "id": "1", + "params": Object { + "alertVal": "My 1 name-of-alert test1 tag-A,tag-B 2 goes here", + "contextVal": "My goes here", + "foo": true, + "stateVal": "My goes here", + }, + "priority": undefined, + "relatedSavedObjects": Array [ + Object { + "id": "1", + "namespace": "test1", + "type": "alert", + "typeId": "test", + }, + ], + "source": Object { + "source": Object { + "id": "1", + "type": "alert", + }, + "type": "SAVED_OBJECT", + }, + "spaceId": "test1", + "uuid": "111-111", + }, + Object { + "actionTypeId": "test", + "apiKey": "MTIzOmFiYw==", + "apiKeyId": undefined, + "consumer": "rule-consumer", + "executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", + "id": "1", + "params": Object { + "alertVal": "My 1 name-of-alert test1 tag-A,tag-B 3 goes here", + "contextVal": "My goes here", + "foo": true, + "stateVal": "My goes here", + }, + "priority": undefined, + "relatedSavedObjects": Array [ + Object { + "id": "1", + "namespace": "test1", + "type": "alert", + "typeId": "test", + }, + ], + "source": Object { + "source": Object { + "id": "1", + "type": "alert", + }, + "type": "SAVED_OBJECT", + }, + "spaceId": "test1", + "uuid": "111-111", + }, + ], + ] + `); + }); + + test('does schedule actions with priority when specified', async () => { + const actionScheduler = new ActionScheduler( + getSchedulerContext({ + ...defaultSchedulerContext, + priority: TaskPriority.Low, + rule: { + ...defaultSchedulerContext.rule, + actions: [ + { + ...defaultSchedulerContext.rule.actions[0], + frequency: { + summary: false, + notifyWhen: RuleNotifyWhen.CHANGE, + throttle: null, + }, + }, + ], + }, + }) + ); + + await actionScheduler.run({ + activeCurrentAlerts: { + ...generateAlert({ + id: 1, + pendingRecoveredCount: 1, + lastScheduledActionsGroup: 'recovered', + }), + ...generateAlert({ + id: 2, + pendingRecoveredCount: 1, + lastScheduledActionsGroup: 'recovered', + }), + ...generateAlert({ + id: 3, + pendingRecoveredCount: 1, + lastScheduledActionsGroup: 'recovered', + }), + }, + recoveredCurrentAlerts: {}, + }); + + expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(1); + expect(actionsClient.bulkEnqueueExecution.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "actionTypeId": "test", + "apiKey": "MTIzOmFiYw==", + "apiKeyId": undefined, "consumer": "rule-consumer", "executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", "id": "1", @@ -1983,6 +2151,7 @@ describe('Action Scheduler', () => { "foo": true, "stateVal": "My goes here", }, + "priority": 1, "relatedSavedObjects": Array [ Object { "id": "1", @@ -2004,6 +2173,7 @@ describe('Action Scheduler', () => { Object { "actionTypeId": "test", "apiKey": "MTIzOmFiYw==", + "apiKeyId": undefined, "consumer": "rule-consumer", "executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", "id": "1", @@ -2013,6 +2183,7 @@ describe('Action Scheduler', () => { "foo": true, "stateVal": "My goes here", }, + "priority": 1, "relatedSavedObjects": Array [ Object { "id": "1", @@ -2034,6 +2205,7 @@ describe('Action Scheduler', () => { Object { "actionTypeId": "test", "apiKey": "MTIzOmFiYw==", + "apiKeyId": undefined, "consumer": "rule-consumer", "executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", "id": "1", @@ -2043,6 +2215,7 @@ describe('Action Scheduler', () => { "foo": true, "stateVal": "My goes here", }, + "priority": 1, "relatedSavedObjects": Array [ Object { "id": "1", @@ -2523,6 +2696,7 @@ describe('Action Scheduler', () => { Object { "actionTypeId": ".test-system-action", "apiKey": "MTIzOmFiYw==", + "apiKeyId": undefined, "consumer": "rule-consumer", "executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", "id": "1", @@ -2530,6 +2704,7 @@ describe('Action Scheduler', () => { "foo": "bar", "myParams": "test", }, + "priority": undefined, "relatedSavedObjects": Array [ Object { "id": "1", diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/build_rule_url.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/build_rule_url.ts index 3df27a512c7f9..3f828709ee940 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/build_rule_url.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/build_rule_url.ts @@ -6,16 +6,17 @@ */ import { Logger } from '@kbn/logging'; -import { RuleTypeParams, SanitizedRule } from '@kbn/alerting-types'; +import { RuleTypeParams } from '@kbn/alerting-types'; import { getRuleDetailsRoute, triggersActionsRoute } from '@kbn/rule-data-utils'; import { GetViewInAppRelativeUrlFn } from '../../../types'; +import { ActionSchedulerRule } from '../types'; interface BuildRuleUrlOpts { end?: number; getViewInAppRelativeUrl?: GetViewInAppRelativeUrlFn; kibanaBaseUrl: string | undefined; logger: Logger; - rule: SanitizedRule; + rule: ActionSchedulerRule; spaceId: string; start?: number; } diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/format_action_to_enqueue.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/format_action_to_enqueue.test.ts index 02ff513c5b639..8afcc95c11fff 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/format_action_to_enqueue.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/format_action_to_enqueue.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { TaskPriority } from '@kbn/task-manager-plugin/server'; import { RULE_SAVED_OBJECT_TYPE } from '../../..'; import { formatActionToEnqueue } from './format_action_to_enqueue'; @@ -65,6 +66,124 @@ describe('formatActionToEnqueue', () => { }); }); + test('should format a rule action with priority as expected', () => { + expect( + formatActionToEnqueue({ + action: { + id: '1', + group: 'default', + actionTypeId: 'test', + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '111-111', + }, + apiKey: 'MTIzOmFiYw==', + executionId: '123', + ruleConsumer: 'rule-consumer', + ruleId: 'aaa', + ruleTypeId: 'security-rule', + spaceId: 'default', + priority: TaskPriority.Low, + }) + ).toEqual({ + id: '1', + uuid: '111-111', + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + spaceId: 'default', + apiKey: 'MTIzOmFiYw==', + consumer: 'rule-consumer', + source: { + source: { + id: 'aaa', + type: RULE_SAVED_OBJECT_TYPE, + }, + type: 'SAVED_OBJECT', + }, + executionId: '123', + relatedSavedObjects: [ + { + id: 'aaa', + type: RULE_SAVED_OBJECT_TYPE, + namespace: undefined, + typeId: 'security-rule', + }, + ], + actionTypeId: 'test', + priority: 1, + }); + }); + + test('should format a rule action with apiKeyId as expected', () => { + expect( + formatActionToEnqueue({ + action: { + id: '1', + group: 'default', + actionTypeId: 'test', + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '111-111', + }, + apiKey: 'MTIzOmFiYw==', + apiKeyId: '4534623462346', + executionId: '123', + ruleConsumer: 'rule-consumer', + ruleId: 'aaa', + ruleTypeId: 'security-rule', + spaceId: 'default', + priority: TaskPriority.Low, + }) + ).toEqual({ + id: '1', + uuid: '111-111', + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + spaceId: 'default', + apiKey: 'MTIzOmFiYw==', + consumer: 'rule-consumer', + source: { + source: { + id: 'aaa', + type: RULE_SAVED_OBJECT_TYPE, + }, + type: 'SAVED_OBJECT', + }, + executionId: '123', + relatedSavedObjects: [ + { + id: 'aaa', + type: RULE_SAVED_OBJECT_TYPE, + namespace: undefined, + typeId: 'security-rule', + }, + ], + actionTypeId: 'test', + priority: 1, + apiKeyId: '4534623462346', + }); + }); + test('should format a rule action with null apiKey as expected', () => { expect( formatActionToEnqueue({ diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/format_action_to_enqueue.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/format_action_to_enqueue.ts index af560a19ab9be..763d653688085 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/format_action_to_enqueue.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/format_action_to_enqueue.ts @@ -7,12 +7,15 @@ import { RuleAction, RuleSystemAction } from '@kbn/alerting-types'; import { asSavedObjectExecutionSource } from '@kbn/actions-plugin/server'; +import { TaskPriority } from '@kbn/task-manager-plugin/server'; import { RULE_SAVED_OBJECT_TYPE } from '../../..'; interface FormatActionToEnqueueOpts { action: RuleAction | RuleSystemAction; + apiKeyId?: string; apiKey: string | null; executionId: string; + priority?: TaskPriority; ruleConsumer: string; ruleId: string; ruleTypeId: string; @@ -20,7 +23,17 @@ interface FormatActionToEnqueueOpts { } export const formatActionToEnqueue = (opts: FormatActionToEnqueueOpts) => { - const { action, apiKey, executionId, ruleConsumer, ruleId, ruleTypeId, spaceId } = opts; + const { + action, + apiKey, + apiKeyId, + executionId, + priority, + ruleConsumer, + ruleId, + ruleTypeId, + spaceId, + } = opts; const namespace = spaceId === 'default' ? {} : { namespace: spaceId }; return { @@ -29,6 +42,7 @@ export const formatActionToEnqueue = (opts: FormatActionToEnqueueOpts) => { params: action.params, spaceId, apiKey: apiKey ?? null, + apiKeyId, consumer: ruleConsumer, source: asSavedObjectExecutionSource({ id: ruleId, @@ -44,5 +58,6 @@ export const formatActionToEnqueue = (opts: FormatActionToEnqueueOpts) => { }, ], actionTypeId: action.actionTypeId, + priority, }; }; diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.test.ts index 62e501f6963af..91bfc0d4d0fa1 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.test.ts @@ -22,6 +22,7 @@ import { AlertInstanceContext, AlertInstanceState, } from '@kbn/alerting-state-types'; +import { TaskPriority } from '@kbn/task-manager-plugin/server'; const alertingEventLogger = alertingEventLoggerMock.create(); const actionsClient = actionsClientMock.create(); @@ -91,7 +92,13 @@ const getSchedulerContext = (params = {}) => { return { ...defaultSchedulerContext, rule, ...params, ruleRunMetricsStore }; }; -const getResult = (actionId: string, alertId: string, actionUuid: string) => ({ +const getResult = ( + actionId: string, + alertId: string, + actionUuid: string, + priority?: number, + apiKeyId?: string +) => ({ actionToEnqueue: { actionTypeId: 'test', apiKey: 'MTIzOmFiYw==', @@ -102,6 +109,8 @@ const getResult = (actionId: string, alertId: string, actionUuid: string) => ({ relatedSavedObjects: [{ id: 'rule-id-1', namespace: 'test1', type: 'alert', typeId: 'test' }], source: { source: { id: 'rule-id-1', type: 'alert' }, type: 'SAVED_OBJECT' }, spaceId: 'test1', + ...(priority ? { priority } : {}), + ...(apiKeyId ? { apiKeyId } : {}), }, actionToLog: { alertGroup: 'default', alertId, id: actionId, uuid: actionUuid, typeId: 'test' }, }); @@ -236,6 +245,64 @@ describe('Per-Alert Action Scheduler', () => { ]); }); + test('test should create action to schedule with priority if specified for each alert and each action', async () => { + // 2 per-alert actions * 2 alerts = 4 actions to schedule + const scheduler = new PerAlertActionScheduler({ + ...getSchedulerContext(), + priority: TaskPriority.Low, + }); + const results = await scheduler.getActionsToSchedule({ + activeCurrentAlerts: alerts, + }); + + expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); + expect(logger.debug).not.toHaveBeenCalled(); + + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(4); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(4); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 4, + numberOfTriggeredActions: 4, + }); + + expect(results).toHaveLength(4); + expect(results).toEqual([ + getResult('action-1', '1', '111-111', 1), + getResult('action-1', '2', '111-111', 1), + getResult('action-2', '1', '222-222', 1), + getResult('action-2', '2', '222-222', 1), + ]); + }); + + test('test should create action to schedule with apiKeyId if specified for each alert and each action', async () => { + // 2 per-alert actions * 2 alerts = 4 actions to schedule + const scheduler = new PerAlertActionScheduler({ + ...getSchedulerContext(), + apiKeyId: '23534ybfsdsnsdf', + }); + const results = await scheduler.getActionsToSchedule({ + activeCurrentAlerts: alerts, + }); + + expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); + expect(logger.debug).not.toHaveBeenCalled(); + + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(4); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(4); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 4, + numberOfTriggeredActions: 4, + }); + + expect(results).toHaveLength(4); + expect(results).toEqual([ + getResult('action-1', '1', '111-111', undefined, '23534ybfsdsnsdf'), + getResult('action-1', '2', '111-111', undefined, '23534ybfsdsnsdf'), + getResult('action-2', '1', '222-222', undefined, '23534ybfsdsnsdf'), + getResult('action-2', '2', '222-222', undefined, '23534ybfsdsnsdf'), + ]); + }); + test('should skip creating actions to schedule when alert has maintenance window', async () => { // 2 per-alert actions * 2 alerts = 4 actions to schedule // but alert 1 has maintenance window, so only actions for alert 2 should be scheduled diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.ts index 28b35d885b3d2..0ad9422b7b69d 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.ts @@ -240,7 +240,9 @@ export class PerAlertActionScheduler< actionToEnqueue: formatActionToEnqueue({ action: actionToRun, apiKey: this.context.apiKey, + apiKeyId: this.context.apiKeyId, executionId: this.context.executionId, + priority: this.context.priority, ruleConsumer: this.context.ruleConsumer, ruleId: this.context.rule.id, ruleTypeId: this.context.ruleType.id, diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.test.ts index cb19cb781ae3e..87356cdbacdaa 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.test.ts @@ -28,6 +28,7 @@ import { } from '@kbn/task-manager-plugin/server/task_running/errors'; import { CombinedSummarizedAlerts } from '../../../types'; import { ActionsCompletion } from '@kbn/alerting-state-types'; +import { TaskPriority } from '@kbn/task-manager-plugin/server'; const alertingEventLogger = alertingEventLoggerMock.create(); const actionsClient = actionsClientMock.create(); @@ -97,7 +98,13 @@ const getSchedulerContext = (params = {}) => { return { ...defaultSchedulerContext, rule, ...params, ruleRunMetricsStore }; }; -const getResult = (actionId: string, actionUuid: string, summary: CombinedSummarizedAlerts) => ({ +const getResult = ( + actionId: string, + actionUuid: string, + summary: CombinedSummarizedAlerts, + priority?: number, + apiKeyId?: string +) => ({ actionToEnqueue: { actionTypeId: 'test', apiKey: 'MTIzOmFiYw==', @@ -108,6 +115,8 @@ const getResult = (actionId: string, actionUuid: string, summary: CombinedSummar relatedSavedObjects: [{ id: 'rule-id-1', namespace: 'test1', type: 'alert', typeId: 'test' }], source: { source: { id: 'rule-id-1', type: 'alert' }, type: 'SAVED_OBJECT' }, spaceId: 'test1', + ...(priority && { priority }), + ...(apiKeyId && { apiKeyId }), }, actionToLog: { alertSummary: { @@ -261,6 +270,108 @@ describe('Summary Action Scheduler', () => { ]); }); + test('should create action to schedule with priority if specified for summary action when summary action is per rule run', async () => { + alertsClient.getProcessedAlerts.mockReturnValue(alerts); + const summarizedAlerts = { + new: { count: 2, data: [mockAAD, mockAAD] }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + + const throttledSummaryActions = {}; + const scheduler = new SummaryActionScheduler({ + ...getSchedulerContext(), + priority: TaskPriority.Low, + }); + const results = await scheduler.getActionsToSchedule({ + activeCurrentAlerts: alerts, + throttledSummaryActions, + }); + + expect(throttledSummaryActions).toEqual({}); + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(2); + expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(1, { + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: 'rule-id-1', + spaceId: 'test1', + }); + expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(2, { + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: 'rule-id-1', + spaceId: 'test1', + }); + expect(logger.debug).not.toHaveBeenCalled(); + + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(2); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 2, + numberOfTriggeredActions: 2, + }); + + expect(results).toHaveLength(2); + + const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } }; + expect(results).toEqual([ + getResult('action-2', '222-222', finalSummary, 1), + getResult('action-3', '333-333', finalSummary, 1), + ]); + }); + + test('should create action to schedule with apiKeyId if specified for summary action when summary action is per rule run', async () => { + alertsClient.getProcessedAlerts.mockReturnValue(alerts); + const summarizedAlerts = { + new: { count: 2, data: [mockAAD, mockAAD] }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + + const throttledSummaryActions = {}; + const scheduler = new SummaryActionScheduler({ + ...getSchedulerContext(), + apiKeyId: '24534wy3wydfbs', + }); + const results = await scheduler.getActionsToSchedule({ + activeCurrentAlerts: alerts, + throttledSummaryActions, + }); + + expect(throttledSummaryActions).toEqual({}); + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(2); + expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(1, { + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: 'rule-id-1', + spaceId: 'test1', + }); + expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(2, { + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: 'rule-id-1', + spaceId: 'test1', + }); + expect(logger.debug).not.toHaveBeenCalled(); + + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(2); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 2, + numberOfTriggeredActions: 2, + }); + + expect(results).toHaveLength(2); + + const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } }; + expect(results).toEqual([ + getResult('action-2', '222-222', finalSummary, undefined, '24534wy3wydfbs'), + getResult('action-3', '333-333', finalSummary, undefined, '24534wy3wydfbs'), + ]); + }); + test('should create actions to schedule for summary action when summary action has alertsFilter', async () => { alertsClient.getProcessedAlerts.mockReturnValue(alerts); const summarizedAlerts = { diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.ts index db53f15be2180..a606819f93af8 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.ts @@ -206,7 +206,9 @@ export class SummaryActionScheduler< actionToEnqueue: formatActionToEnqueue({ action: actionToRun, apiKey: this.context.apiKey, + apiKeyId: this.context.apiKeyId, executionId: this.context.executionId, + priority: this.context.priority, ruleConsumer: this.context.ruleConsumer, ruleId: this.context.rule.id, ruleTypeId: this.context.ruleType.id, diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.test.ts index 71a7584c7280b..153af5f2051e9 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.test.ts @@ -27,6 +27,7 @@ import { } from '@kbn/task-manager-plugin/server/task_running/errors'; import { CombinedSummarizedAlerts } from '../../../types'; import { schema } from '@kbn/config-schema'; +import { TaskPriority } from '@kbn/task-manager-plugin/server'; const alertingEventLogger = alertingEventLoggerMock.create(); const actionsClient = actionsClientMock.create(); @@ -68,7 +69,13 @@ const getSchedulerContext = (params = {}) => { return { ...defaultSchedulerContext, rule, ...params, ruleRunMetricsStore }; }; -const getResult = (actionId: string, actionUuid: string, summary: CombinedSummarizedAlerts) => ({ +const getResult = ( + actionId: string, + actionUuid: string, + summary: CombinedSummarizedAlerts, + priority?: number, + apiKeyId?: string +) => ({ actionToEnqueue: { actionTypeId: '.test-system-action', apiKey: 'MTIzOmFiYw==', @@ -79,6 +86,8 @@ const getResult = (actionId: string, actionUuid: string, summary: CombinedSummar relatedSavedObjects: [{ id: 'rule-id-1', namespace: 'test1', type: 'alert', typeId: 'test' }], source: { source: { id: 'rule-id-1', type: 'alert' }, type: 'SAVED_OBJECT' }, spaceId: 'test1', + ...(priority && { priority }), + ...(apiKeyId && { apiKeyId }), }, actionToLog: { alertSummary: { @@ -183,6 +192,82 @@ describe('System Action Scheduler', () => { expect(results).toEqual([getResult('system-action-1', 'xxx-xxx', finalSummary)]); }); + test('should create actions to schedule with priority if specified for each system action', async () => { + alertsClient.getProcessedAlerts.mockReturnValue(alerts); + + const summarizedAlerts = { + new: { count: 2, data: [mockAAD, mockAAD] }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + + const scheduler = new SystemActionScheduler({ + ...getSchedulerContext(), + priority: TaskPriority.Low, + }); + const results = await scheduler.getActionsToSchedule({}); + + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: 'rule-id-1', + spaceId: 'test1', + }); + + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(1); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1); + expect(ruleRunMetricsStore.getStatusByConnectorType('.test-system-action')).toEqual({ + numberOfGeneratedActions: 1, + numberOfTriggeredActions: 1, + }); + + expect(results).toHaveLength(1); + + const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } }; + expect(results).toEqual([getResult('system-action-1', 'xxx-xxx', finalSummary, 1)]); + }); + + test('should create actions to schedule with apiKeyId if specified for each system action', async () => { + alertsClient.getProcessedAlerts.mockReturnValue(alerts); + + const summarizedAlerts = { + new: { count: 2, data: [mockAAD, mockAAD] }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + + const scheduler = new SystemActionScheduler({ + ...getSchedulerContext(), + apiKeyId: '464tfbwer5q43h', + }); + const results = await scheduler.getActionsToSchedule({}); + + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: 'rule-id-1', + spaceId: 'test1', + }); + + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(1); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1); + expect(ruleRunMetricsStore.getStatusByConnectorType('.test-system-action')).toEqual({ + numberOfGeneratedActions: 1, + numberOfTriggeredActions: 1, + }); + + expect(results).toHaveLength(1); + + const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } }; + expect(results).toEqual([ + getResult('system-action-1', 'xxx-xxx', finalSummary, undefined, '464tfbwer5q43h'), + ]); + }); + test('should remove new alerts from summary if suppressed by maintenance window', async () => { const newAlertWithMaintenanceWindow = generateAlert({ id: 1, diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.ts index 0c5cceb0f0a52..01be6e4202ec8 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.ts @@ -156,7 +156,9 @@ export class SystemActionScheduler< actionToEnqueue: formatActionToEnqueue({ action: actionToRun, apiKey: this.context.apiKey, + apiKeyId: this.context.apiKeyId, executionId: this.context.executionId, + priority: this.context.priority, ruleConsumer: this.context.ruleConsumer, ruleId: this.context.rule.id, ruleTypeId: this.context.ruleType.id, diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/types.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/types.ts index 02b9647f91866..d14c871aa4f61 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/types.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/types.ts @@ -9,6 +9,7 @@ import type { Logger } from '@kbn/core/server'; import { PublicMethodsOf } from '@kbn/utility-types'; import { ActionsClient } from '@kbn/actions-plugin/server/actions_client'; import { ExecuteOptions as EnqueueExecutionOptions } from '@kbn/actions-plugin/server/create_execute_function'; +import { TaskPriority } from '@kbn/task-manager-plugin/server'; import { IAlertsClient } from '../../alerts_client/types'; import { Alert } from '../../alert'; import { @@ -31,6 +32,10 @@ import { } from '../../lib/alerting_event_logger/alerting_event_logger'; import { RuleTaskInstance, TaskRunnerContext } from '../types'; +export type ActionSchedulerRule = Omit< + SanitizedRule, + 'executionStatus' +>; export interface ActionSchedulerOptions< Params extends RuleTypeParams, ExtractedParams extends RuleTypeParams, @@ -53,10 +58,11 @@ export interface ActionSchedulerOptions< >; logger: Logger; alertingEventLogger: PublicMethodsOf; - rule: SanitizedRule; + rule: ActionSchedulerRule; taskRunnerContext: TaskRunnerContext; taskInstance: RuleTaskInstance; ruleRunMetricsStore: RuleRunMetricsStore; + apiKeyId?: string; apiKey: RawRule['apiKey']; ruleConsumer: string; executionId: string; @@ -64,6 +70,7 @@ export interface ActionSchedulerOptions< previousStartedAt: Date | null; actionsClient: PublicMethodsOf; alertsClient: IAlertsClient; + priority?: TaskPriority; } export type Executable< diff --git a/x-pack/plugins/alerting/server/task_runner/ad_hoc_task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/ad_hoc_task_runner.test.ts index 0e0d7983e59ff..cf8cc177c096e 100644 --- a/x-pack/plugins/alerting/server/task_runner/ad_hoc_task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/ad_hoc_task_runner.test.ts @@ -7,7 +7,7 @@ import sinon from 'sinon'; import { PluginStartContract as ActionsPluginStart } from '@kbn/actions-plugin/server'; -import { actionsMock } from '@kbn/actions-plugin/server/mocks'; +import { actionsClientMock, actionsMock } from '@kbn/actions-plugin/server/mocks'; import { SavedObject } from '@kbn/core/server'; import { elasticsearchServiceMock, @@ -25,7 +25,7 @@ import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/s import { IEventLogger } from '@kbn/event-log-plugin/server'; import { eventLoggerMock } from '@kbn/event-log-plugin/server/mocks'; import { SharePluginStart } from '@kbn/share-plugin/server'; -import { ConcreteTaskInstance, TaskStatus } from '@kbn/task-manager-plugin/server'; +import { ConcreteTaskInstance, TaskPriority, TaskStatus } from '@kbn/task-manager-plugin/server'; import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; import { AdHocTaskRunner } from './ad_hoc_task_runner'; import { TaskRunnerContext } from './types'; @@ -39,7 +39,7 @@ import { import { AdHocRunSchedule, AdHocRunSO } from '../data/ad_hoc_run/types'; import { AD_HOC_RUN_SAVED_OBJECT_TYPE, RULE_SAVED_OBJECT_TYPE } from '../saved_objects'; import { adHocRunStatus } from '../../common/constants'; -import { DATE_1970, ruleType } from './fixtures'; +import { DATE_1970, generateEnqueueFunctionInput, mockAAD, ruleType } from './fixtures'; import { alertingEventLoggerMock } from '../lib/alerting_event_logger/alerting_event_logger.mock'; import { alertsMock } from '../mocks'; import { UntypedNormalizedRuleType } from '../rule_type_registry'; @@ -93,6 +93,8 @@ import { RuleRunMetricsStore } from '../lib/rule_run_metrics_store'; import { ConnectorAdapterRegistry } from '../connector_adapters/connector_adapter_registry'; import { rulesSettingsServiceMock } from '../rules_settings/rules_settings_service.mock'; import { maintenanceWindowsServiceMock } from './maintenance_windows/maintenance_windows_service.mock'; +import { alertsClientMock } from '../alerts_client/alerts_client.mock'; +import { alertsServiceMock } from '../alerts_service/alerts_service.mock'; const UUID = '5f6aa57d-3e22-484e-bae8-cbed868f4d28'; @@ -121,7 +123,7 @@ type TaskRunnerFactoryInitializerParamsType = jest.Mocked & { executionContext: ReturnType; }; const clusterClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - +const mockAlertsService = alertsServiceMock.create(); const alertingEventLogger = alertingEventLoggerMock.create(); const elasticsearchAndSOAvailability$ = of(true); const alertsService = new AlertsService({ @@ -132,6 +134,8 @@ const alertsService = new AlertsService({ dataStreamAdapter: getDataStreamAdapter({ useDataStreamForAlerts }), elasticsearchAndSOAvailability$, }); +const alertsClient = alertsClientMock.create(); +const actionsClient = actionsClientMock.create(); const backfillClient = backfillClientMock.create(); const dataPlugin = dataPluginMock.createStartContract(); const dataViewsMock = { @@ -280,7 +284,7 @@ describe('Ad Hoc Task Runner', () => { name: 'test', tags: [], alertTypeId: 'siem.queryRule', - // @ts-expect-error + actions: [], params: { author: [], description: 'test', @@ -367,10 +371,22 @@ describe('Ad Hoc Task Runner', () => { taskRunnerFactoryInitializerParams.executionContext.withContext.mockImplementation((ctx, fn) => fn() ); + taskRunnerFactoryInitializerParams.actionsPlugin.getActionsClientWithRequest.mockResolvedValue( + actionsClient + ); + taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); + taskRunnerFactoryInitializerParams.actionsPlugin.renderActionParameterTemplates.mockImplementation( + (_, __, params) => params + ); + maintenanceWindowsService.getMaintenanceWindows.mockResolvedValue({ + maintenanceWindows: [], + maintenanceWindowsWithoutScopedQueryIds: [], + }); ruleTypeRegistry.get.mockReturnValue(ruleTypeWithAlerts); ruleTypeWithAlerts.executor.mockResolvedValue({ state: {} }); mockValidateRuleTypeParams.mockReturnValue(mockedAdHocRunSO.attributes.rule.params); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(mockedAdHocRunSO); + actionsClient.bulkEnqueueExecution.mockResolvedValue({ errors: false, items: [] }); }); afterAll(() => fakeTimer.restore()); @@ -541,6 +557,127 @@ describe('Ad Hoc Task Runner', () => { expect(logger.error).not.toHaveBeenCalled(); }); + test('should schedule actions for rule with actions', async () => { + const mockedAdHocRunSOWithActions = { + ...mockedAdHocRunSO, + attributes: { + ...mockedAdHocRunSO.attributes, + rule: { + ...mockedAdHocRunSO.attributes.rule, + id: '1', + actions: [ + { + uuid: '123abc', + group: 'default', + actionRef: 'action_0', + actionTypeId: 'action', + params: { foo: true }, + frequency: { + notifyWhen: 'onActiveAlert', + summary: true, + throttle: null, + }, + }, + ], + }, + }, + references: [ + { type: RULE_SAVED_OBJECT_TYPE, name: 'rule', id: '1' }, + { id: '4', name: 'action_0', type: 'action' }, + ], + }; + alertsClient.getProcessedAlerts.mockReturnValue({}); + alertsClient.getSummarizedAlerts.mockResolvedValue({ + new: { count: 1, data: [mockAAD] }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }); + mockAlertsService.createAlertsClient.mockImplementation(() => alertsClient); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue( + mockedAdHocRunSOWithActions + ); + + ruleTypeWithAlerts.executor.mockImplementation( + async ({ + services: executorServices, + }: RuleExecutorOptions< + RuleTypeParams, + RuleTypeState, + AlertInstanceState, + AlertInstanceContext, + string, + RuleAlertData + >) => { + executorServices.alertsClient?.report({ + id: '1', + actionGroup: 'default', + payload: { textField: 'foo', numericField: 27 }, + }); + return { state: {} }; + } + ); + + const taskRunner = new AdHocTaskRunner({ + context: { ...taskRunnerFactoryInitializerParams, alertsService: mockAlertsService }, + internalSavedObjectsRepository, + taskInstance: mockedTaskInstance, + }); + expect(AlertingEventLogger).toHaveBeenCalledTimes(1); + + const runnerResult = await taskRunner.run(); + expect(runnerResult).toEqual({ state: {}, runAt: new Date('1970-01-01T00:00:00.000Z') }); + await taskRunner.cleanup(); + + // Verify all the expected calls were made before calling the rule executor + expect(encryptedSavedObjectsClient.getDecryptedAsInternalUser).toHaveBeenCalledWith( + AD_HOC_RUN_SAVED_OBJECT_TYPE, + 'abc', + {} + ); + expect(ruleTypeRegistry.ensureRuleTypeEnabled).toHaveBeenCalledWith('siem.queryRule'); + expect(mockValidateRuleTypeParams).toHaveBeenCalledWith( + mockedAdHocRunSO.attributes.rule.params, + ruleTypeWithAlerts.validate.params + ); + // @ts-ignore - accessing private variable + // should run the first entry in the schedule + expect(taskRunner.scheduleToRunIndex).toEqual(0); + + // Verify all the expected calls were made while calling the rule executor + expect(RuleRunMetricsStore).toHaveBeenCalledTimes(1); + expect(ruleTypeWithAlerts.executor).toHaveBeenCalledTimes(1); + + expect(internalSavedObjectsRepository.update).toHaveBeenCalledWith( + AD_HOC_RUN_SAVED_OBJECT_TYPE, + mockedAdHocRunSO.id, + { + schedule: [ + { ...schedule1, status: adHocRunStatus.COMPLETE }, + schedule2, + schedule3, + schedule4, + schedule5, + ], + }, + { namespace: undefined, refresh: false } + ); + + expect(internalSavedObjectsRepository.delete).not.toHaveBeenCalled(); + + expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(1); + expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledWith( + generateEnqueueFunctionInput({ + isBulk: true, + id: '4', + foo: true, + consumer: 'siem', + uuid: '123abc', + priority: TaskPriority.Low, + apiKeyId: 'apiKeyId', + }) + ); + }); + test('should run with the next pending schedule', async () => { ruleTypeWithAlerts.executor.mockImplementation( async ({ diff --git a/x-pack/plugins/alerting/server/task_runner/ad_hoc_task_runner.ts b/x-pack/plugins/alerting/server/task_runner/ad_hoc_task_runner.ts index d126151030672..81a2e3df3203c 100644 --- a/x-pack/plugins/alerting/server/task_runner/ad_hoc_task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/ad_hoc_task_runner.ts @@ -20,7 +20,7 @@ import { TaskErrorSource, } from '@kbn/task-manager-plugin/server'; import { nanosToMillis } from '@kbn/event-log-plugin/common'; -import { CancellableTask, RunResult } from '@kbn/task-manager-plugin/server/task'; +import { CancellableTask, RunResult, TaskPriority } from '@kbn/task-manager-plugin/server/task'; import { AdHocRunStatus, adHocRunStatus } from '../../common/constants'; import { RuleRunnerErrorStackTraceLog, RuleTaskStateAndMetrics, TaskRunnerContext } from './types'; import { getExecutorServices } from './get_executor_services'; @@ -35,7 +35,7 @@ import { RuleTypeState, } from '../types'; import { TaskRunnerTimer, TaskRunnerTimerSpan } from './task_runner_timer'; -import { AdHocRun, AdHocRunSchedule, AdHocRunSO } from '../data/ad_hoc_run/types'; +import { AdHocRun, AdHocRunSO, AdHocRunSchedule } from '../data/ad_hoc_run/types'; import { AD_HOC_RUN_SAVED_OBJECT_TYPE } from '../saved_objects'; import { RuleMonitoringService } from '../monitoring/rule_monitoring_service'; import { AdHocTaskRunningHandler } from './ad_hoc_task_running_handler'; @@ -52,6 +52,8 @@ import { import { RuleRunMetrics, RuleRunMetricsStore } from '../lib/rule_run_metrics_store'; import { getEsErrorMessage } from '../lib/errors'; import { Result, isOk, asOk, asErr } from '../lib/result_type'; +import { ActionScheduler } from './action_scheduler'; +import { transformAdHocRunToAdHocRunData } from '../application/backfill/transforms/transform_ad_hoc_run_to_backfill_result'; interface ConstructorParams { context: TaskRunnerContext; @@ -173,7 +175,7 @@ export class AdHocTaskRunner implements CancellableTask { return ruleRunMetricsStore.getMetrics(); } - const { rule } = adHocRunData; + const { rule, apiKeyToUse, apiKeyId } = adHocRunData; const ruleType = this.ruleTypeRegistry.get(rule.alertTypeId); const ruleLabel = `${ruleType.id}:${rule.id}: '${rule.name}'`; @@ -253,6 +255,36 @@ export class AdHocTaskRunner implements CancellableTask { throw error; } + const actionScheduler = new ActionScheduler({ + rule: { + ...rule, + muteAll: false, + mutedInstanceIds: [], + createdAt: new Date(rule.createdAt), + updatedAt: new Date(rule.updatedAt), + }, + ruleType, + logger: this.logger, + taskRunnerContext: this.context, + taskInstance: this.taskInstance, + ruleRunMetricsStore, + apiKey: apiKeyToUse, + apiKeyId, + ruleConsumer: rule.consumer, + executionId: this.executionId, + ruleLabel, + previousStartedAt: null, + alertingEventLogger: this.alertingEventLogger, + actionsClient: await this.context.actionsPlugin.getActionsClientWithRequest(fakeRequest), + alertsClient, + priority: TaskPriority.Low, + }); + + await actionScheduler.run({ + activeCurrentAlerts: alertsClient.getProcessedAlerts('activeCurrent'), + recoveredCurrentAlerts: alertsClient.getProcessedAlerts('recoveredCurrent'), + }); + return ruleRunMetricsStore.getMetrics(); } @@ -298,14 +330,12 @@ export class AdHocTaskRunner implements CancellableTask { { namespace } ); - adHocRunData = { - id: adHocRunSO.id, - ...adHocRunSO.attributes, - rule: { - ...adHocRunSO.attributes.rule, - id: adHocRunSO.references[0].id, - }, - }; + adHocRunData = transformAdHocRunToAdHocRunData({ + adHocRunSO, + isSystemAction: (connectorId: string) => + this.context.actionsPlugin.isSystemActionConnector(connectorId), + omitGeneratedActionValues: false, + }); } catch (err) { const errorSource = SavedObjectsErrorHelpers.isNotFoundError(err) ? TaskErrorSource.USER diff --git a/x-pack/plugins/alerting/server/task_runner/fixtures.ts b/x-pack/plugins/alerting/server/task_runner/fixtures.ts index d820f2690caeb..090c633b79602 100644 --- a/x-pack/plugins/alerting/server/task_runner/fixtures.ts +++ b/x-pack/plugins/alerting/server/task_runner/fixtures.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { TaskStatus } from '@kbn/task-manager-plugin/server'; +import { TaskPriority, TaskStatus } from '@kbn/task-manager-plugin/server'; import { SavedObject } from '@kbn/core/server'; import { ALERTING_CASES_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; import { @@ -408,14 +408,20 @@ export const generateEnqueueFunctionInput = ({ isBulk = false, isResolved, foo, + consumer, actionTypeId, + priority, + apiKeyId, }: { uuid?: string; id: string; isBulk?: boolean; isResolved?: boolean; foo?: boolean; + consumer?: string; actionTypeId?: string; + priority?: TaskPriority; + apiKeyId?: string; }) => { const input = { actionTypeId: actionTypeId || 'action', @@ -427,7 +433,7 @@ export const generateEnqueueFunctionInput = ({ ...(isResolved !== undefined ? { isResolved } : {}), ...(foo !== undefined ? { foo } : {}), }, - consumer: 'bar', + consumer: consumer ?? 'bar', relatedSavedObjects: [ { id: '1', @@ -444,6 +450,8 @@ export const generateEnqueueFunctionInput = ({ type: 'SAVED_OBJECT', }, spaceId: 'default', + ...(priority && { priority }), + ...(apiKeyId && { apiKeyId }), }; return isBulk ? [input] : input; }; diff --git a/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts b/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts index fb934db0ffb7d..d4fc0907f8f9a 100644 --- a/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts +++ b/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts @@ -14,8 +14,8 @@ import { AlertInstanceState, AlertInstanceContext, RuleTypeParams, - SanitizedRule, } from '../types'; +import { ActionSchedulerRule } from './action_scheduler/types'; export interface TransformActionParamsOptions { actionsPlugin: ActionsPluginStartContract; @@ -146,7 +146,7 @@ export function transformSummaryActionParams({ kibanaBaseUrl, }: { alerts: SummarizedAlertsWithAll; - rule: SanitizedRule; + rule: ActionSchedulerRule; ruleTypeId: string; actionsPlugin: ActionsPluginStartContract; actionId: string; diff --git a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts index ec99c6ad5bf80..b428cb0dd889d 100644 --- a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts +++ b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts @@ -128,7 +128,9 @@ function getSortByPriority(definitions: TaskTypeDictionary): estypes.SortCombina // TODO: we could do this locally as well, but they may starve source: ` String taskType = doc['task.taskType'].value; - if (params.priority_map.containsKey(taskType)) { + if (doc['task.priority'].size() != 0) { + return doc['task.priority'].value; + } else if (params.priority_map.containsKey(taskType)) { return params.priority_map[taskType]; } else { return ${TaskPriority.Normal}; diff --git a/x-pack/plugins/task_manager/server/saved_objects/mappings.ts b/x-pack/plugins/task_manager/server/saved_objects/mappings.ts index 8ad641b56a58f..7ebd5091c7d47 100644 --- a/x-pack/plugins/task_manager/server/saved_objects/mappings.ts +++ b/x-pack/plugins/task_manager/server/saved_objects/mappings.ts @@ -65,6 +65,9 @@ export const taskMappings: SavedObjectsTypeMappingDefinition = { partition: { type: 'integer', }, + priority: { + type: 'integer', + }, }, }; diff --git a/x-pack/plugins/task_manager/server/saved_objects/model_versions/task_model_versions.ts b/x-pack/plugins/task_manager/server/saved_objects/model_versions/task_model_versions.ts index 775b3ea2f8cad..4c7ae514f0bb7 100644 --- a/x-pack/plugins/task_manager/server/saved_objects/model_versions/task_model_versions.ts +++ b/x-pack/plugins/task_manager/server/saved_objects/model_versions/task_model_versions.ts @@ -6,7 +6,7 @@ */ import { SavedObjectsModelVersionMap } from '@kbn/core-saved-objects-server'; -import { taskSchemaV1, taskSchemaV2 } from '../schemas/task'; +import { taskSchemaV1, taskSchemaV2, taskSchemaV3 } from '../schemas/task'; export const taskModelVersions: SavedObjectsModelVersionMap = { '1': { @@ -35,4 +35,18 @@ export const taskModelVersions: SavedObjectsModelVersionMap = { create: taskSchemaV2, }, }, + '3': { + changes: [ + { + type: 'mappings_addition', + addedMappings: { + priority: { type: 'integer' }, + }, + }, + ], + schemas: { + forwardCompatibility: taskSchemaV3.extends({}, { unknowns: 'ignore' }), + create: taskSchemaV3, + }, + }, }; diff --git a/x-pack/plugins/task_manager/server/saved_objects/schemas/task.ts b/x-pack/plugins/task_manager/server/saved_objects/schemas/task.ts index 2a6ee5c92198c..b6e724099912d 100644 --- a/x-pack/plugins/task_manager/server/saved_objects/schemas/task.ts +++ b/x-pack/plugins/task_manager/server/saved_objects/schemas/task.ts @@ -42,3 +42,7 @@ export const taskSchemaV1 = schema.object({ export const taskSchemaV2 = taskSchemaV1.extends({ partition: schema.maybe(schema.number()), }); + +export const taskSchemaV3 = taskSchemaV2.extends({ + priority: schema.maybe(schema.number()), +}); diff --git a/x-pack/plugins/task_manager/server/task.ts b/x-pack/plugins/task_manager/server/task.ts index bbe2935bdfc6d..23fcc2aa4bac7 100644 --- a/x-pack/plugins/task_manager/server/task.ts +++ b/x-pack/plugins/task_manager/server/task.ts @@ -351,6 +351,11 @@ export interface TaskInstance { * Used to break up tasks so each Kibana node can claim tasks on a subset of the partitions */ partition?: number; + + /* + * Optionally override the priority defined in the task type for this specific task instance + */ + priority?: TaskPriority; } /** diff --git a/x-pack/plugins/task_manager/server/task_claimers/strategy_update_by_query.test.ts b/x-pack/plugins/task_manager/server/task_claimers/strategy_update_by_query.test.ts index 623693e71c54d..cfb7d24025327 100644 --- a/x-pack/plugins/task_manager/server/task_claimers/strategy_update_by_query.test.ts +++ b/x-pack/plugins/task_manager/server/task_claimers/strategy_update_by_query.test.ts @@ -356,7 +356,9 @@ describe('TaskClaiming', () => { }, source: ` String taskType = doc['task.taskType'].value; - if (params.priority_map.containsKey(taskType)) { + if (doc['task.priority'].size() != 0) { + return doc['task.priority'].value; + } else if (params.priority_map.containsKey(taskType)) { return params.priority_map[taskType]; } else { return 50; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/index.ts index d7fe17b4aa1f5..11b62918abe99 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/index.ts @@ -17,5 +17,6 @@ export default function backfillTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./delete_rule')); loadTestFile(require.resolve('./task_runner')); + loadTestFile(require.resolve('./task_runner_with_actions')); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/schedule.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/schedule.ts index ab4734239ce0d..d00702ce7e0bb 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/schedule.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/schedule.ts @@ -13,14 +13,9 @@ import { get } from 'lodash'; import { AD_HOC_RUN_SAVED_OBJECT_TYPE } from '@kbn/alerting-plugin/server/saved_objects'; import { asyncForEach } from '../../../../../../functional/services/transform/api'; import { UserAtSpaceScenarios } from '../../../../scenarios'; -import { - checkAAD, - getTestRuleData, - getUrlPrefix, - ObjectRemover, - TaskManagerDoc, -} from '../../../../../common/lib'; +import { checkAAD, getTestRuleData, getUrlPrefix, ObjectRemover } from '../../../../../common/lib'; import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { TEST_ACTIONS_INDEX, getScheduledTask } from './test_utils'; // eslint-disable-next-line import/no-default-export export default function scheduleBackfillTests({ getService }: FtrProviderContext) { @@ -36,6 +31,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext await asyncForEach(backfillIds, async ({ id, spaceId }: { id: string; spaceId: string }) => { await supertest .delete(`${getUrlPrefix(spaceId)}/internal/alerting/rules/backfill/${id}`) + .set('x-elastic-internal-origin', 'xxx') .set('kbn-xsrf', 'foo'); }); backfillIds = []; @@ -50,16 +46,13 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext return result._source; } - async function getScheduledTask(id: string): Promise { - const scheduledTask = await es.get({ - id: `task:${id}`, - index: '.kibana_task_manager', - }); - return scheduledTask._source!; - } - function getRule(overwrites = {}) { - return getTestRuleData({ + return { + name: 'abc', + enabled: true, + tags: ['foo'], + consumer: 'alertsFixture', + actions: [], rule_type_id: 'test.patternFiringAutoRecoverFalse', params: { pattern: { @@ -68,7 +61,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext }, schedule: { interval: '12h' }, ...overwrites, - }); + }; } function getLifecycleRule(overwrites = {}) { @@ -150,6 +143,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext // schedule backfill for both rules as current user const response = await supertestWithoutAuth .post(`${getUrlPrefix(apiOptions.spaceId)}/internal/alerting/rules/backfill/_schedule`) + .set('x-elastic-internal-origin', 'xxx') .set('kbn-xsrf', 'foo') .auth(apiOptions.username, apiOptions.password) .send([ @@ -282,7 +276,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext expect(adHocRunSO2.references).to.eql([{ id: ruleId2, name: 'rule', type: 'alert' }]); // check that the task was scheduled correctly - const taskRecord1 = await getScheduledTask(result[0].id); + const taskRecord1 = await getScheduledTask(es, result[0].id); expect(taskRecord1.type).to.eql('task'); expect(taskRecord1.task.taskType).to.eql('ad_hoc_run-backfill'); expect(taskRecord1.task.timeoutOverride).to.eql('10s'); @@ -291,7 +285,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext adHocRunParamsId: result[0].id, spaceId: space.id, }); - const taskRecord2 = await getScheduledTask(result[1].id); + const taskRecord2 = await getScheduledTask(es, result[1].id); expect(taskRecord2.type).to.eql('task'); expect(taskRecord2.task.taskType).to.eql('ad_hoc_run-backfill'); expect(taskRecord2.task.timeoutOverride).to.eql('10s'); @@ -338,6 +332,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext // schedule 3 backfill jobs for rule as current user const response = await supertestWithoutAuth .post(`${getUrlPrefix(apiOptions.spaceId)}/internal/alerting/rules/backfill/_schedule`) + .set('x-elastic-internal-origin', 'xxx') .set('kbn-xsrf', 'foo') .auth(apiOptions.username, apiOptions.password) .send([ @@ -513,7 +508,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext expect(adHocRunSO3.references).to.eql([{ id: ruleId, name: 'rule', type: 'alert' }]); // check that the task was scheduled correctly - const taskRecord1 = await getScheduledTask(result[0].id); + const taskRecord1 = await getScheduledTask(es, result[0].id); expect(taskRecord1.type).to.eql('task'); expect(taskRecord1.task.taskType).to.eql('ad_hoc_run-backfill'); expect(taskRecord1.task.timeoutOverride).to.eql('10s'); @@ -522,7 +517,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext adHocRunParamsId: result[0].id, spaceId: space.id, }); - const taskRecord2 = await getScheduledTask(result[1].id); + const taskRecord2 = await getScheduledTask(es, result[1].id); expect(taskRecord2.type).to.eql('task'); expect(taskRecord2.task.taskType).to.eql('ad_hoc_run-backfill'); expect(taskRecord2.task.timeoutOverride).to.eql('10s'); @@ -531,7 +526,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext adHocRunParamsId: result[1].id, spaceId: space.id, }); - const taskRecord3 = await getScheduledTask(result[2].id); + const taskRecord3 = await getScheduledTask(es, result[2].id); expect(taskRecord3.type).to.eql('task'); expect(taskRecord3.task.taskType).to.eql('ad_hoc_run-backfill'); expect(taskRecord3.task.timeoutOverride).to.eql('10s'); @@ -571,6 +566,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext // invalid start time const response1 = await supertestWithoutAuth .post(`${getUrlPrefix(apiOptions.spaceId)}/internal/alerting/rules/backfill/_schedule`) + .set('x-elastic-internal-origin', 'xxx') .set('kbn-xsrf', 'foo') .auth(apiOptions.username, apiOptions.password) .send([{ rule_id: 'abc', start: 'foo' }]); @@ -578,6 +574,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext // invalid end time const response2 = await supertestWithoutAuth .post(`${getUrlPrefix(apiOptions.spaceId)}/internal/alerting/rules/backfill/_schedule`) + .set('x-elastic-internal-origin', 'xxx') .set('kbn-xsrf', 'foo') .auth(apiOptions.username, apiOptions.password) .send([ @@ -592,6 +589,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext const time = moment().utc().startOf('day').subtract(7, 'days').toISOString(); const response3 = await supertestWithoutAuth .post(`${getUrlPrefix(apiOptions.spaceId)}/internal/alerting/rules/backfill/_schedule`) + .set('x-elastic-internal-origin', 'xxx') .set('kbn-xsrf', 'foo') .auth(apiOptions.username, apiOptions.password) .send([{ rule_id: 'abc', start: time, end: time }]); @@ -599,6 +597,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext // end time is before start time const response4 = await supertestWithoutAuth .post(`${getUrlPrefix(apiOptions.spaceId)}/internal/alerting/rules/backfill/_schedule`) + .set('x-elastic-internal-origin', 'xxx') .set('kbn-xsrf', 'foo') .auth(apiOptions.username, apiOptions.password) .send([ @@ -612,6 +611,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext // start time is too far in the past const response5 = await supertestWithoutAuth .post(`${getUrlPrefix(apiOptions.spaceId)}/internal/alerting/rules/backfill/_schedule`) + .set('x-elastic-internal-origin', 'xxx') .set('kbn-xsrf', 'foo') .auth(apiOptions.username, apiOptions.password) .send([{ rule_id: 'abc', start: '2023-04-30T00:00:00.000Z' }]); @@ -619,6 +619,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext // start time is in the future const response6 = await supertestWithoutAuth .post(`${getUrlPrefix(apiOptions.spaceId)}/internal/alerting/rules/backfill/_schedule`) + .set('x-elastic-internal-origin', 'xxx') .set('kbn-xsrf', 'foo') .auth(apiOptions.username, apiOptions.password) .send([ @@ -628,6 +629,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext // end time is in the future const response7 = await supertestWithoutAuth .post(`${getUrlPrefix(apiOptions.spaceId)}/internal/alerting/rules/backfill/_schedule`) + .set('x-elastic-internal-origin', 'xxx') .set('kbn-xsrf', 'foo') .auth(apiOptions.username, apiOptions.password) .send([ @@ -714,6 +716,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext // schedule backfill for non-existent rule const response = await supertestWithoutAuth .post(`${getUrlPrefix(apiOptions.spaceId)}/internal/alerting/rules/backfill/_schedule`) + .set('x-elastic-internal-origin', 'xxx') .set('kbn-xsrf', 'foo') .auth(apiOptions.username, apiOptions.password) .send([ @@ -813,6 +816,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext // schedule backfill as current user const response = await supertestWithoutAuth .post(`${getUrlPrefix(apiOptions.spaceId)}/internal/alerting/rules/backfill/_schedule`) + .set('x-elastic-internal-origin', 'xxx') .set('kbn-xsrf', 'foo') .auth(apiOptions.username, apiOptions.password) .send([ @@ -1018,7 +1022,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext expect(adHocRunSO3.references).to.eql([{ id: ruleId1, name: 'rule', type: 'alert' }]); // check that the task was scheduled correctly - const taskRecord1 = await getScheduledTask(result[0].id); + const taskRecord1 = await getScheduledTask(es, result[0].id); expect(taskRecord1.type).to.eql('task'); expect(taskRecord1.task.taskType).to.eql('ad_hoc_run-backfill'); expect(taskRecord1.task.timeoutOverride).to.eql('10s'); @@ -1027,7 +1031,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext adHocRunParamsId: result[0].id, spaceId: space.id, }); - const taskRecord2 = await getScheduledTask(result[1].id); + const taskRecord2 = await getScheduledTask(es, result[1].id); expect(taskRecord2.type).to.eql('task'); expect(taskRecord2.task.taskType).to.eql('ad_hoc_run-backfill'); expect(taskRecord2.task.timeoutOverride).to.eql('10s'); @@ -1036,7 +1040,7 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext adHocRunParamsId: result[1].id, spaceId: space.id, }); - const taskRecord3 = await getScheduledTask(result[5].id); + const taskRecord3 = await getScheduledTask(es, result[5].id); expect(taskRecord3.type).to.eql('task'); expect(taskRecord3.task.taskType).to.eql('ad_hoc_run-backfill'); expect(taskRecord3.task.timeoutOverride).to.eql('10s'); @@ -1070,6 +1074,267 @@ export default function scheduleBackfillTests({ getService }: FtrProviderContext throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } }); + + it('should handle schedule request where rule has supported and unsupported actions', async () => { + // create a connector + const cresponse = await supertest + .post(`${getUrlPrefix(apiOptions.spaceId)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'An index connector', + connector_type_id: '.index', + config: { + index: TEST_ACTIONS_INDEX, + refresh: true, + }, + secrets: {}, + }) + .expect(200); + const connectorId = cresponse.body.id; + objectRemover.add(apiOptions.spaceId, connectorId, 'connector', 'actions'); + + const start = moment().utc().startOf('day').subtract(14, 'days').toISOString(); + const end = moment().utc().startOf('day').subtract(5, 'days').toISOString(); + // create 2 rules + const rresponse1 = await supertest + .post(`${getUrlPrefix(apiOptions.spaceId)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getRule({ + actions: [ + { + group: 'default', + id: connectorId, + uuid: '111-111', + params: { documents: [{ alertUuid: '{{alert.uuid}}' }] }, + frequency: { notify_when: 'onActiveAlert', throttle: null, summary: true }, + }, + { + group: 'default', + id: connectorId, + uuid: '222-222', + params: { documents: [{ alertUuid: '{{alert.uuid}}' }] }, + frequency: { + notify_when: 'onActionGroupChange', + throttle: null, + summary: true, + }, + }, + ], + }) + ) + .expect(200); + const ruleId1 = rresponse1.body.id; + objectRemover.add(apiOptions.spaceId, ruleId1, 'rule', 'alerting'); + + const rresponse2 = await supertest + .post(`${getUrlPrefix(apiOptions.spaceId)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getRule({ + actions: [ + { + group: 'default', + id: connectorId, + uuid: '333-333', + params: { documents: [{ alertUuid: '{{alert.uuid}}' }] }, + frequency: { notify_when: 'onActiveAlert', throttle: null, summary: false }, + }, + ], + }) + ) + .expect(200); + const ruleId2 = rresponse2.body.id; + objectRemover.add(apiOptions.spaceId, ruleId2, 'rule', 'alerting'); + + // schedule backfill as current user + const response = await supertestWithoutAuth + .post(`${getUrlPrefix(apiOptions.spaceId)}/internal/alerting/rules/backfill/_schedule`) + .set('kbn-xsrf', 'foo') + .set('x-elastic-internal-origin', 'xxx') + .auth(apiOptions.username, apiOptions.password) + .send([ + { rule_id: ruleId1, start, end, run_actions: true }, + { rule_id: ruleId2, start, end, run_actions: true }, + ]); + + switch (scenario.id) { + // User can't do anything in this space + case 'no_kibana_privileges at space1': + // User has no privileges in this space + case 'space_1_all at space2': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to find rules for any rule types`, + statusCode: 403, + }); + break; + // User has read privileges in this space + case 'global_read at space1': + expect(response.statusCode).to.eql(403); + expect(response.body.error).to.eql('Forbidden'); + expect(response.body.message).to.match( + /Unauthorized by "alertsFixture" to scheduleBackfill "[^"]+" rule/ + ); + break; + // User doesn't have access to actions + case 'space_1_all_alerts_none_actions at space1': + expect(response.statusCode).to.eql(403); + expect(response.body.error).to.eql('Forbidden'); + expect(response.body.message).to.eql('Unauthorized to get actions'); + break; + // Superuser has access to everything + case 'superuser at space1': + // User has all privileges in this space + case 'space_1_all at space1': + // User has all privileges in this space + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(200); + const result = response.body; + + expect(result.length).to.eql(2); + + // successful schedule with warning for unsupported action + expect(typeof result[0].id).to.be('string'); + backfillIds.push({ id: result[0].id, spaceId: apiOptions.spaceId }); + expect(result[0].duration).to.eql('12h'); + expect(result[0].enabled).to.eql(true); + expect(result[0].start).to.eql(start); + expect(result[0].end).to.eql(end); + expect(result[0].status).to.eql('pending'); + expect(result[0].space_id).to.eql(space.id); + expect(typeof result[0].created_at).to.be('string'); + expect(result[0].rule.actions.length).to.eql(1); + expect(result[0].rule.actions[0]).to.eql({ + actionTypeId: '.index', + group: 'default', + id: connectorId, + uuid: '111-111', + params: { documents: [{ alertUuid: '{{alert.uuid}}' }] }, + frequency: { notifyWhen: 'onActiveAlert', throttle: null, summary: true }, + }); + expect(result[0].warnings).to.eql([ + `Rule has actions that are not supported for backfill. Those actions will be skipped.`, + ]); + + let currentStart = start; + result[0].schedule.forEach((sched: any) => { + expect(sched.interval).to.eql('12h'); + expect(sched.status).to.eql('pending'); + const runAt = moment(currentStart).add(12, 'hours').toISOString(); + expect(sched.run_at).to.eql(runAt); + currentStart = runAt; + }); + + // // successful schedule + expect(typeof result[1].id).to.be('string'); + backfillIds.push({ id: result[1].id, spaceId: apiOptions.spaceId }); + expect(result[1].duration).to.eql('12h'); + expect(result[1].enabled).to.eql(true); + expect(result[1].start).to.eql(start); + expect(result[1].end).to.eql(end); + expect(result[1].status).to.eql('pending'); + expect(result[1].space_id).to.eql(space.id); + expect(typeof result[1].created_at).to.be('string'); + expect(result[1].rule.actions.length).to.eql(1); + expect(result[1].rule.actions[0]).to.eql({ + actionTypeId: '.index', + group: 'default', + id: connectorId, + uuid: '333-333', + params: { documents: [{ alertUuid: '{{alert.uuid}}' }] }, + frequency: { notifyWhen: 'onActiveAlert', throttle: null, summary: false }, + }); + expect(result[1].warnings).to.be(undefined); + + currentStart = start; + result[1].schedule.forEach((sched: any) => { + expect(sched.interval).to.eql('12h'); + expect(sched.status).to.eql('pending'); + const runAt = moment(currentStart).add(12, 'hours').toISOString(); + expect(sched.run_at).to.eql(runAt); + currentStart = runAt; + }); + + // check that the expected ad hoc run SOs were created + const adHocRunSO1 = (await getAdHocRunSO(result[0].id)) as SavedObject; + const adHocRun1: AdHocRunSO = get(adHocRunSO1, 'ad_hoc_run_params')!; + const adHocRunSO2 = (await getAdHocRunSO(result[1].id)) as SavedObject; + const adHocRun2: AdHocRunSO = get(adHocRunSO2, 'ad_hoc_run_params')!; + + expect(typeof adHocRun1.apiKeyId).to.be('string'); + expect(typeof adHocRun1.apiKeyToUse).to.be('string'); + expect(typeof adHocRun1.createdAt).to.be('string'); + expect(adHocRun1.duration).to.eql('12h'); + expect(adHocRun1.enabled).to.eql(true); + expect(adHocRun1.start).to.eql(start); + expect(adHocRun1.end).to.eql(end); + expect(adHocRun1.status).to.eql('pending'); + expect(adHocRun1.spaceId).to.eql(space.id); + + currentStart = start; + adHocRun1.schedule.forEach((sched: any) => { + expect(sched.interval).to.eql('12h'); + expect(sched.status).to.eql('pending'); + const runAt = moment(currentStart).add(12, 'hours').toISOString(); + expect(sched.runAt).to.eql(runAt); + currentStart = runAt; + }); + + expect(typeof adHocRun2.apiKeyId).to.be('string'); + expect(typeof adHocRun2.apiKeyToUse).to.be('string'); + expect(typeof adHocRun2.createdAt).to.be('string'); + expect(adHocRun2.duration).to.eql('12h'); + expect(adHocRun2.enabled).to.eql(true); + expect(adHocRun2.start).to.eql(start); + expect(adHocRun2.end).to.eql(end); + expect(adHocRun2.status).to.eql('pending'); + expect(adHocRun2.spaceId).to.eql(space.id); + + currentStart = start; + adHocRun2.schedule.forEach((sched: any) => { + expect(sched.interval).to.eql('12h'); + expect(sched.status).to.eql('pending'); + const runAt = moment(currentStart).add(12, 'hours').toISOString(); + expect(sched.runAt).to.eql(runAt); + currentStart = runAt; + }); + + // check references are stored correctly + expect(adHocRunSO1.references).to.eql([ + { id: ruleId1, name: 'rule', type: 'alert' }, + { id: connectorId, name: 'action_0', type: 'action' }, + ]); + expect(adHocRunSO2.references).to.eql([ + { id: ruleId2, name: 'rule', type: 'alert' }, + { id: connectorId, name: 'action_0', type: 'action' }, + ]); + + // check that the task was scheduled correctly + const taskRecord1 = await getScheduledTask(es, result[0].id); + expect(taskRecord1.type).to.eql('task'); + expect(taskRecord1.task.taskType).to.eql('ad_hoc_run-backfill'); + expect(taskRecord1.task.timeoutOverride).to.eql('10s'); + expect(taskRecord1.task.enabled).to.eql(true); + expect(JSON.parse(taskRecord1.task.params)).to.eql({ + adHocRunParamsId: result[0].id, + spaceId: space.id, + }); + const taskRecord2 = await getScheduledTask(es, result[1].id); + expect(taskRecord2.type).to.eql('task'); + expect(taskRecord2.task.taskType).to.eql('ad_hoc_run-backfill'); + expect(taskRecord2.task.timeoutOverride).to.eql('10s'); + expect(taskRecord2.task.enabled).to.eql(true); + expect(JSON.parse(taskRecord2.task.params)).to.eql({ + adHocRunParamsId: result[1].id, + spaceId: space.id, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); }); } }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/task_runner.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/task_runner.ts index 2083a79c0d0f5..0bd46289c416f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/task_runner.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/task_runner.ts @@ -7,7 +7,6 @@ import expect from '@kbn/expect'; import moment from 'moment'; -import { SearchHit } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { SecurityAlert } from '@kbn/alerts-as-data-utils'; import { ALERT_LAST_DETECTED, @@ -33,27 +32,23 @@ import { RULE_SAVED_OBJECT_TYPE, } from '@kbn/alerting-plugin/server/saved_objects'; import { ALERT_ORIGINAL_TIME } from '@kbn/security-solution-plugin/common/field_maps/field_names'; -import { - createEsDocument, - DOCUMENT_REFERENCE, - DOCUMENT_SOURCE, -} from '../../../../../spaces_only/tests/alerting/create_test_data'; -import { asyncForEach } from '../../../../../../functional/services/transform/api'; +import { DOCUMENT_SOURCE } from '../../../../../spaces_only/tests/alerting/create_test_data'; import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { SuperuserAtSpace1 } from '../../../../scenarios'; +import { getTestRuleData, getUrlPrefix, ObjectRemover } from '../../../../../common/lib'; import { - getEventLog, - getTestRuleData, - getUrlPrefix, - ObjectRemover, - TaskManagerDoc, -} from '../../../../../common/lib'; + getScheduledTask, + indexTestDocs, + queryForAlertDocs, + searchScheduledTask, + testDocTimestamps, + waitForEventLogDocs, +} from './test_utils'; // eslint-disable-next-line import/no-default-export export default function createBackfillTaskRunnerTests({ getService }: FtrProviderContext) { const es = getService('es'); const retry = getService('retry'); - const log = getService('log'); const esTestIndexTool = new ESTestIndexTool(es, retry); const supertestWithoutAuth = getService('supertestWithoutAuth'); const supertest = getService('supertest'); @@ -61,31 +56,6 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide const alertsAsDataIndex = '.alerts-security.alerts-space1'; const timestampPattern = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; - const originalDocTimestamps = [ - // before first backfill run - moment().utc().subtract(14, 'days').toISOString(), - - // backfill execution set 1 - moment().utc().startOf('day').subtract(13, 'days').add(10, 'minutes').toISOString(), - moment().utc().startOf('day').subtract(13, 'days').add(11, 'minutes').toISOString(), - moment().utc().startOf('day').subtract(13, 'days').add(12, 'minutes').toISOString(), - - // backfill execution set 2 - moment().utc().startOf('day').subtract(12, 'days').add(20, 'minutes').toISOString(), - - // backfill execution set 3 - moment().utc().startOf('day').subtract(11, 'days').add(30, 'minutes').toISOString(), - moment().utc().startOf('day').subtract(11, 'days').add(31, 'minutes').toISOString(), - moment().utc().startOf('day').subtract(11, 'days').add(32, 'minutes').toISOString(), - moment().utc().startOf('day').subtract(11, 'days').add(33, 'minutes').toISOString(), - moment().utc().startOf('day').subtract(11, 'days').add(34, 'minutes').toISOString(), - - // backfill execution set 4 purposely left empty - - // after last backfill - moment().utc().startOf('day').subtract(9, 'days').add(40, 'minutes').toISOString(), - moment().utc().startOf('day').subtract(9, 'days').add(41, 'minutes').toISOString(), - ]; describe('ad hoc backfill task', () => { beforeEach(async () => { @@ -113,7 +83,7 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide const spaceId = SuperuserAtSpace1.space.id; // Index documents - await indexTestDocs(); + await indexTestDocs(es, esTestIndexTool); // Create siem.queryRule const response1 = await supertestWithoutAuth @@ -165,8 +135,8 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide const ruleId = response1.body.id; objectRemover.add(spaceId, ruleId, 'rule', 'alerting'); - const start = moment(originalDocTimestamps[1]).utc().startOf('day').toISOString(); - const end = moment(originalDocTimestamps[11]).utc().startOf('day').toISOString(); + const start = moment(testDocTimestamps[1]).utc().startOf('day').toISOString(); + const end = moment(testDocTimestamps[11]).utc().startOf('day').toISOString(); // Schedule backfill for this rule const response2 = await supertestWithoutAuth @@ -176,9 +146,6 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide .send([{ rule_id: ruleId, start, end }]) .expect(200); - log.info(`originalDocTimestamps ${JSON.stringify(originalDocTimestamps)}`); - log.info(`scheduledBackfill ${JSON.stringify(response2.body)}`); - const scheduleResult = response2.body; expect(scheduleResult.length).to.eql(1); @@ -196,7 +163,7 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide const backfillId = scheduleResult[0].id; // check that the task was scheduled correctly - const taskRecord = await getScheduledTask(backfillId); + const taskRecord = await getScheduledTask(es, backfillId); expect(taskRecord.type).to.eql('task'); expect(taskRecord.task.taskType).to.eql('ad_hoc_run-backfill'); expect(taskRecord.task.timeoutOverride).to.eql('5m'); @@ -208,6 +175,8 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide // get the execute-backfill events const events: IValidatedEvent[] = await waitForEventLogDocs( + retry, + getService, backfillId, spaceId, new Map([['execute-backfill', { equal: 4 }]]) @@ -284,7 +253,7 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide ); // query for alert docs - const alertDocs = await queryForAlertDocs(); + const alertDocs = await queryForAlertDocs(es, alertsAsDataIndex); expect(alertDocs.length).to.eql(9); // each alert doc should have these fields @@ -320,9 +289,9 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide ); } - expect(alertDocsBackfill1[0]._source![ALERT_ORIGINAL_TIME]).to.eql(originalDocTimestamps[1]); - expect(alertDocsBackfill1[1]._source![ALERT_ORIGINAL_TIME]).to.eql(originalDocTimestamps[2]); - expect(alertDocsBackfill1[2]._source![ALERT_ORIGINAL_TIME]).to.eql(originalDocTimestamps[3]); + expect(alertDocsBackfill1[0]._source![ALERT_ORIGINAL_TIME]).to.eql(testDocTimestamps[1]); + expect(alertDocsBackfill1[1]._source![ALERT_ORIGINAL_TIME]).to.eql(testDocTimestamps[2]); + expect(alertDocsBackfill1[2]._source![ALERT_ORIGINAL_TIME]).to.eql(testDocTimestamps[3]); // backfill run 2 alerts const alertDocsBackfill2 = alertDocs.filter( @@ -342,7 +311,7 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide ); } - expect(alertDocsBackfill2[0]._source![ALERT_ORIGINAL_TIME]).to.eql(originalDocTimestamps[4]); + expect(alertDocsBackfill2[0]._source![ALERT_ORIGINAL_TIME]).to.eql(testDocTimestamps[4]); // backfill run 3 alerts const alertDocsBackfill3 = alertDocs.filter( @@ -362,11 +331,11 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide ); } - expect(alertDocsBackfill3[0]._source![ALERT_ORIGINAL_TIME]).to.eql(originalDocTimestamps[5]); - expect(alertDocsBackfill3[1]._source![ALERT_ORIGINAL_TIME]).to.eql(originalDocTimestamps[6]); - expect(alertDocsBackfill3[2]._source![ALERT_ORIGINAL_TIME]).to.eql(originalDocTimestamps[7]); - expect(alertDocsBackfill3[3]._source![ALERT_ORIGINAL_TIME]).to.eql(originalDocTimestamps[8]); - expect(alertDocsBackfill3[4]._source![ALERT_ORIGINAL_TIME]).to.eql(originalDocTimestamps[9]); + expect(alertDocsBackfill3[0]._source![ALERT_ORIGINAL_TIME]).to.eql(testDocTimestamps[5]); + expect(alertDocsBackfill3[1]._source![ALERT_ORIGINAL_TIME]).to.eql(testDocTimestamps[6]); + expect(alertDocsBackfill3[2]._source![ALERT_ORIGINAL_TIME]).to.eql(testDocTimestamps[7]); + expect(alertDocsBackfill3[3]._source![ALERT_ORIGINAL_TIME]).to.eql(testDocTimestamps[8]); + expect(alertDocsBackfill3[4]._source![ALERT_ORIGINAL_TIME]).to.eql(testDocTimestamps[9]); // backfill run 4 alerts const alertDocsBackfill4 = alertDocs.filter( @@ -375,7 +344,7 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide expect(alertDocsBackfill4.length).to.eql(0); // task should have been deleted after backfill runs have finished - const numHits = await searchScheduledTask(backfillId); + const numHits = await searchScheduledTask(es, backfillId); expect(numHits).to.eql(0); }); @@ -425,7 +394,7 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide const backfillId = scheduleResult[0].id; // check that the task was scheduled correctly - const taskRecord = await getScheduledTask(backfillId); + const taskRecord = await getScheduledTask(es, backfillId); expect(taskRecord.type).to.eql('task'); expect(taskRecord.task.taskType).to.eql('ad_hoc_run-backfill'); expect(taskRecord.task.timeoutOverride).to.eql('10s'); @@ -437,6 +406,8 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide // get the execute-timeout and execute-backfill events const events: IValidatedEvent[] = await waitForEventLogDocs( + retry, + getService, backfillId, spaceId, new Map([ @@ -515,7 +486,7 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide } // task should have been deleted after backfill runs have finished - const numHits = await searchScheduledTask(backfillId); + const numHits = await searchScheduledTask(es, backfillId); expect(numHits).to.eql(0); }); @@ -567,7 +538,7 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide const backfillId = scheduleResult[0].id; // check that the task was scheduled correctly - const taskRecord = await getScheduledTask(backfillId); + const taskRecord = await getScheduledTask(es, backfillId); expect(taskRecord.type).to.eql('task'); expect(taskRecord.task.taskType).to.eql('ad_hoc_run-backfill'); expect(taskRecord.task.timeoutOverride).to.eql('10s'); @@ -579,6 +550,8 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide // get the execute-backfill events const events: IValidatedEvent[] = await waitForEventLogDocs( + retry, + getService, backfillId, spaceId, new Map([['execute-backfill', { equal: 4 }]]) @@ -652,83 +625,8 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide ); // task should have been deleted after backfill runs have finished - const numHits = await searchScheduledTask(backfillId); + const numHits = await searchScheduledTask(es, backfillId); expect(numHits).to.eql(0); }); - - async function indexTestDocs() { - await asyncForEach(originalDocTimestamps, async (timestamp: string) => { - await createEsDocument(es, new Date(timestamp).valueOf(), 1, ES_TEST_INDEX_NAME); - }); - - await esTestIndexTool.waitForDocs( - DOCUMENT_SOURCE, - DOCUMENT_REFERENCE, - originalDocTimestamps.length - ); - } }); - - async function queryForAlertDocs(): Promise>> { - const searchResult = await es.search({ - index: alertsAsDataIndex, - body: { - sort: [{ [ALERT_ORIGINAL_TIME]: { order: 'asc' } }], - query: { match_all: {} }, - }, - }); - return searchResult.hits.hits as Array>; - } - - async function waitForEventLogDocs( - id: string, - spaceId: string, - actions: Map - ) { - return await retry.try(async () => { - return await getEventLog({ - getService, - spaceId, - type: AD_HOC_RUN_SAVED_OBJECT_TYPE, - id, - provider: 'alerting', - actions, - }); - }); - } - - async function getScheduledTask(id: string): Promise { - const scheduledTask = await es.get({ - id: `task:${id}`, - index: '.kibana_task_manager', - }); - return scheduledTask._source!; - } - - async function searchScheduledTask(id: string) { - const searchResult = await es.search({ - index: '.kibana_task_manager', - body: { - query: { - bool: { - must: [ - { - term: { - 'task.id': `task:${id}`, - }, - }, - { - terms: { - 'task.scope': ['alerting'], - }, - }, - ], - }, - }, - }, - }); - - // @ts-expect-error - return searchResult.hits.total.value; - } } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/task_runner_with_actions.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/task_runner_with_actions.ts new file mode 100644 index 0000000000000..c6e8bb72015f0 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/task_runner_with_actions.ts @@ -0,0 +1,267 @@ +/* + * 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 moment from 'moment'; +import { ESTestIndexTool } from '@kbn/alerting-api-integration-helpers'; +import { asyncForEach } from '../../../../../../functional/services/transform/api'; +import { SuperuserAtSpace1 } from '../../../../scenarios'; +import { getUrlPrefix, ObjectRemover } from '../../../../../common/lib'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { + TEST_ACTIONS_INDEX, + indexTestDocs, + getSecurityRule, + testDocTimestamps, + waitForEventLogDocs, +} from './test_utils'; + +// eslint-disable-next-line import/no-default-export +export default function scheduleBackfillTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('es'); + const retry = getService('retry'); + const esTestIndexTool = new ESTestIndexTool(es, retry); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('ad hoc backfill with rule actions', () => { + const spaceId = SuperuserAtSpace1.space.id; + let backfillIds: string[] = []; + const objectRemover = new ObjectRemover(supertest); + let connectorId: string; + + beforeEach(async () => { + await esTestIndexTool.destroy(); + await esTestIndexTool.setup(); + + // Index documents + await indexTestDocs(es, esTestIndexTool); + + // create a connector + const cresponse = await supertest + .post(`${getUrlPrefix(spaceId)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .auth(SuperuserAtSpace1.user.username, SuperuserAtSpace1.user.password) + .send({ + name: 'An index connector', + connector_type_id: '.index', + config: { + index: TEST_ACTIONS_INDEX, + refresh: true, + }, + secrets: {}, + }) + .expect(200); + connectorId = cresponse.body.id; + objectRemover.add(spaceId, connectorId, 'connector', 'actions'); + }); + + afterEach(async () => { + await es.deleteByQuery({ + index: TEST_ACTIONS_INDEX, + query: { match_all: {} }, + conflicts: 'proceed', + }); + await asyncForEach(backfillIds, async (id: string) => { + await supertest + .delete(`${getUrlPrefix(spaceId)}/internal/alerting/rules/backfill/${id}`) + .set('kbn-xsrf', 'foo'); + }); + backfillIds = []; + await objectRemover.removeAll(); + await esTestIndexTool.destroy(); + }); + + it('should run summary actions for backfill jobs when run_actions=true', async () => { + // create a siem query rule with an action + const rresponse = await supertest + .post(`${getUrlPrefix(spaceId)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .auth(SuperuserAtSpace1.user.username, SuperuserAtSpace1.user.password) + .send( + getSecurityRule({ + actions: [ + { + group: 'default', + id: connectorId, + params: { documents: [{ alertUuid: '{{alert.uuid}}' }] }, + frequency: { notify_when: 'onActiveAlert', throttle: null, summary: true }, + }, + ], + }) + ) + .expect(200); + + const ruleId = rresponse.body.id; + objectRemover.add(spaceId, ruleId, 'rule', 'alerting'); + + const start = moment(testDocTimestamps[1]).utc().startOf('day').toISOString(); + const end = moment(testDocTimestamps[11]).utc().startOf('day').toISOString(); + + // schedule backfill for this rule + const response = await supertestWithoutAuth + .post(`${getUrlPrefix(spaceId)}/internal/alerting/rules/backfill/_schedule`) + .set('kbn-xsrf', 'foo') + .auth(SuperuserAtSpace1.user.username, SuperuserAtSpace1.user.password) + .send([{ rule_id: ruleId, start, end, run_actions: true }]) + .expect(200); + + const scheduleResult = response.body; + expect(scheduleResult.length).to.eql(1); + expect(scheduleResult[0].schedule.length).to.eql(4); + + const backfillId = scheduleResult[0].id; + + // wait for backfills to run + await waitForEventLogDocs( + retry, + getService, + backfillId, + spaceId, + new Map([['execute-backfill', { equal: 4 }]]) + ); + + retry.try(async () => { + // verify that the correct number of actions were executed + const actions = await es.search({ + index: TEST_ACTIONS_INDEX, + body: { query: { match_all: {} } }, + }); + + // 3 backfill executions resulted in alerts so 3 notifications should have + // been generated. + expect(actions.hits.hits.length).to.eql(3); + }); + }); + + it('should run per-alert actions for backfill jobs when run_actions=true', async () => { + // create a siem query rule with an action + const rresponse = await supertest + .post(`${getUrlPrefix(spaceId)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .auth(SuperuserAtSpace1.user.username, SuperuserAtSpace1.user.password) + .send( + getSecurityRule({ + actions: [ + { + group: 'default', + id: connectorId, + params: { documents: [{ alertUuid: '{{alert.uuid}}' }] }, + frequency: { notify_when: 'onActiveAlert', throttle: null, summary: false }, + }, + ], + }) + ) + .expect(200); + + const ruleId = rresponse.body.id; + objectRemover.add(spaceId, ruleId, 'rule', 'alerting'); + + const start = moment(testDocTimestamps[1]).utc().startOf('day').toISOString(); + const end = moment(testDocTimestamps[11]).utc().startOf('day').toISOString(); + + // schedule backfill for this rule + const response = await supertestWithoutAuth + .post(`${getUrlPrefix(spaceId)}/internal/alerting/rules/backfill/_schedule`) + .set('kbn-xsrf', 'foo') + .auth(SuperuserAtSpace1.user.username, SuperuserAtSpace1.user.password) + .send([{ rule_id: ruleId, start, end, run_actions: true }]) + .expect(200); + + const scheduleResult = response.body; + expect(scheduleResult.length).to.eql(1); + expect(scheduleResult[0].schedule.length).to.eql(4); + + const backfillId = scheduleResult[0].id; + + // wait for backfills to run + await waitForEventLogDocs( + retry, + getService, + backfillId, + spaceId, + new Map([['execute-backfill', { equal: 4 }]]) + ); + + retry.try(async () => { + // verify that the correct number of actions were executed + const actions = await es.search({ + index: TEST_ACTIONS_INDEX, + body: { query: { match_all: {} } }, + }); + + // 3 backfill executions resulted in 9 alerts so 9 notifications should have + // been generated. + expect(actions.hits.hits.length).to.eql(9); + }); + }); + + it('should not run actions for backfill jobs when run_actions=false', async () => { + // create a siem query rule with an action + const rresponse = await supertest + .post(`${getUrlPrefix(spaceId)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .auth(SuperuserAtSpace1.user.username, SuperuserAtSpace1.user.password) + .send( + getSecurityRule({ + actions: [ + { + group: 'default', + id: connectorId, + params: { documents: [{ alertUuid: '{{alert.uuid}}' }] }, + frequency: { notify_when: 'onActiveAlert', throttle: null, summary: true }, + }, + ], + }) + ) + .expect(200); + + const ruleId = rresponse.body.id; + objectRemover.add(spaceId, ruleId, 'rule', 'alerting'); + + const start = moment(testDocTimestamps[1]).utc().startOf('day').toISOString(); + const end = moment(testDocTimestamps[11]).utc().startOf('day').toISOString(); + + // schedule backfill for this rule + const response = await supertestWithoutAuth + .post(`${getUrlPrefix(spaceId)}/internal/alerting/rules/backfill/_schedule`) + .set('kbn-xsrf', 'foo') + .auth(SuperuserAtSpace1.user.username, SuperuserAtSpace1.user.password) + .send([{ rule_id: ruleId, start, end, run_actions: false }]) + .expect(200); + + const scheduleResult = response.body; + expect(scheduleResult.length).to.eql(1); + expect(scheduleResult[0].schedule.length).to.eql(4); + expect(scheduleResult[0].rule.actions).to.eql([]); + + const backfillId = scheduleResult[0].id; + + // wait for backfills to run + await waitForEventLogDocs( + retry, + getService, + backfillId, + spaceId, + new Map([['execute-backfill', { equal: 4 }]]) + ); + + // since we want to check that no actions were executed and they might take a bit to run + // add a small delay + await new Promise((resolve) => setTimeout(resolve, 5000)); + + // verify that the correct number of actions were executed + const actions = await es.search({ + index: TEST_ACTIONS_INDEX, + body: { query: { match_all: {} } }, + }); + + // no actions should be generated + expect(actions.hits.hits.length).to.eql(0); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/test_utils.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/test_utils.ts new file mode 100644 index 0000000000000..ad4fde6b39ec6 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/test_utils.ts @@ -0,0 +1,166 @@ +/* + * 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 { asyncForEach } from '@kbn/std'; +import { ESTestIndexTool, ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers'; +import type { Client } from '@elastic/elasticsearch'; +import moment from 'moment'; +import { FtrProviderContext, RetryService } from '@kbn/ftr-common-functional-services'; +import { AD_HOC_RUN_SAVED_OBJECT_TYPE } from '@kbn/alerting-plugin/server/saved_objects'; +import { ALERT_ORIGINAL_TIME } from '@kbn/security-solution-plugin/common/field_maps/field_names'; +import { SearchHit } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { TaskManagerDoc, getEventLog } from '../../../../../common/lib'; +import { + DOCUMENT_REFERENCE, + DOCUMENT_SOURCE, + createEsDocument, +} from '../../../../../spaces_only/tests/alerting/create_test_data'; + +export const TEST_ACTIONS_INDEX = 'alerting-backfill-test-data'; + +export const testDocTimestamps = [ + // before first backfill run + moment().utc().subtract(14, 'days').toISOString(), + + // backfill execution set 1 + moment().utc().startOf('day').subtract(13, 'days').add(10, 'minutes').toISOString(), + moment().utc().startOf('day').subtract(13, 'days').add(11, 'minutes').toISOString(), + moment().utc().startOf('day').subtract(13, 'days').add(12, 'minutes').toISOString(), + + // backfill execution set 2 + moment().utc().startOf('day').subtract(12, 'days').add(20, 'minutes').toISOString(), + + // backfill execution set 3 + moment().utc().startOf('day').subtract(11, 'days').add(30, 'minutes').toISOString(), + moment().utc().startOf('day').subtract(11, 'days').add(31, 'minutes').toISOString(), + moment().utc().startOf('day').subtract(11, 'days').add(32, 'minutes').toISOString(), + moment().utc().startOf('day').subtract(11, 'days').add(33, 'minutes').toISOString(), + moment().utc().startOf('day').subtract(11, 'days').add(34, 'minutes').toISOString(), + + // backfill execution set 4 purposely left empty + + // after last backfill + moment().utc().startOf('day').subtract(9, 'days').add(40, 'minutes').toISOString(), + moment().utc().startOf('day').subtract(9, 'days').add(41, 'minutes').toISOString(), +]; + +export async function indexTestDocs(es: Client, esTestIndexTool: ESTestIndexTool) { + await asyncForEach(testDocTimestamps, async (timestamp: string) => { + await createEsDocument(es, new Date(timestamp).valueOf(), 1, ES_TEST_INDEX_NAME); + }); + + await esTestIndexTool.waitForDocs(DOCUMENT_SOURCE, DOCUMENT_REFERENCE, testDocTimestamps.length); +} + +export async function waitForEventLogDocs( + retry: RetryService, + getService: FtrProviderContext['getService'], + id: string, + spaceId: string, + actions: Map +) { + return await retry.try(async () => { + return await getEventLog({ + getService, + spaceId, + type: AD_HOC_RUN_SAVED_OBJECT_TYPE, + id, + provider: 'alerting', + actions, + }); + }); +} + +export async function getScheduledTask(es: Client, id: string): Promise { + const scheduledTask = await es.get({ + id: `task:${id}`, + index: '.kibana_task_manager', + }); + return scheduledTask._source!; +} + +export async function queryForAlertDocs( + es: Client, + index: string +): Promise>> { + const searchResult = await es.search({ + index, + body: { + sort: [{ [ALERT_ORIGINAL_TIME]: { order: 'asc' } }], + query: { match_all: {} }, + }, + }); + return searchResult.hits.hits as Array>; +} + +export async function searchScheduledTask(es: Client, id: string) { + const searchResult = await es.search({ + index: '.kibana_task_manager', + body: { + query: { + bool: { + must: [ + { + term: { + 'task.id': `task:${id}`, + }, + }, + { + terms: { + 'task.scope': ['alerting'], + }, + }, + ], + }, + }, + }, + }); + + // @ts-expect-error + return searchResult.hits.total.value; +} + +export function getSecurityRule(overwrites = {}) { + return { + name: 'test siem query rule with actions', + rule_type_id: 'siem.queryRule', + consumer: 'siem', + enabled: true, + actions: [], + schedule: { interval: '24h' }, + params: { + author: [], + description: 'test', + falsePositives: [], + from: 'now-86460s', + ruleId: '31c54f10-9d3b-45a8-b064-b92e8c6fcbe7', + immutable: false, + license: '', + outputIndex: '', + meta: { from: '1m', kibana_siem_app_url: 'https://localhost:5601/app/security' }, + maxSignals: 20, + riskScore: 21, + riskScoreMapping: [], + severity: 'low', + severityMapping: [], + threat: [], + to: 'now', + references: [], + version: 1, + exceptionsList: [], + relatedIntegrations: [], + requiredFields: [], + setup: '', + type: 'query', + language: 'kuery', + index: [ES_TEST_INDEX_NAME], + query: `source:${DOCUMENT_SOURCE}`, + filters: [], + }, + ...overwrites, + }; +}