[RAM][HTTP Versioning] Version Rule Bulk Edit Endpoint (#161912)

## Summary

Resolves: https://github.com/elastic/kibana/issues/161395
Parent Issue: https://github.com/elastic/kibana/issues/157883

Adds versioned types to the rule `bulk_edit` endpoint.

This PR also moves around the folder structure slightly, by adding a sub
folder for the `data`/`route`/`application` methods:

## Before

![image](3ae19871-cc5c-4180-b099-5fed86c3870b)

## After

![image](bbd67bb4-4899-4f65-966e-64964f994a04)

Notice I added a `methods` folder to contain the methods, I did the same
for the `data` and `route` folders as well. I think this improves the
hierarchy of these modules, If folks are ok with it then I will update
the doc with the new folder structure.

### Checklist
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Jiawei Wu 2023-07-24 17:01:18 -07:00 committed by GitHub
parent 71ebc38c42
commit d4e6a19798
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
101 changed files with 2289 additions and 1253 deletions

View file

@ -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';

View file

@ -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'),
})
),
});

View file

@ -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<typeof rRuleSchemaV1>;

View file

@ -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';

View file

@ -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;
};

View file

@ -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 = <T>(name: string, array: T[]) => {
if (array.length === 0) {
return `rRule ${name} cannot be empty`;
}
};
export const createValidateRecurrenceBy = <T>(name: string) => {
return (array: T[]) => validateRecurrenceBy(name, array);
};

View file

@ -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;
};

View file

@ -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';

View file

@ -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,
});

View file

@ -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';

View file

@ -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<typeof ruleSnoozeScheduleSchemaV1>;
export type BulkEditRulesRequestBody = TypeOf<typeof bulkEditRulesRequestBodySchemaV1>;
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<Params extends RuleParamsV1 = never> {
body: {
rules: Array<RuleResponseV1<Params>>;
skipped: BulkEditActionSkippedResult[];
errors: BulkEditOperationError[];
total: number;
};
}

View file

@ -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';

View file

@ -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';

View file

@ -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';
}
};

View file

@ -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';

View file

@ -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(
[

View file

@ -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';

View file

@ -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<typeof actionSchemaV1>;

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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)),

View file

@ -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';

View file

@ -6,9 +6,10 @@
*/
import type { TypeOf } from '@kbn/config-schema';
import { ruleParamsSchemaV1, ruleResponseSchemaV1 } from '..';
import { ruleParamsSchemaV1, ruleResponseSchemaV1, ruleSnoozeScheduleSchemaV1 } from '..';
export type RuleParams = TypeOf<typeof ruleParamsSchemaV1>;
export type RuleSnoozeSchedule = TypeOf<typeof ruleSnoozeScheduleSchemaV1>;
type RuleResponseSchemaType = TypeOf<typeof ruleResponseSchemaV1>;
export interface RuleResponse<Params extends RuleParams = never> {

View file

@ -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)) {

View file

@ -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';

View file

@ -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'),
})
),
});

View file

@ -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()),
});

View file

@ -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';

View file

@ -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<typeof rRuleSchema>;

View file

@ -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<typeof rRuleRequestSchema>;

View file

@ -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';

View file

@ -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;
};

View file

@ -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 = <T>(name: string, array: T[]) => {
if (array.length === 0) {
return `rRule ${name} cannot be empty`;
}
};
export const createValidateRecurrenceBy = <T>(name: string) => {
return (array: T[]) => validateRecurrenceBy(name, array);
};

View file

@ -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;
};

View file

