diff --git a/x-pack/plugins/alerting/common/routes/r_rule/index.ts b/x-pack/plugins/alerting/common/routes/r_rule/index.ts new file mode 100644 index 0000000000000..43de19735a40c --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/r_rule/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { rRuleSchema } from './schemas/latest'; +export type { RRule } from './types/latest'; + +export { rRuleSchema as rRuleSchemaV1 } from './schemas/v1'; +export type { RRule as RRuleV1 } from './types/latest'; diff --git a/x-pack/plugins/alerting/common/routes/rule/create/schemas/latest.ts b/x-pack/plugins/alerting/common/routes/r_rule/schemas/latest.ts similarity index 100% rename from x-pack/plugins/alerting/common/routes/rule/create/schemas/latest.ts rename to x-pack/plugins/alerting/common/routes/r_rule/schemas/latest.ts diff --git a/x-pack/plugins/alerting/common/routes/r_rule/schemas/v1.ts b/x-pack/plugins/alerting/common/routes/r_rule/schemas/v1.ts new file mode 100644 index 0000000000000..5e10629e4f7ee --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/r_rule/schemas/v1.ts @@ -0,0 +1,51 @@ +/* + * 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 { + validateStartDateV1, + validateEndDateV1, + createValidateRecurrenceByV1, +} from '../validation'; + +export const rRuleSchema = schema.object({ + dtstart: schema.string({ validate: validateStartDateV1 }), + tzid: schema.string(), + freq: schema.maybe( + schema.oneOf([schema.literal(0), schema.literal(1), schema.literal(2), schema.literal(3)]) + ), + interval: schema.maybe( + schema.number({ + validate: (interval: number) => { + if (interval < 1) return 'rRule interval must be > 0'; + }, + }) + ), + until: schema.maybe(schema.string({ validate: validateEndDateV1 })), + count: schema.maybe( + schema.number({ + validate: (count: number) => { + if (count < 1) return 'rRule count must be > 0'; + }, + }) + ), + byweekday: schema.maybe( + schema.arrayOf(schema.string(), { + validate: createValidateRecurrenceByV1('byweekday'), + }) + ), + bymonthday: schema.maybe( + schema.arrayOf(schema.number(), { + validate: createValidateRecurrenceByV1('bymonthday'), + }) + ), + bymonth: schema.maybe( + schema.arrayOf(schema.number(), { + validate: createValidateRecurrenceByV1('bymonth'), + }) + ), +}); diff --git a/x-pack/plugins/alerting/common/routes/rule/create/types/latest.ts b/x-pack/plugins/alerting/common/routes/r_rule/types/latest.ts similarity index 100% rename from x-pack/plugins/alerting/common/routes/rule/create/types/latest.ts rename to x-pack/plugins/alerting/common/routes/r_rule/types/latest.ts diff --git a/x-pack/plugins/alerting/common/routes/r_rule/types/v1.ts b/x-pack/plugins/alerting/common/routes/r_rule/types/v1.ts new file mode 100644 index 0000000000000..a8a2cf0f65e04 --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/r_rule/types/v1.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { TypeOf } from '@kbn/config-schema'; +import { rRuleSchemaV1 } from '..'; + +export type RRule = TypeOf; diff --git a/x-pack/plugins/alerting/common/routes/r_rule/validation/index.ts b/x-pack/plugins/alerting/common/routes/r_rule/validation/index.ts new file mode 100644 index 0000000000000..fc98963e03189 --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/r_rule/validation/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { validateStartDate } from './validate_start_date/latest'; +export { validateEndDate } from './validate_end_date/latest'; +export { createValidateRecurrenceBy } from './validate_recurrence_by/latest'; + +export { validateStartDate as validateStartDateV1 } from './validate_start_date/v1'; +export { validateEndDate as validateEndDateV1 } from './validate_end_date/v1'; +export { createValidateRecurrenceBy as createValidateRecurrenceByV1 } from './validate_recurrence_by/v1'; diff --git a/x-pack/plugins/alerting/common/routes/rule/rule_response/constants/latest.ts b/x-pack/plugins/alerting/common/routes/r_rule/validation/validate_end_date/latest.ts similarity index 100% rename from x-pack/plugins/alerting/common/routes/rule/rule_response/constants/latest.ts rename to x-pack/plugins/alerting/common/routes/r_rule/validation/validate_end_date/latest.ts diff --git a/x-pack/plugins/alerting/common/routes/r_rule/validation/validate_end_date/v1.ts b/x-pack/plugins/alerting/common/routes/r_rule/validation/validate_end_date/v1.ts new file mode 100644 index 0000000000000..895c1aeea4dfb --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/r_rule/validation/validate_end_date/v1.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. + */ + +export const validateEndDate = (date: string) => { + const parsedValue = Date.parse(date); + if (isNaN(parsedValue)) return `Invalid date: ${date}`; + if (parsedValue <= Date.now()) return `Invalid snooze date as it is in the past: ${date}`; + return; +}; diff --git a/x-pack/plugins/alerting/common/routes/rule/rule_response/schemas/latest.ts b/x-pack/plugins/alerting/common/routes/r_rule/validation/validate_recurrence_by/latest.ts similarity index 100% rename from x-pack/plugins/alerting/common/routes/rule/rule_response/schemas/latest.ts rename to x-pack/plugins/alerting/common/routes/r_rule/validation/validate_recurrence_by/latest.ts diff --git a/x-pack/plugins/alerting/common/routes/r_rule/validation/validate_recurrence_by/v1.ts b/x-pack/plugins/alerting/common/routes/r_rule/validation/validate_recurrence_by/v1.ts new file mode 100644 index 0000000000000..0ab609355c680 --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/r_rule/validation/validate_recurrence_by/v1.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const validateRecurrenceBy = (name: string, array: T[]) => { + if (array.length === 0) { + return `rRule ${name} cannot be empty`; + } +}; + +export const createValidateRecurrenceBy = (name: string) => { + return (array: T[]) => validateRecurrenceBy(name, array); +}; diff --git a/x-pack/plugins/alerting/common/routes/rule/rule_response/types/latest.ts b/x-pack/plugins/alerting/common/routes/r_rule/validation/validate_start_date/latest.ts similarity index 100% rename from x-pack/plugins/alerting/common/routes/rule/rule_response/types/latest.ts rename to x-pack/plugins/alerting/common/routes/r_rule/validation/validate_start_date/latest.ts diff --git a/x-pack/plugins/alerting/common/routes/r_rule/validation/validate_start_date/v1.ts b/x-pack/plugins/alerting/common/routes/r_rule/validation/validate_start_date/v1.ts new file mode 100644 index 0000000000000..3cbc0da0af1b5 --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/r_rule/validation/validate_start_date/v1.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const validateStartDate = (date: string) => { + const parsedValue = Date.parse(date); + if (isNaN(parsedValue)) return `Invalid date: ${date}`; + return; +}; diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/bulk_edit/index.ts b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_edit/index.ts new file mode 100644 index 0000000000000..1ae4a238cbf21 --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_edit/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { + ruleSnoozeScheduleSchema, + bulkEditOperationsSchema, + bulkEditRulesRequestBodySchema, +} from './schemas/latest'; +export type { + RuleSnoozeSchedule, + BulkEditRulesRequestBody, + BulkEditRulesResponse, +} from './types/latest'; + +export { + ruleSnoozeScheduleSchema as ruleSnoozeScheduleSchemaV1, + bulkEditOperationsSchema as bulkEditOperationsSchemaV1, + bulkEditRulesRequestBodySchema as bulkEditRulesRequestBodySchemaV1, +} from './schemas/v1'; +export type { + RuleSnoozeSchedule as RuleSnoozeScheduleV1, + BulkEditRulesRequestBody as BulkEditRulesRequestBodyV1, + BulkEditRulesResponse as BulkEditRulesResponseV1, +} from './types/v1'; diff --git a/x-pack/plugins/alerting/server/routes/rule/create/transforms/transform_create_body/latest.ts b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_edit/schemas/latest.ts similarity index 100% rename from x-pack/plugins/alerting/server/routes/rule/create/transforms/transform_create_body/latest.ts rename to x-pack/plugins/alerting/common/routes/rule/apis/bulk_edit/schemas/latest.ts diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/bulk_edit/schemas/v1.ts b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_edit/schemas/v1.ts new file mode 100644 index 0000000000000..adbebc679c218 --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_edit/schemas/v1.ts @@ -0,0 +1,107 @@ +/* + * 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 { validateDurationV1, validateNotifyWhenV1 } from '../../../validation'; +import { validateSnoozeScheduleV1 } from '../validation'; +import { rRuleSchemaV1 } from '../../../../r_rule'; +import { ruleNotifyWhenV1 } from '../../../response'; + +const notifyWhenSchema = schema.oneOf( + [ + schema.literal(ruleNotifyWhenV1.CHANGE), + schema.literal(ruleNotifyWhenV1.ACTIVE), + schema.literal(ruleNotifyWhenV1.THROTTLE), + ], + { validate: validateNotifyWhenV1 } +); + +export const scheduleIdsSchema = schema.maybe(schema.arrayOf(schema.string())); + +export const ruleSnoozeScheduleSchema = schema.object({ + id: schema.maybe(schema.string()), + duration: schema.number(), + rRule: rRuleSchemaV1, +}); + +const ruleSnoozeScheduleSchemaWithValidation = schema.object( + { + id: schema.maybe(schema.string()), + duration: schema.number(), + rRule: rRuleSchemaV1, + }, + { validate: validateSnoozeScheduleV1 } +); + +const ruleActionSchema = schema.object({ + group: schema.string(), + id: schema.string(), + params: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), + uuid: schema.maybe(schema.string()), + frequency: schema.maybe( + schema.object({ + summary: schema.boolean(), + throttle: schema.nullable(schema.string()), + notifyWhen: notifyWhenSchema, + }) + ), +}); + +export const bulkEditOperationsSchema = schema.arrayOf( + schema.oneOf([ + schema.object({ + operation: schema.oneOf([ + schema.literal('add'), + schema.literal('delete'), + schema.literal('set'), + ]), + field: schema.literal('tags'), + value: schema.arrayOf(schema.string()), + }), + schema.object({ + operation: schema.oneOf([schema.literal('add'), schema.literal('set')]), + field: schema.literal('actions'), + value: schema.arrayOf(ruleActionSchema), + }), + schema.object({ + operation: schema.literal('set'), + field: schema.literal('schedule'), + value: schema.object({ interval: schema.string({ validate: validateDurationV1 }) }), + }), + schema.object({ + operation: schema.literal('set'), + field: schema.literal('throttle'), + value: schema.nullable(schema.string()), + }), + schema.object({ + operation: schema.literal('set'), + field: schema.literal('notifyWhen'), + value: notifyWhenSchema, + }), + schema.object({ + operation: schema.oneOf([schema.literal('set')]), + field: schema.literal('snoozeSchedule'), + value: ruleSnoozeScheduleSchemaWithValidation, + }), + schema.object({ + operation: schema.oneOf([schema.literal('delete')]), + field: schema.literal('snoozeSchedule'), + value: schema.maybe(scheduleIdsSchema), + }), + schema.object({ + operation: schema.literal('set'), + field: schema.literal('apiKey'), + }), + ]), + { minSize: 1 } +); + +export const bulkEditRulesRequestBodySchema = schema.object({ + filter: schema.maybe(schema.string()), + ids: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })), + operations: bulkEditOperationsSchema, +}); diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/bulk_edit/types/latest.ts b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_edit/types/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_edit/types/latest.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './v1'; diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/bulk_edit/types/v1.ts b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_edit/types/v1.ts new file mode 100644 index 0000000000000..8f98d1c140746 --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_edit/types/v1.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 type { TypeOf } from '@kbn/config-schema'; +import { RuleParamsV1, RuleResponseV1 } from '../../../response'; +import { ruleSnoozeScheduleSchemaV1, bulkEditRulesRequestBodySchemaV1 } from '..'; + +export type RuleSnoozeSchedule = TypeOf; +export type BulkEditRulesRequestBody = TypeOf; + +interface BulkEditActionSkippedResult { + id: RuleResponseV1['id']; + name?: RuleResponseV1['name']; + skip_reason: 'RULE_NOT_MODIFIED'; +} + +interface BulkEditOperationError { + message: string; + status?: number; + rule: { + id: string; + name: string; + }; +} + +export interface BulkEditRulesResponse { + body: { + rules: Array>; + skipped: BulkEditActionSkippedResult[]; + errors: BulkEditOperationError[]; + total: number; + }; +} diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/bulk_edit/validation/index.ts b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_edit/validation/index.ts new file mode 100644 index 0000000000000..c11ce4f73f42c --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_edit/validation/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { validateSnoozeSchedule } from './validate_snooze_schedule/latest'; + +export { validateSnoozeSchedule as validateSnoozeScheduleV1 } from './validate_snooze_schedule/v1'; diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/bulk_edit/validation/validate_snooze_schedule/latest.ts b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_edit/validation/validate_snooze_schedule/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_edit/validation/validate_snooze_schedule/latest.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './v1'; diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/bulk_edit/validation/validate_snooze_schedule/v1.ts b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_edit/validation/validate_snooze_schedule/v1.ts new file mode 100644 index 0000000000000..65460110cd107 --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_edit/validation/validate_snooze_schedule/v1.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Frequency } from '@kbn/rrule'; +import moment from 'moment'; +import { RuleSnoozeScheduleV1 } from '../..'; + +export const validateSnoozeSchedule = (schedule: RuleSnoozeScheduleV1) => { + const intervalIsDaily = schedule.rRule.freq === Frequency.DAILY; + const durationInDays = moment.duration(schedule.duration, 'milliseconds').asDays(); + if (intervalIsDaily && schedule.rRule.interval && durationInDays >= schedule.rRule.interval) { + return 'Recurrence interval must be longer than the snooze duration'; + } +}; diff --git a/x-pack/plugins/alerting/common/routes/rule/create/index.ts b/x-pack/plugins/alerting/common/routes/rule/apis/create/index.ts similarity index 100% rename from x-pack/plugins/alerting/common/routes/rule/create/index.ts rename to x-pack/plugins/alerting/common/routes/rule/apis/create/index.ts diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/create/schemas/latest.ts b/x-pack/plugins/alerting/common/routes/rule/apis/create/schemas/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/create/schemas/latest.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './v1'; diff --git a/x-pack/plugins/alerting/common/routes/rule/create/schemas/v1.ts b/x-pack/plugins/alerting/common/routes/rule/apis/create/schemas/v1.ts similarity index 97% rename from x-pack/plugins/alerting/common/routes/rule/create/schemas/v1.ts rename to x-pack/plugins/alerting/common/routes/rule/apis/create/schemas/v1.ts index 2fa5f7660461b..98d82abf62be4 100644 --- a/x-pack/plugins/alerting/common/routes/rule/create/schemas/v1.ts +++ b/x-pack/plugins/alerting/common/routes/rule/apis/create/schemas/v1.ts @@ -6,13 +6,13 @@ */ import { schema } from '@kbn/config-schema'; -import { ruleNotifyWhenV1 } from '../../rule_response'; +import { ruleNotifyWhenV1 } from '../../../response'; import { validateNotifyWhenV1, validateDurationV1, validateHoursV1, validateTimezoneV1, -} from '../../validation'; +} from '../../../validation'; export const notifyWhenSchema = schema.oneOf( [ diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/create/types/latest.ts b/x-pack/plugins/alerting/common/routes/rule/apis/create/types/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/create/types/latest.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './v1'; diff --git a/x-pack/plugins/alerting/common/routes/rule/create/types/v1.ts b/x-pack/plugins/alerting/common/routes/rule/apis/create/types/v1.ts similarity index 83% rename from x-pack/plugins/alerting/common/routes/rule/create/types/v1.ts rename to x-pack/plugins/alerting/common/routes/rule/apis/create/types/v1.ts index d97af8bb6f69f..466f5d61eac46 100644 --- a/x-pack/plugins/alerting/common/routes/rule/create/types/v1.ts +++ b/x-pack/plugins/alerting/common/routes/rule/apis/create/types/v1.ts @@ -5,12 +5,12 @@ * 2.0. */ import type { TypeOf } from '@kbn/config-schema'; -import { RuleParamsV1, RuleResponseV1 } from '../../rule_response'; +import { RuleParamsV1, RuleResponseV1 } from '../../../response'; import { - actionSchema as actionSchemaV1, - actionFrequencySchema as actionFrequencySchemaV1, - createParamsSchema as createParamsSchemaV1, - createBodySchema as createBodySchemaV1, + actionSchemaV1, + actionFrequencySchemaV1, + createParamsSchemaV1, + createBodySchemaV1, } from '..'; export type CreateRuleAction = TypeOf; diff --git a/x-pack/plugins/alerting/common/routes/rule/response/constants/latest.ts b/x-pack/plugins/alerting/common/routes/rule/response/constants/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/response/constants/latest.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './v1'; diff --git a/x-pack/plugins/alerting/common/routes/rule/rule_response/constants/v1.ts b/x-pack/plugins/alerting/common/routes/rule/response/constants/v1.ts similarity index 100% rename from x-pack/plugins/alerting/common/routes/rule/rule_response/constants/v1.ts rename to x-pack/plugins/alerting/common/routes/rule/response/constants/v1.ts diff --git a/x-pack/plugins/alerting/common/routes/rule/rule_response/index.ts b/x-pack/plugins/alerting/common/routes/rule/response/index.ts similarity index 85% rename from x-pack/plugins/alerting/common/routes/rule/rule_response/index.ts rename to x-pack/plugins/alerting/common/routes/rule/response/index.ts index 451266e3a3483..1b7180910ca0a 100644 --- a/x-pack/plugins/alerting/common/routes/rule/rule_response/index.ts +++ b/x-pack/plugins/alerting/common/routes/rule/response/index.ts @@ -14,9 +14,10 @@ export { monitoringSchema, rRuleSchema, ruleResponseSchema, + ruleSnoozeScheduleSchema, } from './schemas/latest'; -export type { RuleParams, RuleResponse } from './types/latest'; +export type { RuleParams, RuleResponse, RuleSnoozeSchedule } from './types/latest'; export { ruleNotifyWhen, @@ -43,6 +44,7 @@ export { monitoringSchema as monitoringSchemaV1, rRuleSchema as rRuleSchemaV1, ruleResponseSchema as ruleResponseSchemaV1, + ruleSnoozeScheduleSchema as ruleSnoozeScheduleSchemaV1, } from './schemas/v1'; export { @@ -61,4 +63,8 @@ export type { RuleExecutionStatusWarningReason as RuleExecutionStatusWarningReasonV1, } from './constants/v1'; -export type { RuleParams as RuleParamsV1, RuleResponse as RuleResponseV1 } from './types/v1'; +export type { + RuleParams as RuleParamsV1, + RuleResponse as RuleResponseV1, + RuleSnoozeSchedule as RuleSnoozeScheduleV1, +} from './types/v1'; diff --git a/x-pack/plugins/alerting/common/routes/rule/response/schemas/latest.ts b/x-pack/plugins/alerting/common/routes/rule/response/schemas/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/response/schemas/latest.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './v1'; diff --git a/x-pack/plugins/alerting/common/routes/rule/rule_response/schemas/v1.ts b/x-pack/plugins/alerting/common/routes/rule/response/schemas/v1.ts similarity index 98% rename from x-pack/plugins/alerting/common/routes/rule/rule_response/schemas/v1.ts rename to x-pack/plugins/alerting/common/routes/rule/response/schemas/v1.ts index 8dbc37ea6aaad..3aecec56c3a41 100644 --- a/x-pack/plugins/alerting/common/routes/rule/rule_response/schemas/v1.ts +++ b/x-pack/plugins/alerting/common/routes/rule/response/schemas/v1.ts @@ -217,7 +217,7 @@ export const rRuleSchema = schema.object({ bysecond: schema.arrayOf(schema.number()), }); -const snoozeScheduleSchema = schema.object({ +export const ruleSnoozeScheduleSchema = schema.object({ duration: schema.number(), rRule: rRuleSchema, id: schema.maybe(schema.string()), @@ -248,7 +248,7 @@ export const ruleResponseSchema = schema.object({ muted_alert_ids: schema.arrayOf(schema.string()), execution_status: ruleExecutionStatusSchema, monitoring: schema.maybe(monitoringSchema), - snooze_schedule: schema.maybe(schema.arrayOf(snoozeScheduleSchema)), + snooze_schedule: schema.maybe(schema.arrayOf(ruleSnoozeScheduleSchema)), active_snoozes: schema.maybe(schema.arrayOf(schema.string())), is_snoozed_until: schema.maybe(schema.nullable(schema.string())), last_run: schema.maybe(schema.nullable(ruleLastRunSchema)), diff --git a/x-pack/plugins/alerting/common/routes/rule/response/types/latest.ts b/x-pack/plugins/alerting/common/routes/rule/response/types/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/response/types/latest.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './v1'; diff --git a/x-pack/plugins/alerting/common/routes/rule/rule_response/types/v1.ts b/x-pack/plugins/alerting/common/routes/rule/response/types/v1.ts similarity index 92% rename from x-pack/plugins/alerting/common/routes/rule/rule_response/types/v1.ts rename to x-pack/plugins/alerting/common/routes/rule/response/types/v1.ts index affd98575e270..cd19cf9fa5c5e 100644 --- a/x-pack/plugins/alerting/common/routes/rule/rule_response/types/v1.ts +++ b/x-pack/plugins/alerting/common/routes/rule/response/types/v1.ts @@ -6,9 +6,10 @@ */ import type { TypeOf } from '@kbn/config-schema'; -import { ruleParamsSchemaV1, ruleResponseSchemaV1 } from '..'; +import { ruleParamsSchemaV1, ruleResponseSchemaV1, ruleSnoozeScheduleSchemaV1 } from '..'; export type RuleParams = TypeOf; +export type RuleSnoozeSchedule = TypeOf; type RuleResponseSchemaType = TypeOf; export interface RuleResponse { diff --git a/x-pack/plugins/alerting/common/routes/rule/validation/validate_notify_when/v1.ts b/x-pack/plugins/alerting/common/routes/rule/validation/validate_notify_when/v1.ts index e239b1990f7b4..e58e5f5ef7cfd 100644 --- a/x-pack/plugins/alerting/common/routes/rule/validation/validate_notify_when/v1.ts +++ b/x-pack/plugins/alerting/common/routes/rule/validation/validate_notify_when/v1.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ruleNotifyWhenV1, RuleNotifyWhenV1 } from '../../rule_response'; +import { ruleNotifyWhenV1, RuleNotifyWhenV1 } from '../../response'; export function validateNotifyWhen(notifyWhen: string) { if (Object.values(ruleNotifyWhenV1).includes(notifyWhen as RuleNotifyWhenV1)) { diff --git a/x-pack/plugins/alerting/server/application/r_rule/schemas/index.ts b/x-pack/plugins/alerting/server/application/r_rule/schemas/index.ts new file mode 100644 index 0000000000000..12e793318558d --- /dev/null +++ b/x-pack/plugins/alerting/server/application/r_rule/schemas/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { rRuleSchema } from './r_rule_schema'; +export { rRuleRequestSchema } from './r_rule_request_schema'; diff --git a/x-pack/plugins/alerting/server/application/r_rule/schemas/r_rule_request_schema.ts b/x-pack/plugins/alerting/server/application/r_rule/schemas/r_rule_request_schema.ts new file mode 100644 index 0000000000000..0c83d16dee024 --- /dev/null +++ b/x-pack/plugins/alerting/server/application/r_rule/schemas/r_rule_request_schema.ts @@ -0,0 +1,46 @@ +/* + * 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 { validateStartDate, validateEndDate, createValidateRecurrenceBy } from '../validation'; + +export const rRuleRequestSchema = schema.object({ + dtstart: schema.string({ validate: validateStartDate }), + tzid: schema.string(), + freq: schema.maybe( + schema.oneOf([schema.literal(0), schema.literal(1), schema.literal(2), schema.literal(3)]) + ), + interval: schema.maybe( + schema.number({ + validate: (interval: number) => { + if (interval < 1) return 'rRule interval must be > 0'; + }, + }) + ), + until: schema.maybe(schema.string({ validate: validateEndDate })), + count: schema.maybe( + schema.number({ + validate: (count: number) => { + if (count < 1) return 'rRule count must be > 0'; + }, + }) + ), + byweekday: schema.maybe( + schema.arrayOf(schema.string(), { + validate: createValidateRecurrenceBy('byweekday'), + }) + ), + bymonthday: schema.maybe( + schema.arrayOf(schema.number(), { + validate: createValidateRecurrenceBy('bymonthday'), + }) + ), + bymonth: schema.maybe( + schema.arrayOf(schema.number(), { + validate: createValidateRecurrenceBy('bymonth'), + }) + ), +}); diff --git a/x-pack/plugins/alerting/server/application/r_rule/schemas/r_rule_schema.ts b/x-pack/plugins/alerting/server/application/r_rule/schemas/r_rule_schema.ts new file mode 100644 index 0000000000000..6f8b2f8f4fc7a --- /dev/null +++ b/x-pack/plugins/alerting/server/application/r_rule/schemas/r_rule_schema.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { schema } from '@kbn/config-schema'; + +export const rRuleSchema = schema.object({ + dtstart: schema.string(), + tzid: schema.string(), + freq: schema.maybe( + schema.oneOf([ + schema.literal(0), + schema.literal(1), + schema.literal(2), + schema.literal(3), + schema.literal(4), + schema.literal(5), + schema.literal(6), + ]) + ), + until: schema.maybe(schema.string()), + count: schema.maybe(schema.number()), + interval: schema.maybe(schema.number()), + wkst: schema.maybe( + schema.oneOf([ + schema.literal('MO'), + schema.literal('TU'), + schema.literal('WE'), + schema.literal('TH'), + schema.literal('FR'), + schema.literal('SA'), + schema.literal('SU'), + ]) + ), + byweekday: schema.maybe(schema.arrayOf(schema.oneOf([schema.string(), schema.number()]))), + bymonth: schema.maybe(schema.arrayOf(schema.number())), + bysetpos: schema.maybe(schema.arrayOf(schema.number())), + bymonthday: schema.arrayOf(schema.number()), + byyearday: schema.arrayOf(schema.number()), + byweekno: schema.arrayOf(schema.number()), + byhour: schema.arrayOf(schema.number()), + byminute: schema.arrayOf(schema.number()), + bysecond: schema.arrayOf(schema.number()), +}); diff --git a/x-pack/plugins/alerting/server/application/r_rule/types/index.ts b/x-pack/plugins/alerting/server/application/r_rule/types/index.ts new file mode 100644 index 0000000000000..080598aca88e3 --- /dev/null +++ b/x-pack/plugins/alerting/server/application/r_rule/types/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type { RRule } from './r_rule'; +export type { RRuleRequest } from './r_rule_request'; diff --git a/x-pack/plugins/alerting/server/application/r_rule/types/r_rule.ts b/x-pack/plugins/alerting/server/application/r_rule/types/r_rule.ts new file mode 100644 index 0000000000000..57700ea1b57d2 --- /dev/null +++ b/x-pack/plugins/alerting/server/application/r_rule/types/r_rule.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 { rRuleSchema } from '../schemas/r_rule_schema'; + +export type RRule = TypeOf; diff --git a/x-pack/plugins/alerting/server/application/r_rule/types/r_rule_request.ts b/x-pack/plugins/alerting/server/application/r_rule/types/r_rule_request.ts new file mode 100644 index 0000000000000..4f90eae946935 --- /dev/null +++ b/x-pack/plugins/alerting/server/application/r_rule/types/r_rule_request.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { TypeOf } from '@kbn/config-schema'; +import { rRuleRequestSchema } from '../schemas/r_rule_request_schema'; + +export type RRuleRequest = TypeOf; diff --git a/x-pack/plugins/alerting/server/application/r_rule/validation/index.ts b/x-pack/plugins/alerting/server/application/r_rule/validation/index.ts new file mode 100644 index 0000000000000..f48e8d5bf1c9a --- /dev/null +++ b/x-pack/plugins/alerting/server/application/r_rule/validation/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { validateStartDate } from './validate_start_date'; +export { validateEndDate } from './validate_end_date'; +export { validateRecurrenceBy, createValidateRecurrenceBy } from './validate_recurrence_by'; diff --git a/x-pack/plugins/alerting/server/application/r_rule/validation/validate_end_date.ts b/x-pack/plugins/alerting/server/application/r_rule/validation/validate_end_date.ts new file mode 100644 index 0000000000000..895c1aeea4dfb --- /dev/null +++ b/x-pack/plugins/alerting/server/application/r_rule/validation/validate_end_date.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. + */ + +export const validateEndDate = (date: string) => { + const parsedValue = Date.parse(date); + if (isNaN(parsedValue)) return `Invalid date: ${date}`; + if (parsedValue <= Date.now()) return `Invalid snooze date as it is in the past: ${date}`; + return; +}; diff --git a/x-pack/plugins/alerting/server/application/r_rule/validation/validate_recurrence_by.ts b/x-pack/plugins/alerting/server/application/r_rule/validation/validate_recurrence_by.ts new file mode 100644 index 0000000000000..0ab609355c680 --- /dev/null +++ b/x-pack/plugins/alerting/server/application/r_rule/validation/validate_recurrence_by.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const validateRecurrenceBy = (name: string, array: T[]) => { + if (array.length === 0) { + return `rRule ${name} cannot be empty`; + } +}; + +export const createValidateRecurrenceBy = (name: string) => { + return (array: T[]) => validateRecurrenceBy(name, array); +}; diff --git a/x-pack/plugins/alerting/server/application/r_rule/validation/validate_start_date.ts b/x-pack/plugins/alerting/server/application/r_rule/validation/validate_start_date.ts new file mode 100644 index 0000000000000..3cbc0da0af1b5 --- /dev/null +++ b/x-pack/plugins/alerting/server/application/r_rule/validation/validate_start_date.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const validateStartDate = (date: string) => { + const parsedValue = Date.parse(date); + if (isNaN(parsedValue)) return `Invalid date: ${date}`; + return; +}; diff --git a/x-pack/plugins/alerting/server/rules_client/tests/bulk_edit.test.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.test.ts similarity index 94% rename from x-pack/plugins/alerting/server/rules_client/tests/bulk_edit.test.ts rename to x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.test.ts index 1f6e86945b1b5..9eeba46aeb865 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/bulk_edit.test.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.test.ts @@ -6,27 +6,33 @@ */ import { schema } from '@kbn/config-schema'; +import { omit } from 'lodash'; import { v4 as uuidv4 } from 'uuid'; import { AlertConsumers } from '@kbn/rule-data-utils'; -import { RulesClient, ConstructorOptions } from '../rules_client'; +import { RulesClient, ConstructorOptions } from '../../../../rules_client/rules_client'; import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks'; import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; -import { ruleTypeRegistryMock } from '../../rule_type_registry.mock'; -import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock'; -import { RecoveredActionGroup, RuleTypeParams } from '../../../common'; +import { ruleTypeRegistryMock } from '../../../../rule_type_registry.mock'; +import { alertingAuthorizationMock } from '../../../../authorization/alerting_authorization.mock'; +import { RecoveredActionGroup, RuleTypeParams } from '../../../../../common'; import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks'; import { actionsAuthorizationMock } from '@kbn/actions-plugin/server/mocks'; -import { AlertingAuthorization } from '../../authorization/alerting_authorization'; +import { AlertingAuthorization } from '../../../../authorization/alerting_authorization'; import { ActionsAuthorization, ActionsClient } from '@kbn/actions-plugin/server'; import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks'; -import { getBeforeSetup, setGlobalDate } from './lib'; -import { bulkMarkApiKeysForInvalidation } from '../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation'; -import { NormalizedAlertAction } from '../types'; -import { enabledRule1, enabledRule2, siemRule1, siemRule2 } from './test_helpers'; -import { migrateLegacyActions } from '../lib'; -import { migrateLegacyActionsMock } from '../lib/siem_legacy_actions/retrieve_migrated_legacy_actions.mock'; - -jest.mock('../lib/siem_legacy_actions/migrate_legacy_actions', () => { +import { getBeforeSetup, setGlobalDate } from '../../../../rules_client/tests/lib'; +import { bulkMarkApiKeysForInvalidation } from '../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation'; +import { NormalizedAlertAction } from '../../../../rules_client/types'; +import { + enabledRule1, + enabledRule2, + siemRule1, + siemRule2, +} from '../../../../rules_client/tests/test_helpers'; +import { migrateLegacyActions } from '../../../../rules_client/lib'; +import { migrateLegacyActionsMock } from '../../../../rules_client/lib/siem_legacy_actions/retrieve_migrated_legacy_actions.mock'; + +jest.mock('../../../../rules_client/lib/siem_legacy_actions/migrate_legacy_actions', () => { return { migrateLegacyActions: jest.fn(), }; @@ -37,11 +43,11 @@ jest.mock('../lib/siem_legacy_actions/migrate_legacy_actions', () => { resultedReferences: [], }); -jest.mock('../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({ +jest.mock('../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({ bulkMarkApiKeysForInvalidation: jest.fn(), })); -jest.mock('../../lib/snooze/is_snooze_active', () => ({ +jest.mock('../../../../lib/snooze/is_snooze_active', () => ({ isSnoozeActive: jest.fn(), })); @@ -50,7 +56,7 @@ jest.mock('uuid', () => { return { v4: () => `${uuid++}` }; }); -const { isSnoozeActive } = jest.requireMock('../../lib/snooze/is_snooze_active'); +const { isSnoozeActive } = jest.requireMock('../../../../lib/snooze/is_snooze_active'); const taskManager = taskManagerMock.createStart(); const ruleTypeRegistry = ruleTypeRegistryMock.create(); @@ -105,10 +111,21 @@ describe('bulkEdit()', () => { attributes: { enabled: false, tags: ['foo'], + createdBy: 'user', + createdAt: '2019-02-12T21:01:22.479Z', + updatedAt: '2019-02-12T21:01:22.479Z', + legacyId: null, + muteAll: false, + mutedInstanceIds: [], + snoozeSchedule: [], alertTypeId: 'myType', schedule: { interval: '1m' }, consumer: 'myApp', scheduledTaskId: 'task-123', + executionStatus: { + lastExecutionDate: '2019-02-12T21:01:22.479Z', + status: 'pending', + }, params: {}, throttle: null, notifyWhen: null, @@ -244,6 +261,10 @@ describe('bulkEdit()', () => { schedule: { interval: '1m' }, consumer: 'myApp', scheduledTaskId: 'task-123', + executionStatus: { + lastExecutionDate: '2019-02-12T21:01:22.479Z', + status: 'pending', + }, params: {}, throttle: null, notifyWhen: null, @@ -301,6 +322,10 @@ describe('bulkEdit()', () => { schedule: { interval: '1m' }, consumer: 'myApp', scheduledTaskId: 'task-123', + executionStatus: { + lastExecutionDate: '2019-02-12T21:01:22.479Z', + status: 'pending', + }, params: {}, throttle: null, notifyWhen: null, @@ -354,6 +379,10 @@ describe('bulkEdit()', () => { schedule: { interval: '1m' }, consumer: 'myApp', scheduledTaskId: 'task-123', + executionStatus: { + lastExecutionDate: '2019-02-12T21:01:22.479Z', + status: 'pending', + }, params: {}, throttle: null, notifyWhen: null, @@ -621,8 +650,15 @@ describe('bulkEdit()', () => { ], { overwrite: true } ); + expect(result.rules[0]).toEqual({ - ...existingRule.attributes, + ...omit(existingRule.attributes, 'legacyId'), + createdAt: new Date(existingRule.attributes.createdAt), + updatedAt: new Date(existingRule.attributes.updatedAt), + executionStatus: { + ...existingRule.attributes.executionStatus, + lastExecutionDate: new Date(existingRule.attributes.executionStatus.lastExecutionDate), + }, actions: [existingAction, { ...newAction, uuid: '222' }], id: existingRule.id, snoozeSchedule: [], @@ -827,7 +863,13 @@ describe('bulkEdit()', () => { { overwrite: true } ); expect(result.rules[0]).toEqual({ - ...existingRule.attributes, + ...omit(existingRule.attributes, 'legacyId'), + createdAt: new Date(existingRule.attributes.createdAt), + updatedAt: new Date(existingRule.attributes.updatedAt), + executionStatus: { + ...existingRule.attributes.executionStatus, + lastExecutionDate: new Date(existingRule.attributes.executionStatus.lastExecutionDate), + }, actions: [ existingAction, { @@ -875,6 +917,10 @@ describe('bulkEdit()', () => { schedule: { interval: '1m' }, consumer: 'myApp', scheduledTaskId: 'task-123', + executionStatus: { + lastExecutionDate: '2019-02-12T21:01:22.479Z', + status: 'pending', + }, params: { index: ['test-1', 'test-2', 'test-4', 'test-5'], }, @@ -940,6 +986,10 @@ describe('bulkEdit()', () => { schedule: { interval: '1m' }, consumer: 'myApp', scheduledTaskId: 'task-123', + executionStatus: { + lastExecutionDate: '2019-02-12T21:01:22.479Z', + status: 'pending', + }, params: { index: ['test-1'], }, @@ -1040,6 +1090,10 @@ describe('bulkEdit()', () => { schedule: { interval: '1m' }, consumer: 'myApp', scheduledTaskId: 'task-123', + executionStatus: { + lastExecutionDate: '2019-02-12T21:01:22.479Z', + status: 'pending', + }, params: {}, throttle: null, notifyWhen: null, @@ -1414,7 +1468,7 @@ describe('bulkEdit()', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (unsecuredSavedObjectsClient.bulkCreate.mock.calls[0][0][0].attributes as any) .snoozeSchedule - ).toBeUndefined(); + ).toEqual([]); }); }); @@ -1491,6 +1545,10 @@ describe('bulkEdit()', () => { schedule: { interval: '1m' }, consumer: 'myApp', scheduledTaskId: 'task-123', + executionStatus: { + lastExecutionDate: '2019-02-12T21:01:22.479Z', + status: 'pending', + }, params: { index: ['index-1', 'index-2', 'index-3'], }, @@ -1559,6 +1617,10 @@ describe('bulkEdit()', () => { schedule: { interval: '1m' }, consumer: 'myApp', scheduledTaskId: 'task-123', + executionStatus: { + lastExecutionDate: '2019-02-12T21:01:22.479Z', + status: 'pending', + }, params: { index: ['index-1', 'index-2'], }, @@ -1628,6 +1690,10 @@ describe('bulkEdit()', () => { schedule: { interval: '1m' }, consumer: 'myApp', scheduledTaskId: 'task-123', + executionStatus: { + lastExecutionDate: '2019-02-12T21:01:22.479Z', + status: 'pending', + }, params: { index: ['index-1', 'index-2', 'index-3'], }, @@ -2041,6 +2107,10 @@ describe('bulkEdit()', () => { schedule: { interval: '1m' }, consumer: 'myApp', scheduledTaskId: 'task-123', + executionStatus: { + lastExecutionDate: '2019-02-12T21:01:22.479Z', + status: 'pending', + }, params: { index: ['test-index-*'] }, throttle: null, notifyWhen: null, @@ -2437,6 +2507,10 @@ describe('bulkEdit()', () => { schedule: { interval: '1m' }, consumer: 'myApp', scheduledTaskId: 'task-123', + executionStatus: { + lastExecutionDate: '2019-02-12T21:01:22.479Z', + status: 'pending', + }, params: { index: ['test-index-*'] }, throttle: null, notifyWhen: null, @@ -2514,6 +2588,10 @@ describe('bulkEdit()', () => { schedule: { interval: '1m' }, consumer: 'myApp', scheduledTaskId: 'task-123', + executionStatus: { + lastExecutionDate: '2019-02-12T21:01:22.479Z', + status: 'pending', + }, params: { index: ['test-index-*'] }, throttle: null, notifyWhen: null, @@ -2551,6 +2629,10 @@ describe('bulkEdit()', () => { tags: ['foo'], alertTypeId: 'myType', schedule: { interval: '1m' }, + executionStatus: { + lastExecutionDate: '2019-02-12T21:01:22.479Z', + status: 'pending', + }, consumer: 'myApp', params: { index: ['test-index-*'] }, throttle: null, diff --git a/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.ts new file mode 100644 index 0000000000000..050b138eddd6b --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.ts @@ -0,0 +1,890 @@ +/* + * 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 pMap from 'p-map'; +import Boom from '@hapi/boom'; +import { cloneDeep } from 'lodash'; +import { AlertConsumers } from '@kbn/rule-data-utils'; +import { KueryNode, nodeBuilder } from '@kbn/es-query'; +import { + SavedObjectsBulkUpdateObject, + SavedObjectsBulkCreateObject, + SavedObjectsFindResult, + SavedObjectsUpdateResponse, +} from '@kbn/core/server'; +import { BulkActionSkipResult } from '../../../../../common/bulk_edit'; +import { RuleTypeRegistry } from '../../../../types'; +import { + validateRuleTypeParams, + getRuleNotifyWhenType, + validateMutatedRuleTypeParams, + convertRuleIdsToKueryNode, +} from '../../../../lib'; +import { WriteOperations, AlertingAuthorizationEntity } from '../../../../authorization'; +import { parseDuration } from '../../../../../common/parse_duration'; +import { bulkMarkApiKeysForInvalidation } from '../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation'; +import { ruleAuditEvent, RuleAuditAction } from '../../../../rules_client/common/audit_events'; +import { + retryIfBulkEditConflicts, + applyBulkEditOperation, + buildKueryNodeFilter, + injectReferencesIntoActions, + getBulkSnooze, + getBulkUnsnooze, + verifySnoozeScheduleLimit, +} from '../../../../rules_client/common'; +import { + alertingAuthorizationFilterOpts, + MAX_RULES_NUMBER_FOR_BULK_OPERATION, + RULE_TYPE_CHECKS_CONCURRENCY, + API_KEY_GENERATE_CONCURRENCY, +} from '../../../../rules_client/common/constants'; +import { getMappedParams } from '../../../../rules_client/common/mapped_params_utils'; +import { + extractReferences, + validateActions, + updateMeta, + addGeneratedActionValues, + createNewAPIKeySet, +} from '../../../../rules_client/lib'; +import { + BulkOperationError, + RuleBulkOperationAggregation, + RulesClientContext, + NormalizedAlertActionWithGeneratedValues, +} from '../../../../rules_client/types'; +import { migrateLegacyActions } from '../../../../rules_client/lib'; +import { + BulkEditFields, + BulkEditOperation, + BulkEditOptionsFilter, + BulkEditOptionsIds, + ParamsModifier, + ShouldIncrementRevision, +} from './types'; +import { RawRuleAction, RawRule, SanitizedRule } from '../../../../types'; +import { ruleNotifyWhen } from '../../constants'; +import { ruleDomainSchema } from '../../schemas'; +import { RuleParams, RuleDomain, RuleSnoozeSchedule } from '../../types'; +import { findRulesSo, bulkCreateRulesSo } from '../../../../data/rule'; +import { RuleAttributes, RuleActionAttributes } from '../../../../data/rule/types'; +import { + transformRuleAttributesToRuleDomain, + transformRuleDomainToRuleAttributes, + transformRuleDomainToRule, +} from '../../transforms'; + +export const bulkEditFieldsToExcludeFromRevisionUpdates = new Set(['snoozeSchedule', 'apiKey']); + +type ApiKeysMap = Map< + string, + { + oldApiKey?: string; + newApiKey?: string; + oldApiKeyCreatedByUser?: boolean | null; + newApiKeyCreatedByUser?: boolean | null; + } +>; + +type ApiKeyAttributes = Pick; + +type RuleType = ReturnType; + +// TODO (http-versioning): This should be of type Rule, change this when all rule types are fixed +export interface BulkEditResult { + rules: Array>; + skipped: BulkActionSkipResult[]; + errors: BulkOperationError[]; + total: number; +} + +export type BulkEditOptions = + | BulkEditOptionsFilter + | BulkEditOptionsIds; + +export async function bulkEditRules( + context: RulesClientContext, + options: BulkEditOptions +): Promise> { + const queryFilter = (options as BulkEditOptionsFilter).filter; + const ids = (options as BulkEditOptionsIds).ids; + + if (ids && queryFilter) { + throw Boom.badRequest( + "Both 'filter' and 'ids' are supplied. Define either 'ids' or 'filter' properties in method arguments" + ); + } + + const qNodeQueryFilter = buildKueryNodeFilter(queryFilter); + + const qNodeFilter = ids ? convertRuleIdsToKueryNode(ids) : qNodeQueryFilter; + let authorizationTuple; + try { + authorizationTuple = await context.authorization.getFindAuthorizationFilter( + AlertingAuthorizationEntity.Rule, + alertingAuthorizationFilterOpts + ); + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.BULK_EDIT, + error, + }) + ); + throw error; + } + const { filter: authorizationFilter } = authorizationTuple; + const qNodeFilterWithAuth = + authorizationFilter && qNodeFilter + ? nodeBuilder.and([qNodeFilter, authorizationFilter as KueryNode]) + : qNodeFilter; + + const { aggregations, total } = await findRulesSo({ + savedObjectsClient: context.unsecuredSavedObjectsClient, + savedObjectsFindOptions: { + filter: qNodeFilterWithAuth, + page: 1, + perPage: 0, + aggs: { + alertTypeId: { + multi_terms: { + terms: [ + { field: 'alert.attributes.alertTypeId' }, + { field: 'alert.attributes.consumer' }, + ], + }, + }, + }, + }, + }); + + if (total > MAX_RULES_NUMBER_FOR_BULK_OPERATION) { + throw Boom.badRequest( + `More than ${MAX_RULES_NUMBER_FOR_BULK_OPERATION} rules matched for bulk edit` + ); + } + const buckets = aggregations?.alertTypeId.buckets; + + if (buckets === undefined) { + throw Error('No rules found for bulk edit'); + } + + await pMap( + buckets, + async ({ key: [ruleType, consumer] }) => { + context.ruleTypeRegistry.ensureRuleTypeEnabled(ruleType); + + try { + await context.authorization.ensureAuthorized({ + ruleTypeId: ruleType, + consumer, + operation: WriteOperations.BulkEdit, + entity: AlertingAuthorizationEntity.Rule, + }); + } catch (error) { + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.BULK_EDIT, + error, + }) + ); + throw error; + } + }, + { concurrency: RULE_TYPE_CHECKS_CONCURRENCY } + ); + + const { apiKeysToInvalidate, results, errors, skipped } = await retryIfBulkEditConflicts( + context.logger, + `rulesClient.update('operations=${JSON.stringify(options.operations)}, paramsModifier=${ + options.paramsModifier ? '[Function]' : undefined + }', shouldIncrementRevision=${options.shouldIncrementRevision ? '[Function]' : undefined}')`, + (filterKueryNode: KueryNode | null) => + bulkEditRulesOcc(context, { + filter: filterKueryNode, + operations: options.operations, + paramsModifier: options.paramsModifier, + shouldIncrementRevision: options.shouldIncrementRevision, + }), + qNodeFilterWithAuth + ); + + if (apiKeysToInvalidate.length > 0) { + await bulkMarkApiKeysForInvalidation( + { apiKeys: apiKeysToInvalidate }, + context.logger, + context.unsecuredSavedObjectsClient + ); + } + + const updatedRules = results.map(({ id, attributes, references }) => { + // TODO (http-versioning): alertTypeId should never be null, but we need to + // fix the type cast from SavedObjectsBulkUpdateObject to SavedObjectsBulkUpdateObject + // when we are doing the bulk create and this should fix itself + const ruleType = context.ruleTypeRegistry.get(attributes.alertTypeId!); + const ruleDomain = transformRuleAttributesToRuleDomain(attributes as RuleAttributes, { + id, + logger: context.logger, + ruleType, + references, + omitGeneratedValues: false, + }); + try { + ruleDomainSchema.validate(ruleDomain); + } catch (e) { + context.logger.warn(`Error validating bulk edited rule domain object for id: ${id}, ${e}`); + } + return ruleDomain; + }); + + // TODO (http-versioning): This should be of type Rule, change this when all rule types are fixed + const publicRules = updatedRules.map((rule: RuleDomain) => { + return transformRuleDomainToRule(rule); + }) as Array>; + + await bulkUpdateSchedules(context, options.operations, updatedRules); + + return { rules: publicRules, skipped, errors, total }; +} + +async function bulkEditRulesOcc( + context: RulesClientContext, + { + filter, + operations, + paramsModifier, + shouldIncrementRevision, + }: { + filter: KueryNode | null; + operations: BulkEditOperation[]; + paramsModifier?: ParamsModifier; + shouldIncrementRevision?: ShouldIncrementRevision; + } +): Promise<{ + apiKeysToInvalidate: string[]; + rules: Array>; + resultSavedObjects: Array>; + errors: BulkOperationError[]; + skipped: BulkActionSkipResult[]; +}> { + const rulesFinder = + await context.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser( + { + filter, + type: 'alert', + perPage: 100, + ...(context.namespace ? { namespaces: [context.namespace] } : undefined), + } + ); + + const rules: Array> = []; + const skipped: BulkActionSkipResult[] = []; + const errors: BulkOperationError[] = []; + const apiKeysMap: ApiKeysMap = new Map(); + const username = await context.getUserName(); + + for await (const response of rulesFinder.find()) { + await pMap( + response.saved_objects, + async (rule: SavedObjectsFindResult) => + updateRuleAttributesAndParamsInMemory({ + context, + rule, + operations, + paramsModifier, + apiKeysMap, + rules, + skipped, + errors, + username, + shouldIncrementRevision, + }), + { concurrency: API_KEY_GENERATE_CONCURRENCY } + ); + } + await rulesFinder.close(); + + const { result, apiKeysToInvalidate } = + rules.length > 0 + ? await saveBulkUpdatedRules(context, rules, apiKeysMap) + : { + result: { saved_objects: [] }, + apiKeysToInvalidate: [], + }; + + return { + apiKeysToInvalidate, + resultSavedObjects: result.saved_objects, + errors, + rules, + skipped, + }; +} + +async function bulkUpdateSchedules( + context: RulesClientContext, + operations: BulkEditOperation[], + updatedRules: Array> +): Promise { + const scheduleOperation = operations.find( + ( + operation + ): operation is Extract }> => + operation.field === 'schedule' + ); + + if (!scheduleOperation?.value) { + return; + } + const taskIds = updatedRules.reduce((acc, rule) => { + if (rule.scheduledTaskId) { + acc.push(rule.scheduledTaskId); + } + return acc; + }, []); + + try { + await context.taskManager.bulkUpdateSchedules(taskIds, scheduleOperation.value); + context.logger.debug( + `Successfully updated schedules for underlying tasks: ${taskIds.join(', ')}` + ); + } catch (error) { + context.logger.error( + `Failure to update schedules for underlying tasks: ${taskIds.join( + ', ' + )}. TaskManager bulkUpdateSchedules failed with Error: ${error.message}` + ); + } +} + +async function updateRuleAttributesAndParamsInMemory({ + context, + rule, + operations, + paramsModifier, + apiKeysMap, + rules, + skipped, + errors, + username, + shouldIncrementRevision = () => true, +}: { + context: RulesClientContext; + rule: SavedObjectsFindResult; + operations: BulkEditOperation[]; + paramsModifier?: ParamsModifier; + apiKeysMap: ApiKeysMap; + rules: Array>; + skipped: BulkActionSkipResult[]; + errors: BulkOperationError[]; + username: string | null; + shouldIncrementRevision?: ShouldIncrementRevision; +}): Promise { + try { + if (rule.attributes.apiKey) { + apiKeysMap.set(rule.id, { + oldApiKey: rule.attributes.apiKey, + oldApiKeyCreatedByUser: rule.attributes.apiKeyCreatedByUser, + }); + } + + const ruleType = context.ruleTypeRegistry.get(rule.attributes.alertTypeId); + + await ensureAuthorizationForBulkUpdate(context, operations, rule); + + // migrate legacy actions only for SIEM rules + // TODO (http-versioning) Remove RawRuleAction and RawRule casts + const migratedActions = await migrateLegacyActions(context, { + ruleId: rule.id, + actions: rule.attributes.actions as RawRuleAction[], + references: rule.references, + attributes: rule.attributes as RawRule, + }); + + if (migratedActions.hasLegacyActions) { + rule.attributes.actions = migratedActions.resultedActions; + rule.references = migratedActions.resultedReferences; + } + + const ruleActions = injectReferencesIntoActions( + rule.id, + rule.attributes.actions || [], + rule.references || [] + ); + + const ruleDomain: RuleDomain = transformRuleAttributesToRuleDomain( + rule.attributes, + { + id: rule.id, + logger: context.logger, + ruleType: context.ruleTypeRegistry.get(rule.attributes.alertTypeId), + references: rule.references, + } + ); + + const { + rule: updatedRule, + ruleActions: updatedRuleActions, + hasUpdateApiKeyOperation, + isAttributesUpdateSkipped, + } = await getUpdatedAttributesFromOperations({ + context, + operations, + rule: ruleDomain, + ruleActions, + ruleType, + }); + + validateScheduleInterval(context, updatedRule.schedule.interval, ruleType.id, rule.id); + + const { modifiedParams: ruleParams, isParamsUpdateSkipped } = paramsModifier + ? await paramsModifier(updatedRule.params) + : { + modifiedParams: updatedRule.params, + isParamsUpdateSkipped: true, + }; + + // Increment revision if params ended up being modified AND it wasn't already incremented as part of attribute update + if ( + shouldIncrementRevision(ruleParams) && + !isParamsUpdateSkipped && + rule.attributes.revision === updatedRule.revision + ) { + updatedRule.revision += 1; + } + + // If neither attributes nor parameters were updated, mark + // the rule as skipped and continue to the next rule. + if (isAttributesUpdateSkipped && isParamsUpdateSkipped) { + skipped.push({ + id: rule.id, + name: rule.attributes.name, + skip_reason: 'RULE_NOT_MODIFIED', + }); + return; + } + + // validate rule params + const validatedAlertTypeParams = validateRuleTypeParams(ruleParams, ruleType.validate.params); + const validatedMutatedAlertTypeParams = validateMutatedRuleTypeParams( + validatedAlertTypeParams, + rule.attributes.params, + ruleType.validate.params + ); + + const { + actions: rawAlertActions, + references, + params: updatedParams, + } = await extractReferences( + context, + ruleType, + updatedRuleActions as NormalizedAlertActionWithGeneratedValues[], + validatedMutatedAlertTypeParams + ); + + const ruleAttributes = transformRuleDomainToRuleAttributes(updatedRule, { + legacyId: rule.attributes.legacyId, + actionsWithRefs: rawAlertActions, + paramsWithRefs: updatedParams as RuleAttributes['params'], + }); + + const { apiKeyAttributes } = await prepareApiKeys( + context, + rule, + ruleType, + apiKeysMap, + ruleAttributes, + hasUpdateApiKeyOperation, + username + ); + + const { updatedAttributes } = updateAttributes( + context, + ruleAttributes, + apiKeyAttributes, + updatedParams, + rawAlertActions, + username + ); + + rules.push({ + ...rule, + references, + attributes: updatedAttributes, + }); + } catch (error) { + errors.push({ + message: error.message, + rule: { + id: rule.id, + name: rule.attributes?.name, + }, + }); + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.BULK_EDIT, + error, + }) + ); + } +} + +async function ensureAuthorizationForBulkUpdate( + context: RulesClientContext, + operations: BulkEditOperation[], + rule: SavedObjectsFindResult +): Promise { + if (rule.attributes.actions.length === 0) { + return; + } + + for (const operation of operations) { + const { field } = operation; + if (field === 'snoozeSchedule' || field === 'apiKey') { + try { + await context.actionsAuthorization.ensureAuthorized({ operation: 'execute' }); + break; + } catch (error) { + throw Error(`Rule not authorized for bulk ${field} update - ${error.message}`); + } + } + } +} + +async function getUpdatedAttributesFromOperations({ + context, + operations, + rule, + ruleActions, + ruleType, +}: { + context: RulesClientContext; + operations: BulkEditOperation[]; + rule: RuleDomain; + ruleActions: RuleDomain['actions']; + ruleType: RuleType; +}) { + let updatedRule = cloneDeep(rule); + let updatedRuleActions = ruleActions; + let hasUpdateApiKeyOperation = false; + let isAttributesUpdateSkipped = true; + + for (const operation of operations) { + // Check if the update should be skipped for the current action. + // If it should, save the skip reasons in attributesUpdateSkipReasons + // and continue to the next operation before without + // the `isAttributesUpdateSkipped` flag to false. + switch (operation.field) { + case 'actions': { + const updatedOperation = { + ...operation, + value: addGeneratedActionValues(operation.value), + }; + + try { + await validateActions(context, ruleType, { + ...updatedRule, + actions: updatedOperation.value, + }); + } catch (e) { + // If validateActions fails on the first attempt, it may be because of legacy rule-level frequency params + updatedRule = await attemptToMigrateLegacyFrequency( + context, + updatedOperation, + updatedRule, + ruleType + ); + } + + const { modifiedAttributes, isAttributeModified } = applyBulkEditOperation( + updatedOperation, + { + actions: updatedRuleActions, + } + ); + if (isAttributeModified) { + updatedRuleActions = modifiedAttributes.actions; + isAttributesUpdateSkipped = false; + } + + break; + } + case 'snoozeSchedule': { + // Silently skip adding snooze or snooze schedules on security + // rules until we implement snoozing of their rules + if (updatedRule.consumer === AlertConsumers.SIEM) { + // While the rule is technically not updated, we are still marking + // the rule as updated in case of snoozing, until support + // for snoozing is added. + isAttributesUpdateSkipped = false; + break; + } + if (operation.operation === 'set') { + const snoozeAttributes = getBulkSnooze( + updatedRule, + operation.value as RuleSnoozeSchedule + ); + try { + verifySnoozeScheduleLimit(snoozeAttributes.snoozeSchedule); + } catch (error) { + throw Error(`Error updating rule: could not add snooze - ${error.message}`); + } + updatedRule = { + ...updatedRule, + muteAll: snoozeAttributes.muteAll, + snoozeSchedule: snoozeAttributes.snoozeSchedule as RuleDomain['snoozeSchedule'], + }; + } + if (operation.operation === 'delete') { + const idsToDelete = operation.value && [...operation.value]; + if (idsToDelete?.length === 0) { + updatedRule.snoozeSchedule?.forEach((schedule) => { + if (schedule.id) { + idsToDelete.push(schedule.id); + } + }); + } + const snoozeAttributes = getBulkUnsnooze(updatedRule, idsToDelete); + updatedRule = { + ...updatedRule, + muteAll: snoozeAttributes.muteAll, + snoozeSchedule: snoozeAttributes.snoozeSchedule as RuleDomain['snoozeSchedule'], + }; + } + isAttributesUpdateSkipped = false; + break; + } + case 'apiKey': { + hasUpdateApiKeyOperation = true; + isAttributesUpdateSkipped = false; + break; + } + default: { + if (operation.field === 'schedule') { + validateScheduleOperation(operation.value, updatedRule.actions, rule.id); + } + const { modifiedAttributes, isAttributeModified } = applyBulkEditOperation( + operation, + updatedRule + ); + + if (isAttributeModified) { + updatedRule = { + ...updatedRule, + ...modifiedAttributes, + }; + isAttributesUpdateSkipped = false; + } + } + } + // Only increment revision if update wasn't skipped and `operation.field` should result in a revision increment + if ( + !isAttributesUpdateSkipped && + !bulkEditFieldsToExcludeFromRevisionUpdates.has(operation.field) && + rule.revision - updatedRule.revision === 0 + ) { + updatedRule.revision += 1; + } + } + return { + rule: updatedRule, + ruleActions: updatedRuleActions, + hasUpdateApiKeyOperation, + isAttributesUpdateSkipped, + }; +} + +function validateScheduleInterval( + context: RulesClientContext, + scheduleInterval: string, + ruleTypeId: string, + ruleId: string +): void { + if (!scheduleInterval) { + return; + } + const isIntervalInvalid = parseDuration(scheduleInterval) < context.minimumScheduleIntervalInMs; + if (isIntervalInvalid && context.minimumScheduleInterval.enforce) { + throw Error( + `Error updating rule: the interval is less than the allowed minimum interval of ${context.minimumScheduleInterval.value}` + ); + } else if (isIntervalInvalid && !context.minimumScheduleInterval.enforce) { + context.logger.warn( + `Rule schedule interval (${scheduleInterval}) for "${ruleTypeId}" rule type with ID "${ruleId}" is less than the minimum value (${context.minimumScheduleInterval.value}). Running rules at this interval may impact alerting performance. Set "xpack.alerting.rules.minimumScheduleInterval.enforce" to true to prevent such changes.` + ); + } +} + +/** + * Validate that updated schedule interval is not longer than any of the existing action frequencies + * @param schedule Schedule interval that user tries to set + * @param actions Rule actions + */ +function validateScheduleOperation( + schedule: RuleDomain['schedule'], + actions: RuleDomain['actions'], + ruleId: string +): void { + const scheduleInterval = parseDuration(schedule.interval); + const actionsWithInvalidThrottles = []; + + for (const action of actions) { + // check for actions throttled shorter than the rule schedule + if ( + action.frequency?.notifyWhen === ruleNotifyWhen.THROTTLE && + parseDuration(action.frequency.throttle!) < scheduleInterval + ) { + actionsWithInvalidThrottles.push(action); + } + } + + if (actionsWithInvalidThrottles.length > 0) { + throw Error( + `Error updating rule with ID "${ruleId}": the interval ${schedule.interval} is longer than the action frequencies` + ); + } +} + +async function prepareApiKeys( + context: RulesClientContext, + rule: SavedObjectsFindResult, + ruleType: RuleType, + apiKeysMap: ApiKeysMap, + attributes: RuleAttributes, + hasUpdateApiKeyOperation: boolean, + username: string | null +): Promise<{ apiKeyAttributes: ApiKeyAttributes }> { + const apiKeyAttributes = await createNewAPIKeySet(context, { + id: ruleType.id, + ruleName: attributes.name, + username, + shouldUpdateApiKey: attributes.enabled || hasUpdateApiKeyOperation, + errorMessage: 'Error updating rule: could not create API key', + }); + + // collect generated API keys + if (apiKeyAttributes.apiKey) { + apiKeysMap.set(rule.id, { + ...apiKeysMap.get(rule.id), + newApiKey: apiKeyAttributes.apiKey, + newApiKeyCreatedByUser: apiKeyAttributes.apiKeyCreatedByUser, + }); + } + + return { + apiKeyAttributes, + }; +} + +function updateAttributes( + context: RulesClientContext, + attributes: RuleAttributes, + apiKeyAttributes: ApiKeyAttributes, + updatedParams: RuleParams, + rawAlertActions: RuleActionAttributes[], + username: string | null +): { + updatedAttributes: RuleAttributes; +} { + // get notifyWhen + const notifyWhen = getRuleNotifyWhenType( + attributes.notifyWhen ?? null, + attributes.throttle ?? null + ); + + // TODO (http-versioning) Remove casts when updateMeta has been converted + const castedAttributes = attributes as RawRule; + const updatedAttributes = updateMeta(context, { + ...castedAttributes, + ...apiKeyAttributes, + params: updatedParams as RawRule['params'], + actions: rawAlertActions as RawRule['actions'], + notifyWhen, + updatedBy: username, + updatedAt: new Date().toISOString(), + }) as RuleAttributes; + + // add mapped_params + const mappedParams = getMappedParams(updatedParams); + + if (Object.keys(mappedParams).length) { + updatedAttributes.mapped_params = mappedParams; + } + + return { + updatedAttributes, + }; +} + +async function saveBulkUpdatedRules( + context: RulesClientContext, + rules: Array>, + apiKeysMap: ApiKeysMap +) { + const apiKeysToInvalidate: string[] = []; + let result; + try { + // TODO (http-versioning): for whatever reasoning we are using SavedObjectsBulkUpdateObject + // everywhere when it should be SavedObjectsBulkCreateObject. We need to fix it in + // bulk_disable, bulk_enable, etc. to fix this cast + result = await bulkCreateRulesSo({ + savedObjectsClient: context.unsecuredSavedObjectsClient, + bulkCreateRuleAttributes: rules as Array>, + savedObjectsBulkCreateOptions: { overwrite: true }, + }); + } catch (e) { + // avoid unused newly generated API keys + if (apiKeysMap.size > 0) { + await bulkMarkApiKeysForInvalidation( + { + apiKeys: Array.from(apiKeysMap.values()) + .filter((value) => value.newApiKey && !value.newApiKeyCreatedByUser) + .map((value) => value.newApiKey as string), + }, + context.logger, + context.unsecuredSavedObjectsClient + ); + } + throw e; + } + + result.saved_objects.map(({ id, error }) => { + const oldApiKey = apiKeysMap.get(id)?.oldApiKey; + const oldApiKeyCreatedByUser = apiKeysMap.get(id)?.oldApiKeyCreatedByUser; + const newApiKey = apiKeysMap.get(id)?.newApiKey; + const newApiKeyCreatedByUser = apiKeysMap.get(id)?.newApiKeyCreatedByUser; + + // if SO wasn't saved and has new API key it will be invalidated + if (error && newApiKey && !newApiKeyCreatedByUser) { + apiKeysToInvalidate.push(newApiKey); + // if SO saved and has old Api Key it will be invalidate + } else if (!error && oldApiKey && !oldApiKeyCreatedByUser) { + apiKeysToInvalidate.push(oldApiKey); + } + }); + + return { result, apiKeysToInvalidate }; +} + +async function attemptToMigrateLegacyFrequency( + context: RulesClientContext, + operation: BulkEditOperation, + rule: RuleDomain, + ruleType: RuleType +) { + if (operation.field !== 'actions') + throw new Error('Can only perform frequency migration on an action operation'); + // Try to remove the rule-level frequency params, and then validate actions + if (typeof rule.notifyWhen !== 'undefined') rule.notifyWhen = undefined; + if (rule.throttle) rule.throttle = undefined; + await validateActions(context, ruleType, { + ...rule, + actions: operation.value, + }); + return rule; +} diff --git a/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/index.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/index.ts new file mode 100644 index 0000000000000..e470e73eb3dba --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type { BulkEditRuleSnoozeSchedule, BulkEditOperation } from './types'; +export { bulkEditRules } from './bulk_edit_rules'; diff --git a/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/schemas/bulk_edit_rules_option_schemas.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/schemas/bulk_edit_rules_option_schemas.ts new file mode 100644 index 0000000000000..f80d63210cf4a --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/schemas/bulk_edit_rules_option_schemas.ts @@ -0,0 +1,101 @@ +/* + * 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 { rRuleRequestSchema } from '../../../../r_rule/schemas'; +import { notifyWhenSchema } from '../../../schemas'; +import { validateDuration } from '../../../validation'; +import { validateSnoozeSchedule } from '../validation'; + +export const scheduleIdsSchema = schema.maybe(schema.arrayOf(schema.string())); + +export const bulkEditRuleSnoozeScheduleSchema = schema.object({ + id: schema.maybe(schema.string()), + duration: schema.number(), + rRule: rRuleRequestSchema, +}); +const bulkEditRuleSnoozeScheduleSchemaWithValidation = schema.object( + { + id: schema.maybe(schema.string()), + duration: schema.number(), + rRule: rRuleRequestSchema, + }, + { validate: validateSnoozeSchedule } +); + +const bulkEditActionSchema = schema.object({ + group: schema.string(), + id: schema.string(), + params: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), + uuid: schema.maybe(schema.string()), + frequency: schema.maybe( + schema.object({ + summary: schema.boolean(), + throttle: schema.nullable(schema.string()), + notifyWhen: notifyWhenSchema, + }) + ), +}); + +const bulkEditTagSchema = schema.object({ + operation: schema.oneOf([schema.literal('add'), schema.literal('delete'), schema.literal('set')]), + field: schema.literal('tags'), + value: schema.arrayOf(schema.string()), +}); + +const bulkEditActionsSchema = schema.object({ + operation: schema.oneOf([schema.literal('add'), schema.literal('set')]), + field: schema.literal('actions'), + value: schema.arrayOf(bulkEditActionSchema), +}); + +const bulkEditScheduleSchema = schema.object({ + operation: schema.literal('set'), + field: schema.literal('schedule'), + value: schema.object({ interval: schema.string({ validate: validateDuration }) }), +}); + +const bulkEditThrottleSchema = schema.object({ + operation: schema.literal('set'), + field: schema.literal('throttle'), + value: schema.nullable(schema.string()), +}); + +const bulkEditNotifyWhenSchema = schema.object({ + operation: schema.literal('set'), + field: schema.literal('notifyWhen'), + value: notifyWhenSchema, +}); + +const bulkEditSnoozeSchema = schema.object({ + operation: schema.oneOf([schema.literal('set')]), + field: schema.literal('snoozeSchedule'), + value: bulkEditRuleSnoozeScheduleSchemaWithValidation, +}); + +const bulkEditUnsnoozeSchema = schema.object({ + operation: schema.oneOf([schema.literal('delete')]), + field: schema.literal('snoozeSchedule'), + value: schema.maybe(scheduleIdsSchema), +}); + +const bulkEditApiKeySchema = schema.object({ + operation: schema.literal('set'), + field: schema.literal('apiKey'), +}); + +export const bulkEditOperationSchema = schema.oneOf([ + bulkEditTagSchema, + bulkEditActionsSchema, + bulkEditScheduleSchema, + bulkEditThrottleSchema, + bulkEditNotifyWhenSchema, + bulkEditSnoozeSchema, + bulkEditUnsnoozeSchema, + bulkEditApiKeySchema, +]); + +export const bulkEditOperationsSchema = schema.arrayOf(bulkEditOperationSchema, { minSize: 1 }); diff --git a/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/schemas/index.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/schemas/index.ts new file mode 100644 index 0000000000000..9ebceff7a0283 --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/schemas/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { + bulkEditRuleSnoozeScheduleSchema, + bulkEditOperationsSchema, + bulkEditOperationSchema, +} from './bulk_edit_rules_option_schemas'; diff --git a/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/types/bulk_edit_rules_options.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/types/bulk_edit_rules_options.ts new file mode 100644 index 0000000000000..a74b7fe152069 --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/types/bulk_edit_rules_options.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { TypeOf } from '@kbn/config-schema'; +import { KueryNode } from '@kbn/es-query'; +import { + bulkEditRuleSnoozeScheduleSchema, + bulkEditOperationsSchema, + bulkEditOperationSchema, +} from '../schemas'; +import { RuleParams, RuleDomain, Rule } from '../../../types'; + +export type BulkEditRuleSnoozeSchedule = TypeOf; +export type BulkEditOperation = TypeOf; +export type BulkEditOperations = TypeOf; + +export type ParamsModifier = ( + params: Params +) => Promise>; + +interface ParamsModifierResult { + modifiedParams: Params; + isParamsUpdateSkipped: boolean; +} + +export type ShouldIncrementRevision = (params?: Params) => boolean; + +export type BulkEditFields = keyof Pick< + RuleDomain, + 'actions' | 'tags' | 'schedule' | 'throttle' | 'notifyWhen' | 'snoozeSchedule' | 'apiKey' +>; + +export interface BulkEditOptionsCommon { + operations: BulkEditOperation[]; + paramsModifier?: ParamsModifier; + shouldIncrementRevision?: ShouldIncrementRevision; +} + +export type BulkEditOptionsFilter = BulkEditOptionsCommon & { + filter?: string | KueryNode; +}; + +export type BulkEditOptionsIds = BulkEditOptionsCommon & { + ids: string[]; +}; + +export type BulkEditSkipReason = 'RULE_NOT_MODIFIED'; + +export interface BulkActionSkipResult { + id: Rule['id']; + name?: Rule['name']; + skip_reason: BulkEditSkipReason; +} + +export interface BulkOperationError { + message: string; + status?: number; + rule: { + id: string; + name: string; + }; +} diff --git a/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/types/index.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/types/index.ts new file mode 100644 index 0000000000000..100e29880ef40 --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/types/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type { + BulkEditRuleSnoozeSchedule, + BulkEditOperation, + BulkEditOperations, + BulkEditFields, + BulkEditOptionsFilter, + BulkEditOptionsIds, + BulkActionSkipResult, + BulkEditOptionsCommon, + BulkOperationError, + ParamsModifier, + ShouldIncrementRevision, +} from './bulk_edit_rules_options'; diff --git a/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/validation/index.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/validation/index.ts new file mode 100644 index 0000000000000..243daccc5a15a --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/validation/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { validateSnoozeSchedule } from './validate_snooze_schedule'; diff --git a/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/validation/validate_snooze_schedule.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/validation/validate_snooze_schedule.ts new file mode 100644 index 0000000000000..16fd324cfcb7c --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/validation/validate_snooze_schedule.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Frequency } from '@kbn/rrule'; +import moment from 'moment'; +import { BulkEditRuleSnoozeSchedule } from '../types'; + +export const validateSnoozeSchedule = (schedule: BulkEditRuleSnoozeSchedule) => { + const intervalIsDaily = schedule.rRule.freq === Frequency.DAILY; + const durationInDays = moment.duration(schedule.duration, 'milliseconds').asDays(); + if (intervalIsDaily && schedule.rRule.interval && durationInDays >= schedule.rRule.interval) { + return 'Recurrence interval must be longer than the snooze duration'; + } +}; diff --git a/x-pack/plugins/alerting/server/application/rule/create/create_rule.test.ts b/x-pack/plugins/alerting/server/application/rule/methods/create/create_rule.test.ts similarity index 99% rename from x-pack/plugins/alerting/server/application/rule/create/create_rule.test.ts rename to x-pack/plugins/alerting/server/application/rule/methods/create/create_rule.test.ts index e8779face1e8a..b0c20db945b0e 100644 --- a/x-pack/plugins/alerting/server/application/rule/create/create_rule.test.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/create/create_rule.test.ts @@ -7,24 +7,24 @@ import { schema } from '@kbn/config-schema'; import { CreateRuleParams } from './create_rule'; -import { RulesClient, ConstructorOptions } from '../../../rules_client'; +import { RulesClient, ConstructorOptions } from '../../../../rules_client'; import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks'; import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; -import { ruleTypeRegistryMock } from '../../../rule_type_registry.mock'; -import { alertingAuthorizationMock } from '../../../authorization/alerting_authorization.mock'; +import { ruleTypeRegistryMock } from '../../../../rule_type_registry.mock'; +import { alertingAuthorizationMock } from '../../../../authorization/alerting_authorization.mock'; import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks'; import { actionsAuthorizationMock } from '@kbn/actions-plugin/server/mocks'; -import { AlertingAuthorization } from '../../../authorization/alerting_authorization'; +import { AlertingAuthorization } from '../../../../authorization/alerting_authorization'; import { ActionsAuthorization, ActionsClient } from '@kbn/actions-plugin/server'; -import { ruleNotifyWhen } from '../constants'; +import { ruleNotifyWhen } from '../../constants'; import { TaskStatus } from '@kbn/task-manager-plugin/server'; import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks'; -import { getBeforeSetup, setGlobalDate } from '../../../rules_client/tests/lib'; -import { RecoveredActionGroup } from '../../../../common'; -import { bulkMarkApiKeysForInvalidation } from '../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation'; -import { getRuleExecutionStatusPending, getDefaultMonitoring } from '../../../lib'; +import { getBeforeSetup, setGlobalDate } from '../../../../rules_client/tests/lib'; +import { RecoveredActionGroup } from '../../../../../common'; +import { bulkMarkApiKeysForInvalidation } from '../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation'; +import { getRuleExecutionStatusPending, getDefaultMonitoring } from '../../../../lib'; -jest.mock('../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({ +jest.mock('../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({ bulkMarkApiKeysForInvalidation: jest.fn(), })); diff --git a/x-pack/plugins/alerting/server/application/rule/create/create_rule.ts b/x-pack/plugins/alerting/server/application/rule/methods/create/create_rule.ts similarity index 81% rename from x-pack/plugins/alerting/server/application/rule/create/create_rule.ts rename to x-pack/plugins/alerting/server/application/rule/methods/create/create_rule.ts index 89dea65bbe85a..ddd6691a635a3 100644 --- a/x-pack/plugins/alerting/server/application/rule/create/create_rule.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/create/create_rule.ts @@ -8,30 +8,34 @@ import Semver from 'semver'; import Boom from '@hapi/boom'; import { SavedObject, SavedObjectsUtils } from '@kbn/core/server'; import { withSpan } from '@kbn/apm-utils'; -import { parseDuration } from '../../../../common/parse_duration'; -import { WriteOperations, AlertingAuthorizationEntity } from '../../../authorization'; -import { validateRuleTypeParams, getRuleNotifyWhenType, getDefaultMonitoring } from '../../../lib'; -import { getRuleExecutionStatusPending } from '../../../lib/rule_execution_status'; +import { parseDuration } from '../../../../../common/parse_duration'; +import { WriteOperations, AlertingAuthorizationEntity } from '../../../../authorization'; +import { + validateRuleTypeParams, + getRuleNotifyWhenType, + getDefaultMonitoringRuleDomainProperties, +} from '../../../../lib'; +import { getRuleExecutionStatusPending } from '../../../../lib/rule_execution_status'; import { extractReferences, validateActions, addGeneratedActionValues, -} from '../../../rules_client/lib'; -import { generateAPIKeyName, apiKeyAsAlertAttributes } from '../../../rules_client/common'; -import { ruleAuditEvent, RuleAuditAction } from '../../../rules_client/common/audit_events'; -import { RulesClientContext } from '../../../rules_client/types'; -import { Rule, RuleDomain, RuleParams } from '../types'; -import { SanitizedRule } from '../../../types'; +} from '../../../../rules_client/lib'; +import { generateAPIKeyName, apiKeyAsRuleDomainProperties } from '../../../../rules_client/common'; +import { ruleAuditEvent, RuleAuditAction } from '../../../../rules_client/common/audit_events'; +import { RulesClientContext } from '../../../../rules_client/types'; +import { RuleDomain, RuleParams } from '../../types'; +import { SanitizedRule } from '../../../../types'; import { transformRuleAttributesToRuleDomain, transformRuleDomainToRuleAttributes, transformRuleDomainToRule, -} from '../transforms'; -import { ruleDomainSchema } from '../schemas'; -import { RuleAttributes } from '../../../data/rule/types'; +} from '../../transforms'; +import { ruleDomainSchema } from '../../schemas'; +import { RuleAttributes } from '../../../../data/rule/types'; import type { CreateRuleData } from './types'; import { createRuleDataSchema } from './schemas'; -import { createRuleSavedObject } from '../../../rules_client/lib'; +import { createRuleSavedObject } from '../../../../rules_client/lib'; export interface CreateRuleOptions { id?: string; @@ -142,7 +146,9 @@ export async function createRule( const ruleAttributes = transformRuleDomainToRuleAttributes( { ...data, - ...apiKeyAsAlertAttributes(createdAPIKey, username, isAuthTypeApiKey), + // TODO (http-versioning) create a rule domain version of this function + // Right now this works because the 2 types can interop but it's not ideal + ...apiKeyAsRuleDomainProperties(createdAPIKey, username, isAuthTypeApiKey), id, createdBy: username, updatedBy: username, @@ -154,13 +160,13 @@ export async function createRule( notifyWhen, throttle, executionStatus: getRuleExecutionStatusPending(lastRunTimestamp.toISOString()), - monitoring: getDefaultMonitoring(lastRunTimestamp.toISOString()) as Rule['monitoring'], + monitoring: getDefaultMonitoringRuleDomainProperties(lastRunTimestamp.toISOString()), revision: 0, running: false, }, { legacyId, - actionsWithRefs: actions as RuleAttributes['actions'], + actionsWithRefs: actions, paramsWithRefs: updatedParams, } ); @@ -193,7 +199,7 @@ export async function createRule( try { ruleDomainSchema.validate(ruleDomain); } catch (e) { - context.logger.warn(`Error validating rule domain object for id: ${id}, ${e}`); + context.logger.warn(`Error validating created rule domain object for id: ${id}, ${e}`); } // Convert domain rule to rule (Remove certain properties) diff --git a/x-pack/plugins/alerting/server/application/rule/create/index.ts b/x-pack/plugins/alerting/server/application/rule/methods/create/index.ts similarity index 100% rename from x-pack/plugins/alerting/server/application/rule/create/index.ts rename to x-pack/plugins/alerting/server/application/rule/methods/create/index.ts diff --git a/x-pack/plugins/alerting/server/application/rule/create/schemas/create_rule_data_schema.ts b/x-pack/plugins/alerting/server/application/rule/methods/create/schemas/create_rule_data_schema.ts similarity index 94% rename from x-pack/plugins/alerting/server/application/rule/create/schemas/create_rule_data_schema.ts rename to x-pack/plugins/alerting/server/application/rule/methods/create/schemas/create_rule_data_schema.ts index da19c3fdde7d7..ce51e88c5ce73 100644 --- a/x-pack/plugins/alerting/server/application/rule/create/schemas/create_rule_data_schema.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/create/schemas/create_rule_data_schema.ts @@ -6,8 +6,8 @@ */ import { schema } from '@kbn/config-schema'; -import { validateDuration } from '../../../../../common/routes/rule/validation'; -import { notifyWhenSchema, actionAlertsFilterSchema } from '../../schemas'; +import { validateDuration } from '../../../validation'; +import { notifyWhenSchema, actionAlertsFilterSchema } from '../../../schemas'; export const createRuleDataSchema = schema.object({ name: schema.string(), diff --git a/x-pack/plugins/alerting/server/application/rule/create/schemas/index.ts b/x-pack/plugins/alerting/server/application/rule/methods/create/schemas/index.ts similarity index 100% rename from x-pack/plugins/alerting/server/application/rule/create/schemas/index.ts rename to x-pack/plugins/alerting/server/application/rule/methods/create/schemas/index.ts diff --git a/x-pack/plugins/alerting/server/application/rule/create/types/create_rule_data.ts b/x-pack/plugins/alerting/server/application/rule/methods/create/types/create_rule_data.ts similarity index 95% rename from x-pack/plugins/alerting/server/application/rule/create/types/create_rule_data.ts rename to x-pack/plugins/alerting/server/application/rule/methods/create/types/create_rule_data.ts index d1caf4b47260a..e75cbb5456c22 100644 --- a/x-pack/plugins/alerting/server/application/rule/create/types/create_rule_data.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/create/types/create_rule_data.ts @@ -7,7 +7,7 @@ import { TypeOf } from '@kbn/config-schema'; import { createRuleDataSchema } from '../schemas'; -import { RuleParams } from '../../types'; +import { RuleParams } from '../../../types'; type CreateRuleDataType = TypeOf; diff --git a/x-pack/plugins/alerting/server/application/rule/create/types/index.ts b/x-pack/plugins/alerting/server/application/rule/methods/create/types/index.ts similarity index 100% rename from x-pack/plugins/alerting/server/application/rule/create/types/index.ts rename to x-pack/plugins/alerting/server/application/rule/methods/create/types/index.ts diff --git a/x-pack/plugins/alerting/server/application/rule/schemas/index.ts b/x-pack/plugins/alerting/server/application/rule/schemas/index.ts index 5b366be10f817..50cecadfe4a71 100644 --- a/x-pack/plugins/alerting/server/application/rule/schemas/index.ts +++ b/x-pack/plugins/alerting/server/application/rule/schemas/index.ts @@ -7,7 +7,6 @@ export { ruleParamsSchema, - rRuleSchema, snoozeScheduleSchema, ruleExecutionStatusSchema, ruleLastRunSchema, diff --git a/x-pack/plugins/alerting/server/application/rule/schemas/rule_schemas.ts b/x-pack/plugins/alerting/server/application/rule/schemas/rule_schemas.ts index 8c6312ce42bed..07efe4793b562 100644 --- a/x-pack/plugins/alerting/server/application/rule/schemas/rule_schemas.ts +++ b/x-pack/plugins/alerting/server/application/rule/schemas/rule_schemas.ts @@ -12,6 +12,7 @@ import { ruleExecutionStatusErrorReason, ruleExecutionStatusWarningReason, } from '../constants'; +import { rRuleSchema } from '../../r_rule/schemas'; import { dateSchema } from './date_schema'; import { notifyWhenSchema } from './notify_when_schema'; import { actionDomainSchema, actionSchema } from './action_schemas'; @@ -122,45 +123,6 @@ export const monitoringSchema = schema.object({ }), }); -export const rRuleSchema = schema.object({ - dtstart: schema.string(), - tzid: schema.string(), - freq: schema.maybe( - schema.oneOf([ - schema.literal(0), - schema.literal(1), - schema.literal(2), - schema.literal(3), - schema.literal(4), - schema.literal(5), - schema.literal(6), - ]) - ), - until: schema.maybe(schema.string()), - count: schema.maybe(schema.number()), - interval: schema.maybe(schema.number()), - wkst: schema.maybe( - schema.oneOf([ - schema.literal('MO'), - schema.literal('TU'), - schema.literal('WE'), - schema.literal('TH'), - schema.literal('FR'), - schema.literal('SA'), - schema.literal('SU'), - ]) - ), - byweekday: schema.maybe(schema.arrayOf(schema.oneOf([schema.string(), schema.number()]))), - bymonth: schema.maybe(schema.arrayOf(schema.number())), - bysetpos: schema.maybe(schema.arrayOf(schema.number())), - bymonthday: schema.arrayOf(schema.number()), - byyearday: schema.arrayOf(schema.number()), - byweekno: schema.arrayOf(schema.number()), - byhour: schema.arrayOf(schema.number()), - byminute: schema.arrayOf(schema.number()), - bysecond: schema.arrayOf(schema.number()), -}); - export const snoozeScheduleSchema = schema.object({ duration: schema.number(), rRule: rRuleSchema, diff --git a/x-pack/plugins/alerting/server/application/rule/types/index.ts b/x-pack/plugins/alerting/server/application/rule/types/index.ts index f27a0ad9f2813..21c0935e08a7a 100644 --- a/x-pack/plugins/alerting/server/application/rule/types/index.ts +++ b/x-pack/plugins/alerting/server/application/rule/types/index.ts @@ -5,4 +5,12 @@ * 2.0. */ -export type { Rule, RuleDomain, RuleLastRun, Monitoring, RuleParams, RuleNotifyWhen } from './rule'; +export type { + Rule, + RuleDomain, + RuleLastRun, + Monitoring, + RuleParams, + RuleNotifyWhen, + RuleSnoozeSchedule, +} from './rule'; diff --git a/x-pack/plugins/alerting/server/application/rule/types/rule.ts b/x-pack/plugins/alerting/server/application/rule/types/rule.ts index fe81ed156e9bf..04e37a125aa40 100644 --- a/x-pack/plugins/alerting/server/application/rule/types/rule.ts +++ b/x-pack/plugins/alerting/server/application/rule/types/rule.ts @@ -15,7 +15,6 @@ import { } from '../constants'; import { ruleParamsSchema, - rRuleSchema, snoozeScheduleSchema, ruleExecutionStatusSchema, ruleLastRunSchema, @@ -36,8 +35,7 @@ export type RuleExecutionStatusWarningReason = typeof ruleExecutionStatusWarningReason[keyof typeof ruleExecutionStatusWarningReason]; export type RuleParams = TypeOf; -export type RRule = TypeOf; -export type SnoozeSchedule = TypeOf; +export type RuleSnoozeSchedule = TypeOf; export type RuleLastRun = TypeOf; export type Monitoring = TypeOf; export type Action = TypeOf; diff --git a/x-pack/plugins/alerting/server/application/rule/validation/index.ts b/x-pack/plugins/alerting/server/application/rule/validation/index.ts new file mode 100644 index 0000000000000..4bb530eb39f99 --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/validation/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { validateDuration } from './validate_duration'; diff --git a/x-pack/plugins/alerting/server/application/rule/validation/validate_duration.ts b/x-pack/plugins/alerting/server/application/rule/validation/validate_duration.ts new file mode 100644 index 0000000000000..d07d710687cc0 --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/validation/validate_duration.ts @@ -0,0 +1,27 @@ +/* + * 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. + */ + +const SECONDS_REGEX = /^[1-9][0-9]*s$/; +const MINUTES_REGEX = /^[1-9][0-9]*m$/; +const HOURS_REGEX = /^[1-9][0-9]*h$/; +const DAYS_REGEX = /^[1-9][0-9]*d$/; + +export function validateDuration(duration: string) { + if (duration.match(SECONDS_REGEX)) { + return; + } + if (duration.match(MINUTES_REGEX)) { + return; + } + if (duration.match(HOURS_REGEX)) { + return; + } + if (duration.match(DAYS_REGEX)) { + return; + } + return 'string is not a valid duration: ' + duration; +} diff --git a/x-pack/plugins/alerting/server/data/rule/index.ts b/x-pack/plugins/alerting/server/data/rule/index.ts index 295ede18c8f9a..6256ac9b2278a 100644 --- a/x-pack/plugins/alerting/server/data/rule/index.ts +++ b/x-pack/plugins/alerting/server/data/rule/index.ts @@ -5,9 +5,13 @@ * 2.0. */ -export { createRuleSo } from './create_rule_so'; -export type { CreateRuleSoParams } from './create_rule_so'; -export { updateRuleSo } from './update_rule_so'; -export type { UpdateRuleSoParams } from './update_rule_so'; -export { deleteRuleSo } from './delete_rule_so'; -export type { DeleteRuleSoParams } from './delete_rule_so'; +export { createRuleSo } from './methods/create_rule_so'; +export type { CreateRuleSoParams } from './methods/create_rule_so'; +export { updateRuleSo } from './methods/update_rule_so'; +export type { UpdateRuleSoParams } from './methods/update_rule_so'; +export { deleteRuleSo } from './methods/delete_rule_so'; +export type { DeleteRuleSoParams } from './methods/delete_rule_so'; +export { findRulesSo } from './methods/find_rules_so'; +export type { FindRulesSoParams } from './methods/find_rules_so'; +export { bulkCreateRulesSo } from './methods/bulk_create_rule_so'; +export type { BulkCreateRulesSoParams } from './methods/bulk_create_rule_so'; diff --git a/x-pack/plugins/alerting/server/data/rule/methods/bulk_create_rule_so.ts b/x-pack/plugins/alerting/server/data/rule/methods/bulk_create_rule_so.ts new file mode 100644 index 0000000000000..6bc0ee04b394d --- /dev/null +++ b/x-pack/plugins/alerting/server/data/rule/methods/bulk_create_rule_so.ts @@ -0,0 +1,31 @@ +/* + * 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 { + SavedObjectsClientContract, + SavedObjectsCreateOptions, + SavedObjectsBulkCreateObject, + SavedObjectsBulkResponse, +} from '@kbn/core/server'; +import { RuleAttributes } from '../types'; + +export interface BulkCreateRulesSoParams { + savedObjectsClient: SavedObjectsClientContract; + bulkCreateRuleAttributes: Array>; + savedObjectsBulkCreateOptions?: SavedObjectsCreateOptions; +} + +export const bulkCreateRulesSo = ( + params: BulkCreateRulesSoParams +): Promise> => { + const { savedObjectsClient, bulkCreateRuleAttributes, savedObjectsBulkCreateOptions } = params; + + return savedObjectsClient.bulkCreate( + bulkCreateRuleAttributes, + savedObjectsBulkCreateOptions + ); +}; diff --git a/x-pack/plugins/alerting/server/data/rule/create_rule_so.ts b/x-pack/plugins/alerting/server/data/rule/methods/create_rule_so.ts similarity index 62% rename from x-pack/plugins/alerting/server/data/rule/create_rule_so.ts rename to x-pack/plugins/alerting/server/data/rule/methods/create_rule_so.ts index a9670c8a46d25..7574e9aca1608 100644 --- a/x-pack/plugins/alerting/server/data/rule/create_rule_so.ts +++ b/x-pack/plugins/alerting/server/data/rule/methods/create_rule_so.ts @@ -10,16 +10,16 @@ import { SavedObjectsCreateOptions, SavedObject, } from '@kbn/core/server'; -import { RuleAttributes } from './types'; +import { RuleAttributes } from '../types'; export interface CreateRuleSoParams { - savedObjectClient: SavedObjectsClientContract; + savedObjectsClient: SavedObjectsClientContract; ruleAttributes: RuleAttributes; - savedObjectCreateOptions?: SavedObjectsCreateOptions; + savedObjectsCreateOptions?: SavedObjectsCreateOptions; } export const createRuleSo = (params: CreateRuleSoParams): Promise> => { - const { savedObjectClient, ruleAttributes, savedObjectCreateOptions } = params; + const { savedObjectsClient, ruleAttributes, savedObjectsCreateOptions } = params; - return savedObjectClient.create('alert', ruleAttributes, savedObjectCreateOptions); + return savedObjectsClient.create('alert', ruleAttributes, savedObjectsCreateOptions); }; diff --git a/x-pack/plugins/alerting/server/data/rule/delete_rule_so.ts b/x-pack/plugins/alerting/server/data/rule/methods/delete_rule_so.ts similarity index 65% rename from x-pack/plugins/alerting/server/data/rule/delete_rule_so.ts rename to x-pack/plugins/alerting/server/data/rule/methods/delete_rule_so.ts index 26af10436ede3..e3428cfc78f63 100644 --- a/x-pack/plugins/alerting/server/data/rule/delete_rule_so.ts +++ b/x-pack/plugins/alerting/server/data/rule/methods/delete_rule_so.ts @@ -8,13 +8,13 @@ import { SavedObjectsClientContract, SavedObjectsDeleteOptions } from '@kbn/core/server'; export interface DeleteRuleSoParams { - savedObjectClient: SavedObjectsClientContract; + savedObjectsClient: SavedObjectsClientContract; id: string; - savedObjectDeleteOptions?: SavedObjectsDeleteOptions; + savedObjectsDeleteOptions?: SavedObjectsDeleteOptions; } export const deleteRuleSo = (params: DeleteRuleSoParams): Promise<{}> => { - const { savedObjectClient, id, savedObjectDeleteOptions } = params; + const { savedObjectsClient, id, savedObjectsDeleteOptions } = params; - return savedObjectClient.delete('alert', id, savedObjectDeleteOptions); + return savedObjectsClient.delete('alert', id, savedObjectsDeleteOptions); }; diff --git a/x-pack/plugins/alerting/server/data/rule/methods/find_rules_so.ts b/x-pack/plugins/alerting/server/data/rule/methods/find_rules_so.ts new file mode 100644 index 0000000000000..1362c2406dbae --- /dev/null +++ b/x-pack/plugins/alerting/server/data/rule/methods/find_rules_so.ts @@ -0,0 +1,29 @@ +/* + * 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 { + SavedObjectsClientContract, + SavedObjectsFindOptions, + SavedObjectsFindResponse, +} from '@kbn/core/server'; +import { RuleAttributes } from '../types'; + +export interface FindRulesSoParams { + savedObjectsClient: SavedObjectsClientContract; + savedObjectsFindOptions: Omit; +} + +export const findRulesSo = >( + params: FindRulesSoParams +): Promise> => { + const { savedObjectsClient, savedObjectsFindOptions } = params; + + return savedObjectsClient.find({ + ...savedObjectsFindOptions, + type: 'alert', + }); +}; diff --git a/x-pack/plugins/alerting/server/data/rule/update_rule_so.ts b/x-pack/plugins/alerting/server/data/rule/methods/update_rule_so.ts similarity index 65% rename from x-pack/plugins/alerting/server/data/rule/update_rule_so.ts rename to x-pack/plugins/alerting/server/data/rule/methods/update_rule_so.ts index 7d68e21412f5b..8739202c237ea 100644 --- a/x-pack/plugins/alerting/server/data/rule/update_rule_so.ts +++ b/x-pack/plugins/alerting/server/data/rule/methods/update_rule_so.ts @@ -10,24 +10,24 @@ import { SavedObjectsUpdateOptions, SavedObjectsUpdateResponse, } from '@kbn/core/server'; -import { RuleAttributes } from './types'; +import { RuleAttributes } from '../types'; export interface UpdateRuleSoParams { - savedObjectClient: SavedObjectsClientContract; + savedObjectsClient: SavedObjectsClientContract; id: string; updateRuleAttributes: Partial; - savedObjectUpdateOptions?: SavedObjectsUpdateOptions; + savedObjectsUpdateOptions?: SavedObjectsUpdateOptions; } export const updateRuleSo = ( params: UpdateRuleSoParams ): Promise> => { - const { savedObjectClient, id, updateRuleAttributes, savedObjectUpdateOptions } = params; + const { savedObjectsClient, id, updateRuleAttributes, savedObjectsUpdateOptions } = params; - return savedObjectClient.update( + return savedObjectsClient.update( 'alert', id, updateRuleAttributes, - savedObjectUpdateOptions + savedObjectsUpdateOptions ); }; diff --git a/x-pack/plugins/alerting/server/data/rule/types/index.ts b/x-pack/plugins/alerting/server/data/rule/types/index.ts index eda48efddd60e..d0eef33f65547 100644 --- a/x-pack/plugins/alerting/server/data/rule/types/index.ts +++ b/x-pack/plugins/alerting/server/data/rule/types/index.ts @@ -8,6 +8,7 @@ export type { RuleNotifyWhenAttributes, RuleLastRunOutcomeValuesAttributes, + RuleActionAttributes, RuleExecutionStatusValuesAttributes, RuleExecutionStatusErrorReasonAttributes, RuleExecutionStatusWarningReasonAttributes, 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 4e2ff63ad8e8c..03ccf7c8a6b0d 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 @@ -144,7 +144,7 @@ interface AlertsFilterAttributes { timeframe?: AlertsFilterTimeFrameAttributes; } -interface RuleActionAttributes { +export interface RuleActionAttributes { uuid: string; group: string; actionRef: string; diff --git a/x-pack/plugins/alerting/server/index.ts b/x-pack/plugins/alerting/server/index.ts index 74deec270826b..263e014fa492c 100644 --- a/x-pack/plugins/alerting/server/index.ts +++ b/x-pack/plugins/alerting/server/index.ts @@ -36,14 +36,7 @@ export type { export { RuleNotifyWhen } from '../common'; export { DEFAULT_MAX_EPHEMERAL_ACTIONS_PER_ALERT } from './config'; export type { PluginSetupContract, PluginStartContract } from './plugin'; -export type { - FindResult, - BulkEditOperation, - BulkOperationError, - BulkEditOptions, - BulkEditOptionsFilter, - BulkEditOptionsIds, -} from './rules_client'; +export type { FindResult, BulkEditOperation, BulkOperationError } from './rules_client'; export type { Rule } from './application/rule/types'; export type { PublicAlert as Alert } from './alert'; export { parseDuration, isRuleSnoozed } from './lib'; diff --git a/x-pack/plugins/alerting/server/lib/index.ts b/x-pack/plugins/alerting/server/lib/index.ts index 365b9e5b56747..db28ae5ff5649 100644 --- a/x-pack/plugins/alerting/server/lib/index.ts +++ b/x-pack/plugins/alerting/server/lib/index.ts @@ -31,6 +31,7 @@ export { lastRunFromState, lastRunFromError, lastRunToRaw } from './last_run_sta export { resetMonitoringLastRun, getDefaultMonitoring, + getDefaultMonitoringRuleDomainProperties, convertMonitoringFromRawAndVerify, } from './monitoring'; export { getNextRun } from './next_run'; diff --git a/x-pack/plugins/alerting/server/lib/monitoring.ts b/x-pack/plugins/alerting/server/lib/monitoring.ts index 9224600eeffc7..bff533cfc591f 100644 --- a/x-pack/plugins/alerting/server/lib/monitoring.ts +++ b/x-pack/plugins/alerting/server/lib/monitoring.ts @@ -12,6 +12,7 @@ import { RuleMonitoringHistory, RuleMonitoringLastRunMetrics, } from '../types'; +import { RuleDomain } from '../application/rule/types'; const INITIAL_LAST_RUN_METRICS: RuleMonitoringLastRunMetrics = { duration: 0, @@ -37,6 +38,23 @@ export const getDefaultMonitoring = (timestamp: string): RawRuleMonitoring => { }; }; +export const getDefaultMonitoringRuleDomainProperties = ( + timestamp: string +): RuleDomain['monitoring'] => { + return { + run: { + history: [], + calculated_metrics: { + success_ratio: 0, + }, + last_run: { + timestamp, + metrics: INITIAL_LAST_RUN_METRICS, + }, + }, + }; +}; + export const resetMonitoringLastRun = (monitoring: RuleMonitoring): RawRuleMonitoring => { const { run, ...restMonitoring } = monitoring; const { last_run: lastRun, ...restRun } = run; diff --git a/x-pack/plugins/alerting/server/routes/bulk_edit_rules.ts b/x-pack/plugins/alerting/server/routes/bulk_edit_rules.ts deleted file mode 100644 index f22d7d2055b09..0000000000000 --- a/x-pack/plugins/alerting/server/routes/bulk_edit_rules.ts +++ /dev/null @@ -1,145 +0,0 @@ -/* - * 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 { IRouter } from '@kbn/core/server'; - -import { ILicenseState, RuleTypeDisabledError, validateDurationSchema } from '../lib'; -import { verifyAccessAndContext, rewriteRule, handleDisabledApiKeysError } from './lib'; -import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../types'; -import { snoozeScheduleSchema } from './snooze_rule'; -import { scheduleIdsSchema } from './unsnooze_rule'; - -const ruleActionSchema = schema.object({ - group: schema.string(), - id: schema.string(), - params: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), - uuid: schema.maybe(schema.string()), - frequency: schema.maybe( - schema.object({ - summary: schema.boolean(), - throttle: schema.nullable(schema.string()), - notifyWhen: schema.oneOf([ - schema.literal('onActionGroupChange'), - schema.literal('onActiveAlert'), - schema.literal('onThrottleInterval'), - ]), - }) - ), -}); - -const operationsSchema = schema.arrayOf( - schema.oneOf([ - schema.object({ - operation: schema.oneOf([ - schema.literal('add'), - schema.literal('delete'), - schema.literal('set'), - ]), - field: schema.literal('tags'), - value: schema.arrayOf(schema.string()), - }), - schema.object({ - operation: schema.oneOf([schema.literal('add'), schema.literal('set')]), - field: schema.literal('actions'), - value: schema.arrayOf(ruleActionSchema), - }), - schema.object({ - operation: schema.literal('set'), - field: schema.literal('schedule'), - value: schema.object({ interval: schema.string({ validate: validateDurationSchema }) }), - }), - schema.object({ - operation: schema.literal('set'), - field: schema.literal('throttle'), - value: schema.nullable(schema.string()), - }), - schema.object({ - operation: schema.literal('set'), - field: schema.literal('notifyWhen'), - value: schema.nullable( - schema.oneOf([ - schema.literal('onActionGroupChange'), - schema.literal('onActiveAlert'), - schema.literal('onThrottleInterval'), - ]) - ), - }), - schema.object({ - operation: schema.oneOf([schema.literal('set')]), - field: schema.literal('snoozeSchedule'), - value: snoozeScheduleSchema, - }), - schema.object({ - operation: schema.oneOf([schema.literal('delete')]), - field: schema.literal('snoozeSchedule'), - value: schema.maybe(scheduleIdsSchema), - }), - schema.object({ - operation: schema.literal('set'), - field: schema.literal('apiKey'), - }), - ]), - { minSize: 1 } -); - -const bodySchema = schema.object({ - filter: schema.maybe(schema.string()), - ids: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })), - operations: operationsSchema, -}); - -interface BuildBulkEditRulesRouteParams { - licenseState: ILicenseState; - path: string; - router: IRouter; -} - -const buildBulkEditRulesRoute = ({ licenseState, path, router }: BuildBulkEditRulesRouteParams) => { - router.post( - { - path, - validate: { - body: bodySchema, - }, - }, - handleDisabledApiKeysError( - router.handleLegacyErrors( - verifyAccessAndContext(licenseState, async function (context, req, res) { - const rulesClient = (await context.alerting).getRulesClient(); - const { filter, operations, ids } = req.body; - - try { - const bulkEditResults = await rulesClient.bulkEdit({ - filter, - ids: ids as string[], - operations, - }); - return res.ok({ - body: { ...bulkEditResults, rules: bulkEditResults.rules.map(rewriteRule) }, - }); - } catch (e) { - if (e instanceof RuleTypeDisabledError) { - return e.sendResponse(res); - } - throw e; - } - }) - ) - ) - ); -}; - -export const bulkEditInternalRulesRoute = ( - router: IRouter, - licenseState: ILicenseState -) => - buildBulkEditRulesRoute({ - licenseState, - path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/_bulk_edit`, - router, - }); diff --git a/x-pack/plugins/alerting/server/routes/index.ts b/x-pack/plugins/alerting/server/routes/index.ts index 8d3c1c7ac5079..f9962d72eb772 100644 --- a/x-pack/plugins/alerting/server/routes/index.ts +++ b/x-pack/plugins/alerting/server/routes/index.ts @@ -13,7 +13,7 @@ import { Observable } from 'rxjs'; import { ILicenseState } from '../lib'; import { defineLegacyRoutes } from './legacy'; import { AlertingRequestHandlerContext } from '../types'; -import { createRuleRoute } from './rule/create'; +import { createRuleRoute } from './rule/apis/create'; import { getRuleRoute, getInternalRuleRoute } from './get_rule'; import { updateRuleRoute } from './update_rule'; import { deleteRuleRoute } from './delete_rule'; @@ -36,7 +36,7 @@ import { muteAlertRoute } from './mute_alert'; import { unmuteAllRuleRoute } from './unmute_all_rule'; import { unmuteAlertRoute } from './unmute_alert'; import { updateRuleApiKeyRoute } from './update_rule_api_key'; -import { bulkEditInternalRulesRoute } from './bulk_edit_rules'; +import { bulkEditInternalRulesRoute } from './rule/apis/bulk_edit/bulk_edit_rules_route'; import { snoozeRuleRoute } from './snooze_rule'; import { unsnoozeRuleRoute } from './unsnooze_rule'; import { runSoonRoute } from './run_soon'; diff --git a/x-pack/plugins/alerting/server/routes/bulk_edit_rules.test.ts b/x-pack/plugins/alerting/server/routes/rule/apis/bulk_edit/bulk_edit_rules_route.test.ts similarity index 89% rename from x-pack/plugins/alerting/server/routes/bulk_edit_rules.test.ts rename to x-pack/plugins/alerting/server/routes/rule/apis/bulk_edit/bulk_edit_rules_route.test.ts index b2819fda6f60f..1b1ed454c5207 100644 --- a/x-pack/plugins/alerting/server/routes/bulk_edit_rules.test.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/bulk_edit/bulk_edit_rules_route.test.ts @@ -7,23 +7,23 @@ import { httpServiceMock } from '@kbn/core/server/mocks'; -import { bulkEditInternalRulesRoute } from './bulk_edit_rules'; -import { licenseStateMock } from '../lib/license_state.mock'; -import { verifyApiAccess } from '../lib/license_api_access'; -import { RuleTypeDisabledError } from '../lib/errors/rule_type_disabled'; -import { mockHandlerArguments } from './_mock_handler_arguments'; -import { rulesClientMock } from '../rules_client.mock'; -import { SanitizedRule } from '../types'; +import { bulkEditInternalRulesRoute } from './bulk_edit_rules_route'; +import { licenseStateMock } from '../../../../lib/license_state.mock'; +import { verifyApiAccess } from '../../../../lib/license_api_access'; +import { RuleTypeDisabledError } from '../../../../lib/errors/rule_type_disabled'; +import { mockHandlerArguments } from '../../../_mock_handler_arguments'; +import { rulesClientMock } from '../../../../rules_client.mock'; +import { SanitizedRule } from '../../../../types'; const rulesClient = rulesClientMock.create(); -jest.mock('../lib/license_api_access', () => ({ +jest.mock('../../../../lib/license_api_access', () => ({ verifyApiAccess: jest.fn(), })); beforeEach(() => { jest.resetAllMocks(); }); -describe('bulkEditInternalRulesRoute', () => { +describe('bulkEditRulesRoute', () => { const mockedAlert: SanitizedRule<{}> = { id: '1', alertTypeId: '1', diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/bulk_edit/bulk_edit_rules_route.ts b/x-pack/plugins/alerting/server/routes/rule/apis/bulk_edit/bulk_edit_rules_route.ts new file mode 100644 index 0000000000000..ae39ceba1ceb3 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/rule/apis/bulk_edit/bulk_edit_rules_route.ts @@ -0,0 +1,84 @@ +/* + * 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 { IRouter } from '@kbn/core/server'; + +import { ILicenseState, RuleTypeDisabledError } from '../../../../lib'; +import { verifyAccessAndContext, handleDisabledApiKeysError } from '../../../lib'; +import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../../../types'; + +import { + bulkEditRulesRequestBodySchemaV1, + BulkEditRulesRequestBodyV1, + BulkEditRulesResponseV1, +} from '../../../../../common/routes/rule/apis/bulk_edit'; +import { Rule } from '../../../../application/rule/types'; +import type { RuleParamsV1 } from '../../../../../common/routes/rule/response'; + +import { transformRuleToRuleResponseV1 } from '../../transforms'; + +interface BuildBulkEditRulesRouteParams { + licenseState: ILicenseState; + path: string; + router: IRouter; +} + +const buildBulkEditRulesRoute = ({ licenseState, path, router }: BuildBulkEditRulesRouteParams) => { + router.post( + { + path, + validate: { + body: bulkEditRulesRequestBodySchemaV1, + }, + }, + handleDisabledApiKeysError( + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const rulesClient = (await context.alerting).getRulesClient(); + const bulkEditData: BulkEditRulesRequestBodyV1 = req.body; + + const { filter, operations, ids } = bulkEditData; + + try { + const bulkEditResults = await rulesClient.bulkEdit({ + filter, + ids, + operations, + }); + + const resultBody: BulkEditRulesResponseV1 = { + body: { + ...bulkEditResults, + rules: bulkEditResults.rules.map((rule) => { + // TODO (http-versioning): Remove this cast, this enables us to move forward + // without fixing all of other solution types + return transformRuleToRuleResponseV1(rule as Rule); + }), + }, + }; + return res.ok(resultBody); + } catch (e) { + if (e instanceof RuleTypeDisabledError) { + return e.sendResponse(res); + } + throw e; + } + }) + ) + ) + ); +}; + +export const bulkEditInternalRulesRoute = ( + router: IRouter, + licenseState: ILicenseState +) => + buildBulkEditRulesRoute({ + licenseState, + path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/_bulk_edit`, + router, + }); diff --git a/x-pack/plugins/alerting/server/routes/rule/create/create_rule_route.test.ts b/x-pack/plugins/alerting/server/routes/rule/apis/create/create_rule_route.test.ts similarity index 97% rename from x-pack/plugins/alerting/server/routes/rule/create/create_rule_route.test.ts rename to x-pack/plugins/alerting/server/routes/rule/apis/create/create_rule_route.test.ts index 63f4789f1289d..e952a72ec3859 100644 --- a/x-pack/plugins/alerting/server/routes/rule/create/create_rule_route.test.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/create/create_rule_route.test.ts @@ -8,20 +8,20 @@ import { pick } from 'lodash'; import { createRuleRoute } from './create_rule_route'; import { httpServiceMock } from '@kbn/core/server/mocks'; -import { licenseStateMock } from '../../../lib/license_state.mock'; -import { verifyApiAccess } from '../../../lib/license_api_access'; -import { mockHandlerArguments } from '../../_mock_handler_arguments'; -import type { CreateRuleRequestBodyV1 } from '../../../../common/routes/rule/create'; -import { rulesClientMock } from '../../../rules_client.mock'; -import { RuleTypeDisabledError } from '../../../lib'; -import { AsApiContract } from '../../lib'; -import { SanitizedRule } from '../../../types'; +import { licenseStateMock } from '../../../../lib/license_state.mock'; +import { verifyApiAccess } from '../../../../lib/license_api_access'; +import { mockHandlerArguments } from '../../../_mock_handler_arguments'; +import type { CreateRuleRequestBodyV1 } from '../../../../../common/routes/rule/apis/create'; +import { rulesClientMock } from '../../../../rules_client.mock'; +import { RuleTypeDisabledError } from '../../../../lib'; +import { AsApiContract } from '../../../lib'; +import { SanitizedRule } from '../../../../types'; import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks'; import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; const rulesClient = rulesClientMock.create(); -jest.mock('../../../lib/license_api_access', () => ({ +jest.mock('../../../../lib/license_api_access', () => ({ verifyApiAccess: jest.fn(), })); diff --git a/x-pack/plugins/alerting/server/routes/rule/create/create_rule_route.ts b/x-pack/plugins/alerting/server/routes/rule/apis/create/create_rule_route.ts similarity index 79% rename from x-pack/plugins/alerting/server/routes/rule/create/create_rule_route.ts rename to x-pack/plugins/alerting/server/routes/rule/apis/create/create_rule_route.ts index 8f11c2cbc46a0..6b28b64284904 100644 --- a/x-pack/plugins/alerting/server/routes/rule/create/create_rule_route.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/create/create_rule_route.ts @@ -5,24 +5,27 @@ * 2.0. */ -import { RuleTypeDisabledError } from '../../../lib'; +import { RuleTypeDisabledError } from '../../../../lib'; import { handleDisabledApiKeysError, verifyAccessAndContext, countUsageOfPredefinedIds, -} from '../../lib'; -import { BASE_ALERTING_API_PATH } from '../../../types'; -import { RouteOptions } from '../..'; +} from '../../../lib'; +import { BASE_ALERTING_API_PATH } from '../../../../types'; +import { RouteOptions } from '../../..'; import type { CreateRuleRequestBodyV1, CreateRuleRequestParamsV1, CreateRuleResponseV1, -} from '../../../../common/routes/rule/create'; -import { createBodySchemaV1, createParamsSchemaV1 } from '../../../../common/routes/rule/create'; -import type { RuleParamsV1 } from '../../../../common/routes/rule/rule_response'; -import { Rule } from '../../../application/rule/types'; +} from '../../../../../common/routes/rule/apis/create'; +import { + createBodySchemaV1, + createParamsSchemaV1, +} from '../../../../../common/routes/rule/apis/create'; +import type { RuleParamsV1 } from '../../../../../common/routes/rule/response'; +import { Rule } from '../../../../application/rule/types'; import { transformCreateBodyV1 } from './transforms'; -import { transformRuleToRuleResponseV1 } from '../transforms'; +import { transformRuleToRuleResponseV1 } from '../../transforms'; export const createRuleRoute = ({ router, licenseState, usageCounter }: RouteOptions) => { router.post( diff --git a/x-pack/plugins/alerting/server/routes/rule/create/index.ts b/x-pack/plugins/alerting/server/routes/rule/apis/create/index.ts similarity index 100% rename from x-pack/plugins/alerting/server/routes/rule/create/index.ts rename to x-pack/plugins/alerting/server/routes/rule/apis/create/index.ts diff --git a/x-pack/plugins/alerting/server/routes/rule/create/transforms/index.ts b/x-pack/plugins/alerting/server/routes/rule/apis/create/transforms/index.ts similarity index 100% rename from x-pack/plugins/alerting/server/routes/rule/create/transforms/index.ts rename to x-pack/plugins/alerting/server/routes/rule/apis/create/transforms/index.ts diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/create/transforms/transform_create_body/latest.ts b/x-pack/plugins/alerting/server/routes/rule/apis/create/transforms/transform_create_body/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/rule/apis/create/transforms/transform_create_body/latest.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './v1'; diff --git a/x-pack/plugins/alerting/server/routes/rule/create/transforms/transform_create_body/v1.ts b/x-pack/plugins/alerting/server/routes/rule/apis/create/transforms/transform_create_body/v1.ts similarity index 87% rename from x-pack/plugins/alerting/server/routes/rule/create/transforms/transform_create_body/v1.ts rename to x-pack/plugins/alerting/server/routes/rule/apis/create/transforms/transform_create_body/v1.ts index 38c081b2168ce..c6b29f4577f4c 100644 --- a/x-pack/plugins/alerting/server/routes/rule/create/transforms/transform_create_body/v1.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/create/transforms/transform_create_body/v1.ts @@ -8,9 +8,9 @@ import type { CreateRuleActionV1, CreateRuleRequestBodyV1, -} from '../../../../../../common/routes/rule/create'; -import type { CreateRuleData } from '../../../../../application/rule/create'; -import type { RuleParams } from '../../../../../application/rule/types'; +} from '../../../../../../../common/routes/rule/apis/create'; +import type { CreateRuleData } from '../../../../../../application/rule/methods/create'; +import type { RuleParams } from '../../../../../../application/rule/types'; const transformCreateBodyActions = (actions: CreateRuleActionV1[]): CreateRuleData['actions'] => { if (!actions) return []; diff --git a/x-pack/plugins/alerting/server/routes/rule/transforms/transform_rule_to_rule_response/v1.ts b/x-pack/plugins/alerting/server/routes/rule/transforms/transform_rule_to_rule_response/v1.ts index 38e6da6c11429..9c38431f24b9e 100644 --- a/x-pack/plugins/alerting/server/routes/rule/transforms/transform_rule_to_rule_response/v1.ts +++ b/x-pack/plugins/alerting/server/routes/rule/transforms/transform_rule_to_rule_response/v1.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { RuleResponseV1, RuleParamsV1 } from '../../../../../common/routes/rule/rule_response'; +import { RuleResponseV1, RuleParamsV1 } from '../../../../../common/routes/rule/response'; import { Rule, RuleLastRun, RuleParams } from '../../../../application/rule/types'; const transformRuleLastRun = (lastRun: RuleLastRun): RuleResponseV1['last_run'] => { diff --git a/x-pack/plugins/alerting/server/rules_client/common/api_key_as_alert_attributes.ts b/x-pack/plugins/alerting/server/rules_client/common/api_key_as_alert_attributes.ts index 841ffdd1703a7..c1941a8d0fa4b 100644 --- a/x-pack/plugins/alerting/server/rules_client/common/api_key_as_alert_attributes.ts +++ b/x-pack/plugins/alerting/server/rules_client/common/api_key_as_alert_attributes.ts @@ -7,7 +7,12 @@ import { RawRule } from '../../types'; import { CreateAPIKeyResult } from '../types'; +import { RuleDomain } from '../../application/rule/types'; +/** + * @deprecated TODO (http-versioning) make sure this is deprecated + * once all of the RawRules are phased out + */ export function apiKeyAsAlertAttributes( apiKey: CreateAPIKeyResult | null, username: string | null, @@ -25,3 +30,21 @@ export function apiKeyAsAlertAttributes( apiKeyCreatedByUser: null, }; } + +export function apiKeyAsRuleDomainProperties( + apiKey: CreateAPIKeyResult | null, + username: string | null, + createdByUser: boolean +): Pick { + return apiKey && apiKey.apiKeysEnabled + ? { + apiKeyOwner: username, + apiKey: Buffer.from(`${apiKey.result.id}:${apiKey.result.api_key}`).toString('base64'), + apiKeyCreatedByUser: createdByUser, + } + : { + apiKeyOwner: null, + apiKey: null, + apiKeyCreatedByUser: null, + }; +} diff --git a/x-pack/plugins/alerting/server/rules_client/common/apply_bulk_edit_operation.ts b/x-pack/plugins/alerting/server/rules_client/common/apply_bulk_edit_operation.ts index e20624c77b2c5..c718ce0d5fa32 100644 --- a/x-pack/plugins/alerting/server/rules_client/common/apply_bulk_edit_operation.ts +++ b/x-pack/plugins/alerting/server/rules_client/common/apply_bulk_edit_operation.ts @@ -32,7 +32,9 @@ export const applyBulkEditOperation = (operation: BulkEditOper switch (operation.operation) { case 'set': - set(rule, operation.field, operation.value); + if (operation.field !== 'apiKey') { + set(rule, operation.field, operation.value); + } break; case 'add': diff --git a/x-pack/plugins/alerting/server/rules_client/common/index.ts b/x-pack/plugins/alerting/server/rules_client/common/index.ts index ab380a6ca178b..6f8835d0c7260 100644 --- a/x-pack/plugins/alerting/server/rules_client/common/index.ts +++ b/x-pack/plugins/alerting/server/rules_client/common/index.ts @@ -13,7 +13,10 @@ export { applyBulkEditOperation } from './apply_bulk_edit_operation'; export { buildKueryNodeFilter } from './build_kuery_node_filter'; export { generateAPIKeyName } from './generate_api_key_name'; export * from './mapped_params_utils'; -export { apiKeyAsAlertAttributes } from './api_key_as_alert_attributes'; +export { + apiKeyAsAlertAttributes, + apiKeyAsRuleDomainProperties, +} from './api_key_as_alert_attributes'; export * from './inject_references'; export { parseDate } from './parse_date'; export { includeFieldsRequiredForAuthentication } from './include_fields_required_for_authentication'; diff --git a/x-pack/plugins/alerting/server/rules_client/common/inject_references.ts b/x-pack/plugins/alerting/server/rules_client/common/inject_references.ts index 07565240ed5c4..4aa9474c5873c 100644 --- a/x-pack/plugins/alerting/server/rules_client/common/inject_references.ts +++ b/x-pack/plugins/alerting/server/rules_client/common/inject_references.ts @@ -10,6 +10,7 @@ import { omit } from 'lodash'; import { SavedObjectReference, SavedObjectAttributes } from '@kbn/core/server'; import { UntypedNormalizedRuleType } from '../../rule_type_registry'; import { Rule, RawRule, RuleTypeParams } from '../../types'; +import { RuleActionAttributes } from '../../data/rule/types'; import { preconfiguredConnectorActionRefPrefix, extractedSavedObjectParamReferenceNamePrefix, @@ -17,7 +18,7 @@ import { export function injectReferencesIntoActions( alertId: string, - actions: RawRule['actions'], + actions: RawRule['actions'] | RuleActionAttributes[], references: SavedObjectReference[] ) { return actions.map((action) => { diff --git a/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_edit_conflicts.ts b/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_edit_conflicts.ts index 8d03d01df5af4..1a506b1b3fa86 100644 --- a/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_edit_conflicts.ts +++ b/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_edit_conflicts.ts @@ -12,7 +12,7 @@ import { Logger, SavedObjectsBulkUpdateObject, SavedObjectsUpdateResponse } from import { BulkActionSkipResult } from '../../../common/bulk_edit'; import { convertRuleIdsToKueryNode } from '../../lib'; import { BulkOperationError } from '../types'; -import { RawRule } from '../../types'; +import { RuleAttributes } from '../../data/rule/types'; import { waitBeforeNextRetry, RETRY_IF_CONFLICTS_ATTEMPTS } from './wait_before_next_retry'; // max number of failed SO ids in one retry filter @@ -20,15 +20,15 @@ const MaxIdsNumberInRetryFilter = 1000; type BulkEditOperation = (filter: KueryNode | null) => Promise<{ apiKeysToInvalidate: string[]; - rules: Array>; - resultSavedObjects: Array>; + rules: Array>; + resultSavedObjects: Array>; errors: BulkOperationError[]; skipped: BulkActionSkipResult[]; }>; interface ReturnRetry { apiKeysToInvalidate: string[]; - results: Array>; + results: Array>; errors: BulkOperationError[]; skipped: BulkActionSkipResult[]; } @@ -54,7 +54,7 @@ export const retryIfBulkEditConflicts = async ( filter: KueryNode | null, retries: number = RETRY_IF_CONFLICTS_ATTEMPTS, accApiKeysToInvalidate: string[] = [], - accResults: Array> = [], + accResults: Array> = [], accErrors: BulkOperationError[] = [], accSkipped: BulkActionSkipResult[] = [] ): Promise => { diff --git a/x-pack/plugins/alerting/server/rules_client/common/snooze_utils.ts b/x-pack/plugins/alerting/server/rules_client/common/snooze_utils.ts index 2a6d1b3b06e7a..edf61427b8339 100644 --- a/x-pack/plugins/alerting/server/rules_client/common/snooze_utils.ts +++ b/x-pack/plugins/alerting/server/rules_client/common/snooze_utils.ts @@ -7,8 +7,16 @@ import { i18n } from '@kbn/i18n'; import { RawRule, RuleSnoozeSchedule } from '../../types'; +import { + RuleDomain, + RuleParams, + RuleSnoozeSchedule as RuleDomainSnoozeSchedule, +} from '../../application/rule/types'; import { getActiveScheduledSnoozes } from '../../lib/is_rule_snoozed'; +/** + * @deprecated TODO (http-versioning): Deprecate this once we fix all RawRule types + */ export function getSnoozeAttributes(attributes: RawRule, snoozeSchedule: RuleSnoozeSchedule) { // If duration is -1, instead mute all const { id: snoozeId, duration } = snoozeSchedule; @@ -16,34 +24,40 @@ export function getSnoozeAttributes(attributes: RawRule, snoozeSchedule: RuleSno if (duration === -1) { return { muteAll: true, - snoozeSchedule: clearUnscheduledSnooze(attributes), + snoozeSchedule: clearUnscheduledSnoozeAttributes(attributes), }; } return { snoozeSchedule: (snoozeId - ? clearScheduledSnoozesById(attributes, [snoozeId]) - : clearUnscheduledSnooze(attributes) + ? clearScheduledSnoozesAttributesById(attributes, [snoozeId]) + : clearUnscheduledSnoozeAttributes(attributes) ).concat(snoozeSchedule), muteAll: false, }; } -export function getBulkSnoozeAttributes(attributes: RawRule, snoozeSchedule: RuleSnoozeSchedule) { +export function getBulkSnooze( + rule: RuleDomain, + snoozeSchedule: RuleDomainSnoozeSchedule +): { + muteAll: RuleDomain['muteAll']; + snoozeSchedule: RuleDomain['snoozeSchedule']; +} { // If duration is -1, instead mute all const { id: snoozeId, duration } = snoozeSchedule; if (duration === -1) { return { muteAll: true, - snoozeSchedule: clearUnscheduledSnooze(attributes), + snoozeSchedule: clearUnscheduledSnooze(rule), }; } // Bulk adding snooze schedule, don't touch the existing snooze/indefinite snooze if (snoozeId) { - const existingSnoozeSchedules = attributes.snoozeSchedule || []; + const existingSnoozeSchedules = rule.snoozeSchedule || []; return { - muteAll: attributes.muteAll, + muteAll: rule.muteAll, snoozeSchedule: [...existingSnoozeSchedules, snoozeSchedule], }; } @@ -51,14 +65,17 @@ export function getBulkSnoozeAttributes(attributes: RawRule, snoozeSchedule: Rul // Bulk snoozing, don't touch the existing snooze schedules return { muteAll: false, - snoozeSchedule: [...clearUnscheduledSnooze(attributes), snoozeSchedule], + snoozeSchedule: [...(clearUnscheduledSnooze(rule) || []), snoozeSchedule], }; } +/** + * @deprecated TODO (http-versioning): Deprecate this once we fix all RawRule types + */ export function getUnsnoozeAttributes(attributes: RawRule, scheduleIds?: string[]) { const snoozeSchedule = scheduleIds - ? clearScheduledSnoozesById(attributes, scheduleIds) - : clearCurrentActiveSnooze(attributes); + ? clearScheduledSnoozesAttributesById(attributes, scheduleIds) + : clearCurrentActiveSnoozeAttributes(attributes); return { snoozeSchedule, @@ -66,43 +83,65 @@ export function getUnsnoozeAttributes(attributes: RawRule, scheduleIds?: string[ }; } -export function getBulkUnsnoozeAttributes(attributes: RawRule, scheduleIds?: string[]) { +export function getBulkUnsnooze( + rule: RuleDomain, + scheduleIds?: string[] +) { // Bulk removing snooze schedules, don't touch the current snooze/indefinite snooze if (scheduleIds) { - const newSchedules = clearScheduledSnoozesById(attributes, scheduleIds); + const newSchedules = clearScheduledSnoozesById(rule, scheduleIds); // Unscheduled snooze is also known as snooze now - const unscheduledSnooze = - attributes.snoozeSchedule?.filter((s) => typeof s.id === 'undefined') || []; + const unscheduledSnooze = rule.snoozeSchedule?.filter((s) => typeof s.id === 'undefined') || []; return { snoozeSchedule: [...unscheduledSnooze, ...newSchedules], - muteAll: attributes.muteAll, + muteAll: rule.muteAll, }; } // Bulk unsnoozing, don't touch current snooze schedules that are NOT active return { - snoozeSchedule: clearCurrentActiveSnooze(attributes), + snoozeSchedule: clearCurrentActiveSnooze(rule), muteAll: false, }; } -export function clearUnscheduledSnooze(attributes: RawRule) { +/** + * @deprecated TODO (http-versioning): Deprecate this once we fix all RawRule types + */ +export function clearUnscheduledSnoozeAttributes(attributes: RawRule) { // Clear any snoozes that have no ID property. These are "simple" snoozes created with the quick UI, e.g. snooze for 3 days starting now return attributes.snoozeSchedule ? attributes.snoozeSchedule.filter((s) => typeof s.id !== 'undefined') : []; } -export function clearScheduledSnoozesById(attributes: RawRule, ids: string[]) { +export function clearUnscheduledSnooze(rule: RuleDomain) { + return rule.snoozeSchedule ? rule.snoozeSchedule.filter((s) => typeof s.id !== 'undefined') : []; +} + +/** + * @deprecated TODO (http-versioning): Deprecate this once we fix all RawRule types + */ +export function clearScheduledSnoozesAttributesById(attributes: RawRule, ids: string[]) { return attributes.snoozeSchedule ? attributes.snoozeSchedule.filter((s) => s.id && !ids.includes(s.id)) : []; } -export function clearCurrentActiveSnooze(attributes: RawRule) { +export function clearScheduledSnoozesById( + rule: RuleDomain, + ids: string[] +) { + return rule.snoozeSchedule ? rule.snoozeSchedule.filter((s) => s.id && !ids.includes(s.id)) : []; +} + +/** + * @deprecated TODO (http-versioning): Deprecate this once we fix all RawRule types + */ +export function clearCurrentActiveSnoozeAttributes(attributes: RawRule) { // First attempt to cancel a simple (unscheduled) snooze - const clearedUnscheduledSnoozes = clearUnscheduledSnooze(attributes); + const clearedUnscheduledSnoozes = clearUnscheduledSnoozeAttributes(attributes); // Now clear any scheduled snoozes that are currently active and never recur const activeSnoozes = getActiveScheduledSnoozes(attributes); const activeSnoozeIds = activeSnoozes?.map((s) => s.id) ?? []; @@ -127,7 +166,37 @@ export function clearCurrentActiveSnooze(attributes: RawRule) { return clearedSnoozesAndSkippedRecurringSnoozes; } -export function verifySnoozeScheduleLimit(attributes: Partial) { +export function clearCurrentActiveSnooze(rule: RuleDomain) { + // First attempt to cancel a simple (unscheduled) snooze + const clearedUnscheduledSnoozes = clearUnscheduledSnooze(rule); + // Now clear any scheduled snoozes that are currently active and never recur + const activeSnoozes = getActiveScheduledSnoozes(rule); + const activeSnoozeIds = activeSnoozes?.map((s) => s.id) ?? []; + const recurringSnoozesToSkip: string[] = []; + const clearedNonRecurringActiveSnoozes = clearedUnscheduledSnoozes.filter((s) => { + if (!activeSnoozeIds.includes(s.id!)) return true; + // Check if this is a recurring snooze, and return true if so + if (s.rRule.freq && s.rRule.count !== 1) { + recurringSnoozesToSkip.push(s.id!); + return true; + } + }); + const clearedSnoozesAndSkippedRecurringSnoozes = clearedNonRecurringActiveSnoozes.map((s) => { + if (s.id && !recurringSnoozesToSkip.includes(s.id)) return s; + const currentRecurrence = activeSnoozes?.find((a) => a.id === s.id)?.lastOccurrence; + if (!currentRecurrence) return s; + return { + ...s, + skipRecurrences: (s.skipRecurrences ?? []).concat(currentRecurrence.toISOString()), + }; + }); + return clearedSnoozesAndSkippedRecurringSnoozes; +} + +/** + * @deprecated TODO (http-versioning): Deprecate this once we fix all RawRule types + */ +export function verifySnoozeAttributeScheduleLimit(attributes: Partial) { const schedules = attributes.snoozeSchedule?.filter((snooze) => snooze.id); if (schedules && schedules.length > 5) { throw Error( @@ -137,3 +206,16 @@ export function verifySnoozeScheduleLimit(attributes: Partial) { ); } } + +export function verifySnoozeScheduleLimit( + snoozeSchedule: RuleDomain['snoozeSchedule'] +) { + const schedules = snoozeSchedule?.filter((snooze) => snooze.id); + if (schedules && schedules.length > 5) { + throw Error( + i18n.translate('xpack.alerting.rulesClient.snoozeSchedule.limitReached', { + defaultMessage: 'Rule cannot have more than 5 snooze schedules', + }) + ); + } +} diff --git a/x-pack/plugins/alerting/server/rules_client/lib/create_rule_saved_object.ts b/x-pack/plugins/alerting/server/rules_client/lib/create_rule_saved_object.ts index 6d45866dc73d3..14662953ba73b 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/create_rule_saved_object.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/create_rule_saved_object.ts @@ -68,8 +68,8 @@ export async function createRuleSavedObject createRuleSo({ ruleAttributes: updateMeta(context, rawRule as RawRule) as RuleAttributes, - savedObjectClient: context.unsecuredSavedObjectsClient, - savedObjectCreateOptions: { + savedObjectsClient: context.unsecuredSavedObjectsClient, + savedObjectsCreateOptions: { ...options, references, id: ruleId, @@ -101,7 +101,7 @@ export async function createRuleSavedObject updateRuleSo({ - savedObjectClient: context.unsecuredSavedObjectsClient, + savedObjectsClient: context.unsecuredSavedObjectsClient, id: createdAlert.id, updateRuleAttributes: { scheduledTaskId, diff --git a/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts b/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts index b87d71aa275bc..a78516b34cad0 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts @@ -5,898 +5,9 @@ * 2.0. */ -import pMap from 'p-map'; -import Boom from '@hapi/boom'; -import { cloneDeep } from 'lodash'; -import { AlertConsumers } from '@kbn/rule-data-utils'; -import { KueryNode, nodeBuilder } from '@kbn/es-query'; -import { - SavedObjectsBulkUpdateObject, - SavedObjectsFindResult, - SavedObjectsUpdateResponse, -} from '@kbn/core/server'; -import { BulkActionSkipResult } from '../../../common/bulk_edit'; -import { - RawRule, - SanitizedRule, - RuleTypeParams, - Rule, - RuleSnoozeSchedule, - RuleWithLegacyId, - RuleTypeRegistry, - RawRuleAction, - RuleNotifyWhen, -} from '../../types'; -import { - validateRuleTypeParams, - getRuleNotifyWhenType, - validateMutatedRuleTypeParams, - convertRuleIdsToKueryNode, -} from '../../lib'; -import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization'; -import { parseDuration } from '../../../common/parse_duration'; -import { bulkMarkApiKeysForInvalidation } from '../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation'; -import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; -import { - retryIfBulkEditConflicts, - applyBulkEditOperation, - buildKueryNodeFilter, - injectReferencesIntoActions, - getBulkSnoozeAttributes, - getBulkUnsnoozeAttributes, - verifySnoozeScheduleLimit, - injectReferencesIntoParams, -} from '../common'; -import { - alertingAuthorizationFilterOpts, - MAX_RULES_NUMBER_FOR_BULK_OPERATION, - RULE_TYPE_CHECKS_CONCURRENCY, - API_KEY_GENERATE_CONCURRENCY, -} from '../common/constants'; -import { getMappedParams } from '../common/mapped_params_utils'; -import { - getAlertFromRaw, - extractReferences, - validateActions, - updateMeta, - addGeneratedActionValues, - createNewAPIKeySet, -} from '../lib'; -import { - NormalizedAlertAction, - BulkOperationError, - RuleBulkOperationAggregation, - RulesClientContext, - NormalizedAlertActionWithGeneratedValues, -} from '../types'; - -import { migrateLegacyActions } from '../lib'; - -export type BulkEditFields = keyof Pick< - Rule, - 'actions' | 'tags' | 'schedule' | 'throttle' | 'notifyWhen' | 'snoozeSchedule' | 'apiKey' ->; - -export const bulkEditFieldsToExcludeFromRevisionUpdates: ReadonlySet = - new Set(['snoozeSchedule', 'apiKey']); - -export type BulkEditOperation = - | { - operation: 'add' | 'delete' | 'set'; - field: Extract; - value: string[]; - } - | { - operation: 'add' | 'set'; - field: Extract; - value: NormalizedAlertAction[]; - } - | { - operation: 'set'; - field: Extract; - value: Rule['schedule']; - } - | { - operation: 'set'; - field: Extract; - value: Rule['throttle']; - } - | { - operation: 'set'; - field: Extract; - value: Rule['notifyWhen']; - } - | { - operation: 'set'; - field: Extract; - value: RuleSnoozeSchedule; - } - | { - operation: 'delete'; - field: Extract; - value?: string[]; - } - | { - operation: 'set'; - field: Extract; - value?: undefined; - }; - -type ApiKeysMap = Map< - string, - { - oldApiKey?: string; - newApiKey?: string; - oldApiKeyCreatedByUser?: boolean | null; - newApiKeyCreatedByUser?: boolean | null; - } ->; - -type ApiKeyAttributes = Pick; - -type RuleType = ReturnType; - +// TODO (http-versioning): This file exists only to provide the type export for +// security solution, once we version all of our types we can remove this file export interface RuleParamsModifierResult { modifiedParams: Params; isParamsUpdateSkipped: boolean; } - -export type RuleParamsModifier = ( - params: Params -) => Promise>; - -export type ShouldIncrementRevision = ( - params?: RuleTypeParams -) => boolean; - -export interface BulkEditOptionsFilter { - filter?: string | KueryNode; - operations: BulkEditOperation[]; - paramsModifier?: RuleParamsModifier; - shouldIncrementRevision?: ShouldIncrementRevision; -} - -export interface BulkEditOptionsIds { - ids: string[]; - operations: BulkEditOperation[]; - paramsModifier?: RuleParamsModifier; - shouldIncrementRevision?: ShouldIncrementRevision; -} - -export type BulkEditOptions = - | BulkEditOptionsFilter - | BulkEditOptionsIds; - -export async function bulkEdit( - context: RulesClientContext, - options: BulkEditOptions -): Promise<{ - rules: Array>; - skipped: BulkActionSkipResult[]; - errors: BulkOperationError[]; - total: number; -}> { - const queryFilter = (options as BulkEditOptionsFilter).filter; - const ids = (options as BulkEditOptionsIds).ids; - - if (ids && queryFilter) { - throw Boom.badRequest( - "Both 'filter' and 'ids' are supplied. Define either 'ids' or 'filter' properties in method arguments" - ); - } - - const qNodeQueryFilter = buildKueryNodeFilter(queryFilter); - - const qNodeFilter = ids ? convertRuleIdsToKueryNode(ids) : qNodeQueryFilter; - let authorizationTuple; - try { - authorizationTuple = await context.authorization.getFindAuthorizationFilter( - AlertingAuthorizationEntity.Rule, - alertingAuthorizationFilterOpts - ); - } catch (error) { - context.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.BULK_EDIT, - error, - }) - ); - throw error; - } - const { filter: authorizationFilter } = authorizationTuple; - const qNodeFilterWithAuth = - authorizationFilter && qNodeFilter - ? nodeBuilder.and([qNodeFilter, authorizationFilter as KueryNode]) - : qNodeFilter; - - const { aggregations, total } = await context.unsecuredSavedObjectsClient.find< - RawRule, - RuleBulkOperationAggregation - >({ - filter: qNodeFilterWithAuth, - page: 1, - perPage: 0, - type: 'alert', - aggs: { - alertTypeId: { - multi_terms: { - terms: [ - { field: 'alert.attributes.alertTypeId' }, - { field: 'alert.attributes.consumer' }, - ], - }, - }, - }, - }); - - if (total > MAX_RULES_NUMBER_FOR_BULK_OPERATION) { - throw Boom.badRequest( - `More than ${MAX_RULES_NUMBER_FOR_BULK_OPERATION} rules matched for bulk edit` - ); - } - const buckets = aggregations?.alertTypeId.buckets; - - if (buckets === undefined) { - throw Error('No rules found for bulk edit'); - } - - await pMap( - buckets, - async ({ key: [ruleType, consumer] }) => { - context.ruleTypeRegistry.ensureRuleTypeEnabled(ruleType); - - try { - await context.authorization.ensureAuthorized({ - ruleTypeId: ruleType, - consumer, - operation: WriteOperations.BulkEdit, - entity: AlertingAuthorizationEntity.Rule, - }); - } catch (error) { - context.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.BULK_EDIT, - error, - }) - ); - throw error; - } - }, - { concurrency: RULE_TYPE_CHECKS_CONCURRENCY } - ); - - const { apiKeysToInvalidate, results, errors, skipped } = await retryIfBulkEditConflicts( - context.logger, - `rulesClient.update('operations=${JSON.stringify(options.operations)}, paramsModifier=${ - options.paramsModifier ? '[Function]' : undefined - }', shouldIncrementRevision=${options.shouldIncrementRevision ? '[Function]' : undefined}')`, - (filterKueryNode: KueryNode | null) => - bulkEditOcc(context, { - filter: filterKueryNode, - operations: options.operations, - paramsModifier: options.paramsModifier, - shouldIncrementRevision: options.shouldIncrementRevision, - }), - qNodeFilterWithAuth - ); - - if (apiKeysToInvalidate.length > 0) { - await bulkMarkApiKeysForInvalidation( - { apiKeys: apiKeysToInvalidate }, - context.logger, - context.unsecuredSavedObjectsClient - ); - } - - const updatedRules = results.map(({ id, attributes, references }) => { - return getAlertFromRaw( - context, - id, - attributes.alertTypeId as string, - attributes as RawRule, - references, - false, - false, - false, - false - ); - }); - - await bulkUpdateSchedules(context, options.operations, updatedRules); - - return { rules: updatedRules, skipped, errors, total }; -} - -async function bulkEditOcc( - context: RulesClientContext, - { - filter, - operations, - paramsModifier, - shouldIncrementRevision, - }: { - filter: KueryNode | null; - operations: BulkEditOptions['operations']; - paramsModifier: BulkEditOptions['paramsModifier']; - shouldIncrementRevision?: BulkEditOptions['shouldIncrementRevision']; - } -): Promise<{ - apiKeysToInvalidate: string[]; - rules: Array>; - resultSavedObjects: Array>; - errors: BulkOperationError[]; - skipped: BulkActionSkipResult[]; -}> { - const rulesFinder = - await context.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser( - { - filter, - type: 'alert', - perPage: 100, - ...(context.namespace ? { namespaces: [context.namespace] } : undefined), - } - ); - - const rules: Array> = []; - const skipped: BulkActionSkipResult[] = []; - const errors: BulkOperationError[] = []; - const apiKeysMap: ApiKeysMap = new Map(); - const username = await context.getUserName(); - - for await (const response of rulesFinder.find()) { - await pMap( - response.saved_objects, - async (rule: SavedObjectsFindResult) => - updateRuleAttributesAndParamsInMemory({ - context, - rule, - operations, - paramsModifier, - apiKeysMap, - rules, - skipped, - errors, - username, - shouldIncrementRevision, - }), - { concurrency: API_KEY_GENERATE_CONCURRENCY } - ); - } - await rulesFinder.close(); - - const { result, apiKeysToInvalidate } = - rules.length > 0 - ? await saveBulkUpdatedRules(context, rules, apiKeysMap) - : { - result: { saved_objects: [] }, - apiKeysToInvalidate: [], - }; - - return { - apiKeysToInvalidate, - resultSavedObjects: result.saved_objects, - errors, - rules, - skipped, - }; -} - -async function bulkUpdateSchedules( - context: RulesClientContext, - operations: BulkEditOperation[], - updatedRules: Array -): Promise { - const scheduleOperation = operations.find( - ( - operation - ): operation is Extract }> => - operation.field === 'schedule' - ); - - if (!scheduleOperation?.value) { - return; - } - const taskIds = updatedRules.reduce((acc, rule) => { - if (rule.scheduledTaskId) { - acc.push(rule.scheduledTaskId); - } - return acc; - }, []); - - try { - await context.taskManager.bulkUpdateSchedules(taskIds, scheduleOperation.value); - context.logger.debug( - `Successfully updated schedules for underlying tasks: ${taskIds.join(', ')}` - ); - } catch (error) { - context.logger.error( - `Failure to update schedules for underlying tasks: ${taskIds.join( - ', ' - )}. TaskManager bulkUpdateSchedules failed with Error: ${error.message}` - ); - } -} - -async function updateRuleAttributesAndParamsInMemory({ - context, - rule, - operations, - paramsModifier, - apiKeysMap, - rules, - skipped, - errors, - username, - shouldIncrementRevision = () => true, -}: { - context: RulesClientContext; - rule: SavedObjectsFindResult; - operations: BulkEditOptions['operations']; - paramsModifier: BulkEditOptions['paramsModifier']; - apiKeysMap: ApiKeysMap; - rules: Array>; - skipped: BulkActionSkipResult[]; - errors: BulkOperationError[]; - username: string | null; - shouldIncrementRevision: BulkEditOptions['shouldIncrementRevision']; -}): Promise { - try { - if (rule.attributes.apiKey) { - apiKeysMap.set(rule.id, { - oldApiKey: rule.attributes.apiKey, - oldApiKeyCreatedByUser: rule.attributes.apiKeyCreatedByUser, - }); - } - - const ruleType = context.ruleTypeRegistry.get(rule.attributes.alertTypeId); - - await ensureAuthorizationForBulkUpdate(context, operations, rule); - - // migrate legacy actions only for SIEM rules - const migratedActions = await migrateLegacyActions(context, { - ruleId: rule.id, - actions: rule.attributes.actions, - references: rule.references, - attributes: rule.attributes, - }); - - if (migratedActions.hasLegacyActions) { - rule.attributes.actions = migratedActions.resultedActions; - rule.references = migratedActions.resultedReferences; - } - - const { attributes, ruleActions, hasUpdateApiKeyOperation, isAttributesUpdateSkipped } = - await getUpdatedAttributesFromOperations(context, operations, rule, ruleType); - - validateScheduleInterval(context, attributes.schedule.interval, ruleType.id, rule.id); - - const params = injectReferencesIntoParams( - rule.id, - ruleType, - attributes.params, - rule.references || [] - ); - const { modifiedParams: ruleParams, isParamsUpdateSkipped } = paramsModifier - ? await paramsModifier(params) - : { - modifiedParams: params, - isParamsUpdateSkipped: true, - }; - - // Increment revision if params ended up being modified AND it wasn't already incremented as part of attribute update - if ( - shouldIncrementRevision(ruleParams) && - !isParamsUpdateSkipped && - rule.attributes.revision === attributes.revision - ) { - attributes.revision += 1; - } - - // If neither attributes nor parameters were updated, mark - // the rule as skipped and continue to the next rule. - if (isAttributesUpdateSkipped && isParamsUpdateSkipped) { - skipped.push({ - id: rule.id, - name: rule.attributes.name, - skip_reason: 'RULE_NOT_MODIFIED', - }); - return; - } - - // validate rule params - const validatedAlertTypeParams = validateRuleTypeParams(ruleParams, ruleType.validate.params); - const validatedMutatedAlertTypeParams = validateMutatedRuleTypeParams( - validatedAlertTypeParams, - rule.attributes.params, - ruleType.validate.params - ); - - const { - actions: rawAlertActions, - references, - params: updatedParams, - } = await extractReferences( - context, - ruleType, - ruleActions.actions as NormalizedAlertActionWithGeneratedValues[], - validatedMutatedAlertTypeParams - ); - - const { apiKeyAttributes } = await prepareApiKeys( - context, - rule, - ruleType, - apiKeysMap, - attributes, - hasUpdateApiKeyOperation, - username - ); - - const { updatedAttributes } = updateAttributes( - context, - attributes, - apiKeyAttributes, - updatedParams, - rawAlertActions, - username - ); - - rules.push({ - ...rule, - references, - attributes: updatedAttributes, - }); - } catch (error) { - errors.push({ - message: error.message, - rule: { - id: rule.id, - name: rule.attributes?.name, - }, - }); - context.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.BULK_EDIT, - error, - }) - ); - } -} - -async function ensureAuthorizationForBulkUpdate( - context: RulesClientContext, - operations: BulkEditOperation[], - rule: SavedObjectsFindResult -): Promise { - if (rule.attributes.actions.length === 0) { - return; - } - - for (const operation of operations) { - const { field } = operation; - if (field === 'snoozeSchedule' || field === 'apiKey') { - try { - await context.actionsAuthorization.ensureAuthorized({ operation: 'execute' }); - break; - } catch (error) { - throw Error(`Rule not authorized for bulk ${field} update - ${error.message}`); - } - } - } -} - -async function getUpdatedAttributesFromOperations( - context: RulesClientContext, - operations: BulkEditOperation[], - rule: SavedObjectsFindResult, - ruleType: RuleType -) { - let attributes = cloneDeep(rule.attributes); - let ruleActions = { - actions: injectReferencesIntoActions( - rule.id, - rule.attributes.actions || [], - rule.references || [] - ), - }; - - let hasUpdateApiKeyOperation = false; - let isAttributesUpdateSkipped = true; - - for (const operation of operations) { - // Check if the update should be skipped for the current action. - // If it should, save the skip reasons in attributesUpdateSkipReasons - // and continue to the next operation before without - // the `isAttributesUpdateSkipped` flag to false. - switch (operation.field) { - case 'actions': { - const updatedOperation = { - ...operation, - value: addGeneratedActionValues(operation.value), - }; - - try { - await validateActions(context, ruleType, { - ...attributes, - actions: updatedOperation.value, - }); - } catch (e) { - // If validateActions fails on the first attempt, it may be because of legacy rule-level frequency params - attributes = await attemptToMigrateLegacyFrequency( - context, - updatedOperation, - attributes, - ruleType - ); - } - - const { modifiedAttributes, isAttributeModified } = applyBulkEditOperation( - updatedOperation, - ruleActions - ); - if (isAttributeModified) { - ruleActions = modifiedAttributes; - isAttributesUpdateSkipped = false; - } - - break; - } - case 'snoozeSchedule': { - // Silently skip adding snooze or snooze schedules on security - // rules until we implement snoozing of their rules - if (rule.attributes.consumer === AlertConsumers.SIEM) { - // While the rule is technically not updated, we are still marking - // the rule as updated in case of snoozing, until support - // for snoozing is added. - isAttributesUpdateSkipped = false; - break; - } - if (operation.operation === 'set') { - const snoozeAttributes = getBulkSnoozeAttributes(rule.attributes, operation.value); - try { - verifySnoozeScheduleLimit(snoozeAttributes); - } catch (error) { - throw Error(`Error updating rule: could not add snooze - ${error.message}`); - } - attributes = { - ...attributes, - ...snoozeAttributes, - }; - } - if (operation.operation === 'delete') { - const idsToDelete = operation.value && [...operation.value]; - if (idsToDelete?.length === 0) { - attributes.snoozeSchedule?.forEach((schedule) => { - if (schedule.id) { - idsToDelete.push(schedule.id); - } - }); - } - attributes = { - ...attributes, - ...getBulkUnsnoozeAttributes(attributes, idsToDelete), - }; - } - isAttributesUpdateSkipped = false; - break; - } - case 'apiKey': { - hasUpdateApiKeyOperation = true; - isAttributesUpdateSkipped = false; - break; - } - default: { - if (operation.field === 'schedule') { - validateScheduleOperation(operation.value, attributes.actions, rule.id); - } - const { modifiedAttributes, isAttributeModified } = applyBulkEditOperation( - operation, - rule.attributes - ); - - if (isAttributeModified) { - attributes = { - ...attributes, - ...modifiedAttributes, - }; - isAttributesUpdateSkipped = false; - } - } - } - // Only increment revision if update wasn't skipped and `operation.field` should result in a revision increment - if ( - !isAttributesUpdateSkipped && - !bulkEditFieldsToExcludeFromRevisionUpdates.has(operation.field) && - rule.attributes.revision - attributes.revision === 0 - ) { - attributes.revision += 1; - } - } - return { - attributes, - ruleActions, - hasUpdateApiKeyOperation, - isAttributesUpdateSkipped, - }; -} - -function validateScheduleInterval( - context: RulesClientContext, - scheduleInterval: string, - ruleTypeId: string, - ruleId: string -): void { - if (!scheduleInterval) { - return; - } - const isIntervalInvalid = parseDuration(scheduleInterval) < context.minimumScheduleIntervalInMs; - if (isIntervalInvalid && context.minimumScheduleInterval.enforce) { - throw Error( - `Error updating rule: the interval is less than the allowed minimum interval of ${context.minimumScheduleInterval.value}` - ); - } else if (isIntervalInvalid && !context.minimumScheduleInterval.enforce) { - context.logger.warn( - `Rule schedule interval (${scheduleInterval}) for "${ruleTypeId}" rule type with ID "${ruleId}" is less than the minimum value (${context.minimumScheduleInterval.value}). Running rules at this interval may impact alerting performance. Set "xpack.alerting.rules.minimumScheduleInterval.enforce" to true to prevent such changes.` - ); - } -} - -/** - * Validate that updated schedule interval is not longer than any of the existing action frequencies - * @param schedule Schedule interval that user tries to set - * @param actions Rule actions - */ -function validateScheduleOperation( - schedule: RawRule['schedule'], - actions: RawRule['actions'], - ruleId: string -): void { - const scheduleInterval = parseDuration(schedule.interval); - const actionsWithInvalidThrottles = []; - - for (const action of actions) { - // check for actions throttled shorter than the rule schedule - if ( - action.frequency?.notifyWhen === RuleNotifyWhen.THROTTLE && - parseDuration(action.frequency.throttle!) < scheduleInterval - ) { - actionsWithInvalidThrottles.push(action); - } - } - - if (actionsWithInvalidThrottles.length > 0) { - throw Error( - `Error updating rule with ID "${ruleId}": the interval ${schedule.interval} is longer than the action frequencies` - ); - } -} - -async function prepareApiKeys( - context: RulesClientContext, - rule: SavedObjectsFindResult, - ruleType: RuleType, - apiKeysMap: ApiKeysMap, - attributes: RawRule, - hasUpdateApiKeyOperation: boolean, - username: string | null -): Promise<{ apiKeyAttributes: ApiKeyAttributes }> { - const apiKeyAttributes = await createNewAPIKeySet(context, { - id: ruleType.id, - ruleName: attributes.name, - username, - shouldUpdateApiKey: attributes.enabled || hasUpdateApiKeyOperation, - errorMessage: 'Error updating rule: could not create API key', - }); - - // collect generated API keys - if (apiKeyAttributes.apiKey) { - apiKeysMap.set(rule.id, { - ...apiKeysMap.get(rule.id), - newApiKey: apiKeyAttributes.apiKey, - newApiKeyCreatedByUser: apiKeyAttributes.apiKeyCreatedByUser, - }); - } - - return { - apiKeyAttributes, - }; -} - -function updateAttributes( - context: RulesClientContext, - attributes: RawRule, - apiKeyAttributes: ApiKeyAttributes, - updatedParams: RuleTypeParams, - rawAlertActions: RawRuleAction[], - username: string | null -): { - updatedAttributes: RawRule; -} { - // get notifyWhen - const notifyWhen = getRuleNotifyWhenType( - attributes.notifyWhen ?? null, - attributes.throttle ?? null - ); - - const updatedAttributes = updateMeta(context, { - ...attributes, - ...apiKeyAttributes, - params: updatedParams as RawRule['params'], - actions: rawAlertActions, - notifyWhen, - updatedBy: username, - updatedAt: new Date().toISOString(), - }); - - // add mapped_params - const mappedParams = getMappedParams(updatedParams); - - if (Object.keys(mappedParams).length) { - updatedAttributes.mapped_params = mappedParams; - } - - return { - updatedAttributes, - }; -} - -async function saveBulkUpdatedRules( - context: RulesClientContext, - rules: Array>, - apiKeysMap: ApiKeysMap -) { - const apiKeysToInvalidate: string[] = []; - let result; - try { - result = await context.unsecuredSavedObjectsClient.bulkCreate(rules, { overwrite: true }); - } catch (e) { - // avoid unused newly generated API keys - if (apiKeysMap.size > 0) { - await bulkMarkApiKeysForInvalidation( - { - apiKeys: Array.from(apiKeysMap.values()) - .filter((value) => value.newApiKey && !value.newApiKeyCreatedByUser) - .map((value) => value.newApiKey as string), - }, - context.logger, - context.unsecuredSavedObjectsClient - ); - } - throw e; - } - - result.saved_objects.map(({ id, error }) => { - const oldApiKey = apiKeysMap.get(id)?.oldApiKey; - const oldApiKeyCreatedByUser = apiKeysMap.get(id)?.oldApiKeyCreatedByUser; - const newApiKey = apiKeysMap.get(id)?.newApiKey; - const newApiKeyCreatedByUser = apiKeysMap.get(id)?.newApiKeyCreatedByUser; - - // if SO wasn't saved and has new API key it will be invalidated - if (error && newApiKey && !newApiKeyCreatedByUser) { - apiKeysToInvalidate.push(newApiKey); - // if SO saved and has old Api Key it will be invalidate - } else if (!error && oldApiKey && !oldApiKeyCreatedByUser) { - apiKeysToInvalidate.push(oldApiKey); - } - }); - - return { result, apiKeysToInvalidate }; -} - -async function attemptToMigrateLegacyFrequency( - context: RulesClientContext, - operation: BulkEditOperation, - attributes: SavedObjectsFindResult['attributes'], - ruleType: RuleType -) { - if (operation.field !== 'actions') - throw new Error('Can only perform frequency migration on an action operation'); - // Try to remove the rule-level frequency params, and then validate actions - if (typeof attributes.notifyWhen !== 'undefined') attributes.notifyWhen = undefined; - if (attributes.throttle) attributes.throttle = undefined; - await validateActions(context, ruleType, { - ...attributes, - actions: operation.value, - }); - return attributes; -} diff --git a/x-pack/plugins/alerting/server/rules_client/methods/mute_all.ts b/x-pack/plugins/alerting/server/rules_client/methods/mute_all.ts index dbc00b2135176..ca5584da706a7 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/mute_all.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/mute_all.ts @@ -12,7 +12,7 @@ import { partiallyUpdateAlert } from '../../saved_objects'; import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; import { RulesClientContext } from '../types'; import { updateMeta } from '../lib'; -import { clearUnscheduledSnooze } from '../common'; +import { clearUnscheduledSnoozeAttributes } from '../common'; export async function muteAll(context: RulesClientContext, { id }: { id: string }): Promise { return await retryIfConflicts( @@ -63,7 +63,7 @@ async function muteAllWithOCC(context: RulesClientContext, { id }: { id: string const updateAttributes = updateMeta(context, { muteAll: true, mutedInstanceIds: [], - snoozeSchedule: clearUnscheduledSnooze(attributes), + snoozeSchedule: clearUnscheduledSnoozeAttributes(attributes), updatedBy: await context.getUserName(), updatedAt: new Date().toISOString(), }); diff --git a/x-pack/plugins/alerting/server/rules_client/methods/snooze.ts b/x-pack/plugins/alerting/server/rules_client/methods/snooze.ts index 8bfd19bc9c583..6f1187526b521 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/snooze.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/snooze.ts @@ -14,7 +14,7 @@ import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; import { validateSnoozeStartDate } from '../../lib/validate_snooze_date'; import { RuleMutedError } from '../../lib/errors/rule_muted'; import { RulesClientContext } from '../types'; -import { getSnoozeAttributes, verifySnoozeScheduleLimit } from '../common'; +import { getSnoozeAttributes, verifySnoozeAttributeScheduleLimit } from '../common'; import { updateMeta } from '../lib'; export interface SnoozeParams { @@ -88,7 +88,7 @@ async function snoozeWithOCC( const newAttrs = getSnoozeAttributes(attributes, snoozeSchedule); try { - verifySnoozeScheduleLimit(newAttrs); + verifySnoozeAttributeScheduleLimit(newAttrs); } catch (error) { throw Boom.badRequest(error.message); } diff --git a/x-pack/plugins/alerting/server/rules_client/methods/unmute_all.ts b/x-pack/plugins/alerting/server/rules_client/methods/unmute_all.ts index d5cd81b2664d6..e69f09a219700 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/unmute_all.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/unmute_all.ts @@ -12,7 +12,7 @@ import { partiallyUpdateAlert } from '../../saved_objects'; import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; import { RulesClientContext } from '../types'; import { updateMeta } from '../lib'; -import { clearUnscheduledSnooze } from '../common'; +import { clearUnscheduledSnoozeAttributes } from '../common'; export async function unmuteAll( context: RulesClientContext, @@ -66,7 +66,7 @@ async function unmuteAllWithOCC(context: RulesClientContext, { id }: { id: strin const updateAttributes = updateMeta(context, { muteAll: false, mutedInstanceIds: [], - snoozeSchedule: clearUnscheduledSnooze(attributes), + snoozeSchedule: clearUnscheduledSnoozeAttributes(attributes), updatedBy: await context.getUserName(), updatedAt: new Date().toISOString(), }); diff --git a/x-pack/plugins/alerting/server/rules_client/methods/update.ts b/x-pack/plugins/alerting/server/rules_client/methods/update.ts index f68b41067ddae..68dc7fa0dd6a7 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/update.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/update.ts @@ -8,7 +8,6 @@ import Boom from '@hapi/boom'; import { isEqual } from 'lodash'; import { SavedObject } from '@kbn/core/server'; -import type { ShouldIncrementRevision } from './bulk_edit'; import { PartialRule, RawRule, @@ -35,6 +34,8 @@ import { migrateLegacyActions, } from '../lib'; +type ShouldIncrementRevision = (params?: RuleTypeParams) => boolean; + export interface UpdateOptions { id: string; data: { @@ -47,7 +48,7 @@ export interface UpdateOptions { notifyWhen?: RuleNotifyWhenType | null; }; allowMissingConnectorSecrets?: boolean; - shouldIncrementRevision?: ShouldIncrementRevision; + shouldIncrementRevision?: ShouldIncrementRevision; } export async function update( diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index a509fd3145c44..d34a8d10ef172 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -10,7 +10,7 @@ import { parseDuration } from '../../common/parse_duration'; import { RulesClientContext, BulkOptions, MuteOptions } from './types'; import { clone, CloneArguments } from './methods/clone'; -import { createRule, CreateRuleParams } from '../application/rule/create'; +import { createRule, CreateRuleParams } from '../application/rule/methods/create'; import { get, GetParams } from './methods/get'; import { resolve, ResolveParams } from './methods/resolve'; import { getAlertState, GetAlertStateParams } from './methods/get_alert_state'; @@ -37,7 +37,10 @@ import { aggregate, AggregateParams } from './methods/aggregate'; import { deleteRule } from './methods/delete'; import { update, UpdateOptions } from './methods/update'; import { bulkDeleteRules } from './methods/bulk_delete'; -import { bulkEdit, BulkEditOptions } from './methods/bulk_edit'; +import { + bulkEditRules, + BulkEditOptions, +} from '../application/rule/methods/bulk_edit/bulk_edit_rules'; import { bulkEnableRules } from './methods/bulk_enable'; import { bulkDisableRules } from './methods/bulk_disable'; import { updateApiKey } from './methods/update_api_key'; @@ -136,7 +139,7 @@ export class RulesClient { public bulkDeleteRules = (options: BulkOptions) => bulkDeleteRules(this.context, options); public bulkEdit = (options: BulkEditOptions) => - bulkEdit(this.context, options); + bulkEditRules(this.context, options); public bulkEnableRules = (options: BulkOptions) => bulkEnableRules(this.context, options); public bulkDisableRules = (options: BulkOptions) => bulkDisableRules(this.context, options); diff --git a/x-pack/plugins/alerting/server/rules_client/types.ts b/x-pack/plugins/alerting/server/rules_client/types.ts index fceac39d6bbdf..afcaaaaf34499 100644 --- a/x-pack/plugins/alerting/server/rules_client/types.ts +++ b/x-pack/plugins/alerting/server/rules_client/types.ts @@ -31,10 +31,7 @@ import { AlertingRulesConfig } from '../config'; export type { BulkEditOperation, BulkEditFields, - BulkEditOptions, - BulkEditOptionsFilter, - BulkEditOptionsIds, -} from './methods/bulk_edit'; +} from '../application/rule/methods/bulk_edit/types'; export type { FindOptions, FindResult } from './methods/find'; export type { UpdateOptions } from './methods/update'; export type { GetAlertSummaryParams } from './methods/get_alert_summary';