diff --git a/x-pack/plugins/alerting/common/index.ts b/x-pack/plugins/alerting/common/index.ts index e2e9e477cc4cc..e0a23e50ca222 100644 --- a/x-pack/plugins/alerting/common/index.ts +++ b/x-pack/plugins/alerting/common/index.ts @@ -38,6 +38,8 @@ export * from './rule_tags_aggregation'; export * from './iso_weekdays'; export * from './saved_objects/rules/mappings'; +export { isSystemAction } from './system_actions/is_system_action'; + export type { MaintenanceWindowModificationMetadata, DateRange, diff --git a/x-pack/plugins/alerting/common/rule.ts b/x-pack/plugins/alerting/common/rule.ts index 63bd8dd4f8f4a..065816c321aba 100644 --- a/x-pack/plugins/alerting/common/rule.ts +++ b/x-pack/plugins/alerting/common/rule.ts @@ -82,13 +82,13 @@ export interface RuleExecutionStatus { export type RuleActionParams = SavedObjectAttributes; export type RuleActionParam = SavedObjectAttribute; -export interface RuleActionFrequency extends SavedObjectAttributes { +export interface RuleActionFrequency { summary: boolean; notifyWhen: RuleNotifyWhenType; throttle: string | null; } -export interface AlertsFilterTimeframe extends SavedObjectAttributes { +export interface AlertsFilterTimeframe { days: IsoWeekday[]; timezone: string; hours: { @@ -97,7 +97,7 @@ export interface AlertsFilterTimeframe extends SavedObjectAttributes { }; } -export interface AlertsFilter extends SavedObjectAttributes { +export interface AlertsFilter { query?: { kql: string; filters: Filter[]; @@ -121,7 +121,7 @@ export const RuleActionTypes = { export type RuleActionTypes = typeof RuleActionTypes[keyof typeof RuleActionTypes]; -export interface RuleAction { +export interface RuleDefaultAction { uuid?: string; group: string; id: string; @@ -129,9 +129,19 @@ export interface RuleAction { params: RuleActionParams; frequency?: RuleActionFrequency; alertsFilter?: AlertsFilter; - type?: typeof RuleActionTypes.DEFAULT; + type: typeof RuleActionTypes.DEFAULT; } +export interface RuleSystemAction { + uuid?: string; + id: string; + actionTypeId: string; + params: RuleActionParams; + type: typeof RuleActionTypes.SYSTEM; +} + +export type RuleAction = RuleDefaultAction | RuleSystemAction; + export interface RuleLastRun { outcome: RuleLastRunOutcomes; outcomeOrder?: number; @@ -195,10 +205,12 @@ export interface SanitizedAlertsFilter extends AlertsFilter { timeframe?: AlertsFilterTimeframe; } -export type SanitizedRuleAction = Omit & { +export type SanitizedDefaultRuleAction = Omit & { alertsFilter?: SanitizedAlertsFilter; }; +export type SanitizedRuleAction = SanitizedDefaultRuleAction | RuleSystemAction; + export type SanitizedRule = Omit< Rule, 'apiKey' | 'actions' diff --git a/x-pack/plugins/alerting/common/system_actions/is_system_action.test.ts b/x-pack/plugins/alerting/common/system_actions/is_system_action.test.ts new file mode 100644 index 0000000000000..ee5c030c2c5dc --- /dev/null +++ b/x-pack/plugins/alerting/common/system_actions/is_system_action.test.ts @@ -0,0 +1,36 @@ +/* + * 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 { RuleSystemAction, RuleActionTypes, RuleDefaultAction } from '../rule'; +import { isSystemAction } from './is_system_action'; + +describe('isSystemAction', () => { + const defaultAction: RuleDefaultAction = { + actionTypeId: '.test', + uuid: '111', + group: 'default', + id: '1', + params: {}, + type: RuleActionTypes.DEFAULT, + }; + + const systemAction: RuleSystemAction = { + id: '1', + uuid: '123', + params: { 'not-exist': 'test' }, + actionTypeId: '.test', + type: RuleActionTypes.SYSTEM, + }; + + it('returns true if it is a system action', () => { + expect(isSystemAction(systemAction)).toBe(true); + }); + + it('returns false if it is not a system action', () => { + expect(isSystemAction(defaultAction)).toBe(false); + }); +}); diff --git a/x-pack/plugins/alerting/common/system_actions/is_system_action.ts b/x-pack/plugins/alerting/common/system_actions/is_system_action.ts new file mode 100644 index 0000000000000..ae9958b20b6b8 --- /dev/null +++ b/x-pack/plugins/alerting/common/system_actions/is_system_action.ts @@ -0,0 +1,17 @@ +/* + * 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 { AsApiContract } from '@kbn/actions-plugin/common'; +import { RuleAction, RuleSystemAction, RuleActionTypes } from '../rule'; + +type GetSystemActionType = T extends RuleAction + ? RuleSystemAction + : AsApiContract; + +export const isSystemAction = ( + action: RuleAction | AsApiContract +): action is GetSystemActionType => action.type === RuleActionTypes.SYSTEM; diff --git a/x-pack/plugins/alerting/server/data/rule/types/rule_attributes.ts b/x-pack/plugins/alerting/server/data/rule/types/rule_attributes.ts index aa8adda873cde..b0e2a58a72b21 100644 --- a/x-pack/plugins/alerting/server/data/rule/types/rule_attributes.ts +++ b/x-pack/plugins/alerting/server/data/rule/types/rule_attributes.ts @@ -125,7 +125,7 @@ interface AlertsFilterAttributes { export interface RuleActionAttributes { uuid: string; - group: string; + group?: string; actionRef: string; actionTypeId: string; params: SavedObjectAttributes; diff --git a/x-pack/plugins/alerting/server/index.ts b/x-pack/plugins/alerting/server/index.ts index 6aa1c44fe6e81..66d1f707bb623 100644 --- a/x-pack/plugins/alerting/server/index.ts +++ b/x-pack/plugins/alerting/server/index.ts @@ -68,6 +68,7 @@ export { isValidAlertIndexName, } from './alerts_service'; export { getDataStreamAdapter } from './alerts_service/lib/data_stream_adapter'; +export type { ConnectorAdapter } from './connector_adapters/types'; export const plugin = (initContext: PluginInitializerContext) => new AlertingPlugin(initContext); diff --git a/x-pack/plugins/alerting/server/rules_client/types.ts b/x-pack/plugins/alerting/server/rules_client/types.ts index 89dce3ed90451..14f85886bb698 100644 --- a/x-pack/plugins/alerting/server/rules_client/types.ts +++ b/x-pack/plugins/alerting/server/rules_client/types.ts @@ -21,6 +21,7 @@ import { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin import { TaskManagerStartContract } from '@kbn/task-manager-plugin/server'; import { IEventLogClient, IEventLogger } from '@kbn/event-log-plugin/server'; import { AuditLogger } from '@kbn/security-plugin/server'; +import { DistributiveOmit } from '@elastic/eui'; import { RegistryRuleType } from '../rule_type_registry'; import { RuleTypeRegistry, @@ -29,6 +30,8 @@ import { SanitizedRule, RuleSnoozeSchedule, RawRuleAlertsFilter, + RuleSystemAction, + RuleDefaultAction, } from '../types'; import { AlertingAuthorization } from '../authorization'; import { AlertingRulesConfig } from '../config'; @@ -80,18 +83,29 @@ export interface RulesClientContext { readonly connectorAdapterRegistry: ConnectorAdapterRegistry; readonly getAlertIndicesAlias: GetAlertIndicesAlias; readonly alertsService: AlertsService | null; + readonly isSystemAction: (actionId: string) => boolean; } -export type NormalizedAlertAction = Omit; +export type NormalizedAlertAction = DistributiveOmit; +export type NormalizedSystemAction = Omit; -export type NormalizedAlertActionWithGeneratedValues = Omit< - NormalizedAlertAction, - 'uuid' | 'alertsFilter' +export type NormalizedAlertDefaultActionWithGeneratedValues = Omit< + RuleDefaultAction, + 'uuid' | 'alertsFilter' | 'actionTypeId' > & { uuid: string; alertsFilter?: RawRuleAlertsFilter; }; +export type NormalizedAlertSystemActionWithGeneratedValues = Omit< + RuleSystemAction, + 'uuid' | 'actionTypeId' +> & { uuid: string }; + +export type NormalizedAlertActionWithGeneratedValues = + | NormalizedAlertDefaultActionWithGeneratedValues + | NormalizedAlertSystemActionWithGeneratedValues; + export interface RegistryAlertTypeWithAuth extends RegistryRuleType { authorizedConsumers: string[]; } diff --git a/x-pack/plugins/alerting/server/rules_client_conflict_retries.test.ts b/x-pack/plugins/alerting/server/rules_client_conflict_retries.test.ts index 62b6259afb43c..8bde9a52b3805 100644 --- a/x-pack/plugins/alerting/server/rules_client_conflict_retries.test.ts +++ b/x-pack/plugins/alerting/server/rules_client_conflict_retries.test.ts @@ -70,6 +70,7 @@ const rulesClientParams: jest.Mocked = { getAlertIndicesAlias: jest.fn(), alertsService: null, connectorAdapterRegistry: new ConnectorAdapterRegistry(), + isSystemAction: jest.fn(), }; // this suite consists of two suites running tests against mutable RulesClient APIs: diff --git a/x-pack/plugins/alerting/server/rules_client_factory.test.ts b/x-pack/plugins/alerting/server/rules_client_factory.test.ts index 31f06344d45f4..857a3e3de43aa 100644 --- a/x-pack/plugins/alerting/server/rules_client_factory.test.ts +++ b/x-pack/plugins/alerting/server/rules_client_factory.test.ts @@ -119,6 +119,7 @@ test('creates a rules client with proper constructor arguments when security is getAlertIndicesAlias: expect.any(Function), alertsService: null, connectorAdapterRegistry: expect.any(ConnectorAdapterRegistry), + isSystemAction: expect.any(Function), }); }); @@ -164,6 +165,7 @@ test('creates a rules client with proper constructor arguments', async () => { getAlertIndicesAlias: expect.any(Function), alertsService: null, connectorAdapterRegistry: expect.any(ConnectorAdapterRegistry), + isSystemAction: expect.any(Function), }); }); diff --git a/x-pack/plugins/alerting/server/rules_client_factory.ts b/x-pack/plugins/alerting/server/rules_client_factory.ts index 337d4daab903b..fba3d89f5abf4 100644 --- a/x-pack/plugins/alerting/server/rules_client_factory.ts +++ b/x-pack/plugins/alerting/server/rules_client_factory.ts @@ -185,6 +185,9 @@ export class RulesClientFactory { } return { apiKeysEnabled: false }; }, + isSystemAction(actionId: string) { + return actions.isSystemActionConnector(actionId); + }, }); } } diff --git a/x-pack/plugins/alerting/server/task_runner/execution_handler.ts b/x-pack/plugins/alerting/server/task_runner/execution_handler.ts index d12b3428e62eb..fde39f906485a 100644 --- a/x-pack/plugins/alerting/server/task_runner/execution_handler.ts +++ b/x-pack/plugins/alerting/server/task_runner/execution_handler.ts @@ -39,7 +39,8 @@ import { RuleTypeState, SanitizedRule, RuleAlertData, - RuleActionTypes, + RuleDefaultAction, + RuleSystemAction, RuleNotifyWhen, } from '../../common'; import { @@ -52,6 +53,7 @@ import { isSummaryActionThrottled, } from './rule_action_helper'; import { ConnectorAdapter } from '../connector_adapters/types'; +import { isSystemAction } from '../../common/system_actions/is_system_action'; enum Reasons { MUTED = 'muted', @@ -64,7 +66,7 @@ export interface RunResult { } interface RunSummarizedActionArgs { - action: RuleAction; + action: RuleDefaultAction; summarizedAlerts: CombinedSummarizedAlerts; spaceId: string; } @@ -75,14 +77,14 @@ interface RunActionArgs< ActionGroupIds extends string, RecoveryActionGroupId extends string > { - action: RuleAction; + action: RuleDefaultAction; alert: Alert; ruleId: string; spaceId: string; } interface RunSystemActionArgs { - action: RuleAction; + action: RuleSystemAction; connectorAdapter: ConnectorAdapter; summarizedAlerts: CombinedSummarizedAlerts; rule: SanitizedRule; @@ -615,7 +617,7 @@ export class ExecutionHandler< action, }: { alert: Alert; - action: RuleAction; + action: RuleDefaultAction; }) { const alertId = alert.getId(); const { rule, ruleLabel, logger } = this; @@ -943,15 +945,3 @@ export class ExecutionHandler< return bulkActions; } } - -/** - * TODO: Substitute with a function which takes into - * account system actions. - * - * Because RuleAction has the type set as RuleActionTypes.DEFAULT - * TS produce an error as the check below will always return false. - * We need the check to be able to test. - */ - -// @ts-expect-error -const isSystemAction = (action: RuleAction) => action.type === RuleActionTypes.SYSTEM; diff --git a/x-pack/plugins/alerting/server/task_runner/fixtures.ts b/x-pack/plugins/alerting/server/task_runner/fixtures.ts index 163b9415c7e5d..fb795f4777b34 100644 --- a/x-pack/plugins/alerting/server/task_runner/fixtures.ts +++ b/x-pack/plugins/alerting/server/task_runner/fixtures.ts @@ -15,6 +15,8 @@ import { RuleLastRunOutcomeOrderMap, RuleLastRunOutcomes, SanitizedRule, + SanitizedRuleAction, + RuleActionTypes, } from '../../common'; import { getDefaultMonitoring } from '../lib/monitoring'; import { UntypedNormalizedRuleType } from '../rule_type_registry'; @@ -43,6 +45,7 @@ export const RULE_ACTIONS = [ foo: true, }, uuid: '111-111', + type: RuleActionTypes.DEFAULT, }, { actionTypeId: 'action', @@ -52,6 +55,7 @@ export const RULE_ACTIONS = [ isResolved: true, }, uuid: '222-222', + type: RuleActionTypes.DEFAULT, }, ]; @@ -192,6 +196,7 @@ export const mockedRuleTypeSavedObject: Rule = { foo: true, }, uuid: '111-111', + type: RuleActionTypes.DEFAULT, }, { group: RecoveredActionGroup.id, @@ -201,6 +206,7 @@ export const mockedRuleTypeSavedObject: Rule = { isResolved: true, }, uuid: '222-222', + type: RuleActionTypes.DEFAULT, }, ], executionStatus: { @@ -283,7 +289,8 @@ export const mockedRule: SanitizedRule return { ...action, id: action.uuid, - }; + type: RuleActionTypes.DEFAULT, + } as SanitizedRuleAction; }), isSnoozedUntil: undefined, }; diff --git a/x-pack/plugins/alerting/server/task_runner/rule_action_helper.test.ts b/x-pack/plugins/alerting/server/task_runner/rule_action_helper.test.ts index 84fb89de01cbf..ca2dceb497841 100644 --- a/x-pack/plugins/alerting/server/task_runner/rule_action_helper.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/rule_action_helper.test.ts @@ -6,7 +6,7 @@ */ import { Logger } from '@kbn/logging'; -import { RuleAction, RuleActionTypes } from '../types'; +import { RuleAction, RuleActionTypes, RuleSystemAction } from '../types'; import { generateActionHash, getSummaryActionsFromTaskState, @@ -26,6 +26,7 @@ const mockOldAction: RuleAction = { actionTypeId: 'slack', params: {}, uuid: '123-456', + type: RuleActionTypes.DEFAULT, }; const mockAction: RuleAction = { @@ -39,6 +40,7 @@ const mockAction: RuleAction = { throttle: null, }, uuid: '123-456', + type: RuleActionTypes.DEFAULT, }; const mockSummaryAction: RuleAction = { @@ -52,11 +54,11 @@ const mockSummaryAction: RuleAction = { throttle: '1d', }, uuid: '111-111', + type: RuleActionTypes.DEFAULT, }; -const mockSystemAction = { +const mockSystemAction: RuleSystemAction = { id: '1', - group: 'default', actionTypeId: '.test', params: {}, uuid: '123-456', @@ -71,8 +73,6 @@ describe('rule_action_helper', () => { }); test('should return false if the action is a system action', () => { - // TODO: Remove when system actions are introduced in types - // @ts-expect-error: cannot accept system actions at the moment const result = isSummaryAction(mockSystemAction); expect(result).toBe(false); }); @@ -105,8 +105,6 @@ describe('rule_action_helper', () => { }); test('should return false if the action is a system action', () => { - // TODO: Remove when system actions are introduced in types - // @ts-expect-error: cannot accept system actions at the moment const result = isActionOnInterval(mockSystemAction); expect(result).toBe(false); }); @@ -150,8 +148,6 @@ describe('rule_action_helper', () => { }); test('should return a hash for system actions action', () => { - // TODO: Remove when system actions are introduced in types - // @ts-expect-error: cannot accept system actions at the moment const result = generateActionHash(mockSystemAction); expect(result).toBe('system-action:.test:summary'); }); @@ -186,8 +182,6 @@ describe('rule_action_helper', () => { test('should filtered out system actions', () => { const result = getSummaryActionsFromTaskState({ - // TODO: Remove when system actions are introduced in types - // @ts-expect-error: cannot accept system actions at the moment actions: [mockSummaryAction, mockSystemAction], summaryActions: { '111-111': { date: new Date('01.01.2020').toISOString() }, @@ -236,8 +230,6 @@ describe('rule_action_helper', () => { }); test('should return false if the action is a system action', () => { - // TODO: Remove when system actions are introduced in types - // @ts-expect-error: cannot accept system actions at the moment const result = isSummaryActionThrottled({ action: mockSystemAction, logger }); expect(result).toBe(false); }); @@ -347,8 +339,6 @@ describe('rule_action_helper', () => { }); test('should return false if the action is a system action', () => { - // TODO: Remove when system actions are introduced in types - // @ts-expect-error: cannot accept system actions at the moment const result = isSummaryActionOnInterval(mockSystemAction); expect(result).toBe(false); }); @@ -381,8 +371,6 @@ describe('rule_action_helper', () => { }); test('returns undefined start and end action is a system action', () => { - // TODO: Remove when system actions are introduced in types - // @ts-expect-error: cannot accept system actions at the moment expect(getSummaryActionTimeBounds(mockSystemAction, { interval: '1m' }, null)).toEqual({ start: undefined, end: undefined, diff --git a/x-pack/plugins/alerting/server/task_runner/rule_action_helper.ts b/x-pack/plugins/alerting/server/task_runner/rule_action_helper.ts index 158d5bbcbcf37..94d3486ec96aa 100644 --- a/x-pack/plugins/alerting/server/task_runner/rule_action_helper.ts +++ b/x-pack/plugins/alerting/server/task_runner/rule_action_helper.ts @@ -10,10 +10,11 @@ import { IntervalSchedule, parseDuration, RuleAction, + RuleDefaultAction, RuleNotifyWhenTypeValues, ThrottledActions, - RuleActionTypes, } from '../../common'; +import { isSystemAction } from '../../common/system_actions/is_system_action'; export const isSummaryAction = (action?: RuleAction) => { if (action != null && isSystemAction(action)) { @@ -108,7 +109,7 @@ export const getSummaryActionsFromTaskState = ({ summaryActions?: ThrottledActions; }) => { const actionsWithoutSystemActions = actions.filter( - (action): action is RuleAction => !isSystemAction(action) + (action): action is RuleDefaultAction => !isSystemAction(action) ); return Object.entries(summaryActions).reduce((newObj, [key, val]) => { @@ -154,15 +155,3 @@ export const getSummaryActionTimeBounds = ( return { start: startDate.valueOf(), end: now.valueOf() }; }; - -/** - * TODO: Substitute with a function which takes into - * account system actions. - * - * Because RuleAction has the type set as RuleActionTypes.DEFAULT - * TS produce an error as the check below will always return false. - * We need the check to be able to test. - */ - -// @ts-expect-error -const isSystemAction = (action: RuleAction) => action.type === RuleActionTypes.SYSTEM; diff --git a/x-pack/plugins/alerting/server/task_runner/rule_loader.ts b/x-pack/plugins/alerting/server/task_runner/rule_loader.ts index f6bb71aef7453..0218d24652155 100644 --- a/x-pack/plugins/alerting/server/task_runner/rule_loader.ts +++ b/x-pack/plugins/alerting/server/task_runner/rule_loader.ts @@ -25,8 +25,12 @@ import { import { MONITORING_HISTORY_LIMIT, RuleTypeParams } from '../../common'; import { AlertingEventLogger } from '../lib/alerting_event_logger/alerting_event_logger'; -export interface RuleData extends LoadedIndirectParams { - indirectParams: RawRule; +interface SerializableRawRule extends RawRule { + [key: string]: unknown; +} + +export interface RuleData + extends LoadedIndirectParams { rule: SanitizedRule; version: string | undefined; fakeRequest: CoreKibanaRequest; @@ -121,6 +125,7 @@ export async function getRuleAttributes( const fakeRequest = getFakeKibanaRequest(context, spaceId, rawRule.attributes.apiKey); const rulesClient = context.getRulesClientWithRequest(fakeRequest); + const rule = rulesClient.getAlertFromRaw({ id: ruleId, ruleTypeId: rawRule.attributes.alertTypeId as string, @@ -133,7 +138,7 @@ export async function getRuleAttributes( return { rule, version: rawRule.version, - indirectParams: rawRule.attributes, + indirectParams: rawRule.attributes as SerializableRawRule, fakeRequest, rulesClient, }; diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index fe50e6e3632b6..2ccfcd84400b7 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -17,6 +17,7 @@ import { Rule, RuleAction, RuleAlertData, + RuleActionTypes, } from '../types'; import { ConcreteTaskInstance, isUnrecoverableError } from '@kbn/task-manager-plugin/server'; import { TaskRunnerContext } from './task_runner_factory'; @@ -1525,6 +1526,7 @@ describe('Task Runner', () => { foo: true, }, uuid: '111-111', + type: RuleActionTypes.DEFAULT, }, { group: recoveryActionGroup.id, @@ -1534,6 +1536,7 @@ describe('Task Runner', () => { isResolved: true, }, uuid: '222-222', + type: RuleActionTypes.DEFAULT, }, ], }); @@ -1622,6 +1625,7 @@ describe('Task Runner', () => { foo: true, }, uuid: '111-111', + type: RuleActionTypes.DEFAULT, }, ], }); @@ -1685,6 +1689,7 @@ describe('Task Runner', () => { foo: true, }, uuid: '111-111', + type: RuleActionTypes.DEFAULT, }, ], }); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 939dfe9406f69..142bfc215a3b1 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -50,6 +50,7 @@ import { RuleAlertData, SanitizedRule, RuleNotifyWhen, + RuleActionTypes, } from '../../common'; import { NormalizedRuleType, UntypedNormalizedRuleType } from '../rule_type_registry'; import { getEsErrorMessage } from '../lib/errors'; @@ -560,7 +561,12 @@ export class TaskRunner< flappingSettings, notifyOnActionGroupChange: notifyWhen === RuleNotifyWhen.CHANGE || - some(actions, (action) => action.frequency?.notifyWhen === RuleNotifyWhen.CHANGE), + some( + actions, + (action) => + action.type !== RuleActionTypes.SYSTEM && + action.frequency?.notifyWhen === RuleNotifyWhen.CHANGE + ), maintenanceWindowIds, }); }); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts index 41140b5b25df7..f7528eaffe654 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts @@ -111,6 +111,7 @@ describe('Task Runner Cancel', () => { const uiSettingsService = uiSettingsServiceMock.createStartContract(); const dataPlugin = dataPluginMock.createStartContract(); const inMemoryMetrics = inMemoryMetricsMock.create(); + const connectorAdapterRegistry = new ConnectorAdapterRegistry(); type TaskRunnerFactoryInitializerParamsType = jest.Mocked & { actionsPlugin: jest.Mocked; @@ -151,7 +152,7 @@ describe('Task Runner Cancel', () => { getMaintenanceWindowClientWithRequest: jest .fn() .mockReturnValue(maintenanceWindowClientMock.create()), - connectorAdapterRegistry: new ConnectorAdapterRegistry(), + connectorAdapterRegistry, }; beforeEach(() => { diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 7fdd7668135a4..2a19e31381fcd 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -68,6 +68,7 @@ const enabledActionTypes = [ 'test.capped', 'test.system-action', 'test.system-action-kibana-privileges', + 'test.system-action-connector-adapter', ]; export function createTestConfig(name: string, options: CreateTestConfigOptions) { diff --git a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts index 3307211694cc0..fb6d84e81772f 100644 --- a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts +++ b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts @@ -6,7 +6,7 @@ */ import { ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers'; -import { AlertsFilter } from '@kbn/alerting-plugin/common/rule'; +import { AlertsFilter, RuleActionTypes } from '@kbn/alerting-plugin/common/rule'; import { Space, User } from '../types'; import { ObjectRemover } from './object_remover'; import { getUrlPrefix } from './space_test_utils'; @@ -351,6 +351,36 @@ export class AlertUtils { return response; } + public async createAlwaysFiringSystemAction({ + objectRemover, + overwrites = {}, + reference, + }: CreateAlertWithActionOpts) { + const objRemover = objectRemover || this.objectRemover; + + if (!objRemover) { + throw new Error('objectRemover is required'); + } + + let request = this.supertestWithoutAuth + .post(`${getUrlPrefix(this.space.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo'); + + if (this.user) { + request = request.auth(this.user.username, this.user.password); + } + + const rule = getAlwaysFiringRuleWithSystemAction(reference); + + const response = await request.send({ ...rule, ...overwrites }); + + if (response.statusCode === 200) { + objRemover.add(this.space.id, response.body.id, 'rule', 'alerting'); + } + + return response; + } + public async updateAlwaysFiringAction({ alertId, actionId, @@ -655,3 +685,35 @@ function getPatternFiringRuleWithSummaryAction( ], }; } + +function getAlwaysFiringRuleWithSystemAction(reference: string) { + return { + enabled: true, + name: 'abc', + schedule: { interval: '1m' }, + tags: ['tag-A', 'tag-B'], + rule_type_id: 'test.always-firing-alert-as-data', + consumer: 'alertsFixture', + params: { + index: ES_TEST_INDEX_NAME, + reference, + }, + actions: [ + { + id: 'system-connector-test.system-action-connector-adapter', + actionTypeId: 'test.system-action-connector-adapter', + uuid: '123', + /** + * The injected param required by the action will be set by the corresponding + * connector adapter. Setting it here it will lead to a 400 error by the + * rules API as only the connector adapter can set the injected property. + * + * Adapter: x-pack/test/alerting_api_integration/common/plugins/alerts/server/connector_adapters.ts + * Connector type: x-pack/test/alerting_api_integration/common/plugins/alerts/server/action_types.ts + */ + params: { myParam: 'param from rule action', index: ES_TEST_INDEX_NAME, reference }, + type: RuleActionTypes.SYSTEM, + }, + ], + }; +} diff --git a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/action_types.ts b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/action_types.ts index a7d5dbc138ea4..f2753cb99a24a 100644 --- a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/action_types.ts +++ b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/action_types.ts @@ -80,6 +80,7 @@ export function defineActionTypes( */ actions.registerType(getSystemActionType()); actions.registerType(getSystemActionTypeWithKibanaPrivileges()); + actions.registerType(getSystemActionTypeWithConnectorAdapter()); /** Sub action framework */ @@ -484,3 +485,67 @@ function getSystemActionTypeWithKibanaPrivileges() { return result; } + +function getSystemActionTypeWithConnectorAdapter() { + const result: ActionType< + {}, + {}, + { myParam: string; injected: string; index?: string; reference?: string } + > = { + id: 'test.system-action-connector-adapter', + name: 'Test system action with a connector adapter set', + minimumLicenseRequired: 'platinum', + supportedFeatureIds: ['alerting'], + validate: { + params: { + /** + * The injected params will be set by the + * connector adapter while executing the action. + * + * Adapter: x-pack/test/alerting_api_integration/common/plugins/alerts/server/connector_adapters.ts + */ + schema: schema.object({ + myParam: schema.string(), + injected: schema.string(), + index: schema.maybe(schema.string()), + reference: schema.maybe(schema.string()), + }), + }, + + config: { + schema: schema.any(), + }, + secrets: { + schema: schema.any(), + }, + }, + isSystemActionType: true, + /** + * The executor writes a doc to the + * testing index. The test uses the doc + * to verify that the action is executed + * correctly + */ + async executor({ params, services, actionId }) { + const { index, reference } = params; + + if (index == null || reference == null) { + return { status: 'ok', actionId }; + } + + await services.scopedClusterClient.index({ + index, + refresh: 'wait_for', + body: { + params, + reference, + source: 'action:test.system-action-connector-adapter', + }, + }); + + return { status: 'ok', actionId }; + }, + }; + + return result; +} diff --git a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/connector_adapters.ts b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/connector_adapters.ts new file mode 100644 index 0000000000000..41526e0949de3 --- /dev/null +++ b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/connector_adapters.ts @@ -0,0 +1,37 @@ +/* + * 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 { ConnectorAdapter } from '@kbn/alerting-plugin/server'; +import { CoreSetup } from '@kbn/core/server'; +import { schema } from '@kbn/config-schema'; +import { FixtureStartDeps, FixtureSetupDeps } from './plugin'; + +export function defineConnectorAdapters( + core: CoreSetup, + { alerting }: Pick +) { + const systemActionConnectorAdapter: ConnectorAdapter = { + connectorTypeId: 'test.system-action-connector-adapter', + ruleActionParamsSchema: schema.object({ + myParam: schema.string(), + index: schema.maybe(schema.string()), + reference: schema.maybe(schema.string()), + }), + /** + * The connector adapter will inject a new param property which is required + * by the action. The injected value cannot be set in the actions of the rule + * as the schema validation will thrown an error. Only through the connector + * adapter this value can be set. The tests are using the connector adapter test + * that the new property is injected correctly + */ + buildActionParams: ({ alerts, rule, params, spaceId, ruleUrl }) => { + return { ...params, injected: 'param from connector adapter' }; + }, + }; + + alerting.registerConnectorAdapter(systemActionConnectorAdapter); +} diff --git a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/plugin.ts b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/plugin.ts index 7a257d214f26a..c24a15682649f 100644 --- a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/plugin.ts @@ -27,6 +27,7 @@ import { NotificationsPluginStart } from '@kbn/notifications-plugin/server'; import { defineRoutes } from './routes'; import { defineActionTypes } from './action_types'; import { defineAlertTypes } from './alert_types'; +import { defineConnectorAdapters } from './connector_adapters'; export interface FixtureSetupDeps { features: FeaturesPluginSetup; @@ -162,6 +163,7 @@ export class FixturePlugin implements Plugin objectRemover.removeAll()); + after(async () => { await esTestIndexTool.destroy(); await es.indices.delete({ index: authorizationIndex }); @@ -1866,6 +1868,76 @@ instanceStateValue: true }); }); } + + describe('connector adapters', () => { + const space = SuperuserAtSpace1.space; + + const connectorId = 'system-connector-test.system-action-connector-adapter'; + const name = 'System action: test.system-action-connector-adapter'; + + it('should use connector adapters correctly on system actions', async () => { + const alertUtils = new AlertUtils({ + supertestWithoutAuth, + objectRemover, + space, + user: SuperuserAtSpace1.user, + }); + + const startDate = new Date().toISOString(); + const reference = alertUtils.generateReference(); + /** + * Creates a rule that always fire with a system action + * that has configured a connector adapter. + * + * System action: x-pack/test/alerting_api_integration/common/plugins/alerts/server/action_types.ts + * Adapter: x-pack/test/alerting_api_integration/common/plugins/alerts/server/connector_adapters.ts + */ + const response = await alertUtils.createAlwaysFiringSystemAction({ + reference, + overwrites: { schedule: { interval: '1s' } }, + }); + + expect(response.status).to.eql(200); + + await validateSystemActionEventLog({ + spaceId: space.id, + connectorId, + outcome: 'success', + message: `action executed: test.system-action-connector-adapter:${connectorId}: ${name}`, + startDate, + }); + + /** + * The executor function of the system action + * writes the params in the test index. We + * get the doc to verify that the connector adapter + * injected the param correctly. + */ + await esTestIndexTool.waitForDocs( + 'action:test.system-action-connector-adapter', + reference, + 1 + ); + + const docs = await esTestIndexTool.search( + 'action:test.system-action-connector-adapter', + reference + ); + + const doc = docs.body.hits.hits[0]._source as { params: Record }; + + expect(doc.params).to.eql({ + myParam: 'param from rule action', + index: '.kibana-alerting-test-data', + reference: 'alert-utils-ref:1:superuser', + /** + * Param was injected by the connector adapter in + * x-pack/test/alerting_api_integration/common/plugins/alerts/server/connector_adapters.ts + */ + injected: 'param from connector adapter', + }); + }); + }); }); interface ValidateEventLogParams { @@ -1970,4 +2042,46 @@ instanceStateValue: true expect(event?.error?.message).to.eql(errorMessage); } } + + interface ValidateSystemActionEventLogParams { + spaceId: string; + connectorId: string; + outcome: string; + message: string; + startDate: string; + errorMessage?: string; + } + + const validateSystemActionEventLog = async ( + params: ValidateSystemActionEventLogParams + ): Promise => { + const { spaceId, connectorId, outcome, message, startDate, errorMessage } = params; + + const events: IValidatedEvent[] = await retry.try(async () => { + const events_ = await getEventLog({ + getService, + spaceId, + type: 'action', + id: connectorId, + provider: 'actions', + actions: new Map([['execute', { gte: 1 }]]), + }); + + const filteredEvents = events_.filter((event) => event!['@timestamp']! >= startDate); + if (filteredEvents.length < 1) throw new Error('no recent events found yet'); + + return filteredEvents; + }); + + expect(events.length).to.be(1); + + const event = events[0]; + + expect(event?.message).to.eql(message); + expect(event?.event?.outcome).to.eql(outcome); + + if (errorMessage) { + expect(event?.error?.message).to.eql(errorMessage); + } + }; }