@ -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';
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('../lib/siem_legacy_actions/migrate_legacy_actions', () => {
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,

View file

@ -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<RuleAttributes, 'apiKey' | 'apiKeyOwner' | 'apiKeyCreatedByUser'>;
type RuleType = ReturnType<RuleTypeRegistry['get']>;
// TODO (http-versioning): This should be of type Rule, change this when all rule types are fixed
export interface BulkEditResult<Params extends RuleParams> {
rules: Array<SanitizedRule<Params>>;
skipped: BulkActionSkipResult[];
errors: BulkOperationError[];
total: number;
}
export type BulkEditOptions<Params extends RuleParams> =
| BulkEditOptionsFilter<Params>
| BulkEditOptionsIds<Params>;
export async function bulkEditRules<Params extends RuleParams>(
context: RulesClientContext,
options: BulkEditOptions<Params>
): Promise<BulkEditResult<Params>> {
const queryFilter = (options as BulkEditOptionsFilter<Params>).filter;
const ids = (options as BulkEditOptionsIds<Params>).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<RuleBulkOperationAggregation>({
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<Params>(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<Params>) => {
return transformRuleDomainToRule<Params>(rule);
}) as Array<SanitizedRule<Params>>;
await bulkUpdateSchedules(context, options.operations, updatedRules);
return { rules: publicRules, skipped, errors, total };
}
async function bulkEditRulesOcc<Params extends RuleParams>(
context: RulesClientContext,
{
filter,
operations,
paramsModifier,
shouldIncrementRevision,
}: {
filter: KueryNode | null;
operations: BulkEditOperation[];
paramsModifier?: ParamsModifier<Params>;
shouldIncrementRevision?: ShouldIncrementRevision<Params>;
}
): Promise<{
apiKeysToInvalidate: string[];
rules: Array<SavedObjectsBulkUpdateObject<RuleAttributes>>;
resultSavedObjects: Array<SavedObjectsUpdateResponse<RuleAttributes>>;
errors: BulkOperationError[];
skipped: BulkActionSkipResult[];
}> {
const rulesFinder =
await context.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser<RuleAttributes>(
{
filter,
type: 'alert',
perPage: 100,
...(context.namespace ? { namespaces: [context.namespace] } : undefined),
}
);
const rules: Array<SavedObjectsBulkUpdateObject<RuleAttributes>> = [];
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<RuleAttributes>) =>
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<Params extends RuleParams>(
context: RulesClientContext,
operations: BulkEditOperation[],
updatedRules: Array<RuleDomain<Params>>
): Promise<void> {
const scheduleOperation = operations.find(
(
operation
): operation is Extract<BulkEditOperation, { field: Extract<BulkEditFields, 'schedule'> }> =>
operation.field === 'schedule'
);
if (!scheduleOperation?.value) {
return;
}
const taskIds = updatedRules.reduce<string[]>((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<Params extends RuleParams>({
context,
rule,
operations,
paramsModifier,
apiKeysMap,
rules,
skipped,
errors,
username,
shouldIncrementRevision = () => true,
}: {
context: RulesClientContext;
rule: SavedObjectsFindResult<RuleAttributes>;
operations: BulkEditOperation[];
paramsModifier?: ParamsModifier<Params>;
apiKeysMap: ApiKeysMap;
rules: Array<SavedObjectsBulkUpdateObject<RuleAttributes>>;
skipped: BulkActionSkipResult[];
errors: BulkOperationError[];
username: string | null;
shouldIncrementRevision?: ShouldIncrementRevision<Params>;
}): Promise<void> {
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<Params> = transformRuleAttributesToRuleDomain<Params>(
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<Params>({
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<RuleAttributes>
): Promise<void> {
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<Params extends RuleParams>({
context,
operations,
rule,
ruleActions,
ruleType,
}: {
context: RulesClientContext;
operations: BulkEditOperation[];
rule: RuleDomain<Params>;
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<Params>(
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<RuleAttributes>,
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<SavedObjectsBulkUpdateObject<RuleAttributes>>,
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<SavedObjectsBulkCreateObject<RuleAttributes>>,
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<Params extends RuleParams>(
context: RulesClientContext,
operation: BulkEditOperation,
rule: RuleDomain<Params>,
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;
}

View file

@ -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';

View file

@ -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 });

View file

@ -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';

View file

@ -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<typeof bulkEditRuleSnoozeScheduleSchema>;
export type BulkEditOperation = TypeOf<typeof bulkEditOperationSchema>;
export type BulkEditOperations = TypeOf<typeof bulkEditOperationsSchema>;
export type ParamsModifier<Params extends RuleParams> = (
params: Params
) => Promise<ParamsModifierResult<Params>>;
interface ParamsModifierResult<Params extends RuleParams> {
modifiedParams: Params;
isParamsUpdateSkipped: boolean;
}
export type ShouldIncrementRevision<Params extends RuleParams> = (params?: Params) => boolean;
export type BulkEditFields = keyof Pick<
RuleDomain,
'actions' | 'tags' | 'schedule' | 'throttle' | 'notifyWhen' | 'snoozeSchedule' | 'apiKey'
>;
export interface BulkEditOptionsCommon<Params extends RuleParams> {
operations: BulkEditOperation[];
paramsModifier?: ParamsModifier<Params>;
shouldIncrementRevision?: ShouldIncrementRevision<Params>;
}
export type BulkEditOptionsFilter<Params extends RuleParams> = BulkEditOptionsCommon<Params> & {
filter?: string | KueryNode;
};
export type BulkEditOptionsIds<Params extends RuleParams> = BulkEditOptionsCommon<Params> & {
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;
};
}

View file

@ -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';

View file

@ -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';

View file

@ -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';
}
};

View file

@ -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(),
}));

View file

@ -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<Params extends RuleParams = never>(
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<Params extends RuleParams = never>(
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<Params extends RuleParams = never>(
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)

View file

@ -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(),

View file

@ -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<typeof createRuleDataSchema>;

View file

@ -7,7 +7,6 @@
export {
ruleParamsSchema,
rRuleSchema,
snoozeScheduleSchema,
ruleExecutionStatusSchema,
ruleLastRunSchema,

View file

@ -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,

View file

@ -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';

View file

@ -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<typeof ruleParamsSchema>;
export type RRule = TypeOf<typeof rRuleSchema>;
export type SnoozeSchedule = TypeOf<typeof snoozeScheduleSchema>;
export type RuleSnoozeSchedule = TypeOf<typeof snoozeScheduleSchema>;
export type RuleLastRun = TypeOf<typeof ruleLastRunSchema>;
export type Monitoring = TypeOf<typeof monitoringSchema>;
export type Action = TypeOf<typeof actionSchema>;

View file

@ -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';

View file

@ -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;
}

View file

@ -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';

View file

@ -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<SavedObjectsBulkCreateObject<RuleAttributes>>;
savedObjectsBulkCreateOptions?: SavedObjectsCreateOptions;
}
export const bulkCreateRulesSo = (
params: BulkCreateRulesSoParams
): Promise<SavedObjectsBulkResponse<RuleAttributes>> => {
const { savedObjectsClient, bulkCreateRuleAttributes, savedObjectsBulkCreateOptions } = params;
return savedObjectsClient.bulkCreate<RuleAttributes>(
bulkCreateRuleAttributes,
savedObjectsBulkCreateOptions
);
};

View file

@ -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<SavedObject<RuleAttributes>> => {
const { savedObjectClient, ruleAttributes, savedObjectCreateOptions } = params;
const { savedObjectsClient, ruleAttributes, savedObjectsCreateOptions } = params;
return savedObjectClient.create('alert', ruleAttributes, savedObjectCreateOptions);
return savedObjectsClient.create('alert', ruleAttributes, savedObjectsCreateOptions);
};

View file

@ -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);
};

View file

@ -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<SavedObjectsFindOptions, 'type'>;
}
export const findRulesSo = <RuleAggregation = Record<string, unknown>>(
params: FindRulesSoParams
): Promise<SavedObjectsFindResponse<RuleAttributes, RuleAggregation>> => {
const { savedObjectsClient, savedObjectsFindOptions } = params;
return savedObjectsClient.find<RuleAttributes, RuleAggregation>({
...savedObjectsFindOptions,
type: 'alert',
});
};

View file

@ -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<RuleAttributes>;
savedObjectUpdateOptions?: SavedObjectsUpdateOptions<RuleAttributes>;
savedObjectsUpdateOptions?: SavedObjectsUpdateOptions<RuleAttributes>;
}
export const updateRuleSo = (
params: UpdateRuleSoParams
): Promise<SavedObjectsUpdateResponse<RuleAttributes>> => {
const { savedObjectClient, id, updateRuleAttributes, savedObjectUpdateOptions } = params;
const { savedObjectsClient, id, updateRuleAttributes, savedObjectsUpdateOptions } = params;
return savedObjectClient.update<RuleAttributes>(
return savedObjectsClient.update<RuleAttributes>(
'alert',
id,
updateRuleAttributes,
savedObjectUpdateOptions
savedObjectsUpdateOptions
);
};

View file

@ -8,6 +8,7 @@
export type {
RuleNotifyWhenAttributes,
RuleLastRunOutcomeValuesAttributes,
RuleActionAttributes,
RuleExecutionStatusValuesAttributes,
RuleExecutionStatusErrorReasonAttributes,
RuleExecutionStatusWarningReasonAttributes,

View file

@ -144,7 +144,7 @@ interface AlertsFilterAttributes {
timeframe?: AlertsFilterTimeFrameAttributes;
}
interface RuleActionAttributes {
export interface RuleActionAttributes {
uuid: string;
group: string;
actionRef: string;

View file

@ -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';

View file

@ -31,6 +31,7 @@ export { lastRunFromState, lastRunFromError, lastRunToRaw } from './last_run_sta
export {
resetMonitoringLastRun,
getDefaultMonitoring,
getDefaultMonitoringRuleDomainProperties,
convertMonitoringFromRawAndVerify,
} from './monitoring';
export { getNextRun } from './next_run';

View file

@ -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;

View file

@ -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<AlertingRequestHandlerContext>;
}
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<AlertingRequestHandlerContext>,
licenseState: ILicenseState
) =>
buildBulkEditRulesRoute({
licenseState,
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/_bulk_edit`,
router,
});

View file

@ -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';

View file

@ -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',

View file

@ -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<AlertingRequestHandlerContext>;
}
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<RuleParamsV1>({
filter,
ids,
operations,
});
const resultBody: BulkEditRulesResponseV1<RuleParamsV1> = {
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<RuleParamsV1>(rule as Rule<RuleParamsV1>);
}),
},
};
return res.ok(resultBody);
} catch (e) {
if (e instanceof RuleTypeDisabledError) {
return e.sendResponse(res);
}
throw e;
}
})
)
)
);
};
export const bulkEditInternalRulesRoute = (
router: IRouter<AlertingRequestHandlerContext>,
licenseState: ILicenseState
) =>
buildBulkEditRulesRoute({
licenseState,
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/_bulk_edit`,
router,
});

View file

@ -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(),
}));

View file

@ -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(

View file

@ -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';

View file

@ -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 [];

View file

@ -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'] => {

View file

@ -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<RuleDomain, 'apiKey' | 'apiKeyOwner' | 'apiKeyCreatedByUser'> {
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,
};
}

View file

@ -32,7 +32,9 @@ export const applyBulkEditOperation = <R extends object>(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':

View file

@ -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';

View file

@ -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) => {

View file

@ -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<SavedObjectsBulkUpdateObject<RawRule>>;
resultSavedObjects: Array<SavedObjectsUpdateResponse<RawRule>>;
rules: Array<SavedObjectsBulkUpdateObject<RuleAttributes>>;
resultSavedObjects: Array<SavedObjectsUpdateResponse<RuleAttributes>>;
errors: BulkOperationError[];
skipped: BulkActionSkipResult[];
}>;
interface ReturnRetry {
apiKeysToInvalidate: string[];
results: Array<SavedObjectsUpdateResponse<RawRule>>;
results: Array<SavedObjectsUpdateResponse<RuleAttributes>>;
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<SavedObjectsUpdateResponse<RawRule>> = [],
accResults: Array<SavedObjectsUpdateResponse<RuleAttributes>> = [],
accErrors: BulkOperationError[] = [],
accSkipped: BulkActionSkipResult[] = []
): Promise<ReturnRetry> => {

View file

@ -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<Params extends RuleParams>(
rule: RuleDomain<Params>,
snoozeSchedule: RuleDomainSnoozeSchedule
): {
muteAll: RuleDomain<Params>['muteAll'];
snoozeSchedule: RuleDomain<Params>['snoozeSchedule'];
} {
// If duration is -1, instead mute all
const { id: snoozeId, duration } = snoozeSchedule;
if (duration === -1) {
return {
muteAll: true,
snoozeSchedule: clearUnscheduledSnooze(attributes),
snoozeSchedule: clearUnscheduledSnooze<Params>(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<Params>(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<Params extends RuleParams>(
rule: RuleDomain<Params>,
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<Params extends RuleParams>(rule: RuleDomain<Params>) {
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<Params extends RuleParams>(
rule: RuleDomain<Params>,
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<RawRule>) {
export function clearCurrentActiveSnooze<Params extends RuleParams>(rule: RuleDomain<Params>) {
// 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<RawRule>) {
const schedules = attributes.snoozeSchedule?.filter((snooze) => snooze.id);
if (schedules && schedules.length > 5) {
throw Error(
@ -137,3 +206,16 @@ export function verifySnoozeScheduleLimit(attributes: Partial<RawRule>) {
);
}
}
export function verifySnoozeScheduleLimit<Params extends RuleParams>(
snoozeSchedule: RuleDomain<Params>['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',
})
);
}
}

View file

@ -68,8 +68,8 @@ export async function createRuleSavedObject<Params extends RuleTypeParams = neve
() =>
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<Params extends RuleTypeParams = neve
// Cleanup data, something went wrong scheduling the task
try {
await deleteRuleSo({
savedObjectClient: context.unsecuredSavedObjectsClient,
savedObjectsClient: context.unsecuredSavedObjectsClient,
id: createdAlert.id,
});
} catch (err) {
@ -115,7 +115,7 @@ export async function createRuleSavedObject<Params extends RuleTypeParams = neve
await withSpan({ name: 'unsecuredSavedObjectsClient.update', type: 'rules' }, () =>
updateRuleSo({
savedObjectClient: context.unsecuredSavedObjectsClient,
savedObjectsClient: context.unsecuredSavedObjectsClient,
id: createdAlert.id,
updateRuleAttributes: {
scheduledTaskId,

View file

@ -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<BulkEditOperation['field']> =
new Set(['snoozeSchedule', 'apiKey']);
export type BulkEditOperation =
| {
operation: 'add' | 'delete' | 'set';
field: Extract<BulkEditFields, 'tags'>;
value: string[];
}
| {
operation: 'add' | 'set';
field: Extract<BulkEditFields, 'actions'>;
value: NormalizedAlertAction[];
}
| {
operation: 'set';
field: Extract<BulkEditFields, 'schedule'>;
value: Rule['schedule'];
}
| {
operation: 'set';
field: Extract<BulkEditFields, 'throttle'>;
value: Rule['throttle'];
}
| {
operation: 'set';
field: Extract<BulkEditFields, 'notifyWhen'>;
value: Rule['notifyWhen'];
}
| {
operation: 'set';
field: Extract<BulkEditFields, 'snoozeSchedule'>;
value: RuleSnoozeSchedule;
}
| {
operation: 'delete';
field: Extract<BulkEditFields, 'snoozeSchedule'>;
value?: string[];
}
| {
operation: 'set';
field: Extract<BulkEditFields, 'apiKey'>;
value?: undefined;
};
type ApiKeysMap = Map<
string,
{
oldApiKey?: string;
newApiKey?: string;
oldApiKeyCreatedByUser?: boolean | null;
newApiKeyCreatedByUser?: boolean | null;
}
>;
type ApiKeyAttributes = Pick<RawRule, 'apiKey' | 'apiKeyOwner' | 'apiKeyCreatedByUser'>;
type RuleType = ReturnType<RuleTypeRegistry['get']>;
// 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<Params> {
modifiedParams: Params;
isParamsUpdateSkipped: boolean;
}
export type RuleParamsModifier<Params extends RuleTypeParams> = (
params: Params
) => Promise<RuleParamsModifierResult<Params>>;
export type ShouldIncrementRevision<Params extends RuleTypeParams> = (
params?: RuleTypeParams
) => boolean;
export interface BulkEditOptionsFilter<Params extends RuleTypeParams> {
filter?: string | KueryNode;
operations: BulkEditOperation[];
paramsModifier?: RuleParamsModifier<Params>;
shouldIncrementRevision?: ShouldIncrementRevision<Params>;
}
export interface BulkEditOptionsIds<Params extends RuleTypeParams> {
ids: string[];
operations: BulkEditOperation[];
paramsModifier?: RuleParamsModifier<Params>;
shouldIncrementRevision?: ShouldIncrementRevision<Params>;
}
export type BulkEditOptions<Params extends RuleTypeParams> =
| BulkEditOptionsFilter<Params>
| BulkEditOptionsIds<Params>;
export async function bulkEdit<Params extends RuleTypeParams>(
context: RulesClientContext,
options: BulkEditOptions<Params>
): Promise<{
rules: Array<SanitizedRule<Params>>;
skipped: BulkActionSkipResult[];
errors: BulkOperationError[];
total: number;
}> {
const queryFilter = (options as BulkEditOptionsFilter<Params>).filter;
const ids = (options as BulkEditOptionsIds<Params>).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<Params>(
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<Params extends RuleTypeParams>(
context: RulesClientContext,
{
filter,
operations,
paramsModifier,
shouldIncrementRevision,
}: {
filter: KueryNode | null;
operations: BulkEditOptions<Params>['operations'];
paramsModifier: BulkEditOptions<Params>['paramsModifier'];
shouldIncrementRevision?: BulkEditOptions<Params>['shouldIncrementRevision'];
}
): Promise<{
apiKeysToInvalidate: string[];
rules: Array<SavedObjectsBulkUpdateObject<RawRule>>;
resultSavedObjects: Array<SavedObjectsUpdateResponse<RawRule>>;
errors: BulkOperationError[];
skipped: BulkActionSkipResult[];
}> {
const rulesFinder =
await context.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser<RawRule>(
{
filter,
type: 'alert',
perPage: 100,
...(context.namespace ? { namespaces: [context.namespace] } : undefined),
}
);
const rules: Array<SavedObjectsBulkUpdateObject<RawRule>> = [];
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<RawRule>) =>
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<Rule | RuleWithLegacyId>
): Promise<void> {
const scheduleOperation = operations.find(
(
operation
): operation is Extract<BulkEditOperation, { field: Extract<BulkEditFields, 'schedule'> }> =>
operation.field === 'schedule'
);
if (!scheduleOperation?.value) {
return;
}
const taskIds = updatedRules.reduce<string[]>((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<Params extends RuleTypeParams>({
context,
rule,
operations,
paramsModifier,
apiKeysMap,
rules,
skipped,
errors,
username,
shouldIncrementRevision = () => true,
}: {
context: RulesClientContext;
rule: SavedObjectsFindResult<RawRule>;
operations: BulkEditOptions<Params>['operations'];
paramsModifier: BulkEditOptions<Params>['paramsModifier'];
apiKeysMap: ApiKeysMap;
rules: Array<SavedObjectsBulkUpdateObject<RawRule>>;
skipped: BulkActionSkipResult[];
errors: BulkOperationError[];
username: string | null;
shouldIncrementRevision: BulkEditOptions<Params>['shouldIncrementRevision'];
}): Promise<void> {
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<Params, RuleTypeParams>(
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<RawRule>
): Promise<void> {
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<RawRule>,
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<RawRule>,
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<SavedObjectsBulkUpdateObject<RawRule>>,
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<RawRule>['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;
}

View file

@ -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<void> {
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(),
});

View file

@ -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);
}

View file

@ -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(),
});

View file

@ -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<Params extends RuleTypeParams> {
id: string;
data: {
@ -47,7 +48,7 @@ export interface UpdateOptions<Params extends RuleTypeParams> {
notifyWhen?: RuleNotifyWhenType | null;
};
allowMissingConnectorSecrets?: boolean;
shouldIncrementRevision?: ShouldIncrementRevision<Params>;
shouldIncrementRevision?: ShouldIncrementRevision;
}
export async function update<Params extends RuleTypeParams = never>(

View file

@ -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 = <Params extends RuleTypeParams>(options: BulkEditOptions<Params>) =>
bulkEdit<Params>(this.context, options);
bulkEditRules<Params>(this.context, options);
public bulkEnableRules = (options: BulkOptions) => bulkEnableRules(this.context, options);
public bulkDisableRules = (options: BulkOptions) => bulkDisableRules(this.context, options);

Some files were not shown because too many files have changed in this diff Show more